Skip to content

test(budget): add budget management coverage#284

Merged
SantiagoDePolonia merged 2 commits intomainfrom
test/budget-management-coverage
Apr 27, 2026
Merged

test(budget): add budget management coverage#284
SantiagoDePolonia merged 2 commits intomainfrom
test/budget-management-coverage

Conversation

@SantiagoDePolonia
Copy link
Copy Markdown
Contributor

@SantiagoDePolonia SantiagoDePolonia commented Apr 27, 2026

Summary

  • rebase test-only branch onto main after the budget management base PR was merged
  • add deterministic budget coverage across unit, component e2e, integration, contract, and release curl scenarios
  • verify budget enforcement, DB persistence, endpoint responses, audit-on/off behavior, and provider usage normalization
  • remove stale standalone CHANGELOG.md; it was unreferenced and release notes are handled through the PR/release workflow

Tests

  • go test -count=1 ./internal/budget
  • go test -count=1 -tags=e2e ./tests/e2e
  • go test -count=1 -tags=contract ./tests/contract
  • go test -tags=integration ./tests/integration
  • bash -n tests/e2e/manage-release-e2e-stack.sh tests/e2e/run-release-e2e.sh

Summary by CodeRabbit

  • Tests

    • Expanded test coverage for budget enforcement across SQLite, PostgreSQL, and MongoDB.
    • Added end-to-end tests for budget management APIs and enforcement workflows (including admin flows and retry behavior).
    • Added integration tests validating audit-logging behavior during budget enforcement.
    • Introduced contract tests verifying usage reporting across supported LLM providers.
  • Documentation

    • Expanded E2E release scenarios with budget lifecycle and polling helpers; removed prior CHANGELOG contents.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 27, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 9e328aa4-e525-49dd-b22c-892a64e9ec21

📥 Commits

Reviewing files that changed from the base of the PR and between 534a460 and b764228.

📒 Files selected for processing (1)
  • CHANGELOG.md
💤 Files with no reviewable changes (1)
  • CHANGELOG.md

📝 Walkthrough

Walkthrough

Adds extensive tests and test helpers for budget enforcement across unit, SQLite, integration (Postgres/Mongo), contract, and E2E suites; wires budget checker and pricing resolver into test servers and updates test storage assertions and usage cost fields.

Changes

Cohort / File(s) Summary
Budget Service Unit Tests
internal/budget/service_test.go
Adds parameterized unit tests for enforcement boundaries (spent < amount allowed; ==/> amount -> ExceededError) and reset-time handling for period-start calculation.
SQLite Store Integration Test
internal/budget/store_sqlite_test.go
Adds SQLite integration test seeding usage entries across paths/time windows and asserts SumUsageCost returns correct sum/hasUsage; includes local UsageEntry helper.
Provider Usage Contract Test
tests/contract/usage_contract_test.go
Adds contract test (build tag contract) validating provider chat replay responses and normalized usage fields across multiple providers.
E2E Budget Tests & Server Wiring
tests/e2e/budget_test.go, tests/e2e/setup_test.go
Introduces E2E tests for budget enforcement and admin APIs; wires BudgetChecker and PricingResolver into E2E server; adds helpers for requests, polling, and CRUD admin flows.
E2E Stack & Scenarios
tests/e2e/manage-release-e2e-stack.sh, tests/e2e/release-e2e-scenarios.md
Passes explicit BASE_PATH env var to gateways; documents new budget env vars, adds polling and full budget enforcement lifecycle helpers, and appends four new budget scenarios (S86–S89).
Integration Tests & Setup
tests/integration/budget_test.go, tests/integration/setup_test.go
Adds integration suite for budget enforcement over Postgres and MongoDB (audit matrix), seeds daily budget, verifies blocking behavior and audit entries, and wires BudgetsEnabled/BudgetUserPaths into test server config with pricing metadata.
DB Assertion Helpers & Usage Schema
tests/integration/dbassert/budget.go, tests/integration/dbassert/usage.go
Adds helpers to query/assert budgets and storage existence across Postgres/Mongo; extends UsageEntry with nullable InputCost, OutputCost, TotalCost and updates DB/BSON scanning logic.
Changelog
CHANGELOG.md
Removes existing CHANGELOG content (deleted "Unreleased" migration notes).

Sequence Diagram(s)

sequenceDiagram
  participant Client as Client
  participant Server as E2E Server
  participant Budget as BudgetChecker
  participant Store as UsageStore
  participant Provider as UpstreamProvider
  participant Audit as AuditLog

  Client->>Server: POST /chat (user-path, req-id, payload)
  Server->>Budget: Check(user-path, req-id, costEstimate)
  Budget->>Store: SumUsageCost(user-path, periodStart, now)
  Store-->>Budget: currentSpent
  alt currentSpent + costEstimate < budget.Amount
    Budget-->>Server: Allowed
    Server->>Provider: Forward request
    Provider-->>Server: Response + usage
    Server->>Store: Persist usage entry
    Server->>Audit: Write audit (if enabled)
    Server-->>Client: 200 OK + response
  else currentSpent + costEstimate >= budget.Amount
    Budget-->>Server: ExceededError (spent)
    Server->>Audit: Write audit with rate_limit (if enabled)
    Server-->>Client: 429 Too Many Requests (Retry-After, budget_exceeded)
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐇 I hopped through tests both near and wide,
Counted tokens, costs, and periods with pride.
From SQLite to Postgres and Mongo's den,
I blocked one more request — then wrote it again. 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 29.73% 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.
Title check ✅ Passed The title accurately describes the main objective: adding budget management test coverage across multiple test layers (unit, integration, e2e, contract, release scenarios).
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch test/budget-management-coverage

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 and usage tips.

@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!

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Apr 27, 2026

Greptile Summary

This PR adds comprehensive budget management test coverage across unit, component E2E, integration, contract, and release curl scenarios. It also rebases a test-only branch onto main after the budget management feature was merged, adds BASE_PATH= to all five gateways in the release E2E stack script, and extends the usage DB assertions to include cost columns.

All changes are test-only and the test logic is generally well-structured, with deterministic boundary cases, proper async polling patterns, and correct per-fixture DB isolation.

Confidence Score: 5/5

Safe to merge — all changes are test-only and the single finding is a minor semantic mismatch that does not affect test correctness in practice.

The only finding is a P2 style issue (> vs >= in a polling helper) that won't cause failures given the mock always returns costs above the budget amount. All remaining findings are P2 or lower and do not block merge.

tests/e2e/budget_test.go – waitForBudgetSpent polling condition

Important Files Changed

Filename Overview
internal/budget/service_test.go Adds two targeted unit tests: TestServiceCheckBudgetAmountBoundary (table-driven boundary at exactly-equal / below / above amount) and TestServiceCheckIgnoresManualResetOlderThanPeriodStart (verifies period start wins over a stale manual reset). Both use the existing fakeStore pattern cleanly.
internal/budget/store_sqlite_test.go Adds TestSQLiteStoreSumUsageCostHonorsUserPathBoundaryAndCacheType which verifies path-prefix matching, cache-type exclusion, time-window filtering, and the missing-path (hasUsage=false) case in a single in-memory SQLite test.
tests/e2e/budget_test.go New E2E tests for budget enforcement and admin CRUD against an in-memory SQLite fixture. One minor issue: waitForBudgetSpent uses strict > instead of >=, inconsistent with the enforcement threshold.
tests/integration/budget_test.go Integration test covering budget enforcement with audit on/off across PostgreSQL and MongoDB. DB isolation is handled by per-subtest storage reset in SetupTestServer, and fixture.MockLLM starts fresh per call. All assertions look correct.
tests/integration/dbassert/budget.go New dbassert helpers for querying budget rows from PostgreSQL and MongoDB, checking table/collection existence, and the AssertOneSeededBudget convenience assertion. Well-factored and consistent with the existing usage.go style.
tests/integration/dbassert/usage.go Extends UsageEntry and the PostgreSQL/MongoDB query helpers to include input_cost, output_cost, and total_cost columns, needed for the budget enforcement assertions.
tests/contract/usage_contract_test.go New contract test that replays fixture responses for OpenAI, Anthropic, Gemini, Groq, and xAI, asserting that usage tokens are normalised to the OpenAI-compatible shape.
tests/e2e/manage-release-e2e-stack.sh Adds BASE_PATH= (explicit empty) to all five gateway start invocations so no accidental path prefix is inherited from the environment.
tests/e2e/release-e2e-scenarios.md Adds four new curl scenarios (S86–S89) covering budget settings validation, lifecycle (create/reset/delete), and enforcement across all three storage backends. The reset-one call uses "period":"daily" which is valid because the API accepts both the string form and period_seconds.
tests/e2e/setup_test.go Adds adminOptions, budgetChecker, and pricingResolver to e2eServerOptions and wires them into the server config and admin handler constructor to support the new budget E2E tests.
tests/integration/setup_test.go Adds BudgetsEnabled/BudgetUserPaths to TestServerConfig, includes budgets/budget_settings in the DB reset lists, adds Budgets to buildAppConfig, and introduces testPricingMetadata() with high-enough per-token pricing to exceed the small test budget in a single request.

Sequence Diagram

sequenceDiagram
    participant T as Test
    participant S as Gateway Server
    participant B as BudgetService
    participant U as UsageLogger
    participant DB as SQLite/PG/Mongo

    T->>S: POST /v1/chat/completions (req1)
    S->>B: Check(userPath, now)
    B->>DB: SumUsageCost(userPath, periodStart, now)
    DB-->>B: (0, false, nil) — no usage yet
    B-->>S: nil — allow
    S->>T: 200 OK
    S->>U: LogUsage(req1, cost)
    U-->>DB: flush after 10ms

    T->>T: waitForBudgetSpent polls Statuses() until Spent >= amount

    T->>S: POST /v1/chat/completions (req2)
    S->>B: Check(userPath, now)
    B->>DB: SumUsageCost(userPath, periodStart, now)
    DB-->>B: (0.018, true, nil) — usage flushed
    B-->>S: ExceededError (spent >= amount)
    S->>T: 429 Too Many Requests + Retry-After header
Loading

Reviews (1): Last reviewed commit: "test(budget): add budget management cove..." | Re-trigger Greptile

Comment thread tests/e2e/budget_test.go
if err != nil || len(statuses) != 1 {
return false
}
return statuses[0].Budget.UserPath == userPath && statuses[0].HasUsage && statuses[0].Spent > amount
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 waitForBudgetSpent polling condition uses strict > instead of >=

The enforcement threshold is spent >= amount (as verified by TestServiceCheckBudgetAmountBoundary's "equal amount blocks" case), but the polling here uses >. If the first request's cost lands exactly on the budget amount, this helper will not return, timing out after 2 seconds even though the budget is already enforced. In practice the mock always produces cost > amount, but the condition is semantically misleading.

Suggested change
return statuses[0].Budget.UserPath == userPath && statuses[0].HasUsage && statuses[0].Spent > amount
return statuses[0].Budget.UserPath == userPath && statuses[0].HasUsage && statuses[0].Spent >= amount

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tests/e2e/budget_test.go`:
- Around line 168-178: The close method currently captures only the first error
from usageLogger.Close() and discards db.Close()'s error; change f.closeErr
assignment inside f.closeOnce.Do to join both errors using errors.Join so both
usageLogger.Close() and db.Close() failures are preserved; update the block
handling f.closeErr to set f.closeErr = errors.Join(f.closeErr, err) (importing
the "errors" package) while keeping the existing require.NoError(t, f.closeErr)
behavior so CI logs show both errors.

In `@tests/e2e/release-e2e-scenarios.md`:
- Around line 1488-1491: The test currently hardcodes a trailing PUT to
"$BASE_URL/admin/api/v1/budgets/settings" with fixed values (daily_reset_hour:0,
weekly_reset_weekday:1, monthly_reset_day:1, etc.); change it to capture and
persist the pre-test settings before running S86 and then restore those exact
values after S86 completes by PUTting the saved payload back to the same
endpoint. Specifically, add a read GET to
"$BASE_URL/admin/api/v1/budgets/settings" to save the returned JSON, and replace
the hardcoded body in the final PUT with the saved JSON values for keys
daily_reset_hour, daily_reset_minute, weekly_reset_weekday, weekly_reset_hour,
weekly_reset_minute, monthly_reset_day, monthly_reset_hour, and
monthly_reset_minute so the environment is restored to its original state.

In `@tests/integration/budget_test.go`:
- Around line 134-151: The t.Fatalf inside the require.Eventually callback runs
on a worker goroutine and will not report properly; fix by resolving the DB-type
dispatch before calling require.Eventually: in waitForAuditEntry determine a
fetch function or closure based on fixture.DBType (call
dbassert.QueryAuditLogsByRequestID or dbassert.QueryAuditLogsByRequestIDMongo)
and call t.Fatalf immediately for an unsupported DB type, then use that fetcher
inside the require.Eventually callback; apply the same pattern to
waitForBudgetUsage / usageEntriesByRequestID so no t.Fatalf executes from within
the Eventually worker goroutine.

In `@tests/integration/dbassert/usage.go`:
- Around line 238-246: The cost decoding only checks for float64 when reading
doc["input_cost"], doc["output_cost"], and doc["total_cost"], so whole-number
BSON integers can leave those pointers nil; update the decode logic (the blocks
that set entry.InputCost, entry.OutputCost, entry.TotalCost) to also accept
int32 and int64 by converting them to float64 (or factor the logic into a helper
like bsonToFloat64(v any) (float64, bool)) and use its result to set the
pointers when present.
🪄 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: bae5b06a-d963-40f4-b655-ed4d2706aa26

📥 Commits

Reviewing files that changed from the base of the PR and between 005c035 and 534a460.

📒 Files selected for processing (11)
  • internal/budget/service_test.go
  • internal/budget/store_sqlite_test.go
  • tests/contract/usage_contract_test.go
  • tests/e2e/budget_test.go
  • tests/e2e/manage-release-e2e-stack.sh
  • tests/e2e/release-e2e-scenarios.md
  • tests/e2e/setup_test.go
  • tests/integration/budget_test.go
  • tests/integration/dbassert/budget.go
  • tests/integration/dbassert/usage.go
  • tests/integration/setup_test.go

Comment thread tests/e2e/budget_test.go
Comment on lines +168 to +178
func (f *budgetE2EFixture) close(t *testing.T) {
t.Helper()

f.closeOnce.Do(func() {
f.closeErr = f.usageLogger.Close()
if err := f.db.Close(); err != nil && f.closeErr == nil {
f.closeErr = err
}
})
require.NoError(t, f.closeErr)
}
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.

🧹 Nitpick | 🔵 Trivial

Close-error precedence drops the secondary error silently.

If usageLogger.Close() fails, db.Close()'s error is intentionally swallowed (first-error-wins). This is a common pattern but means a leaked SQLite handle wouldn't be visible alongside a logger flush failure. Acceptable for tests; flagging only because both are real resources and either failure is interesting in CI logs.

♻️ Surface both close errors via errors.Join
 	f.closeOnce.Do(func() {
-		f.closeErr = f.usageLogger.Close()
-		if err := f.db.Close(); err != nil && f.closeErr == nil {
-			f.closeErr = err
-		}
+		f.closeErr = errors.Join(f.usageLogger.Close(), f.db.Close())
 	})

(Requires importing "errors".)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/e2e/budget_test.go` around lines 168 - 178, The close method currently
captures only the first error from usageLogger.Close() and discards db.Close()'s
error; change f.closeErr assignment inside f.closeOnce.Do to join both errors
using errors.Join so both usageLogger.Close() and db.Close() failures are
preserved; update the block handling f.closeErr to set f.closeErr =
errors.Join(f.closeErr, err) (importing the "errors" package) while keeping the
existing require.NoError(t, f.closeErr) behavior so CI logs show both errors.

Comment on lines +1488 to +1491
curl -fsS -X PUT "$BASE_URL/admin/api/v1/budgets/settings" \
-H 'Content-Type: application/json' \
-d '{"daily_reset_hour":0,"daily_reset_minute":0,"weekly_reset_weekday":1,"weekly_reset_hour":0,"weekly_reset_minute":0,"monthly_reset_day":1,"monthly_reset_hour":0,"monthly_reset_minute":0}' \
>/dev/null
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.

⚠️ Potential issue | 🟡 Minor

Settings "reset" hardcodes assumed defaults rather than restoring pre-test state.

The trailing PUT writes daily_reset_hour:0, weekly_reset_weekday:1, monthly_reset_day:1, etc., which is a fixed known state, not the values that were in place before S86 ran. If the system's actual defaults ever change, this will silently leave the gateway in a non-default configuration after S86. Since later scenarios (S87–S89) only touch budgets and not settings, this is not a functional issue today, but worth flagging if you want this matrix to be fully idempotent.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/e2e/release-e2e-scenarios.md` around lines 1488 - 1491, The test
currently hardcodes a trailing PUT to "$BASE_URL/admin/api/v1/budgets/settings"
with fixed values (daily_reset_hour:0, weekly_reset_weekday:1,
monthly_reset_day:1, etc.); change it to capture and persist the pre-test
settings before running S86 and then restore those exact values after S86
completes by PUTting the saved payload back to the same endpoint. Specifically,
add a read GET to "$BASE_URL/admin/api/v1/budgets/settings" to save the returned
JSON, and replace the hardcoded body in the final PUT with the saved JSON values
for keys daily_reset_hour, daily_reset_minute, weekly_reset_weekday,
weekly_reset_hour, weekly_reset_minute, monthly_reset_day, monthly_reset_hour,
and monthly_reset_minute so the environment is restored to its original state.

Comment on lines +134 to +151
func waitForAuditEntry(t *testing.T, fixture *TestServerFixture, requestID string) dbassert.AuditLogEntry {
t.Helper()

var entries []dbassert.AuditLogEntry
require.Eventually(t, func() bool {
switch fixture.DBType {
case "postgresql":
entries = dbassert.QueryAuditLogsByRequestID(t, fixture.PgPool, requestID)
case "mongodb":
entries = dbassert.QueryAuditLogsByRequestIDMongo(t, fixture.MongoDb, requestID)
default:
t.Fatalf("unsupported DB type %q", fixture.DBType)
}
return len(entries) == 1
}, 5*time.Second, 100*time.Millisecond)

return entries[0]
}
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.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

testify require.Eventually goroutine t.Fatal

💡 Result:

Do not use require.Eventually(t, ...) with require.* assertions inside the condition function, as the condition runs in a separate goroutine spawned by assert.Eventually (which require.Eventually forwards to). require.* calls t.FailNow, which is unsafe from non-main test goroutines per Go's testing package docs and can cause panics, hangs, or flaky tests (e.g., goroutine leaks if condition overruns timeout). Use assert.Eventually or assert.EventuallyWithT instead inside the condition: - For simple bool conditions: assert.Eventually(t, func bool { ... }, waitFor, tick) - For multiple assertions: assert.EventuallyWithT(t, func(c assert.CollectT) { assert.NoError(c, err); ... }, waitFor, tick) - Use assert.(c, ...) or require.(c, ...) with the provided assert.CollectT; these collect failures for retry until final tick. - Do NOT pass outer t to require.(t, ...) in condition. require.Eventually(t, ...) is safe for the top-level call (runs in main goroutine) but problematic inside condition due to goroutine. Known issues: goroutine leaks (#1611), hangs on FailNow/Goexit (#1810), panics post-completion. Use assert versions to avoid. Examples: require.Eventually(t, func bool { return len(queue) == 0 // simple bool, no assertions }, time.Second, 10time.Millisecond) // Better for assertions: assert.EventuallyWithT(t, func(c assert.CollectT) { items, err := fetch require.NoError(c, err) // safe with c assert.Len(c, items, 0) }, time.Second, 10time.Millisecond) pkg.go.dev docs warn: require.* must be from main test goroutine.

Citations:


🏁 Script executed:

# Find and examine the test file
fd budget_test.go

Repository: ENTERPILOT/GoModel

Length of output: 120


🏁 Script executed:

# Read the relevant sections of the test file
head -160 tests/integration/budget_test.go | tail -60

Repository: ENTERPILOT/GoModel

Length of output: 1988


🏁 Script executed:

# Also check the beginning to see the test table
head -50 tests/integration/budget_test.go

Repository: ENTERPILOT/GoModel

Length of output: 1411


t.Fatalf inside require.Eventually callback runs on a worker goroutine.

require.Eventually invokes the condition function in a separate goroutine. The Go testing package requires Fatal/Fatalf to be called only from the test goroutine; from a worker goroutine runtime.Goexit only kills the worker, so Eventually keeps ticking until the 5s timeout and you lose the error message. The same concern applies transitively in waitForBudgetUsage (line 113) via usageEntriesByRequestID's t.Fatalf (line 129).

In practice the table-driven test only passes "postgresql"/"mongodb" so the default branches never trigger, but this pattern will silently degrade error reporting if DB type validation or fallback logic ever changes.

♻️ Resolve the DB selection once, outside Eventually
 func waitForAuditEntry(t *testing.T, fixture *TestServerFixture, requestID string) dbassert.AuditLogEntry {
 	t.Helper()
 
+	var query func() []dbassert.AuditLogEntry
+	switch fixture.DBType {
+	case "postgresql":
+		query = func() []dbassert.AuditLogEntry {
+			return dbassert.QueryAuditLogsByRequestID(t, fixture.PgPool, requestID)
+		}
+	case "mongodb":
+		query = func() []dbassert.AuditLogEntry {
+			return dbassert.QueryAuditLogsByRequestIDMongo(t, fixture.MongoDb, requestID)
+		}
+	default:
+		t.Fatalf("unsupported DB type %q", fixture.DBType)
+	}
+
 	var entries []dbassert.AuditLogEntry
 	require.Eventually(t, func() bool {
-		switch fixture.DBType {
-		case "postgresql":
-			entries = dbassert.QueryAuditLogsByRequestID(t, fixture.PgPool, requestID)
-		case "mongodb":
-			entries = dbassert.QueryAuditLogsByRequestIDMongo(t, fixture.MongoDb, requestID)
-		default:
-			t.Fatalf("unsupported DB type %q", fixture.DBType)
-		}
+		entries = query()
 		return len(entries) == 1
 	}, 5*time.Second, 100*time.Millisecond)
 
 	return entries[0]
 }

A similar treatment for waitForBudgetUsage resolves the indirect t.Fatalf reached via usageEntriesByRequestID: move the DB-type dispatch into a closure or helper, and call it from the Eventually callback to ensure the error check runs in the test goroutine.

📝 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
func waitForAuditEntry(t *testing.T, fixture *TestServerFixture, requestID string) dbassert.AuditLogEntry {
t.Helper()
var entries []dbassert.AuditLogEntry
require.Eventually(t, func() bool {
switch fixture.DBType {
case "postgresql":
entries = dbassert.QueryAuditLogsByRequestID(t, fixture.PgPool, requestID)
case "mongodb":
entries = dbassert.QueryAuditLogsByRequestIDMongo(t, fixture.MongoDb, requestID)
default:
t.Fatalf("unsupported DB type %q", fixture.DBType)
}
return len(entries) == 1
}, 5*time.Second, 100*time.Millisecond)
return entries[0]
}
func waitForAuditEntry(t *testing.T, fixture *TestServerFixture, requestID string) dbassert.AuditLogEntry {
t.Helper()
var query func() []dbassert.AuditLogEntry
switch fixture.DBType {
case "postgresql":
query = func() []dbassert.AuditLogEntry {
return dbassert.QueryAuditLogsByRequestID(t, fixture.PgPool, requestID)
}
case "mongodb":
query = func() []dbassert.AuditLogEntry {
return dbassert.QueryAuditLogsByRequestIDMongo(t, fixture.MongoDb, requestID)
}
default:
t.Fatalf("unsupported DB type %q", fixture.DBType)
}
var entries []dbassert.AuditLogEntry
require.Eventually(t, func() bool {
entries = query()
return len(entries) == 1
}, 5*time.Second, 100*time.Millisecond)
return entries[0]
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/integration/budget_test.go` around lines 134 - 151, The t.Fatalf inside
the require.Eventually callback runs on a worker goroutine and will not report
properly; fix by resolving the DB-type dispatch before calling
require.Eventually: in waitForAuditEntry determine a fetch function or closure
based on fixture.DBType (call dbassert.QueryAuditLogsByRequestID or
dbassert.QueryAuditLogsByRequestIDMongo) and call t.Fatalf immediately for an
unsupported DB type, then use that fetcher inside the require.Eventually
callback; apply the same pattern to waitForBudgetUsage / usageEntriesByRequestID
so no t.Fatalf executes from within the Eventually worker goroutine.

Comment on lines +238 to +246
if v, ok := doc["input_cost"].(float64); ok {
entry.InputCost = &v
}
if v, ok := doc["output_cost"].(float64); ok {
entry.OutputCost = &v
}
if v, ok := doc["total_cost"].(float64); ok {
entry.TotalCost = &v
}
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.

🧹 Nitpick | 🔵 Trivial

Consider int32/int64 fallback for cost decoding for parity with token decoding.

Token fields above (lines 223-237) fall back through int32/int64 because BSON encodes whole numbers as integer types. Cost fields here only check float64, so a document where a cost happens to be persisted as a whole number (e.g., a fixture written with bson.M{"input_cost": 0}) would silently leave the pointer nil and surface as a hard-to-debug assertion failure in tests/integration/budget_test.go (which waits for non-nil TotalCost). In normal flows the producer writes *float64, so this is unlikely to occur in production data, but adding the fallback would make this assertion helper more robust.

♻️ Optional refactor
-	if v, ok := doc["input_cost"].(float64); ok {
-		entry.InputCost = &v
-	}
-	if v, ok := doc["output_cost"].(float64); ok {
-		entry.OutputCost = &v
-	}
-	if v, ok := doc["total_cost"].(float64); ok {
-		entry.TotalCost = &v
-	}
+	if v, ok := bsonToFloat64(doc["input_cost"]); ok {
+		entry.InputCost = &v
+	}
+	if v, ok := bsonToFloat64(doc["output_cost"]); ok {
+		entry.OutputCost = &v
+	}
+	if v, ok := bsonToFloat64(doc["total_cost"]); ok {
+		entry.TotalCost = &v
+	}
func bsonToFloat64(v any) (float64, bool) {
    switch n := v.(type) {
    case float64:
        return n, true
    case int32:
        return float64(n), true
    case int64:
        return float64(n), true
    }
    return 0, false
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/integration/dbassert/usage.go` around lines 238 - 246, The cost
decoding only checks for float64 when reading doc["input_cost"],
doc["output_cost"], and doc["total_cost"], so whole-number BSON integers can
leave those pointers nil; update the decode logic (the blocks that set
entry.InputCost, entry.OutputCost, entry.TotalCost) to also accept int32 and
int64 by converting them to float64 (or factor the logic into a helper like
bsonToFloat64(v any) (float64, bool)) and use its result to set the pointers
when present.

@SantiagoDePolonia SantiagoDePolonia merged commit 1bc20ab into main Apr 27, 2026
20 checks passed
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