feat(bus): the bus host runs an explicit Claude Code binary (#1561, row 68)
MOSAIC_CLAUDE_BIN names the claude binary by absolute path instead of finding it on PATH. scripts/bus-service.sh render|install --claude PATH writes it into the unit (default: command -v claude in the installing shell); the claude adapter, the launcher's version read and the seat session environment all use it. Mos's Q18 ask: the unit's PATH found /usr/bin/claude 2.1.259, which refuses claude-opus-5-5. Candidate: round 2 manifest 21c1ab05, round 1 (a2008d8b) rebased ontof2f2861bafter row 55 changed docs/TOOLS.md and packages/cli/README.md; the changed lines are byte-identical. Approved by Filbert (27349, 27374) and Dewey (27350, 27375). Sage's gate onf2f2861bplus the patch: cli 112, seat 27, harness 57, nine scripts/test-*.sh 0 failed, Docker on. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -4,8 +4,8 @@ import { spawn, spawnSync } from "node:child_process";
|
||||
import { subscribe, unsubscribe } from "node:diagnostics_channel";
|
||||
import { createServer } from "node:http";
|
||||
import { once } from "node:events";
|
||||
import { existsSync, readFileSync, statSync, writeFileSync, mkdirSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { chmodSync, existsSync, readFileSync, readdirSync, statSync, symlinkSync, writeFileSync, mkdirSync } from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { Client } from "../../bus/src/client.mjs";
|
||||
import { bootConfig, loadSystem, REPO } from "../src/config.mjs";
|
||||
@@ -482,7 +482,7 @@ test("bus-service.sh renders the unit and installs it into a given directory", (
|
||||
assert.equal(render.status, 0, render.stderr);
|
||||
assert.match(render.stdout, new RegExp(`ExecStart=${REPO.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}/scripts/mosaic bus start %i`));
|
||||
assert.match(render.stdout, /RestartPreventExitStatus=2 3 4/);
|
||||
assert.doesNotMatch(render.stdout, /@REPO@|@PATH@/);
|
||||
assert.doesNotMatch(render.stdout, /@REPO@|@PATH@|@CLAUDE_ENV@/);
|
||||
// A user unit cannot order on a system target (Darkwing F3).
|
||||
assert.doesNotMatch(render.stdout, /network-online/);
|
||||
const first = spawnSync(script, ["install", "--dir", dir, "--no-reload"], { encoding: "utf8" });
|
||||
@@ -494,3 +494,68 @@ test("bus-service.sh renders the unit and installs it into a given directory", (
|
||||
assert.match(spawnSync(script, ["uninstall", "--dir", dir, "--no-reload"], { encoding: "utf8" }).stdout, /removed: /);
|
||||
assert.equal(spawnSync(script, ["bogus"], { encoding: "utf8" }).status, 4);
|
||||
});
|
||||
|
||||
// Row 68: the unit pins the Claude Code binary as MOSAIC_CLAUDE_BIN, from
|
||||
// --claude or else `command -v claude`, kept as given (not resolved).
|
||||
test("bus-service.sh pins the claude binary, refuses one the unit can't carry, and renders without one", (t) => {
|
||||
const dir = tmp(t);
|
||||
const script = join(REPO, "scripts", "bus-service.sh");
|
||||
const stand = (p, mode = 0o755) => {
|
||||
writeFileSync(p, '#!/bin/sh\necho "9.9.9 (Claude Code)"\n');
|
||||
chmodSync(p, mode);
|
||||
return p;
|
||||
};
|
||||
mkdirSync(join(dir, "bin"));
|
||||
const onPath = stand(join(dir, "bin", "claude"));
|
||||
const named = stand(join(dir, "claude-named"));
|
||||
symlinkSync(named, join(dir, "claude-link"));
|
||||
const env = { ...process.env, PATH: `${join(dir, "bin")}:${dirname(process.execPath)}:/usr/bin:/bin` };
|
||||
const run = (args, e = env) => spawnSync(script, args, { encoding: "utf8", env: e });
|
||||
const pinned = (out) => out.match(/^Environment=MOSAIC_CLAUDE_BIN=(.*)$/m)?.[1];
|
||||
|
||||
assert.equal(pinned(run(["render"]).stdout), onPath);
|
||||
assert.equal(pinned(run(["render", "--claude", named]).stdout), named);
|
||||
assert.equal(pinned(run(["render", "--claude", join(dir, "claude-link")]).stdout), join(dir, "claude-link"));
|
||||
|
||||
const refusals = [
|
||||
["claude", /must be absolute/],
|
||||
[stand(join(dir, "claude-noexec"), 0o644), /not an executable file/],
|
||||
[join(dir, "missing"), /not an executable file/],
|
||||
[stand(join(dir, "claude%h")), /cannot carry/],
|
||||
[stand(join(dir, "claude x")), /cannot carry/],
|
||||
];
|
||||
for (const [path, why] of refusals) {
|
||||
const r = run(["render", "--claude", path]);
|
||||
assert.equal(r.status, 1, path);
|
||||
assert.match(r.stderr, why, path);
|
||||
assert.equal(r.stdout, "", path);
|
||||
}
|
||||
|
||||
// install prints the pin and its version; a refused pin writes nothing.
|
||||
const units = join(dir, "units");
|
||||
const bad = run(["install", "--dir", units, "--no-reload", "--claude", "claude"]);
|
||||
assert.equal(bad.status, 1);
|
||||
assert.deepEqual(readdirSync(units), []);
|
||||
const ok = run(["install", "--dir", units, "--no-reload", "--claude", named]);
|
||||
assert.equal(ok.status, 0, ok.stderr);
|
||||
assert.match(ok.stdout, new RegExp(`^claude: ${named.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")} \\(9\\.9\\.9 \\(Claude Code\\)\\)$`, "m"));
|
||||
assert.equal(pinned(readFileSync(join(units, "[email protected]"), "utf8")), named);
|
||||
assert.equal(run(["uninstall", "--dir", units, "--no-reload", "--claude", named]).status, 4);
|
||||
assert.equal(run(["render", "--dir", units]).status, 4);
|
||||
|
||||
// No claude anywhere on PATH: the unit carries a comment, not the variable.
|
||||
const tools = join(dir, "tools");
|
||||
mkdirSync(tools);
|
||||
symlinkSync(process.execPath, join(tools, "node"));
|
||||
for (const tool of ["dirname", "sed"]) {
|
||||
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 none = spawnSync(bash, [script, "render"], { encoding: "utf8", env: { PATH: tools, HOME: dir } });
|
||||
assert.equal(none.status, 0, none.stderr);
|
||||
assert.match(none.stderr, /no claude on PATH; MOSAIC_CLAUDE_BIN left unset/);
|
||||
assert.equal(pinned(none.stdout), undefined);
|
||||
assert.match(none.stdout, /^# MOSAIC_CLAUDE_BIN unset: no claude on PATH at install time$/m);
|
||||
assert.doesNotMatch(none.stdout, /@CLAUDE_ENV@/);
|
||||
});
|
||||
|
||||
@@ -9,14 +9,14 @@ import { spawn, spawnSync } from "node:child_process";
|
||||
import { once } from "node:events";
|
||||
import { createInterface } from "node:readline";
|
||||
import { connect } from "node:net";
|
||||
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
||||
import { chmodSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { Client } from "../../bus/src/client.mjs";
|
||||
import { launchLogFile, launchSocketPath, readSessions, Registry, sessionsFile, UNSHARE } from "../../seat/src/session.mjs";
|
||||
import { bootConfig, loadSystem } from "../src/config.mjs";
|
||||
import { startHost, startTimeOf } from "../src/host.mjs";
|
||||
import { createLauncher } from "../src/launcher.mjs";
|
||||
import { claudeVersion, createLauncher } from "../src/launcher.mjs";
|
||||
import { fixture, tmp } from "./helpers.mjs";
|
||||
|
||||
const FAKE = fileURLToPath(new URL("../../harness/tests/fixtures/fake-adapter.mjs", import.meta.url));
|
||||
@@ -392,3 +392,20 @@ test("a runner that ignores SIGTERM is killed when the host closes", { skip, tim
|
||||
assert.equal(ends.length, 1);
|
||||
assert.equal(ends[0].signal, "SIGKILL");
|
||||
});
|
||||
|
||||
// Row 68: the manifest's claudeVersion asks the binary sessions run.
|
||||
test("claudeVersion asks MOSAIC_CLAUDE_BIN when set, else claude on PATH", (t) => {
|
||||
const dir = tmp(t);
|
||||
mkdirSync(join(dir, "bin"));
|
||||
const stand = (p, v) => {
|
||||
writeFileSync(p, `#!/bin/sh\necho "${v} (Claude Code)"\n`);
|
||||
chmodSync(p, 0o755);
|
||||
return p;
|
||||
};
|
||||
stand(join(dir, "bin", "claude"), "1.0.0");
|
||||
const pinned = stand(join(dir, "pinned"), "9.9.9");
|
||||
const PATH = join(dir, "bin");
|
||||
assert.equal(claudeVersion({ PATH, MOSAIC_CLAUDE_BIN: pinned }), "9.9.9 (Claude Code)");
|
||||
assert.equal(claudeVersion({ PATH }), "1.0.0 (Claude Code)");
|
||||
assert.equal(claudeVersion({ PATH: join(dir, "none") }), null);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user