@dxos/react-ui-trace 0.11.1

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 (148) hide show
  1. package/LICENSE +105 -0
  2. package/README.md +3 -0
  3. package/dist/lib/index.mjs +2868 -0
  4. package/dist/lib/index.mjs.map +1 -0
  5. package/dist/lib/testing.mjs +338 -0
  6. package/dist/lib/testing.mjs.map +1 -0
  7. package/dist/lib/translations.mjs +19 -0
  8. package/dist/lib/translations.mjs.map +1 -0
  9. package/dist/types/src/components/Gantt/Gantt.d.ts +68 -0
  10. package/dist/types/src/components/Gantt/Gantt.d.ts.map +1 -0
  11. package/dist/types/src/components/Gantt/Gantt.stories.d.ts +30 -0
  12. package/dist/types/src/components/Gantt/Gantt.stories.d.ts.map +1 -0
  13. package/dist/types/src/components/Gantt/index.d.ts +2 -0
  14. package/dist/types/src/components/Gantt/index.d.ts.map +1 -0
  15. package/dist/types/src/components/ProcessTree/ProcessTree.d.ts +32 -0
  16. package/dist/types/src/components/ProcessTree/ProcessTree.d.ts.map +1 -0
  17. package/dist/types/src/components/ProcessTree/ProcessTree.stories.d.ts +8 -0
  18. package/dist/types/src/components/ProcessTree/ProcessTree.stories.d.ts.map +1 -0
  19. package/dist/types/src/components/ProcessTree/index.d.ts +2 -0
  20. package/dist/types/src/components/ProcessTree/index.d.ts.map +1 -0
  21. package/dist/types/src/components/Timeline/Timeline.d.ts +50 -0
  22. package/dist/types/src/components/Timeline/Timeline.d.ts.map +1 -0
  23. package/dist/types/src/components/Timeline/Timeline.stories.d.ts +24 -0
  24. package/dist/types/src/components/Timeline/Timeline.stories.d.ts.map +1 -0
  25. package/dist/types/src/components/Timeline/index.d.ts +4 -0
  26. package/dist/types/src/components/Timeline/index.d.ts.map +1 -0
  27. package/dist/types/src/components/Timeline/timeline-options.d.ts +9 -0
  28. package/dist/types/src/components/Timeline/timeline-options.d.ts.map +1 -0
  29. package/dist/types/src/components/Timeline/timeline-printer.d.ts +7 -0
  30. package/dist/types/src/components/Timeline/timeline-printer.d.ts.map +1 -0
  31. package/dist/types/src/components/Timeline/timeline-printer.test.d.ts +2 -0
  32. package/dist/types/src/components/Timeline/timeline-printer.test.d.ts.map +1 -0
  33. package/dist/types/src/components/TracePanel/TracePanel.d.ts +28 -0
  34. package/dist/types/src/components/TracePanel/TracePanel.d.ts.map +1 -0
  35. package/dist/types/src/components/TracePanel/dxn-extractor.d.ts +19 -0
  36. package/dist/types/src/components/TracePanel/dxn-extractor.d.ts.map +1 -0
  37. package/dist/types/src/components/TracePanel/dxn-extractor.test.d.ts +2 -0
  38. package/dist/types/src/components/TracePanel/dxn-extractor.test.d.ts.map +1 -0
  39. package/dist/types/src/components/TracePanel/index.d.ts +4 -0
  40. package/dist/types/src/components/TracePanel/index.d.ts.map +1 -0
  41. package/dist/types/src/components/TracePanel/trace-filter.d.ts +39 -0
  42. package/dist/types/src/components/TracePanel/trace-filter.d.ts.map +1 -0
  43. package/dist/types/src/components/TracePanel/trace-filter.test.d.ts +2 -0
  44. package/dist/types/src/components/TracePanel/trace-filter.test.d.ts.map +1 -0
  45. package/dist/types/src/components/TracePanel/useTraceMenu.d.ts +15 -0
  46. package/dist/types/src/components/TracePanel/useTraceMenu.d.ts.map +1 -0
  47. package/dist/types/src/components/index.d.ts +5 -0
  48. package/dist/types/src/components/index.d.ts.map +1 -0
  49. package/dist/types/src/execution-graph/execution-graph.d.ts +139 -0
  50. package/dist/types/src/execution-graph/execution-graph.d.ts.map +1 -0
  51. package/dist/types/src/execution-graph/execution-graph.test.d.ts +2 -0
  52. package/dist/types/src/execution-graph/execution-graph.test.d.ts.map +1 -0
  53. package/dist/types/src/execution-graph/index.d.ts +4 -0
  54. package/dist/types/src/execution-graph/index.d.ts.map +1 -0
  55. package/dist/types/src/execution-graph/pending-block-status.d.ts +26 -0
  56. package/dist/types/src/execution-graph/pending-block-status.d.ts.map +1 -0
  57. package/dist/types/src/execution-graph/pending-block-status.test.d.ts +2 -0
  58. package/dist/types/src/execution-graph/pending-block-status.test.d.ts.map +1 -0
  59. package/dist/types/src/execution-graph/remote-snapshot.test.d.ts +2 -0
  60. package/dist/types/src/execution-graph/remote-snapshot.test.d.ts.map +1 -0
  61. package/dist/types/src/execution-graph/span-tree.d.ts +106 -0
  62. package/dist/types/src/execution-graph/span-tree.d.ts.map +1 -0
  63. package/dist/types/src/execution-graph/span-tree.test.d.ts +2 -0
  64. package/dist/types/src/execution-graph/span-tree.test.d.ts.map +1 -0
  65. package/dist/types/src/execution-graph/sub-agent-delegation.test.d.ts +2 -0
  66. package/dist/types/src/execution-graph/sub-agent-delegation.test.d.ts.map +1 -0
  67. package/dist/types/src/execution-graph/testing/collect-trace-events.d.ts +40 -0
  68. package/dist/types/src/execution-graph/testing/collect-trace-events.d.ts.map +1 -0
  69. package/dist/types/src/execution-graph/testing/index.d.ts +2 -0
  70. package/dist/types/src/execution-graph/testing/index.d.ts.map +1 -0
  71. package/dist/types/src/hooks/index.d.ts +4 -0
  72. package/dist/types/src/hooks/index.d.ts.map +1 -0
  73. package/dist/types/src/hooks/useExecutionGraph.d.ts +17 -0
  74. package/dist/types/src/hooks/useExecutionGraph.d.ts.map +1 -0
  75. package/dist/types/src/hooks/useSessionTimeline.d.ts +17 -0
  76. package/dist/types/src/hooks/useSessionTimeline.d.ts.map +1 -0
  77. package/dist/types/src/hooks/useTraceMessages.d.ts +17 -0
  78. package/dist/types/src/hooks/useTraceMessages.d.ts.map +1 -0
  79. package/dist/types/src/index.d.ts +5 -0
  80. package/dist/types/src/index.d.ts.map +1 -0
  81. package/dist/types/src/session-timeline/index.d.ts +3 -0
  82. package/dist/types/src/session-timeline/index.d.ts.map +1 -0
  83. package/dist/types/src/session-timeline/session-timeline.d.ts +19 -0
  84. package/dist/types/src/session-timeline/session-timeline.d.ts.map +1 -0
  85. package/dist/types/src/session-timeline/session-timeline.test.d.ts +2 -0
  86. package/dist/types/src/session-timeline/session-timeline.test.d.ts.map +1 -0
  87. package/dist/types/src/session-timeline/types.d.ts +129 -0
  88. package/dist/types/src/session-timeline/types.d.ts.map +1 -0
  89. package/dist/types/src/testing/index.d.ts +5 -0
  90. package/dist/types/src/testing/index.d.ts.map +1 -0
  91. package/dist/types/src/testing/simulated-agent.d.ts +63 -0
  92. package/dist/types/src/testing/simulated-agent.d.ts.map +1 -0
  93. package/dist/types/src/testing/snapshot-playback.d.ts +12 -0
  94. package/dist/types/src/testing/snapshot-playback.d.ts.map +1 -0
  95. package/dist/types/src/testing/test-trace.d.ts +5 -0
  96. package/dist/types/src/testing/test-trace.d.ts.map +1 -0
  97. package/dist/types/src/translations.d.ts +19 -0
  98. package/dist/types/src/translations.d.ts.map +1 -0
  99. package/dist/types/tsconfig.tsbuildinfo +1 -0
  100. package/package.json +77 -0
  101. package/src/components/Gantt/Gantt.stories.tsx +241 -0
  102. package/src/components/Gantt/Gantt.tsx +589 -0
  103. package/src/components/Gantt/index.ts +5 -0
  104. package/src/components/ProcessTree/ProcessTree.stories.tsx +95 -0
  105. package/src/components/ProcessTree/ProcessTree.tsx +265 -0
  106. package/src/components/ProcessTree/index.ts +5 -0
  107. package/src/components/Timeline/Timeline.stories.tsx +326 -0
  108. package/src/components/Timeline/Timeline.tsx +534 -0
  109. package/src/components/Timeline/git.drawio.svg +821 -0
  110. package/src/components/Timeline/index.ts +7 -0
  111. package/src/components/Timeline/timeline-options.ts +27 -0
  112. package/src/components/Timeline/timeline-printer.test.ts +76 -0
  113. package/src/components/Timeline/timeline-printer.ts +223 -0
  114. package/src/components/TracePanel/TracePanel.tsx +248 -0
  115. package/src/components/TracePanel/dxn-extractor.test.ts +153 -0
  116. package/src/components/TracePanel/dxn-extractor.ts +166 -0
  117. package/src/components/TracePanel/index.ts +7 -0
  118. package/src/components/TracePanel/trace-filter.test.ts +137 -0
  119. package/src/components/TracePanel/trace-filter.ts +120 -0
  120. package/src/components/TracePanel/useTraceMenu.ts +95 -0
  121. package/src/components/index.ts +8 -0
  122. package/src/execution-graph/SPEC.md +749 -0
  123. package/src/execution-graph/execution-graph.test.ts +1205 -0
  124. package/src/execution-graph/execution-graph.ts +1076 -0
  125. package/src/execution-graph/index.ts +35 -0
  126. package/src/execution-graph/pending-block-status.test.ts +118 -0
  127. package/src/execution-graph/pending-block-status.ts +88 -0
  128. package/src/execution-graph/remote-snapshot.test.ts +116 -0
  129. package/src/execution-graph/span-tree.test.ts +370 -0
  130. package/src/execution-graph/span-tree.ts +404 -0
  131. package/src/execution-graph/sub-agent-delegation.test.ts +65 -0
  132. package/src/execution-graph/testing/collect-trace-events.ts +76 -0
  133. package/src/execution-graph/testing/index.ts +5 -0
  134. package/src/execution-graph/testing/sub-agent-delegation.json +1285 -0
  135. package/src/hooks/index.ts +7 -0
  136. package/src/hooks/useExecutionGraph.ts +107 -0
  137. package/src/hooks/useSessionTimeline.ts +53 -0
  138. package/src/hooks/useTraceMessages.ts +58 -0
  139. package/src/index.ts +8 -0
  140. package/src/session-timeline/index.ts +6 -0
  141. package/src/session-timeline/session-timeline.test.ts +662 -0
  142. package/src/session-timeline/session-timeline.ts +605 -0
  143. package/src/session-timeline/types.ts +100 -0
  144. package/src/testing/index.ts +8 -0
  145. package/src/testing/simulated-agent.ts +293 -0
  146. package/src/testing/snapshot-playback.ts +45 -0
  147. package/src/testing/test-trace.ts +22 -0
  148. package/src/translations.ts +29 -0
@@ -0,0 +1,370 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import * as Effect from 'effect/Effect';
6
+ import * as Schema from 'effect/Schema';
7
+ import { describe, test } from 'vitest';
8
+
9
+ import * as Trace from '@dxos/compute/Trace';
10
+
11
+ import {
12
+ DEFAULT_SPAN_TIMEOUT_MS,
13
+ ROOT_SPAN_ID,
14
+ buildSpanTree,
15
+ collectSpanTreeEventsCategorically,
16
+ flattenSpanTree,
17
+ } from './span-tree.ts';
18
+ import { collectTraceEvents, withMeta } from './testing/index.ts';
19
+
20
+ const NoteEvent = Trace.EventType('note', {
21
+ schema: Schema.Struct({
22
+ text: Schema.optional(Schema.String),
23
+ index: Schema.optional(Schema.Number),
24
+ }),
25
+ isEphemeral: false,
26
+ });
27
+
28
+ const FooEvent = Trace.EventType('foo', {
29
+ schema: Schema.Struct({}),
30
+ isEphemeral: false,
31
+ });
32
+
33
+ describe('buildSpanTree', () => {
34
+ test('returns a root span for empty input', ({ expect }) => {
35
+ const tree = buildSpanTree([]);
36
+ expect(tree.id).toBe(ROOT_SPAN_ID);
37
+ expect(tree.children).toEqual([]);
38
+ expect(tree.events).toEqual([]);
39
+ });
40
+
41
+ test('events without pid attach to root span', ({ expect }) => {
42
+ const messages = collectTraceEvents(Trace.write(FooEvent, {}));
43
+ const tree = buildSpanTree(messages);
44
+ expect(tree.children).toEqual([]);
45
+ expect(tree.events).toHaveLength(1);
46
+ expect(tree.events[0].type).toBe('foo');
47
+ });
48
+
49
+ test('a single operation start/end pair produces one span under root', ({ expect }) => {
50
+ const messages = collectTraceEvents(
51
+ withMeta(
52
+ { pid: 'op-1' },
53
+ Effect.gen(function* () {
54
+ yield* Trace.write(Trace.OperationStart, { key: 'reply', name: 'Reply' });
55
+ yield* Trace.write(Trace.OperationEnd, { key: 'reply', name: 'Reply', outcome: 'success' });
56
+ }),
57
+ ),
58
+ );
59
+ const tree = buildSpanTree(messages);
60
+ expect(tree.children).toHaveLength(1);
61
+ expect(tree.children[0].meta.pid).toBe('op-1');
62
+ expect(tree.children[0].events).toHaveLength(2);
63
+ expect(tree.children[0].children).toEqual([]);
64
+ });
65
+
66
+ test('a nested operation attaches to its open parent', ({ expect }) => {
67
+ const messages = collectTraceEvents(
68
+ Effect.gen(function* () {
69
+ yield* withMeta({ pid: 'agent-1' }, Trace.write(Trace.AgentRequestBegin, {}));
70
+ yield* withMeta(
71
+ { pid: 'op-1', parentPid: 'agent-1' },
72
+ Effect.gen(function* () {
73
+ yield* Trace.write(Trace.OperationStart, { key: 'lookup', name: 'Lookup' });
74
+ yield* Trace.write(Trace.OperationEnd, { key: 'lookup', name: 'Lookup', outcome: 'success' });
75
+ }),
76
+ );
77
+ yield* withMeta({ pid: 'agent-1' }, Trace.write(Trace.AgentRequestEnd, { status: 'success' }));
78
+ }),
79
+ );
80
+ const tree = buildSpanTree(messages);
81
+ expect(tree.children).toHaveLength(1);
82
+ const agentSpan = tree.children[0];
83
+ expect(agentSpan.meta.pid).toBe('agent-1');
84
+ expect(agentSpan.children).toHaveLength(1);
85
+ expect(agentSpan.children[0].meta.pid).toBe('op-1');
86
+ expect(agentSpan.children[0].events).toHaveLength(2);
87
+ });
88
+
89
+ test('sequential agent requests in one process become sibling spans', ({ expect }) => {
90
+ const messages = collectTraceEvents(
91
+ withMeta(
92
+ { pid: 'agent-1' },
93
+ Effect.gen(function* () {
94
+ yield* Trace.write(Trace.AgentRequestBegin, {});
95
+ yield* Trace.write(Trace.AgentRequestEnd, { status: 'success' });
96
+ yield* Trace.write(Trace.AgentRequestBegin, {});
97
+ yield* Trace.write(Trace.AgentRequestEnd, { status: 'success' });
98
+ }),
99
+ ),
100
+ );
101
+ const tree = buildSpanTree(messages);
102
+ expect(tree.children).toHaveLength(2);
103
+ expect(tree.children.every((child) => child.meta.pid === 'agent-1')).toBe(true);
104
+ // Span ids are distinct even though pids are shared.
105
+ expect(tree.children[0].id).not.toBe(tree.children[1].id);
106
+ });
107
+
108
+ test('non-boundary events attach to the currently-open span', ({ expect }) => {
109
+ const messages = collectTraceEvents(
110
+ withMeta(
111
+ { pid: 'agent-1' },
112
+ Effect.gen(function* () {
113
+ yield* Trace.write(Trace.AgentRequestBegin, {});
114
+ yield* Trace.write(NoteEvent, {});
115
+ yield* Trace.write(Trace.AgentRequestEnd, { status: 'success' });
116
+ }),
117
+ ),
118
+ );
119
+ const tree = buildSpanTree(messages);
120
+ expect(tree.children[0].events).toHaveLength(3);
121
+ expect(tree.children[0].events.map((event) => event.type)).toEqual([
122
+ Trace.AgentRequestBegin.key,
123
+ 'note',
124
+ Trace.AgentRequestEnd.key,
125
+ ]);
126
+ });
127
+
128
+ test('non-boundary event without an open span lands on root', ({ expect }) => {
129
+ const messages = collectTraceEvents(withMeta({ pid: 'orphan-pid' }, Trace.write(NoteEvent, {})));
130
+ const tree = buildSpanTree(messages);
131
+ expect(tree.children).toEqual([]);
132
+ expect(tree.events).toHaveLength(1);
133
+ expect(tree.events[0].type).toBe('note');
134
+ });
135
+
136
+ test('end event without a matching begin attaches to root', ({ expect }) => {
137
+ const messages = collectTraceEvents(
138
+ withMeta({ pid: 'op-1' }, Trace.write(Trace.OperationEnd, { key: 'reply', name: 'Reply', outcome: 'success' })),
139
+ );
140
+ const tree = buildSpanTree(messages);
141
+ expect(tree.children).toEqual([]);
142
+ expect(tree.events).toHaveLength(1);
143
+ });
144
+
145
+ test('sorts sibling spans chronologically by their first event', ({ expect }) => {
146
+ // Build events in chronological order, then reverse the message array before feeding
147
+ // into `buildSpanTree` to verify that the algorithm orders siblings by their first
148
+ // event's timestamp rather than by input order.
149
+ const messages = collectTraceEvents(
150
+ Effect.gen(function* () {
151
+ yield* withMeta(
152
+ { pid: 'op-early' },
153
+ Effect.gen(function* () {
154
+ yield* Trace.write(Trace.OperationStart, { key: 'a' });
155
+ yield* Trace.write(Trace.OperationEnd, { key: 'a', outcome: 'success' });
156
+ }),
157
+ );
158
+ yield* withMeta(
159
+ { pid: 'op-late' },
160
+ Effect.gen(function* () {
161
+ yield* Trace.write(Trace.OperationStart, { key: 'b' });
162
+ yield* Trace.write(Trace.OperationEnd, { key: 'b', outcome: 'success' });
163
+ }),
164
+ );
165
+ }),
166
+ );
167
+ const tree = buildSpanTree([...messages].reverse());
168
+ expect(tree.children.map((child) => child.meta.pid)).toEqual(['op-early', 'op-late']);
169
+ });
170
+
171
+ test('eventLimit drops oldest non-boundary events but always retains span boundaries', ({ expect }) => {
172
+ // Three non-boundary events (`note 1..3`), all boundary events should be kept regardless.
173
+ // With `eventLimit: 1`, only the most recent `note` should survive.
174
+ const messages = collectTraceEvents(
175
+ withMeta(
176
+ { pid: 'agent-1' },
177
+ Effect.gen(function* () {
178
+ yield* Trace.write(Trace.AgentRequestBegin, {});
179
+ yield* Trace.write(NoteEvent, { text: 'note 1' });
180
+ yield* Trace.write(NoteEvent, { text: 'note 2' });
181
+ yield* Trace.write(NoteEvent, { text: 'note 3' });
182
+ yield* Trace.write(Trace.AgentRequestEnd, { status: 'success' });
183
+ }),
184
+ ),
185
+ );
186
+ const tree = buildSpanTree(messages, { eventLimit: 1 });
187
+ expect(tree.children).toHaveLength(1);
188
+ const agentSpan = tree.children[0];
189
+ expect(agentSpan.events.map((event) => event.type)).toEqual([
190
+ Trace.AgentRequestBegin.key,
191
+ 'note',
192
+ Trace.AgentRequestEnd.key,
193
+ ]);
194
+ expect((agentSpan.events[1].data as { text: string }).text).toBe('note 3');
195
+ });
196
+
197
+ test('eventLimit preserves parent-child structure when the parent begin is older than the window', ({ expect }) => {
198
+ // Regression: previously `events.slice(-eventLimit)` would drop the parent's begin event
199
+ // when the limit fell mid-span, orphaning every child span onto root. The fix retains all
200
+ // boundary events so the parent stays open and children remain nested.
201
+ const messages = collectTraceEvents(
202
+ Effect.gen(function* () {
203
+ yield* withMeta(
204
+ { pid: 'parent-1' },
205
+ Effect.gen(function* () {
206
+ yield* Trace.write(Trace.AgentRequestBegin, {});
207
+ for (let index = 0; index < 20; index += 1) {
208
+ yield* Trace.write(NoteEvent, { index });
209
+ }
210
+ }),
211
+ );
212
+ yield* withMeta(
213
+ { pid: 'child-1', parentPid: 'parent-1' },
214
+ Effect.gen(function* () {
215
+ yield* Trace.write(Trace.OperationStart, { key: 'lookup', name: 'Lookup' });
216
+ yield* Trace.write(Trace.OperationEnd, { key: 'lookup', name: 'Lookup', outcome: 'success' });
217
+ }),
218
+ );
219
+ yield* withMeta({ pid: 'parent-1' }, Trace.write(Trace.AgentRequestEnd, { status: 'success' }));
220
+ }),
221
+ );
222
+ const tree = buildSpanTree(messages, {
223
+ // Limit far below the 20 `note` events — naive slicing would drop the parent's begin.
224
+ eventLimit: 2,
225
+ });
226
+ expect(tree.children).toHaveLength(1);
227
+ const parentSpan = tree.children[0];
228
+ expect(parentSpan.meta.pid).toBe('parent-1');
229
+ expect(parentSpan.children).toHaveLength(1);
230
+ expect(parentSpan.children[0].meta.pid).toBe('child-1');
231
+ expect(parentSpan.children[0].events.map((event) => event.type)).toEqual([
232
+ Trace.OperationStart.key,
233
+ Trace.OperationEnd.key,
234
+ ]);
235
+ });
236
+
237
+ test('flattenSpanTree returns root + all descendants in depth-first order', ({ expect }) => {
238
+ const messages = collectTraceEvents(
239
+ Effect.gen(function* () {
240
+ yield* withMeta({ pid: 'agent-1' }, Trace.write(Trace.AgentRequestBegin, {}));
241
+ yield* withMeta(
242
+ { pid: 'op-1', parentPid: 'agent-1' },
243
+ Effect.gen(function* () {
244
+ yield* Trace.write(Trace.OperationStart, { key: 'a' });
245
+ yield* Trace.write(Trace.OperationEnd, { key: 'a', outcome: 'success' });
246
+ }),
247
+ );
248
+ yield* withMeta({ pid: 'agent-1' }, Trace.write(Trace.AgentRequestEnd, { status: 'success' }));
249
+ }),
250
+ );
251
+ const tree = buildSpanTree(messages);
252
+ const order = flattenSpanTree(tree);
253
+ expect(order[0].id).toBe(ROOT_SPAN_ID);
254
+ expect(order[1].meta.pid).toBe('agent-1');
255
+ expect(order[2].meta.pid).toBe('op-1');
256
+ expect(order).toHaveLength(3);
257
+ });
258
+
259
+ test('an open operation span past the timeout is force-closed with a synthetic failure end', ({ expect }) => {
260
+ const messages = collectTraceEvents(
261
+ withMeta({ pid: 'op-1' }, Trace.write(Trace.OperationStart, { key: 'reply', name: 'Reply' })),
262
+ );
263
+ // The single event lands at timestamp 1; `now` is well past the default timeout.
264
+ const tree = buildSpanTree(messages, { now: 1 + DEFAULT_SPAN_TIMEOUT_MS });
265
+ expect(tree.children).toHaveLength(1);
266
+ const span = tree.children[0];
267
+ expect(span.events).toHaveLength(2);
268
+ expect(span.events[1].type).toBe(Trace.OperationEnd.key);
269
+ expect(span.events[1].data).toMatchObject({ key: 'reply', name: 'Reply', outcome: 'failure' });
270
+ });
271
+
272
+ test('an open agent request span past the timeout is force-closed as interrupted', ({ expect }) => {
273
+ const messages = collectTraceEvents(withMeta({ pid: 'agent-1' }, Trace.write(Trace.AgentRequestBegin, {})));
274
+ const tree = buildSpanTree(messages, { now: 1 + DEFAULT_SPAN_TIMEOUT_MS });
275
+ const span = tree.children[0];
276
+ expect(span.events).toHaveLength(2);
277
+ expect(span.events[1].type).toBe(Trace.AgentRequestEnd.key);
278
+ expect(span.events[1].data).toMatchObject({ status: 'interrupted' });
279
+ });
280
+
281
+ test('an open span within the timeout window stays open', ({ expect }) => {
282
+ const messages = collectTraceEvents(
283
+ withMeta({ pid: 'op-1' }, Trace.write(Trace.OperationStart, { key: 'reply', name: 'Reply' })),
284
+ );
285
+ const tree = buildSpanTree(messages, { now: 1 + DEFAULT_SPAN_TIMEOUT_MS - 1 });
286
+ const span = tree.children[0];
287
+ expect(span.events).toHaveLength(1);
288
+ });
289
+
290
+ test('spanTimeoutMs is configurable', ({ expect }) => {
291
+ const messages = collectTraceEvents(
292
+ withMeta({ pid: 'op-1' }, Trace.write(Trace.OperationStart, { key: 'reply', name: 'Reply' })),
293
+ );
294
+ const closed = buildSpanTree(messages, { now: 100, spanTimeoutMs: 10 });
295
+ expect(closed.children[0].events).toHaveLength(2);
296
+
297
+ const stillOpen = buildSpanTree(messages, { now: 100, spanTimeoutMs: 1_000 });
298
+ expect(stillOpen.children[0].events).toHaveLength(1);
299
+ });
300
+
301
+ test('a span superseded by a later same-pid begin event is force-closed too', ({ expect }) => {
302
+ // The first begin never gets an end (interleaved B1 B2 E1 pattern, §15) — `openSpans` only
303
+ // tracks the second span once it supersedes the first, so the sweep must reach both.
304
+ const messages = collectTraceEvents(
305
+ withMeta(
306
+ { pid: 'agent-1' },
307
+ Effect.gen(function* () {
308
+ yield* Trace.write(Trace.AgentRequestBegin, {});
309
+ yield* Trace.write(Trace.AgentRequestBegin, {});
310
+ yield* Trace.write(Trace.AgentRequestEnd, { status: 'success' });
311
+ }),
312
+ ),
313
+ );
314
+ const tree = buildSpanTree(messages, { now: 1 + DEFAULT_SPAN_TIMEOUT_MS });
315
+ expect(tree.children).toHaveLength(2);
316
+ const [superseded, current] = tree.children;
317
+ expect(superseded.events).toHaveLength(2);
318
+ expect(superseded.events[1].type).toBe(Trace.AgentRequestEnd.key);
319
+ expect(superseded.events[1].data).toMatchObject({ status: 'interrupted' });
320
+ expect(current.events).toHaveLength(2);
321
+ expect(current.events[1].data).toMatchObject({ status: 'success' });
322
+ });
323
+
324
+ test('a completed span is unaffected by the timeout', ({ expect }) => {
325
+ const messages = collectTraceEvents(
326
+ withMeta(
327
+ { pid: 'op-1' },
328
+ Effect.gen(function* () {
329
+ yield* Trace.write(Trace.OperationStart, { key: 'reply', name: 'Reply' });
330
+ yield* Trace.write(Trace.OperationEnd, { key: 'reply', name: 'Reply', outcome: 'success' });
331
+ }),
332
+ ),
333
+ );
334
+ const tree = buildSpanTree(messages, { now: 1 + DEFAULT_SPAN_TIMEOUT_MS });
335
+ expect(tree.children[0].events).toHaveLength(2);
336
+ expect(tree.children[0].events[1].data).toMatchObject({ outcome: 'success' });
337
+ });
338
+
339
+ test('collectSpanTreeEventsCategorically emits begin, child subtrees, then remaining events', ({ expect }) => {
340
+ const messages = collectTraceEvents(
341
+ Effect.gen(function* () {
342
+ yield* withMeta({ pid: 'parent-1' }, Trace.write(Trace.OperationStart, { key: 'parent', name: 'Parent' }));
343
+ yield* withMeta(
344
+ { pid: 'child-1', parentPid: 'parent-1' },
345
+ Effect.gen(function* () {
346
+ yield* Trace.write(Trace.OperationStart, { key: 'child', name: 'Child' });
347
+ yield* Trace.write(Trace.OperationEnd, { key: 'child', name: 'Child', outcome: 'success' });
348
+ }),
349
+ );
350
+ yield* withMeta(
351
+ { pid: 'parent-1' },
352
+ Trace.write(Trace.OperationEnd, { key: 'parent', name: 'Parent', outcome: 'success' }),
353
+ );
354
+ }),
355
+ );
356
+
357
+ const tree = buildSpanTree(messages);
358
+ const order = collectSpanTreeEventsCategorically(tree).map(({ span, event }) => ({
359
+ pid: span.meta.pid ?? ROOT_SPAN_ID,
360
+ type: event.type,
361
+ }));
362
+
363
+ expect(order).toEqual([
364
+ { pid: 'parent-1', type: Trace.OperationStart.key },
365
+ { pid: 'child-1', type: Trace.OperationStart.key },
366
+ { pid: 'child-1', type: Trace.OperationEnd.key },
367
+ { pid: 'parent-1', type: Trace.OperationEnd.key },
368
+ ]);
369
+ });
370
+ });