test(cli, harness): Claude pin tests, a directory pin, a symlink default, a pin with a space (#1566, row 72)
One test in packages/cli/tests/host.test.mjs: a directory given with --claude is refused, and a symlink default stays the symlink path. One in packages/harness/tests/claude-session.test.mjs: a directory pin exits 2 before claude runs, and a pin under a directory with a space runs. agents/rocko/work/queue-72/mutants.sh (committed with the packet in036afdb6) reruns the six row 68 mutants: all killed on the candidate, all surviving on the base. Accepted deviation: bash's command -v never returns a directory, so a directory named claude on PATH takes the no-claude branch, which the test asserts. Author: Rocko. Candidate manifest e6531a5311f17ea147700e417dfdbd766a9c38acf3c3b44755267c285b78e006 (basea0080da8, no drift on the candidate paths, bus-service.sh or adapter.sh). Approved by Filbert, comment 27460 on #1566; their N1 is row 78, #1572. Sage gate rerun on a worktree ofdf676fa1with the patch: manifest OK; cli 113/113, harness 58/58, 0 skipped; test-auth 15, conductor 17, config 24, discord 66, extension-package 18, foundation 44, queue 27, release 14, task 98, all 0 failed. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -559,3 +559,55 @@ test("bus-service.sh pins the claude binary, refuses one the unit can't carry, a
|
||||
assert.match(none.stdout, /^# MOSAIC_CLAUDE_BIN unset: no claude on PATH at install time$/m);
|
||||
assert.doesNotMatch(none.stdout, /@CLAUDE_ENV@/);
|
||||
});
|
||||
|
||||
// Row 72 (#1566): mutants row 68's tests left alive. A directory is -x, so
|
||||
// only the -f check keeps a directory pin out of the unit; the default pin
|
||||
// keeps a symlink as given, so a claude that updates through it is followed.
|
||||
test("bus-service.sh refuses a directory pin and keeps a symlink default unresolved", (t) => {
|
||||
const dir = tmp(t);
|
||||
const script = join(REPO, "scripts", "bus-service.sh");
|
||||
const pinned = (out) => out.match(/^Environment=MOSAIC_CLAUDE_BIN=(.*)$/m)?.[1];
|
||||
const tools = join(dir, "tools");
|
||||
mkdirSync(tools);
|
||||
symlinkSync(process.execPath, join(tools, "node"));
|
||||
for (const tool of ["dirname", "sed", "readlink"]) {
|
||||
const at = ["/usr/bin", "/bin"].map((d) => join(d, tool)).find((p) => existsSync(p));
|
||||
symlinkSync(at, join(tools, tool));
|
||||
}
|
||||
const bash = ["/usr/bin/bash", "/bin/bash"].find((p) => existsSync(p));
|
||||
const run = (args, bin) => spawnSync(bash, [script, ...args], { encoding: "utf8", env: { PATH: `${bin}:${tools}`, HOME: dir } });
|
||||
|
||||
// --claude DIR: a directory passes -x and fails -f.
|
||||
const pinDir = join(dir, "claude-dir");
|
||||
mkdirSync(pinDir);
|
||||
chmodSync(pinDir, 0o755);
|
||||
let r = run(["render", "--claude", pinDir], join(dir, "empty"));
|
||||
assert.equal(r.status, 1);
|
||||
assert.match(r.stderr, /not an executable file/);
|
||||
assert.equal(r.stdout, "");
|
||||
|
||||
// A directory named claude on PATH: bash's `command -v` never returns a
|
||||
// directory, so the default finds no claude and pins nothing. It never
|
||||
// pins the directory.
|
||||
const dirBin = join(dir, "dir-bin");
|
||||
mkdirSync(join(dirBin, "claude"), { recursive: true });
|
||||
chmodSync(join(dirBin, "claude"), 0o755);
|
||||
r = run(["render"], dirBin);
|
||||
assert.equal(r.status, 0, r.stderr);
|
||||
assert.match(r.stderr, /no claude on PATH; MOSAIC_CLAUDE_BIN left unset/);
|
||||
assert.equal(pinned(r.stdout), undefined);
|
||||
assert.ok(!r.stdout.includes(join(dirBin, "claude")));
|
||||
|
||||
// A symlink named claude on PATH: the pin is the link, not its target.
|
||||
const real = join(dir, "versions", "claude-9.9.9");
|
||||
mkdirSync(dirname(real));
|
||||
writeFileSync(real, '#!/bin/sh\necho "9.9.9 (Claude Code)"\n');
|
||||
chmodSync(real, 0o755);
|
||||
const linkBin = join(dir, "link-bin");
|
||||
mkdirSync(linkBin);
|
||||
symlinkSync(real, join(linkBin, "claude"));
|
||||
r = run(["render"], linkBin);
|
||||
assert.equal(r.status, 0, r.stderr);
|
||||
assert.equal(pinned(r.stdout), join(linkBin, "claude"));
|
||||
assert.ok(!r.stdout.includes(real), "the target never reaches the unit");
|
||||
});
|
||||
|
||||
@@ -215,6 +215,50 @@ test("claude adapter: MOSAIC_CLAUDE_BIN wins over PATH and must be an absolute e
|
||||
}
|
||||
});
|
||||
|
||||
// Row 72 (#1566): a directory pin exits 2 with the adapter's own message,
|
||||
// not 126 at exec; a pin under a directory whose name has a space runs,
|
||||
// which needs "$CLAUDE" quoted.
|
||||
test("claude adapter: a directory pin exits 2 before it runs; a pin with a space runs", (t) => {
|
||||
const dir = scratch(t);
|
||||
mkdirSync(join(dir, "bin"));
|
||||
const stand = (p, word) => {
|
||||
writeFileSync(p, `#!/bin/sh\necho ${word} > "$WHO_OUT"\necho ok\n`);
|
||||
chmodSync(p, 0o755);
|
||||
return p;
|
||||
};
|
||||
stand(join(dir, "bin", "claude"), "path");
|
||||
for (const f of ["prompt.md", "settings.json", "mcp.json"]) writeFileSync(join(dir, f), "{}");
|
||||
const run = (bin) => spawnSync("/bin/sh", [ADAPTER], {
|
||||
encoding: "utf8",
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
env: {
|
||||
PATH: `${join(dir, "bin")}:/usr/bin:/bin`,
|
||||
WHO_OUT: join(dir, "who"),
|
||||
MOSAIC_SYSTEM_PROMPT_FILE: join(dir, "prompt.md"),
|
||||
MOSAIC_REQUEST: "x",
|
||||
MOSAIC_WORKSPACE: join(dir, "ws"),
|
||||
MOSAIC_SESSION_DIR: join(dir, "session"),
|
||||
MOSAIC_MODEL: "claude-sonnet-5-5",
|
||||
MOSAIC_CLAUDE_SETTINGS: join(dir, "settings.json"),
|
||||
MOSAIC_CLAUDE_MCP_CONFIG: join(dir, "mcp.json"),
|
||||
MOSAIC_CLAUDE_BIN: bin,
|
||||
},
|
||||
});
|
||||
const pinDir = join(dir, "claude-dir");
|
||||
mkdirSync(pinDir);
|
||||
chmodSync(pinDir, 0o755);
|
||||
let r = run(pinDir);
|
||||
assert.equal(r.status, 2, r.stderr);
|
||||
assert.match(r.stderr, /MOSAIC_CLAUDE_BIN is not an executable file/);
|
||||
assert.equal(existsSync(join(dir, "who")), false);
|
||||
|
||||
mkdirSync(join(dir, "Claude Code"));
|
||||
const spaced = stand(join(dir, "Claude Code", "claude"), "spaced");
|
||||
r = run(spaced);
|
||||
assert.equal(r.status, 0, r.stderr);
|
||||
assert.equal(readFileSync(join(dir, "who"), "utf8").trim(), "spaced");
|
||||
});
|
||||
|
||||
test("claude: CLAUDE.md files and auto-memory don't reach the model; without --restricted they do", { skip }, async (t) => {
|
||||
const { dir, workspace, env } = await session(t, []);
|
||||
const slug = realpathSync(workspace).replace(/[/.]/g, "-");
|
||||
|
||||
Reference in New Issue
Block a user