"""A-3 — shift buffer isolation & garbage collection. Defects addressed: ① Colliding buffer names (e.g. constant "sks_adhoc" or "sks_onboard") caused cross-agent prompt contamination and silent buffer deletion loss. ② Call-unique buffer names (sks___) fixed ① but introduced a permanent leak of abandoned buffers (SIGINT/SIGTERM/timeout between set-buffer and delete-buffer). Fixed with automatic GC (-mmin +60). ③ F2's atomic write created DOT-prefixed temp files ('.buf_sks_....tmp') that F4's initial GC pattern ('buf_sks_*') missed. Fixed by widening the GC pattern to include '.buf_sks_*' and cleaning up on write/rename failure. """ import os import subprocess import time import pytest BARE_PATH = "/usr/bin:/bin:/usr/sbin:/sbin" def _bash(sandbox, snippet, extra_env=None): env = dict(os.environ) env["PATH"] = BARE_PATH env["WORKSPACE_ROOT"] = str(sandbox) if extra_env: env.update(extra_env) script = f'source "{sandbox}/.agents/skills/lib.sh" >/dev/null 2>&1\n{snippet}' return subprocess.run(["bash", "-c", script], capture_output=True, text=True, cwd=str(sandbox), env=env) # Harness to invoke send_keys_safe without a running herdr daemon or tmux def _run_send_keys_safe(sandbox, sess, text, job_id=None): buflog = sandbox / f"buflog_{time.time_ns()}.txt" job_arg = f' "{job_id}"' if job_id is not None else "" snippet = f""" _pane_quiescent() {{ return 0; }} _pane_dialog_open() {{ return 1; }} _pane_capture() {{ printf '%s' "$SKS_FAKE_PANE"; }} _sks_herdr() {{ case "$1" in set-buffer|paste-buffer|delete-buffer) [ "$2" = "-b" ] && printf '%s %s\\n' "$1" "$3" >> "{buflog}" ;; esac return 0 }} export SKS_FAKE_PANE="● {text}" send_keys_safe "{sess}" "{text}"{job_arg} """ res = _bash(sandbox, snippet) log_lines = buflog.read_text().splitlines() if buflog.exists() else [] return res, log_lines # X-1 — Two calls with the same job_id (e.g. "onboard") get DIFFERENT buffer names def test_a3_same_job_id_gets_different_buffer_names(mam_sandbox): res1, lines1 = _run_send_keys_safe(mam_sandbox, "test_sess1", "hello", "onboard") res2, lines2 = _run_send_keys_safe(mam_sandbox, "test_sess2", "world", "onboard") assert res1.returncode == 0 and res2.returncode == 0 buf1 = lines1[0].split()[1] buf2 = lines2[0].split()[1] assert buf1 != buf2, f"buffer names collided: {buf1} == {buf2}" # X-2 — Within one call, set-buffer, paste-buffer, and delete-buffer use the SAME name def test_a3_triple_uses_same_buffer_name(mam_sandbox): res, lines = _run_send_keys_safe(mam_sandbox, "test_sess1", "test payload", "job1") assert res.returncode == 0 assert len(lines) == 3 cmd0, name0 = lines[0].split() cmd1, name1 = lines[1].split() cmd2, name2 = lines[2].split() assert cmd0 == "set-buffer" and cmd1 == "paste-buffer" and cmd2 == "delete-buffer" assert name0 == name1 == name2 # X-3 — Different buffer names map to different files; delete does not touch another's file def test_a3_different_names_do_not_interfere(mam_sandbox): res = _bash(mam_sandbox, """ SHIM="$WORKSPACE_ROOT/.mam/shim/herdr" DIR="$WORKSPACE_ROOT/.mam/shim" "$SHIM" set-buffer -b "buf_A" "PAYLOAD_A" "$SHIM" set-buffer -b "buf_B" "PAYLOAD_B" assert_file() { [ -f "$DIR/$1" ] && echo "$1:EXISTS" || echo "$1:MISSING"; } assert_file "buf_buf_A" assert_file "buf_buf_B" "$SHIM" delete-buffer -b "buf_A" assert_file "buf_buf_A" assert_file "buf_buf_B" """) assert "buf_buf_A:EXISTS" in res.stdout assert "buf_buf_B:EXISTS" in res.stdout assert "buf_buf_A:MISSING" in res.stdout assert "buf_buf_B:EXISTS" in res.stdout # X-4 — Invalid/empty -b names are rejected explicitly, not falling back to tmp_buffer def test_a3_unusable_buffer_name_is_rejected(mam_sandbox): res = _bash(mam_sandbox, """ SHIM="$WORKSPACE_ROOT/.mam/shim/herdr" "$SHIM" set-buffer -b "!!!" "PAYLOAD" 2>&1 echo "RC:$?" """) assert "RC:1" in res.stdout assert "contains no usable characters" in res.stdout + res.stderr # X-5 — Parallel execution test: 12 callers get isolated buffer text def test_a3_parallel_calls_are_isolated(mam_sandbox): res = _bash(mam_sandbox, """ SHIM="$WORKSPACE_ROOT/.mam/shim/herdr" DIR="$WORKSPACE_ROOT/.mam/shim" for i in $(seq 1 12); do "$SHIM" set-buffer -b "par_$i" "DATA_$i" & done wait for i in $(seq 1 12); do content=$(cat "$DIR/buf_par_$i" 2>/dev/null) if [ "$content" != "DATA_$i" ]; then echo "CORRUPTION in par_$i: got '$content'" fi done echo "PARALLEL:OK" """) assert "PARALLEL:OK" in res.stdout assert "CORRUPTION" not in res.stdout # X-6 — Atomic write does not leave .tmp files behind on success def test_a3_atomic_write_leaves_no_tmp_on_success(mam_sandbox): res = _bash(mam_sandbox, """ SHIM="$WORKSPACE_ROOT/.mam/shim/herdr" DIR="$WORKSPACE_ROOT/.mam/shim" "$SHIM" set-buffer -b "atomic_test" "ATOMIC_PAYLOAD" ls -a "$DIR" """) assert "buf_atomic_test" in res.stdout assert not any(f.endswith(".tmp") for f in res.stdout.split()) # X-7 — Abandoned buffers (older than GC threshold) are reclaimed def test_a3_abandoned_buffers_are_collected(mam_sandbox): res = _bash(mam_sandbox, """ DIR="$WORKSPACE_ROOT/.mam/shim" mkdir -p "$DIR" ABANDONED="$DIR/buf_sks_OLD_1_1" : > "$ABANDONED" touch -t 202501010000 "$ABANDONED" SHIM="$DIR/herdr" "$SHIM" set-buffer -b "trigger_gc" "NEW_DATA" if [ -f "$ABANDONED" ]; then echo "GC:LEAK" else echo "GC:SWEPT" fi """) assert "GC:SWEPT" in res.stdout # X-8 — Live buffers (newer than GC threshold) are preserved def test_a3_live_buffers_are_preserved(mam_sandbox): res = _bash(mam_sandbox, """ DIR="$WORKSPACE_ROOT/.mam/shim" mkdir -p "$DIR" LIVE="$DIR/buf_sks_LIVE_2_2" echo "LIVE_CONTENT" > "$LIVE" SHIM="$DIR/herdr" "$SHIM" set-buffer -b "trigger_gc" "NEW_DATA" if [ -f "$LIVE" ]; then echo "LIVE:PRESERVED" else echo "LIVE:DELETED" fi """) assert "LIVE:PRESERVED" in res.stdout # X-9 — Sweep is scoped to buf_sks_* and does not touch non-matching files def test_a3_sweep_is_scoped(mam_sandbox): res = _bash(mam_sandbox, """ DIR="$WORKSPACE_ROOT/.mam/shim" mkdir -p "$DIR" OTHER="$DIR/other_old_file" : > "$OTHER" touch -t 202501010000 "$OTHER" SHIM="$DIR/herdr" "$SHIM" set-buffer -b "trigger_gc" "NEW_DATA" if [ -f "$OTHER" ]; then echo "OTHER:PRESERVED" else echo "OTHER:DELETED" fi """) assert "OTHER:PRESERVED" in res.stdout # X-10 — Abandoned atomic write DOT-temp files (.buf_sks_*.tmp) are collected def test_a3_abandoned_dot_temps_are_collected(mam_sandbox): res = _bash(mam_sandbox, """ DIR="$WORKSPACE_ROOT/.mam/shim" mkdir -p "$DIR" DOT_TEMP="$DIR/.buf_sks_ORPHAN_1_1.99.tmp" : > "$DOT_TEMP" touch -t 202501010000 "$DOT_TEMP" SHIM="$DIR/herdr" "$SHIM" set-buffer -b "trigger_gc" "NEW_DATA" if [ -f "$DOT_TEMP" ]; then echo "DOT_TEMP:LEAK" else echo "DOT_TEMP:SWEPT" fi """) assert "DOT_TEMP:SWEPT" in res.stdout # X-11 — Failed rename cleans up temp file immediately and fails with exit code 1 def test_a3_failed_rename_cleans_up_and_fails(mam_sandbox): res = _bash(mam_sandbox, """ DIR="$WORKSPACE_ROOT/.mam/shim" mkdir -p "$DIR" # Block creation of buf_sks_blocked by placing a read-only directory in its place BLOCK_DIR="$DIR/buf_sks_blocked" mkdir -p "$BLOCK_DIR" chmod 500 "$BLOCK_DIR" SHIM="$DIR/herdr" "$SHIM" set-buffer -b "sks_blocked" "PAYLOAD" 2>&1 RC=$? echo "RC:$RC" # Clean up permissions so test sandbox can clean up chmod 700 "$BLOCK_DIR" # Check if any .tmp files were left behind ls -a "$DIR" | grep -q '\.tmp$' && echo "TEMP:LEAKED" || echo "TEMP:CLEAN" """) assert "RC:1" in res.stdout assert "TEMP:CLEAN" in res.stdout # X-12 — Scoped GC preserves unrelated dot-files def test_a3_unrelated_dot_files_preserved(mam_sandbox): res = _bash(mam_sandbox, """ DIR="$WORKSPACE_ROOT/.mam/shim" mkdir -p "$DIR" DOT_OTHER="$DIR/.some_other_dot_file" : > "$DOT_OTHER" touch -t 202501010000 "$DOT_OTHER" SHIM="$DIR/herdr" "$SHIM" set-buffer -b "trigger_gc" "NEW_DATA" if [ -f "$DOT_OTHER" ]; then echo "DOT_OTHER:PRESERVED" else echo "DOT_OTHER:DELETED" fi """) assert "DOT_OTHER:PRESERVED" in res.stdout