agentboot 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -7
- package/agentboot.config.json +4 -1
- package/package.json +2 -2
- package/scripts/cli.ts +42 -14
- package/scripts/compile.ts +30 -7
- package/scripts/dev-sync.ts +1 -1
- package/scripts/lib/config.ts +17 -1
- package/scripts/validate.ts +12 -7
- package/.github/ISSUE_TEMPLATE/persona-request.md +0 -62
- package/.github/ISSUE_TEMPLATE/quality-feedback.md +0 -67
- package/.github/workflows/cla.yml +0 -25
- package/.github/workflows/validate.yml +0 -49
- package/.idea/agentboot.iml +0 -9
- package/.idea/misc.xml +0 -6
- package/.idea/modules.xml +0 -8
- package/.idea/vcs.xml +0 -6
- package/CLAUDE.md +0 -230
- package/CONTRIBUTING.md +0 -168
- package/PERSONAS.md +0 -156
- package/core/instructions/baseline.instructions.md +0 -133
- package/core/instructions/security.instructions.md +0 -186
- package/core/personas/code-reviewer/SKILL.md +0 -175
- package/core/personas/security-reviewer/SKILL.md +0 -233
- package/core/personas/test-data-expert/SKILL.md +0 -234
- package/core/personas/test-generator/SKILL.md +0 -262
- package/core/traits/audit-trail.md +0 -182
- package/core/traits/confidence-signaling.md +0 -172
- package/core/traits/critical-thinking.md +0 -129
- package/core/traits/schema-awareness.md +0 -132
- package/core/traits/source-citation.md +0 -174
- package/core/traits/structured-output.md +0 -199
- package/docs/ci-cd-automation.md +0 -548
- package/docs/claude-code-reference/README.md +0 -21
- package/docs/claude-code-reference/agentboot-coverage.md +0 -484
- package/docs/claude-code-reference/feature-inventory.md +0 -906
- package/docs/cli-commands-audit.md +0 -112
- package/docs/cli-design.md +0 -924
- package/docs/concepts.md +0 -1117
- package/docs/config-schema-audit.md +0 -121
- package/docs/configuration.md +0 -645
- package/docs/delivery-methods.md +0 -758
- package/docs/developer-onboarding.md +0 -342
- package/docs/extending.md +0 -448
- package/docs/getting-started.md +0 -298
- package/docs/knowledge-layer.md +0 -464
- package/docs/marketplace.md +0 -822
- package/docs/org-connection.md +0 -570
- package/docs/plans/architecture.md +0 -2429
- package/docs/plans/design.md +0 -2018
- package/docs/plans/prd.md +0 -1862
- package/docs/plans/stack-rank.md +0 -261
- package/docs/plans/technical-spec.md +0 -2755
- package/docs/privacy-and-safety.md +0 -807
- package/docs/prompt-optimization.md +0 -1071
- package/docs/test-plan.md +0 -972
- package/docs/third-party-ecosystem.md +0 -496
- package/domains/compliance-template/README.md +0 -173
- package/domains/compliance-template/traits/compliance-aware.md +0 -228
- package/examples/enterprise/agentboot.config.json +0 -184
- package/examples/minimal/agentboot.config.json +0 -46
- package/tests/REGRESSION-PLAN.md +0 -705
- package/tests/TEST-PLAN.md +0 -111
- package/tests/cli.test.ts +0 -705
- package/tests/pipeline.test.ts +0 -608
- package/tests/validate.test.ts +0 -278
- package/tsconfig.json +0 -62
package/tests/cli.test.ts
DELETED
|
@@ -1,705 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for CLI commands and AB-2 compile features.
|
|
3
|
-
*
|
|
4
|
-
* Covers: AB-18 (context:fork), AB-33 (setup), AB-34/35 (add), AB-36 (doctor),
|
|
5
|
-
* AB-37 (status), AB-38 (lint), AB-45 (uninstall), AB-52 (gotchas),
|
|
6
|
-
* AB-55 (prompt style guide), AB-77 (welcome fragment), config command.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { describe, it, expect, beforeAll, afterAll, beforeEach, afterEach } from "vitest";
|
|
10
|
-
import { execSync } from "node:child_process";
|
|
11
|
-
import { createHash } from "node:crypto";
|
|
12
|
-
import fs from "node:fs";
|
|
13
|
-
import path from "node:path";
|
|
14
|
-
import os from "node:os";
|
|
15
|
-
|
|
16
|
-
const ROOT = path.resolve(__dirname, "..");
|
|
17
|
-
const TSX = path.join(ROOT, "node_modules", ".bin", "tsx");
|
|
18
|
-
const CLI = path.join(ROOT, "scripts", "cli.ts");
|
|
19
|
-
|
|
20
|
-
function run(args: string, cwd = ROOT): string {
|
|
21
|
-
return execSync(`${TSX} ${CLI} ${args}`, {
|
|
22
|
-
cwd,
|
|
23
|
-
env: { ...process.env, NODE_NO_WARNINGS: "1", FORCE_COLOR: "0" },
|
|
24
|
-
timeout: 30_000,
|
|
25
|
-
}).toString();
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function runExpectFail(args: string, cwd = ROOT): string {
|
|
29
|
-
try {
|
|
30
|
-
run(args, cwd);
|
|
31
|
-
throw new Error("Expected command to fail but it succeeded");
|
|
32
|
-
} catch (err: any) {
|
|
33
|
-
if (err.message === "Expected command to fail but it succeeded") throw err;
|
|
34
|
-
const stdout = err.stdout?.toString() ?? "";
|
|
35
|
-
const stderr = err.stderr?.toString() ?? "";
|
|
36
|
-
return stdout + stderr || err.message;
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// ===========================================================================
|
|
41
|
-
// AB-18: Skills with context:fork
|
|
42
|
-
// ===========================================================================
|
|
43
|
-
|
|
44
|
-
describe("AB-18: skill frontmatter with context:fork", () => {
|
|
45
|
-
const skillPath = path.join(ROOT, "dist", "claude", "core", "skills", "review-code", "SKILL.md");
|
|
46
|
-
|
|
47
|
-
it("skill frontmatter contains context: fork", () => {
|
|
48
|
-
const content = fs.readFileSync(skillPath, "utf-8");
|
|
49
|
-
expect(content).toMatch(/^---\n/);
|
|
50
|
-
expect(content).toContain("context: fork");
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
it("skill frontmatter contains agent: pointing to persona name", () => {
|
|
54
|
-
const content = fs.readFileSync(skillPath, "utf-8");
|
|
55
|
-
expect(content).toContain('agent: "code-reviewer"');
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it("skill description is properly quoted in YAML", () => {
|
|
59
|
-
const content = fs.readFileSync(skillPath, "utf-8");
|
|
60
|
-
expect(content).toMatch(/description: ".*"/);
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
it("all 4 skills have context:fork and correct agent references", () => {
|
|
64
|
-
const skills = [
|
|
65
|
-
{ dir: "review-code", agent: "code-reviewer" },
|
|
66
|
-
{ dir: "review-security", agent: "security-reviewer" },
|
|
67
|
-
{ dir: "gen-tests", agent: "test-generator" },
|
|
68
|
-
{ dir: "gen-testdata", agent: "test-data-expert" },
|
|
69
|
-
];
|
|
70
|
-
for (const skill of skills) {
|
|
71
|
-
const content = fs.readFileSync(
|
|
72
|
-
path.join(ROOT, "dist", "claude", "core", "skills", skill.dir, "SKILL.md"),
|
|
73
|
-
"utf-8",
|
|
74
|
-
);
|
|
75
|
-
expect(content, `${skill.dir} should have context: fork`).toContain("context: fork");
|
|
76
|
-
expect(content, `${skill.dir} should reference agent ${skill.agent}`).toContain(
|
|
77
|
-
`agent: "${skill.agent}"`,
|
|
78
|
-
);
|
|
79
|
-
}
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
it("skill frontmatter does NOT have double frontmatter blocks", () => {
|
|
83
|
-
const content = fs.readFileSync(skillPath, "utf-8");
|
|
84
|
-
// The file should start with exactly one frontmatter block (---\n...\n---)
|
|
85
|
-
// Check that there's only one frontmatter opening at the very start
|
|
86
|
-
const frontmatterOpens = content.match(/^---\n/gm);
|
|
87
|
-
// First line must be --- and there should be exactly one more --- that closes it
|
|
88
|
-
expect(content.startsWith("---\n")).toBe(true);
|
|
89
|
-
// After stripping the frontmatter, the remaining content should NOT start with ---
|
|
90
|
-
const afterFm = content.replace(/^---\n[\s\S]*?\n---\n*/, "");
|
|
91
|
-
expect(afterFm.startsWith("---")).toBe(false);
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
it("original SKILL.md frontmatter is stripped (not duplicated)", () => {
|
|
95
|
-
const content = fs.readFileSync(skillPath, "utf-8");
|
|
96
|
-
// The source SKILL.md has "name:" and "version:" in frontmatter — these should be stripped
|
|
97
|
-
expect(content).not.toMatch(/^name:/m);
|
|
98
|
-
expect(content).not.toMatch(/^version:/m);
|
|
99
|
-
});
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
// ===========================================================================
|
|
103
|
-
// AB-77: Welcome fragment in CLAUDE.md
|
|
104
|
-
// ===========================================================================
|
|
105
|
-
|
|
106
|
-
describe("AB-77: welcome fragment in CLAUDE.md", () => {
|
|
107
|
-
const claudeMdPath = path.join(ROOT, "dist", "claude", "core", "CLAUDE.md");
|
|
108
|
-
|
|
109
|
-
it("contains Available Personas section", () => {
|
|
110
|
-
const content = fs.readFileSync(claudeMdPath, "utf-8");
|
|
111
|
-
expect(content).toContain("## Available Personas");
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
it("lists all 4 personas with invocation commands", () => {
|
|
115
|
-
const content = fs.readFileSync(claudeMdPath, "utf-8");
|
|
116
|
-
expect(content).toContain("`/review-code`");
|
|
117
|
-
expect(content).toContain("`/review-security`");
|
|
118
|
-
expect(content).toContain("`/gen-tests`");
|
|
119
|
-
expect(content).toContain("`/gen-testdata`");
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
it("includes persona descriptions", () => {
|
|
123
|
-
const content = fs.readFileSync(claudeMdPath, "utf-8");
|
|
124
|
-
expect(content).toContain("Senior code reviewer");
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
it("instruction @imports do NOT have double .md extension", () => {
|
|
128
|
-
const content = fs.readFileSync(claudeMdPath, "utf-8");
|
|
129
|
-
expect(content).not.toContain(".md.md");
|
|
130
|
-
});
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
// ===========================================================================
|
|
134
|
-
// AB-52: Gotchas compilation
|
|
135
|
-
// ===========================================================================
|
|
136
|
-
|
|
137
|
-
describe("AB-52: gotchas compilation", () => {
|
|
138
|
-
// compile.ts resolves coreDir from its own ROOT, so we test gotchas
|
|
139
|
-
// by creating a gotcha in the actual project, building, then cleaning up.
|
|
140
|
-
const gotchaPath = path.join(ROOT, "core", "gotchas", "test-gotcha.md");
|
|
141
|
-
|
|
142
|
-
afterEach(() => {
|
|
143
|
-
if (fs.existsSync(gotchaPath)) fs.unlinkSync(gotchaPath);
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
it("compiles gotcha files from core/gotchas/ to dist/claude/core/rules/", () => {
|
|
147
|
-
// Create a test gotcha in the real project
|
|
148
|
-
fs.writeFileSync(
|
|
149
|
-
gotchaPath,
|
|
150
|
-
'---\npaths:\n - "**/*.lambda.ts"\n---\n\n# Test Gotcha\n\n- Cold start penalty\n',
|
|
151
|
-
);
|
|
152
|
-
|
|
153
|
-
// Rebuild
|
|
154
|
-
run("build");
|
|
155
|
-
|
|
156
|
-
// Gotcha should appear in claude rules
|
|
157
|
-
const rulesDir = path.join(ROOT, "dist", "claude", "core", "rules");
|
|
158
|
-
expect(fs.existsSync(path.join(rulesDir, "test-gotcha.md"))).toBe(true);
|
|
159
|
-
const content = fs.readFileSync(path.join(rulesDir, "test-gotcha.md"), "utf-8");
|
|
160
|
-
expect(content).toContain("Cold start penalty");
|
|
161
|
-
|
|
162
|
-
// Gotcha should appear in skill gotchas
|
|
163
|
-
const skillGotchasDir = path.join(ROOT, "dist", "skill", "core", "gotchas");
|
|
164
|
-
expect(fs.existsSync(path.join(skillGotchasDir, "test-gotcha.md"))).toBe(true);
|
|
165
|
-
|
|
166
|
-
// README should NOT be compiled
|
|
167
|
-
expect(fs.existsSync(path.join(rulesDir, "README.md"))).toBe(false);
|
|
168
|
-
|
|
169
|
-
// Clean up dist gotcha files
|
|
170
|
-
fs.unlinkSync(path.join(rulesDir, "test-gotcha.md"));
|
|
171
|
-
if (fs.existsSync(path.join(skillGotchasDir, "test-gotcha.md"))) {
|
|
172
|
-
fs.unlinkSync(path.join(skillGotchasDir, "test-gotcha.md"));
|
|
173
|
-
}
|
|
174
|
-
});
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
// ===========================================================================
|
|
178
|
-
// AB-33: setup command
|
|
179
|
-
// ===========================================================================
|
|
180
|
-
|
|
181
|
-
describe("AB-33: setup command", () => {
|
|
182
|
-
let tempDir: string;
|
|
183
|
-
|
|
184
|
-
beforeEach(() => {
|
|
185
|
-
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "agentboot-setup-"));
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
afterEach(() => {
|
|
189
|
-
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
it("scaffolds agentboot.config.json in a fresh directory", () => {
|
|
193
|
-
run("setup --skip-detect", tempDir);
|
|
194
|
-
const configPath = path.join(tempDir, "agentboot.config.json");
|
|
195
|
-
expect(fs.existsSync(configPath)).toBe(true);
|
|
196
|
-
const config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
197
|
-
expect(config.org).toBeDefined();
|
|
198
|
-
expect(config.personas.enabled).toContain("code-reviewer");
|
|
199
|
-
expect(config.traits.enabled).toContain("critical-thinking");
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
it("scaffolds repos.json", () => {
|
|
203
|
-
run("setup --skip-detect", tempDir);
|
|
204
|
-
expect(fs.existsSync(path.join(tempDir, "repos.json"))).toBe(true);
|
|
205
|
-
const repos = JSON.parse(fs.readFileSync(path.join(tempDir, "repos.json"), "utf-8"));
|
|
206
|
-
expect(repos).toEqual([]);
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
it("creates core directory structure", () => {
|
|
210
|
-
run("setup --skip-detect", tempDir);
|
|
211
|
-
expect(fs.existsSync(path.join(tempDir, "core", "personas"))).toBe(true);
|
|
212
|
-
expect(fs.existsSync(path.join(tempDir, "core", "traits"))).toBe(true);
|
|
213
|
-
expect(fs.existsSync(path.join(tempDir, "core", "instructions"))).toBe(true);
|
|
214
|
-
expect(fs.existsSync(path.join(tempDir, "core", "gotchas"))).toBe(true);
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
it("does not overwrite existing agentboot.config.json", () => {
|
|
218
|
-
fs.writeFileSync(path.join(tempDir, "agentboot.config.json"), '{"org": "existing"}');
|
|
219
|
-
const output = run("setup", tempDir);
|
|
220
|
-
expect(output).toContain("already exists");
|
|
221
|
-
// Config should not be overwritten
|
|
222
|
-
const config = JSON.parse(fs.readFileSync(path.join(tempDir, "agentboot.config.json"), "utf-8"));
|
|
223
|
-
expect(config.org).toBe("existing");
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
it("generates valid JSON config (parseable, no syntax errors)", () => {
|
|
227
|
-
run("setup --skip-detect", tempDir);
|
|
228
|
-
const raw = fs.readFileSync(path.join(tempDir, "agentboot.config.json"), "utf-8");
|
|
229
|
-
expect(() => JSON.parse(raw)).not.toThrow();
|
|
230
|
-
});
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
// ===========================================================================
|
|
234
|
-
// AB-34/35/55: add command
|
|
235
|
-
// ===========================================================================
|
|
236
|
-
|
|
237
|
-
describe("AB-34/35/55: add command", () => {
|
|
238
|
-
let tempDir: string;
|
|
239
|
-
|
|
240
|
-
beforeEach(() => {
|
|
241
|
-
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "agentboot-add-"));
|
|
242
|
-
// Create the core directories the add command expects
|
|
243
|
-
fs.mkdirSync(path.join(tempDir, "core", "personas"), { recursive: true });
|
|
244
|
-
fs.mkdirSync(path.join(tempDir, "core", "traits"), { recursive: true });
|
|
245
|
-
fs.mkdirSync(path.join(tempDir, "core", "gotchas"), { recursive: true });
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
afterEach(() => {
|
|
249
|
-
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
250
|
-
});
|
|
251
|
-
|
|
252
|
-
it("scaffolds a persona with SKILL.md and persona.config.json", () => {
|
|
253
|
-
run("add persona my-reviewer", tempDir);
|
|
254
|
-
const personaDir = path.join(tempDir, "core", "personas", "my-reviewer");
|
|
255
|
-
expect(fs.existsSync(path.join(personaDir, "SKILL.md"))).toBe(true);
|
|
256
|
-
expect(fs.existsSync(path.join(personaDir, "persona.config.json"))).toBe(true);
|
|
257
|
-
});
|
|
258
|
-
|
|
259
|
-
it("persona SKILL.md has trait injection markers", () => {
|
|
260
|
-
run("add persona my-reviewer", tempDir);
|
|
261
|
-
const content = fs.readFileSync(
|
|
262
|
-
path.join(tempDir, "core", "personas", "my-reviewer", "SKILL.md"),
|
|
263
|
-
"utf-8",
|
|
264
|
-
);
|
|
265
|
-
expect(content).toContain("<!-- traits:start -->");
|
|
266
|
-
expect(content).toContain("<!-- traits:end -->");
|
|
267
|
-
});
|
|
268
|
-
|
|
269
|
-
it("persona SKILL.md has required frontmatter", () => {
|
|
270
|
-
run("add persona my-reviewer", tempDir);
|
|
271
|
-
const content = fs.readFileSync(
|
|
272
|
-
path.join(tempDir, "core", "personas", "my-reviewer", "SKILL.md"),
|
|
273
|
-
"utf-8",
|
|
274
|
-
);
|
|
275
|
-
expect(content).toMatch(/^---\n/);
|
|
276
|
-
expect(content).toContain("name: my-reviewer");
|
|
277
|
-
expect(content).toContain("description:");
|
|
278
|
-
});
|
|
279
|
-
|
|
280
|
-
it("persona SKILL.md has style guide sections (AB-55)", () => {
|
|
281
|
-
run("add persona my-reviewer", tempDir);
|
|
282
|
-
const content = fs.readFileSync(
|
|
283
|
-
path.join(tempDir, "core", "personas", "my-reviewer", "SKILL.md"),
|
|
284
|
-
"utf-8",
|
|
285
|
-
);
|
|
286
|
-
expect(content).toContain("## Identity");
|
|
287
|
-
expect(content).toContain("## Setup");
|
|
288
|
-
expect(content).toContain("## Rules");
|
|
289
|
-
expect(content).toContain("## Output Format");
|
|
290
|
-
expect(content).toContain("## What Not To Do");
|
|
291
|
-
// Style guide comments
|
|
292
|
-
expect(content).toContain("imperative voice");
|
|
293
|
-
expect(content).toContain("20 rules maximum");
|
|
294
|
-
});
|
|
295
|
-
|
|
296
|
-
it("persona.config.json is valid JSON with required fields", () => {
|
|
297
|
-
run("add persona my-reviewer", tempDir);
|
|
298
|
-
const config = JSON.parse(
|
|
299
|
-
fs.readFileSync(path.join(tempDir, "core", "personas", "my-reviewer", "persona.config.json"), "utf-8"),
|
|
300
|
-
);
|
|
301
|
-
expect(config.name).toBe("My Reviewer");
|
|
302
|
-
expect(config.description).toBeDefined();
|
|
303
|
-
expect(config.invocation).toBe("/my-reviewer");
|
|
304
|
-
expect(Array.isArray(config.traits)).toBe(true);
|
|
305
|
-
});
|
|
306
|
-
|
|
307
|
-
it("rejects duplicate persona names", () => {
|
|
308
|
-
run("add persona my-reviewer", tempDir);
|
|
309
|
-
const output = runExpectFail("add persona my-reviewer", tempDir);
|
|
310
|
-
expect(output).toContain("already exists");
|
|
311
|
-
});
|
|
312
|
-
|
|
313
|
-
it("rejects invalid names (uppercase, special chars, leading digit)", () => {
|
|
314
|
-
expect(() => run("add persona MyReviewer", tempDir)).toThrow();
|
|
315
|
-
expect(() => run("add persona 1bad", tempDir)).toThrow();
|
|
316
|
-
expect(() => run("add persona has_underscore", tempDir)).toThrow();
|
|
317
|
-
});
|
|
318
|
-
|
|
319
|
-
it("scaffolds a trait with correct sections", () => {
|
|
320
|
-
run("add trait error-handling", tempDir);
|
|
321
|
-
const traitPath = path.join(tempDir, "core", "traits", "error-handling.md");
|
|
322
|
-
expect(fs.existsSync(traitPath)).toBe(true);
|
|
323
|
-
const content = fs.readFileSync(traitPath, "utf-8");
|
|
324
|
-
expect(content).toContain("# Error Handling");
|
|
325
|
-
expect(content).toContain("## When to Apply");
|
|
326
|
-
expect(content).toContain("## What to Do");
|
|
327
|
-
expect(content).toContain("## What Not to Do");
|
|
328
|
-
});
|
|
329
|
-
|
|
330
|
-
it("scaffolds a gotcha with paths frontmatter", () => {
|
|
331
|
-
run("add gotcha lambda-cold-starts", tempDir);
|
|
332
|
-
const gotchaPath = path.join(tempDir, "core", "gotchas", "lambda-cold-starts.md");
|
|
333
|
-
expect(fs.existsSync(gotchaPath)).toBe(true);
|
|
334
|
-
const content = fs.readFileSync(gotchaPath, "utf-8");
|
|
335
|
-
expect(content).toMatch(/^---\n/);
|
|
336
|
-
expect(content).toContain("paths:");
|
|
337
|
-
expect(content).toContain("# Lambda Cold Starts");
|
|
338
|
-
});
|
|
339
|
-
|
|
340
|
-
it("rejects unknown type", () => {
|
|
341
|
-
const output = runExpectFail("add widget foo", tempDir);
|
|
342
|
-
expect(output).toContain("Unknown type");
|
|
343
|
-
});
|
|
344
|
-
|
|
345
|
-
it("capitalizes hyphenated names correctly", () => {
|
|
346
|
-
run("add persona multi-word-name", tempDir);
|
|
347
|
-
const config = JSON.parse(
|
|
348
|
-
fs.readFileSync(path.join(tempDir, "core", "personas", "multi-word-name", "persona.config.json"), "utf-8"),
|
|
349
|
-
);
|
|
350
|
-
expect(config.name).toBe("Multi Word Name");
|
|
351
|
-
});
|
|
352
|
-
});
|
|
353
|
-
|
|
354
|
-
// ===========================================================================
|
|
355
|
-
// AB-36: doctor command
|
|
356
|
-
// ===========================================================================
|
|
357
|
-
|
|
358
|
-
describe("AB-36: doctor command", () => {
|
|
359
|
-
it("passes all checks on the project root", () => {
|
|
360
|
-
const output = run("doctor");
|
|
361
|
-
expect(output).toContain("All checks passed");
|
|
362
|
-
});
|
|
363
|
-
|
|
364
|
-
it("detects missing config", () => {
|
|
365
|
-
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "agentboot-doc-"));
|
|
366
|
-
try {
|
|
367
|
-
const output = runExpectFail(`doctor --config ${path.join(tempDir, "nonexistent.json")}`, tempDir);
|
|
368
|
-
expect(output).toContain("not found");
|
|
369
|
-
} finally {
|
|
370
|
-
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
371
|
-
}
|
|
372
|
-
});
|
|
373
|
-
|
|
374
|
-
it("reports exit code 0 on success", () => {
|
|
375
|
-
// Should not throw
|
|
376
|
-
run("doctor");
|
|
377
|
-
});
|
|
378
|
-
});
|
|
379
|
-
|
|
380
|
-
// ===========================================================================
|
|
381
|
-
// AB-37: status command
|
|
382
|
-
// ===========================================================================
|
|
383
|
-
|
|
384
|
-
describe("AB-37: status command", () => {
|
|
385
|
-
it("shows org and persona info", () => {
|
|
386
|
-
const output = run("status");
|
|
387
|
-
expect(output).toContain("Your Organization");
|
|
388
|
-
expect(output).toContain("code-reviewer");
|
|
389
|
-
expect(output).toContain("4 enabled");
|
|
390
|
-
});
|
|
391
|
-
|
|
392
|
-
it("--format json produces valid JSON", () => {
|
|
393
|
-
const output = run("status --format json");
|
|
394
|
-
const status = JSON.parse(output);
|
|
395
|
-
expect(status.org).toBe("your-org");
|
|
396
|
-
expect(status.personas).toContain("code-reviewer");
|
|
397
|
-
expect(Array.isArray(status.repos)).toBe(true);
|
|
398
|
-
});
|
|
399
|
-
|
|
400
|
-
it("shows 'No repos' when repos.json is empty", () => {
|
|
401
|
-
const output = run("status");
|
|
402
|
-
expect(output).toContain("No repos");
|
|
403
|
-
});
|
|
404
|
-
});
|
|
405
|
-
|
|
406
|
-
// ===========================================================================
|
|
407
|
-
// AB-38: lint command
|
|
408
|
-
// ===========================================================================
|
|
409
|
-
|
|
410
|
-
describe("AB-38: lint command", () => {
|
|
411
|
-
it("reports trait-too-long for current traits", () => {
|
|
412
|
-
const output = run("lint");
|
|
413
|
-
expect(output).toContain("trait-too-long");
|
|
414
|
-
});
|
|
415
|
-
|
|
416
|
-
it("--severity error hides warnings", () => {
|
|
417
|
-
const output = run("lint --severity error");
|
|
418
|
-
// Should only show errors or pass with no issues
|
|
419
|
-
expect(output).not.toContain("WARN");
|
|
420
|
-
});
|
|
421
|
-
|
|
422
|
-
it("--severity info shows warnings (info threshold includes all)", () => {
|
|
423
|
-
const output = run("lint --severity info");
|
|
424
|
-
// With info threshold, warnings are still shown
|
|
425
|
-
expect(output).toContain("WARN");
|
|
426
|
-
});
|
|
427
|
-
|
|
428
|
-
it("--format json produces valid JSON output", () => {
|
|
429
|
-
const output = run("lint --format json --severity info");
|
|
430
|
-
const findings = JSON.parse(output);
|
|
431
|
-
expect(Array.isArray(findings)).toBe(true);
|
|
432
|
-
for (const f of findings) {
|
|
433
|
-
expect(f.rule).toBeDefined();
|
|
434
|
-
expect(f.severity).toMatch(/^(info|warn|error)$/);
|
|
435
|
-
expect(f.file).toBeDefined();
|
|
436
|
-
expect(f.message).toBeDefined();
|
|
437
|
-
}
|
|
438
|
-
});
|
|
439
|
-
|
|
440
|
-
it("--persona filters to specific persona", () => {
|
|
441
|
-
const output = run("lint --format json --severity info --persona code-reviewer");
|
|
442
|
-
const findings = JSON.parse(output);
|
|
443
|
-
// All findings should be for code-reviewer persona or general (traits)
|
|
444
|
-
for (const f of findings) {
|
|
445
|
-
const isPersonaFile = f.file.includes("code-reviewer");
|
|
446
|
-
const isTraitFile = f.file.startsWith("core/traits/");
|
|
447
|
-
expect(isPersonaFile || isTraitFile, `Unexpected file in filtered output: ${f.file}`).toBe(true);
|
|
448
|
-
}
|
|
449
|
-
});
|
|
450
|
-
|
|
451
|
-
it("detects vague language in a test persona", () => {
|
|
452
|
-
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "agentboot-lint-"));
|
|
453
|
-
const personaDir = path.join(tempDir, "core", "personas", "vague-persona");
|
|
454
|
-
fs.mkdirSync(personaDir, { recursive: true });
|
|
455
|
-
fs.writeFileSync(
|
|
456
|
-
path.join(personaDir, "SKILL.md"),
|
|
457
|
-
'---\nname: vague\ndescription: test\n---\n\nBe thorough when reviewing.\nTry to check if possible.\n',
|
|
458
|
-
);
|
|
459
|
-
fs.writeFileSync(
|
|
460
|
-
path.join(tempDir, "agentboot.config.json"),
|
|
461
|
-
JSON.stringify({
|
|
462
|
-
org: "test",
|
|
463
|
-
personas: { enabled: ["vague-persona"] },
|
|
464
|
-
traits: { enabled: [] },
|
|
465
|
-
}),
|
|
466
|
-
);
|
|
467
|
-
|
|
468
|
-
try {
|
|
469
|
-
const output = run(`lint --config ${path.join(tempDir, "agentboot.config.json")} --format json --severity warn`, tempDir);
|
|
470
|
-
const findings = JSON.parse(output);
|
|
471
|
-
const vagueFindings = findings.filter((f: any) => f.rule === "vague-instruction");
|
|
472
|
-
expect(vagueFindings.length).toBeGreaterThanOrEqual(2);
|
|
473
|
-
} finally {
|
|
474
|
-
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
475
|
-
}
|
|
476
|
-
});
|
|
477
|
-
|
|
478
|
-
it("detects secrets in persona files", () => {
|
|
479
|
-
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "agentboot-lint-secret-"));
|
|
480
|
-
const personaDir = path.join(tempDir, "core", "personas", "secret-persona");
|
|
481
|
-
fs.mkdirSync(personaDir, { recursive: true });
|
|
482
|
-
fs.writeFileSync(
|
|
483
|
-
path.join(personaDir, "SKILL.md"),
|
|
484
|
-
'---\nname: secret\ndescription: test\n---\n\nUse api key sk-abcdefghijklmnopqrstuvwxyz1234 for testing.\n',
|
|
485
|
-
);
|
|
486
|
-
fs.writeFileSync(
|
|
487
|
-
path.join(tempDir, "agentboot.config.json"),
|
|
488
|
-
JSON.stringify({
|
|
489
|
-
org: "test",
|
|
490
|
-
personas: { enabled: ["secret-persona"] },
|
|
491
|
-
traits: { enabled: [] },
|
|
492
|
-
}),
|
|
493
|
-
);
|
|
494
|
-
|
|
495
|
-
try {
|
|
496
|
-
const output = runExpectFail(
|
|
497
|
-
`lint --config ${path.join(tempDir, "agentboot.config.json")} --format json`,
|
|
498
|
-
tempDir,
|
|
499
|
-
);
|
|
500
|
-
const findings = JSON.parse(output);
|
|
501
|
-
expect(findings.some((f: any) => f.rule === "credential-in-prompt")).toBe(true);
|
|
502
|
-
} finally {
|
|
503
|
-
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
504
|
-
}
|
|
505
|
-
});
|
|
506
|
-
});
|
|
507
|
-
|
|
508
|
-
// ===========================================================================
|
|
509
|
-
// AB-45: uninstall command
|
|
510
|
-
// ===========================================================================
|
|
511
|
-
|
|
512
|
-
describe("AB-45: uninstall command", () => {
|
|
513
|
-
let syncTarget: string;
|
|
514
|
-
let originalRepos: string;
|
|
515
|
-
|
|
516
|
-
beforeAll(() => {
|
|
517
|
-
// Build + sync to a temp target to have something to uninstall
|
|
518
|
-
originalRepos = fs.readFileSync(path.join(ROOT, "repos.json"), "utf-8");
|
|
519
|
-
syncTarget = fs.mkdtempSync(path.join(os.tmpdir(), "agentboot-uninstall-"));
|
|
520
|
-
fs.writeFileSync(
|
|
521
|
-
path.join(ROOT, "repos.json"),
|
|
522
|
-
JSON.stringify([{ path: syncTarget, label: "uninstall-test", platform: "claude" }]),
|
|
523
|
-
);
|
|
524
|
-
run("sync");
|
|
525
|
-
});
|
|
526
|
-
|
|
527
|
-
afterAll(() => {
|
|
528
|
-
fs.writeFileSync(path.join(ROOT, "repos.json"), originalRepos);
|
|
529
|
-
if (syncTarget) fs.rmSync(syncTarget, { recursive: true, force: true });
|
|
530
|
-
});
|
|
531
|
-
|
|
532
|
-
it("dry-run lists files without removing them", () => {
|
|
533
|
-
const output = run(`uninstall --repo ${syncTarget} --dry-run`);
|
|
534
|
-
expect(output).toContain("would remove");
|
|
535
|
-
// Files should still exist
|
|
536
|
-
expect(fs.existsSync(path.join(syncTarget, ".claude", "skills", "review-code", "SKILL.md"))).toBe(true);
|
|
537
|
-
});
|
|
538
|
-
|
|
539
|
-
it("uninstall removes synced files with matching hashes", () => {
|
|
540
|
-
// Verify manifest exists before uninstall
|
|
541
|
-
const manifestPath = path.join(syncTarget, ".claude", ".agentboot-manifest.json");
|
|
542
|
-
expect(fs.existsSync(manifestPath)).toBe(true);
|
|
543
|
-
|
|
544
|
-
const output = run(`uninstall --repo ${syncTarget}`);
|
|
545
|
-
expect(output).toContain("removed");
|
|
546
|
-
|
|
547
|
-
// Skills should be gone
|
|
548
|
-
expect(fs.existsSync(path.join(syncTarget, ".claude", "skills", "review-code", "SKILL.md"))).toBe(false);
|
|
549
|
-
// Manifest should be gone
|
|
550
|
-
expect(fs.existsSync(manifestPath)).toBe(false);
|
|
551
|
-
});
|
|
552
|
-
|
|
553
|
-
it("skips modified files (hash mismatch)", () => {
|
|
554
|
-
// Re-sync to have fresh files
|
|
555
|
-
fs.writeFileSync(
|
|
556
|
-
path.join(ROOT, "repos.json"),
|
|
557
|
-
JSON.stringify([{ path: syncTarget, label: "uninstall-test", platform: "claude" }]),
|
|
558
|
-
);
|
|
559
|
-
run("sync");
|
|
560
|
-
|
|
561
|
-
// Modify one file
|
|
562
|
-
const skillPath = path.join(syncTarget, ".claude", "skills", "review-code", "SKILL.md");
|
|
563
|
-
fs.appendFileSync(skillPath, "\n<!-- manually modified -->\n");
|
|
564
|
-
|
|
565
|
-
const output = run(`uninstall --repo ${syncTarget}`);
|
|
566
|
-
expect(output).toContain("modified");
|
|
567
|
-
// Modified file should still exist
|
|
568
|
-
expect(fs.existsSync(skillPath)).toBe(true);
|
|
569
|
-
});
|
|
570
|
-
|
|
571
|
-
it("rejects path traversal in manifest entries", () => {
|
|
572
|
-
// Create a malicious manifest
|
|
573
|
-
const maliciousTarget = fs.mkdtempSync(path.join(os.tmpdir(), "agentboot-traversal-"));
|
|
574
|
-
const claudeDir = path.join(maliciousTarget, ".claude");
|
|
575
|
-
fs.mkdirSync(claudeDir, { recursive: true });
|
|
576
|
-
|
|
577
|
-
// Create a canary file outside .claude/ that should NOT be deleted
|
|
578
|
-
const canaryPath = path.join(maliciousTarget, "important-file.txt");
|
|
579
|
-
fs.writeFileSync(canaryPath, "do not delete");
|
|
580
|
-
|
|
581
|
-
// Write manifest with traversal attempt
|
|
582
|
-
const canaryContent = fs.readFileSync(canaryPath);
|
|
583
|
-
const canaryHash = createHash("sha256").update(canaryContent).digest("hex");
|
|
584
|
-
fs.writeFileSync(
|
|
585
|
-
path.join(claudeDir, ".agentboot-manifest.json"),
|
|
586
|
-
JSON.stringify({
|
|
587
|
-
managed_by: "agentboot",
|
|
588
|
-
version: "0.1.0",
|
|
589
|
-
synced_at: new Date().toISOString(),
|
|
590
|
-
files: [{ path: "../important-file.txt", hash: canaryHash }],
|
|
591
|
-
}),
|
|
592
|
-
);
|
|
593
|
-
|
|
594
|
-
try {
|
|
595
|
-
const output = run(`uninstall --repo ${maliciousTarget}`);
|
|
596
|
-
expect(output).toContain("rejected");
|
|
597
|
-
// Canary file should still exist
|
|
598
|
-
expect(fs.existsSync(canaryPath)).toBe(true);
|
|
599
|
-
} finally {
|
|
600
|
-
fs.rmSync(maliciousTarget, { recursive: true, force: true });
|
|
601
|
-
}
|
|
602
|
-
});
|
|
603
|
-
|
|
604
|
-
it("handles no manifest gracefully", () => {
|
|
605
|
-
const emptyTarget = fs.mkdtempSync(path.join(os.tmpdir(), "agentboot-empty-"));
|
|
606
|
-
try {
|
|
607
|
-
const output = run(`uninstall --repo ${emptyTarget}`);
|
|
608
|
-
expect(output).toContain("No .agentboot-manifest.json found");
|
|
609
|
-
} finally {
|
|
610
|
-
fs.rmSync(emptyTarget, { recursive: true, force: true });
|
|
611
|
-
}
|
|
612
|
-
});
|
|
613
|
-
});
|
|
614
|
-
|
|
615
|
-
// ===========================================================================
|
|
616
|
-
// Config command
|
|
617
|
-
// ===========================================================================
|
|
618
|
-
|
|
619
|
-
describe("config command", () => {
|
|
620
|
-
it("reads a top-level key", () => {
|
|
621
|
-
const output = run("config org");
|
|
622
|
-
expect(output.trim()).toBe("your-org");
|
|
623
|
-
});
|
|
624
|
-
|
|
625
|
-
it("reads a nested key with dot notation", () => {
|
|
626
|
-
const output = run("config personas.enabled");
|
|
627
|
-
const parsed = JSON.parse(output);
|
|
628
|
-
expect(parsed).toContain("code-reviewer");
|
|
629
|
-
});
|
|
630
|
-
|
|
631
|
-
it("errors on nonexistent key", () => {
|
|
632
|
-
const output = runExpectFail("config nonexistent.key");
|
|
633
|
-
expect(output).toContain("Key not found");
|
|
634
|
-
});
|
|
635
|
-
|
|
636
|
-
it("config mutation exits non-zero", () => {
|
|
637
|
-
expect(() => run("config org newvalue")).toThrow();
|
|
638
|
-
});
|
|
639
|
-
});
|
|
640
|
-
|
|
641
|
-
// ===========================================================================
|
|
642
|
-
// YAML frontmatter safety
|
|
643
|
-
// ===========================================================================
|
|
644
|
-
|
|
645
|
-
describe("YAML frontmatter safety", () => {
|
|
646
|
-
it("all compiled skill descriptions are quoted in YAML frontmatter", () => {
|
|
647
|
-
// Verify existing compiled output has quoted descriptions
|
|
648
|
-
const skills = ["review-code", "review-security", "gen-tests", "gen-testdata"];
|
|
649
|
-
for (const skill of skills) {
|
|
650
|
-
const content = fs.readFileSync(
|
|
651
|
-
path.join(ROOT, "dist", "claude", "core", "skills", skill, "SKILL.md"),
|
|
652
|
-
"utf-8",
|
|
653
|
-
);
|
|
654
|
-
expect(content, `${skill} description should be quoted`).toMatch(/description: ".*"/);
|
|
655
|
-
expect(content, `${skill} agent should be quoted`).toMatch(/agent: ".*"/);
|
|
656
|
-
}
|
|
657
|
-
});
|
|
658
|
-
|
|
659
|
-
it("all compiled agent names and descriptions are quoted", () => {
|
|
660
|
-
const agents = ["code-reviewer", "security-reviewer", "test-generator", "test-data-expert"];
|
|
661
|
-
for (const agent of agents) {
|
|
662
|
-
const content = fs.readFileSync(
|
|
663
|
-
path.join(ROOT, "dist", "claude", "core", "agents", `${agent}.md`),
|
|
664
|
-
"utf-8",
|
|
665
|
-
);
|
|
666
|
-
expect(content, `${agent} name should be quoted`).toMatch(/name: ".*"/);
|
|
667
|
-
expect(content, `${agent} description should be quoted`).toMatch(/description: ".*"/);
|
|
668
|
-
}
|
|
669
|
-
});
|
|
670
|
-
|
|
671
|
-
it("description containing special YAML characters is safely escaped", () => {
|
|
672
|
-
// The security-reviewer description contains an em-dash (—) which is safe but worth verifying
|
|
673
|
-
const content = fs.readFileSync(
|
|
674
|
-
path.join(ROOT, "dist", "claude", "core", "skills", "review-security", "SKILL.md"),
|
|
675
|
-
"utf-8",
|
|
676
|
-
);
|
|
677
|
-
// Description should be quoted and contain the full text
|
|
678
|
-
expect(content).toContain('description: "Adversarial security reviewer');
|
|
679
|
-
});
|
|
680
|
-
});
|
|
681
|
-
|
|
682
|
-
// ===========================================================================
|
|
683
|
-
// CLI global behavior
|
|
684
|
-
// ===========================================================================
|
|
685
|
-
|
|
686
|
-
describe("CLI global behavior", () => {
|
|
687
|
-
it("--version outputs the version from package.json", () => {
|
|
688
|
-
const output = run("--version");
|
|
689
|
-
expect(output.trim()).toMatch(/^\d+\.\d+\.\d+$/);
|
|
690
|
-
});
|
|
691
|
-
|
|
692
|
-
it("--help lists all commands", () => {
|
|
693
|
-
const output = run("--help");
|
|
694
|
-
expect(output).toContain("build");
|
|
695
|
-
expect(output).toContain("validate");
|
|
696
|
-
expect(output).toContain("sync");
|
|
697
|
-
expect(output).toContain("setup");
|
|
698
|
-
expect(output).toContain("add");
|
|
699
|
-
expect(output).toContain("doctor");
|
|
700
|
-
expect(output).toContain("status");
|
|
701
|
-
expect(output).toContain("lint");
|
|
702
|
-
expect(output).toContain("uninstall");
|
|
703
|
-
expect(output).toContain("config");
|
|
704
|
-
});
|
|
705
|
-
});
|