Skip to content

test: increased test coverage for failover#450

Merged
SantiagoDePolonia merged 1 commit into
mainfrom
tests/failover-tests-improvements
Jun 30, 2026
Merged

test: increased test coverage for failover#450
SantiagoDePolonia merged 1 commit into
mainfrom
tests/failover-tests-improvements

Conversation

@SantiagoDePolonia

@SantiagoDePolonia SantiagoDePolonia commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Summary by CodeRabbit

  • Documentation

    • Updated end-to-end release scenarios to cover a larger set of failover and dashboard-managed mapping workflows.
  • Tests

    • Expanded coverage for failover storage behavior, including create/read/update/delete flows, empty target handling, and timestamp consistency.
    • Added validation for dynamic fallback rules and disabled overrides to ensure fallback results match expected user-facing behavior.

@coderabbitai

coderabbitai Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds test coverage without modifying production code. New unit tests exercise SQLite failover store CRUD operations, timestamps, and list ordering, plus fallback resolver behavior with dynamic rule providers. The E2E scenario document is expanded from 132 to 141 scenarios, adding a manual failover management section.

Changes

Test Coverage Additions

Layer / File(s) Summary
SQLite failover store CRUD tests
internal/failover/store_sqlite_test.go
Adds an in-memory SQLite test helper and tests for Upsert/Get/List/Delete/DeleteAll, source trimming, timestamp behavior across upserts, list ordering, ErrNotFound handling, and nil-targets round-trip.
Fallback resolver dynamic rule provider tests
internal/fallback/resolver_test.go
Adds a fakeRuleProvider test struct and tests confirming NewResolverWithRuleProvider allows dynamic rules to override static manual targets and dynamic disabled entries to suppress static fallbacks.
E2E manual failover management scenarios
tests/e2e/release-e2e-scenarios.md
Expands the scenario count from 132 to 141, updates the stateful-note list, and adds a new section with scenarios S133-S141 covering creation, listing/updating, disabled targets, suggestions, reset, negative validation, and auth-gating.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~15 minutes

Possibly related PRs

  • ENTERPILOT/GoModel#203: Both PRs modify tests/e2e/release-e2e-scenarios.md to expand the release E2E scenario set.

Poem

A rabbit tests its burrow deep,
Each row upserted, none asleep.
Timestamps tick, the targets round-trip clean,
Nine new scenarios on the scene.
🐇 Hop, test, pass — repeat the dream!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title matches the PR’s main goal of expanding failover test coverage and is clear enough for history scans.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch tests/failover-tests-improvements

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@SantiagoDePolonia SantiagoDePolonia changed the title tests: better test coverage for failover test: increased test coverage for failover Jun 30, 2026
@SantiagoDePolonia SantiagoDePolonia merged commit 33e4d75 into main Jun 30, 2026
17 of 21 checks passed
@codecov-commenter

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/failover/store_sqlite_test.go`:
- Around line 65-89: The update-path test in store_sqlite_test.go documents that
updated_at advances, but it never verifies that behavior in the Rule upsert
flow. In the test around store.Upsert and store.Get, capture the original
UpdatedAt after the initial insert, then assert the post-update UpdatedAt is not
earlier than the original value since timestamps are stored as Unix seconds and
can tie within the same second. Use the existing Rule fields and the Upsert/Get
assertions to locate the check, and keep the verification focused on the
updated_at field alongside the existing CreatedAt preservation assertions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 5e0c08b2-27e8-45e0-988d-5b4b7e667edd

📥 Commits

Reviewing files that changed from the base of the PR and between 7694aec and bba51e3.

📒 Files selected for processing (3)
  • internal/failover/store_sqlite_test.go
  • internal/fallback/resolver_test.go
  • tests/e2e/release-e2e-scenarios.md

Comment on lines +65 to +89
// Update via ON CONFLICT: targets/enabled change, created_at is preserved even
// though the caller passes a different value, while updated_at advances.
update := Rule{
Source: "openai/gpt-5",
Targets: []string{"groq/llama-3.3-70b-versatile"},
Enabled: false,
ManagedSource: ManagedSourceDashboard,
CreatedAt: time.Unix(5000, 0).UTC(),
}
if err := store.Upsert(ctx, update); err != nil {
t.Fatalf("Upsert(update) error = %v", err)
}
got, err = store.Get(ctx, "openai/gpt-5")
if err != nil || got == nil {
t.Fatalf("Get() after update = %+v, %v", got, err)
}
if got.Enabled {
t.Fatalf("Get().Enabled = true after disabling; enabled=false must round-trip")
}
if !reflect.DeepEqual(got.Targets, []string{"groq/llama-3.3-70b-versatile"}) {
t.Fatalf("Get().Targets = %v, want updated single target", got.Targets)
}
if got.CreatedAt.Unix() != 1000 {
t.Fatalf("Get().CreatedAt = %d after update, want 1000 (ON CONFLICT must not touch created_at)", got.CreatedAt.Unix())
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Test doesn't actually verify the "updated_at advances" claim it documents.

The comment at Lines 65-66 states the update path should show updated_at advancing, but only Enabled, Targets, and CreatedAt are asserted after the update (Lines 81-89). Capture UpdatedAt after the insert and assert against it post-update. Since timestamps are persisted as Unix seconds (per sqliteUpsertArgs/stampUpsert), a strict After check could be flaky if both calls land in the same second — use a non-decreasing check instead.

🔧 Proposed fix
 	if got.UpdatedAt.IsZero() {
 		t.Fatalf("Get().UpdatedAt is zero; stampUpsert should set it")
 	}
+	firstUpdatedAt := got.UpdatedAt
 
 	// Update via ON CONFLICT: targets/enabled change, created_at is preserved even
 	// though the caller passes a different value, while updated_at advances.
@@
 	if got.CreatedAt.Unix() != 1000 {
 		t.Fatalf("Get().CreatedAt = %d after update, want 1000 (ON CONFLICT must not touch created_at)", got.CreatedAt.Unix())
 	}
+	if got.UpdatedAt.Before(firstUpdatedAt) {
+		t.Fatalf("Get().UpdatedAt = %v after update, want >= %v (ON CONFLICT must advance updated_at)", got.UpdatedAt, firstUpdatedAt)
+	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Update via ON CONFLICT: targets/enabled change, created_at is preserved even
// though the caller passes a different value, while updated_at advances.
update := Rule{
Source: "openai/gpt-5",
Targets: []string{"groq/llama-3.3-70b-versatile"},
Enabled: false,
ManagedSource: ManagedSourceDashboard,
CreatedAt: time.Unix(5000, 0).UTC(),
}
if err := store.Upsert(ctx, update); err != nil {
t.Fatalf("Upsert(update) error = %v", err)
}
got, err = store.Get(ctx, "openai/gpt-5")
if err != nil || got == nil {
t.Fatalf("Get() after update = %+v, %v", got, err)
}
if got.Enabled {
t.Fatalf("Get().Enabled = true after disabling; enabled=false must round-trip")
}
if !reflect.DeepEqual(got.Targets, []string{"groq/llama-3.3-70b-versatile"}) {
t.Fatalf("Get().Targets = %v, want updated single target", got.Targets)
}
if got.CreatedAt.Unix() != 1000 {
t.Fatalf("Get().CreatedAt = %d after update, want 1000 (ON CONFLICT must not touch created_at)", got.CreatedAt.Unix())
}
// Update via ON CONFLICT: targets/enabled change, created_at is preserved even
// though the caller passes a different value, while updated_at advances.
update := Rule{
Source: "openai/gpt-5",
Targets: []string{"groq/llama-3.3-70b-versatile"},
Enabled: false,
ManagedSource: ManagedSourceDashboard,
CreatedAt: time.Unix(5000, 0).UTC(),
}
if err := store.Upsert(ctx, update); err != nil {
t.Fatalf("Upsert(update) error = %v", err)
}
got, err = store.Get(ctx, "openai/gpt-5")
if err != nil || got == nil {
t.Fatalf("Get() after update = %+v, %v", got, err)
}
if got.Enabled {
t.Fatalf("Get().Enabled = true after disabling; enabled=false must round-trip")
}
if !reflect.DeepEqual(got.Targets, []string{"groq/llama-3.3-70b-versatile"}) {
t.Fatalf("Get().Targets = %v, want updated single target", got.Targets)
}
if got.CreatedAt.Unix() != 1000 {
t.Fatalf("Get().CreatedAt = %d after update, want 1000 (ON CONFLICT must not touch created_at)", got.CreatedAt.Unix())
}
if got.UpdatedAt.Before(firstUpdatedAt) {
t.Fatalf("Get().UpdatedAt = %v after update, want >= %v (ON CONFLICT must advance updated_at)", got.UpdatedAt, firstUpdatedAt)
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/failover/store_sqlite_test.go` around lines 65 - 89, The update-path
test in store_sqlite_test.go documents that updated_at advances, but it never
verifies that behavior in the Rule upsert flow. In the test around store.Upsert
and store.Get, capture the original UpdatedAt after the initial insert, then
assert the post-update UpdatedAt is not earlier than the original value since
timestamps are stored as Unix seconds and can tie within the same second. Use
the existing Rule fields and the Upsert/Get assertions to locate the check, and
keep the verification focused on the updated_at field alongside the existing
CreatedAt preservation assertions.

@greptile-apps

greptile-apps Bot commented Jun 30, 2026

Copy link
Copy Markdown

Confidence Score: 4/5

Safe to merge; all changes are tests and documentation, with no production code modified.

The new unit tests and e2e scenarios are correct and add meaningful coverage. The two findings are minor: the CRUD test omits a follow-up assertion for updated_at advancement that its own comment promises, and one e2e scenario pins a known-incorrect 502 status code as its pass condition, creating future friction when that status is corrected.

The UpdatedAt gap in internal/failover/store_sqlite_test.go and the 502 anchor in tests/e2e/release-e2e-scenarios.md are the only spots worth a second look.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant T as Test
    participant S as SQLiteStore
    participant DB as SQLite :memory:

    T->>S: "Upsert(Rule{Source: "  openai/gpt-5  ", CreatedAt: 1000})"
    S->>DB: INSERT ... ON CONFLICT DO UPDATE (sets updated_at, preserves created_at)
    T->>S: Get("openai/gpt-5")
    DB-->>T: "Rule{Source: "openai/gpt-5", CreatedAt: 1000, UpdatedAt: now}"

    T->>S: "Upsert(Rule{Source: "openai/gpt-5", CreatedAt: 5000, Enabled: false})"
    S->>DB: INSERT ... ON CONFLICT DO UPDATE (created_at NOT updated by SQL)
    T->>S: Get("openai/gpt-5")
    DB-->>T: "Rule{Enabled: false, CreatedAt: 1000, Targets: ["groq/..."]}"

    T->>S: List()
    DB-->>T: [anthropic/claude-opus-4-8, openai/gpt-5] (ASC order)

    T->>S: Get("does/not-exist")
    S-->>T: ErrNotFound

    T->>S: Delete("openai/gpt-5")
    DB-->>T: OK
    T->>S: Delete("openai/gpt-5")
    S-->>T: ErrNotFound

    T->>S: DeleteAll()
    DB-->>T: OK
    T->>S: List()
    DB-->>T: []
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant T as Test
    participant S as SQLiteStore
    participant DB as SQLite :memory:

    T->>S: "Upsert(Rule{Source: "  openai/gpt-5  ", CreatedAt: 1000})"
    S->>DB: INSERT ... ON CONFLICT DO UPDATE (sets updated_at, preserves created_at)
    T->>S: Get("openai/gpt-5")
    DB-->>T: "Rule{Source: "openai/gpt-5", CreatedAt: 1000, UpdatedAt: now}"

    T->>S: "Upsert(Rule{Source: "openai/gpt-5", CreatedAt: 5000, Enabled: false})"
    S->>DB: INSERT ... ON CONFLICT DO UPDATE (created_at NOT updated by SQL)
    T->>S: Get("openai/gpt-5")
    DB-->>T: "Rule{Enabled: false, CreatedAt: 1000, Targets: ["groq/..."]}"

    T->>S: List()
    DB-->>T: [anthropic/claude-opus-4-8, openai/gpt-5] (ASC order)

    T->>S: Get("does/not-exist")
    S-->>T: ErrNotFound

    T->>S: Delete("openai/gpt-5")
    DB-->>T: OK
    T->>S: Delete("openai/gpt-5")
    S-->>T: ErrNotFound

    T->>S: DeleteAll()
    DB-->>T: OK
    T->>S: List()
    DB-->>T: []
Loading

Reviews (1): Last reviewed commit: "tests: better test coverage for failover" | Re-trigger Greptile

Comment on lines +65 to +89
// Update via ON CONFLICT: targets/enabled change, created_at is preserved even
// though the caller passes a different value, while updated_at advances.
update := Rule{
Source: "openai/gpt-5",
Targets: []string{"groq/llama-3.3-70b-versatile"},
Enabled: false,
ManagedSource: ManagedSourceDashboard,
CreatedAt: time.Unix(5000, 0).UTC(),
}
if err := store.Upsert(ctx, update); err != nil {
t.Fatalf("Upsert(update) error = %v", err)
}
got, err = store.Get(ctx, "openai/gpt-5")
if err != nil || got == nil {
t.Fatalf("Get() after update = %+v, %v", got, err)
}
if got.Enabled {
t.Fatalf("Get().Enabled = true after disabling; enabled=false must round-trip")
}
if !reflect.DeepEqual(got.Targets, []string{"groq/llama-3.3-70b-versatile"}) {
t.Fatalf("Get().Targets = %v, want updated single target", got.Targets)
}
if got.CreatedAt.Unix() != 1000 {
t.Fatalf("Get().CreatedAt = %d after update, want 1000 (ON CONFLICT must not touch created_at)", got.CreatedAt.Unix())
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Missing UpdatedAt advancement assertion after upsert

The comment on line 65 says "while updated_at advances", but no assertion after the second Upsert verifies that UpdatedAt actually changed. The post-update block only checks Enabled, Targets, and CreatedAt. If stampUpsert stopped writing UpdatedAt on a conflict update, this test would still pass, defeating its stated purpose of catching that regression.

Comment on lines +2792 to +2812
.primary_model == $s
and (.fallback_models | length) == 2
and .fallback_models[0] == "openai/gpt-4.1-mini"
and .enabled == false
' >/dev/null
curl -fsS "$BASE_URL/admin/failover" \
| jq -e --arg s "$SRC" 'any(.[]; .primary_model == $s and .enabled == false)' >/dev/null
HEADERS_FILE=$(mktemp "$QA_RUN_DIR/s134.headers.XXXXXX")
curl -sS -D "$HEADERS_FILE" -o /dev/null -X DELETE "$BASE_URL/admin/failover" \
-H 'Content-Type: application/json' -d "{\"primary_model\":\"$SRC\"}"
grep -Eiq '^HTTP/.* 204 ' "$HEADERS_FILE"
```

### S135 Disable a primary with an empty target list

A disabled mapping is allowed to omit targets, which records the primary as a
failover-disabled source rather than rejecting the request.

```bash
SRC="qa-fo-dis-$QA_SUFFIX"
curl -fsS -X PUT "$BASE_URL/admin/failover" -H 'Content-Type: application/json' \

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 S139 anchors incorrect HTTP status code as pass condition

The scenario explicitly notes "the current status code is 502 provider_error" for what is a client-supplied validation error (an enabled mapping with no targets). This makes grep -Eiq '^HTTP/.* 502 ' the pass condition. When this bug is fixed and the endpoint returns the semantically correct 400 invalid_request_error, S139 will start failing and could block a release, forcing the fixer to remember to update this scenario. Consider using a looser assertion — e.g., grep -Eiq '^HTTP/.* [45]' — plus the message match as the durable invariant, so that fixing the status code does not require a simultaneous doc update.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants