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,657 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the interactive wizard phases.
|
|
3
|
+
*
|
|
4
|
+
* These tests mock @inquirer/prompts to inject canned answers, then run
|
|
5
|
+
* the actual phase functions end-to-end — verifying the full flow including
|
|
6
|
+
* conditional logic, CLI wizard delegation, and state mutations.
|
|
7
|
+
*/
|
|
8
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
9
|
+
import { mkdirSync, rmSync, existsSync, readFileSync, writeFileSync, realpathSync, } from "node:fs";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import { tmpdir } from "node:os";
|
|
12
|
+
import { randomUUID } from "node:crypto";
|
|
13
|
+
import { execFile, execSync } from "node:child_process";
|
|
14
|
+
import { promisify } from "node:util";
|
|
15
|
+
const execFileAsync = promisify(execFile);
|
|
16
|
+
// ─── Mocks ──────────────────────────────────────────────────────────────────
|
|
17
|
+
// Mock homedir AND @inquirer/prompts.
|
|
18
|
+
// The prompt mock queues answers that are consumed in order.
|
|
19
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
20
|
+
let testHome;
|
|
21
|
+
vi.mock("node:os", async () => {
|
|
22
|
+
const actual = await import("node:os");
|
|
23
|
+
return { ...actual, homedir: () => testHome };
|
|
24
|
+
});
|
|
25
|
+
// Answer queue: each call to confirm/select/input/password pops the next answer
|
|
26
|
+
let answerQueue = [];
|
|
27
|
+
vi.mock("@inquirer/prompts", () => ({
|
|
28
|
+
confirm: vi.fn(async () => answerQueue.shift()),
|
|
29
|
+
select: vi.fn(async () => answerQueue.shift()),
|
|
30
|
+
input: vi.fn(async () => answerQueue.shift()),
|
|
31
|
+
password: vi.fn(async () => answerQueue.shift()),
|
|
32
|
+
}));
|
|
33
|
+
const { selectUseCase } = await import("./use-case.js");
|
|
34
|
+
const { configurePackages } = await import("./package-config.js");
|
|
35
|
+
const { configureIntegrations } = await import("./integrations.js");
|
|
36
|
+
const { initGlobal } = await import("./global-setup.js");
|
|
37
|
+
const { initProjectPackage, isProjectInit, PROJECT_INIT_ORDER, } = await import("../../../packages/setup.js");
|
|
38
|
+
import { createEmptyState } from "../state.js";
|
|
39
|
+
import { getAllPackageNames } from "../../../packages/registry.js";
|
|
40
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
41
|
+
let testDir;
|
|
42
|
+
beforeEach(() => {
|
|
43
|
+
const realTmp = realpathSync(tmpdir());
|
|
44
|
+
testDir = join(realTmp, `swarmkit-wizard-flow-${randomUUID()}`);
|
|
45
|
+
testHome = join(realTmp, `swarmkit-wizard-home-${randomUUID()}`);
|
|
46
|
+
mkdirSync(testDir, { recursive: true });
|
|
47
|
+
mkdirSync(testHome, { recursive: true });
|
|
48
|
+
answerQueue = [];
|
|
49
|
+
});
|
|
50
|
+
afterEach(() => {
|
|
51
|
+
rmSync(testDir, { recursive: true, force: true });
|
|
52
|
+
rmSync(testHome, { recursive: true, force: true });
|
|
53
|
+
});
|
|
54
|
+
async function hasCliInstalled(command) {
|
|
55
|
+
try {
|
|
56
|
+
await execFileAsync("which", [command]);
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function createProject(name, dir = testDir) {
|
|
64
|
+
execSync("git init -q", { cwd: dir });
|
|
65
|
+
writeFileSync(join(dir, "package.json"), JSON.stringify({ name, version: "1.0.0" }));
|
|
66
|
+
return dir;
|
|
67
|
+
}
|
|
68
|
+
function makeState(overrides = {}) {
|
|
69
|
+
return { ...createEmptyState(), ...overrides };
|
|
70
|
+
}
|
|
71
|
+
// ─── selectUseCase ───────────────────────────────────────────────────────────
|
|
72
|
+
describe("selectUseCase phase", () => {
|
|
73
|
+
it("selects all packages when user chooses 'all'", async () => {
|
|
74
|
+
answerQueue = ["all"];
|
|
75
|
+
const result = await selectUseCase(createEmptyState());
|
|
76
|
+
expect(result.bundle).toBe("all");
|
|
77
|
+
expect(result.selectedPackages).toEqual(getAllPackageNames());
|
|
78
|
+
});
|
|
79
|
+
it("walks through manual selection accepting all", async () => {
|
|
80
|
+
const allNames = getAllPackageNames();
|
|
81
|
+
// "manual" choice, then confirm=true for each package
|
|
82
|
+
answerQueue = ["manual", ...allNames.map(() => true)];
|
|
83
|
+
const result = await selectUseCase(createEmptyState());
|
|
84
|
+
expect(result.bundle).toBe("manual");
|
|
85
|
+
// Order differs from getAllPackageNames because manual walks by CATEGORY_ORDER
|
|
86
|
+
expect(result.selectedPackages.sort()).toEqual([...allNames].sort());
|
|
87
|
+
});
|
|
88
|
+
it("walks through manual selection accepting only some", async () => {
|
|
89
|
+
const allNames = getAllPackageNames();
|
|
90
|
+
// "manual" choice, then alternate true/false
|
|
91
|
+
// The walk order is by CATEGORY_ORDER, not registry order,
|
|
92
|
+
// so we just verify count and that only some were selected
|
|
93
|
+
answerQueue = [
|
|
94
|
+
"manual",
|
|
95
|
+
...allNames.map((_, i) => i % 2 === 0),
|
|
96
|
+
];
|
|
97
|
+
const result = await selectUseCase(createEmptyState());
|
|
98
|
+
expect(result.bundle).toBe("manual");
|
|
99
|
+
const expectedCount = allNames.filter((_, i) => i % 2 === 0).length;
|
|
100
|
+
expect(result.selectedPackages.length).toBe(expectedCount);
|
|
101
|
+
// All selected packages should be known
|
|
102
|
+
for (const pkg of result.selectedPackages) {
|
|
103
|
+
expect(allNames).toContain(pkg);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
// ─── configurePackages ───────────────────────────────────────────────────────
|
|
108
|
+
describe("configurePackages phase", () => {
|
|
109
|
+
it("skips entirely in quick mode", async () => {
|
|
110
|
+
const state = makeState({
|
|
111
|
+
selectedPackages: ["minimem", "sessionlog"],
|
|
112
|
+
quick: true,
|
|
113
|
+
});
|
|
114
|
+
const result = await configurePackages(state);
|
|
115
|
+
expect(result.packageConfigs).toEqual({});
|
|
116
|
+
// No prompts should have been consumed
|
|
117
|
+
expect(answerQueue).toEqual([]);
|
|
118
|
+
});
|
|
119
|
+
it("skips packages without setup config", async () => {
|
|
120
|
+
const state = makeState({
|
|
121
|
+
selectedPackages: ["cognitive-core", "multi-agent-protocol"],
|
|
122
|
+
});
|
|
123
|
+
// No prompts needed — these packages have no setup config
|
|
124
|
+
const result = await configurePackages(state);
|
|
125
|
+
expect(Object.keys(result.packageConfigs)).toEqual([]);
|
|
126
|
+
});
|
|
127
|
+
it("declines wizard and declines customize → empty config", async () => {
|
|
128
|
+
const state = makeState({
|
|
129
|
+
selectedPackages: ["minimem"],
|
|
130
|
+
});
|
|
131
|
+
// minimem has cliWizard + inlineOptions
|
|
132
|
+
answerQueue = [
|
|
133
|
+
false, // "Run minimem's interactive setup wizard?" → No
|
|
134
|
+
false, // "Customize minimem settings?" → No
|
|
135
|
+
];
|
|
136
|
+
const result = await configurePackages(state);
|
|
137
|
+
expect(result.packageConfigs.minimem).toBeDefined();
|
|
138
|
+
expect(result.packageConfigs.minimem.usedCliWizard).toBe(false);
|
|
139
|
+
expect(Object.keys(result.packageConfigs.minimem.values)).toEqual([]);
|
|
140
|
+
});
|
|
141
|
+
it("declines wizard, accepts customize → collects inline options", async () => {
|
|
142
|
+
const state = makeState({
|
|
143
|
+
selectedPackages: ["minimem"],
|
|
144
|
+
});
|
|
145
|
+
answerQueue = [
|
|
146
|
+
false, // "Run minimem's interactive setup wizard?" → No
|
|
147
|
+
true, // "Customize minimem settings?" → Yes
|
|
148
|
+
"0.9", // "Vector search weight (0-1):" → 0.9
|
|
149
|
+
"0.1", // "Text search weight (0-1):" → 0.1
|
|
150
|
+
"25", // "Max search results:" → 25
|
|
151
|
+
"0.5", // "Min relevance score (0-1):" → 0.5
|
|
152
|
+
];
|
|
153
|
+
const result = await configurePackages(state);
|
|
154
|
+
expect(result.packageConfigs.minimem.usedCliWizard).toBe(false);
|
|
155
|
+
expect(result.packageConfigs.minimem.values).toEqual({
|
|
156
|
+
"hybrid.vectorWeight": "0.9",
|
|
157
|
+
"hybrid.textWeight": "0.1",
|
|
158
|
+
"query.maxResults": "25",
|
|
159
|
+
"query.minScore": "0.5",
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
it("configures sessionlog inline (no CLI wizard)", async () => {
|
|
163
|
+
const state = makeState({
|
|
164
|
+
selectedPackages: ["sessionlog"],
|
|
165
|
+
});
|
|
166
|
+
// sessionlog has no cliWizard — goes straight to inline
|
|
167
|
+
answerQueue = [
|
|
168
|
+
true, // "Customize sessionlog settings?" → Yes
|
|
169
|
+
true, // "Enable session logging by default?" → Yes
|
|
170
|
+
"auto-commit", // "Commit strategy:" → auto-commit
|
|
171
|
+
"git@github.com:org/sessions.git", // "Session repo remote URL:" → URL
|
|
172
|
+
"my-project", // "Subdirectory in session repo:" → dir
|
|
173
|
+
true, // "Enable session summarization?" → Yes
|
|
174
|
+
];
|
|
175
|
+
const result = await configurePackages(state);
|
|
176
|
+
expect(result.packageConfigs.sessionlog.values).toEqual({
|
|
177
|
+
enabled: true,
|
|
178
|
+
strategy: "auto-commit",
|
|
179
|
+
"sessionRepo.remote": "git@github.com:org/sessions.git",
|
|
180
|
+
"sessionRepo.directory": "my-project",
|
|
181
|
+
summarizationEnabled: true,
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
it("configures claude-code-swarm with conditional options", async () => {
|
|
185
|
+
// With multi-agent-protocol AND sessionlog installed → all options shown
|
|
186
|
+
// configurePackages iterates selectedPackages in order, filtering to those with setup.
|
|
187
|
+
// multi-agent-protocol has no setup, so the order is: claude-code-swarm, sessionlog
|
|
188
|
+
const state = makeState({
|
|
189
|
+
selectedPackages: [
|
|
190
|
+
"claude-code-swarm",
|
|
191
|
+
"multi-agent-protocol",
|
|
192
|
+
"sessionlog",
|
|
193
|
+
],
|
|
194
|
+
});
|
|
195
|
+
answerQueue = [
|
|
196
|
+
// claude-code-swarm first (no wizard, has inline)
|
|
197
|
+
true, // "Customize claude-code-swarm settings?" → Yes
|
|
198
|
+
true, // "Enable MAP observability?" → Yes
|
|
199
|
+
"ws://custom:9090", // "MAP server URL:" (shown because MAP pkg installed)
|
|
200
|
+
"my-scope", // "MAP scope:" (shown because MAP pkg installed)
|
|
201
|
+
true, // "Enable sessionlog bridging?" (shown because sessionlog installed)
|
|
202
|
+
// sessionlog next (no wizard, has inline)
|
|
203
|
+
true, // "Customize sessionlog settings?" → Yes
|
|
204
|
+
true, // "Enable session logging?" → Yes
|
|
205
|
+
"auto-commit", // "Commit strategy" → auto-commit
|
|
206
|
+
"", // "Session repo remote URL:" → blank (skip)
|
|
207
|
+
"", // "Subdirectory in session repo:" → blank (auto)
|
|
208
|
+
false, // "Enable summarization?" → No
|
|
209
|
+
];
|
|
210
|
+
const result = await configurePackages(state);
|
|
211
|
+
expect(result.packageConfigs["claude-code-swarm"].values).toEqual({
|
|
212
|
+
"map.enabled": true,
|
|
213
|
+
"map.server": "ws://custom:9090",
|
|
214
|
+
"map.scope": "my-scope",
|
|
215
|
+
"sessionlog.enabled": true,
|
|
216
|
+
});
|
|
217
|
+
expect(result.packageConfigs.sessionlog.values).toEqual({
|
|
218
|
+
enabled: true,
|
|
219
|
+
strategy: "auto-commit",
|
|
220
|
+
"sessionRepo.remote": "",
|
|
221
|
+
"sessionRepo.directory": "",
|
|
222
|
+
summarizationEnabled: false,
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
it("hides conditional options when dependency packages not installed", async () => {
|
|
226
|
+
// Without multi-agent-protocol or sessionlog → conditional options hidden
|
|
227
|
+
const state = makeState({
|
|
228
|
+
selectedPackages: ["claude-code-swarm"],
|
|
229
|
+
});
|
|
230
|
+
answerQueue = [
|
|
231
|
+
true, // "Customize claude-code-swarm settings?" → Yes
|
|
232
|
+
true, // "Enable MAP observability?" → Yes (always shown)
|
|
233
|
+
// "MAP server URL" NOT shown (multi-agent-protocol not installed)
|
|
234
|
+
// "MAP scope" NOT shown
|
|
235
|
+
// "Enable sessionlog bridging" NOT shown (sessionlog not installed)
|
|
236
|
+
];
|
|
237
|
+
const result = await configurePackages(state);
|
|
238
|
+
// Only the non-conditional option was collected
|
|
239
|
+
expect(result.packageConfigs["claude-code-swarm"].values).toEqual({
|
|
240
|
+
"map.enabled": true,
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
it("configures multiple packages in sequence", async () => {
|
|
244
|
+
const state = makeState({
|
|
245
|
+
selectedPackages: ["minimem", "sessionlog", "claude-code-swarm"],
|
|
246
|
+
});
|
|
247
|
+
answerQueue = [
|
|
248
|
+
// minimem
|
|
249
|
+
false, // wizard → No
|
|
250
|
+
false, // customize → No
|
|
251
|
+
// sessionlog
|
|
252
|
+
true, // customize → Yes
|
|
253
|
+
true, // enabled → Yes
|
|
254
|
+
"manual-commit", // strategy
|
|
255
|
+
"", // session repo remote → blank (skip)
|
|
256
|
+
"", // session repo directory → blank (auto)
|
|
257
|
+
false, // summarization → No
|
|
258
|
+
// claude-code-swarm
|
|
259
|
+
true, // customize → Yes
|
|
260
|
+
false, // MAP → No
|
|
261
|
+
];
|
|
262
|
+
const result = await configurePackages(state);
|
|
263
|
+
expect(Object.keys(result.packageConfigs)).toEqual([
|
|
264
|
+
"minimem",
|
|
265
|
+
"sessionlog",
|
|
266
|
+
"claude-code-swarm",
|
|
267
|
+
]);
|
|
268
|
+
expect(result.packageConfigs.sessionlog.values.enabled).toBe(true);
|
|
269
|
+
expect(result.packageConfigs["claude-code-swarm"].values["map.enabled"]).toBe(false);
|
|
270
|
+
});
|
|
271
|
+
it("handles self-driving-repo inline fallback (template selection)", async () => {
|
|
272
|
+
const state = makeState({
|
|
273
|
+
selectedPackages: ["self-driving-repo"],
|
|
274
|
+
});
|
|
275
|
+
answerQueue = [
|
|
276
|
+
false, // wizard → No
|
|
277
|
+
true, // customize → Yes
|
|
278
|
+
"pr-review", // template selection
|
|
279
|
+
];
|
|
280
|
+
const result = await configurePackages(state);
|
|
281
|
+
expect(result.packageConfigs["self-driving-repo"].values).toEqual({
|
|
282
|
+
template: "pr-review",
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
// ─── configureIntegrations ───────────────────────────────────────────────────
|
|
287
|
+
describe("configureIntegrations phase", () => {
|
|
288
|
+
it("returns unchanged state when no integrations active", async () => {
|
|
289
|
+
const state = makeState({
|
|
290
|
+
selectedPackages: ["opentasks"], // no integration pairs
|
|
291
|
+
});
|
|
292
|
+
const result = await configureIntegrations(state);
|
|
293
|
+
expect(result.integrationWiring).toEqual([]);
|
|
294
|
+
});
|
|
295
|
+
it("shows auto-detected integrations without prompting", async () => {
|
|
296
|
+
const state = makeState({
|
|
297
|
+
selectedPackages: ["cognitive-core", "minimem"],
|
|
298
|
+
});
|
|
299
|
+
// Auto-detected → no prompts needed
|
|
300
|
+
const result = await configureIntegrations(state);
|
|
301
|
+
// No wiring entries for auto-detected
|
|
302
|
+
expect(result.integrationWiring).toEqual([]);
|
|
303
|
+
});
|
|
304
|
+
it("skips wirable integrations in quick mode", async () => {
|
|
305
|
+
const state = makeState({
|
|
306
|
+
selectedPackages: ["claude-code-swarm", "sessionlog"],
|
|
307
|
+
quick: true,
|
|
308
|
+
});
|
|
309
|
+
const result = await configureIntegrations(state);
|
|
310
|
+
expect(result.integrationWiring).toEqual([]);
|
|
311
|
+
});
|
|
312
|
+
it("prompts for wirable integrations and collects config", async () => {
|
|
313
|
+
const state = makeState({
|
|
314
|
+
selectedPackages: ["claude-code-swarm", "sessionlog"],
|
|
315
|
+
});
|
|
316
|
+
answerQueue = [
|
|
317
|
+
true, // "Configure this integration?" → Yes
|
|
318
|
+
true, // "Enable session data bridging?" → Yes
|
|
319
|
+
"live", // "Session sync mode:" → live
|
|
320
|
+
];
|
|
321
|
+
const result = await configureIntegrations(state);
|
|
322
|
+
expect(result.integrationWiring).toHaveLength(1);
|
|
323
|
+
expect(result.integrationWiring[0]).toEqual({
|
|
324
|
+
key: "claude-code-swarm:sessionlog",
|
|
325
|
+
enabled: true,
|
|
326
|
+
values: {
|
|
327
|
+
sessionBridging: true,
|
|
328
|
+
sessionSync: "live",
|
|
329
|
+
},
|
|
330
|
+
});
|
|
331
|
+
});
|
|
332
|
+
it("records disabled wiring when user declines integration", async () => {
|
|
333
|
+
const state = makeState({
|
|
334
|
+
selectedPackages: ["claude-code-swarm", "sessionlog"],
|
|
335
|
+
});
|
|
336
|
+
answerQueue = [
|
|
337
|
+
false, // "Configure this integration?" → No
|
|
338
|
+
];
|
|
339
|
+
const result = await configureIntegrations(state);
|
|
340
|
+
expect(result.integrationWiring).toHaveLength(1);
|
|
341
|
+
expect(result.integrationWiring[0].enabled).toBe(false);
|
|
342
|
+
expect(result.integrationWiring[0].values).toEqual({});
|
|
343
|
+
});
|
|
344
|
+
it("handles multiple wirable integrations", async () => {
|
|
345
|
+
const state = makeState({
|
|
346
|
+
selectedPackages: [
|
|
347
|
+
"claude-code-swarm",
|
|
348
|
+
"openteams",
|
|
349
|
+
"multi-agent-protocol",
|
|
350
|
+
"sessionlog",
|
|
351
|
+
],
|
|
352
|
+
});
|
|
353
|
+
answerQueue = [
|
|
354
|
+
// claude-code-swarm ↔ openteams
|
|
355
|
+
true, // configure → Yes
|
|
356
|
+
"gsd", // template → gsd
|
|
357
|
+
// claude-code-swarm ↔ multi-agent-protocol
|
|
358
|
+
true, // configure → Yes
|
|
359
|
+
false, // enable MAP → No
|
|
360
|
+
"ws://localhost:8080", // MAP server
|
|
361
|
+
// claude-code-swarm ↔ sessionlog
|
|
362
|
+
false, // configure → No
|
|
363
|
+
];
|
|
364
|
+
const result = await configureIntegrations(state);
|
|
365
|
+
expect(result.integrationWiring).toHaveLength(3);
|
|
366
|
+
const openteamsWiring = result.integrationWiring.find((w) => w.key === "claude-code-swarm:openteams");
|
|
367
|
+
expect(openteamsWiring?.enabled).toBe(true);
|
|
368
|
+
expect(openteamsWiring?.values.template).toBe("gsd");
|
|
369
|
+
const mapWiring = result.integrationWiring.find((w) => w.key === "claude-code-swarm:multi-agent-protocol");
|
|
370
|
+
expect(mapWiring?.enabled).toBe(true);
|
|
371
|
+
const sessionlogWiring = result.integrationWiring.find((w) => w.key === "claude-code-swarm:sessionlog");
|
|
372
|
+
expect(sessionlogWiring?.enabled).toBe(false);
|
|
373
|
+
});
|
|
374
|
+
it("handles mixed auto-detected + wirable integrations", async () => {
|
|
375
|
+
const state = makeState({
|
|
376
|
+
selectedPackages: [
|
|
377
|
+
"cognitive-core",
|
|
378
|
+
"minimem",
|
|
379
|
+
"claude-code-swarm",
|
|
380
|
+
"sessionlog",
|
|
381
|
+
],
|
|
382
|
+
});
|
|
383
|
+
answerQueue = [
|
|
384
|
+
// Only claude-code-swarm ↔ sessionlog is wirable
|
|
385
|
+
true, // configure → Yes
|
|
386
|
+
false, // bridging → No
|
|
387
|
+
"off", // sync mode → off
|
|
388
|
+
];
|
|
389
|
+
const result = await configureIntegrations(state);
|
|
390
|
+
// Only wirable integration has wiring entry
|
|
391
|
+
expect(result.integrationWiring).toHaveLength(1);
|
|
392
|
+
expect(result.integrationWiring[0].key).toBe("claude-code-swarm:sessionlog");
|
|
393
|
+
});
|
|
394
|
+
});
|
|
395
|
+
// ─── configurePackages: CLI wizard delegation path ───────────────────────────
|
|
396
|
+
//
|
|
397
|
+
// These tests answer "true" to "Run wizard?" so configurePackages actually
|
|
398
|
+
// calls runPackageWizard → spawns the real CLI → verifies filesystem results.
|
|
399
|
+
// We chdir into testDir so process.cwd() matches where the wizard should run.
|
|
400
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
401
|
+
describe("configurePackages — CLI wizard delegation (real CLIs)", async () => {
|
|
402
|
+
const opentasksOk = await hasCliInstalled("opentasks");
|
|
403
|
+
const skOk = await hasCliInstalled("skill-tree");
|
|
404
|
+
let originalCwd;
|
|
405
|
+
beforeEach(() => {
|
|
406
|
+
originalCwd = process.cwd();
|
|
407
|
+
});
|
|
408
|
+
afterEach(() => {
|
|
409
|
+
process.chdir(originalCwd);
|
|
410
|
+
});
|
|
411
|
+
it.skipIf(!opentasksOk)("accepts wizard → spawns opentasks init → sets usedCliWizard + creates config", async () => {
|
|
412
|
+
createProject("wizard-delegate-ot");
|
|
413
|
+
process.chdir(testDir);
|
|
414
|
+
const state = makeState({
|
|
415
|
+
selectedPackages: ["opentasks"],
|
|
416
|
+
usePrefix: true,
|
|
417
|
+
});
|
|
418
|
+
answerQueue = [
|
|
419
|
+
true, // "Run opentasks's interactive setup wizard?" → Yes
|
|
420
|
+
];
|
|
421
|
+
const result = await configurePackages(state);
|
|
422
|
+
// usedCliWizard should be set
|
|
423
|
+
expect(result.packageConfigs.opentasks).toBeDefined();
|
|
424
|
+
expect(result.packageConfigs.opentasks.usedCliWizard).toBe(true);
|
|
425
|
+
expect(result.packageConfigs.opentasks.values).toEqual({});
|
|
426
|
+
// opentasks should have created config — either at .swarm/opentasks/ (if
|
|
427
|
+
// the env var was respected) or at .opentasks/ (relocated after)
|
|
428
|
+
const prefixed = existsSync(join(testDir, ".swarm", "opentasks", "config.json"));
|
|
429
|
+
const flat = existsSync(join(testDir, ".opentasks", "config.json"));
|
|
430
|
+
expect(prefixed || flat).toBe(true);
|
|
431
|
+
// Verify the config has valid opentasks structure
|
|
432
|
+
if (prefixed) {
|
|
433
|
+
const config = JSON.parse(readFileSync(join(testDir, ".swarm", "opentasks", "config.json"), "utf-8"));
|
|
434
|
+
expect(config.version).toBe("1.0");
|
|
435
|
+
expect(config.location).toBeDefined();
|
|
436
|
+
expect(config.location.hash).toBeDefined();
|
|
437
|
+
}
|
|
438
|
+
});
|
|
439
|
+
it.skipIf(!opentasksOk)("wizard path with usePrefix=false creates at flat location", async () => {
|
|
440
|
+
createProject("wizard-flat-ot");
|
|
441
|
+
process.chdir(testDir);
|
|
442
|
+
const state = makeState({
|
|
443
|
+
selectedPackages: ["opentasks"],
|
|
444
|
+
usePrefix: false,
|
|
445
|
+
});
|
|
446
|
+
answerQueue = [
|
|
447
|
+
true, // wizard → Yes
|
|
448
|
+
];
|
|
449
|
+
const result = await configurePackages(state);
|
|
450
|
+
expect(result.packageConfigs.opentasks.usedCliWizard).toBe(true);
|
|
451
|
+
// With usePrefix=false, should be at .opentasks/
|
|
452
|
+
expect(existsSync(join(testDir, ".opentasks", "config.json"))).toBe(true);
|
|
453
|
+
// Should NOT be relocated to .swarm/
|
|
454
|
+
expect(existsSync(join(testDir, ".swarm", "opentasks"))).toBe(false);
|
|
455
|
+
});
|
|
456
|
+
it.skipIf(!opentasksOk)("wizard fallback: wizard fails → falls through to inline options", async () => {
|
|
457
|
+
createProject("wizard-fail-fallback");
|
|
458
|
+
process.chdir(testDir);
|
|
459
|
+
// Use a package that has both cliWizard and inlineOptions: self-driving-repo
|
|
460
|
+
// sdr is not installed, so the wizard will fail → should fall through to inline
|
|
461
|
+
const sdrInstalled = await hasCliInstalled("sdr");
|
|
462
|
+
if (!sdrInstalled) {
|
|
463
|
+
const state = makeState({
|
|
464
|
+
selectedPackages: ["self-driving-repo"],
|
|
465
|
+
usePrefix: true,
|
|
466
|
+
});
|
|
467
|
+
answerQueue = [
|
|
468
|
+
true, // "Run wizard?" → Yes (but sdr not installed → fails)
|
|
469
|
+
true, // "Customize settings?" → Yes (fallback)
|
|
470
|
+
"pr-review", // template selection
|
|
471
|
+
];
|
|
472
|
+
const result = await configurePackages(state);
|
|
473
|
+
// Should have fallen through to inline
|
|
474
|
+
expect(result.packageConfigs["self-driving-repo"].usedCliWizard).toBe(false);
|
|
475
|
+
expect(result.packageConfigs["self-driving-repo"].values.template).toBe("pr-review");
|
|
476
|
+
}
|
|
477
|
+
});
|
|
478
|
+
it.skipIf(!opentasksOk)("wizard + inline for different packages in same run", async () => {
|
|
479
|
+
createProject("wizard-mixed");
|
|
480
|
+
process.chdir(testDir);
|
|
481
|
+
const state = makeState({
|
|
482
|
+
selectedPackages: ["opentasks", "sessionlog"],
|
|
483
|
+
usePrefix: true,
|
|
484
|
+
});
|
|
485
|
+
answerQueue = [
|
|
486
|
+
// opentasks: accept wizard
|
|
487
|
+
true, // "Run wizard?" → Yes
|
|
488
|
+
// sessionlog: no wizard available, goes to inline
|
|
489
|
+
true, // "Customize?" → Yes
|
|
490
|
+
true, // enabled → Yes
|
|
491
|
+
"auto-commit", // strategy
|
|
492
|
+
false, // summarization → No
|
|
493
|
+
];
|
|
494
|
+
const result = await configurePackages(state);
|
|
495
|
+
// opentasks used wizard
|
|
496
|
+
expect(result.packageConfigs.opentasks.usedCliWizard).toBe(true);
|
|
497
|
+
// sessionlog used inline
|
|
498
|
+
expect(result.packageConfigs.sessionlog.usedCliWizard).toBe(false);
|
|
499
|
+
expect(result.packageConfigs.sessionlog.values.enabled).toBe(true);
|
|
500
|
+
expect(result.packageConfigs.sessionlog.values.strategy).toBe("auto-commit");
|
|
501
|
+
// opentasks config exists on disk
|
|
502
|
+
const otExists = existsSync(join(testDir, ".swarm", "opentasks", "config.json")) ||
|
|
503
|
+
existsSync(join(testDir, ".opentasks", "config.json"));
|
|
504
|
+
expect(otExists).toBe(true);
|
|
505
|
+
});
|
|
506
|
+
it.skipIf(!opentasksOk)("wizard-configured package is skipped by initGlobal and initProject", async () => {
|
|
507
|
+
createProject("wizard-skip-init");
|
|
508
|
+
process.chdir(testDir);
|
|
509
|
+
const state = makeState({
|
|
510
|
+
selectedPackages: ["opentasks", "sessionlog", "claude-code-swarm"],
|
|
511
|
+
usePrefix: true,
|
|
512
|
+
embeddingProvider: null,
|
|
513
|
+
});
|
|
514
|
+
// Phase 4: opentasks via wizard, sessionlog+claude-code-swarm via inline defaults
|
|
515
|
+
answerQueue = [
|
|
516
|
+
true, // opentasks wizard → Yes
|
|
517
|
+
false, // sessionlog customize → No
|
|
518
|
+
false, // claude-code-swarm customize → No
|
|
519
|
+
];
|
|
520
|
+
const updated = await configurePackages(state);
|
|
521
|
+
expect(updated.packageConfigs.opentasks.usedCliWizard).toBe(true);
|
|
522
|
+
expect(updated.packageConfigs.sessionlog.usedCliWizard).toBe(false);
|
|
523
|
+
expect(updated.packageConfigs["claude-code-swarm"].usedCliWizard).toBe(false);
|
|
524
|
+
// Phase 5: initGlobal — claude-code-swarm global should still init
|
|
525
|
+
await initGlobal(updated);
|
|
526
|
+
expect(existsSync(join(testHome, ".claude-swarm", "config.json"))).toBe(true);
|
|
527
|
+
// Phase 6: initProject (manually) — opentasks should be skipped
|
|
528
|
+
// Either because isProjectInit sees the wizard's output, or because
|
|
529
|
+
// usedCliWizard is true. Both paths correctly skip re-init.
|
|
530
|
+
const results = [];
|
|
531
|
+
for (const pkg of PROJECT_INIT_ORDER) {
|
|
532
|
+
if (!updated.selectedPackages.includes(pkg))
|
|
533
|
+
continue;
|
|
534
|
+
if (isProjectInit(testDir, pkg)) {
|
|
535
|
+
results.push(`${pkg}:already-init`);
|
|
536
|
+
continue;
|
|
537
|
+
}
|
|
538
|
+
if (updated.packageConfigs[pkg]?.usedCliWizard) {
|
|
539
|
+
results.push(`${pkg}:wizard-skip`);
|
|
540
|
+
continue;
|
|
541
|
+
}
|
|
542
|
+
const ctx = {
|
|
543
|
+
cwd: testDir,
|
|
544
|
+
packages: updated.selectedPackages,
|
|
545
|
+
embeddingProvider: updated.embeddingProvider,
|
|
546
|
+
apiKeys: updated.apiKeys,
|
|
547
|
+
usePrefix: updated.usePrefix,
|
|
548
|
+
packageConfigs: updated.packageConfigs,
|
|
549
|
+
};
|
|
550
|
+
await initProjectPackage(pkg, ctx);
|
|
551
|
+
results.push(`${pkg}:init`);
|
|
552
|
+
}
|
|
553
|
+
// opentasks was skipped (wizard created its config, isProjectInit returns true)
|
|
554
|
+
const otResult = results.find((r) => r.startsWith("opentasks:"));
|
|
555
|
+
expect(otResult).toMatch(/^opentasks:(already-init|wizard-skip)$/);
|
|
556
|
+
// sessionlog + claude-code-swarm were inited normally
|
|
557
|
+
expect(results).toContain("sessionlog:init");
|
|
558
|
+
expect(results).toContain("claude-code-swarm:init");
|
|
559
|
+
// All configs exist on disk
|
|
560
|
+
expect(existsSync(join(testDir, ".swarm", "sessionlog", "settings.json"))).toBe(true);
|
|
561
|
+
expect(existsSync(join(testDir, ".swarm", "claude-swarm", "config.json"))).toBe(true);
|
|
562
|
+
});
|
|
563
|
+
});
|
|
564
|
+
// ─── E2E: full wizard flow simulation ────────────────────────────────────────
|
|
565
|
+
describe("e2e: full wizard flow with mocked prompts", async () => {
|
|
566
|
+
const opentasksOk = await hasCliInstalled("opentasks");
|
|
567
|
+
it.skipIf(!opentasksOk)("selectUseCase → configurePackages → initGlobal → initProject → configureIntegrations", async () => {
|
|
568
|
+
createProject("full-wizard-e2e");
|
|
569
|
+
// Phase 1: select "all"
|
|
570
|
+
answerQueue = ["all"];
|
|
571
|
+
let state = await selectUseCase(createEmptyState());
|
|
572
|
+
expect(state.bundle).toBe("all");
|
|
573
|
+
state.usePrefix = true;
|
|
574
|
+
state.embeddingProvider = "openai";
|
|
575
|
+
// Phase 4: configurePackages — decline all wizards, customize sessionlog + claude-code-swarm
|
|
576
|
+
answerQueue = [
|
|
577
|
+
// self-driving-repo (has wizard + inline)
|
|
578
|
+
false, // wizard → No
|
|
579
|
+
false, // customize → No
|
|
580
|
+
// opentasks (has wizard only)
|
|
581
|
+
false, // wizard → No
|
|
582
|
+
// minimem (has wizard + inline)
|
|
583
|
+
false, // wizard → No
|
|
584
|
+
false, // customize → No
|
|
585
|
+
// skill-tree (has wizard only)
|
|
586
|
+
false, // wizard → No
|
|
587
|
+
// openhive (has wizard + inline)
|
|
588
|
+
false, // wizard → No
|
|
589
|
+
false, // customize → No
|
|
590
|
+
// openteams (has wizard only)
|
|
591
|
+
false, // wizard → No
|
|
592
|
+
// sessionlog (inline only)
|
|
593
|
+
true, // customize → Yes
|
|
594
|
+
true, // enabled → Yes
|
|
595
|
+
"auto-commit", // strategy
|
|
596
|
+
"", // session repo remote → blank (skip)
|
|
597
|
+
"", // session repo directory → blank (auto)
|
|
598
|
+
true, // summarization → Yes
|
|
599
|
+
// claude-code-swarm (inline only)
|
|
600
|
+
true, // customize → Yes
|
|
601
|
+
true, // MAP enabled → Yes
|
|
602
|
+
"ws://test:8080", // MAP server (shown because multi-agent-protocol selected)
|
|
603
|
+
"test-scope", // MAP scope
|
|
604
|
+
true, // sessionlog bridging (shown because sessionlog selected)
|
|
605
|
+
];
|
|
606
|
+
state = await configurePackages(state);
|
|
607
|
+
expect(state.packageConfigs.sessionlog).toBeDefined();
|
|
608
|
+
expect(state.packageConfigs.sessionlog.values.enabled).toBe(true);
|
|
609
|
+
expect(state.packageConfigs["claude-code-swarm"].values["map.enabled"]).toBe(true);
|
|
610
|
+
// Phase 5: initGlobal — creates skill-tree + claude-code-swarm global
|
|
611
|
+
await initGlobal(state);
|
|
612
|
+
expect(existsSync(join(testHome, ".claude-swarm", "config.json"))).toBe(true);
|
|
613
|
+
// Verify global claude-code-swarm got overrides
|
|
614
|
+
const globalCs = JSON.parse(readFileSync(join(testHome, ".claude-swarm", "config.json"), "utf-8"));
|
|
615
|
+
expect(globalCs.map.enabled).toBe(true);
|
|
616
|
+
expect(globalCs.map.server).toBe("ws://test:8080");
|
|
617
|
+
// Phase 6: initProject (call directly, skipping the confirm prompt)
|
|
618
|
+
const packages = ["opentasks", "minimem", "sessionlog", "claude-code-swarm"];
|
|
619
|
+
for (const pkg of PROJECT_INIT_ORDER) {
|
|
620
|
+
if (!packages.includes(pkg))
|
|
621
|
+
continue;
|
|
622
|
+
if (isProjectInit(testDir, pkg))
|
|
623
|
+
continue;
|
|
624
|
+
const ctx = {
|
|
625
|
+
cwd: testDir,
|
|
626
|
+
packages: state.selectedPackages,
|
|
627
|
+
embeddingProvider: state.embeddingProvider,
|
|
628
|
+
apiKeys: state.apiKeys,
|
|
629
|
+
usePrefix: state.usePrefix,
|
|
630
|
+
packageConfigs: state.packageConfigs,
|
|
631
|
+
};
|
|
632
|
+
await initProjectPackage(pkg, ctx);
|
|
633
|
+
}
|
|
634
|
+
// Verify project configs
|
|
635
|
+
const slSettings = JSON.parse(readFileSync(join(testDir, ".swarm", "sessionlog", "settings.json"), "utf-8"));
|
|
636
|
+
expect(slSettings.enabled).toBe(true);
|
|
637
|
+
expect(slSettings.strategy).toBe("auto-commit");
|
|
638
|
+
expect(slSettings.summarizationEnabled).toBe(true);
|
|
639
|
+
const csConfig = JSON.parse(readFileSync(join(testDir, ".swarm", "claude-swarm", "config.json"), "utf-8"));
|
|
640
|
+
expect(csConfig.map.enabled).toBe(true);
|
|
641
|
+
expect(csConfig.map.server).toBe("ws://test:8080");
|
|
642
|
+
expect(csConfig.sessionlog.enabled).toBe(true);
|
|
643
|
+
// Phase 7: configureIntegrations
|
|
644
|
+
answerQueue = [
|
|
645
|
+
// claude-code-swarm ↔ openteams
|
|
646
|
+
true, "gsd",
|
|
647
|
+
// claude-code-swarm ↔ multi-agent-protocol
|
|
648
|
+
false,
|
|
649
|
+
// claude-code-swarm ↔ sessionlog
|
|
650
|
+
true, true, "live",
|
|
651
|
+
];
|
|
652
|
+
state = await configureIntegrations(state);
|
|
653
|
+
const sessionWiring = state.integrationWiring.find((w) => w.key === "claude-code-swarm:sessionlog");
|
|
654
|
+
expect(sessionWiring?.enabled).toBe(true);
|
|
655
|
+
expect(sessionWiring?.values.sessionSync).toBe("live");
|
|
656
|
+
});
|
|
657
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|