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.
- package/.agent-meta/_inhibitors.ndjson +1287 -0
- package/.agent-meta/_quarantine.json +45 -0
- package/.agent-meta/config.json +9 -0
- package/.claude/memory.db +0 -0
- package/.claude/settings.local.json +4 -1
- package/README.md +81 -235
- package/SECURITY.md +145 -0
- package/dist/agent/worker.d.ts +8 -0
- package/dist/agent/worker.d.ts.map +1 -0
- package/dist/agent/worker.js +97 -0
- package/dist/agent/worker.js.map +1 -0
- package/dist/bin.d.ts +7 -0
- package/dist/bin.d.ts.map +1 -0
- package/dist/bin.js +11 -0
- package/dist/bin.js.map +1 -0
- package/dist/cli/cost.d.ts +10 -0
- package/dist/cli/cost.d.ts.map +1 -0
- package/dist/cli/cost.js +163 -0
- package/dist/cli/cost.js.map +1 -0
- package/dist/cli/gc.d.ts +10 -0
- package/dist/cli/gc.d.ts.map +1 -0
- package/dist/cli/gc.js +108 -0
- package/dist/cli/gc.js.map +1 -0
- package/dist/cli/gradients.d.ts +10 -0
- package/dist/cli/gradients.d.ts.map +1 -0
- package/dist/cli/gradients.js +69 -0
- package/dist/cli/gradients.js.map +1 -0
- package/dist/cli/index.d.ts +17 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +72 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/init.d.ts +11 -0
- package/dist/cli/init.d.ts.map +1 -0
- package/dist/cli/init.js +97 -0
- package/dist/cli/init.js.map +1 -0
- package/dist/cli/status.d.ts +10 -0
- package/dist/cli/status.d.ts.map +1 -0
- package/dist/cli/status.js +191 -0
- package/dist/cli/status.js.map +1 -0
- package/dist/coordination/file-locks.d.ts +42 -0
- package/dist/coordination/file-locks.d.ts.map +1 -0
- package/dist/coordination/file-locks.js +269 -0
- package/dist/coordination/file-locks.js.map +1 -0
- package/dist/coordination/index.d.ts +4 -0
- package/dist/coordination/index.d.ts.map +1 -1
- package/dist/coordination/index.js +4 -0
- package/dist/coordination/index.js.map +1 -1
- package/dist/coordination/inhibitors.d.ts +84 -0
- package/dist/coordination/inhibitors.d.ts.map +1 -0
- package/dist/coordination/inhibitors.js +290 -0
- package/dist/coordination/inhibitors.js.map +1 -0
- package/dist/coordination/process-manager.d.ts +73 -0
- package/dist/coordination/process-manager.d.ts.map +1 -0
- package/dist/coordination/process-manager.js +144 -0
- package/dist/coordination/process-manager.js.map +1 -0
- package/dist/core/agent-executor.d.ts.map +1 -1
- package/dist/core/agent-executor.js +28 -10
- package/dist/core/agent-executor.js.map +1 -1
- package/dist/core/change-applier.d.ts +29 -5
- package/dist/core/change-applier.d.ts.map +1 -1
- package/dist/core/change-applier.js +254 -24
- package/dist/core/change-applier.js.map +1 -1
- package/dist/core/signals/churn.d.ts.map +1 -1
- package/dist/core/signals/churn.js +6 -4
- package/dist/core/signals/churn.js.map +1 -1
- package/dist/core/signals/debt.d.ts.map +1 -1
- package/dist/core/signals/debt.js +4 -3
- package/dist/core/signals/debt.js.map +1 -1
- package/dist/cost/cost-tracker.d.ts.map +1 -1
- package/dist/cost/cost-tracker.js +2 -0
- package/dist/cost/cost-tracker.js.map +1 -1
- package/dist/gc/index.d.ts +17 -0
- package/dist/gc/index.d.ts.map +1 -0
- package/dist/gc/index.js +17 -0
- package/dist/gc/index.js.map +1 -0
- package/dist/gc/runner.d.ts +39 -0
- package/dist/gc/runner.d.ts.map +1 -0
- package/dist/gc/runner.js +277 -0
- package/dist/gc/runner.js.map +1 -0
- package/dist/gc/trace-compactor.d.ts +31 -0
- package/dist/gc/trace-compactor.d.ts.map +1 -0
- package/dist/gc/trace-compactor.js +162 -0
- package/dist/gc/trace-compactor.js.map +1 -0
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -1
- package/dist/index.js.map +1 -1
- package/dist/prompts/index.d.ts +2 -1
- package/dist/prompts/index.d.ts.map +1 -1
- package/dist/prompts/index.js.map +1 -1
- package/dist/quarantine/explorer.d.ts +65 -0
- package/dist/quarantine/explorer.d.ts.map +1 -0
- package/dist/quarantine/explorer.js +175 -0
- package/dist/quarantine/explorer.js.map +1 -0
- package/dist/quarantine/index.d.ts +7 -0
- package/dist/quarantine/index.d.ts.map +1 -0
- package/dist/quarantine/index.js +7 -0
- package/dist/quarantine/index.js.map +1 -0
- package/dist/quarantine/manager.d.ts +75 -0
- package/dist/quarantine/manager.d.ts.map +1 -0
- package/dist/quarantine/manager.js +275 -0
- package/dist/quarantine/manager.js.map +1 -0
- package/dist/task/acceptance.d.ts +29 -0
- package/dist/task/acceptance.d.ts.map +1 -0
- package/dist/task/acceptance.js +228 -0
- package/dist/task/acceptance.js.map +1 -0
- package/dist/task/executor.d.ts +30 -0
- package/dist/task/executor.d.ts.map +1 -0
- package/dist/task/executor.js +429 -0
- package/dist/task/executor.js.map +1 -0
- package/dist/task/index.d.ts +12 -0
- package/dist/task/index.d.ts.map +1 -0
- package/dist/task/index.js +12 -0
- package/dist/task/index.js.map +1 -0
- package/dist/task/planner.d.ts +21 -0
- package/dist/task/planner.d.ts.map +1 -0
- package/dist/task/planner.js +253 -0
- package/dist/task/planner.js.map +1 -0
- package/dist/task/storage.d.ts +46 -0
- package/dist/task/storage.d.ts.map +1 -0
- package/dist/task/storage.js +266 -0
- package/dist/task/storage.js.map +1 -0
- package/dist/trace/trace-event.d.ts +2 -18
- package/dist/trace/trace-event.d.ts.map +1 -1
- package/dist/trace/trace-event.js +6 -6
- package/dist/trace/trace-event.js.map +1 -1
- package/dist/utils/file-utils.d.ts.map +1 -1
- package/dist/utils/file-utils.js +54 -15
- package/dist/utils/file-utils.js.map +1 -1
- package/docs/PHASE5_IMPLEMENTATION.md +237 -0
- package/docs/PHASES-3-7-COMPLETE.md +177 -0
- package/docs/PHASE_4_COMPLETE.md +135 -0
- package/docs/PHASE_7_DELIVERABLES.md +295 -0
- package/docs/PHASE_7_IMPLEMENTATION.md +306 -0
- package/docs/PHASE_7_SUMMARY.txt +195 -0
- package/docs/RELEASE-NOTES-v2.1.md +213 -0
- package/docs/ROADMAP.md +64 -57
- package/docs/SECURITY-AUDIT.md +387 -0
- package/docs/SNAPSHOT.md +59 -32
- package/docs/implementation/phase3-summary.md +220 -0
- package/package.json +19 -11
- package/src/agent/worker.ts +111 -0
- package/src/bin.ts +13 -0
- package/src/cli/cost.ts +210 -0
- package/src/cli/gc.ts +138 -0
- package/src/cli/gradients.ts +95 -0
- package/src/cli/index.ts +79 -0
- package/src/cli/init.ts +139 -0
- package/src/cli/status.ts +218 -0
- package/src/coordination/file-locks.ts +300 -0
- package/src/coordination/index.ts +4 -0
- package/src/coordination/inhibitors.ts +345 -0
- package/src/coordination/process-manager.ts +199 -0
- package/src/core/agent-executor.ts +20 -4
- package/src/core/signals/churn.ts +8 -5
- package/src/core/signals/debt.ts +4 -3
- package/src/cost/cost-tracker.ts +2 -0
- package/src/gc/index.ts +17 -0
- package/src/gc/runner.ts +314 -0
- package/src/gc/trace-compactor.ts +187 -0
- package/src/index.ts +7 -1
- package/src/prompts/index.ts +2 -1
- package/src/quarantine/explorer.ts +234 -0
- package/src/quarantine/index.ts +7 -0
- package/src/quarantine/manager.ts +336 -0
- package/src/task/acceptance.ts +267 -0
- package/src/task/executor.ts +538 -0
- package/src/task/index.ts +38 -0
- package/src/task/planner.ts +294 -0
- package/src/task/storage.ts +332 -0
- package/src/trace/trace-event.ts +7 -26
- package/src/utils/file-utils.ts +61 -15
- package/tests/cli/gc.test.ts +206 -0
- package/tests/cli/init.test.ts +181 -0
- package/tests/cli/status.test.ts +282 -0
- package/tests/coordination/file-locks.test.ts +196 -0
- package/tests/coordination/inhibitors.test.ts +459 -0
- package/tests/coordination/integration.test.ts +195 -0
- package/tests/coordination/process-manager.test.ts +165 -0
- package/tests/gc/trace-compactor.test.ts +245 -0
- package/tests/integration/phase-7.test.ts +145 -0
- package/tests/quarantine/explorer.test.ts +381 -0
- package/tests/quarantine/manager.test.ts +399 -0
- package/tests/security/command-injection.test.ts +88 -0
- package/tests/security/path-traversal.test.ts +103 -0
- package/tests/task/acceptance.test.ts +411 -0
- package/tests/task/executor.test.ts +421 -0
- package/tests/task/planner.test.ts +359 -0
- 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
|
+
});
|