@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.
- package/LICENSE +105 -0
- package/README.md +3 -0
- package/dist/lib/index.mjs +2868 -0
- package/dist/lib/index.mjs.map +1 -0
- package/dist/lib/testing.mjs +338 -0
- package/dist/lib/testing.mjs.map +1 -0
- package/dist/lib/translations.mjs +19 -0
- package/dist/lib/translations.mjs.map +1 -0
- package/dist/types/src/components/Gantt/Gantt.d.ts +68 -0
- package/dist/types/src/components/Gantt/Gantt.d.ts.map +1 -0
- package/dist/types/src/components/Gantt/Gantt.stories.d.ts +30 -0
- package/dist/types/src/components/Gantt/Gantt.stories.d.ts.map +1 -0
- package/dist/types/src/components/Gantt/index.d.ts +2 -0
- package/dist/types/src/components/Gantt/index.d.ts.map +1 -0
- package/dist/types/src/components/ProcessTree/ProcessTree.d.ts +32 -0
- package/dist/types/src/components/ProcessTree/ProcessTree.d.ts.map +1 -0
- package/dist/types/src/components/ProcessTree/ProcessTree.stories.d.ts +8 -0
- package/dist/types/src/components/ProcessTree/ProcessTree.stories.d.ts.map +1 -0
- package/dist/types/src/components/ProcessTree/index.d.ts +2 -0
- package/dist/types/src/components/ProcessTree/index.d.ts.map +1 -0
- package/dist/types/src/components/Timeline/Timeline.d.ts +50 -0
- package/dist/types/src/components/Timeline/Timeline.d.ts.map +1 -0
- package/dist/types/src/components/Timeline/Timeline.stories.d.ts +24 -0
- package/dist/types/src/components/Timeline/Timeline.stories.d.ts.map +1 -0
- package/dist/types/src/components/Timeline/index.d.ts +4 -0
- package/dist/types/src/components/Timeline/index.d.ts.map +1 -0
- package/dist/types/src/components/Timeline/timeline-options.d.ts +9 -0
- package/dist/types/src/components/Timeline/timeline-options.d.ts.map +1 -0
- package/dist/types/src/components/Timeline/timeline-printer.d.ts +7 -0
- package/dist/types/src/components/Timeline/timeline-printer.d.ts.map +1 -0
- package/dist/types/src/components/Timeline/timeline-printer.test.d.ts +2 -0
- package/dist/types/src/components/Timeline/timeline-printer.test.d.ts.map +1 -0
- package/dist/types/src/components/TracePanel/TracePanel.d.ts +28 -0
- package/dist/types/src/components/TracePanel/TracePanel.d.ts.map +1 -0
- package/dist/types/src/components/TracePanel/dxn-extractor.d.ts +19 -0
- package/dist/types/src/components/TracePanel/dxn-extractor.d.ts.map +1 -0
- package/dist/types/src/components/TracePanel/dxn-extractor.test.d.ts +2 -0
- package/dist/types/src/components/TracePanel/dxn-extractor.test.d.ts.map +1 -0
- package/dist/types/src/components/TracePanel/index.d.ts +4 -0
- package/dist/types/src/components/TracePanel/index.d.ts.map +1 -0
- package/dist/types/src/components/TracePanel/trace-filter.d.ts +39 -0
- package/dist/types/src/components/TracePanel/trace-filter.d.ts.map +1 -0
- package/dist/types/src/components/TracePanel/trace-filter.test.d.ts +2 -0
- package/dist/types/src/components/TracePanel/trace-filter.test.d.ts.map +1 -0
- package/dist/types/src/components/TracePanel/useTraceMenu.d.ts +15 -0
- package/dist/types/src/components/TracePanel/useTraceMenu.d.ts.map +1 -0
- package/dist/types/src/components/index.d.ts +5 -0
- package/dist/types/src/components/index.d.ts.map +1 -0
- package/dist/types/src/execution-graph/execution-graph.d.ts +139 -0
- package/dist/types/src/execution-graph/execution-graph.d.ts.map +1 -0
- package/dist/types/src/execution-graph/execution-graph.test.d.ts +2 -0
- package/dist/types/src/execution-graph/execution-graph.test.d.ts.map +1 -0
- package/dist/types/src/execution-graph/index.d.ts +4 -0
- package/dist/types/src/execution-graph/index.d.ts.map +1 -0
- package/dist/types/src/execution-graph/pending-block-status.d.ts +26 -0
- package/dist/types/src/execution-graph/pending-block-status.d.ts.map +1 -0
- package/dist/types/src/execution-graph/pending-block-status.test.d.ts +2 -0
- package/dist/types/src/execution-graph/pending-block-status.test.d.ts.map +1 -0
- package/dist/types/src/execution-graph/remote-snapshot.test.d.ts +2 -0
- package/dist/types/src/execution-graph/remote-snapshot.test.d.ts.map +1 -0
- package/dist/types/src/execution-graph/span-tree.d.ts +106 -0
- package/dist/types/src/execution-graph/span-tree.d.ts.map +1 -0
- package/dist/types/src/execution-graph/span-tree.test.d.ts +2 -0
- package/dist/types/src/execution-graph/span-tree.test.d.ts.map +1 -0
- package/dist/types/src/execution-graph/sub-agent-delegation.test.d.ts +2 -0
- package/dist/types/src/execution-graph/sub-agent-delegation.test.d.ts.map +1 -0
- package/dist/types/src/execution-graph/testing/collect-trace-events.d.ts +40 -0
- package/dist/types/src/execution-graph/testing/collect-trace-events.d.ts.map +1 -0
- package/dist/types/src/execution-graph/testing/index.d.ts +2 -0
- package/dist/types/src/execution-graph/testing/index.d.ts.map +1 -0
- package/dist/types/src/hooks/index.d.ts +4 -0
- package/dist/types/src/hooks/index.d.ts.map +1 -0
- package/dist/types/src/hooks/useExecutionGraph.d.ts +17 -0
- package/dist/types/src/hooks/useExecutionGraph.d.ts.map +1 -0
- package/dist/types/src/hooks/useSessionTimeline.d.ts +17 -0
- package/dist/types/src/hooks/useSessionTimeline.d.ts.map +1 -0
- package/dist/types/src/hooks/useTraceMessages.d.ts +17 -0
- package/dist/types/src/hooks/useTraceMessages.d.ts.map +1 -0
- package/dist/types/src/index.d.ts +5 -0
- package/dist/types/src/index.d.ts.map +1 -0
- package/dist/types/src/session-timeline/index.d.ts +3 -0
- package/dist/types/src/session-timeline/index.d.ts.map +1 -0
- package/dist/types/src/session-timeline/session-timeline.d.ts +19 -0
- package/dist/types/src/session-timeline/session-timeline.d.ts.map +1 -0
- package/dist/types/src/session-timeline/session-timeline.test.d.ts +2 -0
- package/dist/types/src/session-timeline/session-timeline.test.d.ts.map +1 -0
- package/dist/types/src/session-timeline/types.d.ts +129 -0
- package/dist/types/src/session-timeline/types.d.ts.map +1 -0
- package/dist/types/src/testing/index.d.ts +5 -0
- package/dist/types/src/testing/index.d.ts.map +1 -0
- package/dist/types/src/testing/simulated-agent.d.ts +63 -0
- package/dist/types/src/testing/simulated-agent.d.ts.map +1 -0
- package/dist/types/src/testing/snapshot-playback.d.ts +12 -0
- package/dist/types/src/testing/snapshot-playback.d.ts.map +1 -0
- package/dist/types/src/testing/test-trace.d.ts +5 -0
- package/dist/types/src/testing/test-trace.d.ts.map +1 -0
- package/dist/types/src/translations.d.ts +19 -0
- package/dist/types/src/translations.d.ts.map +1 -0
- package/dist/types/tsconfig.tsbuildinfo +1 -0
- package/package.json +77 -0
- package/src/components/Gantt/Gantt.stories.tsx +241 -0
- package/src/components/Gantt/Gantt.tsx +589 -0
- package/src/components/Gantt/index.ts +5 -0
- package/src/components/ProcessTree/ProcessTree.stories.tsx +95 -0
- package/src/components/ProcessTree/ProcessTree.tsx +265 -0
- package/src/components/ProcessTree/index.ts +5 -0
- package/src/components/Timeline/Timeline.stories.tsx +326 -0
- package/src/components/Timeline/Timeline.tsx +534 -0
- package/src/components/Timeline/git.drawio.svg +821 -0
- package/src/components/Timeline/index.ts +7 -0
- package/src/components/Timeline/timeline-options.ts +27 -0
- package/src/components/Timeline/timeline-printer.test.ts +76 -0
- package/src/components/Timeline/timeline-printer.ts +223 -0
- package/src/components/TracePanel/TracePanel.tsx +248 -0
- package/src/components/TracePanel/dxn-extractor.test.ts +153 -0
- package/src/components/TracePanel/dxn-extractor.ts +166 -0
- package/src/components/TracePanel/index.ts +7 -0
- package/src/components/TracePanel/trace-filter.test.ts +137 -0
- package/src/components/TracePanel/trace-filter.ts +120 -0
- package/src/components/TracePanel/useTraceMenu.ts +95 -0
- package/src/components/index.ts +8 -0
- package/src/execution-graph/SPEC.md +749 -0
- package/src/execution-graph/execution-graph.test.ts +1205 -0
- package/src/execution-graph/execution-graph.ts +1076 -0
- package/src/execution-graph/index.ts +35 -0
- package/src/execution-graph/pending-block-status.test.ts +118 -0
- package/src/execution-graph/pending-block-status.ts +88 -0
- package/src/execution-graph/remote-snapshot.test.ts +116 -0
- package/src/execution-graph/span-tree.test.ts +370 -0
- package/src/execution-graph/span-tree.ts +404 -0
- package/src/execution-graph/sub-agent-delegation.test.ts +65 -0
- package/src/execution-graph/testing/collect-trace-events.ts +76 -0
- package/src/execution-graph/testing/index.ts +5 -0
- package/src/execution-graph/testing/sub-agent-delegation.json +1285 -0
- package/src/hooks/index.ts +7 -0
- package/src/hooks/useExecutionGraph.ts +107 -0
- package/src/hooks/useSessionTimeline.ts +53 -0
- package/src/hooks/useTraceMessages.ts +58 -0
- package/src/index.ts +8 -0
- package/src/session-timeline/index.ts +6 -0
- package/src/session-timeline/session-timeline.test.ts +662 -0
- package/src/session-timeline/session-timeline.ts +605 -0
- package/src/session-timeline/types.ts +100 -0
- package/src/testing/index.ts +8 -0
- package/src/testing/simulated-agent.ts +293 -0
- package/src/testing/snapshot-playback.ts +45 -0
- package/src/testing/test-trace.ts +22 -0
- package/src/translations.ts +29 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
export {
|
|
6
|
+
type BuildExecutionGraphParams,
|
|
7
|
+
type CollectProcessActivityOptions,
|
|
8
|
+
CommitSelector,
|
|
9
|
+
type ExecutionGraph,
|
|
10
|
+
type ExecutionGraphDetailsMap,
|
|
11
|
+
buildExecutionGraph,
|
|
12
|
+
collectProcessActivityLines,
|
|
13
|
+
deriveInFlightActivityLine,
|
|
14
|
+
} from './execution-graph.ts';
|
|
15
|
+
export {
|
|
16
|
+
type EphemeralStatusUpdate,
|
|
17
|
+
estimateTokenCount,
|
|
18
|
+
formatPendingBlockStatus,
|
|
19
|
+
pendingStatusFromEphemeralMessage,
|
|
20
|
+
resolveEphemeralStatusUpdate,
|
|
21
|
+
} from './pending-block-status.ts';
|
|
22
|
+
export {
|
|
23
|
+
BEGIN_EVENT_TYPES,
|
|
24
|
+
type BuildSpanTreeOptions,
|
|
25
|
+
DEFAULT_SPAN_TIMEOUT_MS,
|
|
26
|
+
END_EVENT_TYPES,
|
|
27
|
+
ROOT_SPAN_ID,
|
|
28
|
+
type Span,
|
|
29
|
+
type SpanMeta,
|
|
30
|
+
buildSpanTree,
|
|
31
|
+
flattenSpanTree,
|
|
32
|
+
isSpanBeginEvent,
|
|
33
|
+
isSpanEndEvent,
|
|
34
|
+
walkSpanTree,
|
|
35
|
+
} from './span-tree.ts';
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { describe, test } from 'vitest';
|
|
6
|
+
|
|
7
|
+
import * as Trace from '@dxos/compute/Trace';
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
formatPendingBlockStatus,
|
|
11
|
+
pendingStatusFromEphemeralMessage,
|
|
12
|
+
resolveEphemeralStatusUpdate,
|
|
13
|
+
} from './pending-block-status.ts';
|
|
14
|
+
|
|
15
|
+
describe('formatPendingBlockStatus', () => {
|
|
16
|
+
test('formats pending text as generating tokens', ({ expect }) => {
|
|
17
|
+
const status = formatPendingBlockStatus({
|
|
18
|
+
_tag: 'text',
|
|
19
|
+
text: 'Hello world',
|
|
20
|
+
pending: true,
|
|
21
|
+
});
|
|
22
|
+
expect(status).toMatch(/^Generating \d+ tokens\.\.\.\.$/);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('formats pending tool call with byte count', ({ expect }) => {
|
|
26
|
+
const status = formatPendingBlockStatus({
|
|
27
|
+
_tag: 'toolCall',
|
|
28
|
+
toolCallId: 'call-1',
|
|
29
|
+
name: 'create-document',
|
|
30
|
+
input: '{"title":"Haiku"}',
|
|
31
|
+
providerExecuted: false,
|
|
32
|
+
pending: true,
|
|
33
|
+
});
|
|
34
|
+
expect(status).toBe('Calling create-document (17 bytes)...');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test('returns undefined for completed blocks', ({ expect }) => {
|
|
38
|
+
expect(
|
|
39
|
+
formatPendingBlockStatus({
|
|
40
|
+
_tag: 'text',
|
|
41
|
+
text: 'Done',
|
|
42
|
+
pending: false,
|
|
43
|
+
}),
|
|
44
|
+
).toBeUndefined();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('formats operation input as calling with byte count', ({ expect }) => {
|
|
48
|
+
const status = pendingStatusFromEphemeralMessage(
|
|
49
|
+
ephemeralMessage([
|
|
50
|
+
{
|
|
51
|
+
type: Trace.OperationInput.key,
|
|
52
|
+
timestamp: Date.now(),
|
|
53
|
+
data: {
|
|
54
|
+
key: 'org.dxos.operation.assistantToolkit.getContext',
|
|
55
|
+
name: 'Get Agent Context',
|
|
56
|
+
input: { foo: 'bar' },
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
]),
|
|
60
|
+
);
|
|
61
|
+
expect(status).toBe('Calling Get Agent Context (13 bytes)...');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('leaves status unchanged (sticky) when partial block stream completes', ({ expect }) => {
|
|
65
|
+
expect(
|
|
66
|
+
resolveEphemeralStatusUpdate(
|
|
67
|
+
ephemeralMessage([
|
|
68
|
+
{
|
|
69
|
+
type: 'assistant.partialBlock',
|
|
70
|
+
timestamp: Date.now(),
|
|
71
|
+
data: {
|
|
72
|
+
messageId: 'msg-1',
|
|
73
|
+
role: 'assistant',
|
|
74
|
+
block: {
|
|
75
|
+
_tag: 'toolCall',
|
|
76
|
+
toolCallId: 'call-1',
|
|
77
|
+
name: 'update-tasks',
|
|
78
|
+
operationName: 'Update tasks',
|
|
79
|
+
input: '{}',
|
|
80
|
+
providerExecuted: false,
|
|
81
|
+
pending: false,
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
]),
|
|
86
|
+
),
|
|
87
|
+
).toBe('unchanged');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test('suppresses raw routine ULID status updates', ({ expect }) => {
|
|
91
|
+
expect(
|
|
92
|
+
pendingStatusFromEphemeralMessage(
|
|
93
|
+
ephemeralMessage([
|
|
94
|
+
{
|
|
95
|
+
type: Trace.StatusUpdate.key,
|
|
96
|
+
timestamp: Date.now(),
|
|
97
|
+
data: { message: 'Running 01KVB9WBRG0000000000000000' },
|
|
98
|
+
},
|
|
99
|
+
]),
|
|
100
|
+
),
|
|
101
|
+
).toBeUndefined();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test('keeps descriptive status updates', ({ expect }) => {
|
|
105
|
+
expect(
|
|
106
|
+
pendingStatusFromEphemeralMessage(
|
|
107
|
+
ephemeralMessage([
|
|
108
|
+
{ type: Trace.StatusUpdate.key, timestamp: Date.now(), data: { message: 'Thinking about the plan' } },
|
|
109
|
+
]),
|
|
110
|
+
),
|
|
111
|
+
).toBe('Thinking about the plan');
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// `Trace.Message` is an ECHO object type, so a plain test literal does not structurally overlap it;
|
|
116
|
+
// build the minimal ephemeral envelope the status helpers read via a single boundary cast.
|
|
117
|
+
const ephemeralMessage = (events: readonly Trace.Event[]): Trace.Message =>
|
|
118
|
+
({ meta: { pid: 'pid-1' }, isEphemeral: true, events }) as unknown as Trace.Message;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import * as Trace from '@dxos/compute/Trace';
|
|
6
|
+
import { type ContentBlock } from '@dxos/types';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Approximate streamed token count from partial text (UTF-8 length / 4).
|
|
10
|
+
*/
|
|
11
|
+
export const estimateTokenCount = (text: string): number =>
|
|
12
|
+
Math.max(1, Math.ceil(new TextEncoder().encode(text).length / 4));
|
|
13
|
+
|
|
14
|
+
const formatOperationInputStatus = (data: { key: string; name?: string; input: unknown }): string => {
|
|
15
|
+
const name = data.name ?? data.key;
|
|
16
|
+
const bytes = new TextEncoder().encode(JSON.stringify(data.input)).length;
|
|
17
|
+
return `Calling ${name} (${bytes} bytes)...`;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/** Raw routine status updates (e.g. `Running 01KVB9WBRG…`) carry an opaque ULID, not a useful label. */
|
|
21
|
+
const ROUTINE_ULID_STATUS = /^Running [0-9A-HJKMNP-TV-Z]{26}$/i;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Formats a pending assistant block for the delegated-task status line.
|
|
25
|
+
*/
|
|
26
|
+
export const formatPendingBlockStatus = (block: ContentBlock.Any): string | undefined => {
|
|
27
|
+
if (!block.pending) {
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (block._tag === 'text') {
|
|
32
|
+
return `Generating ${estimateTokenCount(block.text)} tokens....`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (block._tag === 'toolCall') {
|
|
36
|
+
const bytes = new TextEncoder().encode(block.input).length;
|
|
37
|
+
const name = block.operationName ?? block.name;
|
|
38
|
+
return `Calling ${name} (${bytes} bytes)...`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return undefined;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Reads the latest pending partial block from an ephemeral trace message.
|
|
46
|
+
*/
|
|
47
|
+
export const pendingStatusFromEphemeralMessage = (message: Trace.Message): string | undefined => {
|
|
48
|
+
for (const event of message.events) {
|
|
49
|
+
if (Trace.isOfType(Trace.PartialBlock, event)) {
|
|
50
|
+
const status = formatPendingBlockStatus(event.data.block);
|
|
51
|
+
if (status) {
|
|
52
|
+
return status;
|
|
53
|
+
}
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (Trace.isOfType(Trace.OperationInput, event)) {
|
|
58
|
+
return formatOperationInputStatus(event.data);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (Trace.isOfType(Trace.StatusUpdate, event)) {
|
|
62
|
+
const message = event.data.message?.trim();
|
|
63
|
+
// Suppress opaque routine ULID lines so descriptive partial-block / operation-input lines win.
|
|
64
|
+
if (!message || ROUTINE_ULID_STATUS.test(message)) {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
return message;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return undefined;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export type EphemeralStatusUpdate = 'unchanged' | { readonly line: string };
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Maps an ephemeral trace message to a task-status update.
|
|
77
|
+
*
|
|
78
|
+
* The status line is "sticky": a message without a pending line (e.g. a completed block during an
|
|
79
|
+
* LLM-thinking gap) leaves the previous line in place rather than clearing it, so the row keeps
|
|
80
|
+
* showing the latest meaningful activity until a new line arrives or the subscription ends.
|
|
81
|
+
*/
|
|
82
|
+
export const resolveEphemeralStatusUpdate = (message: Trace.Message): EphemeralStatusUpdate => {
|
|
83
|
+
const line = pendingStatusFromEphemeralMessage(message);
|
|
84
|
+
if (line) {
|
|
85
|
+
return { line };
|
|
86
|
+
}
|
|
87
|
+
return 'unchanged';
|
|
88
|
+
};
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { readFileSync } from 'node:fs';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
import { describe, test } from 'vitest';
|
|
8
|
+
|
|
9
|
+
import * as Trace from '@dxos/compute/Trace';
|
|
10
|
+
import { Obj } from '@dxos/echo';
|
|
11
|
+
|
|
12
|
+
import { buildExecutionGraph } from './execution-graph.ts';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Regression for sub-operations collapsing onto `main` when the event window in `buildSpanTree`
|
|
16
|
+
* dropped the parent span's `operation.start`.
|
|
17
|
+
*
|
|
18
|
+
* The remote trace snapshot has 714 chronological events. The 4th `Run Routine` span hosts every
|
|
19
|
+
* `Create object` / `Add schema` / `List schemas` / `Query` / `Add to context` sub-operation.
|
|
20
|
+
* Its `operation.start` lives near the front of the stream (index 70). Before the fix, the
|
|
21
|
+
* default `eventLimit = 300` would `slice(-300)` away that begin event, leaving the sub-ops'
|
|
22
|
+
* `parentPid` lookup unsatisfied — they ended up as top-level collapsible spans rendering their
|
|
23
|
+
* end commits on `main`.
|
|
24
|
+
*/
|
|
25
|
+
describe('TracePanel remote snapshot', () => {
|
|
26
|
+
test('sub-operations land on their parent Routine branch, never on main', ({ expect }) => {
|
|
27
|
+
const messages = loadRemoteTraceMessages(REMOTE_SNAPSHOT);
|
|
28
|
+
const { commits } = buildExecutionGraph({ traceMessages: messages });
|
|
29
|
+
|
|
30
|
+
// Commits whose message starts with one of these names originate from sub-operations
|
|
31
|
+
// (Trace.OperationEnd) — every one should live on the Routine's branch, never on `main`.
|
|
32
|
+
const subOpPrefixes = ['Create object', 'Add schema', 'List schemas', 'Query', 'Add to context'];
|
|
33
|
+
const isSubOpEnd = (message: string | undefined): boolean =>
|
|
34
|
+
message !== undefined && subOpPrefixes.some((prefix) => message.startsWith(prefix));
|
|
35
|
+
|
|
36
|
+
const subOpCommits = commits.filter((commit) => isSubOpEnd(commit.message));
|
|
37
|
+
expect(subOpCommits.length).toBeGreaterThan(0);
|
|
38
|
+
|
|
39
|
+
const onMain = subOpCommits.filter((commit) => commit.branch === 'main');
|
|
40
|
+
expect(onMain).toEqual([]);
|
|
41
|
+
|
|
42
|
+
// All sub-op end commits share the parent Routine's pid as their branch.
|
|
43
|
+
const branches = new Set(subOpCommits.map((commit) => commit.branch));
|
|
44
|
+
expect(branches.size).toBe(1);
|
|
45
|
+
expect(branches.has('main')).toBe(false);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('Routine end commits anchor to the previous main commit, not back to their own begin', ({ expect }) => {
|
|
49
|
+
// Regression: the multi-Routine snapshot has overlapping top-level Routine spans (R2 starts
|
|
50
|
+
// before R1 ends, R4 starts before R3 ends). Previously, R1.end's first parent was
|
|
51
|
+
// R1.begin (looked up by `beginCommitIdBySpan`), which drew an edge across R2.begin in the
|
|
52
|
+
// rendered timeline. The fix anchors end commits to the immediate predecessor on the
|
|
53
|
+
// parent branch (R2.begin) and treats the own-branch tail as a separate merge parent.
|
|
54
|
+
const messages = loadRemoteTraceMessages(MULTI_SNAPSHOT);
|
|
55
|
+
const { commits } = buildExecutionGraph({ traceMessages: messages });
|
|
56
|
+
|
|
57
|
+
const mainCommits = commits.filter((commit) => commit.branch === 'main');
|
|
58
|
+
// Sanity check: 4 begin + 4 end Run-Routine commits on main.
|
|
59
|
+
expect(mainCommits).toHaveLength(8);
|
|
60
|
+
|
|
61
|
+
// For each commit on main, its first parent (if any) must be a commit that appears
|
|
62
|
+
// *earlier* in the main commit sequence — no "skip-back" edges to an older begin commit.
|
|
63
|
+
const idToMainIndex = new Map(mainCommits.map((commit, index) => [commit.id, index]));
|
|
64
|
+
for (let index = 1; index < mainCommits.length; index += 1) {
|
|
65
|
+
const commit = mainCommits[index];
|
|
66
|
+
const onMainParents = (commit.parents ?? []).filter((parentId) => idToMainIndex.has(parentId));
|
|
67
|
+
expect(onMainParents.length).toBeGreaterThan(0);
|
|
68
|
+
for (const parentId of onMainParents) {
|
|
69
|
+
const parentIndex = idToMainIndex.get(parentId)!;
|
|
70
|
+
// Every parent on main must be the immediate predecessor (index - 1). An older parent
|
|
71
|
+
// would render an edge that crosses over the intervening commits.
|
|
72
|
+
expect(parentIndex).toBe(index - 1);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const REMOTE_SNAPSHOT = 'trace-timeline-remote.dx.json';
|
|
79
|
+
const MULTI_SNAPSHOT = 'trace-timeline-multiple.dx.json';
|
|
80
|
+
|
|
81
|
+
interface RawMessage {
|
|
82
|
+
meta?: Trace.Meta;
|
|
83
|
+
isEphemeral?: boolean;
|
|
84
|
+
events?: Array<{ type: string; timestamp: number; data: unknown }>;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const loadRemoteTraceMessages = (filename: string): Trace.Message[] => {
|
|
88
|
+
const snapshotPath = join(__dirname, '../testing/data', filename);
|
|
89
|
+
const json = JSON.parse(readFileSync(snapshotPath, 'utf8')) as unknown;
|
|
90
|
+
|
|
91
|
+
const messages: Trace.Message[] = [];
|
|
92
|
+
const visit = (node: unknown): void => {
|
|
93
|
+
if (Array.isArray(node)) {
|
|
94
|
+
node.forEach(visit);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (node === null || typeof node !== 'object') {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
const candidate = node as RawMessage & Record<string, unknown>;
|
|
101
|
+
if (Array.isArray(candidate.events) && candidate.meta && typeof candidate.meta === 'object') {
|
|
102
|
+
messages.push(
|
|
103
|
+
Obj.make(Trace.Message, {
|
|
104
|
+
meta: candidate.meta,
|
|
105
|
+
isEphemeral: candidate.isEphemeral ?? false,
|
|
106
|
+
events: candidate.events,
|
|
107
|
+
}),
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
for (const value of Object.values(node as Record<string, unknown>)) {
|
|
111
|
+
visit(value);
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
visit(json);
|
|
115
|
+
return messages;
|
|
116
|
+
};
|