Repository navigation
test(config): share one migrated Postgres container across integration tests - #1165
Conversation
…n tests The internal/config integration tests started a PostgreSQL container and ran the full migration chain for almost every test: 239 containers per run, about 15 of the CI job's 20-minute go test timeout. testhelpers.RequireMigratedDatabase starts one container per test binary, migrates a template database once, and gives each test its own CREATE DATABASE ... TEMPLATE copy, dropped WITH (FORCE) when the test ends. Every test still starts from a freshly migrated database nothing else writes to; skip/fail semantics match RequirePostgresContainer (#1597). The template is sealed against connections so no test can change later tests' starting state. All internal/config call sites now use it, a TestMain stops the shared container, and a parse-only guard keeps per-test containers out of the package. Locally the package goes from 353-438s to 23-27s with the same 1312 passing results and 11 env-gated skips. Closes LeanerCloud#982 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Note Currently processing new changes in this PR. This may take a few minutes, please wait... ⚙️ Run configuration
📒 Files selected for processing: 14
✨ Finishing Touches
Comment |
… onto the shared database LeanerCloud#1088 merged with its own per-test container; the guard from this PR flags it. Use testhelpers.RequireMigratedDatabase like the rest of the package. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
VERDICT: APPROVE a730b17 PR #1165 (closes #982), head a730b17, base origin/main f4387e4 (9 commits ahead of the merge base). Findings (none blocking):
Leakage and parallelism: per-test databases are distinct and dropped with FORCE. Roles, ALTER SYSTEM and per-database settings (ALTER DATABASE) do not appear in migrations; the one ALTER DATABASE in tests (store_postgres_ladder_lock_integration_test.go:220) targets the test's own copy, which is then dropped. Advisory locks are scoped per database. No integration-tagged config test uses t.Parallel. The unfiltered pg_stat_activity probes (ladder_amend:139, marketplace_claim:67, ri_exchange_*) are safe only while the tests stay sequential, as the PR body says. Connection budget: the admin pool (10) plus 8 concurrent pools of 10 stays under PG's default limit of 100. Attacked:
|
|
Independent review 2 (claude-fleet, cold): APPROVE a730b17 Findings (none blocking):
Attacked:
Verified locally, on a
Reviewed by an independent agent with no author context (second reviewer). Review only: nothing was pushed, and the GitHub review state is unchanged. |
|
Local verification at
|
|
Marked ready and merged by the owner's agent after 2 independent reviews and local verification (see the review and verification comments above, all at |
Resolve the conflict in internal/config/store_postgres_recommendations_test.go with LeanerCloud#1165 (shared migrated Postgres container): keep main's setupRecommendationsStore on testhelpers.RequireMigratedDatabase and drop TestPostgresStore_ReplaceRecommendations, which this branch removes along with the method. The wipeRecommendations helper stays; its comment now says subtests share one test database rather than one container. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tgres container After merging main, TestIntegrationTestsShareOnePostgresContainer (LeanerCloud#1165) flagged the two new ClaimCollectionInvocation integration tests for starting a container each. They now take a migrated database from testhelpers.RequireMigratedDatabase, like the rest of internal/config. Also from review and the merge: - give sharedTaskLocker AdvisoryLockHeldFor, which main added to TaskLocker - claimCollectionInvocation clears OwnerToken on a copy of params instead of returning a fresh struct, so a field added later is not dropped - point the markerStore doc at store_postgres_collection_invocation_test.go and the claim doc at runScheduledTask - use mocks.MockInvocationToken in the analytics and server test fakes Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ed container Main's LeanerCloud#1165 moved internal/config's integration tests onto one shared PostgreSQL container, and TestIntegrationTestsShareOnePostgresContainer fails on any test that starts its own. TestPurchaseHistoryLegacyDuplicates_Postgres now gets a freshly migrated database of its own from testhelpers.RequireMigratedDatabase, like setupTestContainerDB does. Refs LeanerCloud#267 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… precision (#1252) checkRIExchangeCloudCheck and validateLadderUnverifiedFail refuse a cloud check that does not postdate the row's last change, but compared the operator's nanosecond checked_at against an updated_at that Postgres had truncated to microseconds. A check stamped at the change's own instant read as a few hundred nanoseconds after it and was accepted, which is what TestPostgresStoreDB_ResolveStaleRIExchange_ValidatesCloudCheck catches now that #1165 runs it against the shared container. Truncate checked_at to microseconds before the "after the last change" comparison in both paths (checkPostdatesDBTime), and add unit tests that pin the boundary without a database. Closes #1251 Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…, not the marker (#1099) * fix(scheduler): scope the collection marker release to the invocation, not the marker The owner token MarkCollectionStarted hands the refresh handler is shared by every Lambda delivery of the async collect event, and async delivery is at-least-once. When a duplicate delivery was skipped by the task advisory lock, releaseSkippedCollectionMarker cleared the marker with that shared token while the lock winner was still collecting, so the in-progress banner dropped early and a refresh in the remaining window returned 202 without collecting. The lock winner now calls ClaimCollectionInvocation right after taking the lock. It re-stamps last_collection_owner_id with a fresh invocation token when the marker is still owned by the marker token, or free or stale (the latter covers a duplicate that cleared in the window between the winner's lock and its re-stamp, and a late redelivery). The winner collects and clears under the new token, so the duplicate's skip-path clear no longer matches. A run skipped behind a tokenless cron run still releases its own marker. No schema change: the owner column already exists. Closes #84 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(config): move the #84 store tests onto the shared Postgres container After merging main, TestIntegrationTestsShareOnePostgresContainer (#1165) flagged the two new ClaimCollectionInvocation integration tests for starting a container each. They now take a migrated database from testhelpers.RequireMigratedDatabase, like the rest of internal/config. Also from review and the merge: - give sharedTaskLocker AdvisoryLockHeldFor, which main added to TaskLocker - claimCollectionInvocation clears OwnerToken on a copy of params instead of returning a fresh struct, so a field added later is not dropped - point the markerStore doc at store_postgres_collection_invocation_test.go and the claim doc at runScheduledTask - use mocks.MockInvocationToken in the analytics and server test fakes Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…purchase_id (#1103) * fix(config): use the earliest row when purchase_history duplicates a purchase_id Rows saved before migration 000108 can still share a purchase_id, and deleting them is a pending owner decision. GetPurchaseHistoryByPurchaseID returned whichever duplicate the planner found first, and ClaimMarketplaceListingSlot joined every duplicate to every other, so with duplicates whose listing state diverged it could claim an RI that already had an active listing and report another row's prior state and listing id. The lookup now orders by (created_at, id), and the claim locks and updates only that earliest row, refusing while any duplicate records an active listing. Closes #267 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(config): take the legacy-duplicate test's database from the shared container Main's #1165 moved internal/config's integration tests onto one shared PostgreSQL container, and TestIntegrationTestsShareOnePostgresContainer fails on any test that starts its own. TestPurchaseHistoryLegacyDuplicates_Postgres now gets a freshly migrated database of its own from testhelpers.RequireMigratedDatabase, like setupTestContainerDB does. Refs #267 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Root cause
Almost every
internal/configintegration test started its own PostgreSQL container and then applied all 123 migrations to it. The helpers involved weresetupTestContainerDB,setupLadderStore,setupRampStepStore,setupRecommendationsStore, and 14 directRequirePostgresContainer/SetupPostgresContainercall sites. One local run started 239 containers, so the package's runtime grows with every container-backed test added. In CI that came to roughly 15 of the 20 minutes allowed bygo test -timeout=20m(#202).Fix
testhelpers.RequireMigratedDatabase(ctx, t, migrationsPath)(new,internal/database/postgres/testhelpers/shared.go):migrations.RunMigrations. It then seals the template withIS_TEMPLATE true ALLOW_CONNECTIONS false, so no test can connect to it and change the starting state of later tests.CREATE DATABASE … TEMPLATEcopy and closes the pool when the test ends. It then runsDROP DATABASE … WITH (FORCE), so a leaked connection can't keep the database around. A failed drop fails the test.RequirePostgresContainer(#1597). It skips only whenSkipIfProviderIsNotHealthysays Docker is unusable. A container that won't start, or a migration that won't apply, fails the test. Both errors are remembered, so the rest of the binary fails immediately instead of each test waiting out its own startup timeout.internal/configchanges:TestMain(integration tag) stops the shared container. Ryuk still reaps it if the process dies.TestIntegrationTestsShareOnePostgresContaineris a parse-only guard with no build tag. It fails if a test file in the package callstesthelpers.RequirePostgresContainerorSetupPostgresContaineragain.No test was removed, skipped, or weakened. The assertions are untouched. The diff only changes how each test gets its database.
One side effect, in the safe direction for CI: the 12 recommendations and
store_postgres_test.gotests used the bareSetupPostgresContainerand failed when Docker was absent. Through the new helper they now skip on a missing Docker provider, like the rest of the package under #1597. CI has Docker, so CI behavior is unchanged.Closes #982
Verification
All runs were local on macOS (Docker Desktop, 10 CPUs) with CI's flags.
AWS_CONFIG_FILE=/dev/null AWS_SHARED_CREDENTIALS_FILE=/dev/nullwas set because this machine's default AWS profile is invalid.Runtime, before vs after. This is integration evidence against real PostgreSQL 16 in testcontainers:
docker events … event=start)-shuffle=on(seed 1792454946712618000),-timeout=20mA per-test diff of the
-jsonoutput shows the same test set with no status changes. The only addition is the new guard. The 11 skips are the same env-gated tests on both sides (*_TEST_DATABASE_URL, unset in CI too). CI's runner is slower than this machine (about 900s on main per #202/#984), but nearly all of that time was per-test container and migration setup, and that setup now happens once.Fail-before / pass-after for the guard (unit, parse-only):
With main's test files it fails and lists 18 per-test container call sites. On this branch it passes.
Helper integration tests (
internal/database/postgres/testhelpers/shared_integration_test.go,-race -tags=integration) all pass:IsolatedCopiesOnOneContainer: distinct databases on one server, both at the latest migration version and not dirty. A table and a row written in one copy are invisible to a sibling and to a copy made later, so the template stays clean.TemplateRefusesConnections: connecting to the template is refused.DropsDatabaseWhenTestEnds: the per-test database is gone after its test, even though the test leaked an open connection.ConcurrentFirstUse: starts cold, then 8 parallel callers. They get 8 distinct, fully migrated databases on 1 container from 1 template.Mutation probes on
shared.go. Each was reverted afterwards, and each one fails at least one of the tests above:ALLOW_CONNECTIONS falseremoved):TemplateRefusesConnectionsfails.FORCE:DropsDatabaseWhenTestEndsfails.DropsDatabaseWhenTestEndsfails.newDatabase:ConcurrentFirstUsefails. 5 callers error out, and only 3 distinct databases are created for 8 callers.Other checks, all clean:
go build ./...go vetandgo vet -tags=integrationon./internal/config/and./internal/database/.... The second confirms no import cycle now that testhelpers imports migrations; all migrations tests are inmigrations_test.--build-tags integration, on./internal/config/... ./internal/database/postgres/testhelpers/...: 0 issues.gocyclo -over 10 -ignore "_test\.go"on both packages: silent.gofmt -l: clean.go test ./internal/mocks/ ./internal/database/postgres/testhelpers/(repo-wide guards, includingTestNoSkipOnDatabaseSetupFailure): pass.go test -short -race ./internal/config/: pass.scripts/test-git-secrets-allowlist.sh) passes 42/42. A scan of the changed files in a fresh repo with an isolated HOME is clean.Notes and out of scope
testhelpers.RequirePostgresContainerfor the RI-exchange reservation tests ininternal/config. After merging main, the guard flagged it, so a730b17 switches it toRequireMigratedDatabase. Its 7 tests pass on the shared database (go test -tags integration -race -run 'ReserveApprovedRIExchange|DailySpend|ReserveRIExchange' ./internal/config/).t.Parallelwould now be cheap to add, but it wasn't needed to get well under the timeout, and it would introduce cross-test interleavings. Before anyone adds it, thepg_stat_activityprobes instore_postgres_ladder_amend_integration_test.go,store_postgres_marketplace_claim_integration_test.go, and the RI-exchange tests should filter ondatname = current_database(), astesthelpers.WaitForLockWaitalready does.internal/api,internal/server,internal/purchase, …) still start a container per test. They can switch to the same helper when they start to matter. I didn't change them here.Maintainer: please mirror the issue's triage labels on this PR:
impact/internal,type/chore,triaged,priority/p3,severity/low,urgency/this-quarter,effort/s.🤖 Generated with Claude Code