package storesqlite import ( "context" "database/sql" "errors" "fmt" "path/filepath" "strings" "testing" _ "modernc.org/sqlite" ) var errTestWorkspaceNotFound = errors.New("workspace not found") const ( testTargetIDCodex = "local:codex" testTargetIDClaude = "local:claude-code" ) type staticProjectPaths struct { paths []string } func (s *staticProjectPaths) ProjectPaths(context.Context, Querier) ([]string, error) { return append([]string(nil), s.paths...), nil } func testSeedTargets(now int64) []Target { return []Target{ { ID: testTargetIDCodex, Provider: "codex", LaunchRefJSON: `{"type":"local_cli","provider":"codex"}`, Name: "Codex", IconKey: "codex", IconURL: "tutti-asset://agent/codex.png", MaskIconURL: "tutti-asset://agent/codex-mask.svg", Enabled: true, Source: "system", SortOrder: 10, CreatedAtUnixMS: now, UpdatedAtUnixMS: now, }, { ID: testTargetIDClaude, Provider: "claude-code", LaunchRefJSON: `{"type":"local_cli","provider":"claude-code"}`, Name: "Claude Code", IconKey: "claude-code", IconURL: "tutti-asset://agent/claudecode.png", MaskIconURL: "tutti-asset://agent/claudecode-mask.svg", Enabled: true, Source: "system", SortOrder: 20, CreatedAtUnixMS: now, UpdatedAtUnixMS: now, }, } } func testOptions(projects *staticProjectPaths) Options { return Options{ ProjectPaths: projects, SeedSystemTargets: testSeedTargets, LegacySystemTargetIDRenames: map[string]string{ "local-codex": testTargetIDCodex, "local-claude-code": testTargetIDClaude, }, TargetIDBackfillByProvider: map[string]string{ "codex": testTargetIDCodex, "claude-code": testTargetIDClaude, }, } } func openTestDB(t testing.TB) *sql.DB { t.Helper() dbPath := filepath.Join(t.TempDir(), "agent-store.db") db, err := sql.Open("sqlite", dbPath) if err != nil { t.Fatalf("open sqlite database: %v", err) } t.Cleanup(func() { _ = db.Close() }) db.SetMaxOpenConns(1) for _, pragma := range []string{ "PRAGMA busy_timeout = 5000", "PRAGMA foreign_keys = ON", "PRAGMA journal_mode = WAL", } { if _, err := db.Exec(pragma); err != nil { t.Fatalf("%s: %v", pragma, err) } } return db } func openTestStore(t testing.TB, opts Options) *Store { t.Helper() store := New(openTestDB(t), opts) if err := store.Migrate(context.Background()); err != nil { t.Fatalf("Migrate() error = %v", err) } return store } func TestStoreFreshMigrateCreatesSessionTurnReferenceWithoutHostForeignKey(t *testing.T) { t.Parallel() store := openTestStore(t, testOptions(&staticProjectPaths{})) ctx := context.Background() rows, err := store.db.QueryContext(ctx, `PRAGMA foreign_key_list(workspace_agent_sessions)`) if err != nil { t.Fatalf("foreign_key_list error = %v", err) } defer rows.Close() hasTurnsFK := false hasHostFK := false for rows.Next() { var id, seq int var table, from, to, onUpdate, onDelete, match string if err := rows.Scan(&id, &seq, &table, &from, &to, &onUpdate, &onDelete, &match); err != nil { t.Fatalf("scan foreign_key_list: %v", err) } hasTurnsFK = hasTurnsFK || table == "workspace_agent_turns" hasHostFK = hasHostFK || table == "workspaces" } if err := rows.Err(); err != nil { t.Fatalf("iterate foreign_key_list: %v", err) } if !hasTurnsFK || hasHostFK { t.Fatalf("session foreign keys turns=%v host=%v, want exact turn reference without host coupling", hasTurnsFK, hasHostFK) } targets, err := store.ListAgentTargets(ctx) if err != nil { t.Fatalf("ListAgentTargets() error = %v", err) } if len(targets) != 2 || targets[0].ID != testTargetIDCodex || targets[1].ID != testTargetIDClaude { t.Fatalf("seeded targets = %#v, want codex and claude-code system targets", targets) } } func TestStoreMigrateRefreshesOnlySystemSeedTargets(t *testing.T) { t.Parallel() store := openTestStore(t, testOptions(&staticProjectPaths{})) ctx := context.Background() if _, err := store.db.ExecContext(ctx, ` UPDATE agent_targets SET provider = 'stale-provider', launch_ref_json = '{}', name = 'Stale Codex', icon_key = 'stale', icon_url = 'stale.png', mask_icon_url = 'stale-mask.svg', hero_image_url = 'stale-hero.jpg', enabled = 0, sort_order = 999, created_at_ms = 7, updated_at_ms = 8 WHERE id = ?; `, testTargetIDCodex); err != nil { t.Fatalf("seed stale system target: %v", err) } if _, err := store.db.ExecContext(ctx, ` UPDATE agent_targets SET provider = 'custom-provider', launch_ref_json = '{"custom":true}', name = 'Custom', icon_key = 'custom', enabled = 0, source = 'user', sort_order = 777, created_at_ms = 9, updated_at_ms = 10 WHERE id = ?; `, testTargetIDClaude); err != nil { t.Fatalf("seed custom target: %v", err) } if err := store.Migrate(ctx); err != nil { t.Fatalf("Migrate() error = %v", err) } var codex Target if err := store.db.QueryRowContext(ctx, ` SELECT id, provider, launch_ref_json, name, icon_key, icon_url, mask_icon_url, hero_image_url, enabled, source, sort_order, created_at_ms, updated_at_ms FROM agent_targets WHERE id = ? `, testTargetIDCodex).Scan(&codex.ID, &codex.Provider, &codex.LaunchRefJSON, &codex.Name, &codex.IconKey, &codex.IconURL, &codex.MaskIconURL, &codex.HeroImageURL, &codex.Enabled, &codex.Source, &codex.SortOrder, &codex.CreatedAtUnixMS, &codex.UpdatedAtUnixMS); err != nil { t.Fatalf("query refreshed codex target: %v", err) } if codex.Provider != "codex" || codex.LaunchRefJSON != `{"type":"local_cli","provider":"codex"}` || codex.Name != "Codex" || codex.IconKey != "codex" || codex.IconURL != "tutti-asset://agent/codex.png" || codex.MaskIconURL != "tutti-asset://agent/codex-mask.svg" || codex.HeroImageURL != "" || codex.Enabled || codex.SortOrder != 10 { t.Fatalf("refreshed system target = %#v", codex) } if codex.CreatedAtUnixMS != 7 || codex.Source != systemTargetSource || codex.UpdatedAtUnixMS == 8 { t.Fatalf("system target preserved fields/timestamp = %#v", codex) } refreshedAt := codex.UpdatedAtUnixMS if err := store.seedSystemAgentTargets(ctx, refreshedAt+1000); err != nil { t.Fatalf("seed unchanged system targets: %v", err) } if err := store.db.QueryRowContext(ctx, `SELECT updated_at_ms FROM agent_targets WHERE id = ?`, testTargetIDCodex).Scan(&codex.UpdatedAtUnixMS); err != nil { t.Fatalf("query unchanged codex target: %v", err) } if codex.UpdatedAtUnixMS != refreshedAt { t.Fatalf("unchanged system target updated_at_ms = %d, want %d", codex.UpdatedAtUnixMS, refreshedAt) } var custom Target if err := store.db.QueryRowContext(ctx, ` SELECT id, provider, launch_ref_json, name, icon_key, enabled, source, sort_order, created_at_ms, updated_at_ms FROM agent_targets WHERE id = ? `, testTargetIDClaude).Scan(&custom.ID, &custom.Provider, &custom.LaunchRefJSON, &custom.Name, &custom.IconKey, &custom.Enabled, &custom.Source, &custom.SortOrder, &custom.CreatedAtUnixMS, &custom.UpdatedAtUnixMS); err != nil { t.Fatalf("query custom target: %v", err) } if custom.Provider != "custom-provider" || custom.LaunchRefJSON != `{"custom":true}` || custom.Name != "Custom" || custom.IconKey != "custom" || custom.Enabled || custom.Source != "user" || custom.SortOrder != 777 || custom.CreatedAtUnixMS != 9 || custom.UpdatedAtUnixMS != 10 { t.Fatalf("custom target was overwritten = %#v", custom) } } func TestStoreMigratePreservesEnabledSystemTargetWithDisabledDefault(t *testing.T) { t.Parallel() opts := testOptions(&staticProjectPaths{}) opts.SeedSystemTargets = func(now int64) []Target { targets := testSeedTargets(now) targets[0].Enabled = false return targets } store := openTestStore(t, opts) ctx := context.Background() target, err := store.GetAgentTarget(ctx, testTargetIDCodex) if err != nil { t.Fatalf("GetAgentTarget() error = %v", err) } if target.Enabled { t.Fatalf("fresh system target enabled = true, want descriptor default false") } target.Enabled = true target, err = store.PutAgentTarget(ctx, target) if err != nil { t.Fatalf("PutAgentTarget() error = %v", err) } enabledAt := target.UpdatedAtUnixMS if err := store.Migrate(ctx); err != nil { t.Fatalf("Migrate() error = %v", err) } target, err = store.GetAgentTarget(ctx, testTargetIDCodex) if err != nil { t.Fatalf("GetAgentTarget() after Migrate error = %v", err) } if !target.Enabled { t.Fatalf("system target enabled = false after Migrate, want persisted user value true") } if target.UpdatedAtUnixMS != enabledAt { t.Fatalf("system target updated_at_ms = %d after enabled-only descriptor mismatch, want %d", target.UpdatedAtUnixMS, enabledAt) } } func TestStoreReportAndListSessionLifecycle(t *testing.T) { t.Parallel() store := openTestStore(t, testOptions(&staticProjectPaths{})) ctx := context.Background() state, err := store.ReportSessionState(ctx, SessionStateReport{ WorkspaceID: "ws-1", AgentSessionID: "session-1", Origin: "runtime", UserID: "user-1", Provider: "codex", ProviderSessionID: "provider-1", Cwd: "/workspace", Title: "@renderer.js", Status: "running", OccurredAtUnixMS: 100, }) if err != nil { t.Fatalf("ReportSessionState() error = %v", err) } if !state.Accepted || state.LastEventUnixMS != 100 { t.Fatalf("state result = %#v", state) } if state.Session.UserID != "user-1" { t.Fatalf("state session user id = %q", state.Session.UserID) } if state.Session.Title != "@renderer.js" { t.Fatalf("state session title = %q, want canonical title", state.Session.Title) } if _, accepted, err := store.RecordTurnTransition(ctx, TurnTransition{ WorkspaceID: "ws-1", AgentSessionID: "session-1", TurnID: "turn-1", Phase: TurnPhaseRunning, OccurredAtUnixMS: 105, }); err != nil || !accepted { t.Fatalf("RecordTurnTransition accepted=%v error=%v", accepted, err) } first, err := store.ReportSessionMessages(ctx, SessionMessageReport{ WorkspaceID: "ws-1", AgentSessionID: "session-1", Origin: "runtime", Messages: []MessageUpdate{{ MessageID: "message-1", TurnID: "turn-1", Role: "assistant", Kind: "text", Status: "running", Payload: map[string]any{"text": "hel"}, OccurredAtUnixMS: 110, }}, }) if err != nil { t.Fatalf("ReportSessionMessages(first) error = %v", err) } if first.AcceptedCount != 1 || first.LatestVersion != 1 { t.Fatalf("first result = %#v", first) } second, err := store.ReportSessionMessages(ctx, SessionMessageReport{ WorkspaceID: "ws-1", AgentSessionID: "session-1", Origin: "runtime", Messages: []MessageUpdate{{ MessageID: "message-1", Status: "completed", ContentDelta: "lo", CompletedAtUnixMS: 120, }}, }) if err != nil { t.Fatalf("ReportSessionMessages(second) error = %v", err) } if second.AcceptedCount != 1 || second.LatestVersion != 2 { t.Fatalf("second result = %#v", second) } page, ok, err := store.ListSessionMessages(ctx, ListSessionMessagesInput{ WorkspaceID: "ws-1", AgentSessionID: "session-1", Limit: 10, }) if err != nil || !ok { t.Fatalf("ListSessionMessages() ok=%v error=%v", ok, err) } if len(page.Messages) != 1 || page.LatestVersion != 2 || page.Messages[0].Payload["text"] != "hello" { t.Fatalf("page = %#v, want merged message payload", page) } session, ok, err := store.GetSession(ctx, "ws-1", "session-1") if err != nil || !ok || session.UserID != "user-1" { t.Fatalf("GetSession() = %#v ok=%v error=%v, want user id", session, ok, err) } pinned, ok, err := store.UpdateSessionPinned(ctx, "ws-1", "session-1", true) if err != nil || !ok || pinned.PinnedAtUnixMS <= 0 { t.Fatalf("UpdateSessionPinned() = %#v ok=%v error=%v", pinned, ok, err) } renamed, ok, err := store.UpdateSessionTitle(ctx, "ws-1", "session-1", " final ") if err != nil || !ok || renamed.Title != "final" || renamed.UpdatedAtUnixMS < pinned.UpdatedAtUnixMS { t.Fatalf("UpdateSessionTitle() = %#v ok=%v error=%v", renamed, ok, err) } if _, err := store.db.ExecContext(ctx, ` UPDATE workspace_agent_sessions SET updated_at_unix_ms = 9999999999999 WHERE workspace_id = 'ws-1' AND agent_session_id = 'session-1' `); err != nil { t.Fatalf("force session updated_at_unix_ms error = %v", err) } monotonicRenamed, ok, err := store.UpdateSessionTitle(ctx, "ws-1", "session-1", " monotonic ") if err != nil || !ok || monotonicRenamed.Title != "monotonic" || monotonicRenamed.UpdatedAtUnixMS != 10000000000000 { t.Fatalf("UpdateSessionTitle(monotonic) = %#v ok=%v error=%v", monotonicRenamed, ok, err) } if _, err := store.ReportSessionState(ctx, SessionStateReport{ WorkspaceID: "ws-1", AgentSessionID: "session-1", Provider: "codex", Title: "runtime title", OccurredAtUnixMS: monotonicRenamed.UpdatedAtUnixMS + 1, }); err != nil { t.Fatalf("ReportSessionState(runtime title) error = %v", err) } sessionAfterRuntimeTitle, ok, err := store.GetSession(ctx, "ws-1", "session-1") if err != nil || !ok || sessionAfterRuntimeTitle.Title != "monotonic" { t.Fatalf("GetSession() after runtime title = %#v ok=%v error=%v, want explicit title", sessionAfterRuntimeTitle, ok, err) } updatedSettings, ok, err := store.UpdateSessionSettings(ctx, "ws-1", "session-1", "gpt-5.4", map[string]any{ "model": "gpt-5.4", "permissionModeId": "full-access", }) if err != nil || !ok || updatedSettings.Model != "gpt-5.4" || updatedSettings.Settings["permissionModeId"] != "full-access" || updatedSettings.UpdatedAtUnixMS < monotonicRenamed.UpdatedAtUnixMS { t.Fatalf("UpdateSessionSettings() = %#v ok=%v error=%v", updatedSettings, ok, err) } blankRenamed, ok, err := store.UpdateSessionTitle(ctx, "ws-1", "session-1", " ") if err != nil || !ok || blankRenamed.Title != "" || blankRenamed.InternalRuntimeContext["tuttiInitialTitleEstablished"] != true { t.Fatalf("UpdateSessionTitle(blank) = %#v ok=%v error=%v, want cleared canonical title", blankRenamed, ok, err) } sessionAfterBlankTitle, ok, err := store.GetSession(ctx, "ws-1", "session-1") if err != nil || !ok || sessionAfterBlankTitle.Title != "" { t.Fatalf("GetSession() after blank title = %#v ok=%v error=%v", sessionAfterBlankTitle, ok, err) } expectedRuntimeContext := cloneJSONMap(sessionAfterBlankTitle.InternalRuntimeContext) replacementRuntimeContext := cloneJSONMap(expectedRuntimeContext) replacementRuntimeContext["sessionRuntimeSnapshot"] = map[string]any{"revision": float64(2)} rebound, updated, err := store.CompareAndSwapSessionRuntimeContext( ctx, "ws-1", "session-1", expectedRuntimeContext, replacementRuntimeContext, ) if err != nil || !updated || rebound.InternalRuntimeContext["sessionRuntimeSnapshot"] == nil { t.Fatalf("CompareAndSwapSessionRuntimeContext() = %#v updated=%v error=%v", rebound, updated, err) } if _, updated, err := store.CompareAndSwapSessionRuntimeContext( ctx, "ws-1", "session-1", expectedRuntimeContext, map[string]any{"stale": true}, ); err != nil || updated { t.Fatalf("stale CompareAndSwapSessionRuntimeContext() updated=%v error=%v", updated, err) } removed, err := store.DeleteSession(ctx, "ws-1", "session-1") if err != nil || !removed { t.Fatalf("DeleteSession() removed=%v error=%v", removed, err) } if _, ok, err := store.GetSession(ctx, "ws-1", "session-1"); err != nil || ok { t.Fatalf("GetSession() after delete ok=%v error=%v", ok, err) } result, err := store.ClearSessions(ctx, "ws-1") if err != nil { t.Fatalf("ClearSessions() error = %v", err) } if result.RemovedSessions != 1 || result.RemovedMessages != 1 { t.Fatalf("ClearSessions() = %#v, want tombstoned rows hard-deleted", result) } } func TestStoreMessageVersionsAreSnapshotCursorsAndMayHaveGaps(t *testing.T) { t.Parallel() store := openTestStore(t, testOptions(&staticProjectPaths{})) ctx := context.Background() if _, err := store.ReportSessionState(ctx, SessionStateReport{ WorkspaceID: "ws-1", AgentSessionID: "session-1", Origin: "runtime", Provider: "codex", ProviderSessionID: "provider-1", Status: "running", OccurredAtUnixMS: 100, }); err != nil { t.Fatal(err) } if _, accepted, err := store.RecordTurnTransition(ctx, TurnTransition{ WorkspaceID: "ws-1", AgentSessionID: "session-1", TurnID: "turn-1", Phase: TurnPhaseSubmitted, OccurredAtUnixMS: 101, }); err != nil || !accepted { t.Fatalf("RecordTurnTransition() accepted=%v error=%v", accepted, err) } for _, report := range []SessionMessageReport{ {WorkspaceID: "ws-1", AgentSessionID: "session-1", Messages: []MessageUpdate{{ MessageID: "message-a", TurnID: "turn-1", Role: "user", Kind: "text", Status: "completed", Payload: map[string]any{"text": "a"}, }}}, {WorkspaceID: "ws-1", AgentSessionID: "session-1", Messages: []MessageUpdate{{ MessageID: "message-b", TurnID: "turn-1", Role: "assistant", Kind: "text", Status: "running", Payload: map[string]any{"text": "b"}, }}}, {WorkspaceID: "ws-1", AgentSessionID: "session-1", Messages: []MessageUpdate{{ MessageID: "message-b", Status: "completed", ContentDelta: " done", }}}, } { if result, err := store.ReportSessionMessages(ctx, report); err != nil || result.AcceptedCount != 1 { t.Fatalf("ReportSessionMessages() result=%#v error=%v", result, err) } } page, ok, err := store.ListSessionMessages(ctx, ListSessionMessagesInput{WorkspaceID: "ws-1", AgentSessionID: "session-1", Limit: 10}) if err != nil || !ok || page.LatestVersion != 3 || len(page.Messages) != 2 { t.Fatalf("ListSessionMessages() page=%#v ok=%v error=%v", page, ok, err) } if page.Messages[0].MessageID != "message-a" || page.Messages[0].Version != 1 || page.Messages[1].MessageID != "message-b" || page.Messages[1].Version != 3 { t.Fatalf("current message snapshot versions=%#v, want message-a@1 and message-b@3", page.Messages) } incremental, ok, err := store.ListSessionMessages(ctx, ListSessionMessagesInput{WorkspaceID: "ws-1", AgentSessionID: "session-1", AfterVersion: 1, Limit: 10}) if err != nil || !ok || len(incremental.Messages) != 1 || incremental.Messages[0].MessageID != "message-b" || incremental.LatestVersion != 3 { t.Fatalf("incremental page=%#v ok=%v error=%v", incremental, ok, err) } rejected, err := store.ReportSessionMessages(ctx, SessionMessageReport{WorkspaceID: "ws-1", AgentSessionID: "session-1", Messages: []MessageUpdate{{ MessageID: "message-a", TurnID: "turn-2", Status: "completed", }}}) if err != nil || rejected.AcceptedCount != 0 { t.Fatalf("rejected update result=%#v error=%v", rejected, err) } session, ok, err := store.GetSession(ctx, "ws-1", "session-1") if err != nil || !ok || session.MessageVersion != 3 { t.Fatalf("session after rejected update=%#v ok=%v error=%v", session, ok, err) } } func TestSessionAuditIsTurnlessAndDoesNotChangeActiveTurn(t *testing.T) { t.Parallel() store := openTestStore(t, testOptions(&staticProjectPaths{})) ctx := context.Background() if _, err := store.ReportSessionState(ctx, SessionStateReport{ WorkspaceID: "ws-audit", AgentSessionID: "session-audit", Origin: "runtime", Provider: "codex", Status: "running", CurrentPhase: "working", OccurredAtUnixMS: 100, }); err != nil { t.Fatal(err) } if _, accepted, err := store.RecordTurnTransition(ctx, TurnTransition{ WorkspaceID: "ws-audit", AgentSessionID: "session-audit", TurnID: "turn-active", Phase: TurnPhaseRunning, Origin: TurnOriginUserPrompt, OccurredAtUnixMS: 101, }); err != nil || !accepted { t.Fatalf("RecordTurnTransition() accepted=%v error=%v", accepted, err) } result, err := store.ReportSessionMessages(ctx, SessionMessageReport{ WorkspaceID: "ws-audit", AgentSessionID: "session-audit", Origin: "runtime", Provider: "codex", Messages: []MessageUpdate{{MessageID: "goal-control:op-1", Role: "user", Kind: "session_audit", Status: "completed", Payload: map[string]any{"text": "/goal clear"}, OccurredAtUnixMS: 102}}, }) if err != nil || result.AcceptedCount != 1 || result.Messages[0].TurnID != "" { t.Fatalf("audit result=%#v error=%v", result, err) } turn, ok, err := store.GetTurn(ctx, "ws-audit", "session-audit", "turn-active") if err != nil || !ok || turn.Phase != TurnPhaseRunning { t.Fatalf("active turn=%#v ok=%v error=%v", turn, ok, err) } for _, invalid := range []MessageUpdate{ {MessageID: "ordinary-empty", Role: "assistant", Kind: "text", OccurredAtUnixMS: 103}, {MessageID: "audit-with-turn", TurnID: "turn-active", Role: "user", Kind: "session_audit", OccurredAtUnixMS: 104}, } { if _, err := store.ReportSessionMessages(ctx, SessionMessageReport{WorkspaceID: "ws-audit", AgentSessionID: "session-audit", Origin: "runtime", Messages: []MessageUpdate{invalid}}); err == nil { t.Fatalf("ReportSessionMessages(%s) error=nil", invalid.MessageID) } } } func TestCollaborationMessageIsTurnlessAndDoesNotChangeActiveTurn(t *testing.T) { t.Parallel() store := openTestStore(t, testOptions(&staticProjectPaths{})) ctx := context.Background() if _, err := store.ReportSessionState(ctx, SessionStateReport{ WorkspaceID: "ws-collaboration", AgentSessionID: "session-collaboration", Origin: "runtime", Provider: "codex", Status: "running", CurrentPhase: "working", OccurredAtUnixMS: 100, }); err != nil { t.Fatal(err) } if _, accepted, err := store.RecordTurnTransition(ctx, TurnTransition{ WorkspaceID: "ws-collaboration", AgentSessionID: "session-collaboration", TurnID: "turn-active", Phase: TurnPhaseRunning, Origin: TurnOriginUserPrompt, OccurredAtUnixMS: 101, }); err != nil || !accepted { t.Fatalf("RecordTurnTransition() accepted=%v error=%v", accepted, err) } result, err := store.ReportSessionMessages(ctx, SessionMessageReport{ WorkspaceID: "ws-collaboration", AgentSessionID: "session-collaboration", Origin: "runtime", Provider: "codex", Messages: []MessageUpdate{{ MessageID: "collab:run-1", Role: "assistant", Kind: "collaboration", Status: "completed", Payload: map[string]any{"runId": "run-1", "adoption": "pending"}, OccurredAtUnixMS: 102, }}, }) if err != nil || result.AcceptedCount != 1 || result.Messages[0].TurnID != "" { t.Fatalf("collaboration result=%#v error=%v", result, err) } turn, ok, err := store.GetTurn(ctx, "ws-collaboration", "session-collaboration", "turn-active") if err != nil || !ok || turn.Phase != TurnPhaseRunning { t.Fatalf("active turn=%#v ok=%v error=%v", turn, ok, err) } if _, err := store.ReportSessionMessages(ctx, SessionMessageReport{ WorkspaceID: "ws-collaboration", AgentSessionID: "session-collaboration", Origin: "runtime", Messages: []MessageUpdate{{ MessageID: "collab:run-with-turn", TurnID: "turn-active", Role: "assistant", Kind: "collaboration", OccurredAtUnixMS: 103, }}, }); err == nil { t.Fatal("turn-scoped collaboration message was accepted") } } func TestHistoricalImportCompatibilityCannotBeForgedByOrigin(t *testing.T) { t.Parallel() store := openTestStore(t, testOptions(&staticProjectPaths{})) ctx := context.Background() input := SessionMessageReport{ WorkspaceID: "ws-import-boundary", AgentSessionID: "session-import", Origin: "WORKSPACE_AGENT_SESSION_ORIGIN_IMPORTED", Messages: []MessageUpdate{{MessageID: "legacy", Role: "assistant", Kind: "text", OccurredAtUnixMS: 10}}, } if _, err := store.ReportSessionMessages(ctx, input); err == nil { t.Fatal("import origin alone bypassed Turn invariant") } input.HistoricalImport = true result, err := store.ReportSessionMessages(ctx, input) if err != nil || result.AcceptedCount != 1 || result.Messages[0].TurnID != "" { t.Fatalf("historical import result=%#v error=%v", result, err) } } func TestHistoricalImportPersistsTrustworthyTurnBoundaries(t *testing.T) { t.Parallel() store := openTestStore(t, testOptions(&staticProjectPaths{})) ctx := context.Background() result, err := store.ReportSessionMessages(ctx, SessionMessageReport{ WorkspaceID: "ws-import-turns", AgentSessionID: "session-import", Origin: "WORKSPACE_AGENT_SESSION_ORIGIN_IMPORTED", HistoricalImport: true, Messages: []MessageUpdate{ {MessageID: "user-1", TurnID: "imported-turn-1", Role: "user", Kind: "text", Status: "completed", OccurredAtUnixMS: 10, StartedAtUnixMS: 10, CompletedAtUnixMS: 10}, {MessageID: "tool-1", TurnID: "imported-turn-1", Role: "assistant", Kind: "tool_call", Status: "completed", OccurredAtUnixMS: 20, StartedAtUnixMS: 20, CompletedAtUnixMS: 30}, {MessageID: "assistant-1", TurnID: "imported-turn-1", Role: "assistant", Kind: "text", Status: "completed", OccurredAtUnixMS: 40, StartedAtUnixMS: 40, CompletedAtUnixMS: 40}, {MessageID: "user-2", TurnID: "imported-turn-2", Role: "user", Kind: "text", Status: "completed", OccurredAtUnixMS: 50, StartedAtUnixMS: 50, CompletedAtUnixMS: 50}, {MessageID: "assistant-2", TurnID: "imported-turn-2", Role: "assistant", Kind: "text", Status: "completed", OccurredAtUnixMS: 60, StartedAtUnixMS: 60, CompletedAtUnixMS: 60}, }, }) if err != nil || result.AcceptedCount != 5 { t.Fatalf("historical import result=%#v error=%v", result, err) } for _, expected := range []struct { turnID string startedAtUnixMS int64 settledAtUnixMS int64 finalAssistantMessage string }{ {turnID: "imported-turn-1", startedAtUnixMS: 10, settledAtUnixMS: 40, finalAssistantMessage: "assistant-1"}, {turnID: "imported-turn-2", startedAtUnixMS: 50, settledAtUnixMS: 60, finalAssistantMessage: "assistant-2"}, } { turn, ok, err := store.GetTurn(ctx, "ws-import-turns", "session-import", expected.turnID) if err != nil || !ok { t.Fatalf("GetTurn(%s) ok=%v error=%v", expected.turnID, ok, err) } if turn.Phase != TurnPhaseSettled || turn.Outcome != TurnOutcomeCompleted || !turn.Backfilled || turn.Origin != TurnOriginUserPrompt { t.Fatalf("turn %s = %#v, want settled completed user-prompt backfill", expected.turnID, turn) } if turn.StartedAtUnixMS != expected.startedAtUnixMS || turn.SettledAtUnixMS != expected.settledAtUnixMS { t.Fatalf("turn %s timestamps = %d..%d, want %d..%d", expected.turnID, turn.StartedAtUnixMS, turn.SettledAtUnixMS, expected.startedAtUnixMS, expected.settledAtUnixMS) } if !turn.FinalAssistantMessageResolved || turn.FinalAssistantMessageID != expected.finalAssistantMessage { t.Fatalf("turn %s final assistant = resolved:%v id:%q, want %q", expected.turnID, turn.FinalAssistantMessageResolved, turn.FinalAssistantMessageID, expected.finalAssistantMessage) } } } func TestStoreMessageSemanticsRoundTrip(t *testing.T) { t.Parallel() store := openTestStore(t, testOptions(&staticProjectPaths{})) ctx := context.Background() if _, err := store.ReportSessionState(ctx, SessionStateReport{WorkspaceID: "ws-semantics", AgentSessionID: "session-semantics", Provider: "codex", OccurredAtUnixMS: 1}); err != nil { t.Fatal(err) } if _, accepted, err := store.RecordTurnTransition(ctx, TurnTransition{WorkspaceID: "ws-semantics", AgentSessionID: "session-semantics", TurnID: "turn-1", Phase: TurnPhaseRunning, OccurredAtUnixMS: 2}); err != nil || !accepted { t.Fatalf("turn accepted=%v err=%v", accepted, err) } semantics := &MessageSemantics{UserVisibleAssistantResponse: true, TurnSettling: true, NoticeCommand: "compact", NoticeCommandStatus: "running"} if _, err := store.ReportSessionMessages(ctx, SessionMessageReport{WorkspaceID: "ws-semantics", AgentSessionID: "session-semantics", Messages: []MessageUpdate{{MessageID: "message-1", TurnID: "turn-1", Role: "assistant", Kind: "text", Semantics: semantics, OccurredAtUnixMS: 3}}}); err != nil { t.Fatal(err) } page, ok, err := store.ListSessionMessages(ctx, ListSessionMessagesInput{WorkspaceID: "ws-semantics", AgentSessionID: "session-semantics", Limit: 10}) if err != nil || !ok || len(page.Messages) != 1 || page.Messages[0].Semantics == nil || !page.Messages[0].Semantics.UserVisibleAssistantResponse || page.Messages[0].Semantics.NoticeCommand != "compact" { t.Fatalf("message semantics page=%#v ok=%v err=%v", page, ok, err) } } func TestReportActivityStateRejectsTurnForDeletedSession(t *testing.T) { t.Parallel() store := openTestStore(t, testOptions(&staticProjectPaths{})) ctx := context.Background() if _, err := store.ReportSessionState(ctx, SessionStateReport{WorkspaceID: "ws-deleted-activity", AgentSessionID: "session-deleted-activity", Provider: "codex", OccurredAtUnixMS: 1}); err != nil { t.Fatal(err) } if removed, err := store.DeleteSession(ctx, "ws-deleted-activity", "session-deleted-activity"); err != nil || !removed { t.Fatalf("DeleteSession removed=%v err=%v", removed, err) } result, err := store.ReportActivityState(ctx, ActivityStateReport{ Session: SessionStateReport{WorkspaceID: "ws-deleted-activity", AgentSessionID: "session-deleted-activity", Provider: "codex", OccurredAtUnixMS: 2}, Turn: &TurnTransition{WorkspaceID: "ws-deleted-activity", AgentSessionID: "session-deleted-activity", TurnID: "late-turn", Phase: TurnPhaseSettled, Outcome: TurnOutcomeCompleted, OccurredAtUnixMS: 2}, }) if err != nil || result.State.Accepted || result.TurnAccepted { t.Fatalf("late activity result=%#v err=%v", result, err) } if _, found, err := store.GetTurn(ctx, "ws-deleted-activity", "session-deleted-activity", "late-turn"); err != nil || found { t.Fatalf("late turn found=%v err=%v", found, err) } } func TestStoreRejectsMessageReferencingUnknownTurn(t *testing.T) { t.Parallel() store := openTestStore(t, testOptions(&staticProjectPaths{})) ctx := context.Background() if _, err := store.ReportSessionState(ctx, SessionStateReport{ WorkspaceID: "ws-1", AgentSessionID: "session-1", Origin: "runtime", Provider: "codex", ProviderSessionID: "provider-1", Status: "running", OccurredAtUnixMS: 100, }); err != nil { t.Fatal(err) } _, err := store.ReportSessionMessages(ctx, SessionMessageReport{ WorkspaceID: "ws-1", AgentSessionID: "session-1", Messages: []MessageUpdate{{ MessageID: "message-1", TurnID: "missing-turn", Role: "user", Kind: "text", Status: "completed", Payload: map[string]any{"text": "/goal clear"}, OccurredAtUnixMS: 110, }}, }) if err == nil || !strings.Contains(err.Error(), `references unknown turn "missing-turn"`) { t.Fatalf("ReportSessionMessages() error=%v, want unknown-turn rejection", err) } if _, ok, err := store.GetTurn(ctx, "ws-1", "session-1", "missing-turn"); err != nil || ok { t.Fatalf("GetTurn(missing-turn) ok=%v error=%v, want no manufactured turn", ok, err) } } func TestStoreClearSessionsTxJoinsCallerTransaction(t *testing.T) { t.Parallel() store := openTestStore(t, testOptions(&staticProjectPaths{})) ctx := context.Background() if _, err := store.ReportSessionState(ctx, SessionStateReport{ WorkspaceID: "ws-tx", AgentSessionID: "session-1", Origin: "runtime", Provider: "codex", Status: "completed", OccurredAtUnixMS: 100, }); err != nil { t.Fatalf("ReportSessionState() error = %v", err) } // A rollback of the caller's transaction must undo the clear. tx, err := store.db.BeginTx(ctx, nil) if err != nil { t.Fatalf("BeginTx() error = %v", err) } result, err := store.ClearSessionsTx(ctx, tx, "ws-tx") if err != nil { t.Fatalf("ClearSessionsTx() error = %v", err) } if result.RemovedSessions != 1 { t.Fatalf("ClearSessionsTx() = %#v, want one removed session", result) } if err := tx.Rollback(); err != nil { t.Fatalf("Rollback() error = %v", err) } if _, ok, err := store.GetSession(ctx, "ws-tx", "session-1"); err != nil || !ok { t.Fatalf("GetSession() after rollback ok=%v error=%v, want session restored", ok, err) } // A committed transaction applies it. tx, err = store.db.BeginTx(ctx, nil) if err != nil { t.Fatalf("BeginTx(second) error = %v", err) } if _, err := store.ClearSessionsTx(ctx, tx, "ws-tx"); err != nil { t.Fatalf("ClearSessionsTx(second) error = %v", err) } if err := tx.Commit(); err != nil { t.Fatalf("Commit() error = %v", err) } if _, ok, err := store.GetSession(ctx, "ws-tx", "session-1"); err != nil || ok { t.Fatalf("GetSession() after commit ok=%v error=%v, want cleared", ok, err) } } func TestStoreClearSessionsDeletesGoalSagaWithForeignKeysDisabledAndSessionIDReuseStartsClean(t *testing.T) { t.Parallel() store := openTestStore(t, testOptions(&staticProjectPaths{})) ctx := context.Background() if _, err := store.ReportSessionState(ctx, SessionStateReport{ WorkspaceID: "ws-clear-goal", AgentSessionID: "session-reused", Provider: "codex", OccurredAtUnixMS: 10, }); err != nil { t.Fatal(err) } if _, state, created, err := store.PrepareGoalControlOperation(ctx, GoalControlOperationPrepare{ OperationID: "old-goal-op", WorkspaceID: "ws-clear-goal", AgentSessionID: "session-reused", Action: "set", Objective: "old objective", OccurredAtUnixMS: 20, }); err != nil || !created || state.Revision != 1 { t.Fatalf("prepare old goal state=%#v created=%v error=%v", state, created, err) } if _, created, err := store.PrepareGoalGenerationFence(ctx, GoalGenerationFencePrepare{ FenceID: "old-goal-fence", WorkspaceID: "ws-clear-goal", AgentSessionID: "session-reused", TargetOperationID: "old-goal-op", ClientSubmitID: "old-binding-revoke", OccurredAtUnixMS: 21, }); err != nil || !created { t.Fatalf("prepare old goal fence created=%v error=%v", created, err) } if _, err := store.db.ExecContext(ctx, `PRAGMA foreign_keys = OFF`); err != nil { t.Fatalf("disable foreign keys: %v", err) } seedRuntimeDeletionSaga(t, store, "ws-clear-goal", "session-reused") result, err := store.ClearSessions(ctx, "ws-clear-goal") if err != nil || result.RemovedSessions != 1 { t.Fatalf("ClearSessions() result=%#v error=%v", result, err) } for _, table := range []string{"workspace_agent_runtime_operation_events", "workspace_agent_runtime_operations", "workspace_agent_goal_generation_fences", "workspace_agent_goal_control_operations", "workspace_agent_session_goals"} { var count int if err := store.db.QueryRowContext(ctx, `SELECT COUNT(*) FROM `+table+` WHERE workspace_id = ?`, "ws-clear-goal").Scan(&count); err != nil || count != 0 { t.Fatalf("%s count=%d error=%v, want empty", table, count, err) } } if _, err := store.ReportSessionState(ctx, SessionStateReport{ WorkspaceID: "ws-clear-goal", AgentSessionID: "session-reused", Provider: "codex", OccurredAtUnixMS: 30, }); err != nil { t.Fatalf("recreate session: %v", err) } if state, found, err := store.GetSessionGoalState(ctx, "ws-clear-goal", "session-reused"); err != nil || found { t.Fatalf("recreated goal state=%#v found=%v error=%v, want absent", state, found, err) } if _, state, created, err := store.PrepareGoalControlOperation(ctx, GoalControlOperationPrepare{ OperationID: "new-goal-op", WorkspaceID: "ws-clear-goal", AgentSessionID: "session-reused", Action: "set", Objective: "new objective", OccurredAtUnixMS: 40, }); err != nil || !created || state.Revision != 1 { t.Fatalf("prepare recreated goal state=%#v created=%v error=%v, want revision 1", state, created, err) } seedInteractionTurn(t, store, "ws-clear-goal", "session-reused", "turn-prepared", 50) if _, result, err := store.UpsertInteraction(ctx, InteractionUpsert{ WorkspaceID: "ws-clear-goal", AgentSessionID: "session-reused", TurnID: "turn-prepared", RequestID: "request-reused", Kind: InteractionKindQuestion, Status: InteractionStatusPending, OccurredAtUnixMS: 51, }); err != nil || result != InteractionTransitionApplied { t.Fatalf("recreate interaction result=%v error=%v", result, err) } if _, created, err := store.PrepareRuntimeOperation(ctx, RuntimeOperationPrepare{ OperationID: "new-runtime-op", WorkspaceID: "ws-clear-goal", AgentSessionID: "session-reused", Kind: RuntimeOperationKindInteractiveResponse, TurnID: "turn-prepared", RequestID: "request-reused", OccurredAtMS: 52, }); err != nil || !created { t.Fatalf("prepare recreated runtime operation created=%v error=%v", created, err) } claimable, err := store.ListClaimableRuntimeOperations(ctx, ListClaimableRuntimeOperationsInput{WorkspaceID: "ws-clear-goal", NowUnixMS: 100, Limit: 10}) if err != nil || len(claimable) != 1 || claimable[0].OperationID != "new-runtime-op" { t.Fatalf("claimable after recreation=%#v error=%v", claimable, err) } } func TestStoreSessionDeleteVariantsPreserveAndTerminalizeSagaWithForeignKeysDisabled(t *testing.T) { t.Parallel() for _, tc := range []struct { name string remove func(context.Context, *Store) error }{ {name: "single", remove: func(ctx context.Context, store *Store) error { _, err := store.DeleteSession(ctx, "ws-delete-goal", "session-1") return err }}, {name: "batch", remove: func(ctx context.Context, store *Store) error { _, err := store.DeleteSessionsBatch(ctx, DeleteSessionsBatchInput{WorkspaceID: "ws-delete-goal", SessionIDs: []string{"session-1"}}) return err }}, } { t.Run(tc.name, func(t *testing.T) { store := openTestStore(t, testOptions(&staticProjectPaths{})) ctx := context.Background() if _, err := store.ReportSessionState(ctx, SessionStateReport{ WorkspaceID: "ws-delete-goal", AgentSessionID: "session-1", Provider: "codex", OccurredAtUnixMS: 10, }); err != nil { t.Fatal(err) } goalOperationID := "goal-op-" + tc.name if _, _, _, err := store.PrepareGoalControlOperation(ctx, GoalControlOperationPrepare{ OperationID: goalOperationID, WorkspaceID: "ws-delete-goal", AgentSessionID: "session-1", Action: "set", Objective: "objective", OccurredAtUnixMS: 20, }); err != nil { t.Fatal(err) } if _, created, err := store.PrepareGoalGenerationFence(ctx, GoalGenerationFencePrepare{ FenceID: "goal-fence-" + tc.name, WorkspaceID: "ws-delete-goal", AgentSessionID: "session-1", TargetOperationID: goalOperationID, ClientSubmitID: "binding-revoke-" + tc.name, OccurredAtUnixMS: 21, }); err != nil || !created { t.Fatalf("prepare goal fence created=%v error=%v", created, err) } if _, err := store.db.ExecContext(ctx, `PRAGMA foreign_keys = OFF`); err != nil { t.Fatal(err) } seedRuntimeDeletionSaga(t, store, "ws-delete-goal", "session-1") if err := tc.remove(ctx, store); err != nil { t.Fatal(err) } for _, table := range []string{"workspace_agent_runtime_operation_events", "workspace_agent_runtime_operations", "workspace_agent_goal_generation_fences", "workspace_agent_goal_control_operations", "workspace_agent_session_goals"} { var count int if err := store.db.QueryRowContext(ctx, `SELECT COUNT(*) FROM `+table+` WHERE workspace_id = ?`, "ws-delete-goal").Scan(&count); err != nil || count == 0 { t.Fatalf("%s count=%d error=%v, want preserved rows", table, count, err) } } var runtimePending, goalPending, fencePending int if err := store.db.QueryRowContext(ctx, `SELECT COUNT(*) FROM workspace_agent_runtime_operations WHERE workspace_id=? AND status IN ('prepared','leased')`, "ws-delete-goal").Scan(&runtimePending); err != nil { t.Fatal(err) } if err := store.db.QueryRowContext(ctx, `SELECT COUNT(*) FROM workspace_agent_goal_control_operations WHERE workspace_id=? AND status IN ('prepared','dispatched')`, "ws-delete-goal").Scan(&goalPending); err != nil { t.Fatal(err) } if err := store.db.QueryRowContext(ctx, `SELECT COUNT(*) FROM workspace_agent_goal_generation_fences WHERE workspace_id=? AND status IN ('pending','processing')`, "ws-delete-goal").Scan(&fencePending); err != nil { t.Fatal(err) } if runtimePending != 0 || goalPending != 0 || fencePending != 0 { t.Fatalf("executable saga state remains: runtime=%d goal=%d fence=%d", runtimePending, goalPending, fencePending) } }) } } func seedRuntimeDeletionSaga(t *testing.T, store *Store, workspaceID string, sessionID string) { t.Helper() for _, row := range []struct { operationID string status string turnID string result any leaseOwner any leaseExpiry any nextAttempt any completedAt any }{ {operationID: "old-runtime-prepared", status: RuntimeOperationStatusPrepared, turnID: "turn-prepared", nextAttempt: int64(10)}, {operationID: "old-runtime-leased", status: RuntimeOperationStatusLeased, turnID: "turn-leased", leaseOwner: "worker", leaseExpiry: int64(1000)}, {operationID: "old-runtime-completed", status: RuntimeOperationStatusCompleted, turnID: "turn-completed", result: RuntimeOperationResultCanceled, completedAt: int64(30)}, } { query := `INSERT INTO workspace_agent_runtime_operations ( operation_id, workspace_id, agent_session_id, kind, status, result, turn_id, payload_json, lease_owner, lease_expires_at_unix_ms, next_attempt_at_unix_ms, created_at_unix_ms, updated_at_unix_ms, completed_at_unix_ms ) VALUES (?, ?, ?, 'cancel_turn', ?, ?, ?, '{}', ?, ?, ?, 20, 20, ?)` if _, err := store.db.Exec(query, row.operationID, workspaceID, sessionID, row.status, row.result, row.turnID, row.leaseOwner, row.leaseExpiry, row.nextAttempt, row.completedAt); err != nil { t.Fatalf("seed runtime operation %s: %v", row.status, err) } } if _, err := store.db.Exec(`INSERT INTO workspace_agent_runtime_operation_events ( operation_id, workspace_id, agent_session_id, kind, payload_json, created_at_unix_ms ) VALUES ('old-runtime-completed', ?, ?, 'turn_canceled', '{}', 30)`, workspaceID, sessionID); err != nil { t.Fatalf("seed runtime operation event: %v", err) } if _, err := store.db.Exec(`INSERT INTO workspace_agent_runtime_operations ( operation_id, workspace_id, agent_session_id, kind, status, result, turn_id, request_id, payload_json, created_at_unix_ms, updated_at_unix_ms, completed_at_unix_ms ) VALUES ('old-runtime-interactive', ?, ?, 'interactive_response', 'completed', 'answered', 'turn-prepared', 'request-reused', '{}', 31, 31, 31)`, workspaceID, sessionID); err != nil { t.Fatalf("seed reused interactive runtime operation: %v", err) } if _, err := store.db.Exec(`INSERT INTO workspace_agent_runtime_operation_events ( operation_id, workspace_id, agent_session_id, kind, payload_json, created_at_unix_ms ) VALUES ('old-runtime-interactive', ?, ?, 'interactive_completed', '{}', 31)`, workspaceID, sessionID); err != nil { t.Fatalf("seed reused interactive runtime operation event: %v", err) } } func TestStoreWorkspaceExistsCallbackGatesWrites(t *testing.T) { t.Parallel() opts := testOptions(&staticProjectPaths{}) opts.WorkspaceExists = func(_ context.Context, workspaceID string) error { if workspaceID != "ws-known" { return errTestWorkspaceNotFound } return nil } store := openTestStore(t, opts) ctx := context.Background() if _, err := store.ReportSessionState(ctx, SessionStateReport{ WorkspaceID: "ws-unknown", AgentSessionID: "session-1", Status: "running", }); !errors.Is(err, errTestWorkspaceNotFound) { t.Fatalf("ReportSessionState(unknown) error = %v, want workspace not found", err) } if _, err := store.ReportSessionState(ctx, SessionStateReport{ WorkspaceID: "ws-known", AgentSessionID: "session-1", Status: "running", }); err != nil { t.Fatalf("ReportSessionState(known) error = %v", err) } if _, _, err := store.ListWorkspaceGeneratedFileTurns(ctx, ListWorkspaceGeneratedFileTurnsInput{ WorkspaceID: "ws-unknown", SectionKey: RailSectionKeyConversations, }); !errors.Is(err, errTestWorkspaceNotFound) { t.Fatalf("ListWorkspaceGeneratedFileTurns(unknown) error = %v, want workspace not found", err) } } func TestStoreClassifiesRailSectionsWithInjectedProjectPaths(t *testing.T) { t.Parallel() projects := &staticProjectPaths{} store := openTestStore(t, testOptions(projects)) ctx := context.Background() root := t.TempDir() repo := filepath.Join(root, "repo") repoSubdir := filepath.Join(repo, "pkg") otherDir := filepath.Join(root, "other") for _, path := range []string{repoSubdir, otherDir} { if err := mkdirAll(path); err != nil { t.Fatalf("mkdir %q error = %v", path, err) } } projects.paths = []string{repo} repoCanonical := NormalizeProjectPath(repo) projectResult, err := store.ReportSessionState(ctx, SessionStateReport{ WorkspaceID: "ws-rail", AgentSessionID: "session-project", Origin: "runtime", Provider: "codex", Cwd: repoSubdir, Status: "completed", OccurredAtUnixMS: 100, }) if err != nil { t.Fatalf("ReportSessionState(project) error = %v", err) } wantProjectKey := RailSectionKeyForProject(repoCanonical) if projectResult.Session.RailSectionKey != wantProjectKey { t.Fatalf("ReportSessionState(project) rail key = %q, want %q", projectResult.Session.RailSectionKey, wantProjectKey) } projects.paths = []string{otherDir} updatedProjectResult, err := store.ReportSessionState(ctx, SessionStateReport{ WorkspaceID: "ws-rail", AgentSessionID: "session-project", Origin: "runtime", Provider: "codex", Cwd: otherDir, Status: "completed", OccurredAtUnixMS: 200, }) if err != nil { t.Fatalf("ReportSessionState(project cwd changed) error = %v", err) } if updatedProjectResult.Session.RailSectionKey != wantProjectKey { t.Fatalf( "ReportSessionState(project cwd changed) rail key = %q, want immutable %q", updatedProjectResult.Session.RailSectionKey, wantProjectKey, ) } projects.paths = []string{repo} if _, err := store.ReportSessionState(ctx, SessionStateReport{ WorkspaceID: "ws-rail", AgentSessionID: "session-other", Origin: "runtime", Provider: "codex", Cwd: otherDir, Status: "completed", OccurredAtUnixMS: 100, }); err != nil { t.Fatalf("ReportSessionState(other) error = %v", err) } projectPage, ok, err := store.ListSessionSection(ctx, ListSessionSectionInput{ WorkspaceID: "ws-rail", SectionKey: wantProjectKey, Limit: 10, }) if err != nil || !ok { t.Fatalf("ListSessionSection(project) ok=%v error=%v", ok, err) } if len(projectPage.Sessions) != 1 || projectPage.Sessions[0].ID != "session-project" || projectPage.Sessions[0].RailSectionKey != wantProjectKey { t.Fatalf("project page = %#v, want session-project", projectPage.Sessions) } projectSession, ok, err := store.GetSession(ctx, "ws-rail", "session-project") if err != nil || !ok || projectSession.RailSectionKey != wantProjectKey { t.Fatalf("GetSession(project) = %#v ok=%v error=%v, want rail key %q", projectSession, ok, err, wantProjectKey) } conversationsPage, ok, err := store.ListSessionSection(ctx, ListSessionSectionInput{ WorkspaceID: "ws-rail", SectionKey: RailSectionKeyConversations, Limit: 10, }) if err != nil || !ok { t.Fatalf("ListSessionSection(conversations) ok=%v error=%v", ok, err) } if len(conversationsPage.Sessions) != 1 || conversationsPage.Sessions[0].ID != "session-other" || conversationsPage.Sessions[0].RailSectionKey != RailSectionKeyConversations { t.Fatalf("conversations page = %#v, want session-other", conversationsPage.Sessions) } } func TestStoreImportedRailUsesSelectedNestedProjectAndRepairsAncestor(t *testing.T) { t.Parallel() root := t.TempDir() parent := filepath.Join(root, "dev") nested := filepath.Join(parent, "kage") if err := mkdirAll(nested); err != nil { t.Fatalf("mkdir nested project error = %v", err) } parent = NormalizeProjectPath(parent) nested = NormalizeProjectPath(nested) projects := &staticProjectPaths{paths: []string{parent}} store := openTestStore(t, testOptions(projects)) ctx := context.Background() importedContext := map[string]any{"imported": true} initial, err := store.ReportSessionState(ctx, SessionStateReport{ WorkspaceID: "ws-import-rail", AgentSessionID: "selected-nested", Origin: "WORKSPACE_AGENT_SESSION_ORIGIN_IMPORTED", Provider: "codex", RuntimeContext: importedContext, Cwd: nested, ImportProjectPath: nested, Status: "completed", OccurredAtUnixMS: 100, }) if err != nil { t.Fatalf("ReportSessionState(selected nested) error = %v", err) } wantNestedKey := RailSectionKeyForProject(nested) if initial.Session.RailSectionKey != wantNestedKey { t.Fatalf("selected nested rail key = %q, want %q", initial.Session.RailSectionKey, wantNestedKey) } legacy, err := store.ReportSessionState(ctx, SessionStateReport{ WorkspaceID: "ws-import-rail", AgentSessionID: "legacy-parent", Origin: "WORKSPACE_AGENT_SESSION_ORIGIN_IMPORTED", Provider: "codex", RuntimeContext: importedContext, Cwd: nested, Status: "completed", OccurredAtUnixMS: 100, }) if err != nil { t.Fatalf("ReportSessionState(legacy parent) error = %v", err) } if legacy.Session.RailSectionKey != RailSectionKeyForProject(parent) { t.Fatalf("legacy rail key = %q, want parent before repair", legacy.Session.RailSectionKey) } repaired, err := store.ReportSessionState(ctx, SessionStateReport{ WorkspaceID: "ws-import-rail", AgentSessionID: "legacy-parent", Origin: "WORKSPACE_AGENT_SESSION_ORIGIN_IMPORTED", Provider: "codex", RuntimeContext: importedContext, Cwd: nested, ImportProjectPath: nested, Status: "completed", OccurredAtUnixMS: 50, }) if err != nil { t.Fatalf("ReportSessionState(repair nested) error = %v", err) } if repaired.Session.RailSectionKey != wantNestedKey { t.Fatalf("repaired rail key = %q, want %q", repaired.Session.RailSectionKey, wantNestedKey) } preserved, err := store.ReportSessionState(ctx, SessionStateReport{ WorkspaceID: "ws-import-rail", AgentSessionID: "legacy-parent", Origin: "WORKSPACE_AGENT_SESSION_ORIGIN_IMPORTED", Provider: "codex", RuntimeContext: importedContext, Cwd: nested, ImportProjectPath: parent, Status: "completed", OccurredAtUnixMS: 300, }) if err != nil { t.Fatalf("ReportSessionState(preserve nested) error = %v", err) } if preserved.Session.RailSectionKey != wantNestedKey { t.Fatalf("ancestor reimport rail key = %q, want existing nested %q", preserved.Session.RailSectionKey, wantNestedKey) } ordinary, err := store.ReportSessionState(ctx, SessionStateReport{ WorkspaceID: "ws-import-rail", AgentSessionID: "ordinary-runtime", Origin: "runtime", Provider: "codex", RuntimeContext: map[string]any{"imported": false}, Cwd: nested, ImportProjectPath: nested, Status: "completed", OccurredAtUnixMS: 100, }) if err != nil { t.Fatalf("ReportSessionState(ordinary runtime) error = %v", err) } wantParentKey := RailSectionKeyForProject(parent) if ordinary.Session.RailSectionKey != wantParentKey { t.Fatalf("ordinary runtime rail key = %q, want injected parent %q", ordinary.Session.RailSectionKey, wantParentKey) } noProject, err := store.ReportSessionState(ctx, SessionStateReport{ WorkspaceID: "ws-import-rail", AgentSessionID: "no-project-import", Origin: "WORKSPACE_AGENT_SESSION_ORIGIN_IMPORTED", Provider: "codex", RuntimeContext: map[string]any{"externalImportNoProject": true, "imported": true}, Cwd: nested, ImportProjectPath: nested, Status: "completed", OccurredAtUnixMS: 100, }) if err != nil { t.Fatalf("ReportSessionState(no-project import) error = %v", err) } if noProject.Session.RailSectionKey != RailSectionKeyConversations { t.Fatalf("no-project import rail key = %q, want conversations", noProject.Session.RailSectionKey) } } func TestStoreListSessionSectionFiltersHiddenSessionsBeforePagination(t *testing.T) { t.Parallel() projects := &staticProjectPaths{paths: []string{"/workspace/app"}} store := openTestStore(t, testOptions(projects)) ctx := context.Background() for _, input := range []SessionStateReport{ { WorkspaceID: "ws-rail-visible", AgentSessionID: "bbb-visible-newer", Origin: "runtime", Provider: "codex", Cwd: "/workspace/app", Title: "visible newer", Status: "completed", OccurredAtUnixMS: 100, }, { WorkspaceID: "ws-rail-visible", AgentSessionID: "ccc-visible-older", Origin: "runtime", Provider: "codex", Cwd: "/workspace/app", Title: "visible older", Status: "completed", RuntimeContext: map[string]any{"visible": true}, OccurredAtUnixMS: 100, }, { WorkspaceID: "ws-rail-visible", AgentSessionID: "aaa-hidden", Origin: "runtime", Provider: "claude-code", Cwd: "/workspace/app", Title: "hidden", Status: "completed", RuntimeContext: map[string]any{"visible": false}, OccurredAtUnixMS: 100, }, } { if _, err := store.ReportSessionState(ctx, input); err != nil { t.Fatalf("ReportSessionState(%s) error = %v", input.AgentSessionID, err) } } if _, err := store.db.ExecContext(ctx, ` UPDATE workspace_agent_sessions SET created_at_unix_ms = 1000, updated_at_unix_ms = 1000 WHERE workspace_id = ?`, "ws-rail-visible"); err != nil { t.Fatalf("normalize session timestamps error = %v", err) } page, ok, err := store.ListSessionSection(ctx, ListSessionSectionInput{ WorkspaceID: "ws-rail-visible", SectionKey: RailSectionKeyForProject("/workspace/app"), Limit: 1, }) if err != nil || !ok { t.Fatalf("ListSessionSection(first) ok=%v error=%v", ok, err) } if len(page.Sessions) != 1 || page.Sessions[0].ID != "bbb-visible-newer" { t.Fatalf("first page sessions = %#v, want bbb-visible-newer", page.Sessions) } if !page.HasMore || !strings.HasSuffix(page.NextCursor, "|bbb-visible-newer") { t.Fatalf("first page state = hasMore %v cursor %q, want visible cursor with more", page.HasMore, page.NextCursor) } if page.TotalCount != 2 { t.Fatalf("first page total count = %d, want 2 visible sessions", page.TotalCount) } next, ok, err := store.ListSessionSection(ctx, ListSessionSectionInput{ WorkspaceID: "ws-rail-visible", SectionKey: RailSectionKeyForProject("/workspace/app"), CursorSortTimeUnixMS: page.Sessions[0].CreatedAtUnixMS, CursorSessionID: page.Sessions[0].ID, Limit: 1, }) if err != nil || !ok { t.Fatalf("ListSessionSection(next) ok=%v error=%v", ok, err) } if len(next.Sessions) != 1 || next.Sessions[0].ID != "ccc-visible-older" { t.Fatalf("next page sessions = %#v, want ccc-visible-older", next.Sessions) } if next.HasMore || next.NextCursor != "" { t.Fatalf("next page state = hasMore %v cursor %q, want exhausted", next.HasMore, next.NextCursor) } if next.TotalCount != 2 { t.Fatalf("next page total count = %d, want stable total 2", next.TotalCount) } } func TestStoreListSessionSectionOrdersAndPagesByLatestTurnStart(t *testing.T) { t.Parallel() store := openTestStore(t, testOptions(&staticProjectPaths{paths: []string{"/workspace/app"}})) ctx := context.Background() for _, report := range []ActivityStateReport{ { Session: SessionStateReport{ WorkspaceID: "ws-turn-order", AgentSessionID: "older-start-newer-update", Origin: "runtime", Provider: "codex", Cwd: "/workspace/app", Status: "working", OccurredAtUnixMS: 9_000, }, Turn: &TurnTransition{ WorkspaceID: "ws-turn-order", AgentSessionID: "older-start-newer-update", TurnID: "turn-older", Phase: TurnPhaseRunning, StartedAtUnixMS: 2_000, OccurredAtUnixMS: 9_000, }, }, { Session: SessionStateReport{ WorkspaceID: "ws-turn-order", AgentSessionID: "newer-start-older-update", Origin: "runtime", Provider: "codex", Cwd: "/workspace/app", Status: "working", OccurredAtUnixMS: 4_000, }, Turn: &TurnTransition{ WorkspaceID: "ws-turn-order", AgentSessionID: "newer-start-older-update", TurnID: "turn-newer", Phase: TurnPhaseRunning, StartedAtUnixMS: 3_000, OccurredAtUnixMS: 4_000, }, }, } { if _, err := store.ReportActivityState(ctx, report); err != nil { t.Fatalf("ReportActivityState(%s) error = %v", report.Session.AgentSessionID, err) } } page, ok, err := store.ListSessionSection(ctx, ListSessionSectionInput{ WorkspaceID: "ws-turn-order", SectionKey: RailSectionKeyForProject("/workspace/app"), Limit: 1, }) if err != nil || !ok { t.Fatalf("ListSessionSection(first) ok=%v error=%v", ok, err) } if len(page.Sessions) != 1 || page.Sessions[0].ID != "newer-start-older-update" { t.Fatalf("first page sessions = %#v, want latest turn start", page.Sessions) } if !page.HasMore || page.NextCursor != "3000|newer-start-older-update" { t.Fatalf("first page hasMore=%v cursor=%q", page.HasMore, page.NextCursor) } if page.TotalCount != 2 { t.Fatalf("first page total count = %d, want 2", page.TotalCount) } next, ok, err := store.ListSessionSection(ctx, ListSessionSectionInput{ WorkspaceID: "ws-turn-order", SectionKey: RailSectionKeyForProject("/workspace/app"), CursorSortTimeUnixMS: 3_000, CursorSessionID: "newer-start-older-update", Limit: 1, }) if err != nil || !ok { t.Fatalf("ListSessionSection(next) ok=%v error=%v", ok, err) } if len(next.Sessions) != 1 || next.Sessions[0].ID != "older-start-newer-update" { t.Fatalf("next page sessions = %#v, want older turn start", next.Sessions) } if next.HasMore || next.NextCursor != "" { t.Fatalf("next page hasMore=%v cursor=%q, want exhausted", next.HasMore, next.NextCursor) } if next.TotalCount != 2 { t.Fatalf("next page total count = %d, want stable total 2", next.TotalCount) } } func TestStoreListPinnedSessionPageOrdersByPinnedTime(t *testing.T) { t.Parallel() store := openTestStore(t, testOptions(&staticProjectPaths{})) ctx := context.Background() for _, input := range []SessionStateReport{ { WorkspaceID: "ws-pinned-page", AgentSessionID: "newer-pinned", Origin: "runtime", Provider: "codex", Status: "completed", OccurredAtUnixMS: 100, }, { WorkspaceID: "ws-pinned-page", AgentSessionID: "older-pinned", Origin: "runtime", Provider: "codex", Status: "completed", OccurredAtUnixMS: 100, }, { WorkspaceID: "ws-pinned-page", AgentSessionID: "unpinned", Origin: "runtime", Provider: "codex", Status: "completed", OccurredAtUnixMS: 100, }, } { if _, err := store.ReportSessionState(ctx, input); err != nil { t.Fatalf("ReportSessionState(%s) error = %v", input.AgentSessionID, err) } } for sessionID, pinnedAt := range map[string]int64{ "newer-pinned": 2000, "older-pinned": 1000, } { if _, err := store.db.ExecContext(ctx, ` UPDATE workspace_agent_sessions SET pinned_at_unix_ms = ? WHERE workspace_id = ? AND agent_session_id = ?`, pinnedAt, "ws-pinned-page", sessionID); err != nil { t.Fatalf("set pinned_at_unix_ms(%s) error = %v", sessionID, err) } } conversations, ok, err := store.ListSessionSection(ctx, ListSessionSectionInput{ WorkspaceID: "ws-pinned-page", SectionKey: RailSectionKeyConversations, Limit: 10, }) if err != nil || !ok { t.Fatalf("ListSessionSection(conversations) ok=%v error=%v", ok, err) } if len(conversations.Sessions) != 1 || conversations.Sessions[0].ID != "unpinned" { t.Fatalf("ordinary conversations = %#v, want only unpinned", conversations.Sessions) } if conversations.TotalCount != 1 { t.Fatalf("ordinary conversations total count = %d, want 1", conversations.TotalCount) } page, ok, err := store.ListSessionSection(ctx, ListSessionSectionInput{ WorkspaceID: "ws-pinned-page", SectionKey: PinnedSessionPageKey, Limit: 1, }) if err != nil || !ok { t.Fatalf("ListSessionSection(pinned first) ok=%v error=%v", ok, err) } if len(page.Sessions) != 1 || page.Sessions[0].ID != "newer-pinned" { t.Fatalf("pinned first sessions = %#v, want newer-pinned", page.Sessions) } if !page.HasMore || page.NextCursor != "2000|newer-pinned" { t.Fatalf("pinned first page state = hasMore %v cursor %q", page.HasMore, page.NextCursor) } if page.TotalCount != 2 { t.Fatalf("pinned first page total count = %d, want 2", page.TotalCount) } next, ok, err := store.ListSessionSection(ctx, ListSessionSectionInput{ WorkspaceID: "ws-pinned-page", SectionKey: PinnedSessionPageKey, CursorSortTimeUnixMS: page.Sessions[0].PinnedAtUnixMS, CursorSessionID: page.Sessions[0].ID, Limit: 2, }) if err != nil || !ok { t.Fatalf("ListSessionSection(pinned next) ok=%v error=%v", ok, err) } if len(next.Sessions) != 1 || next.Sessions[0].ID != "older-pinned" { t.Fatalf("pinned next sessions = %#v, want older-pinned", next.Sessions) } if next.HasMore || next.NextCursor != "" { t.Fatalf("pinned next page state = hasMore %v cursor %q, want exhausted", next.HasMore, next.NextCursor) } if next.TotalCount != 2 { t.Fatalf("pinned next page total count = %d, want stable total 2", next.TotalCount) } } func TestStoreTargetNormalizationAndSkippableRows(t *testing.T) { t.Parallel() errInvalidTestTarget := errors.New("invalid test target") opts := testOptions(&staticProjectPaths{}) opts.NormalizeTarget = func(target Target) (Target, error) { target.Name = strings.TrimSpace(target.Name) if strings.Contains(target.Name, "broken") { return Target{}, fmt.Errorf("%w: %s", errInvalidTestTarget, target.Name) } return target, nil } opts.IsSkippableTargetError = func(err error) bool { return errors.Is(err, errInvalidTestTarget) } store := openTestStore(t, opts) ctx := context.Background() if _, err := store.PutAgentTarget(ctx, Target{ ID: "custom", Provider: "codex", LaunchRefJSON: `{"type":"local_cli","provider":"codex"}`, Name: " Custom ", MaskIconURL: " data:image/svg+xml;base64,mask ", Enabled: true, Source: "user", }); err != nil { t.Fatalf("PutAgentTarget() error = %v", err) } target, err := store.GetAgentTarget(ctx, "custom") if err != nil { t.Fatalf("GetAgentTarget() error = %v", err) } if target.Name != "Custom" { t.Fatalf("target name = %q, want normalized Custom", target.Name) } if target.MaskIconURL != " data:image/svg+xml;base64,mask " { t.Fatalf("target mask icon URL = %q", target.MaskIconURL) } if _, err := store.PutAgentTarget(ctx, Target{ID: "bad", Name: "broken row"}); !errors.Is(err, errInvalidTestTarget) { t.Fatalf("PutAgentTarget(invalid) error = %v, want invalid test target", err) } now := int64(1700000000000) if _, err := store.db.ExecContext(ctx, ` INSERT INTO agent_targets (id, provider, launch_ref_json, name, icon_key, enabled, source, sort_order, created_at_ms, updated_at_ms) VALUES ('broken-row', 'codex', '{}', 'broken stored row', NULL, 1, 'user', 5, ?, ?) `, now, now); err != nil { t.Fatalf("insert broken target fixture: %v", err) } targets, err := store.ListAgentTargets(ctx) if err != nil { t.Fatalf("ListAgentTargets() error = %v", err) } for _, listed := range targets { if listed.ID == "broken-row" { t.Fatalf("ListAgentTargets() returned skippable row: %#v", targets) } } if err := store.DeleteAgentTarget(ctx, "custom"); err != nil { t.Fatalf("DeleteAgentTarget() error = %v", err) } if _, err := store.GetAgentTarget(ctx, "custom"); !errors.Is(err, ErrAgentTargetNotFound) { t.Fatalf("GetAgentTarget(deleted) error = %v, want ErrAgentTargetNotFound", err) } }