#!/bin/bash # board-selftest.sh - prove board.sh end-to-end against a throwaway repo tree # and a throwaway mailbox (never touches ~/repos or ~/.claude/coord). Re-run # after any edit to board.sh. ASCII only, bash 3.2 safe. # # Multibyte STATE.md content (em-dash, the NESTE pointing-hand) is generated # with printf octal escapes so this script's own source stays pure ASCII - a # literal em-dash in shell source has crashed bash 3.2 under `set -u` before. set -u export LC_ALL=C DIR="$(cd "$(dirname "$0")" && pwd)" BOARD="$DIR/board.sh" ROOT="$(mktemp -d)" CLAUDE_COORD_DIR="$(mktemp -d)" export CLAUDE_COORD_DIR cleanup() { /bin/rm -rf "$ROOT" "$CLAUDE_COORD_DIR" 2>/dev/null; } trap cleanup EXIT PASS=0; FAIL=0 check() { if [ "$2" -eq 0 ]; then PASS=$((PASS+1)); echo " ok - $1"; else FAIL=$((FAIL+1)); echo " FAIL - $1"; fi; } # Multibyte building blocks (octal escapes keep this source ASCII). EMDASH="$(printf '\342\200\224')" HAND="$(printf '\360\237\221\211')" OSLASH="$(printf '\303\270')" mkrepo() { mkdir -p "$1" && git -C "$1" init -q 2>/dev/null; } echo "board-selftest (root: $ROOT, mailbox: $CLAUDE_COORD_DIR)" # --- 0. Empty root: no repos, still exits cleanly. --- out0="$("$BOARD" --roots "$ROOT" 2>/dev/null)"; rc=$? [ "$rc" -eq 0 ]; check "empty root exits 0" $? # --- Fixture tree --------------------------------------------------------- # repo-a: full board line, in-progress, cheap next step, unhandled inbox. mkrepo "$ROOT/repo-a" { echo "# STATE - repo-a" echo "" printf '## %s NESTE %s START HER\n' "$HAND" "$EMDASH" echo "" printf '**Lukk to MAJOR fra reviewen** %s deretter S3.3 concurrent fan-out.\n' "$EMDASH" } > "$ROOT/repo-a/STATE.md" # repo-b: STATE + NESTE prose but NO board line (the un-backfilled majority). mkrepo "$ROOT/repo-b" { echo "# STATE - repo-b" echo "" printf '## %s NESTE %s START HER\n' "$HAND" "$EMDASH" echo "" printf '> Kj%sr forskningstema 1 headless %s hard gate for motorens arkitektur.\n' "$OSLASH" "$EMDASH" } > "$ROOT/repo-b/STATE.md" # repo-c: no STATE.md at all (dormant class). mkrepo "$ROOT/repo-c" # repo-d: dirty working tree. mkrepo "$ROOT/repo-d" { echo "# STATE - repo-d" printf '## %s NESTE %s START HER\n' "$HAND" "$EMDASH" echo "" echo "Parkert med vilje." } > "$ROOT/repo-d/STATE.md" # Commit STATE.md first, so the dirty count proves board counts UNCOMMITTED # files only (2 junk files) rather than every file in a fresh tree. git -C "$ROOT/repo-d" add STATE.md >/dev/null 2>&1 git -C "$ROOT/repo-d" -c user.email=t@t -c user.name=t commit -qm init >/dev/null 2>&1 : > "$ROOT/repo-d/junk1.txt" : > "$ROOT/repo-d/junk2.txt" # repo-e: malformed status token (must be flagged, never silently accepted). mkrepo "$ROOT/repo-e" { echo "# STATE - repo-e" printf '## %s NESTE %s START HER\n' "$HAND" "$EMDASH" echo "" echo "Ugyldig token." } > "$ROOT/repo-e/STATE.md" # repo-g: prose containing a 'board:'-lookalike ABOVE the real board line. # An unanchored substring grep would match 'dashboard:' first and mis-parse the # whole repo; the board line is defined as living under the NESTE heading. mkrepo "$ROOT/repo-g" { echo "# STATE - repo-g" echo "Vi bygde et dashboard: status=done ble diskutert i forrige runde." printf '## %s NESTE %s START HER\n' "$HAND" "$EMDASH" echo "" echo "Ekte neste steg her." } > "$ROOT/repo-g/STATE.md" # repo-h: next-cost written the way the model rubric actually names the models, # with a space and capitals ("Sonnet 5/xhigh"). This is spec-conformant # (/) and must parse - a lowercase-only pattern silently drops # it to "?" and hides the cost column exactly where it is needed. mkrepo "$ROOT/repo-h" { echo "# STATE - repo-h" printf '## %s NESTE %s START HER\n' "$HAND" "$EMDASH" echo "" echo "Neste steg her." } > "$ROOT/repo-h/STATE.md" # polyrepo/: NOT a git repo itself, but holds git repos one level down. mkdir -p "$ROOT/polyrepo" mkrepo "$ROOT/polyrepo/plug-x" { echo "# STATE - plug-x" printf '## %s NESTE %s START HER\n' "$HAND" "$EMDASH" echo "" printf 'Venter p%s amendment-pakken.\n' "$EMDASH" } > "$ROOT/polyrepo/plug-x/STATE.md" # repo-wt + wt-feature: a git WORKTREE (and a submodule) has .git as a FILE, # not a directory. A `git worktree add /feature-x` yields a sibling # directory that can CARRY its own STATE.md. Testing discovery for a .git # DIRECTORY only drops it silently - the same silent-loss class as the V6 # marker fix. mkrepo "$ROOT/repo-wt" { echo "# STATE - repo-wt" printf '## %s NESTE %s START HER\n' "$HAND" "$EMDASH" echo "" printf 'Hovedtreet %s ferdig.\n' "$EMDASH" } > "$ROOT/repo-wt/STATE.md" git -C "$ROOT/repo-wt" add STATE.md >/dev/null 2>&1 git -C "$ROOT/repo-wt" -c user.email=t@t -c user.name=t commit -qm init >/dev/null 2>&1 git -C "$ROOT/repo-wt" -c user.email=t@t -c user.name=t \ worktree add -q -b feature-x "$ROOT/wt-feature" >/dev/null 2>&1 # Distinct board line, so the check proves board read the STATE INSIDE the # worktree rather than matching the source repo's row by accident. { echo "# STATE - wt-feature" printf '## %s NESTE %s START HER\n' "$HAND" "$EMDASH" echo "" printf 'Arbeid i worktree %s eget neste steg.\n' "$EMDASH" } > "$ROOT/wt-feature/STATE.md" # plain-dir/: no git repo anywhere under it - must be ignored entirely. mkdir -p "$ROOT/plain-dir/sub" echo "hei" > "$ROOT/plain-dir/sub/file.txt" # Coord fixture: 3 unhandled for repo-a, 1 archived (must not be counted). mkdir -p "$CLAUDE_COORD_DIR/repo-a/inbox" "$CLAUDE_COORD_DIR/repo-a/archive" for n in 1 2 3; do echo "msg" > "$CLAUDE_COORD_DIR/repo-a/inbox/2026-msg$n-from-x.md"; done echo "old" > "$CLAUDE_COORD_DIR/repo-a/archive/2026-old-from-x.md" OUT="$("$BOARD" --roots "$ROOT" 2>/dev/null)" # --- 1. Discovery --------------------------------------------------------- printf '%s' "$OUT" | grep -q 'repo-a'; check "discovers top-level git repo" $? printf '%s' "$OUT" | grep -q 'plug-x'; check "discovers nested polyrepo git repo (depth 2)" $? printf '%s' "$OUT" | grep -q 'repo-c'; check "lists git repo without STATE.md" $? printf '%s' "$OUT" | grep -q 'plain-dir'; [ $? -ne 0 ]; check "ignores non-git directory tree" $? printf '%s' "$OUT" | grep -q 'polyrepo'; [ $? -ne 0 ]; check "polyrepo container itself is not listed as a repo" $? # Guard the fixture itself: if git ever stops writing a .git FILE for # worktrees, the next two checks would pass for the wrong reason. [ -f "$ROOT/wt-feature/.git" ]; check "fixture: worktree .git is a FILE, not a dir" $? printf '%s' "$OUT" | grep -q 'wt-feature'; check "discovers git worktree (.git is a file)" $? printf '%s' "$OUT" | grep -qE 'wt-feature.*fable/xhigh' check "reads STATE.md from inside the worktree, not the source repo" $? # --- 2. Board line parsing (the B field) ---------------------------------- printf '%s' "$OUT" | grep -q 'in-progress'; check "parses status token from board line" $? printf '%s' "$OUT" | grep -q 'sonnet/xhigh'; check "parses next-cost from board line" $? printf '%s' "$OUT" | grep -qE 'repo-h.*Sonnet 5/xhigh' check "next-cost accepts spaces and capitals (rubric model names)" $? printf '%s' "$OUT" | grep -qE 'plug-x.*repo-a' check "blocked repo names its blocker on its own row" $? printf '%s' "$OUT" | grep -q 'deferred'; check "distinguishes deferred from blocked" $? # --- 3. Malformed input is flagged, not swallowed ------------------------- printf '%s' "$OUT" | grep -qi 'malformed\|ugyldig\|invalid'; check "malformed status token is flagged" $? # A 'dashboard:' lookalike earlier in the file must not win over the real line. printf '%s' "$OUT" | grep -qE 'repo-g.*planned.*opus/xhigh' check "board-line parse ignores 'board:' lookalikes in prose" $? # --- 4. Heuristic fallback when board line is absent ----------------------- printf '%s' "$OUT" | grep -q 'repo-b'; check "repo without board line still listed" $? printf '%s' "$OUT" | grep -qi 'forskningstema\|headless'; check "shows NESTE excerpt when board line absent" $? # Markdown noise must be stripped: a blockquoted NESTE line renders as prose. printf '%s' "$OUT" | grep -qE 'repo-b.*[[:space:]]>'; [ $? -ne 0 ] check "blockquote marker stripped from NESTE excerpt" $? # --- 5. Git + coord signals ------------------------------------------------ printf '%s' "$OUT" | grep -qE 'repo-d.*[^0-9]2([^0-9]|$)'; check "reports dirty file count" $? printf '%s' "$OUT" | grep -qE 'repo-a.*[^0-9]3([^0-9]|$)'; check "reports unhandled coord inbox count (archive excluded)" $? # --- 6. Bucketing ---------------------------------------------------------- printf '%s' "$OUT" | grep -qi 'blocked'; check "blocked repo surfaced" $? printf '%s' "$OUT" | grep -qiE 'dormant|sovende|uten STATE'; check "STATE-less repos bucketed separately" $? # --- 7. Robustness --------------------------------------------------------- # Multibyte prose must not crash the reader nor emit split-character garbage. [ -n "$OUT" ]; check "produces output over multibyte STATE prose" $? printf '%s' "$OUT" | iconv -f UTF-8 -t UTF-8 >/dev/null 2>&1 check "output is valid UTF-8 (no split multibyte truncation)" $? # A STATE.md with a NESTE heading and nothing after it must not hang or crash. mkrepo "$ROOT/repo-f" { echo "# STATE - repo-f"; printf '## %s NESTE %s START HER\n' "$HAND" "$EMDASH"; } > "$ROOT/repo-f/STATE.md" "$BOARD" --roots "$ROOT" >/dev/null 2>&1; check "empty NESTE block does not crash" $? "$BOARD" --help >/dev/null 2>&1; check "--help exits 0" $? # Unreadable root is a no-op, not a crash. "$BOARD" --roots "$ROOT/does-not-exist" >/dev/null 2>&1; check "missing root is a clean no-op" $? echo "" echo "board-selftest: $PASS passed, $FAIL failed" [ "$FAIL" -eq 0 ] || exit 1 exit 0