#!/usr/bin/env bats # # Tests for claude-templates/.ai/scripts/task-review-staleness.sh — # counts top-level todo.org tasks whose review has gone stale. # # Strategy: write a synthetic todo.org into a temp dir per test, with # LAST_REVIEWED dates generated relative to the real `date` (never # hardcoded). Run the real script against it and assert the count it # prints on stdout. # # Staleness rule under test: # - A qualifying task is a depth-2 (**) heading with a TODO/DOING/VERIFY # keyword and an [#A]/[#B]/[#C] priority cookie. # - It is stale when LAST_REVIEWED is missing/malformed (NIL → oldest), # or when its age strictly exceeds the threshold (age > N days). # - age == N exactly is fresh (the spec's wording is ">N days"). # The script under test is always the sibling-of-parent of this test file # (scripts/task-review-staleness.sh next to scripts/tests/). This holds in # both the canonical claude-templates/ tree and the rsync'd project mirror, # so the suite runs identically from either location. SCRIPT="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)/task-review-staleness.sh" setup() { TEST_DIR="$(mktemp -d -t task-review-bats.XXXXXX)" TODO="$TEST_DIR/todo.org" TODAY="$(date +%F)" D5="$(date -d '5 days ago' +%F)" D30="$(date -d '30 days ago' +%F)" D31="$(date -d '31 days ago' +%F)" D40="$(date -d '40 days ago' +%F)" } teardown() { rm -rf "$TEST_DIR" } # Emit a qualifying task with an explicit LAST_REVIEWED date. task_reviewed() { local keyword="$1" prio="$2" title="$3" date="$4" printf '** %s [#%s] %s\n:PROPERTIES:\n:LAST_REVIEWED: %s\n:END:\nBody.\n\n' \ "$keyword" "$prio" "$title" "$date" >> "$TODO" } # Emit a qualifying task with no PROPERTIES drawer at all. task_unreviewed() { local keyword="$1" prio="$2" title="$3" printf '** %s [#%s] %s\nBody.\n\n' "$keyword" "$prio" "$title" >> "$TODO" } # ---- Normal cases ---------------------------------------------------- @test "staleness: empty file reports zero" { : > "$TODO" run bash "$SCRIPT" "$TODO" 30 [ "$status" -eq 0 ] [ "$output" = "0" ] } @test "staleness: all tasks fresh reports zero" { task_reviewed TODO A "Fresh one" "$D5" task_reviewed TODO B "Fresh two" "$D5" task_reviewed DOING A "Fresh three" "$TODAY" run bash "$SCRIPT" "$TODO" 30 [ "$status" -eq 0 ] [ "$output" = "0" ] } @test "staleness: all tasks stale reports full count" { task_reviewed TODO A "Stale one" "$D40" task_reviewed TODO B "Stale two" "$D40" task_reviewed VERIFY C "Stale three" "$D40" run bash "$SCRIPT" "$TODO" 30 [ "$status" -eq 0 ] [ "$output" = "3" ] } @test "staleness: mixed fresh, stale, and unreviewed counts only the latter two" { task_reviewed TODO A "Fresh" "$D5" task_reviewed TODO B "Stale" "$D40" task_unreviewed DOING A "Never reviewed" run bash "$SCRIPT" "$TODO" 30 [ "$status" -eq 0 ] [ "$output" = "2" ] } # ---- Boundary cases -------------------------------------------------- @test "staleness: age exactly equal to threshold is fresh" { task_reviewed TODO A "Exactly at cutoff" "$D30" run bash "$SCRIPT" "$TODO" 30 [ "$status" -eq 0 ] [ "$output" = "0" ] } @test "staleness: age one day past threshold is stale" { task_reviewed TODO A "One day over" "$D31" run bash "$SCRIPT" "$TODO" 30 [ "$status" -eq 0 ] [ "$output" = "1" ] } @test "staleness: unreviewed task (no drawer) counts as stale" { task_unreviewed TODO A "Never reviewed" run bash "$SCRIPT" "$TODO" 30 [ "$status" -eq 0 ] [ "$output" = "1" ] } @test "staleness: threshold of 7 is softer than 30 on the same list" { task_reviewed TODO A "Reviewed five days ago" "$D5" task_reviewed TODO B "Reviewed thirty-one days ago" "$D31" run bash "$SCRIPT" "$TODO" 7 [ "$status" -eq 0 ] [ "$output" = "1" ] } # ---- Error / exclusion cases ----------------------------------------- @test "staleness: DONE and CANCELLED tasks are excluded even when old" { task_reviewed DONE A "Shipped long ago" "$D40" task_reviewed CANCELLED B "Abandoned long ago" "$D40" run bash "$SCRIPT" "$TODO" 30 [ "$status" -eq 0 ] [ "$output" = "0" ] } @test "staleness: deeper headings and cookie-less headings are excluded" { # Depth-3 child with an old review date — not a review unit. printf '*** TODO [#A] Child task\n:PROPERTIES:\n:LAST_REVIEWED: %s\n:END:\n\n' "$D40" >> "$TODO" # Depth-2 but no priority cookie — not a review unit. printf '** TODO Cookie-less task\nBody.\n\n' >> "$TODO" run bash "$SCRIPT" "$TODO" 30 [ "$status" -eq 0 ] [ "$output" = "0" ] } @test "staleness: malformed LAST_REVIEWED is treated as stale" { task_reviewed TODO A "Bad date" "not-a-date" run bash "$SCRIPT" "$TODO" 30 [ "$status" -eq 0 ] [ "$output" = "1" ] } @test "staleness: missing todo file exits non-zero" { run bash "$SCRIPT" "$TEST_DIR/does-not-exist.org" 30 [ "$status" -ne 0 ] } # ---- --list mode ----------------------------------------------------- @test "staleness --list: orders oldest-reviewed first, unreviewed before dated" { task_reviewed TODO A "Reviewed recently" "$D5" task_reviewed TODO B "Reviewed long ago" "$D40" task_unreviewed DOING A "Never reviewed" run bash "$SCRIPT" --list "$TODO" 10 [ "$status" -eq 0 ] [[ "${lines[0]}" == *"Never reviewed"* ]] [[ "${lines[1]}" == *"Reviewed long ago"* ]] [[ "${lines[2]}" == *"Reviewed recently"* ]] } @test "staleness --list: takes only the requested count" { task_unreviewed TODO A "First" task_reviewed TODO B "Second" "$D40" task_reviewed TODO C "Third" "$D30" run bash "$SCRIPT" --list "$TODO" 2 [ "$status" -eq 0 ] [ "${#lines[@]}" -eq 2 ] } @test "staleness --list: count larger than available returns all candidates" { task_unreviewed TODO A "Only one" run bash "$SCRIPT" --list "$TODO" 10 [ "$status" -eq 0 ] [ "${#lines[@]}" -eq 1 ] } @test "staleness --list: excludes DONE, deeper headings, and cookie-less headings" { task_reviewed DONE A "Shipped" "$D40" printf '*** TODO [#A] Child task\n\n' >> "$TODO" printf '** TODO Cookie-less\n\n' >> "$TODO" task_unreviewed TODO B "Real one" run bash "$SCRIPT" --list "$TODO" 10 [ "$status" -eq 0 ] [ "${#lines[@]}" -eq 1 ] [[ "${lines[0]}" == *"Real one"* ]] } @test "staleness --list: emits line, review value, and heading tab-separated" { task_unreviewed TODO A "Never reviewed" run bash "$SCRIPT" --list "$TODO" 10 [ "$status" -eq 0 ] line_no=$(printf '%s' "${lines[0]}" | cut -f1) value=$(printf '%s' "${lines[0]}" | cut -f2) heading=$(printf '%s' "${lines[0]}" | cut -f3) [ "$line_no" = "1" ] [ "$value" = "NONE" ] [ "$heading" = "** TODO [#A] Never reviewed" ] } @test "staleness --list: empty file produces no output" { : > "$TODO" run bash "$SCRIPT" --list "$TODO" 10 [ "$status" -eq 0 ] [ -z "$output" ] } @test "staleness --list: missing file exits non-zero" { run bash "$SCRIPT" --list "$TEST_DIR/nope.org" 10 [ "$status" -ne 0 ] }