@hybridlabor-api/bdb-synapse 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +173 -0
- package/bin/synapse +0 -0
- package/cmd/rubriceval/main.go +308 -0
- package/cmd/synapse/main.go +280 -0
- package/cmd/synapse/main_test.go +16 -0
- package/go.mod +7 -0
- package/go.sum +6 -0
- package/internal/adapter/adapter.go +1117 -0
- package/internal/adapter/adapter_test.go +518 -0
- package/internal/adapter/agy/adapter.go +193 -0
- package/internal/adapter/claudecode/adapter.go +415 -0
- package/internal/adapter/claudecode/adapter_test.go +260 -0
- package/internal/adapter/claudecode/agents.go +387 -0
- package/internal/adapter/claudecode/agents_test.go +480 -0
- package/internal/adapter/claudecode/summary_inputs_test.go +19 -0
- package/internal/adapter/codex/adapter.go +919 -0
- package/internal/adapter/codex/adapter_test.go +920 -0
- package/internal/adapter/codex/agents.go +401 -0
- package/internal/adapter/codex/agents_test.go +610 -0
- package/internal/adapter/codex/summary_inputs_test.go +20 -0
- package/internal/adapter/pi/adapter.go +483 -0
- package/internal/adapter/pi/adapter_test.go +517 -0
- package/internal/citymap/builder.go +1124 -0
- package/internal/citymap/builder_test.go +818 -0
- package/internal/judge/cache.go +180 -0
- package/internal/judge/cli.go +240 -0
- package/internal/judge/cli_test.go +68 -0
- package/internal/judge/fresh_summary_test.go +37 -0
- package/internal/judge/input.go +233 -0
- package/internal/judge/judge.go +416 -0
- package/internal/judge/judge_test.go +288 -0
- package/internal/judge/prompt.go +129 -0
- package/internal/judge/rubric.go +275 -0
- package/internal/judge/rubric_test.go +642 -0
- package/internal/model/agent.go +63 -0
- package/internal/model/agent_schema_test.go +86 -0
- package/internal/model/agent_test.go +64 -0
- package/internal/model/model.go +175 -0
- package/internal/model/report.go +166 -0
- package/internal/model/stats.go +151 -0
- package/internal/model/stats_test.go +89 -0
- package/internal/model/trace_schema_test.go +67 -0
- package/internal/server/analyze.go +286 -0
- package/internal/server/analyze_test.go +297 -0
- package/internal/server/codex_index_test.go +40 -0
- package/internal/server/hardening_test.go +147 -0
- package/internal/server/reportindex.go +91 -0
- package/internal/server/reportindex_test.go +116 -0
- package/internal/server/server.go +1099 -0
- package/internal/server/server_test.go +1389 -0
- package/internal/server/static/assets/fraunces-latin-ext-standard-italic-CGbN9UgK.woff2 +0 -0
- package/internal/server/static/assets/fraunces-latin-ext-standard-normal-CJcjJNj7.woff2 +0 -0
- package/internal/server/static/assets/fraunces-latin-standard-italic-lSdLDfvT.woff2 +0 -0
- package/internal/server/static/assets/fraunces-latin-standard-normal-DihXLNYH.woff2 +0 -0
- package/internal/server/static/assets/fraunces-vietnamese-standard-italic-DxWqP7Ku.woff2 +0 -0
- package/internal/server/static/assets/fraunces-vietnamese-standard-normal-Czevyj-6.woff2 +0 -0
- package/internal/server/static/assets/index-BNoY_BiB.css +1 -0
- package/internal/server/static/assets/index-C_adLrJr.js +3 -0
- package/internal/server/static/assets/react-gcHzaSmV.js +10 -0
- package/internal/server/static/assets/schibsted-grotesk-latin-ext-wght-normal-hsMS0n0O.woff2 +0 -0
- package/internal/server/static/assets/schibsted-grotesk-latin-wght-normal-Bb8VGrTG.woff2 +0 -0
- package/internal/server/static/assets/three-DnGjZfD1.js +4012 -0
- package/internal/server/static/index.html +26 -0
- package/internal/server/tracestore.go +173 -0
- package/internal/server/tracestore_test.go +51 -0
- package/internal/textutil/truncate.go +30 -0
- package/internal/textutil/truncate_test.go +39 -0
- package/package.json +35 -0
- package/web/e2e/agent-lens.spec.ts +688 -0
- package/web/index.html +23 -0
- package/web/package-lock.json +1933 -0
- package/web/package.json +33 -0
- package/web/playwright.config.ts +24 -0
- package/web/src/App.tsx +876 -0
- package/web/src/api/client.ts +74 -0
- package/web/src/main.tsx +12 -0
- package/web/src/playback/recorder.ts +160 -0
- package/web/src/playback/reducer.ts +91 -0
- package/web/src/scene/CityScene.tsx +638 -0
- package/web/src/scene/TreeScene.tsx +656 -0
- package/web/src/scene/dirLabels.ts +145 -0
- package/web/src/scene/sceneUtils.ts +144 -0
- package/web/src/scene/textures.ts +60 -0
- package/web/src/scene/trail.ts +79 -0
- package/web/src/scene/treeLayout.ts +169 -0
- package/web/src/state/filters.ts +40 -0
- package/web/src/state/store.ts +83 -0
- package/web/src/styles.css +2565 -0
- package/web/src/types.ts +315 -0
- package/web/src/ui/AgentsPanel.tsx +376 -0
- package/web/src/ui/Dock.tsx +104 -0
- package/web/src/ui/Hud.tsx +335 -0
- package/web/src/ui/Inspector.tsx +107 -0
- package/web/src/ui/LogoMark.tsx +38 -0
- package/web/src/ui/ReportPanel.tsx +491 -0
- package/web/src/ui/SessionRail.tsx +316 -0
- package/web/src/ui/Timeline.tsx +458 -0
- package/web/src/ui/ViewPanel.tsx +45 -0
- package/web/src/ui/shortcuts.ts +4 -0
- package/web/tsconfig.json +21 -0
- package/web/vite.config.ts +28 -0
|
@@ -0,0 +1,920 @@
|
|
|
1
|
+
package codex
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"encoding/json"
|
|
5
|
+
"math"
|
|
6
|
+
"os"
|
|
7
|
+
"path/filepath"
|
|
8
|
+
"testing"
|
|
9
|
+
|
|
10
|
+
"github.com/hybridlabor-api/bdb-synapse/internal/judge"
|
|
11
|
+
"github.com/hybridlabor-api/bdb-synapse/internal/model"
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
func TestParseCodexSession(t *testing.T) {
|
|
15
|
+
root := t.TempDir()
|
|
16
|
+
if err := os.WriteFile(filepath.Join(root, "README.md"), []byte("# Demo\n"), 0o644); err != nil {
|
|
17
|
+
t.Fatal(err)
|
|
18
|
+
}
|
|
19
|
+
dir := t.TempDir()
|
|
20
|
+
session := filepath.Join(dir, "rollout-2026-07-09T00-00-00-codex-demo.jsonl")
|
|
21
|
+
index := filepath.Join(dir, "session_index.jsonl")
|
|
22
|
+
writeJSONL(t, index, map[string]any{
|
|
23
|
+
"id": "codex-demo",
|
|
24
|
+
"thread_name": "Codex demo trace",
|
|
25
|
+
"updated_at": "2026-07-09T00:00:09Z",
|
|
26
|
+
})
|
|
27
|
+
writeJSONL(t, session,
|
|
28
|
+
map[string]any{
|
|
29
|
+
"timestamp": "2026-07-09T00:00:00Z",
|
|
30
|
+
"type": "session_meta",
|
|
31
|
+
"payload": map[string]any{
|
|
32
|
+
"id": "codex-demo",
|
|
33
|
+
"session_id": "codex-demo",
|
|
34
|
+
"timestamp": "2026-07-09T00:00:00Z",
|
|
35
|
+
"cwd": filepath.ToSlash(root),
|
|
36
|
+
"git": map[string]any{
|
|
37
|
+
"branch": "main",
|
|
38
|
+
"commit_hash": "abc123",
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
map[string]any{
|
|
43
|
+
"timestamp": "2026-07-09T00:00:01Z",
|
|
44
|
+
"type": "turn_context",
|
|
45
|
+
"payload": map[string]any{
|
|
46
|
+
"cwd": filepath.ToSlash(root),
|
|
47
|
+
"model": "gpt-5.5",
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
map[string]any{
|
|
51
|
+
"timestamp": "2026-07-09T00:00:02Z",
|
|
52
|
+
"type": "response_item",
|
|
53
|
+
"payload": map[string]any{
|
|
54
|
+
"type": "message",
|
|
55
|
+
"role": "user",
|
|
56
|
+
"content": []any{map[string]any{"type": "input_text", "text": "inspect"}},
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
call("2026-07-09T00:00:03Z", "fc-read", "call-read", "exec_command", map[string]any{
|
|
60
|
+
"cmd": "sed -n '1,40p' README.md",
|
|
61
|
+
"workdir": filepath.ToSlash(root),
|
|
62
|
+
}),
|
|
63
|
+
output("2026-07-09T00:00:04Z", "call-read", "Chunk ID: read\nProcess exited with code 0\nOutput:\n# Demo\n"),
|
|
64
|
+
callRaw("2026-07-09T00:00:05Z", "fc-edit", "call-edit", "apply_patch", "*** Begin Patch\n*** Update File: README.md\n@@\n-# Demo\n+# Demo updated\n*** End Patch\n"),
|
|
65
|
+
output("2026-07-09T00:00:06Z", "call-edit", "Success. Updated the following files:\nM README.md\n"),
|
|
66
|
+
call("2026-07-09T00:00:07Z", "fc-test", "call-test", "exec_command", map[string]any{
|
|
67
|
+
"cmd": "go test ./...",
|
|
68
|
+
"workdir": filepath.ToSlash(root),
|
|
69
|
+
}),
|
|
70
|
+
output("2026-07-09T00:00:08Z", "call-test", "Chunk ID: test\nProcess exited with code 1\nOutput:\nFAIL ./...\n"),
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
adapter := Adapter{Dir: dir, IndexPath: index}
|
|
74
|
+
meta, err := adapter.Summarize(session)
|
|
75
|
+
if err != nil {
|
|
76
|
+
t.Fatal(err)
|
|
77
|
+
}
|
|
78
|
+
if meta.ID != "codex-demo" || meta.Harness != "codex" || meta.Title != "Codex demo trace" {
|
|
79
|
+
t.Fatalf("meta = %#v", meta)
|
|
80
|
+
}
|
|
81
|
+
if meta.Model != "gpt-5.5" || meta.GitBranch != "main" || meta.EventCount != 3 {
|
|
82
|
+
t.Fatalf("meta = %#v", meta)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
trace, err := adapter.Parse(session)
|
|
86
|
+
if err != nil {
|
|
87
|
+
t.Fatal(err)
|
|
88
|
+
}
|
|
89
|
+
if trace.Session.ID != "codex-demo" || trace.Session.Harness != "codex" || trace.Session.Commit != "abc123" {
|
|
90
|
+
t.Fatalf("session = %#v", trace.Session)
|
|
91
|
+
}
|
|
92
|
+
if trace.Session.Title != "Codex demo trace" || trace.Session.Model != "gpt-5.5" {
|
|
93
|
+
t.Fatalf("session = %#v", trace.Session)
|
|
94
|
+
}
|
|
95
|
+
if len(trace.Marks) != 1 || trace.Marks[0].Type != "user-message" || trace.Marks[0].Note != "inspect" {
|
|
96
|
+
t.Fatalf("marks = %#v", trace.Marks)
|
|
97
|
+
}
|
|
98
|
+
if len(trace.Events) != 3 {
|
|
99
|
+
t.Fatalf("events = %d", len(trace.Events))
|
|
100
|
+
}
|
|
101
|
+
if got := trace.Events[0]; got.Tool != "exec_command" || got.Action != "read" || len(got.Targets) != 1 || got.Targets[0].Path != "README.md" || got.Targets[0].Touch != "read" {
|
|
102
|
+
t.Fatalf("read event = %#v", got)
|
|
103
|
+
}
|
|
104
|
+
if got := trace.Events[1]; got.Tool != "apply_patch" || got.Action != "edit" || got.Targets[0].Touch != "edit" {
|
|
105
|
+
t.Fatalf("edit event = %#v", got)
|
|
106
|
+
}
|
|
107
|
+
if got := trace.Events[2]; got.Action != "verify" || !got.IsError {
|
|
108
|
+
t.Fatalf("verify event = %#v", got)
|
|
109
|
+
}
|
|
110
|
+
if trace.Stats.Edited != 1 || trace.Stats.EventsBeforeFirstEdit != 1 || math.Abs(trace.Stats.ErrorRate-1.0/3.0) > 0.0001 {
|
|
111
|
+
t.Fatalf("stats = %#v", trace.Stats)
|
|
112
|
+
}
|
|
113
|
+
want := model.Observability{Reads: model.ObservabilityEstimated, Errors: model.ObservabilityEstimated}
|
|
114
|
+
if trace.Stats.Observability != want {
|
|
115
|
+
t.Fatalf("observability = %#v", trace.Stats.Observability)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
func TestParseCodexContextCompactedBecomesCompactionMark(t *testing.T) {
|
|
120
|
+
dir := t.TempDir()
|
|
121
|
+
session := filepath.Join(dir, "rollout-2026-07-09T00-00-00-codex-compact.jsonl")
|
|
122
|
+
writeJSONL(t, session,
|
|
123
|
+
map[string]any{
|
|
124
|
+
"timestamp": "2026-07-09T00:00:00Z",
|
|
125
|
+
"type": "session_meta",
|
|
126
|
+
"payload": map[string]any{"id": "codex-compact", "timestamp": "2026-07-09T00:00:00Z"},
|
|
127
|
+
},
|
|
128
|
+
call("2026-07-09T00:00:01Z", "fc-1", "call-1", "exec_command", map[string]any{"cmd": "true"}),
|
|
129
|
+
output("2026-07-09T00:00:02Z", "call-1", "Process exited with code 0"),
|
|
130
|
+
map[string]any{
|
|
131
|
+
"timestamp": "2026-07-09T00:00:03Z",
|
|
132
|
+
"type": "event_msg",
|
|
133
|
+
"payload": map[string]any{"type": "context_compacted"},
|
|
134
|
+
},
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
trace, err := Adapter{Dir: dir}.Parse(session)
|
|
138
|
+
if err != nil {
|
|
139
|
+
t.Fatal(err)
|
|
140
|
+
}
|
|
141
|
+
if len(trace.Marks) != 1 || trace.Marks[0].Type != "compaction" || trace.Marks[0].Seq != 1 {
|
|
142
|
+
t.Fatalf("marks = %#v", trace.Marks)
|
|
143
|
+
}
|
|
144
|
+
if trace.Stats.Compactions != 1 {
|
|
145
|
+
t.Fatalf("compactions = %d", trace.Stats.Compactions)
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
func TestParseCodexSpawnAgentBecomesSubagentMark(t *testing.T) {
|
|
150
|
+
dir := t.TempDir()
|
|
151
|
+
session := filepath.Join(dir, "rollout-2026-07-14T00-00-00-codex-subagent.jsonl")
|
|
152
|
+
writeJSONL(t, session,
|
|
153
|
+
map[string]any{
|
|
154
|
+
"timestamp": "2026-07-14T00:00:00Z",
|
|
155
|
+
"type": "session_meta",
|
|
156
|
+
"payload": map[string]any{"id": "codex-subagent", "timestamp": "2026-07-14T00:00:00Z"},
|
|
157
|
+
},
|
|
158
|
+
call("2026-07-14T00:00:01Z", "fc-1", "call-1", "exec_command", map[string]any{"cmd": "true"}),
|
|
159
|
+
output("2026-07-14T00:00:02Z", "call-1", "Process exited with code 0"),
|
|
160
|
+
call("2026-07-14T00:00:03Z", "fc-2", "call-2", "spawn_agent", map[string]any{
|
|
161
|
+
"task_name": "qa",
|
|
162
|
+
"message": "Review the change",
|
|
163
|
+
}),
|
|
164
|
+
output("2026-07-14T00:00:04Z", "call-2", `{"agent_id":"agent-1"}`),
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
trace, err := Adapter{Dir: dir}.Parse(session)
|
|
168
|
+
if err != nil {
|
|
169
|
+
t.Fatal(err)
|
|
170
|
+
}
|
|
171
|
+
if len(trace.Marks) != 1 || trace.Marks[0].Type != "subagent" || trace.Marks[0].Seq != 1 || trace.Marks[0].Note != "spawn_agent" {
|
|
172
|
+
t.Fatalf("marks = %#v", trace.Marks)
|
|
173
|
+
}
|
|
174
|
+
if trace.Stats.Subagents != 1 {
|
|
175
|
+
t.Fatalf("subagents = %d", trace.Stats.Subagents)
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
func TestParseCodexCustomCallsCanonicalizesOrderAndPatchResults(t *testing.T) {
|
|
180
|
+
root := t.TempDir()
|
|
181
|
+
readme := filepath.Join(root, "README.md")
|
|
182
|
+
added := filepath.Join(root, "added.go")
|
|
183
|
+
if err := os.WriteFile(readme, []byte("# Demo\n"), 0o644); err != nil {
|
|
184
|
+
t.Fatal(err)
|
|
185
|
+
}
|
|
186
|
+
session := filepath.Join(t.TempDir(), "custom.jsonl")
|
|
187
|
+
failed := false
|
|
188
|
+
succeeded := true
|
|
189
|
+
writeJSONL(t, session,
|
|
190
|
+
map[string]any{
|
|
191
|
+
"timestamp": "2026-07-10T00:00:00Z",
|
|
192
|
+
"type": "session_meta",
|
|
193
|
+
"payload": map[string]any{
|
|
194
|
+
"id": "custom-demo",
|
|
195
|
+
"cwd": filepath.ToSlash(root),
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
userMessage("2026-07-10T00:00:01Z", "before calls"),
|
|
199
|
+
customCall("2026-07-10T00:00:02Z", "", "", "apply_patch", "*** Begin Patch\n"),
|
|
200
|
+
customCall("2026-07-10T00:00:03Z", "ctc-patch", "call-patch", "apply_patch", "*** Begin Patch\n*** Update File: README.md\n*** End Patch\n"),
|
|
201
|
+
userMessage("2026-07-10T00:00:04Z", "between call and output"),
|
|
202
|
+
customOutput("2026-07-10T00:00:05Z", "call-late", "orphan output"),
|
|
203
|
+
customCall("2026-07-10T00:00:06Z", "ctc-image", "call-image", "view_image", map[string]any{
|
|
204
|
+
"path": filepath.ToSlash(readme),
|
|
205
|
+
}),
|
|
206
|
+
customOutput("2026-07-10T00:00:07Z", "call-image", []any{map[string]any{"text": "first image output"}}),
|
|
207
|
+
customOutput("2026-07-10T00:00:08Z", "call-image", "Process exited with code 1"),
|
|
208
|
+
customOutput("2026-07-10T00:00:09Z", "call-patch", "first patch output"),
|
|
209
|
+
customOutput("2026-07-10T00:00:10Z", "call-patch", "second patch output"),
|
|
210
|
+
patchApplyEnd("2026-07-10T00:00:11Z", "call-patch", &failed, map[string]string{
|
|
211
|
+
filepath.ToSlash(readme): "update",
|
|
212
|
+
filepath.ToSlash(added): "add",
|
|
213
|
+
}),
|
|
214
|
+
patchApplyEnd("2026-07-10T00:00:12Z", "call-patch", &succeeded, map[string]string{
|
|
215
|
+
filepath.Join(root, "ignored.go"): "add",
|
|
216
|
+
}),
|
|
217
|
+
patchApplyEnd("2026-07-10T00:00:13Z", "orphan-patch", &failed, map[string]string{
|
|
218
|
+
filepath.Join(root, "orphan.go"): "add",
|
|
219
|
+
}),
|
|
220
|
+
call("2026-07-10T00:00:14Z", "fc-late", "call-late", "exec_command", map[string]any{
|
|
221
|
+
"cmd": "go test ./...",
|
|
222
|
+
"workdir": filepath.ToSlash(root),
|
|
223
|
+
}),
|
|
224
|
+
userMessage("2026-07-10T00:00:15Z", "after calls"),
|
|
225
|
+
customCall("2026-07-10T00:00:16Z", "duplicate", "call-patch", "view_image", map[string]any{
|
|
226
|
+
"path": filepath.Join(root, "ignored.png"),
|
|
227
|
+
}),
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
a := Adapter{}
|
|
231
|
+
meta, err := a.Summarize(session)
|
|
232
|
+
if err != nil {
|
|
233
|
+
t.Fatal(err)
|
|
234
|
+
}
|
|
235
|
+
if meta.EventCount != 3 {
|
|
236
|
+
t.Fatalf("event count = %d, want 3", meta.EventCount)
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
trace, err := a.Parse(session)
|
|
240
|
+
if err != nil {
|
|
241
|
+
t.Fatal(err)
|
|
242
|
+
}
|
|
243
|
+
if trace.Session.EventCount != meta.EventCount || len(trace.Events) != 3 {
|
|
244
|
+
t.Fatalf("meta events = %d, trace session events = %d, events = %d", meta.EventCount, trace.Session.EventCount, len(trace.Events))
|
|
245
|
+
}
|
|
246
|
+
if len(trace.Marks) != 3 || trace.Marks[0].Seq != 0 || trace.Marks[1].Seq != 1 || trace.Marks[2].Seq != 3 {
|
|
247
|
+
t.Fatalf("marks = %#v", trace.Marks)
|
|
248
|
+
}
|
|
249
|
+
patchEvent := trace.Events[0]
|
|
250
|
+
if patchEvent.Tool != "apply_patch" || patchEvent.Action != "edit" || !patchEvent.IsError || patchEvent.ResultBytes != len("first patch output") {
|
|
251
|
+
t.Fatalf("patch event = %#v", patchEvent)
|
|
252
|
+
}
|
|
253
|
+
if len(patchEvent.Targets) != 2 || patchEvent.Targets[0].Path != "README.md" || patchEvent.Targets[1].Path != "added.go" {
|
|
254
|
+
t.Fatalf("patch targets = %#v", patchEvent.Targets)
|
|
255
|
+
}
|
|
256
|
+
imageEvent := trace.Events[1]
|
|
257
|
+
if imageEvent.Tool != "view_image" || imageEvent.IsError || imageEvent.ResultBytes != len("first image output") {
|
|
258
|
+
t.Fatalf("image event = %#v", imageEvent)
|
|
259
|
+
}
|
|
260
|
+
if len(imageEvent.Targets) != 1 || imageEvent.Targets[0].Path != "README.md" {
|
|
261
|
+
t.Fatalf("image targets = %#v", imageEvent.Targets)
|
|
262
|
+
}
|
|
263
|
+
lateEvent := trace.Events[2]
|
|
264
|
+
if lateEvent.Tool != "exec_command" || lateEvent.Action != "verify" || lateEvent.ResultBytes != 0 || lateEvent.IsError {
|
|
265
|
+
t.Fatalf("late event = %#v", lateEvent)
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
func TestPatchApplyEndSuccessOverridesTextualFailure(t *testing.T) {
|
|
270
|
+
root := t.TempDir()
|
|
271
|
+
session := filepath.Join(t.TempDir(), "patch-success.jsonl")
|
|
272
|
+
succeeded := true
|
|
273
|
+
writeJSONL(t, session,
|
|
274
|
+
map[string]any{
|
|
275
|
+
"timestamp": "2026-07-10T00:00:00Z",
|
|
276
|
+
"type": "session_meta",
|
|
277
|
+
"payload": map[string]any{
|
|
278
|
+
"id": "patch-success",
|
|
279
|
+
"cwd": filepath.ToSlash(root),
|
|
280
|
+
},
|
|
281
|
+
},
|
|
282
|
+
customCall("2026-07-10T00:00:01Z", "ctc", "call-patch", "apply_patch", "*** Begin Patch\n*** Add File: added.go\n*** End Patch\n"),
|
|
283
|
+
customOutput("2026-07-10T00:00:02Z", "call-patch", "Process exited with code 1"),
|
|
284
|
+
patchApplyEnd("2026-07-10T00:00:03Z", "call-patch", &succeeded, map[string]string{
|
|
285
|
+
filepath.Join(root, "added.go"): "add",
|
|
286
|
+
}),
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
trace, err := (Adapter{}).Parse(session)
|
|
290
|
+
if err != nil {
|
|
291
|
+
t.Fatal(err)
|
|
292
|
+
}
|
|
293
|
+
if len(trace.Events) != 1 || trace.Events[0].IsError {
|
|
294
|
+
t.Fatalf("events = %#v", trace.Events)
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
func TestParseCodexCustomExec(t *testing.T) {
|
|
299
|
+
root := t.TempDir()
|
|
300
|
+
if err := os.WriteFile(filepath.Join(root, "README.md"), []byte("# Demo\n"), 0o644); err != nil {
|
|
301
|
+
t.Fatal(err)
|
|
302
|
+
}
|
|
303
|
+
session := filepath.Join(t.TempDir(), "custom-exec.jsonl")
|
|
304
|
+
script := `const r = await tools.exec_command({"cmd":"sed -n '1,20p' README.md","workdir":` + mustJSON(t, filepath.ToSlash(root)) + `}); text(r.output);`
|
|
305
|
+
writeJSONL(t, session,
|
|
306
|
+
map[string]any{
|
|
307
|
+
"timestamp": "2026-07-10T00:00:00Z",
|
|
308
|
+
"type": "session_meta",
|
|
309
|
+
"payload": map[string]any{
|
|
310
|
+
"id": "custom-exec",
|
|
311
|
+
"cwd": filepath.ToSlash(root),
|
|
312
|
+
},
|
|
313
|
+
},
|
|
314
|
+
customCall("2026-07-10T00:00:01Z", "ctc", "call-exec", "exec", script),
|
|
315
|
+
customOutput("2026-07-10T00:00:02Z", "call-exec", []any{map[string]any{"type": "input_text", "text": "# Demo\n"}}),
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
a := Adapter{}
|
|
319
|
+
meta, err := a.Summarize(session)
|
|
320
|
+
if err != nil {
|
|
321
|
+
t.Fatal(err)
|
|
322
|
+
}
|
|
323
|
+
trace, err := a.Parse(session)
|
|
324
|
+
if err != nil {
|
|
325
|
+
t.Fatal(err)
|
|
326
|
+
}
|
|
327
|
+
if meta.EventCount != 1 || len(trace.Events) != 1 {
|
|
328
|
+
t.Fatalf("meta=%d events=%d", meta.EventCount, len(trace.Events))
|
|
329
|
+
}
|
|
330
|
+
event := trace.Events[0]
|
|
331
|
+
if event.Tool != "exec" || event.Action != "read" || len(event.Targets) != 1 || event.Targets[0].Path != "README.md" || event.Targets[0].Touch != "read" {
|
|
332
|
+
t.Fatalf("event = %#v", event)
|
|
333
|
+
}
|
|
334
|
+
if want := "sed -n '1,20p' README.md -> 1 targets, 0 outside"; event.Summary != want {
|
|
335
|
+
t.Fatalf("summary = %q, want %q", event.Summary, want)
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
func TestCallIDFallsBackToResponseID(t *testing.T) {
|
|
340
|
+
root := t.TempDir()
|
|
341
|
+
path := filepath.Join(root, "README.md")
|
|
342
|
+
if err := os.WriteFile(path, []byte("# Demo\n"), 0o644); err != nil {
|
|
343
|
+
t.Fatal(err)
|
|
344
|
+
}
|
|
345
|
+
session := filepath.Join(t.TempDir(), "fallback-id.jsonl")
|
|
346
|
+
writeJSONL(t, session,
|
|
347
|
+
map[string]any{
|
|
348
|
+
"timestamp": "2026-07-10T00:00:00Z",
|
|
349
|
+
"type": "session_meta",
|
|
350
|
+
"payload": map[string]any{
|
|
351
|
+
"id": "fallback-session",
|
|
352
|
+
"cwd": filepath.ToSlash(root),
|
|
353
|
+
},
|
|
354
|
+
},
|
|
355
|
+
customCall("2026-07-10T00:00:01Z", "fallback-call", "", "view_image", map[string]any{"path": path}),
|
|
356
|
+
customOutput("2026-07-10T00:00:02Z", "fallback-call", "ok"),
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
a := Adapter{}
|
|
360
|
+
meta, err := a.Summarize(session)
|
|
361
|
+
if err != nil {
|
|
362
|
+
t.Fatal(err)
|
|
363
|
+
}
|
|
364
|
+
trace, err := a.Parse(session)
|
|
365
|
+
if err != nil {
|
|
366
|
+
t.Fatal(err)
|
|
367
|
+
}
|
|
368
|
+
if meta.EventCount != 1 || len(trace.Events) != 1 || trace.Events[0].ResultBytes != 2 {
|
|
369
|
+
t.Fatalf("meta=%#v events=%#v", meta, trace.Events)
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
func TestParseCodexJSRepl(t *testing.T) {
|
|
374
|
+
root := t.TempDir()
|
|
375
|
+
path := filepath.Join(root, "packages", "db", "src", "index.ts")
|
|
376
|
+
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
|
377
|
+
t.Fatal(err)
|
|
378
|
+
}
|
|
379
|
+
if err := os.WriteFile(path, []byte("export const db = {}\n"), 0o644); err != nil {
|
|
380
|
+
t.Fatal(err)
|
|
381
|
+
}
|
|
382
|
+
session := filepath.Join(t.TempDir(), "js-repl.jsonl")
|
|
383
|
+
writeJSONL(t, session,
|
|
384
|
+
map[string]any{
|
|
385
|
+
"timestamp": "2026-07-10T00:00:00Z",
|
|
386
|
+
"type": "session_meta",
|
|
387
|
+
"payload": map[string]any{
|
|
388
|
+
"id": "js-repl",
|
|
389
|
+
"cwd": filepath.ToSlash(root),
|
|
390
|
+
},
|
|
391
|
+
},
|
|
392
|
+
customCall("2026-07-10T00:00:01Z", "ctc", "call-js", "js_repl", `await import("./packages/db/src/index.ts")`),
|
|
393
|
+
customOutput("2026-07-10T00:00:02Z", "call-js", "loaded"),
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
trace, err := (Adapter{}).Parse(session)
|
|
397
|
+
if err != nil {
|
|
398
|
+
t.Fatal(err)
|
|
399
|
+
}
|
|
400
|
+
if len(trace.Events) != 1 {
|
|
401
|
+
t.Fatalf("events = %#v", trace.Events)
|
|
402
|
+
}
|
|
403
|
+
event := trace.Events[0]
|
|
404
|
+
if event.Tool != "js_repl" || event.Action != "exec" || len(event.Targets) != 1 || event.Targets[0].Path != "packages/db/src/index.ts" {
|
|
405
|
+
t.Fatalf("event = %#v", event)
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
func TestCommandOutputStatus(t *testing.T) {
|
|
410
|
+
tests := []struct {
|
|
411
|
+
output string
|
|
412
|
+
wantFailed bool
|
|
413
|
+
wantKnown bool
|
|
414
|
+
}{
|
|
415
|
+
{output: "Process exited with code 1", wantFailed: true, wantKnown: true},
|
|
416
|
+
{output: "Exit code: 2", wantFailed: true, wantKnown: true},
|
|
417
|
+
{output: `{"output":"ok","metadata":{"exit_code":0}}`, wantKnown: true},
|
|
418
|
+
{output: `{"output":"failed","metadata":{"exit_code":1}}`, wantFailed: true, wantKnown: true},
|
|
419
|
+
{output: `{"output":"failed","exit_code":3}`, wantFailed: true, wantKnown: true},
|
|
420
|
+
{output: `{"exit_code":3}`},
|
|
421
|
+
{output: `{"output":"failed","Exit_Code":3}`},
|
|
422
|
+
{output: `{"output":"failed","metadata":{"Exit_Code":1}}`},
|
|
423
|
+
{output: `{"output":"Exit code: 1","metadata":{"exit_code":0}}`, wantKnown: true},
|
|
424
|
+
{output: "Script completed\nWall time 0.1 seconds\nOutput:\nExit code: 1", wantKnown: true},
|
|
425
|
+
{output: "Script running with cell ID 28\nExit code: 1"},
|
|
426
|
+
{output: "Script failed\nExit code: 0", wantFailed: true, wantKnown: true},
|
|
427
|
+
{output: "plain output"},
|
|
428
|
+
{output: `{"message":"Wait timed out after 20000ms","timed_out":true}`, wantFailed: true, wantKnown: true},
|
|
429
|
+
{output: `{"message":"Wait timed out after 20000ms","Timed_Out":true}`},
|
|
430
|
+
{output: `{"message":"still running","timed_out":false}`},
|
|
431
|
+
{output: "apply_patch verification failed: Failed to find expected lines in a.go"},
|
|
432
|
+
{output: "Wall time: 8.9 seconds\naborted by user", wantFailed: true, wantKnown: true},
|
|
433
|
+
{output: "Wall time: 8.9 seconds\nOutput:\naborted by user"},
|
|
434
|
+
{output: "Script completedly\nWall time 0.1 seconds"},
|
|
435
|
+
}
|
|
436
|
+
for _, tt := range tests {
|
|
437
|
+
failed, known := commandOutputStatus(tt.output)
|
|
438
|
+
if failed != tt.wantFailed || known != tt.wantKnown {
|
|
439
|
+
t.Errorf("commandOutputStatus(%q) = (%v, %v), want (%v, %v)", tt.output, failed, known, tt.wantFailed, tt.wantKnown)
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
func TestParseCodexScopesOutcomeInferenceAndMatchesExactKeys(t *testing.T) {
|
|
445
|
+
session := filepath.Join(t.TempDir(), "outcome-scope.jsonl")
|
|
446
|
+
writeJSONL(t, session,
|
|
447
|
+
map[string]any{
|
|
448
|
+
"timestamp": "2026-07-10T00:00:00Z",
|
|
449
|
+
"type": "session_meta",
|
|
450
|
+
"payload": map[string]any{"id": "outcome-scope", "cwd": filepath.ToSlash(t.TempDir())},
|
|
451
|
+
},
|
|
452
|
+
customCall("2026-07-10T00:00:01Z", "ctc-script", "call-script", "view_image", map[string]any{"path": "image.png"}),
|
|
453
|
+
customOutput("2026-07-10T00:00:02Z", "call-script", "Script completed\nWall time 0.1 seconds"),
|
|
454
|
+
customCall("2026-07-10T00:00:03Z", "ctc-json", "call-json", "view_image", map[string]any{"path": "image.png"}),
|
|
455
|
+
customOutput("2026-07-10T00:00:04Z", "call-json", `{"output":"failed","exit_code":1}`),
|
|
456
|
+
call("2026-07-10T00:00:05Z", "fc-wrong", "call-wrong", "exec_command", map[string]any{"cmd": "false"}),
|
|
457
|
+
output("2026-07-10T00:00:06Z", "call-wrong", `{"output":"failed","Exit_Code":1}`),
|
|
458
|
+
customCall("2026-07-10T00:00:07Z", "ctc-exact", "call-exact", "exec", `text(await tools.exec_command({cmd: "false"}))`),
|
|
459
|
+
customOutput("2026-07-10T00:00:08Z", "call-exact", `{"output":"failed","exit_code":1}`),
|
|
460
|
+
customCall("2026-07-10T00:00:09Z", "ctc-patch", "call-patch", "apply_patch", "*** Begin Patch\n*** Add File: added.go\n*** End Patch\n"),
|
|
461
|
+
customOutput("2026-07-10T00:00:10Z", "call-patch", "plain output"),
|
|
462
|
+
map[string]any{
|
|
463
|
+
"timestamp": "2026-07-10T00:00:11Z",
|
|
464
|
+
"type": "event_msg",
|
|
465
|
+
"payload": map[string]any{
|
|
466
|
+
"type": "patch_apply_end",
|
|
467
|
+
"call_id": "call-patch",
|
|
468
|
+
"Success": true,
|
|
469
|
+
},
|
|
470
|
+
},
|
|
471
|
+
customCall("2026-07-10T00:00:12Z", "ctc-patch-fail", "call-patch-fail", "apply_patch", "*** Begin Patch\n*** Update File: missing.go\n*** End Patch\n"),
|
|
472
|
+
customOutput("2026-07-10T00:00:13Z", "call-patch-fail", "apply_patch verification failed: missing.go"),
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
trace, err := (Adapter{}).Parse(session)
|
|
476
|
+
if err != nil {
|
|
477
|
+
t.Fatal(err)
|
|
478
|
+
}
|
|
479
|
+
if len(trace.Events) != 6 {
|
|
480
|
+
t.Fatalf("events = %#v", trace.Events)
|
|
481
|
+
}
|
|
482
|
+
for _, index := range []int{0, 1, 2, 4} {
|
|
483
|
+
if got := trace.Events[index]; got.OutcomeKnown || got.IsError {
|
|
484
|
+
t.Fatalf("event %d promoted to known outcome: %#v", index, got)
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
if got := trace.Events[3]; !got.OutcomeKnown || !got.IsError {
|
|
488
|
+
t.Fatalf("exact command envelope = %#v", got)
|
|
489
|
+
}
|
|
490
|
+
if got := trace.Events[5]; !got.OutcomeKnown || !got.IsError {
|
|
491
|
+
t.Fatalf("apply_patch failure = %#v", got)
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
func TestListSessionsSkipsNonCodexJSONL(t *testing.T) {
|
|
496
|
+
dir := t.TempDir()
|
|
497
|
+
writeJSONL(t, filepath.Join(dir, "not-codex.jsonl"), map[string]any{
|
|
498
|
+
"type": "assistant",
|
|
499
|
+
"message": map[string]any{
|
|
500
|
+
"role": "assistant",
|
|
501
|
+
"content": "hello",
|
|
502
|
+
},
|
|
503
|
+
})
|
|
504
|
+
writeJSONL(t, filepath.Join(dir, "codex.jsonl"), map[string]any{
|
|
505
|
+
"timestamp": "2026-07-09T00:00:00Z",
|
|
506
|
+
"type": "session_meta",
|
|
507
|
+
"payload": map[string]any{
|
|
508
|
+
"id": "codex-list",
|
|
509
|
+
"cwd": "/tmp",
|
|
510
|
+
},
|
|
511
|
+
})
|
|
512
|
+
|
|
513
|
+
sessions, err := (Adapter{Dir: dir}).ListSessions()
|
|
514
|
+
if err != nil {
|
|
515
|
+
t.Fatal(err)
|
|
516
|
+
}
|
|
517
|
+
if len(sessions) != 1 || sessions[0].ID != "codex-list" {
|
|
518
|
+
t.Fatalf("sessions = %#v", sessions)
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
func TestListSessionsSkipsCodexSubagents(t *testing.T) {
|
|
523
|
+
dir := t.TempDir()
|
|
524
|
+
mainSession := filepath.Join(dir, "main.jsonl")
|
|
525
|
+
legacySession := filepath.Join(dir, "legacy.jsonl")
|
|
526
|
+
subagentSession := filepath.Join(dir, "subagent.jsonl")
|
|
527
|
+
transitionalSubagentSession := filepath.Join(dir, "transitional-subagent.jsonl")
|
|
528
|
+
writeJSONL(t, mainSession,
|
|
529
|
+
map[string]any{
|
|
530
|
+
"timestamp": "2026-07-10T00:00:00Z",
|
|
531
|
+
"type": "session_meta",
|
|
532
|
+
"payload": map[string]any{
|
|
533
|
+
"id": "main-thread",
|
|
534
|
+
"cwd": "/tmp",
|
|
535
|
+
"thread_source": "user",
|
|
536
|
+
"source": "vscode",
|
|
537
|
+
"forked_from_id": "user-owned-parent",
|
|
538
|
+
},
|
|
539
|
+
},
|
|
540
|
+
// Only the first valid session_meta controls visibility. Later context
|
|
541
|
+
// records must not turn a main thread into an auxiliary session.
|
|
542
|
+
map[string]any{
|
|
543
|
+
"timestamp": "2026-07-10T00:00:00.500Z",
|
|
544
|
+
"type": "session_meta",
|
|
545
|
+
"payload": map[string]any{
|
|
546
|
+
"id": "replayed-child-context",
|
|
547
|
+
"cwd": "/tmp",
|
|
548
|
+
"source": map[string]any{
|
|
549
|
+
"subagent": "replayed-child-context",
|
|
550
|
+
},
|
|
551
|
+
},
|
|
552
|
+
},
|
|
553
|
+
)
|
|
554
|
+
writeJSONL(t, subagentSession,
|
|
555
|
+
map[string]any{
|
|
556
|
+
"timestamp": "2026-07-10T00:00:01Z",
|
|
557
|
+
"type": "session_meta",
|
|
558
|
+
"payload": map[string]any{
|
|
559
|
+
"id": "child-thread",
|
|
560
|
+
"cwd": "/tmp",
|
|
561
|
+
"source": map[string]any{
|
|
562
|
+
"subagent": map[string]any{
|
|
563
|
+
"thread_spawn": map[string]any{"parent_thread_id": "main-thread"},
|
|
564
|
+
},
|
|
565
|
+
},
|
|
566
|
+
},
|
|
567
|
+
},
|
|
568
|
+
// Forked context can replay parent metadata. The first record remains
|
|
569
|
+
// authoritative for deciding whether this file belongs in the list.
|
|
570
|
+
map[string]any{
|
|
571
|
+
"timestamp": "2026-07-10T00:00:02Z",
|
|
572
|
+
"type": "session_meta",
|
|
573
|
+
"payload": map[string]any{
|
|
574
|
+
"id": "main-thread",
|
|
575
|
+
"cwd": "/tmp",
|
|
576
|
+
"source": "vscode",
|
|
577
|
+
},
|
|
578
|
+
},
|
|
579
|
+
call("2026-07-10T00:00:03Z", "fc-child", "call-child", "exec_command", map[string]any{
|
|
580
|
+
"cmd": "pwd",
|
|
581
|
+
"workdir": "/tmp",
|
|
582
|
+
}),
|
|
583
|
+
output("2026-07-10T00:00:04Z", "call-child", "Chunk ID: child\nProcess exited with code 0\nOutput:\n/tmp\n"),
|
|
584
|
+
)
|
|
585
|
+
writeJSONL(t, transitionalSubagentSession, map[string]any{
|
|
586
|
+
"timestamp": "2026-07-10T00:00:03Z",
|
|
587
|
+
"type": "session_meta",
|
|
588
|
+
"payload": map[string]any{
|
|
589
|
+
"id": "transitional-child",
|
|
590
|
+
"cwd": "/tmp",
|
|
591
|
+
"thread_source": "subagent",
|
|
592
|
+
"source": "vscode",
|
|
593
|
+
},
|
|
594
|
+
})
|
|
595
|
+
writeJSONL(t, legacySession, map[string]any{
|
|
596
|
+
"id": "legacy-main",
|
|
597
|
+
"timestamp": "2025-06-13T14:42:30.751Z",
|
|
598
|
+
})
|
|
599
|
+
|
|
600
|
+
adapter := Adapter{Dir: dir}
|
|
601
|
+
mainMeta, err := adapter.Summarize(mainSession)
|
|
602
|
+
if err != nil || mainMeta.Auxiliary {
|
|
603
|
+
t.Fatalf("main meta = %#v, err = %v", mainMeta, err)
|
|
604
|
+
}
|
|
605
|
+
subagentMeta, err := adapter.Summarize(subagentSession)
|
|
606
|
+
if err != nil || !subagentMeta.Auxiliary || subagentMeta.ID != "child-thread" || subagentMeta.EventCount != 1 {
|
|
607
|
+
t.Fatalf("subagent meta = %#v, err = %v", subagentMeta, err)
|
|
608
|
+
}
|
|
609
|
+
subagentTrace, err := adapter.Parse(subagentSession)
|
|
610
|
+
if err != nil {
|
|
611
|
+
t.Fatal(err)
|
|
612
|
+
}
|
|
613
|
+
if subagentTrace.Session.ID != subagentMeta.ID || subagentTrace.Session.Title != subagentMeta.Title ||
|
|
614
|
+
subagentTrace.Session.EndedAt != subagentMeta.EndedAt || subagentTrace.Session.EventCount != subagentMeta.EventCount {
|
|
615
|
+
t.Fatalf("subagent summary/trace mismatch: meta=%#v trace=%#v", subagentMeta, subagentTrace.Session)
|
|
616
|
+
}
|
|
617
|
+
transitionalMeta, err := adapter.Summarize(transitionalSubagentSession)
|
|
618
|
+
if err != nil || !transitionalMeta.Auxiliary {
|
|
619
|
+
t.Fatalf("transitional subagent meta = %#v, err = %v", transitionalMeta, err)
|
|
620
|
+
}
|
|
621
|
+
sessions, err := adapter.ListSessions()
|
|
622
|
+
if err != nil {
|
|
623
|
+
t.Fatal(err)
|
|
624
|
+
}
|
|
625
|
+
if len(sessions) != 2 {
|
|
626
|
+
t.Fatalf("sessions = %#v", sessions)
|
|
627
|
+
}
|
|
628
|
+
visible := map[string]string{}
|
|
629
|
+
for _, session := range sessions {
|
|
630
|
+
visible[session.ID] = session.Path
|
|
631
|
+
}
|
|
632
|
+
if visible["main-thread"] != mainSession || visible["legacy-main"] != legacySession {
|
|
633
|
+
t.Fatalf("visible sessions = %#v", visible)
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
func TestSummarizePopulatesSubagentMetadata(t *testing.T) {
|
|
638
|
+
session := filepath.Join(t.TempDir(), "subagent.jsonl")
|
|
639
|
+
writeJSONL(t, session, map[string]any{
|
|
640
|
+
"timestamp": "2026-07-10T00:00:01Z",
|
|
641
|
+
"type": "session_meta",
|
|
642
|
+
"payload": map[string]any{
|
|
643
|
+
"id": "child-thread",
|
|
644
|
+
"session_id": "root-thread",
|
|
645
|
+
"thread_source": "subagent",
|
|
646
|
+
"source": map[string]any{
|
|
647
|
+
"subagent": map[string]any{
|
|
648
|
+
"thread_spawn": map[string]any{
|
|
649
|
+
"parent_thread_id": "parent-thread",
|
|
650
|
+
"depth": 2,
|
|
651
|
+
"agent_nickname": "Tesla",
|
|
652
|
+
"agent_role": "explorer",
|
|
653
|
+
},
|
|
654
|
+
},
|
|
655
|
+
},
|
|
656
|
+
},
|
|
657
|
+
})
|
|
658
|
+
|
|
659
|
+
meta, err := (Adapter{}).Summarize(session)
|
|
660
|
+
if err != nil {
|
|
661
|
+
t.Fatal(err)
|
|
662
|
+
}
|
|
663
|
+
if !meta.Auxiliary || meta.Agent == nil {
|
|
664
|
+
t.Fatalf("meta = %#v", meta)
|
|
665
|
+
}
|
|
666
|
+
want := model.AgentSessionMeta{
|
|
667
|
+
SourceID: "child-thread",
|
|
668
|
+
RootSessionID: "root-thread",
|
|
669
|
+
ParentSessionID: "parent-thread",
|
|
670
|
+
Depth: 2,
|
|
671
|
+
Label: "Tesla",
|
|
672
|
+
Role: "explorer",
|
|
673
|
+
}
|
|
674
|
+
if *meta.Agent != want {
|
|
675
|
+
t.Fatalf("agent meta = %#v, want %#v", *meta.Agent, want)
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
func TestSessionMetaPayloadDetectsSubagentFormats(t *testing.T) {
|
|
680
|
+
tests := []struct {
|
|
681
|
+
name string
|
|
682
|
+
payload sessionMetaPayload
|
|
683
|
+
want bool
|
|
684
|
+
}{
|
|
685
|
+
{name: "thread source", payload: sessionMetaPayload{ThreadSource: "subagent"}, want: true},
|
|
686
|
+
{name: "legacy object", payload: sessionMetaPayload{Source: json.RawMessage(`{"subagent":{"thread_spawn":{}}}`)}, want: true},
|
|
687
|
+
{name: "legacy scalar", payload: sessionMetaPayload{Source: json.RawMessage(`{"subagent":"memory_consolidation"}`)}, want: true},
|
|
688
|
+
{name: "null subagent", payload: sessionMetaPayload{Source: json.RawMessage(`{"subagent":null}`)}, want: false},
|
|
689
|
+
{name: "top level source", payload: sessionMetaPayload{Source: json.RawMessage(`"vscode"`)}, want: false},
|
|
690
|
+
{name: "unknown object", payload: sessionMetaPayload{Source: json.RawMessage(`{"other":"user"}`)}, want: false},
|
|
691
|
+
{name: "malformed source", payload: sessionMetaPayload{Source: json.RawMessage(`{`)}, want: false},
|
|
692
|
+
}
|
|
693
|
+
for _, tt := range tests {
|
|
694
|
+
t.Run(tt.name, func(t *testing.T) {
|
|
695
|
+
if got := tt.payload.isSubagent(); got != tt.want {
|
|
696
|
+
t.Fatalf("isSubagent() = %v, want %v", got, tt.want)
|
|
697
|
+
}
|
|
698
|
+
})
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
func call(ts, id, callID, name string, args map[string]any) map[string]any {
|
|
703
|
+
b, _ := json.Marshal(args)
|
|
704
|
+
return callRaw(ts, id, callID, name, string(b))
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
func callRaw(ts, id, callID, name, args string) map[string]any {
|
|
708
|
+
return map[string]any{
|
|
709
|
+
"timestamp": ts,
|
|
710
|
+
"type": "response_item",
|
|
711
|
+
"payload": map[string]any{
|
|
712
|
+
"type": "function_call",
|
|
713
|
+
"id": id,
|
|
714
|
+
"name": name,
|
|
715
|
+
"arguments": args,
|
|
716
|
+
"call_id": callID,
|
|
717
|
+
},
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
func output(ts, callID, text string) map[string]any {
|
|
722
|
+
return map[string]any{
|
|
723
|
+
"timestamp": ts,
|
|
724
|
+
"type": "response_item",
|
|
725
|
+
"payload": map[string]any{
|
|
726
|
+
"type": "function_call_output",
|
|
727
|
+
"call_id": callID,
|
|
728
|
+
"output": text,
|
|
729
|
+
},
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
func customCall(ts, id, callID, name string, input any) map[string]any {
|
|
734
|
+
return map[string]any{
|
|
735
|
+
"timestamp": ts,
|
|
736
|
+
"type": "response_item",
|
|
737
|
+
"payload": map[string]any{
|
|
738
|
+
"type": "custom_tool_call",
|
|
739
|
+
"id": id,
|
|
740
|
+
"name": name,
|
|
741
|
+
"input": input,
|
|
742
|
+
"call_id": callID,
|
|
743
|
+
},
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
func customOutput(ts, callID string, value any) map[string]any {
|
|
748
|
+
return map[string]any{
|
|
749
|
+
"timestamp": ts,
|
|
750
|
+
"type": "response_item",
|
|
751
|
+
"payload": map[string]any{
|
|
752
|
+
"type": "custom_tool_call_output",
|
|
753
|
+
"call_id": callID,
|
|
754
|
+
"output": value,
|
|
755
|
+
},
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
func userMessage(ts, text string) map[string]any {
|
|
760
|
+
return map[string]any{
|
|
761
|
+
"timestamp": ts,
|
|
762
|
+
"type": "response_item",
|
|
763
|
+
"payload": map[string]any{
|
|
764
|
+
"type": "message",
|
|
765
|
+
"role": "user",
|
|
766
|
+
"content": []any{map[string]any{"type": "input_text", "text": text}},
|
|
767
|
+
},
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
func patchApplyEnd(ts, callID string, success *bool, changes map[string]string) map[string]any {
|
|
772
|
+
payloadChanges := make(map[string]any, len(changes))
|
|
773
|
+
for path, changeType := range changes {
|
|
774
|
+
payloadChanges[path] = map[string]any{"type": changeType}
|
|
775
|
+
}
|
|
776
|
+
return map[string]any{
|
|
777
|
+
"timestamp": ts,
|
|
778
|
+
"type": "event_msg",
|
|
779
|
+
"payload": map[string]any{
|
|
780
|
+
"type": "patch_apply_end",
|
|
781
|
+
"call_id": callID,
|
|
782
|
+
"success": success,
|
|
783
|
+
"changes": payloadChanges,
|
|
784
|
+
},
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
func mustJSON(t *testing.T, value any) string {
|
|
789
|
+
t.Helper()
|
|
790
|
+
encoded, err := json.Marshal(value)
|
|
791
|
+
if err != nil {
|
|
792
|
+
t.Fatal(err)
|
|
793
|
+
}
|
|
794
|
+
return string(encoded)
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
func writeJSONL(t *testing.T, path string, values ...any) {
|
|
798
|
+
t.Helper()
|
|
799
|
+
content := ""
|
|
800
|
+
for _, value := range values {
|
|
801
|
+
b, err := json.Marshal(value)
|
|
802
|
+
if err != nil {
|
|
803
|
+
t.Fatal(err)
|
|
804
|
+
}
|
|
805
|
+
content += string(b) + "\n"
|
|
806
|
+
}
|
|
807
|
+
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
|
|
808
|
+
t.Fatal(err)
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
func TestSummarizeMarksJudgeWorkdirAuxiliary(t *testing.T) {
|
|
813
|
+
workdir := judge.WorkDir()
|
|
814
|
+
if workdir == "" {
|
|
815
|
+
t.Skip("no home directory available")
|
|
816
|
+
}
|
|
817
|
+
dir := t.TempDir()
|
|
818
|
+
session := filepath.Join(dir, "rollout-2026-07-14T00-00-00-judge-run.jsonl")
|
|
819
|
+
writeJSONL(t, session,
|
|
820
|
+
map[string]any{
|
|
821
|
+
"timestamp": "2026-07-14T00:00:00Z",
|
|
822
|
+
"type": "session_meta",
|
|
823
|
+
"payload": map[string]any{
|
|
824
|
+
"id": "judge-run",
|
|
825
|
+
"session_id": "judge-run",
|
|
826
|
+
"timestamp": "2026-07-14T00:00:00Z",
|
|
827
|
+
"cwd": workdir,
|
|
828
|
+
},
|
|
829
|
+
},
|
|
830
|
+
)
|
|
831
|
+
adapter := Adapter{Dir: dir}
|
|
832
|
+
meta, err := adapter.Summarize(session)
|
|
833
|
+
if err != nil {
|
|
834
|
+
t.Fatal(err)
|
|
835
|
+
}
|
|
836
|
+
if !meta.Auxiliary {
|
|
837
|
+
t.Fatalf("session recorded in judge workdir %s should be auxiliary", workdir)
|
|
838
|
+
}
|
|
839
|
+
metas, err := adapter.ListSessions()
|
|
840
|
+
if err != nil {
|
|
841
|
+
t.Fatal(err)
|
|
842
|
+
}
|
|
843
|
+
for _, m := range metas {
|
|
844
|
+
if m.ID == "judge-run" {
|
|
845
|
+
t.Fatal("judge session leaked into ListSessions")
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
func TestParseSkipsInjectedUserMessages(t *testing.T) {
|
|
851
|
+
dir := t.TempDir()
|
|
852
|
+
session := filepath.Join(dir, "rollout-2026-07-14T00-00-01-injected.jsonl")
|
|
853
|
+
userMessage := func(text string) map[string]any {
|
|
854
|
+
return map[string]any{
|
|
855
|
+
"timestamp": "2026-07-14T00:00:01Z",
|
|
856
|
+
"type": "response_item",
|
|
857
|
+
"payload": map[string]any{
|
|
858
|
+
"type": "message",
|
|
859
|
+
"role": "user",
|
|
860
|
+
"content": []any{map[string]any{"type": "input_text", "text": text}},
|
|
861
|
+
},
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
writeJSONL(t, session,
|
|
865
|
+
map[string]any{
|
|
866
|
+
"timestamp": "2026-07-14T00:00:00Z",
|
|
867
|
+
"type": "session_meta",
|
|
868
|
+
"payload": map[string]any{"id": "injected", "session_id": "injected", "timestamp": "2026-07-14T00:00:00Z", "cwd": "/repo"},
|
|
869
|
+
},
|
|
870
|
+
userMessage("# AGENTS.md instructions for /repo\n\nproject rules"),
|
|
871
|
+
userMessage("<environment_context>shell: zsh</environment_context>"),
|
|
872
|
+
userMessage("the real task"),
|
|
873
|
+
)
|
|
874
|
+
trace, err := (Adapter{Dir: dir}).Parse(session)
|
|
875
|
+
if err != nil {
|
|
876
|
+
t.Fatal(err)
|
|
877
|
+
}
|
|
878
|
+
var notes []string
|
|
879
|
+
for _, mark := range trace.Marks {
|
|
880
|
+
if mark.Type == "user-message" {
|
|
881
|
+
notes = append(notes, mark.Note)
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
// Injected AGENTS.md and markup wrappers stay out of marks — they would
|
|
885
|
+
// inflate userTurns and pose as the task in judge input.
|
|
886
|
+
if len(notes) != 1 || notes[0] != "the real task" {
|
|
887
|
+
t.Fatalf("user-message notes = %#v", notes)
|
|
888
|
+
}
|
|
889
|
+
if trace.Stats.UserTurns != 1 {
|
|
890
|
+
t.Fatalf("userTurns = %d, want 1", trace.Stats.UserTurns)
|
|
891
|
+
}
|
|
892
|
+
// Summarize must count the same turns as the parse, or the rail badge's
|
|
893
|
+
// staleness check would disagree with the report.
|
|
894
|
+
meta, err := (Adapter{Dir: dir}).Summarize(session)
|
|
895
|
+
if err != nil {
|
|
896
|
+
t.Fatal(err)
|
|
897
|
+
}
|
|
898
|
+
if meta.UserTurns != 1 {
|
|
899
|
+
t.Fatalf("summarized userTurns = %d, want 1", meta.UserTurns)
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
func TestParseRejectsRolelessMessageLines(t *testing.T) {
|
|
904
|
+
// pi sessions also write {"type":"message"} lines but nest the payload
|
|
905
|
+
// under "message" with no top-level role; those files are not Codex's.
|
|
906
|
+
dir := t.TempDir()
|
|
907
|
+
path := filepath.Join(dir, "session.jsonl")
|
|
908
|
+
lines := `{"type":"session","version":3,"id":"s1","timestamp":"2026-07-21T14:34:07.238Z","cwd":"/tmp"}
|
|
909
|
+
{"type":"message","id":"e1","parentId":null,"timestamp":"2026-07-21T14:34:22.963Z","message":{"role":"user","content":"hi"}}
|
|
910
|
+
`
|
|
911
|
+
if err := os.WriteFile(path, []byte(lines), 0o644); err != nil {
|
|
912
|
+
t.Fatal(err)
|
|
913
|
+
}
|
|
914
|
+
if _, err := (Adapter{}).Parse(path); err == nil {
|
|
915
|
+
t.Fatal("Parse claimed a roleless message file")
|
|
916
|
+
}
|
|
917
|
+
if _, err := (Adapter{}).Summarize(path); err == nil {
|
|
918
|
+
t.Fatal("Summarize claimed a roleless message file")
|
|
919
|
+
}
|
|
920
|
+
}
|