mm-worktree-task: --gate re-runs the test suite itself, never trusts the agent's claim
Across ~12 supervised MiniMax units, more than half reported a green suite where a manual re-run found red (see memory/gotchas-minimax.md). Add --gate "<command>" (with --gate-timeout, default 900s): after the detached sub-session exits, mm-worktree-task itself re-runs the command in the worktree against the sub-session's LAST commit, in its own reapable process group (setsid + group kill, so a hung gate and everything it forked are bounded and cleaned up), and appends an unambiguous PASSED/FAILED verdict — plus an explicit MISMATCH callout when the agent's own report claimed success — to the result file. A missing command, non-zero exit, timeout, or dirty worktree are all failures, never a silent skip. mm-worktree-task's own exit code is unchanged: the launch already returns before the sub-session (and gate) run, detached. Claude-Session: https://claude.ai/code/session_014KNyfHmHpMbdMrHZXDRkAp
This commit is contained in:
@@ -33,6 +33,12 @@ Options:
|
||||
--docs all|recent|none Which item documents to bundle (default: recent)
|
||||
--tools "<list>" Extra --allowedTools entries (comma-separated)
|
||||
--timeout <secs> Sub-session timeout (default: 1800)
|
||||
--gate "<command>" Re-run this command in the worktree yourself, after
|
||||
the sub-session exits, against its LAST commit.
|
||||
Appended as a "Supervisor gate" section in the
|
||||
result file; a missing command, non-zero exit, or
|
||||
timeout are all reported as FAILED, never skipped.
|
||||
--gate-timeout <secs> Max seconds the --gate command may run (default: 900)
|
||||
--context <name> cp-cli planner context (default: the ~/.cpconfig context
|
||||
whose project_id equals the repo name, else cp-cli's own
|
||||
current_context)
|
||||
@@ -62,6 +68,8 @@ BASE="main"
|
||||
DOCS="recent"
|
||||
EXTRA_TOOLS=""
|
||||
TIMEOUT=1800
|
||||
GATE=""
|
||||
GATE_TIMEOUT=900
|
||||
UPLOAD=0
|
||||
COLLECT=0
|
||||
DRYRUN=0
|
||||
@@ -76,6 +84,8 @@ while [[ $# -gt 0 ]]; do
|
||||
--docs) DOCS="$2"; shift 2 ;;
|
||||
--tools) EXTRA_TOOLS="$2"; shift 2 ;;
|
||||
--timeout) TIMEOUT="$2"; shift 2 ;;
|
||||
--gate) GATE="$2"; shift 2 ;;
|
||||
--gate-timeout) GATE_TIMEOUT="$2"; shift 2 ;;
|
||||
--context) CP_CONTEXT="$2"; shift 2 ;;
|
||||
--upload) UPLOAD=1; shift ;;
|
||||
--collect) COLLECT=1; shift ;;
|
||||
@@ -381,6 +391,8 @@ if [[ $DRYRUN -eq 1 ]]; then
|
||||
echo " Prompt path: ${PROMPT_FILE}"
|
||||
echo " Timeout: ${TIMEOUT}s"
|
||||
echo " Allowlist: ${ALLOWLIST}"
|
||||
echo " Gate: ${GATE:-<none>}"
|
||||
[[ -n "$GATE" ]] && echo " Gate timeout: ${GATE_TIMEOUT}s"
|
||||
echo
|
||||
echo "--- Bundle manifest ---"
|
||||
echo "$BUNDLE_CONTENT"
|
||||
@@ -406,6 +418,93 @@ fi
|
||||
info "creating worktree ${WORKTREE} on branch ${BRANCH}"
|
||||
git -C "$REPO_ROOT" worktree add "$WORKTREE" -b "$BRANCH" "$BASE"
|
||||
|
||||
# --- supervisor gate: re-run --gate ourselves after the sub-session exits,
|
||||
# against its LAST commit. Never trust the agent's own "tests pass" claim —
|
||||
# see memory/gotchas-minimax.md: more than half of supervised units reported
|
||||
# green where a re-run found red. A missing command, non-zero exit, or
|
||||
# timeout are all FAILED, never a silent skip.
|
||||
run_gate() {
|
||||
local gate_sha status_out dirty_flag=0 gate_out_file gate_rc timed_out=0
|
||||
local gate_tail verdict had_report=0 agent_claims_pass=0
|
||||
local gate_pid watchdog_pid timeout_flag
|
||||
|
||||
gate_sha="$(git -C "$WORKTREE" rev-parse HEAD 2>&1)" || gate_sha="<unresolvable HEAD: ${gate_sha}>"
|
||||
status_out="$(git -C "$WORKTREE" status --porcelain 2>&1)"
|
||||
[[ -n "$status_out" ]] && dirty_flag=1
|
||||
|
||||
# Plain `timeout bash -c "$GATE"` only signals the immediate bash wrapper —
|
||||
# any grandchildren the gate command forks (a test runner's own workers,
|
||||
# say) are reparented and keep running unbounded. `setsid` makes the gate
|
||||
# its own process group (pgid == its pid), so the watchdog can signal the
|
||||
# whole group (`-$gate_pid`) and actually reap everything the gate started.
|
||||
gate_out_file="$(mktemp)"
|
||||
timeout_flag="$(mktemp -u)"
|
||||
set +e
|
||||
setsid bash -c "$GATE" > "$gate_out_file" 2>&1 &
|
||||
gate_pid=$!
|
||||
(
|
||||
sleep "$GATE_TIMEOUT"
|
||||
if kill -0 "$gate_pid" 2>/dev/null; then
|
||||
: > "$timeout_flag"
|
||||
kill -TERM -- "-${gate_pid}" 2>/dev/null
|
||||
sleep 5
|
||||
kill -KILL -- "-${gate_pid}" 2>/dev/null
|
||||
fi
|
||||
) &
|
||||
watchdog_pid=$!
|
||||
wait "$gate_pid"
|
||||
gate_rc=$?
|
||||
kill "$watchdog_pid" 2>/dev/null
|
||||
wait "$watchdog_pid" 2>/dev/null
|
||||
set -e
|
||||
[[ -f "$timeout_flag" ]] && timed_out=1
|
||||
rm -f "$timeout_flag"
|
||||
gate_tail="$(tail -n 60 "$gate_out_file" 2>/dev/null)"
|
||||
rm -f "$gate_out_file"
|
||||
|
||||
if [[ $dirty_flag -eq 1 || $gate_rc -ne 0 ]]; then
|
||||
verdict="FAILED"
|
||||
else
|
||||
verdict="PASSED"
|
||||
fi
|
||||
|
||||
[[ -f "$RESULT_FILE" ]] && had_report=1
|
||||
if [[ $had_report -eq 1 ]] \
|
||||
&& grep -qiE '(all tests? pass|0 failed|tests? pass(ed)?|passing)' "$RESULT_FILE" 2>/dev/null \
|
||||
&& ! grep -qiE '[1-9][0-9]* (failed|failing)' "$RESULT_FILE" 2>/dev/null; then
|
||||
agent_claims_pass=1
|
||||
fi
|
||||
|
||||
{
|
||||
echo
|
||||
echo "## Supervisor gate (re-run by mm-worktree-task)"
|
||||
echo
|
||||
[[ $had_report -eq 0 ]] && echo "(No agent report was found at this path before the gate ran.)"
|
||||
echo "- HEAD: ${gate_sha}"
|
||||
echo "- Command: \`${GATE}\`"
|
||||
if [[ $dirty_flag -eq 1 ]]; then
|
||||
echo "- Worktree status: DIRTY — uncommitted changes present; result does not correspond to a fully committed state"
|
||||
else
|
||||
echo "- Worktree status: clean"
|
||||
fi
|
||||
if [[ $timed_out -eq 1 ]]; then
|
||||
echo "- Exit code: ${gate_rc} (TIMED OUT after ${GATE_TIMEOUT}s)"
|
||||
else
|
||||
echo "- Exit code: ${gate_rc}"
|
||||
fi
|
||||
echo "- Verdict: **${verdict}**"
|
||||
if [[ "$verdict" == "FAILED" && $agent_claims_pass -eq 1 ]]; then
|
||||
echo
|
||||
echo "**MISMATCH:** the agent's own report above claims a passing test run; this independent re-run of the gate FAILED. Treat this section as authoritative, not the agent's claim."
|
||||
fi
|
||||
echo
|
||||
echo "Output (tail):"
|
||||
echo '```'
|
||||
echo "$gate_tail"
|
||||
echo '```'
|
||||
} >> "$RESULT_FILE"
|
||||
}
|
||||
|
||||
info "launching ask-minimax-run (detached, timeout ${TIMEOUT}s)"
|
||||
(
|
||||
cd "$WORKTREE"
|
||||
@@ -417,6 +516,9 @@ info "launching ask-minimax-run (detached, timeout ${TIMEOUT}s)"
|
||||
> "$STDOUT_FILE" 2>&1
|
||||
rc=$?
|
||||
set -e
|
||||
if [[ -n "$GATE" ]]; then
|
||||
run_gate
|
||||
fi
|
||||
echo "${SLUG} rc=${rc} elapsed=$(( $(date +%s) - start ))s" >> "$LAUNCH_LOG"
|
||||
) &
|
||||
disown
|
||||
|
||||
@@ -26,6 +26,8 @@ mm-worktree-task <item-uuid> [OPTIONS]
|
||||
| `--docs all\|recent\|none` | `recent` | Which item documents to bundle. `recent` = documents updated in the last 14 days; falls back to `all` if the CLI does not expose update dates. The full list of document names is always included in the prompt regardless of which are bundled. |
|
||||
| `--tools "<list>"` | — | Extra `--allowedTools` entries merged into the default allowlist (comma-separated). |
|
||||
| `--timeout <secs>` | `1800` | Max seconds the sub-session may run (`timeout <secs>` wrapper). |
|
||||
| `--gate "<command>"` | — | Supervisor gate: after the sub-session exits, `mm-worktree-task` itself re-runs this command in the worktree against the sub-session's LAST commit and appends a `## Supervisor gate` section to the result file (see Behaviour §8). Never trust the agent's own "tests pass" claim — this re-run is the check. |
|
||||
| `--gate-timeout <secs>` | `900` | Max seconds `--gate`'s command may run before it is killed and recorded as a timed-out (failed) gate. |
|
||||
| `--context <name>` | resolved (see §0) | `cp-cli --context` used for every planner call. |
|
||||
| `--upload` | off | After the sub-session exits, upload its report as a document on the item and patch `flow_state` to `impl-complete`. Implemented only in combination with `--collect` (see Behaviour §6) since launch is detached. |
|
||||
| `--base <ref>` | `main` | Branch or commit the worktree is created from, and the ref `--collect` logs/diffs against. Needed when work lives on a long-running branch, or when one agent's branch must build on another's (e.g. an implementation agent starting from a test agent's `mm/<slug>` branch). |
|
||||
@@ -81,6 +83,14 @@ mm-worktree-task <item-uuid> [OPTIONS]
|
||||
```
|
||||
wrapped in a backgrounded subshell that, on exit, appends `<slug> rc=<rc> elapsed=<s>s` to `<report-dir>/launch.log`. The parent process returns immediately.
|
||||
Default allowlist: `Read,Write,Edit,Glob,Grep,Bash(pytest *),Bash(python3 *),Bash(ruff *),Bash(grep *),Bash(ls *),Bash(git add *),Bash(git commit *),Bash(git status*),Bash(git diff*),Bash(git rev-parse*),Bash(git log*)`.
|
||||
- **Supervisor gate (`--gate "<command>"`, optional).** Runs inside the same backgrounded subshell, immediately after the sub-session's `ask-minimax-run` process exits and before the `launch.log` line is appended — so it always evaluates the worktree exactly as the sub-session left it, never a snapshot from mid-run:
|
||||
1. Record `git -C <worktree> rev-parse HEAD` as the gate sha — this is the sub-session's LAST commit, not whatever commit existed when the sub-session claimed success.
|
||||
2. Check whether the worktree is dirty (`git status --porcelain` non-empty). A dirty worktree makes the recorded sha not fully represent what was tested, so it is treated as a **failed** gate regardless of the command's own exit code.
|
||||
3. Run the command as `setsid bash -c "<command>"` in the worktree (its own process group, `pgid == pid`), capturing combined stdout+stderr and its exit code, with a watchdog that after `--gate-timeout` (default `900`) seconds sends `SIGTERM` to the whole group (`kill -TERM -- -<pid>`), waits 5s, then `SIGKILL`s the group — a command that forks children (a test runner's own workers, say) is fully reaped, not just its immediate wrapper. A timed-out gate is recorded as **TIMED OUT** and failed regardless of the eventual exit code. A command that does not exist exits non-zero (typically `127`) and is failed the same way — there is no separate "skipped" state.
|
||||
4. Append `## Supervisor gate (re-run by mm-worktree-task)` to `<report-dir>/<slug>-result.md` (created if the sub-session never wrote one, noting that explicitly) with: the HEAD sha, the exact command, the clean/dirty flag, the exit code, an explicit **PASSED**/**FAILED** verdict, and the tail (last 60 lines) of captured output.
|
||||
5. If the verdict is FAILED and the text of `<result-file>` *as the sub-session left it* (before this section was appended) reads as a claim that tests passed, the appended section says so explicitly (`**MISMATCH:** the agent's own report ... claims a passing test run; this independent re-run ... FAILED`) — the agent's claim is never the last word.
|
||||
6. `mm-worktree-task`'s own exit code is unaffected by the gate outcome: the launch has already returned 0 before the sub-session (and gate) run at all, detached. The gate's PASSED/FAILED verdict is only ever visible in the result file / `--collect` output — never in a process exit code.
|
||||
With no `--gate`, none of the above runs and the result file is exactly what the sub-session wrote, unchanged from pre-`--gate` behaviour.
|
||||
9. Print: report dir, prompt file path, stdout file path, launch.log path, and a one-line `poll with: tail -f <stdout file>` hint.
|
||||
10. `--collect`: instead of steps 6-9, for the given `<slug>`'s existing worktree, print `git -C <worktree> log --oneline <base>..HEAD`, `git -C <worktree> diff --stat <base>...HEAD`, and the contents of `<report-dir>/<slug>-result.md` if present. If `--upload` is also given and the report file exists:
|
||||
- `python3 -m lib.cp_cli planner items documents put <uuid> mm-<slug>-<YYYY-MM-DD>.md --file <report>`
|
||||
@@ -91,7 +101,7 @@ mm-worktree-task <item-uuid> [OPTIONS]
|
||||
|
||||
With `--dryrun`/`-n`:
|
||||
- No worktree, branch, bundle file, prompt file, or report-dir directory is created; no `ask-minimax-run` process is started.
|
||||
- Prints, in order: resolved slug/uuid/worktree path/branch, the bundle manifest (section names + document names, bundled vs. available-only), the full assembled prompt text, and the exact `ask-minimax-run` command line that would run (including the resolved allowlist and timeout).
|
||||
- Prints, in order: resolved slug/uuid/worktree path/branch, the bundle manifest (section names + document names, bundled vs. available-only), the full assembled prompt text, and the exact `ask-minimax-run` command line that would run (including the resolved allowlist and timeout), plus the resolved `--gate` command and `--gate-timeout` (or `<none>` if not given). The gate itself is never executed under `--dryrun`.
|
||||
- Exit 0.
|
||||
|
||||
## Edge Cases
|
||||
@@ -105,6 +115,8 @@ With `--dryrun`/`-n`:
|
||||
| `--upload` without `--collect` | Error, exit 1 |
|
||||
| `--docs recent` but CLI gives no `updated_at` | Fall back to `--docs all` behaviour, note this in bundle `## Documents` section |
|
||||
| cp_cli sibling-list command unsupported | Skip `## Siblings` section silently (best-effort) |
|
||||
| `--gate` command missing, non-zero, or times out | Recorded as `Verdict: **FAILED**` in the `## Supervisor gate` section — never a silent skip |
|
||||
| `--gate` runs against a dirty worktree | Recorded as `Verdict: **FAILED**` regardless of the command's own exit code (the sha would not represent everything present) |
|
||||
|
||||
## Security
|
||||
|
||||
@@ -130,4 +142,9 @@ mm-worktree-task 1f17f61e-317e-4e10-bcd6-bee6a3264379 --slug ci-gate --collect
|
||||
|
||||
# Collect and upload the report + mark impl-complete
|
||||
mm-worktree-task 1f17f61e-317e-4e10-bcd6-bee6a3264379 --slug ci-gate --collect --upload
|
||||
|
||||
# Launch with a supervisor gate: re-run the real suite yourself after the
|
||||
# sub-session's last commit, bounded to 10 minutes
|
||||
mm-worktree-task 1f17f61e-317e-4e10-bcd6-bee6a3264379 --slug ci-gate \
|
||||
--gate "python3 -m pytest -q" --gate-timeout 600
|
||||
```
|
||||
|
||||
@@ -153,6 +153,178 @@ 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 "<missing>")"
|
||||
HEAD_SHA="$(git -C "$REPO_ROOT/.claude/worktrees/mm-${SLUG}" rev-parse HEAD 2>/dev/null || echo "<no-worktree>")"
|
||||
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 "<missing>")"
|
||||
HEAD_SHA="$(git -C "$REPO_ROOT/.claude/worktrees/mm-${SLUG}" rev-parse HEAD 2>/dev/null || echo "<no-worktree>")"
|
||||
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 "<missing>")"
|
||||
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 "<missing>")"
|
||||
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 "<missing>")"
|
||||
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 "<missing>")"
|
||||
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"
|
||||
|
||||
Reference in New Issue
Block a user