From 2b619f1769cae4a96202ca521a9ed4a9db65bc8f Mon Sep 17 00:00:00 2001 From: Craig Jennings Date: Sun, 19 Jul 2026 20:47:44 -0500 Subject: refactor(ai-launcher): extract pure decision cores from the git/tmux paths I moved the decision logic out of the launcher's I/O-coupled functions so it's unit-testable on plain inputs, the hardening the audit called for. Three pure cores now carry logic that was buried in git and tmux calls: _git_prep_action classifies a repo's state into none/pull/report, _order_windows sorts the window listing, and _match_window_id resolves a name to a window id. _git_is_dirty replaces the staged/unstaged/untracked check that was copy-pasted across three functions. prep_git_single and auto_pull_if_clean both route their pull-or-report choice through _git_prep_action, so the "never auto-pull a dirty repo" rule lives in one place with its own test instead of two parallel branches. sort_windows keeps its two-pass move (park high, then reassign) but hands the ordering to _order_windows, and find_window_id is now a thin tmux wrapper over _match_window_id. Each core gets a Normal/Boundary/Error set: the git-prep truth table including diverged and behind-while-dirty, the window ordering including whole-line-not-substring matching. The 42 launcher tests stay green and a live black-box run still prints the right launch command for claude and codex, so the extraction is behavior-neutral. I also ran shfmt (-i 2 -ci) to match the house style the other bin/ scripts follow. --- scripts/tests/ai-launcher-characterization.bats | 79 +++++++++++++++++++++++++ 1 file changed, 79 insertions(+) (limited to 'scripts') diff --git a/scripts/tests/ai-launcher-characterization.bats b/scripts/tests/ai-launcher-characterization.bats index 773181d..f53855f 100644 --- a/scripts/tests/ai-launcher-characterization.bats +++ b/scripts/tests/ai-launcher-characterization.bats @@ -243,3 +243,82 @@ _mk_repo_upstream() { [ "$status" -eq 1 ] [[ "$output" == *"no 'ai' session"* ]] } + +# --- extracted pure cores ----------------------------------------------------- + +@test "_git_prep_action: no upstream is always 'none'" { + run _git_prep_action 0 0 0 5 + [ "$output" = none ] +} + +@test "_git_prep_action: clean and purely behind is 'pull'" { + run _git_prep_action 1 0 0 3 + [ "$output" = pull ] +} + +@test "_git_prep_action: in sync with upstream is 'none'" { + run _git_prep_action 1 0 0 0 + [ "$output" = none ] +} + +@test "_git_prep_action: ahead is 'report', never 'pull'" { + run _git_prep_action 1 0 2 0 + [ "$output" = report ] +} + +@test "_git_prep_action: dirty is 'report', never 'pull'" { + run _git_prep_action 1 1 0 0 + [ "$output" = report ] +} + +@test "_git_prep_action: behind AND dirty is 'report' — a dirty repo is never auto-pulled" { + run _git_prep_action 1 1 0 3 + [ "$output" = report ] +} + +@test "_git_prep_action: diverged (ahead and behind) is 'report'" { + run _git_prep_action 1 0 2 3 + [ "$output" = report ] +} + +@test "_order_windows: others alpha, then projects alpha" { + local listing names out + listing="$(printf 'beta\t@1\nzzz-other\t@2\nalpha\t@3\naaa-other\t@4')" + names="$(printf 'alpha\nbeta')" + out="$(printf '%s\n' "$listing" | _order_windows "$names" | cut -f1 | paste -sd, -)" + [ "$out" = "aaa-other,zzz-other,alpha,beta" ] +} + +@test "_order_windows: all-projects input keeps only the projects, sorted" { + local out + out="$(printf 'beta\t@1\nalpha\t@2\n' | _order_windows "$(printf 'alpha\nbeta')" | cut -f1 | paste -sd, -)" + [ "$out" = "alpha,beta" ] +} + +@test "_order_windows: empty input yields empty output" { + run _order_windows "$(printf 'alpha\nbeta')" <<<"" + [ -z "$output" ] +} + +@test "_order_windows: a name matches only as a whole line, not as a substring" { + # 'alph' must not match project 'alpha' (grep -qxF is a full-line match). + local out + out="$(printf 'alph\t@1\n' | _order_windows "$(printf 'alpha')" | cut -f1)" + # 'alph' is not a project, so it lands in others, still present. + [ "$out" = "alph" ] +} + +@test "_match_window_id: returns the id for a name, empty for a miss" { + local listing out + listing="$(printf 'one\t@1\ntwo\t@2\n')" + out="$(printf '%s\n' "$listing" | _match_window_id two)" + [ "$out" = "@2" ] + out="$(printf '%s\n' "$listing" | _match_window_id nope)" + [ -z "$out" ] +} + +@test "_match_window_id: first match wins and stops" { + local out + out="$(printf 'dup\t@1\ndup\t@2\n' | _match_window_id dup)" + [ "$out" = "@1" ] +} -- cgit v1.2.3