Skip to content
Cockpit0.29.0
A snapshot: read at commit 707a04c7ce8 on 2026-10-18T00:06:01Z by majordomus 0.29.0. Nothing on this page is read again. See it live

test/cases/06_finish.sh

An object of kind test

What it is

URI
majordomus://test/test/cases/06_finish.sh
Kind
test
Identity
test/cases/06_finish.sh
Path
test/cases/06_finish.sh
Provenance
declared
Media type
text/plain
As a capability
test.test/cases/06_finish.sh

Front matter

{}

Content

# majordomus-covers: finish
# majordomus-negative: finish
# claims: finish-contract, typed-outcome
. "$ROOT/test/lib.sh"
"$MJ" init >/dev/null; "$MJ" update >/dev/null
mkdir -p lib test && echo a > lib/a && echo t > test/a_test && git add . && git commit -qm base
# --check with no task is not a failure
expect_exit 0 "$MJ" finish --check
expect_grep 'nothing to enforce'
LEDGER=.ai/local/state/ledger.jsonl
refusals() { grep -c '"event":"task.refused"' "$LEDGER" 2>/dev/null || true; }
expect_exit 12 "$MJ" finish --outcome completed
"$MJ" start "t1" --scope lib,test --profile debugging >/dev/null
expect_exit 2 "$MJ" finish
expect_exit 2 "$MJ" finish --outcome nonsense
echo b >> lib/a
# contract printed line by line; verification required by profile
expect_exit 10 "$MJ" finish --outcome completed
expect_grep 'OK +scope'
expect_grep 'FAIL verification .* requires --verify-command'
expect_grep 'FAIL note .* no handover'
expect_grep 'finish: refused, 3 unmet'
expect_grep '^outcome: active$' .ai/local/state/current.yaml
# claim finish-refusal-is-recorded: the refusal is a record, naming the outcome claimed, how
# much was unmet and which doctrines refused — not only a line on a terminal that scrolls away
[ "$(refusals)" = 1 ] || { echo "    a refused finish left $(refusals) task.refused line(s), not 1"; exit 1; }
expect_grep '"event":"task.refused".*"outcome":"completed".*"unmet":3.*"refused":\[[^]]*verification[^]]*\]' "$LEDGER"
expect_no_grep '"event":"task.finished"' "$LEDGER"
# failing verify command is recorded as a failure
expect_exit 10 "$MJ" finish --outcome completed --verify-command "false"
expect_grep 'FAIL verification .* false — exit 1'
# every refusal is its own line: the second claim of done is the second record
[ "$(refusals)" = 2 ] || { echo "    two refused finishes left $(refusals) task.refused line(s)"; exit 1; }
# a check asks and does not claim, so it writes nothing
"$MJ" finish --check >/dev/null 2>&1 || true
[ "$(refusals)" = 2 ] || { echo "    finish --check wrote a refusal; a check is a question, not a claim"; exit 1; }
# handover supplies the note; regression test path is required by debugging profile
printf '# Objective\no\n# Current State\nc\n# Next Action\nn\n' | "$MJ" handover >/dev/null
# The verify command is the project's own verification, run as a worker would run it. The
# outcome being claimed is finish's own state: a `check` the verify command runs must not
# see it, or that check judges its obligations and gates as a completed finish and refuses.
venv="$(mktemp "${TMPDIR:-/tmp}/mj-venv.XXXXXX")"
expect_exit 10 "$MJ" finish --outcome completed --verify-command "env > '$venv'"
expect_grep 'OK +note'
expect_grep 'FAIL regression .* no test path'
grep -q '^PATH=' "$venv" || { echo "    the verify command did not run"; exit 1; }
grep -q '^MJ_FINISH_OUTCOME=' "$venv" \
  && { echo "    the verify command inherited $(grep '^MJ_FINISH_OUTCOME=' "$venv")"; exit 1; }
rm -f "$venv"
echo t2 >> test/a_test
# three claims of done refused so far: no verify command, a failing one, a regression path missing
[ "$(refusals)" = 3 ] || { echo "    three refused finishes left $(refusals) task.refused line(s)"; exit 1; }
# A verification command that cannot verify anything is refused before it runs, whatever it
# exits: `true` was accepted here until 2026-09-15, and the ledger recorded it as the proof
# that the work was verified. Every no-op shape the rule names is refused, and names the rule.
vacuous_shapes=0
for vacuous in "true" ":" "exit 0" "   " "true && echo ok" "echo verified"; do
  vacuous_shapes=$((vacuous_shapes + 1))
  expect_exit 10 "$MJ" finish --outcome completed --verify-command "$vacuous"
  expect_grep 'FAIL verification .* cannot verify anything'
  expect_grep 'majordomus\.verification-integrity'
  expect_grep '^outcome: active$' .ai/local/state/current.yaml
  # a refused no-op is a refused claim of done, recorded like every other
  [ "$(refusals)" = $((3 + vacuous_shapes)) ] \
    || { echo "    '$vacuous' left $(refusals) task.refused line(s), not $((3 + vacuous_shapes))"; exit 1; }
done
# ... and a real command that happens to include a no-op part is still a real command
expect_exit 0 "$MJ" finish --outcome completed --verify-command "true && test -f test/a_test"
expect_grep 'OK +regression'
expect_grep 'finish: t-.* completed'
expect_grep '^outcome: completed$' .ai/local/state/current.yaml
expect_grep '"event":"task.finished".*"outcome":"completed".*"majordomus.verification-integrity":"pass".*"verify":\{"command":"true && test -f test/a_test","exit":0' .ai/local/state/ledger.jsonl
# the point of the record: a task accepted after refusals is no longer indistinguishable from
# one accepted first try — its false dones are still there, in order, before the one that held
[ "$(refusals)" = $((3 + vacuous_shapes)) ] \
  || { echo "    the accepted task's history lost its refusals: $(refusals) left, not $((3 + vacuous_shapes))"; exit 1; }
last_two="$(grep -E '"event":"task\.(refused|finished)"' "$LEDGER" | tail -n 2 | sed 's/.*"event":"\([^"]*\)".*/\1/' | tr '\n' ' ')"
[ "$last_two" = "task.refused task.finished " ] || { echo "    ledger order is '$last_two', not refused then finished"; exit 1; }
# and history shows a refusal as what it is
expect_exit 0 "$MJ" history --event task.refused --all
expect_grep 'task.refused .* outcome=completed unmet='
# finishing twice is refused; --check on a finished task passes
expect_exit 15 "$MJ" finish --outcome completed
expect_exit 0 "$MJ" finish --check
# new task archives the finished one (commit t1's work first so it is not "touched" by t2)
git add -A && git commit -qm t1
expect_exit 0 "$MJ" start "t2" --scope lib
[ "$(find .ai/local/state/archive -name '*.yaml' | wc -l | tr -d ' ')" -ge 1 ]
# blocked: needs a note with Next Action, blockers expected
id=$(sed -n 's/^id: //p' .ai/local/state/current.yaml)
printf -- '- [unresolved] %s — which db? (2026-09-03)\n' "$id" >> .ai/local/state/open-questions.md
note="$(mktemp "${TMPDIR:-/tmp}/mj-note.XXXXXX")"; printf '# Objective\no\n# Current State\nwaiting\n' > "$note"
expect_exit 10 "$MJ" finish --outcome blocked --note "$note"
expect_grep 'lacks section\(s\): Next Action'
printf '# Next Action\nask\n' >> "$note"
expect_exit 0 "$MJ" finish --outcome blocked --note "$note"
expect_grep 'INFO blockers .* open questions do not refuse it'
[ -f ".ai/local/state/completed/$id.md" ]
# no_match needs # Reason; verification skipped
"$MJ" start "t3" --scope lib >/dev/null
n2="$(mktemp "${TMPDIR:-/tmp}/mj-note.XXXXXX")"; printf '# Objective\no\n# Current State\nc\n# Next Action\nn\n' > "$n2"
expect_exit 10 "$MJ" finish --outcome no_match --note "$n2"
expect_grep 'lacks section\(s\): Reason'
printf '# Reason\nnot there\n' >> "$n2"
expect_exit 0 "$MJ" finish --outcome no_match --note "$n2"
expect_grep 'INFO verification .* skipped for outcome no_match'
# A note from a pipe (here a process substitution, as /dev/stdin with a heredoc is) is read
# once: the doctrine and the record take the same text. A second read used to find it empty,
# and the record's cp failed after the outcome was written: completed, no task.finished line.
"$MJ" start "t3b" --scope lib >/dev/null
id=$(sed -n 's/^id: //p' .ai/local/state/current.yaml)
expect_exit 0 "$MJ" finish --outcome no_match \
  --note <(printf '# Objective\no\n# Current State\nc\n# Next Action\nn\n# Reason\npiped\n')
expect_grep 'OK +note'
expect_grep '^# Reason$' ".ai/local/state/completed/$id.md"
expect_grep '^piped$' ".ai/local/state/completed/$id.md"
[ ! -e ".ai/local/state/completed/$id.md.pending" ] || { echo "    the staged note was left behind"; exit 1; }
expect_grep "\"event\":\"task.finished\".*\"task_id\":\"$id\".*\"outcome\":\"no_match\"" "$LEDGER"
# a note that cannot be read is a refusal of the note doctrine, as before, and writes no record
"$MJ" start "t3c" --scope lib >/dev/null
expect_exit 10 "$MJ" finish --outcome no_match --note /nonexistent/note.md
expect_grep 'FAIL note'
expect_grep '^outcome: active$' .ai/local/state/current.yaml
expect_exit 0 "$MJ" finish --outcome no_match --note "$n2"
# --check fails on out-of-scope work
"$MJ" start "t4" --scope lib >/dev/null
echo x > test/other && git add test/other && git commit -qm oos
expect_exit 10 "$MJ" finish --check
expect_grep 'FAIL scope +test/other'
# a scope failure blocks a completed finish even with everything else present
printf '# Objective\no\n# Current State\nc\n# Next Action\nn\n' | "$MJ" handover >/dev/null
expect_exit 10 "$MJ" finish --outcome completed --verify-command "test -d .ai"
expect_grep 'FAIL scope'