Row 139 round 2, Dewey's candidate a4c5657f (14 paths) on5661bd233. 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 on26b78d483plus 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]>
410 lines
21 KiB
Bash
410 lines
21 KiB
Bash
#!/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 ]
|