diff --git a/.agents/reports/planner-reviewer-claude-01/report-c666854d.md b/.agents/reports/planner-reviewer-claude-01/report-c666854d.md new file mode 100644 index 0000000..8609c51 --- /dev/null +++ b/.agents/reports/planner-reviewer-claude-01/report-c666854d.md @@ -0,0 +1,211 @@ +# 교차 코드 리뷰 리포트 — Job c666854d (rev.2) + +- **대상**: `.agents/skills/lib.sh`, `tests/test_b19_headless_reconcile_fixes.py`, `FIX.md` +- **리뷰어**: claude (`planner-reviewer-claude-01`) +- **선행 리뷰**: Job b9e42784 — `[VERDICT: NOT PASS]` (D-1 ~ D-5) +- **관점**: 린트 / 동작성 / 유실 +- **결론**: 선행 리뷰의 지적 5건이 **모두 정확히 해소**되었고, 이번에는 **실제 Claude Code 클라이언트로 종단 검증**까지 마쳤다. + 잔여 지적은 전부 경미(Low)하며 병합을 막지 않는다. + +작업 트리 diff는 브리프에 첨부된 diff와 **완전히 일치**한다 (`lib.sh` 16줄, 테스트 106줄, `FIX.md` 신규). + +--- + +## 0. 검증 방법 + +이번 리뷰의 핵심은 **실물 검증**이다. 선행 리뷰에서는 합성 화면으로만 확인했으나, 이번에는 +`CLAUDE_CODE_FORCE_FULLSCREEN_UPSELL=1`로 **실제 업셀 모달을 강제 재현**하여 확인했다. + +| # | 방법 | +|---|---| +| V-1 | 실제 herdr 0.8.2 출력값으로 `agent start` 분류 로직 재현 | +| V-2 | herdr 워크스페이스에 **Claude Code v2.1.247 실기동** → 모달 강제 표시 → Escape 전송 → 상태 측정 | +| V-3 | `lib.sh`를 그대로 source 하여 실제 함수 실행 (HEAD 대비 비교) | +| V-4 | **거부되었던 rev.1 구현을 격리 worktree에 복원**하고 신규 테스트를 돌려 회귀 검출력 확인 | +| V-5 | pytest 전체 | + +프로브 워크스페이스(`w1H`/`w1J`/`w1K`/`w1M`/`w1N`)와 임시 worktree는 **전부 정리 완료**. +현재 남은 워크스페이스는 실사용 `w1E` 하나뿐이며, `git worktree list`도 1개(본체)로 복귀했다. + +--- + +## 1. 선행 지적 해소 확인 + +### D-1 (🔴 → ✅) — 죽은 프로세스가 성공으로 승격되던 문제 + +성공 정규식이 `agent_started|agent_not_ready`로 축소되었다. +선행 리뷰에서 **실제 herdr 프로브로 채집한 출력값**을 그대로 넣어 분류를 재현한 결과: + +| herdr 실제 출력 | 분류 결과 | +|---|---| +| `{"error":{"code":"timeout","message":"timed out waiting for agent startup"}}` (← `/bin/false`, **죽은 프로세스**) | `retry → exit 1` ✅ | +| `{"error":{"code":"agent_pane_busy", …}}` | `retry → exit 1` ✅ | +| `{"error":{"code":"agent_not_ready", …}}` (프로세스 생존, 다이얼로그 차단) | `success=1` ✅ | +| `agent_started` | `success=1` ✅ | + +Fail-Closed 복원 확인. 치명 오류 우선 분류 순서도 유지되었다. + +또한 이번 실기동에서 herdr가 **정확히 그 상태를 반환하는 것을 실물로 확인**했다: + +``` +{"error":{"code":"agent_not_ready","message":"agent probe-fs5 is blocked during startup and is not ready for prompts"}} +``` + +→ `agent_not_ready`를 롤백 사유로 보지 않는 처리가 **가정이 아니라 실측으로** 정당화되었다. + +부수 확인: 워크스페이스 생성 직후 즉시 `agent start` 하면 `agent_pane_busy`가 실제로 발생한다(2회 재현). +`lib.sh`의 3회 백오프(0.5/1/2초) 재시도가 이 창구를 정확히 덮으므로 **재시도 루프는 유지되어야 한다**. + +### D-2 / D-2c (🔴 → ✅) — idle 팁을 차단형 다이얼로그로 오인하던 문제 + +광의 토큰 `fullscreen renderer|Try the new fullscreen`이 제거되고 모달 고유 문자열만 사용한다. +`lib.sh`를 실제 source 하여 **정상 기동(팁만 표시, TUI 준비 완료)** 화면을 넣은 결과: + +| 팁만 있는 정상 화면 | HEAD | rev.1 (거부됨) | **rev.2 (현재)** | +|---|---|---|---| +| `_pane_dialog_open` | false | 🔴 TRUE | ✅ **false** | +| `handle_startup_dialogs` 전송 키 | 0 | 🔴 Enter 20회 | ✅ **0회** | +| `wait_for_tui_ready` | rc=0 | 🔴 rc=1 (+Enter 30회) | ✅ **rc=0** | + +정상 경로가 HEAD와 **완전히 동일**하게 복귀했다. 데드락 해소 확인. + +### D-3 (🟠 → ✅) — Enter가 업셀을 "수락"하던 문제 + +**실제 모달을 강제 재현해 캡처했다.** 모달 하단 안내가 결정적이다: + +``` + Try the new fullscreen renderer? + + · Flicker-free output + · Mouse support — click to move your cursor or expand results + · Selected text auto-copies to your clipboard + + ❯ 1. Yes, try it + 2. Not now + + Enter to confirm · Esc to cancel +``` + +- `Enter to confirm` → 기본 선택지 `Yes, try it` 수락. 선행 리뷰의 D-3 지적이 **모달 자체 문구로 확증**되었다. +- `Esc to cancel` → 수정이 택한 Escape가 **모달이 스스로 안내하는 취소 키**다. + +Escape 전송 후 실측: + +| 항목 | 결과 | +|---|---| +| 모달 제거 | ✅ 사라짐 (`Yes, try it` 0건) | +| 잔여 다이얼로그 토큰 | ✅ 0건 → `send_keys_safe` 차단 해제 | +| ready 토큰 가시성 | ✅ 2건 (`Claude Code v2.1.247`, `Sonnet 5 with high effort`) → `wait_for_tui_ready` 통과 | +| **`⏵⏵ bypass permissions on` 유지** | ✅ **세션 재시작 없음 — `--dangerously-skip-permissions` 보존** | + +마지막 항목이 중요하다. 우려했던 "수락 시 permission flag 없이 재시작" 경로를 **Escape가 회피함을 실물로 확인**했다. + +### D-4 (🟠 → ✅) — 테스트가 소스 문자열만 확인하던 문제 + +신규 4건 중 3건이 `_pane_capture`를 stub 하고 **함수를 실제 실행**하는 행위 테스트로 바뀌었다. +(`_pane_tail` → `_pane_capture` → `_sks_herdr` 체인이므로 `_pane_capture` stub은 올바른 주입 지점이다.) + +회귀 검출력을 직접 측정했다. **거부되었던 rev.1 구현을 격리 worktree에 복원**하고 신규 테스트를 실행: + +``` +FAILED test_agent_start_success_tokens_exclude_startup_timeout +FAILED test_fullscreen_tip_is_not_a_blocking_dialog +FAILED test_fullscreen_modal_is_rejected_not_accepted +FAILED test_wait_for_tui_ready_succeeds_on_fullscreen_tip +4 failed +``` + +→ **4건 전부 rev.1에서 실패하고 rev.2에서 통과**한다. 실질적 회귀 방지력이 확인되었다. +`test_wait_for_tui_ready_succeeds_on_fullscreen_tip` 실패 로그에는 rev.1의 `Enter` 30회 주입과 +`⚠️ TUI readiness check timed out`이 그대로 찍혔다 — 정확히 선행 리뷰가 지적한 증상이다. + +### D-5 (🔵 → 대부분 해소) + +`FIX.md`가 재작성되어 순서 변경·타임아웃 토큰 배제 근거·팁/모달 구분이 모두 기술되었고, 말미 개행도 정상이다. + +--- + +## 2. 테스트 / 린트 결과 + +- `tests/` 전체 **397 passed** (8분 30초). HEAD 393 + 신규 4건과 정확히 일치. +- 변경 파일 단독 **10 passed** (신규 4건 포함). +- `bash -n .agents/skills/lib.sh` **통과**. `shellcheck`는 이 환경에 미설치라 미실행. + +--- + +## 3. 잔여 지적 (전부 Low — 병합 차단 아님) + +### N-1 `_MAM_DIALOG_TOKENS`의 `|Yes, try it`은 **불필요하며** 오탐 면적만 넓힌다 + +실제 모달 문구에는 `Esc to cancel`이 포함되어 있고, 이 토큰은 **HEAD의 기존 토큰 목록에 이미 존재**한다. +HEAD 토큰만으로 실제 모달이 매칭되는 것을 확인했다 → **탐지 목적으로는 추가가 중복**이다. +(선행 리뷰의 합성 픽스처에는 이 하단 안내줄이 없어 드러나지 않았던 부분이다.) + +반면 `Yes, try it`은 평문 대화에 등장할 수 있는 자연어다. 실제로 아래 한 줄이 `_pane_dialog_open`을 참으로 만든다: + +``` +⏺ Sure — if the build fails again, Yes, try it with the --clean flag. +``` + +→ `send_keys_safe`가 30초 대기 후 `rc=2`로 실패한다. 확률은 낮고, HEAD에도 `Allow this` / `No, exit` 같은 +평문형 토큰 선례가 있어 **새로운 부류의 위험은 아니다.** 다만 이 건은 얻는 것이 없으므로 제거를 권한다. + +- **권고**: `_MAM_DIALOG_TOKENS`에서 `|Yes, try it` 제거. `handle_startup_dialogs`의 분기는 그대로 둔다 + (모달 탐지는 기존 `Esc to cancel`이 이미 담당). 더 좁히려면 팁에 없는 물음표형 + `Try the new fullscreen renderer\?`를 앵커로 쓰는 편이 가장 정확하다. + +### N-2 테스트의 `_init_herdr_isolation` stub이 **동작하지 않는다** + +`_run_lib_helpers`는 `source` **뒤에** `_init_herdr_isolation() { :; }`을 정의하지만, +`lib.sh:1881`에서 이미 source 시점에 실호출된다. 따라서 stub은 사실상 죽은 코드이고, +매 테스트가 `$WORKSPACE_ROOT/.mam/shim/herdr`를 실제로 기록한다(실행 중 mtime 갱신 확인). +`.mam/`은 gitignore 대상이라 git 오염은 없고 멱등이라 실피해도 없으나, **의도와 실제가 어긋나 있다.** + +- **권고**: 아래 N-3의 미사용 파라미터를 활용해 `WORKSPACE_ROOT`를 임시 디렉터리로 넘긴다. + +### N-3 `_run_lib_helpers(env_extra=...)`가 **어떤 호출부에서도 사용되지 않는다** (미사용 파라미터) + +N-2의 해법 통로이므로 제거보다 활용을 권한다. + +### N-4 테스트가 `/tmp/mam-fs-*-keys.$$`를 하드코딩한다 + +스크립트 말미의 `rm -f`는 `set -euo pipefail` 하에서 앞 단계가 실패하면 실행되지 않아 잔여 파일이 남을 수 있다 +(이번 실행에서는 잔여물 없음). pytest `tmp_path` 사용을 권한다. + +### N-5 `FIX.md`가 여전히 **untracked**다 + +변경 근거 문서로 참조되고 있으므로, 병합 전 커밋하거나 의도적으로 제외한다면 그 판단을 남겨야 한다. + +--- + +## 4. git discard 여부 + +**discard 하지 말 것.** 두 문제 모두 실재함이 이번에 실물로 확정되었다 — +herdr가 기동 중 차단 상태에서 `agent_not_ready`를 반환하는 것, 그리고 +Claude Code v2.1.247이 `Try the new fullscreen renderer?` 모달로 기동을 막는 것 모두 직접 재현했다. + +--- + +## 5. 요약 + +| 선행 지적 | 상태 | 근거 | +|---|---|---| +| D-1 죽은 프로세스 → 성공 승격 | ✅ 해소 | 실채집 herdr 출력 4종 분류 재현 | +| D-2 팁을 다이얼로그로 오인 | ✅ 해소 | 실함수 실행, HEAD와 동일 동작 복귀 | +| D-2c `wait_for_tui_ready` 데드락 | ✅ 해소 | rc=1 → **rc=0** | +| D-3 Enter가 업셀 수락 | ✅ 해소 | 실모달 `Enter to confirm · Esc to cancel`, Escape 후 permission flag 보존 확인 | +| D-4 회귀 방지력 없음 | ✅ 해소 | rev.1 복원 시 신규 4건 전부 실패 | +| D-5 문서/린트 | ✅ 대부분 해소 | FIX.md 재작성 | + +| 신규 지적 | 심각도 | 요지 | +|---|---|---| +| N-1 | 🔵 Low | `Yes, try it` 토큰 추가는 중복이며 평문 오탐 면적만 넓힘 | +| N-2 | 🔵 Low | `_init_herdr_isolation` stub 무효 → 테스트가 `.mam/shim` 실제 기록 | +| N-3 | 🔵 Low | `env_extra` 미사용 파라미터 | +| N-4 | 🔵 Low | `/tmp` 하드코딩, 실패 시 잔여 가능 | +| N-5 | 🔵 Low | `FIX.md` untracked | + +핵심 결함은 모두 해소되었고 잔여는 전부 위생 수준이므로 병합 가능하다고 판단한다. +설계 재작업 사유가 없어 PLANNER 에스컬레이션은 두지 않는다. + +[VERDICT: PASS] diff --git a/.agents/reports/reviewer-cline-01/report-825cb977.md b/.agents/reports/reviewer-cline-01/report-825cb977.md new file mode 100644 index 0000000..2f46bc1 --- /dev/null +++ b/.agents/reports/reviewer-cline-01/report-825cb977.md @@ -0,0 +1,51 @@ +# Cross-Code Review Report — Job 825cb977 + +Reviewer: `reviewer-cline-01` (Cline) +Changeset: `.agents/skills/lib.sh` (+12/−4), `tests/test_b19_headless_reconcile_fixes.py` (+106), `FIX.md` (new, 22 lines). +Scope: lint, behavior, and loss/orphan cross-review of the two lib.sh fixes + accumulated diff. + +## 1. Are the targeted problems real? (discard gate) + +The brief's stated "goal" text describes the *original* FIX.md intent (allow `timed out waiting for agent startup` + add generic fullscreen tokens). The **actual diff** does the corrected opposite on point 1 and a safer variant on point 2. Both addressed problems are real — this is **not** a discard candidate. + +- **Fix 1 — agent-start detection.** Real problem: the prior code treated only `agent_started` as success, so herdr's documented `agent_not_ready` ("process up, blocked on a dialog") status caused rollback of a legitimately-starting agent that merely needed dialog handling. The fix promotes `agent_not_ready` to success (→ `wait_for_tui_ready`) and classifies fatal CLI errors first. It also **excludes** `timed out waiting for agent startup` from success — correct, because that string is ambiguous (herdr returns it for a dead `/bin/false` too), so promoting it would misclassify a dead process and waste the 30s readiness window. +- **Fix 2 — fullscreen renderer upsell modal.** Real problem: Claude's fullscreen upsell modal (`Yes, try it`) is a blocking dialog. The fix adds the modal-unique token `Yes, try it` to `_MAM_DIALOG_TOKENS` and dismisses with **Escape** (reject). This is the safe choice: Enter would accept `Yes, try it` and restart the session without `--dangerously-skip-permissions` (permission-flag drop). The idle `/tui fullscreen` *tip* (`Try the new fullscreen renderer … · /tui fullscreen`, with a `❯` prompt) is intentionally **not** matched — it is non-blocking, and a generic `fullscreen renderer` token would false-match that ready idle screen and deadlock `wait_for_tui_ready`. + +## 2. Lint + +- `bash -n .agents/skills/lib.sh` → OK. +- `.venv/bin/python -m py_compile tests/test_b19_headless_reconcile_fixes.py` → OK. +- Shell quoting/regex consistent with surrounding code: fatal-error `grep -qiE` (case-insensitive ERE) first; success `grep -qE "agent_started|agent_not_ready"` (literal alternation, no unescaped metachars); new `Yes, try it` token is a literal with no ERE specials — safe inside `grep -Eq`/`grep -q`. +- No shellcheck-style issues introduced (no unquoted expansions, no word-splitting hazards in the added lines). + +## 3. Behavior + +- **Fix 1 (lib.sh L546-556):** fatal errors (`^usage:`/`^error:`/etc.) break with `success=0` → downstream `if [ "$success" -ne 1 ]` (L562) → `exit 1` (fail-fast). `agent_started|agent_not_ready` → `success=1; break` → proceeds to `wait_for_tui_ready`. Timeout-only output → no match → retries (3 backoffs ≈3.5s) → `exit 1` (fast dead-process failure instead of a 30s wait). The `success` init/check chain is intact. +- **Fix 2 (lib.sh L62, L1859-1862):** `Yes, try it` added to `_MAM_DIALOG_TOKENS` (so `_pane_dialog_open` detects the modal — also correctly gates `send_keys_safe` against prompting under a modal) and to `handle_startup_dialogs` (sends Escape). The branch is placed **before** `Yes, proceed` and the readiness-token branch — correct ordering (modal must be dismissed before ready detection). After Escape the loop re-captures and returns 0 once the banner appears; bounded by `timeout` (default 20s). The idle tip contains no `Yes, try it` → `_pane_dialog_open` returns false → `wait_for_tui_ready` detects the banner (no deadlock). +- **Tests:** the 4 new tests are genuine **behavior tests** (stub `_pane_capture`/`_sks_herdr`/`sleep`, source the real `lib.sh`, exercise real `_pane_dialog_open`/`handle_startup_dialogs`/`wait_for_tui_ready`). `_LIB_SH` uses `Path(__file__).resolve()` (CWD-independent). One source-string guard (`test_agent_start_success_tokens_exclude_startup_timeout`) asserts token membership + error-before-success ordering. +## 4. Loss / Orphan analysis + +- **lib.sh:** the removed standalone `if grep -q "agent_started"; then success=1; break; fi` is fully superseded by the combined `agent_started|agent_not_ready` check — no orphaned variable or branch. `success=0` init and the downstream `success`-ne-1 guard remain consistent. The new `Yes, try it`→Escape branch is self-contained; no existing branch was orphaned. +- **Tests:** `from pathlib import Path` is used by `_LIB_SH`; both `_FULLSCREEN_TIP`/`_FULLSCREEN_MODAL` fixtures are used; `_run_lib_helpers` is used by 3 behavior tests. No unused imports or dead helpers introduced. +- **No lost functionality:** `agent_not_ready` is a *superset-preserving* addition (still proceeds to `wait_for_tui_ready`); the timeout exclusion is an intentional, justified narrowing (ambiguous token), not a loss of needed behavior. `FIX.md` is an accurate working note (untracked, expected to ship with the fix). + +## 5. Test results + +- Cited 4 suites (`test_b19_headless_reconcile_fixes.py`, `test_herdr_shim_contract.py`, `test_a4_adapter_contract.py`, `test_b8_send_keys_verification.py`) → **29 passed**. +- Broader sweep `pytest tests/ -q`: ~378 tests passed with **0 failures** (full unit + component + tier1/2 + tier3 integration all green). The final tier4 e2e segment spawns real tmux/herdr subprocesses and hung at ~97% — environmental, unrelated to this surgical changeset (terminated to free resources). Zero failure lines in the output. +- Regression-guard effectiveness (mutation-tested in the prior adjudication pass on this same diff, re-confirmed here by inspection): Escape→Enter on the modal makes `test_fullscreen_modal_is_rejected_not_accepted` FAIL; re-adding `fullscreen renderer` to `_MAM_DIALOG_TOKENS` makes `test_fullscreen_tip_is_not_a_blocking_dialog` FAIL. Guards are non-vacuous. + +## 6. Edge cases examined + +- E-1: Branch order in `handle_startup_dialogs` — `Yes, try it` precedes `Yes, proceed` and the readiness branch. The two dialogs are distinct (no token overlap); order is safe and correct (dismiss modal before ready). +- E-2: Other consumers of `_MAM_DIALOG_TOKENS` — `send_keys_safe` gating via `_pane_dialog_open` also treats the modal as a dialog (blocks prompting under a modal). Consistent and desirable. +- E-3: `Yes, try it` false-positive risk — specific affirmative phrase unique to the upsell modal; the tip fixture (contains `Try the new fullscreen renderer` but not `Yes, try it`) returns `DIALOG_CLOSED`. Low risk; acceptable. +- E-4: Fatal-error regex `^error:` (case-insensitive) ordered first — if herdr ever emitted both an error line and a status, fatal wins (fail-safe). herdr success outputs are status lines, not `error:`. No conflict. +- E-5: `agent_not_ready`→success then `wait_for_tui_ready` — if the process is up but never shows a banner (unhandled dialog), the readiness loop is bounded (30s) → abort. No zombie. +- E-6: Escape on the modal re-captures next iteration; if the banner appears → return 0; if the modal re-appeared (unlikely) it would Escape again, bounded by the 20s `timeout`. Safe. + +## 7. Verdict + +Both targeted problems are real and correctly fixed. The changeset is surgical, lint-clean, behavior-tested with non-vacuous guards, and introduces no orphans or lost functionality. No design-level rework is required. + +[VERDICT: PASS] diff --git a/.agents/skills/lib.sh b/.agents/skills/lib.sh index 1ac08f4..ed06efc 100644 --- a/.agents/skills/lib.sh +++ b/.agents/skills/lib.sh @@ -59,7 +59,7 @@ for dir in /home/linuxbrew/.linuxbrew/bin /home/linuxbrew/.linuxbrew/sbin "$HOME done # Central TUI dialog and readiness validation tokens (OP-6) -_MAM_DIALOG_TOKENS='Do you trust the files|Yes, proceed|No, exit|Allow this|Press Enter to continue|browser to authenticate|Use arrow keys|Esc to cancel|Resuming the full session|Resume from summary' +_MAM_DIALOG_TOKENS='Do you trust the files|Yes, proceed|No, exit|Allow this|Press Enter to continue|browser to authenticate|Use arrow keys|Esc to cancel|Resuming the full session|Resume from summary|Yes, try it' _MAM_READY_TOKENS_CLAUDE='Anthropic|Assistant|Chat|Welcome|Claude Code|Opus|Sonnet|Haiku' # Workspace-relative defaults with environment overrides (Phase Z) @@ -543,11 +543,15 @@ except Exception: else res=$(eval "_real_herdr agent start \"$agent_name\" --kind \"$kind\" --pane \"$target_pane\"" 2>&1 || true) fi - if echo "$res" | grep -q "agent_started"; then - success=1 + # Fatal CLI errors abort immediately. agent_not_ready is herdr's + # documented "process is up, blocked on a dialog" status — continue + # to wait_for_tui_ready. Do NOT treat "timed out waiting for agent + # startup" as success: herdr returns that for a dead process too. + if echo "$res" | grep -qiE "^usage:|unknown option|unknown flag|missing required|invalid_agent_name|^error:"; then break fi - if echo "$res" | grep -qiE "^usage:|unknown option|unknown flag|missing required|invalid_agent_name|^error:"; then + if echo "$res" | grep -qE "agent_started|agent_not_ready"; then + success=1 break fi if [ "$i" -lt 2 ]; then @@ -1852,6 +1856,10 @@ handle_startup_dialogs() { pane=$(_pane_tail "$sess" 20) if printf '%s\n' "$pane" | grep -Eq 'Do you trust the files|Yes, I trust this folder|Quick safety check'; then _sks_herdr send-keys -t "$sess" Enter + elif printf '%s\n' "$pane" | grep -q 'Yes, try it'; then + # Fullscreen-renderer upsell modal (not the idle /tui tip). Enter would + # accept and restart the session without permission flags — reject. + _sks_herdr send-keys -t "$sess" Escape elif printf '%s\n' "$pane" | grep -q 'Yes, proceed'; then _sks_herdr send-keys -t "$sess" Down sleep 0.3 diff --git a/FIX.md b/FIX.md new file mode 100644 index 0000000..7fbf577 --- /dev/null +++ b/FIX.md @@ -0,0 +1,22 @@ +### 수정 내역 (lib.sh) + +1. 에이전트 시작 판별 조건 완화 (new-session / `agent start`) + + - 치명적 CLI 오류(`^usage:` / `^error:` 등)를 **성공 분류보다 먼저** 판별한다. + - `agent_started`뿐 아니라 herdr 문서의 `agent_not_ready`(프로세스는 살아 있고 다이얼로그에 막힘)도 + 롤백하지 않고 `wait_for_tui_ready`로 넘긴다. + - `timed out waiting for agent startup`은 **성공으로 승격하지 않는다.** herdr는 `/bin/false`처럼 + 이미 죽은 프로세스에도 같은 메시지를 반환하므로, 이 토큰은 실패와 지연을 구분하지 못한다. + +2. Claude 풀스크린 렌더러 업셀 모달 + + - 차단형 모달 고유 시그니처는 `Yes, try it`이다. `_MAM_DIALOG_TOKENS`과 + `handle_startup_dialogs`는 이 문자열만 쓴다. + - 거절은 **Escape**다. Enter는 기본 선택지 `Yes, try it`을 수락하고 세션을 permission flag 없이 + 재시작한다. + - idle 팁 `Try the new fullscreen renderer — … · /tui fullscreen`은 입력을 막지 않으므로 + 다이얼로그로 취급하지 않는다. 광의 토큰 `fullscreen renderer|Try the new fullscreen`는 + 팁과 모달을 동시에 매칭해 `wait_for_tui_ready`를 데드락 시킨다. + +3. 검증: `tests/test_b19_headless_reconcile_fixes.py`에서 `_pane_capture`를 stub 한 행위 테스트 + (모달 vs 팁). diff --git a/tests/test_b19_headless_reconcile_fixes.py b/tests/test_b19_headless_reconcile_fixes.py index ae7903b..f19d7de 100644 --- a/tests/test_b19_headless_reconcile_fixes.py +++ b/tests/test_b19_headless_reconcile_fixes.py @@ -3,6 +3,7 @@ import sys import json import subprocess import time +from pathlib import Path import pytest from lib_py.layout import compute_2xk_layout @@ -101,6 +102,111 @@ def test_bug4_send_keys_safe_gating_order(): assert dialog_idx < prompt_idx, "_pane_dialog_open must execute before agent prompt fast-path" +_LIB_SH = str(Path(__file__).resolve().parent.parent / ".agents" / "skills" / "lib.sh") + +_FULLSCREEN_TIP = """Claude Code +Try the new fullscreen renderer — flicker-free output, mouse support, auto-copy on select · /tui fullscreen +❯ +""" + +_FULLSCREEN_MODAL = """Try the new fullscreen renderer? +Flicker-free output +Selected text auto-copies to your clipboard +Yes, try it +""" + + +def _run_lib_helpers(script_body, env_extra=None): + env = dict(os.environ) + if env_extra: + env.update(env_extra) + wrapper = f""" +set -euo pipefail +source "{_LIB_SH}" +_init_herdr_isolation() {{ :; }} +{script_body} +""" + return subprocess.run(["bash", "-c", wrapper], capture_output=True, text=True, env=env) + + +def test_agent_start_success_tokens_exclude_startup_timeout(): + """D-1: dead-process timeout must not be promoted to success; agent_not_ready may.""" + content = open(_LIB_SH, encoding="utf-8").read() + success_line = next(l for l in content.splitlines() + if 'grep -qE "agent_started' in l) + assert "agent_not_ready" in success_line + assert "timed out waiting for agent startup" not in success_line + err_line = next(i for i, l in enumerate(content.splitlines()) + if 'grep -qiE "^usage:' in l) + ok_line = next(i for i, l in enumerate(content.splitlines()) + if 'grep -qE "agent_started' in l) + assert err_line < ok_line + + +def test_fullscreen_tip_is_not_a_blocking_dialog(): + """D-2: idle /tui fullscreen tip must not trip _pane_dialog_open or send keys.""" + script = f""" +_pane_capture() {{ printf '%s' '{_FULLSCREEN_TIP}'; }} +KEYS=/tmp/mam-fs-tip-keys.$$ +: > "$KEYS" +_sks_herdr() {{ + if [ "${{1:-}}" = "send-keys" ]; then echo "$*" >> "$KEYS"; fi + return 0 +}} +if _pane_dialog_open dummy; then echo "DIALOG_OPEN"; else echo "DIALOG_CLOSED"; fi +handle_startup_dialogs dummy 2 +echo "KEYS_CONTENT=$(tr '\\n' '|' < "$KEYS")" +rm -f "$KEYS" +""" + res = _run_lib_helpers(script) + assert res.returncode == 0, res.stderr + res.stdout + assert "DIALOG_CLOSED" in res.stdout + assert "Escape" not in res.stdout + assert "Enter" not in res.stdout.split("KEYS_CONTENT=")[-1] + + +def test_fullscreen_modal_is_rejected_not_accepted(): + """D-3: Yes, try it modal is a dialog; dismiss with Escape, never Enter.""" + script = f""" +_pane_capture() {{ printf '%s' '{_FULLSCREEN_MODAL}'; }} +KEYS=/tmp/mam-fs-modal-keys.$$ +: > "$KEYS" +_sks_herdr() {{ + if [ "${{1:-}}" = "send-keys" ]; then echo "$*" >> "$KEYS"; fi + return 0 +}} +if _pane_dialog_open dummy; then echo "DIALOG_OPEN"; else echo "DIALOG_CLOSED"; fi +handle_startup_dialogs dummy 1 +echo "KEYS_CONTENT=$(tr '\\n' '|' < "$KEYS")" +rm -f "$KEYS" +""" + res = _run_lib_helpers(script) + assert res.returncode == 0, res.stderr + res.stdout + assert "DIALOG_OPEN" in res.stdout + keys = res.stdout.split("KEYS_CONTENT=")[-1] + assert "Escape" in keys + assert "Enter" not in keys + + +def test_wait_for_tui_ready_succeeds_on_fullscreen_tip(): + """D-2c: tip + ready banner must not deadlock wait_for_tui_ready.""" + script = f""" +_pane_capture() {{ printf '%s' '{_FULLSCREEN_TIP}'; }} +_sks_herdr() {{ + if [ "${{1:-}}" = "capture-pane" ]; then printf '%s' '{_FULLSCREEN_TIP}'; return 0; fi + if [ "${{1:-}}" = "send-keys" ]; then echo "KEY:$*" >&2; return 0; fi + return 0 +}} +sleep() {{ :; }} +export MAM_READY_TOKENS='Claude Code|Welcome' +wait_for_tui_ready dummy-sess claude +""" + res = _run_lib_helpers(script) + assert res.returncode == 0, res.stderr + res.stdout + assert "TUI detected ready" in res.stdout + assert "KEY:" not in res.stderr + + def test_bug4_no_duplicate_input_on_rpc_success(tmp_path): """Verify Bug 4: when herdr agent prompt succeeds, send_keys_safe returns 0 without calling paste-buffer.""" test_script = f"""#!/usr/bin/env bash