package store_test import ( "context" "database/sql" "path/filepath" "testing" "time" _ "modernc.org/sqlite" "go.uber.org/zap" "iop/apps/node/internal/store" ) const oldRunsSchema = ` CREATE TABLE runs ( run_id TEXT PRIMARY KEY, adapter TEXT NOT NULL, model TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'running', created_at DATETIME NOT NULL, completed_at DATETIME, error TEXT ); ` func seedOldSchemaDB(t *testing.T) string { t.Helper() dir := t.TempDir() dsn := filepath.Join(dir, "old.db") db, err := sql.Open("sqlite", dsn) if err != nil { t.Fatalf("open: %v", err) } if _, err := db.Exec(oldRunsSchema); err != nil { t.Fatalf("seed schema: %v", err) } if err := db.Close(); err != nil { t.Fatalf("close: %v", err) } return dsn } func newTestStore(t *testing.T) *store.Store { t.Helper() s, err := store.New(":memory:", zap.NewNop()) if err != nil { t.Fatalf("store.New: %v", err) } t.Cleanup(func() { _ = s.Close() }) return s } func TestStore_RunSessionFields(t *testing.T) { s := newTestStore(t) ctx := context.Background() r := store.RunRecord{ RunID: "run-1", Adapter: "cli", Model: "codex", SessionID: "session-a", Background: true, Status: "running", CreatedAt: time.Now(), } if err := s.InsertRun(ctx, r); err != nil { t.Fatalf("InsertRun: %v", err) } got, err := s.GetRun(ctx, "run-1") if err != nil { t.Fatalf("GetRun: %v", err) } if got == nil { t.Fatal("expected run record") } if got.SessionID != "session-a" { t.Errorf("SessionID: got %q want %q", got.SessionID, "session-a") } if !got.Background { t.Error("Background: expected true") } if got.Status != "running" { t.Errorf("Status: got %q want %q", got.Status, "running") } if err := s.CompleteRun(ctx, "run-1", "cancelled", ""); err != nil { t.Fatalf("CompleteRun: %v", err) } got, err = s.GetRun(ctx, "run-1") if err != nil { t.Fatalf("GetRun after complete: %v", err) } if got.Status != "cancelled" { t.Errorf("Status after cancel: got %q want %q", got.Status, "cancelled") } } func TestStore_DefaultSessionID(t *testing.T) { s := newTestStore(t) ctx := context.Background() r := store.RunRecord{ RunID: "run-2", Adapter: "cli", Model: "codex", SessionID: "", // empty → should default to "default" Status: "running", CreatedAt: time.Now(), } if err := s.InsertRun(ctx, r); err != nil { t.Fatalf("InsertRun: %v", err) } got, err := s.GetRun(ctx, "run-2") if err != nil { t.Fatalf("GetRun: %v", err) } if got.SessionID != "default" { t.Errorf("SessionID: got %q want %q", got.SessionID, "default") } } func TestStore_MigratesExistingRunsTable(t *testing.T) { dsn := seedOldSchemaDB(t) s, err := store.New(dsn, zap.NewNop()) if err != nil { t.Fatalf("store.New: %v", err) } t.Cleanup(func() { _ = s.Close() }) ctx := context.Background() r := store.RunRecord{ RunID: "run-mig", Adapter: "cli", Model: "codex", SessionID: "session-a", Background: true, Status: "running", CreatedAt: time.Now(), } if err := s.InsertRun(ctx, r); err != nil { t.Fatalf("InsertRun: %v", err) } got, err := s.GetRun(ctx, "run-mig") if err != nil { t.Fatalf("GetRun: %v", err) } if got == nil { t.Fatal("expected run record") } if got.SessionID != "session-a" { t.Errorf("SessionID: got %q want %q", got.SessionID, "session-a") } if !got.Background { t.Error("Background: expected true") } } func TestStore_MigrationIsIdempotent(t *testing.T) { dsn := seedOldSchemaDB(t) s1, err := store.New(dsn, zap.NewNop()) if err != nil { t.Fatalf("store.New first: %v", err) } if err := s1.Close(); err != nil { t.Fatalf("close first: %v", err) } s2, err := store.New(dsn, zap.NewNop()) if err != nil { t.Fatalf("store.New second: %v", err) } t.Cleanup(func() { _ = s2.Close() }) }