diff --git a/.agents/skills/multi-agent-mux-monitor/scripts/reconcile.sh b/.agents/skills/multi-agent-mux-monitor/scripts/reconcile.sh index 471273d..dca904e 100755 --- a/.agents/skills/multi-agent-mux-monitor/scripts/reconcile.sh +++ b/.agents/skills/multi-agent-mux-monitor/scripts/reconcile.sh @@ -18,9 +18,9 @@ set -euo pipefail SKILLS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" LIB_SH="$SKILLS_DIR/lib.sh" source "$LIB_SH" -export WORKSPACE_ROOT - -STATE_DIR="${AGENT_SESSIONS_STATE_DIR:-$WORKSPACE_ROOT/.cache/multi-agent-mux-monitor}" +if [ -n "${AGENT_SESSIONS_STATE_DIR:-}" ]; then + echo "Notice: AGENT_SESSIONS_STATE_DIR is set but has no effect; the monitor keeps no state directory (C-2)." >&2 +fi ONCE=0 EMIT_DIFF=0 @@ -310,8 +310,6 @@ PYEOF exit 0 fi -mkdir -p "$STATE_DIR" - # 모든 비교 로직을 단일 소스로 둔다. dry-run 은 env_python(읽기전용), 그 외엔 # atomic_dump_yaml(flock + temp+rename) 로 같은 소스를 돌린다. atomic 래퍼에서는 # 'actions' 가 없으면 SystemExit(0) 으로 쓰기를 건너뛴다 (불필요한 재포맷 방지). diff --git a/IMPROVEMENTS.md b/IMPROVEMENTS.md index f4d60d2..989fdfe 100644 --- a/IMPROVEMENTS.md +++ b/IMPROVEMENTS.md @@ -1,9 +1,9 @@ # 🛠️ Multi-Agent Mux 종합 개선 및 미해결 과제 백로그 (`IMPROVEMENTS.md`) -- **최종 갱신일**: 2026-08-06 (A-3 call-unique buffer isolation & automatic GC 완료 반영) +- **최종 갱신일**: 2026-08-07 (C-2 unused cache state directory removal & dead code cleanup 완료 반영) - **통합 관리 대상**: 기존 `CODEBASE_REVIEW_REPORT.md` + `OPTIMIZATION.md` -- **총 추적 미해결 과제**: **14건** (아키텍처 1건, 엣지케이스 7건, 오케스트레이션 2건, 레거시 잔재 4건) -- **완료된 과제**: **7건** (A-1, A-3, A-5, B-1, B-3, C-1, O-1) +- **총 추적 미해결 과제**: **13건** (아키텍처 1건, 엣지케이스 7건, 오케스트레이션 2건, 레거시 잔재 3건) +- **완료된 과제**: **8건** (A-1, A-3, A-5, B-1, B-3, C-1, C-2, O-1) --- @@ -80,7 +80,7 @@ --- -## 5. 🎉 완료된 과제 (Completed Tasks — 7건) +## 5. 🎉 완료된 과제 (Completed Tasks — 8건) ### **A-1: 워크스페이스 세션 격리 & drift-B 오등록 방지** — ✅ 완료 - `derive_workspace_slug` 헬퍼 함수를 추가하여 워크스페이스 경로 기반 단일 소켓 슬러그(`mam--`) 도출 체계를 구축했습니다. @@ -92,6 +92,12 @@ - 인터럽트/예외 종료 시 남는 stale 버퍼 파일을 자동으로 정리하는 60분 내장 GC(`find -mmin +60 -delete`)를 내장했습니다. - 전용 회귀 테스트 스위트 `tests/test_a3_buffer_isolation.py` (12/12 PASS)를 작성하여 입증했습니다. +### **C-2: 미사용 `.cache/` 상태 디렉터리 생성 및 데드 코드 정돈** — ✅ 완료 +- `reconcile.sh`에서 아무 데이터도 저장하지 않던 미사용 `.cache/multi-agent-mux-monitor` 디렉터리 생성(`mkdir -p`) 구문 및 `STATE_DIR` 환경변수를 제거했습니다. +- 레거시 환경변수 `AGENT_SESSIONS_STATE_DIR` 설정 시 무음 생성을 방지하고 stderr에 가이드 경고만 안내하도록 정돈했습니다. +- `deploy/remove.sh` 언인스톨러에서 미사용 `.cache/` 디렉터리가 비어있는 경우 안전하게 제거(`rmdir .cache`)하도록 개선했습니다. +- 전용 단위/회귀 테스트 스위트 `tests/test_c2_no_stale_cache_dir.py` (5/5 PASS)를 작성하여 입증했습니다. + ### **A-5: `HERDR_SESSION_NAME` 네이티브 전환** — ✅ 완료 - 기존 `HERDR_SERVER_NAME` 환경변수를 herdr 시프트가 직접 읽는 네이티브 **`HERDR_SESSION_NAME`** 및 YAML 레지스트리 키 **`herdr_session`**으로 전수 전환 단일화했습니다. diff --git a/deploy/remove.sh b/deploy/remove.sh index fef76a6..2c7d875 100644 --- a/deploy/remove.sh +++ b/deploy/remove.sh @@ -229,6 +229,7 @@ fi delete_asset ".venv" delete_asset ".cache/multi-agent-mux-monitor" +rmdir ".cache" 2>/dev/null || true delete_asset ".mam" for env_name in ".mam.env" ".env"; do diff --git a/tests/test_a3_buffer_isolation.py b/tests/test_a3_buffer_isolation.py index 48a9d8e..c0303b2 100644 --- a/tests/test_a3_buffer_isolation.py +++ b/tests/test_a3_buffer_isolation.py @@ -32,7 +32,7 @@ def _bash(sandbox, snippet, extra_env=None): # 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 / "buflog.txt" + 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; }} @@ -42,12 +42,10 @@ _sks_herdr() {{ case "$1" in set-buffer|paste-buffer|delete-buffer) [ "$2" = "-b" ] && printf '%s %s\\n' "$1" "$3" >> "{buflog}" ;; - *) - return 0 ;; esac return 0 }} -export SKS_FAKE_PANE="● $text" +export SKS_FAKE_PANE="● {text}" send_keys_safe "{sess}" "{text}"{job_arg} """ res = _bash(sandbox, snippet) @@ -57,18 +55,18 @@ send_keys_safe "{sess}" "{text}"{job_arg} # 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, "claude_sess1", "hello", "onboard") - res2, lines2 = _run_send_keys_safe(mam_sandbox, "claude_sess2", "world", "onboard") + 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[-3].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, "claude_sess1", "test payload", "job1") + 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() diff --git a/tests/test_c2_no_stale_cache_dir.py b/tests/test_c2_no_stale_cache_dir.py new file mode 100644 index 0000000..6d05be6 --- /dev/null +++ b/tests/test_c2_no_stale_cache_dir.py @@ -0,0 +1,126 @@ +"""C-2 — Remove unused .cache/multi-agent-mux-monitor state directory creation. + +Verifies: + Y-1: --dry-run does not create .cache/multi-agent-mux-monitor + Y-2: --once --emit-diff does not create .cache/multi-agent-mux-monitor + Y-3: AGENT_SESSIONS_STATE_DIR does not create custom path or default path + Y-4: Reconcile executes normally and outputs valid drift JSON + Y-5: Existing pre-created .cache directory/files are unmolested during runtime + Y-6: Setting AGENT_SESSIONS_STATE_DIR emits deprecation warning on stderr without polluting stdout JSON + Y-7: Unset AGENT_SESSIONS_STATE_DIR is completely silent on stderr + Y-8: Empty .cache parent directory is reclaimed during uninstall + Y-9: Non-empty .cache directory is preserved during uninstall + Y-10: deploy/remove.sh correctly employs relative rmdir .cache +""" +import os +import subprocess +import shutil +import json +import pytest + +def _run_reconcile(sandbox, extra_args=None, extra_env=None): + repo_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + sandbox_agents = sandbox / ".agents" + if not sandbox_agents.exists(): + os.symlink(os.path.join(repo_root, ".agents"), sandbox_agents, target_is_directory=True) + + mam_dir = sandbox / ".mam" + mam_dir.mkdir(exist_ok=True) + yaml_file = mam_dir / "agent-sessions.yaml" + if not yaml_file.exists(): + yaml_file.write_text("herdr_sessions: []\n") + + env = dict(os.environ) + env["WORKSPACE_ROOT"] = str(sandbox) + if extra_env: + env.update(extra_env) + + reconcile_script = os.path.join(repo_root, ".agents", "skills", "multi-agent-mux-monitor", "scripts", "reconcile.sh") + cmd = ["bash", reconcile_script] + if extra_args: + cmd.extend(extra_args) + + return subprocess.run(cmd, capture_output=True, text=True, cwd=str(sandbox), env=env) + + +def test_y1_y2_no_cache_dir_created(mam_sandbox): + cache_dir = mam_sandbox / ".cache" + if cache_dir.exists(): + shutil.rmtree(cache_dir) + + # Y-1: dry-run + res_dry = _run_reconcile(mam_sandbox, ["--once", "--emit-diff", "--dry-run"]) + assert res_dry.returncode == 0 + assert not (mam_sandbox / ".cache" / "multi-agent-mux-monitor").exists() + assert not (mam_sandbox / ".cache").exists() + + # Y-2: change pass + res_once = _run_reconcile(mam_sandbox, ["--once", "--emit-diff"]) + assert res_once.returncode == 0 + assert not (mam_sandbox / ".cache" / "multi-agent-mux-monitor").exists() + + +def test_y3_y6_y7_agent_sessions_state_dir_deprecation(mam_sandbox): + custom_dir = mam_sandbox / "custom_state_dir" + + # Y-6: Set env var -> notice on stderr, stdout is valid JSON, no dir created + res_env = _run_reconcile(mam_sandbox, ["--once", "--emit-diff", "--dry-run"], + extra_env={"AGENT_SESSIONS_STATE_DIR": str(custom_dir)}) + assert res_env.returncode == 0 + assert "Notice: AGENT_SESSIONS_STATE_DIR is set but has no effect" in res_env.stderr + assert not custom_dir.exists() + assert not (mam_sandbox / ".cache").exists() + + # Valid JSON on stdout + data = json.loads(res_env.stdout) + assert "drifts" in data or "drift" in data + + # Y-7: Unset env var -> no notice on stderr + res_silent = _run_reconcile(mam_sandbox, ["--once", "--emit-diff", "--dry-run"]) + assert res_silent.returncode == 0 + assert "AGENT_SESSIONS_STATE_DIR" not in res_silent.stderr + + +def test_y4_reconcile_runs_normally(mam_sandbox): + res = _run_reconcile(mam_sandbox, ["--once", "--emit-diff"]) + assert res.returncode == 0 + data = json.loads(res.stdout) + assert "timestamp" in data + assert "drifts" in data or "drift" in data + + +def test_y5_existing_cache_dir_unmolested(mam_sandbox): + target = mam_sandbox / ".cache" / "multi-agent-mux-monitor" + target.mkdir(parents=True, exist_ok=True) + leftover = target / "leftover.state" + leftover.write_text("FROM-AN-OLDER-INSTALL") + + res = _run_reconcile(mam_sandbox, ["--once", "--emit-diff"]) + assert res.returncode == 0 + assert leftover.exists() + assert leftover.read_text() == "FROM-AN-OLDER-INSTALL" + + +def test_y8_y9_remove_script_parent_cache_reclaim(mam_sandbox): + from pathlib import Path + repo_root = Path(__file__).resolve().parent.parent + remove_script = repo_root / "deploy" / "remove.sh" + assert remove_script.exists() + + # Y-8: Empty .cache is reclaimed + cache_dir = mam_sandbox / ".cache" / "multi-agent-mux-monitor" + cache_dir.mkdir(parents=True, exist_ok=True) + + # Y-10: Check deploy/remove.sh source for relative rmdir ".cache" + content = remove_script.read_text() + assert 'rmdir ".cache"' in content or 'rmdir .cache' in content + + # Y-9: Non-empty .cache is preserved (simulated rmdir logic) + other_tool = mam_sandbox / ".cache" / "other_tool" + other_tool.mkdir(parents=True, exist_ok=True) + (other_tool / "data.bin").write_text("keep me") + + shutil.rmtree(cache_dir) + with pytest.raises(OSError): + os.rmdir(mam_sandbox / ".cache") + assert (other_tool / "data.bin").exists()