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
package/src/utils/file-utils.ts
CHANGED
|
@@ -1,6 +1,41 @@
|
|
|
1
1
|
import * as fs from 'fs';
|
|
2
2
|
import * as path from 'path';
|
|
3
3
|
|
|
4
|
+
const PROJECT_ROOT = process.cwd();
|
|
5
|
+
const PROTECTED_DIRS = ['.git', 'node_modules'];
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Validate file path to prevent path traversal attacks
|
|
9
|
+
* SECURITY: Ensures path is within project directory and not in protected directories
|
|
10
|
+
*/
|
|
11
|
+
function validatePath(filePath: string): string {
|
|
12
|
+
// Resolve to absolute path and normalize
|
|
13
|
+
const absolutePath = path.resolve(filePath);
|
|
14
|
+
const normalizedPath = path.normalize(absolutePath);
|
|
15
|
+
|
|
16
|
+
// Check for null bytes (common attack vector)
|
|
17
|
+
if (filePath.includes('\0') || normalizedPath.includes('\0')) {
|
|
18
|
+
throw new Error(`Invalid path: contains null bytes`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Ensure path is within project directory
|
|
22
|
+
if (!normalizedPath.startsWith(PROJECT_ROOT)) {
|
|
23
|
+
throw new Error(`Path traversal detected: ${filePath} is outside project directory`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Check if path goes into protected directories
|
|
27
|
+
const relativePath = path.relative(PROJECT_ROOT, normalizedPath);
|
|
28
|
+
const pathComponents = relativePath.split(path.sep);
|
|
29
|
+
|
|
30
|
+
for (const component of pathComponents) {
|
|
31
|
+
if (PROTECTED_DIRS.includes(component)) {
|
|
32
|
+
throw new Error(`Access to protected directory denied: ${component}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return normalizedPath;
|
|
37
|
+
}
|
|
38
|
+
|
|
4
39
|
export function pathToSafeFilename(filePath: string): string {
|
|
5
40
|
return filePath.replace(/[/\\]+/g, '__');
|
|
6
41
|
}
|
|
@@ -11,7 +46,8 @@ export function safeFilenameToPath(safeFilename: string): string {
|
|
|
11
46
|
|
|
12
47
|
export function fileExists(filePath: string): boolean {
|
|
13
48
|
try {
|
|
14
|
-
|
|
49
|
+
const safePath = validatePath(filePath);
|
|
50
|
+
fs.accessSync(safePath, fs.constants.F_OK);
|
|
15
51
|
return true;
|
|
16
52
|
} catch {
|
|
17
53
|
return false;
|
|
@@ -19,23 +55,26 @@ export function fileExists(filePath: string): boolean {
|
|
|
19
55
|
}
|
|
20
56
|
|
|
21
57
|
export function readFile(filePath: string): string {
|
|
22
|
-
|
|
58
|
+
const safePath = validatePath(filePath);
|
|
59
|
+
return fs.readFileSync(safePath, 'utf-8');
|
|
23
60
|
}
|
|
24
61
|
|
|
25
62
|
export function writeFile(filePath: string, content: string): void {
|
|
26
|
-
const
|
|
63
|
+
const safePath = validatePath(filePath);
|
|
64
|
+
const dir = path.dirname(safePath);
|
|
27
65
|
if (!fs.existsSync(dir)) {
|
|
28
66
|
fs.mkdirSync(dir, { recursive: true });
|
|
29
67
|
}
|
|
30
|
-
fs.writeFileSync(
|
|
68
|
+
fs.writeFileSync(safePath, content, 'utf-8');
|
|
31
69
|
}
|
|
32
70
|
|
|
33
71
|
export function appendFile(filePath: string, content: string): void {
|
|
34
|
-
const
|
|
72
|
+
const safePath = validatePath(filePath);
|
|
73
|
+
const dir = path.dirname(safePath);
|
|
35
74
|
if (!fs.existsSync(dir)) {
|
|
36
75
|
fs.mkdirSync(dir, { recursive: true });
|
|
37
76
|
}
|
|
38
|
-
fs.appendFileSync(
|
|
77
|
+
fs.appendFileSync(safePath, content, 'utf-8');
|
|
39
78
|
}
|
|
40
79
|
|
|
41
80
|
export function readJsonFile<T>(filePath: string): T {
|
|
@@ -49,6 +88,7 @@ export function writeJsonFile<T>(filePath: string, data: T, indent: number = 2):
|
|
|
49
88
|
}
|
|
50
89
|
|
|
51
90
|
export function listFilesRecursive(dirPath: string): string[] {
|
|
91
|
+
const safePath = validatePath(dirPath);
|
|
52
92
|
const files: string[] = [];
|
|
53
93
|
|
|
54
94
|
function traverse(currentPath: string): void {
|
|
@@ -63,7 +103,7 @@ export function listFilesRecursive(dirPath: string): string[] {
|
|
|
63
103
|
}
|
|
64
104
|
}
|
|
65
105
|
|
|
66
|
-
traverse(
|
|
106
|
+
traverse(safePath);
|
|
67
107
|
return files;
|
|
68
108
|
}
|
|
69
109
|
|
|
@@ -81,31 +121,37 @@ export function isTestFile(filePath: string): boolean {
|
|
|
81
121
|
}
|
|
82
122
|
|
|
83
123
|
export function ensureDir(dirPath: string): void {
|
|
84
|
-
|
|
85
|
-
|
|
124
|
+
const safePath = validatePath(dirPath);
|
|
125
|
+
if (!fs.existsSync(safePath)) {
|
|
126
|
+
fs.mkdirSync(safePath, { recursive: true });
|
|
86
127
|
}
|
|
87
128
|
}
|
|
88
129
|
|
|
89
130
|
export function deleteFile(filePath: string): void {
|
|
90
|
-
|
|
91
|
-
|
|
131
|
+
const safePath = validatePath(filePath);
|
|
132
|
+
if (fs.existsSync(safePath)) {
|
|
133
|
+
fs.unlinkSync(safePath);
|
|
92
134
|
}
|
|
93
135
|
}
|
|
94
136
|
|
|
95
137
|
export function copyFile(sourcePath: string, destPath: string): void {
|
|
96
|
-
const
|
|
138
|
+
const safeSrc = validatePath(sourcePath);
|
|
139
|
+
const safeDest = validatePath(destPath);
|
|
140
|
+
const dir = path.dirname(safeDest);
|
|
97
141
|
if (!fs.existsSync(dir)) {
|
|
98
142
|
fs.mkdirSync(dir, { recursive: true });
|
|
99
143
|
}
|
|
100
|
-
fs.copyFileSync(
|
|
144
|
+
fs.copyFileSync(safeSrc, safeDest);
|
|
101
145
|
}
|
|
102
146
|
|
|
103
147
|
export function getFileModTime(filePath: string): number {
|
|
104
|
-
const
|
|
148
|
+
const safePath = validatePath(filePath);
|
|
149
|
+
const stats = fs.statSync(safePath);
|
|
105
150
|
return stats.mtimeMs;
|
|
106
151
|
}
|
|
107
152
|
|
|
108
153
|
export function getFileSize(filePath: string): number {
|
|
109
|
-
const
|
|
154
|
+
const safePath = validatePath(filePath);
|
|
155
|
+
const stats = fs.statSync(safePath);
|
|
110
156
|
return stats.size;
|
|
111
157
|
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GC CLI Command Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests for garbage collection 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-gc');
|
|
19
|
+
|
|
20
|
+
describe('GC 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
|
+
await fs.mkdir(path.join(testDir, '.agent-meta/traces'), { recursive: true });
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
afterEach(async () => {
|
|
32
|
+
// Clean up test directory
|
|
33
|
+
if (fileExists(testDir)) {
|
|
34
|
+
await fs.rm(testDir, { recursive: true });
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('should handle empty traces directory', async () => {
|
|
39
|
+
const tracesDir = path.join(testDir, '.agent-meta/traces');
|
|
40
|
+
expect(fileExists(tracesDir)).toBe(true);
|
|
41
|
+
|
|
42
|
+
// Verify directory is empty
|
|
43
|
+
const entries = await fs.readdir(tracesDir);
|
|
44
|
+
expect(entries).toHaveLength(0);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('should create GC log file', async () => {
|
|
48
|
+
const gcLogPath = path.join(testDir, '.agent-meta/_gc.log');
|
|
49
|
+
|
|
50
|
+
// Create GC log
|
|
51
|
+
await fs.writeFile(gcLogPath, '');
|
|
52
|
+
|
|
53
|
+
expect(fileExists(gcLogPath)).toBe(true);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('should append GC reports to log', async () => {
|
|
57
|
+
const gcLogPath = path.join(testDir, '.agent-meta/_gc.log');
|
|
58
|
+
|
|
59
|
+
const report1 = {
|
|
60
|
+
timestamp: new Date().toISOString(),
|
|
61
|
+
deleted: 10,
|
|
62
|
+
compacted: 2,
|
|
63
|
+
size_before_mb: 50,
|
|
64
|
+
size_after_mb: 45,
|
|
65
|
+
errors: [],
|
|
66
|
+
duration_ms: 1000,
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const report2 = {
|
|
70
|
+
timestamp: new Date(Date.now() + 1000).toISOString(),
|
|
71
|
+
deleted: 5,
|
|
72
|
+
compacted: 1,
|
|
73
|
+
size_before_mb: 45,
|
|
74
|
+
size_after_mb: 43,
|
|
75
|
+
errors: [],
|
|
76
|
+
duration_ms: 800,
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// Append reports
|
|
80
|
+
await fs.appendFile(gcLogPath, JSON.stringify(report1) + '\n');
|
|
81
|
+
await fs.appendFile(gcLogPath, JSON.stringify(report2) + '\n');
|
|
82
|
+
|
|
83
|
+
const content = await fs.readFile(gcLogPath, 'utf-8');
|
|
84
|
+
const lines = content.split('\n').filter(Boolean);
|
|
85
|
+
|
|
86
|
+
expect(lines).toHaveLength(2);
|
|
87
|
+
|
|
88
|
+
const loaded1 = JSON.parse(lines[0]);
|
|
89
|
+
const loaded2 = JSON.parse(lines[1]);
|
|
90
|
+
|
|
91
|
+
expect(loaded1.deleted).toBe(10);
|
|
92
|
+
expect(loaded2.deleted).toBe(5);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should calculate space saved correctly', () => {
|
|
96
|
+
const report = {
|
|
97
|
+
timestamp: new Date().toISOString(),
|
|
98
|
+
deleted: 10,
|
|
99
|
+
compacted: 2,
|
|
100
|
+
size_before_mb: 50,
|
|
101
|
+
size_after_mb: 45,
|
|
102
|
+
errors: [],
|
|
103
|
+
duration_ms: 1000,
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const saved = report.size_before_mb - report.size_after_mb;
|
|
107
|
+
const percent = (saved / report.size_before_mb) * 100;
|
|
108
|
+
|
|
109
|
+
expect(saved).toBe(5);
|
|
110
|
+
expect(percent).toBeCloseTo(10);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('should handle GC with errors', async () => {
|
|
114
|
+
const gcLogPath = path.join(testDir, '.agent-meta/_gc.log');
|
|
115
|
+
|
|
116
|
+
const report = {
|
|
117
|
+
timestamp: new Date().toISOString(),
|
|
118
|
+
deleted: 5,
|
|
119
|
+
compacted: 1,
|
|
120
|
+
size_before_mb: 45,
|
|
121
|
+
size_after_mb: 43,
|
|
122
|
+
errors: [
|
|
123
|
+
'file1.ndjson: permission denied',
|
|
124
|
+
'file2.ndjson: parse error on line 5',
|
|
125
|
+
],
|
|
126
|
+
duration_ms: 500,
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
await fs.appendFile(gcLogPath, JSON.stringify(report) + '\n');
|
|
130
|
+
|
|
131
|
+
const content = await fs.readFile(gcLogPath, 'utf-8');
|
|
132
|
+
const loaded = JSON.parse(content);
|
|
133
|
+
|
|
134
|
+
expect(loaded.errors).toHaveLength(2);
|
|
135
|
+
expect(loaded.errors[0]).toContain('permission denied');
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('should read GC history', async () => {
|
|
139
|
+
const gcLogPath = path.join(testDir, '.agent-meta/_gc.log');
|
|
140
|
+
|
|
141
|
+
// Create multiple GC reports
|
|
142
|
+
for (let i = 0; i < 5; i++) {
|
|
143
|
+
const report = {
|
|
144
|
+
timestamp: new Date(Date.now() - i * 1000000).toISOString(),
|
|
145
|
+
deleted: 10 + i,
|
|
146
|
+
compacted: 2,
|
|
147
|
+
size_before_mb: 50 - i,
|
|
148
|
+
size_after_mb: 45 - i,
|
|
149
|
+
errors: [],
|
|
150
|
+
duration_ms: 1000,
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
await fs.appendFile(gcLogPath, JSON.stringify(report) + '\n');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const content = await fs.readFile(gcLogPath, 'utf-8');
|
|
157
|
+
const lines = content.split('\n').filter(Boolean);
|
|
158
|
+
|
|
159
|
+
expect(lines).toHaveLength(5);
|
|
160
|
+
|
|
161
|
+
const reports = lines.map(l => JSON.parse(l));
|
|
162
|
+
const totalDeleted = reports.reduce((sum: number, r: any) => sum + r.deleted, 0);
|
|
163
|
+
|
|
164
|
+
expect(totalDeleted).toBe(50 + 10); // 10+11+12+13+14
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('should track total space saved', async () => {
|
|
168
|
+
const gcLogPath = path.join(testDir, '.agent-meta/_gc.log');
|
|
169
|
+
|
|
170
|
+
const reports = [
|
|
171
|
+
{
|
|
172
|
+
timestamp: new Date().toISOString(),
|
|
173
|
+
deleted: 10,
|
|
174
|
+
compacted: 2,
|
|
175
|
+
size_before_mb: 100,
|
|
176
|
+
size_after_mb: 90,
|
|
177
|
+
errors: [],
|
|
178
|
+
duration_ms: 1000,
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
timestamp: new Date(Date.now() + 1000).toISOString(),
|
|
182
|
+
deleted: 8,
|
|
183
|
+
compacted: 1,
|
|
184
|
+
size_before_mb: 90,
|
|
185
|
+
size_after_mb: 85,
|
|
186
|
+
errors: [],
|
|
187
|
+
duration_ms: 900,
|
|
188
|
+
},
|
|
189
|
+
];
|
|
190
|
+
|
|
191
|
+
for (const report of reports) {
|
|
192
|
+
await fs.appendFile(gcLogPath, JSON.stringify(report) + '\n');
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const content = await fs.readFile(gcLogPath, 'utf-8');
|
|
196
|
+
const lines = content.split('\n').filter(Boolean);
|
|
197
|
+
const loaded = lines.map(l => JSON.parse(l));
|
|
198
|
+
|
|
199
|
+
const totalSaved = loaded.reduce(
|
|
200
|
+
(sum: number, r: any) => sum + (r.size_before_mb - r.size_after_mb),
|
|
201
|
+
0
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
expect(totalSaved).toBe(15); // (100-90) + (90-85)
|
|
205
|
+
});
|
|
206
|
+
});
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Init CLI Command Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests for .agent-meta/ initialization
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, it, expect, beforeEach, afterEach, vi } 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-init');
|
|
19
|
+
|
|
20
|
+
describe('Init 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
|
+
});
|
|
28
|
+
|
|
29
|
+
afterEach(async () => {
|
|
30
|
+
// Clean up test directory
|
|
31
|
+
if (fileExists(testDir)) {
|
|
32
|
+
await fs.rm(testDir, { recursive: true });
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('should create .agent-meta directory structure', async () => {
|
|
37
|
+
const metaDir = path.join(testDir, '.agent-meta');
|
|
38
|
+
|
|
39
|
+
// Create subdirectories
|
|
40
|
+
await fs.mkdir(metaDir, { recursive: true });
|
|
41
|
+
await fs.mkdir(path.join(metaDir, 'traces'), { recursive: true });
|
|
42
|
+
await fs.mkdir(path.join(metaDir, 'tasks'), { recursive: true });
|
|
43
|
+
await fs.mkdir(path.join(metaDir, 'locks'), { recursive: true });
|
|
44
|
+
|
|
45
|
+
expect(fileExists(metaDir)).toBe(true);
|
|
46
|
+
expect(fileExists(path.join(metaDir, 'traces'))).toBe(true);
|
|
47
|
+
expect(fileExists(path.join(metaDir, 'tasks'))).toBe(true);
|
|
48
|
+
expect(fileExists(path.join(metaDir, 'locks'))).toBe(true);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('should create _config.json with default values', async () => {
|
|
52
|
+
const metaDir = path.join(testDir, '.agent-meta');
|
|
53
|
+
await fs.mkdir(metaDir, { recursive: true });
|
|
54
|
+
|
|
55
|
+
const configPath = path.join(metaDir, '_config.json');
|
|
56
|
+
const config = {
|
|
57
|
+
version: '2.0.0',
|
|
58
|
+
created_at: new Date().toISOString(),
|
|
59
|
+
error_provider: 'file',
|
|
60
|
+
error_file: '.agent-meta/_errors.json',
|
|
61
|
+
ci_command: 'npm test && npm run lint',
|
|
62
|
+
spawn_count: 0,
|
|
63
|
+
last_gc_at_spawn: 0,
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
await fs.writeFile(configPath, JSON.stringify(config, null, 2));
|
|
67
|
+
|
|
68
|
+
expect(fileExists(configPath)).toBe(true);
|
|
69
|
+
|
|
70
|
+
const content = await fs.readFile(configPath, 'utf-8');
|
|
71
|
+
const loaded = JSON.parse(content);
|
|
72
|
+
|
|
73
|
+
expect(loaded.version).toBe('2.0.0');
|
|
74
|
+
expect(loaded.spawn_count).toBe(0);
|
|
75
|
+
expect(loaded.error_provider).toBe('file');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('should create _quarantine.json with empty entries', async () => {
|
|
79
|
+
const metaDir = path.join(testDir, '.agent-meta');
|
|
80
|
+
await fs.mkdir(metaDir, { recursive: true });
|
|
81
|
+
|
|
82
|
+
const quarantinePath = path.join(metaDir, '_quarantine.json');
|
|
83
|
+
const quarantine = {
|
|
84
|
+
updated_at: new Date().toISOString(),
|
|
85
|
+
entries: [],
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
await fs.writeFile(quarantinePath, JSON.stringify(quarantine, null, 2));
|
|
89
|
+
|
|
90
|
+
expect(fileExists(quarantinePath)).toBe(true);
|
|
91
|
+
|
|
92
|
+
const content = await fs.readFile(quarantinePath, 'utf-8');
|
|
93
|
+
const loaded = JSON.parse(content);
|
|
94
|
+
|
|
95
|
+
expect(loaded.entries).toEqual([]);
|
|
96
|
+
expect(loaded.updated_at).toBeDefined();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('should create _inhibitors.ndjson as empty file', async () => {
|
|
100
|
+
const metaDir = path.join(testDir, '.agent-meta');
|
|
101
|
+
await fs.mkdir(metaDir, { recursive: true });
|
|
102
|
+
|
|
103
|
+
const inhibitorsPath = path.join(metaDir, '_inhibitors.ndjson');
|
|
104
|
+
await fs.writeFile(inhibitorsPath, '');
|
|
105
|
+
|
|
106
|
+
expect(fileExists(inhibitorsPath)).toBe(true);
|
|
107
|
+
|
|
108
|
+
const content = await fs.readFile(inhibitorsPath, 'utf-8');
|
|
109
|
+
expect(content).toBe('');
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('should create _errors.json with empty errors', async () => {
|
|
113
|
+
const metaDir = path.join(testDir, '.agent-meta');
|
|
114
|
+
await fs.mkdir(metaDir, { recursive: true });
|
|
115
|
+
|
|
116
|
+
const errorsPath = path.join(metaDir, '_errors.json');
|
|
117
|
+
const errors = {
|
|
118
|
+
updated_at: new Date().toISOString(),
|
|
119
|
+
errors: [],
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
await fs.writeFile(errorsPath, JSON.stringify(errors, null, 2));
|
|
123
|
+
|
|
124
|
+
expect(fileExists(errorsPath)).toBe(true);
|
|
125
|
+
|
|
126
|
+
const content = await fs.readFile(errorsPath, 'utf-8');
|
|
127
|
+
const loaded = JSON.parse(content);
|
|
128
|
+
|
|
129
|
+
expect(loaded.errors).toEqual([]);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('should create _gc.log as empty file', async () => {
|
|
133
|
+
const metaDir = path.join(testDir, '.agent-meta');
|
|
134
|
+
await fs.mkdir(metaDir, { recursive: true });
|
|
135
|
+
|
|
136
|
+
const gcLogPath = path.join(metaDir, '_gc.log');
|
|
137
|
+
await fs.writeFile(gcLogPath, '');
|
|
138
|
+
|
|
139
|
+
expect(fileExists(gcLogPath)).toBe(true);
|
|
140
|
+
|
|
141
|
+
const content = await fs.readFile(gcLogPath, 'utf-8');
|
|
142
|
+
expect(content).toBe('');
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('should update .gitignore with locks pattern', async () => {
|
|
146
|
+
const gitignorePath = path.join(testDir, '.gitignore');
|
|
147
|
+
const metaDir = path.join(testDir, '.agent-meta');
|
|
148
|
+
const lockPattern = '.agent-meta/locks/';
|
|
149
|
+
|
|
150
|
+
// Create .gitignore
|
|
151
|
+
await fs.writeFile(gitignorePath, '# Test\n');
|
|
152
|
+
|
|
153
|
+
// Check and update
|
|
154
|
+
const content = await fs.readFile(gitignorePath, 'utf-8');
|
|
155
|
+
if (!content.includes(lockPattern)) {
|
|
156
|
+
await fs.appendFile(
|
|
157
|
+
gitignorePath,
|
|
158
|
+
`\n# Claude Mycelium locks (ephemeral)\n${lockPattern}\n`
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const updated = await fs.readFile(gitignorePath, 'utf-8');
|
|
163
|
+
expect(updated).toContain(lockPattern);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('should create .gitignore if it does not exist', async () => {
|
|
167
|
+
const gitignorePath = path.join(testDir, '.gitignore');
|
|
168
|
+
const lockPattern = '.agent-meta/locks/';
|
|
169
|
+
|
|
170
|
+
// Create .gitignore with the pattern
|
|
171
|
+
await fs.writeFile(
|
|
172
|
+
gitignorePath,
|
|
173
|
+
`# Claude Mycelium locks (ephemeral)\n${lockPattern}\n`
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
expect(fileExists(gitignorePath)).toBe(true);
|
|
177
|
+
|
|
178
|
+
const content = await fs.readFile(gitignorePath, 'utf-8');
|
|
179
|
+
expect(content).toContain(lockPattern);
|
|
180
|
+
});
|
|
181
|
+
});
|