diff options
| author | Craig Jennings <c@cjennings.net> | 2026-06-15 01:14:46 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-06-15 01:14:46 -0500 |
| commit | f8bdf302fd71ba73ae1424d86275e1e223f09cf3 (patch) | |
| tree | 4f46c33a70d89ecb939d5f87c5fed3412515be58 /.ai/scripts/tests | |
| parent | e0f914d510c081db45cafaf4fe5c8f7b65e46fec (diff) | |
| download | rulesets-f8bdf302fd71ba73ae1424d86275e1e223f09cf3.tar.gz rulesets-f8bdf302fd71ba73ae1424d86275e1e223f09cf3.zip | |
feat(ai): add agent-roster detection script with tests
agent-roster is the single detection primitive for concurrent same-project Claude sessions: pgrep -x claude, resolve each pid's cwd from /proc, keep those at or inside the project root, and drop the scanner's own ancestry. It exits 0 when alone, 1 when other agents are present (printed pid + cwd), and 2 when the roster can't run. Both the helper launcher and the in-session startup check will call this rather than re-scanning.
pgrep and /proc are the system boundary, so I made them injectable (ROSTER_PGREP, ROSTER_PROC, ROSTER_SELF_PID) and the bats exercise the real include/exclude logic against fixtures, no agents spawned. The unavailable paths (no /proc, or pgrep absent) report on stderr and exit 2 rather than a false "alone".
This is the first slice of the helper-instance task. Startup and ai --helper wiring follow.
Diffstat (limited to '.ai/scripts/tests')
| -rw-r--r-- | .ai/scripts/tests/agent-roster.bats | 141 |
1 files changed, 141 insertions, 0 deletions
diff --git a/.ai/scripts/tests/agent-roster.bats b/.ai/scripts/tests/agent-roster.bats new file mode 100644 index 0000000..939a7df --- /dev/null +++ b/.ai/scripts/tests/agent-roster.bats @@ -0,0 +1,141 @@ +#!/usr/bin/env bats +# Tests for agent-roster: report other live Claude agents in a project. +# +# pgrep and /proc are the system boundary, so the test injects both and runs +# the real include/exclude logic against fixtures — no Claude processes are +# spawned. Injection points: +# ROSTER_PGREP command standing in for pgrep (a stub printing $FAKE_PIDS) +# ROSTER_PROC proc dir (a fixture of <pid>/cwd symlinks + <pid>/status) +# ROSTER_SELF_PID the scanner's own pid, so the ancestry walk is testable +# +# Fixture process tree: pid 1000 (the scanner) is a child of 999 (the current +# session's claude), which is a child of init (1). So 999 must always be +# excluded as scanner ancestry; other claude pids are judged by cwd. + +setup() { + SCRIPT_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)" + ROSTER="$SCRIPT_DIR/agent-roster" + + PROC="$BATS_TEST_TMPDIR/proc" + ROOT="$BATS_TEST_TMPDIR/project" + mkdir -p "$PROC" "$ROOT/sub" "$BATS_TEST_TMPDIR/elsewhere" + + PGREP_STUB="$BATS_TEST_TMPDIR/pgrep" + cat >"$PGREP_STUB" <<'EOF' +#!/usr/bin/env bash +printf '%s\n' $FAKE_PIDS +EOF + chmod +x "$PGREP_STUB" + + SELF=1000 + # scanner (1000) <- session claude (999) <- init (1) + mkproc 1000 "$ROOT" 999 + mkproc 999 "$ROOT" 1 +} + +# mkproc PID CWD PPID — register a fake process in the fixture proc dir. +mkproc() { + mkdir -p "$PROC/$1" + ln -sf "$2" "$PROC/$1/cwd" + printf 'PPid:\t%s\n' "$3" >"$PROC/$1/status" +} + +run_roster() { + ROSTER_PGREP="$PGREP_STUB" ROSTER_PROC="$PROC" ROSTER_SELF_PID="$SELF" \ + FAKE_PIDS="$FAKE_PIDS" run "$ROSTER" "$ROOT" +} + +@test "agent-roster: alone (only the session's own claude) exits 0, no output" { + FAKE_PIDS="999" + run_roster + [ "$status" -eq 0 ] + [ -z "$output" ] +} + +@test "agent-roster: one other agent in-project is printed, exit 1" { + mkproc 2000 "$ROOT" 1 + FAKE_PIDS="999 2000" + run_roster + [ "$status" -eq 1 ] + [[ "$output" == *"2000"* ]] + [[ "$output" == *"$ROOT"* ]] +} + +@test "agent-roster: two other agents both printed, exit 1" { + mkproc 2000 "$ROOT" 1 + mkproc 2001 "$ROOT/sub" 1 + FAKE_PIDS="999 2000 2001" + run_roster + [ "$status" -eq 1 ] + [[ "$output" == *"2000"* ]] + [[ "$output" == *"2001"* ]] + [ "${#lines[@]}" -eq 2 ] +} + +@test "agent-roster: the scanner's session-claude ancestor is excluded even with matching cwd" { + # 999 has cwd == ROOT but is scanner ancestry; must not appear. + FAKE_PIDS="999" + run_roster + [ "$status" -eq 0 ] + [[ "$output" != *"999"* ]] +} + +@test "agent-roster: cwd outside the project root is excluded" { + mkproc 3000 "$BATS_TEST_TMPDIR/elsewhere" 1 + FAKE_PIDS="999 3000" + run_roster + [ "$status" -eq 0 ] + [ -z "$output" ] +} + +@test "agent-roster: cwd in a subdirectory of root is included" { + mkproc 2002 "$ROOT/sub" 1 + FAKE_PIDS="999 2002" + run_roster + [ "$status" -eq 1 ] + [[ "$output" == *"2002"* ]] +} + +@test "agent-roster: a sibling path sharing a prefix is not a false match" { + # ROOT is .../project; .../project-other must not count as inside it. + mkdir -p "$BATS_TEST_TMPDIR/project-other" + mkproc 3100 "$BATS_TEST_TMPDIR/project-other" 1 + FAKE_PIDS="999 3100" + run_roster + [ "$status" -eq 0 ] + [ -z "$output" ] +} + +@test "agent-roster: a pid that vanished between pgrep and the proc read is skipped" { + # 4000 has no fixture dir, simulating a process gone by readlink time. + FAKE_PIDS="999 4000" + run_roster + [ "$status" -eq 0 ] + [ -z "$output" ] +} + +@test "agent-roster: missing proc reports unavailable on stderr, exit 2, never silent-alone" { + ROSTER_PGREP="$PGREP_STUB" ROSTER_PROC="$BATS_TEST_TMPDIR/nonexistent" \ + ROSTER_SELF_PID="$SELF" FAKE_PIDS="999 2000" run "$ROSTER" "$ROOT" + [ "$status" -eq 2 ] + [[ "$output" == *"roster unavailable"* ]] +} + +@test "agent-roster: a missing pgrep reports unavailable, exit 2, never silent-alone" { + # If pgrep itself is absent, the scan can't run; reporting "alone" would be a + # false negative the "never silent-alone" invariant forbids. + mkproc 2000 "$ROOT" 1 + ROSTER_PGREP="$BATS_TEST_TMPDIR/no-such-pgrep" ROSTER_PROC="$PROC" \ + ROSTER_SELF_PID="$SELF" FAKE_PIDS="999 2000" run "$ROSTER" "$ROOT" + [ "$status" -eq 2 ] + [[ "$output" == *"roster unavailable"* ]] +} + +@test "agent-roster: defaults project root to PWD when no argument is given" { + mkproc 2000 "$ROOT" 1 + FAKE_PIDS="999 2000" + ROSTER_PGREP="$PGREP_STUB" ROSTER_PROC="$PROC" ROSTER_SELF_PID="$SELF" \ + FAKE_PIDS="$FAKE_PIDS" run env -C "$ROOT" "$ROSTER" + [ "$status" -eq 1 ] + [[ "$output" == *"2000"* ]] +} |
