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
1495 lines
40 KiB
Go
1495 lines
40 KiB
Go
package parser
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// setupFileSystem creates a temporary directory and populates
|
|
// it with the given relative file paths and contents.
|
|
func setupFileSystem(t *testing.T, dir string, files map[string]string) {
|
|
t.Helper()
|
|
for path, content := range files {
|
|
fullPath := filepath.Join(dir, path)
|
|
|
|
require.NoError(t, os.MkdirAll(filepath.Dir(fullPath), 0o755),
|
|
"mkdir %s", filepath.Dir(fullPath))
|
|
require.NoError(t, os.WriteFile(fullPath, []byte(content), 0o644),
|
|
"write %s", fullPath)
|
|
}
|
|
}
|
|
|
|
// assertDiscoveredFiles verifies that the discovered files match the expected filenames and agent type.
|
|
func assertDiscoveredFiles(t *testing.T, got []DiscoveredFile, wantFilenames []string, wantAgent AgentType) {
|
|
t.Helper()
|
|
|
|
want := make(map[string]bool)
|
|
for _, f := range wantFilenames {
|
|
want[f] = true
|
|
}
|
|
|
|
gotMap := make(map[string]bool)
|
|
for _, f := range got {
|
|
base := filepath.Base(f.Path)
|
|
gotMap[base] = true
|
|
assert.Equalf(t, wantAgent, f.Agent, "file %q: agent", base)
|
|
}
|
|
|
|
assert.Equal(t, len(want), len(got), "files total")
|
|
|
|
for file := range want {
|
|
assert.Truef(t, gotMap[file], "missing expected file: %q", file)
|
|
}
|
|
|
|
// Check for unexpected files
|
|
for file := range gotMap {
|
|
assert.Truef(t, want[file], "got unexpected file: %q", file)
|
|
}
|
|
}
|
|
|
|
func assertSourceRefs(t *testing.T, got []SourceRef, wantFilenames []string, wantAgent AgentType) {
|
|
t.Helper()
|
|
|
|
want := make(map[string]bool)
|
|
for _, f := range wantFilenames {
|
|
want[f] = true
|
|
}
|
|
|
|
gotMap := make(map[string]bool)
|
|
for _, f := range got {
|
|
base := filepath.Base(f.DisplayPath)
|
|
gotMap[base] = true
|
|
assert.Equalf(t, wantAgent, f.Provider, "file %q: provider", base)
|
|
}
|
|
|
|
assert.Equal(t, len(want), len(got), "files total")
|
|
|
|
for file := range want {
|
|
assert.Truef(t, gotMap[file], "missing expected file: %q", file)
|
|
}
|
|
for file := range gotMap {
|
|
assert.Truef(t, want[file], "got unexpected file: %q", file)
|
|
}
|
|
}
|
|
|
|
func TestDiscoverClaudeProjects(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
files map[string]string
|
|
wantFiles []string
|
|
}{
|
|
{
|
|
name: "Basic",
|
|
files: map[string]string{
|
|
filepath.Join("project-a", "abc.jsonl"): "{}",
|
|
filepath.Join("project-a", "def.jsonl"): "{}",
|
|
filepath.Join("project-a", "agent-123.jsonl"): "{}", // Should be ignored
|
|
filepath.Join("project-b", "xyz.jsonl"): "{}",
|
|
},
|
|
wantFiles: []string{
|
|
"abc.jsonl",
|
|
"def.jsonl",
|
|
"xyz.jsonl",
|
|
},
|
|
},
|
|
{
|
|
name: "Subagents",
|
|
files: map[string]string{
|
|
filepath.Join("project-a", "parent-session.jsonl"): "{}",
|
|
filepath.Join("project-a", "parent-session", "subagents", "agent-abc.jsonl"): "{}",
|
|
filepath.Join("project-a", "parent-session", "subagents", "agent-def.jsonl"): "{}",
|
|
filepath.Join("project-a", "parent-session", "subagents", "not-agent.jsonl"): "{}",
|
|
},
|
|
wantFiles: []string{
|
|
"parent-session.jsonl",
|
|
"agent-abc.jsonl",
|
|
"agent-def.jsonl",
|
|
},
|
|
},
|
|
{
|
|
name: "Nested workflow subagents",
|
|
files: map[string]string{
|
|
filepath.Join("project-a", "parent-session.jsonl"): "{}",
|
|
filepath.Join("project-a", "parent-session", "subagents", "workflows", "wf-123", "agent-deep.jsonl"): "{}",
|
|
filepath.Join("project-a", "parent-session", "subagents", "workflows", "wf-123", "agent-deep.meta.json"): "{}",
|
|
filepath.Join("project-a", "parent-session", "subagents", "workflows", "wf-123", "not-agent.jsonl"): "{}",
|
|
},
|
|
wantFiles: []string{
|
|
"parent-session.jsonl",
|
|
"agent-deep.jsonl",
|
|
},
|
|
},
|
|
{
|
|
name: "Empty",
|
|
files: map[string]string{},
|
|
wantFiles: nil,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
dir := t.TempDir()
|
|
setupFileSystem(t, dir, tt.files)
|
|
files := ClaudeProjectSessionFiles(dir)
|
|
|
|
assertDiscoveredFiles(t, files, tt.wantFiles, AgentClaude)
|
|
|
|
if tt.name == "Subagents" {
|
|
for _, f := range files {
|
|
assert.Equalf(t, "project-a", f.Project,
|
|
"file %q: project", filepath.Base(f.Path))
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
t.Run("Nonexistent", func(t *testing.T) {
|
|
dir := filepath.Join(t.TempDir(), "does-not-exist")
|
|
files := ClaudeProjectSessionFiles(dir)
|
|
assert.Nil(t, files, "expected nil")
|
|
})
|
|
}
|
|
|
|
func TestDiscoverCodexSessions(t *testing.T) {
|
|
file1 := "rollout-123-abc-def-ghi-jkl-mno.jsonl"
|
|
file2 := "rollout-456-abc-def-ghi-jkl-mno.jsonl"
|
|
flat := "rollout-2026-05-04T02-10-04-019df19b-ad1a-7f82-bfc3-38701744cc66.jsonl"
|
|
|
|
tests := []struct {
|
|
name string
|
|
files map[string]string
|
|
wantFiles []string
|
|
}{
|
|
{
|
|
name: "Basic",
|
|
files: map[string]string{
|
|
filepath.Join("2024", "01", "15", file1): "{}",
|
|
filepath.Join("2024", "02", "01", file2): "{}",
|
|
},
|
|
wantFiles: []string{file1, file2},
|
|
},
|
|
{
|
|
name: "FlatArchivedDir",
|
|
files: map[string]string{
|
|
flat: "{}",
|
|
},
|
|
wantFiles: []string{flat},
|
|
},
|
|
{
|
|
name: "SkipsNonDigit",
|
|
files: map[string]string{
|
|
filepath.Join("notes", "01", "01", "x.jsonl"): "{}",
|
|
},
|
|
wantFiles: nil,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
dir := t.TempDir()
|
|
setupFileSystem(t, dir, tt.files)
|
|
files := discoverCodexTestSessions(t, dir)
|
|
assertDiscoveredFiles(t, files, tt.wantFiles, AgentCodex)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAmpProviderDiscoversSessions(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
files map[string]string
|
|
wantFiles []string
|
|
}{
|
|
{
|
|
name: "Basic",
|
|
files: map[string]string{
|
|
"T-019ca26f-aaaa-bbbb-cccc-dddddddddddd.json": "{}",
|
|
"T-019ca26f-ffff-eeee-dddd-cccccccccccc.json": "{}",
|
|
"T-.json": "{}",
|
|
"T--invalid.json": "{}",
|
|
"README.md": "{}",
|
|
"T-not-json.txt": "{}",
|
|
},
|
|
wantFiles: []string{
|
|
"T-019ca26f-aaaa-bbbb-cccc-dddddddddddd.json",
|
|
"T-019ca26f-ffff-eeee-dddd-cccccccccccc.json",
|
|
},
|
|
},
|
|
{
|
|
name: "Empty",
|
|
files: map[string]string{},
|
|
wantFiles: nil,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
dir := t.TempDir()
|
|
setupFileSystem(t, dir, tt.files)
|
|
provider, ok := NewProvider(AgentAmp, ProviderConfig{
|
|
Roots: []string{dir},
|
|
Machine: "local",
|
|
})
|
|
require.True(t, ok)
|
|
files, err := provider.Discover(context.Background())
|
|
require.NoError(t, err)
|
|
assertSourceRefs(t, files, tt.wantFiles, AgentAmp)
|
|
})
|
|
}
|
|
|
|
t.Run("Nonexistent", func(t *testing.T) {
|
|
dir := filepath.Join(t.TempDir(), "does-not-exist")
|
|
provider, ok := NewProvider(AgentAmp, ProviderConfig{
|
|
Roots: []string{dir},
|
|
Machine: "local",
|
|
})
|
|
require.True(t, ok)
|
|
files, err := provider.Discover(context.Background())
|
|
require.NoError(t, err)
|
|
assert.Empty(t, files, "expected empty")
|
|
})
|
|
}
|
|
|
|
func TestFindClaudeSourceFile(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
files map[string]string
|
|
targetID string
|
|
wantFile string
|
|
}{
|
|
{
|
|
name: "Found",
|
|
files: map[string]string{
|
|
filepath.Join("project-a", "session-abc.jsonl"): "{}",
|
|
},
|
|
targetID: "session-abc",
|
|
wantFile: filepath.Join("project-a", "session-abc.jsonl"),
|
|
},
|
|
{
|
|
name: "Subagent",
|
|
files: map[string]string{
|
|
filepath.Join("project-a", "parent-sess", "subagents", "agent-sub1.jsonl"): "{}",
|
|
},
|
|
targetID: "agent-sub1",
|
|
wantFile: filepath.Join("project-a", "parent-sess", "subagents", "agent-sub1.jsonl"),
|
|
},
|
|
{
|
|
name: "Nested workflow subagent",
|
|
files: map[string]string{
|
|
filepath.Join("project-a", "parent-sess", "subagents", "workflows", "wf-123", "agent-deep.jsonl"): "{}",
|
|
},
|
|
targetID: "agent-deep",
|
|
wantFile: filepath.Join("project-a", "parent-sess", "subagents", "workflows", "wf-123", "agent-deep.jsonl"),
|
|
},
|
|
{
|
|
name: "Nonexistent",
|
|
files: map[string]string{
|
|
filepath.Join("project-a", "session-abc.jsonl"): "{}",
|
|
},
|
|
targetID: "nonexistent",
|
|
wantFile: "",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
dir := t.TempDir()
|
|
setupFileSystem(t, dir, tt.files)
|
|
|
|
got := claudeFindSourceFile(dir, tt.targetID)
|
|
want := ""
|
|
if tt.wantFile != "" {
|
|
want = filepath.Join(dir, tt.wantFile)
|
|
}
|
|
|
|
assert.Equal(t, want, got)
|
|
})
|
|
}
|
|
|
|
t.Run("Validation", func(t *testing.T) {
|
|
dir := t.TempDir()
|
|
tests := []string{"", "../etc/passwd", "a/b", "a b"}
|
|
for _, id := range tests {
|
|
got := claudeFindSourceFile(dir, id)
|
|
assert.Emptyf(t, got, "claudeFindSourceFile(%q)", id)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestAmpProviderFindsSourceFile(t *testing.T) {
|
|
t.Run("Found", func(t *testing.T) {
|
|
dir := t.TempDir()
|
|
rel := "T-019ca26f-aaaa-bbbb-cccc-dddddddddddd.json"
|
|
setupFileSystem(t, dir, map[string]string{
|
|
rel: "{}",
|
|
})
|
|
provider, ok := NewProvider(AgentAmp, ProviderConfig{
|
|
Roots: []string{dir},
|
|
Machine: "local",
|
|
})
|
|
require.True(t, ok)
|
|
got, ok, err := provider.FindSource(
|
|
context.Background(),
|
|
FindSourceRequest{
|
|
RawSessionID: "T-019ca26f-aaaa-bbbb-cccc-dddddddddddd",
|
|
},
|
|
)
|
|
require.NoError(t, err)
|
|
require.True(t, ok)
|
|
want := filepath.Join(dir, rel)
|
|
assert.Equal(t, want, got.DisplayPath)
|
|
})
|
|
|
|
t.Run("Nonexistent", func(t *testing.T) {
|
|
dir := t.TempDir()
|
|
provider, ok := NewProvider(AgentAmp, ProviderConfig{
|
|
Roots: []string{dir},
|
|
Machine: "local",
|
|
})
|
|
require.True(t, ok)
|
|
_, ok, err := provider.FindSource(
|
|
context.Background(),
|
|
FindSourceRequest{
|
|
RawSessionID: "T-019ca26f-aaaa-bbbb-cccc-dddddddddddd",
|
|
},
|
|
)
|
|
require.NoError(t, err)
|
|
assert.False(t, ok, "expected empty")
|
|
})
|
|
|
|
t.Run("Validation", func(t *testing.T) {
|
|
dir := t.TempDir()
|
|
provider, ok := NewProvider(AgentAmp, ProviderConfig{
|
|
Roots: []string{dir},
|
|
Machine: "local",
|
|
})
|
|
require.True(t, ok)
|
|
tests := []string{
|
|
"",
|
|
"../bad",
|
|
"T bad",
|
|
"bad",
|
|
"T-",
|
|
}
|
|
for _, id := range tests {
|
|
_, ok, err := provider.FindSource(
|
|
context.Background(),
|
|
FindSourceRequest{RawSessionID: id},
|
|
)
|
|
require.NoError(t, err)
|
|
assert.Falsef(t, ok, "Amp provider FindSource(%q)", id)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestFindCodexSourceFile(t *testing.T) {
|
|
uuid := "abc12345-1234-5678-9abc-def012345678"
|
|
filename := "rollout-20240115-" + uuid + ".jsonl"
|
|
relPath := filepath.Join("2024", "01", "15", filename)
|
|
flatName := "rollout-2026-05-04T02-10-04-" + uuid + ".jsonl"
|
|
|
|
tests := []struct {
|
|
name string
|
|
files map[string]string
|
|
targetID string
|
|
wantFile string
|
|
}{
|
|
{
|
|
name: "FoundDated",
|
|
files: map[string]string{relPath: "{}"},
|
|
targetID: uuid,
|
|
wantFile: relPath,
|
|
},
|
|
{
|
|
name: "FoundFlatArchived",
|
|
files: map[string]string{flatName: "{}"},
|
|
targetID: uuid,
|
|
wantFile: flatName,
|
|
},
|
|
{
|
|
name: "PrefersDatedOverFlat",
|
|
files: map[string]string{
|
|
relPath: "{}",
|
|
flatName: "{}",
|
|
},
|
|
targetID: uuid,
|
|
wantFile: relPath,
|
|
},
|
|
{
|
|
name: "Nonexistent",
|
|
files: map[string]string{relPath: "{}"},
|
|
targetID: "nonexistent-uuid",
|
|
wantFile: "",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
dir := t.TempDir()
|
|
setupFileSystem(t, dir, tt.files)
|
|
|
|
got := findCodexTestSourceFile(t, dir, tt.targetID)
|
|
want := ""
|
|
if tt.wantFile != "" {
|
|
want = filepath.Join(dir, tt.wantFile)
|
|
}
|
|
|
|
assert.Equal(t, want, got)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestExtractUUIDFromRollout(t *testing.T) {
|
|
tests := []struct {
|
|
filename string
|
|
want string
|
|
}{
|
|
{
|
|
"rollout-20240115-abc12345-1234-5678-9abc-def012345678.jsonl",
|
|
"abc12345-1234-5678-9abc-def012345678",
|
|
},
|
|
{
|
|
"rollout-20240115T100000-abc12345-1234-5678-9abc-def012345678.jsonl",
|
|
"abc12345-1234-5678-9abc-def012345678",
|
|
},
|
|
{
|
|
"short.jsonl",
|
|
"",
|
|
},
|
|
{
|
|
"rollout-20240115-12345678-1234-1234-1234-1234567890ab-abc12345-1234-5678-9abc-def012345678.jsonl",
|
|
"abc12345-1234-5678-9abc-def012345678",
|
|
},
|
|
{
|
|
"rollout-20240115-abc12345-1234-5678-9abc-def012345678-suffix.jsonl",
|
|
"",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.filename, func(t *testing.T) {
|
|
got := extractUUIDFromRollout(tt.filename)
|
|
assert.Equalf(t, tt.want, got, "extractUUID(%q)", tt.filename)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestIsValidSessionID(t *testing.T) {
|
|
tests := []struct {
|
|
id string
|
|
want bool
|
|
}{
|
|
{"abc-123", true},
|
|
{"session_1", true},
|
|
{"abc123", true},
|
|
{"", false},
|
|
{"../etc", false},
|
|
{"a b", false},
|
|
{"a/b", false},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.id, func(t *testing.T) {
|
|
got := IsValidSessionID(tt.id)
|
|
assert.Equalf(t, tt.want, got, "IsValidSessionID(%q)", tt.id)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestIsDigits(t *testing.T) {
|
|
tests := []struct {
|
|
s string
|
|
want bool
|
|
}{
|
|
{"123", true},
|
|
{"0", true},
|
|
{"", false},
|
|
{"12a", false},
|
|
{"abc", false},
|
|
{"\uff11\uff12\uff13", true}, // Fullwidth digits are supported
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.s, func(t *testing.T) {
|
|
got := IsDigits(tt.s)
|
|
assert.Equalf(t, tt.want, got, "IsDigits(%q)", tt.s)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDiscoverGeminiSessions(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
files map[string]string
|
|
wantFiles []string
|
|
}{
|
|
{
|
|
name: "Basic",
|
|
files: map[string]string{
|
|
filepath.Join("tmp", "hash1", geminiChatsDir, "session-2026-01-01T10-00-abc123.json"): "{}",
|
|
filepath.Join("tmp", "hash1", geminiChatsDir, "session-2026-01-02T10-00-def456.json"): "{}",
|
|
filepath.Join("tmp", "hash2", geminiChatsDir, "session-2026-01-03T10-00-ghi789.json"): "{}",
|
|
},
|
|
wantFiles: []string{
|
|
filepath.Join("tmp", "hash1", geminiChatsDir, "session-2026-01-01T10-00-abc123.json"),
|
|
filepath.Join("tmp", "hash1", geminiChatsDir, "session-2026-01-02T10-00-def456.json"),
|
|
filepath.Join("tmp", "hash2", geminiChatsDir, "session-2026-01-03T10-00-ghi789.json"),
|
|
},
|
|
},
|
|
{
|
|
name: "NoChatDir",
|
|
files: map[string]string{
|
|
filepath.Join("tmp", "hash1", "other.txt"): "{}",
|
|
},
|
|
wantFiles: nil,
|
|
},
|
|
{
|
|
name: "SkipsNonSessionFiles",
|
|
files: map[string]string{
|
|
filepath.Join("tmp", "hash1", geminiChatsDir, "session-abc.json"): "{}",
|
|
filepath.Join("tmp", "hash1", geminiChatsDir, "session-def.jsonl"): "{}",
|
|
filepath.Join("tmp", "hash1", geminiChatsDir, "other.json"): "{}",
|
|
filepath.Join("tmp", "hash1", geminiChatsDir, "session-def.txt"): "{}",
|
|
},
|
|
wantFiles: []string{
|
|
filepath.Join("tmp", "hash1", geminiChatsDir, "session-abc.json"),
|
|
filepath.Join("tmp", "hash1", geminiChatsDir, "session-def.jsonl"),
|
|
},
|
|
},
|
|
{
|
|
name: "NamedDirs",
|
|
files: map[string]string{
|
|
filepath.Join("tmp", "my-project", geminiChatsDir, "session-2026-01-01T10-00-abc.json"): "{}",
|
|
},
|
|
wantFiles: []string{
|
|
filepath.Join("tmp", "my-project", geminiChatsDir, "session-2026-01-01T10-00-abc.json"),
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
dir := t.TempDir()
|
|
setupFileSystem(t, dir, tt.files)
|
|
|
|
files := discoverGeminiTestSessions(t, dir)
|
|
|
|
require.Len(t, files, len(tt.wantFiles), "files count")
|
|
|
|
wantMap := make(map[string]bool)
|
|
for _, p := range tt.wantFiles {
|
|
wantMap[filepath.Join(dir, p)] = true
|
|
}
|
|
|
|
for _, f := range files {
|
|
assert.Equal(t, AgentGemini, f.Agent, "agent")
|
|
assert.Truef(t, wantMap[f.Path],
|
|
"unexpected file discovered: %q", f.Path)
|
|
}
|
|
})
|
|
}
|
|
|
|
t.Run("EmptyChatDir", func(t *testing.T) {
|
|
dir := t.TempDir()
|
|
require.NoError(t, os.MkdirAll(filepath.Join(dir, "tmp", "hash1", geminiChatsDir), 0o755), "mkdir")
|
|
files := discoverGeminiTestSessions(t, dir)
|
|
assert.Nil(t, files, "expected nil")
|
|
})
|
|
|
|
t.Run("Nonexistent", func(t *testing.T) {
|
|
files := discoverGeminiTestSessions(t, filepath.Join(t.TempDir(), "does-not-exist"))
|
|
assert.Nil(t, files, "expected nil")
|
|
})
|
|
|
|
t.Run("EmptyDir", func(t *testing.T) {
|
|
files := discoverGeminiTestSessions(t, "")
|
|
assert.Nil(t, files, "expected nil")
|
|
})
|
|
}
|
|
|
|
// TestDiscoverGeminiBuildsProjectMapOncePerRoot guards against rebuilding the
|
|
// project map for every discovered source. The map depends only on the root,
|
|
// and BuildGeminiProjectMap re-reads and SHA-256-hashes projects.json each
|
|
// call, so a per-source rebuild makes discovery scale with session count.
|
|
func TestDiscoverGeminiBuildsProjectMapOncePerRoot(t *testing.T) {
|
|
dir := t.TempDir()
|
|
setupFileSystem(t, dir, map[string]string{
|
|
filepath.Join("tmp", "hash1", geminiChatsDir, "session-2026-01-01T10-00-a.json"): "{}",
|
|
filepath.Join("tmp", "hash1", geminiChatsDir, "session-2026-01-02T10-00-b.json"): "{}",
|
|
filepath.Join("tmp", "hash2", geminiChatsDir, "session-2026-01-03T10-00-c.json"): "{}",
|
|
filepath.Join("tmp", "hash3", geminiChatsDir, "session-2026-01-04T10-00-d.json"): "{}",
|
|
})
|
|
|
|
var calls int
|
|
orig := buildGeminiProjectMap
|
|
buildGeminiProjectMap = func(root string) map[string]string {
|
|
calls++
|
|
return orig(root)
|
|
}
|
|
t.Cleanup(func() { buildGeminiProjectMap = orig })
|
|
|
|
got := discoverGeminiTestSessions(t, dir)
|
|
require.Len(t, got, 4, "expected all sessions discovered")
|
|
assert.Equal(t, 1, calls,
|
|
"project map should be built once per root, not once per source")
|
|
}
|
|
|
|
func TestFindGeminiSourceFile(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
files map[string]string
|
|
targetID string
|
|
wantFile string
|
|
}{
|
|
{
|
|
name: "Found",
|
|
files: map[string]string{
|
|
filepath.Join("tmp", "hash1", geminiChatsDir, "session-2026-01-19T18-21-b0a4eadd.json"): `{"sessionId":"b0a4eadd-cb99-4165-94d9-64cad5a66d24","messages":[]}`,
|
|
},
|
|
targetID: "b0a4eadd-cb99-4165-94d9-64cad5a66d24",
|
|
wantFile: filepath.Join("tmp", "hash1", geminiChatsDir, "session-2026-01-19T18-21-b0a4eadd.json"),
|
|
},
|
|
{
|
|
name: "FoundJSONL",
|
|
files: map[string]string{
|
|
filepath.Join("tmp", "hash1", geminiChatsDir, "session-2026-01-19T18-21-b0a4eadd.jsonl"): "{\"sessionId\":\"b0a4eadd-cb99-4165-94d9-64cad5a66d24\",\"kind\":\"main\"}\n",
|
|
},
|
|
targetID: "b0a4eadd-cb99-4165-94d9-64cad5a66d24",
|
|
wantFile: filepath.Join("tmp", "hash1", geminiChatsDir, "session-2026-01-19T18-21-b0a4eadd.jsonl"),
|
|
},
|
|
{
|
|
name: "Nonexistent",
|
|
files: map[string]string{
|
|
filepath.Join("tmp", "hash1", geminiChatsDir, "session-2026-01-19T18-21-b0a4eadd.json"): `{"sessionId":"b0a4eadd-cb99-4165-94d9-64cad5a66d24","messages":[]}`,
|
|
},
|
|
targetID: "nonexistent-uuid-1234",
|
|
wantFile: "",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
dir := t.TempDir()
|
|
setupFileSystem(t, dir, tt.files)
|
|
|
|
got := findGeminiTestSourceFile(t, dir, tt.targetID)
|
|
want := ""
|
|
if tt.wantFile != "" {
|
|
want = filepath.Join(dir, tt.wantFile)
|
|
}
|
|
|
|
assert.Equal(t, want, got)
|
|
})
|
|
}
|
|
|
|
t.Run("ShortID", func(t *testing.T) {
|
|
dir := t.TempDir()
|
|
for _, id := range []string{"", "a", "abc", "1234567"} {
|
|
got := findGeminiTestSourceFile(t, dir, id)
|
|
assert.Emptyf(t, got, "FindGeminiSourceFile(%q)", id)
|
|
}
|
|
})
|
|
|
|
t.Run("EmptyDir", func(t *testing.T) {
|
|
got := findGeminiTestSourceFile(t, "", "b0a4eadd-cb99-4165-94d9-64cad5a66d24")
|
|
assert.Empty(t, got, "expected empty")
|
|
})
|
|
}
|
|
|
|
func TestGeminiPathHash(t *testing.T) {
|
|
hash := geminiPathHash("/Users/alice/code/sample-repo")
|
|
assert.Len(t, hash, 64, "hash length")
|
|
assert.Equal(t, hash, geminiPathHash("/Users/alice/code/sample-repo"),
|
|
"hash not deterministic")
|
|
}
|
|
|
|
func TestBuildGeminiProjectMap(t *testing.T) {
|
|
dir := t.TempDir()
|
|
projectsJSON := `{"projects":{"/Users/alice/code/my-app":"my-app"}}`
|
|
require.NoError(t, os.WriteFile(
|
|
filepath.Join(dir, "projects.json"),
|
|
[]byte(projectsJSON), 0o644,
|
|
), "write")
|
|
|
|
m := BuildGeminiProjectMap(dir)
|
|
|
|
hash := geminiPathHash("/Users/alice/code/my-app")
|
|
assert.Equal(t, "my_app", m[hash], "project for hash")
|
|
assert.Equal(t, "my_app", m["my-app"], "project for name")
|
|
}
|
|
|
|
func TestBuildGeminiProjectMapMissingFile(t *testing.T) {
|
|
m := BuildGeminiProjectMap(t.TempDir())
|
|
assert.Empty(t, m, "expected empty map")
|
|
}
|
|
|
|
func TestResolveGeminiProject(t *testing.T) {
|
|
projectMap := map[string]string{
|
|
geminiPathHash("/Users/alice/code/my-app"): "my_app",
|
|
"my-app": "my_app",
|
|
"worktree1": "main_repo",
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
dirName string
|
|
want string
|
|
}{
|
|
{
|
|
"HashLookupHit",
|
|
geminiPathHash("/Users/alice/code/my-app"),
|
|
"my_app",
|
|
},
|
|
{
|
|
"HashLookupMiss",
|
|
geminiPathHash("/Users/alice/code/other"),
|
|
"unknown",
|
|
},
|
|
{
|
|
"NamedDirInMap",
|
|
"my-app",
|
|
"my_app",
|
|
},
|
|
{
|
|
"NamedDirWorktreeResolved",
|
|
"worktree1",
|
|
"main_repo",
|
|
},
|
|
{
|
|
"NamedDirNotInMap",
|
|
"new-project",
|
|
"new_project",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := ResolveGeminiProject(
|
|
tt.dirName, projectMap,
|
|
)
|
|
assert.Equal(t, tt.want, got)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestBuildGeminiProjectMapTrustedFolders(t *testing.T) {
|
|
dir := t.TempDir()
|
|
|
|
tfJSON := `{"trustedFolders":["/Users/alice/code/my-app","/Users/alice/code/other"]}`
|
|
require.NoError(t, os.WriteFile(
|
|
filepath.Join(dir, "trustedFolders.json"),
|
|
[]byte(tfJSON), 0o644,
|
|
), "write")
|
|
|
|
m := BuildGeminiProjectMap(dir)
|
|
|
|
hash1 := geminiPathHash("/Users/alice/code/my-app")
|
|
assert.Equal(t, "my_app", m[hash1], "hash for my-app")
|
|
hash2 := geminiPathHash("/Users/alice/code/other")
|
|
assert.Equal(t, "other", m[hash2], "hash for other")
|
|
}
|
|
|
|
func TestBuildGeminiProjectMapBothFiles(t *testing.T) {
|
|
dir := t.TempDir()
|
|
|
|
pJSON := `{"projects":{"/Users/alice/code/proj-a":"proj-a"}}`
|
|
require.NoError(t, os.WriteFile(
|
|
filepath.Join(dir, "projects.json"),
|
|
[]byte(pJSON), 0o644,
|
|
), "write")
|
|
|
|
tfJSON := `{"trustedFolders":["/Users/alice/code/proj-b"]}`
|
|
require.NoError(t, os.WriteFile(
|
|
filepath.Join(dir, "trustedFolders.json"),
|
|
[]byte(tfJSON), 0o644,
|
|
), "write")
|
|
|
|
m := BuildGeminiProjectMap(dir)
|
|
|
|
hashA := geminiPathHash("/Users/alice/code/proj-a")
|
|
assert.Equal(t, "proj_a", m[hashA], "proj-a hash")
|
|
hashB := geminiPathHash("/Users/alice/code/proj-b")
|
|
assert.Equal(t, "proj_b", m[hashB], "proj-b hash")
|
|
}
|
|
|
|
func TestBuildGeminiProjectMapProjectsWin(t *testing.T) {
|
|
dir := t.TempDir()
|
|
|
|
pJSON := `{"projects":{"/Users/alice/code/my-app":"my-app"}}`
|
|
require.NoError(t, os.WriteFile(
|
|
filepath.Join(dir, "projects.json"),
|
|
[]byte(pJSON), 0o644,
|
|
), "write")
|
|
|
|
tfJSON := `{"trustedFolders":["/Users/alice/code/my-app"]}`
|
|
require.NoError(t, os.WriteFile(
|
|
filepath.Join(dir, "trustedFolders.json"),
|
|
[]byte(tfJSON), 0o644,
|
|
), "write")
|
|
|
|
m := BuildGeminiProjectMap(dir)
|
|
|
|
hash := geminiPathHash("/Users/alice/code/my-app")
|
|
assert.Equal(t, "my_app", m[hash], "hash")
|
|
assert.Equal(t, "my_app", m["my-app"], "name key")
|
|
}
|
|
|
|
// --- Copilot discovery tests ---
|
|
|
|
func TestDiscoverCopilotSessions(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
files map[string]string
|
|
wantFiles []string
|
|
}{
|
|
{
|
|
name: "BareFormat",
|
|
files: map[string]string{
|
|
filepath.Join(copilotStateDir, "abc-123.jsonl"): "{}",
|
|
filepath.Join(copilotStateDir, "def-456.jsonl"): "{}",
|
|
},
|
|
wantFiles: []string{
|
|
filepath.Join(copilotStateDir, "abc-123.jsonl"),
|
|
filepath.Join(copilotStateDir, "def-456.jsonl"),
|
|
},
|
|
},
|
|
{
|
|
name: "DirFormat",
|
|
files: map[string]string{
|
|
filepath.Join(copilotStateDir, "sess-1", "events.jsonl"): "{}",
|
|
filepath.Join(copilotStateDir, "sess-2", "events.jsonl"): "{}",
|
|
},
|
|
wantFiles: []string{
|
|
filepath.Join(copilotStateDir, "sess-1", "events.jsonl"),
|
|
filepath.Join(copilotStateDir, "sess-2", "events.jsonl"),
|
|
},
|
|
},
|
|
{
|
|
name: "Mixed",
|
|
files: map[string]string{
|
|
filepath.Join(copilotStateDir, "bare-1.jsonl"): "{}",
|
|
filepath.Join(copilotStateDir, "dir-1", "events.jsonl"): "{}",
|
|
},
|
|
wantFiles: []string{
|
|
filepath.Join(copilotStateDir, "bare-1.jsonl"),
|
|
filepath.Join(copilotStateDir, "dir-1", "events.jsonl"),
|
|
},
|
|
},
|
|
{
|
|
name: "BareWithInvalidDir",
|
|
files: map[string]string{
|
|
filepath.Join(copilotStateDir, "invalid-dir-uuid.jsonl"): "{}",
|
|
filepath.Join(copilotStateDir, "invalid-dir-uuid", "other.txt"): "{}",
|
|
},
|
|
wantFiles: []string{
|
|
filepath.Join(copilotStateDir, "invalid-dir-uuid.jsonl"),
|
|
},
|
|
},
|
|
{
|
|
name: "DedupBareAndDir",
|
|
files: map[string]string{
|
|
filepath.Join(copilotStateDir, "dup-uuid-1234.jsonl"): "{}",
|
|
filepath.Join(copilotStateDir, "dup-uuid-1234", "events.jsonl"): "{}",
|
|
},
|
|
wantFiles: []string{
|
|
filepath.Join(copilotStateDir, "dup-uuid-1234", "events.jsonl"),
|
|
},
|
|
},
|
|
{
|
|
name: "DirWithoutEvents",
|
|
files: map[string]string{
|
|
filepath.Join(copilotStateDir, "no-events", "other.txt"): "{}",
|
|
},
|
|
wantFiles: nil,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
dir := t.TempDir()
|
|
setupFileSystem(t, dir, tt.files)
|
|
|
|
files := discoverCopilotTestSessions(t, dir)
|
|
|
|
require.Len(t, files, len(tt.wantFiles), "files count")
|
|
|
|
wantMap := make(map[string]bool)
|
|
for _, p := range tt.wantFiles {
|
|
wantMap[filepath.Join(dir, p)] = true
|
|
}
|
|
|
|
for _, f := range files {
|
|
assert.Equal(t, AgentCopilot, f.Agent, "agent")
|
|
assert.Truef(t, wantMap[f.Path],
|
|
"unexpected file discovered: %q", f.Path)
|
|
}
|
|
})
|
|
}
|
|
|
|
t.Run("EmptyDir", func(t *testing.T) {
|
|
files := discoverCopilotTestSessions(t, "")
|
|
assert.Nil(t, files, "expected nil")
|
|
})
|
|
|
|
t.Run("Nonexistent", func(t *testing.T) {
|
|
files := discoverCopilotTestSessions(t, filepath.Join(t.TempDir(), "does-not-exist"))
|
|
assert.Nil(t, files, "expected nil")
|
|
})
|
|
}
|
|
|
|
func TestFindCopilotSourceFile(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
files map[string]string
|
|
targetID string
|
|
wantFile string
|
|
}{
|
|
{
|
|
name: "Bare",
|
|
files: map[string]string{filepath.Join(copilotStateDir, "abc-123.jsonl"): "{}"},
|
|
targetID: "abc-123",
|
|
wantFile: filepath.Join(copilotStateDir, "abc-123.jsonl"),
|
|
},
|
|
{
|
|
name: "DirFormat",
|
|
files: map[string]string{filepath.Join(copilotStateDir, "sess-42", "events.jsonl"): "{}"},
|
|
targetID: "sess-42",
|
|
wantFile: filepath.Join(copilotStateDir, "sess-42", "events.jsonl"),
|
|
},
|
|
{
|
|
name: "Nonexistent",
|
|
files: map[string]string{filepath.Join(copilotStateDir, "abc-123.jsonl"): "{}"},
|
|
targetID: "nonexistent",
|
|
wantFile: "",
|
|
},
|
|
{
|
|
name: "DirPreferred",
|
|
files: map[string]string{
|
|
filepath.Join(copilotStateDir, "dual-1.jsonl"): "{}",
|
|
filepath.Join(copilotStateDir, "dual-1", "events.jsonl"): "{}",
|
|
},
|
|
targetID: "dual-1",
|
|
wantFile: filepath.Join(copilotStateDir, "dual-1", "events.jsonl"),
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
dir := t.TempDir()
|
|
setupFileSystem(t, dir, tt.files)
|
|
|
|
got := findCopilotTestSourceFile(t, dir, tt.targetID)
|
|
want := ""
|
|
if tt.wantFile != "" {
|
|
want = filepath.Join(dir, tt.wantFile)
|
|
}
|
|
|
|
assert.Equal(t, want, got)
|
|
})
|
|
}
|
|
|
|
t.Run("InvalidID", func(t *testing.T) {
|
|
dir := t.TempDir()
|
|
for _, id := range []string{"", "../etc/passwd", "a/b", "a b"} {
|
|
got := findCopilotTestSourceFile(t, dir, id)
|
|
assert.Emptyf(t, got, "FindCopilotSourceFile(%q)", id)
|
|
}
|
|
})
|
|
|
|
t.Run("EmptyDir", func(t *testing.T) {
|
|
got := findCopilotTestSourceFile(t, "", "abc-123")
|
|
assert.Empty(t, got, "expected empty")
|
|
})
|
|
}
|
|
|
|
// --- Symlink tests ---
|
|
|
|
func TestIsDirOrSymlink(t *testing.T) {
|
|
dir := t.TempDir()
|
|
|
|
realDir := filepath.Join(dir, "real-dir")
|
|
require.NoError(t, os.MkdirAll(realDir, 0o755), "mkdir")
|
|
|
|
realFile := filepath.Join(dir, "file.txt")
|
|
require.NoError(t, os.WriteFile(realFile, []byte("hi"), 0o644), "write")
|
|
|
|
if err := os.Symlink(
|
|
realDir, filepath.Join(dir, "link-to-dir"),
|
|
); err != nil {
|
|
t.Skipf("symlink not supported: %v", err)
|
|
}
|
|
|
|
require.NoError(t, os.Symlink(
|
|
realFile, filepath.Join(dir, "link-to-file"),
|
|
), "symlink")
|
|
|
|
require.NoError(t, os.Symlink(
|
|
filepath.Join(dir, "gone"),
|
|
filepath.Join(dir, "broken"),
|
|
), "symlink")
|
|
|
|
entries, err := os.ReadDir(dir)
|
|
require.NoError(t, err, "readdir")
|
|
|
|
want := map[string]bool{
|
|
"real-dir": true,
|
|
"file.txt": false,
|
|
"link-to-dir": true,
|
|
"link-to-file": false,
|
|
"broken": false,
|
|
}
|
|
|
|
for _, e := range entries {
|
|
expected, ok := want[e.Name()]
|
|
if !ok {
|
|
continue
|
|
}
|
|
got := isDirOrSymlink(e, dir)
|
|
assert.Equalf(t, expected, got, "isDirOrSymlink(%q)", e.Name())
|
|
}
|
|
}
|
|
|
|
func TestFindClaudeSourceFile_Symlink(t *testing.T) {
|
|
externalDir := t.TempDir()
|
|
realDir := filepath.Join(externalDir, "real-project")
|
|
require.NoError(t, os.MkdirAll(realDir, 0o755), "mkdir")
|
|
require.NoError(t, os.WriteFile(
|
|
filepath.Join(realDir, "sess-abc.jsonl"),
|
|
[]byte("{}"), 0o644,
|
|
), "write")
|
|
|
|
searchDir := t.TempDir()
|
|
linkDir := filepath.Join(searchDir, "linked-project")
|
|
if err := os.Symlink(realDir, linkDir); err != nil {
|
|
t.Skipf("symlink not supported: %v", err)
|
|
}
|
|
|
|
got := claudeFindSourceFile(searchDir, "sess-abc")
|
|
require.NotEmpty(t, got, "expected to find session via symlink")
|
|
assert.Equal(t, linkDir, filepath.Dir(got),
|
|
"expected path through symlink")
|
|
}
|
|
|
|
func TestIsContainedIn(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
child, root string
|
|
want bool
|
|
}{
|
|
{
|
|
name: "child under root",
|
|
child: "/a/b/c",
|
|
root: "/a/b",
|
|
want: true,
|
|
},
|
|
{
|
|
name: "same path",
|
|
child: "/a/b",
|
|
root: "/a/b",
|
|
want: false,
|
|
},
|
|
{
|
|
name: "child outside root",
|
|
child: "/a/x",
|
|
root: "/a/b",
|
|
want: false,
|
|
},
|
|
{
|
|
name: "traversal",
|
|
child: "/a/b/../x",
|
|
root: "/a/b",
|
|
want: false,
|
|
},
|
|
{
|
|
name: "parent of root",
|
|
child: "/a",
|
|
root: "/a/b",
|
|
want: false,
|
|
},
|
|
{
|
|
name: "dotdot-prefixed name",
|
|
child: "/a/b/..hidden",
|
|
root: "/a/b",
|
|
want: true,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := isContainedIn(tt.child, tt.root)
|
|
assert.Equalf(t, tt.want, got,
|
|
"isContainedIn(%q, %q)", tt.child, tt.root)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDiscoverCursorSessions(t *testing.T) {
|
|
cursorTranscripts := filepath.Join(
|
|
"proj-dir", "agent-transcripts",
|
|
)
|
|
|
|
tests := []struct {
|
|
name string
|
|
files map[string]string
|
|
wantCount int
|
|
}{
|
|
{
|
|
name: "TxtOnly",
|
|
files: map[string]string{
|
|
filepath.Join(cursorTranscripts, "aaa.txt"): "user:\nhi",
|
|
},
|
|
wantCount: 1,
|
|
},
|
|
{
|
|
name: "JsonlOnly",
|
|
files: map[string]string{
|
|
filepath.Join(cursorTranscripts, "bbb.jsonl"): `{"role":"user"}`,
|
|
},
|
|
wantCount: 1,
|
|
},
|
|
{
|
|
name: "BothExtensionsDedupToJsonl",
|
|
files: map[string]string{
|
|
filepath.Join(cursorTranscripts, "ccc.txt"): "user:\nhi",
|
|
filepath.Join(cursorTranscripts, "ccc.jsonl"): `{"role":"user"}`,
|
|
},
|
|
wantCount: 1,
|
|
},
|
|
{
|
|
name: "IgnoresOtherExtensions",
|
|
files: map[string]string{
|
|
filepath.Join(cursorTranscripts, "ddd.json"): "{}",
|
|
filepath.Join(cursorTranscripts, "eee.log"): "log",
|
|
},
|
|
wantCount: 0,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
dir := t.TempDir()
|
|
setupFileSystem(t, dir, tt.files)
|
|
set := newCursorSourceSet([]string{dir})
|
|
paths := set.discoverTranscriptPaths(dir)
|
|
require.Len(t, paths, tt.wantCount, "paths count")
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDiscoverCursorSessions_NestedLayout(t *testing.T) {
|
|
cursorTranscripts := filepath.Join(
|
|
"proj-dir", "agent-transcripts",
|
|
)
|
|
|
|
tests := []struct {
|
|
name string
|
|
files map[string]string
|
|
wantCount int
|
|
}{
|
|
{
|
|
name: "NestedJsonl",
|
|
files: map[string]string{
|
|
filepath.Join(cursorTranscripts, "aaa", "aaa.jsonl"): `{"role":"user"}`,
|
|
},
|
|
wantCount: 1,
|
|
},
|
|
{
|
|
name: "NestedTxt",
|
|
files: map[string]string{
|
|
filepath.Join(cursorTranscripts, "bbb", "bbb.txt"): "user:\nhi",
|
|
},
|
|
wantCount: 1,
|
|
},
|
|
{
|
|
name: "NestedWithSubagentsIgnored",
|
|
files: map[string]string{
|
|
filepath.Join(cursorTranscripts, "ccc", "ccc.jsonl"): `{"role":"user"}`,
|
|
filepath.Join(cursorTranscripts, "ccc", "subagents", "sub1.jsonl"): `{"role":"user"}`,
|
|
filepath.Join(cursorTranscripts, "ccc", "subagents", "sub2.jsonl"): `{"role":"user"}`,
|
|
},
|
|
wantCount: 1,
|
|
},
|
|
{
|
|
name: "NestedDedupPrefersJsonl",
|
|
files: map[string]string{
|
|
filepath.Join(cursorTranscripts, "ddd", "ddd.txt"): "user:\nhi",
|
|
filepath.Join(cursorTranscripts, "ddd", "ddd.jsonl"): `{"role":"user"}`,
|
|
},
|
|
wantCount: 1,
|
|
},
|
|
{
|
|
name: "NestedIgnoresAuxiliaryFiles",
|
|
files: map[string]string{
|
|
filepath.Join(cursorTranscripts, "eee", "eee.jsonl"): `{"role":"user"}`,
|
|
filepath.Join(cursorTranscripts, "eee", "other.jsonl"): `{"role":"user"}`,
|
|
filepath.Join(cursorTranscripts, "eee", "notes.txt"): "notes",
|
|
},
|
|
wantCount: 1,
|
|
},
|
|
{
|
|
name: "MixedFlatAndNested",
|
|
files: map[string]string{
|
|
filepath.Join(cursorTranscripts, "flat.txt"): "user:\nhi",
|
|
filepath.Join(cursorTranscripts, "nested", "nested.jsonl"): `{"role":"user"}`,
|
|
},
|
|
wantCount: 2,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
dir := t.TempDir()
|
|
setupFileSystem(t, dir, tt.files)
|
|
set := newCursorSourceSet([]string{dir})
|
|
paths := set.discoverTranscriptPaths(dir)
|
|
require.Len(t, paths, tt.wantCount, "paths count")
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDiscoverCursorSessions_DedupPrefersJsonl(t *testing.T) {
|
|
dir := t.TempDir()
|
|
transcripts := filepath.Join(
|
|
"proj-dir", "agent-transcripts",
|
|
)
|
|
setupFileSystem(t, dir, map[string]string{
|
|
filepath.Join(transcripts, "sess.txt"): "user:\nhi",
|
|
filepath.Join(transcripts, "sess.jsonl"): `{"role":"user"}`,
|
|
})
|
|
set := newCursorSourceSet([]string{dir})
|
|
paths := set.discoverTranscriptPaths(dir)
|
|
require.Len(t, paths, 1, "paths count")
|
|
assert.True(t, strings.HasSuffix(paths[0], ".jsonl"),
|
|
"expected .jsonl path, got %q", paths[0])
|
|
}
|
|
|
|
func TestParseCursorTranscriptRelPath(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
rel string
|
|
wantProject string
|
|
wantOK bool
|
|
}{
|
|
{
|
|
name: "flat txt",
|
|
rel: filepath.Join("proj-dir", "agent-transcripts", "sess.txt"),
|
|
wantProject: "proj-dir",
|
|
wantOK: true,
|
|
},
|
|
{
|
|
name: "flat jsonl",
|
|
rel: filepath.Join("proj-dir", "agent-transcripts", "sess.jsonl"),
|
|
wantProject: "proj-dir",
|
|
wantOK: true,
|
|
},
|
|
{
|
|
name: "nested jsonl",
|
|
rel: filepath.Join("proj-dir", "agent-transcripts", "sess", "sess.jsonl"),
|
|
wantProject: "proj-dir",
|
|
wantOK: true,
|
|
},
|
|
{
|
|
name: "nested txt",
|
|
rel: filepath.Join("proj-dir", "agent-transcripts", "sess", "sess.txt"),
|
|
wantProject: "proj-dir",
|
|
wantOK: true,
|
|
},
|
|
{
|
|
name: "nested mismatched filename",
|
|
rel: filepath.Join("proj-dir", "agent-transcripts", "sess", "other.jsonl"),
|
|
wantOK: false,
|
|
},
|
|
{
|
|
name: "nested auxiliary file",
|
|
rel: filepath.Join("proj-dir", "agent-transcripts", "sess", "notes.txt"),
|
|
wantOK: false,
|
|
},
|
|
{
|
|
name: "subagent file ignored",
|
|
rel: filepath.Join("proj-dir", "agent-transcripts", "sess", "subagents", "child.jsonl"),
|
|
wantOK: false,
|
|
},
|
|
{
|
|
name: "wrong extension",
|
|
rel: filepath.Join("proj-dir", "agent-transcripts", "sess.json"),
|
|
wantOK: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
gotProject, gotOK := ParseCursorTranscriptRelPath(tt.rel)
|
|
require.Equal(t, tt.wantOK, gotOK, "ok")
|
|
assert.Equal(t, tt.wantProject, gotProject, "project")
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFindCursorSourceFile(t *testing.T) {
|
|
cursorTranscripts := filepath.Join(
|
|
"proj-dir", "agent-transcripts",
|
|
)
|
|
|
|
t.Run("FindsTxt", func(t *testing.T) {
|
|
dir := t.TempDir()
|
|
setupFileSystem(t, dir, map[string]string{
|
|
filepath.Join(cursorTranscripts, "sess1.txt"): "data",
|
|
})
|
|
got := cursorFindSourceFile(dir, "sess1")
|
|
assert.NotEmpty(t, got, "expected to find .txt file")
|
|
})
|
|
|
|
t.Run("FindsJsonl", func(t *testing.T) {
|
|
dir := t.TempDir()
|
|
setupFileSystem(t, dir, map[string]string{
|
|
filepath.Join(cursorTranscripts, "sess2.jsonl"): "{}",
|
|
})
|
|
got := cursorFindSourceFile(dir, "sess2")
|
|
assert.NotEmpty(t, got, "expected to find .jsonl file")
|
|
})
|
|
|
|
t.Run("PrefersJsonlWhenBothExist", func(t *testing.T) {
|
|
dir := t.TempDir()
|
|
setupFileSystem(t, dir, map[string]string{
|
|
filepath.Join(cursorTranscripts, "sess3.txt"): "old",
|
|
filepath.Join(cursorTranscripts, "sess3.jsonl"): "new",
|
|
})
|
|
jsonlPath := filepath.Join(
|
|
dir, cursorTranscripts, "sess3.jsonl",
|
|
)
|
|
got := cursorFindSourceFile(dir, "sess3")
|
|
assert.Equal(t, jsonlPath, got, "(.jsonl preferred)")
|
|
})
|
|
|
|
t.Run("FindsNestedJsonl", func(t *testing.T) {
|
|
dir := t.TempDir()
|
|
setupFileSystem(t, dir, map[string]string{
|
|
filepath.Join(cursorTranscripts, "sess4", "sess4.jsonl"): "{}",
|
|
})
|
|
got := cursorFindSourceFile(dir, "sess4")
|
|
require.NotEmpty(t, got, "expected to find nested .jsonl file")
|
|
assert.True(t, strings.HasSuffix(got, filepath.Join("sess4", "sess4.jsonl")),
|
|
"unexpected path %q", got)
|
|
})
|
|
|
|
t.Run("PrefersJsonlOverNestedTxt", func(t *testing.T) {
|
|
dir := t.TempDir()
|
|
setupFileSystem(t, dir, map[string]string{
|
|
filepath.Join(cursorTranscripts, "sess5", "sess5.txt"): "old",
|
|
filepath.Join(cursorTranscripts, "sess5", "sess5.jsonl"): "new",
|
|
})
|
|
got := cursorFindSourceFile(dir, "sess5")
|
|
assert.True(t, strings.HasSuffix(got, "sess5.jsonl"),
|
|
"expected .jsonl path, got %q", got)
|
|
})
|
|
|
|
t.Run("NotFound", func(t *testing.T) {
|
|
dir := t.TempDir()
|
|
got := cursorFindSourceFile(dir, "nonexistent")
|
|
assert.Empty(t, got, "expected empty")
|
|
})
|
|
}
|
|
|
|
func TestIsPiSessionFile(t *testing.T) {
|
|
t.Run("ValidSession", func(t *testing.T) {
|
|
f, err := os.CreateTemp(t.TempDir(), "pi-*.jsonl")
|
|
require.NoError(t, err)
|
|
_, _ = f.WriteString(`{"type":"session","id":"abc"}` + "\n")
|
|
f.Close()
|
|
assert.True(t, IsPiSessionFile(f.Name()),
|
|
"expected true for valid session file")
|
|
})
|
|
|
|
t.Run("LongHeaderLine", func(t *testing.T) {
|
|
// Header line longer than 1 MiB — the old 1 MiB buffer would fail.
|
|
padding := strings.Repeat("x", 2*1024*1024)
|
|
line := `{"type":"session","id":"abc","pad":"` + padding + `"}` + "\n"
|
|
f, err := os.CreateTemp(t.TempDir(), "pi-*.jsonl")
|
|
require.NoError(t, err)
|
|
_, _ = f.WriteString(line)
|
|
f.Close()
|
|
assert.True(t, IsPiSessionFile(f.Name()),
|
|
"expected true for session file with long header line (>1 MiB)")
|
|
})
|
|
|
|
t.Run("LeadingBlankLines", func(t *testing.T) {
|
|
f, err := os.CreateTemp(t.TempDir(), "pi-*.jsonl")
|
|
require.NoError(t, err)
|
|
_, _ = f.WriteString("\n\n" + `{"type":"session","id":"abc"}` + "\n")
|
|
f.Close()
|
|
assert.True(t, IsPiSessionFile(f.Name()),
|
|
"expected true for session file with leading blank lines")
|
|
})
|
|
|
|
t.Run("NonSessionJSON", func(t *testing.T) {
|
|
f, err := os.CreateTemp(t.TempDir(), "pi-*.jsonl")
|
|
require.NoError(t, err)
|
|
_, _ = f.WriteString(`{"type":"message","id":"abc"}` + "\n")
|
|
f.Close()
|
|
assert.False(t, IsPiSessionFile(f.Name()),
|
|
"expected false for non-session JSON")
|
|
})
|
|
|
|
t.Run("OMPTitleSlotBeforeSession", func(t *testing.T) {
|
|
// OMP (Oh My Pi) v16.3+ writes a fixed-width rewritable title slot
|
|
// as the first line; the session header is on the second line.
|
|
f, err := os.CreateTemp(t.TempDir(), "omp-*.jsonl")
|
|
require.NoError(t, err)
|
|
_, _ = f.WriteString(
|
|
`{"type":"title","v":1,"title":"","updatedAt":"2026-07-02T09:48:32.328Z","pad":" "}` + "\n" +
|
|
`{"type":"session","version":3,"id":"abc","timestamp":"2026-07-02T09:48:32.328Z","cwd":"/repos/x"}` + "\n")
|
|
f.Close()
|
|
assert.True(t, IsPiSessionFile(f.Name()),
|
|
"expected true for OMP file with title slot before session header")
|
|
})
|
|
|
|
t.Run("OMPTitleSlotWithoutSession", func(t *testing.T) {
|
|
f, err := os.CreateTemp(t.TempDir(), "omp-*.jsonl")
|
|
require.NoError(t, err)
|
|
_, _ = f.WriteString(
|
|
`{"type":"title","v":1,"title":"orphan","updatedAt":"2026-07-02T09:48:32.328Z","pad":" "}` + "\n")
|
|
f.Close()
|
|
assert.False(t, IsPiSessionFile(f.Name()),
|
|
"expected false for title slot with no session header")
|
|
})
|
|
|
|
t.Run("EmptyFile", func(t *testing.T) {
|
|
f, err := os.CreateTemp(t.TempDir(), "pi-*.jsonl")
|
|
require.NoError(t, err)
|
|
f.Close()
|
|
assert.False(t, IsPiSessionFile(f.Name()),
|
|
"expected false for empty file")
|
|
})
|
|
}
|
|
|
|
func TestDiscoverVibeSessionsIntegration(t *testing.T) {
|
|
// Test discovery with testdata
|
|
files := discoverVibeTestSessions(t, "testdata/vibe")
|
|
|
|
// Should find all session directories with messages.jsonl
|
|
require.NotEmpty(t, files)
|
|
|
|
// Verify all files are Vibe sessions
|
|
for _, f := range files {
|
|
assert.Equal(t, AgentVibe, f.Agent)
|
|
assert.Contains(t, f.Path, "messages.jsonl")
|
|
}
|
|
|
|
// Should find at least 3 sessions (basic, with_tools, empty)
|
|
assert.GreaterOrEqual(t, len(files), 3)
|
|
}
|
|
|
|
func TestFindVibeSourceFileIntegration(t *testing.T) {
|
|
// Test with actual testdata
|
|
sessionID := "session_basic"
|
|
result := findVibeTestSourceFile(t, "testdata/vibe", sessionID)
|
|
|
|
expected := filepath.Join("testdata", "vibe", sessionID, "messages.jsonl")
|
|
assert.Equal(t, expected, result)
|
|
}
|