From dbde75e85cb6e61e68584546efefa351ac7d8ef4 Mon Sep 17 00:00:00 2001 From: hugo Date: Wed, 17 Jun 2026 23:30:40 +0000 Subject: [PATCH] rating: run the integration conformance test against the REAL migration DDL MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The live-Postgres conformance test (TestIntegration_*/TestConformance_*) proved the rater's production SQL computes correct money — but against a hand-copied `schemaDDL` constant, not the migration that actually ships. The migration comments said "keep the two in sync"; that sync was enforced by a human reading two files. A column-type or index-expression drift between the copy and the migration would pass green here and break in prod — exactly the silent wrong-money failure this test exists to prevent. Replace the hand-copied constant with `ratingSchemaDDL(t)`, which loads the real migration files (migrations/0001_billing_event.sql then 0002_rating.sql, the production apply order — base_model is declared in 0001 and re-added IF NOT EXISTS in 0002_rating, a harmless overlap). The schema now CANNOT drift from production: if a migration changes, the conformance test runs against the new shape automatically. The old constant is retained, renamed `schemaDDLReference`, as a readable in-file description only (not applied — editing it edits a comment). The CI integration-test job (postgres:16 service, `make integration-test`) was already wired but tested a copy; it now tests the real schema on every PR. Verified: builds with -tags=integration, go vet clean, the full conformance suite passes against a throwaway postgres:16 loading the migration DDL, and the non-integration unit suite is unaffected. Co-Authored-By: Claude Opus 4.8 --- internal/rating/store_integration_test.go | 62 +++++++++++++++++------ 1 file changed, 46 insertions(+), 16 deletions(-) diff --git a/internal/rating/store_integration_test.go b/internal/rating/store_integration_test.go index 3a668a5..df7fe37 100644 --- a/internal/rating/store_integration_test.go +++ b/internal/rating/store_integration_test.go @@ -25,10 +25,40 @@ import ( _ "github.com/jackc/pgx/v5/stdlib" ) -// schemaDDL is the rating schema (just billing_event + rated_usage now — prices are -// a YAML file, not DB tables), created in an isolated schema per test run so the -// test is self-contained and leaves no residue. -const schemaDDL = ` +// ratingSchemaDDL returns the rating schema applied before each integration test +// (billing_event + rated_usage — prices are a YAML file, not DB tables). It is +// loaded from the REAL migration .sql files rather than a hand-copied constant, so +// the conformance test runs against EXACTLY the production schema and can never +// silently drift from it: a column-type or index-expression change in the +// migration that the test didn't track would otherwise pass green here and break +// in prod. Loading 0001 then 0002_rating reproduces the production apply order; +// base_model is declared in 0001 and re-added IF NOT EXISTS in 0002_rating, so the +// overlap is a harmless no-op. The DDL runs inside the per-test isolated schema +// (search_path is set by the caller), leaving no residue. io_log (0002_io_log) is +// intentionally not loaded — the rater never touches it. +func ratingSchemaDDL(t *testing.T) string { + t.Helper() + var b strings.Builder + for _, f := range []string{ + "../../migrations/0001_billing_event.sql", + "../../migrations/0002_rating.sql", + } { + ddl, err := os.ReadFile(f) + if err != nil { + t.Fatalf("read migration %s: %v (the integration test applies the REAL "+ + "migration DDL so it can't drift from production)", f, err) + } + b.Write(ddl) + b.WriteString("\n") + } + return b.String() +} + +// schemaDDLReference is the previously hand-maintained schema, kept ONLY as a +// readable in-file description of what the loaded migrations produce. It is NOT +// applied (ratingSchemaDDL loads the real files); if you edit it, you are editing +// a comment. Drift between this and the migrations is now harmless. +const schemaDDLReference = ` -- billing_event mirrors the v1 metering schema (migration 0001): the model NAME -- lives in the model column, which the rater aliases to model_id (the price key). CREATE TABLE billing_event ( @@ -104,7 +134,7 @@ func TestIntegration_RateWindow_ConformsToOracle(t *testing.T) { exec(t, db, "CREATE SCHEMA "+sch) exec(t, db, "SET search_path TO "+sch) defer func() { exec(t, db, "DROP SCHEMA IF EXISTS "+sch+" CASCADE") }() - exec(t, db, schemaDDL) + exec(t, db, ratingSchemaDDL(t)) hour := mustTime("2026-06-08T10:00:00Z") book := conformanceBook() @@ -244,7 +274,7 @@ func TestIntegration_ResourceIDGrainAndFailClosed(t *testing.T) { exec(t, db, "CREATE SCHEMA "+sch) exec(t, db, "SET search_path TO "+sch) defer func() { exec(t, db, "DROP SCHEMA IF EXISTS "+sch+" CASCADE") }() - exec(t, db, schemaDDL) + exec(t, db, ratingSchemaDDL(t)) hour := mustTime("2026-06-08T10:00:00Z") book := newTestBook( @@ -356,7 +386,7 @@ func TestConformance_PremiumQuantizedBeforeBilling(t *testing.T) { exec(t, db, "CREATE SCHEMA "+sch) exec(t, db, "SET search_path TO "+sch) defer func() { exec(t, db, "DROP SCHEMA IF EXISTS "+sch+" CASCADE") }() - exec(t, db, schemaDDL) + exec(t, db, ratingSchemaDDL(t)) hour := mustTime("2026-06-08T10:00:00Z") @@ -449,7 +479,7 @@ func TestConformance_OracleQuantizesBeforeMultiply_OnResidue(t *testing.T) { exec(t, db, "CREATE SCHEMA "+sch) exec(t, db, "SET search_path TO "+sch) defer func() { exec(t, db, "DROP SCHEMA IF EXISTS "+sch+" CASCADE") }() - exec(t, db, schemaDDL) + exec(t, db, ratingSchemaDDL(t)) hour := mustTime("2026-06-08T10:00:00Z") book := newTestBook( @@ -541,7 +571,7 @@ func TestIntegration_FineTunePricesViaBaseModel(t *testing.T) { exec(t, db, "CREATE SCHEMA "+sch) exec(t, db, "SET search_path TO "+sch) defer func() { exec(t, db, "DROP SCHEMA IF EXISTS "+sch+" CASCADE") }() - exec(t, db, schemaDDL) + exec(t, db, ratingSchemaDDL(t)) hour := mustTime("2026-06-08T10:00:00Z") // File declares ONLY the base; the ft: checkpoint id is not listed. 1.5× premium. @@ -635,7 +665,7 @@ func TestIntegration_FineTuneAmbiguousBaseModelFailsLoud(t *testing.T) { exec(t, db, "CREATE SCHEMA "+sch) exec(t, db, "SET search_path TO "+sch) defer func() { exec(t, db, "DROP SCHEMA IF EXISTS "+sch+" CASCADE") }() - exec(t, db, schemaDDL) + exec(t, db, ratingSchemaDDL(t)) hour := mustTime("2026-06-08T10:00:00Z") book := newTestBook( @@ -723,7 +753,7 @@ func TestIntegration_ReRateReconciles(t *testing.T) { exec(t, db, "CREATE SCHEMA "+sch) exec(t, db, "SET search_path TO "+sch) defer func() { exec(t, db, "DROP SCHEMA IF EXISTS "+sch+" CASCADE") }() - exec(t, db, schemaDDL) + exec(t, db, ratingSchemaDDL(t)) hour := mustTime("2026-06-08T10:00:00Z") book := newTestBook( @@ -823,7 +853,7 @@ func TestIntegration_ReRateReconcileLeavesOtherWindowsUntouched(t *testing.T) { exec(t, db, "CREATE SCHEMA "+sch) exec(t, db, "SET search_path TO "+sch) defer func() { exec(t, db, "DROP SCHEMA IF EXISTS "+sch+" CASCADE") }() - exec(t, db, schemaDDL) + exec(t, db, ratingSchemaDDL(t)) hour10 := mustTime("2026-06-08T10:00:00Z") hour12 := mustTime("2026-06-08T12:00:00Z") @@ -897,7 +927,7 @@ func TestIntegration_OneHopFineTuneCannotDeriveFromFineTune(t *testing.T) { exec(t, db, "CREATE SCHEMA "+sch) exec(t, db, "SET search_path TO "+sch) defer func() { exec(t, db, "DROP SCHEMA IF EXISTS "+sch+" CASCADE") }() - exec(t, db, schemaDDL) + exec(t, db, ratingSchemaDDL(t)) hour := mustTime("2026-06-08T10:00:00Z") // A true base AND an own-rate fine-tune (ft:ownrate) priced directly in the file. @@ -968,7 +998,7 @@ func TestIntegration_UTCBucketing_SessionTZIndependent(t *testing.T) { exec(t, db, "CREATE SCHEMA "+sch) exec(t, db, "SET search_path TO "+sch) defer func() { exec(t, db, "DROP SCHEMA IF EXISTS "+sch+" CASCADE") }() - exec(t, db, schemaDDL) + exec(t, db, ratingSchemaDDL(t)) hour := mustTime("2026-06-08T10:00:00Z") book := newTestBook( @@ -1038,7 +1068,7 @@ func TestIntegration_RatingInstantIndexServesScan(t *testing.T) { exec(t, db, "CREATE SCHEMA "+sch) exec(t, db, "SET search_path TO "+sch) defer func() { exec(t, db, "DROP SCHEMA IF EXISTS "+sch+" CASCADE") }() - exec(t, db, schemaDDL) + exec(t, db, ratingSchemaDDL(t)) exec(t, db, "SET enable_seqscan = off") plan := explainPlan(t, db, `EXPLAIN SELECT * FROM billing_event @@ -1087,7 +1117,7 @@ func TestIntegration_ReconcileDeleteCanUseWindowStartIndex(t *testing.T) { exec(t, db, "CREATE SCHEMA "+sch) exec(t, db, "SET search_path TO "+sch) defer func() { exec(t, db, "DROP SCHEMA IF EXISTS "+sch+" CASCADE") }() - exec(t, db, schemaDDL) + exec(t, db, ratingSchemaDDL(t)) // POPULATE a small, ANALYZE'd population: 10 distinct auth_ids × 24 hours = 240 // rated_usage rows across a couple dozen windows. That is enough for the