package sync import ( "context" "encoding/json" "errors" "os" "path/filepath" "strings" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.kenn.io/agentsview/internal/db" "go.kenn.io/agentsview/internal/dbtest" "go.kenn.io/agentsview/internal/parser" ) // pdBaseSession returns a stored/prepared session pair that compares // as identical, for tests to mutate. func pdBaseSession() db.Session { return db.Session{ ID: "pd-sess", Agent: "claude", MessageCount: 4, UserMessageCount: 2, FirstMessage: new("hello there"), SessionName: new("My Session"), TotalOutputTokens: 120, PeakContextTokens: 9000, HasTotalOutputTokens: true, HasPeakContextTokens: true, TerminationStatus: new("clean"), } } func TestCompareSessionFields(t *testing.T) { type want struct { field string stored string parsed string informational bool } tests := []struct { name string stored func(*db.Session) prepared func(*db.Session) want []want }{ { name: "identical sessions produce no diffs", }, { name: "message count drift", stored: func(s *db.Session) { s.MessageCount = 3 }, want: []want{{ field: FieldMessageCount, stored: "3", parsed: "4", }}, }, { name: "user message count drift", stored: func(s *db.Session) { s.UserMessageCount = 1 }, want: []want{{ field: FieldUserMessageCount, stored: "1", parsed: "2", }}, }, { name: "first message nil stored vs empty parsed is no diff", stored: func(s *db.Session) { s.FirstMessage = nil }, prepared: func(s *db.Session) { s.FirstMessage = new("") }, }, { name: "first message empty stored vs nil parsed is no diff", stored: func(s *db.Session) { s.FirstMessage = new("") }, prepared: func(s *db.Session) { s.FirstMessage = nil }, }, { name: "first message drift", prepared: func(s *db.Session) { s.FirstMessage = new("different opener") }, want: []want{{ field: FieldFirstMessage, stored: "hello there", parsed: "different opener", }}, }, { name: "first message null vs value renders (null)", stored: func(s *db.Session) { s.FirstMessage = nil }, want: []want{{ field: FieldFirstMessage, stored: "(null)", parsed: "hello there", }}, }, { name: "session name nil vs empty is no diff", stored: func(s *db.Session) { s.SessionName = new("") }, prepared: func(s *db.Session) { s.SessionName = nil }, }, { name: "session name drift is informational for incremental agents", prepared: func(s *db.Session) { s.SessionName = new("Renamed") }, want: []want{{ field: FieldSessionName, stored: "My Session", parsed: "Renamed", informational: true, }}, }, { name: "session name drift is a real diff for full-replace agents", stored: func(s *db.Session) { s.Agent = "gemini" }, prepared: func(s *db.Session) { s.Agent = "gemini" s.SessionName = new("Renamed") }, want: []want{{ field: FieldSessionName, stored: "My Session", parsed: "Renamed", }}, }, { name: "total output tokens value drift", prepared: func(s *db.Session) { s.TotalOutputTokens = 121 }, want: []want{{ field: FieldTotalOutputTokens, stored: "120", parsed: "121", }}, }, { name: "total output coverage-flag-only flip is a real diff", prepared: func(s *db.Session) { s.HasTotalOutputTokens = false }, want: []want{{ field: FieldTotalOutputTokens, stored: "120", parsed: "absent", }}, }, { name: "absent-on-both-sides values are not compared", stored: func(s *db.Session) { s.HasTotalOutputTokens = false s.TotalOutputTokens = 0 }, prepared: func(s *db.Session) { s.HasTotalOutputTokens = false s.TotalOutputTokens = 7 }, }, { name: "peak context tokens drift", stored: func(s *db.Session) { s.PeakContextTokens = 8000 }, want: []want{{ field: FieldPeakContextTokens, stored: "8000", parsed: "9000", }}, }, { name: "termination stored null parsed value is informational", stored: func(s *db.Session) { s.TerminationStatus = nil }, want: []want{{ field: FieldTerminationStatus, stored: "(null)", parsed: "clean", informational: true, }}, }, { name: "termination stored value parsed null is a real diff", prepared: func(s *db.Session) { s.TerminationStatus = nil }, want: []want{{ field: FieldTerminationStatus, stored: "clean", parsed: "(null)", }}, }, { name: "termination value drift is a real diff", prepared: func(s *db.Session) { s.TerminationStatus = new("awaiting_user") }, want: []want{{ field: FieldTerminationStatus, stored: "clean", parsed: "awaiting_user", }}, }, { name: "termination null on both sides is no diff", stored: func(s *db.Session) { s.TerminationStatus = nil }, prepared: func(s *db.Session) { s.TerminationStatus = nil }, }, { name: "termination stored null parsed value is real drift for full-replace agents", stored: func(s *db.Session) { s.Agent = "antigravity-cli" s.TerminationStatus = nil }, prepared: func(s *db.Session) { s.Agent = "antigravity-cli" }, want: []want{{ field: FieldTerminationStatus, stored: "(null)", parsed: "clean", informational: false, }}, }, { name: "started_at drift is a real diff", stored: func(s *db.Session) { s.StartedAt = new("2026-01-01T00:00:00Z") }, prepared: func(s *db.Session) { s.StartedAt = new("2026-01-02T00:00:00Z") }, want: []want{{ field: FieldStartedAt, stored: "2026-01-01T00:00:00Z", parsed: "2026-01-02T00:00:00Z", }}, }, { name: "ended_at drift is a real diff", stored: func(s *db.Session) { s.EndedAt = new("2026-01-01T01:00:00Z") }, prepared: func(s *db.Session) { s.EndedAt = new("2026-01-01T02:00:00Z") }, want: []want{{ field: FieldEndedAt, stored: "2026-01-01T01:00:00Z", parsed: "2026-01-01T02:00:00Z", }}, }, { name: "cwd drift is informational for incremental agents", stored: func(s *db.Session) { s.Cwd = "/home/me/old" }, prepared: func(s *db.Session) { s.Cwd = "/home/me/new" }, want: []want{{ field: FieldCwd, stored: "/home/me/old", parsed: "/home/me/new", informational: true, }}, }, { name: "cwd drift is a real diff for full-replace agents", stored: func(s *db.Session) { s.Agent = "gemini" s.Cwd = "/home/me/old" }, prepared: func(s *db.Session) { s.Agent = "gemini" s.Cwd = "/home/me/new" }, want: []want{{ field: FieldCwd, stored: "/home/me/old", parsed: "/home/me/new", }}, }, { name: "git_branch drift is a real diff for full-replace agents", stored: func(s *db.Session) { s.Agent = "gemini" s.GitBranch = "main" }, prepared: func(s *db.Session) { s.Agent = "gemini" s.GitBranch = "feature" }, want: []want{{ field: FieldGitBranch, stored: "main", parsed: "feature", }}, }, { name: "relationship_type drift is a real diff", stored: func(s *db.Session) { s.Agent = "gemini" s.RelationshipType = "continuation" }, prepared: func(s *db.Session) { s.Agent = "gemini" s.RelationshipType = "fork" }, want: []want{{ field: FieldRelationshipType, stored: "continuation", parsed: "fork", }}, }, { name: "source_session_id drift is a real diff", stored: func(s *db.Session) { s.Agent = "gemini" s.SourceSessionID = "abc" }, prepared: func(s *db.Session) { s.Agent = "gemini" s.SourceSessionID = "xyz" }, want: []want{{ field: FieldSourceSessionID, stored: "abc", parsed: "xyz", }}, }, { name: "source_version drift is a real diff", stored: func(s *db.Session) { s.Agent = "gemini" s.SourceVersion = "1.0.0" }, prepared: func(s *db.Session) { s.Agent = "gemini" s.SourceVersion = "1.1.0" }, want: []want{{ field: FieldSourceVersion, stored: "1.0.0", parsed: "1.1.0", }}, }, { name: "transcript_fidelity drift is a real diff", stored: func(s *db.Session) { s.Agent = "gemini" s.TranscriptFidelity = "summary" }, prepared: func(s *db.Session) { s.Agent = "gemini" s.TranscriptFidelity = "full" }, want: []want{{ field: FieldTranscriptFidelity, stored: "summary", parsed: "full", }}, }, { name: "transcript_fidelity equal values produce no diff", stored: func(s *db.Session) { s.Agent = "gemini" s.TranscriptFidelity = "full" }, prepared: func(s *db.Session) { s.Agent = "gemini" s.TranscriptFidelity = "full" }, }, { name: "parent_session_id nil stored vs empty parsed is no diff", stored: func(s *db.Session) { s.Agent = "gemini" s.ParentSessionID = nil }, prepared: func(s *db.Session) { s.Agent = "gemini" s.ParentSessionID = new("") }, }, { name: "parent_session_id drift renders (null) for nil stored", stored: func(s *db.Session) { s.Agent = "gemini" s.ParentSessionID = nil }, prepared: func(s *db.Session) { s.Agent = "gemini" s.ParentSessionID = new("parent-1") }, want: []want{{ field: FieldParentSessionID, stored: "(null)", parsed: "parent-1", }}, }, { name: "parser_malformed_lines drift is a real diff", stored: func(s *db.Session) { s.Agent = "gemini" s.ParserMalformedLines = 0 }, prepared: func(s *db.Session) { s.Agent = "gemini" s.ParserMalformedLines = 3 }, want: []want{{ field: FieldParserMalformedLines, stored: "0", parsed: "3", }}, }, { name: "parser_malformed_lines drift is informational for incremental agents", prepared: func(s *db.Session) { s.ParserMalformedLines = 2 }, want: []want{{ field: FieldParserMalformedLines, stored: "0", parsed: "2", informational: true, }}, }, { name: "is_truncated drift is a real diff", stored: func(s *db.Session) { s.Agent = "gemini" s.IsTruncated = false }, prepared: func(s *db.Session) { s.Agent = "gemini" s.IsTruncated = true }, want: []want{{ field: FieldIsTruncated, stored: "false", parsed: "true", }}, }, { name: "project is not compared (resolver-derived)", stored: func(s *db.Session) { s.Agent = "gemini" s.Project = "project-old" }, prepared: func(s *db.Session) { s.Agent = "gemini" s.Project = "project-new" }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { stored := pdBaseSession() prepared := pdBaseSession() if tt.stored != nil { tt.stored(&stored) } if tt.prepared != nil { tt.prepared(&prepared) } diffs := compareSessionFields(&stored, prepared) require.Len(t, diffs, len(tt.want)) for i, w := range tt.want { assert.Equal(t, w.field, diffs[i].Field) assert.Equal(t, w.stored, diffs[i].Stored) assert.Equal(t, w.parsed, diffs[i].Parsed) assert.Equal( t, w.informational, diffs[i].Informational, ) } }) } } func TestCompareSessionFieldsTruncatesLongValues(t *testing.T) { long := strings.Repeat("x", 200) stored := pdBaseSession() prepared := pdBaseSession() prepared.FirstMessage = new(long) diffs := compareSessionFields(&stored, prepared) require.Len(t, diffs, 1) assert.Equal(t, FieldFirstMessage, diffs[0].Field) assert.Equal( t, strings.Repeat("x", maxRenderedValueRunes)+"...", diffs[0].Parsed, ) assert.Contains(t, diffs[0].Detail, "parsed 200 runes") } func TestCompareSessionFieldsInformationalTerminationDetail(t *testing.T) { stored := pdBaseSession() stored.TerminationStatus = nil prepared := pdBaseSession() diffs := compareSessionFields(&stored, prepared) require.Len(t, diffs, 1) assert.True(t, diffs[0].Informational) assert.Equal(t, "incremental-append history", diffs[0].Detail) } func pdMsg(ordinal int, model string, ctx, out int) db.Message { return db.Message{ Ordinal: ordinal, Role: "assistant", Model: model, ContextTokens: ctx, OutputTokens: out, HasContextTokens: ctx != 0, HasOutputTokens: out != 0, } } func TestCompareMessageMetadata(t *testing.T) { t.Run("identical slices produce no diffs", func(t *testing.T) { stored := []db.Message{ pdMsg(0, "", 0, 0), pdMsg(1, "model-a", 100, 5), } parsed := []db.Message{ pdMsg(0, "", 0, 0), pdMsg(1, "model-a", 100, 5), } assert.Empty(t, compareMessageMetadata(stored, parsed, false, false, false)) }) t.Run("model drift reports count and first ordinal", func(t *testing.T) { stored := []db.Message{ pdMsg(0, "model-a", 0, 0), pdMsg(1, "model-a", 0, 0), pdMsg(2, "model-a", 0, 0), } parsed := []db.Message{ pdMsg(0, "model-a", 0, 0), pdMsg(1, "model-b", 0, 0), pdMsg(2, "model-b", 0, 0), } diffs := compareMessageMetadata(stored, parsed, true, false, false) require.Len(t, diffs, 1) assert.Equal(t, FieldModels, diffs[0].Field) assert.Equal(t, "model-a", diffs[0].Stored) assert.Equal(t, "model-a, model-b", diffs[0].Parsed) assert.Equal( t, "2/3 messages differ; first at ordinal 1: model-a -> model-b", diffs[0].Detail, ) }) t.Run("token value drift reports message tokens", func(t *testing.T) { stored := []db.Message{pdMsg(0, "m", 100, 5)} parsed := []db.Message{pdMsg(0, "m", 110, 5)} diffs := compareMessageMetadata(stored, parsed, true, false, false) require.Len(t, diffs, 1) assert.Equal(t, FieldMessageTokens, diffs[0].Field) assert.Equal( t, "context=100 output=5 usage_bytes=0", diffs[0].Stored, ) assert.Equal( t, "context=110 output=5 usage_bytes=0", diffs[0].Parsed, ) assert.Equal( t, "1/1 messages differ; first at ordinal 0", diffs[0].Detail, ) }) t.Run("presence-flag-only flip is a token diff", func(t *testing.T) { stored := []db.Message{{ Ordinal: 0, Role: "assistant", OutputTokens: 0, HasOutputTokens: true, }} parsed := []db.Message{{ Ordinal: 0, Role: "assistant", OutputTokens: 0, HasOutputTokens: false, }} diffs := compareMessageMetadata(stored, parsed, true, false, false) require.Len(t, diffs, 1) assert.Equal(t, FieldMessageTokens, diffs[0].Field) assert.Contains(t, diffs[0].Stored, "output=0") assert.Contains(t, diffs[0].Parsed, "output=absent") }) t.Run("token_usage payload drift is a token diff", func(t *testing.T) { stored := []db.Message{{ Ordinal: 0, Role: "assistant", TokenUsage: json.RawMessage(`{"input_tokens":1}`), }} parsed := []db.Message{{ Ordinal: 0, Role: "assistant", TokenUsage: json.RawMessage(`{"input_tokens":2}`), }} diffs := compareMessageMetadata(stored, parsed, true, false, false) require.Len(t, diffs, 1) assert.Equal(t, FieldMessageTokens, diffs[0].Field) }) t.Run("equal-length body rewrite is a content diff", func(t *testing.T) { stored := []db.Message{{ Ordinal: 0, Role: "assistant", Content: "aaa", ContentLength: 3, }} parsed := []db.Message{{ Ordinal: 0, Role: "assistant", Content: "bbb", ContentLength: 3, }} diffs := compareMessageMetadata(stored, parsed, false, true, false) require.Len(t, diffs, 1) assert.Equal(t, FieldMessageContent, diffs[0].Field) assert.Contains(t, diffs[0].Detail, "body differs at equal length (3 bytes)") }) t.Run("length mismatch compares the overlap only", func(t *testing.T) { stored := []db.Message{pdMsg(0, "m", 100, 5)} parsed := []db.Message{ pdMsg(0, "m", 100, 5), pdMsg(1, "different-model", 999, 9), } assert.Empty(t, compareMessageMetadata(stored, parsed, false, false, false)) }) t.Run("alignment matches ordinal values not indices", func(t *testing.T) { stored := []db.Message{ pdMsg(0, "m", 100, 5), pdMsg(2, "m", 200, 6), } parsed := []db.Message{ pdMsg(2, "m", 200, 6), pdMsg(0, "m", 100, 5), } assert.Empty(t, compareMessageMetadata(stored, parsed, false, false, false)) }) t.Run("is_system flip is a metadata diff", func(t *testing.T) { stored := []db.Message{{Ordinal: 0, Role: "user", IsSystem: false}} parsed := []db.Message{{Ordinal: 0, Role: "user", IsSystem: true}} diffs := compareMessageMetadata(stored, parsed, true, false, false) require.Len(t, diffs, 1) assert.Equal(t, FieldMessageMetadata, diffs[0].Field) assert.Contains(t, diffs[0].Detail, "is_system false -> true") }) t.Run("has_thinking flip is a metadata diff", func(t *testing.T) { stored := []db.Message{{Ordinal: 0, Role: "assistant"}} parsed := []db.Message{{Ordinal: 0, Role: "assistant", HasThinking: true}} diffs := compareMessageMetadata(stored, parsed, true, false, false) require.Len(t, diffs, 1) assert.Equal(t, FieldMessageMetadata, diffs[0].Field) assert.Contains(t, diffs[0].Detail, "has_thinking false -> true") }) t.Run("has_tool_use flip is a metadata diff", func(t *testing.T) { stored := []db.Message{{Ordinal: 0, Role: "assistant"}} parsed := []db.Message{{Ordinal: 0, Role: "assistant", HasToolUse: true}} diffs := compareMessageMetadata(stored, parsed, true, false, false) require.Len(t, diffs, 1) assert.Equal(t, FieldMessageMetadata, diffs[0].Field) assert.Contains(t, diffs[0].Detail, "has_tool_use false -> true") }) t.Run("thinking_text drift is a metadata diff", func(t *testing.T) { stored := []db.Message{{ Ordinal: 0, Role: "assistant", ThinkingText: "old reasoning", }} parsed := []db.Message{{ Ordinal: 0, Role: "assistant", ThinkingText: "new reasoning", }} diffs := compareMessageMetadata(stored, parsed, true, false, false) require.Len(t, diffs, 1) assert.Equal(t, FieldMessageMetadata, diffs[0].Field) assert.Contains(t, diffs[0].Detail, "thinking_text differs") }) t.Run("tool_name drift is a tool_calls diff", func(t *testing.T) { stored := []db.Message{{ Ordinal: 0, Role: "assistant", HasToolUse: true, ToolCalls: []db.ToolCall{ {ToolName: "Read", Category: "Read", ToolUseID: "tu1"}, }, }} parsed := []db.Message{{ Ordinal: 0, Role: "assistant", HasToolUse: true, ToolCalls: []db.ToolCall{ {ToolName: "Bash", Category: "Read", ToolUseID: "tu1"}, }, }} diffs := compareMessageMetadata(stored, parsed, false, false, true) require.Len(t, diffs, 1) assert.Equal(t, FieldToolCalls, diffs[0].Field) assert.Contains(t, diffs[0].Detail, `tool_name "Read" -> "Bash"`) assert.Contains(t, diffs[0].Detail, "first at ordinal 0") }) t.Run("tool_call sub-field drift attributes to tool_calls", func(t *testing.T) { base := db.ToolCall{ ToolName: "Read", Category: "Read", ToolUseID: "tu1", InputJSON: `{"file":"a"}`, SkillName: "s1", SubagentSessionID: "agent-1", } subCases := []struct { name string mutate func(*db.ToolCall) detail string }{ {"category", func(tc *db.ToolCall) { tc.Category = "Bash" }, `category "Read" -> "Bash"`}, {"tool_use_id", func(tc *db.ToolCall) { tc.ToolUseID = "tu2" }, "tool_use_id differs"}, {"input_json", func(tc *db.ToolCall) { tc.InputJSON = `{"file":"b"}` }, "input_json differs"}, {"skill_name", func(tc *db.ToolCall) { tc.SkillName = "s2" }, `skill_name "s1" -> "s2"`}, {"subagent_session_id", func(tc *db.ToolCall) { tc.SubagentSessionID = "agent-2" }, "subagent_session_id differs"}, } for _, sc := range subCases { t.Run(sc.name, func(t *testing.T) { parsedTC := base sc.mutate(&parsedTC) stored := []db.Message{{ Ordinal: 0, Role: "assistant", ToolCalls: []db.ToolCall{base}, }} parsed := []db.Message{{ Ordinal: 0, Role: "assistant", ToolCalls: []db.ToolCall{parsedTC}, }} diffs := compareMessageMetadata( stored, parsed, false, false, true, ) require.Len(t, diffs, 1) assert.Equal(t, FieldToolCalls, diffs[0].Field) assert.Contains(t, diffs[0].Detail, sc.detail) }) } }) t.Run("tool_call count drift is a tool_calls diff", func(t *testing.T) { stored := []db.Message{{ Ordinal: 0, Role: "assistant", ToolCalls: []db.ToolCall{{ToolName: "Read", ToolUseID: "tu1"}}, }} parsed := []db.Message{{ Ordinal: 0, Role: "assistant", ToolCalls: []db.ToolCall{ {ToolName: "Read", ToolUseID: "tu1"}, {ToolName: "Bash", ToolUseID: "tu2"}, }, }} diffs := compareMessageMetadata(stored, parsed, false, false, true) require.Len(t, diffs, 1) assert.Equal(t, FieldToolCalls, diffs[0].Field) assert.Contains(t, diffs[0].Detail, "tool_call count 1 -> 2") }) t.Run("result_content_length drift is a tool_calls diff", func(t *testing.T) { stored := []db.Message{{ Ordinal: 0, Role: "assistant", ToolCalls: []db.ToolCall{ {ToolName: "Read", ToolUseID: "tu1", ResultContentLength: 100}, }, }} parsed := []db.Message{{ Ordinal: 0, Role: "assistant", ToolCalls: []db.ToolCall{ {ToolName: "Read", ToolUseID: "tu1", ResultContentLength: 250}, }, }} diffs := compareMessageMetadata(stored, parsed, false, false, true) require.Len(t, diffs, 1) assert.Equal(t, FieldToolCalls, diffs[0].Field) assert.Contains(t, diffs[0].Detail, "result_content_length 100 -> 250") }) t.Run("tool fingerprint differs but overlap matches yields fallback", func(t *testing.T) { // No aligned-ordinal tool diff (e.g. an ordinal-set shift), // but the tool fingerprint asserted inequality: the session // must not report identical. stored := []db.Message{{Ordinal: 0, Role: "assistant"}} parsed := []db.Message{{Ordinal: 0, Role: "assistant"}} diffs := compareMessageMetadata(stored, parsed, false, false, true) require.Len(t, diffs, 1) assert.Equal(t, FieldToolCalls, diffs[0].Field) assert.Equal(t, "fingerprint", diffs[0].Stored) assert.Contains(t, diffs[0].Detail, "tool-call fingerprint differs") }) } func pdEvent( source, model string, in, out int, occurredAt, dedup string, ordinal *int, ) db.UsageEvent { return db.UsageEvent{ Source: source, Model: model, InputTokens: in, OutputTokens: out, OccurredAt: occurredAt, DedupKey: dedup, MessageOrdinal: ordinal, } } func TestCompareUsageEvents(t *testing.T) { t.Run("both empty produces no diffs", func(t *testing.T) { assert.Empty(t, compareUsageEvents(nil, nil)) }) t.Run("shuffled order produces no diffs", func(t *testing.T) { a := pdEvent("api", "m1", 10, 2, "2026-01-01T00:00:00Z", "", new(0)) b := pdEvent("api", "m2", 20, 4, "2026-01-01T00:01:00Z", "", new(1)) c := pdEvent("api", "m1", 10, 2, "2026-01-01T00:02:00Z", "k1", nil) stored := []db.UsageEvent{a, b, c} parsed := []db.UsageEvent{c, a, b} assert.Empty(t, compareUsageEvents(stored, parsed)) }) t.Run("removed event reports count and totals", func(t *testing.T) { a := pdEvent("api", "m1", 10, 2, "2026-01-01T00:00:00Z", "", nil) b := pdEvent("api", "m2", 20, 4, "2026-01-01T00:01:00Z", "", nil) diffs := compareUsageEvents( []db.UsageEvent{a, b}, []db.UsageEvent{a}, ) require.Len(t, diffs, 2) assert.Equal(t, FieldUsageEventCount, diffs[0].Field) assert.Equal(t, "2", diffs[0].Stored) assert.Equal(t, "1", diffs[0].Parsed) assert.Equal(t, FieldUsageEventTotals, diffs[1].Field) assert.Equal(t, "input 30 -> 10; output 6 -> 2", diffs[1].Detail) }) t.Run("total drift with equal count reports totals only", func(t *testing.T) { stored := []db.UsageEvent{ pdEvent("api", "m1", 10, 2, "2026-01-01T00:00:00Z", "", nil), } parsed := []db.UsageEvent{ pdEvent("api", "m1", 10, 3, "2026-01-01T00:00:00Z", "", nil), } diffs := compareUsageEvents(stored, parsed) require.Len(t, diffs, 1) assert.Equal(t, FieldUsageEventTotals, diffs[0].Field) assert.Equal(t, "output 2 -> 3", diffs[0].Detail) assert.Contains(t, diffs[0].Stored, "output=2") assert.Contains(t, diffs[0].Parsed, "output=3") }) t.Run("matching dedup keys override tuple drift", func(t *testing.T) { // Same dedup key with a different occurred_at: the key is // authoritative, totals match, so no diff. stored := []db.UsageEvent{ pdEvent("api", "m1", 10, 2, "2026-01-01T00:00:00Z", "k1", nil), } parsed := []db.UsageEvent{ pdEvent("api", "m1", 10, 2, "2026-01-01T09:09:09Z", "k1", nil), } assert.Empty(t, compareUsageEvents(stored, parsed)) }) t.Run("dedup key drift with equal totals reports composition", func(t *testing.T) { stored := []db.UsageEvent{ pdEvent("api", "m1", 10, 2, "2026-01-01T00:00:00Z", "k1", nil), } parsed := []db.UsageEvent{ pdEvent("api", "m1", 10, 2, "2026-01-01T00:00:00Z", "k2", nil), } diffs := compareUsageEvents(stored, parsed) require.Len(t, diffs, 1) assert.Equal(t, FieldUsageEventTotals, diffs[0].Field) assert.Contains(t, diffs[0].Detail, "event composition differs") assert.Contains(t, diffs[0].Detail, "dedup|k1") }) t.Run("model drift under stable dedup key surfaces", func(t *testing.T) { // Same dedup key and equal token totals, but the event is // re-attributed to a different model: must not pass silently. stored := []db.UsageEvent{ pdEvent("api", "m1", 10, 2, "2026-01-01T00:00:00Z", "k1", nil), } parsed := []db.UsageEvent{ pdEvent("api", "m2", 10, 2, "2026-01-01T00:00:00Z", "k1", nil), } diffs := compareUsageEvents(stored, parsed) require.Len(t, diffs, 1) assert.Equal(t, FieldUsageEventTotals, diffs[0].Field) assert.Contains(t, diffs[0].Detail, "event composition differs") }) t.Run("dedup keyed token redistribution surfaces", func(t *testing.T) { // Equal event count, equal aggregate totals, and stable // dedup keys, but the per-event token payload changed. // --fail-on-change must not pass over that data drift. stored := []db.UsageEvent{ pdEvent("api", "m1", 10, 2, "2026-01-01T00:00:00Z", "k1", nil), pdEvent("api", "m1", 20, 4, "2026-01-01T00:01:00Z", "k2", nil), } stored[0].CacheCreationInputTokens = 1 stored[1].CacheCreationInputTokens = 3 stored[0].CacheReadInputTokens = 5 stored[1].CacheReadInputTokens = 7 stored[0].ReasoningTokens = 11 stored[1].ReasoningTokens = 13 parsed := []db.UsageEvent{ pdEvent("api", "m1", 20, 4, "2026-01-01T00:00:00Z", "k1", nil), pdEvent("api", "m1", 10, 2, "2026-01-01T00:01:00Z", "k2", nil), } parsed[0].CacheCreationInputTokens = 3 parsed[1].CacheCreationInputTokens = 1 parsed[0].CacheReadInputTokens = 7 parsed[1].CacheReadInputTokens = 5 parsed[0].ReasoningTokens = 13 parsed[1].ReasoningTokens = 11 diffs := compareUsageEvents(stored, parsed) require.Len(t, diffs, 1) assert.Equal(t, FieldUsageEventTotals, diffs[0].Field) assert.Equal(t, sumUsageTokenTotals(stored).render(), diffs[0].Stored) assert.Equal(t, sumUsageTokenTotals(parsed).render(), diffs[0].Parsed) assert.Contains(t, diffs[0].Detail, "event composition differs") assert.Contains(t, diffs[0].Detail, "dedup|k1") }) t.Run("tuple fallback detects occurred_at drift", func(t *testing.T) { stored := []db.UsageEvent{ pdEvent("api", "m1", 10, 2, "2026-01-01T00:00:00Z", "", nil), } parsed := []db.UsageEvent{ pdEvent("api", "m1", 10, 2, "2026-01-01T09:09:09Z", "", nil), } diffs := compareUsageEvents(stored, parsed) require.Len(t, diffs, 1) assert.Equal(t, FieldUsageEventTotals, diffs[0].Field) assert.Contains(t, diffs[0].Detail, "event composition differs") }) t.Run("dedup key drift in totals still reports totals", func(t *testing.T) { // Same dedup key but token drift: the multiset is equal by // key, yet the per-class sums must still be compared. stored := []db.UsageEvent{ pdEvent("api", "m1", 10, 2, "2026-01-01T00:00:00Z", "k1", nil), } parsed := []db.UsageEvent{ pdEvent("api", "m1", 99, 2, "2026-01-01T00:00:00Z", "k1", nil), } diffs := compareUsageEvents(stored, parsed) require.Len(t, diffs, 1) assert.Equal(t, FieldUsageEventTotals, diffs[0].Field) assert.Equal(t, "input 10 -> 99", diffs[0].Detail) }) t.Run("cost columns are ignored", func(t *testing.T) { cost := 0.42 stored := []db.UsageEvent{ pdEvent("api", "m1", 10, 2, "2026-01-01T00:00:00Z", "", nil), } stored[0].CostUSD = &cost stored[0].CostStatus = "final" stored[0].CostSource = "pricing" parsed := []db.UsageEvent{ pdEvent("api", "m1", 10, 2, "2026-01-01T00:00:00Z", "", nil), } assert.Empty(t, compareUsageEvents(stored, parsed)) }) } // TestFingerprintTwinMatchesDB pins the in-memory fingerprint twin // against db.MessageTokenFingerprint for messages written through the // real bulk sync pipeline. Twin drift silently breaks the tier-1 fast // path, so this parity is the design's top risk. func TestFingerprintTwinMatchesDB(t *testing.T) { d := openTestDB(t) e := NewEngine(d, EngineConfig{Machine: "test-machine"}) ts := time.Date(2026, 6, 1, 10, 0, 0, 0, time.UTC) pw := pendingWrite{ sess: parser.ParsedSession{ ID: "pd-twin-session", Project: "proj", Machine: "test-machine", Agent: parser.AgentClaude, FirstMessage: "hello", StartedAt: ts, EndedAt: ts.Add(time.Minute), MessageCount: 3, File: parser.FileInfo{ Path: "/tmp/pd-twin.jsonl", Size: 100, Mtime: ts.UnixNano(), }, }, msgs: []parser.ParsedMessage{ { Ordinal: 0, Role: parser.RoleUser, Content: "hello", ContentLength: 5, Timestamp: ts, SourceType: "user", SourceUUID: "uuid-0", }, { Ordinal: 1, Role: parser.RoleAssistant, Content: "hi with NUL \x00 and unicode ünï", ContentLength: 20, Timestamp: ts.Add(time.Second), Model: "claude-op\x00us-4", TokenUsage: json.RawMessage( `{"input_tokens":10,"output_tokens":2}`, ), ContextTokens: 12, OutputTokens: 2, HasContextTokens: true, HasOutputTokens: true, ClaudeMessageID: "msg_01", ClaudeRequestID: "req_01", SourceType: "assistant", SourceUUID: "uuid-1", SourceParentUUID: "uuid-0", }, { Ordinal: 2, Role: parser.RoleAssistant, Content: "sidechain boundary", ContentLength: 18, Timestamp: ts.Add(2 * time.Second), Model: "claude-haiku", IsSidechain: true, IsCompactBoundary: true, }, }, } written, _, failed, _ := e.writeBatch( []pendingWrite{pw}, syncWriteBulk, false, ) require.Equal(t, 1, written, "session must be written") require.Zero(t, failed) prepared, msgs, verdict := e.prepareSessionWrite(pw, nil) require.Equal(t, sessionWriteOK, verdict) require.NotEmpty(t, msgs) storedFP, err := d.MessageTokenFingerprint(prepared.ID) require.NoError(t, err) require.NotEmpty(t, storedFP) assert.Equal( t, storedFP, messageTokenFingerprintTwin(msgs), "in-memory twin must match db.MessageTokenFingerprint exactly", ) storedRoleTimeFP, err := d.MessageRoleTimeFingerprint(prepared.ID) require.NoError(t, err) require.NotEmpty(t, storedRoleTimeFP) assert.Equal( t, storedRoleTimeFP, messageRoleTimeFingerprintTwin(msgs), "in-memory twin must match db.MessageRoleTimeFingerprint exactly", ) storedContentFP, err := d.MessageContentHashFingerprint(prepared.ID) require.NoError(t, err) require.NotEmpty(t, storedContentFP) assert.Equal( t, storedContentFP, messageContentHashFingerprintTwin(msgs), "in-memory twin must match db.MessageContentHashFingerprint exactly", ) } // TestCompareStoredSessionRoundTrip proves that a session written // through the real pipeline compares as identical against itself, // covering the session row, message metadata, and usage events. func TestCompareStoredSessionRoundTrip(t *testing.T) { d := openTestDB(t) e := NewEngine(d, EngineConfig{Machine: "test-machine"}) ts := time.Date(2026, 6, 1, 10, 0, 0, 0, time.UTC) pw := pendingWrite{ sess: parser.ParsedSession{ ID: "pd-roundtrip", Project: "proj", Machine: "test-machine", Agent: parser.AgentClaude, FirstMessage: "round trip", SessionName: "Round Trip", StartedAt: ts, EndedAt: ts.Add(time.Minute), MessageCount: 2, UserMessageCount: 1, TerminationStatus: parser.TerminationClean, File: parser.FileInfo{ Path: "/tmp/pd-roundtrip.jsonl", Size: 64, Mtime: ts.UnixNano(), }, }, msgs: []parser.ParsedMessage{ { Ordinal: 0, Role: parser.RoleUser, Content: "round trip", ContentLength: 10, Timestamp: ts, }, { Ordinal: 1, Role: parser.RoleAssistant, Content: "ack", ContentLength: 3, Timestamp: ts.Add(time.Second), Model: "claude-sonnet", TokenUsage: json.RawMessage( `{"input_tokens":7,"output_tokens":3}`, ), ContextTokens: 10, OutputTokens: 3, HasContextTokens: true, HasOutputTokens: true, }, }, usageEvents: []parser.ParsedUsageEvent{ { MessageOrdinal: new(1), Source: "transcript", Model: "claude-sonnet", InputTokens: 7, OutputTokens: 3, OccurredAt: "2026-06-01T10:00:01Z", DedupKey: "msg_rt:req_rt", }, }, } written, _, failed, _ := e.writeBatch( []pendingWrite{pw}, syncWriteBulk, false, ) require.Equal(t, 1, written) require.Zero(t, failed) prepared, msgs, verdict := e.prepareSessionWrite(pw, nil) require.Equal(t, sessionWriteOK, verdict) events, _ := toDBUsageEvents(prepared.ID, pw.usageEvents) stored := pdFetchStored(t, d, prepared.ID) diffs, err := e.compareStoredSession( context.Background(), stored, prepared, msgs, events, ) require.NoError(t, err) assert.Empty(t, diffs, "self-written session must be identical") } // TestCompareStoredSessionDetectsDrift mutates one stored column and // expects the comparator to attribute it through the database path. func TestCompareStoredSessionDetectsDrift(t *testing.T) { d := openTestDB(t) e := NewEngine(d, EngineConfig{Machine: "test-machine"}) ts := time.Date(2026, 6, 1, 10, 0, 0, 0, time.UTC) pw := pendingWrite{ sess: parser.ParsedSession{ ID: "pd-drift", Project: "proj", Machine: "test-machine", Agent: parser.AgentClaude, FirstMessage: "drift", StartedAt: ts, MessageCount: 1, File: parser.FileInfo{ Path: "/tmp/pd-drift.jsonl", Size: 32, Mtime: ts.UnixNano(), }, }, msgs: []parser.ParsedMessage{ { Ordinal: 0, Role: parser.RoleAssistant, Content: "drift", ContentLength: 5, Timestamp: ts, Model: "claude-sonnet", OutputTokens: 3, }, }, } written, _, failed, _ := e.writeBatch( []pendingWrite{pw}, syncWriteBulk, false, ) require.Equal(t, 1, written) require.Zero(t, failed) // Simulate parser drift: the new parse reports a different model. pw.msgs[0].Model = "claude-haiku" prepared, msgs, verdict := e.prepareSessionWrite(pw, nil) require.Equal(t, sessionWriteOK, verdict) stored := pdFetchStored(t, d, prepared.ID) diffs, err := e.compareStoredSession( context.Background(), stored, prepared, msgs, nil, ) require.NoError(t, err) require.Len(t, diffs, 1) assert.Equal(t, FieldModels, diffs[0].Field) assert.Equal(t, "claude-sonnet", diffs[0].Stored) assert.Equal(t, "claude-haiku", diffs[0].Parsed) assert.Contains(t, diffs[0].Detail, "first at ordinal 0") } // pdWriteSingleMessageSession writes a one-message session through the // real pipeline and returns the engine plus DB for re-parse comparison. func pdWriteSingleMessageSession( t *testing.T, id string, msg parser.ParsedMessage, ) (*Engine, *db.DB, pendingWrite) { t.Helper() d := openTestDB(t) e := NewEngine(d, EngineConfig{Machine: "test-machine"}) ts := time.Date(2026, 6, 1, 10, 0, 0, 0, time.UTC) pw := pendingWrite{ sess: parser.ParsedSession{ ID: id, Project: "proj", Machine: "test-machine", Agent: parser.AgentClaude, FirstMessage: "x", StartedAt: ts, MessageCount: 1, File: parser.FileInfo{ Path: "/tmp/" + id + ".jsonl", Size: 32, Mtime: ts.UnixNano(), }, }, msgs: []parser.ParsedMessage{msg}, } written, _, failed, _ := e.writeBatch( []pendingWrite{pw}, syncWriteBulk, false, ) require.Equal(t, 1, written) require.Zero(t, failed) return e, d, pw } // TestCompareStoredSessionDetectsContentDrift proves the content tier // catches a message-body change the token fingerprint cannot see: only // the content length moves, model and tokens are unchanged. func TestCompareStoredSessionDetectsContentDrift(t *testing.T) { ts := time.Date(2026, 6, 1, 10, 0, 0, 0, time.UTC) e, d, pw := pdWriteSingleMessageSession(t, "pd-content", parser.ParsedMessage{ Ordinal: 0, Role: parser.RoleAssistant, Content: "short", ContentLength: 5, Timestamp: ts, Model: "claude-sonnet", }) // New parse: same model and tokens, longer body. pw.msgs[0].Content = "a much longer reply body" pw.msgs[0].ContentLength = len(pw.msgs[0].Content) prepared, msgs, verdict := e.prepareSessionWrite(pw, nil) require.Equal(t, sessionWriteOK, verdict) stored := pdFetchStored(t, d, prepared.ID) diffs, err := e.compareStoredSession( context.Background(), stored, prepared, msgs, nil, ) require.NoError(t, err) require.Len(t, diffs, 1) assert.Equal(t, FieldMessageContent, diffs[0].Field) assert.Contains(t, diffs[0].Detail, "first at ordinal 0") } // TestCompareStoredSessionDetectsMetadataDrift proves a fingerprint // mismatch confined to a non-model/token field (is_sidechain) is // surfaced as message_metadata rather than silently reported identical. func TestCompareStoredSessionDetectsMetadataDrift(t *testing.T) { ts := time.Date(2026, 6, 1, 10, 0, 0, 0, time.UTC) e, d, pw := pdWriteSingleMessageSession(t, "pd-meta", parser.ParsedMessage{ Ordinal: 0, Role: parser.RoleAssistant, Content: "body", ContentLength: 4, Timestamp: ts, Model: "claude-sonnet", IsSidechain: false, }) // New parse flips only is_sidechain: same model, tokens, content. pw.msgs[0].IsSidechain = true prepared, msgs, verdict := e.prepareSessionWrite(pw, nil) require.Equal(t, sessionWriteOK, verdict) stored := pdFetchStored(t, d, prepared.ID) diffs, err := e.compareStoredSession( context.Background(), stored, prepared, msgs, nil, ) require.NoError(t, err) require.Len(t, diffs, 1) assert.Equal(t, FieldMessageMetadata, diffs[0].Field) assert.Contains(t, diffs[0].Detail, "is_sidechain") } // pdToolSession builds a Claude pendingWrite that exercises the // flags and tool-call comparison paths: a thinking block, two tool calls // (one with a paired result), and a system message. func pdToolSession(id string) pendingWrite { ts := time.Date(2026, 6, 1, 10, 0, 0, 0, time.UTC) return pendingWrite{ sess: parser.ParsedSession{ ID: id, Project: "proj", Machine: "test-machine", Agent: parser.AgentClaude, FirstMessage: "do a thing", StartedAt: ts, MessageCount: 3, File: parser.FileInfo{ Path: "/tmp/" + id + ".jsonl", Size: 128, Mtime: ts.UnixNano(), }, }, msgs: []parser.ParsedMessage{ { Ordinal: 0, Role: parser.RoleUser, Content: "do a thing", ContentLength: 10, Timestamp: ts, }, { Ordinal: 1, Role: parser.RoleAssistant, Content: "working on it", ContentLength: 13, Timestamp: ts.Add(time.Second), Model: "claude-sonnet", HasThinking: true, ThinkingText: "let me reason about this", HasToolUse: true, ToolCalls: []parser.ParsedToolCall{ { ToolUseID: "tu1", ToolName: "Read", Category: "Read", InputJSON: `{"file":"a.go"}`, }, { ToolUseID: "tu2", ToolName: "Bash", Category: "Bash", InputJSON: `{"cmd":"ls"}`, }, }, }, { Ordinal: 2, Role: parser.RoleUser, Content: "looks good", ContentLength: 10, Timestamp: ts.Add(2 * time.Second), IsSystem: true, ToolResults: []parser.ParsedToolResult{ { ToolUseID: "tu1", ContentLength: 18, ContentRaw: `"file contents x"`, }, }, }, }, } } // pdWriteToolSession writes pdToolSession through the real pipeline. func pdWriteToolSession( t *testing.T, id string, ) (*Engine, *db.DB, pendingWrite) { t.Helper() d := openTestDB(t) e := NewEngine(d, EngineConfig{Machine: "test-machine"}) pw := pdToolSession(id) written, _, failed, _ := e.writeBatch( []pendingWrite{pw}, syncWriteBulk, false, ) require.Equal(t, 1, written) require.Zero(t, failed) return e, d, pw } // TestToolCallAndFlagsFingerprintTwinsMatchDB pins the two new in-memory // twins against their DB queries through the real write pipeline, the // way TestFingerprintTwinMatchesDB does for the message fingerprints. func TestToolCallAndFlagsFingerprintTwinsMatchDB(t *testing.T) { e, d, pw := pdWriteToolSession(t, "pd-tool-twin") prepared, msgs, verdict := e.prepareSessionWrite(pw, nil) require.Equal(t, sessionWriteOK, verdict) storedFlagsFP, err := d.MessageFlagsFingerprint(prepared.ID) require.NoError(t, err) require.NotEmpty(t, storedFlagsFP) assert.Equal(t, storedFlagsFP, messageFlagsFingerprintTwin(msgs), "flags twin must match db.MessageFlagsFingerprint exactly") storedToolFP, err := d.ToolCallParseDiffFingerprint(prepared.ID) require.NoError(t, err) require.NotEmpty(t, storedToolFP) assert.Equal(t, storedToolFP, toolCallParseDiffFingerprintTwin(msgs), "tool-call twin must match db.ToolCallParseDiffFingerprint exactly") } func TestToolCallDiffDetectsFilePath(t *testing.T) { base := db.ToolCall{ ToolName: "Edit", Category: "Edit", ToolUseID: "t1", InputJSON: `{"x":1}`, FilePath: "a.go", } assert.Empty(t, toolCallDiff(base, base), "identical calls do not diff") moved := base moved.FilePath = "b.go" // A file_path-only parser change must be detected so the resync rewrites // the row and the mirrors pick up the corrected path. assert.Contains(t, toolCallDiff(base, moved), "file_path") } // TestCompareStoredSessionRoundTripToolCalls is the false-diff acid test // for the new fields: a session with tool calls, a thinking block, and a // system message must compare identical against itself. func TestCompareStoredSessionRoundTripToolCalls(t *testing.T) { e, d, pw := pdWriteToolSession(t, "pd-tool-rt") prepared, msgs, verdict := e.prepareSessionWrite(pw, nil) require.Equal(t, sessionWriteOK, verdict) stored := pdFetchStored(t, d, prepared.ID) diffs, err := e.compareStoredSession( context.Background(), stored, prepared, msgs, nil, ) require.NoError(t, err) assert.Empty(t, diffs, "tool calls, thinking, and a system message must round-trip clean") } // TestCompareStoredSessionDetectsToolCallDrift proves a change confined // to a tool_call column is caught: none of the message // token/role/content/flags fingerprints move, so it surfaces only if the // tool-call fingerprint triggers the tier-2 comparison. func TestCompareStoredSessionDetectsToolCallDrift(t *testing.T) { e, d, pw := pdWriteToolSession(t, "pd-tool-drift") pw.msgs[1].ToolCalls[0].ToolName = "Grep" prepared, msgs, verdict := e.prepareSessionWrite(pw, nil) require.Equal(t, sessionWriteOK, verdict) stored := pdFetchStored(t, d, prepared.ID) diffs, err := e.compareStoredSession( context.Background(), stored, prepared, msgs, nil, ) require.NoError(t, err) require.Len(t, diffs, 1) assert.Equal(t, FieldToolCalls, diffs[0].Field) assert.Contains(t, diffs[0].Detail, `tool_name "Read" -> "Grep"`) } // TestCompareStoredSessionDetectsFlagDrift proves a change confined to a // per-message flag (has_thinking) is caught only via the flags // fingerprint triggering the tier-2 comparison. func TestCompareStoredSessionDetectsFlagDrift(t *testing.T) { e, d, pw := pdWriteToolSession(t, "pd-flag-drift") pw.msgs[1].HasThinking = false prepared, msgs, verdict := e.prepareSessionWrite(pw, nil) require.Equal(t, sessionWriteOK, verdict) stored := pdFetchStored(t, d, prepared.ID) diffs, err := e.compareStoredSession( context.Background(), stored, prepared, msgs, nil, ) require.NoError(t, err) require.Len(t, diffs, 1) assert.Equal(t, FieldMessageMetadata, diffs[0].Field) assert.Contains(t, diffs[0].Detail, "has_thinking true -> false") } func pdFetchStored(t *testing.T, d *db.DB, id string) *db.Session { t.Helper() sessions, err := d.ListSessionsModifiedBetween( context.Background(), "", "", nil, nil, ) require.NoError(t, err) for i := range sessions { if sessions[i].ID == id { return &sessions[i] } } require.Failf(t, "session not found", "id %s", id) return nil } func TestParseDiffClassifyPrecedence(t *testing.T) { tests := []struct { name string needsRetry bool prepared bool hasStored bool storedTrashed bool pendingResync bool realDiffs int raced bool incrementalSkew bool wantClass DiffClass wantReason string }{ { name: "needs retry wins over everything", needsRetry: true, prepared: false, hasStored: true, storedTrashed: true, pendingResync: true, realDiffs: 3, raced: true, incrementalSkew: true, wantClass: DiffNeedsRetry, wantReason: "transient low-fidelity parse; differences expected", }, { name: "archive-preserve veto wins over missing stored row", prepared: false, hasStored: false, wantClass: DiffExcluded, wantReason: "archive-preserved", }, { name: "no stored row is new on disk", prepared: true, hasStored: false, wantClass: DiffNewOnDisk, }, { name: "trashed stored row is skipped, not excluded", prepared: true, hasStored: true, storedTrashed: true, pendingResync: true, realDiffs: 2, wantClass: DiffSkipped, wantReason: "trashed in archive", }, { name: "pending resync wins over changed", prepared: true, hasStored: true, pendingResync: true, realDiffs: 2, wantClass: DiffPendingResync, }, { name: "pending resync wins over raced", prepared: true, hasStored: true, pendingResync: true, realDiffs: 2, raced: true, wantClass: DiffPendingResync, }, { name: "pending resync wins over incremental skew", prepared: true, hasStored: true, pendingResync: true, realDiffs: 2, incrementalSkew: true, wantClass: DiffPendingResync, }, { name: "raced wins over changed when source moved", prepared: true, hasStored: true, realDiffs: 1, raced: true, wantClass: DiffRaced, wantReason: "source file changed after snapshot (live-write skew)", }, { name: "raced wins over incremental skew when both apply", prepared: true, hasStored: true, realDiffs: 1, raced: true, incrementalSkew: true, wantClass: DiffRaced, wantReason: "source file changed after snapshot (live-write skew)", }, { name: "incremental skew wins over changed", prepared: true, hasStored: true, realDiffs: 1, incrementalSkew: true, wantClass: DiffIncrementalSkew, wantReason: "stored row last written incrementally " + "(incremental-append skew)", }, { name: "real diffs mean changed", prepared: true, hasStored: true, realDiffs: 1, wantClass: DiffChanged, }, { name: "raced flag is inert without a real diff", prepared: true, hasStored: true, realDiffs: 0, raced: true, wantClass: DiffIdentical, }, { name: "incremental skew flag is inert without a real diff", prepared: true, hasStored: true, realDiffs: 0, incrementalSkew: true, wantClass: DiffIdentical, }, { name: "no real diffs mean identical", prepared: true, hasStored: true, realDiffs: 0, wantClass: DiffIdentical, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { class, reason := classifyParseDiffSession( tt.needsRetry, tt.prepared, tt.hasStored, tt.storedTrashed, tt.pendingResync, tt.realDiffs, tt.raced, tt.incrementalSkew, ) assert.Equal(t, tt.wantClass, class) assert.Equal(t, tt.wantReason, reason) }) } } func TestDiffsConfinedToIncrementalArtifacts(t *testing.T) { tests := []struct { name string fields []FieldDiff want bool }{ { name: "message_metadata alone is an incremental artifact", fields: []FieldDiff{ {Field: FieldMessageMetadata}, }, want: true, }, { name: "informational session fields are ignored", fields: []FieldDiff{ {Field: FieldTerminationStatus, Informational: true}, {Field: FieldSessionName, Informational: true}, {Field: FieldMessageMetadata}, }, want: true, }, { name: "no diffs are vacuously confined", fields: nil, want: true, }, { name: "first_message drift is not an artifact", fields: []FieldDiff{ {Field: FieldFirstMessage}, }, want: false, }, { name: "message_content drift is not an artifact", fields: []FieldDiff{ {Field: FieldMessageContent}, }, want: false, }, { name: "usage totals drift is not an artifact", fields: []FieldDiff{ {Field: FieldTotalOutputTokens}, }, want: false, }, { name: "an artifact mixed with a non-artifact is not confined", fields: []FieldDiff{ {Field: FieldMessageMetadata}, {Field: FieldFirstMessage}, }, want: false, }, { name: "tool_calls drift is not an artifact", fields: []FieldDiff{ {Field: FieldToolCalls}, }, want: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { assert.Equal(t, tt.want, diffsConfinedToIncrementalArtifacts(tt.fields)) }) } } // TestParseDiffSourceRaced pins the conservative mtime-skew verdict: a // source that moved past the snapshot mtime (or whose mtime cannot be // resolved) is raced; one that is demonstrably at or before the snapshot // is not, so a genuine change there is never masked. func TestParseDiffSourceRaced(t *testing.T) { mtime := func(v int64) *int64 { return &v } tests := []struct { name string storedMtime *int64 liveMtime int64 liveOK bool want bool }{ { name: "live mtime advanced past snapshot is raced", storedMtime: mtime(1000), liveMtime: 2000, liveOK: true, want: true, }, { name: "live mtime equal to snapshot is not raced", storedMtime: mtime(1000), liveMtime: 1000, liveOK: true, want: false, }, { name: "live mtime before snapshot is not raced", storedMtime: mtime(2000), liveMtime: 1000, liveOK: true, want: false, }, { name: "one nanosecond advance is raced (no truncation)", storedMtime: mtime(1000), liveMtime: 1001, liveOK: true, want: true, }, { name: "unreadable source is conservatively raced", storedMtime: mtime(1000), liveMtime: 0, liveOK: false, want: true, }, { name: "missing stored mtime is conservatively raced", storedMtime: nil, liveMtime: 1000, liveOK: true, want: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := parseDiffSourceRaced( tt.storedMtime, tt.liveMtime, tt.liveOK, ) assert.Equal(t, tt.want, got) }) } } // TestParseDiffLiveMtimeIgnoresCodexIndex pins that Codex's raced guard uses // the transcript mtime, not the global session_index.jsonl mtime. The index is // shared by every Codex session, so an unrelated title/index write is not a // per-session signal that transcript-derived diffs raced with live content. func TestParseDiffLiveMtimeIgnoresCodexIndex(t *testing.T) { root := t.TempDir() sessionsDir := filepath.Join(root, "sessions", "2024", "01", "01") require.NoError(t, os.MkdirAll(sessionsDir, 0o755)) rollout := filepath.Join(sessionsDir, "rollout-x.jsonl") require.NoError(t, os.WriteFile(rollout, []byte("{}\n"), 0o644)) indexPath := filepath.Join(root, "session_index.jsonl") require.NoError(t, os.WriteFile(indexPath, []byte("{}\n"), 0o644)) // Index not newer than the rollout: raced mtime is the rollout's. base := time.Now().Add(-2 * time.Hour) require.NoError(t, os.Chtimes(rollout, base, base), "chtimes rollout") require.NoError(t, os.Chtimes(indexPath, base, base), "chtimes index") m1, err := parseDiffLiveMtime(parser.AgentCodex, rollout) require.NoError(t, err) rollInfo, err := os.Stat(rollout) require.NoError(t, err) assert.Equal(t, rollInfo.ModTime().UnixNano(), m1, "codex raced mtime is the rollout's when the index is not newer") // A sibling write advances the global index past the rollout AFTER the // first resolution; the Codex raced resolver must keep reporting the // transcript mtime. future := time.Now().Add(time.Hour) require.NoError(t, os.Chtimes(indexPath, future, future), "advance index") m2, err := parseDiffLiveMtime(parser.AgentCodex, rollout) require.NoError(t, err) assert.Equal(t, rollInfo.ModTime().UnixNano(), m2, "codex raced mtime ignores the advanced session_index.jsonl") assert.Equal(t, m1, m2, "the global index write is not observed") } func TestParseDiffCodexTranscriptChangedRecomputesConsumedSize(t *testing.T) { path := filepath.Join(t.TempDir(), "rollout-x.jsonl") initial := "{}\n" require.NoError(t, os.WriteFile(path, []byte(initial), 0o644)) storedSize := int64(len(initial)) stored := &db.Session{FileSize: &storedSize} parsed := parser.ParsedSession{ Agent: parser.AgentCodex, File: parser.FileInfo{ Path: path, Size: storedSize, }, } require.NoError(t, os.WriteFile( path, []byte(initial+`{"appended":true}`+"\n"), 0o644, )) assert.True(t, parseDiffCodexTranscriptChangedSinceStored(stored, parsed), "collect-time Codex race check must not trust the parser's stale size") } // TestParseDiffSourceReliableForRaced pins the reliability gate that decides // whether the live-write skew (raced) reclassification may run for a session. // Only plain file-based agents reading a literal on-disk file have a live // mtime that is basis-matching with the stored file_mtime; every virtual-path // or DB-backed source must be treated as unreliable so the raced guard is // skipped and genuine parser drift is never masked. func TestParseDiffSourceReliableForRaced(t *testing.T) { // Each virtual-path constructor is paired with a basename its parser // accepts; stripVirtualSourceSuffix only recognizes the real shapes. kiroPath := parser.KiroSQLiteVirtualPath( "/data/data.sqlite3", "kiro_sess", ) zedPath := parser.ZedSQLiteVirtualPath("/data/threads.db", "zed_thread") shelleyPath := parser.ShelleyVirtualPath( "/data/shelley.db", "shelley_conv", ) vsCopilotPath := parser.VisualStudioCopilotVirtualPath( "/traces/20260612T194439_abc_VSGitHubCopilot_traces.jsonl", "conv_1", ) tests := []struct { name string agent parser.AgentType path string want bool }{ { name: "plain file-based literal path is reliable", agent: parser.AgentClaude, path: "/projects/proj/session.jsonl", want: true, }, { name: "another plain file-based literal path is reliable", agent: parser.AgentCodex, path: "/sessions/2026/06/rollout.jsonl", want: true, }, { name: "aider virtual run-index path is unreliable", agent: parser.AgentAider, path: parser.AiderVirtualPath("/repo/.aider.chat.history.md", 3), want: false, }, { name: "kiro shared-db virtual path is unreliable", agent: parser.AgentKiro, path: kiroPath, want: false, }, { name: "zed shared-db virtual path is unreliable", agent: parser.AgentZed, path: zedPath, want: false, }, { name: "shelley shared-db virtual path is unreliable", agent: parser.AgentShelley, path: shelleyPath, want: false, }, { name: "visual studio copilot virtual path is unreliable", agent: parser.AgentVSCopilot, path: vsCopilotPath, want: false, }, { name: "db-backed agent on a literal path is unreliable", agent: parser.AgentForge, path: "/forge/store.db", want: false, }, { name: "unknown agent is unreliable", agent: parser.AgentType("does-not-exist"), path: "/anywhere/file.jsonl", want: false, }, } engine := NewDiffEngine(dbtest.OpenTestDB(t), EngineConfig{}) for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := engine.parseDiffSourceReliableForRaced(tt.agent, tt.path) assert.Equal(t, tt.want, got) }) } } func TestParseDiffPresenceSweepKeepsMixedProviderRetryCoverage(t *testing.T) { sourcePath := "/tmp/provider-source.jsonl" filePath := sourcePath current := &db.Session{ ID: "provider-current", Agent: string(parser.AgentClaude), Machine: "devbox", Project: "provider-project", FilePath: &filePath, DataVersion: db.CurrentDataVersion(), } retry := &db.Session{ ID: "provider-retry", Agent: string(parser.AgentClaude), Machine: "devbox", Project: "provider-project", FilePath: &filePath, DataVersion: db.CurrentDataVersion(), } missing := &db.Session{ ID: "provider-missing", Agent: string(parser.AgentClaude), Machine: "devbox", Project: "provider-project", FilePath: &filePath, DataVersion: db.CurrentDataVersion(), } storedByID := map[string]*db.Session{ current.ID: current, retry.ID: retry, missing.ID: missing, } storedByPath := map[string][]*db.Session{ sourcePath: {current, retry, missing}, } job := syncJob{ path: sourcePath, processResult: processResult{ results: []parser.ParseResult{ {Session: parser.ParsedSession{ ID: current.ID, Agent: parser.AgentClaude, Machine: "devbox", Project: "provider-project", File: parser.FileInfo{ Path: sourcePath, }, }}, {Session: parser.ParsedSession{ ID: retry.ID, Agent: parser.AgentClaude, Machine: "devbox", Project: "provider-project", File: parser.FileInfo{ Path: sourcePath, }, }}, }, retrySessionIDs: map[string]bool{ retry.ID: true, }, }, } engine := &Engine{db: dbtest.OpenTestDB(t)} report := &ParseDiffReport{FieldCounts: map[string]int{}} visited := map[string]bool{} var presencePaths []string err := engine.parseDiffCollectFile( context.Background(), report, job, map[string]parser.AgentType{sourcePath: parser.AgentClaude}, storedByID, storedByPath, visited, engine.loadWorktreeProjectResolver(), &presencePaths, ) require.NoError(t, err) engine.parseDiffPresenceSweep( report, presencePaths, storedByPath, visited, ) assert.Equal(t, 1, report.Totals.NeedsRetry) assert.Equal(t, 1, report.Totals.Changed) byID := map[string]SessionDiff{} for _, session := range report.Sessions { byID[session.SessionID] = session } assert.Equal(t, DiffNeedsRetry, byID[retry.ID].Class) assert.Equal(t, DiffChanged, byID[missing.ID].Class) require.NotEmpty(t, byID[missing.ID].Fields) assert.Equal(t, FieldPresence, byID[missing.ID].Fields[0].Field) } func TestParseDiffPresenceSweepSkipsIncompleteProviderResults(t *testing.T) { sourcePath := "/tmp/incomplete-provider-source.jsonl" filePath := sourcePath missing := &db.Session{ ID: "provider-missing", Agent: string(parser.AgentClaude), Machine: "devbox", Project: "provider-project", FilePath: &filePath, DataVersion: db.CurrentDataVersion(), } storedByPath := map[string][]*db.Session{ sourcePath: {missing}, } job := syncJob{ path: sourcePath, processResult: processResult{ suppressPresenceSweep: true, }, } engine := &Engine{db: dbtest.OpenTestDB(t)} report := &ParseDiffReport{FieldCounts: map[string]int{}} visited := map[string]bool{} var presencePaths []string err := engine.parseDiffCollectFile( context.Background(), report, job, map[string]parser.AgentType{sourcePath: parser.AgentClaude}, map[string]*db.Session{missing.ID: missing}, storedByPath, visited, engine.loadWorktreeProjectResolver(), &presencePaths, ) require.NoError(t, err) engine.parseDiffPresenceSweep( report, presencePaths, storedByPath, visited, ) assert.Equal(t, 0, report.Totals.Changed) assert.Empty(t, report.Sessions) } func TestParseDiffProviderVirtualSQLiteErrorUsesExactSource(t *testing.T) { dbPath := "/tmp/opencode.db" firstPath := parser.OpenCodeSQLiteVirtualPath(dbPath, "ses_one") secondPath := parser.OpenCodeSQLiteVirtualPath(dbPath, "ses_two") first := &db.Session{ ID: "opencode:ses_one", Agent: string(parser.AgentOpenCode), Machine: "devbox", Project: "project", FilePath: &firstPath, DataVersion: db.CurrentDataVersion(), } second := &db.Session{ ID: "opencode:ses_two", Agent: string(parser.AgentOpenCode), Machine: "devbox", Project: "project", FilePath: &secondPath, DataVersion: db.CurrentDataVersion(), } storedByPath := map[string][]*db.Session{ parseDiffSourceKey(firstPath): {first}, parseDiffSourceKey(secondPath): {second}, } job := syncJob{ path: firstPath, processResult: processResult{ err: errors.New("bad virtual session"), }, } engine := &Engine{db: dbtest.OpenTestDB(t)} report := &ParseDiffReport{FieldCounts: map[string]int{}} visited := map[string]bool{} var presencePaths []string err := engine.parseDiffCollectFile( context.Background(), report, job, map[string]parser.AgentType{firstPath: parser.AgentOpenCode}, map[string]*db.Session{ first.ID: first, second.ID: second, }, storedByPath, visited, engine.loadWorktreeProjectResolver(), &presencePaths, ) require.NoError(t, err) require.Len(t, report.Sessions, 1) assert.Equal(t, first.ID, report.Sessions[0].SessionID) assert.Equal(t, DiffParseError, report.Sessions[0].Class) assert.True(t, visited[first.ID]) assert.False(t, visited[second.ID]) assert.Empty(t, presencePaths) assert.Equal(t, ParseDiffTotals{ParseErrors: 1}, report.Totals) } func TestParseDiffProviderVirtualSQLitePresenceUsesExactSource(t *testing.T) { dbPath := "/tmp/opencode.db" firstPath := parser.OpenCodeSQLiteVirtualPath(dbPath, "ses_one") secondPath := parser.OpenCodeSQLiteVirtualPath(dbPath, "ses_two") first := &db.Session{ ID: "opencode:ses_one", Agent: string(parser.AgentOpenCode), Machine: "devbox", Project: "project", FilePath: &firstPath, DataVersion: db.CurrentDataVersion(), } second := &db.Session{ ID: "opencode:ses_two", Agent: string(parser.AgentOpenCode), Machine: "devbox", Project: "project", FilePath: &secondPath, DataVersion: db.CurrentDataVersion(), } storedByPath := map[string][]*db.Session{ parseDiffSourceKey(firstPath): {first}, parseDiffSourceKey(secondPath): {second}, } job := syncJob{path: firstPath} engine := &Engine{db: dbtest.OpenTestDB(t)} report := &ParseDiffReport{FieldCounts: map[string]int{}} visited := map[string]bool{} var presencePaths []string err := engine.parseDiffCollectFile( context.Background(), report, job, map[string]parser.AgentType{firstPath: parser.AgentOpenCode}, map[string]*db.Session{ first.ID: first, second.ID: second, }, storedByPath, visited, engine.loadWorktreeProjectResolver(), &presencePaths, ) require.NoError(t, err) engine.parseDiffPresenceSweep( report, presencePaths, storedByPath, visited, ) require.Len(t, report.Sessions, 1) assert.Equal(t, first.ID, report.Sessions[0].SessionID) assert.Equal(t, DiffChanged, report.Sessions[0].Class) assert.True(t, visited[first.ID]) assert.False(t, visited[second.ID]) assert.Equal(t, ParseDiffTotals{Changed: 1}, report.Totals) } func TestParseDiffProviderVirtualSQLiteLimitUsesExactSource(t *testing.T) { dbPath := "/tmp/opencode.db" firstPath := parser.OpenCodeSQLiteVirtualPath(dbPath, "ses_one") secondPath := parser.OpenCodeSQLiteVirtualPath(dbPath, "ses_two") _, cutPaths, limited := sortAndLimitParseDiffFiles( []parser.DiscoveredFile{ {Path: firstPath, Agent: parser.AgentOpenCode}, {Path: secondPath, Agent: parser.AgentOpenCode}, }, 1, ) require.True(t, limited) assert.Len(t, cutPaths, 1) assert.False(t, cutPaths[dbPath]) for path := range cutPaths { assert.True(t, path == firstPath || path == secondPath, "cut path %q must be one exact virtual source", path, ) } } func TestParseDiffDevinVirtualSQLiteLimitUsesExactSource(t *testing.T) { dbPath := filepath.Join("/tmp", "devin", "cli", "sessions.db") firstPath := parser.VirtualSourcePath(dbPath, "ses_one") secondPath := parser.VirtualSourcePath(dbPath, "ses_two") _, cutPaths, limited := sortAndLimitParseDiffFiles( []parser.DiscoveredFile{ {Path: firstPath, Agent: parser.AgentDevin}, {Path: secondPath, Agent: parser.AgentDevin}, }, 1, ) require.True(t, limited) assert.Len(t, cutPaths, 1) assert.False(t, cutPaths[dbPath]) for path := range cutPaths { assert.True(t, path == firstPath || path == secondPath, "cut path %q must be one exact Devin virtual source", path, ) } } // TestParseDiffDBBackedLimitOrdersByDiscoveryMtime pins the ordering fix at the // sorter boundary: two DB-backed virtual sources whose paths sort in the // opposite order to their per-session mtimes. Before the fix both stat to 0 and // --limit keeps the lexicographically-earlier (older) path, cutting the newer // session; the sorter must instead honor SourceRef.DiscoveryMTimeNS and keep the // newer one. func TestParseDiffDBBackedLimitOrdersByDiscoveryMtime(t *testing.T) { dbPath := "/tmp/.forge.db" // "a-older" sorts before "z-newer" as a path but carries the older mtime. olderPath := dbPath + "#a-older" newerPath := dbPath + "#z-newer" older := parser.SourceRef{Provider: parser.AgentForge, DiscoveryMTimeNS: 100} newer := parser.SourceRef{Provider: parser.AgentForge, DiscoveryMTimeNS: 200} kept, cutPaths, limited := sortAndLimitParseDiffFiles( []parser.DiscoveredFile{ {Path: olderPath, Agent: parser.AgentForge, ProviderSource: &older}, {Path: newerPath, Agent: parser.AgentForge, ProviderSource: &newer}, }, 1, ) require.True(t, limited) require.Len(t, kept, 1) assert.Equal(t, newerPath, kept[0].Path, "newer per-session mtime must be sampled even though its path sorts later") assert.Len(t, cutPaths, 1) assert.True(t, cutPaths[olderPath], "the older session must be the one cut by --limit") } func TestParseDiffReportHasFailures(t *testing.T) { tests := []struct { name string totals ParseDiffTotals want bool }{ {name: "empty report has no failures"}, { name: "identical only", totals: ParseDiffTotals{Identical: 10, Examined: 10}, }, { name: "changed sessions fail", totals: ParseDiffTotals{Changed: 1}, want: true, }, { name: "parse errors fail", totals: ParseDiffTotals{ParseErrors: 1}, want: true, }, { name: "both fail", totals: ParseDiffTotals{Changed: 2, ParseErrors: 3}, want: true, }, { name: "pending resync, skipped, retry, new do not fail", totals: ParseDiffTotals{ PendingResync: 4, Skipped: 9, NeedsRetry: 2, NewOnDisk: 3, ExcludedByParser: 1, InformationalOnly: 5, }, }, { name: "raced sessions alone do not fail", totals: ParseDiffTotals{Examined: 3, Identical: 2, Raced: 1}, }, { name: "incremental-skew sessions alone do not fail", totals: ParseDiffTotals{ Examined: 3, Identical: 2, IncrementalSkew: 1, }, }, { name: "a real change still fails alongside raced sessions", totals: ParseDiffTotals{Changed: 1, Raced: 2}, want: true, }, { name: "a real change still fails alongside incremental-skew", totals: ParseDiffTotals{Changed: 1, IncrementalSkew: 2}, want: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &ParseDiffReport{Totals: tt.totals} assert.Equal(t, tt.want, r.HasFailures()) }) } } // TestStripVirtualSourceSuffixVisualStudioCopilot verifies that a Visual Studio // Copilot # virtual path strips to its physical // trace, so parse-diff limit accounting and source classification key on the // on-disk file rather than the conversation-scoped virtual path. func TestStripVirtualSourceSuffixVisualStudioCopilot(t *testing.T) { tracePath := "/logs/20260612T194439_257709a3_VSGitHubCopilot_traces.jsonl" virtual := parser.VisualStudioCopilotVirtualPath( tracePath, "4a8f63f6-7626-4416-a874-fc7bd2c3f005", ) assert.Equal(t, tracePath, stripVirtualSourceSuffix(virtual), "the conversation suffix must strip to the physical trace path") }