From a9b2612b6082a751063714303026f8b602cc7d3b Mon Sep 17 00:00:00 2001 From: omartelo Date: Thu, 16 Jul 2026 13:56:03 -0300 Subject: [PATCH] test: cover the pre-migration store schema path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit open()'s ADD COLUMN migrations were only ever run against a current-schema database, where every ALTER is a duplicate-column no-op — the branch where a migration actually applies (err == nil) had no test. Seed a database with the pre-migration schema (sessions without kind/path/…, projects without position) and assert open() adds the missing columns and they round-trip through AddSession/LoadState. Mutation testing flagged the gap: internal/store efficacy goes 97.4% -> 100%, killing the surviving `err != nil` -> `err == nil` mutant at store.go:130. --- internal/store/store_test.go | 62 ++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/internal/store/store_test.go b/internal/store/store_test.go index 098d4f49b..5cb666076 100644 --- a/internal/store/store_test.go +++ b/internal/store/store_test.go @@ -1,6 +1,7 @@ package store import ( + "database/sql" "os" "path/filepath" "testing" @@ -521,3 +522,64 @@ func TestReopenProjectKeepsDraggedPosition(t *testing.T) { t.Errorf("project order = %v, want [p3 p1 p2]", got) } } + +// TestOpenMigratesPreMigrationDatabase proves open() applies the ADD COLUMN +// migrations to a database created before those columns existed — the path +// where a migration actually succeeds (err == nil). A current-schema database +// never reaches it: every ALTER there is a duplicate-column no-op, which is why +// only this test kills the mutant that flips migrate's `err != nil` to +// `err == nil` (that inversion dereferences a nil error here and panics). +func TestOpenMigratesPreMigrationDatabase(t *testing.T) { + path := filepath.Join(t.TempDir(), "old.db") + + // The schema as it stood before the migrations: sessions without + // kind/path/claude_session_id/label_auto/position, projects without + // position. + const oldSchema = ` +CREATE TABLE projects ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + path TEXT NOT NULL, + is_open INTEGER NOT NULL DEFAULT 1, + next_seq INTEGER NOT NULL DEFAULT 1, + active_session_id TEXT NOT NULL DEFAULT '' +); +CREATE TABLE sessions ( + id TEXT NOT NULL PRIMARY KEY, + project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE, + label TEXT NOT NULL +);` + + seed, err := sql.Open("sqlite", path) + if err != nil { + t.Fatalf("open seed db: %v", err) + } + if _, err := seed.Exec(oldSchema); err != nil { + t.Fatalf("seed old schema: %v", err) + } + _ = seed.Close() + + svc, err := open(path) + if err != nil { + t.Fatalf("open pre-migration db: %v", err) + } + t.Cleanup(func() { _ = svc.Close() }) + + // The columns the migrations added must now be usable end to end. + if err := svc.AddProject("p1", "alpha", "/tmp/alpha"); err != nil { + t.Fatalf("AddProject: %v", err) + } + if err := svc.AddSession("p1", "s1", "mellow-otter", "shell", "/data/wt", 2); err != nil { + t.Fatalf("AddSession: %v", err) + } + got, err := svc.LoadState() + if err != nil { + t.Fatalf("LoadState: %v", err) + } + if len(got) != 1 || len(got[0].Sessions) != 1 { + t.Fatalf("state after migration = %+v", got) + } + if s := got[0].Sessions[0]; s.Kind != "shell" || s.Path != "/data/wt" { + t.Errorf("migrated session = %+v, want kind=shell path=/data/wt", s) + } +}