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.
- package/.agent-meta/_inhibitors.ndjson +1287 -0
- package/.agent-meta/_quarantine.json +45 -0
- package/.agent-meta/config.json +9 -0
- package/.agent-meta/tasks/_active.json +4 -0
- package/.agent-meta/tasks/task_0657b028-05a0-4b0c-b0b9-a4eae3d66cd9.json +168 -0
- package/.claude/memory.db +0 -0
- package/.claude/settings.local.json +4 -1
- package/README.md +85 -233
- package/SECURITY.md +145 -0
- package/dist/agent/task-worker.d.ts +11 -0
- package/dist/agent/task-worker.d.ts.map +1 -0
- package/dist/agent/task-worker.js +173 -0
- package/dist/agent/task-worker.js.map +1 -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 +70 -0
- package/dist/cli/gradients.js.map +1 -0
- package/dist/cli/grow.d.ts +17 -0
- package/dist/cli/grow.d.ts.map +1 -0
- package/dist/cli/grow.js +373 -0
- package/dist/cli/grow.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 +74 -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 +4 -1
- package/dist/core/agent-executor.d.ts.map +1 -1
- package/dist/core/agent-executor.js +38 -12
- 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/agent-coordinator.d.ts +40 -0
- package/dist/task/agent-coordinator.d.ts.map +1 -0
- package/dist/task/agent-coordinator.js +168 -0
- package/dist/task/agent-coordinator.js.map +1 -0
- package/dist/task/executor.d.ts +37 -0
- package/dist/task/executor.d.ts.map +1 -0
- package/dist/task/executor.js +462 -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 +194 -107
- package/docs/SECURITY-AUDIT.md +387 -0
- package/docs/SNAPSHOT.md +59 -32
- package/docs/implementation/phase3-summary.md +220 -0
- package/package.json +27 -11
- package/src/agent/task-worker.ts +196 -0
- 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 +97 -0
- package/src/cli/grow.ts +416 -0
- package/src/cli/index.ts +81 -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 +37 -8
- 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/agent-coordinator.ts +220 -0
- package/src/task/executor.ts +543 -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/tests/trace/trace-event.test.ts +62 -20
- package/tsconfig.json +2 -2
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for Inhibitor Signal System
|
|
3
|
+
* Validates emission, decay, and querying
|
|
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
|
+
emitInhibitor,
|
|
11
|
+
queryInhibitors,
|
|
12
|
+
calculateCurrentStrength,
|
|
13
|
+
readInhibitors,
|
|
14
|
+
extractFailurePattern,
|
|
15
|
+
summarizeCIFailure,
|
|
16
|
+
maybeEmitInhibitor,
|
|
17
|
+
checkForLoop,
|
|
18
|
+
gcInhibitors,
|
|
19
|
+
formatInhibitorForPrompt,
|
|
20
|
+
} from '../../src/coordination/inhibitors.js';
|
|
21
|
+
import type { Inhibitor, Mode } from '../../src/types/index.js';
|
|
22
|
+
import type { TraceEvent } from '../../src/trace/trace-event.js';
|
|
23
|
+
|
|
24
|
+
describe('Inhibitor System', () => {
|
|
25
|
+
let TEST_DIR: string;
|
|
26
|
+
let INHIBITORS_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
|
+
INHIBITORS_FILE = path.join(TEST_DIR, '_inhibitors.ndjson');
|
|
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
|
+
describe('emitInhibitor', () => {
|
|
51
|
+
it('should emit inhibitor with correct structure', async () => {
|
|
52
|
+
const inhibitor = await emitInhibitor({
|
|
53
|
+
trigger: {
|
|
54
|
+
file: 'src/test.ts',
|
|
55
|
+
mode: 'complexity_reducer',
|
|
56
|
+
pattern: 'removing null checks',
|
|
57
|
+
},
|
|
58
|
+
signal: 'CI failed: TypeError: Cannot read property x of undefined',
|
|
59
|
+
origin: {
|
|
60
|
+
agent_id: 'agent-123',
|
|
61
|
+
trace_id: 'trace-456',
|
|
62
|
+
energy_wasted: 0.04,
|
|
63
|
+
failure_type: 'ci_failed',
|
|
64
|
+
},
|
|
65
|
+
strength: 1.0,
|
|
66
|
+
half_life_days: 30,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
expect(inhibitor.id).toBeDefined();
|
|
70
|
+
expect(inhibitor.timestamp).toBeDefined();
|
|
71
|
+
expect(inhibitor.trigger.file).toBe('src/test.ts');
|
|
72
|
+
expect(inhibitor.trigger.mode).toBe('complexity_reducer');
|
|
73
|
+
expect(inhibitor.trigger.pattern).toBe('removing null checks');
|
|
74
|
+
expect(inhibitor.strength).toBe(1.0);
|
|
75
|
+
expect(inhibitor.half_life_days).toBe(30);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('should append to NDJSON file', async () => {
|
|
79
|
+
await emitInhibitor({
|
|
80
|
+
trigger: { file: 'test1.ts', mode: 'error_reducer' },
|
|
81
|
+
signal: 'Test failure 1',
|
|
82
|
+
origin: {
|
|
83
|
+
agent_id: 'a1',
|
|
84
|
+
trace_id: 't1',
|
|
85
|
+
energy_wasted: 0.01,
|
|
86
|
+
failure_type: 'ci_failed',
|
|
87
|
+
},
|
|
88
|
+
strength: 1.0,
|
|
89
|
+
half_life_days: 30,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
await emitInhibitor({
|
|
93
|
+
trigger: { file: 'test2.ts', mode: 'debt_payer' },
|
|
94
|
+
signal: 'Test failure 2',
|
|
95
|
+
origin: {
|
|
96
|
+
agent_id: 'a2',
|
|
97
|
+
trace_id: 't2',
|
|
98
|
+
energy_wasted: 0.02,
|
|
99
|
+
failure_type: 'regression',
|
|
100
|
+
},
|
|
101
|
+
strength: 0.8,
|
|
102
|
+
half_life_days: 30,
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
const all = await readInhibitors();
|
|
106
|
+
expect(all.length).toBe(2);
|
|
107
|
+
expect(all[0].trigger.file).toBe('test1.ts');
|
|
108
|
+
expect(all[1].trigger.file).toBe('test2.ts');
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe('calculateCurrentStrength', () => {
|
|
113
|
+
it('should calculate correct decay at 0 days', () => {
|
|
114
|
+
const inhibitor: Inhibitor = {
|
|
115
|
+
id: '1',
|
|
116
|
+
timestamp: new Date().toISOString(),
|
|
117
|
+
trigger: {},
|
|
118
|
+
signal: 'test',
|
|
119
|
+
origin: {
|
|
120
|
+
agent_id: 'a1',
|
|
121
|
+
trace_id: 't1',
|
|
122
|
+
energy_wasted: 0.01,
|
|
123
|
+
failure_type: 'ci_failed',
|
|
124
|
+
},
|
|
125
|
+
strength: 1.0,
|
|
126
|
+
half_life_days: 30,
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const strength = calculateCurrentStrength(inhibitor);
|
|
130
|
+
expect(strength).toBeCloseTo(1.0, 2);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('should decay to 0.5 at one half-life (30 days)', () => {
|
|
134
|
+
const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
|
|
135
|
+
const inhibitor: Inhibitor = {
|
|
136
|
+
id: '1',
|
|
137
|
+
timestamp: thirtyDaysAgo.toISOString(),
|
|
138
|
+
trigger: {},
|
|
139
|
+
signal: 'test',
|
|
140
|
+
origin: {
|
|
141
|
+
agent_id: 'a1',
|
|
142
|
+
trace_id: 't1',
|
|
143
|
+
energy_wasted: 0.01,
|
|
144
|
+
failure_type: 'ci_failed',
|
|
145
|
+
},
|
|
146
|
+
strength: 1.0,
|
|
147
|
+
half_life_days: 30,
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const strength = calculateCurrentStrength(inhibitor);
|
|
151
|
+
expect(strength).toBeCloseTo(0.5, 2);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('should decay to 0.25 at two half-lives (60 days)', () => {
|
|
155
|
+
const sixtyDaysAgo = new Date(Date.now() - 60 * 24 * 60 * 60 * 1000);
|
|
156
|
+
const inhibitor: Inhibitor = {
|
|
157
|
+
id: '1',
|
|
158
|
+
timestamp: sixtyDaysAgo.toISOString(),
|
|
159
|
+
trigger: {},
|
|
160
|
+
signal: 'test',
|
|
161
|
+
origin: {
|
|
162
|
+
agent_id: 'a1',
|
|
163
|
+
trace_id: 't1',
|
|
164
|
+
energy_wasted: 0.01,
|
|
165
|
+
failure_type: 'ci_failed',
|
|
166
|
+
},
|
|
167
|
+
strength: 1.0,
|
|
168
|
+
half_life_days: 30,
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const strength = calculateCurrentStrength(inhibitor);
|
|
172
|
+
expect(strength).toBeCloseTo(0.25, 2);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('should decay to 0.125 at three half-lives (90 days)', () => {
|
|
176
|
+
const ninetyDaysAgo = new Date(Date.now() - 90 * 24 * 60 * 60 * 1000);
|
|
177
|
+
const inhibitor: Inhibitor = {
|
|
178
|
+
id: '1',
|
|
179
|
+
timestamp: ninetyDaysAgo.toISOString(),
|
|
180
|
+
trigger: {},
|
|
181
|
+
signal: 'test',
|
|
182
|
+
origin: {
|
|
183
|
+
agent_id: 'a1',
|
|
184
|
+
trace_id: 't1',
|
|
185
|
+
energy_wasted: 0.01,
|
|
186
|
+
failure_type: 'ci_failed',
|
|
187
|
+
},
|
|
188
|
+
strength: 1.0,
|
|
189
|
+
half_life_days: 30,
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
const strength = calculateCurrentStrength(inhibitor);
|
|
193
|
+
expect(strength).toBeCloseTo(0.125, 2);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('should respect custom half-life', () => {
|
|
197
|
+
const fifteenDaysAgo = new Date(Date.now() - 15 * 24 * 60 * 60 * 1000);
|
|
198
|
+
const inhibitor: Inhibitor = {
|
|
199
|
+
id: '1',
|
|
200
|
+
timestamp: fifteenDaysAgo.toISOString(),
|
|
201
|
+
trigger: {},
|
|
202
|
+
signal: 'test',
|
|
203
|
+
origin: {
|
|
204
|
+
agent_id: 'a1',
|
|
205
|
+
trace_id: 't1',
|
|
206
|
+
energy_wasted: 0.01,
|
|
207
|
+
failure_type: 'ci_failed',
|
|
208
|
+
},
|
|
209
|
+
strength: 1.0,
|
|
210
|
+
half_life_days: 15, // 15-day half-life instead of 30
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const strength = calculateCurrentStrength(inhibitor);
|
|
214
|
+
expect(strength).toBeCloseTo(0.5, 2); // One half-life at 15 days
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
describe('queryInhibitors', () => {
|
|
219
|
+
it('should filter by relevance threshold (>0.2)', async () => {
|
|
220
|
+
// Fresh inhibitor (strength 1.0)
|
|
221
|
+
await emitInhibitor({
|
|
222
|
+
trigger: { file: 'test.ts', mode: 'error_reducer' },
|
|
223
|
+
signal: 'Fresh failure',
|
|
224
|
+
origin: {
|
|
225
|
+
agent_id: 'a1',
|
|
226
|
+
trace_id: 't1',
|
|
227
|
+
energy_wasted: 0.01,
|
|
228
|
+
failure_type: 'ci_failed',
|
|
229
|
+
},
|
|
230
|
+
strength: 1.0,
|
|
231
|
+
half_life_days: 30,
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
// Old inhibitor (strength ~0.125 at 90 days)
|
|
235
|
+
const ninetyDaysAgo = new Date(Date.now() - 90 * 24 * 60 * 60 * 1000);
|
|
236
|
+
const oldInhibitor: Inhibitor = {
|
|
237
|
+
id: '2',
|
|
238
|
+
timestamp: ninetyDaysAgo.toISOString(),
|
|
239
|
+
trigger: { file: 'test.ts', mode: 'error_reducer' },
|
|
240
|
+
signal: 'Old failure',
|
|
241
|
+
origin: {
|
|
242
|
+
agent_id: 'a2',
|
|
243
|
+
trace_id: 't2',
|
|
244
|
+
energy_wasted: 0.01,
|
|
245
|
+
failure_type: 'regression',
|
|
246
|
+
},
|
|
247
|
+
strength: 1.0,
|
|
248
|
+
half_life_days: 30,
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
// Manually write old inhibitor
|
|
252
|
+
const inhibitorsFile = path.join(TEST_DIR, '_inhibitors.ndjson');
|
|
253
|
+
fs.appendFileSync(inhibitorsFile, JSON.stringify(oldInhibitor) + '\n');
|
|
254
|
+
|
|
255
|
+
const relevant = await queryInhibitors('test.ts', 'error_reducer');
|
|
256
|
+
|
|
257
|
+
// Only fresh one should be included (old one is 0.125 < 0.2)
|
|
258
|
+
expect(relevant.length).toBe(1);
|
|
259
|
+
expect(relevant[0].signal).toBe('Fresh failure');
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it('should match by file', async () => {
|
|
263
|
+
await emitInhibitor({
|
|
264
|
+
trigger: { file: 'match.ts', mode: 'error_reducer' },
|
|
265
|
+
signal: 'Match',
|
|
266
|
+
origin: {
|
|
267
|
+
agent_id: 'a1',
|
|
268
|
+
trace_id: 't1',
|
|
269
|
+
energy_wasted: 0.01,
|
|
270
|
+
failure_type: 'ci_failed',
|
|
271
|
+
},
|
|
272
|
+
strength: 1.0,
|
|
273
|
+
half_life_days: 30,
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
await emitInhibitor({
|
|
277
|
+
trigger: { file: 'nomatch.ts', mode: 'error_reducer' },
|
|
278
|
+
signal: 'No match',
|
|
279
|
+
origin: {
|
|
280
|
+
agent_id: 'a2',
|
|
281
|
+
trace_id: 't2',
|
|
282
|
+
energy_wasted: 0.01,
|
|
283
|
+
failure_type: 'ci_failed',
|
|
284
|
+
},
|
|
285
|
+
strength: 1.0,
|
|
286
|
+
half_life_days: 30,
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
const relevant = await queryInhibitors('match.ts', 'error_reducer');
|
|
290
|
+
expect(relevant.length).toBe(1);
|
|
291
|
+
expect(relevant[0].signal).toBe('Match');
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it('should match by mode', async () => {
|
|
295
|
+
await emitInhibitor({
|
|
296
|
+
trigger: { file: 'test.ts', mode: 'error_reducer' },
|
|
297
|
+
signal: 'Error reducer',
|
|
298
|
+
origin: {
|
|
299
|
+
agent_id: 'a1',
|
|
300
|
+
trace_id: 't1',
|
|
301
|
+
energy_wasted: 0.01,
|
|
302
|
+
failure_type: 'ci_failed',
|
|
303
|
+
},
|
|
304
|
+
strength: 1.0,
|
|
305
|
+
half_life_days: 30,
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
await emitInhibitor({
|
|
309
|
+
trigger: { file: 'test.ts', mode: 'complexity_reducer' },
|
|
310
|
+
signal: 'Complexity reducer',
|
|
311
|
+
origin: {
|
|
312
|
+
agent_id: 'a2',
|
|
313
|
+
trace_id: 't2',
|
|
314
|
+
energy_wasted: 0.01,
|
|
315
|
+
failure_type: 'ci_failed',
|
|
316
|
+
},
|
|
317
|
+
strength: 1.0,
|
|
318
|
+
half_life_days: 30,
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
const relevant = await queryInhibitors('test.ts', 'error_reducer');
|
|
322
|
+
expect(relevant.length).toBe(1);
|
|
323
|
+
expect(relevant[0].signal).toBe('Error reducer');
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it('should sort by strength (strongest first)', async () => {
|
|
327
|
+
await emitInhibitor({
|
|
328
|
+
trigger: { file: 'test.ts' },
|
|
329
|
+
signal: 'Weak',
|
|
330
|
+
origin: {
|
|
331
|
+
agent_id: 'a1',
|
|
332
|
+
trace_id: 't1',
|
|
333
|
+
energy_wasted: 0.01,
|
|
334
|
+
failure_type: 'ci_failed',
|
|
335
|
+
},
|
|
336
|
+
strength: 0.3,
|
|
337
|
+
half_life_days: 30,
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
await emitInhibitor({
|
|
341
|
+
trigger: { file: 'test.ts' },
|
|
342
|
+
signal: 'Strong',
|
|
343
|
+
origin: {
|
|
344
|
+
agent_id: 'a2',
|
|
345
|
+
trace_id: 't2',
|
|
346
|
+
energy_wasted: 0.01,
|
|
347
|
+
failure_type: 'ci_failed',
|
|
348
|
+
},
|
|
349
|
+
strength: 0.9,
|
|
350
|
+
half_life_days: 30,
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
const relevant = await queryInhibitors('test.ts', 'error_reducer');
|
|
354
|
+
expect(relevant[0].signal).toBe('Strong');
|
|
355
|
+
expect(relevant[1].signal).toBe('Weak');
|
|
356
|
+
});
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
describe('extractFailurePattern', () => {
|
|
360
|
+
it('should extract TypeError pattern', () => {
|
|
361
|
+
const output = 'TypeError: Cannot read property x of undefined';
|
|
362
|
+
expect(extractFailurePattern(output)).toBe('changes causing TypeError');
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
it('should extract module import pattern', () => {
|
|
366
|
+
const output = "Error: Cannot find module './missing'";
|
|
367
|
+
expect(extractFailurePattern(output)).toBe('breaking imports');
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
it('should extract type assignment pattern', () => {
|
|
371
|
+
const output = 'error TS2322: Type string is not assignable to type number';
|
|
372
|
+
expect(extractFailurePattern(output)).toBe('type mismatches');
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
it('should return undefined for unknown patterns', () => {
|
|
376
|
+
const output = 'Some unknown error';
|
|
377
|
+
expect(extractFailurePattern(output)).toBeUndefined();
|
|
378
|
+
});
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
describe('gcInhibitors', () => {
|
|
382
|
+
it('should remove decayed inhibitors (strength < 0.05)', async () => {
|
|
383
|
+
// Fresh inhibitor (keep)
|
|
384
|
+
await emitInhibitor({
|
|
385
|
+
trigger: { file: 'keep.ts' },
|
|
386
|
+
signal: 'Keep me',
|
|
387
|
+
origin: {
|
|
388
|
+
agent_id: 'a1',
|
|
389
|
+
trace_id: 't1',
|
|
390
|
+
energy_wasted: 0.01,
|
|
391
|
+
failure_type: 'ci_failed',
|
|
392
|
+
},
|
|
393
|
+
strength: 1.0,
|
|
394
|
+
half_life_days: 30,
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
// Very old inhibitor (remove - strength ~0.03125 at 150 days, 5 half-lives)
|
|
398
|
+
// At 150 days with 30-day half-life: 0.5^5 = 0.03125 < 0.05
|
|
399
|
+
const veryOld = new Date(Date.now() - 150 * 24 * 60 * 60 * 1000);
|
|
400
|
+
const oldInhibitor: Inhibitor = {
|
|
401
|
+
id: '2',
|
|
402
|
+
timestamp: veryOld.toISOString(),
|
|
403
|
+
trigger: { file: 'remove.ts' },
|
|
404
|
+
signal: 'Remove me',
|
|
405
|
+
origin: {
|
|
406
|
+
agent_id: 'a2',
|
|
407
|
+
trace_id: 't2',
|
|
408
|
+
energy_wasted: 0.01,
|
|
409
|
+
failure_type: 'regression',
|
|
410
|
+
},
|
|
411
|
+
strength: 1.0,
|
|
412
|
+
half_life_days: 30,
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
const inhibitorsFile = path.join(TEST_DIR, '_inhibitors.ndjson');
|
|
416
|
+
fs.appendFileSync(inhibitorsFile, JSON.stringify(oldInhibitor) + '\n');
|
|
417
|
+
|
|
418
|
+
const result = await gcInhibitors();
|
|
419
|
+
|
|
420
|
+
expect(result.removed).toBeGreaterThan(0);
|
|
421
|
+
expect(result.kept).toBeGreaterThan(0);
|
|
422
|
+
|
|
423
|
+
const remaining = await readInhibitors();
|
|
424
|
+
expect(remaining.every(i => i.signal !== 'Remove me')).toBe(true);
|
|
425
|
+
});
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
describe('formatInhibitorForPrompt', () => {
|
|
429
|
+
it('should format inhibitor for display', () => {
|
|
430
|
+
const inhibitor = {
|
|
431
|
+
id: '1',
|
|
432
|
+
timestamp: new Date().toISOString(),
|
|
433
|
+
trigger: {
|
|
434
|
+
file: 'test.ts',
|
|
435
|
+
mode: 'complexity_reducer' as Mode,
|
|
436
|
+
pattern: 'removing null checks',
|
|
437
|
+
},
|
|
438
|
+
signal: 'CI failed: TypeError',
|
|
439
|
+
origin: {
|
|
440
|
+
agent_id: 'agent-123',
|
|
441
|
+
trace_id: 'trace-456',
|
|
442
|
+
energy_wasted: 0.04,
|
|
443
|
+
failure_type: 'ci_failed' as const,
|
|
444
|
+
},
|
|
445
|
+
strength: 1.0,
|
|
446
|
+
half_life_days: 30,
|
|
447
|
+
currentStrength: 0.85,
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
const formatted = formatInhibitorForPrompt(inhibitor);
|
|
451
|
+
|
|
452
|
+
expect(formatted).toContain('85%');
|
|
453
|
+
expect(formatted).toContain('removing null checks');
|
|
454
|
+
expect(formatted).toContain('CI failed: TypeError');
|
|
455
|
+
expect(formatted).toContain('agent-123');
|
|
456
|
+
expect(formatted).toContain('$0.0400');
|
|
457
|
+
});
|
|
458
|
+
});
|
|
459
|
+
});
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration tests for Phase 3: Concurrency & Coordination
|
|
3
|
+
* Tests the full flow of multi-agent coordination
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
7
|
+
import * as fs from 'fs/promises';
|
|
8
|
+
import {
|
|
9
|
+
acquireLock,
|
|
10
|
+
releaseLock,
|
|
11
|
+
listLocks,
|
|
12
|
+
} from '../../src/coordination/file-locks.js';
|
|
13
|
+
import {
|
|
14
|
+
spawnAgent,
|
|
15
|
+
killAllAgents,
|
|
16
|
+
listAgents,
|
|
17
|
+
} from '../../src/coordination/process-manager.js';
|
|
18
|
+
|
|
19
|
+
const LOCK_DIR = '.agent-meta/locks';
|
|
20
|
+
|
|
21
|
+
describe('Phase 3 Integration: Multi-Agent Coordination', () => {
|
|
22
|
+
beforeEach(async () => {
|
|
23
|
+
// Clean up before tests
|
|
24
|
+
killAllAgents();
|
|
25
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
26
|
+
try {
|
|
27
|
+
await fs.rm(LOCK_DIR, { recursive: true, force: true });
|
|
28
|
+
} catch {
|
|
29
|
+
// Ignore
|
|
30
|
+
}
|
|
31
|
+
await fs.mkdir(LOCK_DIR, { recursive: true });
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
afterEach(async () => {
|
|
35
|
+
killAllAgents();
|
|
36
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
37
|
+
try {
|
|
38
|
+
await fs.rm(LOCK_DIR, { recursive: true, force: true });
|
|
39
|
+
} catch {
|
|
40
|
+
// Ignore
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('should coordinate multiple agents working on different files', async () => {
|
|
45
|
+
const files = ['file1.ts', 'file2.ts', 'file3.ts'];
|
|
46
|
+
|
|
47
|
+
// Simulate three agents each acquiring a lock on different files
|
|
48
|
+
const locks = await Promise.all(
|
|
49
|
+
files.map((file, i) =>
|
|
50
|
+
acquireLock(file, `agent-${i + 1}`, 'error_reducer')
|
|
51
|
+
)
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
// All should succeed (different files)
|
|
55
|
+
expect(locks.every((l) => l === true)).toBe(true);
|
|
56
|
+
|
|
57
|
+
const activeLocks = await listLocks();
|
|
58
|
+
expect(activeLocks).toHaveLength(3);
|
|
59
|
+
|
|
60
|
+
// Clean up
|
|
61
|
+
await Promise.all(files.map((file) => releaseLock(file)));
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('should prevent multiple agents from working on the same file', async () => {
|
|
65
|
+
const file = 'contested.ts';
|
|
66
|
+
|
|
67
|
+
// Three agents try to lock the same file
|
|
68
|
+
const results = await Promise.all([
|
|
69
|
+
acquireLock(file, 'agent-1', 'error_reducer'),
|
|
70
|
+
acquireLock(file, 'agent-2', 'complexity_reducer'),
|
|
71
|
+
acquireLock(file, 'agent-3', 'debt_payer'),
|
|
72
|
+
]);
|
|
73
|
+
|
|
74
|
+
// Only one should succeed
|
|
75
|
+
const successes = results.filter(Boolean);
|
|
76
|
+
expect(successes).toHaveLength(1);
|
|
77
|
+
|
|
78
|
+
const activeLocks = await listLocks();
|
|
79
|
+
expect(activeLocks).toHaveLength(1);
|
|
80
|
+
|
|
81
|
+
await releaseLock(file);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('should spawn multiple agents and track them', async () => {
|
|
85
|
+
// Spawn multiple agents
|
|
86
|
+
spawnAgent({ agentId: 'worker-1', maxIterations: 5, timeout: 5000 });
|
|
87
|
+
spawnAgent({ agentId: 'worker-2', maxIterations: 5, timeout: 5000 });
|
|
88
|
+
spawnAgent({ agentId: 'worker-3', maxIterations: 5, timeout: 5000 });
|
|
89
|
+
|
|
90
|
+
// Wait a moment for them to start
|
|
91
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
92
|
+
|
|
93
|
+
const agents = listAgents();
|
|
94
|
+
expect(agents).toHaveLength(3);
|
|
95
|
+
|
|
96
|
+
const agentIds = agents.map((a) => a.agentId).sort();
|
|
97
|
+
expect(agentIds).toEqual(['worker-1', 'worker-2', 'worker-3']);
|
|
98
|
+
|
|
99
|
+
// All should have PIDs
|
|
100
|
+
expect(agents.every((a) => a.pid > 0)).toBe(true);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('should handle agent lock lifecycle', async () => {
|
|
104
|
+
const file = 'work-file.ts';
|
|
105
|
+
|
|
106
|
+
// Agent acquires lock
|
|
107
|
+
const locked = await acquireLock(file, 'agent-1', 'error_reducer');
|
|
108
|
+
expect(locked).toBe(true);
|
|
109
|
+
|
|
110
|
+
// Verify lock exists
|
|
111
|
+
let locks = await listLocks();
|
|
112
|
+
expect(locks).toHaveLength(1);
|
|
113
|
+
expect(locks[0].agent_id).toBe('agent-1');
|
|
114
|
+
|
|
115
|
+
// Agent releases lock
|
|
116
|
+
const released = await releaseLock(file);
|
|
117
|
+
expect(released).toBe(true);
|
|
118
|
+
|
|
119
|
+
// Verify lock is gone
|
|
120
|
+
locks = await listLocks();
|
|
121
|
+
expect(locks).toHaveLength(0);
|
|
122
|
+
|
|
123
|
+
// Another agent can now acquire
|
|
124
|
+
const locked2 = await acquireLock(file, 'agent-2', 'complexity_reducer');
|
|
125
|
+
expect(locked2).toBe(true);
|
|
126
|
+
|
|
127
|
+
await releaseLock(file);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('should demonstrate full coordination scenario', async () => {
|
|
131
|
+
/**
|
|
132
|
+
* Scenario: 4 agents, 3 files
|
|
133
|
+
* - Agent 1 gets file1
|
|
134
|
+
* - Agent 2 gets file2
|
|
135
|
+
* - Agent 3 gets file3
|
|
136
|
+
* - Agent 4 fails to get any (all locked)
|
|
137
|
+
* - Agent 1 releases file1
|
|
138
|
+
* - Agent 4 gets file1
|
|
139
|
+
*/
|
|
140
|
+
|
|
141
|
+
const files = ['file1.ts', 'file2.ts', 'file3.ts'];
|
|
142
|
+
|
|
143
|
+
// Agents 1-3 acquire locks
|
|
144
|
+
const [lock1, lock2, lock3] = await Promise.all([
|
|
145
|
+
acquireLock(files[0], 'agent-1', 'error_reducer'),
|
|
146
|
+
acquireLock(files[1], 'agent-2', 'complexity_reducer'),
|
|
147
|
+
acquireLock(files[2], 'agent-3', 'debt_payer'),
|
|
148
|
+
]);
|
|
149
|
+
|
|
150
|
+
expect([lock1, lock2, lock3]).toEqual([true, true, true]);
|
|
151
|
+
|
|
152
|
+
// Agent 4 tries all files, all should fail
|
|
153
|
+
const [lock4a, lock4b, lock4c] = await Promise.all([
|
|
154
|
+
acquireLock(files[0], 'agent-4', 'error_reducer'),
|
|
155
|
+
acquireLock(files[1], 'agent-4', 'error_reducer'),
|
|
156
|
+
acquireLock(files[2], 'agent-4', 'error_reducer'),
|
|
157
|
+
]);
|
|
158
|
+
|
|
159
|
+
expect([lock4a, lock4b, lock4c]).toEqual([false, false, false]);
|
|
160
|
+
|
|
161
|
+
// Agent 1 releases file1
|
|
162
|
+
await releaseLock(files[0]);
|
|
163
|
+
|
|
164
|
+
// Agent 4 can now acquire file1
|
|
165
|
+
const lock4 = await acquireLock(files[0], 'agent-4', 'stabilizer');
|
|
166
|
+
expect(lock4).toBe(true);
|
|
167
|
+
|
|
168
|
+
// Verify final state
|
|
169
|
+
const locks = await listLocks();
|
|
170
|
+
expect(locks).toHaveLength(3);
|
|
171
|
+
|
|
172
|
+
const agentIds = locks.map((l) => l.agent_id).sort();
|
|
173
|
+
expect(agentIds).toEqual(['agent-2', 'agent-3', 'agent-4']);
|
|
174
|
+
|
|
175
|
+
// Clean up
|
|
176
|
+
await Promise.all(files.map((f) => releaseLock(f)));
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('should handle rapid lock acquire/release cycles', async () => {
|
|
180
|
+
const file = 'cycle-test.ts';
|
|
181
|
+
const cycles = 10;
|
|
182
|
+
|
|
183
|
+
for (let i = 0; i < cycles; i++) {
|
|
184
|
+
const locked = await acquireLock(file, `agent-${i}`, 'error_reducer');
|
|
185
|
+
expect(locked).toBe(true);
|
|
186
|
+
|
|
187
|
+
const released = await releaseLock(file);
|
|
188
|
+
expect(released).toBe(true);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Should have no locks at the end
|
|
192
|
+
const locks = await listLocks();
|
|
193
|
+
expect(locks).toHaveLength(0);
|
|
194
|
+
});
|
|
195
|
+
});
|