Candidate manifest 8b4ab31014a124ec0e3abba44238976d6f4c9af48cbdb36c2c77d18f0137f888 from Darkwing's round 2 packet (0a4076e8e), on BASE1223a0f37. No manifest path changed between BASE and the landing target. Filbert approved round 2 in 28468, closing C1 and D1 from round 1 (28464). Sage reran the full gate at the landing target with the candidate applied. The replay acceptance follows ruling sage-0204. Co-Authored-By: Claude Opus 5.5 <[email protected]>
405 lines
21 KiB
Bash
405 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;
|
||
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' } : {}),
|
||
},
|
||
];
|
||
// 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: 'pm', 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' : 'jason', choice,
|
||
via: variant === 'via-webui' ? 'webui' : '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') 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-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 ]
|