feat(mm-worktree-task): run from any planner-backed repo — cp_cli dir + context resolution (--context, MM_WORKTREE_TASK_CP_*), empty-docs guard, tests + spec

cp_cli lives in agent-runtimes; previously every planner call ran 'python3 -m lib.cp_cli' from the current repo with no --context, so the script only worked inside agent-runtimes. The context now resolves from ~/.cpconfig by matching project_id to the repo directory name. Verified live from hatchmate (item + parent + documents fetched via the hatchmate context).

Claude-Session: https://claude.ai/code/session_01JgJbWzVJioVznVTFyQFEfx
This commit is contained in:
Paul O'Reilly
2026-09-11 16:27:08 +12:00
parent 9488335713
commit f50561ea78
9 changed files with 969 additions and 10 deletions

268
tests/test-bg-model-call.sh Executable file
View File

@@ -0,0 +1,268 @@
#!/usr/bin/env bash
# test-bg-model-call.sh — Verify bg-model-call dryrun behaviour against the spec.
#
# Strategy: all assertions use --dryrun (-n), so no CP calls are made and
# no output files are written. Prompt files are created in a tempdir.
set -uo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
BG_MODEL_CALL="$REPO_ROOT/scripts/bg-model-call"
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"
printf " actual output:\n"
printf '%s\n' "$haystack" | sed 's/^/ /'
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_exit_code() {
local label=$1 expected=$2 actual=$3
if [[ "$expected" == "$actual" ]]; then
printf " ${GREEN}PASS${NC} %s (exit=%s)\n" "$label" "$actual"
PASS=$((PASS + 1))
else
printf " ${RED}FAIL${NC} %s (expected exit=%s, got exit=%s)\n" "$label" "$expected" "$actual"
FAIL=$((FAIL + 1))
fi
}
# Create a temp directory for prompt and output files
TMPDIR_TEST="$(mktemp -d)"
trap 'rm -rf "$TMPDIR_TEST"' EXIT
PROMPT_FILE="${TMPDIR_TEST}/prompt.txt"
OUTPUT_FILE="${TMPDIR_TEST}/result.txt"
echo "This is a test prompt for the spec review." > "$PROMPT_FILE"
run() {
"$BG_MODEL_CALL" "$@" 2>&1
}
# === Pre-flight ===
if [[ ! -x "$BG_MODEL_CALL" ]]; then
echo "bg-model-call not found or not executable at $BG_MODEL_CALL" >&2
exit 1
fi
# === Test 1: --help ===
echo "Test 1: --help"
out=$(run --help)
rc=$?
assert_exit_code "--help exits 0" 0 "$rc"
assert_contains "--help shows script name" "bg-model-call" "$out"
assert_contains "--help shows --model" "--model" "$out"
assert_contains "--help shows --prompt" "--prompt" "$out"
assert_contains "--help shows --output" "--output" "$out"
assert_contains "--help shows --dryrun" "--dryrun" "$out"
assert_contains "--help shows --template" "--template" "$out"
# === Test 2: basic minimax dryrun ===
echo
echo "Test 2: minimax dryrun"
out=$(run --dryrun --model minimax --prompt "$PROMPT_FILE" --output "$OUTPUT_FILE")
rc=$?
assert_exit_code "minimax dryrun exits 0" 0 "$rc"
assert_contains "shows dryrun header" "[dryrun]" "$out"
assert_contains "shows model" "minimax" "$out"
assert_contains "shows resolved template" "minimax-planning" "$out"
assert_contains "shows CP URL default" "http://localhost:8100" "$out"
assert_contains "shows project ID default" "bg-model-call" "$out"
assert_contains "shows task name" "bg-model-call-minimax" "$out"
assert_contains "shows prompt file path" "$PROMPT_FILE" "$out"
assert_contains "shows output file path" "$OUTPUT_FILE" "$out"
assert_contains "shows sentinel path" "${OUTPUT_FILE}.done" "$out"
assert_contains "shows prompt size" "bytes" "$out"
assert_not_contains "no CP calls in dryrun" "Submitting task" "$out"
# === Test 3: sonnet dryrun — template mapping ===
echo
echo "Test 3: sonnet dryrun — template mapping"
out=$(run --dryrun --model sonnet --prompt "$PROMPT_FILE" --output "$OUTPUT_FILE")
rc=$?
assert_exit_code "sonnet dryrun exits 0" 0 "$rc"
assert_contains "sonnet maps to sonnet-code-repo" "sonnet-code-repo" "$out"
assert_contains "sonnet task name correct" "bg-model-call-sonnet" "$out"
# === Test 4: haiku dryrun — template mapping ===
echo
echo "Test 4: haiku dryrun — template mapping"
out=$(run --dryrun --model haiku --prompt "$PROMPT_FILE" --output "$OUTPUT_FILE")
rc=$?
assert_exit_code "haiku dryrun exits 0" 0 "$rc"
assert_contains "haiku maps to haiku-code-repo" "haiku-code-repo" "$out"
# === Test 5: opus dryrun — template mapping ===
echo
echo "Test 5: opus dryrun — template mapping"
out=$(run --dryrun --model opus --prompt "$PROMPT_FILE" --output "$OUTPUT_FILE")
rc=$?
assert_exit_code "opus dryrun exits 0" 0 "$rc"
assert_contains "opus maps to opus-code-repo" "opus-code-repo" "$out"
# === Test 6: --template override ===
echo
echo "Test 6: --template override"
out=$(run --dryrun --template minimax-planning --prompt "$PROMPT_FILE" --output "$OUTPUT_FILE")
rc=$?
assert_exit_code "--template override exits 0" 0 "$rc"
assert_contains "template override shown" "minimax-planning" "$out"
# No model line since only --template was given, not --model
assert_not_contains "no model line when using --template only" "Model:" "$out"
# === Test 7: custom --project and --task-name ===
echo
echo "Test 7: custom project and task name"
out=$(run --dryrun --model minimax --prompt "$PROMPT_FILE" --output "$OUTPUT_FILE" \
--project my-project --task-name review-task)
rc=$?
assert_exit_code "custom project/task exits 0" 0 "$rc"
assert_contains "custom project shown" "my-project" "$out"
assert_contains "custom task name shown" "review-task" "$out"
# === Test 8: custom --url ===
echo
echo "Test 8: custom --url"
out=$(run --dryrun --model minimax --prompt "$PROMPT_FILE" --output "$OUTPUT_FILE" \
--url "http://10.111.1.11:31800")
rc=$?
assert_exit_code "custom url exits 0" 0 "$rc"
assert_contains "custom URL shown" "http://10.111.1.11:31800" "$out"
# === Test 9: CP_URL env var ===
echo
echo "Test 9: CP_URL env var"
out=$(CP_URL="http://my-cp:9000" run --dryrun --model minimax --prompt "$PROMPT_FILE" --output "$OUTPUT_FILE")
rc=$?
assert_exit_code "CP_URL env var exits 0" 0 "$rc"
assert_contains "CP_URL env var used" "http://my-cp:9000" "$out"
# === Test 10: -n shorthand ===
echo
echo "Test 10: -n shorthand for dryrun"
out=$(run -n --model sonnet --prompt "$PROMPT_FILE" --output "$OUTPUT_FILE")
rc=$?
assert_exit_code "-n shorthand exits 0" 0 "$rc"
assert_contains "-n shows dryrun header" "[dryrun]" "$out"
# === Test 11: error — missing --model and --template ===
echo
echo "Test 11: error — missing --model and --template"
out=$(run --dryrun --prompt "$PROMPT_FILE" --output "$OUTPUT_FILE")
rc=$?
assert_exit_code "missing model exits 1" 1 "$rc"
assert_contains "missing model error message" "--model is required" "$out"
# === Test 12: error — missing --prompt ===
echo
echo "Test 12: error — missing --prompt"
out=$(run --dryrun --model minimax --output "$OUTPUT_FILE")
rc=$?
assert_exit_code "missing prompt exits 1" 1 "$rc"
assert_contains "missing prompt error" "--prompt is required" "$out"
# === Test 13: error — missing --output ===
echo
echo "Test 13: error — missing --output"
out=$(run --dryrun --model minimax --prompt "$PROMPT_FILE")
rc=$?
assert_exit_code "missing output exits 1" 1 "$rc"
assert_contains "missing output error" "--output is required" "$out"
# === Test 14: error — unknown model alias ===
echo
echo "Test 14: error — unknown model alias"
out=$(run --dryrun --model bogusmodel --prompt "$PROMPT_FILE" --output "$OUTPUT_FILE")
rc=$?
assert_exit_code "unknown model exits 1" 1 "$rc"
assert_contains "unknown model error" "Unknown model alias" "$out"
assert_contains "unknown model error lists valid values" "minimax" "$out"
# === Test 15: error — prompt file not found ===
echo
echo "Test 15: error — prompt file not found"
out=$(run --dryrun --model minimax --prompt "/tmp/definitely-does-not-exist-99999.txt" --output "$OUTPUT_FILE")
rc=$?
assert_exit_code "missing prompt file exits 1" 1 "$rc"
assert_contains "missing prompt file error" "Cannot read prompt file" "$out"
# === Test 16: error — output directory does not exist ===
echo
echo "Test 16: error — output directory does not exist"
out=$(run --dryrun --model minimax --prompt "$PROMPT_FILE" --output "/nonexistent/dir/result.txt")
rc=$?
assert_exit_code "missing output dir exits 1" 1 "$rc"
assert_contains "missing output dir error" "Output directory does not exist" "$out"
# === Test 17: error — invalid timeout ===
echo
echo "Test 17: error — invalid --timeout"
out=$(run --dryrun --model minimax --prompt "$PROMPT_FILE" --output "$OUTPUT_FILE" --timeout abc)
rc=$?
assert_exit_code "invalid timeout exits 1" 1 "$rc"
assert_contains "invalid timeout error" "--timeout must be a positive integer" "$out"
# === Test 18: error — invalid poll-interval ===
echo
echo "Test 18: error — invalid --poll-interval"
out=$(run --dryrun --model minimax --prompt "$PROMPT_FILE" --output "$OUTPUT_FILE" --poll-interval 0)
rc=$?
assert_exit_code "invalid poll-interval exits 1" 1 "$rc"
assert_contains "invalid poll-interval error" "--poll-interval must be a positive integer" "$out"
# === Test 19: custom timeout and poll-interval shown in dryrun ===
echo
echo "Test 19: custom timeout and poll-interval in dryrun"
out=$(run --dryrun --model minimax --prompt "$PROMPT_FILE" --output "$OUTPUT_FILE" \
--timeout 3600 --poll-interval 30)
rc=$?
assert_exit_code "custom timeout/poll exits 0" 0 "$rc"
assert_contains "custom timeout shown" "3600s" "$out"
assert_contains "custom poll shown" "30s" "$out"
# === Test 20: unknown flag ===
echo
echo "Test 20: unknown flag"
out=$(run --dryrun --bogus-flag --model minimax --prompt "$PROMPT_FILE" --output "$OUTPUT_FILE")
rc=$?
assert_exit_code "unknown flag exits 1" 1 "$rc"
assert_contains "unknown flag error" "Unknown flag" "$out"
# === Summary ===
echo
echo "================================"
TOTAL=$((PASS + FAIL))
if [[ "$FAIL" -eq 0 ]]; then
printf "${GREEN}All %d tests passed${NC}\n" "$TOTAL"
exit 0
else
printf "${RED}%d of %d tests failed${NC}\n" "$FAIL" "$TOTAL"
exit 1
fi

View File

@@ -116,6 +116,29 @@ 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: <cp-cli default>" "$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)"

310
tests/test-wait-for-version.sh Executable file
View File

@@ -0,0 +1,310 @@
#!/usr/bin/env bash
set -uo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
WAIT="$SCRIPT_DIR/scripts/wait-for-version"
RED='\033[0;31m'
GREEN='\033[0;32m'
NC='\033[0m'
pass=0
fail=0
assert_eq() {
local desc="$1" expected="$2" actual="$3"
if [[ "$expected" == "$actual" ]]; then
echo -e "${GREEN}PASS${NC}: $desc"
((pass++)) || true
else
echo -e "${RED}FAIL${NC}: $desc"
echo " expected: $expected"
echo " actual: $actual"
((fail++)) || true
fi
}
assert_match() {
local desc="$1" pattern="$2" actual="$3"
if [[ "$actual" =~ $pattern ]]; then
echo -e "${GREEN}PASS${NC}: $desc"
((pass++)) || true
else
echo -e "${RED}FAIL${NC}: $desc"
echo " pattern: $pattern"
echo " actual: $actual"
((fail++)) || true
fi
}
assert_exit() {
local desc="$1" expected="$2" actual="$3"
if [[ "$expected" -eq "$actual" ]]; then
echo -e "${GREEN}PASS${NC}: $desc"
((pass++)) || true
else
echo -e "${RED}FAIL${NC}: $desc"
echo " expected exit: $expected"
echo " actual exit: $actual"
((fail++)) || true
fi
}
assert_between() {
local desc="$1" lo="$2" hi="$3" val="$4"
if (( val >= lo && val <= hi )); then
echo -e "${GREEN}PASS${NC}: $desc (${val} in [${lo},${hi}])"
((pass++)) || true
else
echo -e "${RED}FAIL${NC}: $desc (${val} not in [${lo},${hi}])"
((fail++)) || true
fi
}
# --- test env setup ---
tmp_root="$(mktemp -d)"
trap 'rm -rf "$tmp_root"' EXIT
MOCK_DIR="$tmp_root/bin"
mkdir -p "$MOCK_DIR"
# URL-aware mock curl: routes to different fixture files based on URL path.
# Environment variables:
# MOCK_BRANCH_BODY / MOCK_BRANCH_CODE — for /branches/main
# MOCK_RUNS_BODY / MOCK_RUNS_CODE — for /actions/runs
# MOCK_RELEASE_BODY / MOCK_RELEASE_CODE — for /releases/latest
# MOCK_GLOBAL_CODE — override all codes (for auth/404 tests)
# MOCK_CURL_FAIL — make curl exit non-zero
cat > "$MOCK_DIR/curl" <<'MOCK'
#!/usr/bin/env bash
url=""
while [[ $# -gt 0 ]]; do
case "$1" in
http://*|https://*) url="$1"; shift ;;
*) shift ;;
esac
done
[[ -n "${MOCK_CURL_FAIL:-}" ]] && exit 7
if [[ -n "${MOCK_GLOBAL_CODE:-}" ]]; then
printf '\n%s' "$MOCK_GLOBAL_CODE"
exit 0
fi
if [[ "$url" == */branches/main ]]; then
[[ -n "${MOCK_BRANCH_BODY:-}" && -f "${MOCK_BRANCH_BODY}" ]] && cat "$MOCK_BRANCH_BODY"
printf '\n%s' "${MOCK_BRANCH_CODE:-200}"
elif [[ "$url" == */actions/runs* ]]; then
[[ -n "${MOCK_RUNS_BODY:-}" && -f "${MOCK_RUNS_BODY}" ]] && cat "$MOCK_RUNS_BODY"
printf '\n%s' "${MOCK_RUNS_CODE:-200}"
elif [[ "$url" == */releases/latest ]]; then
[[ -n "${MOCK_RELEASE_BODY:-}" && -f "${MOCK_RELEASE_BODY}" ]] && cat "$MOCK_RELEASE_BODY"
printf '\n%s' "${MOCK_RELEASE_CODE:-200}"
fi
MOCK
chmod +x "$MOCK_DIR/curl"
run_with_mock() {
PATH="$MOCK_DIR:$PATH" "$WAIT" "$@"
}
# --- Fixtures ---
FIX_BRANCH="$tmp_root/branch.json"
cat > "$FIX_BRANCH" <<'JSON'
{"name":"main","commit":{"id":"abc1234567890def1234567890def1234567890de"}}
JSON
FIX_RUNS_SUCCESS="$tmp_root/runs_success.json"
cat > "$FIX_RUNS_SUCCESS" <<'JSON'
{"workflow_runs":[
{"id":42,"name":"version","status":"completed","conclusion":"success"},
{"id":41,"name":"test", "status":"completed","conclusion":"success"}
]}
JSON
FIX_RUNS_FAILED="$tmp_root/runs_failed.json"
cat > "$FIX_RUNS_FAILED" <<'JSON'
{"workflow_runs":[
{"id":42,"name":"version","status":"completed","conclusion":"failure"}
]}
JSON
FIX_RUNS_INPROGRESS="$tmp_root/runs_inprogress.json"
cat > "$FIX_RUNS_INPROGRESS" <<'JSON'
{"workflow_runs":[
{"id":42,"name":"version","status":"in_progress","conclusion":null}
]}
JSON
FIX_RUNS_EMPTY="$tmp_root/runs_empty.json"
cat > "$FIX_RUNS_EMPTY" <<'JSON'
{"workflow_runs":[]}
JSON
FIX_RUNS_WRONG_WORKFLOW="$tmp_root/runs_wrong.json"
cat > "$FIX_RUNS_WRONG_WORKFLOW" <<'JSON'
{"workflow_runs":[
{"id":42,"name":"test","status":"completed","conclusion":"success"}
]}
JSON
FIX_RELEASE="$tmp_root/release.json"
cat > "$FIX_RELEASE" <<'JSON'
{"id":10,"tag_name":"v1.2.3.45","name":"v1.2.3.45"}
JSON
echo "=== wait-for-version tests ==="
echo
# --- Help ---
out=$("$WAIT" --help 2>&1); rc=$?
assert_exit "help exits 0" 0 "$rc"
assert_match "help mentions REPO" "REPO" "$out"
assert_match "help lists exit codes" "Exit codes" "$out"
assert_match "help mentions --workflow" "\-\-workflow" "$out"
# --- Usage errors ---
out=$("$WAIT" 2>&1); rc=$?
assert_exit "no args exits 2" 2 "$rc"
assert_match "no args error" "REPO is required" "$out"
out=$("$WAIT" noslash 2>&1); rc=$?
assert_exit "REPO without slash exits 2" 2 "$rc"
assert_match "REPO slash error" "owner/name" "$out"
out=$("$WAIT" --timeout abc owner/repo 2>&1); rc=$?
assert_exit "non-numeric timeout exits 2" 2 "$rc"
out=$("$WAIT" --interval 0 owner/repo 2>&1); rc=$?
assert_exit "interval 0 exits 2" 2 "$rc"
out=$("$WAIT" --unknown-flag owner/repo 2>&1); rc=$?
assert_exit "unknown flag exits 2" 2 "$rc"
out=$("$WAIT" owner/repo extra 2>&1); rc=$?
assert_exit "extra positional arg exits 2" 2 "$rc"
# --- Dryrun ---
out=$(env -u GITEA_TOKEN -u GITHUB_TOKEN "$WAIT" --dryrun myorg/myrepo 2>&1); rc=$?
assert_exit "dryrun exits 0" 0 "$rc"
assert_match "dryrun shows server" "Server: " "$out"
assert_match "dryrun shows repo" "Repo: myorg/myrepo" "$out"
assert_match "dryrun shows workflow" "Workflow: version" "$out"
assert_match "dryrun shows timeout" "Timeout: 300s" "$out"
assert_match "dryrun shows interval" "Interval: 15s" "$out"
assert_match "dryrun shows Auth: no" "Auth: no" "$out"
assert_match "dryrun shows Actions API" "api/v1/repos/myorg/myrepo/actions/runs" "$out"
# Auth: yes via --token
out=$(env -u GITEA_TOKEN -u GITHUB_TOKEN "$WAIT" --dryrun -t tok123 myorg/myrepo 2>&1)
assert_match "--token -> Auth: yes" "Auth: yes" "$out"
# Auth: yes via GITEA_TOKEN env
out=$(env -u GITHUB_TOKEN GITEA_TOKEN=xyz "$WAIT" --dryrun myorg/myrepo 2>&1)
assert_match "GITEA_TOKEN -> Auth: yes" "Auth: yes" "$out"
# Custom --server appears in Actions API URL
out=$("$WAIT" --dryrun --server https://example.test myorg/myrepo 2>&1)
assert_match "custom --server in Actions API" "https://example\.test/api/v1/repos/myorg/myrepo/actions/runs" "$out"
# Custom --workflow reflected in dryrun
out=$("$WAIT" --dryrun --workflow release myorg/myrepo 2>&1)
assert_match "custom --workflow shown" "Workflow: release" "$out"
# --- Successful flow via mock ---
export MOCK_BRANCH_BODY="$FIX_BRANCH"
export MOCK_RUNS_BODY="$FIX_RUNS_SUCCESS"
export MOCK_RELEASE_BODY="$FIX_RELEASE"
unset MOCK_GLOBAL_CODE MOCK_CURL_FAIL MOCK_BRANCH_CODE MOCK_RUNS_CODE MOCK_RELEASE_CODE
out=$(run_with_mock --timeout 5 --interval 1 test/repo 2>&1)
rc=$?
# stdout is the last line (tag); stderr has the "Waiting..." message
tag=$(run_with_mock --timeout 5 --interval 1 test/repo 2>/dev/null)
assert_exit "success exits 0" 0 "$rc"
assert_eq "success prints tag" "v1.2.3.45" "$tag"
# Only the version workflow is selected, not the test workflow
out=$(run_with_mock --timeout 5 --interval 1 test/repo 2>/dev/null)
assert_eq "selects correct workflow by name" "v1.2.3.45" "$out"
# -- Verbose output to stderr ---
stderr_out=$(run_with_mock --verbose --timeout 5 --interval 1 test/repo 2>&1 >/dev/null)
assert_match "verbose mentions Waiting" "Waiting for" "$stderr_out"
# --- CI failure -> exit 1 ---
export MOCK_RUNS_BODY="$FIX_RUNS_FAILED"
out=$(run_with_mock --timeout 5 --interval 1 test/repo 2>&1); rc=$?
assert_exit "CI failure exits 1" 1 "$rc"
assert_match "CI failure message includes conclusion" "failure" "$out"
# --- In-progress -> timeout (no polling loop, just immediate timeout) ---
export MOCK_RUNS_BODY="$FIX_RUNS_INPROGRESS"
start_t=$(date +%s)
out=$(run_with_mock --timeout 2 --interval 1 test/repo 2>&1); rc=$?
end_t=$(date +%s)
elapsed=$(( end_t - start_t ))
assert_exit "in_progress times out -> exit 1" 1 "$rc"
assert_match "timeout error message" "timeout after 2s" "$out"
assert_between "elapsed within tolerance" 2 4 "$elapsed"
# --- No matching workflow in runs -> timeout ---
export MOCK_RUNS_BODY="$FIX_RUNS_WRONG_WORKFLOW"
out=$(run_with_mock --timeout 2 --interval 1 test/repo 2>&1); rc=$?
assert_exit "no matching workflow times out -> exit 1" 1 "$rc"
# --- Empty runs list -> timeout ---
export MOCK_RUNS_BODY="$FIX_RUNS_EMPTY"
out=$(run_with_mock --timeout 1 --interval 1 test/repo 2>&1); rc=$?
assert_exit "empty runs times out -> exit 1" 1 "$rc"
# --- Branch API auth errors -> exit 3 ---
export MOCK_GLOBAL_CODE=401
out=$(run_with_mock --timeout 5 --interval 1 test/repo 2>&1); rc=$?
assert_exit "401 on branch lookup exits 3" 3 "$rc"
assert_match "401 error message" "authentication failed" "$out"
export MOCK_GLOBAL_CODE=403
out=$(run_with_mock --timeout 5 --interval 1 test/repo 2>&1); rc=$?
assert_exit "403 exits 3" 3 "$rc"
export MOCK_GLOBAL_CODE=404
out=$(run_with_mock --timeout 5 --interval 1 test/repo 2>&1); rc=$?
assert_exit "404 exits 3" 3 "$rc"
assert_match "404 error message" "not found" "$out"
# --- Runs API 401 -> exit 3 ---
unset MOCK_GLOBAL_CODE
export MOCK_BRANCH_BODY="$FIX_BRANCH"
export MOCK_BRANCH_CODE=200
export MOCK_RUNS_CODE=401
out=$(run_with_mock --timeout 5 --interval 1 test/repo 2>&1); rc=$?
assert_exit "runs 401 exits 3" 3 "$rc"
# --- Transient 500 on runs -> retry until timeout ---
export MOCK_RUNS_CODE=500
out=$(run_with_mock --timeout 2 --interval 1 test/repo 2>&1); rc=$?
assert_exit "persistent 500 on runs exits 1 (timeout)" 1 "$rc"
# --- Network error -> retry until timeout ---
unset MOCK_RUNS_CODE
export MOCK_CURL_FAIL=1
out=$(run_with_mock --timeout 2 --interval 1 test/repo 2>&1); rc=$?
assert_exit "network error exits 3 (curl fail on branch)" 3 "$rc"
unset MOCK_CURL_FAIL
# --- With explicit token ---
unset MOCK_BRANCH_CODE MOCK_RUNS_CODE MOCK_GLOBAL_CODE
export MOCK_RUNS_BODY="$FIX_RUNS_SUCCESS"
export MOCK_RELEASE_BODY="$FIX_RELEASE"
out=$(run_with_mock --token secret123 --timeout 5 --interval 1 test/repo 2>/dev/null)
assert_eq "polling with --token still works" "v1.2.3.45" "$out"
# --- Summary ---
echo
total=$(( pass + fail ))
echo -e "Results: ${GREEN}${pass}${NC}/${total} passed"
if (( fail > 0 )); then
echo -e "${RED}${fail} test(s) failed${NC}"
exit 1
fi