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.
Files changed (64) hide show
  1. package/.github/FUNDING.yml +4 -0
  2. package/.prettierrc +7 -0
  3. package/README.md +240 -0
  4. package/commitlint.config.cjs +3 -0
  5. package/extensions/pi-var/__tests__/integration/smoke.test.ts +308 -0
  6. package/extensions/pi-var/__tests__/unit/command.test.ts +322 -0
  7. package/extensions/pi-var/__tests__/unit/cow-detection.test.ts +340 -0
  8. package/extensions/pi-var/__tests__/unit/cow-git-merge.test.ts +404 -0
  9. package/extensions/pi-var/__tests__/unit/edr.test.ts +119 -0
  10. package/extensions/pi-var/__tests__/unit/environment.test.ts +369 -0
  11. package/extensions/pi-var/__tests__/unit/file-redirect.test.ts +295 -0
  12. package/extensions/pi-var/__tests__/unit/store.test.ts +489 -0
  13. package/extensions/pi-var/__tests__/unit/tools.test.ts +133 -0
  14. package/extensions/pi-var/__tests__/unit/variations.test.ts +352 -0
  15. package/extensions/pi-var/index.ts +111 -0
  16. package/extensions/pi-var/src/config.ts +30 -0
  17. package/extensions/pi-var/src/cow/clone.ts +46 -0
  18. package/extensions/pi-var/src/cow/detection.ts +215 -0
  19. package/extensions/pi-var/src/cow/index.ts +6 -0
  20. package/extensions/pi-var/src/edr/constants.ts +199 -0
  21. package/extensions/pi-var/src/edr/crowdstrike.ts +62 -0
  22. package/extensions/pi-var/src/edr/detection.ts +97 -0
  23. package/extensions/pi-var/src/edr/index.ts +6 -0
  24. package/extensions/pi-var/src/environment/copy-env.ts +79 -0
  25. package/extensions/pi-var/src/environment/detect.ts +91 -0
  26. package/extensions/pi-var/src/environment/index.ts +54 -0
  27. package/extensions/pi-var/src/environment/marker.ts +42 -0
  28. package/extensions/pi-var/src/environment/symlink.ts +82 -0
  29. package/extensions/pi-var/src/gitignore/manager.ts +77 -0
  30. package/extensions/pi-var/src/state/index.ts +7 -0
  31. package/extensions/pi-var/src/state/keys.ts +14 -0
  32. package/extensions/pi-var/src/state/store.ts +110 -0
  33. package/extensions/pi-var/src/state/types.ts +70 -0
  34. package/extensions/pi-var/src/tools/command/clean.ts +87 -0
  35. package/extensions/pi-var/src/tools/command/index.ts +95 -0
  36. package/extensions/pi-var/src/tools/command/status.ts +58 -0
  37. package/extensions/pi-var/src/tools/command/stop.ts +37 -0
  38. package/extensions/pi-var/src/tools/file-redirect.ts +126 -0
  39. package/extensions/pi-var/src/tools/redirect/bash.ts +75 -0
  40. package/extensions/pi-var/src/tools/redirect/edit.ts +60 -0
  41. package/extensions/pi-var/src/tools/redirect/index.ts +64 -0
  42. package/extensions/pi-var/src/tools/redirect/read.ts +61 -0
  43. package/extensions/pi-var/src/tools/redirect/write.ts +60 -0
  44. package/extensions/pi-var/src/tools/variation/create.ts +108 -0
  45. package/extensions/pi-var/src/tools/variation/merge.ts +80 -0
  46. package/extensions/pi-var/src/types/index.ts +139 -0
  47. package/extensions/pi-var/src/utils/names.ts +120 -0
  48. package/extensions/pi-var/src/variation/create-copy.ts +35 -0
  49. package/extensions/pi-var/src/variation/create-cow.ts +81 -0
  50. package/extensions/pi-var/src/variation/create-worktree.ts +31 -0
  51. package/extensions/pi-var/src/variation/create.ts +127 -0
  52. package/extensions/pi-var/src/variation/index.ts +9 -0
  53. package/extensions/pi-var/src/variation/merge/copy.ts +151 -0
  54. package/extensions/pi-var/src/variation/merge/git.ts +176 -0
  55. package/extensions/pi-var/src/variation/merge/index.ts +36 -0
  56. package/extensions/pi-var/src/variation/merge/rsync.ts +74 -0
  57. package/extensions/pi-var/src/variation/merge/strategy.ts +62 -0
  58. package/extensions/pi-var/src/variation/paths.ts +26 -0
  59. package/extensions/pi-var/src/variation/remove.ts +79 -0
  60. package/npm-shrinkwrap.json +6908 -0
  61. package/package.json +63 -0
  62. package/skills/variations/SKILL.md +182 -0
  63. package/tsconfig.json +25 -0
  64. package/vitest.config.ts +19 -0
@@ -0,0 +1,322 @@
1
+ /**
2
+ * Unit tests for /var command (autoregressive version)
3
+ */
4
+
5
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
6
+ import { registerVarCommand } from '../../src/tools/command/index';
7
+ import type { ExtensionAPI, ExtensionContext } from '@earendil-works/pi-coding-agent';
8
+ import type { VarRuntime, Variation } from '../../src/types/index';
9
+ import type { RuntimeStore } from '../../src/state/types';
10
+
11
+ // Mock the dependencies
12
+ function createMockRuntimeStore(): RuntimeStore {
13
+ return {
14
+ ensure: vi.fn(),
15
+ get: vi.fn(),
16
+ delete: vi.fn(),
17
+ persistState: vi.fn(),
18
+ restoreState: vi.fn(),
19
+ };
20
+ }
21
+
22
+ function createMockDeps(runtime: VarRuntime = createMockRuntime()) {
23
+ return {
24
+ pi: {
25
+ registerCommand: vi.fn(),
26
+ on: vi.fn(),
27
+ exec: vi.fn().mockResolvedValue({ code: 0, stdout: '', stderr: '' }),
28
+ appendEntry: vi.fn(),
29
+ } as unknown as ExtensionAPI,
30
+ getRuntime: () => runtime,
31
+ runtimeStore: createMockRuntimeStore(),
32
+ };
33
+ }
34
+
35
+ function createMockRuntime(
36
+ overrides: Partial<VarRuntime> & { state?: Partial<VarRuntime['state']> } = {}
37
+ ): VarRuntime {
38
+ return {
39
+ state: {
40
+ activeVariationId: null,
41
+ variations: [],
42
+ sessionId: 'test-session',
43
+ ...overrides.state,
44
+ },
45
+
46
+ lastPersisted: Date.now(),
47
+ ...overrides,
48
+ };
49
+ }
50
+
51
+ function createMockCtx(overrides: Partial<ExtensionContext> = {}): ExtensionContext {
52
+ return {
53
+ cwd: '/home/user/project',
54
+ ui: {
55
+ notify: vi.fn(),
56
+ confirm: vi.fn().mockResolvedValue(true),
57
+ setStatus: vi.fn(),
58
+ },
59
+ hasUI: true,
60
+ sessionManager: {
61
+ getSessionId: vi.fn().mockReturnValue('test-session'),
62
+ getEntries: vi.fn().mockReturnValue([]),
63
+ },
64
+ ...overrides,
65
+ } as unknown as ExtensionContext;
66
+ }
67
+
68
+ function createMockVariation(overrides: Partial<Variation> = {}): Variation {
69
+ return {
70
+ id: 'var-123',
71
+ name: 'test-variation',
72
+ path: '/tmp/variations/project-abc/test-var',
73
+ sourcePath: '/home/user/project',
74
+ type: 'cow',
75
+ createdAt: new Date().toISOString(),
76
+ lastAccessed: new Date().toISOString(),
77
+ ...overrides,
78
+ };
79
+ }
80
+
81
+ describe('registerVarCommand', () => {
82
+ it('should register the /var command', () => {
83
+ const deps = createMockDeps();
84
+ registerVarCommand(deps.pi, deps);
85
+
86
+ expect(deps.pi.registerCommand).toHaveBeenCalledWith(
87
+ 'var',
88
+ expect.objectContaining({
89
+ description: expect.stringContaining('status'),
90
+ handler: expect.any(Function),
91
+ })
92
+ );
93
+ });
94
+
95
+ it('should register command handler', () => {
96
+ const deps = createMockDeps();
97
+ registerVarCommand(deps.pi, deps);
98
+
99
+ expect(deps.pi.registerCommand).toHaveBeenCalled();
100
+ });
101
+ });
102
+
103
+ describe('var command - status (no args)', () => {
104
+ let deps: ReturnType<typeof createMockDeps>;
105
+ let handler: Function;
106
+
107
+ beforeEach(() => {
108
+ deps = createMockDeps();
109
+ registerVarCommand(deps.pi, deps);
110
+ handler = vi.mocked(deps.pi.registerCommand).mock.calls[0][1].handler;
111
+ });
112
+
113
+ it('should show autoregressive message when no variations', async () => {
114
+ const ctx = createMockCtx();
115
+ await handler('', ctx);
116
+
117
+ expect(ctx.ui.notify).toHaveBeenCalledWith(expect.stringContaining('create_variation'), 'info');
118
+ });
119
+
120
+ it('should show active variation in status', async () => {
121
+ const variation = createMockVariation({ id: 'var-1', name: 'feature-auth' });
122
+ const runtime = createMockRuntime({
123
+ state: {
124
+ variations: [variation],
125
+ activeVariationId: 'var-1',
126
+ sessionId: 'test-session',
127
+ },
128
+ });
129
+ deps = createMockDeps(runtime);
130
+ registerVarCommand(deps.pi, deps);
131
+ handler = vi.mocked(deps.pi.registerCommand).mock.calls[0][1].handler;
132
+
133
+ const ctx = createMockCtx();
134
+ await handler('', ctx);
135
+
136
+ expect(ctx.ui.notify).toHaveBeenCalledWith(expect.stringContaining('feature-auth'), 'info');
137
+ expect(ctx.ui.notify).toHaveBeenCalledWith(expect.stringContaining('🌿'), 'info');
138
+ });
139
+ });
140
+
141
+ describe('var command - list', () => {
142
+ let deps: ReturnType<typeof createMockDeps>;
143
+ let handler: Function;
144
+
145
+ beforeEach(() => {
146
+ deps = createMockDeps();
147
+ registerVarCommand(deps.pi, deps);
148
+ handler = vi.mocked(deps.pi.registerCommand).mock.calls[0][1].handler;
149
+ });
150
+
151
+ it('should list variations with type indicators', async () => {
152
+ const variation = createMockVariation({ id: 'var-1', name: 'feature-auth', type: 'cow' });
153
+ const runtime = createMockRuntime({
154
+ state: {
155
+ variations: [variation],
156
+ activeVariationId: null,
157
+ sessionId: 'test-session',
158
+ },
159
+ });
160
+ deps = createMockDeps(runtime);
161
+ registerVarCommand(deps.pi, deps);
162
+ handler = vi.mocked(deps.pi.registerCommand).mock.calls[0][1].handler;
163
+
164
+ const ctx = createMockCtx();
165
+ await handler('list', ctx);
166
+
167
+ expect(ctx.ui.notify).toHaveBeenCalledWith(expect.stringContaining('feature-auth'), 'info');
168
+ expect(ctx.ui.notify).toHaveBeenCalledWith(expect.stringContaining('⚡'), 'info');
169
+ });
170
+ });
171
+
172
+ describe('var command - stop (return to source)', () => {
173
+ let deps: ReturnType<typeof createMockDeps>;
174
+ let handler: Function;
175
+
176
+ beforeEach(() => {
177
+ deps = createMockDeps();
178
+ registerVarCommand(deps.pi, deps);
179
+ handler = vi.mocked(deps.pi.registerCommand).mock.calls[0][1].handler;
180
+ });
181
+
182
+ it('should deactivate current variation with stop', async () => {
183
+ const variation = createMockVariation({ id: 'var-1', name: 'feature-auth' });
184
+ const runtime = createMockRuntime({
185
+ state: {
186
+ variations: [variation],
187
+ activeVariationId: 'var-1',
188
+ sessionId: 'test-session',
189
+ },
190
+ });
191
+ deps = createMockDeps(runtime);
192
+ registerVarCommand(deps.pi, deps);
193
+ handler = vi.mocked(deps.pi.registerCommand).mock.calls[0][1].handler;
194
+
195
+ const ctx = createMockCtx();
196
+ await handler('stop', ctx);
197
+
198
+ expect(runtime.state.activeVariationId).toBeNull();
199
+
200
+ expect(ctx.ui.setStatus).toHaveBeenCalledWith('pi-var', '');
201
+ // Should persist state after stop
202
+ expect(deps.runtimeStore.persistState).toHaveBeenCalled();
203
+ });
204
+
205
+ it('should handle stop when already in source', async () => {
206
+ const runtime = createMockRuntime({
207
+ state: {
208
+ variations: [],
209
+ activeVariationId: null,
210
+ sessionId: 'test-session',
211
+ },
212
+ });
213
+ deps = createMockDeps(runtime);
214
+ registerVarCommand(deps.pi, deps);
215
+ handler = vi.mocked(deps.pi.registerCommand).mock.calls[0][1].handler;
216
+
217
+ const ctx = createMockCtx();
218
+ await handler('stop', ctx);
219
+
220
+ expect(ctx.ui.notify).toHaveBeenCalledWith(expect.stringContaining('Already'), 'info');
221
+ });
222
+ });
223
+
224
+ describe('var command - clean', () => {
225
+ let deps: ReturnType<typeof createMockDeps>;
226
+ let handler: Function;
227
+
228
+ beforeEach(() => {
229
+ deps = createMockDeps();
230
+ registerVarCommand(deps.pi, deps);
231
+ handler = vi.mocked(deps.pi.registerCommand).mock.calls[0][1].handler;
232
+ });
233
+
234
+ it('should show usage when no name or stale flag', async () => {
235
+ const ctx = createMockCtx();
236
+ await handler('clean', ctx);
237
+
238
+ expect(ctx.ui.notify).toHaveBeenCalledWith(expect.stringContaining('Usage'), 'warning');
239
+ });
240
+
241
+ it('should require confirmation before cleaning', async () => {
242
+ const variation = createMockVariation({ id: 'var-1', name: 'old-feature' });
243
+ const runtime = createMockRuntime({
244
+ state: { variations: [variation], activeVariationId: null, sessionId: 'test-session' },
245
+ });
246
+ deps = createMockDeps(runtime);
247
+ registerVarCommand(deps.pi, deps);
248
+ handler = vi.mocked(deps.pi.registerCommand).mock.calls[0][1].handler;
249
+
250
+ const ctx = createMockCtx();
251
+ ctx.ui.confirm = vi.fn().mockResolvedValue(false);
252
+
253
+ await handler('clean old-feature', ctx);
254
+
255
+ expect(ctx.ui.confirm).toHaveBeenCalled();
256
+ expect(ctx.ui.notify).toHaveBeenCalledWith('Clean cancelled', 'info');
257
+ });
258
+
259
+ it('should error when variation not found', async () => {
260
+ const ctx = createMockCtx();
261
+ await handler('clean non-existent', ctx);
262
+
263
+ expect(ctx.ui.notify).toHaveBeenCalledWith(expect.stringContaining('not found'), 'error');
264
+ });
265
+ });
266
+
267
+ describe('var command - stale cleanup', () => {
268
+ let deps: ReturnType<typeof createMockDeps>;
269
+ let handler: Function;
270
+
271
+ beforeEach(() => {
272
+ deps = createMockDeps();
273
+ registerVarCommand(deps.pi, deps);
274
+ handler = vi.mocked(deps.pi.registerCommand).mock.calls[0][1].handler;
275
+ });
276
+
277
+ it('should clean variations older than specified days', async () => {
278
+ const oldVariation = createMockVariation({
279
+ id: 'var-1',
280
+ name: 'old-feature',
281
+ lastAccessed: new Date(Date.now() - 10 * 24 * 60 * 60 * 1000).toISOString(), // 10 days ago
282
+ });
283
+ const runtime = createMockRuntime({
284
+ state: {
285
+ variations: [oldVariation],
286
+ activeVariationId: null,
287
+ sessionId: 'test-session',
288
+ },
289
+ });
290
+ deps = createMockDeps(runtime);
291
+ registerVarCommand(deps.pi, deps);
292
+ handler = vi.mocked(deps.pi.registerCommand).mock.calls[0][1].handler;
293
+
294
+ const ctx = createMockCtx();
295
+ await handler('clean --stale 7', ctx);
296
+
297
+ expect(ctx.ui.confirm).toHaveBeenCalled();
298
+ });
299
+
300
+ it('should show message when no stale variations found', async () => {
301
+ const freshVariation = createMockVariation({
302
+ id: 'var-1',
303
+ name: 'fresh-feature',
304
+ lastAccessed: new Date().toISOString(),
305
+ });
306
+ const runtime = createMockRuntime({
307
+ state: {
308
+ variations: [freshVariation],
309
+ activeVariationId: null,
310
+ sessionId: 'test-session',
311
+ },
312
+ });
313
+ deps = createMockDeps(runtime);
314
+ registerVarCommand(deps.pi, deps);
315
+ handler = vi.mocked(deps.pi.registerCommand).mock.calls[0][1].handler;
316
+
317
+ const ctx = createMockCtx();
318
+ await handler('clean --stale 7', ctx);
319
+
320
+ expect(ctx.ui.notify).toHaveBeenCalledWith(expect.stringContaining('No variations'), 'info');
321
+ });
322
+ });
@@ -0,0 +1,340 @@
1
+ /**
2
+ * Unit tests for CoW detection types and logic
3
+ * Tests the decision-making logic rather than mocking system calls
4
+ */
5
+
6
+ import { describe, it, expect } from 'vitest';
7
+ import type { CoWDetectionResult } from '../../src/types/index';
8
+
9
+ describe('CoWDetectionResult type structure', () => {
10
+ it('should have the correct structure for successful CoW without EDR', () => {
11
+ const result: CoWDetectionResult = {
12
+ supported: true,
13
+ method: 'clonefile',
14
+ edr: {
15
+ detected: false,
16
+ products: [],
17
+ hasSlowCoWEDR: false,
18
+ },
19
+ performance: {
20
+ fast: true,
21
+ durationMs: 10,
22
+ samples: 2,
23
+ maxDurationMs: 12,
24
+ confidence: 'high',
25
+ },
26
+ recommendedType: 'cow',
27
+ };
28
+
29
+ expect(result.supported).toBe(true);
30
+ expect(result.method).toBe('clonefile');
31
+ expect(result.edr?.detected).toBe(false);
32
+ expect(result.edr?.hasSlowCoWEDR).toBe(false);
33
+ expect(result.performance?.fast).toBe(true);
34
+ expect(result.performance?.confidence).toBe('high');
35
+ expect(result.recommendedType).toBe('cow');
36
+ });
37
+
38
+ it('should have the correct structure when known slow EDR detected (short-circuit)', () => {
39
+ // When CrowdStrike is detected, we short-circuit to worktree without timing
40
+ const result: CoWDetectionResult = {
41
+ supported: true,
42
+ method: 'clonefile',
43
+ edr: {
44
+ detected: true,
45
+ products: ['CrowdStrike Falcon'],
46
+ hasSlowCoWEDR: true,
47
+ },
48
+ recommendedType: 'worktree',
49
+ };
50
+
51
+ expect(result.supported).toBe(true);
52
+ expect(result.edr?.detected).toBe(true);
53
+ expect(result.edr?.hasSlowCoWEDR).toBe(true);
54
+ expect(result.performance).toBeUndefined(); // No timing needed
55
+ expect(result.recommendedType).toBe('worktree');
56
+ });
57
+
58
+ it('should have the correct structure when EDR detected but CoW still fast', () => {
59
+ const result: CoWDetectionResult = {
60
+ supported: true,
61
+ method: 'clonefile',
62
+ edr: {
63
+ detected: true,
64
+ products: ['Microsoft Defender'],
65
+ hasSlowCoWEDR: false,
66
+ },
67
+ performance: {
68
+ fast: true,
69
+ durationMs: 15,
70
+ samples: 2,
71
+ maxDurationMs: 18,
72
+ confidence: 'high',
73
+ },
74
+ recommendedType: 'cow',
75
+ };
76
+
77
+ expect(result.supported).toBe(true);
78
+ expect(result.edr?.detected).toBe(true);
79
+ expect(result.edr?.hasSlowCoWEDR).toBe(false);
80
+ expect(result.performance?.fast).toBe(true);
81
+ expect(result.performance?.confidence).toBe('high');
82
+ expect(result.recommendedType).toBe('cow');
83
+ });
84
+
85
+ it('should recommend worktree in medium confidence (gray zone)', () => {
86
+ // 20-100ms is the gray zone - conservative approach uses worktree
87
+ const result: CoWDetectionResult = {
88
+ supported: true,
89
+ method: 'clonefile',
90
+ edr: {
91
+ detected: false,
92
+ products: [],
93
+ hasSlowCoWEDR: false,
94
+ },
95
+ performance: {
96
+ fast: false, // Conservative in gray zone
97
+ durationMs: 45,
98
+ samples: 2,
99
+ maxDurationMs: 48,
100
+ confidence: 'medium',
101
+ },
102
+ recommendedType: 'worktree',
103
+ };
104
+
105
+ expect(result.supported).toBe(true);
106
+ expect(result.performance?.confidence).toBe('medium');
107
+ expect(result.performance?.fast).toBe(false);
108
+ expect(result.recommendedType).toBe('worktree');
109
+ });
110
+
111
+ it('should recommend worktree when timing shows spikes', () => {
112
+ // Spike detection: max >> average indicates intermittent interference
113
+ const result: CoWDetectionResult = {
114
+ supported: true,
115
+ method: 'reflink',
116
+ edr: {
117
+ detected: true,
118
+ products: ['Unknown EDR'],
119
+ hasSlowCoWEDR: false,
120
+ },
121
+ performance: {
122
+ fast: false,
123
+ durationMs: 25, // Average in gray zone
124
+ samples: 2,
125
+ maxDurationMs: 150, // But spike into slow territory
126
+ confidence: 'medium',
127
+ },
128
+ recommendedType: 'worktree', // Spike triggers conservative choice
129
+ };
130
+
131
+ expect(result.supported).toBe(true);
132
+ expect(result.performance?.maxDurationMs).toBe(150);
133
+ expect(result.recommendedType).toBe('worktree');
134
+ });
135
+
136
+ it('should recommend worktree when CoW is not supported', () => {
137
+ const result: CoWDetectionResult = {
138
+ supported: false,
139
+ edr: {
140
+ detected: false,
141
+ products: [],
142
+ hasSlowCoWEDR: false,
143
+ },
144
+ recommendedType: 'worktree',
145
+ };
146
+
147
+ expect(result.supported).toBe(false);
148
+ expect(result.method).toBeUndefined();
149
+ expect(result.recommendedType).toBe('worktree');
150
+ });
151
+
152
+ it('should handle Windows case (no CoW support)', () => {
153
+ const result: CoWDetectionResult = {
154
+ supported: false,
155
+ edr: {
156
+ detected: true,
157
+ products: ['Microsoft Defender'],
158
+ hasSlowCoWEDR: false,
159
+ },
160
+ recommendedType: 'worktree',
161
+ };
162
+
163
+ expect(result.supported).toBe(false);
164
+ expect(result.edr?.detected).toBe(true);
165
+ expect(result.recommendedType).toBe('worktree');
166
+ });
167
+ });
168
+
169
+ describe('Confidence level logic', () => {
170
+ function getConfidence(
171
+ avgTiming: number,
172
+ fastThreshold = 20,
173
+ slowThreshold = 100
174
+ ): 'high' | 'medium' | 'low' {
175
+ if (avgTiming < fastThreshold) return 'high';
176
+ if (avgTiming > slowThreshold) return 'high';
177
+ return 'medium';
178
+ }
179
+
180
+ it('should assign high confidence to fast timings', () => {
181
+ expect(getConfidence(5)).toBe('high');
182
+ expect(getConfidence(10)).toBe('high');
183
+ expect(getConfidence(19)).toBe('high');
184
+ });
185
+
186
+ it('should assign high confidence to clearly slow timings', () => {
187
+ expect(getConfidence(101)).toBe('high');
188
+ expect(getConfidence(150)).toBe('high');
189
+ expect(getConfidence(500)).toBe('high');
190
+ });
191
+
192
+ it('should assign medium confidence to gray zone timings', () => {
193
+ expect(getConfidence(20)).toBe('medium');
194
+ expect(getConfidence(50)).toBe('medium');
195
+ expect(getConfidence(99)).toBe('medium');
196
+ expect(getConfidence(100)).toBe('medium'); // At boundary
197
+ });
198
+ });
199
+
200
+ describe('Spike detection logic', () => {
201
+ function hasSpike(
202
+ avgTiming: number,
203
+ maxTiming: number,
204
+ slowThreshold = 100,
205
+ ratioThreshold = 3
206
+ ): boolean {
207
+ return maxTiming > avgTiming * ratioThreshold && maxTiming > slowThreshold;
208
+ }
209
+
210
+ it('should detect spikes when max is much higher than average', () => {
211
+ expect(hasSpike(10, 150)).toBe(true); // 15x average, above threshold
212
+ expect(hasSpike(20, 150)).toBe(true); // 7.5x average, above threshold
213
+ expect(hasSpike(30, 150)).toBe(true); // 5x average, above threshold
214
+ });
215
+
216
+ it('should not detect spikes when max is close to average', () => {
217
+ expect(hasSpike(10, 15)).toBe(false); // 1.5x average
218
+ expect(hasSpike(50, 80)).toBe(false); // 1.6x average
219
+ expect(hasSpike(40, 100)).toBe(false); // 2.5x average, at threshold
220
+ });
221
+
222
+ it('should not detect spikes when max is below slow threshold', () => {
223
+ expect(hasSpike(10, 50)).toBe(false); // 5x average but below threshold
224
+ expect(hasSpike(20, 80)).toBe(false); // 4x average but below threshold
225
+ });
226
+ });
227
+
228
+ describe('Recommendation decision matrix', () => {
229
+ function getRecommendation(
230
+ cowSupported: boolean,
231
+ hasSlowCoWEDR: boolean,
232
+ cowFast: boolean,
233
+ confidence: 'high' | 'medium' | 'low',
234
+ hasSpike: boolean,
235
+ isGitRepo: boolean
236
+ ): 'cow' | 'worktree' | 'copy' {
237
+ // Priority 1: Known slow EDR - always worktree
238
+ if (hasSlowCoWEDR) {
239
+ return isGitRepo ? 'worktree' : 'copy';
240
+ }
241
+
242
+ // Priority 2: No CoW support
243
+ if (!cowSupported) {
244
+ return isGitRepo ? 'worktree' : 'copy';
245
+ }
246
+
247
+ // Priority 3: Uncertain or spikes - conservative (check BEFORE fast path)
248
+ if (hasSpike || confidence === 'medium') {
249
+ return isGitRepo ? 'worktree' : 'copy';
250
+ }
251
+
252
+ // Priority 4: Fast with high confidence
253
+ if (cowFast && confidence === 'high') {
254
+ return 'cow';
255
+ }
256
+
257
+ // Priority 5: Slow but confident
258
+ return isGitRepo ? 'worktree' : 'copy';
259
+ }
260
+
261
+ describe('Known slow EDR detected (CrowdStrike, etc.)', () => {
262
+ it('should recommend worktree regardless of timing', () => {
263
+ expect(getRecommendation(true, true, true, 'high', false, true)).toBe('worktree');
264
+ expect(getRecommendation(true, true, false, 'high', false, true)).toBe('worktree');
265
+ expect(getRecommendation(true, true, true, 'medium', false, true)).toBe('worktree');
266
+ });
267
+
268
+ it('should recommend copy when no git repo and slow EDR detected', () => {
269
+ expect(getRecommendation(true, true, true, 'high', false, false)).toBe('copy');
270
+ });
271
+ });
272
+
273
+ describe('No slow EDR - timing based decisions', () => {
274
+ it('should recommend cow when fast with high confidence', () => {
275
+ expect(getRecommendation(true, false, true, 'high', false, true)).toBe('cow');
276
+ expect(getRecommendation(true, false, true, 'high', false, false)).toBe('cow');
277
+ });
278
+
279
+ it('should recommend worktree when medium confidence (gray zone)', () => {
280
+ expect(getRecommendation(true, false, false, 'medium', false, true)).toBe('worktree');
281
+ });
282
+
283
+ it('should recommend worktree when spikes detected', () => {
284
+ expect(getRecommendation(true, false, true, 'high', true, true)).toBe('worktree');
285
+ });
286
+
287
+ it('should recommend worktree when slow with high confidence', () => {
288
+ expect(getRecommendation(true, false, false, 'high', false, true)).toBe('worktree');
289
+ });
290
+ });
291
+
292
+ describe('No CoW support', () => {
293
+ it('should recommend worktree when git repo exists', () => {
294
+ expect(getRecommendation(false, false, false, 'high', false, true)).toBe('worktree');
295
+ });
296
+
297
+ it('should recommend copy when no git repo', () => {
298
+ expect(getRecommendation(false, false, false, 'high', false, false)).toBe('copy');
299
+ });
300
+ });
301
+ });
302
+
303
+ describe('EDR impact classification', () => {
304
+ const KNOWN_SLOW_EDR_PRODUCTS = [
305
+ 'CrowdStrike Falcon',
306
+ 'SentinelOne',
307
+ 'Cylance',
308
+ 'Carbon Black/VMware',
309
+ 'Microsoft Defender for Endpoint',
310
+ 'FireEye/XAGENT',
311
+ ];
312
+
313
+ it('should classify known CoW-impacting EDRs correctly', () => {
314
+ for (const product of KNOWN_SLOW_EDR_PRODUCTS) {
315
+ expect(product).toBeDefined();
316
+ // These products should be flagged as knownSlowCoW
317
+ expect([
318
+ 'CrowdStrike Falcon',
319
+ 'SentinelOne',
320
+ 'Cylance',
321
+ 'Carbon Black/VMware',
322
+ 'Microsoft Defender for Endpoint',
323
+ 'FireEye/XAGENT',
324
+ ]).toContain(product);
325
+ }
326
+ });
327
+
328
+ it('should short-circuit to worktree when CrowdStrike detected', () => {
329
+ // CrowdStrike is the primary target - should always short-circuit
330
+ const result = [
331
+ 'CrowdStrike Falcon',
332
+ 'SentinelOne',
333
+ 'Cylance',
334
+ 'Carbon Black/VMware',
335
+ 'FireEye/XAGENT',
336
+ ].includes('CrowdStrike Falcon');
337
+
338
+ expect(result).toBe(true);
339
+ });
340
+ });