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

909 lines
26 KiB
Go

package parser
import (
"encoding/json"
"os"
"path/filepath"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestParseVSCodeCopilotSession(t *testing.T) {
tests := []struct {
name string
json string
wantNil bool
wantMessages int
wantTitle string
wantAgent AgentType
wantToolUse bool
}{
{
name: "empty requests",
json: `{"version":3,"sessionId":"abc","requests":[]}`,
wantNil: true,
},
{
name: "single user+assistant turn",
json: `{
"version": 3,
"sessionId": "test-123",
"creationDate": 1755347684754,
"lastMessageDate": 1755347728048,
"customTitle": "Test session",
"requests": [{
"requestId": "req1",
"message": {"text": "Hello world", "parts": []},
"response": [
{"value": "Hi there! ", "supportThemeIcons": false},
{"value": "How can I help?", "supportThemeIcons": false}
],
"timestamp": 1755347728047,
"modelId": "copilot/gpt-5"
}]
}`,
wantMessages: 2,
wantTitle: "Hello world",
wantAgent: AgentVSCodeCopilot,
},
{
name: "with tool invocations",
json: `{
"version": 3,
"sessionId": "tools-456",
"creationDate": 1755347684754,
"lastMessageDate": 1755347728048,
"customTitle": "Tool session",
"requests": [{
"requestId": "req1",
"message": {"text": "Read the file", "parts": []},
"response": [
{"value": "Reading the file... "},
{"kind": "prepareToolInvocation", "toolName": "copilot_readFile"},
{"kind": "toolInvocationSerialized", "toolId": "copilot_readFile", "toolCallId": "tc1", "isConfirmed": true, "isComplete": true},
{"value": "Done reading."}
],
"timestamp": 1755347728047,
"modelId": "copilot/gpt-5"
}]
}`,
wantMessages: 2,
wantToolUse: true,
},
{
name: "multiple requests",
json: `{
"version": 3,
"sessionId": "multi-789",
"creationDate": 1755340000000,
"lastMessageDate": 1755350000000,
"customTitle": "Multi turn",
"requests": [
{
"requestId": "req1",
"message": {"text": "First question"},
"response": [{"value": "First answer"}],
"timestamp": 1755340000000
},
{
"requestId": "req2",
"message": {"text": "Second question"},
"response": [{"value": "Second answer"}],
"timestamp": 1755350000000
}
]
}`,
wantMessages: 4,
wantTitle: "First question",
},
{
name: "no user text uses customTitle",
json: `{
"version": 3,
"sessionId": "notitle-000",
"creationDate": 1755340000000,
"lastMessageDate": 1755340000000,
"customTitle": "Fallback Title",
"requests": [{
"requestId": "req1",
"message": {"text": ""},
"response": [{"value": "Some response"}],
"timestamp": 1755340000000
}]
}`,
wantMessages: 1,
wantTitle: "Fallback Title",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "test-session.json")
require.NoError(t, os.WriteFile(
path, []byte(tt.json), 0644,
))
sess, msgs, err := parseVSCodeCopilotTestSession(t,
path, "testproject", "local",
)
require.NoError(t, err)
if tt.wantNil {
assert.Nil(t, sess, "expected nil session")
return
}
require.NotNil(t, sess, "expected non-nil session")
assert.Len(t, msgs, tt.wantMessages, "messages")
if tt.wantTitle != "" {
assert.Equal(t, tt.wantTitle, sess.FirstMessage, "first message")
}
if tt.wantAgent != "" {
assert.Equal(t, tt.wantAgent, sess.Agent, "agent")
}
assert.Equal(t, "testproject", sess.Project, "project")
if tt.wantToolUse {
found := false
for _, m := range msgs {
if m.HasToolUse {
found = true
break
}
}
assert.True(t, found, "expected tool use in messages")
}
})
}
}
func TestParseVSCodeCopilotSession_NonExistent(t *testing.T) {
sess, msgs, err := parseVSCodeCopilotTestSession(t,
"/nonexistent/path.json", "proj", "local",
)
require.NoError(t, err, "expected nil error")
assert.Nil(t, sess, "expected nil session for non-existent file")
assert.Nil(t, msgs, "expected nil messages for non-existent file")
}
func TestParseVSCodeCopilotSession_MixedTextAndTools(t *testing.T) {
data := `{
"version": 3,
"sessionId": "mixed-001",
"creationDate": 1755340000000,
"lastMessageDate": 1755340000000,
"customTitle": "Mixed content",
"requests": [{
"requestId": "req1",
"message": {"text": "Read the file"},
"response": [
{"kind": "toolInvocationSerialized", "toolId": "copilot_readFile", "toolCallId": "tc1", "isConfirmed": true, "isComplete": true, "pastTenseMessage": {"value": "Read main.go, lines 1 to 50"}},
{"value": "Here is the file content."}
],
"timestamp": 1755340000000
}]
}`
dir := t.TempDir()
path := filepath.Join(dir, "test.json")
require.NoError(t, os.WriteFile(path, []byte(data), 0644))
_, msgs, err := parseVSCodeCopilotTestSession(t, path, "proj", "local")
require.NoError(t, err)
// Find assistant message
var assistant *ParsedMessage
for i := range msgs {
if msgs[i].Role == RoleAssistant {
assistant = &msgs[i]
break
}
}
require.NotNil(t, assistant, "no assistant message")
assert.True(t, assistant.HasToolUse, "expected HasToolUse=true")
// Content should include both tool markers and text
assert.NotEmpty(t, assistant.Content, "expected non-empty content")
// Tool calls should have InputJSON populated
require.Len(t, assistant.ToolCalls, 1)
tc := assistant.ToolCalls[0]
assert.NotEmpty(t, tc.InputJSON, "expected non-empty InputJSON")
assert.Equal(t, "Read", tc.Category, "category")
}
func TestParseVSCodeCopilotSession_TerminalToolData(t *testing.T) {
data := `{
"version": 3,
"sessionId": "term-001",
"creationDate": 1755340000000,
"lastMessageDate": 1755340000000,
"customTitle": "Terminal session",
"requests": [{
"requestId": "req1",
"message": {"text": "Run tests"},
"response": [
{"kind": "toolInvocationSerialized", "toolId": "copilot_runInTerminal", "toolCallId": "tc1", "isConfirmed": true, "isComplete": true, "invocationMessage": "Using \"Run In Terminal\"", "toolSpecificData": {"kind": "terminal", "language": "sh", "command": "npm test"}}
],
"timestamp": 1755340000000
}]
}`
dir := t.TempDir()
path := filepath.Join(dir, "test.json")
require.NoError(t, os.WriteFile(path, []byte(data), 0644))
_, msgs, err := parseVSCodeCopilotTestSession(t, path, "proj", "local")
require.NoError(t, err)
var assistant *ParsedMessage
for i := range msgs {
if msgs[i].Role == RoleAssistant {
assistant = &msgs[i]
break
}
}
require.NotNil(t, assistant, "no assistant message")
require.Len(t, assistant.ToolCalls, 1)
tc := assistant.ToolCalls[0]
assert.Equal(t, "Bash", tc.Category, "category")
assert.NotEmpty(t, tc.InputJSON, "expected non-empty InputJSON")
// Content should include the command
assert.Contains(t, assistant.Content, "npm test",
"content should contain command, got: %s", assistant.Content)
}
func TestExtractProjectFromURI(t *testing.T) {
tests := []struct {
uri string
want string
}{
{"file:///Users/dev/projects/myapp", "myapp"},
{"file:///home/user/code/repo", "repo"},
{"file:///C:/Users/dev/projects/app", "app"},
{"some-name", "some-name"},
}
for _, tt := range tests {
t.Run(tt.uri, func(t *testing.T) {
assert.Equal(t, tt.want, extractProjectFromURI(tt.uri),
"extractProjectFromURI(%q)", tt.uri)
})
}
}
func TestReadVSCodeWorkspaceManifest(t *testing.T) {
dir := t.TempDir()
// Valid workspace.json
content := `{"folder":"file:///Users/dev/projects/agentsview"}`
require.NoError(t, os.WriteFile(
filepath.Join(dir, "workspace.json"),
[]byte(content), 0644,
))
assert.Equal(t, "agentsview", ReadVSCodeWorkspaceManifest(dir))
// Non-existent dir
assert.Empty(t, ReadVSCodeWorkspaceManifest("/nonexistent"))
}
func TestDiscoverVSCodeCopilotSessions(t *testing.T) {
root := t.TempDir()
// Create workspace structure
hash := "abc123def456"
chatDir := filepath.Join(
root, "workspaceStorage", hash, "chatSessions",
)
require.NoError(t, os.MkdirAll(chatDir, 0755))
// workspace.json
wsJSON := `{"folder":"file:///Users/dev/projects/myproject"}`
wsPath := filepath.Join(
root, "workspaceStorage", hash, "workspace.json",
)
require.NoError(t, os.WriteFile(wsPath, []byte(wsJSON), 0644))
// Chat session file
sessionJSON := `{"version":3,"sessionId":"sess1","requests":[{"requestId":"r1","message":{"text":"hi"},"response":[{"value":"hello"}],"timestamp":1755340000000}]}`
sessPath := filepath.Join(chatDir, "sess1.json")
require.NoError(t, os.WriteFile(sessPath, []byte(sessionJSON), 0644))
// globalStorage/emptyWindowChatSessions
globalDir := filepath.Join(
root, "globalStorage", "emptyWindowChatSessions",
)
require.NoError(t, os.MkdirAll(globalDir, 0755))
globalPath := filepath.Join(globalDir, "global-sess.json")
require.NoError(t, os.WriteFile(globalPath, []byte(sessionJSON), 0644))
files := discoverVSCodeCopilotTestSessions(t, root)
require.Len(t, files, 2)
// Check workspace session
var wsFile, globalFile DiscoveredFile
for _, f := range files {
switch f.Project {
case "myproject":
wsFile = f
case "empty-window":
globalFile = f
}
}
assert.NotEmpty(t, wsFile.Path, "missing workspace session file")
assert.Equal(t, AgentVSCodeCopilot, wsFile.Agent, "agent")
assert.NotEmpty(t, globalFile.Path, "missing global session file")
}
func TestNormalizeVSCodeToolName(t *testing.T) {
tests := []struct {
input string
want string
}{
{"copilot_readFile", "read_file"},
{"copilot_replaceString", "edit_file"},
{"copilot_runCommand", "shell"},
{"copilot_searchFiles", "grep"},
{"copilot_listDir", "glob"},
{"copilot_createFile", "create_file"},
{"copilot_runInTerminal", "shell"},
{"copilot_getTerminalOutput", "shell"},
{"copilot_findTextInFiles", "grep"},
{"copilot_findFiles", "glob"},
{"copilot_listDirectory", "glob"},
{"copilot_applyPatch", "edit_file"},
{"copilot_multiReplaceString", "edit_file"},
{"copilot_fetchWebPage", "read_web_page"},
{"copilot_think", "Tool"},
{"runSubagent", "Task"},
{"unknown_tool", "unknown_tool"},
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
assert.Equal(t, tt.want, normalizeVSCodeToolName(tt.input))
})
}
}
func TestExtractVSCopilotInputJSON(t *testing.T) {
tests := []struct {
name string
invMsg string
pastMsg string
toolData string
wantKey string
wantVal string
}{
{
name: "string invocation message",
invMsg: `"Using Run In Terminal"`,
wantKey: "message",
wantVal: "Using Run In Terminal",
},
{
name: "object invocation message",
invMsg: `{"value": "Reading file.txt, lines 1 to 50"}`,
wantKey: "message",
wantVal: "Reading file.txt, lines 1 to 50",
},
{
name: "prefers pastTenseMessage",
invMsg: `"Reading file..."`,
pastMsg: `"Read file.txt, lines 1 to 50"`,
wantKey: "message",
wantVal: "Read file.txt, lines 1 to 50",
},
{
name: "terminal tool data",
invMsg: `"Using Run In Terminal"`,
toolData: `{"kind":"terminal","language":"sh","command":"ls -la"}`,
wantKey: "command",
wantVal: "ls -la",
},
{
name: "empty fields",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var inv, past, td json.RawMessage
if tt.invMsg != "" {
inv = json.RawMessage(tt.invMsg)
}
if tt.pastMsg != "" {
past = json.RawMessage(tt.pastMsg)
}
if tt.toolData != "" {
td = json.RawMessage(tt.toolData)
}
got := extractVSCopilotInputJSON(inv, past, td)
if tt.wantKey == "" {
assert.Empty(t, got, "expected empty")
return
}
var m map[string]any
err := json.Unmarshal([]byte(got), &m)
require.NoError(t, err, "invalid JSON")
val, ok := m[tt.wantKey].(string)
assert.True(t, ok, "value not a string")
assert.Equal(t, tt.wantVal, val, "value for key %q", tt.wantKey)
})
}
}
func TestParseVSCodeCopilotSession_JSONL(t *testing.T) {
tests := []struct {
name string
lines []string
wantNil bool
wantMessages int
wantTitle string
wantToolUse bool
}{
{
name: "simple session with mutations",
lines: []string{
// kind=0: initial snapshot with empty requests
`{"kind":0,"v":{"version":3,"sessionId":"jsonl-001","creationDate":1770650022790,"customTitle":"","requests":[],"responderUsername":"GitHub Copilot"}}`,
// kind=1: set customTitle
`{"kind":1,"k":["customTitle"],"v":"Test JSONL Session"}`,
// kind=2: push a request
`{"kind":2,"k":["requests"],"v":[{"requestId":"req1","timestamp":1770650031889,"message":{"text":"Hello JSONL","parts":[]},"response":[{"value":"Hi from JSONL!"}],"modelId":"copilot/gpt-4o"}]}`,
},
wantMessages: 2,
wantTitle: "Hello JSONL",
},
{
name: "empty session no requests",
lines: []string{
`{"kind":0,"v":{"version":3,"sessionId":"jsonl-empty","creationDate":1770650022790,"requests":[]}}`,
},
wantNil: true,
},
{
name: "session with tool calls",
lines: []string{
`{"kind":0,"v":{"version":3,"sessionId":"jsonl-tools","creationDate":1770650022790,"requests":[]}}`,
`{"kind":2,"k":["requests"],"v":[{"requestId":"req1","timestamp":1770650031889,"message":{"text":"Read file","parts":[]},"response":[{"kind":"toolInvocationSerialized","toolId":"copilot_readFile","toolCallId":"tc1","isConfirmed":true,"isComplete":true},{"value":"Done."}],"modelId":"copilot/gpt-4o"}]}`,
},
wantMessages: 2,
wantToolUse: true,
wantTitle: "Read file",
},
{
name: "multiple requests via push",
lines: []string{
`{"kind":0,"v":{"version":3,"sessionId":"jsonl-multi","creationDate":1770650022790,"requests":[]}}`,
`{"kind":2,"k":["requests"],"v":[{"requestId":"req1","timestamp":1770650031889,"message":{"text":"First","parts":[]},"response":[{"value":"Answer 1"}],"modelId":"copilot/gpt-4o"}]}`,
`{"kind":2,"k":["requests"],"v":[{"requestId":"req2","timestamp":1770650041889,"message":{"text":"Second","parts":[]},"response":[{"value":"Answer 2"}],"modelId":"copilot/gpt-4o"}]}`,
},
wantMessages: 4,
wantTitle: "First",
},
{
name: "set mutation on response",
lines: []string{
`{"kind":0,"v":{"version":3,"sessionId":"jsonl-set","creationDate":1770650022790,"requests":[{"requestId":"req1","timestamp":1770650031889,"message":{"text":"Q","parts":[]},"response":[{"value":"partial"}],"modelId":"copilot/gpt-4o"}]}}`,
// Update the first response item
`{"kind":1,"k":["requests",0,"response",0],"v":{"value":"Complete answer"}}`,
},
wantMessages: 2,
wantTitle: "Q",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "test-session.jsonl")
content := strings.Join(tt.lines, "\n") + "\n"
require.NoError(t, os.WriteFile(
path, []byte(content), 0644,
))
sess, msgs, err := parseVSCodeCopilotTestSession(t,
path, "testproject", "local",
)
require.NoError(t, err)
if tt.wantNil {
assert.Nil(t, sess, "expected nil session")
return
}
require.NotNil(t, sess, "expected non-nil session")
assert.Len(t, msgs, tt.wantMessages, "messages")
if tt.wantTitle != "" {
assert.Equal(t, tt.wantTitle, sess.FirstMessage, "first message")
}
assert.Equal(t, AgentVSCodeCopilot, sess.Agent, "agent")
if tt.wantToolUse {
found := false
for _, m := range msgs {
if m.HasToolUse {
found = true
break
}
}
assert.True(t, found, "expected tool use in messages")
}
})
}
}
func TestReconstructJSONL(t *testing.T) {
tests := []struct {
name string
lines []string
wantErr bool
check func(t *testing.T, data []byte)
}{
{
name: "initial only",
lines: []string{
`{"kind":0,"v":{"sessionId":"s1","version":3}}`,
},
check: func(t *testing.T, data []byte) {
var m map[string]any
require.NoError(t, json.Unmarshal(data, &m))
assert.Equal(t, "s1", m["sessionId"], "sessionId")
},
},
{
name: "set nested property",
lines: []string{
`{"kind":0,"v":{"a":{"b":"old"}}}`,
`{"kind":1,"k":["a","b"],"v":"new"}`,
},
check: func(t *testing.T, data []byte) {
var m map[string]any
require.NoError(t, json.Unmarshal(data, &m))
a := m["a"].(map[string]any)
assert.Equal(t, "new", a["b"])
},
},
{
name: "push to array",
lines: []string{
`{"kind":0,"v":{"items":["a"]}}`,
`{"kind":2,"k":["items"],"v":["b","c"]}`,
},
check: func(t *testing.T, data []byte) {
var m map[string]any
require.NoError(t, json.Unmarshal(data, &m))
items := m["items"].([]any)
require.Len(t, items, 3, "len")
assert.Equal(t, "c", items[2], "items[2]")
},
},
{
name: "push with splice index replaces existing items",
lines: []string{
`{"kind":0,"v":{"items":["a","old","c"]}}`,
`{"kind":2,"k":["items"],"v":["b"],"i":1}`,
},
check: func(t *testing.T, data []byte) {
var m map[string]any
require.NoError(t, json.Unmarshal(data, &m))
items := m["items"].([]any)
require.Len(t, items, 3, "len")
assert.Equal(t, []any{"a", "b", "c"}, items, "items")
},
},
{
name: "push with negative splice index replaces from front",
lines: []string{
`{"kind":0,"v":{"items":["a","b"]}}`,
`{"kind":2,"k":["items"],"v":["z"],"i":-1}`,
},
check: func(t *testing.T, data []byte) {
var m map[string]any
require.NoError(t, json.Unmarshal(data, &m))
items := m["items"].([]any)
require.Len(t, items, 2, "len")
// Negative index clamped to 0: replaced at front.
assert.Equal(t, "z", items[0], "items[0]")
},
},
{
name: "delete property",
lines: []string{
`{"kind":0,"v":{"a":"keep","b":"remove"}}`,
`{"kind":3,"k":["b"]}`,
},
check: func(t *testing.T, data []byte) {
var m map[string]any
require.NoError(t, json.Unmarshal(data, &m))
_, ok := m["b"]
assert.False(t, ok, "expected b to be deleted")
assert.Equal(t, "keep", m["a"], "a")
},
},
{
name: "set array element by index",
lines: []string{
`{"kind":0,"v":{"arr":["x","y","z"]}}`,
`{"kind":1,"k":["arr",1],"v":"Y"}`,
},
check: func(t *testing.T, data []byte) {
var m map[string]any
require.NoError(t, json.Unmarshal(data, &m))
arr := m["arr"].([]any)
assert.Equal(t, "Y", arr[1], "arr[1]")
},
},
{
name: "empty file returns nil",
lines: []string{},
check: func(t *testing.T, data []byte) {
assert.Nil(t, data, "expected nil")
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "test.jsonl")
content := strings.Join(tt.lines, "\n") + "\n"
require.NoError(t, os.WriteFile(
path, []byte(content), 0644,
))
data, err := reconstructJSONL(path)
if tt.wantErr {
require.Error(t, err, "expected error")
return
}
require.NoError(t, err)
tt.check(t, data)
})
}
}
func TestDiscoverVSCodeCopilot_JSONLDedup(t *testing.T) {
root := t.TempDir()
hash := "abc123def456"
chatDir := filepath.Join(
root, "workspaceStorage", hash, "chatSessions",
)
require.NoError(t, os.MkdirAll(chatDir, 0755))
wsJSON := `{"folder":"file:///Users/dev/projects/myproject"}`
wsPath := filepath.Join(
root, "workspaceStorage", hash, "workspace.json",
)
require.NoError(t, os.WriteFile(wsPath, []byte(wsJSON), 0644))
// Session with both .json and .jsonl - jsonl should win
sessionJSON := `{"version":3,"sessionId":"dup1","requests":[{"requestId":"r1","message":{"text":"hi"},"response":[{"value":"hello"}],"timestamp":1755340000000}]}`
require.NoError(t, os.WriteFile(
filepath.Join(chatDir, "dup1.json"),
[]byte(sessionJSON), 0644,
))
jsonlContent := `{"kind":0,"v":{"version":3,"sessionId":"dup1","creationDate":1755340000000,"requests":[{"requestId":"r1","timestamp":1755340000000,"message":{"text":"hi"},"response":[{"value":"hello"}]}]}}` + "\n"
require.NoError(t, os.WriteFile(
filepath.Join(chatDir, "dup1.jsonl"),
[]byte(jsonlContent), 0644,
))
// Session with only .jsonl
require.NoError(t, os.WriteFile(
filepath.Join(chatDir, "only-jsonl.jsonl"),
[]byte(jsonlContent), 0644,
))
// Session with only .json
require.NoError(t, os.WriteFile(
filepath.Join(chatDir, "only-json.json"),
[]byte(sessionJSON), 0644,
))
files := discoverVSCodeCopilotTestSessions(t, root)
// Should get 3 files: dup1.jsonl, only-jsonl.jsonl, only-json.json
if !assert.Len(t, files, 3, "expected 3 files") {
for _, f := range files {
t.Logf(" %s", f.Path)
}
t.FailNow()
}
// Verify dup1.json was excluded (dup1.jsonl present)
for _, f := range files {
assert.NotEqual(t, "dup1.json", filepath.Base(f.Path),
"dup1.json should be excluded when dup1.jsonl exists")
}
}
func TestFindVSCodeCopilotSourceFile(t *testing.T) {
dir := t.TempDir()
uuid := "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
// Set up a workspace session file
chatDir := filepath.Join(
dir, "workspaceStorage", "hash1", "chatSessions",
)
sessionPath := filepath.Join(chatDir, uuid+".json")
require.NoError(t, os.MkdirAll(chatDir, 0o755))
require.NoError(t, os.WriteFile(sessionPath, []byte("{}"), 0o644))
tests := []struct {
name string
dir string
id string
want string
}{
{"valid UUID", dir, uuid, sessionPath},
{"empty dir", "", uuid, ""},
{"empty ID", dir, "", ""},
{"traversal slash", dir, "../etc/passwd", ""},
{"traversal dotdot", dir, "..", ""},
{"path separator", dir, "foo/bar", ""},
{"nonexistent UUID", dir, "00000000-0000-0000-0000-000000000000", ""},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := findVSCodeCopilotTestSourceFile(t,
tt.dir, tt.id,
)
assert.Equal(t, tt.want, got)
})
}
}
func TestParseVSCodeCopilotSession_TokenUsage(t *testing.T) {
// Two turns carry result.metadata token accounting (the
// post-credits VSCode Copilot format); a third turn has no
// metadata and must not produce a usage event.
sessionJSON := `{
"version": 3,
"sessionId": "usage-1",
"creationDate": 1755340000000,
"lastMessageDate": 1755350000000,
"requests": [
{
"requestId": "r1",
"message": {"text": "First"},
"response": [{"value": "answer one"}],
"timestamp": 1755340000000,
"modelId": "copilot/claude-opus-4.8",
"result": {"metadata": {
"promptTokens": 35875,
"outputTokens": 221,
"resolvedModel": "claude-opus-4-8"
}}
},
{
"requestId": "r2",
"message": {"text": "Second"},
"response": [{"value": "answer two"}],
"timestamp": 1755345000000,
"modelId": "copilot/claude-opus-4.8",
"result": {"metadata": {
"promptTokens": 41055,
"outputTokens": 69,
"resolvedModel": "claude-opus-4-8"
}}
},
{
"requestId": "r3",
"message": {"text": "Third"},
"response": [{"value": "answer three"}],
"timestamp": 1755350000000,
"modelId": "copilot/claude-opus-4.8"
}
]
}`
dir := t.TempDir()
path := filepath.Join(dir, "usage.json")
require.NoError(t, os.WriteFile(path, []byte(sessionJSON), 0644))
sess, _, err := parseVSCodeCopilotTestSession(t, path, "proj", "local")
require.NoError(t, err)
require.NotNil(t, sess)
// Only the two turns with metadata yield usage events.
require.Len(t, sess.UsageEvents, 2, "usage events")
for i, ev := range sess.UsageEvents {
assert.Equal(t, "vscode-copilot", ev.Source, "event[%d] source", i)
assert.Equal(t, "claude-opus-4-8", ev.Model, "event[%d] model", i)
assert.NotEmpty(t, ev.OccurredAt, "event[%d] occurredAt", i)
}
assert.Equal(t, 35875, sess.UsageEvents[0].InputTokens)
assert.Equal(t, 221, sess.UsageEvents[0].OutputTokens)
assert.Equal(t, 41055, sess.UsageEvents[1].InputTokens)
assert.Equal(t, 69, sess.UsageEvents[1].OutputTokens)
// Session output total sums the per-turn output tokens.
assert.True(t, sess.HasTotalOutputTokens, "has total output")
assert.Equal(t, 290, sess.TotalOutputTokens, "total output")
// Peak context is the largest per-turn promptTokens.
assert.True(t, sess.HasPeakContextTokens, "has peak context")
assert.Equal(t, 41055, sess.PeakContextTokens, "peak context")
}
func TestParseVSCodeCopilotSession_TokenUsageModelFallback(t *testing.T) {
// No resolvedModel: fall back to the prefixed modelId and
// normalize the claude version dots to hyphens.
sessionJSON := `{
"version": 3,
"sessionId": "usage-2",
"creationDate": 1755340000000,
"requests": [{
"requestId": "r1",
"message": {"text": "Hi"},
"response": [{"value": "Hello"}],
"timestamp": 1755340000000,
"modelId": "copilot/claude-sonnet-4.6",
"result": {"metadata": {"promptTokens": 1000, "outputTokens": 50}}
}]
}`
dir := t.TempDir()
path := filepath.Join(dir, "usage2.json")
require.NoError(t, os.WriteFile(path, []byte(sessionJSON), 0644))
sess, _, err := parseVSCodeCopilotTestSession(t, path, "proj", "local")
require.NoError(t, err)
require.NotNil(t, sess)
require.Len(t, sess.UsageEvents, 1)
assert.Equal(t, "claude-sonnet-4-6", sess.UsageEvents[0].Model)
}
func TestParseVSCodeCopilotSession_NoTokenUsage(t *testing.T) {
// A session whose requests carry no token metadata yields no
// usage events and leaves output totals unset (cost -> n/a).
sessionJSON := `{
"version": 3,
"sessionId": "no-usage",
"creationDate": 1755340000000,
"requests": [{
"requestId": "r1",
"message": {"text": "Hi"},
"response": [{"value": "Hello"}],
"timestamp": 1755340000000
}]
}`
dir := t.TempDir()
path := filepath.Join(dir, "nousage.json")
require.NoError(t, os.WriteFile(path, []byte(sessionJSON), 0644))
sess, _, err := parseVSCodeCopilotTestSession(t, path, "proj", "local")
require.NoError(t, err)
require.NotNil(t, sess)
assert.Empty(t, sess.UsageEvents, "no usage events expected")
assert.False(t, sess.HasTotalOutputTokens, "no total output expected")
assert.False(t, sess.HasPeakContextTokens, "no peak context expected")
}