Files
stack/scripts/test-land-work.sh
T
jason.woltjeandClaude Opus 5.5 63c82f1c47 bus, business: hard waits reach only the human, and only the chief of staff removes the assistant (row 139, #1633)
Row 139 round 2, Dewey's candidate a4c5657f (14 paths) on 5661bd233.
Filbert approved round 2 (comment 28786) after round 1 changes. Dewey's
gate: 14 runs rc 0. Mutants m1-m9 killed. Filbert ran 7 files singly:
0 fail, 0 skipped.

The broker's GATED and the business's GATED_ONLY are now the hard waits.
A cited gated decision goes to the domain arbiter unless it is a hard
wait, which always goes to the human. Under Jason's ruling Q169, a
role.revoke of the assistant is a hard wait. Its end gets past
END_PROTECTED only for the chief of staff, or when the cited decision
was resolved by the human through the CLI. PRD version 1.6 records Q169.
Mos approved the 1.6 text, including the REQ-ROLE-2 sentence on the
chief's end and revoke. scripts/test-land-work.sh adds the not-human
cases for arbiter-resolved merges. land-work's read.mjs is unchanged.

Row 137 lands second, so it carries the gated-merge refusal and the host
land op's arbiter acceptance. Making an end or revoke stop an assistant
run, and configuring the assistant, are row 155 (#1649).

Sage's landing gate ran on 26b78d483 plus the candidate: 25 steps, all rc 0 (15 packages, 10 script suites), 19:23:57Z to 19:28:28Z, network sage-r139_default removed.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-16 14:30:23 -05:00

410 lines
21 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
# Sandboxed selftests for scripts/land-work.sh (row 116, #1610).
#
# Builds a fixture project repository, a working repository holding the
# coder's work refs, and per case a bus database written through the real
# Store (scripts/land-work/fixture.mjs). Every refusal is exercised, then a
# landing, a dry run, a gate failure and a base that moves during the gate.
# Docker is a fake on PATH that logs its calls. Nothing here opens the real
# config, data root or bus: MOSAIC_CONFIG always names a sandbox file.
set -uo pipefail
cd "$(dirname "$0")/.."
REPO="$(pwd)"
SANDBOX="$(cd "$(mktemp -d)" && pwd -P)"
trap 'rm -rf "$SANDBOX"' EXIT
PASS=0
FAIL=0
# Status colors: terminal-only, NO_COLOR-respecting; plain when piped.
if [ -t 1 ] && [ -z "${NO_COLOR:-}" ]; then
C_OK=$'\033[0;32m'; C_FAIL=$'\033[0;31m'; C_RESET=$'\033[0m'
else
C_OK=""; C_FAIL=""; C_RESET=""
fi
check() {
if [ "$2" = "0" ]; then PASS=$((PASS+1)); echo "${C_OK}OK${C_RESET} $1"; else FAIL=$((FAIL+1)); echo "${C_FAIL}FAIL${C_RESET} $1"; fi
}
# Git sees no user or system config: no hooks path, no signing, a fixed identity.
: > "$SANDBOX/gitconfig"
export GIT_CONFIG_NOSYSTEM=1 GIT_CONFIG_GLOBAL="$SANDBOX/gitconfig"
export GIT_AUTHOR_NAME=suite GIT_AUTHOR_EMAIL=suite@local GIT_COMMITTER_NAME=suite GIT_COMMITTER_EMAIL=suite@local
REAL_GIT="$(command -v git)"
# ---- fakes: docker logs its calls; a git that tampers with the applied diff ----
mkdir -p "$SANDBOX/bin" "$SANDBOX/tamper"
cat > "$SANDBOX/bin/docker" <<EOF
#!/usr/bin/env bash
echo "docker \$*" >> "$SANDBOX/docker.log"
[ "\$1 \$2" != "network inspect" ] || [ "\${FAKE_NET_EXISTS:-}" = 1 ]
EOF
cat > "$SANDBOX/tamper/git" <<EOF
#!/usr/bin/env bash
"$REAL_GIT" "\$@"; rc=\$?
if [ "\${1:-}" = -C ] && [ "\${3:-}" = apply ] && [ "\$rc" -eq 0 ]; then
printf 'tampered\n' >> "\$2/README.md" && "$REAL_GIT" -C "\$2" add README.md
fi
exit "\$rc"
EOF
chmod +x "$SANDBOX/bin/docker" "$SANDBOX/tamper/git"
# ---- the project repository at the review's merge base ----
P="$SANDBOX/proto"
T="$P/target"
git init -q -b refactor "$T"
printf 'line1\nline2\nline3\n' > "$T/README.md"
mkdir -p "$T/packages/demo/tests" "$T/packages/queue/tests" "$T/scripts" "$T/src"
cat > "$T/packages/demo/tests/demo.test.mjs" <<'EOF'
import test from 'node:test';
import assert from 'node:assert/strict';
test('demo', () => assert.notEqual(process.env.LW_FAIL, 'pkg'));
EOF
cat > "$T/packages/queue/tests/never.test.mjs" <<'EOF'
import test from 'node:test';
test('the gate runs no pkg-queue step', () => { throw new Error('pkg-queue ran'); });
EOF
cat > "$T/scripts/test-demo.sh" <<'EOF'
#!/usr/bin/env bash
[ "${LW_FAIL:-}" != script ] || exit 1
[ -z "${LW_DIRTY:-}" ] || echo "during the gate" >> "$LW_DIRTY"
if [ -n "${LW_MOVE_BASE:-}" ]; then
moved="$(git -C "$LW_MOVE_BASE" commit-tree "refactor^{tree}" -p refactor -m moved)" \
&& git -C "$LW_MOVE_BASE" update-ref refs/heads/refactor "$moved"
fi
echo "ℹ pass 1"
EOF
echo 'echo legacy' > "$T/src/legacy.sh"
echo node_modules > "$T/.gitignore"
git -C "$T" add -A && git -C "$T" commit -q -m base
MB="$(git -C "$T" rev-parse HEAD)"
# ---- the coder's commits, pushed to the working repository ----
git clone -q "$T" "$P/coder"
CO="$P/coder"
commit_on() { # message: commit what's changed in the coder clone, print the id
git -C "$CO" add -A && git -C "$CO" commit -q -m "$1" && git -C "$CO" rev-parse HEAD
}
git -C "$CO" checkout -q -b w42 "$MB"
printf 'line1\nline2 changed\nline3\n' > "$CO/README.md"
echo feature > "$CO/packages/demo/feature.txt"
W_OK="$(commit_on w42)"
git -C "$CO" checkout -q -b w43 "$MB"; echo notes > "$CO/notes.txt"
W_ROOT="$(commit_on w43)"
git -C "$CO" checkout -q -b w44 "$MB"; mkdir "$CO/tools2"; echo x > "$CO/tools2/x.txt"
W_NEWDIR="$(commit_on w44)"
git -C "$CO" checkout -q -b w45 "$MB"; echo 'export {};' > "$CO/src/parse-requirement-id.mjs"
W_SRC="$(commit_on w45)"
git -C "$CO" checkout -q --orphan w46; git -C "$CO" rm -rfq .; echo o > "$CO/o.txt"
W_ORPHAN="$(commit_on w46)"
git init -q --bare "$P/working"
for pair in "w42:42" "w43:43" "w44:44" "w45:45" "w46:46"; do
git -C "$CO" push -q "$P/working" "${pair%%:*}:refs/heads/work/vikunja-7-${pair#*:}"
done
git -C "$CO" push -q "$P/working" "$MB:refs/heads/work/vikunja-7-47"
# The base moves on after the review, on a path the work doesn't touch.
echo docs > "$T/docs.txt"
git -C "$T" add docs.txt && git -C "$T" commit -q -m "base moves"
OLD="$(git -C "$T" rev-parse HEAD)"
mkdir "$T/node_modules"
echo canary > "$T/node_modules/canary" # cleanup must not empty the linked directory
# ---- bus rows: one decision and its verdicts, in the live rows' shape ----
D="d0000000-0000-4000-8000-000000000001"
V="e0000000-0000-4000-8000-000000000002"
cat > "$SANDBOX/gen.mjs" <<'EOF'
const [variant, p, t, commit] = process.argv.slice(2);
const D = 'd0000000-0000-4000-8000-000000000001';
const L = 'd0000000-0000-4000-8000-000000000009';
const MERGE = 'git.merge.protected';
const biz = variant === 'other-business' ? 'other' : 'fx';
const ref = `work/vikunja-${p}-${t}`;
const task = `vikunja:${p}/${t}`;
const action = variant === 'not-a-merge' ? 'git.push.protected' : MERGE;
// Row 139 (#1633, PRD 1.5 REQ-DEC-2): a merge decision the arbiter resolved, raised cross-role by
// merge-gate or gated with a spec citation by a coder. land-work still lands only a human's.
const arbiter = { 'arbiter-cross': ['merge-gate', 'cross-role'], 'arbiter-cited': ['coder', 'gated'] }[variant];
const raiser = arbiter?.[0] ?? 'pm';
const question =
{
'two-refs': `Squash-merge ${ref} and work/vikunja-7-99 into refactor?`,
'bad-ref': 'Squash-merge work/feature-x into refactor?',
}[variant] ?? `Squash-merge ${ref} into refactor? It carries task ${p}/${t}, approved by the reviewer.`;
const options = [
{ key: 'yes', text: `Squash-merge ${ref} into refactor` },
{ key: 'no', text: "Don't merge yet" },
];
const rows = [
{
table: 'decisions', id: D, business: biz, project: null, action, question, options,
...(variant === 'task-ref-disagrees' ? { task_ref: 'vikunja:7/99' } : {}),
...(arbiter ? { raised_by_role: raiser, class: arbiter[1], route_to: 'cto' } : {}),
},
];
// An earlier merge decision on the same ref, withdrawn before D was raised:
// only a later one supersedes D.
const E = 'd0000000-0000-4000-8000-000000000008';
if (variant === 'earlier-merge') {
rows.unshift({ table: 'decisions', id: E, business: biz, project: null, action: MERGE, task_ref: task, question, options });
rows.push({ table: 'decision_events', decision: E, op: 'withdrawn', by: 'pm', choice: null, via: null });
}
if (variant !== 'no-raise')
rows.push({
table: 'events', business: biz, kind: 'action.allowed', actor_role: raiser, actor_run: 'rfixture', subject: D,
body: { decision: D, action, operation: 'decision.raise',
task_ref: variant === 'raise-task-disagrees' ? 'vikunja:7/99' : null,
target: variant === 'target-main' ? 'main' : 'refactor', approvalChoice: 'yes', holderRun: null },
});
const choice = variant === 'chose-no' ? 'no' : 'yes';
if (variant === 'withdrawn') rows.push({ table: 'decision_events', decision: D, op: 'withdrawn', by: 'pm', choice: null, via: null });
else if (variant !== 'open') {
rows.push({ table: 'decision_events', decision: D, op: 'resolved', by: variant === 'by-agent' ? 'pm' : arbiter ? 'cto' : 'jason', choice,
via: variant === 'via-webui' ? 'webui' : arbiter ? 'broker' : 'cli' });
const answer = {
table: 'events', business: biz, kind: 'human.input', subject: D,
actor_role: variant === 'answer-role' ? 'pm' : null, actor_run: variant === 'answer-run' ? 'rfixture' : null,
body: { kind: 'answer', decision: D, op: 'resolved', choice: variant === 'answer-choice' ? 'no' : choice },
};
if (variant !== 'no-answer' && !arbiter) rows.push(answer);
if (variant === 'two-answers') rows.push({ ...answer });
}
const later = {
'superseded-question': { action: MERGE, question: `Hold ${ref} back after all?` },
'superseded-task': { action: MERGE, task_ref: task, question: `Hold task ${p}/${t} back after all?` },
'superseded-by': { action: 'scope.change', supersedes: D, question: 'Replace the earlier merge decision' },
}[variant];
if (later) rows.push({ table: 'decisions', id: L, business: biz, project: null, options, ...later });
const verdict = (id, v, c, body = {}) => ({
table: 'events', id, business: biz, kind: 'review.verdict', actor_role: 'reviewer', actor_run: 'rrev', subject: task,
body: { task_ref: task, request: 'f0000000-0000-4000-8000-000000000003', commit: c, verdict: v, message: 'fixture', ...body },
});
// An earlier changes verdict, as 19/265 had, so "latest" is what's read.
if (variant !== 'no-verdict') {
rows.push(verdict('e0000000-0000-4000-8000-000000000001', 'changes', '1'.repeat(40)));
rows.push(verdict('e0000000-0000-4000-8000-000000000002', 'approve', variant === 'short-commit' ? commit.slice(0, 12) : commit,
variant === 'wrong-task-body' ? { task_ref: 'vikunja:7/99' } : {}));
}
if (variant === 'changes-later') rows.push(verdict('e0000000-0000-4000-8000-000000000004', 'changes', commit));
process.stdout.write(JSON.stringify(rows));
EOF
# ---- per case: a copy of the target and the working repository, and a bus ----
setup() { # name variant task commit
local c="$SANDBOX/c/$1"
mkdir -p "$c/businesses" "$c/tmp" "$c/data/working/fx"
chmod 700 "$c/data"
cp -a "$T" "$c/target"
cp -a "$P/working" "$c/data/working/fx/stack.git"
printf '{"configVersion":1,"environment":"development","dataRoot":"%s","execution":{"backend":"docker","provider":"zai","model":"m","adapter":"mock"}}\n' \
"$c/data" > "$c/config.json"
printf '{"id":"fx","human":"jason","projects":{"stack":{"root":"%s"}}}\n' "$c/target" > "$c/businesses/fx.json"
node "$SANDBOX/gen.mjs" "$2" 7 "$3" "$4" > "$c/rows.json" \
&& node scripts/land-work/fixture.mjs "$c/data" "$c/rows.json" \
|| echo "setup failed for $1" >&2
}
lw() { # name args...
local c="$SANDBOX/c/$1"
shift
MOSAIC_CONFIG="$c/config.json" TMPDIR="$c/tmp" PATH="${LW_PATH:+$LW_PATH:}$SANDBOX/bin:$PATH" \
bash scripts/land-work.sh "$@" > "$c/out" 2> "$c/err"
}
expect() { # name rc reason args...: run land-work, check the exit and the refusal
local name="$1" want="$2" reason="$3" rc=0
shift 3
lw "$name" "$@" || rc=$?
if [ "$rc" -eq "$want" ] && { [ -z "$reason" ] || grep -q "refused: $reason\b" "$SANDBOX/c/$name/err"; }; then
PASS=$((PASS+1)); echo "${C_OK}OK${C_RESET} $name: exit $rc${reason:+, $reason}"
else
FAIL=$((FAIL+1)); echo "${C_FAIL}FAIL${C_RESET} $name: exit $rc, wanted $want${reason:+ $reason}: $(tail -n2 "$SANDBOX/c/$name/err" | tr '\n' ' ')"
fi
}
cleaned() { # name: one worktree left in the target, no scratch left in TMPDIR
local c="$SANDBOX/c/$1"
[ "$(git -C "$c/target" worktree list --porcelain | grep -c '^worktree ')" -eq 1 ] \
&& [ -z "$(find "$c/tmp" -maxdepth 1 -name 'land-work.*')" ]
}
# ---- arguments and configuration ----
setup args ok 42 "$W_OK"
expect args 2 target-branch "$D" --base next
expect args 2 target-branch "$D" --base main
expect args 4 "" "bad/../id"
expect args 4 "" "$D" --unknown
expect args 4 "" "$D" --subject $'two\nlines'
rc=0; MOSAIC_CONFIG="$SANDBOX/missing.json" TMPDIR="$SANDBOX/c/args/tmp" bash scripts/land-work.sh "$D" --base next >/dev/null 2>&1 || rc=$?
check "--base next refuses before the config is read (got $rc)" "$([ "$rc" -eq 2 ]; echo $?)"
rc=0; MOSAIC_CONFIG="$SANDBOX/missing.json" TMPDIR="$SANDBOX/c/args/tmp" bash scripts/land-work.sh "$D" >/dev/null 2>&1 || rc=$?
check "a missing system config exits 3 (got $rc)" "$([ "$rc" -eq 3 ]; echo $?)"
mkdir -p "$SANDBOX/c/nobus/data/working/fx" "$SANDBOX/c/nobus/businesses" "$SANDBOX/c/nobus/tmp"
chmod 700 "$SANDBOX/c/nobus/data"
cp "$SANDBOX/c/args/businesses/fx.json" "$SANDBOX/c/nobus/businesses/"
sed "s#$SANDBOX/c/args/data#$SANDBOX/c/nobus/data#" "$SANDBOX/c/args/config.json" > "$SANDBOX/c/nobus/config.json"
expect nobus 3 "" "$D"
# ---- the bus database ----
setup schema-table ok 42 "$W_OK"
node -e 'const {DatabaseSync}=require("node:sqlite"); const db=new DatabaseSync(process.argv[1]); db.exec("CREATE TABLE extra(x)"); db.close()' \
"$SANDBOX/c/schema-table/data/bus/bus.sqlite"
expect schema-table 2 bus-schema "$D"
setup schema-meta ok 42 "$W_OK"
# The same schema, so the digest matches, with meta saying another version.
node -e '
const {DatabaseSync}=require("node:sqlite"); const db=new DatabaseSync(process.argv[1]);
const sql=db.prepare("SELECT sql FROM sqlite_master WHERE name=?").get("meta_no_update").sql;
db.exec("DROP TRIGGER meta_no_update"); db.exec("UPDATE meta SET value=\x273a\x27 WHERE key=\x27schema_version\x27"); db.exec(sql); db.close()' \
"$SANDBOX/c/schema-meta/data/bus/bus.sqlite"
expect schema-meta 2 bus-schema "$D"
rc=0
node --input-type=module -e '
import { openBus } from "./scripts/land-work/read.mjs";
const db = openBus(process.argv[1]);
try { db.exec("CREATE TABLE x(y)"); process.exit(1); } catch (e) { process.exit(/readonly/.test(e.message) ? 0 : 1); }' \
"$SANDBOX/c/args/data" || rc=$?
check "openBus opens the database read-only" "$rc"
# ---- the decision ----
for v in "decision-not-found:other-business" "decision-not-found:no-raise" "not-a-merge:not-a-merge" \
"not-resolved:open" "not-resolved:withdrawn" "not-human:by-agent" "not-human:via-webui" \
"not-human:no-answer" "not-human:answer-role" "not-human:answer-run" "not-human:answer-choice" \
"not-human:two-answers" "not-human:arbiter-cross" "not-human:arbiter-cited" "not-merge-option:chose-no" "work-ref:two-refs" "work-ref:bad-ref" \
"work-ref:task-ref-disagrees" "work-ref:raise-task-disagrees" "target-branch:target-main" "superseded:superseded-question" \
"superseded:superseded-task" "superseded:superseded-by" "no-verdict:no-verdict" \
"not-approved:changes-later" "not-approved:short-commit" "not-approved:wrong-task-body"; do
setup "${v#*:}" "${v#*:}" 42 "$W_OK"
expect "${v#*:}" 2 "${v%%:*}" "$D"
done
expect args 2 decision-not-found "d0000000-0000-4000-8000-00000000000f"
# A withdrawn earlier merge decision on the same ref doesn't supersede D.
setup earlier-merge earlier-merge 42 "$W_OK"
expect earlier-merge 0 "" "$D" --dry-run
# ---- the work ref and the reviewed diff ----
setup ref-moved ok 42 "$W_OK"
git -C "$SANDBOX/c/ref-moved/data/working/fx/stack.git" update-ref refs/heads/work/vikunja-7-42 "$W_ROOT"
expect ref-moved 2 ref-moved "$D"
setup root-top ok 43 "$W_ROOT"
expect root-top 2 root-file "$D"
setup root-newdir ok 44 "$W_NEWDIR"
expect root-newdir 2 root-file "$D"
setup root-src ok 45 "$W_SRC"
expect root-src 2 root-file "$D"
setup unrelated ok 46 "$W_ORPHAN"
expect unrelated 2 base-conflict "$D"
setup empty ok 47 "$MB"
expect empty 2 base-conflict "$D"
grep -q "is empty" "$SANDBOX/c/empty/err"
check "an empty reviewed diff says so" "$?"
# The base is checked out in the target: a reviewed path there must be clean.
setup dirty-changed ok 42 "$W_OK"
echo local >> "$SANDBOX/c/dirty-changed/target/README.md"
expect dirty-changed 2 checkout-dirty "$D"
setup dirty-staged ok 42 "$W_OK"
echo local >> "$SANDBOX/c/dirty-staged/target/README.md"
git -C "$SANDBOX/c/dirty-staged/target" add README.md
printf 'line1\nline2\nline3\n' > "$SANDBOX/c/dirty-staged/target/README.md"
expect dirty-staged 2 checkout-dirty "$D"
setup dirty-untracked ok 42 "$W_OK"
echo local > "$SANDBOX/c/dirty-untracked/target/packages/demo/feature.txt"
expect dirty-untracked 2 checkout-dirty "$D"
setup conflict ok 42 "$W_OK"
printf 'line1\nline2 on the base\nline3\n' > "$SANDBOX/c/conflict/target/README.md"
git -C "$SANDBOX/c/conflict/target" commit -q -am "base edits the reviewed line"
expect conflict 2 base-conflict "$D"
check "a refusal after the worktree is added leaves no worktree or scratch" "$(cleaned conflict; echo $?)"
grep -q "network inspect lw-" "$SANDBOX/docker.log" 2>/dev/null
check "cleanup looks for the worktree's Docker network" "$?"
setup dirty-during ok 42 "$W_OK"
LW_DIRTY="$SANDBOX/c/dirty-during/target/README.md" expect dirty-during 2 checkout-dirty "$D"
check "a reviewed path dirtied during the gate moves no ref" \
"$([ "$(git -C "$SANDBOX/c/dirty-during/target" rev-parse refactor)" = "$OLD" ]; echo $?)"
setup drift ok 42 "$W_OK"
LW_PATH="$SANDBOX/tamper" expect drift 2 patch-drift "$D"
check "patch-drift leaves no worktree or scratch" "$(cleaned drift; echo $?)"
# ---- the gate ----
setup gate-pkg ok 42 "$W_OK"
LW_FAIL=pkg expect gate-pkg 2 gate-failed "$D"
log="$(sed -n 's/.*gate-failed: pkg-demo, see \(.*\)$/\1/p' "$SANDBOX/c/gate-pkg/err")"
check "gate-failed names the failing step's log, and it exists" "$([ -n "$log" ] && [ -s "$log" ]; echo $?)"
check "gate-failed leaves no worktree or scratch" "$(cleaned gate-pkg; echo $?)"
setup gate-script ok 42 "$W_OK"
LW_FAIL=script expect gate-script 2 gate-failed "$D"
grep -q "gate-failed: script-demo" "$SANDBOX/c/gate-script/err"
check "a failing scripts/test-*.sh fails the gate" "$?"
check "a failed gate moves no ref" "$([ "$(git -C "$SANDBOX/c/gate-script/target" rev-parse refactor)" = "$OLD" ]; echo $?)"
setup nomods ok 42 "$W_OK"
rm -rf "$SANDBOX/c/nomods/target/node_modules"
expect nomods 4 "" "$D"
check "a target without node_modules leaves no worktree or scratch" "$(cleaned nomods; echo $?)"
setup moved ok 42 "$W_OK"
git -C "$SANDBOX/c/moved/target" checkout -q -b side
LW_MOVE_BASE="$SANDBOX/c/moved/target" expect moved 2 base-moved "$D"
check "base-moved keeps the base where it moved to" \
"$([ "$(git -C "$SANDBOX/c/moved/target" log -1 --format=%s refactor)" = moved ]; echo $?)"
# ---- a dry run ----
setup dry ok 42 "$W_OK"
before="$(git -C "$SANDBOX/c/dry/target" for-each-ref; git -C "$SANDBOX/c/dry/target" reflog refactor; git -C "$SANDBOX/c/dry/target" status --porcelain)"
expect dry 0 "" "$D" --dry-run --subject "Custom subject" --attribution "Applied-under: suite (conductor)"
after="$(git -C "$SANDBOX/c/dry/target" for-each-ref; git -C "$SANDBOX/c/dry/target" reflog refactor; git -C "$SANDBOX/c/dry/target" status --porcelain)"
check "--dry-run writes no ref" "$([ "$before" = "$after" ]; echo $?)"
grep -q "no ref was written" "$SANDBOX/c/dry/out" && grep -qx " Custom subject" "$SANDBOX/c/dry/out" \
&& grep -qx " Applied-under: suite (conductor)" "$SANDBOX/c/dry/out"
check "--dry-run prints the commit with --subject and --attribution" "$?"
grep -q "^GATE_PASSED" "$SANDBOX/c/dry/err"
check "--dry-run runs the gate" "$?"
check "--dry-run leaves no worktree or scratch" "$(cleaned dry; echo $?)"
DRY_TREE="$(sed -n 's/.*would commit tree \([0-9a-f]*\) .*/\1/p' "$SANDBOX/c/dry/out")"
# ---- a landing ----
setup land ok 42 "$W_OK"
L="$SANDBOX/c/land"
echo "local edit" >> "$L/target/docs.txt"
BUS_BEFORE="$(sha256sum "$L/data/bus/bus.sqlite")"
: > "$SANDBOX/docker.log"
FAKE_NET_EXISTS=1 expect land 0 "" "$D"
NEW="$(git -C "$L/target" rev-parse refactor)"
check "the landing is one commit on the old base" "$([ "$(git -C "$L/target" rev-parse "$NEW^")" = "$OLD" ]; echo $?)"
pid() { git -C "$L/target" diff "$1" "$2" | git patch-id --stable | cut -d' ' -f1; }
check "its diff is the reviewed diff by patch-id" "$([ "$(pid "$OLD" "$NEW")" = "$(pid "$MB" "$W_OK")" ]; echo $?)"
check "its tree is the dry run's" "$([ -n "$DRY_TREE" ] && [ "$(git -C "$L/target" rev-parse "$NEW^{tree}")" = "$DRY_TREE" ]; echo $?)"
msg="$(git -C "$L/target" log -1 --format=%B "$NEW")"
PATCH_ID="$(pid "$MB" "$W_OK")"
ok=0
[ "$(head -n1 <<<"$msg")" = "Squash-merge work/vikunja-7-42 into refactor" ] || ok=1
for line in "Decision: $D" "Verdict: $V" "Work-ref: work/vikunja-7-42" "Reviewed-commit: $W_OK" \
"Base: refactor $OLD" "Patch-id: $PATCH_ID"; do
grep -qxF "$line" <<<"$msg" || ok=1
done
[ "$(sed '/^$/d' <<<"$msg" | tail -n1)" = "Applied-under: scripts/land-work.sh (conductor)" ] || ok=1
check "the message has the option's subject, the citations and the attribution last" "$ok"
check "it prints the line for the PM" \
"$([ "$(tail -n1 "$L/out")" = "work/vikunja-7-42 (vikunja:7/42) is merged to refactor as ${NEW:0:9}; push pending." ]; echo $?)"
check "the checked-out base has the reviewed paths and keeps the unrelated edit" \
"$([ "$(git -C "$L/target" status --porcelain)" = " M docs.txt" ] && [ -f "$L/target/packages/demo/feature.txt" ]; echo $?)"
check "only refs/heads/refactor exists in the target" \
"$([ "$(git -C "$L/target" for-each-ref --format='%(refname)')" = refs/heads/refactor ]; echo $?)"
check "the work ref is unchanged" \
"$([ "$(git -C "$L/data/working/fx/stack.git" rev-parse refs/heads/work/vikunja-7-42)" = "$W_OK" ]; echo $?)"
check "the bus database is unchanged" "$([ "$(sha256sum "$L/data/bus/bus.sqlite")" = "$BUS_BEFORE" ]; echo $?)"
logs="$(sed -n 's/^land-work: gate logs in //p' "$L/err")"
grep -q "^pkg-demo rc=0" "$logs/gate.log" && grep -q "^script-demo rc=0" "$logs/gate.log" \
&& grep -q "^GATE_PASSED" "$logs/gate.log" && ! grep -q "pkg-queue" "$logs/gate.log"
check "the gate ran each package suite but queue, and each scripts/test-*.sh" "$?"
check "the landing leaves no worktree or scratch" "$(cleaned land; echo $?)"
check "the target's node_modules is still there, with its contents" \
"$([ -d "$L/target/node_modules" ] && [ ! -L "$L/target/node_modules" ] && [ -f "$L/target/node_modules/canary" ]; echo $?)"
grep -q "^docker network rm lw-[0-9]*-[0-9]*_default$" "$SANDBOX/docker.log"
check "the worktree's Docker network is removed" "$?"
echo "---"
echo "land-work selftests: $PASS passed, $FAIL failed"
[ "$FAIL" -eq 0 ]