swarmkit 0.0.5 → 0.0.7
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/dist/cli.js +2 -0
- package/dist/commands/configure/configure.d.ts +5 -0
- package/dist/commands/configure/configure.js +354 -0
- package/dist/commands/configure/configure.test.d.ts +1 -0
- package/dist/commands/configure/configure.test.js +539 -0
- package/dist/commands/configure/read-config.d.ts +12 -0
- package/dist/commands/configure/read-config.js +81 -0
- package/dist/commands/configure.d.ts +2 -0
- package/dist/commands/configure.js +14 -0
- package/dist/commands/init/phases/configure.js +0 -21
- package/dist/commands/init/phases/global-setup.d.ts +1 -1
- package/dist/commands/init/phases/global-setup.js +22 -44
- package/dist/commands/init/phases/integrations.d.ts +16 -0
- package/dist/commands/init/phases/integrations.js +172 -0
- package/dist/commands/init/phases/package-config.d.ts +10 -0
- package/dist/commands/init/phases/package-config.js +117 -0
- package/dist/commands/init/phases/phases.test.d.ts +1 -0
- package/dist/commands/init/phases/phases.test.js +711 -0
- package/dist/commands/init/phases/project.js +17 -0
- package/dist/commands/init/phases/review.d.ts +8 -0
- package/dist/commands/init/phases/review.js +79 -0
- package/dist/commands/init/phases/use-case.js +41 -27
- package/dist/commands/init/phases/wizard-flow.test.d.ts +1 -0
- package/dist/commands/init/phases/wizard-flow.test.js +657 -0
- package/dist/commands/init/phases/wizard-modes.test.d.ts +1 -0
- package/dist/commands/init/phases/wizard-modes.test.js +270 -0
- package/dist/commands/init/state.d.ts +31 -1
- package/dist/commands/init/state.js +4 -0
- package/dist/commands/init/state.test.js +7 -0
- package/dist/commands/init/wizard.d.ts +1 -0
- package/dist/commands/init/wizard.js +31 -23
- package/dist/commands/init.js +2 -0
- package/dist/packages/registry.d.ts +66 -0
- package/dist/packages/registry.js +258 -0
- package/dist/packages/setup.d.ts +42 -0
- package/dist/packages/setup.js +311 -56
- package/dist/packages/setup.test.js +546 -42
- package/package.json +1 -1
|
@@ -0,0 +1,711 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { mkdirSync, rmSync, existsSync, readFileSync, writeFileSync, realpathSync, } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { randomUUID } from "node:crypto";
|
|
6
|
+
import { execFile, execSync } from "node:child_process";
|
|
7
|
+
import { promisify } from "node:util";
|
|
8
|
+
const execFileAsync = promisify(execFile);
|
|
9
|
+
// ─── Minimal mocking ────────────────────────────────────────────────────────
|
|
10
|
+
// Only mock homedir — everything else (filesystem, CLI shell-outs) is real.
|
|
11
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
12
|
+
let testHome;
|
|
13
|
+
vi.mock("node:os", async () => {
|
|
14
|
+
const actual = await import("node:os");
|
|
15
|
+
return { ...actual, homedir: () => testHome };
|
|
16
|
+
});
|
|
17
|
+
const { initGlobal } = await import("./global-setup.js");
|
|
18
|
+
const { showReview } = await import("./review.js");
|
|
19
|
+
const { applyIntegrationWiring } = await import("./integrations.js");
|
|
20
|
+
const { isProjectInit, isGlobalInit, initProjectPackage, PROJECT_INIT_ORDER, } = await import("../../../packages/setup.js");
|
|
21
|
+
import { createEmptyState } from "../state.js";
|
|
22
|
+
import { readPackageConfig, getNestedValue } from "../../configure/read-config.js";
|
|
23
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
24
|
+
let testDir;
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
const realTmp = realpathSync(tmpdir());
|
|
27
|
+
testDir = join(realTmp, `swarmkit-phases-test-${randomUUID()}`);
|
|
28
|
+
testHome = join(realTmp, `swarmkit-phases-home-${randomUUID()}`);
|
|
29
|
+
mkdirSync(testDir, { recursive: true });
|
|
30
|
+
mkdirSync(testHome, { recursive: true });
|
|
31
|
+
});
|
|
32
|
+
afterEach(() => {
|
|
33
|
+
rmSync(testDir, { recursive: true, force: true });
|
|
34
|
+
rmSync(testHome, { recursive: true, force: true });
|
|
35
|
+
});
|
|
36
|
+
async function hasCliInstalled(command) {
|
|
37
|
+
try {
|
|
38
|
+
await execFileAsync("which", [command]);
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function createProject(name, dir = testDir) {
|
|
46
|
+
execSync("git init -q", { cwd: dir });
|
|
47
|
+
writeFileSync(join(dir, "package.json"), JSON.stringify({ name, version: "1.0.0" }));
|
|
48
|
+
return dir;
|
|
49
|
+
}
|
|
50
|
+
function makeState(overrides = {}) {
|
|
51
|
+
return {
|
|
52
|
+
...createEmptyState(),
|
|
53
|
+
...overrides,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
// ─── initGlobal phase ────────────────────────────────────────────────────────
|
|
57
|
+
describe("initGlobal phase", () => {
|
|
58
|
+
it("skips when no global packages selected", async () => {
|
|
59
|
+
const state = makeState({
|
|
60
|
+
selectedPackages: ["opentasks", "minimem"],
|
|
61
|
+
});
|
|
62
|
+
await initGlobal(state);
|
|
63
|
+
// No global dirs created
|
|
64
|
+
expect(existsSync(join(testHome, ".skill-tree"))).toBe(false);
|
|
65
|
+
expect(existsSync(join(testHome, ".claude-swarm"))).toBe(false);
|
|
66
|
+
expect(state.configsWritten).toEqual([]);
|
|
67
|
+
});
|
|
68
|
+
it("initializes claude-code-swarm global config with defaults", async () => {
|
|
69
|
+
const state = makeState({
|
|
70
|
+
selectedPackages: ["claude-code-swarm"],
|
|
71
|
+
});
|
|
72
|
+
await initGlobal(state);
|
|
73
|
+
expect(existsSync(join(testHome, ".claude-swarm", "config.json"))).toBe(true);
|
|
74
|
+
const config = JSON.parse(readFileSync(join(testHome, ".claude-swarm", "config.json"), "utf-8"));
|
|
75
|
+
expect(config.map.server).toBe("");
|
|
76
|
+
expect(config.map.sidecar).toBe("session");
|
|
77
|
+
expect(config.sessionlog.enabled).toBe(false);
|
|
78
|
+
expect(state.configsWritten).toContainEqual(expect.objectContaining({
|
|
79
|
+
package: "claude-code-swarm",
|
|
80
|
+
path: "~/.claude-swarm/",
|
|
81
|
+
}));
|
|
82
|
+
});
|
|
83
|
+
it("initializes claude-code-swarm global with packageConfigs overrides", async () => {
|
|
84
|
+
const state = makeState({
|
|
85
|
+
selectedPackages: ["claude-code-swarm"],
|
|
86
|
+
packageConfigs: {
|
|
87
|
+
"claude-code-swarm": {
|
|
88
|
+
values: {
|
|
89
|
+
"map.server": "ws://custom:9090",
|
|
90
|
+
"sessionlog.enabled": true,
|
|
91
|
+
},
|
|
92
|
+
usedCliWizard: false,
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
await initGlobal(state);
|
|
97
|
+
const config = JSON.parse(readFileSync(join(testHome, ".claude-swarm", "config.json"), "utf-8"));
|
|
98
|
+
expect(config.map.server).toBe("ws://custom:9090");
|
|
99
|
+
expect(config.sessionlog.enabled).toBe(true);
|
|
100
|
+
// Defaults preserved
|
|
101
|
+
expect(config.map.sidecar).toBe("session");
|
|
102
|
+
});
|
|
103
|
+
it("skips already-configured global packages", async () => {
|
|
104
|
+
// Pre-create claude-swarm global config
|
|
105
|
+
mkdirSync(join(testHome, ".claude-swarm"), { recursive: true });
|
|
106
|
+
writeFileSync(join(testHome, ".claude-swarm", "config.json"), JSON.stringify({ map: { server: "ws://existing:8080" } }));
|
|
107
|
+
const state = makeState({
|
|
108
|
+
selectedPackages: ["claude-code-swarm"],
|
|
109
|
+
});
|
|
110
|
+
await initGlobal(state);
|
|
111
|
+
// Should not overwrite
|
|
112
|
+
const config = JSON.parse(readFileSync(join(testHome, ".claude-swarm", "config.json"), "utf-8"));
|
|
113
|
+
expect(config.map.server).toBe("ws://existing:8080");
|
|
114
|
+
// No configsWritten since it was skipped
|
|
115
|
+
expect(state.configsWritten).toEqual([]);
|
|
116
|
+
});
|
|
117
|
+
it("re-initializes when force=true even if already configured", async () => {
|
|
118
|
+
// Pre-create claude-swarm global config
|
|
119
|
+
mkdirSync(join(testHome, ".claude-swarm"), { recursive: true });
|
|
120
|
+
writeFileSync(join(testHome, ".claude-swarm", "config.json"), JSON.stringify({ map: { server: "ws://old:8080" } }));
|
|
121
|
+
const state = makeState({
|
|
122
|
+
selectedPackages: ["claude-code-swarm"],
|
|
123
|
+
});
|
|
124
|
+
// force=true, but initGlobalPackage still doesn't overwrite existing config.json
|
|
125
|
+
// (that's initClaudeSwarmGlobal's behavior — the `if (!existsSync(configPath))` check)
|
|
126
|
+
await initGlobal(state, true);
|
|
127
|
+
// The config existed, so it was NOT overwritten (existing file protection)
|
|
128
|
+
const config = JSON.parse(readFileSync(join(testHome, ".claude-swarm", "config.json"), "utf-8"));
|
|
129
|
+
expect(config.map.server).toBe("ws://old:8080");
|
|
130
|
+
// But configsWritten is populated since force ran
|
|
131
|
+
expect(state.configsWritten.length).toBeGreaterThan(0);
|
|
132
|
+
});
|
|
133
|
+
it("skips packages whose CLI wizard already ran", async () => {
|
|
134
|
+
const state = makeState({
|
|
135
|
+
selectedPackages: ["claude-code-swarm"],
|
|
136
|
+
packageConfigs: {
|
|
137
|
+
"claude-code-swarm": {
|
|
138
|
+
values: {},
|
|
139
|
+
usedCliWizard: true,
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
await initGlobal(state);
|
|
144
|
+
// Global config should NOT be created (wizard handled it)
|
|
145
|
+
expect(existsSync(join(testHome, ".claude-swarm", "config.json"))).toBe(false);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
describe("initGlobal phase — skill-tree (real CLI)", async () => {
|
|
149
|
+
const installed = await hasCliInstalled("skill-tree");
|
|
150
|
+
it.skipIf(!installed)("initializes skill-tree global config", async () => {
|
|
151
|
+
const state = makeState({
|
|
152
|
+
selectedPackages: ["skill-tree"],
|
|
153
|
+
});
|
|
154
|
+
await initGlobal(state);
|
|
155
|
+
expect(existsSync(join(testHome, ".skill-tree", "config.yaml"))).toBe(true);
|
|
156
|
+
expect(state.configsWritten).toContainEqual(expect.objectContaining({ package: "skill-tree" }));
|
|
157
|
+
});
|
|
158
|
+
it.skipIf(!installed)("initializes skill-tree + claude-code-swarm in correct order", async () => {
|
|
159
|
+
const state = makeState({
|
|
160
|
+
selectedPackages: ["skill-tree", "claude-code-swarm"],
|
|
161
|
+
});
|
|
162
|
+
await initGlobal(state);
|
|
163
|
+
expect(existsSync(join(testHome, ".skill-tree", "config.yaml"))).toBe(true);
|
|
164
|
+
expect(existsSync(join(testHome, ".claude-swarm", "config.json"))).toBe(true);
|
|
165
|
+
// configsWritten order matches GLOBAL_SETUP_ORDER
|
|
166
|
+
const written = state.configsWritten.map((c) => c.package);
|
|
167
|
+
expect(written).toEqual(["skill-tree", "claude-code-swarm"]);
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
// ─── E2E: full init flow simulation ──────────────────────────────────────────
|
|
171
|
+
//
|
|
172
|
+
// Simulates the wizard's runFirstTimeSetup flow without interactive prompts:
|
|
173
|
+
// 1. Build state (simulating phases 1-4)
|
|
174
|
+
// 2. Call initGlobal (phase 5)
|
|
175
|
+
// 3. Run project init directly via initProjectPackage (simulating phase 6)
|
|
176
|
+
// 4. Call showReview (phase 8)
|
|
177
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
178
|
+
describe("e2e: full init flow — solo bundle", async () => {
|
|
179
|
+
const opentasksOk = await hasCliInstalled("opentasks");
|
|
180
|
+
it.skipIf(!opentasksOk)("initializes global + project configs end-to-end", async () => {
|
|
181
|
+
createProject("e2e-solo");
|
|
182
|
+
const state = makeState({
|
|
183
|
+
bundle: "all",
|
|
184
|
+
selectedPackages: ["opentasks", "minimem"],
|
|
185
|
+
embeddingProvider: "openai",
|
|
186
|
+
usePrefix: true,
|
|
187
|
+
});
|
|
188
|
+
// Phase 5: global setup (no global packages in solo bundle)
|
|
189
|
+
await initGlobal(state);
|
|
190
|
+
expect(state.configsWritten.length).toBe(0);
|
|
191
|
+
// Phase 6: project init (simulate — call initProjectPackage directly)
|
|
192
|
+
for (const pkg of PROJECT_INIT_ORDER) {
|
|
193
|
+
if (!state.selectedPackages.includes(pkg))
|
|
194
|
+
continue;
|
|
195
|
+
if (isProjectInit(testDir, pkg))
|
|
196
|
+
continue;
|
|
197
|
+
const ctx = {
|
|
198
|
+
cwd: testDir,
|
|
199
|
+
packages: state.selectedPackages,
|
|
200
|
+
embeddingProvider: state.embeddingProvider,
|
|
201
|
+
apiKeys: state.apiKeys,
|
|
202
|
+
usePrefix: state.usePrefix,
|
|
203
|
+
packageConfigs: state.packageConfigs,
|
|
204
|
+
};
|
|
205
|
+
const result = await initProjectPackage(pkg, ctx);
|
|
206
|
+
if (result.success) {
|
|
207
|
+
state.configsWritten.push({
|
|
208
|
+
package: pkg,
|
|
209
|
+
path: `.swarm/${pkg}/`,
|
|
210
|
+
description: `Project ${pkg} config`,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
// Verify filesystem
|
|
215
|
+
expect(existsSync(join(testDir, ".swarm", "opentasks", "config.json"))).toBe(true);
|
|
216
|
+
expect(existsSync(join(testDir, ".swarm", "minimem", "config.json"))).toBe(true);
|
|
217
|
+
// Verify configs
|
|
218
|
+
const otConfig = readPackageConfig("opentasks", testDir);
|
|
219
|
+
expect(getNestedValue(otConfig, "location.name")).toBe("e2e-solo");
|
|
220
|
+
const mmConfig = readPackageConfig("minimem", testDir);
|
|
221
|
+
expect(getNestedValue(mmConfig, "embedding.provider")).toBe("openai");
|
|
222
|
+
// Verify state tracking
|
|
223
|
+
expect(state.configsWritten.map((c) => c.package)).toEqual([
|
|
224
|
+
"opentasks",
|
|
225
|
+
"minimem",
|
|
226
|
+
]);
|
|
227
|
+
// Phase 8: review runs without error
|
|
228
|
+
await showReview(state);
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
describe("e2e: full init flow — team bundle with learning", async () => {
|
|
232
|
+
const opentasksOk = await hasCliInstalled("opentasks");
|
|
233
|
+
const ccOk = await hasCliInstalled("cognitive-core");
|
|
234
|
+
const stOk = await hasCliInstalled("skill-tree");
|
|
235
|
+
const allOk = opentasksOk && ccOk && stOk;
|
|
236
|
+
it.skipIf(!allOk)("initializes global + project configs with cross-package wiring", async () => {
|
|
237
|
+
createProject("e2e-team");
|
|
238
|
+
const state = makeState({
|
|
239
|
+
bundle: "all",
|
|
240
|
+
selectedPackages: [
|
|
241
|
+
"opentasks",
|
|
242
|
+
"minimem",
|
|
243
|
+
"cognitive-core",
|
|
244
|
+
"skill-tree",
|
|
245
|
+
],
|
|
246
|
+
embeddingProvider: "gemini",
|
|
247
|
+
usePrefix: true,
|
|
248
|
+
});
|
|
249
|
+
// Phase 5: global setup (skill-tree is global)
|
|
250
|
+
await initGlobal(state);
|
|
251
|
+
expect(existsSync(join(testHome, ".skill-tree", "config.yaml"))).toBe(true);
|
|
252
|
+
expect(state.configsWritten).toContainEqual(expect.objectContaining({ package: "skill-tree" }));
|
|
253
|
+
// Phase 6: project init
|
|
254
|
+
for (const pkg of PROJECT_INIT_ORDER) {
|
|
255
|
+
if (!state.selectedPackages.includes(pkg))
|
|
256
|
+
continue;
|
|
257
|
+
if (isProjectInit(testDir, pkg))
|
|
258
|
+
continue;
|
|
259
|
+
const ctx = {
|
|
260
|
+
cwd: testDir,
|
|
261
|
+
packages: state.selectedPackages,
|
|
262
|
+
embeddingProvider: state.embeddingProvider,
|
|
263
|
+
apiKeys: state.apiKeys,
|
|
264
|
+
usePrefix: state.usePrefix,
|
|
265
|
+
packageConfigs: state.packageConfigs,
|
|
266
|
+
};
|
|
267
|
+
const result = await initProjectPackage(pkg, ctx);
|
|
268
|
+
if (result.success) {
|
|
269
|
+
state.configsWritten.push({
|
|
270
|
+
package: pkg,
|
|
271
|
+
path: `.swarm/${pkg}/`,
|
|
272
|
+
description: `Project ${pkg} config`,
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
// All project dirs exist
|
|
277
|
+
expect(existsSync(join(testDir, ".swarm", "opentasks"))).toBe(true);
|
|
278
|
+
expect(existsSync(join(testDir, ".swarm", "minimem"))).toBe(true);
|
|
279
|
+
expect(existsSync(join(testDir, ".swarm", "cognitive-core"))).toBe(true);
|
|
280
|
+
expect(existsSync(join(testDir, ".swarm", "skilltree"))).toBe(true);
|
|
281
|
+
// minimem has gemini provider
|
|
282
|
+
const mmConfig = readPackageConfig("minimem", testDir);
|
|
283
|
+
expect(getNestedValue(mmConfig, "embedding.provider")).toBe("gemini");
|
|
284
|
+
// Phase 8: review runs without error
|
|
285
|
+
await showReview(state);
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
describe("e2e: full init flow — with packageConfigs overrides", async () => {
|
|
289
|
+
const opentasksOk = await hasCliInstalled("opentasks");
|
|
290
|
+
it.skipIf(!opentasksOk)("package config overrides flow through global + project init", async () => {
|
|
291
|
+
createProject("e2e-overrides");
|
|
292
|
+
const state = makeState({
|
|
293
|
+
bundle: "all",
|
|
294
|
+
selectedPackages: [
|
|
295
|
+
"opentasks",
|
|
296
|
+
"minimem",
|
|
297
|
+
"sessionlog",
|
|
298
|
+
"claude-code-swarm",
|
|
299
|
+
],
|
|
300
|
+
embeddingProvider: "openai",
|
|
301
|
+
usePrefix: true,
|
|
302
|
+
packageConfigs: {
|
|
303
|
+
minimem: {
|
|
304
|
+
values: {
|
|
305
|
+
"query.maxResults": "50",
|
|
306
|
+
"hybrid.vectorWeight": "0.85",
|
|
307
|
+
},
|
|
308
|
+
usedCliWizard: false,
|
|
309
|
+
},
|
|
310
|
+
sessionlog: {
|
|
311
|
+
values: {
|
|
312
|
+
enabled: true,
|
|
313
|
+
strategy: "auto-commit",
|
|
314
|
+
summarizationEnabled: true,
|
|
315
|
+
},
|
|
316
|
+
usedCliWizard: false,
|
|
317
|
+
},
|
|
318
|
+
"claude-code-swarm": {
|
|
319
|
+
values: {
|
|
320
|
+
"map.enabled": true,
|
|
321
|
+
"map.server": "ws://production:8080",
|
|
322
|
+
"sessionlog.enabled": true,
|
|
323
|
+
"sessionlog.sync": "on-finish",
|
|
324
|
+
},
|
|
325
|
+
usedCliWizard: false,
|
|
326
|
+
},
|
|
327
|
+
},
|
|
328
|
+
});
|
|
329
|
+
// Phase 5: global setup (claude-code-swarm global)
|
|
330
|
+
await initGlobal(state);
|
|
331
|
+
expect(existsSync(join(testHome, ".claude-swarm", "config.json"))).toBe(true);
|
|
332
|
+
// Global config has overrides applied
|
|
333
|
+
const globalCsConfig = JSON.parse(readFileSync(join(testHome, ".claude-swarm", "config.json"), "utf-8"));
|
|
334
|
+
expect(globalCsConfig.map.enabled).toBe(true);
|
|
335
|
+
expect(globalCsConfig.map.server).toBe("ws://production:8080");
|
|
336
|
+
expect(globalCsConfig.sessionlog.enabled).toBe(true);
|
|
337
|
+
expect(globalCsConfig.sessionlog.sync).toBe("on-finish");
|
|
338
|
+
// Phase 6: project init
|
|
339
|
+
for (const pkg of PROJECT_INIT_ORDER) {
|
|
340
|
+
if (!state.selectedPackages.includes(pkg))
|
|
341
|
+
continue;
|
|
342
|
+
if (isProjectInit(testDir, pkg))
|
|
343
|
+
continue;
|
|
344
|
+
const ctx = {
|
|
345
|
+
cwd: testDir,
|
|
346
|
+
packages: state.selectedPackages,
|
|
347
|
+
embeddingProvider: state.embeddingProvider,
|
|
348
|
+
apiKeys: state.apiKeys,
|
|
349
|
+
usePrefix: state.usePrefix,
|
|
350
|
+
packageConfigs: state.packageConfigs,
|
|
351
|
+
};
|
|
352
|
+
const result = await initProjectPackage(pkg, ctx);
|
|
353
|
+
if (result.success) {
|
|
354
|
+
state.configsWritten.push({
|
|
355
|
+
package: pkg,
|
|
356
|
+
path: `.swarm/${pkg}/`,
|
|
357
|
+
description: `Project ${pkg} config`,
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
// Verify minimem overrides
|
|
362
|
+
const mmConfig = readPackageConfig("minimem", testDir);
|
|
363
|
+
expect(getNestedValue(mmConfig, "query.maxResults")).toBe(50);
|
|
364
|
+
expect(getNestedValue(mmConfig, "hybrid.vectorWeight")).toBe(0.85);
|
|
365
|
+
expect(getNestedValue(mmConfig, "embedding.provider")).toBe("openai"); // from embeddingProvider
|
|
366
|
+
// Verify sessionlog overrides
|
|
367
|
+
const slConfig = readPackageConfig("sessionlog", testDir);
|
|
368
|
+
expect(slConfig.enabled).toBe(true);
|
|
369
|
+
expect(slConfig.strategy).toBe("auto-commit");
|
|
370
|
+
expect(slConfig.summarizationEnabled).toBe(true);
|
|
371
|
+
expect(slConfig.logLevel).toBe("warn"); // default preserved
|
|
372
|
+
// Verify claude-code-swarm project overrides
|
|
373
|
+
const csConfig = readPackageConfig("claude-code-swarm", testDir);
|
|
374
|
+
expect(getNestedValue(csConfig, "map.enabled")).toBe(true);
|
|
375
|
+
expect(getNestedValue(csConfig, "map.server")).toBe("ws://production:8080");
|
|
376
|
+
expect(getNestedValue(csConfig, "sessionlog.enabled")).toBe(true);
|
|
377
|
+
expect(getNestedValue(csConfig, "map.systemId")).toBe("system-claude-swarm"); // default
|
|
378
|
+
// Phase 8: review runs and state is coherent
|
|
379
|
+
await showReview(state);
|
|
380
|
+
// Verify configsWritten captures all packages
|
|
381
|
+
const written = state.configsWritten.map((c) => c.package);
|
|
382
|
+
expect(written).toContain("claude-code-swarm"); // global
|
|
383
|
+
expect(written).toContain("opentasks");
|
|
384
|
+
expect(written).toContain("minimem");
|
|
385
|
+
expect(written).toContain("sessionlog");
|
|
386
|
+
});
|
|
387
|
+
});
|
|
388
|
+
// ─── showReview phase ────────────────────────────────────────────────────────
|
|
389
|
+
describe("showReview phase", () => {
|
|
390
|
+
it("runs without error for empty state", async () => {
|
|
391
|
+
const state = makeState({
|
|
392
|
+
selectedPackages: [],
|
|
393
|
+
});
|
|
394
|
+
// Should not throw
|
|
395
|
+
await showReview(state);
|
|
396
|
+
});
|
|
397
|
+
it("runs without error with packages but no configs written", async () => {
|
|
398
|
+
const state = makeState({
|
|
399
|
+
selectedPackages: ["opentasks", "minimem"],
|
|
400
|
+
});
|
|
401
|
+
await showReview(state);
|
|
402
|
+
});
|
|
403
|
+
it("runs without error with full state", async () => {
|
|
404
|
+
const state = makeState({
|
|
405
|
+
selectedPackages: [
|
|
406
|
+
"opentasks",
|
|
407
|
+
"minimem",
|
|
408
|
+
"cognitive-core",
|
|
409
|
+
"skill-tree",
|
|
410
|
+
"claude-code-swarm",
|
|
411
|
+
"sessionlog",
|
|
412
|
+
],
|
|
413
|
+
packageConfigs: {
|
|
414
|
+
minimem: { values: { "query.maxResults": "50" }, usedCliWizard: false },
|
|
415
|
+
"skill-tree": { values: {}, usedCliWizard: true },
|
|
416
|
+
sessionlog: { values: { enabled: true }, usedCliWizard: false },
|
|
417
|
+
},
|
|
418
|
+
integrationWiring: [
|
|
419
|
+
{ key: "claude-code-swarm:sessionlog", enabled: true, values: {} },
|
|
420
|
+
{ key: "claude-code-swarm:openteams", enabled: false, values: {} },
|
|
421
|
+
],
|
|
422
|
+
configsWritten: [
|
|
423
|
+
{ package: "skill-tree", path: "~/.skill-tree/", description: "Global skill-tree config" },
|
|
424
|
+
{ package: "opentasks", path: ".swarm/opentasks/", description: "Project opentasks config" },
|
|
425
|
+
{ package: "minimem", path: ".swarm/minimem/", description: "Project minimem config" },
|
|
426
|
+
],
|
|
427
|
+
});
|
|
428
|
+
await showReview(state);
|
|
429
|
+
});
|
|
430
|
+
it("handles state with usedCliWizard, custom config, and defaults labels", async () => {
|
|
431
|
+
const state = makeState({
|
|
432
|
+
selectedPackages: ["minimem", "sessionlog", "opentasks"],
|
|
433
|
+
packageConfigs: {
|
|
434
|
+
minimem: { values: { "query.maxResults": "50" }, usedCliWizard: false },
|
|
435
|
+
sessionlog: { values: {}, usedCliWizard: true },
|
|
436
|
+
// opentasks not in packageConfigs → shows "defaults"
|
|
437
|
+
},
|
|
438
|
+
});
|
|
439
|
+
// Capture console output
|
|
440
|
+
const logs = [];
|
|
441
|
+
const origLog = console.log;
|
|
442
|
+
console.log = (...args) => {
|
|
443
|
+
logs.push(args.map(String).join(" "));
|
|
444
|
+
};
|
|
445
|
+
await showReview(state);
|
|
446
|
+
console.log = origLog;
|
|
447
|
+
const output = logs.join("\n");
|
|
448
|
+
// sessionlog should show "configured via wizard"
|
|
449
|
+
expect(output).toContain("configured via wizard");
|
|
450
|
+
// minimem should show "custom config"
|
|
451
|
+
expect(output).toContain("custom config");
|
|
452
|
+
// opentasks should show "defaults"
|
|
453
|
+
expect(output).toContain("defaults");
|
|
454
|
+
});
|
|
455
|
+
});
|
|
456
|
+
// ─── E2E: idempotent init ────────────────────────────────────────────────────
|
|
457
|
+
describe("e2e: idempotent init — re-running does not overwrite", async () => {
|
|
458
|
+
const opentasksOk = await hasCliInstalled("opentasks");
|
|
459
|
+
it.skipIf(!opentasksOk)("second init run skips already-initialized packages", async () => {
|
|
460
|
+
createProject("idem-e2e");
|
|
461
|
+
const packages = ["opentasks", "minimem", "sessionlog"];
|
|
462
|
+
const ctx = {
|
|
463
|
+
cwd: testDir,
|
|
464
|
+
packages,
|
|
465
|
+
embeddingProvider: "openai",
|
|
466
|
+
apiKeys: {},
|
|
467
|
+
usePrefix: true,
|
|
468
|
+
packageConfigs: undefined,
|
|
469
|
+
};
|
|
470
|
+
// First init
|
|
471
|
+
for (const pkg of PROJECT_INIT_ORDER) {
|
|
472
|
+
if (!packages.includes(pkg))
|
|
473
|
+
continue;
|
|
474
|
+
if (isProjectInit(testDir, pkg))
|
|
475
|
+
continue;
|
|
476
|
+
await initProjectPackage(pkg, ctx);
|
|
477
|
+
}
|
|
478
|
+
// Modify sessionlog config after init
|
|
479
|
+
const slPath = join(testDir, ".swarm", "sessionlog", "settings.json");
|
|
480
|
+
const sl = JSON.parse(readFileSync(slPath, "utf-8"));
|
|
481
|
+
sl.enabled = true;
|
|
482
|
+
sl.strategy = "auto-commit";
|
|
483
|
+
writeFileSync(slPath, JSON.stringify(sl, null, 2) + "\n");
|
|
484
|
+
// Second init — all should be skipped (isProjectInit returns true)
|
|
485
|
+
const secondRunResults = [];
|
|
486
|
+
for (const pkg of PROJECT_INIT_ORDER) {
|
|
487
|
+
if (!packages.includes(pkg))
|
|
488
|
+
continue;
|
|
489
|
+
if (isProjectInit(testDir, pkg))
|
|
490
|
+
continue;
|
|
491
|
+
secondRunResults.push(await initProjectPackage(pkg, ctx));
|
|
492
|
+
}
|
|
493
|
+
// Nothing ran in second pass
|
|
494
|
+
expect(secondRunResults).toEqual([]);
|
|
495
|
+
// Modified config preserved
|
|
496
|
+
const slConfig = JSON.parse(readFileSync(slPath, "utf-8"));
|
|
497
|
+
expect(slConfig.enabled).toBe(true);
|
|
498
|
+
expect(slConfig.strategy).toBe("auto-commit");
|
|
499
|
+
});
|
|
500
|
+
});
|
|
501
|
+
// ─── applyIntegrationWiring ──────────────────────────────────────────────────
|
|
502
|
+
describe("applyIntegrationWiring", () => {
|
|
503
|
+
it("writes wiring values to target package config on disk", () => {
|
|
504
|
+
// Create claude-code-swarm project config
|
|
505
|
+
const csDir = join(testDir, ".swarm", "claude-swarm");
|
|
506
|
+
mkdirSync(csDir, { recursive: true });
|
|
507
|
+
writeFileSync(join(csDir, "config.json"), JSON.stringify({
|
|
508
|
+
template: "",
|
|
509
|
+
map: { enabled: false, server: "ws://localhost:8080" },
|
|
510
|
+
sessionlog: { enabled: false, sync: "off" },
|
|
511
|
+
}, null, 2) + "\n");
|
|
512
|
+
// Simulate cwd being testDir
|
|
513
|
+
const originalCwd = process.cwd();
|
|
514
|
+
process.chdir(testDir);
|
|
515
|
+
try {
|
|
516
|
+
const state = makeState({
|
|
517
|
+
selectedPackages: [
|
|
518
|
+
"claude-code-swarm",
|
|
519
|
+
"openteams",
|
|
520
|
+
"sessionlog",
|
|
521
|
+
],
|
|
522
|
+
usePrefix: true,
|
|
523
|
+
integrationWiring: [
|
|
524
|
+
{
|
|
525
|
+
key: "claude-code-swarm:openteams",
|
|
526
|
+
enabled: true,
|
|
527
|
+
values: { template: "gsd" },
|
|
528
|
+
},
|
|
529
|
+
{
|
|
530
|
+
key: "claude-code-swarm:sessionlog",
|
|
531
|
+
enabled: true,
|
|
532
|
+
values: { sessionBridging: true, sessionSync: "live" },
|
|
533
|
+
},
|
|
534
|
+
],
|
|
535
|
+
});
|
|
536
|
+
applyIntegrationWiring(state);
|
|
537
|
+
const config = JSON.parse(readFileSync(join(csDir, "config.json"), "utf-8"));
|
|
538
|
+
expect(config.template).toBe("gsd");
|
|
539
|
+
expect(config.sessionlog.enabled).toBe(true);
|
|
540
|
+
expect(config.sessionlog.sync).toBe("live");
|
|
541
|
+
// Untouched values preserved
|
|
542
|
+
expect(config.map.enabled).toBe(false);
|
|
543
|
+
expect(config.map.server).toBe("ws://localhost:8080");
|
|
544
|
+
}
|
|
545
|
+
finally {
|
|
546
|
+
process.chdir(originalCwd);
|
|
547
|
+
}
|
|
548
|
+
});
|
|
549
|
+
it("skips disabled wiring entries", () => {
|
|
550
|
+
const csDir = join(testDir, ".swarm", "claude-swarm");
|
|
551
|
+
mkdirSync(csDir, { recursive: true });
|
|
552
|
+
writeFileSync(join(csDir, "config.json"), JSON.stringify({
|
|
553
|
+
template: "",
|
|
554
|
+
sessionlog: { enabled: false, sync: "off" },
|
|
555
|
+
}, null, 2) + "\n");
|
|
556
|
+
const originalCwd = process.cwd();
|
|
557
|
+
process.chdir(testDir);
|
|
558
|
+
try {
|
|
559
|
+
const state = makeState({
|
|
560
|
+
selectedPackages: ["claude-code-swarm", "sessionlog"],
|
|
561
|
+
usePrefix: true,
|
|
562
|
+
integrationWiring: [
|
|
563
|
+
{
|
|
564
|
+
key: "claude-code-swarm:sessionlog",
|
|
565
|
+
enabled: false,
|
|
566
|
+
values: {},
|
|
567
|
+
},
|
|
568
|
+
],
|
|
569
|
+
});
|
|
570
|
+
applyIntegrationWiring(state);
|
|
571
|
+
const config = JSON.parse(readFileSync(join(csDir, "config.json"), "utf-8"));
|
|
572
|
+
// Nothing changed
|
|
573
|
+
expect(config.template).toBe("");
|
|
574
|
+
expect(config.sessionlog.enabled).toBe(false);
|
|
575
|
+
expect(config.sessionlog.sync).toBe("off");
|
|
576
|
+
}
|
|
577
|
+
finally {
|
|
578
|
+
process.chdir(originalCwd);
|
|
579
|
+
}
|
|
580
|
+
});
|
|
581
|
+
it("handles empty integrationWiring gracefully", () => {
|
|
582
|
+
const state = makeState({
|
|
583
|
+
selectedPackages: ["opentasks"],
|
|
584
|
+
integrationWiring: [],
|
|
585
|
+
});
|
|
586
|
+
// Should not throw
|
|
587
|
+
applyIntegrationWiring(state);
|
|
588
|
+
});
|
|
589
|
+
it("writes MAP config options to claude-code-swarm", () => {
|
|
590
|
+
const csDir = join(testDir, ".swarm", "claude-swarm");
|
|
591
|
+
mkdirSync(csDir, { recursive: true });
|
|
592
|
+
writeFileSync(join(csDir, "config.json"), JSON.stringify({
|
|
593
|
+
map: { enabled: false, server: "ws://localhost:8080", scope: "" },
|
|
594
|
+
}, null, 2) + "\n");
|
|
595
|
+
const originalCwd = process.cwd();
|
|
596
|
+
process.chdir(testDir);
|
|
597
|
+
try {
|
|
598
|
+
const state = makeState({
|
|
599
|
+
selectedPackages: ["claude-code-swarm", "multi-agent-protocol"],
|
|
600
|
+
usePrefix: true,
|
|
601
|
+
integrationWiring: [
|
|
602
|
+
{
|
|
603
|
+
key: "claude-code-swarm:multi-agent-protocol",
|
|
604
|
+
enabled: true,
|
|
605
|
+
values: { mapEnabled: true, mapServer: "ws://production:9090" },
|
|
606
|
+
},
|
|
607
|
+
],
|
|
608
|
+
});
|
|
609
|
+
applyIntegrationWiring(state);
|
|
610
|
+
const config = JSON.parse(readFileSync(join(csDir, "config.json"), "utf-8"));
|
|
611
|
+
expect(config.map.enabled).toBe(true);
|
|
612
|
+
expect(config.map.server).toBe("ws://production:9090");
|
|
613
|
+
}
|
|
614
|
+
finally {
|
|
615
|
+
process.chdir(originalCwd);
|
|
616
|
+
}
|
|
617
|
+
});
|
|
618
|
+
it("is a no-op when target config file doesn't exist", () => {
|
|
619
|
+
const originalCwd = process.cwd();
|
|
620
|
+
process.chdir(testDir);
|
|
621
|
+
try {
|
|
622
|
+
const state = makeState({
|
|
623
|
+
selectedPackages: ["claude-code-swarm", "sessionlog"],
|
|
624
|
+
usePrefix: true,
|
|
625
|
+
integrationWiring: [
|
|
626
|
+
{
|
|
627
|
+
key: "claude-code-swarm:sessionlog",
|
|
628
|
+
enabled: true,
|
|
629
|
+
values: { sessionBridging: true },
|
|
630
|
+
},
|
|
631
|
+
],
|
|
632
|
+
});
|
|
633
|
+
// No config file exists — should not throw
|
|
634
|
+
applyIntegrationWiring(state);
|
|
635
|
+
// No file created (wiring only patches existing files)
|
|
636
|
+
expect(existsSync(join(testDir, ".swarm", "claude-swarm", "config.json"))).toBe(false);
|
|
637
|
+
}
|
|
638
|
+
finally {
|
|
639
|
+
process.chdir(originalCwd);
|
|
640
|
+
}
|
|
641
|
+
});
|
|
642
|
+
});
|
|
643
|
+
// ─── E2E: init → integration wiring → verify on disk ────────────────────────
|
|
644
|
+
describe("e2e: init → applyIntegrationWiring → verify", async () => {
|
|
645
|
+
const opentasksOk = await hasCliInstalled("opentasks");
|
|
646
|
+
it.skipIf(!opentasksOk)("full flow: init packages then apply wiring to existing configs", async () => {
|
|
647
|
+
createProject("wiring-e2e");
|
|
648
|
+
const state = makeState({
|
|
649
|
+
selectedPackages: [
|
|
650
|
+
"opentasks",
|
|
651
|
+
"claude-code-swarm",
|
|
652
|
+
"sessionlog",
|
|
653
|
+
"openteams",
|
|
654
|
+
],
|
|
655
|
+
embeddingProvider: null,
|
|
656
|
+
usePrefix: true,
|
|
657
|
+
});
|
|
658
|
+
// Phase 5: global setup
|
|
659
|
+
await initGlobal(state);
|
|
660
|
+
// Phase 6: project init
|
|
661
|
+
for (const pkg of PROJECT_INIT_ORDER) {
|
|
662
|
+
if (!state.selectedPackages.includes(pkg))
|
|
663
|
+
continue;
|
|
664
|
+
if (isProjectInit(testDir, pkg))
|
|
665
|
+
continue;
|
|
666
|
+
const ctx = {
|
|
667
|
+
cwd: testDir,
|
|
668
|
+
packages: state.selectedPackages,
|
|
669
|
+
embeddingProvider: state.embeddingProvider,
|
|
670
|
+
apiKeys: state.apiKeys,
|
|
671
|
+
usePrefix: state.usePrefix,
|
|
672
|
+
packageConfigs: state.packageConfigs,
|
|
673
|
+
};
|
|
674
|
+
await initProjectPackage(pkg, ctx);
|
|
675
|
+
}
|
|
676
|
+
// Verify config exists before wiring
|
|
677
|
+
expect(existsSync(join(testDir, ".swarm", "claude-swarm", "config.json"))).toBe(true);
|
|
678
|
+
const configBefore = JSON.parse(readFileSync(join(testDir, ".swarm", "claude-swarm", "config.json"), "utf-8"));
|
|
679
|
+
expect(configBefore.sessionlog.enabled).toBe(false);
|
|
680
|
+
expect(configBefore.template).toBe("");
|
|
681
|
+
// Phase 7.5: apply wiring
|
|
682
|
+
state.integrationWiring = [
|
|
683
|
+
{
|
|
684
|
+
key: "claude-code-swarm:openteams",
|
|
685
|
+
enabled: true,
|
|
686
|
+
values: { template: "gsd" },
|
|
687
|
+
},
|
|
688
|
+
{
|
|
689
|
+
key: "claude-code-swarm:sessionlog",
|
|
690
|
+
enabled: true,
|
|
691
|
+
values: { sessionBridging: true, sessionSync: "on-finish" },
|
|
692
|
+
},
|
|
693
|
+
];
|
|
694
|
+
const originalCwd = process.cwd();
|
|
695
|
+
process.chdir(testDir);
|
|
696
|
+
try {
|
|
697
|
+
applyIntegrationWiring(state);
|
|
698
|
+
}
|
|
699
|
+
finally {
|
|
700
|
+
process.chdir(originalCwd);
|
|
701
|
+
}
|
|
702
|
+
// Verify wiring values persisted
|
|
703
|
+
const configAfter = JSON.parse(readFileSync(join(testDir, ".swarm", "claude-swarm", "config.json"), "utf-8"));
|
|
704
|
+
expect(configAfter.template).toBe("gsd");
|
|
705
|
+
expect(configAfter.sessionlog.enabled).toBe(true);
|
|
706
|
+
expect(configAfter.sessionlog.sync).toBe("on-finish");
|
|
707
|
+
// Defaults preserved
|
|
708
|
+
expect(configAfter.map.enabled).toBe(false);
|
|
709
|
+
expect(configAfter.map.server).toBe("ws://localhost:8080");
|
|
710
|
+
});
|
|
711
|
+
});
|