f99010fae1
CI / lint (push) Failing after 1s
CI / frontend (push) Failing after 1s
CI / scripts (push) Failing after 1s
CI / Go Test (ubuntu-latest) (push) Failing after 0s
CI / frontend-node-25 (push) Failing after 1s
CI / docs (push) Failing after 0s
CI / coverage (push) Failing after 0s
CI / e2e (push) Failing after 0s
Docker / build-and-push (push) Failing after 1s
CI / integration (push) Failing after 4m43s
CI / Go Test (windows-latest) (push) Has been cancelled
CI / Desktop Unit Tests (Windows) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Linux (arm64)) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Linux) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Windows) (push) Has been cancelled
Desktop Artifacts (macOS) / Desktop Build (macOS (aarch64)) (push) Has been cancelled
Desktop Artifacts (macOS) / Desktop Build (macOS (x86_64)) (push) Has been cancelled
2305 lines
67 KiB
Go
2305 lines
67 KiB
Go
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 <traceFile>#<conversationID> 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")
|
|
}
|