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
329 lines
16 KiB
Go
329 lines
16 KiB
Go
package parser
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestQoderRegistry(t *testing.T) {
|
|
def, ok := AgentByType(AgentQoder)
|
|
require.True(t, ok, "AgentQoder missing from Registry")
|
|
assert.Equal(t, "Qoder", def.DisplayName)
|
|
assert.Equal(t, "QODER_PROJECTS_DIR", def.EnvVar)
|
|
assert.Equal(t, "qoder_project_dirs", def.ConfigKey)
|
|
assert.Equal(t, qoderIDPrefix, def.IDPrefix)
|
|
assert.True(t, def.FileBased)
|
|
assert.False(t, def.ShallowWatch)
|
|
assert.Equal(t, []string{".qoder/projects", ".qoderwork/projects"}, def.DefaultDirs)
|
|
factory, ok := ProviderFactoryByType(AgentQoder)
|
|
require.True(t, ok, "AgentQoder provider missing")
|
|
assert.Equal(t, AgentQoder, factory.Definition().Type)
|
|
caps := factory.Capabilities()
|
|
assert.Equal(t, CapabilitySupported, caps.Source.DiscoverSources)
|
|
assert.Equal(t, CapabilitySupported, caps.Source.ClassifyChangedPath)
|
|
assert.Equal(t, CapabilitySupported, caps.Source.FindSource)
|
|
assert.Equal(t, CapabilitySupported, caps.Source.MultiSessionSource)
|
|
assert.Equal(t, CapabilitySupported, caps.Source.ExcludedSessions)
|
|
assert.Equal(t, CapabilitySupported, caps.Source.ForceReplaceOnParse)
|
|
assert.Equal(t, CapabilitySupported, caps.Content.Subagents)
|
|
provider, ok := NewProvider(AgentQoder, ProviderConfig{Roots: []string{t.TempDir()}})
|
|
require.True(t, ok)
|
|
require.NotNil(t, provider)
|
|
}
|
|
|
|
func TestParseQoderSession(t *testing.T) {
|
|
root := t.TempDir()
|
|
cwd := filepath.Join(root, "code", "sample-project")
|
|
require.NoError(t, os.MkdirAll(cwd, 0o755))
|
|
path := filepath.Join(root, "-Users-alice-sample-project", "11111111-1111-4111-8111-111111111111.jsonl")
|
|
require.NoError(t, os.MkdirAll(filepath.Dir(path), 0o755))
|
|
content := fmt.Sprintf(`{"type":"user","uuid":"u1","timestamp":"2026-06-04T09:47:27.966Z","message":{"role":"user","content":"help me"},"cwd":%q,"sessionId":"11111111-1111-4111-8111-111111111111","version":"1.0.8"}
|
|
{"type":"assistant","uuid":"a1","parentUuid":"u1","timestamp":"2026-06-04T09:47:32.116Z","message":{"id":"msg_1","type":"message","role":"assistant","model":"auto","stop_reason":"end_turn","usage":{"input_tokens":100,"cache_creation_input_tokens":7,"cache_read_input_tokens":50,"output_tokens":30},"content":[{"type":"text","text":"done"}]},"sessionId":"11111111-1111-4111-8111-111111111111"}
|
|
{"type":"user","uuid":"u2","parentUuid":"a1","timestamp":"2026-06-04T09:47:39.020Z","message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"toolu_1","content":"ok"}]},"sessionId":"11111111-1111-4111-8111-111111111111"}
|
|
`, cwd)
|
|
require.NoError(t, os.WriteFile(path, []byte(content), 0o644))
|
|
|
|
results, err := ParseQoderSession(path, "sample-project", "local")
|
|
require.NoError(t, err)
|
|
require.Len(t, results, 1)
|
|
sess := results[0].Session
|
|
assert.Equal(t, "qoder:11111111-1111-4111-8111-111111111111", sess.ID)
|
|
assert.Equal(t, AgentQoder, sess.Agent)
|
|
assert.Equal(t, "sample-project", sess.Project)
|
|
assert.Equal(t, cwd, sess.Cwd)
|
|
assert.Equal(t, "help me", sess.FirstMessage)
|
|
assert.True(t, sess.HasTotalOutputTokens)
|
|
assert.Equal(t, 30, sess.TotalOutputTokens)
|
|
assert.True(t, sess.HasPeakContextTokens)
|
|
assert.Equal(t, 157, sess.PeakContextTokens)
|
|
require.Len(t, results[0].Messages, 3)
|
|
assert.Equal(t, 157, results[0].Messages[1].ContextTokens)
|
|
assert.Equal(t, 30, results[0].Messages[1].OutputTokens)
|
|
}
|
|
|
|
func TestParseQoderSessionSidecarMetadata(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "proj", "22222222-2222-4222-8222-222222222222.jsonl")
|
|
require.NoError(t, os.MkdirAll(filepath.Dir(path), 0o755))
|
|
require.NoError(t, os.WriteFile(path, []byte(`{"type":"user","uuid":"u1","timestamp":"2026-06-04T09:47:27.966Z","message":{"role":"user","content":"fork"}}
|
|
`), 0o644))
|
|
require.NoError(t, os.WriteFile(
|
|
strings.TrimSuffix(path, ".jsonl")+"-session.json",
|
|
[]byte(`{"title":"Forked work","working_dir":"/tmp/qoder","fork_from":"11111111-1111-4111-8111-111111111111"}`),
|
|
0o644,
|
|
))
|
|
|
|
results, err := ParseQoderSession(path, "proj", "local")
|
|
require.NoError(t, err)
|
|
require.Len(t, results, 1)
|
|
sess := results[0].Session
|
|
assert.Equal(t, "Forked work", sess.SessionName)
|
|
assert.Equal(t, "/tmp/qoder", sess.Cwd)
|
|
assert.Equal(t, "qoder:11111111-1111-4111-8111-111111111111", sess.ParentSessionID)
|
|
assert.Equal(t, RelFork, sess.RelationshipType)
|
|
}
|
|
|
|
func TestQoderSidecarForkFromPreservesParserForkParent(t *testing.T) {
|
|
t.Run("promotes continuation parent", func(t *testing.T) {
|
|
sess := ParsedSession{
|
|
ParentSessionID: "qoder:22222222-2222-4222-8222-222222222222",
|
|
RelationshipType: RelContinuation,
|
|
}
|
|
|
|
applyQoderMeta(&sess, qoderSessionMeta{
|
|
ForkFrom: "11111111-1111-4111-8111-111111111111",
|
|
})
|
|
|
|
assert.Equal(t, "qoder:11111111-1111-4111-8111-111111111111", sess.ParentSessionID)
|
|
assert.Equal(t, RelFork, sess.RelationshipType)
|
|
})
|
|
|
|
t.Run("keeps parser fork parent", func(t *testing.T) {
|
|
sess := ParsedSession{
|
|
ParentSessionID: "qoder:22222222-2222-4222-8222-222222222222",
|
|
RelationshipType: RelFork,
|
|
}
|
|
|
|
applyQoderMeta(&sess, qoderSessionMeta{
|
|
ForkFrom: "11111111-1111-4111-8111-111111111111",
|
|
})
|
|
|
|
assert.Equal(t, "qoder:22222222-2222-4222-8222-222222222222", sess.ParentSessionID)
|
|
assert.Equal(t, RelFork, sess.RelationshipType)
|
|
})
|
|
}
|
|
|
|
func TestQoderProviderParseStampsCompositeFingerprint(t *testing.T) {
|
|
root := t.TempDir()
|
|
path := filepath.Join(root, "-Users-alice-project", "11111111-1111-4111-8111-111111111111.jsonl")
|
|
require.NoError(t, os.MkdirAll(filepath.Dir(path), 0o755))
|
|
require.NoError(t, os.WriteFile(path, []byte(`{"type":"user","uuid":"u1","timestamp":"2026-06-04T09:47:27.966Z","message":{"role":"user","content":"hello"},"sessionId":"11111111-1111-4111-8111-111111111111"}
|
|
`), 0o644))
|
|
require.NoError(t, os.WriteFile(
|
|
strings.TrimSuffix(path, ".jsonl")+"-session.json",
|
|
[]byte(`{"title":"Composite fingerprint"}`),
|
|
0o644,
|
|
))
|
|
|
|
provider, ok := NewProvider(AgentQoder, ProviderConfig{
|
|
Roots: []string{root},
|
|
Machine: "local",
|
|
})
|
|
require.True(t, ok)
|
|
sources, err := provider.Discover(context.Background())
|
|
require.NoError(t, err)
|
|
require.Len(t, sources, 1)
|
|
fingerprint, err := provider.Fingerprint(context.Background(), sources[0])
|
|
require.NoError(t, err)
|
|
|
|
outcome, err := provider.Parse(context.Background(), ParseRequest{
|
|
Source: sources[0],
|
|
Fingerprint: fingerprint,
|
|
})
|
|
require.NoError(t, err)
|
|
require.Len(t, outcome.Results, 1)
|
|
file := outcome.Results[0].Result.Session.File
|
|
assert.Equal(t, fingerprint.Size, file.Size)
|
|
assert.Equal(t, fingerprint.MTimeNS, file.Mtime)
|
|
assert.Equal(t, fingerprint.Hash, file.Hash)
|
|
}
|
|
|
|
func TestParseQoderSubagentSession(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "proj", "11111111-1111-4111-8111-111111111111", "subagents", "agent-123.jsonl")
|
|
require.NoError(t, os.MkdirAll(filepath.Dir(path), 0o755))
|
|
content := `{"agentId":"123","type":"user","uuid":"u1","timestamp":"2026-06-04T09:47:27.966Z","message":{"role":"user","content":[{"type":"text","text":"sub task"}]},"sessionId":"child-session"}
|
|
`
|
|
require.NoError(t, os.WriteFile(path, []byte(content), 0o644))
|
|
|
|
results, err := ParseQoderSession(path, "proj", "local")
|
|
require.NoError(t, err)
|
|
require.Len(t, results, 1)
|
|
sess := results[0].Session
|
|
assert.Equal(t, "qoder:11111111-1111-4111-8111-111111111111:subagent:agent-123", sess.ID)
|
|
assert.Equal(t, "qoder:11111111-1111-4111-8111-111111111111", sess.ParentSessionID)
|
|
assert.Equal(t, RelSubagent, sess.RelationshipType)
|
|
assert.Equal(t, AgentQoder, sess.Agent)
|
|
}
|
|
|
|
func TestParseQoderSubagentExclusionIDs(t *testing.T) {
|
|
root := t.TempDir()
|
|
path := filepath.Join(root, "proj", "11111111-1111-4111-8111-111111111111", "subagents", "agent-123.jsonl")
|
|
require.NoError(t, os.MkdirAll(filepath.Dir(path), 0o755))
|
|
content := `{"type":"user","uuid":"u1","timestamp":"2026-06-04T09:47:27.966Z","message":{"role":"user","content":"/usage"},"sessionId":"agent-123"}
|
|
`
|
|
require.NoError(t, os.WriteFile(path, []byte(content), 0o644))
|
|
|
|
results, excluded, err := ParseQoderSessionWithExclusions(path, "proj", "local")
|
|
require.NoError(t, err)
|
|
assert.Empty(t, results)
|
|
assert.Equal(t, []string{
|
|
"qoder:11111111-1111-4111-8111-111111111111:subagent:agent-123",
|
|
}, excluded)
|
|
|
|
provider, ok := NewProvider(AgentQoder, ProviderConfig{Roots: []string{root}})
|
|
require.True(t, ok)
|
|
source, found, err := provider.FindSource(context.Background(), FindSourceRequest{
|
|
RawSessionID: "11111111-1111-4111-8111-111111111111:subagent:agent-123",
|
|
})
|
|
require.NoError(t, err)
|
|
require.True(t, found)
|
|
outcome, err := provider.Parse(context.Background(), ParseRequest{
|
|
Source: source,
|
|
Machine: "local",
|
|
})
|
|
require.NoError(t, err)
|
|
assert.Empty(t, outcome.Results)
|
|
assert.Equal(t, []string{
|
|
"qoder:11111111-1111-4111-8111-111111111111:subagent:agent-123",
|
|
}, outcome.ExcludedSessionIDs)
|
|
}
|
|
|
|
func TestParseQoderSessionRetagsToolCallSubagentIDs(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "proj", "11111111-1111-4111-8111-111111111111.jsonl")
|
|
require.NoError(t, os.MkdirAll(filepath.Dir(path), 0o755))
|
|
content := `{"type":"user","uuid":"u1","timestamp":"2026-06-04T09:47:27.966Z","message":{"role":"user","content":"delegate"},"sessionId":"11111111-1111-4111-8111-111111111111"}
|
|
{"type":"assistant","uuid":"a1","parentUuid":"u1","timestamp":"2026-06-04T09:47:32.116Z","message":{"role":"assistant","content":[{"type":"tool_use","id":"toolu_1","name":"Agent","input":{"description":"do it","prompt":"x"}}]},"sessionId":"11111111-1111-4111-8111-111111111111"}
|
|
{"type":"user","uuid":"u2","parentUuid":"a1","timestamp":"2026-06-04T09:47:39.020Z","message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"toolu_1","content":"done"}]},"toolUseResult":{"status":"completed","agentId":"123"}}
|
|
`
|
|
require.NoError(t, os.WriteFile(path, []byte(content), 0o644))
|
|
|
|
results, err := ParseQoderSession(path, "proj", "local")
|
|
require.NoError(t, err)
|
|
require.Len(t, results, 1)
|
|
var calls []ParsedToolCall
|
|
for _, msg := range results[0].Messages {
|
|
calls = append(calls, msg.ToolCalls...)
|
|
}
|
|
require.Len(t, calls, 1)
|
|
assert.Equal(t, "qoder:11111111-1111-4111-8111-111111111111:subagent:agent-123", calls[0].SubagentSessionID)
|
|
}
|
|
|
|
func TestParseQoderForkedSessionRetagsToolCallSubagentIDsToFileStem(t *testing.T) {
|
|
fileStem := "11111111-1111-4111-8111-111111111111"
|
|
path := filepath.Join(t.TempDir(), "proj", fileStem+".jsonl")
|
|
require.NoError(t, os.MkdirAll(filepath.Dir(path), 0o755))
|
|
content := `{"type":"user","uuid":"root","timestamp":"2026-06-04T09:47:20.000Z","message":{"role":"user","content":"start"},"sessionId":"11111111-1111-4111-8111-111111111111"}
|
|
{"type":"assistant","uuid":"split","parentUuid":"root","timestamp":"2026-06-04T09:47:21.000Z","message":{"role":"assistant","content":[{"type":"text","text":"ok"}]},"sessionId":"11111111-1111-4111-8111-111111111111"}
|
|
{"type":"user","uuid":"main-u1","parentUuid":"split","timestamp":"2026-06-04T09:47:22.000Z","message":{"role":"user","content":"main 1"},"sessionId":"11111111-1111-4111-8111-111111111111"}
|
|
{"type":"assistant","uuid":"main-a1","parentUuid":"main-u1","timestamp":"2026-06-04T09:47:23.000Z","message":{"role":"assistant","content":[{"type":"text","text":"main answer 1"}]},"sessionId":"11111111-1111-4111-8111-111111111111"}
|
|
{"type":"user","uuid":"main-u2","parentUuid":"main-a1","timestamp":"2026-06-04T09:47:24.000Z","message":{"role":"user","content":"main 2"},"sessionId":"11111111-1111-4111-8111-111111111111"}
|
|
{"type":"assistant","uuid":"main-a2","parentUuid":"main-u2","timestamp":"2026-06-04T09:47:25.000Z","message":{"role":"assistant","content":[{"type":"text","text":"main answer 2"}]},"sessionId":"11111111-1111-4111-8111-111111111111"}
|
|
{"type":"user","uuid":"main-u3","parentUuid":"main-a2","timestamp":"2026-06-04T09:47:26.000Z","message":{"role":"user","content":"main 3"},"sessionId":"11111111-1111-4111-8111-111111111111"}
|
|
{"type":"assistant","uuid":"main-a3","parentUuid":"main-u3","timestamp":"2026-06-04T09:47:27.000Z","message":{"role":"assistant","content":[{"type":"text","text":"main answer 3"}]},"sessionId":"11111111-1111-4111-8111-111111111111"}
|
|
{"type":"user","uuid":"main-u4","parentUuid":"main-a3","timestamp":"2026-06-04T09:47:28.000Z","message":{"role":"user","content":"main 4"},"sessionId":"11111111-1111-4111-8111-111111111111"}
|
|
{"type":"assistant","uuid":"main-a4","parentUuid":"main-u4","timestamp":"2026-06-04T09:47:29.000Z","message":{"role":"assistant","content":[{"type":"text","text":"main answer 4"}]},"sessionId":"11111111-1111-4111-8111-111111111111"}
|
|
{"type":"user","uuid":"fork-u1","parentUuid":"split","timestamp":"2026-06-04T09:48:00.000Z","message":{"role":"user","content":"fork delegate"},"sessionId":"11111111-1111-4111-8111-111111111111"}
|
|
{"type":"assistant","uuid":"fork-a1","parentUuid":"fork-u1","timestamp":"2026-06-04T09:48:01.000Z","message":{"role":"assistant","content":[{"type":"tool_use","id":"toolu_fork","name":"Agent","input":{"description":"do fork work","prompt":"x"}}]},"sessionId":"11111111-1111-4111-8111-111111111111"}
|
|
{"type":"user","uuid":"fork-r1","parentUuid":"fork-a1","timestamp":"2026-06-04T09:48:02.000Z","message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"toolu_fork","content":"done"}]},"toolUseResult":{"status":"completed","agentId":"123"},"sessionId":"11111111-1111-4111-8111-111111111111"}
|
|
`
|
|
require.NoError(t, os.WriteFile(path, []byte(content), 0o644))
|
|
require.NoError(t, os.WriteFile(
|
|
strings.TrimSuffix(path, ".jsonl")+"-session.json",
|
|
[]byte(`{"fork_from":"22222222-2222-4222-8222-222222222222"}`),
|
|
0o644,
|
|
))
|
|
|
|
results, err := ParseQoderSession(path, "proj", "local")
|
|
require.NoError(t, err)
|
|
require.Len(t, results, 2)
|
|
assert.Equal(t, "qoder:22222222-2222-4222-8222-222222222222", results[0].Session.ParentSessionID)
|
|
var fork *ParseResult
|
|
for i := range results {
|
|
if results[i].Session.RelationshipType == RelFork {
|
|
fork = &results[i]
|
|
}
|
|
}
|
|
require.NotNil(t, fork)
|
|
assert.NotEqual(t, qoderPrefixID(fileStem), fork.Session.ID)
|
|
assert.Equal(t, qoderPrefixID(fileStem), fork.Session.ParentSessionID)
|
|
var calls []ParsedToolCall
|
|
for _, msg := range fork.Messages {
|
|
calls = append(calls, msg.ToolCalls...)
|
|
}
|
|
require.Len(t, calls, 1)
|
|
assert.Equal(t, "qoder:"+fileStem+":subagent:agent-123", calls[0].SubagentSessionID)
|
|
}
|
|
|
|
func TestDiscoverQoderSessions(t *testing.T) {
|
|
root := t.TempDir()
|
|
mainPath := filepath.Join(root, "-Users-alice-project", "11111111-1111-4111-8111-111111111111.jsonl")
|
|
sidecarPath := filepath.Join(root, "-Users-alice-project", "11111111-1111-4111-8111-111111111111-session.json")
|
|
statsPath := filepath.Join(root, "ai-stats", "usage.json")
|
|
rootAgentPath := filepath.Join(root, "-Users-alice-project", "agent-123.jsonl")
|
|
subPath := filepath.Join(root, "-Users-alice-project", "11111111-1111-4111-8111-111111111111", "subagents", "agent-123.jsonl")
|
|
for _, path := range []string{mainPath, sidecarPath, statsPath, rootAgentPath, subPath} {
|
|
require.NoError(t, os.MkdirAll(filepath.Dir(path), 0o755))
|
|
require.NoError(t, os.WriteFile(path, []byte("{}\n"), 0o644))
|
|
}
|
|
|
|
files := DiscoverQoderSessions(root)
|
|
require.Len(t, files, 2)
|
|
assert.Equal(t, mainPath, files[0].Path)
|
|
assert.Equal(t, "project", files[0].Project)
|
|
assert.Equal(t, AgentQoder, files[0].Agent)
|
|
assert.Equal(t, subPath, files[1].Path)
|
|
assert.Equal(t, "project", files[1].Project)
|
|
assert.Equal(t, AgentQoder, files[1].Agent)
|
|
}
|
|
|
|
func TestFindQoderSourceFile(t *testing.T) {
|
|
root := t.TempDir()
|
|
mainPath := filepath.Join(root, "proj", "11111111-1111-4111-8111-111111111111.jsonl")
|
|
subPath := filepath.Join(root, "proj", "11111111-1111-4111-8111-111111111111", "subagents", "agent-123.jsonl")
|
|
for _, path := range []string{mainPath, subPath} {
|
|
require.NoError(t, os.MkdirAll(filepath.Dir(path), 0o755))
|
|
require.NoError(t, os.WriteFile(path, []byte("{}\n"), 0o644))
|
|
}
|
|
|
|
assert.Equal(t, mainPath, FindQoderSourceFile(root, "qoder:11111111-1111-4111-8111-111111111111"))
|
|
assert.Equal(t, subPath, FindQoderSourceFile(root, "qoder:11111111-1111-4111-8111-111111111111:subagent:agent-123"))
|
|
assert.Empty(t, FindQoderSourceFile(root, "qoder:11111111-1111-4111-8111-111111111111:subagent:../agent-123"))
|
|
}
|
|
|
|
func TestDecodeQoderProjectDir(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
want string
|
|
}{
|
|
{"-Users-foo-myproject", "myproject"},
|
|
{"-home-user-work-coding", "coding"},
|
|
{"-Users-alice-code-sample-project", "sample_project"},
|
|
{"-Users-alice-code-dev-tools", "dev_tools"},
|
|
{"-home-alice-projects-work-app", "work_app"},
|
|
{"-home-alice-projects-agent-ui", "agent_ui"},
|
|
{"plain-name", "plain_name"},
|
|
{"", ""},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
assert.Equal(t, tt.want, DecodeQoderProjectDir(tt.name))
|
|
})
|
|
}
|
|
}
|