#!/usr/bin/env bash # test-mm-worktree-task.sh — Verify mm-worktree-task dryrun behaviour against the spec. # # Strategy: all assertions use --dryrun (-n) with fake item/docs fixtures, so # no cp_cli calls, worktrees, branches, or launches happen. set -uo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" SCRIPT="$REPO_ROOT/scripts/mm-worktree-task" FIXTURE_ITEM="$REPO_ROOT/tests/fixtures/mm-worktree-task-item.json" FIXTURE_DOCS_DIR="$REPO_ROOT/tests/fixtures/mm-worktree-task-docs" TEST_UUID="1f17f61e-317e-4e10-bcd6-bee6a3264379" GREEN='\033[0;32m' RED='\033[0;31m' NC='\033[0m' PASS=0 FAIL=0 assert_contains() { local label=$1 needle=$2 haystack=$3 if [[ "$haystack" == *"$needle"* ]]; then printf " ${GREEN}PASS${NC} %s\n" "$label" PASS=$((PASS + 1)) else printf " ${RED}FAIL${NC} %s\n" "$label" printf " expected to contain: %s\n" "$needle" FAIL=$((FAIL + 1)) fi } assert_not_contains() { local label=$1 needle=$2 haystack=$3 if [[ "$haystack" != *"$needle"* ]]; then printf " ${GREEN}PASS${NC} %s\n" "$label" PASS=$((PASS + 1)) else printf " ${RED}FAIL${NC} %s\n" "$label" printf " expected NOT to contain: %s\n" "$needle" FAIL=$((FAIL + 1)) fi } assert_eq() { local label=$1 expected=$2 actual=$3 if [[ "$expected" == "$actual" ]]; then printf " ${GREEN}PASS${NC} %s\n" "$label" PASS=$((PASS + 1)) else printf " ${RED}FAIL${NC} %s\n" "$label" printf " expected: %s\n actual: %s\n" "$expected" "$actual" FAIL=$((FAIL + 1)) fi } echo "== mm-worktree-task: --help ==" HELP_OUT="$("$SCRIPT" --help 2>&1)" HELP_RC=$? assert_eq "help exits 0" "0" "$HELP_RC" assert_contains "help mentions usage" "Usage: mm-worktree-task" "$HELP_OUT" assert_contains "help mentions --dryrun" "--dryrun" "$HELP_OUT" echo echo "== mm-worktree-task: missing uuid errors ==" MISSING_OUT="$("$SCRIPT" --dryrun 2>&1)" MISSING_RC=$? assert_eq "missing uuid exits 1" "1" "$MISSING_RC" assert_contains "missing uuid error message" "item-uuid is required" "$MISSING_OUT" echo echo "== mm-worktree-task: --dryrun with fixture item ==" export MM_WORKTREE_TASK_FAKE_ITEM="$FIXTURE_ITEM" export MM_WORKTREE_TASK_FAKE_DOCS_DIR="$FIXTURE_DOCS_DIR" DRY_OUT="$("$SCRIPT" "$TEST_UUID" --slug testslug --dryrun 2>&1)" DRY_RC=$? assert_eq "dryrun exits 0" "0" "$DRY_RC" assert_contains "dryrun shows the item title" "Wire up CI merge gate" "$DRY_OUT" assert_contains "dryrun shows worktree path" ".claude/worktrees/mm-testslug" "$DRY_OUT" assert_contains "dryrun shows branch name" "mm/testslug" "$DRY_OUT" assert_contains "dryrun shows bundle path" "testslug-bundle.md" "$DRY_OUT" assert_contains "dryrun shows the ask-minimax-run command" "ask-minimax-run" "$DRY_OUT" assert_contains "dryrun command defaults to Flash model" 'MiniMax-M3.1-Flash-Preview' "$DRY_OUT" assert_contains "dryrun default effort max" "--effort max" "$DRY_OUT" M3_OUT="$("$SCRIPT" "$TEST_UUID" --slug testslug --m3 --dryrun 2>&1)" assert_eq "m3 dryrun exits 0" "0" "$?" assert_contains "m3 dryrun uses M3[1m] model" 'MiniMax-M3[1m]' "$M3_OUT" assert_not_contains "m3 dryrun sends no effort" "--effort" "$M3_OUT" EF_OUT="$("$SCRIPT" "$TEST_UUID" --slug testslug --effort low --dryrun 2>&1)" assert_contains "explicit effort overrides default" "--effort low" "$EF_OUT" "$SCRIPT" "$TEST_UUID" --effort bogus --dryrun >/dev/null 2>&1; assert_eq "bad effort exits 1" "1" "$?" assert_contains "dryrun bundles recent doc notes.md" "notes.md (bundled below)" "$DRY_OUT" assert_contains "dryrun lists old doc as available only" "old-notes.md (available only)" "$DRY_OUT" assert_not_contains "dryrun does not bundle stale doc body" "Stale notes, should not be bundled" "$DRY_OUT" assert_contains "dryrun mentions timeout default" "1800" "$DRY_OUT" # No side effects WORKTREE_PATH="$REPO_ROOT/.claude/worktrees/mm-testslug" if [[ -d "$WORKTREE_PATH" ]]; then printf " ${RED}FAIL${NC} dryrun did not create a worktree\n" FAIL=$((FAIL + 1)) else printf " ${GREEN}PASS${NC} dryrun did not create a worktree\n" PASS=$((PASS + 1)) fi echo echo "== mm-worktree-task: --docs all bundles everything ==" DRY_ALL_OUT="$("$SCRIPT" "$TEST_UUID" --slug testslug2 --docs all --dryrun 2>&1)" assert_contains "docs=all bundles old-notes.md" "old-notes.md (bundled below)" "$DRY_ALL_OUT" echo echo "== mm-worktree-task: --docs none bundles nothing ==" DRY_NONE_OUT="$("$SCRIPT" "$TEST_UUID" --slug testslug3 --docs none --dryrun 2>&1)" assert_contains "docs=none has no documents section entries" "(none)" "$DRY_NONE_OUT" echo echo "== mm-worktree-task: --focus is appended to the prompt ==" DRY_FOCUS_OUT="$("$SCRIPT" "$TEST_UUID" --slug testslug4 --focus "Only touch CI files" --dryrun 2>&1)" assert_contains "focus line present" "Only touch CI files" "$DRY_FOCUS_OUT" echo echo "== mm-worktree-task: --collect with no worktree errors ==" # --- context resolution (spec Behaviour §0) --- FAKE_CPCONFIG="$(mktemp)" cat > "$FAKE_CPCONFIG" <<'YAML' current_context: other contexts: - name: other project_id: something-else - name: reponame-ctx project_id: REPO_NAME_PLACEHOLDER YAML sed -i "s/REPO_NAME_PLACEHOLDER/$(basename "$(git rev-parse --show-toplevel)")/" "$FAKE_CPCONFIG" CTX_OUT="$(MM_WORKTREE_TASK_CPCONFIG="$FAKE_CPCONFIG" "$SCRIPT" "$TEST_UUID" --slug testslug --dryrun 2>&1)" assert_contains "context auto-resolves from cpconfig by repo name" "cp-cli ctx: reponame-ctx" "$CTX_OUT" CTX_FLAG_OUT="$(MM_WORKTREE_TASK_CPCONFIG="$FAKE_CPCONFIG" "$SCRIPT" "$TEST_UUID" --slug testslug --dryrun --context explicit 2>&1)" assert_contains "--context overrides cpconfig resolution" "cp-cli ctx: explicit" "$CTX_FLAG_OUT" CTX_ENV_OUT="$(MM_WORKTREE_TASK_CPCONFIG="$FAKE_CPCONFIG" MM_WORKTREE_TASK_CP_CONTEXT=fromenv "$SCRIPT" "$TEST_UUID" --slug testslug --dryrun 2>&1)" assert_contains "MM_WORKTREE_TASK_CP_CONTEXT overrides cpconfig resolution" "cp-cli ctx: fromenv" "$CTX_ENV_OUT" CTX_NONE_OUT="$(MM_WORKTREE_TASK_CPCONFIG=/nonexistent "$SCRIPT" "$TEST_UUID" --slug testslug --dryrun 2>&1)" assert_contains "no matching context falls back to cp-cli default" "cp-cli ctx: " "$CTX_NONE_OUT" CLI_DIR_OUT="$(MM_WORKTREE_TASK_CP_CLI_DIR=/nonexistent "$SCRIPT" "$TEST_UUID" --slug testslug --dryrun 2>&1)" assert_contains "cp-cli dir resolution is reported" "cp-cli from:" "$CLI_DIR_OUT" rm -f "$FAKE_CPCONFIG" unset MM_WORKTREE_TASK_FAKE_ITEM unset MM_WORKTREE_TASK_FAKE_DOCS_DIR COLLECT_OUT="$("$SCRIPT" "$TEST_UUID" --slug does-not-exist-slug --collect 2>&1)" COLLECT_RC=$? assert_eq "collect with missing worktree exits 1" "1" "$COLLECT_RC" assert_contains "collect error message" "no worktree found" "$COLLECT_OUT" echo echo "== mm-worktree-task: --upload without --collect errors ==" UPLOAD_OUT="$("$SCRIPT" "$TEST_UUID" --slug testslug --upload --dryrun 2>&1)" UPLOAD_RC=$? assert_eq "upload without collect exits 1" "1" "$UPLOAD_RC" assert_contains "upload error message" "--upload requires --collect" "$UPLOAD_OUT" echo echo "== mm-worktree-task: --gate supervisor re-run ==" # Strategy: a fake `ask-minimax-run` on PATH stands in for the real sub-session # (it commits in the worktree, optionally writes a result file, optionally # leaves the worktree dirty) so these tests drive the REAL script end to end — # real worktree, real backgrounded subshell, real `timeout`/gate re-run — # without ever launching a real MiniMax session. REPORT_ROOT="${XDG_RUNTIME_DIR:-/tmp}/mm-worktree-task/$(basename "$REPO_ROOT")" FAKE_BIN_DIR="$(mktemp -d)" cat > "$FAKE_BIN_DIR/ask-minimax-run" <<'FAKE_MM' #!/usr/bin/env bash # Stand-in for ask-minimax-run: commits in the worktree it is run from (cwd # is already the worktree — mm-worktree-task cd's there before invoking it), # optionally writes a result file, optionally leaves an uncommitted change. set -e [[ -n "${FAKE_MM_SLEEP:-}" ]] && sleep "$FAKE_MM_SLEEP" git add -A >/dev/null 2>&1 || true git -c user.email=fake-mm@test.invalid -c user.name="fake mm" \ commit -m "fake mm commit" --allow-empty >/dev/null 2>&1 if [[ -n "${FAKE_MM_RESULT_FILE:-}" ]]; then mkdir -p "$(dirname "$FAKE_MM_RESULT_FILE")" printf '%s\n' "${FAKE_MM_RESULT_CONTENT:-Summary: done}" > "$FAKE_MM_RESULT_FILE" fi [[ -n "${FAKE_MM_LEAVE_DIRTY:-}" ]] && echo "uncommitted" > uncommitted-change.txt exit 0 FAKE_MM chmod +x "$FAKE_BIN_DIR/ask-minimax-run" cleanup_slug() { local slug=$1 git -C "$REPO_ROOT" worktree remove --force "$REPO_ROOT/.claude/worktrees/mm-${slug}" >/dev/null 2>&1 || true git -C "$REPO_ROOT" branch -D "mm/${slug}" >/dev/null 2>&1 || true rm -f "$REPORT_ROOT/${slug}-bundle.md" "$REPORT_ROOT/${slug}-prompt.txt" \ "$REPORT_ROOT/${slug}-stdout.txt" "$REPORT_ROOT/${slug}-result.md" } # Bounded poll (not a fixed sleep chain) for the backgrounded subshell — # ask-minimax-run + gate, per the current code path — to finish and append # its launch.log line. wait_for_launch() { local slug=$1 timeout_s=${2:-20} waited=0 while (( waited < timeout_s * 5 )); do grep -q "^${slug} rc=" "$REPORT_ROOT/launch.log" 2>/dev/null && return 0 sleep 0.2 waited=$((waited + 1)) done return 1 } run_real_launch() { # args: slug [extra mm-worktree-task args...] local slug=$1; shift PATH="$FAKE_BIN_DIR:$PATH" \ MM_WORKTREE_TASK_FAKE_ITEM="$FIXTURE_ITEM" \ MM_WORKTREE_TASK_FAKE_DOCS_DIR="$FIXTURE_DOCS_DIR" \ "$SCRIPT" "$TEST_UUID" --slug "$slug" "$@" >/dev/null 2>&1 } export MM_WORKTREE_TASK_FAKE_ITEM="$FIXTURE_ITEM" export MM_WORKTREE_TASK_FAKE_DOCS_DIR="$FIXTURE_DOCS_DIR" # --- gate passes --- SLUG="gate-pass-$$" cleanup_slug "$SLUG" FAKE_MM_RESULT_FILE="${REPORT_ROOT}/${SLUG}-result.md" \ FAKE_MM_RESULT_CONTENT=$'Summary: implemented feature\nTests run: all tests passed' \ run_real_launch "$SLUG" --gate "true" if wait_for_launch "$SLUG"; then RESULT="$(cat "${REPORT_ROOT}/${SLUG}-result.md" 2>/dev/null || echo "")" HEAD_SHA="$(git -C "$REPO_ROOT/.claude/worktrees/mm-${SLUG}" rev-parse HEAD 2>/dev/null || echo "")" assert_contains "gate-pass: section present" "## Supervisor gate" "$RESULT" assert_contains "gate-pass: verdict PASSED" "Verdict: **PASSED**" "$RESULT" assert_contains "gate-pass: names the actual worktree HEAD sha" "$HEAD_SHA" "$RESULT" assert_not_contains "gate-pass: no MISMATCH raised" "MISMATCH" "$RESULT" else printf " ${RED}FAIL${NC} gate-pass: backgrounded launch did not complete in time\n" FAIL=$((FAIL + 1)) fi cleanup_slug "$SLUG" # --- gate fails, and the agent's own report claimed success (the mismatch case) --- SLUG="gate-fail-$$" cleanup_slug "$SLUG" FAKE_MM_RESULT_FILE="${REPORT_ROOT}/${SLUG}-result.md" \ FAKE_MM_RESULT_CONTENT=$'Summary: implemented feature\nTests run: all tests passed, 0 failed' \ run_real_launch "$SLUG" --gate "exit 1" if wait_for_launch "$SLUG"; then RESULT="$(cat "${REPORT_ROOT}/${SLUG}-result.md" 2>/dev/null || echo "")" HEAD_SHA="$(git -C "$REPO_ROOT/.claude/worktrees/mm-${SLUG}" rev-parse HEAD 2>/dev/null || echo "")" assert_contains "gate-fail: verdict FAILED" "Verdict: **FAILED**" "$RESULT" assert_contains "gate-fail: names the actual worktree HEAD sha" "$HEAD_SHA" "$RESULT" assert_contains "gate-fail: exit code recorded" "Exit code: 1" "$RESULT" assert_contains "gate-fail: mismatch called out against agent's own claim" "MISMATCH" "$RESULT" else printf " ${RED}FAIL${NC} gate-fail: backgrounded launch did not complete in time\n" FAIL=$((FAIL + 1)) fi cleanup_slug "$SLUG" # --- gate command does not exist: failure, never a silent skip --- SLUG="gate-missing-$$" cleanup_slug "$SLUG" run_real_launch "$SLUG" --gate "this-command-truly-does-not-exist-xyz" if wait_for_launch "$SLUG"; then RESULT="$(cat "${REPORT_ROOT}/${SLUG}-result.md" 2>/dev/null || echo "")" assert_contains "gate-missing: verdict FAILED (not skipped)" "Verdict: **FAILED**" "$RESULT" assert_not_contains "gate-missing: never reads as a skip" "SKIP" "$RESULT" else printf " ${RED}FAIL${NC} gate-missing: backgrounded launch did not complete in time\n" FAIL=$((FAIL + 1)) fi cleanup_slug "$SLUG" # --- gate times out: bounded, reaped, recorded as failed --- # The sleep duration embeds this test run's own PID as a fractional-second # fingerprint (GNU sleep accepts arbitrary-precision fractions) so the # leftover-process check below can never false-positive on some unrelated # "sleep 30" already running on a shared box. SLUG="gate-timeout-$$" SLEEP_MARKER="30.$$" cleanup_slug "$SLUG" run_real_launch "$SLUG" --gate "sleep ${SLEEP_MARKER}" --gate-timeout 1 if wait_for_launch "$SLUG" 15; then RESULT="$(cat "${REPORT_ROOT}/${SLUG}-result.md" 2>/dev/null || echo "")" assert_contains "gate-timeout: verdict FAILED" "Verdict: **FAILED**" "$RESULT" assert_contains "gate-timeout: TIMED OUT noted with the bound" "TIMED OUT after 1s" "$RESULT" # the marked sleep process must actually have been reaped, not left running sleep 1 LEFTOVER="$(pgrep -f "sleep ${SLEEP_MARKER}" 2>/dev/null || true)" assert_eq "gate-timeout: no leftover marked sleep process" "" "$LEFTOVER" else printf " ${RED}FAIL${NC} gate-timeout: backgrounded launch did not complete within the bound\n" FAIL=$((FAIL + 1)) fi cleanup_slug "$SLUG" # --- dirty worktree after the sub-session: failed regardless of the gate command's own exit --- SLUG="gate-dirty-$$" cleanup_slug "$SLUG" FAKE_MM_LEAVE_DIRTY=1 run_real_launch "$SLUG" --gate "true" if wait_for_launch "$SLUG"; then RESULT="$(cat "${REPORT_ROOT}/${SLUG}-result.md" 2>/dev/null || echo "")" assert_contains "gate-dirty: verdict FAILED despite gate command exiting 0" "Verdict: **FAILED**" "$RESULT" assert_contains "gate-dirty: dirty worktree called out" "DIRTY" "$RESULT" else printf " ${RED}FAIL${NC} gate-dirty: backgrounded launch did not complete in time\n" FAIL=$((FAIL + 1)) fi cleanup_slug "$SLUG" # --- no --gate at all: result file shape is exactly what the sub-session wrote --- SLUG="no-gate-$$" cleanup_slug "$SLUG" FAKE_MM_RESULT_FILE="${REPORT_ROOT}/${SLUG}-result.md" \ FAKE_MM_RESULT_CONTENT=$'Summary: implemented feature\nTests run: all tests passed' \ run_real_launch "$SLUG" if wait_for_launch "$SLUG"; then RESULT="$(cat "${REPORT_ROOT}/${SLUG}-result.md" 2>/dev/null || echo "")" assert_not_contains "no-gate: never adds a Supervisor gate section" "Supervisor gate" "$RESULT" assert_eq "no-gate: report is byte-for-byte what the sub-session wrote" \ $'Summary: implemented feature\nTests run: all tests passed' "$RESULT" else printf " ${RED}FAIL${NC} no-gate: backgrounded launch did not complete in time\n" FAIL=$((FAIL + 1)) fi cleanup_slug "$SLUG" rm -rf "$FAKE_BIN_DIR" unset MM_WORKTREE_TASK_FAKE_ITEM unset MM_WORKTREE_TASK_FAKE_DOCS_DIR echo echo "==================================" echo " PASS: $PASS FAIL: $FAIL" echo "==================================" if [[ $FAIL -gt 0 ]]; then exit 1 fi exit 0