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
@@ -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
- fs.accessSync(filePath, fs.constants.F_OK);
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
- return fs.readFileSync(filePath, 'utf-8');
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 dir = path.dirname(filePath);
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(filePath, content, 'utf-8');
68
+ fs.writeFileSync(safePath, content, 'utf-8');
31
69
  }
32
70
 
33
71
  export function appendFile(filePath: string, content: string): void {
34
- const dir = path.dirname(filePath);
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(filePath, content, 'utf-8');
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(dirPath);
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
- if (!fs.existsSync(dirPath)) {
85
- fs.mkdirSync(dirPath, { recursive: true });
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
- if (fileExists(filePath)) {
91
- fs.unlinkSync(filePath);
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 dir = path.dirname(destPath);
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(sourcePath, destPath);
144
+ fs.copyFileSync(safeSrc, safeDest);
101
145
  }
102
146
 
103
147
  export function getFileModTime(filePath: string): number {
104
- const stats = fs.statSync(filePath);
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 stats = fs.statSync(filePath);
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
+ });