claude-mycelium 2.0.0 → 2.1.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 (189) 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/.claude/memory.db +0 -0
  5. package/.claude/settings.local.json +4 -1
  6. package/README.md +81 -235
  7. package/SECURITY.md +145 -0
  8. package/dist/agent/worker.d.ts +8 -0
  9. package/dist/agent/worker.d.ts.map +1 -0
  10. package/dist/agent/worker.js +97 -0
  11. package/dist/agent/worker.js.map +1 -0
  12. package/dist/bin.d.ts +7 -0
  13. package/dist/bin.d.ts.map +1 -0
  14. package/dist/bin.js +11 -0
  15. package/dist/bin.js.map +1 -0
  16. package/dist/cli/cost.d.ts +10 -0
  17. package/dist/cli/cost.d.ts.map +1 -0
  18. package/dist/cli/cost.js +163 -0
  19. package/dist/cli/cost.js.map +1 -0
  20. package/dist/cli/gc.d.ts +10 -0
  21. package/dist/cli/gc.d.ts.map +1 -0
  22. package/dist/cli/gc.js +108 -0
  23. package/dist/cli/gc.js.map +1 -0
  24. package/dist/cli/gradients.d.ts +10 -0
  25. package/dist/cli/gradients.d.ts.map +1 -0
  26. package/dist/cli/gradients.js +69 -0
  27. package/dist/cli/gradients.js.map +1 -0
  28. package/dist/cli/index.d.ts +17 -0
  29. package/dist/cli/index.d.ts.map +1 -0
  30. package/dist/cli/index.js +72 -0
  31. package/dist/cli/index.js.map +1 -0
  32. package/dist/cli/init.d.ts +11 -0
  33. package/dist/cli/init.d.ts.map +1 -0
  34. package/dist/cli/init.js +97 -0
  35. package/dist/cli/init.js.map +1 -0
  36. package/dist/cli/status.d.ts +10 -0
  37. package/dist/cli/status.d.ts.map +1 -0
  38. package/dist/cli/status.js +191 -0
  39. package/dist/cli/status.js.map +1 -0
  40. package/dist/coordination/file-locks.d.ts +42 -0
  41. package/dist/coordination/file-locks.d.ts.map +1 -0
  42. package/dist/coordination/file-locks.js +269 -0
  43. package/dist/coordination/file-locks.js.map +1 -0
  44. package/dist/coordination/index.d.ts +4 -0
  45. package/dist/coordination/index.d.ts.map +1 -1
  46. package/dist/coordination/index.js +4 -0
  47. package/dist/coordination/index.js.map +1 -1
  48. package/dist/coordination/inhibitors.d.ts +84 -0
  49. package/dist/coordination/inhibitors.d.ts.map +1 -0
  50. package/dist/coordination/inhibitors.js +290 -0
  51. package/dist/coordination/inhibitors.js.map +1 -0
  52. package/dist/coordination/process-manager.d.ts +73 -0
  53. package/dist/coordination/process-manager.d.ts.map +1 -0
  54. package/dist/coordination/process-manager.js +144 -0
  55. package/dist/coordination/process-manager.js.map +1 -0
  56. package/dist/core/agent-executor.d.ts.map +1 -1
  57. package/dist/core/agent-executor.js +28 -10
  58. package/dist/core/agent-executor.js.map +1 -1
  59. package/dist/core/change-applier.d.ts +29 -5
  60. package/dist/core/change-applier.d.ts.map +1 -1
  61. package/dist/core/change-applier.js +254 -24
  62. package/dist/core/change-applier.js.map +1 -1
  63. package/dist/core/signals/churn.d.ts.map +1 -1
  64. package/dist/core/signals/churn.js +6 -4
  65. package/dist/core/signals/churn.js.map +1 -1
  66. package/dist/core/signals/debt.d.ts.map +1 -1
  67. package/dist/core/signals/debt.js +4 -3
  68. package/dist/core/signals/debt.js.map +1 -1
  69. package/dist/cost/cost-tracker.d.ts.map +1 -1
  70. package/dist/cost/cost-tracker.js +2 -0
  71. package/dist/cost/cost-tracker.js.map +1 -1
  72. package/dist/gc/index.d.ts +17 -0
  73. package/dist/gc/index.d.ts.map +1 -0
  74. package/dist/gc/index.js +17 -0
  75. package/dist/gc/index.js.map +1 -0
  76. package/dist/gc/runner.d.ts +39 -0
  77. package/dist/gc/runner.d.ts.map +1 -0
  78. package/dist/gc/runner.js +277 -0
  79. package/dist/gc/runner.js.map +1 -0
  80. package/dist/gc/trace-compactor.d.ts +31 -0
  81. package/dist/gc/trace-compactor.d.ts.map +1 -0
  82. package/dist/gc/trace-compactor.js +162 -0
  83. package/dist/gc/trace-compactor.js.map +1 -0
  84. package/dist/index.d.ts +5 -1
  85. package/dist/index.d.ts.map +1 -1
  86. package/dist/index.js +6 -1
  87. package/dist/index.js.map +1 -1
  88. package/dist/prompts/index.d.ts +2 -1
  89. package/dist/prompts/index.d.ts.map +1 -1
  90. package/dist/prompts/index.js.map +1 -1
  91. package/dist/quarantine/explorer.d.ts +65 -0
  92. package/dist/quarantine/explorer.d.ts.map +1 -0
  93. package/dist/quarantine/explorer.js +175 -0
  94. package/dist/quarantine/explorer.js.map +1 -0
  95. package/dist/quarantine/index.d.ts +7 -0
  96. package/dist/quarantine/index.d.ts.map +1 -0
  97. package/dist/quarantine/index.js +7 -0
  98. package/dist/quarantine/index.js.map +1 -0
  99. package/dist/quarantine/manager.d.ts +75 -0
  100. package/dist/quarantine/manager.d.ts.map +1 -0
  101. package/dist/quarantine/manager.js +275 -0
  102. package/dist/quarantine/manager.js.map +1 -0
  103. package/dist/task/acceptance.d.ts +29 -0
  104. package/dist/task/acceptance.d.ts.map +1 -0
  105. package/dist/task/acceptance.js +228 -0
  106. package/dist/task/acceptance.js.map +1 -0
  107. package/dist/task/executor.d.ts +30 -0
  108. package/dist/task/executor.d.ts.map +1 -0
  109. package/dist/task/executor.js +429 -0
  110. package/dist/task/executor.js.map +1 -0
  111. package/dist/task/index.d.ts +12 -0
  112. package/dist/task/index.d.ts.map +1 -0
  113. package/dist/task/index.js +12 -0
  114. package/dist/task/index.js.map +1 -0
  115. package/dist/task/planner.d.ts +21 -0
  116. package/dist/task/planner.d.ts.map +1 -0
  117. package/dist/task/planner.js +253 -0
  118. package/dist/task/planner.js.map +1 -0
  119. package/dist/task/storage.d.ts +46 -0
  120. package/dist/task/storage.d.ts.map +1 -0
  121. package/dist/task/storage.js +266 -0
  122. package/dist/task/storage.js.map +1 -0
  123. package/dist/trace/trace-event.d.ts +2 -18
  124. package/dist/trace/trace-event.d.ts.map +1 -1
  125. package/dist/trace/trace-event.js +6 -6
  126. package/dist/trace/trace-event.js.map +1 -1
  127. package/dist/utils/file-utils.d.ts.map +1 -1
  128. package/dist/utils/file-utils.js +54 -15
  129. package/dist/utils/file-utils.js.map +1 -1
  130. package/docs/PHASE5_IMPLEMENTATION.md +237 -0
  131. package/docs/PHASES-3-7-COMPLETE.md +177 -0
  132. package/docs/PHASE_4_COMPLETE.md +135 -0
  133. package/docs/PHASE_7_DELIVERABLES.md +295 -0
  134. package/docs/PHASE_7_IMPLEMENTATION.md +306 -0
  135. package/docs/PHASE_7_SUMMARY.txt +195 -0
  136. package/docs/RELEASE-NOTES-v2.1.md +213 -0
  137. package/docs/ROADMAP.md +64 -57
  138. package/docs/SECURITY-AUDIT.md +387 -0
  139. package/docs/SNAPSHOT.md +59 -32
  140. package/docs/implementation/phase3-summary.md +220 -0
  141. package/package.json +19 -11
  142. package/src/agent/worker.ts +111 -0
  143. package/src/bin.ts +13 -0
  144. package/src/cli/cost.ts +210 -0
  145. package/src/cli/gc.ts +138 -0
  146. package/src/cli/gradients.ts +95 -0
  147. package/src/cli/index.ts +79 -0
  148. package/src/cli/init.ts +139 -0
  149. package/src/cli/status.ts +218 -0
  150. package/src/coordination/file-locks.ts +300 -0
  151. package/src/coordination/index.ts +4 -0
  152. package/src/coordination/inhibitors.ts +345 -0
  153. package/src/coordination/process-manager.ts +199 -0
  154. package/src/core/agent-executor.ts +20 -4
  155. package/src/core/signals/churn.ts +8 -5
  156. package/src/core/signals/debt.ts +4 -3
  157. package/src/cost/cost-tracker.ts +2 -0
  158. package/src/gc/index.ts +17 -0
  159. package/src/gc/runner.ts +314 -0
  160. package/src/gc/trace-compactor.ts +187 -0
  161. package/src/index.ts +7 -1
  162. package/src/prompts/index.ts +2 -1
  163. package/src/quarantine/explorer.ts +234 -0
  164. package/src/quarantine/index.ts +7 -0
  165. package/src/quarantine/manager.ts +336 -0
  166. package/src/task/acceptance.ts +267 -0
  167. package/src/task/executor.ts +538 -0
  168. package/src/task/index.ts +38 -0
  169. package/src/task/planner.ts +294 -0
  170. package/src/task/storage.ts +332 -0
  171. package/src/trace/trace-event.ts +7 -26
  172. package/src/utils/file-utils.ts +61 -15
  173. package/tests/cli/gc.test.ts +206 -0
  174. package/tests/cli/init.test.ts +181 -0
  175. package/tests/cli/status.test.ts +282 -0
  176. package/tests/coordination/file-locks.test.ts +196 -0
  177. package/tests/coordination/inhibitors.test.ts +459 -0
  178. package/tests/coordination/integration.test.ts +195 -0
  179. package/tests/coordination/process-manager.test.ts +165 -0
  180. package/tests/gc/trace-compactor.test.ts +245 -0
  181. package/tests/integration/phase-7.test.ts +145 -0
  182. package/tests/quarantine/explorer.test.ts +381 -0
  183. package/tests/quarantine/manager.test.ts +399 -0
  184. package/tests/security/command-injection.test.ts +88 -0
  185. package/tests/security/path-traversal.test.ts +103 -0
  186. package/tests/task/acceptance.test.ts +411 -0
  187. package/tests/task/executor.test.ts +421 -0
  188. package/tests/task/planner.test.ts +359 -0
  189. package/tsconfig.json +2 -2
@@ -0,0 +1,282 @@
1
+ /**
2
+ * Status CLI Command Tests
3
+ *
4
+ * Tests for system status command
5
+ */
6
+
7
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
8
+ import * as fs from 'fs/promises';
9
+ import * as path from 'path';
10
+ import { fileURLToPath } from 'url';
11
+ import { dirname } from 'path';
12
+ import { fileExists } from '../../src/utils/file-utils.js';
13
+
14
+ const __filename = fileURLToPath(import.meta.url);
15
+ const __dirname = dirname(__filename);
16
+
17
+ // Use a temporary test directory
18
+ const testDir = path.join(__dirname, '../../.test-status');
19
+
20
+ describe('Status Command', () => {
21
+ beforeEach(async () => {
22
+ // Clean up test directory
23
+ if (fileExists(testDir)) {
24
+ await fs.rm(testDir, { recursive: true });
25
+ }
26
+ await fs.mkdir(testDir, { recursive: true });
27
+ await fs.mkdir(path.join(testDir, '.agent-meta'), { recursive: true });
28
+ });
29
+
30
+ afterEach(async () => {
31
+ // Clean up test directory
32
+ if (fileExists(testDir)) {
33
+ await fs.rm(testDir, { recursive: true });
34
+ }
35
+ });
36
+
37
+ it('should read system config', async () => {
38
+ const configPath = path.join(testDir, '.agent-meta/_config.json');
39
+
40
+ const config = {
41
+ version: '2.0.0',
42
+ created_at: new Date().toISOString(),
43
+ error_provider: 'file',
44
+ error_file: '.agent-meta/_errors.json',
45
+ ci_command: 'npm test',
46
+ spawn_count: 42,
47
+ last_gc_at_spawn: 0,
48
+ };
49
+
50
+ await fs.writeFile(configPath, JSON.stringify(config, null, 2));
51
+
52
+ const content = await fs.readFile(configPath, 'utf-8');
53
+ const loaded = JSON.parse(content);
54
+
55
+ expect(loaded.version).toBe('2.0.0');
56
+ expect(loaded.spawn_count).toBe(42);
57
+ });
58
+
59
+ it('should count trace events', async () => {
60
+ const tracesDir = path.join(testDir, '.agent-meta/traces');
61
+ await fs.mkdir(tracesDir, { recursive: true });
62
+
63
+ // Create a trace file with events
64
+ const traceFile = path.join(tracesDir, 'file1.ts.ndjson');
65
+ const events = [
66
+ {
67
+ id: 'event1',
68
+ timestamp: new Date().toISOString(),
69
+ file_path: 'file1.ts',
70
+ mode: 'error_reducer',
71
+ gradient_before: 0.5,
72
+ gradient_after: 0.6,
73
+ gradient_delta: 0.1,
74
+ metabolic_cost: 0.01,
75
+ efficiency: 0.8,
76
+ ci_passed: true,
77
+ changes: { additions: 10, deletions: 5, files_touched: ['file1.ts'] },
78
+ notes: [],
79
+ cost: {
80
+ tokens_in: 100,
81
+ tokens_out: 50,
82
+ model: 'claude-3-sonnet',
83
+ estimated_usd: 0.01,
84
+ },
85
+ },
86
+ {
87
+ id: 'event2',
88
+ timestamp: new Date().toISOString(),
89
+ file_path: 'file1.ts',
90
+ mode: 'error_reducer',
91
+ gradient_before: 0.6,
92
+ gradient_after: 0.7,
93
+ gradient_delta: 0.1,
94
+ metabolic_cost: 0.01,
95
+ efficiency: 0.9,
96
+ ci_passed: true,
97
+ changes: { additions: 8, deletions: 3, files_touched: ['file1.ts'] },
98
+ notes: [],
99
+ cost: {
100
+ tokens_in: 95,
101
+ tokens_out: 48,
102
+ model: 'claude-3-sonnet',
103
+ estimated_usd: 0.01,
104
+ },
105
+ },
106
+ ];
107
+
108
+ const content = events.map(e => JSON.stringify(e)).join('\n') + '\n';
109
+ await fs.writeFile(traceFile, content);
110
+
111
+ // Count events
112
+ const fileContent = await fs.readFile(traceFile, 'utf-8');
113
+ const lines = fileContent.split('\n').filter(Boolean);
114
+
115
+ expect(lines).toHaveLength(2);
116
+ });
117
+
118
+ it('should count quarantine entries', async () => {
119
+ const quarantinePath = path.join(testDir, '.agent-meta/_quarantine.json');
120
+
121
+ const quarantine = {
122
+ updated_at: new Date().toISOString(),
123
+ entries: [
124
+ {
125
+ file: 'problematic.ts',
126
+ quarantined_at: new Date().toISOString(),
127
+ reason: 'Too many failures',
128
+ attempts_before_quarantine: 3,
129
+ explorer_attempts: 0,
130
+ max_explorer_attempts: 3,
131
+ },
132
+ ],
133
+ };
134
+
135
+ await fs.writeFile(quarantinePath, JSON.stringify(quarantine, null, 2));
136
+
137
+ const content = await fs.readFile(quarantinePath, 'utf-8');
138
+ const loaded = JSON.parse(content);
139
+
140
+ expect(loaded.entries).toHaveLength(1);
141
+ });
142
+
143
+ it('should count inhibitors', async () => {
144
+ const inhibitorsPath = path.join(testDir, '.agent-meta/_inhibitors.ndjson');
145
+
146
+ const inhibitors = [
147
+ {
148
+ id: 'inh1',
149
+ timestamp: new Date().toISOString(),
150
+ trigger: { file: 'file1.ts', mode: 'error_reducer' },
151
+ signal: 'pattern matches known bad pattern',
152
+ origin: {
153
+ agent_id: 'agent1',
154
+ trace_id: 'trace1',
155
+ energy_wasted: 0.05,
156
+ failure_type: 'ci_failed' as const,
157
+ },
158
+ strength: 0.8,
159
+ half_life_days: 7,
160
+ },
161
+ {
162
+ id: 'inh2',
163
+ timestamp: new Date().toISOString(),
164
+ trigger: { file: 'file2.ts', mode: 'complexity_reducer' },
165
+ signal: 'reverted too many times',
166
+ origin: {
167
+ agent_id: 'agent2',
168
+ trace_id: 'trace2',
169
+ energy_wasted: 0.03,
170
+ failure_type: 'reverted' as const,
171
+ },
172
+ strength: 0.6,
173
+ half_life_days: 7,
174
+ },
175
+ ];
176
+
177
+ const content = inhibitors.map(i => JSON.stringify(i)).join('\n') + '\n';
178
+ await fs.writeFile(inhibitorsPath, content);
179
+
180
+ const fileContent = await fs.readFile(inhibitorsPath, 'utf-8');
181
+ const lines = fileContent.split('\n').filter(Boolean);
182
+
183
+ expect(lines).toHaveLength(2);
184
+ });
185
+
186
+ it('should count errors', async () => {
187
+ const errorsPath = path.join(testDir, '.agent-meta/_errors.json');
188
+
189
+ const errors = {
190
+ updated_at: new Date().toISOString(),
191
+ errors: [
192
+ {
193
+ file: 'file1.ts',
194
+ count: 3,
195
+ types: ['SyntaxError', 'RuntimeError'],
196
+ },
197
+ {
198
+ file: 'file2.ts',
199
+ count: 1,
200
+ types: ['TypeError'],
201
+ },
202
+ ],
203
+ };
204
+
205
+ await fs.writeFile(errorsPath, JSON.stringify(errors, null, 2));
206
+
207
+ const content = await fs.readFile(errorsPath, 'utf-8');
208
+ const loaded = JSON.parse(content);
209
+
210
+ expect(loaded.errors).toHaveLength(2);
211
+ });
212
+
213
+ it('should calculate spawns since last GC', async () => {
214
+ const configPath = path.join(testDir, '.agent-meta/_config.json');
215
+
216
+ const config = {
217
+ version: '2.0.0',
218
+ created_at: new Date().toISOString(),
219
+ error_provider: 'file',
220
+ error_file: '.agent-meta/_errors.json',
221
+ ci_command: 'npm test',
222
+ spawn_count: 150,
223
+ last_gc_at_spawn: 100,
224
+ };
225
+
226
+ await fs.writeFile(configPath, JSON.stringify(config, null, 2));
227
+
228
+ const content = await fs.readFile(configPath, 'utf-8');
229
+ const loaded = JSON.parse(content);
230
+
231
+ const spawnsSinceGC = loaded.spawn_count - loaded.last_gc_at_spawn;
232
+
233
+ expect(spawnsSinceGC).toBe(50);
234
+ });
235
+
236
+ it('should retrieve recent activity', async () => {
237
+ const tracesDir = path.join(testDir, '.agent-meta/traces');
238
+ await fs.mkdir(tracesDir, { recursive: true });
239
+
240
+ // Create multiple trace files
241
+ const events = [];
242
+ for (let i = 0; i < 5; i++) {
243
+ events.push({
244
+ id: `event${i}`,
245
+ timestamp: new Date(Date.now() - i * 1000).toISOString(),
246
+ file_path: `file${i}.ts`,
247
+ mode: i % 2 === 0 ? 'error_reducer' : 'complexity_reducer',
248
+ gradient_before: 0.5,
249
+ gradient_after: 0.6,
250
+ gradient_delta: 0.1,
251
+ metabolic_cost: 0.01,
252
+ efficiency: 0.8,
253
+ ci_passed: true,
254
+ changes: { additions: 10, deletions: 5, files_touched: [`file${i}.ts`] },
255
+ notes: [],
256
+ cost: {
257
+ tokens_in: 100,
258
+ tokens_out: 50,
259
+ model: 'claude-3-sonnet',
260
+ estimated_usd: 0.01,
261
+ },
262
+ });
263
+ }
264
+
265
+ const traceFile = path.join(tracesDir, 'activity.ndjson');
266
+ const content = events.map(e => JSON.stringify(e)).join('\n') + '\n';
267
+ await fs.writeFile(traceFile, content);
268
+
269
+ const fileContent = await fs.readFile(traceFile, 'utf-8');
270
+ const lines = fileContent.split('\n').filter(Boolean);
271
+ const loaded = lines.map(l => JSON.parse(l));
272
+
273
+ // Sort by timestamp descending
274
+ loaded.sort(
275
+ (a: any, b: any) =>
276
+ new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
277
+ );
278
+
279
+ expect(loaded).toHaveLength(5);
280
+ expect(loaded[0].file_path).toBe('file0.ts');
281
+ });
282
+ });
@@ -0,0 +1,196 @@
1
+ /**
2
+ * Tests for file-based locking mechanism
3
+ * Ensures atomic lock acquisition and proper expiration handling
4
+ */
5
+
6
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
7
+ import * as fs from 'fs/promises';
8
+ import * as path from 'path';
9
+ import {
10
+ acquireLock,
11
+ releaseLock,
12
+ checkLock,
13
+ listLocks,
14
+ cleanupStaleLocks,
15
+ } from '../../src/coordination/file-locks.js';
16
+ import type { Mode } from '../../src/types/index.js';
17
+
18
+ const LOCK_DIR = '.agent-meta/locks';
19
+ const TEST_FILE = 'test-file.ts';
20
+
21
+ describe('File Locking', () => {
22
+ beforeEach(async () => {
23
+ // Clean up locks directory before each test
24
+ try {
25
+ await fs.rm(LOCK_DIR, { recursive: true, force: true });
26
+ } catch {
27
+ // Ignore if doesn't exist
28
+ }
29
+ await fs.mkdir(LOCK_DIR, { recursive: true });
30
+ });
31
+
32
+ afterEach(async () => {
33
+ // Clean up after tests
34
+ await new Promise((resolve) => setTimeout(resolve, 10));
35
+ try {
36
+ await fs.rm(LOCK_DIR, { recursive: true, force: true });
37
+ } catch {
38
+ // Ignore
39
+ }
40
+ });
41
+
42
+ it('should acquire a lock successfully', async () => {
43
+ const result = await acquireLock(TEST_FILE, 'agent-1', 'error_reducer');
44
+ expect(result).toBe(true);
45
+
46
+ const lock = await checkLock(TEST_FILE);
47
+ expect(lock).not.toBeNull();
48
+ expect(lock?.agent_id).toBe('agent-1');
49
+ expect(lock?.file).toBe(TEST_FILE);
50
+ expect(lock?.mode).toBe('error_reducer');
51
+ expect(lock?.pid).toBe(process.pid);
52
+ });
53
+
54
+ it('should prevent race conditions - exactly one agent wins', async () => {
55
+ // Simulate two agents racing for the same file
56
+ const [result1, result2] = await Promise.all([
57
+ acquireLock(TEST_FILE, 'agent-1', 'error_reducer'),
58
+ acquireLock(TEST_FILE, 'agent-2', 'error_reducer'),
59
+ ]);
60
+
61
+ // Exactly one should succeed
62
+ const successes = [result1, result2].filter(Boolean);
63
+ expect(successes).toHaveLength(1);
64
+
65
+ // Check which agent got the lock
66
+ const lock = await checkLock(TEST_FILE);
67
+ expect(lock).not.toBeNull();
68
+ expect(['agent-1', 'agent-2']).toContain(lock?.agent_id);
69
+ });
70
+
71
+ it('should release a lock successfully', async () => {
72
+ await acquireLock(TEST_FILE, 'agent-1', 'error_reducer');
73
+
74
+ const released = await releaseLock(TEST_FILE);
75
+ expect(released).toBe(true);
76
+
77
+ const lock = await checkLock(TEST_FILE);
78
+ expect(lock).toBeNull();
79
+ });
80
+
81
+ it('should fail to acquire an already locked file', async () => {
82
+ const result1 = await acquireLock(TEST_FILE, 'agent-1', 'error_reducer');
83
+ expect(result1).toBe(true);
84
+
85
+ const result2 = await acquireLock(TEST_FILE, 'agent-2', 'complexity_reducer');
86
+ expect(result2).toBe(false);
87
+ });
88
+
89
+ it('should detect expired locks (5-minute timeout)', async () => {
90
+ // Create a lock file manually with expired timestamp
91
+ const lockPath = path.join(LOCK_DIR, 'test-file_ts.lock');
92
+ const expiredLock = {
93
+ agent_id: 'agent-old',
94
+ file: TEST_FILE,
95
+ mode: 'error_reducer' as Mode,
96
+ acquired_at: new Date(Date.now() - 10 * 60 * 1000).toISOString(), // 10 mins ago
97
+ expires_at: new Date(Date.now() - 5 * 60 * 1000).toISOString(), // Expired 5 mins ago
98
+ pid: 99999, // Non-existent PID
99
+ };
100
+
101
+ await fs.writeFile(lockPath, JSON.stringify(expiredLock, null, 2));
102
+
103
+ // Check should return null for expired lock
104
+ const lock = await checkLock(TEST_FILE);
105
+ expect(lock).toBeNull();
106
+
107
+ // Should be able to acquire
108
+ const result = await acquireLock(TEST_FILE, 'agent-new', 'error_reducer');
109
+ expect(result).toBe(true);
110
+ });
111
+
112
+ it('should handle dead process takeover', async () => {
113
+ // Create a lock file with a dead process PID
114
+ const lockPath = path.join(LOCK_DIR, 'test-file_ts.lock');
115
+ const deadProcessLock = {
116
+ agent_id: 'agent-old',
117
+ file: TEST_FILE,
118
+ mode: 'error_reducer' as Mode,
119
+ acquired_at: new Date().toISOString(),
120
+ expires_at: new Date(Date.now() + 5 * 60 * 1000).toISOString(), // Not expired
121
+ pid: 99999, // Non-existent PID
122
+ };
123
+
124
+ await fs.writeFile(lockPath, JSON.stringify(deadProcessLock, null, 2));
125
+
126
+ // Check should return null for dead process
127
+ const lock = await checkLock(TEST_FILE);
128
+ expect(lock).toBeNull();
129
+
130
+ // Should be able to take over
131
+ const result = await acquireLock(TEST_FILE, 'agent-new', 'error_reducer');
132
+ expect(result).toBe(true);
133
+
134
+ // Verify new agent has the lock
135
+ const newLock = await checkLock(TEST_FILE);
136
+ expect(newLock?.agent_id).toBe('agent-new');
137
+ });
138
+
139
+ it('should list all active locks', async () => {
140
+ await acquireLock('file1.ts', 'agent-1', 'error_reducer');
141
+ await acquireLock('file2.ts', 'agent-2', 'complexity_reducer');
142
+ await acquireLock('file3.ts', 'agent-3', 'debt_payer');
143
+
144
+ const locks = await listLocks();
145
+ expect(locks).toHaveLength(3);
146
+
147
+ const agentIds = locks.map((l) => l.agent_id).sort();
148
+ expect(agentIds).toEqual(['agent-1', 'agent-2', 'agent-3']);
149
+ });
150
+
151
+ it('should clean up stale locks', async () => {
152
+ // Create one valid lock
153
+ await acquireLock('file1.ts', 'agent-1', 'error_reducer');
154
+
155
+ // Create expired lock manually
156
+ const expiredLockPath = path.join(LOCK_DIR, 'file2_ts.lock');
157
+ const expiredLock = {
158
+ agent_id: 'agent-old',
159
+ file: 'file2.ts',
160
+ mode: 'error_reducer' as Mode,
161
+ acquired_at: new Date(Date.now() - 10 * 60 * 1000).toISOString(),
162
+ expires_at: new Date(Date.now() - 5 * 60 * 1000).toISOString(),
163
+ pid: 99999,
164
+ };
165
+ await fs.writeFile(expiredLockPath, JSON.stringify(expiredLock, null, 2));
166
+
167
+ const cleaned = await cleanupStaleLocks();
168
+ expect(cleaned).toBe(1);
169
+
170
+ // Should only have the valid lock now
171
+ const locks = await listLocks();
172
+ expect(locks).toHaveLength(1);
173
+ expect(locks[0].agent_id).toBe('agent-1');
174
+ });
175
+
176
+ it('should include task_id in lock if provided', async () => {
177
+ await acquireLock(TEST_FILE, 'agent-1', 'error_reducer', 'task-123');
178
+
179
+ const lock = await checkLock(TEST_FILE);
180
+ expect(lock?.task_id).toBe('task-123');
181
+ });
182
+
183
+ it('should handle concurrent lock attempts on different files', async () => {
184
+ const results = await Promise.all([
185
+ acquireLock('file1.ts', 'agent-1', 'error_reducer'),
186
+ acquireLock('file2.ts', 'agent-2', 'complexity_reducer'),
187
+ acquireLock('file3.ts', 'agent-3', 'debt_payer'),
188
+ ]);
189
+
190
+ // All should succeed since they're different files
191
+ expect(results.every((r) => r === true)).toBe(true);
192
+
193
+ const locks = await listLocks();
194
+ expect(locks).toHaveLength(3);
195
+ });
196
+ });