Skip to content

Commit 6f74464

Browse files
committed
refactor(summarize): split summarize_test.go into smaller files
1 parent 2da1948 commit 6f74464

2 files changed

Lines changed: 351 additions & 341 deletions

File tree

cli/summarize/summarize_2_test.go

Lines changed: 351 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,351 @@
1+
package summarize
2+
3+
import (
4+
"encoding/json"
5+
"strings"
6+
"testing"
7+
8+
"github.com/GrayCodeAI/trace/cli/agent"
9+
"github.com/GrayCodeAI/trace/cli/agent/types"
10+
"github.com/GrayCodeAI/trace/redact"
11+
"github.com/stretchr/testify/require"
12+
)
13+
14+
func TestBuildCondensedTranscriptFromBytes_Codex_ExecCommandDetail(t *testing.T) {
15+
t.Parallel()
16+
17+
codexTranscript := []byte(`{"timestamp":"t1","type":"session_meta","payload":{"id":"s1"}}
18+
{"timestamp":"t2","type":"response_item","payload":{"type":"message","role":"assistant","content":[{"type":"output_text","text":"Running command."}]}}
19+
{"timestamp":"t3","type":"response_item","payload":{"type":"function_call","name":"exec_command","call_id":"call_1","arguments":"{\"cmd\":\"ls -la\",\"workdir\":\"/repo\"}"}}
20+
{"timestamp":"t4","type":"response_item","payload":{"type":"function_call_output","call_id":"call_1","output":"total 0"}}
21+
`)
22+
23+
entries, err := BuildCondensedTranscriptFromBytes(redact.AlreadyRedacted(codexTranscript), agent.AgentTypeCodex)
24+
if err != nil {
25+
t.Fatalf("unexpected error: %v", err)
26+
}
27+
28+
// Find the tool entry
29+
var toolEntry *Entry
30+
for i := range entries {
31+
if entries[i].Type == EntryTypeTool {
32+
toolEntry = &entries[i]
33+
break
34+
}
35+
}
36+
require.NotNil(t, toolEntry, "no tool entry found in entries: %#v", entries)
37+
if toolEntry.ToolName != "exec_command" {
38+
t.Fatalf("expected exec_command, got %q", toolEntry.ToolName)
39+
}
40+
if toolEntry.ToolDetail != "ls -la" {
41+
t.Fatalf("expected tool detail 'ls -la', got %q", toolEntry.ToolDetail)
42+
}
43+
}
44+
45+
func TestBuildCondensedTranscriptFromBytes_OpenCodeUserAndAssistant(t *testing.T) {
46+
// OpenCode export JSON format
47+
ocExportJSON := `{
48+
"info": {"id": "test-session"},
49+
"messages": [
50+
{"info": {"id": "msg-1", "role": "user", "time": {"created": 1708300000}}, "parts": [{"type": "text", "text": "Fix the bug in main.go"}]},
51+
{"info": {"id": "msg-2", "role": "assistant", "time": {"created": 1708300001}}, "parts": [{"type": "text", "text": "I'll fix the bug."}]}
52+
]
53+
}`
54+
55+
entries, err := BuildCondensedTranscriptFromBytes(redact.AlreadyRedacted([]byte(ocExportJSON)), agent.AgentTypeOpenCode)
56+
if err != nil {
57+
t.Fatalf("unexpected error: %v", err)
58+
}
59+
60+
if len(entries) != 2 {
61+
t.Fatalf("expected 2 entries, got %d", len(entries))
62+
}
63+
64+
if entries[0].Type != EntryTypeUser {
65+
t.Errorf("entry 0: expected type %s, got %s", EntryTypeUser, entries[0].Type)
66+
}
67+
if entries[0].Content != "Fix the bug in main.go" {
68+
t.Errorf("entry 0: unexpected content: %s", entries[0].Content)
69+
}
70+
71+
if entries[1].Type != EntryTypeAssistant {
72+
t.Errorf("entry 1: expected type %s, got %s", EntryTypeAssistant, entries[1].Type)
73+
}
74+
if entries[1].Content != "I'll fix the bug." {
75+
t.Errorf("entry 1: unexpected content: %s", entries[1].Content)
76+
}
77+
}
78+
79+
func TestBuildCondensedTranscriptFromBytes_OpenCodeToolCalls(t *testing.T) {
80+
// OpenCode export JSON format with tool calls
81+
ocExportJSON := `{
82+
"info": {"id": "test-session"},
83+
"messages": [
84+
{"info": {"id": "msg-1", "role": "user", "time": {"created": 1708300000}}, "parts": [{"type": "text", "text": "Edit main.go"}]},
85+
{"info": {"id": "msg-2", "role": "assistant", "time": {"created": 1708300001}}, "parts": [
86+
{"type": "text", "text": "Editing now."},
87+
{"type": "tool", "tool": "edit", "callID": "call-1", "state": {"status": "completed", "input": {"filePath": "main.go"}, "output": "Applied"}},
88+
{"type": "tool", "tool": "bash", "callID": "call-2", "state": {"status": "completed", "input": {"command": "go test ./..."}, "output": "PASS"}}
89+
]}
90+
]
91+
}`
92+
93+
entries, err := BuildCondensedTranscriptFromBytes(redact.AlreadyRedacted([]byte(ocExportJSON)), agent.AgentTypeOpenCode)
94+
if err != nil {
95+
t.Fatalf("unexpected error: %v", err)
96+
}
97+
98+
// user + assistant + 2 tool calls
99+
if len(entries) != 4 {
100+
t.Fatalf("expected 4 entries, got %d", len(entries))
101+
}
102+
103+
if entries[2].Type != EntryTypeTool {
104+
t.Errorf("entry 2: expected type %s, got %s", EntryTypeTool, entries[2].Type)
105+
}
106+
if entries[2].ToolName != "edit" {
107+
t.Errorf("entry 2: expected tool name edit, got %s", entries[2].ToolName)
108+
}
109+
if entries[2].ToolDetail != testMainGoFile {
110+
t.Errorf("entry 2: expected tool detail main.go, got %s", entries[2].ToolDetail)
111+
}
112+
113+
if entries[3].ToolName != "bash" {
114+
t.Errorf("entry 3: expected tool name bash, got %s", entries[3].ToolName)
115+
}
116+
if entries[3].ToolDetail != "go test ./..." {
117+
t.Errorf("entry 3: expected tool detail 'go test ./...', got %s", entries[3].ToolDetail)
118+
}
119+
}
120+
121+
func TestBuildCondensedTranscriptFromBytes_OpenCodeSkipsEmptyContent(t *testing.T) {
122+
// OpenCode export JSON format with empty content messages
123+
ocExportJSON := `{
124+
"info": {"id": "test-session"},
125+
"messages": [
126+
{"info": {"id": "msg-1", "role": "user", "time": {"created": 1708300000}}, "parts": []},
127+
{"info": {"id": "msg-2", "role": "assistant", "time": {"created": 1708300001}}, "parts": []},
128+
{"info": {"id": "msg-3", "role": "user", "time": {"created": 1708300010}}, "parts": [{"type": "text", "text": "Real prompt"}]}
129+
]
130+
}`
131+
132+
entries, err := BuildCondensedTranscriptFromBytes(redact.AlreadyRedacted([]byte(ocExportJSON)), agent.AgentTypeOpenCode)
133+
if err != nil {
134+
t.Fatalf("unexpected error: %v", err)
135+
}
136+
137+
if len(entries) != 1 {
138+
t.Fatalf("expected 1 entry (empty content skipped), got %d", len(entries))
139+
}
140+
if entries[0].Content != "Real prompt" {
141+
t.Errorf("expected 'Real prompt', got %s", entries[0].Content)
142+
}
143+
}
144+
145+
func TestBuildCondensedTranscriptFromBytes_OpenCodeInvalidJSON(t *testing.T) {
146+
// Invalid JSON now returns an error (not silently skipped like JSONL)
147+
_, err := BuildCondensedTranscriptFromBytes(redact.AlreadyRedacted([]byte("not json")), agent.AgentTypeOpenCode)
148+
if err == nil {
149+
t.Fatal("expected error for invalid JSON")
150+
}
151+
}
152+
153+
func TestBuildCondensedTranscriptFromBytes_CompactTranscriptFallback(t *testing.T) {
154+
compactJSONL := `{"v":1,"agent":"pi","cli_version":"test","type":"user","ts":"2026-01-01T00:00:00Z","content":[{"text":"Create bye.txt"}]}
155+
{"v":1,"agent":"pi","cli_version":"test","type":"assistant","ts":"2026-01-01T00:00:01Z","content":[{"type":"tool_use","id":"tc1","name":"Write","input":{"path":"bye.txt"}},{"type":"text","text":"Created bye.txt"}]}
156+
`
157+
158+
entries, err := BuildCondensedTranscriptFromBytes(redact.AlreadyRedacted([]byte(compactJSONL)), types.AgentType("Pi"))
159+
if err != nil {
160+
t.Fatalf("unexpected error: %v", err)
161+
}
162+
if len(entries) != 3 {
163+
t.Fatalf("expected 3 entries, got %d", len(entries))
164+
}
165+
if entries[0].Type != EntryTypeUser || entries[0].Content != "Create bye.txt" {
166+
t.Fatalf("unexpected first entry: %+v", entries[0])
167+
}
168+
if entries[1].Type != EntryTypeTool || entries[1].ToolName != "Write" || entries[1].ToolDetail != "bye.txt" {
169+
t.Fatalf("unexpected tool entry: %+v", entries[1])
170+
}
171+
if entries[2].Type != EntryTypeAssistant || entries[2].Content != "Created bye.txt" {
172+
t.Fatalf("unexpected assistant entry: %+v", entries[2])
173+
}
174+
}
175+
176+
func TestBuildCondensedTranscriptFromBytes_CursorRoleBasedJSONL(t *testing.T) {
177+
// Cursor transcripts use "role" instead of "type" and wrap user text in <user_query> tags.
178+
// The transcript parser normalizes role→type, so condensation should work.
179+
cursorJSONL := `{"role":"user","message":{"content":[{"type":"text","text":"<user_query>\nhello\n</user_query>"}]}}
180+
{"role":"assistant","message":{"content":[{"type":"text","text":"Hi there!"}]}}
181+
{"role":"user","message":{"content":[{"type":"text","text":"<user_query>\nadd one to a file and commit\n</user_query>"}]}}
182+
{"role":"assistant","message":{"content":[{"type":"text","text":"Created one.txt with one and committed."}]}}
183+
`
184+
185+
entries, err := BuildCondensedTranscriptFromBytes(redact.AlreadyRedacted([]byte(cursorJSONL)), agent.AgentTypeCursor)
186+
if err != nil {
187+
t.Fatalf("unexpected error: %v", err)
188+
}
189+
190+
if len(entries) == 0 {
191+
t.Fatal("expected non-empty entries for Cursor transcript, got 0 (role→type normalization may be broken)")
192+
}
193+
194+
// Should have 4 entries: 2 user + 2 assistant
195+
if len(entries) != 4 {
196+
t.Fatalf("expected 4 entries, got %d", len(entries))
197+
}
198+
199+
if entries[0].Type != EntryTypeUser {
200+
t.Errorf("entry 0: expected type %s, got %s", EntryTypeUser, entries[0].Type)
201+
}
202+
if !strings.Contains(entries[0].Content, "hello") {
203+
t.Errorf("entry 0: expected content containing 'hello', got %q", entries[0].Content)
204+
}
205+
206+
if entries[1].Type != EntryTypeAssistant {
207+
t.Errorf("entry 1: expected type %s, got %s", EntryTypeAssistant, entries[1].Type)
208+
}
209+
if entries[1].Content != "Hi there!" {
210+
t.Errorf("entry 1: expected 'Hi there!', got %q", entries[1].Content)
211+
}
212+
213+
if entries[2].Type != EntryTypeUser {
214+
t.Errorf("entry 2: expected type %s, got %s", EntryTypeUser, entries[2].Type)
215+
}
216+
217+
if entries[3].Type != EntryTypeAssistant {
218+
t.Errorf("entry 3: expected type %s, got %s", EntryTypeAssistant, entries[3].Type)
219+
}
220+
}
221+
222+
func TestBuildCondensedTranscriptFromBytes_CursorNoToolUseBlocks(t *testing.T) {
223+
// Cursor transcripts have no tool_use blocks — only text content.
224+
// This verifies we get entries (not an empty result) even without tool calls.
225+
cursorJSONL := `{"role":"user","message":{"content":[{"type":"text","text":"write a poem"}]}}
226+
{"role":"assistant","message":{"content":[{"type":"text","text":"Here is a poem about code."}]}}
227+
`
228+
229+
entries, err := BuildCondensedTranscriptFromBytes(redact.AlreadyRedacted([]byte(cursorJSONL)), agent.AgentTypeCursor)
230+
if err != nil {
231+
t.Fatalf("unexpected error: %v", err)
232+
}
233+
234+
if len(entries) != 2 {
235+
t.Fatalf("expected 2 entries, got %d", len(entries))
236+
}
237+
238+
// No tool entries should appear
239+
for i, e := range entries {
240+
if e.Type == EntryTypeTool {
241+
t.Errorf("entry %d: unexpected tool entry in Cursor transcript", i)
242+
}
243+
}
244+
}
245+
246+
func TestBuildCondensedTranscriptFromBytes_DroidUserAndAssistant(t *testing.T) {
247+
// Droid uses an envelope: {"type":"message","id":"...","message":{"role":"...","content":[...]}}
248+
droidJSONL := strings.Join([]string{
249+
`{"type":"session_start","session":{"session_id":"s1"}}`,
250+
`{"type":"message","id":"m1","message":{"role":"user","content":[{"type":"text","text":"Help me write a Go function"}]}}`,
251+
`{"type":"message","id":"m2","message":{"role":"assistant","content":[{"type":"text","text":"Sure, here is a function."}]}}`,
252+
`{"type":"message","id":"m3","message":{"role":"assistant","content":[{"type":"tool_use","name":"Write","input":{"file_path":"main.go","content":"package main"}}]}}`,
253+
}, "\n") + "\n"
254+
255+
entries, err := BuildCondensedTranscriptFromBytes(redact.AlreadyRedacted([]byte(droidJSONL)), agent.AgentTypeFactoryAIDroid)
256+
if err != nil {
257+
t.Fatalf("unexpected error: %v", err)
258+
}
259+
260+
// session_start is skipped; expect: user + assistant text + tool
261+
if len(entries) != 3 {
262+
t.Fatalf("expected 3 entries, got %d", len(entries))
263+
}
264+
265+
if entries[0].Type != EntryTypeUser {
266+
t.Errorf("entry 0: expected type %s, got %s", EntryTypeUser, entries[0].Type)
267+
}
268+
if entries[0].Content != "Help me write a Go function" {
269+
t.Errorf("entry 0: unexpected content: %s", entries[0].Content)
270+
}
271+
272+
if entries[1].Type != EntryTypeAssistant {
273+
t.Errorf("entry 1: expected type %s, got %s", EntryTypeAssistant, entries[1].Type)
274+
}
275+
if entries[1].Content != "Sure, here is a function." {
276+
t.Errorf("entry 1: unexpected content: %s", entries[1].Content)
277+
}
278+
279+
if entries[2].Type != EntryTypeTool {
280+
t.Errorf("entry 2: expected type %s, got %s", EntryTypeTool, entries[2].Type)
281+
}
282+
if entries[2].ToolName != "Write" {
283+
t.Errorf("entry 2: expected tool name Write, got %s", entries[2].ToolName)
284+
}
285+
if entries[2].ToolDetail != testMainGoFile {
286+
t.Errorf("entry 2: expected tool detail main.go, got %s", entries[2].ToolDetail)
287+
}
288+
}
289+
290+
func TestBuildCondensedTranscriptFromBytes_DroidMalformedInput(t *testing.T) {
291+
// Completely invalid content should return an error from the Droid parser
292+
_, err := BuildCondensedTranscriptFromBytes(redact.AlreadyRedacted([]byte("not valid jsonl at all{{{")), agent.AgentTypeFactoryAIDroid)
293+
// Droid parser is lenient — malformed lines are skipped. With no valid messages,
294+
// it returns an empty slice (not an error).
295+
if err != nil {
296+
t.Fatalf("unexpected error for malformed Droid input: %v", err)
297+
}
298+
}
299+
300+
func TestBuildCondensedTranscriptFromBytes_DroidEmptyTranscript(t *testing.T) {
301+
entries, err := BuildCondensedTranscriptFromBytes(redact.AlreadyRedacted([]byte("")), agent.AgentTypeFactoryAIDroid)
302+
if err != nil {
303+
t.Fatalf("unexpected error: %v", err)
304+
}
305+
if len(entries) != 0 {
306+
t.Errorf("expected 0 entries for empty Droid transcript, got %d", len(entries))
307+
}
308+
}
309+
310+
// mustMarshal is a test helper that marshals v to JSON, failing the test on error.
311+
func mustMarshal(t *testing.T, v interface{}) json.RawMessage {
312+
t.Helper()
313+
data, err := json.Marshal(v)
314+
if err != nil {
315+
t.Fatalf("failed to marshal: %v", err)
316+
}
317+
return data
318+
}
319+
320+
func TestResolveModel(t *testing.T) {
321+
t.Parallel()
322+
323+
tests := []struct {
324+
name string
325+
provider string
326+
model string
327+
want string
328+
}{
329+
{
330+
name: "claude code with empty model defaults to DefaultModel",
331+
provider: string(agent.AgentNameClaudeCode),
332+
model: "",
333+
want: DefaultModel,
334+
},
335+
{
336+
name: "other provider passes model through unchanged",
337+
provider: "codex",
338+
model: "gpt-5",
339+
want: "gpt-5",
340+
},
341+
}
342+
for _, tt := range tests {
343+
t.Run(tt.name, func(t *testing.T) {
344+
t.Parallel()
345+
got := ResolveModel(types.AgentName(tt.provider), tt.model)
346+
if got != tt.want {
347+
t.Errorf("ResolveModel(%q, %q) = %q, want %q", tt.provider, tt.model, got, tt.want)
348+
}
349+
})
350+
}
351+
}

0 commit comments

Comments
 (0)