Files
kenn-io--agentsview/internal/parser/copilot_ide_provider_test.go
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

1072 lines
36 KiB
Go

package parser
import (
"context"
"os"
"path/filepath"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestVSCodeCopilotProviderSourceMethods(t *testing.T) {
root := t.TempDir()
sessionID := "vscode-provider"
hashDir := filepath.Join(root, "workspaceStorage", "workspace-hash")
chatDir := filepath.Join(hashDir, "chatSessions")
jsonPath := filepath.Join(chatDir, sessionID+".json")
jsonlPath := filepath.Join(chatDir, sessionID+".jsonl")
writeSourceFile(t, filepath.Join(hashDir, "workspace.json"),
`{"folder":"file:///Users/alice/code/copilot-app"}`)
writeSourceFile(t, jsonPath, `{"version":3,"sessionId":"`+sessionID+`","requests":[]}`)
writeSourceFile(t, jsonlPath, strings.Join([]string{
`{"kind":0,"v":{"version":3,"sessionId":"` + sessionID + `","creationDate":1770650022790,"requests":[]}}`,
`{"kind":2,"k":["requests"],"v":[{"requestId":"req1","timestamp":1770650031889,"message":{"text":"Hello VS Code","parts":[]},"response":[{"value":"Hi from VS Code"}],"modelId":"copilot/claude-opus-4.8","result":{"metadata":{"promptTokens":42,"outputTokens":7,"resolvedModel":"claude-opus-4-8"}}}]}`,
}, "\n")+"\n")
provider, ok := NewProvider(AgentVSCodeCopilot, ProviderConfig{
Roots: []string{root},
Machine: "devbox",
})
require.True(t, ok)
plan, err := provider.WatchPlan(context.Background())
require.NoError(t, err)
require.Len(t, plan.Roots, 2)
assert.Equal(t, filepath.Join(root, "workspaceStorage"), plan.Roots[0].Path)
assert.True(t, plan.Roots[0].Recursive)
assert.Equal(t, filepath.Join(root, "globalStorage"), plan.Roots[1].Path)
assert.True(t, plan.Roots[1].Recursive)
discovered, err := provider.Discover(context.Background())
require.NoError(t, err)
require.Len(t, discovered, 1)
assert.Equal(t, jsonlPath, discovered[0].DisplayPath)
assert.Equal(t, "copilot-app", discovered[0].ProjectHint)
found, ok, err := provider.FindSource(context.Background(), FindSourceRequest{
FullSessionID: "host~vscode-copilot:" + sessionID,
})
require.NoError(t, err)
require.True(t, ok)
assert.Equal(t, jsonlPath, found.DisplayPath)
changed, err := provider.SourcesForChangedPath(
context.Background(),
ChangedPathRequest{Path: jsonlPath, EventKind: "write", WatchRoot: filepath.Join(root, "workspaceStorage")},
)
require.NoError(t, err)
require.Len(t, changed, 1)
assert.Equal(t, jsonlPath, changed[0].DisplayPath)
fingerprint, err := provider.Fingerprint(context.Background(), found)
require.NoError(t, err)
assert.Equal(t, jsonlPath, fingerprint.Key)
assert.Positive(t, fingerprint.Size)
assert.Positive(t, fingerprint.MTimeNS)
assert.NotEmpty(t, fingerprint.Hash)
outcome, err := provider.Parse(context.Background(), ParseRequest{
Source: found,
Fingerprint: fingerprint,
})
require.NoError(t, err)
require.True(t, outcome.ResultSetComplete)
require.Len(t, outcome.Results, 1)
require.False(t, outcome.ForceReplace)
result := outcome.Results[0]
assert.Equal(t, DataVersionCurrent, result.DataVersion)
assert.Equal(t, "vscode-copilot:"+sessionID, result.Result.Session.ID)
assert.Equal(t, AgentVSCodeCopilot, result.Result.Session.Agent)
assert.Equal(t, "copilot-app", result.Result.Session.Project)
assert.Equal(t, "devbox", result.Result.Session.Machine)
assert.Equal(t, fingerprint.Hash, result.Result.Session.File.Hash)
assert.Equal(t, fingerprint.Size, result.Result.Session.File.Size)
assert.Equal(t, fingerprint.MTimeNS, result.Result.Session.File.Mtime)
assert.Len(t, result.Result.Messages, 2)
require.Len(t, result.Result.UsageEvents, 1)
assert.Equal(t, "vscode-copilot", result.Result.UsageEvents[0].Source)
assert.Equal(t, "claude-opus-4-8", result.Result.UsageEvents[0].Model)
assert.Equal(t, 42, result.Result.UsageEvents[0].InputTokens)
assert.Equal(t, 7, result.Result.UsageEvents[0].OutputTokens)
}
func TestVSCodeCopilotProviderClassifiesDeletedAndMetadataPaths(t *testing.T) {
root := t.TempDir()
hashDir := filepath.Join(root, "workspaceStorage", "workspace-hash")
chatDir := filepath.Join(hashDir, "chatSessions")
workspacePath := filepath.Join(hashDir, "workspace.json")
jsonlPath := filepath.Join(chatDir, "deleted-jsonl.jsonl")
jsonPath := filepath.Join(chatDir, "fallback-json.json")
globalPath := filepath.Join(
root,
"globalStorage",
"emptyWindowChatSessions",
"deleted-global.json",
)
writeSourceFile(t, workspacePath,
`{"folder":"file:///Users/alice/code/copilot-app"}`)
writeSourceFile(t, jsonlPath, vscodeCopilotProviderJSONL("deleted-jsonl", "Hello deleted"))
writeSourceFile(t, jsonPath, vscodeCopilotProviderJSON("fallback-json", "Hello fallback"))
writeSourceFile(t, globalPath, vscodeCopilotProviderJSON("deleted-global", "Hello global"))
provider, ok := NewProvider(AgentVSCodeCopilot, ProviderConfig{Roots: []string{root}})
require.True(t, ok)
metadataChanged, err := provider.SourcesForChangedPath(
context.Background(),
ChangedPathRequest{Path: workspacePath, EventKind: "write"},
)
require.NoError(t, err)
assert.ElementsMatch(t,
[]string{jsonlPath, jsonPath},
sourceDisplayPaths(metadataChanged),
)
require.Len(t, metadataChanged, 2)
beforeMetadata, err := provider.Fingerprint(context.Background(), metadataChanged[0])
require.NoError(t, err)
writeSourceFile(t, workspacePath,
`{"folder":"file:///Users/alice/code/copilot-renamed-app"}`)
afterMetadata, err := provider.Fingerprint(context.Background(), metadataChanged[0])
require.NoError(t, err)
assert.NotEqual(t, beforeMetadata.Hash, afterMetadata.Hash)
require.NoError(t, os.Remove(jsonlPath))
deletedJSONL, err := provider.SourcesForChangedPath(
context.Background(),
ChangedPathRequest{Path: jsonlPath, EventKind: "remove"},
)
require.NoError(t, err)
require.Len(t, deletedJSONL, 1)
assert.Equal(t, jsonlPath, deletedJSONL[0].DisplayPath)
require.NoError(t, os.Remove(globalPath))
deletedGlobal, err := provider.SourcesForChangedPath(
context.Background(),
ChangedPathRequest{Path: globalPath, EventKind: "remove"},
)
require.NoError(t, err)
require.Len(t, deletedGlobal, 1)
assert.Equal(t, globalPath, deletedGlobal[0].DisplayPath)
}
func TestVisualStudioCopilotProviderSourceMethods(t *testing.T) {
root := t.TempDir()
conversationID := "4a8f63f6-7626-4416-a874-fc7bd2c3f005"
tracePath := filepath.Join(
root,
"20260612T194439_257709a3_VSGitHubCopilot_traces.jsonl",
)
writeSourceFile(t, tracePath, strings.Join([]string{
vsCopilotTraceLineJSON(conversationID,
"execute_tool run_command_in_terminal",
"1781293588624985000", "1781293588769581200",
map[string]string{
"gen_ai.tool.name": "run_command_in_terminal",
"gen_ai.tool.call.id": "call_123",
"gen_ai.tool.call.arguments": `{"command":"go test ./..."}`,
"gen_ai.tool.call.result": `{"Value":"ok"}`,
}),
vsCopilotTraceLineJSON(conversationID,
"invoke_agent GitHub Copilot",
"1781293600000000000", "1781293610000000000",
map[string]string{
"gen_ai.agent.name": "GitHub Copilot",
"gen_ai.request.model": "gpt-5.5",
"copilot_chat.mode": "Agent",
"copilot_chat.turn_count": "1",
}),
}, "\n")+"\n")
provider, ok := NewProvider(AgentVSCopilot, ProviderConfig{
Roots: []string{root},
Machine: "devbox",
})
require.True(t, ok)
plan, err := provider.WatchPlan(context.Background())
require.NoError(t, err)
require.Len(t, plan.Roots, 1)
assert.Equal(t, root, plan.Roots[0].Path)
assert.False(t, plan.Roots[0].Recursive)
assert.Equal(t, []string{"*_VSGitHubCopilot_traces.jsonl"}, plan.Roots[0].IncludeGlobs)
discovered, err := provider.Discover(context.Background())
require.NoError(t, err)
require.Len(t, discovered, 1)
virtualPath := VisualStudioCopilotVirtualPath(tracePath, conversationID)
assert.Equal(t, virtualPath, discovered[0].DisplayPath)
assert.Equal(t, "visualstudio", discovered[0].ProjectHint)
found, ok, err := provider.FindSource(context.Background(), FindSourceRequest{
RawSessionID: conversationID,
})
require.NoError(t, err)
require.True(t, ok)
assert.Equal(t, virtualPath, found.DisplayPath)
changed, err := provider.SourcesForChangedPath(
context.Background(),
ChangedPathRequest{Path: tracePath, EventKind: "write", WatchRoot: root},
)
require.NoError(t, err)
require.Len(t, changed, 1)
assert.Equal(t, tracePath, changed[0].DisplayPath)
fingerprint, err := provider.Fingerprint(context.Background(), found)
require.NoError(t, err)
assert.Equal(t, virtualPath, fingerprint.Key)
assert.Positive(t, fingerprint.Size)
assert.Positive(t, fingerprint.MTimeNS)
assert.NotEmpty(t, fingerprint.Hash)
foundWithProject := found
foundWithProject.ProjectHint = "stored-solution"
outcome, err := provider.Parse(context.Background(), ParseRequest{
Source: foundWithProject,
Fingerprint: fingerprint,
})
require.NoError(t, err)
require.True(t, outcome.ResultSetComplete)
require.True(t, outcome.ForceReplace)
require.Len(t, outcome.Results, 1)
result := outcome.Results[0]
assert.Equal(t, DataVersionCurrent, result.DataVersion)
assert.Equal(t, "visualstudio-copilot:"+conversationID, result.Result.Session.ID)
assert.Equal(t, AgentVSCopilot, result.Result.Session.Agent)
assert.Equal(t, "stored-solution", result.Result.Session.Project)
assert.Equal(t, "devbox", result.Result.Session.Machine)
assert.Equal(t, fingerprint.Hash, result.Result.Session.File.Hash)
assert.Len(t, result.Result.Messages, 1)
}
func TestVisualStudioCopilotProviderClassifiesDeletedTraceAndFansOutPhysicalTrace(
t *testing.T,
) {
root := t.TempDir()
firstConversationID := "4a8f63f6-7626-4416-a874-fc7bd2c3f005"
secondConversationID := "5b9f63f6-7626-4416-a874-fc7bd2c3f006"
tracePath := filepath.Join(
root,
"20260612T194439_257709a3_VSGitHubCopilot_traces.jsonl",
)
writeSourceFile(t, tracePath, strings.Join([]string{
vsCopilotTraceLineJSON(firstConversationID,
"execute_tool run_command_in_terminal",
"1781293588624985000", "1781293588769581200",
map[string]string{
"gen_ai.tool.name": "run_command_in_terminal",
"gen_ai.tool.call.id": "call_123",
"gen_ai.tool.call.arguments": `{"command":"go test ./..."}`,
"gen_ai.tool.call.result": `{"Value":"ok"}`,
}),
vsCopilotTraceLineJSON(secondConversationID,
"execute_tool run_command_in_terminal",
"1781293688624985000", "1781293688769581200",
map[string]string{
"gen_ai.tool.name": "run_command_in_terminal",
"gen_ai.tool.call.id": "call_456",
"gen_ai.tool.call.arguments": `{"command":"go vet ./..."}`,
"gen_ai.tool.call.result": `{"Value":"ok"}`,
}),
}, "\n")+"\n")
provider, ok := NewProvider(AgentVSCopilot, ProviderConfig{
Roots: []string{root},
Machine: "devbox",
})
require.True(t, ok)
discovered, err := provider.Discover(context.Background())
require.NoError(t, err)
require.Len(t, discovered, 2)
assert.ElementsMatch(t, []string{
VisualStudioCopilotVirtualPath(tracePath, firstConversationID),
VisualStudioCopilotVirtualPath(tracePath, secondConversationID),
}, sourceDisplayPaths(discovered))
changed, err := provider.SourcesForChangedPath(
context.Background(),
ChangedPathRequest{Path: tracePath, EventKind: "write"},
)
require.NoError(t, err)
require.Len(t, changed, 1)
assert.Equal(t, tracePath, changed[0].DisplayPath)
fingerprint, err := provider.Fingerprint(context.Background(), changed[0])
require.NoError(t, err)
outcome, err := provider.Parse(context.Background(), ParseRequest{
Source: changed[0],
Fingerprint: fingerprint,
})
require.NoError(t, err)
require.True(t, outcome.ForceReplace)
require.Len(t, outcome.Results, 2)
assert.ElementsMatch(t, []string{
"visualstudio-copilot:" + firstConversationID,
"visualstudio-copilot:" + secondConversationID,
}, parseOutcomeSessionIDs(outcome))
require.NoError(t, os.Remove(tracePath))
deleted, err := provider.SourcesForChangedPath(
context.Background(),
ChangedPathRequest{
Path: tracePath,
EventKind: "remove",
StoredSourcePaths: sourceDisplayPaths(discovered),
},
)
require.NoError(t, err)
require.Len(t, deleted, 1)
assert.Equal(t, tracePath, deleted[0].DisplayPath)
}
func TestVisualStudioCopilotProviderTombstonesDeletedVS2026SessionFile(
t *testing.T,
) {
root := t.TempDir()
conversationID := "5bc5f6d7-9a6e-4f9c-8f3c-b7be2e7d9f20"
sessionPath := filepath.Join(
root, ".vs", "SampleApp", "copilot-chat", "thread", "sessions",
conversationID,
)
writeSourceFile(t, sessionPath, vsCopilotTraceLineJSON(
conversationID,
"chat gpt-5.5", "1781293600000000000", "1781293610000000000",
map[string]string{
"gen_ai.operation.name": "chat",
"gen_ai.input.messages": `[{"role":"user","parts":[{"type":"text","content":"Run the tests."}]}]`,
},
)+"\n")
provider, ok := NewProvider(AgentVSCopilot, ProviderConfig{Roots: []string{root}})
require.True(t, ok)
discovered, err := provider.Discover(context.Background())
require.NoError(t, err)
require.Len(t, discovered, 1)
virtualPath := VisualStudioCopilotVirtualPath(sessionPath, conversationID)
assert.Equal(t, virtualPath, discovered[0].DisplayPath)
require.NoError(t, os.Remove(sessionPath))
deleted, err := provider.SourcesForChangedPath(
context.Background(),
ChangedPathRequest{
Path: sessionPath,
EventKind: "remove",
StoredSourcePaths: sourceDisplayPaths(discovered),
},
)
require.NoError(t, err)
require.Len(t, deleted, 1)
assert.Equal(t, virtualPath, deleted[0].DisplayPath)
}
func TestVisualStudioCopilotProviderCanonicalizesVS2026SessionFileIDCase(
t *testing.T,
) {
root := t.TempDir()
conversationID := "5bc5f6d7-9a6e-4f9c-8f3c-b7be2e7d9f20"
upperID := strings.ToUpper(conversationID)
sessionPath := filepath.Join(
root, ".vs", "SampleApp", "copilot-chat", "thread", "sessions",
upperID,
)
traceData := vsCopilotTraceLineJSON(
conversationID,
"chat gpt-5.5", "1781293600000000000", "1781293610000000000",
map[string]string{
"gen_ai.operation.name": "chat",
"gen_ai.input.messages": `[{"role":"user","parts":[{"type":"text","content":"Run the tests."}]}]`,
},
) + "\n"
legacyPath := filepath.Join(
root,
"20260612T194439_257709a3_VSGitHubCopilot_traces.jsonl",
)
writeSourceFile(t, legacyPath, traceData)
writeSourceFile(t, sessionPath, traceData)
older := time.Date(2026, 6, 12, 0, 0, 0, 0, time.UTC)
newer := time.Date(2026, 6, 13, 0, 0, 0, 0, time.UTC)
require.NoError(t, os.Chtimes(legacyPath, older, older))
require.NoError(t, os.Chtimes(sessionPath, newer, newer))
provider, ok := NewProvider(AgentVSCopilot, ProviderConfig{Roots: []string{root}})
require.True(t, ok)
discovered, err := provider.Discover(context.Background())
require.NoError(t, err)
require.Len(t, discovered, 1)
virtualPath := VisualStudioCopilotVirtualPath(sessionPath, conversationID)
assert.Equal(t, virtualPath, discovered[0].DisplayPath)
fingerprint, err := provider.Fingerprint(context.Background(), discovered[0])
require.NoError(t, err)
outcome, err := provider.Parse(context.Background(), ParseRequest{
Source: discovered[0],
Fingerprint: fingerprint,
})
require.NoError(t, err)
require.Len(t, outcome.Results, 1)
assert.Equal(t,
"visualstudio-copilot:"+conversationID,
outcome.Results[0].Result.Session.ID,
)
}
func TestVisualStudioCopilotProviderTombstonesUppercaseVS2026SessionFileID(
t *testing.T,
) {
root := t.TempDir()
conversationID := "5bc5f6d7-9a6e-4f9c-8f3c-b7be2e7d9f20"
sessionPath := filepath.Join(
root, ".vs", "SampleApp", "copilot-chat", "thread", "sessions",
strings.ToUpper(conversationID),
)
writeSourceFile(t, sessionPath, vsCopilotTraceLineJSON(
conversationID,
"chat gpt-5.5", "1781293600000000000", "1781293610000000000",
map[string]string{
"gen_ai.operation.name": "chat",
"gen_ai.input.messages": `[{"role":"user","parts":[{"type":"text","content":"Run the tests."}]}]`,
},
)+"\n")
provider, ok := NewProvider(AgentVSCopilot, ProviderConfig{Roots: []string{root}})
require.True(t, ok)
discovered, err := provider.Discover(context.Background())
require.NoError(t, err)
require.Len(t, discovered, 1)
virtualPath := VisualStudioCopilotVirtualPath(sessionPath, conversationID)
assert.Equal(t, virtualPath, discovered[0].DisplayPath)
require.NoError(t, os.Remove(sessionPath))
deleted, err := provider.SourcesForChangedPath(
context.Background(),
ChangedPathRequest{
Path: sessionPath,
EventKind: "remove",
StoredSourcePaths: sourceDisplayPaths(discovered),
},
)
require.NoError(t, err)
require.Len(t, deleted, 1)
assert.Equal(t, virtualPath, deleted[0].DisplayPath)
fingerprint, err := provider.Fingerprint(context.Background(), deleted[0])
require.NoError(t, err)
assert.Equal(t, SourceFingerprint{Key: virtualPath}, fingerprint)
outcome, err := provider.Parse(context.Background(), ParseRequest{
Source: deleted[0],
Fingerprint: fingerprint,
})
require.NoError(t, err)
assert.True(t, outcome.ResultSetComplete)
assert.True(t, outcome.ForceReplace)
assert.Equal(t, SkipNoSession, outcome.SkipReason)
assert.Empty(t, outcome.Results)
}
func TestVisualStudioCopilotProviderSupportsVS2026RootModes(
t *testing.T,
) {
root := t.TempDir()
vsRoot := filepath.Join(root, ".vs")
conversationID := "5bc5f6d7-9a6e-4f9c-8f3c-b7be2e7d9f20"
sessionPath := filepath.Join(
vsRoot, "SampleApp", "copilot-chat", "thread", "sessions",
conversationID,
)
traceData := vsCopilotTraceLineJSON(
conversationID,
"chat gpt-5.5", "1781293600000000000", "1781293610000000000",
map[string]string{
"gen_ai.operation.name": "chat",
"gen_ai.input.messages": `[{"role":"user","parts":[{"type":"text","content":"Run the tests."}]}]`,
},
) + "\n"
writeSourceFile(t, sessionPath, traceData)
virtualPath := VisualStudioCopilotVirtualPath(sessionPath, conversationID)
cases := []struct {
name string
root string
}{
{name: "project root", root: root},
{name: ".vs root", root: vsRoot},
{name: "copilot-chat root", root: filepath.Join(vsRoot, "SampleApp", "copilot-chat")},
{name: "thread root", root: filepath.Join(vsRoot, "SampleApp", "copilot-chat", "thread")},
{name: "sessions root", root: filepath.Join(vsRoot, "SampleApp", "copilot-chat", "thread", "sessions")},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
provider, ok := NewProvider(AgentVSCopilot, ProviderConfig{
Roots: []string{tc.root},
})
require.True(t, ok)
plan, err := provider.WatchPlan(context.Background())
require.NoError(t, err)
switch tc.name {
case "project root":
require.Len(t, plan.Roots, 3)
assert.Equal(t, root, plan.Roots[0].Path)
assert.False(t, plan.Roots[0].Recursive)
assert.Equal(t, vsRoot, plan.Roots[1].Path)
assert.True(t, plan.Roots[1].Recursive)
assert.Equal(t, filepath.Dir(sessionPath), plan.Roots[2].Path)
assert.False(t, plan.Roots[2].Recursive)
case "sessions root":
require.Len(t, plan.Roots, 1)
assert.Equal(t, tc.root, plan.Roots[0].Path)
assert.False(t, plan.Roots[0].Recursive)
default:
require.Len(t, plan.Roots, 2)
assert.Equal(t, tc.root, plan.Roots[0].Path)
assert.True(t, plan.Roots[0].Recursive)
assert.Equal(t, filepath.Dir(sessionPath), plan.Roots[1].Path)
assert.False(t, plan.Roots[1].Recursive)
}
discovered, err := provider.Discover(context.Background())
require.NoError(t, err)
require.Len(t, discovered, 1)
assert.Equal(t, virtualPath, discovered[0].DisplayPath)
found, ok, err := provider.FindSource(context.Background(), FindSourceRequest{
RawSessionID: conversationID,
})
require.NoError(t, err)
require.True(t, ok)
assert.Equal(t, virtualPath, found.DisplayPath)
changed, err := provider.SourcesForChangedPath(
context.Background(),
ChangedPathRequest{
Path: sessionPath,
EventKind: "write",
WatchRoot: tc.root,
},
)
require.NoError(t, err)
require.Len(t, changed, 1)
assert.Equal(t, virtualPath, changed[0].DisplayPath)
fingerprint, err := provider.Fingerprint(context.Background(), changed[0])
require.NoError(t, err)
assert.Equal(t, virtualPath, fingerprint.Key)
assert.Positive(t, fingerprint.Size)
assert.Positive(t, fingerprint.MTimeNS)
assert.NotEmpty(t, fingerprint.Hash)
parseOutcome, err := provider.Parse(context.Background(), ParseRequest{
Source: changed[0],
Fingerprint: fingerprint,
})
require.NoError(t, err)
require.True(t, parseOutcome.ForceReplace)
require.Len(t, parseOutcome.Results, 1)
assert.Equal(t, "visualstudio-copilot:"+conversationID,
parseOutcome.Results[0].Result.Session.ID)
assert.Equal(t, virtualPath,
parseOutcome.Results[0].Result.Session.File.Path)
assert.Equal(t, fingerprint.Hash,
parseOutcome.Results[0].Result.Session.File.Hash)
assert.Equal(t, fingerprint.Size,
parseOutcome.Results[0].Result.Session.File.Size)
assert.Equal(t, fingerprint.MTimeNS,
parseOutcome.Results[0].Result.Session.File.Mtime)
})
}
}
func TestVisualStudioCopilotProviderClassifiesMixedCaseVS2026Layout(
t *testing.T,
) {
root := t.TempDir()
conversationID := "5bc5f6d7-9a6e-4f9c-8f3c-b7be2e7d9f20"
sessionPath := filepath.Join(
root, ".VS", "SampleApp", "Copilot-Chat", "thread", "Sessions",
conversationID,
)
writeSourceFile(t, sessionPath, vsCopilotTraceLineJSON(
conversationID,
"chat gpt-5.5", "1781293600000000000", "1781293610000000000",
map[string]string{
"gen_ai.operation.name": "chat",
"gen_ai.input.messages": `[{"role":"user","parts":[{"type":"text","content":"Run the tests."}]}]`,
},
)+"\n")
virtualPath := VisualStudioCopilotVirtualPath(sessionPath, conversationID)
cases := []struct {
name string
root string
}{
{name: "project root", root: root},
{name: ".vs root", root: filepath.Join(root, ".VS")},
{name: "copilot-chat root", root: filepath.Join(root, ".VS", "SampleApp", "Copilot-Chat")},
{name: "thread root", root: filepath.Join(root, ".VS", "SampleApp", "Copilot-Chat", "thread")},
{name: "sessions root", root: filepath.Join(root, ".VS", "SampleApp", "Copilot-Chat", "thread", "Sessions")},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
provider, ok := NewProvider(AgentVSCopilot, ProviderConfig{
Roots: []string{tc.root},
})
require.True(t, ok)
changed, err := provider.SourcesForChangedPath(
context.Background(),
ChangedPathRequest{
Path: sessionPath,
EventKind: "write",
WatchRoot: tc.root,
},
)
require.NoError(t, err)
require.Len(t, changed, 1)
assert.Equal(t, virtualPath, changed[0].DisplayPath)
})
}
}
func TestVisualStudioCopilotProviderDiscoversMixedCaseVS2026Layout(
t *testing.T,
) {
root := t.TempDir()
vsRoot := filepath.Join(root, ".VS")
conversationID := "5bc5f6d7-9a6e-4f9c-8f3c-b7be2e7d9f20"
sessionPath := filepath.Join(
vsRoot, "SampleApp", "Copilot-Chat", "thread", "Sessions",
conversationID,
)
writeSourceFile(t, sessionPath, vsCopilotTraceLineJSON(
conversationID,
"chat gpt-5.5", "1781293600000000000", "1781293610000000000",
map[string]string{
"gen_ai.operation.name": "chat",
"gen_ai.input.messages": `[{"role":"user","parts":[{"type":"text","content":"Run the tests."}]}]`,
},
)+"\n")
provider, ok := NewProvider(AgentVSCopilot, ProviderConfig{
Roots: []string{root},
})
require.True(t, ok)
virtualPath := VisualStudioCopilotVirtualPath(sessionPath, conversationID)
plan, err := provider.WatchPlan(context.Background())
require.NoError(t, err)
require.Len(t, plan.Roots, 3)
assert.Equal(t, root, plan.Roots[0].Path)
assert.False(t, plan.Roots[0].Recursive)
assert.Equal(t, vsRoot, plan.Roots[1].Path)
assert.True(t, plan.Roots[1].Recursive)
assert.Equal(t, filepath.Dir(sessionPath), plan.Roots[2].Path)
assert.False(t, plan.Roots[2].Recursive)
discovered, err := provider.Discover(context.Background())
require.NoError(t, err)
require.Len(t, discovered, 1)
assert.Equal(t, virtualPath, discovered[0].DisplayPath)
found, ok, err := provider.FindSource(context.Background(), FindSourceRequest{
RawSessionID: conversationID,
})
require.NoError(t, err)
require.True(t, ok)
assert.Equal(t, virtualPath, found.DisplayPath)
}
func TestVisualStudioCopilotProviderDiscoversSymlinkedVS2026Dirs(
t *testing.T,
) {
root := t.TempDir()
targetRoot := t.TempDir()
targetVSRoot := filepath.Join(targetRoot, "vs-data")
targetSolutionRoot := filepath.Join(targetRoot, "solution-data")
targetCopilotChatRoot := filepath.Join(targetRoot, "chat-data")
targetThreadRoot := filepath.Join(targetRoot, "thread-data")
targetSessionsRoot := filepath.Join(targetRoot, "sessions-data")
require.NoError(t, os.MkdirAll(targetVSRoot, 0o755))
require.NoError(t, os.MkdirAll(targetSolutionRoot, 0o755))
require.NoError(t, os.MkdirAll(targetCopilotChatRoot, 0o755))
require.NoError(t, os.MkdirAll(targetThreadRoot, 0o755))
require.NoError(t, os.MkdirAll(targetSessionsRoot, 0o755))
vsRoot := filepath.Join(root, ".VS")
if err := os.Symlink(targetVSRoot, vsRoot); err != nil {
t.Skipf("symlink not supported: %v", err)
}
require.NoError(t, os.Symlink(
targetSolutionRoot,
filepath.Join(targetVSRoot, "SampleApp"),
))
require.NoError(t, os.Symlink(
targetCopilotChatRoot,
filepath.Join(targetSolutionRoot, "Copilot-Chat"),
))
require.NoError(t, os.Symlink(
targetThreadRoot,
filepath.Join(targetCopilotChatRoot, "thread"),
))
require.NoError(t, os.Symlink(
targetSessionsRoot,
filepath.Join(targetThreadRoot, "Sessions"),
))
conversationID := "5bc5f6d7-9a6e-4f9c-8f3c-b7be2e7d9f20"
writeSourceFile(t, filepath.Join(targetSessionsRoot, conversationID),
vsCopilotTraceLineJSON(
conversationID,
"chat gpt-5.5", "1781293600000000000", "1781293610000000000",
map[string]string{
"gen_ai.operation.name": "chat",
"gen_ai.input.messages": `[{"role":"user","parts":[{"type":"text","content":"Run the tests."}]}]`,
},
)+"\n")
sessionPath := filepath.Join(
vsRoot, "SampleApp", "Copilot-Chat", "thread", "Sessions",
conversationID,
)
virtualPath := VisualStudioCopilotVirtualPath(sessionPath, conversationID)
provider, ok := NewProvider(AgentVSCopilot, ProviderConfig{
Roots: []string{root},
})
require.True(t, ok)
plan, err := provider.WatchPlan(context.Background())
require.NoError(t, err)
require.Len(t, plan.Roots, 3)
assert.Equal(t, vsRoot, plan.Roots[1].Path)
assert.True(t, plan.Roots[1].Recursive)
assert.Equal(t, filepath.Dir(sessionPath), plan.Roots[2].Path)
assert.False(t, plan.Roots[2].Recursive)
discovered, err := provider.Discover(context.Background())
require.NoError(t, err)
require.Len(t, discovered, 1)
assert.Equal(t, virtualPath, discovered[0].DisplayPath)
found, ok, err := provider.FindSource(context.Background(), FindSourceRequest{
RawSessionID: conversationID,
})
require.NoError(t, err)
require.True(t, ok)
assert.Equal(t, virtualPath, found.DisplayPath)
}
func TestVisualStudioCopilotProviderCanonicalizesMixedLegacyAndVS2026Sources(
t *testing.T,
) {
root := t.TempDir()
conversationID := "5bc5f6d7-9a6e-4f9c-8f3c-b7be2e7d9f20"
legacyPath := filepath.Join(
root,
"20260612T194439_257709a3_VSGitHubCopilot_traces.jsonl",
)
sessionPath := filepath.Join(
root, ".vs", "SampleApp", "copilot-chat", "thread", "sessions",
conversationID,
)
traceData := vsCopilotTraceLineJSON(
conversationID,
"chat gpt-5.5", "1781293600000000000", "1781293610000000000",
map[string]string{
"gen_ai.operation.name": "chat",
"gen_ai.input.messages": `[{"role":"user","parts":[{"type":"text","content":"Run the tests."}]}]`,
},
) + "\n"
writeSourceFile(t, legacyPath, traceData)
writeSourceFile(t, sessionPath, traceData)
older := time.Date(2026, 6, 12, 0, 0, 0, 0, time.UTC)
newer := time.Date(2026, 6, 13, 0, 0, 0, 0, time.UTC)
require.NoError(t, os.Chtimes(legacyPath, older, older))
require.NoError(t, os.Chtimes(sessionPath, newer, newer))
provider, ok := NewProvider(AgentVSCopilot, ProviderConfig{Roots: []string{root}})
require.True(t, ok)
discovered, err := provider.Discover(context.Background())
require.NoError(t, err)
require.Len(t, discovered, 1)
virtualPath := VisualStudioCopilotVirtualPath(sessionPath, conversationID)
assert.Equal(t, virtualPath, discovered[0].DisplayPath)
found, ok, err := provider.FindSource(context.Background(), FindSourceRequest{
RawSessionID: conversationID,
})
require.NoError(t, err)
require.True(t, ok)
assert.Equal(t, virtualPath, found.DisplayPath)
}
func TestVisualStudioCopilotProviderDeletesVS2026SessionTombstone(
t *testing.T,
) {
root := t.TempDir()
conversationID := "5bc5f6d7-9a6e-4f9c-8f3c-b7be2e7d9f20"
sessionPath := filepath.Join(
root, ".vs", "SampleApp", "copilot-chat", "thread", "sessions",
conversationID,
)
writeSourceFile(t, sessionPath, vsCopilotTraceLineJSON(
conversationID,
"chat gpt-5.5", "1781293600000000000", "1781293610000000000",
map[string]string{
"gen_ai.operation.name": "chat",
"gen_ai.input.messages": `[{"role":"user","parts":[{"type":"text","content":"Run the tests."}]}]`,
},
)+"\n")
provider, ok := NewProvider(AgentVSCopilot, ProviderConfig{
Roots: []string{root},
})
require.True(t, ok)
discovered, err := provider.Discover(context.Background())
require.NoError(t, err)
require.Len(t, discovered, 1)
virtualPath := VisualStudioCopilotVirtualPath(sessionPath, conversationID)
assert.Equal(t, virtualPath, discovered[0].DisplayPath)
require.NoError(t, os.Remove(sessionPath))
changed, err := provider.SourcesForChangedPath(
context.Background(),
ChangedPathRequest{
Path: sessionPath,
EventKind: "remove",
WatchRoot: root,
StoredSourcePaths: []string{virtualPath},
},
)
require.NoError(t, err)
require.Len(t, changed, 1)
assert.Equal(t, virtualPath, changed[0].DisplayPath)
fingerprint, err := provider.Fingerprint(context.Background(), changed[0])
require.NoError(t, err)
assert.Equal(t, SourceFingerprint{Key: virtualPath}, fingerprint)
outcome, err := provider.Parse(context.Background(), ParseRequest{
Source: changed[0],
Fingerprint: fingerprint,
})
require.NoError(t, err)
assert.True(t, outcome.ResultSetComplete)
assert.True(t, outcome.ForceReplace,
"deleted VS 2026 session files must force-replace the archived member")
assert.Equal(t, SkipNoSession, outcome.SkipReason)
assert.Empty(t, outcome.Results)
}
func TestVisualStudioCopilotProviderRecanonicalizesDeletedVS2026Session(
t *testing.T,
) {
root := t.TempDir()
conversationID := "5bc5f6d7-9a6e-4f9c-8f3c-b7be2e7d9f20"
legacyPath := filepath.Join(
root,
"20260612T194439_257709a3_VSGitHubCopilot_traces.jsonl",
)
sessionPath := filepath.Join(
root, ".vs", "SampleApp", "copilot-chat", "thread", "sessions",
conversationID,
)
traceData := vsCopilotTraceLineJSON(
conversationID,
"chat gpt-5.5", "1781293600000000000", "1781293610000000000",
map[string]string{
"gen_ai.operation.name": "chat",
"gen_ai.input.messages": `[{"role":"user","parts":[{"type":"text","content":"Run the tests."}]}]`,
},
) + "\n"
writeSourceFile(t, legacyPath, traceData)
writeSourceFile(t, sessionPath, traceData)
older := time.Date(2026, 6, 12, 0, 0, 0, 0, time.UTC)
newer := time.Date(2026, 6, 13, 0, 0, 0, 0, time.UTC)
require.NoError(t, os.Chtimes(legacyPath, older, older))
require.NoError(t, os.Chtimes(sessionPath, newer, newer))
provider, ok := NewProvider(AgentVSCopilot, ProviderConfig{
Roots: []string{root},
})
require.True(t, ok)
discovered, err := provider.Discover(context.Background())
require.NoError(t, err)
require.Len(t, discovered, 1)
vsVirtualPath := VisualStudioCopilotVirtualPath(sessionPath, conversationID)
assert.Equal(t, vsVirtualPath, discovered[0].DisplayPath)
require.NoError(t, os.Remove(sessionPath))
changed, err := provider.SourcesForChangedPath(
context.Background(),
ChangedPathRequest{
Path: sessionPath,
EventKind: "remove",
WatchRoot: root,
StoredSourcePaths: []string{vsVirtualPath},
},
)
require.NoError(t, err)
require.Len(t, changed, 1)
legacyVirtualPath := VisualStudioCopilotVirtualPath(legacyPath, conversationID)
assert.Equal(t, legacyVirtualPath, changed[0].DisplayPath)
fingerprint, err := provider.Fingerprint(context.Background(), changed[0])
require.NoError(t, err)
outcome, err := provider.Parse(context.Background(), ParseRequest{
Source: changed[0],
Fingerprint: fingerprint,
})
require.NoError(t, err)
assert.True(t, outcome.ResultSetComplete)
assert.True(t, outcome.ForceReplace)
require.Len(t, outcome.Results, 1)
assert.Equal(
t,
string(AgentVSCopilot)+":"+conversationID,
outcome.Results[0].Result.Session.ID,
)
}
func TestVisualStudioCopilotProviderFindSourceRejectsMissingVS2026VirtualPath(
t *testing.T,
) {
root := t.TempDir()
conversationID := "5bc5f6d7-9a6e-4f9c-8f3c-b7be2e7d9f20"
sessionPath := filepath.Join(
root, ".vs", "SampleApp", "copilot-chat", "thread", "sessions",
conversationID,
)
virtualPath := VisualStudioCopilotVirtualPath(sessionPath, conversationID)
provider, ok := NewProvider(AgentVSCopilot, ProviderConfig{
Roots: []string{root},
})
require.True(t, ok)
found, ok, err := provider.FindSource(context.Background(), FindSourceRequest{
RawSessionID: conversationID,
StoredFilePath: virtualPath,
FingerprintKey: virtualPath,
RequireFreshSource: true,
PreferStoredSource: true,
})
require.NoError(t, err)
assert.False(t, ok)
assert.Empty(t, found.DisplayPath)
}
func TestVisualStudioCopilotProviderRejectsOutsideVS2026SessionLayout(
t *testing.T,
) {
root := t.TempDir()
conversationID := "8e5bb2d4-ef8e-4a90-a1f2-3d3d4d6fc9e9"
invalidPath := filepath.Join(
root, ".vs", "SampleApp", "copilot-chat", "thread", "transcripts",
conversationID,
)
require.NoError(t, os.MkdirAll(filepath.Dir(invalidPath), 0o755))
writeSourceFile(t, invalidPath, vsCopilotTraceLineJSON(
conversationID,
"chat gpt-5.5", "1781293600000000000", "1781293610000000000",
map[string]string{
"gen_ai.operation.name": "chat",
"gen_ai.input.messages": `[{"role":"user","parts":[{"type":"text","content":"Run the tests."}]}]`,
},
)+"\n")
provider, ok := NewProvider(AgentVSCopilot, ProviderConfig{
Roots: []string{root},
})
require.True(t, ok)
changed, err := provider.SourcesForChangedPath(
context.Background(),
ChangedPathRequest{
Path: invalidPath,
EventKind: "write",
WatchRoot: root,
},
)
require.NoError(t, err)
assert.Empty(t, changed)
discovered, err := provider.Discover(context.Background())
require.NoError(t, err)
assert.Empty(t, discovered)
}
func TestVisualStudioCopilotProviderRejectsNonGUIDVS2026SessionFileNames(
t *testing.T,
) {
root := t.TempDir()
invalidName := "not-a-guid"
invalidPath := filepath.Join(
root, ".vs", "SampleApp", "copilot-chat", "thread", "sessions",
invalidName,
)
writeSourceFile(t, invalidPath, vsCopilotTraceLineJSON(
"5bc5f6d7-9a6e-4f9c-8f3c-b7be2e7d9f20",
"chat gpt-5.5", "1781293600000000000", "1781293610000000000",
map[string]string{
"gen_ai.operation.name": "chat",
"gen_ai.input.messages": `[{"role":"user","parts":[{"type":"text","content":"Run the tests."}]}]`,
},
)+"\n")
provider, ok := NewProvider(AgentVSCopilot, ProviderConfig{
Roots: []string{root},
})
require.True(t, ok)
changed, err := provider.SourcesForChangedPath(
context.Background(),
ChangedPathRequest{
Path: invalidPath,
EventKind: "write",
WatchRoot: root,
},
)
require.NoError(t, err)
assert.Empty(t, changed)
discovered, err := provider.Discover(context.Background())
require.NoError(t, err)
assert.Empty(t, discovered)
}
func vscodeCopilotProviderJSON(sessionID, prompt string) string {
return `{"version":3,"sessionId":"` + sessionID + `","creationDate":1770650022790,"requests":[{"requestId":"req1","timestamp":1770650031889,"message":{"text":"` + prompt + `","parts":[]},"response":[{"value":"Hi from VS Code"}],"modelId":"copilot/gpt-4o"}]}`
}
func vscodeCopilotProviderJSONL(sessionID, prompt string) string {
return strings.Join([]string{
`{"kind":0,"v":{"version":3,"sessionId":"` + sessionID + `","creationDate":1770650022790,"requests":[]}}`,
`{"kind":2,"k":["requests"],"v":[{"requestId":"req1","timestamp":1770650031889,"message":{"text":"` + prompt + `","parts":[]},"response":[{"value":"Hi from VS Code"}],"modelId":"copilot/gpt-4o"}]}`,
}, "\n") + "\n"
}
func parseOutcomeSessionIDs(outcome ParseOutcome) []string {
ids := make([]string, 0, len(outcome.Results))
for _, result := range outcome.Results {
ids = append(ids, result.Result.Session.ID)
}
return ids
}