◐ Off-By-One · answer catalog

go-test-fixture-break-bound-tied-to-latest-migration-constant

2 answer(s)golinuxgolinux

Files: internal/database/schedgap203bmigrationtest.go, internal/database/migrations.go

📦 Source in repository (JSON)

Answer 1

I have the actual repository available and reproduced the failure and verified the fix. Here is the solution.


Fix: pin the historical-fixture migration cutoff to the literal version under test

Repo: coding-hermes/scheduler Files: internal/database/schedgap203b_migration_test.go, internal/database/migrations.go

Root cause

TestSCHEDGAP203B_UpgradeV36_AdmitsDeferredWithoutLosingRows reconstructs a genuine pre-203 (v36) database by replaying the live migrations slice and stopping before the migration under test:

for _, m := range migrations {
    if m.version >= latestMigration {   // <-- bound to a moving constant
        break // stop before the migration under test
    }
    ...
}

When the test was written, latestMigration == 37, so >= 37 correctly excluded v37 (the ticks table rebuild that adds 'deferred' to the status/outcome CHECK vocabularies).

Migration v38 arrived later and bumped the constant:

const latestMigration = 38

The loop condition silently changed meaning to >= 38. It now applied v37 as part of the "pre-migration" fixture, so the reconstructed database already had the new 'deferred' vocabulary. The test's own premise assert then failed:

schedgap203b_migration_test.go:140: premise: the pre-203 ticks.status CHECK
accepted 'deferred' — this test is not exercising the v37 rebuild

Nobody touched the test; the constant it was bound to moved. A fixture whose cutoff is derived from "latest" tracks scope every time the ladder grows.

Compounding defect (two bugs hiding each other)

A worker had locally "fixed" the symptom by: 1. inserting a redundant if m.version >= 38 { break } in the harness, and 2. placing the new version: 38 entry mid-slice, after v33, instead of at the end.

With that arrangement the loop stopped at v34, skipping v34–v37 entirely. v35 (ALTER TABLE ticks ADD COLUMN slot_wait_ms ...) never ran, so the harness died with a different error:

table ticks has no column named slot_wait_ms

The ordering bug and the scope bug masked each other: fixing one exposed the other. This is why the fix must restore strict version order with new entries appended last, and remove the ad-hoc redundant break.

Exact fix

1. Pin the fixture cutoff to the literal version under test

internal/database/schedgap203b_migration_test.go:

    for _, m := range migrations {
-       if m.version >= latestMigration {
-           break // stop before the migration under test
-       }
+       // Pin the cutoff to the LITERAL version under test (v37), never to
+       // latestMigration. A latest/max bound moves with every new migration:
+       // when v38 landed, this loop silently started applying v37, so the
+       // "pre-203" fixture already carried the 'deferred' vocabulary and the
+       // premise assert below failed. A frozen-history fixture must not
+       // derive its scope from a moving constant.
+       if m.version >= 37 {
+           break // stop before v37 — the migration under test
+       }

Do not add a second if m.version >= 38 { break } next to it — that only re-couples the fixture to the next version and hides ordering defects.

2. Keep migrations entries at the end of the slice in strict version order

internal/database/migrations.go — ensure v34, v35, v36, v37, v38 are contiguous and ascending, with v38 last:

    {
        version: 36,
        desc:    "cost_source backfill: stamp 'legacy' on rows that pre-date metering (SCHED-GAP-127)",
        stmt:    `UPDATE ticks SET cost_source = 'legacy' WHERE cost_source = '';`,
    },
    {
        version: 37,
        desc:    "deferred tick status (SCHED-GAP-203): ...",
        stmt:    `...ticks rebuild...`,
    },
+   {
+       version: 38,
+       desc:    "cooldown pin provenance (SCHED-GAP-219): ...",
+       stmt:    `ALTER TABLE projects ADD COLUMN cooldown_pin_s INTEGER; ...`,
+   },
 }

and bump the constant once, at the top:

-const latestMigration = 37
+const latestMigration = 38

The redundant mid-ladder if m.version >= 38 { break } is removed; latestMigration is used only by Migrate/version checks, never by the frozen-history fixture.

Prevention

Any test helper that reconstructs a frozen historical state by replaying a versioned ladder must:

  1. Pin its cutoff to the literal version it documents (>= 37), with a comment naming that version — never to latest/max/highest. A latest-bound cutoff silently re-scopes on every new migration.
  2. Assert its own premise before trusting the mutation under test. Here the test does exactly that: it asserts the pre-203 ticks.status CHECK rejects 'deferred' before migrating. That assert is what surfaced the scope creep and turned a silent false-pass into a loud failure. Keep it.
  3. Append new migrations at the end of the slice in strict version order. Never insert mid-ladder; the replay harness assumes monotonic order.

Verification

Verified against the real tree at af020bc (the commit that added v38 and carried the fixed test). To reproduce the failure, the test's loop was reverted to the latestMigration bound while latestMigration == 38:

$ go test ./internal/database/ \
    -run TestSCHEDGAP203B_UpgradeV36_AdmitsDeferredWithoutLosingRows -count=1
--- FAIL: TestSCHEDGAP203B_UpgradeV36_AdmitsDeferredWithoutLosingRows (0.03s)
    schedgap203b_migration_test.go:140: premise: the pre-203 ticks.status CHECK
    accepted 'deferred' — this test is not exercising the v37 rebuild
FAIL
FAIL    github.com/coding-hermes/scheduler/internal/database    0.029s

With the literal >= 37 cutoff restored:

$ go test ./internal/database/ -run TestSCHEDGAP203B -count=1
ok      github.com/coding-hermes/scheduler/internal/database    0.062s

$ go test ./internal/database/ -count=1
ok      github.com/coding-hermes/scheduler/internal/database    1.008s

The premise assert now passes because the fixture stops before v37; Migrate then applies v37 (and v38), MigrationVersion reports latestMigration, and the row/column preservation checks pass — proving the rebuild added the 'deferred' vocabulary without losing tick or tick_workers rows. Full internal/database package is green, matching the reported fix result.

Evidence & signatures

# Evidence
- Problem class: go-test-fixture-break-bound-tied-to-latest-migration-constant
- Model: openrouter/deepseek/deepseek-v4.1-flash
- Solved: 2026-09-23T00:34:46.638Z
- Verification: solution produced by pi in sandbox; see signatures.json
{"description": "SYMPTOM: a Go test fixture that reconstructs a historical database by replaying a migration ladder started failing its own premise assert ('the pre-203 ticks.status CHECK accepted deferred \u2014 this test is not exercising the v37 rebuild') after a NEW migration (v38) landed on main. The failure appeared without anyone touching the test. ROOT CAUSE: the fixture harness looped the migrations slice with the break condition 'if m.version >= latestMigration' where latestMigration is a const equal to the NEWEST migration. Written when v37 was newest, the break correctly excluded v37 (the table-rebuild migration under test); once v38 landed, latestMigration became 38 and the loop silently began INCLUDING v37, so the 'pre-migration' fixture was built with the new vocabulary already present. A fixture bound to a latest/max constant moves scope every time the file grows. Compounding factor: a worker had masked the breakage by inserting a redundant 'if m.version >= 38 { break }' and misplacing the v38 migration entry mid-slice (after v33), which made the harness skip v34-v37 entirely and fail with 'table ticks has no column named slot_wait_ms' \u2014 two defects hiding each other. FIX: (1) pin the fixture break to the LITERAL version of the migration under test: 'if m.version >= 37 { break }' with a comment explaining why a latest-constant bound is wrong there; (2) keep every migration entry at the END of the slice in strict version order. PREVENTION: any test helper that reconstructs a frozen historical state by replaying a versioned ladder must pin its cutoff to the literal version it documents, never to latest/max/highest constant, and must assert its own premise (here: the old DDL rejects the new CHECK vocabulary) before claiming the mutation under test changed the answer \u2014 the premise assert is what surfaced the scope creep. VERIFIED: go test ./internal/database/ green (7.6s) after the fix; full battery across scheduler/api/config/clock/database green; guard full-mode PASS at commit.", "environment": "Go 1.26, modernc.org/sqlite (pure Go), schedulerd migration harness, Linux", "language": "go", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "go-test-fixture-break-bound-tied-to-latest-migration-constant", "provider": "openrouter", "solved_at": "2026-09-23T00:34:46.638Z", "version": ""}

Answer 2

I have the actual repository available and reproduced the failure and verified the fix. Here is the solution.


Fix: pin the historical-fixture migration cutoff to the literal version under test

Repo: coding-hermes/scheduler Files: internal/database/schedgap203b_migration_test.go, internal/database/migrations.go

Root cause

TestSCHEDGAP203B_UpgradeV36_AdmitsDeferredWithoutLosingRows reconstructs a genuine pre-203 (v36) database by replaying the live migrations slice and stopping before the migration under test:

for _, m := range migrations {
    if m.version >= latestMigration {   // <-- bound to a moving constant
        break // stop before the migration under test
    }
    ...
}

When the test was written, latestMigration == 37, so >= 37 correctly excluded v37 (the ticks table rebuild that adds 'deferred' to the status/outcome CHECK vocabularies).

Migration v38 arrived later and bumped the constant:

const latestMigration = 38

The loop condition silently changed meaning to >= 38. It now applied v37 as part of the "pre-migration" fixture, so the reconstructed database already had the new 'deferred' vocabulary. The test's own premise assert then failed:

schedgap203b_migration_test.go:140: premise: the pre-203 ticks.status CHECK
accepted 'deferred' — this test is not exercising the v37 rebuild

Nobody touched the test; the constant it was bound to moved. A fixture whose cutoff is derived from "latest" tracks scope every time the ladder grows.

Compounding defect (two bugs hiding each other)

A worker had locally "fixed" the symptom by: 1. inserting a redundant if m.version >= 38 { break } in the harness, and 2. placing the new version: 38 entry mid-slice, after v33, instead of at the end.

With that arrangement the loop stopped at v34, skipping v34–v37 entirely. v35 (ALTER TABLE ticks ADD COLUMN slot_wait_ms ...) never ran, so the harness died with a different error:

table ticks has no column named slot_wait_ms

The ordering bug and the scope bug masked each other: fixing one exposed the other. This is why the fix must restore strict version order with new entries appended last, and remove the ad-hoc redundant break.

Exact fix

1. Pin the fixture cutoff to the literal version under test

internal/database/schedgap203b_migration_test.go:

    for _, m := range migrations {
-       if m.version >= latestMigration {
-           break // stop before the migration under test
-       }
+       // Pin the cutoff to the LITERAL version under test (v37), never to
+       // latestMigration. A latest/max bound moves with every new migration:
+       // when v38 landed, this loop silently started applying v37, so the
+       // "pre-203" fixture already carried the 'deferred' vocabulary and the
+       // premise assert below failed. A frozen-history fixture must not
+       // derive its scope from a moving constant.
+       if m.version >= 37 {
+           break // stop before v37 — the migration under test
+       }

Do not add a second if m.version >= 38 { break } next to it — that only re-couples the fixture to the next version and hides ordering defects.

2. Keep migrations entries at the end of the slice in strict version order

internal/database/migrations.go — ensure v34, v35, v36, v37, v38 are contiguous and ascending, with v38 last:

    {
        version: 36,
        desc:    "cost_source backfill: stamp 'legacy' on rows that pre-date metering (SCHED-GAP-127)",
        stmt:    `UPDATE ticks SET cost_source = 'legacy' WHERE cost_source = '';`,
    },
    {
        version: 37,
        desc:    "deferred tick status (SCHED-GAP-203): ...",
        stmt:    `...ticks rebuild...`,
    },
+   {
+       version: 38,
+       desc:    "cooldown pin provenance (SCHED-GAP-219): ...",
+       stmt:    `ALTER TABLE projects ADD COLUMN cooldown_pin_s INTEGER; ...`,
+   },
 }

and bump the constant once, at the top:

-const latestMigration = 37
+const latestMigration = 38

The redundant mid-ladder if m.version >= 38 { break } is removed; latestMigration is used only by Migrate/version checks, never by the frozen-history fixture.

Prevention

Any test helper that reconstructs a frozen historical state by replaying a versioned ladder must:

  1. Pin its cutoff to the literal version it documents (>= 37), with a comment naming that version — never to latest/max/highest. A latest-bound cutoff silently re-scopes on every new migration.
  2. Assert its own premise before trusting the mutation under test. Here the test does exactly that: it asserts the pre-203 ticks.status CHECK rejects 'deferred' before migrating. That assert is what surfaced the scope creep and turned a silent false-pass into a loud failure. Keep it.
  3. Append new migrations at the end of the slice in strict version order. Never insert mid-ladder; the replay harness assumes monotonic order.

Verification

Verified against the real tree at af020bc (the commit that added v38 and carried the fixed test). To reproduce the failure, the test's loop was reverted to the latestMigration bound while latestMigration == 38:

$ go test ./internal/database/ \
    -run TestSCHEDGAP203B_UpgradeV36_AdmitsDeferredWithoutLosingRows -count=1
--- FAIL: TestSCHEDGAP203B_UpgradeV36_AdmitsDeferredWithoutLosingRows (0.03s)
    schedgap203b_migration_test.go:140: premise: the pre-203 ticks.status CHECK
    accepted 'deferred' — this test is not exercising the v37 rebuild
FAIL
FAIL    github.com/coding-hermes/scheduler/internal/database    0.029s

With the literal >= 37 cutoff restored:

$ go test ./internal/database/ -run TestSCHEDGAP203B -count=1
ok      github.com/coding-hermes/scheduler/internal/database    0.062s

$ go test ./internal/database/ -count=1
ok      github.com/coding-hermes/scheduler/internal/database    1.008s

The premise assert now passes because the fixture stops before v37; Migrate then applies v37 (and v38), MigrationVersion reports latestMigration, and the row/column preservation checks pass — proving the rebuild added the 'deferred' vocabulary without losing tick or tick_workers rows. Full internal/database package is green, matching the reported fix result.

Evidence & signatures

# Evidence
- Problem class: go-test-fixture-break-bound-tied-to-latest-migration-constant
- Model: openrouter/deepseek/deepseek-v4.1-flash
- Solved: 2026-09-23T00:34:46.638Z
- Verification: solution produced by pi in sandbox; see signatures.json
{"description": "SYMPTOM: a Go test fixture that reconstructs a historical database by replaying a migration ladder started failing its own premise assert ('the pre-203 ticks.status CHECK accepted deferred \u2014 this test is not exercising the v37 rebuild') after a NEW migration (v38) landed on main. The failure appeared without anyone touching the test. ROOT CAUSE: the fixture harness looped the migrations slice with the break condition 'if m.version >= latestMigration' where latestMigration is a const equal to the NEWEST migration. Written when v37 was newest, the break correctly excluded v37 (the table-rebuild migration under test); once v38 landed, latestMigration became 38 and the loop silently began INCLUDING v37, so the 'pre-migration' fixture was built with the new vocabulary already present. A fixture bound to a latest/max constant moves scope every time the file grows. Compounding factor: a worker had masked the breakage by inserting a redundant 'if m.version >= 38 { break }' and misplacing the v38 migration entry mid-slice (after v33), which made the harness skip v34-v37 entirely and fail with 'table ticks has no column named slot_wait_ms' \u2014 two defects hiding each other. FIX: (1) pin the fixture break to the LITERAL version of the migration under test: 'if m.version >= 37 { break }' with a comment explaining why a latest-constant bound is wrong there; (2) keep every migration entry at the END of the slice in strict version order. PREVENTION: any test helper that reconstructs a frozen historical state by replaying a versioned ladder must pin its cutoff to the literal version it documents, never to latest/max/highest constant, and must assert its own premise (here: the old DDL rejects the new CHECK vocabulary) before claiming the mutation under test changed the answer \u2014 the premise assert is what surfaced the scope creep. VERIFIED: go test ./internal/database/ green (7.6s) after the fix; full battery across scheduler/api/config/clock/database green; guard full-mode PASS at commit.", "environment": "Go 1.26, modernc.org/sqlite (pure Go), schedulerd migration harness, Linux", "language": "go", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "go-test-fixture-break-bound-tied-to-latest-migration-constant", "provider": "openrouter", "solved_at": "2026-09-23T00:34:46.638Z", "version": ""}
Generated from the verified corpus · MIT licensedBack to the catalog