@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.
Files changed (102) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +173 -0
  3. package/bin/synapse +0 -0
  4. package/cmd/rubriceval/main.go +308 -0
  5. package/cmd/synapse/main.go +280 -0
  6. package/cmd/synapse/main_test.go +16 -0
  7. package/go.mod +7 -0
  8. package/go.sum +6 -0
  9. package/internal/adapter/adapter.go +1117 -0
  10. package/internal/adapter/adapter_test.go +518 -0
  11. package/internal/adapter/agy/adapter.go +193 -0
  12. package/internal/adapter/claudecode/adapter.go +415 -0
  13. package/internal/adapter/claudecode/adapter_test.go +260 -0
  14. package/internal/adapter/claudecode/agents.go +387 -0
  15. package/internal/adapter/claudecode/agents_test.go +480 -0
  16. package/internal/adapter/claudecode/summary_inputs_test.go +19 -0
  17. package/internal/adapter/codex/adapter.go +919 -0
  18. package/internal/adapter/codex/adapter_test.go +920 -0
  19. package/internal/adapter/codex/agents.go +401 -0
  20. package/internal/adapter/codex/agents_test.go +610 -0
  21. package/internal/adapter/codex/summary_inputs_test.go +20 -0
  22. package/internal/adapter/pi/adapter.go +483 -0
  23. package/internal/adapter/pi/adapter_test.go +517 -0
  24. package/internal/citymap/builder.go +1124 -0
  25. package/internal/citymap/builder_test.go +818 -0
  26. package/internal/judge/cache.go +180 -0
  27. package/internal/judge/cli.go +240 -0
  28. package/internal/judge/cli_test.go +68 -0
  29. package/internal/judge/fresh_summary_test.go +37 -0
  30. package/internal/judge/input.go +233 -0
  31. package/internal/judge/judge.go +416 -0
  32. package/internal/judge/judge_test.go +288 -0
  33. package/internal/judge/prompt.go +129 -0
  34. package/internal/judge/rubric.go +275 -0
  35. package/internal/judge/rubric_test.go +642 -0
  36. package/internal/model/agent.go +63 -0
  37. package/internal/model/agent_schema_test.go +86 -0
  38. package/internal/model/agent_test.go +64 -0
  39. package/internal/model/model.go +175 -0
  40. package/internal/model/report.go +166 -0
  41. package/internal/model/stats.go +151 -0
  42. package/internal/model/stats_test.go +89 -0
  43. package/internal/model/trace_schema_test.go +67 -0
  44. package/internal/server/analyze.go +286 -0
  45. package/internal/server/analyze_test.go +297 -0
  46. package/internal/server/codex_index_test.go +40 -0
  47. package/internal/server/hardening_test.go +147 -0
  48. package/internal/server/reportindex.go +91 -0
  49. package/internal/server/reportindex_test.go +116 -0
  50. package/internal/server/server.go +1099 -0
  51. package/internal/server/server_test.go +1389 -0
  52. package/internal/server/static/assets/fraunces-latin-ext-standard-italic-CGbN9UgK.woff2 +0 -0
  53. package/internal/server/static/assets/fraunces-latin-ext-standard-normal-CJcjJNj7.woff2 +0 -0
  54. package/internal/server/static/assets/fraunces-latin-standard-italic-lSdLDfvT.woff2 +0 -0
  55. package/internal/server/static/assets/fraunces-latin-standard-normal-DihXLNYH.woff2 +0 -0
  56. package/internal/server/static/assets/fraunces-vietnamese-standard-italic-DxWqP7Ku.woff2 +0 -0
  57. package/internal/server/static/assets/fraunces-vietnamese-standard-normal-Czevyj-6.woff2 +0 -0
  58. package/internal/server/static/assets/index-BNoY_BiB.css +1 -0
  59. package/internal/server/static/assets/index-C_adLrJr.js +3 -0
  60. package/internal/server/static/assets/react-gcHzaSmV.js +10 -0
  61. package/internal/server/static/assets/schibsted-grotesk-latin-ext-wght-normal-hsMS0n0O.woff2 +0 -0
  62. package/internal/server/static/assets/schibsted-grotesk-latin-wght-normal-Bb8VGrTG.woff2 +0 -0
  63. package/internal/server/static/assets/three-DnGjZfD1.js +4012 -0
  64. package/internal/server/static/index.html +26 -0
  65. package/internal/server/tracestore.go +173 -0
  66. package/internal/server/tracestore_test.go +51 -0
  67. package/internal/textutil/truncate.go +30 -0
  68. package/internal/textutil/truncate_test.go +39 -0
  69. package/package.json +35 -0
  70. package/web/e2e/agent-lens.spec.ts +688 -0
  71. package/web/index.html +23 -0
  72. package/web/package-lock.json +1933 -0
  73. package/web/package.json +33 -0
  74. package/web/playwright.config.ts +24 -0
  75. package/web/src/App.tsx +876 -0
  76. package/web/src/api/client.ts +74 -0
  77. package/web/src/main.tsx +12 -0
  78. package/web/src/playback/recorder.ts +160 -0
  79. package/web/src/playback/reducer.ts +91 -0
  80. package/web/src/scene/CityScene.tsx +638 -0
  81. package/web/src/scene/TreeScene.tsx +656 -0
  82. package/web/src/scene/dirLabels.ts +145 -0
  83. package/web/src/scene/sceneUtils.ts +144 -0
  84. package/web/src/scene/textures.ts +60 -0
  85. package/web/src/scene/trail.ts +79 -0
  86. package/web/src/scene/treeLayout.ts +169 -0
  87. package/web/src/state/filters.ts +40 -0
  88. package/web/src/state/store.ts +83 -0
  89. package/web/src/styles.css +2565 -0
  90. package/web/src/types.ts +315 -0
  91. package/web/src/ui/AgentsPanel.tsx +376 -0
  92. package/web/src/ui/Dock.tsx +104 -0
  93. package/web/src/ui/Hud.tsx +335 -0
  94. package/web/src/ui/Inspector.tsx +107 -0
  95. package/web/src/ui/LogoMark.tsx +38 -0
  96. package/web/src/ui/ReportPanel.tsx +491 -0
  97. package/web/src/ui/SessionRail.tsx +316 -0
  98. package/web/src/ui/Timeline.tsx +458 -0
  99. package/web/src/ui/ViewPanel.tsx +45 -0
  100. package/web/src/ui/shortcuts.ts +4 -0
  101. package/web/tsconfig.json +21 -0
  102. package/web/vite.config.ts +28 -0
@@ -0,0 +1,480 @@
1
+ package claudecode
2
+
3
+ import (
4
+ "encoding/json"
5
+ "os"
6
+ "path/filepath"
7
+ "testing"
8
+
9
+ baseadapter "github.com/hybridlabor-api/bdb-synapse/internal/adapter"
10
+ "github.com/hybridlabor-api/bdb-synapse/internal/model"
11
+ )
12
+
13
+ func TestClaudeAgentGraphExactUsesToolUseIDForImmediateParent(t *testing.T) {
14
+ fixture := newClaudeAgentFixture(t, []any{
15
+ claudeToolUse("root-id", "call-read", "Read", map[string]any{"file_path": "/tmp/a.go"}),
16
+ claudeToolResult("root-id", "call-read", false),
17
+ claudeToolUse("root-id", "call-child", "Agent", map[string]any{
18
+ "description": "Launch description",
19
+ "subagent_type": "Explore",
20
+ "prompt": " inspect\n the code ",
21
+ }),
22
+ claudeToolResult("root-id", "call-child", false),
23
+ })
24
+ child := fixture.addChild(t, "agent-child", []any{
25
+ claudeSidechainUser("root-id", "child-id", "child prompt"),
26
+ claudeToolUseForAgent("root-id", "child-id", "call-grand", "Task", map[string]any{
27
+ "description": "Grand launch",
28
+ "subagent_type": "Plan",
29
+ "prompt": "plan the fix",
30
+ }),
31
+ claudeToolResultForAgent("root-id", "child-id", "call-grand", false),
32
+ }, claudeChildMeta{AgentType: "Explore", Description: "Child", ToolUseID: "call-child", SpawnDepth: 1})
33
+ grand := fixture.addChild(t, "agent-grand", []any{
34
+ claudeSidechainUser("root-id", "grand-id", "grand prompt"),
35
+ }, claudeChildMeta{AgentType: "Plan", Description: "Grandchild", ToolUseID: "call-grand", SpawnDepth: 2})
36
+
37
+ graph := fixture.build(t)
38
+ if graph.Version != model.AgentGraphVersion || graph.RootSessionKey != fixture.root.Key {
39
+ t.Fatalf("graph header = %#v", graph)
40
+ }
41
+ if len(graph.Agents) != 3 || graph.Agents[0] != claudeMainNode(fixture.root) {
42
+ t.Fatalf("agents = %#v", graph.Agents)
43
+ }
44
+ childNode := findClaudeAgent(t, graph, "Child")
45
+ childID := claudeAgentID(fixture.root, "claude-tool:call-child")
46
+ assertClaudeAgent(t, childNode, model.AgentNode{
47
+ ID: childID,
48
+ ParentID: claudeMainID(fixture.root),
49
+ Depth: 1,
50
+ Kind: model.AgentKindSubagent,
51
+ Label: "Child",
52
+ Role: "Explore",
53
+ InstructionPreview: "inspect the code",
54
+ LaunchSeq: intPointer(1),
55
+ LaunchCallID: "call-child",
56
+ Status: model.AgentStatusLaunched,
57
+ TraceAvailability: model.TraceAvailabilityAvailable,
58
+ TraceSessionKey: child.Key,
59
+ TraceEventCount: 1,
60
+ LinkQuality: model.AgentLinkQualityExact,
61
+ LinkMethod: model.AgentLinkMethodClaudeToolUseID,
62
+ })
63
+ assertClaudeAgent(t, findClaudeAgent(t, graph, "Grandchild"), model.AgentNode{
64
+ ID: claudeAgentID(fixture.root, "claude-tool:call-grand"),
65
+ ParentID: childID,
66
+ Depth: 2,
67
+ Kind: model.AgentKindSubagent,
68
+ Label: "Grandchild",
69
+ Role: "Plan",
70
+ InstructionPreview: "plan the fix",
71
+ LaunchSeq: intPointer(0),
72
+ LaunchCallID: "call-grand",
73
+ Status: model.AgentStatusLaunched,
74
+ TraceAvailability: model.TraceAvailabilityAvailable,
75
+ TraceSessionKey: grand.Key,
76
+ TraceEventCount: 0,
77
+ LinkQuality: model.AgentLinkQualityExact,
78
+ LinkMethod: model.AgentLinkMethodClaudeToolUseID,
79
+ })
80
+ }
81
+
82
+ func TestClaudeAgentGraphDerivedArtifactWithoutToolUseIDIsLaunched(t *testing.T) {
83
+ fixture := newClaudeAgentFixture(t, nil)
84
+ child := fixture.addChild(t, "agent-derived", []any{
85
+ claudeSidechainUser("root-id", "derived-id", "inspect"),
86
+ }, claudeChildMeta{AgentType: "Explore", Description: "Derived", SpawnDepth: 1})
87
+
88
+ node := findClaudeAgent(t, fixture.build(t), "Derived")
89
+ assertClaudeAgent(t, node, model.AgentNode{
90
+ ID: claudeAgentID(fixture.root, "session:"+child.Key),
91
+ ParentID: claudeMainID(fixture.root),
92
+ Depth: 1,
93
+ Kind: model.AgentKindSubagent,
94
+ Label: "Derived",
95
+ Role: "Explore",
96
+ Status: model.AgentStatusLaunched,
97
+ TraceAvailability: model.TraceAvailabilityAvailable,
98
+ TraceSessionKey: child.Key,
99
+ TraceEventCount: 0,
100
+ LinkQuality: model.AgentLinkQualityDerived,
101
+ LinkMethod: model.AgentLinkMethodClaudeSubagentsDirectory,
102
+ })
103
+ }
104
+
105
+ func TestClaudeAgentGraphUnmatchedMetadataAttachesToMain(t *testing.T) {
106
+ fixture := newClaudeAgentFixture(t, []any{
107
+ claudeToolUse("root-id", "call-other", "Agent", map[string]any{"prompt": "other"}),
108
+ })
109
+ child := fixture.addChild(t, "agent-orphan", []any{
110
+ claudeSidechainUser("root-id", "orphan-id", "inspect"),
111
+ }, claudeChildMeta{AgentType: "Explore", Description: "Orphan", ToolUseID: "call-unmatched", SpawnDepth: 2})
112
+
113
+ node := findClaudeAgent(t, fixture.build(t), "Orphan")
114
+ assertClaudeAgent(t, node, model.AgentNode{
115
+ ID: claudeAgentID(fixture.root, "claude-tool:call-unmatched"),
116
+ ParentID: claudeMainID(fixture.root),
117
+ Depth: 2,
118
+ Kind: model.AgentKindSubagent,
119
+ Label: "Orphan",
120
+ Role: "Explore",
121
+ Status: model.AgentStatusLaunched,
122
+ TraceAvailability: model.TraceAvailabilityAvailable,
123
+ TraceSessionKey: child.Key,
124
+ TraceEventCount: 0,
125
+ LinkQuality: model.AgentLinkQualityDerived,
126
+ LinkMethod: model.AgentLinkMethodClaudeSubagentsDirectory,
127
+ })
128
+ }
129
+
130
+ func TestClaudeAgentGraphMetadataOnlyChildIsMissing(t *testing.T) {
131
+ fixture := newClaudeAgentFixture(t, []any{
132
+ claudeToolUse("root-id", "call-missing", "Task", map[string]any{
133
+ "description": "Missing launch",
134
+ "subagent_type": "Explore",
135
+ "prompt": "find the missing trace",
136
+ }),
137
+ })
138
+ fixture.addMetaOnly(t, "agent-missing", claudeChildMeta{
139
+ AgentType: "Explore", Description: "Missing", ToolUseID: "call-missing", SpawnDepth: 1,
140
+ })
141
+
142
+ node := findClaudeAgent(t, fixture.build(t), "Missing")
143
+ assertClaudeAgent(t, node, model.AgentNode{
144
+ ID: claudeAgentID(fixture.root, "claude-tool:call-missing"),
145
+ ParentID: claudeMainID(fixture.root),
146
+ Depth: 1,
147
+ Kind: model.AgentKindSubagent,
148
+ Label: "Missing",
149
+ Role: "Explore",
150
+ InstructionPreview: "find the missing trace",
151
+ LaunchSeq: intPointer(0),
152
+ LaunchCallID: "call-missing",
153
+ Status: model.AgentStatusLaunched,
154
+ TraceAvailability: model.TraceAvailabilityMissing,
155
+ LinkQuality: model.AgentLinkQualityExact,
156
+ LinkMethod: model.AgentLinkMethodClaudeToolUseID,
157
+ })
158
+ }
159
+
160
+ func TestClaudeAgentGraphZeroEventChildIsAvailable(t *testing.T) {
161
+ fixture := newClaudeAgentFixture(t, []any{
162
+ claudeToolUse("root-id", "call-zero", "Agent", map[string]any{"prompt": "zero events"}),
163
+ })
164
+ child := fixture.addChild(t, "agent-zero", []any{
165
+ map[string]any{"type": "session_meta", "sessionId": "root-id", "agentId": "zero-id", "isSidechain": true},
166
+ claudeSidechainUser("root-id", "zero-id", "user only"),
167
+ }, claudeChildMeta{Description: "Zero", ToolUseID: "call-zero", SpawnDepth: 1})
168
+
169
+ node := findClaudeAgent(t, fixture.build(t), "Zero")
170
+ if node.TraceAvailability != model.TraceAvailabilityAvailable || node.TraceSessionKey != child.Key || node.TraceEventCount != 0 {
171
+ t.Fatalf("zero-event node = %#v", node)
172
+ }
173
+ }
174
+
175
+ func TestClaudeAgentGraphErrorResultIsFailed(t *testing.T) {
176
+ fixture := newClaudeAgentFixture(t, []any{
177
+ claudeToolUse("root-id", "call-failed", "Task", map[string]any{
178
+ "description": "Failed launch", "subagent_type": "Plan", "prompt": "try it",
179
+ }),
180
+ claudeToolResult("root-id", "call-failed", true),
181
+ })
182
+
183
+ node := findClaudeAgent(t, fixture.build(t), "Failed launch")
184
+ assertClaudeAgent(t, node, model.AgentNode{
185
+ ID: claudeAgentID(fixture.root, "launch:"+claudeMainID(fixture.root)+":call-failed"),
186
+ ParentID: claudeMainID(fixture.root),
187
+ Depth: 1,
188
+ Kind: model.AgentKindSubagent,
189
+ Label: "Failed launch",
190
+ Role: "Plan",
191
+ InstructionPreview: "try it",
192
+ LaunchSeq: intPointer(0),
193
+ LaunchCallID: "call-failed",
194
+ Status: model.AgentStatusFailed,
195
+ TraceAvailability: model.TraceAvailabilityUnavailable,
196
+ LinkQuality: model.AgentLinkQualityUnavailable,
197
+ LinkMethod: model.AgentLinkMethodUnavailable,
198
+ })
199
+ }
200
+
201
+ func TestClaudeAgentGraphSuccessfulResultWithoutArtifactStaysUnknown(t *testing.T) {
202
+ fixture := newClaudeAgentFixture(t, []any{
203
+ claudeToolUse("root-id", "call-unknown", "Agent", map[string]any{
204
+ "description": "Unknown launch", "subagent_type": "Explore", "prompt": "try it",
205
+ }),
206
+ claudeToolResult("root-id", "call-unknown", false),
207
+ })
208
+
209
+ node := findClaudeAgent(t, fixture.build(t), "Unknown launch")
210
+ if node.Status != model.AgentStatusUnknown || node.TraceAvailability != model.TraceAvailabilityUnavailable ||
211
+ node.LinkQuality != model.AgentLinkQualityUnavailable || node.LinkMethod != model.AgentLinkMethodUnavailable {
212
+ t.Fatalf("successful result node = %#v", node)
213
+ }
214
+ }
215
+
216
+ func TestClaudeAgentGraphDuplicateCallIDProducesOneNode(t *testing.T) {
217
+ fixture := newClaudeAgentFixture(t, []any{
218
+ claudeToolUse("root-id", "call-shared", "Agent", map[string]any{
219
+ "description": "Shared", "subagent_type": "Explore", "prompt": "inspect",
220
+ }),
221
+ })
222
+ fixture.addChild(t, "agent-shared", []any{
223
+ claudeSidechainUser("root-id", "shared-id", "child prompt"),
224
+ claudeToolUseForAgent("root-id", "shared-id", "call-shared", "Agent", map[string]any{
225
+ "description": "Duplicate transcript entry", "prompt": "inspect",
226
+ }),
227
+ }, claudeChildMeta{Description: "Shared", ToolUseID: "call-shared", SpawnDepth: 1})
228
+
229
+ graph := fixture.build(t)
230
+ if len(graph.Agents) != 2 {
231
+ t.Fatalf("agents = %#v, want Main plus one shared-call node", graph.Agents)
232
+ }
233
+ if graph.Agents[1].Label != "Shared" || graph.Agents[1].TraceAvailability != model.TraceAvailabilityAvailable {
234
+ t.Fatalf("shared node = %#v", graph.Agents[1])
235
+ }
236
+ }
237
+
238
+ func TestClaudeAgentGraphUsesStablePreorder(t *testing.T) {
239
+ fixture := newClaudeAgentFixture(t, []any{
240
+ claudeToolUse("root-id", "call-a", "Agent", map[string]any{"description": "A", "prompt": "A"}),
241
+ claudeToolUse("root-id", "call-b", "Agent", map[string]any{"description": "B", "prompt": "B"}),
242
+ })
243
+ fixture.addChild(t, "agent-a", []any{
244
+ claudeSidechainUser("root-id", "a-id", "A child"),
245
+ claudeToolUseForAgent("root-id", "a-id", "call-a1", "Agent", map[string]any{
246
+ "description": "A1", "prompt": "A1",
247
+ }),
248
+ }, claudeChildMeta{Description: "A", ToolUseID: "call-a", SpawnDepth: 1})
249
+ fixture.addChild(t, "agent-b", []any{
250
+ claudeSidechainUser("root-id", "b-id", "B child"),
251
+ }, claudeChildMeta{Description: "B", ToolUseID: "call-b", SpawnDepth: 1})
252
+ fixture.addChild(t, "agent-a1", []any{
253
+ claudeSidechainUser("root-id", "a1-id", "A1 child"),
254
+ }, claudeChildMeta{Description: "A1", ToolUseID: "call-a1", SpawnDepth: 2})
255
+
256
+ graph := fixture.build(t)
257
+ labels := make([]string, len(graph.Agents))
258
+ for i, node := range graph.Agents {
259
+ labels[i] = node.Label
260
+ }
261
+ want := []string{"Main", "A", "A1", "B"}
262
+ if len(labels) != len(want) {
263
+ t.Fatalf("agent order = %v, want %v", labels, want)
264
+ }
265
+ for i := range want {
266
+ if labels[i] != want[i] {
267
+ t.Fatalf("agent order = %v, want %v", labels, want)
268
+ }
269
+ }
270
+ }
271
+
272
+ func TestClaudeAgentGraphInputsIncludeChildTracesAndSidecars(t *testing.T) {
273
+ fixture := newClaudeAgentFixture(t, nil)
274
+ child := fixture.addChild(t, "agent-child", []any{
275
+ claudeSidechainUser("root-id", "child-id", "child prompt"),
276
+ }, claudeChildMeta{Description: "Child", ToolUseID: "call-child", SpawnDepth: 1})
277
+ fixture.addMetaOnly(t, "agent-missing", claudeChildMeta{Description: "Missing", ToolUseID: "call-missing"})
278
+
279
+ inputs, err := fixture.adapter.AgentGraphInputs(fixture.root, fixture.catalog)
280
+ if err != nil {
281
+ t.Fatal(err)
282
+ }
283
+ want := map[string]bool{
284
+ fixture.root.Path: true,
285
+ child.Path: true,
286
+ filepath.Join(fixture.subagentsDir, "agent-child.meta.json"): true,
287
+ filepath.Join(fixture.subagentsDir, "agent-missing.meta.json"): true,
288
+ }
289
+ if len(inputs) != len(want) {
290
+ t.Fatalf("graph inputs = %v, want %v", inputs, want)
291
+ }
292
+ for _, path := range inputs {
293
+ if !want[path] {
294
+ t.Fatalf("unexpected graph input %q in %v", path, inputs)
295
+ }
296
+ }
297
+ }
298
+
299
+ type claudeAgentFixture struct {
300
+ adapter Adapter
301
+ root model.SessionMeta
302
+ catalog []model.SessionMeta
303
+ subagentsDir string
304
+ }
305
+
306
+ type claudeChildMeta struct {
307
+ AgentType string `json:"agentType,omitempty"`
308
+ Description string `json:"description,omitempty"`
309
+ ToolUseID string `json:"toolUseId,omitempty"`
310
+ SpawnDepth int `json:"spawnDepth,omitempty"`
311
+ }
312
+
313
+ func newClaudeAgentFixture(t *testing.T, rootLines []any) *claudeAgentFixture {
314
+ t.Helper()
315
+ dir := t.TempDir()
316
+ a := Adapter{Dir: dir}
317
+ rootPath := filepath.Join(dir, "root-id.jsonl")
318
+ lines := append([]any{claudeUser("root-id", "root prompt")}, rootLines...)
319
+ writeClaudeAgentJSONL(t, rootPath, lines...)
320
+ root, err := a.Summarize(rootPath)
321
+ if err != nil {
322
+ t.Fatal(err)
323
+ }
324
+ subagentsDir := filepath.Join(dir, "root-id", "subagents")
325
+ if err := os.MkdirAll(subagentsDir, 0o755); err != nil {
326
+ t.Fatal(err)
327
+ }
328
+ return &claudeAgentFixture{adapter: a, root: root, subagentsDir: subagentsDir}
329
+ }
330
+
331
+ func (f *claudeAgentFixture) addChild(t *testing.T, basename string, lines []any, meta claudeChildMeta) model.SessionMeta {
332
+ t.Helper()
333
+ path := filepath.Join(f.subagentsDir, basename+".jsonl")
334
+ writeClaudeAgentJSONL(t, path, lines...)
335
+ f.writeMeta(t, basename, meta)
336
+ session, err := f.adapter.Summarize(path)
337
+ if err != nil {
338
+ t.Fatal(err)
339
+ }
340
+ f.catalog = append(f.catalog, session)
341
+ return session
342
+ }
343
+
344
+ func (f *claudeAgentFixture) addMetaOnly(t *testing.T, basename string, meta claudeChildMeta) {
345
+ t.Helper()
346
+ f.writeMeta(t, basename, meta)
347
+ }
348
+
349
+ func (f *claudeAgentFixture) writeMeta(t *testing.T, basename string, meta claudeChildMeta) {
350
+ t.Helper()
351
+ data, err := json.Marshal(meta)
352
+ if err != nil {
353
+ t.Fatal(err)
354
+ }
355
+ if err := os.WriteFile(filepath.Join(f.subagentsDir, basename+".meta.json"), data, 0o644); err != nil {
356
+ t.Fatal(err)
357
+ }
358
+ }
359
+
360
+ func (f *claudeAgentFixture) build(t *testing.T) *model.AgentGraph {
361
+ t.Helper()
362
+ graph, err := f.adapter.BuildAgentGraph(f.root, f.catalog)
363
+ if err != nil {
364
+ t.Fatal(err)
365
+ }
366
+ return graph
367
+ }
368
+
369
+ func claudeUser(sessionID, text string) map[string]any {
370
+ return map[string]any{
371
+ "type": "user", "timestamp": "2026-07-15T00:00:00Z", "sessionId": sessionID,
372
+ "message": map[string]any{"role": "user", "content": text},
373
+ }
374
+ }
375
+
376
+ func claudeSidechainUser(sessionID, agentID, text string) map[string]any {
377
+ line := claudeUser(sessionID, text)
378
+ line["agentId"] = agentID
379
+ line["isSidechain"] = true
380
+ return line
381
+ }
382
+
383
+ func claudeToolUse(sessionID, id, name string, input map[string]any) map[string]any {
384
+ return claudeMessageItem(sessionID, "", "assistant", map[string]any{
385
+ "type": "tool_use", "id": id, "name": name, "input": input,
386
+ })
387
+ }
388
+
389
+ func claudeToolUseForAgent(sessionID, agentID, id, name string, input map[string]any) map[string]any {
390
+ return claudeMessageItem(sessionID, agentID, "assistant", map[string]any{
391
+ "type": "tool_use", "id": id, "name": name, "input": input,
392
+ })
393
+ }
394
+
395
+ func claudeToolResult(sessionID, toolUseID string, isError bool) map[string]any {
396
+ return claudeMessageItem(sessionID, "", "user", map[string]any{
397
+ "type": "tool_result", "tool_use_id": toolUseID, "content": "result", "is_error": isError,
398
+ })
399
+ }
400
+
401
+ func claudeToolResultForAgent(sessionID, agentID, toolUseID string, isError bool) map[string]any {
402
+ return claudeMessageItem(sessionID, agentID, "user", map[string]any{
403
+ "type": "tool_result", "tool_use_id": toolUseID, "content": "result", "is_error": isError,
404
+ })
405
+ }
406
+
407
+ func claudeMessageItem(sessionID, agentID, role string, item map[string]any) map[string]any {
408
+ line := map[string]any{
409
+ "type": role, "timestamp": "2026-07-15T00:00:01Z", "sessionId": sessionID,
410
+ "message": map[string]any{"role": role, "content": []any{item}},
411
+ }
412
+ if agentID != "" {
413
+ line["agentId"] = agentID
414
+ line["isSidechain"] = true
415
+ }
416
+ return line
417
+ }
418
+
419
+ func writeClaudeAgentJSONL(t *testing.T, path string, values ...any) {
420
+ t.Helper()
421
+ var content []byte
422
+ for _, value := range values {
423
+ line, err := json.Marshal(value)
424
+ if err != nil {
425
+ t.Fatal(err)
426
+ }
427
+ content = append(content, line...)
428
+ content = append(content, '\n')
429
+ }
430
+ if err := os.WriteFile(path, content, 0o644); err != nil {
431
+ t.Fatal(err)
432
+ }
433
+ }
434
+
435
+ func findClaudeAgent(t *testing.T, graph *model.AgentGraph, label string) model.AgentNode {
436
+ t.Helper()
437
+ for _, node := range graph.Agents {
438
+ if node.Label == label {
439
+ return node
440
+ }
441
+ }
442
+ t.Fatalf("agent %q not found in %#v", label, graph.Agents)
443
+ return model.AgentNode{}
444
+ }
445
+
446
+ func assertClaudeAgent(t *testing.T, got, want model.AgentNode) {
447
+ t.Helper()
448
+ gotJSON, _ := json.Marshal(got)
449
+ wantJSON, _ := json.Marshal(want)
450
+ if string(gotJSON) != string(wantJSON) {
451
+ t.Fatalf("agent:\n got: %s\nwant: %s", gotJSON, wantJSON)
452
+ }
453
+ }
454
+
455
+ func claudeMainNode(root model.SessionMeta) model.AgentNode {
456
+ return model.AgentNode{
457
+ ID: claudeMainID(root),
458
+ Depth: 0,
459
+ Kind: model.AgentKindMain,
460
+ Label: "Main",
461
+ Status: model.AgentStatusMain,
462
+ TraceAvailability: model.TraceAvailabilityAvailable,
463
+ TraceSessionKey: root.Key,
464
+ TraceEventCount: root.EventCount,
465
+ LinkQuality: model.AgentLinkQualityExact,
466
+ LinkMethod: model.AgentLinkMethodRoot,
467
+ }
468
+ }
469
+
470
+ func claudeMainID(root model.SessionMeta) string {
471
+ return claudeAgentID(root, "root:"+root.Key)
472
+ }
473
+
474
+ func claudeAgentID(root model.SessionMeta, identity string) string {
475
+ return baseadapter.AgentNodeID((Adapter{}).Harness(), root.Key, identity)
476
+ }
477
+
478
+ func intPointer(value int) *int {
479
+ return &value
480
+ }
@@ -0,0 +1,19 @@
1
+ package claudecode
2
+
3
+ import (
4
+ "path/filepath"
5
+ "testing"
6
+ )
7
+
8
+ func TestSummaryInputsDeclaresAgentSidecarOnly(t *testing.T) {
9
+ a := Adapter{}
10
+ child := filepath.Join("proj", "agent-abc.jsonl")
11
+ inputs := a.SummaryInputs(child)
12
+ want := filepath.Join("proj", "agent-abc.meta.json")
13
+ if len(inputs) != 1 || inputs[0] != want {
14
+ t.Fatalf("SummaryInputs(%q) = %v, want [%s]", child, inputs, want)
15
+ }
16
+ if inputs := a.SummaryInputs(filepath.Join("proj", "root.jsonl")); inputs != nil {
17
+ t.Fatalf("main session declared sidecars: %v", inputs)
18
+ }
19
+ }