#!/usr/bin/env bats # # Tests for claude-templates/.ai/scripts/route-batch — the wrap-up router's # mechanical go path (wrapup-routing spec, Phase 4 / D7 / D9). # # Contract under test: # route-batch --list one "\t" line per task # carrying :ROUTE_CANDIDATE:; silent when none; # never modifies anything # route-batch --go per candidate: write the subtree (minus the # :ROUTE_CANDIDATE: line) as a one-task handoff, # deliver via inbox-send to the destination's # inbox/, then remove the subtree from the local # todo.org. Send failure leaves the task in # place and exits non-zero. Empty set: no-op. # # Strategy: fixture roots under $TEST_DIR hold a source project and two # destination projects; INBOX_SEND_ROOTS sandboxes inbox-send's discovery to # them (the same hook inbox-send's own tests use). SCRIPT="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)/route-batch" setup() { TEST_DIR="$(mktemp -d -t route-batch-bats.XXXXXX)" ROOTS="$TEST_DIR/roots" SRC="$ROOTS/srcproj" mkdir -p "$SRC/.ai" "$SRC/inbox" \ "$ROOTS/alpha/.ai" "$ROOTS/alpha/inbox" \ "$ROOTS/beta/.ai" "$ROOTS/beta/inbox" touch "$ROOTS/alpha/todo.org" # alpha has a todo.org; beta deliberately not cat > "$SRC/todo.org" <<'EOF' * Srcproj Open Work ** TODO [#B] Alpha-bound task :feature: :PROPERTIES: :ROUTE_CANDIDATE: alpha :END: Body line about the alpha work. *** TODO Sub-task that rides along ** TODO [#C] Purely local task Local body stays put. ** TODO [#C] Beta-bound task :quick: :PROPERTIES: :CREATED: [2026-07-01 Tue] :ROUTE_CANDIDATE: beta :END: Beta body. EOF export INBOX_SEND_ROOTS="$ROOTS" cd "$SRC" } teardown() { rm -rf "$TEST_DIR" } # ---- --list ------------------------------------------------------------ @test "route-batch --list: one destination+heading line per candidate, backlog excluded" { run "$SCRIPT" --list [ "$status" -eq 0 ] [[ "$output" == *"alpha"*"Alpha-bound task"* ]] [[ "$output" == *"beta"*"Beta-bound task"* ]] [[ "$output" != *"Purely local task"* ]] } @test "route-batch --list: empty candidate set is silent (exit 0)" { sed -i '/:ROUTE_CANDIDATE:/d' todo.org run "$SCRIPT" --list [ "$status" -eq 0 ] [ -z "$output" ] } @test "route-batch --list: modifies nothing (skip leaves all in place)" { before="$(cat todo.org)" run "$SCRIPT" --list [ "$status" -eq 0 ] [ "$(cat todo.org)" = "$before" ] [ -z "$(ls "$ROOTS/alpha/inbox" "$ROOTS/beta/inbox" 2>/dev/null | grep -v ':')" ] } # ---- --go -------------------------------------------------------------- @test "route-batch --go: delivers each candidate to its destination inbox with provenance" { run "$SCRIPT" --go [ "$status" -eq 0 ] alpha_file=$(find "$ROOTS/alpha/inbox" -name '*from-srcproj*' -type f) beta_file=$(find "$ROOTS/beta/inbox" -name '*from-srcproj*' -type f) [ -n "$alpha_file" ] [ -n "$beta_file" ] grep -q 'Alpha-bound task' "$alpha_file" grep -q 'Sub-task that rides along' "$alpha_file" # children ride along grep -q 'Beta-bound task' "$beta_file" ! grep -q ':ROUTE_CANDIDATE:' "$alpha_file" ! grep -q ':ROUTE_CANDIDATE:' "$beta_file" } @test "route-batch --go: removes routed subtrees from todo.org, leaves local tasks" { run "$SCRIPT" --go [ "$status" -eq 0 ] ! grep -q 'Alpha-bound task' todo.org ! grep -q 'Sub-task that rides along' todo.org ! grep -q 'Beta-bound task' todo.org grep -q 'Purely local task' todo.org grep -q 'Local body stays put' todo.org } @test "route-batch --go: a kept property drawer survives minus the marker" { run "$SCRIPT" --go [ "$status" -eq 0 ] beta_file=$(find "$ROOTS/beta/inbox" -name '*from-srcproj*' -type f) grep -q ':CREATED: \[2026-07-01 Tue\]' "$beta_file" } @test "route-batch --go: destination with inbox/ but no todo.org still delivers" { run "$SCRIPT" --go [ "$status" -eq 0 ] [ ! -f "$ROOTS/beta/todo.org" ] [ -n "$(find "$ROOTS/beta/inbox" -name '*from-srcproj*' -type f)" ] } @test "route-batch --go: empty candidate set is a silent no-op (exit 0)" { sed -i '/:ROUTE_CANDIDATE:/d' todo.org before="$(cat todo.org)" run "$SCRIPT" --go [ "$status" -eq 0 ] [ -z "$output" ] [ "$(cat todo.org)" = "$before" ] } @test "route-batch --go: a failed send leaves that task in place, marker intact, and exits non-zero" { sed -i 's/:ROUTE_CANDIDATE: beta/:ROUTE_CANDIDATE: ghost/' todo.org run "$SCRIPT" --go [ "$status" -ne 0 ] grep -q 'Beta-bound task' todo.org # failed route stays local grep -q ':ROUTE_CANDIDATE: ghost' todo.org # marker survives so it resurfaces next wrap ! grep -q 'Alpha-bound task' todo.org # the good route still landed [ -n "$(find "$ROOTS/alpha/inbox" -name '*from-srcproj*' -type f)" ] } @test "route-batch --go: handoff headings are promoted to top level" { run "$SCRIPT" --go [ "$status" -eq 0 ] alpha_file=$(find "$ROOTS/alpha/inbox" -name '*from-srcproj*' -type f) grep -q '^\* TODO \[#B\] Alpha-bound task' "$alpha_file" grep -q '^\*\* TODO Sub-task that rides along' "$alpha_file" } @test "route-batch --go: a drawer emptied by the marker strip is pruned from the handoff" { run "$SCRIPT" --go [ "$status" -eq 0 ] alpha_file=$(find "$ROOTS/alpha/inbox" -name '*from-srcproj*' -type f) ! grep -q ':PROPERTIES:' "$alpha_file" } # ---- Overlapping candidates (nested marker data-loss regression) -------- @test "route-batch --go: nested candidates conflict — both stay, bystander survives, exit non-zero" { cat > todo.org <<'EOF' * Srcproj Open Work ** TODO [#B] Parent bound for alpha :PROPERTIES: :ROUTE_CANDIDATE: alpha :END: Parent body. *** TODO Child bound for beta :PROPERTIES: :ROUTE_CANDIDATE: beta :END: Child body. ** TODO [#C] Innocent bystander task Bystander body. EOF run "$SCRIPT" --go [ "$status" -ne 0 ] [[ "$output" == *"CONFLICT"* ]] grep -q 'Parent bound for alpha' todo.org grep -q 'Child bound for beta' todo.org grep -q 'Innocent bystander task' todo.org grep -q 'Bystander body' todo.org [ -z "$(find "$ROOTS/alpha/inbox" "$ROOTS/beta/inbox" -name '*from-srcproj*' -type f)" ] } @test "route-batch: duplicate identical markers in one drawer dedupe to a single route" { cat > todo.org <<'EOF' * Srcproj Open Work ** TODO [#B] Double-tagged for alpha :PROPERTIES: :ROUTE_CANDIDATE: alpha :ROUTE_CANDIDATE: alpha :END: Body. EOF run "$SCRIPT" --list [ "$status" -eq 0 ] [ "$(echo "$output" | grep -c 'Double-tagged')" -eq 1 ] [[ "$output" != *"CONFLICT"* ]] run "$SCRIPT" --go [ "$status" -eq 0 ] [ "$(find "$ROOTS/alpha/inbox" -name '*from-srcproj*' -type f | wc -l)" -eq 1 ] }