pi-var 1.0.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/.github/FUNDING.yml +4 -0
- package/.prettierrc +7 -0
- package/README.md +240 -0
- package/commitlint.config.cjs +3 -0
- package/extensions/pi-var/__tests__/integration/smoke.test.ts +308 -0
- package/extensions/pi-var/__tests__/unit/command.test.ts +322 -0
- package/extensions/pi-var/__tests__/unit/cow-detection.test.ts +340 -0
- package/extensions/pi-var/__tests__/unit/cow-git-merge.test.ts +404 -0
- package/extensions/pi-var/__tests__/unit/edr.test.ts +119 -0
- package/extensions/pi-var/__tests__/unit/environment.test.ts +369 -0
- package/extensions/pi-var/__tests__/unit/file-redirect.test.ts +295 -0
- package/extensions/pi-var/__tests__/unit/store.test.ts +489 -0
- package/extensions/pi-var/__tests__/unit/tools.test.ts +133 -0
- package/extensions/pi-var/__tests__/unit/variations.test.ts +352 -0
- package/extensions/pi-var/index.ts +111 -0
- package/extensions/pi-var/src/config.ts +30 -0
- package/extensions/pi-var/src/cow/clone.ts +46 -0
- package/extensions/pi-var/src/cow/detection.ts +215 -0
- package/extensions/pi-var/src/cow/index.ts +6 -0
- package/extensions/pi-var/src/edr/constants.ts +199 -0
- package/extensions/pi-var/src/edr/crowdstrike.ts +62 -0
- package/extensions/pi-var/src/edr/detection.ts +97 -0
- package/extensions/pi-var/src/edr/index.ts +6 -0
- package/extensions/pi-var/src/environment/copy-env.ts +79 -0
- package/extensions/pi-var/src/environment/detect.ts +91 -0
- package/extensions/pi-var/src/environment/index.ts +54 -0
- package/extensions/pi-var/src/environment/marker.ts +42 -0
- package/extensions/pi-var/src/environment/symlink.ts +82 -0
- package/extensions/pi-var/src/gitignore/manager.ts +77 -0
- package/extensions/pi-var/src/state/index.ts +7 -0
- package/extensions/pi-var/src/state/keys.ts +14 -0
- package/extensions/pi-var/src/state/store.ts +110 -0
- package/extensions/pi-var/src/state/types.ts +70 -0
- package/extensions/pi-var/src/tools/command/clean.ts +87 -0
- package/extensions/pi-var/src/tools/command/index.ts +95 -0
- package/extensions/pi-var/src/tools/command/status.ts +58 -0
- package/extensions/pi-var/src/tools/command/stop.ts +37 -0
- package/extensions/pi-var/src/tools/file-redirect.ts +126 -0
- package/extensions/pi-var/src/tools/redirect/bash.ts +75 -0
- package/extensions/pi-var/src/tools/redirect/edit.ts +60 -0
- package/extensions/pi-var/src/tools/redirect/index.ts +64 -0
- package/extensions/pi-var/src/tools/redirect/read.ts +61 -0
- package/extensions/pi-var/src/tools/redirect/write.ts +60 -0
- package/extensions/pi-var/src/tools/variation/create.ts +108 -0
- package/extensions/pi-var/src/tools/variation/merge.ts +80 -0
- package/extensions/pi-var/src/types/index.ts +139 -0
- package/extensions/pi-var/src/utils/names.ts +120 -0
- package/extensions/pi-var/src/variation/create-copy.ts +35 -0
- package/extensions/pi-var/src/variation/create-cow.ts +81 -0
- package/extensions/pi-var/src/variation/create-worktree.ts +31 -0
- package/extensions/pi-var/src/variation/create.ts +127 -0
- package/extensions/pi-var/src/variation/index.ts +9 -0
- package/extensions/pi-var/src/variation/merge/copy.ts +151 -0
- package/extensions/pi-var/src/variation/merge/git.ts +176 -0
- package/extensions/pi-var/src/variation/merge/index.ts +36 -0
- package/extensions/pi-var/src/variation/merge/rsync.ts +74 -0
- package/extensions/pi-var/src/variation/merge/strategy.ts +62 -0
- package/extensions/pi-var/src/variation/paths.ts +26 -0
- package/extensions/pi-var/src/variation/remove.ts +79 -0
- package/npm-shrinkwrap.json +6908 -0
- package/package.json +63 -0
- package/skills/variations/SKILL.md +182 -0
- package/tsconfig.json +25 -0
- package/vitest.config.ts +19 -0
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for RuntimeStore with session persistence
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
6
|
+
import type { RuntimeStore } from '../../src/state/types';
|
|
7
|
+
import {
|
|
8
|
+
createRuntimeStore,
|
|
9
|
+
getSessionKey,
|
|
10
|
+
type ExtensionContext,
|
|
11
|
+
type SessionManager,
|
|
12
|
+
} from '../../src/state/index';
|
|
13
|
+
import type { VarRuntime, Variation } from '../../src/types/index';
|
|
14
|
+
import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
|
|
15
|
+
|
|
16
|
+
describe('RuntimeStore', () => {
|
|
17
|
+
let store: ReturnType<typeof createRuntimeStore>;
|
|
18
|
+
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
store = createRuntimeStore();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe('createRuntimeStore', () => {
|
|
24
|
+
it('should create a store with required methods', () => {
|
|
25
|
+
expect(store).toHaveProperty('ensure');
|
|
26
|
+
expect(store).toHaveProperty('get');
|
|
27
|
+
expect(store).toHaveProperty('delete');
|
|
28
|
+
expect(store).toHaveProperty('persistState');
|
|
29
|
+
expect(store).toHaveProperty('restoreState');
|
|
30
|
+
expect(typeof store.ensure).toBe('function');
|
|
31
|
+
expect(typeof store.get).toBe('function');
|
|
32
|
+
expect(typeof store.delete).toBe('function');
|
|
33
|
+
expect(typeof store.persistState).toBe('function');
|
|
34
|
+
expect(typeof store.restoreState).toBe('function');
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe('ensure', () => {
|
|
39
|
+
it('should create a new runtime for new session key', () => {
|
|
40
|
+
const runtime = store.ensure('session-1');
|
|
41
|
+
|
|
42
|
+
expect(runtime).toBeDefined();
|
|
43
|
+
expect(runtime.state.sessionId).toBe('session-1');
|
|
44
|
+
expect(runtime.state.activeVariationId).toBeNull();
|
|
45
|
+
expect(runtime.state.variations).toEqual([]);
|
|
46
|
+
expect(typeof runtime.lastPersisted).toBe('number');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('should return existing runtime for same session key', () => {
|
|
50
|
+
const runtime1 = store.ensure('session-1');
|
|
51
|
+
runtime1.state.activeVariationId = 'variation-1';
|
|
52
|
+
|
|
53
|
+
const runtime2 = store.ensure('session-1');
|
|
54
|
+
|
|
55
|
+
expect(runtime2).toBe(runtime1);
|
|
56
|
+
expect(runtime2.state.activeVariationId).toBe('variation-1');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('should create separate runtimes for different session keys', () => {
|
|
60
|
+
const runtime1 = store.ensure('session-1');
|
|
61
|
+
const runtime2 = store.ensure('session-2');
|
|
62
|
+
|
|
63
|
+
expect(runtime1).not.toBe(runtime2);
|
|
64
|
+
expect(runtime1.state.sessionId).toBe('session-1');
|
|
65
|
+
expect(runtime2.state.sessionId).toBe('session-2');
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe('get', () => {
|
|
70
|
+
it('should return undefined for non-existent session', () => {
|
|
71
|
+
const runtime = store.get('non-existent');
|
|
72
|
+
expect(runtime).toBeUndefined();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('should return existing runtime', () => {
|
|
76
|
+
const created = store.ensure('session-1');
|
|
77
|
+
const retrieved = store.get('session-1');
|
|
78
|
+
|
|
79
|
+
expect(retrieved).toBe(created);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('should not create new runtime when getting non-existent', () => {
|
|
83
|
+
store.get('non-existent');
|
|
84
|
+
const runtime = store.get('non-existent');
|
|
85
|
+
|
|
86
|
+
expect(runtime).toBeUndefined();
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe('delete', () => {
|
|
91
|
+
it('should return false for non-existent session', () => {
|
|
92
|
+
const result = store.delete('non-existent');
|
|
93
|
+
expect(result).toBe(false);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('should delete existing runtime and return true', () => {
|
|
97
|
+
store.ensure('session-1');
|
|
98
|
+
const result = store.delete('session-1');
|
|
99
|
+
|
|
100
|
+
expect(result).toBe(true);
|
|
101
|
+
expect(store.get('session-1')).toBeUndefined();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('should not affect other sessions', () => {
|
|
105
|
+
store.ensure('session-1');
|
|
106
|
+
store.ensure('session-2');
|
|
107
|
+
|
|
108
|
+
store.delete('session-1');
|
|
109
|
+
|
|
110
|
+
expect(store.get('session-1')).toBeUndefined();
|
|
111
|
+
expect(store.get('session-2')).toBeDefined();
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
describe('persistState', () => {
|
|
116
|
+
it('should call pi.appendEntry with correct data', () => {
|
|
117
|
+
const mockAppendEntry = vi.fn();
|
|
118
|
+
const mockPi = {
|
|
119
|
+
appendEntry: mockAppendEntry,
|
|
120
|
+
} as unknown as ExtensionAPI;
|
|
121
|
+
|
|
122
|
+
const runtime = store.ensure('session-1');
|
|
123
|
+
runtime.state.variations = [
|
|
124
|
+
{
|
|
125
|
+
id: 'var-1',
|
|
126
|
+
name: 'test-variation',
|
|
127
|
+
path: '/tmp/var-1',
|
|
128
|
+
sourcePath: '/tmp/source',
|
|
129
|
+
type: 'cow',
|
|
130
|
+
createdAt: '2024-01-01T00:00:00.000Z',
|
|
131
|
+
lastAccessed: '2024-01-01T00:00:00.000Z',
|
|
132
|
+
} as Variation,
|
|
133
|
+
];
|
|
134
|
+
runtime.state.activeVariationId = 'var-1';
|
|
135
|
+
const beforePersist = runtime.lastPersisted;
|
|
136
|
+
|
|
137
|
+
store.persistState('session-1', mockPi);
|
|
138
|
+
|
|
139
|
+
expect(mockAppendEntry).toHaveBeenCalledOnce();
|
|
140
|
+
expect(mockAppendEntry).toHaveBeenCalledWith('pi-var:state', {
|
|
141
|
+
variations: runtime.state.variations,
|
|
142
|
+
activeVariationId: 'var-1',
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// Should update lastPersisted
|
|
146
|
+
const runtimeAfter = store.get('session-1');
|
|
147
|
+
expect(runtimeAfter?.lastPersisted).toBeGreaterThanOrEqual(beforePersist);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('should handle null activeVariationId', () => {
|
|
151
|
+
const mockAppendEntry = vi.fn();
|
|
152
|
+
const mockPi = {
|
|
153
|
+
appendEntry: mockAppendEntry,
|
|
154
|
+
} as unknown as ExtensionAPI;
|
|
155
|
+
|
|
156
|
+
const runtime = store.ensure('session-1');
|
|
157
|
+
runtime.state.activeVariationId = null;
|
|
158
|
+
runtime.state.variations = [];
|
|
159
|
+
|
|
160
|
+
store.persistState('session-1', mockPi);
|
|
161
|
+
|
|
162
|
+
expect(mockAppendEntry).toHaveBeenCalledWith('pi-var:state', {
|
|
163
|
+
variations: [],
|
|
164
|
+
activeVariationId: null,
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('should not throw for non-existent session', () => {
|
|
169
|
+
const mockAppendEntry = vi.fn();
|
|
170
|
+
const mockPi = {
|
|
171
|
+
appendEntry: mockAppendEntry,
|
|
172
|
+
} as unknown as ExtensionAPI;
|
|
173
|
+
|
|
174
|
+
expect(() => store.persistState('non-existent', mockPi)).not.toThrow();
|
|
175
|
+
expect(mockAppendEntry).not.toHaveBeenCalled();
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
describe('restoreState', () => {
|
|
180
|
+
it('should restore state from session entries (using getEntries)', () => {
|
|
181
|
+
const mockEntries = [
|
|
182
|
+
{ type: 'message', id: '1' },
|
|
183
|
+
{
|
|
184
|
+
type: 'custom',
|
|
185
|
+
customType: 'pi-var:state',
|
|
186
|
+
data: { variations: [], activeVariationId: null },
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
type: 'custom',
|
|
190
|
+
customType: 'pi-var:state',
|
|
191
|
+
data: {
|
|
192
|
+
variations: [
|
|
193
|
+
{
|
|
194
|
+
id: 'var-1',
|
|
195
|
+
name: 'restored-variation',
|
|
196
|
+
path: '/tmp/var-1',
|
|
197
|
+
sourcePath: '/tmp/source',
|
|
198
|
+
type: 'cow',
|
|
199
|
+
createdAt: '2024-01-01T00:00:00.000Z',
|
|
200
|
+
lastAccessed: '2024-01-01T00:00:00.000Z',
|
|
201
|
+
},
|
|
202
|
+
],
|
|
203
|
+
activeVariationId: 'var-1',
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
];
|
|
207
|
+
|
|
208
|
+
const mockSessionManager: SessionManager = {
|
|
209
|
+
getEntries: () => mockEntries,
|
|
210
|
+
getSessionId: () => 'session-1',
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const runtime = store.ensure('session-1');
|
|
214
|
+
store.restoreState('session-1', mockSessionManager);
|
|
215
|
+
|
|
216
|
+
expect(runtime.state.variations).toHaveLength(1);
|
|
217
|
+
expect(runtime.state.variations[0].name).toBe('restored-variation');
|
|
218
|
+
expect(runtime.state.activeVariationId).toBe('var-1');
|
|
219
|
+
expect(runtime.state.activeVariationId).not.toBeNull();
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('should restore state from session entries (using getBranch as fallback)', () => {
|
|
223
|
+
const mockEntries = [
|
|
224
|
+
{
|
|
225
|
+
type: 'custom',
|
|
226
|
+
customType: 'pi-var:state',
|
|
227
|
+
data: {
|
|
228
|
+
variations: [{ id: 'var-2', name: 'fallback-variation' } as Variation],
|
|
229
|
+
activeVariationId: 'var-2',
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
];
|
|
233
|
+
|
|
234
|
+
const mockSessionManager: SessionManager = {
|
|
235
|
+
getBranch: () => mockEntries,
|
|
236
|
+
getSessionId: () => 'session-1',
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
const runtime = store.ensure('session-1');
|
|
240
|
+
store.restoreState('session-1', mockSessionManager);
|
|
241
|
+
|
|
242
|
+
expect(runtime.state.variations[0].name).toBe('fallback-variation');
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it('should take the last state entry when multiple exist', () => {
|
|
246
|
+
const mockEntries = [
|
|
247
|
+
{
|
|
248
|
+
type: 'custom',
|
|
249
|
+
customType: 'pi-var:state',
|
|
250
|
+
data: { variations: [], activeVariationId: 'var-old' },
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
type: 'custom',
|
|
254
|
+
customType: 'pi-var:state',
|
|
255
|
+
data: { variations: [], activeVariationId: 'var-new' },
|
|
256
|
+
},
|
|
257
|
+
];
|
|
258
|
+
|
|
259
|
+
const mockSessionManager: SessionManager = {
|
|
260
|
+
getBranch: () => mockEntries,
|
|
261
|
+
getSessionId: () => 'session-1',
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
const runtime = store.ensure('session-1');
|
|
265
|
+
store.restoreState('session-1', mockSessionManager);
|
|
266
|
+
|
|
267
|
+
expect(runtime.state.activeVariationId).toBe('var-new');
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it('should handle no state entries gracefully', () => {
|
|
271
|
+
const mockEntries = [
|
|
272
|
+
{ type: 'message', id: '1' },
|
|
273
|
+
{ type: 'custom', customType: 'other-extension', data: {} },
|
|
274
|
+
];
|
|
275
|
+
|
|
276
|
+
const mockSessionManager: SessionManager = {
|
|
277
|
+
getBranch: () => mockEntries,
|
|
278
|
+
getSessionId: () => 'session-1',
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
const runtime = store.ensure('session-1');
|
|
282
|
+
runtime.state.variations = [{ id: 'existing', name: 'existing' } as Variation];
|
|
283
|
+
runtime.state.activeVariationId = 'existing';
|
|
284
|
+
|
|
285
|
+
store.restoreState('session-1', mockSessionManager);
|
|
286
|
+
|
|
287
|
+
// Should remain unchanged
|
|
288
|
+
expect(runtime.state.variations).toHaveLength(1);
|
|
289
|
+
expect(runtime.state.activeVariationId).toBe('existing');
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it('should handle null activeVariationId in restored state', () => {
|
|
293
|
+
const mockEntries = [
|
|
294
|
+
{
|
|
295
|
+
type: 'custom',
|
|
296
|
+
customType: 'pi-var:state',
|
|
297
|
+
data: {
|
|
298
|
+
variations: [{ id: 'var-1', name: 'test' } as Variation],
|
|
299
|
+
activeVariationId: null,
|
|
300
|
+
},
|
|
301
|
+
},
|
|
302
|
+
];
|
|
303
|
+
|
|
304
|
+
const mockSessionManager: SessionManager = {
|
|
305
|
+
getBranch: () => mockEntries,
|
|
306
|
+
getSessionId: () => 'session-1',
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
const runtime = store.ensure('session-1');
|
|
310
|
+
runtime.state.activeVariationId = 'some-variation';
|
|
311
|
+
|
|
312
|
+
store.restoreState('session-1', mockSessionManager);
|
|
313
|
+
|
|
314
|
+
expect(runtime.state.activeVariationId).toBeNull();
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it('should not throw for non-existent session', () => {
|
|
318
|
+
const mockSessionManager: SessionManager = {
|
|
319
|
+
getBranch: () => [],
|
|
320
|
+
getSessionId: () => 'session-1',
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
expect(() => store.restoreState('non-existent', mockSessionManager)).not.toThrow();
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it('should not throw when getBranch throws (e.g., during early session_start)', () => {
|
|
327
|
+
const mockSessionManager: SessionManager = {
|
|
328
|
+
getEntries: () => {
|
|
329
|
+
throw new Error('Session not initialized');
|
|
330
|
+
},
|
|
331
|
+
getBranch: () => {
|
|
332
|
+
throw new Error('leafId is undefined');
|
|
333
|
+
},
|
|
334
|
+
getSessionId: () => 'session-1',
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
const runtime = store.ensure('session-1');
|
|
338
|
+
runtime.state.variations = [{ id: 'existing', name: 'existing' } as Variation];
|
|
339
|
+
|
|
340
|
+
// Should not throw and should preserve existing state
|
|
341
|
+
expect(() => store.restoreState('session-1', mockSessionManager)).not.toThrow();
|
|
342
|
+
expect(runtime.state.variations).toHaveLength(1);
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
it('should not throw when getBranch and getEntries are unavailable', () => {
|
|
346
|
+
const mockSessionManager: SessionManager = {
|
|
347
|
+
getSessionId: () => 'session-1',
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
const runtime = store.ensure('session-1');
|
|
351
|
+
|
|
352
|
+
expect(() => store.restoreState('session-1', mockSessionManager)).not.toThrow();
|
|
353
|
+
// State should remain unchanged
|
|
354
|
+
expect(runtime.state.variations).toEqual([]);
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
it('should handle empty entries array', () => {
|
|
358
|
+
const mockSessionManager: SessionManager = {
|
|
359
|
+
getBranch: () => [],
|
|
360
|
+
getSessionId: () => 'session-1',
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
const runtime = store.ensure('session-1');
|
|
364
|
+
|
|
365
|
+
expect(() => store.restoreState('session-1', mockSessionManager)).not.toThrow();
|
|
366
|
+
expect(runtime.state.variations).toEqual([]);
|
|
367
|
+
});
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
describe('integration', () => {
|
|
371
|
+
it('should handle multiple sessions independently', () => {
|
|
372
|
+
const session1 = store.ensure('session-1');
|
|
373
|
+
const session2 = store.ensure('session-2');
|
|
374
|
+
const session3 = store.ensure('session-3');
|
|
375
|
+
|
|
376
|
+
// Modify session 1
|
|
377
|
+
session1.state.activeVariationId = 'var-1';
|
|
378
|
+
session1.state.variations = [
|
|
379
|
+
{
|
|
380
|
+
id: 'var-1',
|
|
381
|
+
name: 'test-variation',
|
|
382
|
+
path: '/tmp/var-1',
|
|
383
|
+
sourcePath: '/tmp/source',
|
|
384
|
+
type: 'cow',
|
|
385
|
+
createdAt: new Date().toISOString(),
|
|
386
|
+
lastAccessed: new Date().toISOString(),
|
|
387
|
+
},
|
|
388
|
+
];
|
|
389
|
+
|
|
390
|
+
// Verify isolation
|
|
391
|
+
expect(session1.state.activeVariationId).toBe('var-1');
|
|
392
|
+
expect(session2.state.activeVariationId).toBeNull();
|
|
393
|
+
expect(session3.state.activeVariationId).toBeNull();
|
|
394
|
+
|
|
395
|
+
expect(session1.state.variations).toHaveLength(1);
|
|
396
|
+
expect(session2.state.variations).toHaveLength(0);
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
it('should persist state modifications', () => {
|
|
400
|
+
const runtime = store.ensure('session-1');
|
|
401
|
+
|
|
402
|
+
runtime.state.activeVariationId = 'test-variation';
|
|
403
|
+
runtime.lastPersisted = 1234567890;
|
|
404
|
+
|
|
405
|
+
const retrieved = store.get('session-1');
|
|
406
|
+
expect(retrieved?.state.activeVariationId).toBe('test-variation');
|
|
407
|
+
expect(retrieved?.lastPersisted).toBe(1234567890);
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
it('should support full persist/restore cycle', () => {
|
|
411
|
+
const mockAppendEntry = vi.fn();
|
|
412
|
+
const mockPi = {
|
|
413
|
+
appendEntry: mockAppendEntry,
|
|
414
|
+
} as unknown as ExtensionAPI;
|
|
415
|
+
|
|
416
|
+
// Setup initial state
|
|
417
|
+
const runtime = store.ensure('session-1');
|
|
418
|
+
runtime.state.variations = [
|
|
419
|
+
{
|
|
420
|
+
id: 'var-1',
|
|
421
|
+
name: 'my-variation',
|
|
422
|
+
path: '/tmp/var-1',
|
|
423
|
+
sourcePath: '/tmp/source',
|
|
424
|
+
type: 'cow',
|
|
425
|
+
createdAt: '2024-01-01T00:00:00.000Z',
|
|
426
|
+
lastAccessed: '2024-01-01T00:00:00.000Z',
|
|
427
|
+
} as Variation,
|
|
428
|
+
];
|
|
429
|
+
runtime.state.activeVariationId = 'var-1';
|
|
430
|
+
|
|
431
|
+
// Persist
|
|
432
|
+
store.persistState('session-1', mockPi);
|
|
433
|
+
|
|
434
|
+
// Get the data that was passed to appendEntry
|
|
435
|
+
const persistedData = mockAppendEntry.mock.calls[0][1];
|
|
436
|
+
|
|
437
|
+
// Create a new store and restore
|
|
438
|
+
const newStore = createRuntimeStore();
|
|
439
|
+
const newRuntime = newStore.ensure('session-1');
|
|
440
|
+
|
|
441
|
+
const mockSessionManager: SessionManager = {
|
|
442
|
+
getBranch: () => [{ type: 'custom', customType: 'pi-var:state', data: persistedData }],
|
|
443
|
+
getSessionId: () => 'session-1',
|
|
444
|
+
};
|
|
445
|
+
|
|
446
|
+
newStore.restoreState('session-1', mockSessionManager);
|
|
447
|
+
|
|
448
|
+
// Verify restored state
|
|
449
|
+
expect(newRuntime.state.variations).toHaveLength(1);
|
|
450
|
+
expect(newRuntime.state.variations[0].name).toBe('my-variation');
|
|
451
|
+
expect(newRuntime.state.activeVariationId).toBe('var-1');
|
|
452
|
+
expect(newRuntime.state.activeVariationId).not.toBeNull();
|
|
453
|
+
});
|
|
454
|
+
});
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
describe('getSessionKey', () => {
|
|
458
|
+
it('should extract session ID from context', () => {
|
|
459
|
+
const mockContext: ExtensionContext = {
|
|
460
|
+
sessionManager: {
|
|
461
|
+
getSessionId: () => 'test-session-123',
|
|
462
|
+
},
|
|
463
|
+
};
|
|
464
|
+
|
|
465
|
+
const key = getSessionKey(mockContext);
|
|
466
|
+
expect(key).toBe('test-session-123');
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
it('should handle different session IDs', () => {
|
|
470
|
+
const ctx1: ExtensionContext = {
|
|
471
|
+
sessionManager: { getSessionId: () => 'session-a' },
|
|
472
|
+
};
|
|
473
|
+
const ctx2: ExtensionContext = {
|
|
474
|
+
sessionManager: { getSessionId: () => 'session-b' },
|
|
475
|
+
};
|
|
476
|
+
|
|
477
|
+
expect(getSessionKey(ctx1)).toBe('session-a');
|
|
478
|
+
expect(getSessionKey(ctx2)).toBe('session-b');
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
it('should return default when getSessionId is unavailable', () => {
|
|
482
|
+
const mockContext: ExtensionContext = {
|
|
483
|
+
sessionManager: {},
|
|
484
|
+
};
|
|
485
|
+
|
|
486
|
+
const key = getSessionKey(mockContext);
|
|
487
|
+
expect(key).toBe('default');
|
|
488
|
+
});
|
|
489
|
+
});
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for tool registration with redirection
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
6
|
+
import type { ExtensionAPI, ExtensionContext } from '@earendil-works/pi-coding-agent';
|
|
7
|
+
import type { VarRuntime, VarState, Variation } from '../../src/types/index';
|
|
8
|
+
|
|
9
|
+
// Mock the SDK
|
|
10
|
+
vi.mock('@earendil-works/pi-coding-agent', async () => {
|
|
11
|
+
const actual = await vi.importActual('@earendil-works/pi-coding-agent');
|
|
12
|
+
return {
|
|
13
|
+
...actual,
|
|
14
|
+
createReadTool: vi.fn((cwd, options) => ({
|
|
15
|
+
name: 'read',
|
|
16
|
+
parameters: {},
|
|
17
|
+
execute: vi.fn(),
|
|
18
|
+
...(options?.operations && { operations: options.operations }),
|
|
19
|
+
})),
|
|
20
|
+
createEditTool: vi.fn((cwd, options) => ({
|
|
21
|
+
name: 'edit',
|
|
22
|
+
parameters: {},
|
|
23
|
+
execute: vi.fn(),
|
|
24
|
+
...(options?.operations && { operations: options.operations }),
|
|
25
|
+
})),
|
|
26
|
+
createWriteTool: vi.fn((cwd, options) => ({
|
|
27
|
+
name: 'write',
|
|
28
|
+
parameters: {},
|
|
29
|
+
execute: vi.fn(),
|
|
30
|
+
...(options?.operations && { operations: options.operations }),
|
|
31
|
+
})),
|
|
32
|
+
createBashTool: vi.fn((cwd, options) => ({
|
|
33
|
+
name: 'bash',
|
|
34
|
+
parameters: {},
|
|
35
|
+
execute: vi.fn(),
|
|
36
|
+
...(options?.spawnHook && { spawnHook: options.spawnHook }),
|
|
37
|
+
})),
|
|
38
|
+
};
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe('registerRedirectedTools', () => {
|
|
42
|
+
let mockPi: ExtensionAPI;
|
|
43
|
+
let mockCtx: ExtensionContext;
|
|
44
|
+
let registeredTools: Map<string, any>;
|
|
45
|
+
|
|
46
|
+
beforeEach(async () => {
|
|
47
|
+
registeredTools = new Map();
|
|
48
|
+
|
|
49
|
+
mockPi = {
|
|
50
|
+
registerTool: vi.fn((tool) => {
|
|
51
|
+
registeredTools.set(tool.name, tool);
|
|
52
|
+
}),
|
|
53
|
+
on: vi.fn(),
|
|
54
|
+
} as unknown as ExtensionAPI;
|
|
55
|
+
|
|
56
|
+
mockCtx = {
|
|
57
|
+
cwd: '/home/user/project',
|
|
58
|
+
sessionManager: {
|
|
59
|
+
getSessionId: vi.fn(() => 'test-session'),
|
|
60
|
+
},
|
|
61
|
+
} as unknown as ExtensionContext;
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Lazy import to get mocked SDK
|
|
65
|
+
const importModule = async () => {
|
|
66
|
+
const { registerRedirectedTools } = await import('../../src/tools/redirect/index');
|
|
67
|
+
return { registerRedirectedTools };
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
it('registers all four tools (read, edit, write, bash)', async () => {
|
|
71
|
+
const { registerRedirectedTools } = await importModule();
|
|
72
|
+
|
|
73
|
+
const getRuntime = vi.fn(() => ({
|
|
74
|
+
state: {
|
|
75
|
+
activeVariationId: null,
|
|
76
|
+
variations: [],
|
|
77
|
+
sessionId: 'test-session',
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
lastPersisted: Date.now(),
|
|
81
|
+
}));
|
|
82
|
+
|
|
83
|
+
registerRedirectedTools(mockPi, getRuntime);
|
|
84
|
+
|
|
85
|
+
expect(registeredTools.has('read')).toBe(true);
|
|
86
|
+
expect(registeredTools.has('edit')).toBe(true);
|
|
87
|
+
expect(registeredTools.has('write')).toBe(true);
|
|
88
|
+
expect(registeredTools.has('bash')).toBe(true);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('tool descriptions mention variation redirection', async () => {
|
|
92
|
+
const { registerRedirectedTools } = await importModule();
|
|
93
|
+
|
|
94
|
+
const getRuntime = vi.fn(() => ({
|
|
95
|
+
state: {
|
|
96
|
+
activeVariationId: null,
|
|
97
|
+
variations: [],
|
|
98
|
+
sessionId: 'test-session',
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
lastPersisted: Date.now(),
|
|
102
|
+
}));
|
|
103
|
+
|
|
104
|
+
registerRedirectedTools(mockPi, getRuntime);
|
|
105
|
+
|
|
106
|
+
expect(registeredTools.get('read')?.description).toContain('variation');
|
|
107
|
+
expect(registeredTools.get('edit')?.description).toContain('variation');
|
|
108
|
+
expect(registeredTools.get('write')?.description).toContain('variation');
|
|
109
|
+
expect(registeredTools.get('bash')?.description).toContain('variation');
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
describe('tool execute behavior', () => {
|
|
114
|
+
const createMockVariation = (): Variation => ({
|
|
115
|
+
id: 'var-123',
|
|
116
|
+
name: 'test-variation',
|
|
117
|
+
path: '/tmp/variations/test-var',
|
|
118
|
+
sourcePath: '/home/user/project',
|
|
119
|
+
type: 'cow',
|
|
120
|
+
createdAt: new Date().toISOString(),
|
|
121
|
+
lastAccessed: new Date().toISOString(),
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('uses default tools when no variation is active', async () => {
|
|
125
|
+
// This test would verify that when no variation is active,
|
|
126
|
+
// the tools delegate to default behavior with ctx.cwd
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('redirects to variation when variation is active', async () => {
|
|
130
|
+
// This test would verify that when a variation is active,
|
|
131
|
+
// the tools use variation.path for operations
|
|
132
|
+
});
|
|
133
|
+
});
|