package runtime import ( "database/sql" "os" "path/filepath" "testing" _ "modernc.org/sqlite" ) func TestStoreMissingSQLiteReturnsEmptySnapshot(t *testing.T) { snapshot, err := Store{CodexHome: t.TempDir()}.Snapshot() if err != nil { t.Fatalf("Snapshot returned error: %v", err) } if len(snapshot.Threads) != 0 || len(snapshot.SpawnEdges) != 0 || len(snapshot.Goals) != 0 { t.Fatalf("expected empty snapshot, got %#v", snapshot) } if snapshot.Source.Confidence != "low" || snapshot.Source.Kind != "sqlite_missing" { t.Fatalf("unexpected source evidence: %#v", snapshot.Source) } if source := snapshot.Sources["state"]; source.Kind != "sqlite_missing" || source.Confidence != "low" { t.Fatalf("unexpected state source evidence: %#v", source) } if source := snapshot.Sources["goals"]; source.Kind != "sqlite_missing" || source.Confidence != "low" { t.Fatalf("unexpected goals source evidence: %#v", source) } } func TestStoreReadsThreadsEdgesAndGoalsFromReadonlySQLite(t *testing.T) { root := t.TempDir() createRuntimeSQLite(t, root) snapshot, err := Store{CodexHome: root}.Snapshot() if err != nil { t.Fatalf("Snapshot returned error: %v", err) } if len(snapshot.Threads) != 2 { t.Fatalf("threads = %#v", snapshot.Threads) } if snapshot.Threads[0].ID != "thread-a" || snapshot.Threads[0].Role != "analyst" { t.Fatalf("unexpected first thread: %#v", snapshot.Threads[0]) } if len(snapshot.SpawnEdges) != 1 || snapshot.SpawnEdges[0].FromThreadID != "thread-a" || snapshot.SpawnEdges[0].ToThreadID != "thread-b" || snapshot.SpawnEdges[0].Reason != "handoff" { t.Fatalf("unexpected edges: %#v", snapshot.SpawnEdges) } if len(snapshot.Goals) != 1 || snapshot.Goals[0].ThreadID != "thread-b" || snapshot.Goals[0].Status != "in_progress" { t.Fatalf("unexpected goals: %#v", snapshot.Goals) } if snapshot.Source.Confidence != "high" || snapshot.Source.Kind != "sqlite_readonly" { t.Fatalf("unexpected source evidence: %#v", snapshot.Source) } if source := snapshot.Sources["state"]; source.Kind != "sqlite_readonly" || source.Confidence != "high" { t.Fatalf("unexpected state source evidence: %#v", source) } if source := snapshot.Sources["goals"]; source.Kind != "sqlite_readonly" || source.Confidence != "high" { t.Fatalf("unexpected goals source evidence: %#v", source) } } func TestStoreMarksStateMissingWhenOnlyGoalsSQLiteExists(t *testing.T) { root := t.TempDir() createRuntimeSQLite(t, root) if err := os.Remove(filepath.Join(root, "state_5.sqlite")); err != nil { t.Fatal(err) } snapshot, err := Store{CodexHome: root}.Snapshot() if err != nil { t.Fatalf("Snapshot returned error: %v", err) } if len(snapshot.Threads) != 0 || len(snapshot.SpawnEdges) != 0 { t.Fatalf("expected no state-backed data, got threads=%#v edges=%#v", snapshot.Threads, snapshot.SpawnEdges) } if len(snapshot.Goals) != 1 { t.Fatalf("goals = %#v, want one goal from goals DB", snapshot.Goals) } if source := snapshot.Sources["state"]; source.Kind != "sqlite_missing" || source.Confidence != "low" { t.Fatalf("unexpected state source evidence: %#v", source) } if source := snapshot.Sources["goals"]; source.Kind != "sqlite_readonly" || source.Confidence != "high" { t.Fatalf("unexpected goals source evidence: %#v", source) } if snapshot.Source.Confidence != "partial" || snapshot.Source.Kind != "sqlite_partial" { t.Fatalf("unexpected aggregate source evidence: %#v", snapshot.Source) } } func TestStoreMarksGoalsMissingWhenOnlyStateSQLiteExists(t *testing.T) { root := t.TempDir() createRuntimeSQLite(t, root) if err := os.Remove(filepath.Join(root, "goals_1.sqlite")); err != nil { t.Fatal(err) } snapshot, err := Store{CodexHome: root}.Snapshot() if err != nil { t.Fatalf("Snapshot returned error: %v", err) } if len(snapshot.Threads) != 2 || len(snapshot.SpawnEdges) != 1 { t.Fatalf("expected state-backed data, got threads=%#v edges=%#v", snapshot.Threads, snapshot.SpawnEdges) } if len(snapshot.Goals) != 0 { t.Fatalf("goals = %#v, want no goals", snapshot.Goals) } if source := snapshot.Sources["state"]; source.Kind != "sqlite_readonly" || source.Confidence != "high" { t.Fatalf("unexpected state source evidence: %#v", source) } if source := snapshot.Sources["goals"]; source.Kind != "sqlite_missing" || source.Confidence != "low" { t.Fatalf("unexpected goals source evidence: %#v", source) } if snapshot.Source.Confidence != "partial" || snapshot.Source.Kind != "sqlite_partial" { t.Fatalf("unexpected aggregate source evidence: %#v", snapshot.Source) } } func createRuntimeSQLite(t *testing.T, root string) { t.Helper() statePath := filepath.Join(root, "state_5.sqlite") goalsPath := filepath.Join(root, "goals_1.sqlite") stateDB, err := sql.Open("sqlite", statePath) if err != nil { t.Fatal(err) } defer stateDB.Close() execSQL(t, stateDB, `CREATE TABLE threads ( id TEXT PRIMARY KEY, role TEXT, status TEXT, created_at TEXT, updated_at TEXT )`) execSQL(t, stateDB, `CREATE TABLE thread_spawn_edges ( from_thread_id TEXT, to_thread_id TEXT, reason TEXT, created_at TEXT )`) execSQL(t, stateDB, `INSERT INTO threads (id, role, status, created_at, updated_at) VALUES ('thread-a', 'analyst', 'done', '2026-05-25T01:00:00Z', '2026-05-25T01:05:00Z'), ('thread-b', 'operator', 'running', '2026-05-25T01:06:00Z', '2026-05-25T01:07:00Z')`) execSQL(t, stateDB, `INSERT INTO thread_spawn_edges (from_thread_id, to_thread_id, reason, created_at) VALUES ('thread-a', 'thread-b', 'handoff', '2026-05-25T01:06:00Z')`) goalsDB, err := sql.Open("sqlite", goalsPath) if err != nil { t.Fatal(err) } defer goalsDB.Close() execSQL(t, goalsDB, `CREATE TABLE thread_goals ( thread_id TEXT, goal TEXT, status TEXT, updated_at TEXT )`) execSQL(t, goalsDB, `INSERT INTO thread_goals (thread_id, goal, status, updated_at) VALUES ('thread-b', 'ship phase 3', 'in_progress', '2026-05-25T01:08:00Z')`) } func execSQL(t *testing.T, db *sql.DB, query string) { t.Helper() if _, err := db.Exec(query); err != nil { t.Fatalf("exec %q: %v", query, err) } }