Unclaimed files left in the canonical checkout. Inventory: canonical-cleanup-inventory.md (commit-as-evidence groups). detect-secrets scan clean on 2026-11-02: hits are sha256 pins, git object ids, test sentinels already in tracked tests and unit-file lines.
84 lines
6.0 KiB
Bash
Executable File
84 lines
6.0 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# Row 128 (#1622): mosaic talks to the assistant in a named thread.
|
||
# Each behavior has a mutant that a named test file must fail. For each mutant this runs one test file against
|
||
# the real source (control) and against the source with one exact-once text edit, in the given worktree, one
|
||
# test file at a time, and restores every source file on exit.
|
||
# Usage: mutants.sh <worktree> <outdir> (MUTANT_DRY=1 only proves every edit matches exactly once;
|
||
# MUTANT_ONLY=<regex> runs just the matching names)
|
||
set -u
|
||
WT=$(cd "$1" && pwd); OUT=$(mkdir -p "$2" && cd "$2" && pwd)
|
||
cd "$WT" || exit 2
|
||
BAK=$(mktemp -d "$OUT/.bak.XXXXXX")
|
||
declare -A SAVED
|
||
save() { [ -n "${SAVED[$1]:-}" ] || { mkdir -p "$BAK/$(dirname "$1")"; cp "$1" "$BAK/$1"; SAVED[$1]=1; }; }
|
||
restore() { for f in "${!SAVED[@]}"; do cp "$BAK/$f" "$f"; done; rm -rf "$BAK"; }
|
||
trap restore EXIT
|
||
edit() { # file old new: exact-once replace
|
||
save "$1"
|
||
node -e '
|
||
const fs = require("fs"); const [f, a, b] = process.argv.slice(1);
|
||
const s = fs.readFileSync(f, "utf8"); const n = s.split(a).length - 1;
|
||
if (n !== 1) { console.error("expected one match, found " + n); process.exit(3); }
|
||
fs.writeFileSync(f, s.replace(a, () => b));
|
||
' "$1" "$2" "$3"
|
||
}
|
||
run() { # name testfile
|
||
if [ -n "${MUTANT_DRY:-}" ]; then echo "$1: edit ok (dry)"; return; fi
|
||
setsid -w timeout -k 10 400 node --test "$2" > "$OUT/$1.out" 2>&1; echo "$?" > "$OUT/$1.rc"
|
||
echo "$1: exit $(cat "$OUT/$1.rc"); $(grep -E '^ℹ (tests|pass|fail)' "$OUT/$1.out" | tr '\n' ' ')"
|
||
grep -E '^✖' "$OUT/$1.out" | sort -u | sed 's/ ([0-9.]*ms)$//' | sed "s/^/ failed: /"
|
||
}
|
||
mutant() { # name testfile file old new
|
||
if [ -n "${MUTANT_ONLY:-}" ] && ! [[ $1 =~ $MUTANT_ONLY ]]; then return; fi
|
||
edit "$3" "$4" "$5" || { echo "$1: edit failed"; return; }
|
||
diff -u "$BAK/$3" "$3" > "$OUT/$1.patch"
|
||
run "$1" "$2"
|
||
cp "$BAK/$3" "$3"
|
||
}
|
||
TT=packages/cli/tests/thread-cli.test.mjs; PT=packages/cli/tests/project.test.mjs
|
||
CL=packages/cli/src/cli.mjs; PR=packages/cli/src/project.mjs; TH=packages/cli/src/thread.mjs
|
||
if [ -z "${MUTANT_DRY:-}" ] && [ -z "${MUTANT_ONLY:-}" ]; then for t in "$TT" "$PT"; do run "control-$(basename "$t" .test.mjs)" "$t"; done; fi
|
||
|
||
# project create / list
|
||
mutant p1-create-drops-repo "$TT" "$PR" "{ name, ...(parsed.values['--repo'] === undefined ? {} : { repo: parsed.values['--repo'] }) }" "{ name }"
|
||
mutant p2-create-prints-no-id "$TT" "$PR" 'created project ${name} id ${made.id}' 'created project ${name}'
|
||
mutant p3-list-blank-repo "$TT" "$PR" "\${p.repo ?? '-'}" "\${p.repo ?? ''}"
|
||
mutant p4-list-json-ignored "$TT" "$PR" "if (parsed.flags.has('--json')) return io.stdout.write" "if (false) return io.stdout.write"
|
||
mutant p5-create-arity "$PT" "$PR" "parsed.positional.length !== (sub === 'create' ? 1 : 0)" "parsed.positional.length !== (sub === 'create' ? parsed.positional.length : 0)"
|
||
mutant p6-init-check-to-bus "$PT" "$CL" '["create", "list"].includes(rest[0]) ? projectBusCommand' 'true ? projectBusCommand'
|
||
|
||
# the resolver
|
||
mutant r1-short-prefix-allowed "$TT" "$TH" 'if (ref.length < 8) throw new CliError(`thread reference' 'if (false) throw new CliError(`thread reference'
|
||
mutant r2-ambiguous-picks-first "$TT" "$TH" 'if (hits.length > 1) throw new CliError(`${ref} matches ${hits.length} threads; use more of the id, or <project>/<name>`, 2);' 'if (hits.length > 1) return { id: hits[0].id, label: labelOf(projects, hits[0]) };'
|
||
mutant r3-label-not-resolved "$TT" "$TH" ' ?? threads.find((t) => labelOf(projects, t) === ref);' ';'
|
||
mutant r4-full-id-not-passed-on "$TT" "$TH" 'if (UUID.test(ref)) return { id: ref, label: ref };' ''
|
||
mutant r5-prefix-not-resolved "$TT" "$TH" 'const hits = threads.filter((t) => t.id.startsWith(ref));' 'const hits = [];'
|
||
mutant r6-project-by-name-only "$TT" "$TH" 'projects.find((p) => p.id === ref) ?? projects.find((p) => p.name === ref)' 'projects.find((p) => p.name === ref)'
|
||
mutant r7-unknown-project-passed-on "$TT" "$TH" 'throw new CliError(`no project is named or numbered ${JSON.stringify(ref)}; see mosaic project list`, 2);' 'return { id: ref, name: ref };'
|
||
|
||
# thread verbs
|
||
mutant v1-create-sends-name-as-project "$TT" "$TH" 'call("thread.create", { project: project.id, name })' 'call("thread.create", { project: ref, name })'
|
||
mutant v2-open-writes-nothing "$TT" "$TH" ' await call("thread.open", { thread: thread.id });
|
||
' ''
|
||
mutant v3-read-one-page "$TT" "$TH" ' if (page.length < PAGE) return out;' ' return out;'
|
||
mutant v4-read-json-ignored "$TT" "$TH" 'if (parsed.flags.has("--json")) return json(io, messages);' ''
|
||
mutant v5-threads-project-ignored "$TT" "$TH" 'project ? { project: project.id } : {}' '{}'
|
||
mutant v6-threads-json-ignored "$TT" "$TH" 'if (parsed.flags.has("--json")) return json(io, threads);' ''
|
||
mutant v7-threads-one-extra-arg "$TT" "$TH" 'if (parsed.positional.length !== 0) throw new CliError(`threads takes no arguments' 'if (false) throw new CliError(`threads takes no arguments'
|
||
|
||
# talk --thread
|
||
mutant t1-send-not-to-thread "$TT" "$CL" 'thread ? "thread.send" : "message.send", thread ?' '"message.send", thread ?'
|
||
mutant t2-send-class "$TT" "$CL" '{ thread: thread.id, body, class: "REQUEST" }' '{ thread: thread.id, body, class: "INFO" }'
|
||
mutant t3-plain-talk-raw-code "$TT" "$CL" 'if (e.code === "thread-required-use-thread-send") {' 'if (false) {'
|
||
mutant t4-reply-filter-dropped "$TT" "$CL" 'if (!follow && m.in_reply_to !== sent.id) continue;' ''
|
||
mutant t5-thread-with-tester "$TT" "$CL" 'if (threadRef !== undefined && parsed.flags.has("--tester")) throw usage("--thread takes no --tester");' ''
|
||
mutant t6-thread-arity "$TT" "$CL" 'receiveOnly ? 0 : threadRef === undefined ? 2 : 1' 'receiveOnly ? 0 : 2'
|
||
|
||
# --follow in a thread
|
||
mutant f1-other-threads-printed "$TT" "$CL" 'if (members && !(members.seq(m.id) > (members.seq(sent?.id) ?? 0))) continue;' ''
|
||
mutant f2-earlier-thread-messages-printed "$TT" "$CL" '(members.seq(sent?.id) ?? 0)' '0'
|
||
mutant f3-thread-not-refreshed "$TT" "$CL" 'if (members && received.length > 0) await members.refresh();' ''
|
||
mutant f4-foreign-thread-received-first "$TT" "$CL" ' await members?.refresh();
|
||
' ''
|
||
echo done
|