@monotykamary/pi-tps 1.3.4 → 1.3.6

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.
@@ -1,390 +0,0 @@
1
- import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
- import type { AssistantMessage } from '@earendil-works/pi-ai';
3
- import { createTestFixture, activateExtension } from './helpers';
4
-
5
- describe('pi-tps extension — dynamic TPS cap', () => {
6
- let fixture: ReturnType<typeof createTestFixture>;
7
-
8
- beforeEach(async () => {
9
- fixture = createTestFixture();
10
- await activateExtension(fixture);
11
- });
12
-
13
- afterEach(() => {
14
- vi.restoreAllMocks();
15
- });
16
-
17
- /**
18
- * Drive a turn with mocked performance.now() timestamps.
19
- * Set `isToolCall: true` to simulate a tool_execution_start during the turn.
20
- */
21
- function driveTurn(clocks: {
22
- turnStart: number;
23
- messageStart: number;
24
- firstUpdate: number;
25
- streamUpdates: number[];
26
- messageEnd: number;
27
- turnEnd?: number;
28
- isToolCall?: boolean;
29
- }) {
30
- const { handlers, notifySpy, appendEntrySpy } = fixture;
31
-
32
- const timestamps = [
33
- clocks.turnStart,
34
- clocks.turnStart,
35
- clocks.messageStart,
36
- clocks.firstUpdate,
37
- ...clocks.streamUpdates,
38
- clocks.messageEnd,
39
- clocks.turnEnd ?? clocks.messageEnd,
40
- ];
41
-
42
- let callIdx = 0;
43
- const spy = vi.spyOn(performance, 'now').mockImplementation(() => {
44
- return timestamps[Math.min(callIdx++, timestamps.length - 1)];
45
- });
46
-
47
- const assistantMessage: AssistantMessage = {
48
- role: 'assistant',
49
- content: [{ type: 'text', text: 'Response' }],
50
- api: 'openai-completions',
51
- provider: 'openai',
52
- model: 'gpt-4',
53
- usage: {
54
- input: 50,
55
- output: 20,
56
- cacheRead: 0,
57
- cacheWrite: 0,
58
- totalTokens: 70,
59
- cost: { input: 0.001, output: 0.002, cacheRead: 0, cacheWrite: 0, total: 0.003 },
60
- },
61
- stopReason: clocks.isToolCall ? 'toolUse' : 'stop',
62
- timestamp: Date.now(),
63
- };
64
-
65
- handlers['turn_start']?.({ type: 'turn_start', turnIndex: 0, timestamp: Date.now() });
66
- handlers['message_start']?.({ type: 'message_start', message: assistantMessage });
67
- handlers['message_update']?.({
68
- type: 'message_update',
69
- message: assistantMessage,
70
- assistantMessageEvent: { type: 'text_delta', delta: 't' },
71
- });
72
- for (const _ts of clocks.streamUpdates) {
73
- handlers['message_update']?.({
74
- type: 'message_update',
75
- message: assistantMessage,
76
- assistantMessageEvent: { type: 'text_delta', delta: 't' },
77
- });
78
- }
79
-
80
- // Simulate tool_execution_start if this is a tool call turn
81
- if (clocks.isToolCall) {
82
- handlers['tool_execution_start']?.({
83
- type: 'tool_execution_start',
84
- toolCallId: 'call_123',
85
- toolName: 'bash',
86
- args: { command: 'ls' },
87
- });
88
- }
89
-
90
- handlers['message_end']?.({ type: 'message_end', message: assistantMessage });
91
- handlers['turn_end']?.(
92
- { type: 'turn_end', turnIndex: 0, message: assistantMessage, toolResults: [] },
93
- fixture.mockCtx
94
- );
95
-
96
- spy.mockRestore();
97
- return { notifySpy, appendEntrySpy };
98
- }
99
-
100
- // ── Cap is set by reliable streaming turns ────────────────────────────────
101
-
102
- it('should set the TPS cap from a reliable streaming turn (primary branch, no tool call)', () => {
103
- // 20 tokens / 0.4s = 50 TPS from primary branch
104
- const { appendEntrySpy } = driveTurn({
105
- turnStart: 0,
106
- messageStart: 200,
107
- firstUpdate: 200.123,
108
- streamUpdates: [400, 500, 600, 700, 800],
109
- messageEnd: 900,
110
- });
111
-
112
- const [, data] = appendEntrySpy.mock.calls[0];
113
- // TPS should be ~50, and isPrimaryBranch should be true
114
- expect(data.tps).toBeGreaterThanOrEqual(40);
115
- expect(data.tps).toBeLessThanOrEqual(60);
116
- expect(data.isPrimaryBranch).toBe(true);
117
- });
118
-
119
- // ── Cap is applied to tool-call turns ─────────────────────────────────────
120
-
121
- it('should clamp tool-call TPS to the cap set by a prior streaming turn', () => {
122
- // Turn 1: reliable streaming response → sets cap at ~50 TPS
123
- driveTurn({
124
- turnStart: 0,
125
- messageStart: 200,
126
- firstUpdate: 200.123,
127
- streamUpdates: [400, 500, 600, 700, 800],
128
- messageEnd: 900,
129
- });
130
-
131
- // Turn 2: tool call with fallback TPS (2 updates, 250ms generationMs)
132
- // Without cap: 20 tokens / 0.25s ≈ 80 TPS (feasible but from short window)
133
- // With cap: min(80, 50) = 50 TPS
134
- const { appendEntrySpy, notifySpy } = driveTurn({
135
- turnStart: 0,
136
- messageStart: 100,
137
- firstUpdate: 100.1,
138
- streamUpdates: [100.15, 100.3],
139
- messageEnd: 350,
140
- isToolCall: true,
141
- });
142
-
143
- const [, data] = appendEntrySpy.mock.calls[1];
144
- expect(data.tps).not.toBeNull();
145
- // Must be clamped to the ~50 TPS cap, not the inflated fallback value
146
- expect(data.tps).toBeLessThanOrEqual(55);
147
- expect(data.tps).toBeGreaterThan(0);
148
- });
149
-
150
- // ── Tool calls do not set the cap from fallback ─────────────────────────────
151
-
152
- it('should not let fallback-branch tool-call turns set the cap', () => {
153
- // Turn 1: tool call with fallback TPS — should NOT set the cap
154
- const { appendEntrySpy: spy1 } = driveTurn({
155
- turnStart: 0,
156
- messageStart: 100,
157
- firstUpdate: 100.1,
158
- streamUpdates: [100.15, 100.3],
159
- messageEnd: 350,
160
- isToolCall: true,
161
- });
162
- const [, data1] = spy1.mock.calls[0];
163
- // No cap → fallback tool call TPS is null
164
- expect(data1.tps).toBeNull();
165
-
166
- // Turn 2: reliable streaming response at ~50 TPS → sets the cap
167
- const { appendEntrySpy: spy2 } = driveTurn({
168
- turnStart: 0,
169
- messageStart: 200,
170
- firstUpdate: 200.123,
171
- streamUpdates: [400, 500, 600, 700, 800],
172
- messageEnd: 900,
173
- });
174
- const [, data2] = spy2.mock.calls[1];
175
- expect(data2.tps).toBeGreaterThanOrEqual(40);
176
- expect(data2.tps).toBeLessThanOrEqual(60);
177
-
178
- // Turn 3: another fallback tool call — should now be clamped to 50
179
- const { appendEntrySpy: spy3 } = driveTurn({
180
- turnStart: 0,
181
- messageStart: 100,
182
- firstUpdate: 100.1,
183
- streamUpdates: [100.15, 100.3],
184
- messageEnd: 350,
185
- isToolCall: true,
186
- });
187
- const [, data3] = spy3.mock.calls[2];
188
- expect(data3.tps).not.toBeNull();
189
- expect(data3.tps).toBeLessThanOrEqual(55);
190
- });
191
-
192
- // ── Primary-branch tool calls (reasoning) set the cap ──────────────────────
193
-
194
- it('should let primary-branch tool-call turns set the cap (e.g. reasoning before tool call)', () => {
195
- // Turn 1: tool call with PRIMARY-branch TPS (reasoning + tool call, enough updates/time)
196
- // 20 tokens / 0.4s = 50 TPS from primary branch, isToolCall = true
197
- const { appendEntrySpy: spy1 } = driveTurn({
198
- turnStart: 0,
199
- messageStart: 200,
200
- firstUpdate: 200.123,
201
- streamUpdates: [400, 500, 600, 700, 800],
202
- messageEnd: 900,
203
- isToolCall: true,
204
- });
205
- const [, data1] = spy1.mock.calls[0];
206
- // Primary branch + isToolCall → TPS is still computed (not null/capped)
207
- expect(data1.tps).toBeGreaterThanOrEqual(40);
208
- expect(data1.tps).toBeLessThanOrEqual(60);
209
- expect(data1.isPrimaryBranch).toBe(true);
210
-
211
- // Turn 2: fallback tool call — should be clamped to the cap from turn 1
212
- const { appendEntrySpy: spy2 } = driveTurn({
213
- turnStart: 0,
214
- messageStart: 100,
215
- firstUpdate: 100.1,
216
- streamUpdates: [100.15, 100.3],
217
- messageEnd: 350,
218
- isToolCall: true,
219
- });
220
- const [, data2] = spy2.mock.calls[1];
221
- expect(data2.tps).not.toBeNull();
222
- // Clamped to ~50 cap set by the primary-branch tool call in turn 1
223
- expect(data2.tps).toBeLessThanOrEqual(55);
224
- });
225
-
226
- // ── Cold start: no cap yet ────────────────────────────────────────────────
227
-
228
- it('should show null TPS for tool calls when no cap exists yet', () => {
229
- const { notifySpy, appendEntrySpy } = driveTurn({
230
- turnStart: 0,
231
- messageStart: 100,
232
- firstUpdate: 100.1,
233
- streamUpdates: [100.15, 100.3],
234
- messageEnd: 350,
235
- isToolCall: true,
236
- });
237
-
238
- const notification = notifySpy.mock.calls[0][0] as string;
239
- // No streaming turn has set the cap yet → tool call TPS is null
240
- expect(notification).toContain('TPS —');
241
-
242
- const [, data] = appendEntrySpy.mock.calls[0];
243
- expect(data.tps).toBeNull();
244
- });
245
-
246
- // ── Non-tool-call fallback turns are not clamped ──────────────────────────
247
-
248
- it('should not clamp non-tool-call fallback TPS', () => {
249
- // Turn 1: set cap at ~50 TPS from a reliable streaming turn
250
- driveTurn({
251
- turnStart: 0,
252
- messageStart: 200,
253
- firstUpdate: 200.123,
254
- streamUpdates: [400, 500, 600, 700, 800],
255
- messageEnd: 900,
256
- });
257
-
258
- // Turn 2: non-tool-call fallback (e.g. short burst response)
259
- // This should NOT be clamped — only tool calls get capped
260
- const { appendEntrySpy } = driveTurn({
261
- turnStart: 0,
262
- messageStart: 100,
263
- firstUpdate: 100.1,
264
- streamUpdates: [100.15, 100.3],
265
- messageEnd: 350,
266
- isToolCall: false,
267
- });
268
-
269
- const [, data] = appendEntrySpy.mock.calls[1];
270
- expect(data.tps).not.toBeNull();
271
- // Non-tool-call fallback TPS is uncapped — may be high
272
- expect(data.tps).toBeGreaterThan(50);
273
- });
274
-
275
- // ── Cap is per-model ──────────────────────────────────────────────────────
276
-
277
- it('should maintain separate caps per model', () => {
278
- // Turn 1: openai/gpt-4 streaming → sets cap at ~50 TPS
279
- driveTurn({
280
- turnStart: 0,
281
- messageStart: 200,
282
- firstUpdate: 200.123,
283
- streamUpdates: [400, 500, 600, 700, 800],
284
- messageEnd: 900,
285
- });
286
-
287
- // Turn 2: deepseek/deepseek-v3 tool call → no cap for deepseek yet, uncapped
288
- // Use driveTurn with a different provider/model to avoid the gpt-4 cap
289
- const { handlers, appendEntrySpy } = fixture;
290
- const deepseek: AssistantMessage = {
291
- role: 'assistant',
292
- content: [{ type: 'text', text: 'Hi' }],
293
- api: 'openai-completions',
294
- provider: 'deepseek',
295
- model: 'deepseek-v3',
296
- usage: {
297
- input: 50,
298
- output: 20,
299
- cacheRead: 0,
300
- cacheWrite: 0,
301
- totalTokens: 70,
302
- cost: { input: 0.001, output: 0.002, cacheRead: 0, cacheWrite: 0, total: 0.003 },
303
- },
304
- stopReason: 'toolUse',
305
- timestamp: Date.now(),
306
- };
307
-
308
- let callIdx = 0;
309
- const timestamps = [0, 0, 100, 100.1, 100.15, 100.3, 300, 300];
310
- const spy = vi.spyOn(performance, 'now').mockImplementation(() => {
311
- return timestamps[Math.min(callIdx++, timestamps.length - 1)];
312
- });
313
-
314
- handlers['turn_start']?.({ type: 'turn_start', turnIndex: 1, timestamp: Date.now() });
315
- handlers['message_start']?.({ type: 'message_start', message: deepseek });
316
- handlers['message_update']?.({
317
- type: 'message_update',
318
- message: deepseek,
319
- assistantMessageEvent: { type: 'text_delta', delta: 't' },
320
- });
321
- handlers['message_update']?.({
322
- type: 'message_update',
323
- message: deepseek,
324
- assistantMessageEvent: { type: 'text_delta', delta: 't' },
325
- });
326
- handlers['message_update']?.({
327
- type: 'message_update',
328
- message: deepseek,
329
- assistantMessageEvent: { type: 'text_delta', delta: 't' },
330
- });
331
- handlers['tool_execution_start']?.({
332
- type: 'tool_execution_start',
333
- toolCallId: 'call_1',
334
- toolName: 'bash',
335
- args: {},
336
- });
337
- handlers['message_end']?.({ type: 'message_end', message: deepseek });
338
- handlers['turn_end']?.(
339
- { type: 'turn_end', turnIndex: 1, message: deepseek, toolResults: [] },
340
- fixture.mockCtx
341
- );
342
- spy.mockRestore();
343
-
344
- const [, data2] = appendEntrySpy.mock.calls[1];
345
- // DeepSeek has no cap yet → tool call TPS is null
346
- expect(data2.tps).toBeNull();
347
- });
348
-
349
- // ── Cap only goes up ──────────────────────────────────────────────────────
350
-
351
- it('should only raise the cap, never lower it', () => {
352
- // Turn 1: sets cap at ~50 TPS
353
- driveTurn({
354
- turnStart: 0,
355
- messageStart: 200,
356
- firstUpdate: 200.123,
357
- streamUpdates: [400, 500, 600, 700, 800],
358
- messageEnd: 900,
359
- });
360
-
361
- // Turn 2: slower streaming response at ~25 TPS → cap stays at 50
362
- const { appendEntrySpy } = driveTurn({
363
- turnStart: 0,
364
- messageStart: 200,
365
- firstUpdate: 200.123,
366
- streamUpdates: [600, 800, 1000, 1200, 1400],
367
- messageEnd: 1500,
368
- });
369
-
370
- const [, data2] = appendEntrySpy.mock.calls[1];
371
- // This turn's TPS is 25, but the cap should still be 50
372
- expect(data2.tps).toBeGreaterThanOrEqual(15);
373
- expect(data2.tps).toBeLessThanOrEqual(35);
374
-
375
- // Turn 3: tool call → should be capped at 50, not 25
376
- const { appendEntrySpy: spy3 } = driveTurn({
377
- turnStart: 0,
378
- messageStart: 100,
379
- firstUpdate: 100.1,
380
- streamUpdates: [100.15, 100.3],
381
- messageEnd: 350,
382
- isToolCall: true,
383
- });
384
-
385
- const [, data3] = spy3.mock.calls[2];
386
- expect(data3.tps).not.toBeNull();
387
- // Capped at 50 (the higher of the two streaming measurements)
388
- expect(data3.tps).toBeLessThanOrEqual(55);
389
- });
390
- });
@@ -1,307 +0,0 @@
1
- import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
- import type { ExtensionCommandContext } from '@earendil-works/pi-coding-agent';
3
- import { unlinkSync, existsSync } from 'fs';
4
- import { createTestFixture, activateExtension, tick } from './helpers';
5
-
6
- vi.mock('child_process', () => ({ execSync: vi.fn() }));
7
-
8
- describe('pi-tps extension — export command', () => {
9
- let fixture: ReturnType<typeof createTestFixture>;
10
-
11
- const branchEntries = [
12
- {
13
- type: 'custom',
14
- customType: 'tps',
15
- data: { tps: 10 },
16
- id: '1',
17
- parentId: null,
18
- timestamp: '2026-01-01T00:00:00Z',
19
- },
20
- {
21
- type: 'custom',
22
- customType: 'neuralwatt-energy',
23
- data: { energy_joules: 100 },
24
- id: '2',
25
- parentId: null,
26
- timestamp: '2026-01-01T00:00:01Z',
27
- },
28
- { type: 'message', role: 'user', content: 'hello' },
29
- ];
30
-
31
- const allEntries = [
32
- ...branchEntries,
33
- {
34
- type: 'custom',
35
- customType: 'tps',
36
- data: { tps: 20 },
37
- id: '3',
38
- parentId: null,
39
- timestamp: '2026-01-01T00:00:02Z',
40
- },
41
- ];
42
-
43
- beforeEach(async () => {
44
- fixture = createTestFixture();
45
- await activateExtension(fixture);
46
- });
47
-
48
- afterEach(() => {
49
- // Clean up any pi-telemetry files written by the export handler
50
- for (const call of fixture.notifySpy.mock.calls) {
51
- const msg = call[0] as string;
52
- if (typeof msg === 'string' && msg.includes('→ ')) {
53
- const filepath = msg.split('→ ')[1];
54
- if (filepath && existsSync(filepath)) {
55
- try {
56
- unlinkSync(filepath);
57
- } catch {
58
- /* ignore */
59
- }
60
- }
61
- }
62
- }
63
- vi.restoreAllMocks();
64
- });
65
-
66
- it('should export current branch custom entries by default', async () => {
67
- const exportCtx = {
68
- ...fixture.mockCtx,
69
- sessionManager: {
70
- getBranch: vi.fn().mockReturnValue(branchEntries),
71
- getSessionId: vi.fn().mockReturnValue('test-session-id'),
72
- },
73
- } as ExtensionCommandContext;
74
-
75
- await fixture.commands['tps-export'].handler('', exportCtx);
76
-
77
- expect(fixture.notifySpy).toHaveBeenCalledOnce();
78
- const msg = fixture.notifySpy.mock.calls[0][0] as string;
79
- expect(msg).toContain('Exported 2 telemetry');
80
- expect(msg).toContain('pi-telemetry-branch-');
81
- expect(msg).toContain('/pi-telemetry/');
82
- });
83
-
84
- it('should export full session with --full flag', async () => {
85
- const exportCtx = {
86
- ...fixture.mockCtx,
87
- sessionManager: {
88
- getEntries: vi.fn().mockReturnValue(allEntries),
89
- getSessionId: vi.fn().mockReturnValue('test-session-id'),
90
- },
91
- } as ExtensionCommandContext;
92
-
93
- await fixture.commands['tps-export'].handler('--full', exportCtx);
94
-
95
- expect(fixture.notifySpy).toHaveBeenCalledOnce();
96
- const msg = fixture.notifySpy.mock.calls[0][0] as string;
97
- expect(msg).toContain('Exported 3 telemetry');
98
- expect(msg).toContain('pi-telemetry-full-');
99
- });
100
-
101
- it('should combine --full with customType filter', async () => {
102
- const exportCtx = {
103
- ...fixture.mockCtx,
104
- sessionManager: {
105
- getEntries: vi.fn().mockReturnValue(allEntries),
106
- getSessionId: vi.fn().mockReturnValue('test-session-id'),
107
- },
108
- } as ExtensionCommandContext;
109
-
110
- await fixture.commands['tps-export'].handler('tps --full', exportCtx);
111
-
112
- expect(fixture.notifySpy).toHaveBeenCalledOnce();
113
- const msg = fixture.notifySpy.mock.calls[0][0] as string;
114
- expect(msg).toContain('Exported 2 telemetry');
115
- expect(msg).toContain('pi-telemetry-full-tps-');
116
- });
117
-
118
- it('should filter branch by customType', async () => {
119
- const exportCtx = {
120
- ...fixture.mockCtx,
121
- sessionManager: {
122
- getBranch: vi.fn().mockReturnValue(branchEntries),
123
- getSessionId: vi.fn().mockReturnValue('test-session-id'),
124
- },
125
- } as ExtensionCommandContext;
126
-
127
- await fixture.commands['tps-export'].handler('tps', exportCtx);
128
-
129
- expect(fixture.notifySpy).toHaveBeenCalledOnce();
130
- const msg = fixture.notifySpy.mock.calls[0][0] as string;
131
- expect(msg).toContain('Exported 1 telemetry');
132
- expect(msg).toContain('pi-telemetry-branch-tps-');
133
- });
134
-
135
- it('should show warning when no matching entries found', async () => {
136
- const exportCtx = {
137
- ...fixture.mockCtx,
138
- sessionManager: {
139
- getBranch: vi.fn().mockReturnValue([{ type: 'message', role: 'user', content: 'hello' }]),
140
- getSessionId: vi.fn().mockReturnValue('test-session-id'),
141
- },
142
- } as ExtensionCommandContext;
143
-
144
- await fixture.commands['tps-export'].handler('nonexistent', exportCtx);
145
-
146
- expect(fixture.notifySpy).toHaveBeenCalledOnce();
147
- const msg = fixture.notifySpy.mock.calls[0][0] as string;
148
- expect(msg).toContain('No matching entries found');
149
- expect(msg).toContain('current-branch');
150
- expect(fixture.notifySpy).toHaveBeenCalledWith(msg, 'warning');
151
- });
152
-
153
- it('should use exact customType match (neuralwatt-energy, not energy)', async () => {
154
- const exportCtx = {
155
- ...fixture.mockCtx,
156
- sessionManager: {
157
- getBranch: vi.fn().mockReturnValue([
158
- {
159
- type: 'custom',
160
- customType: 'neuralwatt-energy',
161
- data: { energy_joules: 100 },
162
- id: '1',
163
- parentId: null,
164
- timestamp: '2026-01-01T00:00:00Z',
165
- },
166
- {
167
- type: 'custom',
168
- customType: 'energy',
169
- data: { joules: 50 },
170
- id: '2',
171
- parentId: null,
172
- timestamp: '2026-01-01T00:00:01Z',
173
- },
174
- ]),
175
- getSessionId: vi.fn().mockReturnValue('test-session-id'),
176
- },
177
- } as ExtensionCommandContext;
178
-
179
- await fixture.commands['tps-export'].handler('neuralwatt-energy', exportCtx);
180
-
181
- expect(fixture.notifySpy).toHaveBeenCalledOnce();
182
- const msg = fixture.notifySpy.mock.calls[0][0] as string;
183
- expect(msg).toContain('Exported 1 telemetry');
184
- expect(msg).toContain('pi-telemetry-branch-neuralwatt-energy-');
185
- });
186
-
187
- it('should include model_change entries and re-chain parentIds', async () => {
188
- const entriesWithModelChange = [
189
- {
190
- type: 'model_change',
191
- id: 'mc1',
192
- parentId: null,
193
- timestamp: '2026-01-01T00:00:00Z',
194
- provider: 'test',
195
- modelId: 'test-model',
196
- },
197
- {
198
- type: 'message',
199
- id: 'msg1',
200
- parentId: 'mc1',
201
- timestamp: '2026-01-01T00:00:01Z',
202
- role: 'user',
203
- content: 'hello',
204
- },
205
- {
206
- type: 'custom',
207
- customType: 'tps',
208
- data: { tps: 10 },
209
- id: 'tps1',
210
- parentId: 'msg1',
211
- timestamp: '2026-01-01T00:00:02Z',
212
- },
213
- {
214
- type: 'message',
215
- id: 'msg2',
216
- parentId: 'tps1',
217
- timestamp: '2026-01-01T00:00:03Z',
218
- role: 'assistant',
219
- content: 'hi',
220
- },
221
- {
222
- type: 'model_change',
223
- id: 'mc2',
224
- parentId: 'msg2',
225
- timestamp: '2026-01-01T00:00:04Z',
226
- provider: 'other',
227
- modelId: 'other-model',
228
- },
229
- {
230
- type: 'custom',
231
- customType: 'tps',
232
- data: { tps: 20 },
233
- id: 'tps2',
234
- parentId: 'mc2',
235
- timestamp: '2026-01-01T00:00:05Z',
236
- },
237
- ];
238
- const exportCtx = {
239
- ...fixture.mockCtx,
240
- sessionManager: {
241
- getBranch: vi.fn().mockReturnValue(entriesWithModelChange),
242
- getSessionId: vi.fn().mockReturnValue('test-session-id'),
243
- },
244
- } as ExtensionCommandContext;
245
-
246
- await fixture.commands['tps-export'].handler('', exportCtx);
247
-
248
- expect(fixture.notifySpy).toHaveBeenCalledOnce();
249
- const msg = fixture.notifySpy.mock.calls[0][0] as string;
250
- expect(msg).toContain('2 telemetry + 2 structural');
251
-
252
- const filepath = msg.split('→ ')[1];
253
- const fs = await import('fs');
254
- const content = fs.readFileSync(filepath, 'utf8');
255
- const lines = content
256
- .trim()
257
- .split('\n')
258
- .map((l: string) => JSON.parse(l));
259
-
260
- expect(lines.find((l: any) => l.id === 'mc1').parentId).toBeNull();
261
- expect(lines.find((l: any) => l.id === 'tps1').parentId).toBe('mc1');
262
- expect(lines.find((l: any) => l.id === 'mc2').parentId).toBe('tps1');
263
- expect(lines.find((l: any) => l.id === 'tps2').parentId).toBe('mc2');
264
- });
265
-
266
- it('should include structural entries even with customType filter', async () => {
267
- const entriesWithModelChange = [
268
- {
269
- type: 'model_change',
270
- id: 'mc1',
271
- parentId: null,
272
- timestamp: '2026-01-01T00:00:00Z',
273
- provider: 'test',
274
- modelId: 'test-model',
275
- },
276
- {
277
- type: 'custom',
278
- customType: 'tps',
279
- data: { tps: 10 },
280
- id: 'tps1',
281
- parentId: 'mc1',
282
- timestamp: '2026-01-01T00:00:01Z',
283
- },
284
- {
285
- type: 'custom',
286
- customType: 'neuralwatt-energy',
287
- data: { energy_joules: 100 },
288
- id: 'ne1',
289
- parentId: 'tps1',
290
- timestamp: '2026-01-01T00:00:02Z',
291
- },
292
- ];
293
- const exportCtx = {
294
- ...fixture.mockCtx,
295
- sessionManager: {
296
- getBranch: vi.fn().mockReturnValue(entriesWithModelChange),
297
- getSessionId: vi.fn().mockReturnValue('test-session-id'),
298
- },
299
- } as ExtensionCommandContext;
300
-
301
- await fixture.commands['tps-export'].handler('tps', exportCtx);
302
-
303
- expect(fixture.notifySpy).toHaveBeenCalledOnce();
304
- const msg = fixture.notifySpy.mock.calls[0][0] as string;
305
- expect(msg).toContain('1 telemetry + 1 structural');
306
- });
307
- });