Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 12:30:36 +08:00

5893 lines
184 KiB
Go

// ABOUTME: Tests for sync engine helper functions.
// ABOUTME: Covers pairToolResults and related conversion logic.
package sync
import (
"context"
"database/sql"
"encoding/json"
"errors"
"os"
"path/filepath"
"runtime"
"strings"
gosync "sync"
"sync/atomic"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"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/export"
"go.kenn.io/agentsview/internal/parser"
"go.kenn.io/agentsview/internal/testjsonl"
)
func openTestDB(t *testing.T) *db.DB {
t.Helper()
return dbtest.OpenTestDB(t)
}
func TestStartupMaintenanceWaitsForForegroundSyncAndSerializesLaterSyncs(
t *testing.T,
) {
database := openTestDB(t)
engine := NewEngine(database, EngineConfig{
Machine: "local",
DeferStartupMaintenance: true,
})
t.Cleanup(engine.Close)
ctx, cancel := context.WithCancel(t.Context())
t.Cleanup(cancel)
closed := func(ch <-chan struct{}) bool {
select {
case <-ch:
return true
default:
return false
}
}
maintenanceStarted := make(chan struct{})
releaseMaintenance := make(chan struct{})
maintenanceDone := make(chan error, 1)
go func() {
maintenanceDone <- engine.RunStartupMaintenance(ctx, func() error {
close(maintenanceStarted)
<-releaseMaintenance
return nil
})
}()
assert.Never(t, func() bool {
return closed(maintenanceStarted)
}, 100*time.Millisecond, 10*time.Millisecond,
"deferred maintenance must wait for the foreground sync")
foregroundStarted := make(chan struct{})
releaseForeground := make(chan struct{})
foregroundDone := make(chan error, 1)
go func() {
_, err := engine.SyncThenRun(ctx, false, nil, func(bool) error {
close(foregroundStarted)
<-releaseForeground
return nil
})
foregroundDone <- err
}()
require.Eventually(t, func() bool {
return closed(foregroundStarted)
}, time.Second, 10*time.Millisecond)
assert.Never(t, func() bool {
return closed(maintenanceStarted)
}, 100*time.Millisecond, 10*time.Millisecond,
"maintenance must remain deferred while the foreground sync runs")
close(releaseForeground)
require.NoError(t, <-foregroundDone)
require.Eventually(t, func() bool {
return closed(maintenanceStarted)
}, time.Second, 10*time.Millisecond,
"foreground completion must release startup maintenance")
laterSyncStarted := make(chan struct{})
releaseLaterSync := make(chan struct{})
laterSyncDone := make(chan error, 1)
go func() {
_, err := engine.SyncThenRun(ctx, false, nil, func(bool) error {
close(laterSyncStarted)
<-releaseLaterSync
return nil
})
laterSyncDone <- err
}()
assert.Never(t, func() bool {
return closed(laterSyncStarted)
}, 100*time.Millisecond, 10*time.Millisecond,
"startup maintenance must hold the sync lock")
close(releaseMaintenance)
require.NoError(t, <-maintenanceDone)
require.Eventually(t, func() bool {
return closed(laterSyncStarted)
}, time.Second, 10*time.Millisecond)
close(releaseLaterSync)
require.NoError(t, <-laterSyncDone)
}
func TestStartupSyncFallbackRunsWhenForegroundSyncNeverArrives(t *testing.T) {
database := openTestDB(t)
engine := NewEngine(database, EngineConfig{
Machine: "local",
DeferStartupMaintenance: true,
})
t.Cleanup(engine.Close)
maintenanceStarted := make(chan struct{})
maintenanceDone := make(chan error, 1)
go func() {
maintenanceDone <- engine.RunStartupMaintenance(
t.Context(),
func() error {
close(maintenanceStarted)
return nil
},
)
}()
stats, ran, err := engine.RunStartupSyncFallback(t.Context(), nil)
require.NoError(t, err)
assert.True(t, ran)
assert.False(t, stats.Aborted)
assert.False(t, engine.LastSyncStartedAt().IsZero(),
"fallback must perform the skipped startup sync")
require.Eventually(t, func() bool {
select {
case <-maintenanceStarted:
return true
default:
return false
}
}, time.Second, 10*time.Millisecond,
"fallback completion must release startup maintenance")
require.NoError(t, <-maintenanceDone)
}
func TestStartupSyncFallbackSkipsAfterForegroundSyncCompletes(t *testing.T) {
database := openTestDB(t)
engine := NewEngine(database, EngineConfig{
Machine: "local",
DeferStartupMaintenance: true,
})
t.Cleanup(engine.Close)
_, err := engine.SyncThenRun(
t.Context(), false, nil, func(bool) error { return nil },
)
require.NoError(t, err)
startedAt := engine.LastSyncStartedAt()
require.False(t, startedAt.IsZero())
_, ran, err := engine.RunStartupSyncFallback(t.Context(), nil)
require.NoError(t, err)
assert.False(t, ran,
"fallback must not duplicate a completed foreground sync")
assert.Equal(t, startedAt, engine.LastSyncStartedAt())
}
func TestStartupSyncFallbackRecoversCanceledForegroundSync(t *testing.T) {
database := openTestDB(t)
engine := NewEngine(database, EngineConfig{
Machine: "local",
DeferStartupMaintenance: true,
})
t.Cleanup(engine.Close)
requestCtx, cancelRequest := context.WithCancel(t.Context())
cancelRequest()
_, err := engine.SyncThenRun(
requestCtx, false, nil, func(bool) error { return nil },
)
require.ErrorIs(t, err, context.Canceled)
_, ran, err := engine.RunStartupSyncFallback(t.Context(), nil)
require.NoError(t, err)
assert.True(t, ran,
"a failed foreground request must leave startup recovery eligible")
}
func TestStartupSyncFallbackReleasesMaintenanceAfterCanceledAttempt(
t *testing.T,
) {
database := openTestDB(t)
engine := NewEngine(database, EngineConfig{
Machine: "local",
DeferStartupMaintenance: true,
})
t.Cleanup(engine.Close)
maintenanceStarted := make(chan struct{})
maintenanceDone := make(chan error, 1)
go func() {
maintenanceDone <- engine.RunStartupMaintenance(
t.Context(),
func() error {
close(maintenanceStarted)
return nil
},
)
}()
fallbackCtx, cancelFallback := context.WithCancel(t.Context())
cancelFallback()
_, ran, err := engine.RunStartupSyncFallback(fallbackCtx, nil)
require.ErrorIs(t, err, context.Canceled)
assert.True(t, ran)
require.Eventually(t, func() bool {
select {
case <-maintenanceStarted:
return true
default:
return false
}
}, time.Second, 10*time.Millisecond,
"an attempted fallback must release startup maintenance")
require.NoError(t, <-maintenanceDone)
}
func TestStartupSyncFallbackRechecksAfterInFlightForegroundSync(t *testing.T) {
database := openTestDB(t)
engine := NewEngine(database, EngineConfig{
Machine: "local",
DeferStartupMaintenance: true,
})
t.Cleanup(engine.Close)
foregroundStarted := make(chan struct{})
releaseForeground := make(chan struct{})
foregroundDone := make(chan error, 1)
go func() {
_, err := engine.SyncThenRun(
t.Context(), false, nil,
func(bool) error {
close(foregroundStarted)
<-releaseForeground
return nil
},
)
foregroundDone <- err
}()
select {
case <-foregroundStarted:
case <-time.After(time.Second):
require.FailNow(t, "foreground sync did not start")
}
type fallbackResult struct {
ran bool
err error
}
fallbackDone := make(chan fallbackResult, 1)
go func() {
_, ran, err := engine.RunStartupSyncFallback(t.Context(), nil)
fallbackDone <- fallbackResult{ran: ran, err: err}
}()
assert.Never(t, func() bool {
select {
case <-fallbackDone:
return true
default:
return false
}
}, 100*time.Millisecond, 10*time.Millisecond,
"fallback must wait for the in-flight foreground sync")
close(releaseForeground)
require.NoError(t, <-foregroundDone)
select {
case result := <-fallbackDone:
require.NoError(t, result.err)
assert.False(t, result.ran,
"fallback must recheck completion after acquiring the sync lock")
case <-time.After(time.Second):
require.FailNow(t, "fallback did not finish")
}
}
// fakeFileInfo implements os.FileInfo for test use.
type fakeFileInfo struct {
size int64
mtime int64 // UnixNano
}
func (f fakeFileInfo) Name() string { return "test" }
func (f fakeFileInfo) Size() int64 { return f.size }
func (f fakeFileInfo) Mode() os.FileMode { return 0 }
func (f fakeFileInfo) ModTime() time.Time {
return time.Unix(0, f.mtime)
}
func (f fakeFileInfo) IsDir() bool { return false }
func (f fakeFileInfo) Sys() any { return nil }
func TestHasLegacyKiroCandidates(t *testing.T) {
tests := []struct {
name string
files []parser.DiscoveredFile
want bool
}{
{
name: "empty",
want: false,
},
{
name: "non-kiro files",
files: []parser.DiscoveredFile{
{Agent: parser.AgentClaude, Path: "/tmp/claude/session.jsonl"},
},
want: false,
},
{
name: "kiro sqlite database source",
files: []parser.DiscoveredFile{
{Agent: parser.AgentKiro, Path: "/tmp/kiro/data.sqlite3"},
},
want: false,
},
{
name: "legacy kiro jsonl",
files: []parser.DiscoveredFile{
{Agent: parser.AgentKiro, Path: "/tmp/kiro/session.jsonl"},
},
want: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.want, hasLegacyKiroCandidates(tt.files))
})
}
}
func TestFilterEmptyMessages(t *testing.T) {
tests := []struct {
name string
msgs []db.Message
want []db.Message
}{
{
name: "removes empty-content user message after pairing",
msgs: []db.Message{
{
Role: "assistant",
Content: "Let me read the file.",
ToolCalls: []db.ToolCall{
{ToolUseID: "t1", ToolName: "Read"},
},
},
{
Role: "user",
Content: "",
ToolResults: []db.ToolResult{
{ToolUseID: "t1", ContentLength: 500},
},
},
},
want: []db.Message{
{
Role: "assistant",
Content: "Let me read the file.",
ToolCalls: []db.ToolCall{
{ToolUseID: "t1", ToolName: "Read", ResultContentLength: 500},
},
},
},
},
{
name: "keeps user message with real content",
msgs: []db.Message{
{
Role: "assistant",
Content: "Here is the result.",
ToolCalls: []db.ToolCall{
{ToolUseID: "t1", ToolName: "Bash"},
},
},
{
Role: "user",
Content: "",
ToolResults: []db.ToolResult{
{ToolUseID: "t1", ContentLength: 100},
},
},
{
Role: "user",
Content: "Thanks, now do something else.",
},
},
want: []db.Message{
{
Role: "assistant",
Content: "Here is the result.",
ToolCalls: []db.ToolCall{
{ToolUseID: "t1", ToolName: "Bash", ResultContentLength: 100},
},
},
{
Role: "user",
Content: "Thanks, now do something else.",
},
},
},
{
name: "whitespace-only content treated as empty",
msgs: []db.Message{
{
Role: "assistant",
Content: "Reading...",
ToolCalls: []db.ToolCall{
{ToolUseID: "t1", ToolName: "Read"},
},
},
{
Role: "user",
Content: " \n\t ",
ToolResults: []db.ToolResult{
{ToolUseID: "t1", ContentLength: 300},
},
},
},
want: []db.Message{
{
Role: "assistant",
Content: "Reading...",
ToolCalls: []db.ToolCall{
{ToolUseID: "t1", ToolName: "Read", ResultContentLength: 300},
},
},
},
},
{
name: "preserves empty assistant message",
msgs: []db.Message{
{
Role: "assistant",
Content: "",
},
},
want: []db.Message{
{
Role: "assistant",
Content: "",
},
},
},
{
name: "only removes user messages with tool results",
msgs: []db.Message{
{
Role: "assistant",
Content: "",
},
{
Role: "user",
Content: "",
},
},
want: []db.Message{
{
Role: "assistant",
Content: "",
},
{
Role: "user",
Content: "",
},
},
},
{
name: "no messages returns empty",
msgs: nil,
want: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := pairAndFilter(tt.msgs, nil)
diff := cmp.Diff(tt.want, got)
assert.Empty(t, diff, "pairAndFilter() mismatch (-want +got):\n%s", diff)
})
}
}
func TestPostFilterCounts(t *testing.T) {
type counts struct {
Total int
User int
}
tests := []struct {
name string
msgs []db.Message
want counts
}{
{
name: "mixed roles",
msgs: []db.Message{
{Role: "user", Content: "hello"},
{Role: "assistant", Content: "hi"},
{Role: "user", Content: "thanks"},
},
want: counts{Total: 3, User: 2},
},
{
name: "no user messages",
msgs: []db.Message{
{Role: "assistant", Content: "hi"},
},
want: counts{Total: 1, User: 0},
},
{
name: "empty slice",
msgs: nil,
want: counts{Total: 0, User: 0},
},
{
name: "all user messages",
msgs: []db.Message{
{Role: "user", Content: "a"},
{Role: "user", Content: "b"},
},
want: counts{Total: 2, User: 2},
},
{
name: "system messages excluded from user count",
msgs: []db.Message{
{Role: "user", Content: "hello", IsSystem: false},
{Role: "user", Content: "system notice", IsSystem: true},
{Role: "assistant", Content: "hi"},
{Role: "user", Content: "[Turn finished: endTurn]", IsSystem: true},
},
want: counts{Total: 4, User: 1},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
total, user := postFilterCounts(tt.msgs)
got := counts{Total: total, User: user}
diff := cmp.Diff(tt.want, got)
assert.Empty(t, diff, "postFilterCounts() mismatch (-want +got):\n%s", diff)
})
}
}
func TestPairToolResults(t *testing.T) {
tests := []struct {
name string
msgs []db.Message
want []db.Message
}{
{
name: "basic pairing across messages",
msgs: []db.Message{
{ToolCalls: []db.ToolCall{
{ToolUseID: "t1", ToolName: "Read"},
{ToolUseID: "t2", ToolName: "Grep"},
}},
{ToolResults: []db.ToolResult{
{ToolUseID: "t1", ContentLength: 100},
{ToolUseID: "t2", ContentLength: 200},
}},
},
want: []db.Message{
{ToolCalls: []db.ToolCall{
{ToolUseID: "t1", ToolName: "Read", ResultContentLength: 100},
{ToolUseID: "t2", ToolName: "Grep", ResultContentLength: 200},
}},
{ToolResults: []db.ToolResult{
{ToolUseID: "t1", ContentLength: 100},
{ToolUseID: "t2", ContentLength: 200},
}},
},
},
{
name: "unmatched tool_result ignored",
msgs: []db.Message{
{ToolCalls: []db.ToolCall{
{ToolUseID: "t1", ToolName: "Read"},
}},
{ToolResults: []db.ToolResult{
{ToolUseID: "t1", ContentLength: 50},
{ToolUseID: "t_unknown", ContentLength: 999},
}},
},
want: []db.Message{
{ToolCalls: []db.ToolCall{
{ToolUseID: "t1", ToolName: "Read", ResultContentLength: 50},
}},
{ToolResults: []db.ToolResult{
{ToolUseID: "t1", ContentLength: 50},
{ToolUseID: "t_unknown", ContentLength: 999},
}},
},
},
{
name: "unmatched tool_call keeps zero",
msgs: []db.Message{
{ToolCalls: []db.ToolCall{
{ToolUseID: "t1", ToolName: "Read"},
{ToolUseID: "t2", ToolName: "Bash"},
}},
{ToolResults: []db.ToolResult{
{ToolUseID: "t1", ContentLength: 42},
}},
},
want: []db.Message{
{ToolCalls: []db.ToolCall{
{ToolUseID: "t1", ToolName: "Read", ResultContentLength: 42},
{ToolUseID: "t2", ToolName: "Bash", ResultContentLength: 0},
}},
{ToolResults: []db.ToolResult{
{ToolUseID: "t1", ContentLength: 42},
}},
},
},
{
name: "empty messages",
msgs: nil,
want: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
pairToolResults(tt.msgs, nil)
diff := cmp.Diff(tt.want, tt.msgs)
assert.Empty(t, diff, "pairToolResults() mismatch (-want +got):\n%s", diff)
})
}
}
func TestPairToolResultsContent(t *testing.T) {
ampToolResultText := "line 1\nline \"2\" output"
ampToolResultRaw := "\"line 1\\nline \\\"2\\\" output\""
tests := []struct {
name string
msgs []db.Message
blocked map[string]bool
want []db.Message
}{
{
name: "content stored for non-blocked category",
msgs: []db.Message{
{ToolCalls: []db.ToolCall{
{ToolUseID: "t1", ToolName: "Bash", Category: "Bash"},
}},
{ToolResults: []db.ToolResult{
{ToolUseID: "t1", ContentLength: 42, ContentRaw: `"output text"`},
}},
},
blocked: map[string]bool{"Read": true, "Glob": true},
want: []db.Message{
{ToolCalls: []db.ToolCall{
{ToolUseID: "t1", ToolName: "Bash", Category: "Bash",
ResultContentLength: 42, ResultContent: "output text"},
}},
{ToolResults: []db.ToolResult{
{ToolUseID: "t1", ContentLength: 42, ContentRaw: `"output text"`},
}},
},
},
{
name: "content blocked for Read category",
msgs: []db.Message{
{ToolCalls: []db.ToolCall{
{ToolUseID: "t1", ToolName: "Read", Category: "Read"},
}},
{ToolResults: []db.ToolResult{
{ToolUseID: "t1", ContentLength: 5000, ContentRaw: `"file data"`},
}},
},
blocked: map[string]bool{"Read": true, "Glob": true},
want: []db.Message{
{ToolCalls: []db.ToolCall{
{ToolUseID: "t1", ToolName: "Read", Category: "Read",
ResultContentLength: 5000, ResultContent: ""},
}},
{ToolResults: []db.ToolResult{
{ToolUseID: "t1", ContentLength: 5000, ContentRaw: `"file data"`},
}},
},
},
{
name: "nil blocked map stores all content",
msgs: []db.Message{
{ToolCalls: []db.ToolCall{
{ToolUseID: "t1", ToolName: "Read", Category: "Read"},
}},
{ToolResults: []db.ToolResult{
{ToolUseID: "t1", ContentLength: 100, ContentRaw: `"file content"`},
}},
},
blocked: nil,
want: []db.Message{
{ToolCalls: []db.ToolCall{
{ToolUseID: "t1", ToolName: "Read", Category: "Read",
ResultContentLength: 100, ResultContent: "file content"},
}},
{ToolResults: []db.ToolResult{
{ToolUseID: "t1", ContentLength: 100, ContentRaw: `"file content"`},
}},
},
},
{
// Mirrors ContentRaw produced by parser.extractAmpToolResults
// (JSON-marshaled plain-text output).
name: "amp: marshaled tool result text decodes into ResultContent",
msgs: []db.Message{
{ToolCalls: []db.ToolCall{
{ToolUseID: "t1", ToolName: "Bash", Category: "Bash"},
}},
{ToolResults: []db.ToolResult{
{
ToolUseID: "t1",
ContentLength: len(ampToolResultText),
ContentRaw: ampToolResultRaw,
},
}},
},
blocked: nil,
want: []db.Message{
{ToolCalls: []db.ToolCall{
{
ToolUseID: "t1", ToolName: "Bash", Category: "Bash",
ResultContentLength: len(ampToolResultText),
ResultContent: ampToolResultText,
},
}},
{ToolResults: []db.ToolResult{
{
ToolUseID: "t1",
ContentLength: len(ampToolResultText),
ContentRaw: ampToolResultRaw,
},
}},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
pairToolResults(tt.msgs, tt.blocked)
diff := cmp.Diff(tt.want, tt.msgs)
assert.Empty(t, diff, "pairToolResults() mismatch (-want +got):\n%s", diff)
})
}
}
func TestPairToolResultEventSummaries(t *testing.T) {
tests := []struct {
name string
msgs []db.Message
blocked map[string]bool
want []db.Message
}{
{
name: "single event becomes summary",
msgs: []db.Message{{
ToolCalls: []db.ToolCall{{
ToolUseID: "call_wait",
ToolName: "wait",
Category: "Other",
ResultEvents: []db.ToolResultEvent{{
ToolUseID: "call_wait",
AgentID: "agent-1",
Source: "wait_output",
Status: "completed",
Content: "Finished successfully",
ContentLength: len("Finished successfully"),
}},
}},
}},
want: []db.Message{{
ToolCalls: []db.ToolCall{{
ToolUseID: "call_wait",
ToolName: "wait",
Category: "Other",
ResultContentLength: len("Finished successfully"),
ResultContent: "Finished successfully",
ResultEvents: []db.ToolResultEvent{{
ToolUseID: "call_wait",
AgentID: "agent-1",
Source: "wait_output",
Status: "completed",
Content: "Finished successfully",
ContentLength: len("Finished successfully"),
}},
}},
}},
},
{
name: "multi-agent latest summary keeps one line per agent",
msgs: []db.Message{{
ToolCalls: []db.ToolCall{{
ToolUseID: "call_wait",
ToolName: "wait",
Category: "Other",
ResultEvents: []db.ToolResultEvent{
{
ToolUseID: "call_wait",
AgentID: "agent-a",
Source: "wait_output",
Status: "completed",
Content: "First finished",
ContentLength: len("First finished"),
},
{
ToolUseID: "call_wait",
AgentID: "agent-b",
Source: "subagent_notification",
Status: "completed",
Content: "Second finished",
ContentLength: len("Second finished"),
},
},
}},
}},
want: []db.Message{{
ToolCalls: []db.ToolCall{{
ToolUseID: "call_wait",
ToolName: "wait",
Category: "Other",
ResultContentLength: len("agent-a:\nFirst finished\n\nagent-b:\nSecond finished"),
ResultContent: "agent-a:\nFirst finished\n\nagent-b:\nSecond finished",
ResultEvents: []db.ToolResultEvent{
{
ToolUseID: "call_wait",
AgentID: "agent-a",
Source: "wait_output",
Status: "completed",
Content: "First finished",
ContentLength: len("First finished"),
},
{
ToolUseID: "call_wait",
AgentID: "agent-b",
Source: "subagent_notification",
Status: "completed",
Content: "Second finished",
ContentLength: len("Second finished"),
},
},
}},
}},
},
{
name: "blocked category keeps length but drops summary content",
msgs: []db.Message{{
ToolCalls: []db.ToolCall{{
ToolUseID: "call_read",
ToolName: "Read",
Category: "Read",
ResultEvents: []db.ToolResultEvent{{
ToolUseID: "call_read",
Source: "wait_output",
Status: "completed",
Content: "secret file body",
ContentLength: len("secret file body"),
}},
}},
}},
blocked: map[string]bool{"Read": true},
want: []db.Message{{
ToolCalls: []db.ToolCall{{
ToolUseID: "call_read",
ToolName: "Read",
Category: "Read",
ResultContentLength: len("secret file body"),
ResultContent: "",
ResultEvents: nil,
}},
}},
},
{
name: "mixed anonymous and multi-agent content keeps both",
msgs: []db.Message{{
ToolCalls: []db.ToolCall{{
ToolUseID: "call_wait",
ToolName: "wait",
Category: "Other",
ResultEvents: []db.ToolResultEvent{
{
ToolUseID: "call_wait",
AgentID: "agent-a",
Source: "wait_output",
Status: "completed",
Content: "First finished",
ContentLength: len("First finished"),
},
{
ToolUseID: "call_wait",
AgentID: "agent-b",
Source: "wait_output",
Status: "completed",
Content: "Second finished",
ContentLength: len("Second finished"),
},
{
ToolUseID: "call_wait",
Source: "subagent_notification",
Status: "completed",
Content: "Detached note",
ContentLength: len("Detached note"),
},
},
}},
}},
want: []db.Message{{
ToolCalls: []db.ToolCall{{
ToolUseID: "call_wait",
ToolName: "wait",
Category: "Other",
ResultContentLength: len("agent-a:\nFirst finished\n\nagent-b:\nSecond finished\n\nDetached note"),
ResultContent: "agent-a:\nFirst finished\n\nagent-b:\nSecond finished\n\nDetached note",
ResultEvents: []db.ToolResultEvent{
{
ToolUseID: "call_wait",
AgentID: "agent-a",
Source: "wait_output",
Status: "completed",
Content: "First finished",
ContentLength: len("First finished"),
},
{
ToolUseID: "call_wait",
AgentID: "agent-b",
Source: "wait_output",
Status: "completed",
Content: "Second finished",
ContentLength: len("Second finished"),
},
{
ToolUseID: "call_wait",
Source: "subagent_notification",
Status: "completed",
Content: "Detached note",
ContentLength: len("Detached note"),
},
},
}},
}},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
pairToolResultEventSummaries(tt.msgs, tt.blocked)
diff := cmp.Diff(tt.want, tt.msgs)
require.Empty(t, diff, "pairToolResultEventSummaries() mismatch (-want +got):\n%s", diff)
})
}
}
func TestApplyRemoteRewrites(t *testing.T) {
tests := []struct {
name string
prefix string
rewriter func(string) string
sess db.Session
msgs []db.Message
wantSessID string
wantParent *string
wantFilePath *string
wantMsgSess string // expected SessionID on messages
wantSubs []string
wantEvSubs []string
}{
{
name: "no prefix is no-op",
prefix: "",
sess: db.Session{
ID: "abc",
},
msgs: []db.Message{
{SessionID: "abc"},
},
wantSessID: "abc",
wantMsgSess: "abc",
},
{
name: "all fields prefixed",
prefix: "host~",
sess: db.Session{
ID: "abc",
ParentSessionID: strPtr("parent-1"),
FilePath: strPtr("/tmp/file"),
},
msgs: []db.Message{
{
SessionID: "abc",
ToolCalls: []db.ToolCall{
{
SessionID: "abc",
SubagentSessionID: "sub-1",
ResultEvents: []db.ToolResultEvent{
{SubagentSessionID: "ev-1"},
{SubagentSessionID: ""},
},
},
{SessionID: "abc"},
},
},
},
wantSessID: "host~abc",
wantParent: strPtr("host~parent-1"),
wantFilePath: strPtr("/tmp/file"),
wantMsgSess: "host~abc",
wantSubs: []string{"host~sub-1", ""},
wantEvSubs: []string{"host~ev-1", ""},
},
{
name: "path rewriter applied",
prefix: "box~",
rewriter: func(p string) string {
return "box:" + p
},
sess: db.Session{
ID: "x",
FilePath: strPtr("/remote/path"),
},
msgs: nil,
wantSessID: "box~x",
wantFilePath: strPtr("box:/remote/path"),
},
{
name: "nil parent stays nil",
prefix: "h~",
sess: db.Session{
ID: "z",
},
wantSessID: "h~z",
wantParent: nil,
},
{
name: "empty parent stays empty",
prefix: "h~",
sess: db.Session{
ID: "z",
ParentSessionID: strPtr(""),
},
wantSessID: "h~z",
wantParent: strPtr(""),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
e := &Engine{
idPrefix: tt.prefix,
pathRewriter: tt.rewriter,
}
e.applyRemoteRewrites(&tt.sess, tt.msgs)
assert.Equal(t, tt.wantSessID, tt.sess.ID)
diff := cmp.Diff(tt.wantParent, tt.sess.ParentSessionID)
assert.Empty(t, diff, "ParentSessionID %s", diff)
if tt.wantFilePath != nil {
diff := cmp.Diff(tt.wantFilePath, tt.sess.FilePath)
assert.Empty(t, diff, "FilePath %s", diff)
}
for _, m := range tt.msgs {
assert.Equal(t, tt.wantMsgSess, m.SessionID)
}
var gotSubs, gotEvSubs []string
for _, m := range tt.msgs {
for _, tc := range m.ToolCalls {
gotSubs = append(
gotSubs, tc.SubagentSessionID,
)
for _, ev := range tc.ResultEvents {
gotEvSubs = append(
gotEvSubs,
ev.SubagentSessionID,
)
}
}
}
diff = cmp.Diff(tt.wantSubs, gotSubs)
assert.Empty(t, diff, "SubagentSessionIDs %s", diff)
diff = cmp.Diff(tt.wantEvSubs, gotEvSubs)
assert.Empty(t, diff, "ResultEvent SubagentSessionIDs %s", diff)
})
}
}
func TestToDBUsageEventsStampsFinalSessionID(t *testing.T) {
tests := []struct {
name string
sessionID string
events []parser.ParsedUsageEvent
wantIDs []string
}{
{
name: "empty event session id gets final id",
sessionID: "antigravity:abc",
events: []parser.ParsedUsageEvent{
{Source: "generation", Model: "gemini"},
},
wantIDs: []string{"antigravity:abc"},
},
{
name: "parser-stamped id matching final id is kept",
sessionID: "antigravity:abc",
events: []parser.ParsedUsageEvent{
{
SessionID: "antigravity:abc",
Source: "generation",
Model: "gemini",
},
},
wantIDs: []string{"antigravity:abc"},
},
{
name: "remote prefix overrides parser-stamped id",
sessionID: "host~antigravity:abc",
events: []parser.ParsedUsageEvent{
{
SessionID: "antigravity:abc",
Source: "generation",
Model: "gemini",
},
{
SessionID: "antigravity:abc",
Source: "generation",
Model: "claude",
},
},
wantIDs: []string{
"host~antigravity:abc",
"host~antigravity:abc",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, _ := toDBUsageEvents(tt.sessionID, tt.events)
require.Len(t, got, len(tt.wantIDs))
for i, ev := range got {
assert.Equal(t, tt.wantIDs[i], ev.SessionID)
}
})
}
}
func TestToDBUsageEventsPreservesSessionSummaryTokenUpperBounds(t *testing.T) {
rawInput := maxPlausibleTokens + 250_000
rawOutput := maxPlausibleTokens + 500_000
got, _ := toDBUsageEvents("hermes:summary", []parser.ParsedUsageEvent{
{
Source: "session",
Model: "gpt-5.4",
InputTokens: rawInput,
OutputTokens: rawOutput,
CacheCreationInputTokens: rawInput + 1,
CacheReadInputTokens: rawInput + 2,
ReasoningTokens: rawOutput + 3,
},
{
Source: "session",
Model: "gpt-5.4",
InputTokens: -1,
OutputTokens: -2,
CacheCreationInputTokens: -3,
CacheReadInputTokens: -4,
ReasoningTokens: -5,
},
})
require.Len(t, got, 2)
assert.Equal(t, rawInput, got[0].InputTokens)
assert.Equal(t, rawOutput, got[0].OutputTokens)
assert.Equal(t, rawInput+1, got[0].CacheCreationInputTokens)
assert.Equal(t, rawInput+2, got[0].CacheReadInputTokens)
assert.Equal(t, rawOutput+3, got[0].ReasoningTokens)
assert.Equal(t, 0, got[1].InputTokens)
assert.Equal(t, 0, got[1].OutputTokens)
assert.Equal(t, 0, got[1].CacheCreationInputTokens)
assert.Equal(t, 0, got[1].CacheReadInputTokens)
assert.Equal(t, 0, got[1].ReasoningTokens)
}
func TestWriteBatchRemoteIDPrefixUsageEvents(t *testing.T) {
database := openTestDB(t)
e := &Engine{db: database, idPrefix: "host~"}
ts := time.Unix(1700000000, 0).UTC()
pw := pendingWrite{
sess: parser.ParsedSession{
ID: "antigravity:abc",
Project: "proj",
Machine: "host",
Agent: parser.AgentAntigravity,
StartedAt: ts,
EndedAt: ts,
MessageCount: 1,
},
msgs: []parser.ParsedMessage{{
Role: parser.RoleUser,
Content: "hello",
Timestamp: ts,
}},
usageEvents: []parser.ParsedUsageEvent{{
// Parsers stamp the unprefixed session ID; the
// write path must replace it with the final
// remote-prefixed ID.
SessionID: "antigravity:abc",
Source: "generation",
Model: "gemini",
InputTokens: 100,
OutputTokens: 50,
OccurredAt: ts.Format(time.RFC3339Nano),
}},
}
written, _, failed, _ := e.writeBatch(
[]pendingWrite{pw}, syncWriteDefault, false,
)
require.Equal(t, 0, failed, "no session writes may fail")
require.Equal(t, 1, written)
events, err := database.GetUsageEvents(
context.Background(), "host~antigravity:abc",
)
require.NoError(t, err, "GetUsageEvents")
require.Len(t, events, 1)
assert.Equal(t, "host~antigravity:abc", events[0].SessionID)
assert.Equal(t, "gemini", events[0].Model)
assert.Equal(t, 100, events[0].InputTokens)
assert.Equal(t, 50, events[0].OutputTokens)
}
func TestProjectIdentityWriteBatchDiscoversLocalGitRemote(t *testing.T) {
database := openTestDB(t)
root := t.TempDir()
require.NoError(t, os.Mkdir(filepath.Join(root, ".git"), 0o755))
require.NoError(t, os.WriteFile(
filepath.Join(root, ".git", "config"),
[]byte("[remote \"origin\"]\n\turl = git@github.com:Org/Repo.git\n"),
0o644,
))
require.NoError(t, os.WriteFile(
filepath.Join(root, ".git", "HEAD"),
[]byte("ref: refs/heads/feature/export\n"), 0o644,
))
cwd := filepath.Join(root, "subdir")
require.NoError(t, os.Mkdir(cwd, 0o755))
e := NewEngine(database, EngineConfig{Machine: "laptop"})
written, _, failed, _ := e.writeBatch([]pendingWrite{{
sess: parser.ParsedSession{
ID: "identity-local",
Project: "repo",
Machine: "laptop",
Agent: parser.AgentCodex,
Cwd: cwd,
StartedAt: time.Now(),
},
}}, syncWriteDefault, true)
require.Equal(t, 1, written)
require.Equal(t, 0, failed)
observations, err := database.ListProjectIdentityObservations(
context.Background(), []string{"repo"},
)
require.NoError(t, err)
require.Len(t, observations, 1)
expectedRoot, err := filepath.EvalSymlinks(root)
require.NoError(t, err)
assert.Equal(t, expectedRoot, observations[0].RootPath)
assert.Equal(t, "origin", observations[0].GitRemoteName)
assert.Equal(t, "github.com:Org/Repo.git", observations[0].GitRemote)
assert.Equal(t, "github.com/Org/Repo", observations[0].NormalizedRemote)
assert.Equal(t, "git_remote", observations[0].KeySource)
assert.Equal(t, expectedRoot, observations[0].RepositoryPath)
assert.Equal(t, expectedRoot, observations[0].WorktreeRootPath)
assert.Equal(t, export.WorktreeMain, observations[0].WorktreeRelationship)
assert.Equal(t, export.CheckoutBranch, observations[0].CheckoutState)
assert.Equal(t, "feature/export", observations[0].GitBranch)
}
func TestProjectIdentityDiscoversLinkedWorktreeRepositoryContext(t *testing.T) {
database := openTestDB(t)
mainRoot := filepath.Join(t.TempDir(), "main")
linkedRoot := filepath.Join(t.TempDir(), "feature")
gitDir := filepath.Join(mainRoot, ".git")
linkedGitDir := filepath.Join(gitDir, "worktrees", "feature")
require.NoError(t, os.MkdirAll(linkedGitDir, 0o755))
require.NoError(t, os.MkdirAll(linkedRoot, 0o755))
require.NoError(t, os.WriteFile(filepath.Join(gitDir, "config"), []byte(
"[remote \"origin\"]\n\turl = https://github.com/acme/app.git\n",
), 0o644))
require.NoError(t, os.WriteFile(filepath.Join(linkedRoot, ".git"), []byte(
"gitdir: "+linkedGitDir+"\n",
), 0o644))
require.NoError(t, os.WriteFile(filepath.Join(linkedGitDir, "commondir"),
[]byte("../..\n"), 0o644))
require.NoError(t, os.WriteFile(filepath.Join(linkedGitDir, "HEAD"),
[]byte("ref: refs/heads/feature/receipts\n"), 0o644))
e := NewEngine(database, EngineConfig{Machine: "laptop"})
written, _, failed, _ := e.writeBatch([]pendingWrite{{
sess: parser.ParsedSession{
ID: "linked-identity", Project: "app", Machine: "laptop",
Agent: parser.AgentCodex, Cwd: linkedRoot, StartedAt: time.Now(),
},
}}, syncWriteDefault, true)
require.Equal(t, 1, written)
require.Equal(t, 0, failed)
observations, err := database.ListProjectIdentityObservations(
context.Background(), []string{"app"},
)
require.NoError(t, err)
require.Len(t, observations, 1)
expectedMainRoot, err := filepath.EvalSymlinks(mainRoot)
require.NoError(t, err)
expectedLinkedRoot, err := filepath.EvalSymlinks(linkedRoot)
require.NoError(t, err)
assert.Equal(t, expectedMainRoot, observations[0].RepositoryPath)
assert.Equal(t, expectedLinkedRoot, observations[0].WorktreeRootPath)
assert.Equal(t, export.WorktreeLinked,
observations[0].WorktreeRelationship)
assert.Equal(t, export.CheckoutBranch, observations[0].CheckoutState)
assert.Equal(t, "feature/receipts", observations[0].GitBranch)
assert.Equal(t, "github.com/acme/app", observations[0].NormalizedRemote)
}
func TestProjectIdentityObservationWriteDeduplicatesSameEngine(t *testing.T) {
database := openTestDB(t)
root := t.TempDir()
e := NewEngine(database, EngineConfig{Machine: "laptop"})
ctx := context.Background()
session := db.Session{
ID: "identity-dedup",
Project: "dedup",
Machine: "laptop",
Agent: "codex",
Cwd: root,
StartedAt: strPtr(time.Now().UTC().Format(time.RFC3339Nano)),
}
require.NoError(t, e.writeProjectIdentityObservation(ctx, session))
observations, err := database.ListProjectIdentityObservations(ctx, []string{"dedup"})
require.NoError(t, err)
require.Len(t, observations, 1)
firstObservedAt := observations[0].ObservedAt
time.Sleep(time.Millisecond)
require.NoError(t, e.writeProjectIdentityObservation(ctx, session))
observations, err = database.ListProjectIdentityObservations(ctx, []string{"dedup"})
require.NoError(t, err)
require.Len(t, observations, 1)
assert.Equal(t, firstObservedAt, observations[0].ObservedAt)
}
func TestProjectIdentityObservationCachesLocalGitDiscovery(t *testing.T) {
database := openTestDB(t)
root := t.TempDir()
require.NoError(t, os.Mkdir(filepath.Join(root, ".git"), 0o755))
configPath := filepath.Join(root, ".git", "config")
require.NoError(t, os.WriteFile(
configPath,
[]byte("[remote \"origin\"]\n\turl = https://github.com/acme/cache.git\n"),
0o644,
))
cwd := filepath.Join(root, "subdir")
require.NoError(t, os.Mkdir(cwd, 0o755))
e := NewEngine(database, EngineConfig{Machine: "laptop"})
ctx := context.Background()
require.NoError(t, e.writeProjectIdentityObservation(ctx, db.Session{
ID: "identity-cache-a",
Project: "cache-a",
Machine: "laptop",
Agent: "codex",
Cwd: cwd,
StartedAt: strPtr(time.Now().UTC().Format(time.RFC3339Nano)),
}))
require.NoError(t, os.Remove(configPath))
require.NoError(t, e.writeProjectIdentityObservation(ctx, db.Session{
ID: "identity-cache-b",
Project: "cache-b",
Machine: "laptop",
Agent: "codex",
Cwd: cwd,
StartedAt: strPtr(time.Now().UTC().Format(time.RFC3339Nano)),
}))
observations, err := database.ListProjectIdentityObservations(ctx, []string{"cache-b"})
require.NoError(t, err)
require.Len(t, observations, 1)
assert.Equal(t, "https://github.com/acme/cache.git", observations[0].GitRemote)
assert.Equal(t, "github.com/acme/cache", observations[0].NormalizedRemote)
}
func TestProjectIdentitySnapshotsPreferParsedBranchesForSharedWorkingDirectory(
t *testing.T,
) {
database := openTestDB(t)
root := t.TempDir()
require.NoError(t, os.Mkdir(filepath.Join(root, ".git"), 0o755))
require.NoError(t, os.WriteFile(
filepath.Join(root, ".git", "config"),
[]byte("[remote \"origin\"]\n\turl = https://github.com/acme/app.git\n"),
0o644,
))
require.NoError(t, os.WriteFile(
filepath.Join(root, ".git", "HEAD"),
[]byte("ref: refs/heads/current-checkout\n"), 0o644,
))
e := NewEngine(database, EngineConfig{Machine: "laptop"})
now := time.Now()
written, _, failed, _ := e.writeBatch([]pendingWrite{
{sess: parser.ParsedSession{
ID: "branch-a", Project: "app", Machine: "laptop",
Agent: parser.AgentCodex, Cwd: root, GitBranch: "historical-a",
StartedAt: now,
}},
{sess: parser.ParsedSession{
ID: "branch-b", Project: "app", Machine: "laptop",
Agent: parser.AgentCodex, Cwd: root, GitBranch: "historical-b",
StartedAt: now.Add(time.Second),
}},
}, syncWriteDefault, true)
require.Equal(t, 2, written)
require.Equal(t, 0, failed)
snapshots, err := database.ListSessionProjectIdentitySnapshots(
context.Background(),
)
require.NoError(t, err)
require.Len(t, snapshots, 2)
assert.Equal(t, "branch-a", snapshots[0].SessionID)
assert.Equal(t, "historical-a", snapshots[0].GitBranch)
assert.Equal(t, export.CheckoutBranch, snapshots[0].CheckoutState)
assert.Equal(t, "branch-b", snapshots[1].SessionID)
assert.Equal(t, "historical-b", snapshots[1].GitBranch)
assert.Equal(t, export.CheckoutBranch, snapshots[1].CheckoutState)
}
func TestProjectIdentitySnapshotsDoNotCacheCheckoutHead(t *testing.T) {
database := openTestDB(t)
root := t.TempDir()
require.NoError(t, os.Mkdir(filepath.Join(root, ".git"), 0o755))
require.NoError(t, os.WriteFile(
filepath.Join(root, ".git", "config"),
[]byte("[remote \"origin\"]\n\turl = https://github.com/acme/app.git\n"),
0o644,
))
headPath := filepath.Join(root, ".git", "HEAD")
require.NoError(t, os.WriteFile(
headPath, []byte("ref: refs/heads/branch-a\n"), 0o644,
))
e := NewEngine(database, EngineConfig{Machine: "laptop"})
ctx := context.Background()
require.NoError(t, e.writeProjectIdentityObservation(ctx, db.Session{
ID: "head-a", Project: "app-a", Machine: "laptop",
Agent: "codex", Cwd: root,
}))
require.NoError(t, os.WriteFile(
headPath, []byte("ref: refs/heads/branch-b\n"), 0o644,
))
require.NoError(t, e.writeProjectIdentityObservation(ctx, db.Session{
ID: "head-b", Project: "app-b", Machine: "laptop",
Agent: "codex", Cwd: root,
}))
observations, err := database.ListProjectIdentityObservations(
ctx, []string{"app-a", "app-b"},
)
require.NoError(t, err)
require.Len(t, observations, 2)
branches := map[string]string{}
for _, observation := range observations {
branches[observation.Project] = observation.GitBranch
}
assert.Equal(t, "branch-a", branches["app-a"])
assert.Equal(t, "branch-b", branches["app-b"])
}
func TestProjectIdentityBackfillPreservesEvidenceAcrossSchemaUpgrade(
t *testing.T,
) {
dir := t.TempDir()
dbPath := filepath.Join(dir, "sessions.db")
repo := filepath.Join(dir, "repo")
require.NoError(t, os.MkdirAll(filepath.Join(repo, ".git"), 0o755))
require.NoError(t, os.WriteFile(
filepath.Join(repo, ".git", "config"),
[]byte("[remote \"origin\"]\n\turl = https://github.com/kenn-io/agentsview.git\n"),
0o644,
))
require.NoError(t, os.WriteFile(
filepath.Join(repo, ".git", "HEAD"),
[]byte("ref: refs/heads/current-checkout\n"), 0o644,
))
database, err := db.Open(dbPath)
require.NoError(t, err)
require.NoError(t, database.UpsertSession(db.Session{
ID: "legacy-evidence", Project: "agentsview", Machine: "laptop",
Agent: "codex", Cwd: repo, GitBranch: "historical-branch",
MessageCount: 2,
}))
require.NoError(t, database.Close())
raw, err := sql.Open("sqlite3", dbPath)
require.NoError(t, err)
_, err = raw.Exec(`
DROP TABLE session_project_identity_snapshots;
DROP TABLE background_migrations;
`)
require.NoError(t, err)
require.NoError(t, raw.Close())
require.NoError(t, db.UpgradeExportSchemaInPlace(dbPath,
&db.SchemaUpgradeRequiredError{
Table: "session_project_identity_snapshots", Column: "session_id",
}))
database, err = db.Open(dbPath)
require.NoError(t, err)
t.Cleanup(func() { require.NoError(t, database.Close()) })
engine := NewEngine(database, EngineConfig{Machine: "laptop"})
require.NoError(t, engine.BackfillProjectIdentitySnapshots(
context.Background()))
snapshots, err := database.ListSessionProjectIdentitySnapshots(
context.Background())
require.NoError(t, err)
require.Len(t, snapshots, 1)
canonicalRepo, err := filepath.EvalSymlinks(repo)
require.NoError(t, err)
assert.Equal(t, "legacy-evidence", snapshots[0].SessionID)
assert.Equal(t, "github.com/kenn-io/agentsview",
snapshots[0].NormalizedRemote)
assert.Equal(t, canonicalRepo, snapshots[0].WorktreeRootPath)
assert.Equal(t, "historical-branch", snapshots[0].GitBranch)
assert.Equal(t, export.CheckoutBranch, snapshots[0].CheckoutState)
status, err := database.ProjectIdentityBackfillStatus(context.Background())
require.NoError(t, err)
assert.Equal(t, "completed", status.State)
assert.Equal(t, 1, status.TotalItems)
assert.Equal(t, 1, status.CompletedItems)
}
func TestCountNormalizedRemoteCandidatesDeduplicatesAliases(t *testing.T) {
assert.Equal(t, 2, countNormalizedRemoteCandidates(map[string]string{
"origin": "git@example.com:acme/app.git",
"mirror": "https://example.com/acme/app.git",
"upstream": "https://example.com/acme/upstream.git",
"local": "/tmp/app.git",
}))
}
// TestProjectIdentityObservationSkipsDiscoveryForRemoteMachine pins that
// local git discovery never probes the filesystem for a session recorded on
// another machine: the cwd names a path on that machine, so resolving it
// locally is wrong (and on macOS, stat'ing a remote /home/... cwd wakes the
// automounter — see cachedProjectIdentity). The cwd here is a real local git
// repo, so if discovery ran anyway the observation would carry its remote.
func TestProjectIdentityObservationSkipsDiscoveryForRemoteMachine(t *testing.T) {
database := openTestDB(t)
root := t.TempDir()
require.NoError(t, os.Mkdir(filepath.Join(root, ".git"), 0o755))
require.NoError(t, os.WriteFile(
filepath.Join(root, ".git", "config"),
[]byte("[remote \"origin\"]\n\turl = https://github.com/acme/remote.git\n"),
0o644,
))
cwd := filepath.Join(root, "subdir")
require.NoError(t, os.Mkdir(cwd, 0o755))
e := NewEngine(database, EngineConfig{Machine: "laptop"})
ctx := context.Background()
require.NoError(t, e.writeProjectIdentityObservation(ctx, db.Session{
ID: "identity-remote-machine",
Project: "remote-proj",
Machine: "remote-linux",
Agent: "codex",
Cwd: cwd,
StartedAt: strPtr(time.Now().UTC().Format(time.RFC3339Nano)),
}))
observations, err := database.ListProjectIdentityObservations(
ctx, []string{"remote-proj"},
)
require.NoError(t, err)
require.Len(t, observations, 1)
assert.Empty(t, observations[0].GitRemote,
"foreign-machine cwd must not be probed for a local git identity")
assert.Equal(t, cwd, observations[0].RootPath,
"root path must stay the raw cwd, not a locally resolved git root")
}
func TestProjectIdentitySafeLocalAbsolutePathHandlesWindowsDriveRootsByOS(t *testing.T) {
wantWindowsDriveLocal := runtime.GOOS == "windows"
assert.Equal(t, wantWindowsDriveLocal, safeLocalAbsolutePath(`C:\repo`))
assert.Equal(t, wantWindowsDriveLocal, safeLocalAbsolutePath("C:/repo"))
assert.False(t, safeLocalAbsolutePath("host:/srv/repo"))
}
func TestProjectIdentityWriteBatchRejectsNonNativeWindowsDriveGitRemote(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("non-Windows guard does not apply on Windows")
}
database := openTestDB(t)
base := t.TempDir()
t.Chdir(base)
root := "C:/repo"
require.NoError(t, os.MkdirAll(filepath.Join(root, ".git"), 0o755))
require.NoError(t, os.WriteFile(
filepath.Join(root, ".git", "config"),
[]byte("[remote \"origin\"]\n\turl = https://github.com/acme/windows.git\n"),
0o644,
))
cwd := root + "/subdir"
require.NoError(t, os.MkdirAll(cwd, 0o755))
e := NewEngine(database, EngineConfig{Machine: "windows-host"})
written, _, failed, _ := e.writeBatch([]pendingWrite{{
sess: parser.ParsedSession{
ID: "identity-windows",
Project: "windows",
Machine: "windows-host",
Agent: parser.AgentCodex,
Cwd: cwd,
StartedAt: time.Now(),
},
}}, syncWriteDefault, true)
require.Equal(t, 1, written)
require.Equal(t, 0, failed)
observations, err := database.ListProjectIdentityObservations(
context.Background(), []string{"windows"},
)
require.NoError(t, err)
require.Len(t, observations, 1)
assert.Equal(t, "C:/repo/subdir", observations[0].RootPath)
assert.Empty(t, observations[0].GitRemoteName)
assert.Empty(t, observations[0].GitRemote)
assert.Empty(t, observations[0].NormalizedRemote)
assert.Equal(t, "root_path", observations[0].KeySource)
assert.NotEmpty(t, observations[0].Key)
}
func TestProjectIdentityRemoteWriteSkipsLiveDiscovery(t *testing.T) {
database := openTestDB(t)
e := NewEngine(database, EngineConfig{
Machine: "remote-host",
IDPrefix: "remote-host~",
PathRewriter: func(path string) string {
return "remote-host:" + path
},
})
written, _, failed, _ := e.writeBatch([]pendingWrite{{
sess: parser.ParsedSession{
ID: "identity-remote",
Project: "remote-project",
Machine: "remote-host",
Agent: parser.AgentCodex,
Cwd: "remote-host:/srv/app",
StartedAt: time.Now(),
},
}}, syncWriteDefault, true)
require.Equal(t, 1, written)
require.Equal(t, 0, failed)
observations, err := database.ListProjectIdentityObservations(
context.Background(), []string{"remote-project"},
)
require.NoError(t, err)
require.Len(t, observations, 1)
assert.Equal(t, "remote-host:/srv/app", observations[0].RootPath)
assert.Empty(t, observations[0].GitRemote)
assert.Empty(t, observations[0].NormalizedRemote)
assert.Empty(t, observations[0].Key)
}
func TestProjectIdentityIncrementalAppendPersistsObservation(t *testing.T) {
database := openTestDB(t)
root := t.TempDir()
require.NoError(t, os.Mkdir(filepath.Join(root, ".git"), 0o755))
require.NoError(t, os.WriteFile(
filepath.Join(root, ".git", "config"),
[]byte("[remote \"origin\"]\n\turl = https://github.com/acme/inc.git\n"),
0o644,
))
e := NewEngine(database, EngineConfig{Machine: "laptop"})
start := time.Date(2026, 7, 3, 10, 0, 0, 0, time.UTC)
require.NoError(t, database.UpsertSession(db.Session{
ID: "inc-identity",
Project: "inc",
Machine: "laptop",
Agent: "codex",
Cwd: root,
StartedAt: strPtr(start.Format(time.RFC3339Nano)),
MessageCount: 1,
}))
err := e.writeIncremental(&incrementalUpdate{
sessionID: "inc-identity",
project: "inc",
machine: "laptop",
cwd: root,
msgs: []parser.ParsedMessage{{
Role: parser.RoleAssistant,
Content: "delta",
Timestamp: start.Add(time.Minute),
Ordinal: 1,
}},
msgCount: 2,
userMsgCount: 1,
fileSize: 100,
fileMtime: start.Add(time.Minute).UnixNano(),
})
require.NoError(t, err)
observations, err := database.ListProjectIdentityObservations(
context.Background(), []string{"inc"},
)
require.NoError(t, err)
require.Len(t, observations, 1)
assert.Equal(t, "https://github.com/acme/inc.git", observations[0].GitRemote)
assert.Equal(t, "github.com/acme/inc", observations[0].NormalizedRemote)
}
func TestProjectIdentityIncrementalRemoteAppendSkipsLiveDiscovery(t *testing.T) {
database := openTestDB(t)
e := NewEngine(database, EngineConfig{
Machine: "remote-host",
IDPrefix: "remote-host~",
PathRewriter: func(path string) string {
return "remote-host:" + path
},
})
require.NoError(t, database.UpsertSession(db.Session{
ID: "remote-host~inc-remote-identity",
Project: "remote-inc",
Machine: "remote-host",
Agent: "codex",
Cwd: "remote-host:/srv/app",
MessageCount: 1,
}))
err := e.writeIncremental(&incrementalUpdate{
sessionID: "remote-host~inc-remote-identity",
project: "remote-inc",
machine: "remote-host",
cwd: "remote-host:/srv/app",
msgs: []parser.ParsedMessage{{
Role: parser.RoleAssistant,
Content: "delta",
Ordinal: 1,
}},
msgCount: 2,
userMsgCount: 1,
fileSize: 100,
fileMtime: time.Now().UnixNano(),
})
require.NoError(t, err)
observations, err := database.ListProjectIdentityObservations(
context.Background(), []string{"remote-inc"},
)
require.NoError(t, err)
require.Len(t, observations, 1)
assert.Equal(t, "remote-host:/srv/app", observations[0].RootPath)
assert.Empty(t, observations[0].GitRemote)
assert.Empty(t, observations[0].Key)
}
// TestWriteBatchAntigravityReplacesMessages covers a live Antigravity
// IDE session synced before its gen_metadata rows exist: the next sync
// re-parses the same ordinals with model/token metadata attached, and
// that enrichment must reach the stored message rows rather than being
// dropped by the append-only write path.
func TestWriteBatchAntigravityReplacesMessages(t *testing.T) {
database := openTestDB(t)
e := &Engine{db: database}
ts := time.Unix(1700000000, 0).UTC()
mkWrite := func(withMeta bool) pendingWrite {
msg := parser.ParsedMessage{
Role: parser.RoleAssistant,
Content: "assistant reply",
Timestamp: ts,
}
if withMeta {
msg.Model = "Test Gemini 3.5"
msg.ContextTokens = 2400
msg.OutputTokens = 210
msg.HasContextTokens = true
msg.HasOutputTokens = true
}
return pendingWrite{
sess: parser.ParsedSession{
ID: "antigravity:meta",
Project: "proj",
Machine: "m",
Agent: parser.AgentAntigravity,
StartedAt: ts,
EndedAt: ts,
MessageCount: 1,
},
msgs: []parser.ParsedMessage{msg},
}
}
written, _, failed, _ := e.writeBatch(
[]pendingWrite{mkWrite(false)}, syncWriteDefault, false,
)
require.Equal(t, 0, failed)
require.Equal(t, 1, written)
written, _, failed, _ = e.writeBatch(
[]pendingWrite{mkWrite(true)}, syncWriteDefault, false,
)
require.Equal(t, 0, failed)
require.Equal(t, 1, written)
msgs, err := database.GetMessages(
context.Background(), "antigravity:meta", 0, 10, true,
)
require.NoError(t, err, "GetMessages")
require.Len(t, msgs, 1)
assert.Equal(t, "Test Gemini 3.5", msgs[0].Model,
"re-parsed model metadata must reach existing message rows")
}
// TestWriteBatchQwenPawReplacesMessages covers a QwenPaw session file
// being rewritten wholesale on every save. QwenPaw's
// _atomic_write_json rewrites the entire sessions/<name>.json on each
// save, and the parser assigns Ordinal by position in
// agent.memory.content. If that array is ever compacted, summarized,
// or reordered — common in agent-memory frameworks — ordinals shift,
// and the append-only writeMessages path would silently keep stale
// rows. The session must go through the replace path so a rewrite is
// applied as a delete+insert, not an ordinal-greater-than append.
func TestWriteBatchQwenPawReplacesMessages(t *testing.T) {
database := openTestDB(t)
e := &Engine{db: database}
ts := time.Unix(1700000000, 0).UTC()
mkWrite := func(content string) pendingWrite {
msg := parser.ParsedMessage{
Ordinal: 0,
Role: parser.RoleAssistant,
Content: content,
Timestamp: ts,
}
return pendingWrite{
sess: parser.ParsedSession{
ID: "qwenpaw:default:rewrite",
Project: "default",
Machine: "m",
Agent: parser.AgentQwenPaw,
StartedAt: ts,
EndedAt: ts,
MessageCount: 1,
},
msgs: []parser.ParsedMessage{msg},
}
}
written, _, failed, _ := e.writeBatch(
[]pendingWrite{mkWrite("old content")}, syncWriteDefault, false,
)
require.Equal(t, 0, failed)
require.Equal(t, 1, written)
written, _, failed, _ = e.writeBatch(
[]pendingWrite{mkWrite("new content")}, syncWriteDefault, false,
)
require.Equal(t, 0, failed)
require.Equal(t, 1, written)
msgs, err := database.GetMessages(
context.Background(), "qwenpaw:default:rewrite", 0, 10, true,
)
require.NoError(t, err, "GetMessages")
require.Len(t, msgs, 1, "rewrite must replace, not append")
assert.Equal(t, "new content", msgs[0].Content,
"rewritten content must reach existing message rows")
}
// TestSyncSingleSession_QwenPawPreservesWorkspaceFromDB covers the
// case where a QwenPaw session's stored DB file_path points outside
// any currently configured QWENPAW_DIR (e.g. the root was removed or
// the session was synced from a custom path). FindSourceFile still
// returns the stored path, and the provider must resolve the workspace
// from that path's implicit <root>/<workspace>/sessions/ layout rather
// than emitting a brand-new qwenpaw::<stem> session that orphans the
// requested qwenpaw:<workspace>:<stem> row.
func TestSyncSingleSession_QwenPawPreservesWorkspaceFromDB(t *testing.T) {
database := openTestDB(t)
// File at an arbitrary path NOT under any configured QWENPAW_DIR.
root := t.TempDir()
sessDir := filepath.Join(root, "my_ws", "sessions")
require.NoError(t, os.MkdirAll(sessDir, 0o755))
path := filepath.Join(sessDir, "default_1.json")
require.NoError(t, os.WriteFile(path, []byte(
`{"agent":{"memory":{"content":[[`+
`{"id":"u1","name":"user","role":"user","content":[{"type":"text","text":"hi"}],"metadata":{},"timestamp":"2026-04-19 22:37:34.000"},[]`+
`]]}}}`), 0o644))
// Engine configured with QWENPAW_DIR pointing somewhere else
// entirely, so the configured-root loop cannot match.
otherDir := t.TempDir()
e := NewEngine(database, EngineConfig{
AgentDirs: map[parser.AgentType][]string{
parser.AgentQwenPaw: {otherDir},
},
Machine: "local",
})
// Seed the DB with the canonical session row. file_path is the
// stored source of truth that FindSourceFile prefers.
const sessionID = "qwenpaw:my_ws:default_1"
fp := path
require.NoError(t, database.UpsertSession(db.Session{
ID: sessionID,
Project: "my_ws",
Machine: "local",
Agent: "qwenpaw",
FilePath: &fp,
}))
require.NoError(t, e.SyncSingleSession(sessionID))
got, err := database.GetSession(context.Background(), sessionID)
require.NoError(t, err)
require.NotNil(t, got, "original session must still exist")
assert.Equal(t, "my_ws", got.Project,
"workspace must be preserved when the file is outside configured roots")
// No empty-workspace orphan should have been written.
orphan, err := database.GetSession(
context.Background(), "qwenpaw::default_1",
)
require.NoError(t, err)
assert.Nil(t, orphan,
"no empty-workspace orphan session should be created")
}
// TestProcessAntigravityWALOnlyUpdateNotSkipped covers a live IDE
// session whose gen_metadata commits land in the SQLite WAL: the main
// .db file's size/mtime are unchanged, so the skip check must consult
// the sidecar set or the session never reparses.
func TestProcessAntigravityWALOnlyUpdateNotSkipped(t *testing.T) {
database := openTestDB(t)
ctx := context.Background()
root := t.TempDir()
e := NewEngine(database, EngineConfig{
AgentDirs: map[parser.AgentType][]string{
parser.AgentAntigravity: {root},
},
Machine: "local",
})
convDir := filepath.Join(root, "conversations")
require.NoError(t, os.MkdirAll(convDir, 0o755))
dbPath := filepath.Join(
convDir, "abcdabcd-1111-2222-3333-444455556666.db",
)
sqlDB, err := sql.Open("sqlite3", dbPath)
require.NoError(t, err)
_, err = sqlDB.Exec(
`CREATE TABLE steps (idx integer, step_type integer, ` +
`step_payload blob, PRIMARY KEY (idx))`,
)
require.NoError(t, err)
require.NoError(t, sqlDB.Close())
// The Antigravity provider is provider-authoritative, so processFile
// routes through processProviderFile. The provider resolves the source,
// fingerprints (folding the WAL/SHM and sidecar set into the freshness
// identity), and parses.
file := parser.DiscoveredFile{
Agent: parser.AgentAntigravity,
Path: dbPath,
}
res := e.processFile(ctx, file)
require.NoError(t, res.err)
require.False(t, res.skip)
require.Len(t, res.results, 1)
pw := pendingWrite{
sess: res.results[0].Session,
msgs: res.results[0].Messages,
usageEvents: res.results[0].UsageEvents,
forceReplace: res.forceReplace,
}
written, _, failed, _ := e.writeBatch(
[]pendingWrite{pw}, syncWriteDefault, false,
)
require.Equal(t, 0, failed)
require.Equal(t, 1, written)
// Record the skip-cache entry the collectAndBatch flow would write so the
// next unchanged processFile sees a cached, current fingerprint.
if res.cacheSkip && res.mtime != 0 && !res.noCacheSkip {
e.cacheSkip(res.skipCacheKey(file.Path), res.mtime)
}
res = e.processFile(ctx, file)
require.True(t, res.skip, "unchanged session should skip")
// WAL-only update: the main .db is untouched.
walPath := dbPath + "-wal"
require.NoError(t, os.WriteFile(walPath, []byte("wal bytes"), 0o644))
info, err := os.Stat(dbPath)
require.NoError(t, err)
walTime := info.ModTime().Add(5 * time.Second)
require.NoError(t, os.Chtimes(walPath, walTime, walTime))
res = e.processFile(ctx, file)
assert.False(t, res.skip, "WAL-only update must trigger a reparse")
}
func TestProcessVibeMetaOnlyUpdateNotSkipped(t *testing.T) {
database := openTestDB(t)
ctx := context.Background()
root := t.TempDir()
e := NewEngine(database, EngineConfig{
AgentDirs: map[parser.AgentType][]string{
parser.AgentVibe: {root},
},
})
sessionDir := filepath.Join(root, "session_20260616_083518_0107f266")
require.NoError(t, os.MkdirAll(sessionDir, 0o755))
msgPath := filepath.Join(sessionDir, "messages.jsonl")
require.NoError(t, os.WriteFile(
msgPath,
[]byte(`{"role":"user","content":"hi"}`+"\n"),
0o644,
))
metaPath := filepath.Join(sessionDir, "meta.json")
require.NoError(t, os.WriteFile(
metaPath,
[]byte(`{"session_id":"abc","title":"Original title"}`+"\n"),
0o644,
))
canonicalID := "vibe:abc"
e.SyncPaths([]string{msgPath})
sess, err := database.GetSession(ctx, canonicalID)
require.NoError(t, err)
require.NotNil(t, sess)
require.NotNil(t, sess.DisplayName)
assert.Equal(t, "Original title", *sess.DisplayName)
// meta.json-only update: messages.jsonl is untouched, but the title
// (sourced from meta.json) changes. The Vibe provider's composite
// fingerprint folds the sibling meta.json mtime in, so the change busts
// the skip cache and triggers a reparse rather than a skip.
info, err := os.Stat(msgPath)
require.NoError(t, err)
metaTime := info.ModTime().Add(5 * time.Second)
require.NoError(t, os.WriteFile(
metaPath,
[]byte(`{"session_id":"abc","title":"Renamed title"}`+"\n"),
0o644,
))
require.NoError(t, os.Chtimes(metaPath, metaTime, metaTime))
e.SyncPaths([]string{msgPath})
sess, err = database.GetSession(ctx, canonicalID)
require.NoError(t, err)
require.NotNil(t, sess)
require.NotNil(t, sess.DisplayName)
assert.Equal(t, "Renamed title", *sess.DisplayName)
}
func TestProcessAntigravityBrainOnlyUpdateNotSkipped(t *testing.T) {
database := openTestDB(t)
ctx := context.Background()
root := t.TempDir()
e := NewEngine(database, EngineConfig{
AgentDirs: map[parser.AgentType][]string{
parser.AgentAntigravity: {root},
},
Machine: "local",
})
convDir := filepath.Join(root, "conversations")
require.NoError(t, os.MkdirAll(convDir, 0o755))
id := "abcdabcd-1111-2222-3333-444455557777"
dbPath := filepath.Join(convDir, id+".db")
sqlDB, err := sql.Open("sqlite3", dbPath)
require.NoError(t, err)
_, err = sqlDB.Exec(
`CREATE TABLE steps (idx integer, step_type integer, ` +
`step_payload blob, PRIMARY KEY (idx))`,
)
require.NoError(t, err)
require.NoError(t, sqlDB.Close())
// Provider-authoritative: the provider Fingerprint folds the brain
// artifacts into the freshness identity, so a brain-only change busts the
// skip cache and triggers a reparse.
file := parser.DiscoveredFile{
Agent: parser.AgentAntigravity,
Path: dbPath,
}
res := e.processFile(ctx, file)
require.NoError(t, res.err)
require.False(t, res.skip)
require.Len(t, res.results, 1)
pw := pendingWrite{
sess: res.results[0].Session,
msgs: res.results[0].Messages,
usageEvents: res.results[0].UsageEvents,
forceReplace: res.forceReplace,
}
written, _, failed, _ := e.writeBatch(
[]pendingWrite{pw}, syncWriteDefault, false,
)
require.Equal(t, 0, failed)
require.Equal(t, 1, written)
if res.cacheSkip && res.mtime != 0 && !res.noCacheSkip {
e.cacheSkip(res.skipCacheKey(file.Path), res.mtime)
}
res = e.processFile(ctx, file)
require.True(t, res.skip, "unchanged session should skip")
// Brain-only update: the conversation DB files are untouched.
brainDir := filepath.Join(root, "brain", id)
require.NoError(t, os.MkdirAll(brainDir, 0o755))
brainPath := filepath.Join(brainDir, "task.md")
require.NoError(t, os.WriteFile(
brainPath, []byte("brain artifact body"), 0o644,
))
info, err := os.Stat(dbPath)
require.NoError(t, err)
brainTime := info.ModTime().Add(5 * time.Second)
require.NoError(t, os.Chtimes(brainPath, brainTime, brainTime))
res = e.processFile(ctx, file)
require.False(t, res.skip,
"brain-only update must trigger a reparse")
require.Len(t, res.results, 1)
var found bool
for _, m := range res.results[0].Messages {
if strings.Contains(m.Content, "brain artifact body") {
found = true
}
}
assert.True(t, found,
"reparse must pick up the brain artifact message")
}
func TestShouldSkipFileWithIDPrefix(t *testing.T) {
database := openTestDB(t)
// Store a session with prefixed ID and file metadata.
sess := db.Session{
ID: "host~abc-123",
Project: "test",
Machine: "host",
Agent: "claude",
FilePath: strPtr("host:/remote/session.jsonl"),
FileSize: int64Ptr(1024),
FileMtime: int64Ptr(
int64(1700000000000000000),
),
}
require.NoError(t, database.UpsertSession(sess))
// data_version is no longer persisted by UpsertSession;
// stamp it explicitly so the skip check sees a current
// row.
require.NoError(t, database.SetSessionDataVersion(
sess.ID, db.CurrentDataVersion(),
))
// Engine with IDPrefix should find the session.
e := &Engine{
db: database,
idPrefix: "host~",
}
got := e.shouldSkipFile(
"abc-123",
fakeFileInfo{size: 1024, mtime: 1700000000000000000},
)
assert.True(t, got, "shouldSkipFile should return true")
// Engine WITHOUT IDPrefix should NOT find it.
e2 := &Engine{db: database}
got2 := e2.shouldSkipFile(
"abc-123",
fakeFileInfo{size: 1024, mtime: 1700000000000000000},
)
assert.False(t, got2, "shouldSkipFile without prefix should return false")
}
func TestShouldSkipCodexReparsesStaleProject(t *testing.T) {
database := openTestDB(t)
path := filepath.Join(t.TempDir(), "rollout-2026-06-21T18-59-38-abc.jsonl")
require.NoError(t, os.WriteFile(path, []byte("{}\n"), 0o600))
info, err := os.Stat(path)
require.NoError(t, err, "stat codex fixture")
sess := db.Session{
ID: "host~codex:abc",
Project: "roborev_ci_28293_3831737461",
Machine: "host",
Agent: "codex",
FilePath: strPtr("host:" + path),
FileSize: int64Ptr(info.Size()),
FileMtime: int64Ptr(info.ModTime().UnixNano()),
}
require.NoError(t, database.UpsertSession(sess))
require.NoError(t, database.SetSessionDataVersion(
sess.ID, db.CurrentDataVersion(),
))
e := &Engine{
db: database,
idPrefix: "host~",
pathRewriter: func(path string) string {
return "host:" + path
},
}
assert.False(t, e.shouldSkipCodexFingerprint(path, parser.SourceFingerprint{
Size: info.Size(),
MTimeNS: info.ModTime().UnixNano(),
}), "stale generated roborev CI projects must be reparsed")
}
func TestProcessFileSkipCacheReparsesStaleCodexProject(t *testing.T) {
database := openTestDB(t)
root := t.TempDir()
path := filepath.Join(root, "rollout-2026-06-21T18-59-38-abc.jsonl")
content := testjsonl.JoinJSONL(
testjsonl.CodexSessionMetaJSON(
"abc",
"/home/roborev/.roborev/ci-worktrees/agentsview/roborev-ci-28293-3831737461",
"user",
"2024-01-01T10:00:00Z",
),
testjsonl.CodexMsgJSON("user", "review this", "2024-01-01T10:00:01Z"),
)
require.NoError(t, os.WriteFile(path, []byte(content), 0o600))
info, err := os.Stat(path)
require.NoError(t, err, "stat codex fixture")
sess := db.Session{
ID: "host~codex:abc",
Project: "roborev_ci_28293_3831737461",
Machine: "host",
Agent: "codex",
FilePath: strPtr("host:" + path),
FileSize: int64Ptr(info.Size()),
FileMtime: int64Ptr(info.ModTime().UnixNano()),
}
require.NoError(t, database.UpsertSession(sess))
require.NoError(t, database.SetSessionDataVersion(
sess.ID, db.CurrentDataVersion(),
))
e := &Engine{
db: database,
idPrefix: "host~",
skipCache: map[string]int64{path: info.ModTime().UnixNano()},
agentDirs: map[parser.AgentType][]string{
parser.AgentCodex: {root},
},
providerFactories: providerFactoryMap(parser.ProviderFactories()),
providerMigrationModes: map[parser.AgentType]parser.ProviderMigrationMode{
parser.AgentCodex: parser.ProviderMigrationProviderAuthoritative,
},
pathRewriter: func(path string) string {
return "host:" + path
},
}
res := e.processFile(context.Background(), parser.DiscoveredFile{
Agent: parser.AgentCodex,
Path: path,
})
require.NoError(t, res.err)
require.False(t, res.skip,
"remote skip cache must not hide stale generated roborev CI projects")
require.Len(t, res.results, 1)
assert.Equal(t, "agentsview", res.results[0].Session.Project)
}
func TestProcessFileSkipCacheReparsesStaleCodexDataVersion(t *testing.T) {
database := openTestDB(t)
root := t.TempDir()
path := filepath.Join(root, "rollout-2026-06-21T18-59-38-abc.jsonl")
content := testjsonl.JoinJSONL(
testjsonl.CodexSessionMetaJSON(
"abc",
"/home/user/code/agentsview",
"user",
"2024-01-01T10:00:00Z",
),
testjsonl.CodexMsgJSON("user", "review this", "2024-01-01T10:00:01Z"),
)
require.NoError(t, os.WriteFile(path, []byte(content), 0o600))
info, err := os.Stat(path)
require.NoError(t, err, "stat codex fixture")
sess := db.Session{
ID: "host~codex:abc",
Project: "agentsview",
Machine: "host",
Agent: "codex",
FilePath: strPtr("host:" + path),
FileSize: int64Ptr(info.Size()),
FileMtime: int64Ptr(info.ModTime().UnixNano()),
}
require.NoError(t, database.UpsertSession(sess))
require.NoError(t, database.SetSessionDataVersion(
sess.ID, db.CurrentDataVersion()-1,
))
e := &Engine{
db: database,
idPrefix: "host~",
skipCache: map[string]int64{path: info.ModTime().UnixNano()},
agentDirs: map[parser.AgentType][]string{
parser.AgentCodex: {root},
},
providerFactories: providerFactoryMap(parser.ProviderFactories()),
providerMigrationModes: map[parser.AgentType]parser.ProviderMigrationMode{
parser.AgentCodex: parser.ProviderMigrationProviderAuthoritative,
},
pathRewriter: func(path string) string {
return "host:" + path
},
}
res := e.processFile(context.Background(), parser.DiscoveredFile{
Agent: parser.AgentCodex,
Path: path,
})
require.NoError(t, res.err)
require.False(t, res.skip,
"skip cache must not hide stale parser data versions")
require.Len(t, res.results, 1)
}
func TestSyncPathsCodexCachedFingerprintStillRefreshesChangedTitle(t *testing.T) {
database := openTestDB(t)
root := t.TempDir()
codexDir := filepath.Join(root, "sessions")
dayDir := filepath.Join(codexDir, "2024", "01", "01")
require.NoError(t, os.MkdirAll(dayDir, 0o755))
const uuid = "019eb791-cf7d-75c1-8439-9ed74c1229ed"
path := filepath.Join(
dayDir, "rollout-2024-01-01T10-00-00-"+uuid+".jsonl",
)
content := testjsonl.JoinJSONL(
testjsonl.CodexSessionMetaJSON(
uuid, "/home/user/code/agentsview", "codex_cli_rs",
"2024-01-01T10:00:00Z",
),
testjsonl.CodexMsgJSON(
"user", "preserve this message", "2024-01-01T10:00:01Z",
),
)
require.NoError(t, os.WriteFile(path, []byte(content), 0o600))
indexPath := filepath.Join(root, parser.CodexSessionIndexFilename)
require.NoError(t, os.WriteFile(indexPath, []byte(
`{"id":"`+uuid+`","thread_name":"Original title"}`+"\n",
), 0o600))
transcriptTime := time.Now().Add(-2 * time.Hour).Truncate(time.Second)
indexTime := time.Now().Add(-time.Hour).Truncate(time.Second)
require.NoError(t, os.Chtimes(path, transcriptTime, transcriptTime))
require.NoError(t, os.Chtimes(indexPath, indexTime, indexTime))
engine := NewEngine(database, EngineConfig{
AgentDirs: map[parser.AgentType][]string{
parser.AgentCodex: {codexDir},
},
Machine: "local",
})
engine.SyncAll(context.Background(), nil)
before, err := database.GetSessionFull(
context.Background(), "codex:"+uuid,
)
require.NoError(t, err)
require.NotNil(t, before)
require.NotNil(t, before.SessionName)
require.NotNil(t, before.FileMtime)
assert.Equal(t, "Original title", *before.SessionName)
engine.cacheSkip(path, *before.FileMtime)
require.Equal(t, *before.FileMtime, engine.SnapshotSkipCache()[path],
"pre-existing skip-cache entry precondition")
require.NoError(t, os.WriteFile(indexPath, []byte(
`{"id":"`+uuid+`","thread_name":"Renamed title"}`+"\n",
), 0o600))
storedMtime := time.Unix(0, *before.FileMtime)
require.NoError(t, os.Chtimes(indexPath, storedMtime, storedMtime))
indexInfo, err := os.Stat(indexPath)
require.NoError(t, err)
require.Equal(t, *before.FileMtime, indexInfo.ModTime().UnixNano())
engine.SyncPaths([]string{path})
after, err := database.GetSessionFull(
context.Background(), "codex:"+uuid,
)
require.NoError(t, err)
require.NotNil(t, after)
require.NotNil(t, after.SessionName)
assert.Equal(t, "Renamed title", *after.SessionName)
assert.False(t, after.LastWriteIncremental)
msgs, err := database.GetAllMessages(context.Background(), "codex:"+uuid)
require.NoError(t, err)
require.Len(t, msgs, 1)
assert.Equal(t, "preserve this message", msgs[0].Content)
}
func cacheCodexProviderFingerprint(
t *testing.T, engine *Engine, path string,
) (string, int64) {
t.Helper()
factory, ok := engine.providerFactories[parser.AgentCodex]
require.True(t, ok)
provider := factory.NewProvider(parser.ProviderConfig{
Roots: engine.agentDirs[parser.AgentCodex],
Machine: engine.machine,
PathRewriter: engine.pathRewriter,
})
file := parser.DiscoveredFile{Agent: parser.AgentCodex, Path: path}
source, found, err := engine.providerSourceForDiscoveredFile(
context.Background(), provider, file,
)
require.NoError(t, err)
require.True(t, found)
fingerprint, err := provider.Fingerprint(context.Background(), source)
require.NoError(t, err)
key := providerProcessCacheKey(file, source, fingerprint)
require.NotEmpty(t, key)
return key, fingerprint.MTimeNS
}
func TestSyncPathsCodexCachedFailureWithoutStoredSessionStaysSkipped(t *testing.T) {
database := openTestDB(t)
root := t.TempDir()
codexDir := filepath.Join(root, "sessions")
dayDir := filepath.Join(codexDir, "2024", "01", "01")
require.NoError(t, os.MkdirAll(dayDir, 0o755))
const uuid = "019eb791-cf7d-75c1-8439-9ed74c1229ee"
path := filepath.Join(
dayDir, "rollout-2024-01-01T10-00-00-"+uuid+".jsonl",
)
require.NoError(t, os.WriteFile(path, []byte(testjsonl.JoinJSONL(
testjsonl.CodexSessionMetaJSON(
uuid, "/home/user/code/project-a", "codex_cli_rs",
"2024-01-01T10:00:00Z",
),
testjsonl.CodexMsgJSON(
"user", "this source must stay suppressed", "2024-01-01T10:00:01Z",
),
)), 0o600))
engine := NewEngine(database, EngineConfig{
AgentDirs: map[parser.AgentType][]string{
parser.AgentCodex: {codexDir},
},
Machine: "local",
})
cacheKey, mtime := cacheCodexProviderFingerprint(t, engine, path)
engine.cacheSkip(cacheKey, mtime)
engine.SyncPaths([]string{path})
sess, err := database.GetSessionFull(context.Background(), "codex:"+uuid)
require.NoError(t, err)
assert.Nil(t, sess,
"a cached failure without stored state must not be reparsed")
assert.Equal(t, mtime, engine.SnapshotSkipCache()[cacheKey],
"cached failure remains applicable after the skipped pass")
}
func TestSyncPathsCodexCachedTitleRefreshUsesRewrittenDBPath(t *testing.T) {
database := openTestDB(t)
root := t.TempDir()
codexDir := filepath.Join(root, "sessions")
dayDir := filepath.Join(codexDir, "2024", "01", "01")
require.NoError(t, os.MkdirAll(dayDir, 0o755))
const uuid = "019eb791-cf7d-75c1-8439-9ed74c1229ef"
filename := "rollout-2024-01-01T10-00-00-" + uuid + ".jsonl"
path := filepath.Join(dayDir, filename)
logicalPath := "remote:/sessions/" + filename
content := testjsonl.JoinJSONL(
testjsonl.CodexSessionMetaJSON(
uuid, "/home/user/code/project-a", "codex_cli_rs",
"2024-01-01T10:00:00Z",
),
testjsonl.CodexMsgJSON(
"user", "preserve rewritten session", "2024-01-01T10:00:01Z",
),
)
require.NoError(t, os.WriteFile(path, []byte(content), 0o600))
indexPath := filepath.Join(root, parser.CodexSessionIndexFilename)
require.NoError(t, os.WriteFile(indexPath, []byte(
`{"id":"`+uuid+`","thread_name":"Original title"}`+"\n",
), 0o600))
transcriptTime := time.Now().Add(-2 * time.Hour).Truncate(time.Second)
indexTime := time.Now().Add(-time.Hour).Truncate(time.Second)
require.NoError(t, os.Chtimes(path, transcriptTime, transcriptTime))
require.NoError(t, os.Chtimes(indexPath, indexTime, indexTime))
engine := NewEngine(database, EngineConfig{
AgentDirs: map[parser.AgentType][]string{
parser.AgentCodex: {codexDir},
},
Machine: "remote",
IDPrefix: "remote~",
PathRewriter: func(candidate string) string {
if filepath.Clean(candidate) == filepath.Clean(path) {
return logicalPath
}
return "remote:" + candidate
},
})
engine.SyncAll(context.Background(), nil)
const sessionID = "remote~codex:" + uuid
before, err := database.GetSessionFull(context.Background(), sessionID)
require.NoError(t, err)
require.NotNil(t, before)
require.NotNil(t, before.FilePath)
require.NotNil(t, before.FileMtime)
require.NotNil(t, before.SessionName)
assert.Equal(t, logicalPath, *before.FilePath)
assert.Equal(t, "Original title", *before.SessionName)
cacheKey, mtime := cacheCodexProviderFingerprint(t, engine, path)
engine.cacheSkip(cacheKey, mtime)
require.Equal(t, mtime, engine.SnapshotSkipCache()[cacheKey])
require.NoError(t, os.WriteFile(indexPath, []byte(
`{"id":"`+uuid+`","thread_name":"Renamed title"}`+"\n",
), 0o600))
storedMtime := time.Unix(0, *before.FileMtime)
require.NoError(t, os.Chtimes(indexPath, storedMtime, storedMtime))
indexInfo, err := os.Stat(indexPath)
require.NoError(t, err)
require.Equal(t, *before.FileMtime, indexInfo.ModTime().UnixNano())
engine.SyncPaths([]string{path})
after, err := database.GetSessionFull(context.Background(), sessionID)
require.NoError(t, err)
require.NotNil(t, after)
require.NotNil(t, after.SessionName)
require.NotNil(t, after.FilePath)
assert.Equal(t, "Renamed title", *after.SessionName)
assert.Equal(t, logicalPath, *after.FilePath)
assert.False(t, after.LastWriteIncremental)
msgs, err := database.GetAllMessages(context.Background(), sessionID)
require.NoError(t, err)
require.Len(t, msgs, 1)
assert.Equal(t, "preserve rewritten session", msgs[0].Content)
}
func TestProcessFileCodexDBFreshSkipIsNotCached(t *testing.T) {
database := openTestDB(t)
root := t.TempDir()
path := filepath.Join(root, "rollout-2026-06-21T18-59-38-abc.jsonl")
content := testjsonl.JoinJSONL(
testjsonl.CodexSessionMetaJSON(
"abc",
"/home/user/code/agentsview",
"user",
"2024-01-01T10:00:00Z",
),
testjsonl.CodexMsgJSON("user", "review this", "2024-01-01T10:00:01Z"),
)
require.NoError(t, os.WriteFile(path, []byte(content), 0o600))
info, err := os.Stat(path)
require.NoError(t, err, "stat codex fixture")
fileHash, err := ComputeFileHash(path)
require.NoError(t, err, "hash codex fixture")
sess := db.Session{
ID: "host~codex:abc",
Project: "agentsview",
Machine: "host",
Agent: "codex",
FilePath: strPtr("host:" + path),
FileSize: int64Ptr(info.Size()),
FileMtime: int64Ptr(info.ModTime().UnixNano()),
FileHash: &fileHash,
}
require.NoError(t, database.UpsertSession(sess))
require.NoError(t, database.SetSessionDataVersion(
sess.ID, db.CurrentDataVersion(),
))
e := &Engine{
db: database,
idPrefix: "host~",
skipCache: map[string]int64{},
agentDirs: map[parser.AgentType][]string{
parser.AgentCodex: {root},
},
providerFactories: providerFactoryMap(parser.ProviderFactories()),
providerMigrationModes: map[parser.AgentType]parser.ProviderMigrationMode{
parser.AgentCodex: parser.ProviderMigrationProviderAuthoritative,
},
pathRewriter: func(path string) string {
return "host:" + path
},
}
res := e.processFile(context.Background(), parser.DiscoveredFile{
Agent: parser.AgentCodex,
Path: path,
})
require.NoError(t, res.err)
require.True(t, res.skip)
assert.True(t, res.noCacheSkip)
assert.Empty(t, e.SnapshotSkipCache())
}
func TestClassifyCodexIndexPathSkipsMissingTranscript(t *testing.T) {
database := openTestDB(t)
root := t.TempDir()
codexDir := filepath.Join(root, "sessions")
require.NoError(t, os.MkdirAll(codexDir, 0o755))
indexPath := filepath.Join(root, parser.CodexSessionIndexFilename)
uuid := "019eb791-cf7d-75c1-8439-9ed74c1229e7"
missingPath := filepath.Join(
codexDir,
"2026", "06", "11",
"rollout-2026-06-11T12-44-06-"+uuid+".jsonl",
)
require.NoError(t, database.UpsertSession(db.Session{
ID: "codex:" + uuid,
Project: "agentsview",
Machine: "local",
Agent: string(parser.AgentCodex),
SessionName: strPtr("Old title"),
FilePath: &missingPath,
}))
require.NoError(t, os.WriteFile(indexPath, []byte(
`{"id":"`+uuid+`","thread_name":"New title",`+
`"updated_at":"2026-06-11T17:34:20Z"}`+"\n",
), 0o644))
engine := NewEngine(database, EngineConfig{
AgentDirs: map[parser.AgentType][]string{
parser.AgentCodex: {codexDir},
},
Machine: "local",
})
files := engine.classifyCodexIndexPath(indexPath)
assert.Empty(t, files)
}
func TestProcessCodexAppendedStaleProjectDoesFullReparse(t *testing.T) {
database := openTestDB(t)
root := t.TempDir()
path := filepath.Join(root, "rollout-2026-06-21T18-59-38-abc.jsonl")
initial := testjsonl.JoinJSONL(
testjsonl.CodexSessionMetaJSON(
"abc",
"/home/roborev/.roborev/ci-worktrees/agentsview/roborev-ci-28293-3831737461",
"user",
"2024-01-01T10:00:00Z",
),
testjsonl.CodexMsgJSON("user", "review this", "2024-01-01T10:00:01Z"),
)
require.NoError(t, os.WriteFile(path, []byte(initial), 0o600))
info, err := os.Stat(path)
require.NoError(t, err, "stat initial codex fixture")
sess := db.Session{
ID: "host~codex:abc",
Project: "roborev_ci_28293_3831737461",
Machine: "host",
Agent: "codex",
FirstMessage: strPtr("review this"),
MessageCount: 1,
UserMessageCount: 1,
FilePath: strPtr("host:" + path),
FileSize: int64Ptr(info.Size()),
FileMtime: int64Ptr(info.ModTime().UnixNano()),
NextOrdinal: 1,
}
require.NoError(t, database.UpsertSession(sess))
require.NoError(t, database.SetSessionDataVersion(
sess.ID, db.CurrentDataVersion(),
))
require.NoError(t, database.InsertMessages([]db.Message{
{
SessionID: "host~codex:abc",
Ordinal: 0,
Role: "user",
Content: "review this",
},
}))
f, err := os.OpenFile(path, os.O_APPEND|os.O_WRONLY, 0)
require.NoError(t, err, "open codex fixture for append")
_, err = f.WriteString(testjsonl.CodexMsgJSON(
"assistant", "done", "2024-01-01T10:00:02Z",
) + "\n")
require.NoError(t, err, "append codex fixture")
require.NoError(t, f.Close(), "close codex fixture")
e := &Engine{
db: database,
idPrefix: "host~",
agentDirs: map[parser.AgentType][]string{
parser.AgentCodex: {root},
},
providerFactories: providerFactoryMap(parser.ProviderFactories()),
providerMigrationModes: map[parser.AgentType]parser.ProviderMigrationMode{
parser.AgentCodex: parser.ProviderMigrationProviderAuthoritative,
},
pathRewriter: func(path string) string {
return "host:" + path
},
}
res := e.processFile(context.Background(), parser.DiscoveredFile{
Agent: parser.AgentCodex,
Path: path,
})
require.NoError(t, res.err)
require.Nil(t, res.incremental,
"stale project metadata must force full parse even when file appended")
require.Len(t, res.results, 1)
assert.Equal(t, "agentsview", res.results[0].Session.Project)
}
func TestProcessCodexAppendedStaleProjectCarriesForceReplace(t *testing.T) {
database := openTestDB(t)
root := t.TempDir()
path := filepath.Join(root, "rollout-2026-06-21T18-59-38-abc.jsonl")
initial := testjsonl.JoinJSONL(
testjsonl.CodexSessionMetaJSON(
"abc",
"/home/roborev/.roborev/ci-worktrees/agentsview/roborev-ci-28293-3831737461",
"user",
"2024-01-01T10:00:00Z",
),
testjsonl.CodexMsgJSON("user", "run command", "2024-01-01T10:00:01Z"),
testjsonl.CodexFunctionCallWithCallIDJSON(
"exec_command",
"call_cmd",
map[string]any{"cmd": "go test"},
"2024-01-01T10:00:02Z",
),
)
require.NoError(t, os.WriteFile(path, []byte(initial), 0o600))
info, err := os.Stat(path)
require.NoError(t, err, "stat initial codex fixture")
sess := db.Session{
ID: "host~codex:abc",
Project: "roborev_ci_28293_3831737461",
Machine: "host",
Agent: "codex",
FirstMessage: strPtr("run command"),
MessageCount: 2,
UserMessageCount: 1,
FilePath: strPtr("host:" + path),
FileSize: int64Ptr(info.Size()),
FileMtime: int64Ptr(info.ModTime().UnixNano()),
NextOrdinal: 2,
}
require.NoError(t, database.UpsertSession(sess))
require.NoError(t, database.SetSessionDataVersion(
sess.ID, db.CurrentDataVersion(),
))
require.NoError(t, database.InsertMessages([]db.Message{
{
SessionID: "host~codex:abc",
Ordinal: 0,
Role: "user",
Content: "run command",
},
{
SessionID: "host~codex:abc",
Ordinal: 1,
Role: "assistant",
ToolCalls: []db.ToolCall{
{
ToolUseID: "call_cmd",
ToolName: "exec_command",
InputJSON: `{"cmd":"go test"}`,
},
},
},
}))
f, err := os.OpenFile(path, os.O_APPEND|os.O_WRONLY, 0)
require.NoError(t, err, "open codex fixture for append")
_, err = f.WriteString(testjsonl.CodexFunctionCallOutputJSON(
"call_cmd", `{"status":"ok"}`, "2024-01-01T10:00:03Z",
) + "\n")
require.NoError(t, err, "append codex fixture")
require.NoError(t, f.Close(), "close codex fixture")
e := &Engine{
db: database,
idPrefix: "host~",
agentDirs: map[parser.AgentType][]string{
parser.AgentCodex: {root},
},
providerFactories: providerFactoryMap(parser.ProviderFactories()),
providerMigrationModes: map[parser.AgentType]parser.ProviderMigrationMode{
parser.AgentCodex: parser.ProviderMigrationProviderAuthoritative,
},
pathRewriter: func(path string) string {
return "host:" + path
},
}
res := e.processFile(context.Background(), parser.DiscoveredFile{
Agent: parser.AgentCodex,
Path: path,
})
require.NoError(t, res.err)
require.Nil(t, res.incremental,
"stale project metadata must force full parse even when file appended")
require.Len(t, res.results, 1)
assert.Equal(t, "agentsview", res.results[0].Session.Project)
assert.True(t, res.forceReplace,
"fallback-triggering appended data must replace existing messages")
}
type incrementalRequestRecorder struct {
parser.ProviderBase
request parser.IncrementalRequest
}
func TestStampProviderFileIdentityPreservesProviderSnapshotIdentity(t *testing.T) {
root := t.TempDir()
path := filepath.Join(root, "session.jsonl")
require.NoError(t, os.WriteFile(path, []byte("old snapshot\n"), 0o600))
oldInfo, err := os.Stat(path)
require.NoError(t, err)
oldInode, oldDevice := getFileIdentity(oldInfo)
replacementPath := path + ".replacement"
require.NoError(t, os.WriteFile(replacementPath, []byte("new pathname\n"), 0o600))
if runtime.GOOS == "windows" {
require.NoError(t, os.Remove(path))
}
require.NoError(t, os.Rename(replacementPath, path))
replacementInfo, err := os.Stat(path)
require.NoError(t, err)
replacementInode, replacementDevice := getFileIdentity(replacementInfo)
// Platforms without file identity report 0/0. Use a provider-owned token
// there so this still proves that an authoritative nonzero result is not
// erased merely because a later path stat cannot supply an identity.
authoritativeInode, authoritativeDevice := oldInode, oldDevice
if authoritativeInode == 0 && authoritativeDevice == 0 {
authoritativeInode, authoritativeDevice = 101, 202
}
provider := &incrementalRequestRecorder{ProviderBase: parser.ProviderBase{
Def: parser.AgentDef{Type: parser.AgentCodex},
Caps: parser.Capabilities{Source: parser.SourceCapabilities{
IncrementalAppend: parser.CapabilitySupported,
}},
}}
results := []parser.ParseResult{
{Session: parser.ParsedSession{File: parser.FileInfo{
Path: path, Inode: authoritativeInode, Device: authoritativeDevice,
}}},
{Session: parser.ParsedSession{File: parser.FileInfo{Path: path}}},
}
(&Engine{}).stampProviderFileIdentity(
provider,
parser.SourceRef{Provider: parser.AgentCodex, DisplayPath: path},
results,
)
assert.Equal(t, authoritativeInode, results[0].Session.File.Inode)
assert.Equal(t, authoritativeDevice, results[0].Session.File.Device)
assert.Equal(t, replacementInode, results[1].Session.File.Inode)
assert.Equal(t, replacementDevice, results[1].Session.File.Device)
}
func TestProviderProcessCacheKeyCodexIncludesContentHash(t *testing.T) {
path := filepath.Join(t.TempDir(), "session.jsonl")
file := parser.DiscoveredFile{Path: path, Agent: parser.AgentCodex}
source := parser.SourceRef{
Provider: parser.AgentCodex,
FingerprintKey: path,
}
first := providerProcessCacheKey(file, source, parser.SourceFingerprint{
Key: path, Hash: "first-content-hash",
})
second := providerProcessCacheKey(file, source, parser.SourceFingerprint{
Key: path, Hash: "second-content-hash",
})
assert.Equal(t, path+"?source_hash=first-content-hash", first)
assert.Equal(t, path+"?source_hash=second-content-hash", second)
assert.NotEqual(t, first, second,
"same-stat content rewrites must not reuse a rowless skip entry")
}
func (p *incrementalRequestRecorder) Parse(
context.Context,
parser.ParseRequest,
) (parser.ParseOutcome, error) {
return parser.ParseOutcome{}, errors.New("unexpected full parse")
}
func (p *incrementalRequestRecorder) ParseIncremental(
_ context.Context,
req parser.IncrementalRequest,
) (parser.IncrementalOutcome, parser.IncrementalStatus, error) {
p.request = req
return parser.IncrementalOutcome{
SessionID: req.SessionID,
ConsumedBytes: 3,
}, parser.IncrementalApplied, nil
}
func TestTryProviderIncrementalAppendPassesPersistedSessionID(t *testing.T) {
database := openTestDB(t)
root := t.TempDir()
path := filepath.Join(
root, "rollout-2024-01-01T00-00-00-abc.jsonl",
)
require.NoError(t, os.WriteFile(path, []byte("{}\n{}\n"), 0o600))
info, err := os.Stat(path)
require.NoError(t, err)
const persistedID = "remote~codex:abc"
require.NoError(t, database.UpsertSession(db.Session{
ID: persistedID,
Project: "project",
Machine: "remote",
Agent: string(parser.AgentCodex),
FirstMessage: strPtr("hello"),
MessageCount: 1,
UserMessageCount: 1,
FilePath: strPtr(path),
FileSize: int64Ptr(3),
FileMtime: int64Ptr(info.ModTime().UnixNano()),
NextOrdinal: 1,
}))
require.NoError(t, database.SetSessionDataVersion(
persistedID, db.CurrentDataVersion(),
))
require.NoError(t, database.InsertMessages([]db.Message{{
SessionID: persistedID,
Ordinal: 0,
Role: "user",
Content: "hello",
}}))
provider := &incrementalRequestRecorder{ProviderBase: parser.ProviderBase{
Def: parser.AgentDef{
Type: parser.AgentCodex,
IDPrefix: "codex:",
},
Caps: parser.Capabilities{Source: parser.SourceCapabilities{
IncrementalAppend: parser.CapabilitySupported,
}},
}}
e := &Engine{
db: database,
idPrefix: "remote~",
}
source := parser.SourceRef{
Provider: parser.AgentCodex,
DisplayPath: path,
FingerprintKey: path,
}
result, applied := e.tryProviderIncrementalAppend(
context.Background(),
provider,
source,
parser.DiscoveredFile{Agent: parser.AgentCodex, Path: path},
parser.SourceFingerprint{
Key: path,
Size: info.Size(),
MTimeNS: info.ModTime().UnixNano(),
},
)
require.True(t, applied)
require.NotNil(t, result.incremental)
assert.Equal(t, persistedID, provider.request.SessionID,
"provider continuation identity must come from the persisted row")
assert.Equal(t, persistedID, result.incremental.sessionID)
}
func TestCollectAndBatchPrefixesParserExcludedIDs(t *testing.T) {
database := openTestDB(t)
ctx := context.Background()
raw := db.Session{
ID: "probe",
Project: "local",
Machine: "local",
Agent: "claude",
}
prefixed := db.Session{
ID: "host~probe",
Project: "remote",
Machine: "host",
Agent: "claude",
}
require.NoError(t, database.UpsertSession(raw))
require.NoError(t, database.UpsertSession(prefixed))
results := make(chan syncJob, 1)
results <- syncJob{
processResult: processResult{
excludedSessionIDs: []string{"probe"},
},
path: "/remote/probe.jsonl",
}
close(results)
e := &Engine{db: database, idPrefix: "host~"}
stats := e.collectAndBatch(
ctx, results, 1, 1, nil, syncWriteDefault,
)
assert.Equal(t, []string{"host~probe"}, stats.parserExcludedIDs)
gotRaw, err := database.GetSession(ctx, "probe")
require.NoError(t, err, "raw local session lookup")
assert.NotNil(t, gotRaw, "raw local session must not be deleted")
gotPrefixed, err := database.GetSession(ctx, "host~probe")
require.NoError(t, err, "prefixed remote session lookup")
assert.Nil(t, gotPrefixed, "prefixed remote session should be deleted")
}
func TestShouldSkipByPathWithRewriter(t *testing.T) {
database := openTestDB(t)
// Store a session with rewritten file path.
sess := db.Session{
ID: "host~codex:abc",
Project: "test",
Machine: "host",
Agent: "codex",
FilePath: strPtr("host:/remote/codex/abc.jsonl"),
FileSize: int64Ptr(2048),
FileMtime: int64Ptr(
int64(1700000000000000000),
),
}
require.NoError(t, database.UpsertSession(sess))
require.NoError(t, database.SetSessionDataVersion(
sess.ID, db.CurrentDataVersion(),
))
rewriter := func(p string) string {
return "host:" + p
}
// Engine with PathRewriter should find the session.
e := &Engine{
db: database,
pathRewriter: rewriter,
}
got := e.shouldSkipByPath(
"/remote/codex/abc.jsonl",
fakeFileInfo{size: 2048, mtime: 1700000000000000000},
)
assert.True(t, got, "shouldSkipByPath should return true")
// Without rewriter, lookup misses.
e2 := &Engine{db: database}
got2 := e2.shouldSkipByPath(
"/remote/codex/abc.jsonl",
fakeFileInfo{size: 2048, mtime: 1700000000000000000},
)
assert.False(t, got2, "shouldSkipByPath without rewriter should return false")
}
// writeAiderHistory writes a two-content-run plus one header-only-run
// history file under a fresh repo dir and returns its path. The header-only
// trailing run produces no session, so a fan-out parse yields two sessions.
func writeAiderHistory(t *testing.T) string {
t.Helper()
repo := filepath.Join(t.TempDir(), "myrepo")
require.NoError(t, os.MkdirAll(repo, 0o755))
path := filepath.Join(repo, parser.AiderHistoryFileName())
content := "# aider chat started at 2026-06-09 14:01:00\n" +
"#### first prompt\nanswer one\n" +
"# aider chat started at 2026-06-09 15:30:00\n" +
"#### second prompt\nanswer two\n" +
"# aider chat started at 2026-06-09 16:45:00\n"
require.NoError(t, os.WriteFile(path, []byte(content), 0o644))
return path
}
func newAiderProviderTestEngine(
database *db.DB,
path string,
forceParse bool,
) *Engine {
root := filepath.Dir(filepath.Dir(path))
return &Engine{
db: database,
machine: "local",
forceParse: forceParse,
skipCache: make(map[string]int64),
agentDirs: map[parser.AgentType][]string{
parser.AgentAider: {root},
},
providerFactories: providerFactoryMap(parser.ProviderFactories()),
providerMigrationModes: map[parser.AgentType]parser.ProviderMigrationMode{
parser.AgentAider: parser.ProviderMigrationProviderAuthoritative,
},
}
}
func persistAiderProviderResults(
t *testing.T,
database *db.DB,
results []parser.ParseResult,
mutate func(index int, session *db.Session, dataVersion *int),
) {
t.Helper()
for i, r := range results {
dataVersion := db.CurrentDataVersion()
row := db.Session{
ID: r.Session.ID,
Project: r.Session.Project,
Machine: "local",
Agent: string(parser.AgentAider),
FilePath: strPtr(r.Session.File.Path),
FileSize: int64Ptr(r.Session.File.Size),
FileMtime: int64Ptr(r.Session.File.Mtime),
FileHash: strPtr(r.Session.File.Hash),
}
if mutate != nil {
mutate(i, &row, &dataVersion)
}
require.NoError(t, database.UpsertSession(row))
require.NoError(t, database.SetSessionDataVersion(row.ID, dataVersion))
}
}
// TestProcessFileAiderProviderFanOut verifies the migrated Aider provider, run
// through processFile, fans one history file out into one session per
// content-bearing run under stable "<history>#<idx>" virtual paths and
// force-replaces on parse. An unchanged re-sync drops every already-current run
// (the per-run skip that the legacy aiderFileUnchanged provided, now handled by
// dropUnchangedSharedSQLiteResults), while a forced parse re-emits them all.
func TestProcessFileAiderProviderFanOut(t *testing.T) {
database := openTestDB(t)
path := writeAiderHistory(t)
file := parser.DiscoveredFile{Agent: parser.AgentAider, Path: path}
res := newAiderProviderTestEngine(database, path, false).
processFile(context.Background(), file)
require.NoError(t, res.err)
require.True(t, res.forceReplace,
"aider fan-out must force-replace stored runs")
require.Len(t, res.results, 2,
"two content-bearing runs must each produce a session")
for _, r := range res.results {
historyPath, _, ok := parser.ParseAiderVirtualPath(r.Session.File.Path)
require.True(t, ok, "each run is stored under a virtual run path")
assert.Equal(t, path, historyPath)
assert.Equal(t, parser.AgentAider, r.Session.Agent)
}
// Persist the parsed runs as current so the unchanged re-sync can drop them.
persistAiderProviderResults(t, database, res.results, nil)
again := newAiderProviderTestEngine(database, path, false).
processFile(context.Background(), file)
require.NoError(t, again.err)
assert.Empty(t, again.results,
"an unchanged aider history must drop every already-current run")
forced := newAiderProviderTestEngine(database, path, true).
processFile(context.Background(), file)
require.NoError(t, forced.err)
assert.Len(t, forced.results, 2,
"a forced parse must re-emit every content-bearing run")
}
func TestProcessFileAiderProviderSameMtimeContentChangeIgnoresSkipCache(t *testing.T) {
database := openTestDB(t)
path := writeAiderHistory(t)
file := parser.DiscoveredFile{Agent: parser.AgentAider, Path: path}
initial := newAiderProviderTestEngine(database, path, false).
processFile(context.Background(), file)
require.NoError(t, initial.err)
require.Len(t, initial.results, 2)
persistAiderProviderResults(t, database, initial.results, nil)
mtime := time.Unix(0, initial.mtime)
updated := "# aider chat started at 2026-06-09 14:01:00\n" +
"#### first prompt\nanswer one changed\n" +
"# aider chat started at 2026-06-09 15:30:00\n" +
"#### second prompt\nanswer two changed\n"
require.NoError(t, os.WriteFile(path, []byte(updated), 0o644))
require.NoError(t, os.Chtimes(path, mtime, mtime))
engine := newAiderProviderTestEngine(database, path, false)
engine.cacheSkip(path, initial.mtime)
after := engine.processFile(context.Background(), file)
require.NoError(t, after.err)
assert.False(t, after.skip,
"a stale mtime-only skip cache entry must not bypass Aider hashing")
assert.Len(t, after.results, 2,
"same-mtime content changes must re-emit every Aider run")
}
func TestProcessFileAiderProviderSkipCacheDoesNotHidePartialOrStaleRows(t *testing.T) {
tests := []struct {
name string
mutate func(index int, session *db.Session, dataVersion *int)
storeRows func([]parser.ParseResult) []parser.ParseResult
}{
{
name: "missing run row",
storeRows: func(results []parser.ParseResult) []parser.ParseResult {
require.Len(t, results, 2)
return results[:1]
},
},
{
name: "stale data version",
mutate: func(_ int, _ *db.Session, dataVersion *int) {
*dataVersion = db.CurrentDataVersion() - 1
},
},
{
name: "stale hash",
mutate: func(_ int, session *db.Session, _ *int) {
session.FileHash = strPtr("stale-hash")
},
},
{
name: "stale mtime",
mutate: func(_ int, session *db.Session, _ *int) {
session.FileMtime = int64Ptr(1)
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
database := openTestDB(t)
path := writeAiderHistory(t)
file := parser.DiscoveredFile{Agent: parser.AgentAider, Path: path}
initial := newAiderProviderTestEngine(database, path, false).
processFile(context.Background(), file)
require.NoError(t, initial.err)
require.Len(t, initial.results, 2)
rows := initial.results
if tt.storeRows != nil {
rows = tt.storeRows(rows)
}
persistAiderProviderResults(t, database, rows, tt.mutate)
engine := newAiderProviderTestEngine(database, path, false)
engine.cacheSkip(path, initial.mtime)
after := engine.processFile(context.Background(), file)
require.NoError(t, after.err)
assert.False(t, after.skip,
"a generic skip cache entry must not hide %s", tt.name)
assert.NotEmpty(t, after.results)
})
}
}
func TestFindSourceFileProviderAuthoritativePrefersProviderOverStoredPath(t *testing.T) {
database := openTestDB(t)
root := t.TempDir()
stalePath := filepath.Join(root, "stale.jsonl")
currentPath := filepath.Join(root, "current.jsonl")
require.NoError(t, os.WriteFile(stalePath, []byte("{}\n"), 0o644))
require.NoError(t, os.WriteFile(currentPath, []byte("{}\n"), 0o644))
require.NoError(t, database.UpsertSession(db.Session{
ID: "cowork:lookup",
Project: "project",
Machine: "local",
Agent: string(parser.AgentCowork),
FilePath: strPtr(stalePath),
}))
provider := &lookupSourceProvider{
ProviderBase: parser.ProviderBase{
Def: parser.AgentDef{
Type: parser.AgentCowork,
DisplayName: "Cowork",
IDPrefix: "cowork:",
FileBased: true,
},
Caps: parser.Capabilities{
Source: parser.SourceCapabilities{
FindSource: parser.CapabilitySupported,
},
},
},
source: parser.SourceRef{
Provider: parser.AgentCowork,
Key: currentPath,
DisplayPath: currentPath,
FingerprintKey: currentPath,
},
}
engine := &Engine{
db: database,
agentDirs: map[parser.AgentType][]string{
parser.AgentCowork: {root},
},
providerFactories: providerFactoryMap([]parser.ProviderFactory{
lookupSourceFactory{provider: provider},
}),
providerMigrationModes: map[parser.AgentType]parser.ProviderMigrationMode{
parser.AgentCowork: parser.ProviderMigrationProviderAuthoritative,
},
}
got := engine.FindSourceFile("cowork:lookup")
assert.Equal(t, currentPath, got)
require.Len(t, provider.findRequests, 1)
assert.Equal(t, "lookup", provider.findRequests[0].RawSessionID)
assert.Equal(t, "cowork:lookup", provider.findRequests[0].FullSessionID)
assert.Equal(t, stalePath, provider.findRequests[0].StoredFilePath)
assert.Equal(t, stalePath, provider.findRequests[0].FingerprintKey)
assert.True(t, provider.findRequests[0].RequireFreshSource)
}
// TestStripVirtualSourceSuffixAider verifies that an aider
// <history>#<runIdx> virtual path strips back to its physical history file,
// so parse-diff missing-run and parse-error reporting keys on the on-disk
// file rather than the run-scoped virtual path.
func TestStripVirtualSourceSuffixAider(t *testing.T) {
historyPath := "/home/user/myrepo/" + parser.AiderHistoryFileName()
virtual := parser.AiderVirtualPath(historyPath, 3)
assert.Equal(t, historyPath, stripVirtualSourceSuffix(virtual),
"the run-index suffix must strip to the physical history path")
}
type lookupSourceFactory struct {
provider *lookupSourceProvider
}
func (f lookupSourceFactory) Definition() parser.AgentDef {
return f.provider.Definition()
}
func (f lookupSourceFactory) Capabilities() parser.Capabilities {
return f.provider.Capabilities()
}
func (f lookupSourceFactory) NewProvider(parser.ProviderConfig) parser.Provider {
return f.provider
}
type lookupSourceProvider struct {
parser.ProviderBase
source parser.SourceRef
findRequests []parser.FindSourceRequest
}
func (p *lookupSourceProvider) FindSource(
_ context.Context,
req parser.FindSourceRequest,
) (parser.SourceRef, bool, error) {
p.findRequests = append(p.findRequests, req)
return p.source, true, nil
}
func (p *lookupSourceProvider) Parse(
context.Context,
parser.ParseRequest,
) (parser.ParseOutcome, error) {
return parser.ParseOutcome{}, nil
}
func TestToDBSessionStoresSessionName(t *testing.T) {
pw := pendingWrite{sess: parser.ParsedSession{
ID: "commandcode:test",
Project: "sample_project",
Machine: "local",
Agent: parser.AgentCommandCode,
SessionName: "Startup investigation",
FirstMessage: "Inspect server logs",
}}
got := toDBSession(pw)
require.NotNil(t, got.SessionName)
assert.Equal(t, "Startup investigation", *got.SessionName)
require.NotNil(t, got.FirstMessage)
assert.Equal(t, "Inspect server logs", *got.FirstMessage)
}
func TestBlockedCategorySet(t *testing.T) {
tests := []struct {
name string
input []string
check string
want bool
}{
{"exact match", []string{"Read"}, "Read", true},
{"lowercase normalized", []string{"read"}, "Read", true},
{"uppercase normalized", []string{"GLOB"}, "Glob", true},
{"trimmed", []string{" Read "}, "Read", true},
{"empty entry skipped", []string{""}, "Read", false},
{"nil input", nil, "Read", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
m := blockedCategorySet(tt.input)
got := m[tt.check]
assert.Equal(t, tt.want, got,
"blockedCategorySet(%v)[%q]", tt.input, tt.check)
})
}
}
func TestOpenCodeLegacyArchiveLooksIncomplete(t *testing.T) {
stored := []db.Message{
{
Ordinal: 1,
Role: "assistant",
ContentLength: 100,
HasOutputTokens: true,
OutputTokens: 200,
HasContextTokens: true,
ContextTokens: 400,
ToolCalls: []db.ToolCall{{ToolName: "Read"}},
HasThinking: true,
},
}
t.Run("extra parsed messages still preserve incomplete prefix", func(t *testing.T) {
parsed := []db.Message{
{
Ordinal: 1,
Role: "assistant",
ContentLength: 50,
HasOutputTokens: false,
HasContextTokens: false,
ToolCalls: nil,
HasThinking: false,
},
{
Ordinal: 2,
Role: "assistant",
ContentLength: 25,
},
}
require.True(t, openCodeLegacyArchiveLooksIncomplete(parsed, stored),
"want incomplete archive detection")
})
t.Run("extra parsed messages with complete prefix do not preserve", func(t *testing.T) {
parsed := []db.Message{
{
Ordinal: 1,
Role: "assistant",
ContentLength: 100,
HasOutputTokens: true,
OutputTokens: 200,
HasContextTokens: true,
ContextTokens: 400,
ToolCalls: []db.ToolCall{{ToolName: "Read"}},
HasThinking: true,
},
{
Ordinal: 2,
Role: "assistant",
ContentLength: 25,
},
}
require.False(t, openCodeLegacyArchiveLooksIncomplete(parsed, stored),
"got incomplete archive detection, want false")
})
t.Run("stripped control bytes cannot pad parsed content", func(t *testing.T) {
stored := []db.Message{
{
Ordinal: 1,
Role: "assistant",
Content: "complete archived content",
ContentLength: len("complete archived content"),
},
}
parsed := []db.Message{
{
Ordinal: 1,
Role: "assistant",
Content: "short" + strings.Repeat("\x00", 20),
ContentLength: len("complete archived content"),
},
}
require.True(t, openCodeLegacyArchiveLooksIncomplete(parsed, stored),
"want sanitized parsed content to preserve complete archive")
})
}
func TestVisualStudioCopilotArchiveDecisionMergesNewRowsWithArchiveOnlyRows(t *testing.T) {
stored := []db.Message{
{
Ordinal: 0,
Role: "assistant",
Content: "Run command: dotnet build",
ContentLength: len("Run command: dotnet build"),
Timestamp: "2026-06-12T19:46:40Z",
},
{
Ordinal: 1,
Role: "user",
Content: "Archived prompt.",
ContentLength: len("Archived prompt."),
Timestamp: "2026-06-12T19:47:00Z",
},
}
parsed := []db.Message{
{
Ordinal: 0,
Role: "assistant",
Content: "Run command: dotnet build",
ContentLength: len("Run command: dotnet build"),
Timestamp: "2026-06-12T19:46:40Z",
},
{
Ordinal: 1,
Role: "user",
Content: "New follow-up.",
ContentLength: len("New follow-up."),
Timestamp: "2026-06-12T19:47:20Z",
},
}
decision := visualStudioCopilotArchiveDecision(parsed, stored)
require.False(t, decision.preserve)
require.Len(t, decision.merged, 3)
assert.Equal(t, "Run command: dotnet build", decision.merged[0].Content)
assert.Equal(t, "Archived prompt.", decision.merged[1].Content)
assert.Equal(t, "New follow-up.", decision.merged[2].Content)
for i, msg := range decision.merged {
assert.Equal(t, i, msg.Ordinal)
}
}
// TestPrepareSessionWriteReclampsMessageDerivedTokenTotals proves the full
// write path does not strand a corrupt per-message token value in the session
// aggregates. A message with an implausible OutputTokens/ContextTokens is
// clamped to maxPlausibleTokens in its row, so the message-derived session
// totals must be re-derived from the clamped rows -- while a legitimately
// large sum over many messages (above the per-message bound) is preserved.
func TestPrepareSessionWriteReclampsMessageDerivedTokenTotals(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)
msgs := []parser.ParsedMessage{
{
Ordinal: 0, Role: parser.RoleAssistant, Content: "a",
ContentLength: 1, Timestamp: ts,
OutputTokens: 1_000_000, HasOutputTokens: true,
ContextTokens: 1_500_000, HasContextTokens: true,
},
{
Ordinal: 1, Role: parser.RoleAssistant, Content: "b",
ContentLength: 1, Timestamp: ts.Add(time.Second),
OutputTokens: 1_500_000, HasOutputTokens: true,
ContextTokens: 1_000_000, HasContextTokens: true,
},
{
Ordinal: 2, Role: parser.RoleAssistant, Content: "c",
ContentLength: 1, Timestamp: ts.Add(2 * time.Second),
// Corrupt: both counts are far above maxPlausibleTokens and
// will be clamped to it in the stored row.
OutputTokens: 999_999_999, HasOutputTokens: true,
ContextTokens: 999_999_999, HasContextTokens: true,
},
}
newSess := func() parser.ParsedSession {
return parser.ParsedSession{
ID: "tok-session", Project: "proj", Machine: "test-machine",
Agent: parser.AgentClaude, StartedAt: ts,
EndedAt: ts.Add(time.Minute), MessageCount: len(msgs),
File: parser.FileInfo{
Path: "/tmp/tok.jsonl", Size: 10, Mtime: ts.UnixNano(),
},
}
}
// Message-derived totals (the parser accumulated them via
// accumulateMessageTokenUsage): sum of output and peak context, raw.
sess := newSess()
sess.TotalOutputTokens = 1_000_000 + 1_500_000 + 999_999_999
sess.HasTotalOutputTokens = true
sess.PeakContextTokens = 999_999_999
sess.HasPeakContextTokens = true
prepared, dbMsgs, verdict := e.prepareSessionWrite(
pendingWrite{sess: sess, msgs: msgs}, nil,
)
require.Equal(t, sessionWriteOK, verdict)
require.Len(t, dbMsgs, 3)
// The corrupt message row is clamped to the per-message bound.
assert.Equal(t, maxPlausibleTokens, dbMsgs[2].OutputTokens,
"corrupt message OutputTokens clamped")
assert.Equal(t, maxPlausibleTokens, dbMsgs[2].ContextTokens,
"corrupt message ContextTokens clamped")
// The session total is re-derived from the clamped rows: a legitimately
// large sum (above maxPlausibleTokens) survives, the corrupt value does
// not pollute it.
assert.Equal(t, 1_000_000+1_500_000+maxPlausibleTokens,
prepared.TotalOutputTokens,
"message-derived total re-derived from clamped rows")
assert.Equal(t, maxPlausibleTokens, prepared.PeakContextTokens,
"message-derived peak re-derived from clamped rows")
// Summary-derived totals (agents like Warp/Vibe set the session totals
// directly, not from per-message rows) must survive the per-message
// clamp untouched: they do not match the message-derived values.
const summaryTotal = 4_242_424
const summaryPeak = 3_333_333
summarySess := newSess()
summarySess.TotalOutputTokens = summaryTotal
summarySess.HasTotalOutputTokens = true
summarySess.PeakContextTokens = summaryPeak
summarySess.HasPeakContextTokens = true
preparedSummary, _, verdict := e.prepareSessionWrite(
pendingWrite{sess: summarySess, msgs: msgs}, nil,
)
require.Equal(t, sessionWriteOK, verdict)
assert.Equal(t, summaryTotal, preparedSummary.TotalOutputTokens,
"summary-derived total left untouched by per-message clamp")
assert.Equal(t, summaryPeak, preparedSummary.PeakContextTokens,
"summary-derived peak left untouched by per-message clamp")
}
// TestPrepareSessionWriteReclampsEventDerivedTokenTotals covers the
// usage-event-derived case (VS Code Copilot accumulates session totals from
// per-turn usage events, not per-message rows). A corrupt usage event is
// clamped in its usage_events row, so the event-derived session aggregates
// must be re-derived from the clamped events rather than left at the raw
// inflated value.
func TestPrepareSessionWriteReclampsEventDerivedTokenTotals(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)
// Per-message rows carry no tokens; the tokens live in usage events.
msgs := []parser.ParsedMessage{
{
Ordinal: 0, Role: parser.RoleUser, Content: "q",
ContentLength: 1, Timestamp: ts,
},
{
Ordinal: 1, Role: parser.RoleAssistant, Content: "a",
ContentLength: 1, Timestamp: ts.Add(time.Second),
},
}
events := []parser.ParsedUsageEvent{
{OutputTokens: 1_000_000, InputTokens: 1_500_000},
{OutputTokens: 1_500_000, InputTokens: 1_000_000},
// Corrupt event: both counts are clamped in the usage_events row.
{OutputTokens: 999_999_999, InputTokens: 999_999_999},
}
sess := parser.ParsedSession{
ID: "evt-session", Project: "proj", Machine: "test-machine",
Agent: parser.AgentVSCodeCopilot, StartedAt: ts,
EndedAt: ts.Add(time.Minute), MessageCount: len(msgs),
File: parser.FileInfo{
Path: "/tmp/evt.json", Size: 10, Mtime: ts.UnixNano(),
},
// Event-derived aggregates, raw (as the parser accumulates them):
// sum of event output tokens, peak of event input tokens.
TotalOutputTokens: 1_000_000 + 1_500_000 + 999_999_999,
HasTotalOutputTokens: true,
PeakContextTokens: 999_999_999,
HasPeakContextTokens: true,
}
prepared, _, verdict := e.prepareSessionWrite(
pendingWrite{sess: sess, msgs: msgs, usageEvents: events}, nil,
)
require.Equal(t, sessionWriteOK, verdict)
assert.Equal(t, 1_000_000+1_500_000+maxPlausibleTokens,
prepared.TotalOutputTokens,
"event-derived total re-derived from clamped usage events")
assert.Equal(t, maxPlausibleTokens, prepared.PeakContextTokens,
"event-derived peak re-derived from clamped usage events")
}
func TestPrepareSessionWritePreservesSummaryUsageEventTokenTotals(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)
rawTotal := maxPlausibleTokens + 500_000
rawPeak := maxPlausibleTokens + 250_000
msgs := []parser.ParsedMessage{
{
Ordinal: 0, Role: parser.RoleUser, Content: "q",
ContentLength: 1, Timestamp: ts,
},
{
Ordinal: 1, Role: parser.RoleAssistant, Content: "a",
ContentLength: 1, Timestamp: ts.Add(time.Second),
},
}
events := []parser.ParsedUsageEvent{{
Source: "session",
Model: "claude-sonnet-4",
InputTokens: rawPeak,
OutputTokens: rawTotal,
}}
sess := parser.ParsedSession{
ID: "summary-event-session", Project: "proj", Machine: "test-machine",
Agent: parser.AgentHermes, StartedAt: ts,
EndedAt: ts.Add(time.Minute), MessageCount: len(msgs),
File: parser.FileInfo{
Path: "/tmp/summary.json", Size: 10, Mtime: ts.UnixNano(),
},
TotalOutputTokens: rawTotal,
HasTotalOutputTokens: true,
PeakContextTokens: rawPeak,
HasPeakContextTokens: true,
}
prepared, _, verdict := e.prepareSessionWrite(
pendingWrite{sess: sess, msgs: msgs, usageEvents: events}, nil,
)
require.Equal(t, sessionWriteOK, verdict)
assert.Equal(t, rawTotal, prepared.TotalOutputTokens,
"session-summary usage event must not make the session aggregate event-derived")
assert.Equal(t, rawPeak, prepared.PeakContextTokens,
"summary-derived peak context must survive the per-row event clamp")
}
// TestPrepareSessionWriteReclampsEventDerivedCacheContext covers an
// event-derived peak context that, like the parser-side rollup, sums input and
// cache tokens. A corrupt cache value is clamped per-component in its
// usage_events row, so the event-derived peak must be re-derived from the
// clamped components rather than left at the raw inflated value.
func TestPrepareSessionWriteReclampsEventDerivedCacheContext(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)
msgs := []parser.ParsedMessage{
{
Ordinal: 0, Role: parser.RoleUser, Content: "q",
ContentLength: 1, Timestamp: ts,
},
{
Ordinal: 1, Role: parser.RoleAssistant, Content: "a",
ContentLength: 1, Timestamp: ts.Add(time.Second),
},
}
// Per-event context = input + cache-creation + cache-read.
events := []parser.ParsedUsageEvent{
{OutputTokens: 100_000, InputTokens: 1_000_000, CacheReadInputTokens: 500_000},
{OutputTokens: 100_000, InputTokens: 800_000, CacheCreationInputTokens: 200_000},
// Corrupt event: each component is clamped to maxPlausibleTokens.
{
OutputTokens: 999_999_999, InputTokens: 999_999_999,
CacheReadInputTokens: 999_999_999,
},
}
rawTotal := 100_000 + 100_000 + 999_999_999
rawPeak := 999_999_999 + 999_999_999 // the corrupt event's input+cache
sess := parser.ParsedSession{
ID: "evt-cache-session", Project: "proj", Machine: "test-machine",
Agent: parser.AgentVSCodeCopilot, StartedAt: ts,
EndedAt: ts.Add(time.Minute), MessageCount: len(msgs),
File: parser.FileInfo{
Path: "/tmp/evtcache.json", Size: 10, Mtime: ts.UnixNano(),
},
TotalOutputTokens: rawTotal,
HasTotalOutputTokens: true,
PeakContextTokens: rawPeak,
HasPeakContextTokens: true,
}
prepared, _, verdict := e.prepareSessionWrite(
pendingWrite{sess: sess, msgs: msgs, usageEvents: events}, nil,
)
require.Equal(t, sessionWriteOK, verdict)
assert.Equal(t, 100_000+100_000+maxPlausibleTokens,
prepared.TotalOutputTokens,
"event-derived total re-derived from clamped output tokens")
// Peak = the corrupt event's input + cache-read, each clamped to the
// per-row bound: 2M + 2M. The sum is not clamped to the per-row bound.
assert.Equal(t, maxPlausibleTokens+maxPlausibleTokens,
prepared.PeakContextTokens,
"event-derived peak re-derived from clamped input+cache components")
}
// TestPrepareSessionWriteReclampsEventDerivedMixedSignTokens covers the
// parser rollup semantics shared with parser.UsageEventTokenAggregate: only
// positive output is summed and only positive context contributes to the peak.
// A mix of a negative (corrupt) event and an over-bound event must still be
// recognized as event-derived and re-derived from the clamped rows.
func TestPrepareSessionWriteReclampsEventDerivedMixedSignTokens(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)
msgs := []parser.ParsedMessage{
{
Ordinal: 0, Role: parser.RoleUser, Content: "q",
ContentLength: 1, Timestamp: ts,
},
{
Ordinal: 1, Role: parser.RoleAssistant, Content: "a",
ContentLength: 1, Timestamp: ts.Add(time.Second),
},
}
events := []parser.ParsedUsageEvent{
{OutputTokens: 1_000_000, InputTokens: 1_500_000},
// Corrupt negative event: excluded from the positive-only rollup.
{OutputTokens: -5, InputTokens: -10},
// Over-bound event: clamped to the per-row cap.
{OutputTokens: 999_999_999, InputTokens: 999_999_999},
}
// Raw rollup (positive-only): output 1M + 999,999,999; peak context
// max(1.5M, 999,999,999).
rawTotal := 1_000_000 + 999_999_999
rawPeak := 999_999_999
sess := parser.ParsedSession{
ID: "evt-mixed-session", Project: "proj", Machine: "test-machine",
Agent: parser.AgentVSCodeCopilot, StartedAt: ts,
EndedAt: ts.Add(time.Minute), MessageCount: len(msgs),
File: parser.FileInfo{
Path: "/tmp/evtmixed.json", Size: 10, Mtime: ts.UnixNano(),
},
TotalOutputTokens: rawTotal,
HasTotalOutputTokens: true,
PeakContextTokens: rawPeak,
HasPeakContextTokens: true,
}
prepared, _, verdict := e.prepareSessionWrite(
pendingWrite{sess: sess, msgs: msgs, usageEvents: events}, nil,
)
require.Equal(t, sessionWriteOK, verdict)
// Negative output floors to 0 (dropped); over-bound output clamps to 2M.
assert.Equal(t, 1_000_000+maxPlausibleTokens, prepared.TotalOutputTokens,
"negative event excluded, over-bound event clamped in event total")
assert.Equal(t, maxPlausibleTokens, prepared.PeakContextTokens,
"event peak re-derived from clamped positive context")
}
// TestVisualStudioCopilotArchiveCompareUsesSanitizedParsed guards against a
// truncated reparse padded with control bytes bypassing archive preservation.
// Stored rows are sanitized and length-adjusted on write, so the reconcile must
// measure the parsed side the same way rather than against its raw length.
func TestVisualStudioCopilotArchiveCompareUsesSanitizedParsed(t *testing.T) {
stored := db.Message{
Role: "assistant",
Content: "complete answer",
ContentLength: len("complete answer"),
}
// Genuinely truncated content ("trunc"), padded with BEL control bytes so
// the RAW length (25) exceeds the stored length (15); sanitized it is 5.
raw := "trunc" + strings.Repeat("\x07", 20)
require.Greater(t, len(raw), stored.ContentLength,
"raw length must look long enough to expose the bug")
truncated := db.Message{Role: "assistant", Content: raw, ContentLength: len(raw)}
assert.True(t,
visualStudioCopilotMessageLooksIncomplete(truncated, stored),
"truncated reparse padded with control bytes must be incomplete")
// A reparse that differs only by stripped control bytes is neither
// incomplete nor an archive update: "complete\x07 answer" sanitizes to the
// stored "complete answer".
withControl := db.Message{
Role: "assistant",
Content: "complete\x07 answer",
ContentLength: len("complete\x07 answer"),
}
assert.False(t,
visualStudioCopilotMessageLooksIncomplete(withControl, stored),
"stripped-control reparse of equal text is not incomplete")
assert.False(t,
visualStudioCopilotMessageHasArchiveUpdate(withControl, stored),
"stripped-control reparse of equal text is not an archive update")
}
func TestVisualStudioCopilotArchiveDecisionMatchesTimestampShiftedToolCall(t *testing.T) {
stored := []db.Message{
{
Ordinal: 0,
Role: "assistant",
Content: "Run command: dotnet build",
ContentLength: len("Run command: dotnet build"),
Timestamp: "2026-06-12T19:46:40Z",
ToolCalls: []db.ToolCall{{
ToolName: "run_command_in_terminal",
ToolUseID: "call_build",
}},
},
{
Ordinal: 1,
Role: "user",
Content: "Archived prompt.",
ContentLength: len("Archived prompt."),
Timestamp: "2026-06-12T19:47:00Z",
},
}
parsed := []db.Message{{
Ordinal: 0,
Role: "assistant",
Content: "Run command: dotnet build",
ContentLength: len("Run command: dotnet build"),
Timestamp: "2026-06-12T19:47:40Z",
ToolCalls: []db.ToolCall{{
ToolName: "run_command_in_terminal",
ToolUseID: "call_build",
ResultEvents: []db.ToolResultEvent{{
ToolUseID: "call_build",
Source: "visualstudio-copilot",
Status: "completed",
Content: "Build succeeded.",
ContentLength: len("Build succeeded."),
}},
}},
}}
decision := visualStudioCopilotArchiveDecision(parsed, stored)
require.False(t, decision.preserve)
require.Len(t, decision.merged, 2)
assert.Equal(t, "Run command: dotnet build", decision.merged[0].Content)
assert.Equal(t, "2026-06-12T19:46:40Z", decision.merged[0].Timestamp,
"fallback merge should preserve the archived transcript anchor")
require.Len(t, decision.merged[0].ToolCalls, 1)
require.Len(t, decision.merged[0].ToolCalls[0].ResultEvents, 1)
assert.Equal(t, "Build succeeded.",
decision.merged[0].ToolCalls[0].ResultEvents[0].Content)
assert.Equal(t, "Archived prompt.", decision.merged[1].Content)
}
func TestVisualStudioCopilotArchiveDecisionMergesOnlyTimestampShiftedToolCall(t *testing.T) {
stored := []db.Message{{
Ordinal: 0,
Role: "assistant",
Content: "Run command: dotnet build",
ContentLength: len("Run command: dotnet build"),
Timestamp: "2026-06-12T19:46:40Z",
ToolCalls: []db.ToolCall{{
ToolName: "run_command_in_terminal",
ToolUseID: "call_build",
}},
}}
parsed := []db.Message{{
Ordinal: 0,
Role: "assistant",
Content: "Run command: dotnet build",
ContentLength: len("Run command: dotnet build"),
Timestamp: "2026-06-12T19:47:40Z",
ToolCalls: []db.ToolCall{{
ToolName: "run_command_in_terminal",
ToolUseID: "call_build",
}},
}}
decision := visualStudioCopilotArchiveDecision(parsed, stored)
require.False(t, decision.preserve)
require.Len(t, decision.merged, 1)
assert.Equal(t, "2026-06-12T19:46:40Z", decision.merged[0].Timestamp)
}
func TestVisualStudioCopilotArchiveDecisionMatchesTimestampShiftedUserPrompt(t *testing.T) {
stored := []db.Message{
{
Ordinal: 0,
Role: "user",
Content: "Archived prompt.",
ContentLength: len("Archived prompt."),
Timestamp: "2026-06-12T19:46:40Z",
},
{
Ordinal: 1,
Role: "assistant",
Content: "Archived answer.",
ContentLength: len("Archived answer."),
Timestamp: "2026-06-12T19:47:00Z",
},
}
parsed := []db.Message{{
Ordinal: 0,
Role: "user",
Content: "Archived prompt.",
ContentLength: len("Archived prompt."),
Timestamp: "2026-06-12T19:47:40Z",
}}
decision := visualStudioCopilotArchiveDecision(parsed, stored)
require.False(t, decision.preserve)
require.Len(t, decision.merged, 2)
assert.Equal(t, "Archived prompt.", decision.merged[0].Content)
assert.Equal(t, "2026-06-12T19:46:40Z", decision.merged[0].Timestamp)
assert.Equal(t, "Archived answer.", decision.merged[1].Content)
}
// fakeEmitter records scopes passed to Emit. Thread-safe so it
// can be called from engine goroutines under test.
type fakeEmitter struct {
mu gosync.Mutex
scopes []string
}
func (f *fakeEmitter) Emit(scope string) {
f.mu.Lock()
defer f.mu.Unlock()
f.scopes = append(f.scopes, scope)
}
func (f *fakeEmitter) got() []string {
f.mu.Lock()
defer f.mu.Unlock()
out := make([]string, len(f.scopes))
copy(out, f.scopes)
return out
}
// engineFixture bundles a *db.DB, a Claude directory, and an
// *Engine for emitter tests. The engine is rebuilt by
// engineWithEmitter so tests can swap emitters in.
type engineFixture struct {
db *db.DB
claudeDir string
engine *Engine
}
func newEngineFixture(t *testing.T) *engineFixture {
t.Helper()
fx := &engineFixture{
db: openTestDB(t),
claudeDir: t.TempDir(),
}
fx.engineWithEmitter(nil)
return fx
}
// engineWithEmitter builds a new *Engine wired to the fixture's
// db and claude dir, using em as the Emitter (nil for no
// emitter).
func (fx *engineFixture) engineWithEmitter(em Emitter) {
fx.engine = NewEngine(fx.db, EngineConfig{
AgentDirs: map[parser.AgentType][]string{
parser.AgentClaude: {fx.claudeDir},
},
Machine: "local",
Emitter: em,
})
}
// writeClaudeSession writes a minimal single-user-message
// Claude JSONL file under <claudeDir>/<proj>/<filename> and
// returns the full path. The session ID derived by the parser
// is the filename with .jsonl stripped.
func (fx *engineFixture) writeClaudeSession(
t *testing.T, proj, filename, firstMessage string,
) string {
t.Helper()
content := testjsonl.NewSessionBuilder().
AddClaudeUser("2024-01-01T00:00:00Z", firstMessage).
String()
path := filepath.Join(fx.claudeDir, proj, filename)
require.NoError(t, os.MkdirAll(filepath.Dir(path), 0o755))
require.NoError(t, os.WriteFile(path, []byte(content), 0o644))
return path
}
// appendClaudeMessage appends a single user message to the
// existing JSONL file so that SyncSingleSession has new data
// to ingest.
func (fx *engineFixture) appendClaudeMessage(
t *testing.T, path, message string,
) {
t.Helper()
line := testjsonl.NewSessionBuilder().
AddClaudeUser("2024-01-01T00:00:05Z", message).
String()
f, err := os.OpenFile(path, os.O_APPEND|os.O_WRONLY, 0o644)
require.NoError(t, err, "OpenFile")
defer f.Close()
_, err = f.WriteString(line)
require.NoError(t, err, "WriteString")
}
// sessionIDFor returns the session ID the engine uses for the
// given Claude JSONL file. For Claude sessions the ID is the
// filename stem (no .jsonl suffix).
func (fx *engineFixture) sessionIDFor(
t *testing.T, path string,
) string {
t.Helper()
return filepath.Base(path[:len(path)-len(".jsonl")])
}
func TestEngine_SyncAllEmitsWhenSessionsChange(t *testing.T) {
fx := newEngineFixture(t)
em := &fakeEmitter{}
fx.engineWithEmitter(em)
fx.writeClaudeSession(t, "proj", "s1.jsonl", "hello")
stats := fx.engine.SyncAll(context.Background(), nil)
require.NotZero(t, stats.Synced, "expected Synced > 0")
got := em.got()
require.Len(t, got, 1, "expected 1 emission, got %v", got)
assert.Equal(t, "sessions", got[0], "SyncAll scope")
}
func TestEngine_SyncAllDoesNotEmitOnEmptyRun(t *testing.T) {
fx := newEngineFixture(t)
em := &fakeEmitter{}
fx.engineWithEmitter(em)
// No session files — sync finds nothing.
stats := fx.engine.SyncAll(context.Background(), nil)
require.Zero(t, stats.Synced)
assert.Empty(t, em.got(), "expected no emissions")
}
func TestEngine_SyncPathsEmitsWhenSessionsChange(t *testing.T) {
fx := newEngineFixture(t)
em := &fakeEmitter{}
fx.engineWithEmitter(em)
path := fx.writeClaudeSession(t, "proj", "s1.jsonl", "hello")
fx.engine.SyncPaths([]string{path})
got := em.got()
require.Len(t, got, 1, "expected 1 emission, got %v", got)
assert.Equal(t, "sessions", got[0], "SyncPaths scope")
}
// emitterFunc adapts a plain function to the Emitter interface so
// tests can inline probing behavior without declaring a new type.
type emitterFunc func(scope string)
func (f emitterFunc) Emit(scope string) { f(scope) }
// TestEngine_SyncPathsEmitsAfterSyncMuReleased asserts that SyncPaths
// releases syncMu BEFORE invoking Emitter.Emit. The probe uses
// sync.Mutex.TryLock() synchronously: if the emit caller still holds
// the lock, TryLock returns false immediately; if the lock is already
// released, TryLock returns true. No goroutines, no wall-clock
// timeouts — deterministic under load.
func TestEngine_SyncPathsEmitsAfterSyncMuReleased(t *testing.T) {
fx := newEngineFixture(t)
var acquired atomic.Bool
em := emitterFunc(func(scope string) {
if fx.engine.syncMu.TryLock() {
fx.engine.syncMu.Unlock()
acquired.Store(true)
}
})
fx.engineWithEmitter(em)
path := fx.writeClaudeSession(t, "proj", "s1.jsonl", "hello")
fx.engine.SyncPaths([]string{path})
assert.True(t, acquired.Load(),
"syncMu was still held when SyncPaths emitted — defer-order regression")
}
func TestEngine_SyncPathsDoesNotEmitOnNoMatches(t *testing.T) {
fx := newEngineFixture(t)
em := &fakeEmitter{}
fx.engineWithEmitter(em)
// Path doesn't match any known session pattern — classifyPaths
// returns zero files and SyncPaths returns early.
fx.engine.SyncPaths([]string{"/nonexistent/bogus.txt"})
assert.Empty(t, em.got(), "expected no emissions")
}
func TestEngine_ClassifyOnePathClaudeStatPermissionErrorStillClassifies(
t *testing.T,
) {
if runtime.GOOS == "windows" {
t.Skip("permission semantics differ on Windows")
}
db := openTestDB(t)
claudeDir := t.TempDir()
engine := NewEngine(db, EngineConfig{
AgentDirs: map[parser.AgentType][]string{
parser.AgentClaude: {claudeDir},
},
Machine: "local",
})
projectDir := filepath.Join(claudeDir, "proj")
path := filepath.Join(projectDir, "session.jsonl")
require.NoError(t, os.MkdirAll(projectDir, 0o755), "MkdirAll(%q)", projectDir)
require.NoError(t, os.WriteFile(path, []byte("[]"), 0o644), "WriteFile(%q)", path)
require.NoError(t, os.Chmod(projectDir, 0o000), "Chmod(%q)", projectDir)
defer func() {
_ = os.Chmod(projectDir, 0o755)
}()
// Claude is provider-authoritative, so classification flows through
// the provider's changed-path handling rather than the legacy
// classifyOnePath Claude block. A transient stat-permission error
// must still classify the path by shape so the change is not dropped.
files := engine.classifyPaths([]string{path})
require.Len(t, files, 1, "expected path to classify despite stat permission error")
assert.Equal(t, path, files[0].Path)
assert.Equal(t, parser.AgentClaude, files[0].Agent)
}
func TestEngine_ClassifyPathsDedupesOpenCodeChildPaths(t *testing.T) {
db := openTestDB(t)
opencodeDir := t.TempDir()
engine := NewEngine(db, EngineConfig{
AgentDirs: map[parser.AgentType][]string{
parser.AgentOpenCode: {opencodeDir},
},
Machine: "local",
})
sessionPath := filepath.Join(
opencodeDir, "storage", "session", "global",
"ses_123.json",
)
messagePath := filepath.Join(
opencodeDir, "storage", "message", "ses_123",
"msg_1.json",
)
partPath := filepath.Join(
opencodeDir, "storage", "part", "msg_1",
"part_1.json",
)
for path, content := range map[string]string{
sessionPath: `{"id":"ses_123","directory":"/tmp/proj","time":{"created":1,"updated":2}}`,
messagePath: `{"id":"msg_1","sessionID":"ses_123","role":"user","time":{"created":1}}`,
partPath: `{"id":"part_1","sessionID":"ses_123","messageID":"msg_1","type":"text","text":"hi","time":{"created":1}}`,
} {
require.NoError(t, os.MkdirAll(filepath.Dir(path), 0o755), "MkdirAll(%q)", path)
require.NoError(t, os.WriteFile(path, []byte(content), 0o644), "WriteFile(%q)", path)
}
files := engine.classifyPaths([]string{
messagePath,
partPath,
})
require.Len(t, files, 1)
assert.Equal(t, sessionPath, files[0].Path)
}
func TestEngine_ClassifyPathsOpenCodeRemovedMessageDir(
t *testing.T,
) {
db := openTestDB(t)
opencodeDir := t.TempDir()
engine := NewEngine(db, EngineConfig{
AgentDirs: map[parser.AgentType][]string{
parser.AgentOpenCode: {opencodeDir},
},
Machine: "local",
})
sessionPath := filepath.Join(
opencodeDir, "storage", "session", "global",
"ses_123.json",
)
messagePath := filepath.Join(
opencodeDir, "storage", "message", "ses_123",
"msg_1.json",
)
for path, content := range map[string]string{
sessionPath: `{"id":"ses_123","directory":"/tmp/proj","time":{"created":1,"updated":2}}`,
messagePath: `{"id":"msg_1","sessionID":"ses_123","role":"user","time":{"created":1}}`,
} {
require.NoError(t, os.MkdirAll(filepath.Dir(path), 0o755), "MkdirAll(%q)", path)
require.NoError(t, os.WriteFile(path, []byte(content), 0o644), "WriteFile(%q)", path)
}
messageDir := filepath.Dir(messagePath)
require.NoError(t, os.RemoveAll(messageDir), "RemoveAll(%q)", messageDir)
files := engine.classifyPaths([]string{messageDir})
require.Len(t, files, 1)
assert.Equal(t, sessionPath, files[0].Path)
}
// TestEngine_ClassifyPathsOpenCodeSQLiteWALFile covers a WAL-file change on
// a pure-SQLite OpenCode root. OpenCode is provider-authoritative, so the
// provider facade classifies the change into the per-session SQLite virtual
// paths it would re-parse rather than the raw opencode.db path.
func TestEngine_ClassifyPathsOpenCodeSQLiteWALFile(
t *testing.T,
) {
db := openTestDB(t)
opencodeDir := t.TempDir()
engine := NewEngine(db, EngineConfig{
AgentDirs: map[parser.AgentType][]string{
parser.AgentOpenCode: {opencodeDir},
},
Machine: "local",
})
dbPath := filepath.Join(opencodeDir, "opencode.db")
seedOpenCodeSQLiteWALSession(t, dbPath, "ses_wal")
walPath := filepath.Join(opencodeDir, "opencode.db-wal")
walInfo, err := os.Stat(walPath)
require.NoError(t, err, "Stat(%q)", walPath)
require.Greater(t, walInfo.Size(), int64(32), "WAL must contain transaction frames")
files := engine.classifyPaths([]string{walPath})
require.Len(t, files, 1)
assert.Equal(t,
parser.OpenCodeSQLiteVirtualPath(dbPath, "ses_wal"),
files[0].Path,
)
assert.Equal(t, parser.AgentOpenCode, files[0].Agent)
}
// seedOpenCodeSQLiteWALSession creates a minimal OpenCode-shaped SQLite
// database and keeps its writer open with the session commit held in the WAL.
// This exercises the same uncheckpointed state produced by a live OpenCode
// process rather than using a synthetic sidecar that SQLite cannot read.
func seedOpenCodeSQLiteWALSession(t *testing.T, dbPath, sessionID string) {
t.Helper()
d, err := sql.Open("sqlite3", dbPath)
require.NoError(t, err, "open opencode db")
t.Cleanup(func() { d.Close() })
_, err = d.Exec(`
CREATE TABLE project (id TEXT PRIMARY KEY, worktree TEXT NOT NULL);
CREATE TABLE session (
id TEXT PRIMARY KEY,
project_id TEXT NOT NULL,
parent_id TEXT,
title TEXT,
time_created INTEGER NOT NULL,
time_updated INTEGER NOT NULL
);
CREATE TABLE message (
id TEXT PRIMARY KEY,
session_id TEXT NOT NULL,
data TEXT NOT NULL,
time_created INTEGER NOT NULL
);
CREATE TABLE part (
id TEXT PRIMARY KEY,
session_id TEXT NOT NULL,
message_id TEXT NOT NULL,
data TEXT NOT NULL,
time_created INTEGER NOT NULL
);
`)
require.NoError(t, err, "create opencode schema")
var journalMode string
require.NoError(t, d.QueryRow("PRAGMA journal_mode=WAL").Scan(&journalMode))
require.Equal(t, "wal", journalMode)
_, err = d.Exec("PRAGMA wal_autocheckpoint=0")
require.NoError(t, err, "disable WAL autocheckpoint")
_, err = d.Exec(
"INSERT INTO project (id, worktree) VALUES ('prj_1', '/home/user/code/app')",
)
require.NoError(t, err, "insert project")
_, err = d.Exec(
`INSERT INTO session (id, project_id, time_created, time_updated)
VALUES (?, 'prj_1', 1, 2)`,
sessionID,
)
require.NoError(t, err, "insert session")
}
func TestEngine_ClassifyPathsOpenCodeRemovedMessageFile(
t *testing.T,
) {
db := openTestDB(t)
opencodeDir := t.TempDir()
engine := NewEngine(db, EngineConfig{
AgentDirs: map[parser.AgentType][]string{
parser.AgentOpenCode: {opencodeDir},
},
Machine: "local",
})
sessionPath := filepath.Join(
opencodeDir, "storage", "session", "global",
"ses_123.json",
)
messagePath := filepath.Join(
opencodeDir, "storage", "message", "ses_123",
"msg_1.json",
)
for path, content := range map[string]string{
sessionPath: `{"id":"ses_123","directory":"/tmp/proj","time":{"created":1,"updated":2}}`,
messagePath: `{"id":"msg_1","sessionID":"ses_123","role":"user","time":{"created":1}}`,
} {
require.NoError(t, os.MkdirAll(filepath.Dir(path), 0o755), "MkdirAll(%q)", path)
require.NoError(t, os.WriteFile(path, []byte(content), 0o644), "WriteFile(%q)", path)
}
require.NoError(t, os.Remove(messagePath), "Remove(%q)", messagePath)
files := engine.classifyPaths([]string{messagePath})
require.Len(t, files, 1)
assert.Equal(t, sessionPath, files[0].Path)
}
// TestEngine_ClassifyPathsOpenCodeFamilyRemovedSessionFile covers a removed
// storage session file for the provider-authoritative OpenCode-format agents.
// A delete event yields no reparse classification: there is no source to
// re-read, and the deletion is reconciled by the presence sweep rather than
// changed-path classification.
func TestEngine_ClassifyPathsOpenCodeFamilyRemovedSessionFile(
t *testing.T,
) {
for _, tc := range []struct {
name string
agent parser.AgentType
sessionSubdir string
}{
{name: "opencode", agent: parser.AgentOpenCode, sessionSubdir: "session"},
{name: "kilo", agent: parser.AgentKilo, sessionSubdir: "session"},
{name: "mimocode", agent: parser.AgentMiMoCode, sessionSubdir: "session_diff"},
} {
t.Run(tc.name, func(t *testing.T) {
db := openTestDB(t)
root := t.TempDir()
engine := NewEngine(db, EngineConfig{
AgentDirs: map[parser.AgentType][]string{
tc.agent: {root},
},
Machine: "local",
})
sessionPath := filepath.Join(
root, "storage", tc.sessionSubdir, "global",
"ses_removed.json",
)
require.NoError(
t, os.MkdirAll(filepath.Dir(sessionPath), 0o755),
"MkdirAll(%q)", sessionPath,
)
require.NoError(
t,
os.WriteFile(
sessionPath,
[]byte(`{"id":"ses_removed","directory":"/tmp/proj","time":{"created":1,"updated":2}}`),
0o644,
),
"WriteFile(%q)", sessionPath,
)
require.NoError(t, os.Remove(sessionPath), "Remove(%q)", sessionPath)
files := engine.classifyPaths([]string{sessionPath})
assert.Empty(t, files)
})
}
}
func TestEngine_ClassifyPathsProviderRemoveKeepsDeletedSQLiteSources(
t *testing.T,
) {
tests := []struct {
name string
agent parser.AgentType
path func(string) string
}{
{
name: "zed",
agent: parser.AgentZed,
path: func(root string) string {
return filepath.Join(root, "threads", "threads.db")
},
},
{
name: "shelley",
agent: parser.AgentShelley,
path: func(root string) string {
return filepath.Join(root, shelleyDBFile)
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
db := openTestDB(t)
root := t.TempDir()
engine := NewEngine(db, EngineConfig{
AgentDirs: map[parser.AgentType][]string{
tt.agent: {root},
},
Machine: "local",
})
dbPath := tt.path(root)
require.NoFileExists(t, dbPath)
files := engine.classifyPaths([]string{dbPath})
require.Len(t, files, 1)
assert.Equal(t, dbPath, files[0].Path)
assert.Equal(t, tt.agent, files[0].Agent)
assert.True(t, files[0].ForceParse)
})
}
}
func TestEngine_ProcessFileProviderDeletedSQLiteSourcesDoNotFail(
t *testing.T,
) {
tests := []struct {
name string
agent parser.AgentType
path func(string) string
}{
{
name: "zed",
agent: parser.AgentZed,
path: func(root string) string {
return filepath.Join(root, "threads", "threads.db")
},
},
{
name: "shelley",
agent: parser.AgentShelley,
path: func(root string) string {
return filepath.Join(root, shelleyDBFile)
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
db := openTestDB(t)
root := t.TempDir()
engine := NewEngine(db, EngineConfig{
AgentDirs: map[parser.AgentType][]string{
tt.agent: {root},
},
Machine: "local",
})
dbPath := tt.path(root)
require.NoFileExists(t, dbPath)
res := engine.processFile(context.Background(), parser.DiscoveredFile{
Path: dbPath,
Agent: tt.agent,
ForceParse: true,
})
require.NoError(t, res.err)
assert.Empty(t, res.results)
assert.True(t, res.forceReplace)
})
}
}
func TestEngine_ClassifyPathsOpenCodeRemovedPartDir(
t *testing.T,
) {
db := openTestDB(t)
opencodeDir := t.TempDir()
engine := NewEngine(db, EngineConfig{
AgentDirs: map[parser.AgentType][]string{
parser.AgentOpenCode: {opencodeDir},
},
Machine: "local",
})
sessionPath := filepath.Join(
opencodeDir, "storage", "session", "global",
"ses_123.json",
)
messagePath := filepath.Join(
opencodeDir, "storage", "message", "ses_123",
"msg_1.json",
)
partPath := filepath.Join(
opencodeDir, "storage", "part", "msg_1",
"part_1.json",
)
for path, content := range map[string]string{
sessionPath: `{"id":"ses_123","directory":"/tmp/proj","time":{"created":1,"updated":2}}`,
messagePath: `{"id":"msg_1","sessionID":"ses_123","role":"user","time":{"created":1}}`,
partPath: `{"id":"part_1","messageID":"msg_1","type":"text","text":"hi","time":{"created":1}}`,
} {
require.NoError(t, os.MkdirAll(filepath.Dir(path), 0o755), "MkdirAll(%q)", path)
require.NoError(t, os.WriteFile(path, []byte(content), 0o644), "WriteFile(%q)", path)
}
partDir := filepath.Dir(partPath)
require.NoError(t, os.RemoveAll(partDir), "RemoveAll(%q)", partDir)
files := engine.classifyPaths([]string{partDir})
require.Len(t, files, 1)
assert.Equal(t, sessionPath, files[0].Path)
}
func TestEngine_ClassifyPathsOpenCodeRemovedPartFile(
t *testing.T,
) {
db := openTestDB(t)
opencodeDir := t.TempDir()
engine := NewEngine(db, EngineConfig{
AgentDirs: map[parser.AgentType][]string{
parser.AgentOpenCode: {opencodeDir},
},
Machine: "local",
})
sessionPath := filepath.Join(
opencodeDir, "storage", "session", "global",
"ses_123.json",
)
messagePath := filepath.Join(
opencodeDir, "storage", "message", "ses_123",
"msg_1.json",
)
partPath := filepath.Join(
opencodeDir, "storage", "part", "msg_1",
"part_1.json",
)
for path, content := range map[string]string{
sessionPath: `{"id":"ses_123","directory":"/tmp/proj","time":{"created":1,"updated":2}}`,
messagePath: `{"id":"msg_1","sessionID":"ses_123","role":"user","time":{"created":1}}`,
partPath: `{"id":"part_1","messageID":"msg_1","type":"text","text":"hi","time":{"created":1}}`,
} {
require.NoError(t, os.MkdirAll(filepath.Dir(path), 0o755), "MkdirAll(%q)", path)
require.NoError(t, os.WriteFile(path, []byte(content), 0o644), "WriteFile(%q)", path)
}
require.NoError(t, os.Remove(partPath), "Remove(%q)", partPath)
files := engine.classifyPaths([]string{partPath})
require.Len(t, files, 1)
assert.Equal(t, sessionPath, files[0].Path)
}
// TestEngine_ClassifyPathsQwenSession verifies fsnotify events for
// Qwen session files (which live two levels deep under the projects
// root, at <projectsDir>/<encoded-project>/chats/<session>.jsonl) are
// classified as AgentQwen — the original WatchSubdirs="chats" wiring
// pointed the watcher at the wrong path, leaving live sync broken
// even after the classifier branch is reachable.
func TestEngine_ClassifyPathsQwenPawRejectsColon(t *testing.T) {
if runtime.GOOS == "windows" {
// ":" is invalid in Windows filenames, so colon-bearing
// workspace/subdir/stem paths cannot be created there.
t.Skip("':' is invalid in Windows filenames")
}
db := openTestDB(t)
qwenpawDir := t.TempDir()
engine := NewEngine(db, EngineConfig{
AgentDirs: map[parser.AgentType][]string{
parser.AgentQwenPaw: {qwenpawDir},
},
Machine: "local",
})
write := func(parts ...string) string {
p := filepath.Join(append([]string{qwenpawDir}, parts...)...)
require.NoError(t, os.MkdirAll(filepath.Dir(p), 0o755))
require.NoError(t, os.WriteFile(p, []byte("{}"), 0o644))
return p
}
rootPath := write("default", "sessions", "ok.json")
subPath := write("default", "sessions", "console", "ok.json")
// ":" in the workspace, subdir, or stem makes the joined ID
// ambiguous, so these must not classify.
colonWorkspace := write("ws:bad", "sessions", "ok.json")
colonSubdir := write("default", "sessions", "sub:bad", "ok.json")
colonStem := write("default", "sessions", "foo:bar.json")
files := engine.classifyPaths([]string{rootPath, subPath})
require.Len(t, files, 2)
for _, f := range files {
assert.Equal(t, parser.AgentQwenPaw, f.Agent)
assert.Equal(t, "default", f.Project)
}
got := engine.classifyPaths([]string{
colonWorkspace, colonSubdir, colonStem,
})
assert.Empty(t, got,
"colon-containing ID parts must not classify: %v", got)
}
func TestEngine_ClassifyPathsQwenSession(t *testing.T) {
db := openTestDB(t)
qwenDir := t.TempDir()
engine := NewEngine(db, EngineConfig{
AgentDirs: map[parser.AgentType][]string{
parser.AgentQwen: {qwenDir},
},
Machine: "local",
})
sessionID := "adc026b4-c620-43e4-8cc4-295593889d18"
encodedProject := "-Users-alice-code-sample-project"
chatsDir := filepath.Join(qwenDir, encodedProject, "chats")
require.NoError(t, os.MkdirAll(chatsDir, 0o755), "MkdirAll(%q)", chatsDir)
sessionPath := filepath.Join(chatsDir, sessionID+".jsonl")
require.NoError(t, os.WriteFile(sessionPath, []byte("{}\n"), 0o644), "WriteFile(%q)", sessionPath)
files := engine.classifyPaths([]string{sessionPath})
require.Len(t, files, 1, "len(files) = %d, want 1 (%v)", len(files), files)
assert.Equal(t, sessionPath, files[0].Path)
assert.Equal(t, parser.AgentQwen, files[0].Agent)
assert.Equal(t, "sample_project", files[0].Project)
// Non-Qwen siblings (a stray file directly under projectsDir, a
// file under <project>/<not-chats>/, a non-jsonl in chats/, and a
// path outside the canonical <encoded-project>/chats/ shape) must
// not classify as Qwen.
bogus := []string{
filepath.Join(qwenDir, "stray.jsonl"),
filepath.Join(qwenDir, "proj", "notes", "a.jsonl"),
filepath.Join(chatsDir, "notes.txt"),
filepath.Join(qwenDir, "chats", sessionID+".jsonl"),
}
for _, p := range bogus {
require.NoError(t, os.MkdirAll(filepath.Dir(p), 0o755), "MkdirAll(%q)", p)
require.NoError(t, os.WriteFile(p, []byte("{}"), 0o644), "WriteFile(%q)", p)
}
got := engine.classifyPaths(bogus)
assert.Empty(t, got, "expected no Qwen classifications for %v, got %v", bogus, got)
}
func TestEngine_ClassifyPathsDeepSeekTUISession(t *testing.T) {
db := openTestDB(t)
deepSeekDir := t.TempDir()
engine := NewEngine(db, EngineConfig{
AgentDirs: map[parser.AgentType][]string{
parser.AgentDeepSeekTUI: {deepSeekDir},
},
Machine: "local",
})
sessionID := "adc026b4-c620-43e4-8cc4-295593889d18"
sessionPath := filepath.Join(deepSeekDir, sessionID+".json")
dbtest.WriteTestFile(t, sessionPath, []byte("{}"))
files := engine.classifyPaths([]string{sessionPath})
require.Len(t, files, 1, "len(files) = %d, want 1 (%v)", len(files), files)
assert.Equal(t, sessionPath, files[0].Path)
assert.Equal(t, parser.AgentDeepSeekTUI, files[0].Agent)
assert.True(t, files[0].ProviderProcess)
require.NotNil(t, files[0].ProviderSource)
assert.Equal(t, sessionPath, files[0].ProviderSource.DisplayPath)
bogus := []string{
filepath.Join(deepSeekDir, "stray.jsonl"),
filepath.Join(deepSeekDir, "latest.json"),
filepath.Join(deepSeekDir, "offline_queue.json"),
filepath.Join(deepSeekDir, "nested", sessionID+".json"),
filepath.Join(deepSeekDir, "..bad.json"),
}
for _, p := range bogus {
require.NoError(t, os.MkdirAll(filepath.Dir(p), 0o755), "MkdirAll(%q)", p)
dbtest.WriteTestFile(t, p, []byte("{}"))
}
got := engine.classifyPaths(bogus)
assert.Empty(t, got, "expected no DeepSeek TUI classifications for %v, got %v", bogus, got)
}
func TestEngine_ClassifyPathsCommandCodeSession(t *testing.T) {
db := openTestDB(t)
commandCodeDir := t.TempDir()
engine := NewEngine(db, EngineConfig{
AgentDirs: map[parser.AgentType][]string{
parser.AgentCommandCode: {commandCodeDir},
},
Machine: "local",
})
sessionID := "adc026b4-c620-43e4-8cc4-295593889d18"
projectDir := filepath.Join(commandCodeDir, "users-alice-code-sample-project")
require.NoError(t, os.MkdirAll(projectDir, 0o755), "MkdirAll(%q)", projectDir)
sessionPath := filepath.Join(projectDir, sessionID+".jsonl")
dbtest.WriteTestFile(t, sessionPath, []byte("{}\n"))
files := engine.classifyPaths([]string{sessionPath})
require.Len(t, files, 1, "len(files) = %d, want 1 (%v)", len(files), files)
assert.Equal(t, sessionPath, files[0].Path)
assert.Equal(t, parser.AgentCommandCode, files[0].Agent)
// Command Code is provider-authoritative: classification attaches a
// provider source and recomputes the project during parse, so the
// classification carries no informational project hint.
assert.Empty(t, files[0].Project)
require.NotNil(t, files[0].ProviderSource)
bogus := []string{
filepath.Join(commandCodeDir, "stray.jsonl"),
filepath.Join(projectDir, "notes.txt"),
filepath.Join(projectDir, sessionID+".checkpoints.jsonl"),
filepath.Join(projectDir, sessionID+".prompts.jsonl"),
filepath.Join(projectDir, "nested", sessionID+".jsonl"),
}
for _, p := range bogus {
require.NoError(t, os.MkdirAll(filepath.Dir(p), 0o755), "MkdirAll(%q)", p)
dbtest.WriteTestFile(t, p, []byte("{}"))
}
got := engine.classifyPaths(bogus)
assert.Empty(t, got, "expected no Command Code classifications for %v, got %v", bogus, got)
metaPath := filepath.Join(projectDir, sessionID+".meta.json")
dbtest.WriteTestFile(t, metaPath, []byte("{}"))
files = engine.classifyPaths([]string{metaPath})
require.Len(t, files, 1, "len(files) = %d, want 1 (%v)", len(files), files)
assert.Equal(t, sessionPath, files[0].Path)
assert.Equal(t, parser.AgentCommandCode, files[0].Agent)
}
func TestEngine_ClassifyPathsQClawSession(t *testing.T) {
db := openTestDB(t)
qclawDir := t.TempDir()
engine := NewEngine(db, EngineConfig{
AgentDirs: map[parser.AgentType][]string{
parser.AgentQClaw: {qclawDir},
},
Machine: "local",
})
agentID := "main"
sessionID := "adc026b4-c620-43e4-8cc4-295593889d18"
sessionsDir := filepath.Join(qclawDir, agentID, "sessions")
sessionPath := filepath.Join(sessionsDir, sessionID+".jsonl")
dbtest.WriteTestFile(t, sessionPath, []byte("{}\n"))
files := engine.classifyPaths([]string{sessionPath})
require.Len(t, files, 1, "len(files) = %d, want 1 (%v)", len(files), files)
assert.Equal(t, sessionPath, files[0].Path)
assert.Equal(t, parser.AgentQClaw, files[0].Agent)
bogus := []string{
filepath.Join(qclawDir, "stray.jsonl"),
filepath.Join(qclawDir, agentID, "notes", sessionID+".jsonl"),
filepath.Join(sessionsDir, "notes.txt"),
filepath.Join(qclawDir, "not a session id", "sessions", sessionID+".jsonl"),
}
for _, p := range bogus {
dbtest.WriteTestFile(t, p, []byte("{}"))
}
got := engine.classifyPaths(bogus)
assert.Empty(t, got, "expected no QClaw classifications for %v, got %v", bogus, got)
}
func TestEngine_ClassifyPathsQClawArchivedSession(t *testing.T) {
db := openTestDB(t)
qclawDir := t.TempDir()
engine := NewEngine(db, EngineConfig{
AgentDirs: map[parser.AgentType][]string{
parser.AgentQClaw: {qclawDir},
},
Machine: "local",
})
agentID := "main"
sessionID := "adc026b4-c620-43e4-8cc4-295593889d18"
sessionsDir := filepath.Join(qclawDir, agentID, "sessions")
active := filepath.Join(sessionsDir, sessionID+".jsonl")
archived := filepath.Join(
sessionsDir,
sessionID+".jsonl.deleted.2026-02-19T08-59-24.951Z",
)
dbtest.WriteTestFile(t, active, []byte("{}\n"))
dbtest.WriteTestFile(t, archived, []byte("{}\n"))
got := engine.classifyPaths([]string{archived})
require.Empty(t, got, "expected archived file shadowed by active to be ignored, got %v", got)
require.NoError(t, os.Remove(active), "Remove(%q)", active)
files := engine.classifyPaths([]string{archived})
require.Len(t, files, 1, "len(files) = %d, want 1 (%v)", len(files), files)
assert.Equal(t, archived, files[0].Path)
assert.Equal(t, parser.AgentQClaw, files[0].Agent)
}
func TestEngine_ClassifyOnePathReasonixProjectBareMeta(t *testing.T) {
db := openTestDB(t)
reasonixDir := t.TempDir()
engine := NewEngine(db, EngineConfig{
AgentDirs: map[parser.AgentType][]string{
parser.AgentReasonix: {reasonixDir},
},
Machine: "local",
})
sessionPath := filepath.Join(
reasonixDir, "projects", "proj", "sessions", "session-123.jsonl",
)
metaPath := sessionPath + ".meta"
dbtest.WriteTestFile(t, sessionPath, []byte(`{"role":"user","content":"hi"}`))
dbtest.WriteTestFile(t, metaPath, []byte(`{"model":"claude"}`))
files := engine.classifyPaths([]string{metaPath})
require.Len(t, files, 1, "expected Reasonix sidecar to classify")
assert.Equal(t, sessionPath, files[0].Path)
assert.Equal(t, "proj", files[0].Project)
assert.Equal(t, parser.AgentReasonix, files[0].Agent)
}
func TestEngine_ClassifyOnePathReasonixDeletedMeta(t *testing.T) {
db := openTestDB(t)
reasonixDir := t.TempDir()
engine := NewEngine(db, EngineConfig{
AgentDirs: map[parser.AgentType][]string{
parser.AgentReasonix: {reasonixDir},
},
Machine: "local",
})
sessionPath := filepath.Join(
reasonixDir, "projects", "proj", "sessions", "session-123.jsonl",
)
metaPath := sessionPath + ".meta"
dbtest.WriteTestFile(t, sessionPath, []byte(`{"role":"user","content":"hi"}`))
files := engine.classifyPaths([]string{metaPath})
require.Len(t, files, 1, "expected deleted Reasonix sidecar to classify")
assert.Equal(t, sessionPath, files[0].Path)
assert.Equal(t, "proj", files[0].Project)
assert.Equal(t, parser.AgentReasonix, files[0].Agent)
}
func TestEngine_ClassifyOnePathReasonixDeletedTranscriptIgnored(t *testing.T) {
db := openTestDB(t)
reasonixDir := t.TempDir()
engine := NewEngine(db, EngineConfig{
AgentDirs: map[parser.AgentType][]string{
parser.AgentReasonix: {reasonixDir},
},
Machine: "local",
})
sessionPath := filepath.Join(
reasonixDir, "projects", "proj", "sessions", "session-123.jsonl",
)
files := engine.classifyPaths([]string{sessionPath})
assert.Empty(t, files, "expected deleted Reasonix transcript to be ignored")
}
func TestEngine_SyncPathsReasonixMetadataOnlySessionFieldUpdate(t *testing.T) {
db := openTestDB(t)
reasonixDir := t.TempDir()
engine := NewEngine(db, EngineConfig{
AgentDirs: map[parser.AgentType][]string{
parser.AgentReasonix: {reasonixDir},
},
Machine: "local",
})
sessionPath := filepath.Join(reasonixDir, "sessions", "session-123.jsonl")
metaPath := sessionPath + ".meta"
require.NoError(t, os.MkdirAll(filepath.Dir(sessionPath), 0o755))
require.NoError(t, os.WriteFile(sessionPath, []byte(
"{\"role\":\"user\",\"content\":\"hi\"}\n{\"role\":\"assistant\",\"content\":\"hello\"}\n",
), 0o644))
initialRoot := filepath.Join("workspace", "my-app")
initialMeta, err := json.Marshal(map[string]string{
"created_at": "2026-06-12T10:42:35.2672024Z",
"updated_at": "2026-06-12T10:58:03.6456434Z",
"topic_title": "Initial title",
"workspace_root": initialRoot,
"model": "claude-opus-4",
})
require.NoError(t, err)
require.NoError(t, os.WriteFile(metaPath, initialMeta, 0o644))
engine.SyncPaths([]string{sessionPath})
got, err := db.GetSessionFull(context.Background(), "reasonix:session-123")
require.NoError(t, err)
require.NotNil(t, got)
require.NotNil(t, got.DisplayName)
require.NotNil(t, got.SessionName)
assert.Equal(t, "Initial title", *got.DisplayName)
assert.Equal(t, "Initial title", *got.SessionName)
assert.Equal(t, initialRoot, got.Cwd)
assert.Equal(t, "my_app", got.Project)
updatedRoot := filepath.Join("workspace", "renamed-app")
updatedMeta, err := json.Marshal(map[string]string{
"created_at": "2026-06-12T10:42:35.2672024Z",
"updated_at": "2026-06-12T10:58:03.6456434Z",
"topic_title": "Updated title",
"workspace_root": updatedRoot,
"model": "claude-opus-4",
})
require.NoError(t, err)
require.NoError(t, os.WriteFile(metaPath, updatedMeta, 0o644))
future := time.Date(2026, time.June, 19, 2, 55, 0, 0, time.UTC)
require.NoError(t, os.Chtimes(metaPath, future, future))
engine.SyncPaths([]string{metaPath})
got, err = db.GetSessionFull(context.Background(), "reasonix:session-123")
require.NoError(t, err)
require.NotNil(t, got)
require.NotNil(t, got.DisplayName)
require.NotNil(t, got.SessionName)
assert.Equal(t, "Updated title", *got.DisplayName)
assert.Equal(t, "Updated title", *got.SessionName)
assert.Equal(t, updatedRoot, got.Cwd)
assert.Equal(t, "renamed_app", got.Project)
}
func TestEngine_SyncPathsReasonixDeletedMetadataClearsSessionFields(t *testing.T) {
db := openTestDB(t)
reasonixDir := t.TempDir()
engine := NewEngine(db, EngineConfig{
AgentDirs: map[parser.AgentType][]string{
parser.AgentReasonix: {reasonixDir},
},
Machine: "local",
})
sessionPath := filepath.Join(reasonixDir, "sessions", "session-123.jsonl")
metaPath := sessionPath + ".meta"
require.NoError(t, os.MkdirAll(filepath.Dir(sessionPath), 0o755))
require.NoError(t, os.WriteFile(sessionPath, []byte(
"{\"role\":\"user\",\"content\":\"hi\"}\n"+
"{\"role\":\"assistant\",\"content\":\"hello\"}\n",
), 0o644))
initialRoot := filepath.Join("workspace", "my-app")
initialMeta, err := json.Marshal(map[string]string{
"created_at": "2026-06-12T10:42:35.2672024Z",
"updated_at": "2026-06-12T10:58:03.6456434Z",
"topic_title": "Initial title",
"workspace_root": initialRoot,
"model": "claude-opus-4",
})
require.NoError(t, err)
require.NoError(t, os.WriteFile(metaPath, initialMeta, 0o644))
engine.SyncPaths([]string{sessionPath})
require.NoError(t, os.Remove(metaPath))
engine.SyncPaths([]string{metaPath})
got, err := db.GetSessionFull(context.Background(), "reasonix:session-123")
require.NoError(t, err)
require.NotNil(t, got)
assert.Nil(t, got.DisplayName)
assert.Nil(t, got.SessionName)
assert.Equal(t, "", got.Cwd)
assert.Equal(t, "", got.Project)
}
func TestEngine_SyncSingleSessionReasonixDeletedMetadataClearsProject(t *testing.T) {
db := openTestDB(t)
reasonixDir := t.TempDir()
engine := NewEngine(db, EngineConfig{
AgentDirs: map[parser.AgentType][]string{
parser.AgentReasonix: {reasonixDir},
},
Machine: "local",
})
sessionPath := filepath.Join(reasonixDir, "sessions", "session-123.jsonl")
metaPath := sessionPath + ".meta"
require.NoError(t, os.MkdirAll(filepath.Dir(sessionPath), 0o755))
require.NoError(t, os.WriteFile(sessionPath, []byte(
"{\"role\":\"user\",\"content\":\"hi\"}\n"+
"{\"role\":\"assistant\",\"content\":\"hello\"}\n",
), 0o644))
initialRoot := filepath.Join("workspace", "my-app")
initialMeta, err := json.Marshal(map[string]string{
"created_at": "2026-06-12T10:42:35.2672024Z",
"updated_at": "2026-06-12T10:58:03.6456434Z",
"topic_title": "Initial title",
"workspace_root": initialRoot,
"model": "claude-opus-4",
})
require.NoError(t, err)
require.NoError(t, os.WriteFile(metaPath, initialMeta, 0o644))
engine.SyncPaths([]string{sessionPath})
got, err := db.GetSessionFull(context.Background(), "reasonix:session-123")
require.NoError(t, err)
require.NotNil(t, got)
assert.Equal(t, "my_app", got.Project)
require.NoError(t, os.Remove(metaPath))
require.NoError(t, db.Update(func(tx *sql.Tx) error {
_, err := tx.Exec(
"UPDATE sessions SET file_mtime = NULL WHERE id = ?",
"reasonix:session-123",
)
return err
}))
require.NoError(t, engine.SyncSingleSession("reasonix:session-123"))
got, err = db.GetSessionFull(context.Background(), "reasonix:session-123")
require.NoError(t, err)
require.NotNil(t, got)
assert.Equal(t, "", got.Project)
}
func TestEngine_SyncPathsReasonixMalformedMetadataPreservesSessionFields(t *testing.T) {
db := openTestDB(t)
reasonixDir := t.TempDir()
engine := NewEngine(db, EngineConfig{
AgentDirs: map[parser.AgentType][]string{
parser.AgentReasonix: {reasonixDir},
},
Machine: "local",
})
sessionPath := filepath.Join(reasonixDir, "sessions", "session-123.jsonl")
metaPath := sessionPath + ".meta"
require.NoError(t, os.MkdirAll(filepath.Dir(sessionPath), 0o755))
require.NoError(t, os.WriteFile(sessionPath, []byte(
"{\"role\":\"user\",\"content\":\"hi\"}\n"+
"{\"role\":\"assistant\",\"content\":\"hello\"}\n",
), 0o644))
initialRoot := filepath.Join("workspace", "my-app")
initialMeta, err := json.Marshal(map[string]string{
"created_at": "2026-06-12T10:42:35.2672024Z",
"updated_at": "2026-06-12T10:58:03.6456434Z",
"topic_title": "Initial title",
"workspace_root": initialRoot,
"model": "claude-opus-4",
})
require.NoError(t, err)
require.NoError(t, os.WriteFile(metaPath, initialMeta, 0o644))
engine.SyncPaths([]string{sessionPath})
require.NoError(t, os.WriteFile(metaPath, []byte(`{"topic_title":`), 0o644))
future := time.Date(2026, time.June, 19, 4, 15, 0, 0, time.UTC)
require.NoError(t, os.Chtimes(metaPath, future, future))
engine.SyncPaths([]string{metaPath})
got, err := db.GetSessionFull(context.Background(), "reasonix:session-123")
require.NoError(t, err)
require.NotNil(t, got)
require.NotNil(t, got.DisplayName)
require.NotNil(t, got.SessionName)
assert.Equal(t, "Initial title", *got.DisplayName)
assert.Equal(t, "Initial title", *got.SessionName)
assert.Equal(t, initialRoot, got.Cwd)
assert.Equal(t, "my_app", got.Project)
}
func TestEngine_SyncPathsReasonixMalformedMetadataRecoveryUpdatesSession(t *testing.T) {
db := openTestDB(t)
reasonixDir := t.TempDir()
engine := NewEngine(db, EngineConfig{
AgentDirs: map[parser.AgentType][]string{
parser.AgentReasonix: {reasonixDir},
},
Machine: "local",
})
sessionPath := filepath.Join(reasonixDir, "sessions", "session-123.jsonl")
metaPath := sessionPath + ".meta"
require.NoError(t, os.MkdirAll(filepath.Dir(sessionPath), 0o755))
require.NoError(t, os.WriteFile(sessionPath, []byte(
"{\"role\":\"user\",\"content\":\"hi\"}\n"+
"{\"role\":\"assistant\",\"content\":\"hello\"}\n",
), 0o644))
initialRoot := filepath.Join("workspace", "my-app")
initialMeta, err := json.Marshal(map[string]string{
"created_at": "2026-06-12T10:42:35.2672024Z",
"updated_at": "2026-06-12T10:58:03.6456434Z",
"topic_title": "Initial title",
"workspace_root": initialRoot,
"model": "claude-opus-4",
})
require.NoError(t, err)
require.NoError(t, os.WriteFile(metaPath, initialMeta, 0o644))
engine.SyncPaths([]string{sessionPath})
transcriptInfo, err := os.Stat(sessionPath)
require.NoError(t, err)
badMtime := transcriptInfo.ModTime().Add(time.Minute)
require.NoError(t, os.WriteFile(metaPath, []byte(`{"topic_title":`), 0o644))
require.NoError(t, os.Chtimes(metaPath, badMtime, badMtime))
engine.SyncPaths([]string{metaPath})
updatedRoot := filepath.Join("workspace", "renamed-app")
updatedMeta, err := json.Marshal(map[string]string{
"created_at": "2026-06-12T10:42:35.2672024Z",
"updated_at": "2026-06-12T10:58:03.6456434Z",
"topic_title": "Recovered title",
"workspace_root": updatedRoot,
"model": "claude-opus-4",
})
require.NoError(t, err)
require.NoError(t, os.WriteFile(metaPath, updatedMeta, 0o644))
recoveredMtime := badMtime.Add(time.Minute)
require.NoError(t, os.Chtimes(metaPath, recoveredMtime, recoveredMtime))
engine.SyncPaths([]string{metaPath})
got, err := db.GetSessionFull(context.Background(), "reasonix:session-123")
require.NoError(t, err)
require.NotNil(t, got)
require.NotNil(t, got.DisplayName)
require.NotNil(t, got.SessionName)
assert.Equal(t, "Recovered title", *got.DisplayName)
assert.Equal(t, "Recovered title", *got.SessionName)
assert.Equal(t, updatedRoot, got.Cwd)
assert.Equal(t, "renamed_app", got.Project)
}
func TestEngine_SyncPathsReasonixProjectLayoutMetadataProjectUpdate(t *testing.T) {
db := openTestDB(t)
reasonixDir := t.TempDir()
engine := NewEngine(db, EngineConfig{
AgentDirs: map[parser.AgentType][]string{
parser.AgentReasonix: {reasonixDir},
},
Machine: "local",
})
sessionPath := filepath.Join(
reasonixDir, "projects", "layout-name", "sessions", "session-123", "session-123.jsonl",
)
metaPath := sessionPath + ".meta"
require.NoError(t, os.MkdirAll(filepath.Dir(sessionPath), 0o755))
require.NoError(t, os.WriteFile(sessionPath, []byte(
"{\"role\":\"user\",\"content\":\"hi\"}\n{\"role\":\"assistant\",\"content\":\"hello\"}\n",
), 0o644))
initialMeta, err := json.Marshal(map[string]string{
"created_at": "2026-06-12T10:42:35.2672024Z",
"updated_at": "2026-06-12T10:58:03.6456434Z",
"topic_title": "Initial title",
"workspace_root": filepath.Join("workspace", "my-app"),
"model": "claude-opus-4",
})
require.NoError(t, err)
require.NoError(t, os.WriteFile(metaPath, initialMeta, 0o644))
engine.SyncPaths([]string{sessionPath})
got, err := db.GetSessionFull(context.Background(), "reasonix:session-123")
require.NoError(t, err)
require.NotNil(t, got)
assert.Equal(t, "my_app", got.Project)
updatedMeta, err := json.Marshal(map[string]string{
"created_at": "2026-06-12T10:42:35.2672024Z",
"updated_at": "2026-06-12T10:58:03.6456434Z",
"topic_title": "Updated title",
"workspace_root": filepath.Join("workspace", "renamed-app"),
"model": "claude-opus-4",
})
require.NoError(t, err)
require.NoError(t, os.WriteFile(metaPath, updatedMeta, 0o644))
future := time.Date(2026, time.June, 19, 3, 30, 0, 0, time.UTC)
require.NoError(t, os.Chtimes(metaPath, future, future))
engine.SyncPaths([]string{metaPath})
got, err = db.GetSessionFull(context.Background(), "reasonix:session-123")
require.NoError(t, err)
require.NotNil(t, got)
assert.Equal(t, "renamed_app", got.Project)
}
func TestEngine_SyncSingleSessionReasonixProjectLayoutPreservesProject(t *testing.T) {
db := openTestDB(t)
reasonixDir := t.TempDir()
engine := NewEngine(db, EngineConfig{
AgentDirs: map[parser.AgentType][]string{
parser.AgentReasonix: {reasonixDir},
},
Machine: "local",
})
sessionPath := filepath.Join(
reasonixDir, "projects", "layout-name", "sessions",
"session-123", "session-123.jsonl",
)
require.NoError(t, os.MkdirAll(filepath.Dir(sessionPath), 0o755))
require.NoError(t, os.WriteFile(sessionPath, []byte(
"{\"role\":\"user\",\"content\":\"hi\"}\n"+
"{\"role\":\"assistant\",\"content\":\"hello\"}\n",
), 0o644))
engine.SyncPaths([]string{sessionPath})
got, err := db.GetSessionFull(context.Background(), "reasonix:session-123")
require.NoError(t, err)
require.NotNil(t, got)
assert.Equal(t, "layout-name", got.Project)
require.NoError(t, db.Update(func(tx *sql.Tx) error {
_, err := tx.Exec(
"UPDATE sessions SET file_mtime = NULL WHERE id = ?",
"reasonix:session-123",
)
return err
}))
require.NoError(t, engine.SyncSingleSession("reasonix:session-123"))
got, err = db.GetSessionFull(context.Background(), "reasonix:session-123")
require.NoError(t, err)
require.NotNil(t, got)
assert.Equal(t, "layout-name", got.Project)
}
func TestEngine_SyncPathsReasonixPersistsToolResultContent(t *testing.T) {
database := openTestDB(t)
reasonixDir := t.TempDir()
engine := NewEngine(database, EngineConfig{
AgentDirs: map[parser.AgentType][]string{
parser.AgentReasonix: {reasonixDir},
},
Machine: "local",
})
sessionPath := filepath.Join(reasonixDir, "sessions", "tool-result.jsonl")
require.NoError(t, os.MkdirAll(filepath.Dir(sessionPath), 0o755))
require.NoError(t, os.WriteFile(sessionPath, []byte(
"{\"role\":\"user\",\"content\":\"Read the file\"}\n"+
"{\"role\":\"assistant\",\"content\":\"I'll read it\","+
"\"tool_calls\":[{\"id\":\"call_1\",\"name\":\"read_file\","+
"\"arguments\":\"{\\\"path\\\":\\\"config.json\\\"}\"}]}\n"+
"{\"role\":\"tool\",\"content\":\"file contents here\","+
"\"tool_call_id\":\"call_1\"}\n",
), 0o644))
engine.SyncPaths([]string{sessionPath})
msgs, err := database.GetAllMessages(context.Background(), "reasonix:tool-result")
require.NoError(t, err)
require.Len(t, msgs, 2)
require.Len(t, msgs[1].ToolCalls, 1)
assert.Equal(t, "file contents here", msgs[1].ToolCalls[0].ResultContent)
assert.Equal(t, len("file contents here"), msgs[1].ToolCalls[0].ResultContentLength)
}
func TestEngine_SyncSingleSessionEmitsOnSuccess(t *testing.T) {
fx := newEngineFixture(t)
em := &fakeEmitter{}
fx.engineWithEmitter(em)
path := fx.writeClaudeSession(t, "proj", "s1.jsonl", "hello")
// Seed DB first so SyncSingleSession has something to find.
fx.engine.SyncPaths([]string{path})
// Clear emissions from the seed, then append + SyncSingleSession.
em.mu.Lock()
em.scopes = em.scopes[:0]
em.mu.Unlock()
fx.appendClaudeMessage(t, path, "world")
sessionID := fx.sessionIDFor(t, path)
require.NoError(t, fx.engine.SyncSingleSession(sessionID), "SyncSingleSession")
got := em.got()
require.Len(t, got, 1, "expected 1 emission, got %v", got)
assert.Equal(t, "messages", got[0], "SyncSingleSession scope")
}
func TestToDBSessionTerminationStatus(t *testing.T) {
tests := []struct {
name string
in parser.TerminationStatus
want *string
}{
{name: "empty maps to nil", in: "", want: nil},
{name: "clean maps to pointer", in: parser.TerminationClean, want: new("clean")},
{name: "tool_call_pending maps to pointer", in: parser.TerminationToolCallPending, want: new("tool_call_pending")},
{name: "truncated maps to pointer", in: parser.TerminationTruncated, want: new("truncated")},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
pw := pendingWrite{
sess: parser.ParsedSession{
ID: "s1",
Project: "p",
Machine: "m",
Agent: parser.AgentClaude,
StartedAt: time.Now(),
EndedAt: time.Now(),
MessageCount: 1,
UserMessageCount: 1,
TerminationStatus: tc.in,
},
}
got := toDBSession(pw)
if tc.want == nil {
assert.Nil(t, got.TerminationStatus)
} else {
require.NotNil(t, got.TerminationStatus)
assert.Equal(t, *tc.want, *got.TerminationStatus)
}
})
}
}
func TestToDBSessionCarriesSessionName(t *testing.T) {
pw := pendingWrite{sess: parser.ParsedSession{
ID: "s1",
Project: "p",
Agent: parser.AgentClaude,
SessionName: "agent-name",
}}
s := toDBSession(pw)
require.NotNil(t, s.SessionName)
assert.Equal(t, "agent-name", *s.SessionName)
// converter must not touch display_name — only RenameSession may write it.
assert.Nil(t, s.DisplayName)
s2 := toDBSession(pendingWrite{sess: parser.ParsedSession{
ID: "s2",
Project: "p",
Agent: parser.AgentClaude,
}})
assert.Nil(t, s2.SessionName)
assert.Nil(t, s2.DisplayName)
}
// TestDiscoveredFileMtimeVisualStudioCopilotResolvesVirtualPath verifies that
// the mtime helper resolves a <traceFile>#<conversationID> virtual path to its
// physical trace before stat. Without resolution os.Stat fails on the virtual
// path, so SyncAllSince's mtime filter cannot drop unchanged Visual Studio
// conversations and re-syncs every one of them on each poll.
func TestDiscoveredFileMtimeVisualStudioCopilotResolvesVirtualPath(t *testing.T) {
dir := t.TempDir()
tracePath := filepath.Join(
dir, "20260612T194439_257709a3_VSGitHubCopilot_traces.jsonl",
)
require.NoError(t, os.WriteFile(tracePath, []byte("{}\n"), 0o644))
info, err := os.Stat(tracePath)
require.NoError(t, err)
virtual := parser.VisualStudioCopilotVirtualPath(
tracePath, "4a8f63f6-7626-4416-a874-fc7bd2c3f005",
)
mtime, err := discoveredFileMtime(parser.DiscoveredFile{
Path: virtual,
Agent: parser.AgentVSCopilot,
})
require.NoError(t, err,
"virtual path must resolve to the physical trace for stat")
assert.Equal(t, info.ModTime().UnixNano(), mtime)
}
// TestWriteIncrementalBlanksImplausibleEndedAt verifies that the
// incremental sync path runs the appended ended_at through the same
// timestamp plausibility check the full path applies in sanitizeSession.
// An out-of-window ended_at must not persist via incremental sync while a
// full sync of the same file would blank it (an incremental-vs-full
// parity divergence).
func TestWriteIncrementalBlanksImplausibleEndedAt(t *testing.T) {
for _, tc := range []struct {
name string
endedAt time.Time
}{
{name: "far past", endedAt: time.Date(1850, 1, 1, 0, 0, 0, 0, time.UTC)},
{name: "far future", endedAt: time.Date(2999, 1, 1, 0, 0, 0, 0, time.UTC)},
} {
t.Run(tc.name, func(t *testing.T) {
database := openTestDB(t)
// writeIncremental needs the signal scheduler, so build
// via NewEngine rather than a bare struct literal.
e := NewEngine(database, EngineConfig{})
plausibleEnd := time.Date(2026, 6, 20, 10, 0, 0, 0, time.UTC)
start := plausibleEnd.Add(-time.Hour)
pw := pendingWrite{
sess: parser.ParsedSession{
ID: "inc-ts",
Project: "proj",
Machine: "host",
Agent: parser.AgentClaude,
StartedAt: start,
EndedAt: plausibleEnd,
MessageCount: 1,
},
msgs: []parser.ParsedMessage{{
Role: parser.RoleUser,
Content: "hello",
Timestamp: start,
}},
}
_, _, failed, _ := e.writeBatch(
[]pendingWrite{pw}, syncWriteDefault, false,
)
require.Equal(t, 0, failed, "initial session write must not fail")
before, err := database.GetSessionFull(context.Background(), "inc-ts")
require.NoError(t, err)
require.NotNil(t, before)
require.NotNil(t, before.EndedAt, "baseline ended_at must be set")
wantEnd := *before.EndedAt
err = e.writeIncremental(&incrementalUpdate{
sessionID: "inc-ts",
msgs: []parser.ParsedMessage{{
Role: parser.RoleAssistant,
Content: "world",
Timestamp: plausibleEnd,
Ordinal: 1,
}},
endedAt: tc.endedAt,
msgCount: 2,
userMsgCount: 1,
fileSize: 100,
fileMtime: plausibleEnd.UnixNano(),
})
require.NoError(t, err, "writeIncremental")
after, err := database.GetSessionFull(context.Background(), "inc-ts")
require.NoError(t, err)
require.NotNil(t, after)
require.NotNil(t, after.EndedAt,
"implausible ended_at must be blanked, leaving the prior value via COALESCE")
// The implausible appended timestamp must not have been
// stored. Because it is blanked to nil, COALESCE keeps the
// prior plausible value.
assert.Equal(t, wantEnd, *after.EndedAt,
"implausible ended_at must not overwrite the plausible value")
assert.NotContains(t, *after.EndedAt, "1850",
"far-past ended_at must not persist")
assert.NotContains(t, *after.EndedAt, "2999",
"far-future ended_at must not persist")
})
}
}
// TestWriteIncrementalKeepsPlausibleEndedAt is the positive control for
// TestWriteIncrementalBlanksImplausibleEndedAt: a plausible appended
// ended_at must still update the column.
func TestWriteIncrementalKeepsPlausibleEndedAt(t *testing.T) {
database := openTestDB(t)
// writeIncremental needs the signal scheduler, so build via
// NewEngine rather than a bare struct literal.
e := NewEngine(database, EngineConfig{})
start := time.Date(2026, 6, 20, 10, 0, 0, 0, time.UTC)
firstEnd := start.Add(time.Hour)
pw := pendingWrite{
sess: parser.ParsedSession{
ID: "inc-ts-ok",
Project: "proj",
Machine: "host",
Agent: parser.AgentClaude,
StartedAt: start,
EndedAt: firstEnd,
MessageCount: 1,
},
msgs: []parser.ParsedMessage{{
Role: parser.RoleUser,
Content: "hello",
Timestamp: start,
}},
}
_, _, failed, _ := e.writeBatch(
[]pendingWrite{pw}, syncWriteDefault, false,
)
require.Equal(t, 0, failed, "initial session write must not fail")
newEnd := start.Add(2 * time.Hour)
err := e.writeIncremental(&incrementalUpdate{
sessionID: "inc-ts-ok",
msgs: []parser.ParsedMessage{{
Role: parser.RoleAssistant,
Content: "world",
Timestamp: newEnd,
Ordinal: 1,
}},
endedAt: newEnd,
msgCount: 2,
userMsgCount: 1,
fileSize: 100,
fileMtime: newEnd.UnixNano(),
})
require.NoError(t, err, "writeIncremental")
after, err := database.GetSessionFull(context.Background(), "inc-ts-ok")
require.NoError(t, err)
require.NotNil(t, after)
require.NotNil(t, after.EndedAt)
gotEnd, ok := parseStoredTimestamp(*after.EndedAt)
require.True(t, ok, "stored ended_at must parse")
assert.True(t, gotEnd.Equal(newEnd),
"plausible appended ended_at must update the column: got %q want %s",
*after.EndedAt, newEnd.Format(time.RFC3339Nano))
}
func TestConvertToolCallsFilePathAndCallIndex(t *testing.T) {
parsed := []parser.ParsedToolCall{
{ToolName: "Edit", Category: "Edit", ToolUseID: "a",
InputJSON: `{"file_path":"/x.go"}`}, // resolved from JSON
{ToolName: "Write", Category: "Write", ToolUseID: "b",
InputJSON: "raw diff not json", FilePath: "/native.go"}, // native wins
{ToolName: "Bash", Category: "Bash", ToolUseID: "c",
InputJSON: `{"command":"ls"}`}, // no path
}
got := convertToolCalls("sess-1", parsed)
require.Len(t, got, 3)
assert.Equal(t, "/x.go", got[0].FilePath)
assert.Equal(t, 0, got[0].CallIndex)
assert.Equal(t, "/native.go", got[1].FilePath)
assert.Equal(t, 1, got[1].CallIndex)
assert.Equal(t, "", got[2].FilePath)
assert.Equal(t, 2, got[2].CallIndex)
}
// codexRenameFixture is a seeded Codex session whose stored file_mtime is the
// folded index-mtime watermark, used to exercise title-rename detection in
// codexIndexSessionNameChanged.
type codexRenameFixture struct {
e *Engine
path string
info os.FileInfo
effectiveMtime int64
root string
uuid string
}
// writeCodexIndexForTest writes the session_index.jsonl mapping uuid -> title
// at indexMtime, the file codexIndexSessionNameChanged's title check reads.
func writeCodexIndexForTest(
t *testing.T, root, uuid, title string, indexMtime time.Time,
) string {
t.Helper()
idxPath := filepath.Join(root, parser.CodexSessionIndexFilename)
line := `{"id":"` + uuid + `","thread_name":"` + title + `"}` + "\n"
require.NoError(t, os.WriteFile(idxPath, []byte(line), 0o600))
require.NoError(t, os.Chtimes(idxPath, indexMtime, indexMtime))
return idxPath
}
// seedCodexRenameCase stores a Codex session whose file_mtime watermark is the
// folded index mtime (the index is newer than the transcript). That is the
// exact shape where a later title-only rename whose index mtime lands at or
// below the watermark is invisible to an mtime comparison.
func seedCodexRenameCase(t *testing.T, database *db.DB) codexRenameFixture {
t.Helper()
root := t.TempDir()
const uuid = "11111111-2222-3333-4444-555555555555"
sessDir := filepath.Join(root, "sessions", "2026", "06", "21")
require.NoError(t, os.MkdirAll(sessDir, 0o755))
path := filepath.Join(sessDir, "rollout-2026-06-21T18-59-38-"+uuid+".jsonl")
content := testjsonl.JoinJSONL(
testjsonl.CodexSessionMetaJSON(
uuid, "/home/user/code/api", "user", "2026-06-21T18:59:38Z",
),
testjsonl.CodexMsgJSON("user", "review this", "2026-06-21T18:59:39Z"),
)
require.NoError(t, os.WriteFile(path, []byte(content), 0o600))
transcriptMtime := time.Unix(1_700_000_000, 0)
require.NoError(t, os.Chtimes(path, transcriptMtime, transcriptMtime))
origIndexMtime := transcriptMtime.Add(time.Hour)
writeCodexIndexForTest(t, root, uuid, "Original Title", origIndexMtime)
info, err := os.Stat(path)
require.NoError(t, err, "stat codex fixture")
effectiveMtime := parser.CodexEffectiveMtime(path, info.ModTime().UnixNano())
require.Equal(t, origIndexMtime.UnixNano(), effectiveMtime,
"folded watermark should be the index mtime")
sess := db.Session{
ID: "host~codex:" + uuid,
Project: "api",
Machine: "host",
Agent: "codex",
SessionName: strPtr("Original Title"),
FilePath: strPtr("host:" + path),
FileSize: int64Ptr(info.Size()),
FileMtime: int64Ptr(effectiveMtime),
}
require.NoError(t, database.UpsertSession(sess))
require.NoError(t, database.SetSessionDataVersion(
sess.ID, db.CurrentDataVersion(),
))
e := &Engine{
db: database,
idPrefix: "host~",
pathRewriter: func(p string) string {
return "host:" + p
},
}
return codexRenameFixture{
e: e, path: path, info: info,
effectiveMtime: effectiveMtime, root: root, uuid: uuid,
}
}
// TestCodexIndexSessionNameChangedDetectsTitleRenameBelowStoredMtime pins the
// masking fix: a title-only rename whose folded index mtime is at or below the
// stored watermark is invisible to an mtime gate, so the skip decision instead
// consults codexIndexSessionNameChanged, which compares the live index title to
// the stored session_name directly. It must report a change for a renamed title
// while staying quiet for an unchanged one.
func TestCodexIndexSessionNameChangedDetectsTitleRenameBelowStoredMtime(t *testing.T) {
database := openTestDB(t)
f := seedCodexRenameCase(t, database)
// Control: nothing changed -> no rename reported, so the caller may skip.
assert.False(t, f.e.codexIndexSessionNameChanged(f.path),
"unchanged title must not report a change")
// Title-only rename whose index mtime lands at or below the stored
// watermark. The transcript bytes are untouched, so the mtime gate would
// skip; the mtime-independent title check must catch the rename.
writeCodexIndexForTest(t, f.root, f.uuid, "Renamed Title",
time.Unix(0, f.effectiveMtime))
renamedEff := parser.CodexEffectiveMtime(f.path, f.info.ModTime().UnixNano())
require.LessOrEqual(t, renamedEff, f.effectiveMtime,
"renamed index mtime must be at or below the stored watermark")
require.Equal(t, "Renamed Title",
parser.LookupCodexThreadName(f.path, f.uuid),
"live index must report the renamed title")
assert.True(t, f.e.codexIndexSessionNameChanged(f.path),
"title-only rename at or below stored watermark must report a change")
}
func TestCodexStoredNameDiffersPreservesMissingSemantics(t *testing.T) {
database := openTestDB(t)
require.NoError(t, database.UpsertSession(db.Session{
ID: "codex:null-name", Project: "p", Machine: "host", Agent: "codex",
}))
require.NoError(t, database.UpsertSession(db.Session{
ID: "codex:stored-name", Project: "p", Machine: "host", Agent: "codex",
SessionName: strPtr("Stored Title"),
}))
e := &Engine{db: database}
tests := []struct {
name string
sessionID string
indexTitle string
missingDiffers bool
want bool
}{
{
name: "direct refresh treats missing as changed", sessionID: "missing",
missingDiffers: true, want: true,
},
{
name: "index-only lookup ignores missing", sessionID: "missing",
indexTitle: "New Title", want: false,
},
{
name: "null name equals blank index title", sessionID: "codex:null-name",
indexTitle: " ", want: false,
},
{
name: "stored name trims whitespace", sessionID: "codex:stored-name",
indexTitle: " Stored Title ", want: false,
},
{
name: "stored rename differs", sessionID: "codex:stored-name",
indexTitle: "Renamed Title", want: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.want, e.codexStoredNameDiffersBySessionID(
tt.sessionID, tt.indexTitle, tt.missingDiffers,
))
})
}
}
func TestEngine_ClassifyPathsProviderRemoveSkipsMissingGeminiSource(
t *testing.T,
) {
db := openTestDB(t)
geminiDir := t.TempDir()
engine := NewEngine(db, EngineConfig{
AgentDirs: map[parser.AgentType][]string{
parser.AgentGemini: {geminiDir},
},
Machine: "local",
})
sessionPath := filepath.Join(
geminiDir, "tmp", "alias", "chats", "session-001.json",
)
dbtest.WriteTestFile(t, sessionPath, []byte("{}"))
require.NoError(t, os.Remove(sessionPath), "Remove(%q)", sessionPath)
files := engine.classifyPaths([]string{sessionPath})
assert.Empty(t, files)
}
func TestEngine_ClassifyPathsProviderSidecarKeepsExistingGeminiSources(
t *testing.T,
) {
db := openTestDB(t)
geminiDir := t.TempDir()
engine := NewEngine(db, EngineConfig{
AgentDirs: map[parser.AgentType][]string{
parser.AgentGemini: {geminiDir},
},
Machine: "local",
})
projectsPath := filepath.Join(geminiDir, "projects.json")
dbtest.WriteTestFile(
t,
projectsPath,
[]byte(`{"projects":{"/Users/alice/code/sample":"alias"}}`),
)
sessionPath := filepath.Join(
geminiDir, "tmp", "alias", "chats", "session-001.json",
)
dbtest.WriteTestFile(t, sessionPath, []byte("{}"))
files := engine.classifyPaths([]string{projectsPath})
require.Len(t, files, 1)
assert.Equal(t, sessionPath, files[0].Path)
assert.Equal(t, parser.AgentGemini, files[0].Agent)
assert.True(t, files[0].ForceParse)
}