Files
przeprogramowani__10x-cli/tests/bench-kit-command.test.ts
psmyrdek 9d957e3a10 feat(bench-kit): shallow base repo clone by default, --deep for history
The bootstrap decides the depth (`baseRepoClone.depth`: 1 = HEAD only,
null = full history); this side only executes it. `--deep` on
`bench-kit init` sends `deepClone: true` in the request.

`--depth` is silently ignored for a local path, so a shallow clone goes
through a `file://` URL: that gives up object hardlinking, which is the
point — one commit's objects instead of the whole history. Origin is
re-pointed at the registered remote as before, so `git fetch
--unshallow` in the clone adds the history later.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-18 09:56:19 +02:00

678 lines
25 KiB
TypeScript

/**
* 10x bench-kit — command-level behavior, narrowed to the bootstrap
* contract. Disk effects of init/update (zones, manifest, placeholders)
* are the KIT's tests (.github/tests/ in 10x-bench-kit); here we assert
* on the request the CLI builds, the rendering of the response, --json
* envelopes and exit codes.
*
* Uses dependency injection (BenchKitDeps) instead of module mocking:
* cloneTemplate materializes a minimal fixture tree (only what the CLI
* itself inspects: .bench-kit/VERSION and the bootstrap entry),
* runBootstrap is a fake returning canned contract responses.
*/
import { afterEach, describe, expect, it, spyOn } from "bun:test";
import * as childProcess from "node:child_process";
import {
cpSync,
existsSync,
mkdirSync,
mkdtempSync,
rmSync,
writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import cac from "cac";
import {
type BenchKitDeps,
type BootstrapRequest,
type BootstrapResponse,
CONTRACT_VERSION,
registerBenchKitCommand,
runBenchKitInit,
runBenchKitUpdate,
} from "../src/commands/bench-kit";
import type { OutputContext } from "../src/lib/output";
interface CaptureResult {
stdout: string;
stderr: string;
exitCode?: number;
}
async function captureStreams(fn: () => Promise<unknown>): Promise<CaptureResult> {
const realExit = process.exit;
const realStdoutWrite = process.stdout.write.bind(process.stdout);
const realStderrWrite = process.stderr.write.bind(process.stderr);
let stdout = "";
let stderr = "";
process.stdout.write = ((chunk: string | Uint8Array) => {
stdout += typeof chunk === "string" ? chunk : Buffer.from(chunk).toString();
return true;
}) as typeof process.stdout.write;
process.stderr.write = ((chunk: string | Uint8Array) => {
stderr += typeof chunk === "string" ? chunk : Buffer.from(chunk).toString();
return true;
}) as typeof process.stderr.write;
process.exit = ((code?: number) => {
throw Object.assign(new Error("__exit__"), { __exitCode: code });
}) as typeof process.exit;
// Restore happens in `finally` BEFORE this function's promise resolves —
// a `.then(resolve).finally(restore)` chain would let the next (nested or
// sequential) capture patch the streams first and then get clobbered by
// this capture's late restore, leaking output to the real stdout.
try {
await fn();
return { stdout, stderr };
} catch (err: unknown) {
if (err && typeof err === "object" && "__exitCode" in err) {
return { stdout, stderr, exitCode: (err as { __exitCode: number }).__exitCode };
}
return {
stdout,
stderr: `${stderr}\n[uncaught: ${err instanceof Error ? err.message : String(err)}]`,
};
} finally {
process.stdout.write = realStdoutWrite;
process.stderr.write = realStderrWrite;
process.exit = realExit;
}
}
const JSON_CTX: OutputContext = { json: true, verbose: false };
const tempDirs: string[] = [];
function tempDir(prefix: string): string {
const dir = mkdtempSync(join(tmpdir(), prefix));
tempDirs.push(dir);
return dir;
}
afterEach(() => {
while (tempDirs.length > 0) {
rmSync(tempDirs.pop()!, { recursive: true, force: true });
}
});
/**
* Minimal template fixture: only what the CLI itself looks at before
* handing off — the VERSION marker and the bootstrap entry.
*/
function buildTemplateFixture(version = "0.10.0", withBootstrap = true): string {
const dir = tempDir("bench-kit-template-");
mkdirSync(join(dir, ".bench-kit", "bootstrap"), { recursive: true });
writeFileSync(join(dir, ".bench-kit", "VERSION"), `${version}\n`);
if (withBootstrap) {
writeFileSync(join(dir, ".bench-kit", "bootstrap", "index.mjs"), "// bootstrap\n");
}
return dir;
}
function okInitResponse(overrides: Partial<BootstrapResponse> = {}): BootstrapResponse {
return {
ok: true,
mode: "init",
templateVersion: "0.10.0",
tool: "claude-code",
skillRoot: ".claude/skills",
filesCopied: 42,
baseRepo: null,
demoTasksPinned: 0,
baseRepoClone: null,
runnerDeps: "installed",
gitInitialized: true,
committed: true,
warnings: [],
...overrides,
};
}
interface FakeDepsResult {
deps: BenchKitDeps;
bootstrapCalls: { entry: string; request: BootstrapRequest }[];
}
function fakeDeps(
templateDir: string,
bootstrapResponse: BootstrapResponse,
overrides: Partial<BenchKitDeps> = {},
): FakeDepsResult {
const bootstrapCalls: { entry: string; request: BootstrapRequest }[] = [];
const deps: BenchKitDeps = {
toolAvailable: () => Promise.resolve(true),
cloneTemplate: (_ref, destDir) => {
cpSync(templateDir, destDir, { recursive: true });
return Promise.resolve({ ok: true, error: "" });
},
runGit: () => Promise.resolve({ ok: true, stdout: "", error: "" }),
detectBaseRepo: () => Promise.resolve(null),
cloneBaseRepo: (_repo, destDir) => {
mkdirSync(destDir, { recursive: true });
return Promise.resolve({ ok: true, error: "" });
},
remoteReachable: () => Promise.resolve(false),
detectToolSignals: () => [],
chooseTool: () => Promise.resolve(null),
runBootstrap: (entry, request) => {
bootstrapCalls.push({ entry, request });
return Promise.resolve(bootstrapResponse);
},
now: () => new Date("2026-08-17T12:00:00.000Z"),
...overrides,
};
return { deps, bootstrapCalls };
}
function parseEnvelope(stdout: string): { status: string; data?: any; error?: any } {
return JSON.parse(stdout.trim());
}
describe("10x bench-kit init", () => {
it("builds a contract request and renders the bootstrap's report", async () => {
const template = buildTemplateFixture();
const target = join(tempDir("bench-kit-target-"), "instance");
const { deps, bootstrapCalls } = fakeDeps(template, okInitResponse());
const result = await captureStreams(() => runBenchKitInit(JSON_CTX, target, {}, deps));
expect(result.exitCode).toBeUndefined();
expect(bootstrapCalls.length).toBe(1);
const { entry, request } = bootstrapCalls[0]!;
// The bootstrap runs FROM the scratch clone, not from the instance.
expect(entry.endsWith(join(".bench-kit", "bootstrap", "index.mjs"))).toBe(true);
expect(entry.startsWith(target)).toBe(false);
expect(request.contractVersion).toBe(CONTRACT_VERSION);
expect(request.mode).toBe("init");
expect(request.targetDir).toBe(target);
expect(request.templateRef).toBe("latest");
expect(request.tool).toEqual({ id: "claude-code", skillRoot: ".claude/skills" });
// Machine knowledge travels with the request: the full profile map…
expect(request.toolProfiles["claude-code"]).toBe(".claude/skills");
expect(Object.keys(request.toolProfiles).length).toBeGreaterThan(1);
// …and a deterministic timestamp.
expect(request.now).toBe("2026-08-17T12:00:00.000Z");
expect(request.detectedBaseRepo).toBeNull();
const envelope = parseEnvelope(result.stdout);
expect(envelope.status).toBe("ok");
expect(envelope.data.mode).toBe("init");
expect(envelope.data.templateVersion).toBe("0.10.0");
expect(envelope.data.tool).toBe("claude-code");
expect(envelope.data.committed).toBe(true);
});
it("passes --tool through as explicit and rejects unknown tools", async () => {
const template = buildTemplateFixture();
const target = join(tempDir("bench-kit-target-"), "instance");
const { deps, bootstrapCalls } = fakeDeps(
template,
okInitResponse({ tool: "codex", skillRoot: ".agents/skills" }),
);
const result = await captureStreams(() =>
runBenchKitInit(JSON_CTX, target, { tool: "codex" }, deps),
);
expect(result.exitCode).toBeUndefined();
expect(bootstrapCalls[0]!.request.tool).toEqual({
id: "codex",
skillRoot: ".agents/skills",
explicit: true,
});
expect(parseEnvelope(result.stdout).data.skillRoot).toBe(".agents/skills");
const legacyAliasTarget = join(tempDir("bench-kit-target-"), "legacy-alias");
await captureStreams(() =>
runBenchKitInit(JSON_CTX, legacyAliasTarget, { tool: "windsurf" }, deps),
);
expect(bootstrapCalls[1]!.request.tool).toEqual({
id: "devin-desktop",
skillRoot: ".devin/skills",
explicit: true,
});
const rejected = await captureStreams(() =>
runBenchKitInit(JSON_CTX, target, { tool: "vim" }, deps),
);
expect(rejected.exitCode).toBe(2);
expect(parseEnvelope(rejected.stdout).error.code).toBe("unknown_tool");
});
it("defaults the tool from marker detection when non-interactive (not explicit)", async () => {
const template = buildTemplateFixture();
const target = join(tempDir("bench-kit-target-"), "instance");
const { deps, bootstrapCalls } = fakeDeps(template, okInitResponse(), {
detectToolSignals: () => [
{ profileId: "cursor", confidence: "strong", reason: ".cursor/rules/" },
],
});
const result = await captureStreams(() =>
runBenchKitInit(JSON_CTX, target, { yes: true }, deps),
);
expect(result.exitCode).toBeUndefined();
expect(bootstrapCalls[0]!.request.tool).toEqual({
id: "cursor",
skillRoot: ".cursor/skills",
});
});
it("detects the product repo, probes https reachability, and forwards both", async () => {
const template = buildTemplateFixture();
const target = join(tempDir("bench-kit-target-"), "instance");
const probed: string[] = [];
const { deps, bootstrapCalls } = fakeDeps(template, okInitResponse(), {
detectBaseRepo: () =>
Promise.resolve({
rootDir: "/somewhere/shop-app",
name: "shop-app",
url: "git@github.com:acme/shop-app.git",
headCommit: "a".repeat(40),
}),
remoteReachable: (url) => {
probed.push(url);
return Promise.resolve(true);
},
});
const result = await captureStreams(() => runBenchKitInit(JSON_CTX, target, {}, deps));
expect(result.exitCode).toBeUndefined();
// The CLI probes the https twin of the SSH remote; the kit decides
// which URL lands in the config.
expect(probed).toEqual(["https://github.com/acme/shop-app.git"]);
expect(bootstrapCalls[0]!.request.detectedBaseRepo).toEqual({
rootDir: "/somewhere/shop-app",
name: "shop-app",
url: "git@github.com:acme/shop-app.git",
headCommit: "a".repeat(40),
httpsReachable: true,
});
});
it("executes the bootstrap's baseRepoClone instruction and reports the outcome", async () => {
const template = buildTemplateFixture();
const target = join(tempDir("bench-kit-target-"), "instance");
const instruction = {
name: "shop-app",
url: "https://github.com/acme/shop-app.git",
rootDir: "/somewhere/shop-app",
dest: ".repos/shop-app",
depth: 1,
};
const cloneCalls: { rootDir: string; depth?: number | null; destDir: string }[] = [];
const { deps } = fakeDeps(
template,
okInitResponse({
baseRepo: { name: "shop-app", url: instruction.url },
baseRepoClone: instruction,
}),
{
cloneBaseRepo: (repo, destDir) => {
cloneCalls.push({ rootDir: repo.rootDir, depth: repo.depth, destDir });
mkdirSync(destDir, { recursive: true });
return Promise.resolve({ ok: true, error: "" });
},
},
);
const result = await captureStreams(() => runBenchKitInit(JSON_CTX, target, {}, deps));
expect(result.exitCode).toBeUndefined();
// The depth decision is the kit's; the CLI only carries it through.
expect(cloneCalls).toEqual([
{ rootDir: "/somewhere/shop-app", depth: 1, destDir: join(target, ".repos", "shop-app") },
]);
const envelope = parseEnvelope(result.stdout);
expect(envelope.data.baseRepoClone).toBe("cloned");
expect(envelope.data.baseRepo.name).toBe("shop-app");
});
it("asks the bootstrap for a full-history clone only with --deep", async () => {
const template = buildTemplateFixture();
const shallowFake = fakeDeps(template, okInitResponse());
const shallow = await captureStreams(() =>
runBenchKitInit(
JSON_CTX,
join(tempDir("bench-kit-target-"), "instance"),
{},
shallowFake.deps,
),
);
expect(shallow.exitCode).toBeUndefined();
expect(shallowFake.bootstrapCalls[0]!.request.deepClone).toBeUndefined();
const deepFake = fakeDeps(template, okInitResponse());
const deep = await captureStreams(() =>
runBenchKitInit(
JSON_CTX,
join(tempDir("bench-kit-target-"), "instance"),
{ deep: true },
deepFake.deps,
),
);
expect(deep.exitCode).toBeUndefined();
expect(deepFake.bootstrapCalls[0]!.request.deepClone).toBe(true);
});
it("degrades to a hint when the base repo clone fails (init still succeeds)", async () => {
const template = buildTemplateFixture();
const target = join(tempDir("bench-kit-target-"), "instance");
const { deps } = fakeDeps(
template,
okInitResponse({
baseRepo: { name: "shop-app", url: "https://github.com/acme/shop-app.git" },
baseRepoClone: {
name: "shop-app",
url: "https://github.com/acme/shop-app.git",
rootDir: "/somewhere/shop-app",
dest: ".repos/shop-app",
},
}),
{ cloneBaseRepo: () => Promise.resolve({ ok: false, error: "disk full" }) },
);
const result = await captureStreams(() => runBenchKitInit(JSON_CTX, target, {}, deps));
expect(result.exitCode).toBeUndefined();
expect(existsSync(join(target, ".repos", "shop-app"))).toBe(false);
expect(parseEnvelope(result.stdout).data.baseRepoClone).toBe("failed");
});
it("skips the base repo clone when the bootstrap sent no instruction", async () => {
const template = buildTemplateFixture();
const target = join(tempDir("bench-kit-target-"), "instance");
const { deps } = fakeDeps(template, okInitResponse(), {
cloneBaseRepo: () => {
throw new Error("must not be called");
},
});
const result = await captureStreams(() => runBenchKitInit(JSON_CTX, target, {}, deps));
expect(result.exitCode).toBeUndefined();
expect(parseEnvelope(result.stdout).data.baseRepoClone).toBe("skipped");
});
it("maps a bootstrap error envelope onto the CLI error contract", async () => {
const template = buildTemplateFixture();
const target = join(tempDir("bench-kit-target-"), "instance");
const { deps } = fakeDeps(template, {
ok: false,
code: "target_not_empty",
message: "Katalog nie jest pusty.",
hint: "Wskaż pusty katalog.",
});
const result = await captureStreams(() => runBenchKitInit(JSON_CTX, target, {}, deps));
expect(result.exitCode).toBe(1);
const envelope = parseEnvelope(result.stdout);
expect(envelope.status).toBe("error");
expect(envelope.error.code).toBe("target_not_empty");
expect(envelope.error.hint).toBe("Wskaż pusty katalog.");
});
it("renders repair mode from the bootstrap's response", async () => {
const template = buildTemplateFixture();
const target = tempDir("bench-kit-target-");
// Existing instance marker — the CLI only reads .bench-kit/VERSION.
mkdirSync(join(target, ".bench-kit"), { recursive: true });
writeFileSync(join(target, ".bench-kit", "VERSION"), "0.10.0\n");
const { deps, bootstrapCalls } = fakeDeps(
template,
okInitResponse({ mode: "repair", filesCopied: 3, gitInitialized: false, committed: false }),
);
const result = await captureStreams(() => runBenchKitInit(JSON_CTX, target, {}, deps));
expect(result.exitCode).toBeUndefined();
// Repair keeps mode=init on the wire — the bootstrap decides it's a repair.
expect(bootstrapCalls[0]!.request.mode).toBe("init");
// No product-repo detection on repair.
expect(bootstrapCalls[0]!.request.detectedBaseRepo).toBeNull();
const envelope = parseEnvelope(result.stdout);
expect(envelope.data.mode).toBe("repair");
expect(envelope.data.filesCopied).toBe(3);
});
it("rejects --template-version on an existing instance, pointing to update", async () => {
const template = buildTemplateFixture();
const target = tempDir("bench-kit-target-");
mkdirSync(join(target, ".bench-kit"), { recursive: true });
writeFileSync(join(target, ".bench-kit", "VERSION"), "0.10.0\n");
const { deps, bootstrapCalls } = fakeDeps(template, okInitResponse());
const result = await captureStreams(() =>
runBenchKitInit(JSON_CTX, target, { templateVersion: "v0.11.0" }, deps),
);
expect(result.exitCode).toBe(2);
const envelope = parseEnvelope(result.stdout);
expect(envelope.error.code).toBe("version_conflict");
expect(envelope.error.hint).toContain("10x bench-kit update");
expect(bootstrapCalls.length).toBe(0);
});
it("fails preflight when git is missing", async () => {
const template = buildTemplateFixture();
const target = join(tempDir("bench-kit-target-"), "instance");
const { deps } = fakeDeps(template, okInitResponse(), {
toolAvailable: (cmd) => Promise.resolve(cmd !== "git"),
});
const result = await captureStreams(() => runBenchKitInit(JSON_CTX, target, {}, deps));
expect(result.exitCode).toBe(1);
expect(parseEnvelope(result.stdout).error.code).toBe("preflight_failed");
});
it("surfaces clone failures without calling the bootstrap", async () => {
const template = buildTemplateFixture();
const target = join(tempDir("bench-kit-target-"), "instance");
const { deps, bootstrapCalls } = fakeDeps(template, okInitResponse(), {
cloneTemplate: () => Promise.resolve({ ok: false, error: "fatal: repository not found" }),
});
const result = await captureStreams(() => runBenchKitInit(JSON_CTX, target, {}, deps));
expect(result.exitCode).toBe(1);
expect(parseEnvelope(result.stdout).error.code).toBe("clone_failed");
expect(bootstrapCalls.length).toBe(0);
expect(existsSync(target)).toBe(false);
});
it("rejects a template without the bootstrap entry (pre-contract tag)", async () => {
const template = buildTemplateFixture("0.9.0", false);
const target = join(tempDir("bench-kit-target-"), "instance");
const { deps, bootstrapCalls } = fakeDeps(template, okInitResponse());
const result = await captureStreams(() => runBenchKitInit(JSON_CTX, target, {}, deps));
expect(result.exitCode).toBe(1);
const envelope = parseEnvelope(result.stdout);
expect(envelope.error.code).toBe("template_incomplete");
expect(envelope.error.hint).toContain("v0.10.0");
expect(bootstrapCalls.length).toBe(0);
});
});
describe("10x bench-kit update", () => {
function existingInstance(version = "0.9.0"): string {
const target = tempDir("bench-kit-target-");
mkdirSync(join(target, ".bench-kit"), { recursive: true });
writeFileSync(join(target, ".bench-kit", "VERSION"), `${version}\n`);
return target;
}
const okUpdateResponse: BootstrapResponse = {
ok: true,
mode: "update",
upToDate: false,
fromVersion: "0.9.0",
templateVersion: "0.10.0",
tool: "claude-code",
skillRoot: ".claude/skills",
runnerDeps: "installed",
zones: {
workflows: { added: 0, updated: 2, unchanged: 0 },
skills: { added: 1, updated: 3, unchanged: 6 },
shared: { added: 0, updated: 1, unchanged: 0 },
},
warnings: [],
};
it("sends an update request with the tool profile map and renders the zone report", async () => {
const template = buildTemplateFixture();
const target = existingInstance();
const { deps, bootstrapCalls } = fakeDeps(template, okUpdateResponse);
const result = await captureStreams(() => runBenchKitUpdate(JSON_CTX, target, {}, deps));
expect(result.exitCode).toBeUndefined();
const { request } = bootstrapCalls[0]!;
expect(request.mode).toBe("update");
expect(request.contractVersion).toBe(CONTRACT_VERSION);
// The manifest's tool is resolved by the KIT — the CLI only ships the map.
expect(request.toolProfiles["codex"]).toBe(".agents/skills");
const envelope = parseEnvelope(result.stdout);
expect(envelope.data.mode).toBe("update");
expect(envelope.data.fromVersion).toBe("0.9.0");
expect(envelope.data.templateVersion).toBe("0.10.0");
expect(envelope.data.zones.skills.updated).toBe(3);
});
it("reports up-to-date without a zone report", async () => {
const template = buildTemplateFixture();
const target = existingInstance("0.10.0");
const { deps } = fakeDeps(template, {
ok: true,
mode: "update",
upToDate: true,
templateVersion: "0.10.0",
});
const result = await captureStreams(() => runBenchKitUpdate(JSON_CTX, target, {}, deps));
expect(result.exitCode).toBeUndefined();
const envelope = parseEnvelope(result.stdout);
expect(envelope.data.upToDate).toBe(true);
expect(envelope.data.templateVersion).toBe("0.10.0");
});
it("refuses to update a dirty worktree before any network clone", async () => {
const template = buildTemplateFixture();
const target = existingInstance();
const cloneCalls: string[] = [];
const { deps, bootstrapCalls } = fakeDeps(template, okUpdateResponse, {
cloneTemplate: (_ref, destDir) => {
cloneCalls.push(destDir);
return Promise.resolve({ ok: true, error: "" });
},
runGit: (args) =>
Promise.resolve(
args[0] === "status"
? { ok: true, stdout: " M bench.config.yaml\n", error: "" }
: { ok: true, stdout: "", error: "" },
),
});
const result = await captureStreams(() => runBenchKitUpdate(JSON_CTX, target, {}, deps));
expect(result.exitCode).toBe(1);
expect(parseEnvelope(result.stdout).error.code).toBe("dirty_worktree");
expect(cloneCalls.length).toBe(0);
expect(bootstrapCalls.length).toBe(0);
});
it("rejects a directory that is not an instance without cloning", async () => {
const template = buildTemplateFixture();
const target = tempDir("bench-kit-target-");
const { deps, bootstrapCalls } = fakeDeps(template, okUpdateResponse);
const result = await captureStreams(() => runBenchKitUpdate(JSON_CTX, target, {}, deps));
expect(result.exitCode).toBe(1);
expect(parseEnvelope(result.stdout).error.code).toBe("not_an_instance");
expect(bootstrapCalls.length).toBe(0);
});
it("maps the bootstrap's dirty_tree defense onto the CLI error contract", async () => {
const template = buildTemplateFixture();
const target = existingInstance();
const { deps } = fakeDeps(template, {
ok: false,
code: "dirty_tree",
message: "Instancja ma niezacommitowane zmiany.",
hint: "Zacommituj albo zestashuj.",
});
const result = await captureStreams(() => runBenchKitUpdate(JSON_CTX, target, {}, deps));
expect(result.exitCode).toBe(1);
expect(parseEnvelope(result.stdout).error.code).toBe("dirty_tree");
});
});
async function runCli(argv: string[]): Promise<CaptureResult> {
return captureStreams(async () => {
const cli = cac("10x");
cli.option("--json", "Output as JSON (auto-detected when piped)");
cli.option("--verbose", "Show detailed output on stderr");
registerBenchKitCommand(cli);
cli.parse(["bun", "10x", ...argv], { run: false });
await cli.runMatchedCommand();
});
}
describe("10x bench-kit dispatch", () => {
it("registers bench-kit unconditionally", () => {
const cli = cac("10x");
registerBenchKitCommand(cli);
expect(cli.commands.map((c) => c.name)).toContain("bench-kit");
});
it("routes 'update' to the real implementation", async () => {
// A temp dir that is not an instance — proves dispatch reaches update.
const target = tempDir("bench-kit-target-");
// Keep CAC → real update → real preflight/process handling, but do not let
// host Docker/Git installations turn this routing test into a tool health check.
const spawn = childProcess.spawn;
const probes: string[] = [];
const children: childProcess.ChildProcess[] = [];
const spy = spyOn(childProcess, "spawn").mockImplementation(((command: string, args: readonly string[] = [], options: childProcess.SpawnOptions = {}) => {
expect(["git", "docker"]).toContain(command);
expect(args).toEqual(["--version"]);
probes.push(command);
const child = spawn(process.execPath, args, options);
children.push(child);
return child;
}) as typeof childProcess.spawn);
try {
const result = await runCli(["bench-kit", "update", target, "--json"]);
expect(result.exitCode).toBe(1);
const envelope = parseEnvelope(result.stdout);
expect(envelope.error.code).toBe("not_an_instance");
expect(probes).toEqual(["git", "docker"]);
expect(children.every((child) => child.exitCode === 0)).toBe(true);
} finally {
spy.mockRestore();
for (const child of children) {
if (child.exitCode === null && child.signalCode === null) child.kill();
}
}
});
it("rejects an unknown action with usage exit code", async () => {
const result = await runCli(["bench-kit", "frobnicate", "--json"]);
expect(result.exitCode).toBe(2);
const envelope = parseEnvelope(result.stdout);
expect(envelope.error.code).toBe("unknown_action");
});
});