@monotykamary/pi-tps 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/FUNDING.yml +4 -0
- package/.github/workflows/test.yml +55 -0
- package/.pi/autoresearch/session-id +1 -0
- package/.prettierrc +7 -0
- package/LICENSE +21 -0
- package/README.md +237 -0
- package/commitlint.config.cjs +1 -0
- package/extensions/pi-tps/__tests__/export-command.test.ts +307 -0
- package/extensions/pi-tps/__tests__/extension-setup.test.ts +41 -0
- package/extensions/pi-tps/__tests__/format-duration.test.ts +83 -0
- package/extensions/pi-tps/__tests__/helpers.ts +154 -0
- package/extensions/pi-tps/__tests__/precision-timing.test.ts +701 -0
- package/extensions/pi-tps/__tests__/rehydration.test.ts +266 -0
- package/extensions/pi-tps/__tests__/session-export.test.ts +204 -0
- package/extensions/pi-tps/__tests__/stall-detection.test.ts +209 -0
- package/extensions/pi-tps/__tests__/stall-reduction.test.ts +139 -0
- package/extensions/pi-tps/__tests__/telemetry-flow.test.ts +654 -0
- package/extensions/pi-tps/index.ts +734 -0
- package/knip.json +10 -0
- package/npm-shrinkwrap.json +6923 -0
- package/package.json +54 -0
- package/tsconfig.json +24 -0
- package/vitest.config.ts +15 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
|
|
3
|
+
describe('formatDuration', () => {
|
|
4
|
+
const importFormatDuration = async () => {
|
|
5
|
+
const mod = await import('../index.js');
|
|
6
|
+
return mod.formatDuration;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
it('formats sub-minute durations with 1 decimal', async () => {
|
|
10
|
+
const formatDuration = await importFormatDuration();
|
|
11
|
+
expect(formatDuration(0.8)).toBe('0.8s');
|
|
12
|
+
expect(formatDuration(1.0)).toBe('1.0s');
|
|
13
|
+
expect(formatDuration(2.3)).toBe('2.3s');
|
|
14
|
+
expect(formatDuration(9.9)).toBe('9.9s');
|
|
15
|
+
expect(formatDuration(10.5)).toBe('10.5s');
|
|
16
|
+
expect(formatDuration(45.0)).toBe('45.0s');
|
|
17
|
+
expect(formatDuration(59.4)).toBe('59.4s');
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('formats minute+ durations as minutes and seconds', async () => {
|
|
21
|
+
const formatDuration = await importFormatDuration();
|
|
22
|
+
expect(formatDuration(60)).toBe('1m 0s');
|
|
23
|
+
expect(formatDuration(90)).toBe('1m 30s');
|
|
24
|
+
expect(formatDuration(300)).toBe('5m 0s');
|
|
25
|
+
expect(formatDuration(323)).toBe('5m 23s');
|
|
26
|
+
expect(formatDuration(3599)).toBe('59m 59s');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('formats hour+ durations as hours and minutes', async () => {
|
|
30
|
+
const formatDuration = await importFormatDuration();
|
|
31
|
+
expect(formatDuration(3600)).toBe('1h 0m');
|
|
32
|
+
expect(formatDuration(4500)).toBe('1h 15m');
|
|
33
|
+
expect(formatDuration(7200)).toBe('2h 0m');
|
|
34
|
+
expect(formatDuration(86399)).toBe('23h 59m');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('formats day+ durations as days and hours', async () => {
|
|
38
|
+
const formatDuration = await importFormatDuration();
|
|
39
|
+
expect(formatDuration(86400)).toBe('1d 0h');
|
|
40
|
+
expect(formatDuration(129600)).toBe('1d 12h');
|
|
41
|
+
expect(formatDuration(172800)).toBe('2d 0h');
|
|
42
|
+
expect(formatDuration(302400)).toBe('3d 12h');
|
|
43
|
+
expect(formatDuration(518400)).toBe('6d 0h');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('formats week+ durations as weeks and days', async () => {
|
|
47
|
+
const formatDuration = await importFormatDuration();
|
|
48
|
+
expect(formatDuration(604800)).toBe('1w 0d');
|
|
49
|
+
expect(formatDuration(907200)).toBe('1w 3d');
|
|
50
|
+
expect(formatDuration(1209600)).toBe('2w 0d');
|
|
51
|
+
expect(formatDuration(1814400)).toBe('3w 0d');
|
|
52
|
+
expect(formatDuration(2419200)).toBe('4w 0d');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('formats month+ durations as months and days', async () => {
|
|
56
|
+
const formatDuration = await importFormatDuration();
|
|
57
|
+
expect(formatDuration(2592000)).toBe('1mo 0d');
|
|
58
|
+
expect(formatDuration(2851200)).toBe('1mo 3d');
|
|
59
|
+
expect(formatDuration(5184000)).toBe('2mo 0d');
|
|
60
|
+
expect(formatDuration(7776000)).toBe('3mo 0d');
|
|
61
|
+
expect(formatDuration(9504000)).toBe('3mo 2w');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('formats year+ durations as years and days', async () => {
|
|
65
|
+
const formatDuration = await importFormatDuration();
|
|
66
|
+
expect(formatDuration(31536000)).toBe('1y 0d');
|
|
67
|
+
expect(formatDuration(31622400)).toBe('1y 1d');
|
|
68
|
+
expect(formatDuration(34128000)).toBe('1y 1mo');
|
|
69
|
+
expect(formatDuration(63072000)).toBe('2y 0d');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('handles large multi-year durations', async () => {
|
|
73
|
+
const formatDuration = await importFormatDuration();
|
|
74
|
+
expect(formatDuration(15552000)).toBe('6mo 0d');
|
|
75
|
+
expect(formatDuration(315360000)).toBe('10y 0d');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('rounds correctly for multi-unit durations', async () => {
|
|
79
|
+
const formatDuration = await importFormatDuration();
|
|
80
|
+
expect(formatDuration(89.9)).toBe('1m 30s');
|
|
81
|
+
expect(formatDuration(90.1)).toBe('1m 30s');
|
|
82
|
+
});
|
|
83
|
+
});
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { vi } from 'vitest';
|
|
2
|
+
import type { ExtensionAPI, ExtensionContext } from '@earendil-works/pi-coding-agent';
|
|
3
|
+
import type { AssistantMessage } from '@earendil-works/pi-ai';
|
|
4
|
+
|
|
5
|
+
// ─── Event types (mirrors extension/index.ts — not exported from pi's public API) ────
|
|
6
|
+
|
|
7
|
+
export interface TurnStartEvent {
|
|
8
|
+
type: 'turn_start';
|
|
9
|
+
turnIndex: number;
|
|
10
|
+
timestamp: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface TurnEndEvent {
|
|
14
|
+
type: 'turn_end';
|
|
15
|
+
turnIndex: number;
|
|
16
|
+
message: unknown;
|
|
17
|
+
toolResults: unknown[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface MessageStartEvent {
|
|
21
|
+
type: 'message_start';
|
|
22
|
+
message: unknown;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface MessageUpdateEvent {
|
|
26
|
+
type: 'message_update';
|
|
27
|
+
message: unknown;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface MessageEndEvent {
|
|
31
|
+
type: 'message_end';
|
|
32
|
+
message: unknown;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
/** Advance real time by ms (for integration-style timing tests) */
|
|
38
|
+
export const tick = (ms = 10) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
39
|
+
|
|
40
|
+
/** Create a standard AssistantMessage with overridable fields */
|
|
41
|
+
export function makeAssistantMessage(
|
|
42
|
+
overrides: { output?: number; input?: number; provider?: string; model?: string } = {}
|
|
43
|
+
): AssistantMessage {
|
|
44
|
+
const { output = 20, input = 10, provider = 'openai', model = 'gpt-4' } = overrides;
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
role: 'assistant',
|
|
48
|
+
content: [{ type: 'text' as const, text: 'Hello' }],
|
|
49
|
+
api: 'openai-completions',
|
|
50
|
+
provider,
|
|
51
|
+
model,
|
|
52
|
+
usage: {
|
|
53
|
+
input,
|
|
54
|
+
output,
|
|
55
|
+
cacheRead: 0,
|
|
56
|
+
cacheWrite: 0,
|
|
57
|
+
totalTokens: input + output,
|
|
58
|
+
cost: {
|
|
59
|
+
input: 0.001,
|
|
60
|
+
output: 0.002,
|
|
61
|
+
cacheRead: 0,
|
|
62
|
+
cacheWrite: 0,
|
|
63
|
+
total: 0.003,
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
stopReason: 'stop',
|
|
67
|
+
timestamp: Date.now(),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ─── Mock setup ──────────────────────────────────────────────────────────────
|
|
72
|
+
|
|
73
|
+
export interface TestFixture {
|
|
74
|
+
mockPi: Partial<ExtensionAPI>;
|
|
75
|
+
handlers: Record<string, (...args: unknown[]) => void>;
|
|
76
|
+
commands: Record<string, { handler: (args: string, ctx: any) => Promise<void> }>;
|
|
77
|
+
notifySpy: ReturnType<typeof vi.fn>;
|
|
78
|
+
appendEntrySpy: ReturnType<typeof vi.fn>;
|
|
79
|
+
eventsEmitSpy: ReturnType<typeof vi.fn>;
|
|
80
|
+
registerCommandSpy: ReturnType<typeof vi.fn>;
|
|
81
|
+
mockEntries: Array<{ type?: string; role?: string; customType?: string; data?: unknown }>;
|
|
82
|
+
mockCtx: ExtensionContext;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Create a fresh set of mocks for one test.
|
|
87
|
+
* Call `activateExtension()` on the result to wire up the extension.
|
|
88
|
+
*/
|
|
89
|
+
export function createTestFixture(): TestFixture {
|
|
90
|
+
const handlers: Record<string, (...args: unknown[]) => void> = {};
|
|
91
|
+
const commands: Record<string, { handler: (args: string, ctx: any) => Promise<void> }> = {};
|
|
92
|
+
const notifySpy = vi.fn();
|
|
93
|
+
const appendEntrySpy = vi.fn();
|
|
94
|
+
const registerCommandSpy = vi.fn((name: string, options: any) => {
|
|
95
|
+
commands[name] = options;
|
|
96
|
+
});
|
|
97
|
+
const mockEntries: Array<{
|
|
98
|
+
type?: string;
|
|
99
|
+
role?: string;
|
|
100
|
+
customType?: string;
|
|
101
|
+
data?: unknown;
|
|
102
|
+
}> = [];
|
|
103
|
+
|
|
104
|
+
const mockCtx = {
|
|
105
|
+
hasUI: true,
|
|
106
|
+
ui: { notify: notifySpy } as any,
|
|
107
|
+
sessionManager: {
|
|
108
|
+
getEntries: vi.fn().mockReturnValue(mockEntries),
|
|
109
|
+
getBranch: vi.fn(),
|
|
110
|
+
getSessionId: vi.fn(),
|
|
111
|
+
},
|
|
112
|
+
modelRegistry: undefined as any,
|
|
113
|
+
model: undefined,
|
|
114
|
+
cwd: '/tmp',
|
|
115
|
+
isIdle: vi.fn(),
|
|
116
|
+
signal: undefined,
|
|
117
|
+
abort: vi.fn(),
|
|
118
|
+
hasPendingMessages: vi.fn(),
|
|
119
|
+
shutdown: vi.fn(),
|
|
120
|
+
getContextUsage: vi.fn(),
|
|
121
|
+
compact: vi.fn(),
|
|
122
|
+
getSystemPrompt: vi.fn(),
|
|
123
|
+
} as any as ExtensionContext;
|
|
124
|
+
|
|
125
|
+
const eventsEmitSpy = vi.fn();
|
|
126
|
+
|
|
127
|
+
const mockPi: Partial<ExtensionAPI> = {
|
|
128
|
+
on: vi.fn((event: string, handler: (...args: unknown[]) => void) => {
|
|
129
|
+
handlers[event] = handler;
|
|
130
|
+
return mockPi as ExtensionAPI;
|
|
131
|
+
}),
|
|
132
|
+
appendEntry: appendEntrySpy,
|
|
133
|
+
registerCommand: registerCommandSpy,
|
|
134
|
+
events: { emit: eventsEmitSpy, on: vi.fn() },
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
mockPi,
|
|
139
|
+
handlers,
|
|
140
|
+
commands,
|
|
141
|
+
notifySpy,
|
|
142
|
+
appendEntrySpy,
|
|
143
|
+
eventsEmitSpy,
|
|
144
|
+
registerCommandSpy,
|
|
145
|
+
mockEntries,
|
|
146
|
+
mockCtx,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Import the extension module and wire it to the test fixture's mockPi */
|
|
151
|
+
export async function activateExtension(fixture: TestFixture) {
|
|
152
|
+
const { default: tpsExtension } = await import('../index.js');
|
|
153
|
+
tpsExtension(fixture.mockPi as ExtensionAPI);
|
|
154
|
+
}
|