#!/usr/bin/env bash # Tests for delegate, against a throwaway jj repo, with opencode stubbed and # the real `workspace` and bwrap. Run from devkit: bash delegate/test set -euo pipefail src="$(cd "$(dirname "$0")/.." && pwd)" T="$(mktemp -d "${TMPDIR:-/tmp}/delegate-test.XXXXXX")" trap 'rm -rf "$T"' EXIT pass=0 fail=0 ok() { pass=$((pass + 1)); echo "ok $*"; } bad() { fail=$((fail + 1)); echo "FAIL $*"; } # expect CODE DESC -- CMD...: CMD exits with CODE. expect() { local want=$1 desc=$2 rc=0; shift 3 "$@" >"$T/out" 2>"$T/err" || rc=$? if [ "$rc" = "$want" ]; then ok "$desc"; else bad "$desc (exit $rc, want $want)"; sed 's/^/ /' "$T/err" | tail -n 15; fi } has() { if grep -q -- "$2" <<<"$3"; then ok "$1"; else bad "$1: no '$2' in:"; sed 's/^/ /' <<<"$3"; fi; } hasnt() { if grep -q -- "$2" <<<"$3"; then bad "$1: '$2' in:"; sed 's/^/ /' <<<"$3"; else ok "$1"; fi; } yes_() { if eval "$2"; then ok "$1"; else bad "$1 ($2)"; fi; } J() { jj --no-pager --ignore-working-copy "$@"; } desc() { J log --no-graph -r "$1" -T description; } cid() { J log --no-graph -r "$1" -T commit_id; } # land_one OLD DESC: trunk got exactly one change (one task == one commit). land_one() { [ "$(cid main-)" = "$1" ] && ok "$2: one change on main" || bad "$2: main- isn't the old main"; } # The stub: runs $FAKE_SCRIPT (from a file named by $FAKE) in the task's # directory, then prints a report event that also says whether the prompt # had a fix section. mkdir -p "$T/bin" "$T/proj" "$T/state" ln -s "$src/workspace/workspace" "$T/bin/workspace" ln -s "$src/delegate/delegate" "$T/bin/delegate" cat >"$T/bin/fake-opencode" <<'EOF' #!/usr/bin/env bash case $1 in session) # export SID: answer with $FAKE_USED, else the model run was given. m="$(cat "$FAKE_STATE/model" 2>/dev/null)"; m=${FAKE_USED:-$m} printf '{"messages":[{"type":"assistant","model":{"providerID":"%s","id":"%s"}}]}\n' "${m%%/*}" "${m#*/}" exit 0 ;; run) ;; *) exit 2 ;; esac args=("$@") for i in "${!args[@]}"; do [ "${args[$i]}" = --model ] && printf '%s' "${args[$((i + 1))]}" >"$FAKE_STATE/model"; done prompt="$(cat)" printf '%s\n' "$prompt" >"$FAKE_STATE/prompt" fixed=no grep -q '^## Fix this round' <<<"$prompt" && fixed=yes [ -z "${FAKE_EARLY:-}" ] || printf '{"type":"text","sessionID":"ses_fake","part":{"text":"EARLY %s"}}\n' "$FAKE_EARLY" [ -z "${FAKE:-}" ] || bash "$FAKE" >&2 # Many events, each with the session ID (a long session). [ -z "${FAKE_SPAM:-}" ] || yes '{"type":"step_start","sessionID":"ses_fake","part":{}}' | head -n "$FAKE_SPAM" printf '{"type":"text","sessionID":"ses_fake","part":{"text":"REPORT fix=%s args=%s"}}\n' "$fixed" "$*" EOF chmod +x "$T/bin/fake-opencode" # devenv, only for re-running delegate in a "fresh" shell (DELEGATE_DEVENV=0 # keeps it out of tasks). cat >"$T/bin/devenv" <<'EOF' #!/usr/bin/env bash [ "$1" = shell ] && [ "$2" = -- ] || exit 2 shift 2 echo ran >>"$FAKE_STATE/devenv" if grep -qs broken broken.nix; then for i in $(seq 80); do printf '\033[1;31m… while evaluating\033[0m frame %s\n' "$i"; done printf '\033[1;31merror:\033[0m src is an alias that throws\n' exit 1 fi exec "$@" EOF chmod +x "$T/bin/devenv" # (The sandbox hides /tmp, where the stub lives.) export DELEGATE_SANDBOX_EXTRA="--ro-bind $T/bin $T/bin --bind $T/state $T/state" # Language caches under $T; secret/ stands for the rest of $HOME's caches. mkdir -p "$T/cache/gleam" "$T/cache/secret" "$T/data/pnpm/store" "$T/gomod" "$T/gobuild" echo s >"$T/cache/secret/x" export XDG_CACHE_HOME="$T/cache" XDG_DATA_HOME="$T/data" GOMODCACHE="$T/gomod" GOCACHE="$T/gobuild" export PATH="$T/bin:$PATH" DELEGATE_OPENCODE="$T/bin/fake-opencode" DELEGATE_DEVENV=0 DELEGATE_CHECK="" DELEGATE_MODEL=fake/model \ DELEGATE_AGENT_DEST="$T/agents/delegate.md" FAKE_STATE="$T/state" fake() { printf "%s\n" "$1" >"$T/bin/fake.sh"; export FAKE="$T/bin/fake.sh"; } mkdir -p "$T/proj/sib" && echo hi >"$T/proj/sib/x" repo="$T/proj/repo" mkdir -p "$repo" && cd "$repo" jj git init >/dev/null 2>&1 printf 'one\ntwo\nthree\n' >shared.txt printf 'def add(a, b):\n return a - b\n' >calc.py printf '.devenv\n' >.gitignore jj commit -m init >/dev/null 2>&1 jj bookmark create main -r @- >/dev/null 2>&1 echo "# start / run / diff / report" expect 0 "start a" -- delegate start a <<<"Fix add() in calc.py" ws_a="$(cat "$T/out")" [ "$ws_a" = "$T/proj/repo-t-a" ] && ok "start prints the root" || bad "start printed '$ws_a'" [ ! -e "$ws_a/.git" ] && ok "task workspace isn't colocated" || bad "task workspace has .git" has "spec carries the trailer" "Delegate-Task: t-a" "$(desc t-a@-)" has "status: idle, no rounds" "^a idle 0 ok clean 0$" "$(delegate status --files)" expect 1 "report before any round" -- delegate report a fake 'sed -i "s/a - b/a + b/" calc.py' expect 0 "run a" -- delegate run a has "report on stdout" "REPORT fix=no" "$(cat "$T/out")" has "model passed" "--model fake/model" "$(cat "$T/out")" has "sandboxed runs are standalone" "--standalone" "$(cat "$T/out")" has "files on stderr" "calc.py" "$(cat "$T/err")" has "round 1 is its own change" "^round 1: spec" "$(desc t-a@-)" yes_ "spec change holds no files" '[ -z "$(J diff -r t-a@-- --name-only)" ]' has "status: 1 round, 1 file" "^a idle 1 ok clean 1$" "$(delegate status --files)" has "status --files lists it" "^ M calc.py$" "$(delegate status --files)" has "diff spans the task" "a + b" "$(delegate diff a)" has "report kept in the repo" "REPORT fix=no" "$(delegate report a)" has "status counts files" "^a idle 1 ok clean 1$" "$(delegate status)" hasnt "report without the trailer" "Delegate-Round" "$(delegate report a)" fake 'echo "# sum" >>calc.py' expect 0 "fix round" -- delegate run a <<<"Add a comment details" has "fix section in the prompt" "REPORT fix=yes" "$(cat "$T/out")" has "round named after the fix's first line" "^round 2: Add a comment" "$(desc t-a@-)" has "round keeps the fix" "details" "$(desc t-a@-)" has "diff --last is the last round only" "# sum" "$(delegate diff a --last)" echo x >"$ws_a/extra.txt" has "diff limited to a path" "calc.py" "$(delegate diff a --name-only -- calc.py)" hasnt "diff limited to a path: nothing else" "extra.txt" "$(delegate diff a --name-only calc.py)" has "diff --name-only" "extra.txt" "$(delegate diff a --name-only)" rm "$ws_a/extra.txt" hasnt "diff --last excludes round 1" "^+ return a + b" "$(delegate diff a --last)" has "status: 2 rounds" "^a idle 2 " "$(delegate status --files)" has "report: latest round" "REPORT fix=yes" "$(delegate report a)" has "report --round 1" "REPORT fix=no" "$(delegate report a --round 1)" expect 1 "report: no round 3" -- delegate report a --round 3 echo "by hand" >"$ws_a/hand.txt" fake 'echo r3 >r3.txt' expect 0 "a round after hand edits" -- delegate run a <<<"Round three" has "says it kept the hand edits apart" "kept the edits made in t-a's workspace" "$(cat "$T/err")" hasnt "the round doesn't list them" "hand.txt" "$(sed -n '/changed 1 files/,$p' "$T/err")" has "the round lists its own" "A r3.txt" "$(cat "$T/err")" has "round numbering skips the edits" "^round 3: Round three" "$(desc t-a@-)" has "the edits are their own change" "^edits by hand between rounds" "$(desc t-a@--)" has "with the hand file" "hand.txt" "$(J diff -r t-a@-- --name-only)" has "status counts 3 rounds" "^a idle 3 " "$(delegate status)" has "report --round 3 is round 3" "REPORT fix=yes" "$(delegate report a --round 3)" hasnt "diff --last is round 3 only" "hand.txt" "$(delegate diff a --last --name-only)" has "diff has both" "hand.txt" "$(delegate diff a --name-only)" [ -f "$T/agents/delegate.md" ] && ok "agent installed before the run" || bad "agent not installed" p="$(cat "$T/state/prompt")" has "rule: conflicting requirements" "When two requirements conflict" "$p" has "rule: main checkout readable" "coordinator's checkout at $repo" "$p" has "rule: no repo, no nix builds" "nix build, nix flake check" "$p" has "rule: lockfiles are fine" "Editing manifests and lockfiles" "$p" has "rule: nix edits don't reach the shell" "doesn't reach it" "$p" hasnt "no check rule without a check" "run exactly" "$p" echo "# model" fake 'true' FAKE_USED=other/model expect 1 "a different model answering fails the run" -- delegate run a has "names both models" "asked for fake/model#default but .* other/model#default" "$(cat "$T/err")" has "the round is marked untrusted" "^Untrusted: " "$(desc t-a@-)" DELEGATE_MODEL= expect 2 "no model refuses to run" -- delegate run a has "a refused run leaves no round" "^a idle 4 " "$(delegate status --files)" expect 0 "--model overrides" -- delegate run a --model fake/other has "--model passed" "--model fake/other" "$(cat "$T/out")" fake 'true' expect 0 "empty round" -- delegate run a <<<"Nothing to do" has "an empty round still counts" "^a idle 6 " "$(delegate status --files)" has "and keeps its report" "REPORT fix=yes" "$(delegate report a)" has "says it changed nothing" "changed nothing" "$(cat "$T/err")" echo "# sandbox" delegate start z <<<"another task" >/dev/null 2>&1 echo work >"$T/proj/repo-t-z/z.txt" fake 'touch inside; touch ../escape ~/escape 2>/dev/null cat ../sib/x >/dev/null 2>&1 && touch saw-sib touch ../sib/w 2>/dev/null && touch wrote-sib cat ../repo/calc.py >/dev/null 2>&1 && touch saw-main touch ../repo/w 2>/dev/null && touch wrote-main [ -z "$(ls -A ../repo/.jj)" ] && touch main-jj-empty [ ! -d ../repo/.git ] || [ -z "$(ls -A ../repo/.git)" ] && touch main-git-empty [ ! -e ../repo-t-z/z.txt ] && touch other-task-hidden jj log >/dev/null 2>&1 && touch jj-works; git status >/dev/null 2>&1 && touch git-works [ "$GOMODCACHE" = "'"$T/gomod"'" ] && echo m >"$GOMODCACHE/m" && touch gomod [ "$GOCACHE" = "'"$T/gobuild"'" ] && echo b >"$GOCACHE/b" && touch gocache echo g >"$XDG_CACHE_HOME/gleam/g" && touch gleam echo p >"$XDG_DATA_HOME/pnpm/store/p" && touch pnpm [ ! -e "$XDG_CACHE_HOME/secret/x" ] && touch secret-hidden true' expect 0 "sandboxed run" -- delegate run a [ -e "$ws_a/inside" ] && ok "workspace is writable" || bad "workspace not writable" [ ! -e "$T/proj/escape" ] && [ ! -e "$HOME/escape" ] && ok "nothing written outside" || bad "wrote outside the workspace" for f in saw-sib saw-main main-jj-empty main-git-empty other-task-hidden gomod gocache gleam pnpm secret-hidden; do [ -e "$ws_a/$f" ] && ok "sandbox: $f" || bad "sandbox: no $f" done for f in wrote-sib wrote-main jj-works git-works; do [ ! -e "$ws_a/$f" ] && ok "sandbox: no $f" || bad "sandbox: $f"; done for f in gomod/m gobuild/b cache/gleam/g data/pnpm/store/p; do [ -e "$T/$f" ] && ok "cache write kept: $f" || bad "cache write lost: $f"; done delegate drop z >/dev/null 2>&1 echo "# long sessions and crashes" fake 'echo long >long.txt' FAKE_SPAM=200000 expect 0 "a long session (many events) doesn't crash" -- delegate run a <<<"long one" has "its report is printed" "REPORT fix=yes" "$(cat "$T/out")" has "and kept" "REPORT fix=yes" "$(delegate report a)" r0="$(delegate status | awk '$1 == "a" { print $3 }')" fake 'sleep 3; echo late >late.txt' delegate run a >/dev/null 2>"$T/err.bg" & bg=$! sleep 1 kill -TERM "$bg" rc=0; wait "$bg" || rc=$? [ "$rc" = 143 ] && ok "SIGTERM: exit 143" || bad "SIGTERM: exit $rc" has "SIGTERM: says it sealed the round" "sealed it" "$(cat "$T/err.bg")" has "SIGTERM: round sealed and marked" "^Interrupted: " "$(desc t-a@-)" has "SIGTERM: idle, one more round" "^a idle $((r0 + 1)) " "$(delegate status)" yes_ "SIGTERM: the round's edits are kept" '[ -n "$(J diff -r t-a@- --name-only)" ]' fake 'sleep 3' FAKE_EARLY=partial delegate run a >/dev/null 2>&1 & bg=$! sleep 1 kill -KILL "$bg"; wait "$bg" 2>/dev/null || true for _ in $(seq 40); do delegate status | grep -q '^a running' || break; sleep 0.25; done has "SIGKILL: status says interrupted" "^a interrupted $((r0 + 2)) " "$(delegate status)" has "SIGKILL: report recovers the last message" "EARLY partial" "$(delegate report a 2>&1)" has "SIGKILL: report says it was interrupted" "was interrupted" "$(delegate report a 2>&1)" expect 1 "SIGKILL: land refuses an interrupted round" -- delegate land a -m x fake 'true' expect 0 "the next run seals it first" -- delegate run a has "says so" "which was interrupted" "$(cat "$T/err")" has "SIGKILL: sealed round marked" "^Interrupted: " "$(desc t-a@--)" has "SIGKILL: sealed with the recovered message" "EARLY partial" "$(desc t-a@--)" has "and the new round follows" "^a idle $((r0 + 3)) " "$(delegate status)" echo "# locks and timeout" fake 'sleep 5' delegate run a >/dev/null 2>&1 & bg=$! sleep 1 has "status: running, no behind" "^a running [0-9]* - " "$(delegate status --files)" expect 75 "second run refused" -- delegate run a expect 75 "land refused while running" -- delegate land a -m x expect 75 "drop refused while running" -- delegate drop a wait $bg || true fake 'sleep 30' expect 124 "timeout exits 124" -- delegate run a --timeout 1s has "a timed-out round says so" "timed out" "$(delegate report a)" expect 0 "drop a" -- delegate drop a hasnt "dropped from status" "^a " "$(delegate status --files)" [ ! -e "$ws_a" ] && ok "workspace deleted" || bad "workspace still there" echo "# two tasks on the same lines" fake 'sed -i "s/two/TWO-b/" shared.txt' delegate start b <<<"b edits line two" >/dev/null 2>&1 delegate run b >/dev/null 2>&1 fake 'echo "b round 2" >b.txt' delegate run b <<<"also add b.txt" >/dev/null 2>&1 fake 'sed -i "s/two/TWO-c/" shared.txt' delegate start c <<<"c edits line two" >/dev/null 2>&1 delegate run c >/dev/null 2>&1 st="$(delegate status --files)" has "overlap listed" "shared.txt b,c\|shared.txt c,b" "$st" old="$(cid main)" expect 0 "land b (two rounds)" -- delegate land b -m "feat: b" land_one "$old" "land b" [ "$(desc main)" = "feat: b" ] && ok "landed change has only the message" || bad "main's description: $(desc main)" has "both rounds in it" "b.txt" "$(J diff -r main --name-only)" yes_ "coordinator's working copy follows main" '[ "$(jj log --no-graph -r @- -T commit_id)" = "$(cid main)" ]' has "says so" "moved your working copy" "$(cat "$T/err")" has "c conflicted after b landed" "^c idle 1 ok conflicted 1$" "$(delegate status --files)" expect 3 "land c refused: conflicts" -- delegate land c -m "feat: c" has "conflict names the trunk change" "feat: b" "$(cat "$T/err")" fake 'printf "one\nTWO-b-and-c\nthree\n" >shared.txt' expect 0 "resolve round" -- delegate run c <<<"Resolve the conflict in shared.txt" has "c clean after resolve" "^c idle 2 ok clean 1$" "$(delegate status --files)" echo wip >"$repo/wip.txt" old="$(cid main)" expect 0 "land c" -- delegate land c -m "feat: c" land_one "$old" "land c" has "c on main" "TWO-b-and-c" "$(J file show -r main shared.txt)" yes_ "a working copy with other edits is rebased onto main" '[ "$(jj log --no-graph -r @- -T commit_id)" = "$(cid main)" ] && [ "$(jj diff --name-only)" = wip.txt ]' has "says so" "rebased your working copy" "$(cat "$T/err")" hasnt "the edits didn't land" "wip.txt" "$(J diff -r main --name-only)" rm "$repo/wip.txt" echo "# commit on trunk" echo base >"$repo/base.txt" echo other >"$repo/other.txt" old="$(cid main)" expect 0 "commit one path on main" -- delegate commit -m "chore: base" base.txt land_one "$old" "commit" has "main is the commit" "chore: base" "$(desc main)" hasnt "only the path given" "other.txt" "$(J diff -r main --name-only)" yes_ "the rest stays in the working copy" '[ "$(jj log --no-graph -r @- -T commit_id)" = "$(cid main)" ] && [ -n "$(jj diff --name-only)" ]' rm "$repo/other.txt" expect 1 "commit refuses an empty working copy" -- delegate commit -m x jj new main- >/dev/null 2>&1 && echo x >"$repo/off.txt" expect 1 "commit refuses off main" -- delegate commit -m x has "hint: rebase" "jj rebase -b @ -d main" "$(cat "$T/err")" jj abandon @ >/dev/null 2>&1 && jj new main >/dev/null 2>&1 echo bump >"$repo/bump.txt" && jj commit -m "chore: my bump" >/dev/null 2>&1 echo x >"$repo/off.txt" expect 1 "commit refuses on your own commits above main" -- delegate commit -m x has "hint: names them" "chore: my bump" "$(cat "$T/err")" has "hint: move main up" "jj bookmark set main -r @-" "$(cat "$T/err")" hasnt "hint: not the no-op rebase" "jj rebase -b" "$(cat "$T/err")" rm "$repo/off.txt"; jj abandon @- >/dev/null 2>&1; jj new main >/dev/null 2>&1 echo "# check and preflight" export DELEGATE_CHECK='echo x >>"$FAKE_STATE/count"; cat ../sib/x >/dev/null' expect 0 "preflight: a check reading a sibling passes" -- delegate preflight has "says it passes" "passes on main" "$(cat "$T/err")" [ ! -e "$T/proj/repo-delegate-preflight" ] && ok "preflight workspace removed" || bad "preflight workspace left" : >"$T/state/count" delegate start p1 <<<"p1" >/dev/null 2>&1 delegate start p2 <<<"p2" >/dev/null 2>&1 [ "$(wc -l <"$T/state/count")" = 0 ] && ok "cached: starts don't re-run it" || bad "preflight ran $(wc -l <"$T/state/count") times" expect 0 "preflight --force runs it again" -- delegate preflight --force [ "$(wc -l <"$T/state/count")" = 1 ] && ok "ran once" || bad "ran $(wc -l <"$T/state/count") times" delegate drop p1 >/dev/null 2>&1; delegate drop p2 >/dev/null 2>&1 export DELEGATE_CHECK='cat "$XDG_CACHE_HOME/secret/x"' expect 5 "start refused: check fails on trunk in the sandbox" -- delegate start q <<<"q" has "says why" "fails on main itself" "$(cat "$T/err")" has "shows the check's output" "secret/x" "$(cat "$T/err")" [ ! -e "$T/proj/repo-t-q" ] && ok "no workspace left" || bad "t-q left behind" expect 0 "--no-preflight starts anyway" -- delegate start q --no-preflight <<<"q" fake 'true' delegate run q >/dev/null 2>&1 has "rule: run exactly the check" 'run exactly `cat' "$(cat "$T/state/prompt")" has "rule: stop when it isn't yours" "don't work around it" "$(cat "$T/state/prompt")" expect 5 "land: trunk fails too" -- delegate land q -m "feat: q" has "not the task's fault" "not t-q's fault" "$(cat "$T/err")" delegate drop q >/dev/null 2>&1 export DELEGATE_CHECK='touch artefact' expect 5 "preflight: a check that adds tracked files fails" -- delegate preflight has "names the file" "artefact" "$(cat "$T/err")" fake 'echo new >new.txt' DELEGATE_CHECK= delegate start d <<<"d adds new.txt" >/dev/null 2>&1 delegate run d >/dev/null 2>&1 DELEGATE_CHECK="false" expect 4 "failing check refuses land" -- delegate land d -m "feat: d" has "hint: preflight" "delegate preflight" "$(cat "$T/err")" DELEGATE_CHECK='for i in $(seq 100); do printf "\033[31mline %s\033[0m\n" $i; done; false' expect 4 "a long check failure" -- delegate land d -m "feat: d" has "shows the end" "^line 100$" "$(cat "$T/err")" hasnt "not all of it" "^line 50$" "$(cat "$T/err")" has "names the full log" "all of them in .*t-d-check.log" "$(cat "$T/err")" hasnt "no colour codes" $'\033' "$(cat "$T/err")" expect 4 "check that adds tracked files refuses land" -- delegate land d -m "feat: d" rm -f "$T/proj/repo-t-d/artefact" export DELEGATE_CHECK='echo x >>"$FAKE_STATE/count"; test -f new.txt' old="$(cid main)" expect 0 "passing check lands" -- delegate land d -m "feat: d" land_one "$old" "land d" : >"$T/state/count" expect 0 "land records a pass for the new main" -- delegate preflight [ "$(wc -l <"$T/state/count")" = 0 ] && ok "no preflight run needed" || bad "preflight ran again" export DELEGATE_CHECK="" echo "# post-land" fake 'echo g >g.txt' delegate start g <<<"g adds g.txt" >/dev/null 2>&1 delegate run g >/dev/null 2>&1 old="$(cid main)" DELEGATE_POST_FIX='echo h >hash.txt' DELEGATE_POST_CHECK='test -f hash.txt && test -f g.txt' expect 0 "land with post-land fix and check" -- delegate land g -m "feat: g" land_one "$old" "land g" has "says what the fix folded" "folded the post-land fix" "$(cat "$T/err")" has "and that the check passed" "post-land check passed" "$(cat "$T/err")" has "the fix is in the landed change" "hash.txt" "$(J diff -r main --name-only)" has "with the task's files" "g.txt" "$(J diff -r main --name-only)" yes_ "working copy is empty on main" '[ "$(jj log --no-graph -r @- -T commit_id)" = "$(cid main)" ] && [ -z "$(jj diff --name-only)" ]' fake 'echo i >i.txt' delegate start i <<<"i" >/dev/null 2>&1 delegate run i >/dev/null 2>&1 DELEGATE_POST_FIX=true DELEGATE_POST_CHECK=false expect 6 "post-land check failure exits 6" -- delegate land i -m "feat: i" has "a fix that changed nothing says so" "post-land fix changed nothing" "$(cat "$T/err")" has "main moved anyway" "feat: i" "$(desc main)" has "says trunk is broken" "main is broken" "$(cat "$T/err")" [ ! -e "$T/proj/repo-t-i" ] && ok "workspace removed" || bad "t-i left" fake 'echo k >k.txt' delegate start k <<<"k" >/dev/null 2>&1 delegate run k >/dev/null 2>&1 echo wip >"$repo/wip.txt" old="$(cid main)" DELEGATE_POST_FIX='echo h2 >hash.txt' expect 0 "post-land with unrelated edits in the working copy" -- delegate land k -m "feat: k" land_one "$old" "land k" has "the fix is folded" "^h2$" "$(J file show -r main hash.txt)" hasnt "the edits aren't" "wip.txt" "$(J diff -r main --name-only)" yes_ "the edits stay in the working copy, on main" '[ "$(jj log --no-graph -r @- -T commit_id)" = "$(cid main)" ] && [ "$(jj diff --name-only)" = wip.txt ]' fake 'echo m >m.txt' delegate start m <<<"m" >/dev/null 2>&1 delegate run m >/dev/null 2>&1 echo mine >"$repo/m.txt" old="$(cid main)" DELEGATE_POST_CHECK=true expect 1 "post-land: edits to the task's files refuse up front" -- delegate land m -m "feat: m" has "names the file" "m.txt" "$(cat "$T/err")" [ "$(cid main)" = "$old" ] && ok "nothing landed" || bad "main moved" expect 0 "without post-land steps it lands" -- delegate land m -m "feat: m" yes_ "and the working copy stays on the old main" '[ "$(jj log --no-graph -r @- -T commit_id)" = "$old" ]' has "saying why" "stays on the old main" "$(cat "$T/err")" rm "$repo/m.txt" "$repo/wip.txt" && jj new main >/dev/null 2>&1 fake 'echo n >n.txt' delegate start n <<<"n" >/dev/null 2>&1 delegate run n >/dev/null 2>&1 jj new main- >/dev/null 2>&1 DELEGATE_POST_CHECK=true expect 1 "post-land: working copy off trunk refuses up front" -- delegate land n -m "feat: n" jj new main >/dev/null 2>&1 echo mine >"$repo/hash.txt" DELEGATE_POST_FIX='echo h3 >hash.txt' expect 6 "post-land fix touching your edited file folds nothing" -- delegate land n -m "feat: n" has "says so" "folded nothing" "$(cat "$T/err")" has "main keeps the old hash" "^h2$" "$(J file show -r main hash.txt)" jj restore >/dev/null 2>&1 echo "# renames" fake 'mkdir -p sub && mv shared.txt sub/moved.txt' delegate start r <<<"r moves shared.txt" >/dev/null 2>&1 expect 0 "run r (a move)" -- delegate run r has "run lists the move" "R shared.txt -> sub/moved.txt" "$(cat "$T/err")" has "status --files too" "^ R shared.txt -> sub/moved.txt$" "$(delegate status --files)" fake 'echo x >>shared.txt' delegate start r2 <<<"r2 edits shared.txt" >/dev/null 2>&1 delegate run r2 >/dev/null 2>&1 has "overlap counts the old path" "shared.txt r,r2\|shared.txt r2,r" "$(delegate status)" echo mine >>"$repo/shared.txt" DELEGATE_POST_CHECK=true expect 1 "land: your edit to a moved file refuses up front" -- delegate land r -m "feat: r" has "names it" "shared.txt" "$(cat "$T/err")" jj restore >/dev/null 2>&1 delegate drop r >/dev/null 2>&1; delegate drop r2 >/dev/null 2>&1 echo "# a round that breaks the devenv shell" fake 'echo broken >broken.nix' delegate start s <<<"s edits nix" >/dev/null 2>&1 DELEGATE_DEVENV=1 expect 4 "the round's check can't tell; delegate can" -- delegate run s has "the report is still printed" "REPORT" "$(cat "$T/out")" has "says the shell doesn't evaluate" "t-s's devenv shell doesn't evaluate after round 1" "$(cat "$T/err")" has "shows the error" "src is an alias" "$(cat "$T/err")" hasnt "without colour codes" $'\033' "$(cat "$T/err")" has "the round says so" "^Shell broken: .*src is an alias" "$(desc t-s@-)" DELEGATE_DEVENV=1 DELEGATE_CHECK=true expect 4 "land: says the shell is broken, not just check failed" -- delegate land s -m "feat: s" has "says so" "t-s's devenv shell doesn't evaluate" "$(cat "$T/err")" fake 'grep -q "the devenv shell is broken" "$FAKE_STATE/prompt" && grep -q "src is an alias" "$FAKE_STATE/prompt" && echo fixed >broken.nix' DELEGATE_DEVENV=1 expect 0 "a fix round runs without the shell, told to fix it" -- delegate run s <<<"Fix the nix file" has "the prompt said so (the fix landed)" "^fixed$" "$(cat "$T/proj/repo-t-s/broken.nix")" has "says it evaluates again" "evaluates again" "$(cat "$T/err")" hasnt "the round isn't marked" "Shell broken" "$(desc t-s@-)" delegate drop s >/dev/null 2>&1 echo "# stacked" fake 'echo base >stack.txt' delegate start e <<<"e makes stack.txt" >/dev/null 2>&1 delegate run e >/dev/null 2>&1 fake 'echo more >>stack.txt' expect 0 "start f on e" -- delegate start f --on 't-e@-' <<<"f extends stack.txt" delegate run f >/dev/null 2>&1 has "f diff is only its own" "+more" "$(delegate diff f)" hasnt "f diff excludes e" "+base" "$(delegate diff f)" expect 1 "f can't land before e" -- delegate land f -m "feat: f" fake 'echo fixed >other.txt' delegate run e <<<"one more line" >/dev/null 2>&1 old="$(cid main)" expect 0 "land e" -- delegate land e -m "feat: e" land_one "$old" "land e" has "f follows e onto main" "^f idle 1 ok " "$(delegate status --files)" old="$(cid main)" expect 0 "land f" -- delegate land f -m "feat: f" land_one "$old" "land f" has "stack on main" "more" "$(J file show -r main stack.txt)" [ -z "$(delegate status --files)" ] && ok "no tasks left" || bad "tasks left: $(delegate status)" echo "# baked settings" : >"$T/state/devenv" h="$(for f in devenv.nix devenv.yaml devenv.local.nix devenv.lock; do if [ -f "$repo/$f" ]; then sha256sum <"$repo/$f" | cut -c1-64; else echo -; fi; done | sha256sum | cut -c1-64)" DELEGATE_CONFIG_HASH="$h" expect 0 "config unchanged: runs as is" -- delegate status [ ! -s "$T/state/devenv" ] && ok "no fresh shell" || bad "ran devenv" echo '{ ... }: { }' >"$repo/devenv.nix" DELEGATE_CONFIG_HASH="$h" expect 0 "config changed: runs again in a fresh devenv shell" -- delegate status [ -s "$T/state/devenv" ] && ok "through devenv" || bad "devenv not run" has "says so" "fresh devenv shell" "$(cat "$T/err")" has "still stale there: warns, no loop" "may be stale" "$(cat "$T/err")" rm "$repo/devenv.nix" echo echo "$pass passed, $fail failed" [ "$fail" = 0 ]