diff --git a/agents/rocko/work/queue-78/mutants.sh b/agents/rocko/work/queue-78/mutants.sh new file mode 100755 index 00000000..0909b5b5 --- /dev/null +++ b/agents/rocko/work/queue-78/mutants.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash +# Row 78 (#1572): rerun the mutants against a tree. Usage: mutants.sh . +# Each mutant makes one check in scripts/bus-service.sh skip the default pin +# (no --claude) while still applying to --claude, runs the cli host tests, +# and restores the file from a saved copy, so the tree need not be +# committed. Prints "killed" or "SURVIVED" per mutant; exit 1 if any +# survives, a sed doesn't change the file, or the unmutated run fails. +set -euo pipefail +TREE="$(cd "${1:?usage: mutants.sh }" && pwd)" +cd "$TREE" +SCRIPT=scripts/bus-service.sh +TESTS=(packages/cli/tests/host.test.mjs) +run_tests() { node --test "${TESTS[@]}" >"$1" 2>&1; } + +# name | sed expression +MUTANTS=( + 'default-skips-exec-check|s/^ \[ -f "\$bin" \] && \[ -x "\$bin" \] || die / [ "$CLAUDE_SET" = 0 ] || { [ -f "$bin" ] \&\& [ -x "$bin" ]; } || die /' + 'default-skips-char-check|s/^ case "\$bin" in \*\[!A-Za-z0-9\/._+-\]\*) die / case "$bin" in *[!A-Za-z0-9\/._+-]*) [ "$CLAUDE_SET" = 0 ] || die /' +) + +OUT="${OUT:-$TREE/.mutants}" +mkdir -p "$OUT" +run_tests "$OUT/baseline.log" || { echo "baseline run failed: $OUT/baseline.log" >&2; exit 1; } +echo "baseline: pass" +status=0 +for m in "${MUTANTS[@]}"; do + IFS='|' read -r name expr <<<"$m" + cp -p "$SCRIPT" "$OUT/orig" + sed -i -e "$expr" "$SCRIPT" + if cmp -s "$OUT/orig" "$SCRIPT"; then + echo "$name: NOT APPLIED (sed matched nothing)"; status=1; continue + fi + diff -u "$OUT/orig" "$SCRIPT" >"$OUT/$name.diff" || true + if run_tests "$OUT/$name.log"; then + echo "$name: SURVIVED"; status=1 + else + echo "$name: killed ($(grep -E '^✖' "$OUT/$name.log" | grep -v 'failing tests' | sed 's/ ([0-9.]*ms)$//' | sort -u | tr '\n' ';'))" + fi + cp -p "$OUT/orig" "$SCRIPT" + cmp -s "$OUT/orig" "$SCRIPT" || { echo "restore failed: $SCRIPT" >&2; exit 1; } + rm -f "$OUT/orig" +done +exit "$status" diff --git a/packages/cli/tests/host.test.mjs b/packages/cli/tests/host.test.mjs index 13a621ae..6f0961af 100644 --- a/packages/cli/tests/host.test.mjs +++ b/packages/cli/tests/host.test.mjs @@ -611,3 +611,41 @@ test("bus-service.sh refuses a directory pin and keeps a symlink default unresol assert.equal(pinned(r.stdout), join(linkBin, "claude")); assert.ok(!r.stdout.includes(real), "the target never reaches the unit"); }); + +// Row 78 (#1572): the default from `command -v claude` takes the same checks +// as --claude. PATH holds only the test's dirs, so a real claude elsewhere on +// the host can't stand in for the one under test. +test("bus-service.sh refuses a default claude without the execute bit, or in a PATH dir with a space", (t) => { + const dir = tmp(t); + const script = join(REPO, "scripts", "bus-service.sh"); + 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 = (bin) => spawnSync(bash, [script, "render"], { encoding: "utf8", env: { PATH: `${bin}:${tools}`, HOME: dir } }); + const stand = (bin, mode) => { + mkdirSync(bin); + writeFileSync(join(bin, "claude"), '#!/bin/sh\necho "9.9.9 (Claude Code)"\n'); + chmodSync(join(bin, "claude"), mode); + return bin; + }; + + // The only claude on PATH has no execute bit: `command -v` still returns + // it, and the -x check refuses it. + let r = run(stand(join(dir, "noexec-bin"), 0o644)); + assert.equal(r.status, 1); + assert.match(r.stderr, /not an executable file/); + assert.equal(r.stdout, ""); + + // claude in a PATH dir with a space: systemd would split the pin there. + const spaced = stand(join(dir, "spaced bin"), 0o755); + r = run(spaced); + assert.equal(r.status, 1); + assert.match(r.stderr, /cannot carry/); + assert.ok(r.stderr.includes(join(spaced, "claude")), r.stderr); + assert.equal(r.stdout, ""); +});