@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.
- package/README.md +12 -5
- package/package.json +9 -4
- package/.github/FUNDING.yml +0 -4
- package/.github/workflows/test.yml +0 -57
- package/.pi/autoresearch/session-id +0 -1
- package/.pi/fabric/mesh/actors/actors.json +0 -4
- package/.prettierrc +0 -7
- package/commitlint.config.cjs +0 -1
- package/extensions/pi-tps/__tests__/cost-rate.test.ts +0 -503
- package/extensions/pi-tps/__tests__/dynamic-tps-cap.test.ts +0 -390
- package/extensions/pi-tps/__tests__/export-command.test.ts +0 -307
- package/extensions/pi-tps/__tests__/extension-setup.test.ts +0 -41
- package/extensions/pi-tps/__tests__/format-duration.test.ts +0 -83
- package/extensions/pi-tps/__tests__/helpers.ts +0 -177
- package/extensions/pi-tps/__tests__/precision-timing.test.ts +0 -701
- package/extensions/pi-tps/__tests__/rehydration.test.ts +0 -282
- package/extensions/pi-tps/__tests__/session-export.test.ts +0 -204
- package/extensions/pi-tps/__tests__/stall-detection.test.ts +0 -209
- package/extensions/pi-tps/__tests__/stall-reduction.test.ts +0 -139
- package/extensions/pi-tps/__tests__/telemetry-flow.test.ts +0 -654
- package/extensions/pi-tps/__tests__/volume-gate.test.ts +0 -372
- package/knip.json +0 -10
- package/npm-shrinkwrap.json +0 -6900
- package/pnpm-workspace.yaml +0 -9
- package/tsconfig.json +0 -24
- package/vitest.config.ts +0 -15
|
@@ -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
|
-
});
|