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
384 lines
12 KiB
Go
384 lines
12 KiB
Go
package parser
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestOpenClaudeProviderCapabilities(t *testing.T) {
|
|
caps := openClaudeProviderCapabilities()
|
|
require.True(t, caps.Source.DiscoverSources == CapabilitySupported)
|
|
require.True(t, caps.Source.WatchSources == CapabilitySupported)
|
|
require.True(t, caps.Source.ClassifyChangedPath == CapabilitySupported)
|
|
require.True(t, caps.Source.FindSource == CapabilitySupported)
|
|
require.True(t, caps.Source.ForceReplaceOnParse == CapabilitySupported)
|
|
|
|
def, ok := AgentByType(AgentOpenClaude)
|
|
require.True(t, ok, "AgentOpenClaude missing from Registry")
|
|
assert.True(t, def.FileBased)
|
|
assert.Equal(t, "OPENCLAUDE_PROJECTS_DIR", def.EnvVar)
|
|
assert.Equal(t, "OPENCLAUDE_CONFIG_DIR", def.DefaultRootEnvVar)
|
|
assert.Equal(t, "openclaude_project_dirs", def.ConfigKey)
|
|
assert.Equal(t, []string{".openclaude/projects"}, def.DefaultDirs)
|
|
assert.Equal(t, "openclaude:", def.IDPrefix)
|
|
assert.Empty(t, def.WatchSubdirs)
|
|
assert.Nil(t, def.WatchRootsFunc)
|
|
}
|
|
|
|
func TestOpenClaudeDiscoverParseAndFindSource(t *testing.T) {
|
|
root := t.TempDir()
|
|
projectDir := filepath.Join(root, "my-project")
|
|
require.NoError(t, os.MkdirAll(projectDir, 0o755))
|
|
|
|
path := filepath.Join(projectDir, "session-123.jsonl")
|
|
content := strings.Join([]string{
|
|
buildMetadataLine(map[string]any{
|
|
"type": "user",
|
|
"timestamp": tsEarly,
|
|
"uuid": "u1",
|
|
"parentUuid": "",
|
|
"cwd": "/workspace/my-project",
|
|
"gitBranch": "main",
|
|
"message": map[string]any{
|
|
"role": "user",
|
|
"content": "hello from openclaude",
|
|
},
|
|
}),
|
|
buildMetadataLine(map[string]any{
|
|
"type": "assistant",
|
|
"timestamp": tsEarlyS1,
|
|
"uuid": "u2",
|
|
"parentUuid": "u1",
|
|
"message": map[string]any{
|
|
"role": "assistant",
|
|
"stop_reason": "end_turn",
|
|
"usage": map[string]any{
|
|
"input_tokens": 12,
|
|
"cache_creation_input_tokens": 3,
|
|
"cache_read_input_tokens": 2,
|
|
"output_tokens": 7,
|
|
},
|
|
"content": []map[string]any{
|
|
{"type": "text", "text": "reply"},
|
|
},
|
|
},
|
|
}),
|
|
buildMetadataLine(map[string]any{
|
|
"type": "ai-title",
|
|
"timestamp": tsEarlyS1,
|
|
"aiTitle": "AI title",
|
|
}),
|
|
buildMetadataLine(map[string]any{
|
|
"type": "custom-title",
|
|
"timestamp": tsEarlyS5,
|
|
"customTitle": "User title",
|
|
}),
|
|
buildMetadataLine(map[string]any{
|
|
"type": "system",
|
|
"subtype": "compact_boundary",
|
|
"timestamp": tsEarlyS5,
|
|
"uuid": "u3",
|
|
"parentUuid": "u2",
|
|
"message": map[string]any{
|
|
"content": []map[string]any{
|
|
{"type": "text", "text": "Compact summary"},
|
|
},
|
|
},
|
|
}),
|
|
}, "\n") + "\n"
|
|
require.NoError(t, os.WriteFile(path, []byte(content), 0o644))
|
|
|
|
provider, ok := NewProvider(AgentOpenClaude, ProviderConfig{
|
|
Roots: []string{root},
|
|
Machine: "devbox",
|
|
})
|
|
require.True(t, ok)
|
|
|
|
discovered, err := provider.Discover(context.Background())
|
|
require.NoError(t, err)
|
|
require.Len(t, discovered, 1)
|
|
assert.Equal(t, path, discovered[0].Key)
|
|
assert.Equal(t, "my-project", discovered[0].ProjectHint)
|
|
|
|
found, ok, err := provider.FindSource(context.Background(), FindSourceRequest{
|
|
FullSessionID: "openclaude:session-123",
|
|
})
|
|
require.NoError(t, err)
|
|
require.True(t, ok)
|
|
assert.Equal(t, path, found.Key)
|
|
|
|
outcome, err := provider.Parse(context.Background(), ParseRequest{
|
|
Source: discovered[0],
|
|
Fingerprint: SourceFingerprint{Hash: "hash-123"},
|
|
Machine: "devbox",
|
|
})
|
|
require.NoError(t, err)
|
|
require.True(t, outcome.ResultSetComplete)
|
|
require.True(t, outcome.ForceReplace)
|
|
require.Len(t, outcome.Results, 1)
|
|
|
|
result := outcome.Results[0].Result
|
|
assert.Equal(t, AgentOpenClaude, result.Session.Agent)
|
|
assert.Equal(t, "openclaude:session-123", result.Session.ID)
|
|
assert.Equal(t, "my_project", result.Session.Project)
|
|
assert.Equal(t, "hello from openclaude", result.Session.FirstMessage)
|
|
assert.Equal(t, "User title", result.Session.SessionName)
|
|
assert.Equal(t, 1, result.Session.UserMessageCount)
|
|
assert.Equal(t, TerminationAwaitingUser, result.Session.TerminationStatus)
|
|
assert.Equal(t, 7, result.Session.TotalOutputTokens)
|
|
assert.Equal(t, 17, result.Session.PeakContextTokens)
|
|
assert.True(t, result.Session.HasTotalOutputTokens)
|
|
assert.True(t, result.Session.HasPeakContextTokens)
|
|
assert.Equal(t, "hash-123", result.Session.File.Hash)
|
|
require.Len(t, result.Messages, 3)
|
|
|
|
assert.Equal(t, RoleUser, result.Messages[0].Role)
|
|
assert.Equal(t, RoleAssistant, result.Messages[1].Role)
|
|
assert.Equal(t, "end_turn", result.Messages[1].StopReason)
|
|
assert.Equal(t, 7, result.Messages[1].OutputTokens)
|
|
assert.Equal(t, 17, result.Messages[1].ContextTokens)
|
|
assert.True(t, result.Messages[1].HasOutputTokens)
|
|
assert.True(t, result.Messages[1].HasContextTokens)
|
|
assert.Equal(t, RoleAssistant, result.Messages[2].Role)
|
|
assert.True(t, result.Messages[2].IsSystem)
|
|
assert.True(t, result.Messages[2].IsCompactBoundary)
|
|
assert.Equal(t, "compact_boundary", result.Messages[2].SourceSubtype)
|
|
assert.Contains(t, result.Messages[2].Content, "Compact summary")
|
|
}
|
|
|
|
func TestOpenClaudeQueuedCommandAttachment(t *testing.T) {
|
|
root := t.TempDir()
|
|
projectDir := filepath.Join(root, "queue-project")
|
|
require.NoError(t, os.MkdirAll(projectDir, 0o755))
|
|
|
|
path := filepath.Join(projectDir, "session-queue.jsonl")
|
|
content := strings.Join([]string{
|
|
buildMetadataLine(map[string]any{
|
|
"type": "user",
|
|
"timestamp": tsEarly,
|
|
"uuid": "u1",
|
|
"message": map[string]any{
|
|
"role": "user",
|
|
"content": "first prompt",
|
|
},
|
|
}),
|
|
buildMetadataLine(map[string]any{
|
|
"type": "attachment",
|
|
"timestamp": tsEarlyS1,
|
|
"attachment": map[string]any{
|
|
"type": "queued_command",
|
|
"commandMode": "prompt",
|
|
"prompt": []map[string]any{
|
|
{"type": "text", "text": "/resume next step"},
|
|
{"type": "text", "text": "with context"},
|
|
},
|
|
},
|
|
}),
|
|
buildMetadataLine(map[string]any{
|
|
"type": "attachment",
|
|
"timestamp": "2024-01-01T10:00:01.500Z",
|
|
"attachment": map[string]any{
|
|
"type": "queued_command",
|
|
"commandMode": "task-notification",
|
|
"prompt": "ignored non-prompt queued command",
|
|
},
|
|
}),
|
|
buildMetadataLine(map[string]any{
|
|
"type": "attachment",
|
|
"timestamp": "2024-01-01T10:00:01.750Z",
|
|
"attachment": map[string]any{
|
|
"type": "queued_command",
|
|
"commandMode": "prompt",
|
|
"isMeta": true,
|
|
"prompt": "ignored meta queued command",
|
|
},
|
|
}),
|
|
buildMetadataLine(map[string]any{
|
|
"type": "attachment",
|
|
"timestamp": "2024-01-01T10:00:02Z",
|
|
"attachment": map[string]any{
|
|
"type": "queued_command",
|
|
"commandMode": "prompt",
|
|
"origin": "task-notification",
|
|
"prompt": "ignored queued prompt",
|
|
},
|
|
}),
|
|
buildMetadataLine(map[string]any{
|
|
"type": "assistant",
|
|
"timestamp": tsEarlyS5,
|
|
"uuid": "u2",
|
|
"parentUuid": "u1",
|
|
"message": map[string]any{
|
|
"role": "assistant",
|
|
"content": []map[string]any{
|
|
{"type": "text", "text": "reply"},
|
|
},
|
|
},
|
|
}),
|
|
}, "\n") + "\n"
|
|
require.NoError(t, os.WriteFile(path, []byte(content), 0o644))
|
|
|
|
provider, ok := NewProvider(AgentOpenClaude, ProviderConfig{
|
|
Roots: []string{root},
|
|
})
|
|
require.True(t, ok)
|
|
|
|
discovered, err := provider.Discover(context.Background())
|
|
require.NoError(t, err)
|
|
require.Len(t, discovered, 1)
|
|
|
|
outcome, err := provider.Parse(context.Background(), ParseRequest{
|
|
Source: discovered[0],
|
|
})
|
|
require.NoError(t, err)
|
|
require.Len(t, outcome.Results, 1)
|
|
|
|
result := outcome.Results[0].Result
|
|
require.Len(t, result.Messages, 3)
|
|
assert.Equal(t, "/resume next step\nwith context", result.Messages[1].Content)
|
|
assert.Equal(t, "queued_command", result.Messages[1].SourceSubtype)
|
|
assert.Equal(t, RoleUser, result.Messages[1].Role)
|
|
assert.Equal(t, 2, result.Session.UserMessageCount)
|
|
assert.Equal(t, "first prompt", result.Session.FirstMessage)
|
|
}
|
|
|
|
func TestOpenClaudeSkipsMetaUserMessages(t *testing.T) {
|
|
root := t.TempDir()
|
|
projectDir := filepath.Join(root, "meta-project")
|
|
require.NoError(t, os.MkdirAll(projectDir, 0o755))
|
|
|
|
path := filepath.Join(projectDir, "session-meta.jsonl")
|
|
content := strings.Join([]string{
|
|
buildMetadataLine(map[string]any{
|
|
"type": "user",
|
|
"timestamp": tsEarly,
|
|
"isMeta": true,
|
|
"uuid": "m1",
|
|
"message": map[string]any{
|
|
"role": "user",
|
|
"content": "hidden meta prompt",
|
|
},
|
|
}),
|
|
buildMetadataLine(map[string]any{
|
|
"type": "user",
|
|
"timestamp": tsEarlyS1,
|
|
"uuid": "u1",
|
|
"message": map[string]any{
|
|
"role": "user",
|
|
"content": "real prompt",
|
|
},
|
|
}),
|
|
buildMetadataLine(map[string]any{
|
|
"type": "assistant",
|
|
"timestamp": "2024-01-01T10:00:02Z",
|
|
"uuid": "a1",
|
|
"message": map[string]any{
|
|
"role": "assistant",
|
|
"stop_reason": "end_turn",
|
|
"content": []map[string]any{
|
|
{"type": "text", "text": "real reply"},
|
|
},
|
|
},
|
|
}),
|
|
buildMetadataLine(map[string]any{
|
|
"type": "user",
|
|
"timestamp": "2024-01-01T10:00:03Z",
|
|
"isMeta": true,
|
|
"uuid": "m2",
|
|
"message": map[string]any{
|
|
"role": "user",
|
|
"content": "hidden trailing meta",
|
|
},
|
|
}),
|
|
}, "\n") + "\n"
|
|
require.NoError(t, os.WriteFile(path, []byte(content), 0o644))
|
|
|
|
provider, ok := NewProvider(AgentOpenClaude, ProviderConfig{
|
|
Roots: []string{root},
|
|
})
|
|
require.True(t, ok)
|
|
|
|
discovered, err := provider.Discover(context.Background())
|
|
require.NoError(t, err)
|
|
require.Len(t, discovered, 1)
|
|
|
|
outcome, err := provider.Parse(context.Background(), ParseRequest{
|
|
Source: discovered[0],
|
|
})
|
|
require.NoError(t, err)
|
|
require.Len(t, outcome.Results, 1)
|
|
|
|
result := outcome.Results[0].Result
|
|
require.Len(t, result.Messages, 2)
|
|
assert.Equal(t, "real prompt", result.Session.FirstMessage)
|
|
assert.Equal(t, 1, result.Session.UserMessageCount)
|
|
assert.Equal(t, TerminationAwaitingUser, result.Session.TerminationStatus)
|
|
assert.Equal(t, RoleUser, result.Messages[0].Role)
|
|
assert.Equal(t, "real prompt", result.Messages[0].Content)
|
|
assert.Equal(t, RoleAssistant, result.Messages[1].Role)
|
|
}
|
|
|
|
func TestOpenClaudeDiscoverParseSubagentRelationship(t *testing.T) {
|
|
root := t.TempDir()
|
|
path := filepath.Join(
|
|
root,
|
|
"proj-name",
|
|
"parent-123",
|
|
"subagents",
|
|
"tasks",
|
|
"agent-worker.jsonl",
|
|
)
|
|
require.NoError(t, os.MkdirAll(filepath.Dir(path), 0o755))
|
|
require.NoError(t, os.WriteFile(path, []byte(strings.Join([]string{
|
|
buildMetadataLine(map[string]any{
|
|
"type": "user",
|
|
"timestamp": tsEarly,
|
|
"uuid": "u1",
|
|
"message": map[string]any{
|
|
"role": "user",
|
|
"content": "subagent prompt",
|
|
},
|
|
}),
|
|
buildMetadataLine(map[string]any{
|
|
"type": "assistant",
|
|
"timestamp": tsEarlyS1,
|
|
"uuid": "u2",
|
|
"parentUuid": "u1",
|
|
"message": map[string]any{
|
|
"role": "assistant",
|
|
"content": []map[string]any{
|
|
{"type": "text", "text": "subagent reply"},
|
|
},
|
|
},
|
|
}),
|
|
}, "\n")+"\n"), 0o644))
|
|
|
|
provider, ok := NewProvider(AgentOpenClaude, ProviderConfig{
|
|
Roots: []string{root},
|
|
Machine: "devbox",
|
|
})
|
|
require.True(t, ok)
|
|
|
|
discovered, err := provider.Discover(context.Background())
|
|
require.NoError(t, err)
|
|
require.Len(t, discovered, 1)
|
|
|
|
outcome, err := provider.Parse(context.Background(), ParseRequest{
|
|
Source: discovered[0],
|
|
Machine: "devbox",
|
|
})
|
|
require.NoError(t, err)
|
|
require.Len(t, outcome.Results, 1)
|
|
|
|
result := outcome.Results[0].Result
|
|
assert.Equal(t, "openclaude:agent-worker", result.Session.ID)
|
|
assert.Equal(t, "openclaude:parent-123", result.Session.ParentSessionID)
|
|
assert.Equal(t, RelSubagent, result.Session.RelationshipType)
|
|
}
|