70 lines
4.9 KiB
Bash
70 lines
4.9 KiB
Bash
#!/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[@]}"
|