swarmkit 0.0.2 → 0.0.4
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/README.md +67 -131
- package/dist/commands/add.js +45 -2
- package/dist/commands/init/phases/global-setup.js +1 -1
- package/dist/commands/init/phases/project.js +5 -3
- package/dist/commands/init/state.d.ts +2 -0
- package/dist/commands/init/state.js +1 -0
- package/dist/commands/init/state.test.js +1 -0
- package/dist/commands/init/wizard.d.ts +5 -1
- package/dist/commands/init/wizard.js +78 -8
- package/dist/commands/init.js +7 -2
- package/dist/commands/remove.js +6 -0
- package/dist/config/global.d.ts +12 -0
- package/dist/config/global.js +34 -1
- package/dist/config/global.test.js +48 -15
- package/dist/doctor/checks.d.ts +1 -1
- package/dist/doctor/checks.js +26 -10
- package/dist/doctor/checks.test.js +52 -27
- package/dist/index.d.ts +3 -2
- package/dist/index.js +3 -2
- package/dist/packages/installer.d.ts +9 -0
- package/dist/packages/installer.js +43 -12
- package/dist/packages/installer.test.js +84 -1
- package/dist/packages/plugin.d.ts +13 -0
- package/dist/packages/plugin.js +33 -0
- package/dist/packages/plugin.test.d.ts +1 -0
- package/dist/packages/plugin.test.js +99 -0
- package/dist/packages/registry.d.ts +1 -1
- package/dist/packages/registry.js +25 -50
- package/dist/packages/registry.test.js +38 -49
- package/dist/packages/setup.d.ts +11 -4
- package/dist/packages/setup.js +208 -103
- package/dist/packages/setup.test.js +337 -246
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
-
import { mkdirSync, rmSync, existsSync, readFileSync,
|
|
2
|
+
import { mkdirSync, rmSync, existsSync, lstatSync, readFileSync, readlinkSync, writeFileSync, realpathSync, } from "node:fs";
|
|
3
3
|
import { join, basename } from "node:path";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
5
|
import { randomUUID } from "node:crypto";
|
|
@@ -19,7 +19,7 @@ vi.mock("node:os", async () => {
|
|
|
19
19
|
const actual = await import("node:os");
|
|
20
20
|
return { ...actual, homedir: () => testHome };
|
|
21
21
|
});
|
|
22
|
-
const { PROJECT_CONFIG_DIRS, PROJECT_INIT_ORDER, GLOBAL_CONFIG_DIRS, isProjectInit, isGlobalInit, initProjectPackage, initGlobalPackage, } = await import("./setup.js");
|
|
22
|
+
const { PROJECT_CONFIG_DIRS, FLAT_PROJECT_CONFIG_DIRS, PROJECT_INIT_ORDER, GLOBAL_CONFIG_DIRS, isProjectInit, isGlobalInit, initProjectPackage, initGlobalPackage, } = await import("./setup.js");
|
|
23
23
|
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
24
24
|
let testDir;
|
|
25
25
|
beforeEach(() => {
|
|
@@ -58,6 +58,7 @@ function projectCtx(overrides = {}) {
|
|
|
58
58
|
packages: overrides.packages ?? [],
|
|
59
59
|
embeddingProvider: overrides.embeddingProvider ?? null,
|
|
60
60
|
apiKeys: overrides.apiKeys ?? {},
|
|
61
|
+
usePrefix: overrides.usePrefix ?? true,
|
|
61
62
|
};
|
|
62
63
|
}
|
|
63
64
|
function globalCtx(overrides = {}) {
|
|
@@ -69,13 +70,16 @@ function globalCtx(overrides = {}) {
|
|
|
69
70
|
}
|
|
70
71
|
// ─── Constants ───────────────────────────────────────────────────────────────
|
|
71
72
|
describe("constants", () => {
|
|
72
|
-
it("PROJECT_INIT_ORDER
|
|
73
|
+
it("PROJECT_INIT_ORDER includes all project-level packages", () => {
|
|
73
74
|
expect(PROJECT_INIT_ORDER).toEqual([
|
|
74
75
|
"opentasks",
|
|
75
76
|
"minimem",
|
|
76
77
|
"cognitive-core",
|
|
77
|
-
"
|
|
78
|
+
"skill-tree",
|
|
78
79
|
"self-driving-repo",
|
|
80
|
+
"openteams",
|
|
81
|
+
"sessionlog",
|
|
82
|
+
"claude-code-swarm",
|
|
79
83
|
]);
|
|
80
84
|
});
|
|
81
85
|
it("every ordered package has a config dir mapping", () => {
|
|
@@ -84,7 +88,7 @@ describe("constants", () => {
|
|
|
84
88
|
}
|
|
85
89
|
});
|
|
86
90
|
it("GLOBAL_CONFIG_DIRS maps all global packages", () => {
|
|
87
|
-
expect(Object.keys(GLOBAL_CONFIG_DIRS).sort()).toEqual(["
|
|
91
|
+
expect(Object.keys(GLOBAL_CONFIG_DIRS).sort()).toEqual(["openhive", "skill-tree"]);
|
|
88
92
|
});
|
|
89
93
|
it("no overlap between project and global config dirs", () => {
|
|
90
94
|
const projectDirs = new Set(Object.values(PROJECT_CONFIG_DIRS));
|
|
@@ -101,7 +105,11 @@ describe("isProjectInit", () => {
|
|
|
101
105
|
expect(isProjectInit(testDir, pkg)).toBe(false);
|
|
102
106
|
}
|
|
103
107
|
});
|
|
104
|
-
it.each(Object.entries(PROJECT_CONFIG_DIRS))("returns true for %s when %s/ exists", (pkg, configDir) => {
|
|
108
|
+
it.each(Object.entries(PROJECT_CONFIG_DIRS))("returns true for %s when %s/ exists (prefixed)", (pkg, configDir) => {
|
|
109
|
+
mkdirSync(join(testDir, configDir), { recursive: true });
|
|
110
|
+
expect(isProjectInit(testDir, pkg)).toBe(true);
|
|
111
|
+
});
|
|
112
|
+
it.each(Object.entries(FLAT_PROJECT_CONFIG_DIRS))("returns true for %s when %s/ exists (flat)", (pkg, configDir) => {
|
|
105
113
|
mkdirSync(join(testDir, configDir), { recursive: true });
|
|
106
114
|
expect(isProjectInit(testDir, pkg)).toBe(true);
|
|
107
115
|
});
|
|
@@ -130,18 +138,24 @@ describe("isGlobalInit", () => {
|
|
|
130
138
|
// ─── Project: opentasks (real CLI) ───────────────────────────────────────────
|
|
131
139
|
describe("initProjectPackage — opentasks (real CLI)", async () => {
|
|
132
140
|
const installed = await hasCliInstalled("opentasks");
|
|
133
|
-
it.skipIf(!installed)("runs opentasks init and creates
|
|
141
|
+
it.skipIf(!installed)("runs opentasks init, relocates to .swarm/, and creates symlink", async () => {
|
|
134
142
|
createProject("test-opentasks");
|
|
135
143
|
const result = await initProjectPackage("opentasks", projectCtx({ cwd: testDir, packages: ["opentasks"] }));
|
|
136
144
|
expect(result.success).toBe(true);
|
|
137
145
|
expect(result.package).toBe("opentasks");
|
|
138
|
-
// Verify real filesystem artifacts
|
|
139
|
-
expect(existsSync(join(testDir, ".opentasks"))).toBe(true);
|
|
146
|
+
// Verify real filesystem artifacts in .swarm/
|
|
147
|
+
expect(existsSync(join(testDir, ".swarm", "opentasks"))).toBe(true);
|
|
148
|
+
expect(existsSync(join(testDir, ".swarm", "opentasks", "config.json"))).toBe(true);
|
|
149
|
+
expect(existsSync(join(testDir, ".swarm", "opentasks", "graph.jsonl"))).toBe(true);
|
|
150
|
+
expect(existsSync(join(testDir, ".swarm", "opentasks", ".gitignore"))).toBe(true);
|
|
151
|
+
// Verify symlink at legacy location
|
|
152
|
+
const link = join(testDir, ".opentasks");
|
|
153
|
+
expect(lstatSync(link).isSymbolicLink()).toBe(true);
|
|
154
|
+
expect(readlinkSync(link)).toBe(".swarm/opentasks");
|
|
155
|
+
// Accessible via symlink
|
|
140
156
|
expect(existsSync(join(testDir, ".opentasks", "config.json"))).toBe(true);
|
|
141
|
-
expect(existsSync(join(testDir, ".opentasks", "graph.jsonl"))).toBe(true);
|
|
142
|
-
expect(existsSync(join(testDir, ".opentasks", ".gitignore"))).toBe(true);
|
|
143
157
|
// Verify config content
|
|
144
|
-
const config = JSON.parse(readFileSync(join(testDir, ".opentasks", "config.json"), "utf-8"));
|
|
158
|
+
const config = JSON.parse(readFileSync(join(testDir, ".swarm", "opentasks", "config.json"), "utf-8"));
|
|
145
159
|
expect(config.version).toBe("1.0");
|
|
146
160
|
expect(config.location.name).toBe("test-opentasks");
|
|
147
161
|
expect(config.location.hash).toBeDefined();
|
|
@@ -150,14 +164,14 @@ describe("initProjectPackage — opentasks (real CLI)", async () => {
|
|
|
150
164
|
it.skipIf(!installed)("uses project name from package.json", async () => {
|
|
151
165
|
createProject("my-custom-name");
|
|
152
166
|
await initProjectPackage("opentasks", projectCtx({ cwd: testDir, packages: ["opentasks"] }));
|
|
153
|
-
const config = JSON.parse(readFileSync(join(testDir, ".opentasks", "config.json"), "utf-8"));
|
|
167
|
+
const config = JSON.parse(readFileSync(join(testDir, ".swarm", "opentasks", "config.json"), "utf-8"));
|
|
154
168
|
expect(config.location.name).toBe("my-custom-name");
|
|
155
169
|
});
|
|
156
170
|
it.skipIf(!installed)("falls back to directory name without package.json", async () => {
|
|
157
171
|
// No package.json, just a real git repo
|
|
158
172
|
execSync("git init -q", { cwd: testDir });
|
|
159
173
|
await initProjectPackage("opentasks", projectCtx({ cwd: testDir, packages: ["opentasks"] }));
|
|
160
|
-
const config = JSON.parse(readFileSync(join(testDir, ".opentasks", "config.json"), "utf-8"));
|
|
174
|
+
const config = JSON.parse(readFileSync(join(testDir, ".swarm", "opentasks", "config.json"), "utf-8"));
|
|
161
175
|
expect(config.location.name).toBe(basename(testDir));
|
|
162
176
|
});
|
|
163
177
|
});
|
|
@@ -169,12 +183,12 @@ describe("initProjectPackage — minimem (real CLI)", async () => {
|
|
|
169
183
|
const result = await initProjectPackage("minimem", projectCtx({ cwd: testDir, packages: ["minimem"] }));
|
|
170
184
|
expect(result.success).toBe(true);
|
|
171
185
|
// Verify real filesystem artifacts
|
|
172
|
-
expect(existsSync(join(testDir, ".minimem"))).toBe(true);
|
|
173
|
-
expect(existsSync(join(testDir, ".minimem", "config.json"))).toBe(true);
|
|
174
|
-
expect(existsSync(join(testDir, ".minimem", ".gitignore"))).toBe(true);
|
|
186
|
+
expect(existsSync(join(testDir, ".swarm", "minimem"))).toBe(true);
|
|
187
|
+
expect(existsSync(join(testDir, ".swarm", "minimem", "config.json"))).toBe(true);
|
|
188
|
+
expect(existsSync(join(testDir, ".swarm", "minimem", ".gitignore"))).toBe(true);
|
|
175
189
|
expect(existsSync(join(testDir, "MEMORY.md"))).toBe(true);
|
|
176
190
|
// Verify default config
|
|
177
|
-
const config = JSON.parse(readFileSync(join(testDir, ".minimem", "config.json"), "utf-8"));
|
|
191
|
+
const config = JSON.parse(readFileSync(join(testDir, ".swarm", "minimem", "config.json"), "utf-8"));
|
|
178
192
|
expect(config.embedding.provider).toBe("auto");
|
|
179
193
|
expect(config.hybrid.enabled).toBe(true);
|
|
180
194
|
});
|
|
@@ -185,7 +199,7 @@ describe("initProjectPackage — minimem (real CLI)", async () => {
|
|
|
185
199
|
packages: ["minimem"],
|
|
186
200
|
embeddingProvider: "openai",
|
|
187
201
|
}));
|
|
188
|
-
const config = JSON.parse(readFileSync(join(testDir, ".minimem", "config.json"), "utf-8"));
|
|
202
|
+
const config = JSON.parse(readFileSync(join(testDir, ".swarm", "minimem", "config.json"), "utf-8"));
|
|
189
203
|
expect(config.embedding.provider).toBe("openai");
|
|
190
204
|
// Other fields preserved
|
|
191
205
|
expect(config.hybrid.enabled).toBe(true);
|
|
@@ -198,7 +212,7 @@ describe("initProjectPackage — minimem (real CLI)", async () => {
|
|
|
198
212
|
packages: ["minimem"],
|
|
199
213
|
embeddingProvider: "gemini",
|
|
200
214
|
}));
|
|
201
|
-
const config = JSON.parse(readFileSync(join(testDir, ".minimem", "config.json"), "utf-8"));
|
|
215
|
+
const config = JSON.parse(readFileSync(join(testDir, ".swarm", "minimem", "config.json"), "utf-8"));
|
|
202
216
|
expect(config.embedding.provider).toBe("gemini");
|
|
203
217
|
});
|
|
204
218
|
it.skipIf(!installed)("does NOT patch when provider is local", async () => {
|
|
@@ -208,7 +222,7 @@ describe("initProjectPackage — minimem (real CLI)", async () => {
|
|
|
208
222
|
packages: ["minimem"],
|
|
209
223
|
embeddingProvider: "local",
|
|
210
224
|
}));
|
|
211
|
-
const config = JSON.parse(readFileSync(join(testDir, ".minimem", "config.json"), "utf-8"));
|
|
225
|
+
const config = JSON.parse(readFileSync(join(testDir, ".swarm", "minimem", "config.json"), "utf-8"));
|
|
212
226
|
expect(config.embedding.provider).toBe("auto");
|
|
213
227
|
});
|
|
214
228
|
it.skipIf(!installed)("does NOT patch when provider is null", async () => {
|
|
@@ -218,7 +232,7 @@ describe("initProjectPackage — minimem (real CLI)", async () => {
|
|
|
218
232
|
packages: ["minimem"],
|
|
219
233
|
embeddingProvider: null,
|
|
220
234
|
}));
|
|
221
|
-
const config = JSON.parse(readFileSync(join(testDir, ".minimem", "config.json"), "utf-8"));
|
|
235
|
+
const config = JSON.parse(readFileSync(join(testDir, ".swarm", "minimem", "config.json"), "utf-8"));
|
|
222
236
|
expect(config.embedding.provider).toBe("auto");
|
|
223
237
|
});
|
|
224
238
|
});
|
|
@@ -230,53 +244,11 @@ describe("initProjectPackage — cognitive-core (real CLI)", async () => {
|
|
|
230
244
|
const result = await initProjectPackage("cognitive-core", projectCtx({ cwd: testDir, packages: ["cognitive-core"] }));
|
|
231
245
|
expect(result.success).toBe(true);
|
|
232
246
|
// Verify directory structure
|
|
233
|
-
expect(existsSync(join(testDir, ".cognitive-core"))).toBe(true);
|
|
234
|
-
expect(existsSync(join(testDir, ".cognitive-core", "experiences"))).toBe(true);
|
|
235
|
-
expect(existsSync(join(testDir, ".cognitive-core", "playbooks"))).toBe(true);
|
|
236
|
-
expect(existsSync(join(testDir, ".cognitive-core", "meta-strategies"))).toBe(true);
|
|
237
|
-
expect(existsSync(join(testDir, ".cognitive-core", "meta-observations"))).toBe(true);
|
|
238
|
-
});
|
|
239
|
-
});
|
|
240
|
-
// ─── Project: macro-agent (direct write — no CLI) ───────────────────────────
|
|
241
|
-
describe("initProjectPackage — macro-agent (direct config write)", () => {
|
|
242
|
-
it("creates .multiagent/config.json", async () => {
|
|
243
|
-
const result = await initProjectPackage("macro-agent", projectCtx({ cwd: testDir, packages: ["macro-agent"] }));
|
|
244
|
-
expect(result.success).toBe(true);
|
|
245
|
-
expect(existsSync(join(testDir, ".multiagent", "config.json"))).toBe(true);
|
|
246
|
-
});
|
|
247
|
-
it("config has correct defaults", async () => {
|
|
248
|
-
await initProjectPackage("macro-agent", projectCtx({ cwd: testDir, packages: ["macro-agent"] }));
|
|
249
|
-
const config = JSON.parse(readFileSync(join(testDir, ".multiagent", "config.json"), "utf-8"));
|
|
250
|
-
expect(config).toEqual({
|
|
251
|
-
team: "default",
|
|
252
|
-
port: 3001,
|
|
253
|
-
host: "localhost",
|
|
254
|
-
});
|
|
255
|
-
});
|
|
256
|
-
it("wires opentasks as task backend when opentasks is selected", async () => {
|
|
257
|
-
await initProjectPackage("macro-agent", projectCtx({ cwd: testDir, packages: ["macro-agent", "opentasks"] }));
|
|
258
|
-
const config = JSON.parse(readFileSync(join(testDir, ".multiagent", "config.json"), "utf-8"));
|
|
259
|
-
expect(config.task).toEqual({
|
|
260
|
-
backend: "opentasks",
|
|
261
|
-
opentasks: { auto_start: true },
|
|
262
|
-
});
|
|
263
|
-
});
|
|
264
|
-
it("omits task backend when opentasks not selected", async () => {
|
|
265
|
-
await initProjectPackage("macro-agent", projectCtx({ cwd: testDir, packages: ["macro-agent"] }));
|
|
266
|
-
const config = JSON.parse(readFileSync(join(testDir, ".multiagent", "config.json"), "utf-8"));
|
|
267
|
-
expect(config.task).toBeUndefined();
|
|
268
|
-
});
|
|
269
|
-
it("is idempotent — second run overwrites cleanly", async () => {
|
|
270
|
-
await initProjectPackage("macro-agent", projectCtx({ cwd: testDir, packages: ["macro-agent", "opentasks"] }));
|
|
271
|
-
await initProjectPackage("macro-agent", projectCtx({ cwd: testDir, packages: ["macro-agent"] }));
|
|
272
|
-
const config = JSON.parse(readFileSync(join(testDir, ".multiagent", "config.json"), "utf-8"));
|
|
273
|
-
expect(config.task).toBeUndefined();
|
|
274
|
-
});
|
|
275
|
-
it("produces valid JSON with trailing newline", async () => {
|
|
276
|
-
await initProjectPackage("macro-agent", projectCtx({ cwd: testDir, packages: ["macro-agent", "opentasks"] }));
|
|
277
|
-
const raw = readFileSync(join(testDir, ".multiagent", "config.json"), "utf-8");
|
|
278
|
-
expect(() => JSON.parse(raw)).not.toThrow();
|
|
279
|
-
expect(raw.endsWith("\n")).toBe(true);
|
|
247
|
+
expect(existsSync(join(testDir, ".swarm", "cognitive-core"))).toBe(true);
|
|
248
|
+
expect(existsSync(join(testDir, ".swarm", "cognitive-core", "experiences"))).toBe(true);
|
|
249
|
+
expect(existsSync(join(testDir, ".swarm", "cognitive-core", "playbooks"))).toBe(true);
|
|
250
|
+
expect(existsSync(join(testDir, ".swarm", "cognitive-core", "meta-strategies"))).toBe(true);
|
|
251
|
+
expect(existsSync(join(testDir, ".swarm", "cognitive-core", "meta-observations"))).toBe(true);
|
|
280
252
|
});
|
|
281
253
|
});
|
|
282
254
|
// ─── Project: self-driving-repo ──────────────────────────────────────────────
|
|
@@ -287,7 +259,7 @@ describe("initProjectPackage — self-driving-repo", async () => {
|
|
|
287
259
|
createProject("test-sdr");
|
|
288
260
|
const result = await initProjectPackage("self-driving-repo", projectCtx({ cwd: testDir, packages: ["self-driving-repo"] }));
|
|
289
261
|
expect(result.success).toBe(true);
|
|
290
|
-
expect(existsSync(join(testDir, ".self-driving"))).toBe(true);
|
|
262
|
+
expect(existsSync(join(testDir, ".swarm", "self-driving"))).toBe(true);
|
|
291
263
|
});
|
|
292
264
|
it.skipIf(installed)("returns failure when sdr CLI is not installed", async () => {
|
|
293
265
|
const result = await initProjectPackage("self-driving-repo", projectCtx({ cwd: testDir, packages: ["self-driving-repo"] }));
|
|
@@ -296,6 +268,134 @@ describe("initProjectPackage — self-driving-repo", async () => {
|
|
|
296
268
|
expect(result.message).toBeDefined();
|
|
297
269
|
});
|
|
298
270
|
});
|
|
271
|
+
// ─── Project: skill-tree (direct directory creation — no CLI) ────────────────
|
|
272
|
+
describe("initProjectPackage — skill-tree", () => {
|
|
273
|
+
it("creates .swarm/skilltree/ with skills/ subdirectory (prefixed)", async () => {
|
|
274
|
+
const result = await initProjectPackage("skill-tree", projectCtx({ cwd: testDir, packages: ["skill-tree"] }));
|
|
275
|
+
expect(result.success).toBe(true);
|
|
276
|
+
expect(result.package).toBe("skill-tree");
|
|
277
|
+
expect(existsSync(join(testDir, ".swarm", "skilltree"))).toBe(true);
|
|
278
|
+
expect(existsSync(join(testDir, ".swarm", "skilltree", "skills"))).toBe(true);
|
|
279
|
+
});
|
|
280
|
+
it("creates .skilltree/ with skills/ subdirectory (flat)", async () => {
|
|
281
|
+
const result = await initProjectPackage("skill-tree", projectCtx({ cwd: testDir, packages: ["skill-tree"], usePrefix: false }));
|
|
282
|
+
expect(result.success).toBe(true);
|
|
283
|
+
expect(existsSync(join(testDir, ".skilltree"))).toBe(true);
|
|
284
|
+
expect(existsSync(join(testDir, ".skilltree", "skills"))).toBe(true);
|
|
285
|
+
});
|
|
286
|
+
it("is idempotent", async () => {
|
|
287
|
+
await initProjectPackage("skill-tree", projectCtx({ cwd: testDir, packages: ["skill-tree"] }));
|
|
288
|
+
const result = await initProjectPackage("skill-tree", projectCtx({ cwd: testDir, packages: ["skill-tree"] }));
|
|
289
|
+
expect(result.success).toBe(true);
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
// ─── Project: openteams (direct directory creation — no CLI) ─────────────────
|
|
293
|
+
describe("initProjectPackage — openteams", () => {
|
|
294
|
+
it("creates .swarm/openteams/ with config.json (prefixed)", async () => {
|
|
295
|
+
const result = await initProjectPackage("openteams", projectCtx({ cwd: testDir, packages: ["openteams"] }));
|
|
296
|
+
expect(result.success).toBe(true);
|
|
297
|
+
expect(result.package).toBe("openteams");
|
|
298
|
+
expect(existsSync(join(testDir, ".swarm", "openteams"))).toBe(true);
|
|
299
|
+
expect(existsSync(join(testDir, ".swarm", "openteams", "config.json"))).toBe(true);
|
|
300
|
+
const config = JSON.parse(readFileSync(join(testDir, ".swarm", "openteams", "config.json"), "utf-8"));
|
|
301
|
+
expect(config).toEqual({});
|
|
302
|
+
});
|
|
303
|
+
it("creates .openteams/ with config.json (flat)", async () => {
|
|
304
|
+
const result = await initProjectPackage("openteams", projectCtx({ cwd: testDir, packages: ["openteams"], usePrefix: false }));
|
|
305
|
+
expect(result.success).toBe(true);
|
|
306
|
+
expect(existsSync(join(testDir, ".openteams"))).toBe(true);
|
|
307
|
+
expect(existsSync(join(testDir, ".openteams", "config.json"))).toBe(true);
|
|
308
|
+
});
|
|
309
|
+
it("does not overwrite existing config.json", async () => {
|
|
310
|
+
mkdirSync(join(testDir, ".swarm", "openteams"), { recursive: true });
|
|
311
|
+
writeFileSync(join(testDir, ".swarm", "openteams", "config.json"), JSON.stringify({ defaults: { include: ["gsd"] } }));
|
|
312
|
+
const result = await initProjectPackage("openteams", projectCtx({ cwd: testDir, packages: ["openteams"] }));
|
|
313
|
+
expect(result.success).toBe(true);
|
|
314
|
+
const config = JSON.parse(readFileSync(join(testDir, ".swarm", "openteams", "config.json"), "utf-8"));
|
|
315
|
+
expect(config.defaults.include).toEqual(["gsd"]);
|
|
316
|
+
});
|
|
317
|
+
it("is idempotent", async () => {
|
|
318
|
+
await initProjectPackage("openteams", projectCtx({ cwd: testDir, packages: ["openteams"] }));
|
|
319
|
+
const result = await initProjectPackage("openteams", projectCtx({ cwd: testDir, packages: ["openteams"] }));
|
|
320
|
+
expect(result.success).toBe(true);
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
// ─── Project: sessionlog (direct directory creation — no CLI) ────────────────
|
|
324
|
+
describe("initProjectPackage — sessionlog", () => {
|
|
325
|
+
it("creates .swarm/sessionlog/ with settings.json (prefixed)", async () => {
|
|
326
|
+
const result = await initProjectPackage("sessionlog", projectCtx({ cwd: testDir, packages: ["sessionlog"] }));
|
|
327
|
+
expect(result.success).toBe(true);
|
|
328
|
+
expect(result.package).toBe("sessionlog");
|
|
329
|
+
expect(existsSync(join(testDir, ".swarm", "sessionlog"))).toBe(true);
|
|
330
|
+
expect(existsSync(join(testDir, ".swarm", "sessionlog", "settings.json"))).toBe(true);
|
|
331
|
+
const settings = JSON.parse(readFileSync(join(testDir, ".swarm", "sessionlog", "settings.json"), "utf-8"));
|
|
332
|
+
expect(settings.enabled).toBe(false);
|
|
333
|
+
expect(settings.strategy).toBe("manual-commit");
|
|
334
|
+
});
|
|
335
|
+
it("creates .sessionlog/ with settings.json (flat)", async () => {
|
|
336
|
+
const result = await initProjectPackage("sessionlog", projectCtx({ cwd: testDir, packages: ["sessionlog"], usePrefix: false }));
|
|
337
|
+
expect(result.success).toBe(true);
|
|
338
|
+
expect(existsSync(join(testDir, ".sessionlog"))).toBe(true);
|
|
339
|
+
expect(existsSync(join(testDir, ".sessionlog", "settings.json"))).toBe(true);
|
|
340
|
+
});
|
|
341
|
+
it("does not overwrite existing settings.json", async () => {
|
|
342
|
+
mkdirSync(join(testDir, ".swarm", "sessionlog"), { recursive: true });
|
|
343
|
+
writeFileSync(join(testDir, ".swarm", "sessionlog", "settings.json"), JSON.stringify({ enabled: true, strategy: "auto" }));
|
|
344
|
+
const result = await initProjectPackage("sessionlog", projectCtx({ cwd: testDir, packages: ["sessionlog"] }));
|
|
345
|
+
expect(result.success).toBe(true);
|
|
346
|
+
const settings = JSON.parse(readFileSync(join(testDir, ".swarm", "sessionlog", "settings.json"), "utf-8"));
|
|
347
|
+
expect(settings.enabled).toBe(true);
|
|
348
|
+
expect(settings.strategy).toBe("auto");
|
|
349
|
+
});
|
|
350
|
+
it("is idempotent", async () => {
|
|
351
|
+
await initProjectPackage("sessionlog", projectCtx({ cwd: testDir, packages: ["sessionlog"] }));
|
|
352
|
+
const result = await initProjectPackage("sessionlog", projectCtx({ cwd: testDir, packages: ["sessionlog"] }));
|
|
353
|
+
expect(result.success).toBe(true);
|
|
354
|
+
});
|
|
355
|
+
});
|
|
356
|
+
// ─── Project: claude-code-swarm (direct directory creation — no CLI) ──────────
|
|
357
|
+
describe("initProjectPackage — claude-code-swarm", () => {
|
|
358
|
+
it("creates .swarm/claude-swarm/ with config.json and .gitignore (prefixed)", async () => {
|
|
359
|
+
const result = await initProjectPackage("claude-code-swarm", projectCtx({ cwd: testDir, packages: ["claude-code-swarm"] }));
|
|
360
|
+
expect(result.success).toBe(true);
|
|
361
|
+
expect(result.package).toBe("claude-code-swarm");
|
|
362
|
+
expect(existsSync(join(testDir, ".swarm", "claude-swarm"))).toBe(true);
|
|
363
|
+
expect(existsSync(join(testDir, ".swarm", "claude-swarm", "config.json"))).toBe(true);
|
|
364
|
+
expect(existsSync(join(testDir, ".swarm", "claude-swarm", ".gitignore"))).toBe(true);
|
|
365
|
+
const config = JSON.parse(readFileSync(join(testDir, ".swarm", "claude-swarm", "config.json"), "utf-8"));
|
|
366
|
+
expect(config).toEqual({});
|
|
367
|
+
const gitignore = readFileSync(join(testDir, ".swarm", "claude-swarm", ".gitignore"), "utf-8");
|
|
368
|
+
expect(gitignore).toBe("tmp/\n");
|
|
369
|
+
});
|
|
370
|
+
it("creates .claude-swarm/ with config.json and .gitignore (flat)", async () => {
|
|
371
|
+
const result = await initProjectPackage("claude-code-swarm", projectCtx({ cwd: testDir, packages: ["claude-code-swarm"], usePrefix: false }));
|
|
372
|
+
expect(result.success).toBe(true);
|
|
373
|
+
expect(existsSync(join(testDir, ".claude-swarm"))).toBe(true);
|
|
374
|
+
expect(existsSync(join(testDir, ".claude-swarm", "config.json"))).toBe(true);
|
|
375
|
+
expect(existsSync(join(testDir, ".claude-swarm", ".gitignore"))).toBe(true);
|
|
376
|
+
});
|
|
377
|
+
it("does not overwrite existing config.json", async () => {
|
|
378
|
+
mkdirSync(join(testDir, ".swarm", "claude-swarm"), { recursive: true });
|
|
379
|
+
writeFileSync(join(testDir, ".swarm", "claude-swarm", "config.json"), JSON.stringify({ template: "gsd" }));
|
|
380
|
+
const result = await initProjectPackage("claude-code-swarm", projectCtx({ cwd: testDir, packages: ["claude-code-swarm"] }));
|
|
381
|
+
expect(result.success).toBe(true);
|
|
382
|
+
const config = JSON.parse(readFileSync(join(testDir, ".swarm", "claude-swarm", "config.json"), "utf-8"));
|
|
383
|
+
expect(config.template).toBe("gsd");
|
|
384
|
+
});
|
|
385
|
+
it("does not overwrite existing .gitignore", async () => {
|
|
386
|
+
mkdirSync(join(testDir, ".swarm", "claude-swarm"), { recursive: true });
|
|
387
|
+
writeFileSync(join(testDir, ".swarm", "claude-swarm", ".gitignore"), "tmp/\ncustom/\n");
|
|
388
|
+
const result = await initProjectPackage("claude-code-swarm", projectCtx({ cwd: testDir, packages: ["claude-code-swarm"] }));
|
|
389
|
+
expect(result.success).toBe(true);
|
|
390
|
+
const gitignore = readFileSync(join(testDir, ".swarm", "claude-swarm", ".gitignore"), "utf-8");
|
|
391
|
+
expect(gitignore).toBe("tmp/\ncustom/\n");
|
|
392
|
+
});
|
|
393
|
+
it("is idempotent", async () => {
|
|
394
|
+
await initProjectPackage("claude-code-swarm", projectCtx({ cwd: testDir, packages: ["claude-code-swarm"] }));
|
|
395
|
+
const result = await initProjectPackage("claude-code-swarm", projectCtx({ cwd: testDir, packages: ["claude-code-swarm"] }));
|
|
396
|
+
expect(result.success).toBe(true);
|
|
397
|
+
});
|
|
398
|
+
});
|
|
299
399
|
// ─── Project: unknown ────────────────────────────────────────────────────────
|
|
300
400
|
describe("initProjectPackage — unknown", () => {
|
|
301
401
|
it("returns failure with descriptive message", async () => {
|
|
@@ -307,61 +407,6 @@ describe("initProjectPackage — unknown", () => {
|
|
|
307
407
|
});
|
|
308
408
|
});
|
|
309
409
|
});
|
|
310
|
-
// ─── Global: agent-iam (real CLI) ────────────────────────────────────────────
|
|
311
|
-
describe("initGlobalPackage — agent-iam (real CLI)", async () => {
|
|
312
|
-
const installed = await hasCliInstalled("agent-iam");
|
|
313
|
-
it.skipIf(!installed)("creates ~/.agent-credentials/ with 0700 permissions", async () => {
|
|
314
|
-
const result = await initGlobalPackage("agent-iam", globalCtx({ packages: ["agent-iam"] }));
|
|
315
|
-
expect(result.success).toBe(true);
|
|
316
|
-
expect(existsSync(join(testHome, ".agent-credentials"))).toBe(true);
|
|
317
|
-
const stats = statSync(join(testHome, ".agent-credentials"));
|
|
318
|
-
expect(stats.mode & 0o777).toBe(0o700);
|
|
319
|
-
});
|
|
320
|
-
it.skipIf(!installed)("registers openai API key via real CLI", async () => {
|
|
321
|
-
const result = await initGlobalPackage("agent-iam", globalCtx({
|
|
322
|
-
packages: ["agent-iam"],
|
|
323
|
-
apiKeys: { openai: "sk-test-integration-key" },
|
|
324
|
-
}));
|
|
325
|
-
expect(result.success).toBe(true);
|
|
326
|
-
// Verify the key was actually stored by agent-iam
|
|
327
|
-
const config = JSON.parse(readFileSync(join(testHome, ".agent-credentials", "config.json"), "utf-8"));
|
|
328
|
-
expect(config.providers.apikeys.openai.apiKey).toBe("sk-test-integration-key");
|
|
329
|
-
expect(config.providers.apikeys.openai.providerName).toBe("openai");
|
|
330
|
-
});
|
|
331
|
-
it.skipIf(!installed)("registers multiple API keys", async () => {
|
|
332
|
-
await initGlobalPackage("agent-iam", globalCtx({
|
|
333
|
-
packages: ["agent-iam"],
|
|
334
|
-
apiKeys: {
|
|
335
|
-
openai: "sk-o",
|
|
336
|
-
anthropic: "sk-a",
|
|
337
|
-
gemini: "g-1",
|
|
338
|
-
},
|
|
339
|
-
}));
|
|
340
|
-
const config = JSON.parse(readFileSync(join(testHome, ".agent-credentials", "config.json"), "utf-8"));
|
|
341
|
-
expect(config.providers.apikeys.openai).toBeDefined();
|
|
342
|
-
expect(config.providers.apikeys.anthropic).toBeDefined();
|
|
343
|
-
expect(config.providers.apikeys.gemini).toBeDefined();
|
|
344
|
-
});
|
|
345
|
-
it.skipIf(!installed)("ignores non-standard provider keys", async () => {
|
|
346
|
-
await initGlobalPackage("agent-iam", globalCtx({
|
|
347
|
-
packages: ["agent-iam"],
|
|
348
|
-
apiKeys: { custom: "xxx" },
|
|
349
|
-
}));
|
|
350
|
-
// Only credentials dir exists, no apikeys config
|
|
351
|
-
const configPath = join(testHome, ".agent-credentials", "config.json");
|
|
352
|
-
if (existsSync(configPath)) {
|
|
353
|
-
const config = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
354
|
-
expect(config.providers?.apikeys?.custom).toBeUndefined();
|
|
355
|
-
}
|
|
356
|
-
});
|
|
357
|
-
it.skipIf(!installed)("does not recreate dir if it already exists", async () => {
|
|
358
|
-
mkdirSync(join(testHome, ".agent-credentials"), { mode: 0o700 });
|
|
359
|
-
writeFileSync(join(testHome, ".agent-credentials", "token_secret"), "existing-secret");
|
|
360
|
-
await initGlobalPackage("agent-iam", globalCtx({ packages: ["agent-iam"] }));
|
|
361
|
-
// Existing file preserved
|
|
362
|
-
expect(readFileSync(join(testHome, ".agent-credentials", "token_secret"), "utf-8")).toBe("existing-secret");
|
|
363
|
-
});
|
|
364
|
-
});
|
|
365
410
|
// ─── Global: skill-tree (real CLI) ───────────────────────────────────────────
|
|
366
411
|
describe("initGlobalPackage — skill-tree (real CLI)", async () => {
|
|
367
412
|
const installed = await hasCliInstalled("skill-tree");
|
|
@@ -393,52 +438,6 @@ describe("initGlobalPackage — skill-tree (real CLI)", async () => {
|
|
|
393
438
|
expect(existsSync(join(testHome, ".skill-tree", "config.yaml"))).toBe(true);
|
|
394
439
|
});
|
|
395
440
|
});
|
|
396
|
-
// ─── Global: openswarm (direct config write — no CLI) ────────────────────────
|
|
397
|
-
describe("initGlobalPackage — openswarm (direct config write)", () => {
|
|
398
|
-
it("creates ~/.openswarm/server.json", async () => {
|
|
399
|
-
const result = await initGlobalPackage("openswarm", globalCtx({ packages: ["openswarm"] }));
|
|
400
|
-
expect(result.success).toBe(true);
|
|
401
|
-
expect(existsSync(join(testHome, ".openswarm", "server.json"))).toBe(true);
|
|
402
|
-
});
|
|
403
|
-
it("config defaults are correct", async () => {
|
|
404
|
-
await initGlobalPackage("openswarm", globalCtx({ packages: ["openswarm"] }));
|
|
405
|
-
const config = JSON.parse(readFileSync(join(testHome, ".openswarm", "server.json"), "utf-8"));
|
|
406
|
-
expect(config).toEqual({
|
|
407
|
-
host: "localhost",
|
|
408
|
-
port: 3000,
|
|
409
|
-
auth: { mode: "none" },
|
|
410
|
-
storage: { type: "memory" },
|
|
411
|
-
logging: { level: "info" },
|
|
412
|
-
});
|
|
413
|
-
});
|
|
414
|
-
it("wires macro-agent adapter when macro-agent is selected", async () => {
|
|
415
|
-
await initGlobalPackage("openswarm", globalCtx({ packages: ["openswarm", "macro-agent"] }));
|
|
416
|
-
const config = JSON.parse(readFileSync(join(testHome, ".openswarm", "server.json"), "utf-8"));
|
|
417
|
-
expect(config.adapter).toEqual({ id: "macro-agent" });
|
|
418
|
-
});
|
|
419
|
-
it("omits adapter when macro-agent not selected", async () => {
|
|
420
|
-
await initGlobalPackage("openswarm", globalCtx({ packages: ["openswarm"] }));
|
|
421
|
-
const config = JSON.parse(readFileSync(join(testHome, ".openswarm", "server.json"), "utf-8"));
|
|
422
|
-
expect(config.adapter).toBeUndefined();
|
|
423
|
-
});
|
|
424
|
-
it("skips when server.json already exists (preserves existing)", async () => {
|
|
425
|
-
mkdirSync(join(testHome, ".openswarm"), { recursive: true });
|
|
426
|
-
writeFileSync(join(testHome, ".openswarm", "server.json"), JSON.stringify({ custom: true }));
|
|
427
|
-
const result = await initGlobalPackage("openswarm", globalCtx({ packages: ["openswarm", "macro-agent"] }));
|
|
428
|
-
expect(result.success).toBe(true);
|
|
429
|
-
expect(result.message).toBe("already configured");
|
|
430
|
-
// Original config NOT overwritten
|
|
431
|
-
const config = JSON.parse(readFileSync(join(testHome, ".openswarm", "server.json"), "utf-8"));
|
|
432
|
-
expect(config.custom).toBe(true);
|
|
433
|
-
expect(config.adapter).toBeUndefined();
|
|
434
|
-
});
|
|
435
|
-
it("produces valid JSON with trailing newline", async () => {
|
|
436
|
-
await initGlobalPackage("openswarm", globalCtx({ packages: ["openswarm"] }));
|
|
437
|
-
const raw = readFileSync(join(testHome, ".openswarm", "server.json"), "utf-8");
|
|
438
|
-
expect(() => JSON.parse(raw)).not.toThrow();
|
|
439
|
-
expect(raw.endsWith("\n")).toBe(true);
|
|
440
|
-
});
|
|
441
|
-
});
|
|
442
441
|
// ─── Global: openhive ────────────────────────────────────────────────────────
|
|
443
442
|
// openhive 0.0.1 is a stub publish without a bin entry
|
|
444
443
|
describe("initGlobalPackage — openhive", async () => {
|
|
@@ -468,7 +467,7 @@ describe("initGlobalPackage — openhive", async () => {
|
|
|
468
467
|
expect(result.message).toBeDefined();
|
|
469
468
|
});
|
|
470
469
|
});
|
|
471
|
-
// ─── Global: openteams (
|
|
470
|
+
// ─── Global: openteams (no global config — project config handled separately) ─
|
|
472
471
|
describe("initGlobalPackage — openteams", () => {
|
|
473
472
|
it("returns success with no-setup message", async () => {
|
|
474
473
|
const result = await initGlobalPackage("openteams", globalCtx({ packages: ["openteams"] }));
|
|
@@ -479,7 +478,7 @@ describe("initGlobalPackage — openteams", () => {
|
|
|
479
478
|
});
|
|
480
479
|
});
|
|
481
480
|
});
|
|
482
|
-
// ─── Global: sessionlog (
|
|
481
|
+
// ─── Global: sessionlog (no global config — project config handled separately) ─
|
|
483
482
|
describe("initGlobalPackage — sessionlog", () => {
|
|
484
483
|
it("returns success with no-setup message", async () => {
|
|
485
484
|
const result = await initGlobalPackage("sessionlog", globalCtx({ packages: ["sessionlog"] }));
|
|
@@ -490,6 +489,17 @@ describe("initGlobalPackage — sessionlog", () => {
|
|
|
490
489
|
});
|
|
491
490
|
});
|
|
492
491
|
});
|
|
492
|
+
// ─── Global: claude-code-swarm (no global config) ────────────────────────────
|
|
493
|
+
describe("initGlobalPackage — claude-code-swarm", () => {
|
|
494
|
+
it("returns success with no-setup message", async () => {
|
|
495
|
+
const result = await initGlobalPackage("claude-code-swarm", globalCtx({ packages: ["claude-code-swarm"] }));
|
|
496
|
+
expect(result).toEqual({
|
|
497
|
+
package: "claude-code-swarm",
|
|
498
|
+
success: true,
|
|
499
|
+
message: "no setup required",
|
|
500
|
+
});
|
|
501
|
+
});
|
|
502
|
+
});
|
|
493
503
|
// ─── Global: unknown ─────────────────────────────────────────────────────────
|
|
494
504
|
describe("initGlobalPackage — unknown", () => {
|
|
495
505
|
it("returns failure", async () => {
|
|
@@ -506,11 +516,11 @@ describe("e2e: project init — solo bundle", async () => {
|
|
|
506
516
|
const opentasksOk = await hasCliInstalled("opentasks");
|
|
507
517
|
const minimemOk = await hasCliInstalled("minimem");
|
|
508
518
|
const allOk = opentasksOk && minimemOk;
|
|
509
|
-
it.skipIf(!allOk)("initializes opentasks → minimem
|
|
519
|
+
it.skipIf(!allOk)("initializes opentasks → minimem with cross-wiring", async () => {
|
|
510
520
|
createProject("solo-e2e");
|
|
511
521
|
const ctx = projectCtx({
|
|
512
522
|
cwd: testDir,
|
|
513
|
-
packages: ["opentasks", "minimem"
|
|
523
|
+
packages: ["opentasks", "minimem"],
|
|
514
524
|
embeddingProvider: "openai",
|
|
515
525
|
});
|
|
516
526
|
const results = [];
|
|
@@ -526,19 +536,14 @@ describe("e2e: project init — solo bundle", async () => {
|
|
|
526
536
|
expect(results.map((r) => r.package)).toEqual([
|
|
527
537
|
"opentasks",
|
|
528
538
|
"minimem",
|
|
529
|
-
"macro-agent",
|
|
530
539
|
]);
|
|
531
540
|
// opentasks: real config with project name
|
|
532
|
-
const otConfig = JSON.parse(readFileSync(join(testDir, ".opentasks", "config.json"), "utf-8"));
|
|
541
|
+
const otConfig = JSON.parse(readFileSync(join(testDir, ".swarm", "opentasks", "config.json"), "utf-8"));
|
|
533
542
|
expect(otConfig.location.name).toBe("solo-e2e");
|
|
534
543
|
// minimem: real config patched with openai
|
|
535
|
-
const mmConfig = JSON.parse(readFileSync(join(testDir, ".minimem", "config.json"), "utf-8"));
|
|
544
|
+
const mmConfig = JSON.parse(readFileSync(join(testDir, ".swarm", "minimem", "config.json"), "utf-8"));
|
|
536
545
|
expect(mmConfig.embedding.provider).toBe("openai");
|
|
537
546
|
expect(mmConfig.hybrid.enabled).toBe(true);
|
|
538
|
-
// macro-agent: config wires opentasks
|
|
539
|
-
const maConfig = JSON.parse(readFileSync(join(testDir, ".multiagent", "config.json"), "utf-8"));
|
|
540
|
-
expect(maConfig.task.backend).toBe("opentasks");
|
|
541
|
-
expect(maConfig.task.opentasks.auto_start).toBe(true);
|
|
542
547
|
});
|
|
543
548
|
it.skipIf(!allOk)("skips already-initialized packages", async () => {
|
|
544
549
|
createProject("skip-e2e");
|
|
@@ -552,7 +557,7 @@ describe("e2e: project init — solo bundle", async () => {
|
|
|
552
557
|
// Now run full init — opentasks should be skipped
|
|
553
558
|
const ctx = projectCtx({
|
|
554
559
|
cwd: testDir,
|
|
555
|
-
packages: ["opentasks", "minimem"
|
|
560
|
+
packages: ["opentasks", "minimem"],
|
|
556
561
|
});
|
|
557
562
|
const results = [];
|
|
558
563
|
for (const pkg of PROJECT_INIT_ORDER) {
|
|
@@ -565,7 +570,6 @@ describe("e2e: project init — solo bundle", async () => {
|
|
|
565
570
|
// opentasks was skipped
|
|
566
571
|
expect(results.map((r) => r.package)).toEqual([
|
|
567
572
|
"minimem",
|
|
568
|
-
"macro-agent",
|
|
569
573
|
]);
|
|
570
574
|
});
|
|
571
575
|
});
|
|
@@ -583,7 +587,6 @@ describe("e2e: project init — team bundle (init order)", async () => {
|
|
|
583
587
|
"opentasks",
|
|
584
588
|
"minimem",
|
|
585
589
|
"cognitive-core",
|
|
586
|
-
"macro-agent",
|
|
587
590
|
],
|
|
588
591
|
embeddingProvider: "gemini",
|
|
589
592
|
});
|
|
@@ -600,34 +603,24 @@ describe("e2e: project init — team bundle (init order)", async () => {
|
|
|
600
603
|
"opentasks",
|
|
601
604
|
"minimem",
|
|
602
605
|
"cognitive-core",
|
|
603
|
-
"macro-agent",
|
|
604
606
|
]);
|
|
605
607
|
// All directories exist
|
|
606
|
-
expect(existsSync(join(testDir, ".opentasks"))).toBe(true);
|
|
607
|
-
expect(existsSync(join(testDir, ".minimem"))).toBe(true);
|
|
608
|
-
expect(existsSync(join(testDir, ".cognitive-core"))).toBe(true);
|
|
609
|
-
expect(existsSync(join(testDir, ".multiagent"))).toBe(true);
|
|
608
|
+
expect(existsSync(join(testDir, ".swarm", "opentasks"))).toBe(true);
|
|
609
|
+
expect(existsSync(join(testDir, ".swarm", "minimem"))).toBe(true);
|
|
610
|
+
expect(existsSync(join(testDir, ".swarm", "cognitive-core"))).toBe(true);
|
|
610
611
|
// minimem was patched to gemini
|
|
611
|
-
const mmConfig = JSON.parse(readFileSync(join(testDir, ".minimem", "config.json"), "utf-8"));
|
|
612
|
+
const mmConfig = JSON.parse(readFileSync(join(testDir, ".swarm", "minimem", "config.json"), "utf-8"));
|
|
612
613
|
expect(mmConfig.embedding.provider).toBe("gemini");
|
|
613
614
|
});
|
|
614
615
|
});
|
|
615
|
-
// ─── E2E: full global init flow
|
|
616
|
-
describe("e2e: global init —
|
|
617
|
-
const iamOk = await hasCliInstalled("agent-iam");
|
|
616
|
+
// ─── E2E: full global init flow ──────────────────────────────────────────────
|
|
617
|
+
describe("e2e: global init — skill-tree", async () => {
|
|
618
618
|
const stOk = await hasCliInstalled("skill-tree");
|
|
619
|
-
|
|
620
|
-
it.skipIf(!allOk)("initializes agent-iam, skill-tree, and openswarm", async () => {
|
|
619
|
+
it.skipIf(!stOk)("initializes skill-tree", async () => {
|
|
621
620
|
const ctx = globalCtx({
|
|
622
|
-
packages: [
|
|
623
|
-
"agent-iam",
|
|
624
|
-
"skill-tree",
|
|
625
|
-
"openswarm",
|
|
626
|
-
"macro-agent",
|
|
627
|
-
],
|
|
628
|
-
apiKeys: { anthropic: "sk-ant-e2e" },
|
|
621
|
+
packages: ["skill-tree"],
|
|
629
622
|
});
|
|
630
|
-
const globalOrder = ["
|
|
623
|
+
const globalOrder = ["skill-tree"];
|
|
631
624
|
const results = [];
|
|
632
625
|
for (const pkg of globalOrder) {
|
|
633
626
|
if (!ctx.packages.includes(pkg))
|
|
@@ -637,32 +630,18 @@ describe("e2e: global init — platform bundle", async () => {
|
|
|
637
630
|
results.push(await initGlobalPackage(pkg, ctx));
|
|
638
631
|
}
|
|
639
632
|
expect(results.every((r) => r.success)).toBe(true);
|
|
640
|
-
expect(results.map((r) => r.package)).toEqual([
|
|
641
|
-
"agent-iam",
|
|
642
|
-
"skill-tree",
|
|
643
|
-
"openswarm",
|
|
644
|
-
]);
|
|
645
|
-
// agent-iam: credentials dir exists, anthropic key registered
|
|
646
|
-
expect(existsSync(join(testHome, ".agent-credentials"))).toBe(true);
|
|
647
|
-
const iamConfig = JSON.parse(readFileSync(join(testHome, ".agent-credentials", "config.json"), "utf-8"));
|
|
648
|
-
expect(iamConfig.providers.apikeys.anthropic.apiKey).toBe("sk-ant-e2e");
|
|
633
|
+
expect(results.map((r) => r.package)).toEqual(["skill-tree"]);
|
|
649
634
|
// skill-tree: config.yaml exists
|
|
650
635
|
expect(existsSync(join(testHome, ".skill-tree", "config.yaml"))).toBe(true);
|
|
651
|
-
// openswarm: server.json wires macro-agent adapter
|
|
652
|
-
const swarmConfig = JSON.parse(readFileSync(join(testHome, ".openswarm", "server.json"), "utf-8"));
|
|
653
|
-
expect(swarmConfig.adapter).toEqual({ id: "macro-agent" });
|
|
654
636
|
});
|
|
655
|
-
it.skipIf(!
|
|
637
|
+
it.skipIf(!stOk)("skips already-configured global packages", async () => {
|
|
656
638
|
// Pre-create skill-tree config
|
|
657
639
|
mkdirSync(join(testHome, ".skill-tree"), { recursive: true });
|
|
658
640
|
writeFileSync(join(testHome, ".skill-tree", "config.yaml"), "existing: true\n");
|
|
659
|
-
// Pre-create openswarm config
|
|
660
|
-
mkdirSync(join(testHome, ".openswarm"), { recursive: true });
|
|
661
|
-
writeFileSync(join(testHome, ".openswarm", "server.json"), JSON.stringify({ existing: true }));
|
|
662
641
|
const ctx = globalCtx({
|
|
663
|
-
packages: ["
|
|
642
|
+
packages: ["skill-tree"],
|
|
664
643
|
});
|
|
665
|
-
const globalOrder = ["
|
|
644
|
+
const globalOrder = ["skill-tree"];
|
|
666
645
|
const results = [];
|
|
667
646
|
for (const pkg of globalOrder) {
|
|
668
647
|
if (!ctx.packages.includes(pkg))
|
|
@@ -671,12 +650,142 @@ describe("e2e: global init — platform bundle", async () => {
|
|
|
671
650
|
continue;
|
|
672
651
|
results.push(await initGlobalPackage(pkg, ctx));
|
|
673
652
|
}
|
|
674
|
-
//
|
|
675
|
-
expect(results
|
|
676
|
-
// Existing
|
|
653
|
+
// Nothing was initialized (already configured)
|
|
654
|
+
expect(results).toEqual([]);
|
|
655
|
+
// Existing config preserved
|
|
677
656
|
expect(readFileSync(join(testHome, ".skill-tree", "config.yaml"), "utf-8")).toBe("existing: true\n");
|
|
678
|
-
|
|
679
|
-
|
|
657
|
+
});
|
|
658
|
+
});
|
|
659
|
+
// ─── Flow 2: plugin bootstrap → swarmkit project init ────────────────────────
|
|
660
|
+
//
|
|
661
|
+
// Simulates what claude-code-swarm's bootstrap.mjs does when it calls swarmkit
|
|
662
|
+
// programmatically: check isProjectInit for each dep, then init those missing.
|
|
663
|
+
// This is the "plugin installs first, swarmkit comes bundled" path.
|
|
664
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
665
|
+
describe("flow: plugin bootstrap — init project dirs via swarmkit", () => {
|
|
666
|
+
it("initializes openteams + claude-code-swarm in correct order", async () => {
|
|
667
|
+
// Mirrors bootstrap.mjs's initSwarmProject() + ensureSwarmDir() flow
|
|
668
|
+
const cwd = testDir;
|
|
669
|
+
// Bootstrap determines required packages from config
|
|
670
|
+
const requiredPackages = ["openteams", "claude-code-swarm"];
|
|
671
|
+
const ctx = projectCtx({
|
|
672
|
+
cwd,
|
|
673
|
+
packages: requiredPackages,
|
|
674
|
+
});
|
|
675
|
+
// Bootstrap checks which packages are already initialized
|
|
676
|
+
for (const pkg of requiredPackages) {
|
|
677
|
+
expect(isProjectInit(cwd, pkg)).toBe(false);
|
|
678
|
+
}
|
|
679
|
+
// Bootstrap initializes missing packages using PROJECT_INIT_ORDER
|
|
680
|
+
const results = [];
|
|
681
|
+
for (const pkg of PROJECT_INIT_ORDER) {
|
|
682
|
+
if (!requiredPackages.includes(pkg))
|
|
683
|
+
continue;
|
|
684
|
+
if (isProjectInit(cwd, pkg))
|
|
685
|
+
continue;
|
|
686
|
+
results.push(await initProjectPackage(pkg, ctx));
|
|
687
|
+
}
|
|
688
|
+
// All succeeded
|
|
689
|
+
expect(results.every((r) => r.success)).toBe(true);
|
|
690
|
+
expect(results.map((r) => r.package)).toEqual([
|
|
691
|
+
"openteams",
|
|
692
|
+
"claude-code-swarm",
|
|
693
|
+
]);
|
|
694
|
+
// openteams: .swarm/openteams/config.json exists
|
|
695
|
+
expect(existsSync(join(cwd, ".swarm", "openteams"))).toBe(true);
|
|
696
|
+
expect(existsSync(join(cwd, ".swarm", "openteams", "config.json"))).toBe(true);
|
|
697
|
+
// claude-code-swarm: .swarm/claude-swarm/ exists with config + .gitignore
|
|
698
|
+
expect(existsSync(join(cwd, ".swarm", "claude-swarm"))).toBe(true);
|
|
699
|
+
expect(existsSync(join(cwd, ".swarm", "claude-swarm", "config.json"))).toBe(true);
|
|
700
|
+
expect(existsSync(join(cwd, ".swarm", "claude-swarm", ".gitignore"))).toBe(true);
|
|
701
|
+
const config = JSON.parse(readFileSync(join(cwd, ".swarm", "claude-swarm", "config.json"), "utf-8"));
|
|
702
|
+
expect(config).toEqual({});
|
|
703
|
+
const gitignore = readFileSync(join(cwd, ".swarm", "claude-swarm", ".gitignore"), "utf-8");
|
|
704
|
+
expect(gitignore).toBe("tmp/\n");
|
|
705
|
+
// isProjectInit now returns true for both
|
|
706
|
+
expect(isProjectInit(cwd, "openteams")).toBe(true);
|
|
707
|
+
expect(isProjectInit(cwd, "claude-code-swarm")).toBe(true);
|
|
708
|
+
});
|
|
709
|
+
it("initializes openteams + sessionlog + claude-code-swarm when sessionlog enabled", async () => {
|
|
710
|
+
const cwd = testDir;
|
|
711
|
+
// Config has sessionlog enabled → bootstrap adds it to required
|
|
712
|
+
const requiredPackages = ["openteams", "sessionlog", "claude-code-swarm"];
|
|
713
|
+
const ctx = projectCtx({
|
|
714
|
+
cwd,
|
|
715
|
+
packages: requiredPackages,
|
|
716
|
+
});
|
|
717
|
+
const results = [];
|
|
718
|
+
for (const pkg of PROJECT_INIT_ORDER) {
|
|
719
|
+
if (!requiredPackages.includes(pkg))
|
|
720
|
+
continue;
|
|
721
|
+
if (isProjectInit(cwd, pkg))
|
|
722
|
+
continue;
|
|
723
|
+
results.push(await initProjectPackage(pkg, ctx));
|
|
724
|
+
}
|
|
725
|
+
expect(results.every((r) => r.success)).toBe(true);
|
|
726
|
+
expect(results.map((r) => r.package)).toEqual([
|
|
727
|
+
"openteams",
|
|
728
|
+
"sessionlog",
|
|
729
|
+
"claude-code-swarm",
|
|
730
|
+
]);
|
|
731
|
+
// All three dirs exist
|
|
732
|
+
expect(existsSync(join(cwd, ".swarm", "openteams"))).toBe(true);
|
|
733
|
+
expect(existsSync(join(cwd, ".swarm", "sessionlog"))).toBe(true);
|
|
734
|
+
expect(existsSync(join(cwd, ".swarm", "claude-swarm"))).toBe(true);
|
|
735
|
+
// sessionlog has default settings
|
|
736
|
+
const settings = JSON.parse(readFileSync(join(cwd, ".swarm", "sessionlog", "settings.json"), "utf-8"));
|
|
737
|
+
expect(settings.enabled).toBe(false);
|
|
738
|
+
expect(settings.strategy).toBe("manual-commit");
|
|
739
|
+
});
|
|
740
|
+
it("skips already-initialized packages on re-run (idempotent bootstrap)", async () => {
|
|
741
|
+
const cwd = testDir;
|
|
742
|
+
const requiredPackages = ["openteams", "claude-code-swarm"];
|
|
743
|
+
const ctx = projectCtx({ cwd, packages: requiredPackages });
|
|
744
|
+
// First bootstrap run
|
|
745
|
+
for (const pkg of PROJECT_INIT_ORDER) {
|
|
746
|
+
if (!requiredPackages.includes(pkg))
|
|
747
|
+
continue;
|
|
748
|
+
if (isProjectInit(cwd, pkg))
|
|
749
|
+
continue;
|
|
750
|
+
await initProjectPackage(pkg, ctx);
|
|
751
|
+
}
|
|
752
|
+
// Write custom config to .swarm/claude-swarm/config.json
|
|
753
|
+
writeFileSync(join(cwd, ".swarm", "claude-swarm", "config.json"), JSON.stringify({ template: "gsd" }));
|
|
754
|
+
// Second bootstrap run — should skip both (already initialized)
|
|
755
|
+
const results = [];
|
|
756
|
+
for (const pkg of PROJECT_INIT_ORDER) {
|
|
757
|
+
if (!requiredPackages.includes(pkg))
|
|
758
|
+
continue;
|
|
759
|
+
if (isProjectInit(cwd, pkg))
|
|
760
|
+
continue;
|
|
761
|
+
results.push(await initProjectPackage(pkg, ctx));
|
|
762
|
+
}
|
|
763
|
+
// Nothing was initialized (all already exist)
|
|
764
|
+
expect(results).toEqual([]);
|
|
765
|
+
// Custom config preserved
|
|
766
|
+
const config = JSON.parse(readFileSync(join(cwd, ".swarm", "claude-swarm", "config.json"), "utf-8"));
|
|
767
|
+
expect(config.template).toBe("gsd");
|
|
768
|
+
});
|
|
769
|
+
it("handles pre-existing openteams, only initializes claude-code-swarm", async () => {
|
|
770
|
+
const cwd = testDir;
|
|
771
|
+
// Simulate swarmkit init already ran (openteams exists)
|
|
772
|
+
mkdirSync(join(cwd, ".swarm", "openteams"), { recursive: true });
|
|
773
|
+
writeFileSync(join(cwd, ".swarm", "openteams", "config.json"), JSON.stringify({ existing: true }));
|
|
774
|
+
const requiredPackages = ["openteams", "claude-code-swarm"];
|
|
775
|
+
const ctx = projectCtx({ cwd, packages: requiredPackages });
|
|
776
|
+
const results = [];
|
|
777
|
+
for (const pkg of PROJECT_INIT_ORDER) {
|
|
778
|
+
if (!requiredPackages.includes(pkg))
|
|
779
|
+
continue;
|
|
780
|
+
if (isProjectInit(cwd, pkg))
|
|
781
|
+
continue;
|
|
782
|
+
results.push(await initProjectPackage(pkg, ctx));
|
|
783
|
+
}
|
|
784
|
+
// Only claude-code-swarm was initialized
|
|
785
|
+
expect(results.map((r) => r.package)).toEqual(["claude-code-swarm"]);
|
|
786
|
+
// openteams config preserved
|
|
787
|
+
const otConfig = JSON.parse(readFileSync(join(cwd, ".swarm", "openteams", "config.json"), "utf-8"));
|
|
788
|
+
expect(otConfig.existing).toBe(true);
|
|
680
789
|
});
|
|
681
790
|
});
|
|
682
791
|
// ─── E2E: embedding provider propagation ─────────────────────────────────────
|
|
@@ -690,28 +799,10 @@ describe("e2e: embedding provider propagation", async () => {
|
|
|
690
799
|
embeddingProvider: "openai",
|
|
691
800
|
apiKeys: { openai: "sk-embed" },
|
|
692
801
|
}));
|
|
693
|
-
const config = JSON.parse(readFileSync(join(testDir, ".minimem", "config.json"), "utf-8"));
|
|
802
|
+
const config = JSON.parse(readFileSync(join(testDir, ".swarm", "minimem", "config.json"), "utf-8"));
|
|
694
803
|
expect(config.embedding.provider).toBe("openai");
|
|
695
804
|
// Real minimem config has these fields too
|
|
696
805
|
expect(config.hybrid).toBeDefined();
|
|
697
806
|
expect(config.query).toBeDefined();
|
|
698
807
|
});
|
|
699
808
|
});
|
|
700
|
-
// ─── E2E: API key propagation to agent-iam ───────────────────────────────────
|
|
701
|
-
describe("e2e: API key propagation to agent-iam", async () => {
|
|
702
|
-
const iamOk = await hasCliInstalled("agent-iam");
|
|
703
|
-
it.skipIf(!iamOk)("all three standard keys end up in agent-iam config", async () => {
|
|
704
|
-
await initGlobalPackage("agent-iam", globalCtx({
|
|
705
|
-
packages: ["agent-iam"],
|
|
706
|
-
apiKeys: {
|
|
707
|
-
openai: "sk-openai-e2e",
|
|
708
|
-
anthropic: "sk-anthropic-e2e",
|
|
709
|
-
gemini: "gemini-e2e",
|
|
710
|
-
},
|
|
711
|
-
}));
|
|
712
|
-
const config = JSON.parse(readFileSync(join(testHome, ".agent-credentials", "config.json"), "utf-8"));
|
|
713
|
-
expect(config.providers.apikeys.openai.apiKey).toBe("sk-openai-e2e");
|
|
714
|
-
expect(config.providers.apikeys.anthropic.apiKey).toBe("sk-anthropic-e2e");
|
|
715
|
-
expect(config.providers.apikeys.gemini.apiKey).toBe("gemini-e2e");
|
|
716
|
-
});
|
|
717
|
-
});
|