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,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
|
+
});
|