#!/usr/bin/env bash # Row 91 (#1585) mutants: one or more per guard in checkout.mjs, then the # launcher's refusal and record, then the prompt line. Run from the # candidate worktree's root: # LOG=/some/dir bash mutants.sh /path/to/mut.mjs > mutants.txt # Each mutant edits one site with mut.mjs (exactly one match, else exit 3), # runs the named tests and restores the file. "killed" means the run # failed; "survived" means it passed. set -u MUT=$1 run() { local name=$1 file=$2 from=$3 to=$4; shift 4 cp -- "$file" "$file.orig" if ! node "$MUT" "$file" "$from" "$to"; then echo "$name: NO MATCH"; mv -- "$file.orig" "$file"; return fi if node --test "$@" > "${LOG:-.}/mut-$name.log" 2>&1; then echo "$name: survived" else echo "$name: killed ($(grep -m1 -o 'ℹ fail [0-9]*' "${LOG:-.}/mut-$name.log"); $(grep '^✖ ' "${LOG:-.}/mut-$name.log" | grep -v 'failing tests' | sort -u | sed 's/^✖ //; s/ ([0-9.]*ms)$//' | paste -sd '|'))" fi mv -- "$file.orig" "$file" } C=packages/seat/src/checkout.mjs L=packages/cli/src/launcher.mjs B=packages/harness/src/bundle.mjs CT=(packages/seat/tests/checkout.test.mjs) LT=(packages/cli/tests/launcher.test.mjs) BT=(packages/harness/tests/bundle.test.mjs) # The walk of .git. run walk-off $C 'if (walk(join(dest, ".git"))) return "not-a-clone";' 'if (false) return "not-a-clone";' "${CT[@]}" run walk-no-nlink $C '} else if (!st.isFile() || st.nlink !== 1) return true;' '} else if (!st.isFile()) return true;' "${CT[@]}" run walk-symlinks-pass $C '} else if (!st.isFile() || st.nlink' '} else if ((!st.isFile() && !st.isSymbolicLink()) || st.nlink' "${CT[@]}" run clone-dir-symlink-ok $C 'return lstatSync(p).isDirectory();' 'return lstatSync(p).isDirectory() || lstatSync(p).isSymbolicLink();' "${CT[@]}" run no-commondir $C 'for (const f of ["commondir", join("objects", "info", "alternates")])' 'for (const f of [join("objects", "info", "alternates")])' "${CT[@]}" run no-alternates $C 'for (const f of ["commondir", join("objects", "info", "alternates")])' 'for (const f of ["commondir"])' "${CT[@]}" # Hooks and the config file. run hooks-ignored $C 'if (readdirSync(join(dest, ".git", "hooks")).length > 0) return "hooks-present";' 'if (false) return "hooks-present";' "${CT[@]}" run config-cap-off $C 'st.size > CONFIG_MAX_BYTES' 'st.size > Infinity' "${CT[@]}" # The allowlist. run allow-any-key $C 'default: return false;' 'default: return true;' "${CT[@]}" run origin-any-url $C 'case "remote.origin.url": return value === root;' 'case "remote.origin.url": return true;' "${CT[@]}" run fetch-any $C 'case "remote.origin.fetch": return value === FETCH;' 'case "remote.origin.fetch": return true;' "${CT[@]}" run branch-remote-any $C 'return branch[1] === "remote" ? value === "origin" :' 'return branch[1] === "remote" ? true :' "${CT[@]}" run branch-merge-any $C ': /^refs\/heads\/./.test(value ?? "");' ': true;' "${CT[@]}" run duplicate-key-ok $C 'if (seen.has(key) || !allowed(key, value, root))' 'if (!allowed(key, value, root))' "${CT[@]}" run no-origin-ok $C 'if (!seen.has("remote.origin.url")) return "config-changed";' '' "${CT[@]}" # The root, the update and the clone. run ceiling-off $C 'const ceiling = { GIT_CEILING_DIRECTORIES: dirname(root) };' 'const ceiling = {};' "${CT[@]}" run detached-root-clones $C 'if (!at.branch) throw new CheckoutError(project, `the root ${root} has no branch checked out`);' '' "${CT[@]}" run other-branch-updates $C 'if (!head.ok || head.out.trim() !== at.branch) return' 'if (!head.ok) return' "${CT[@]}" run dirty-updates $C 'if (status.out !== "") return { outcome: "kept", reason: "dirty" };' '' "${CT[@]}" run untracked-ignored $C '"--untracked-files=all", ' '"--untracked-files=no", ' "${CT[@]}" run merge-not-ff-only $C '"merge", "-q", "--ff-only", "--no-edit"' '"merge", "-q", "--no-edit"' "${CT[@]}" run temp-left $C 'rmSync(tmp, { recursive: true, force: true });' 'void tmp;' "${CT[@]}" run clone-in-place $C 'const tmp = join(workspace, `.${project}.clone-${randomBytes(4).toString("hex")}`);' 'const tmp = dest;' "${CT[@]}" run clone-hardlinks $C '"clone", "-q", "--no-hardlinks", ' '"clone", "-q", ' "${CT[@]}" # Second-line guards: the checks above stop each case first, so these are # expected to survive. run safe-no-hookspath $C '"-c", "core.hooksPath=/dev/null",' '' "${CT[@]}" run safe-no-fsmonitor $C '"-c", "core.fsmonitor=false",' '' "${CT[@]}" # The launcher. run refusal-unmapped $L 'if (e instanceof CheckoutError) throw new Refusal(' 'if (false) throw new Refusal(' "${LT[@]}" run kept-unrecorded $L 'for (const c of checkouts) if (c.outcome === "kept") record(' 'for (const c of checkouts) if (false) record(' "${LT[@]}" run checkouts-not-in-session $L 'session: { run, launchedBy, workspace, toolSocket, turnMarker, checkouts },' 'session: { run, launchedBy, workspace, toolSocket, turnMarker },' "${LT[@]}" # The prompt line. run prompt-no-clones $B '...checkoutLines(session.checkouts ?? []),' '' "${BT[@]}" run prompt-kept-as-current $B '(c.outcome === "kept" ?' '(c.outcome === "never" ?' "${BT[@]}"