iop/agent-task/cli_claude_usage_status/plan_cloud_G07_3.log

232 lines
14 KiB
Text

<!-- task=cli_claude_usage_status plan=3 tag=REVIEW_REVIEW_REVIEW_API -->
# Claude Usage Status Real-Smoke Recovery Plan
## 이 파일을 읽는 구현 에이전트에게
**구현 완료 후 `CODE_REVIEW-*-G??.md`의 모든 섹션을 채우는 것이 필수 최종 단계입니다. 이 파일을 채우기 전까지 작업은 완료가 아닙니다.** 아래 체크리스트와 중간/최종 검증을 실제로 실행하고, 구현 내용과 명령 출력을 `CODE_REVIEW-cloud-G07.md`에 기록하세요. `CODE_REVIEW-cloud-G07.md`의 `이 파일을 읽는 리뷰 에이전트에게` 섹션에 있는 아카이브 지시(`*.log` 변경, `complete.log` 작성)는 구현 에이전트가 수행하지 않습니다.
## 배경
3차 리뷰에서 단위 테스트와 기록된 smoke output은 성공처럼 보였지만, 같은 `./bin/edge.sh` + `./bin/node.sh` smoke를 재실행하면 실제로는 실패했다. edge output은 `[edge] sent command=status ... target=claude` 이후 `failed waiting for usage output: timeout waiting for complete Claude usage block`를 출력했고, `Current session`/`Current week` 검증 `rg`는 둘 다 매칭이 없었다.
이번 라운드는 regex를 한 번 더 추측하는 작업이 아니다. 실제 Claude TUI의 `/usage` 선택 흐름과 PTY raw/screen 형태를 먼저 확인하고, 그 결과가 테스트와 smoke 성공조건에 고정되도록 수정한다.
## 분석 결과
### 읽은 파일
- `agent-ops/skills/common/code-review/SKILL.md`
- `agent-task/cli_claude_usage_status/plan_local_G06_2.log`
- `agent-task/cli_claude_usage_status/code_review_cloud_G07_2.log`
- `apps/node/internal/adapters/cli/status/claude.go`
- `apps/node/internal/adapters/cli/status/parser.go`
- `apps/node/internal/adapters/cli/status/parser_test.go`
- `apps/node/internal/adapters/cli/status/claude_test.go`
- `apps/node/internal/adapters/cli/status/status.go`
- `apps/edge/cmd/edge/console.go`
- `apps/edge/cmd/edge/console_test.go`
- `configs/edge.yaml`
### 실패 재현 요약
리뷰 재실행 결과:
```text
[edge] sent command=status node=local-node adapter=cli target=claude session=default
error: node reported error: failed waiting for usage output: timeout waiting for complete Claude usage block:
Esc to cancel
```
`rg --sort path -n 'Current session: [0-9]+% remaining .*resets|Daily limit: [0-9]+% remaining .*resets' "$tmpdir/edge.out"`와 `rg --sort path -n 'Current week: [0-9]+% remaining .*resets|Weekly limit: [0-9]+% remaining .*resets' "$tmpdir/edge.out"`는 출력이 없었다.
### 범위 결정
- `proto/**`, transport schema, runtime command routing은 수정하지 않는다.
- `apps/edge/cmd/edge` 출력 포맷은 이미 metadata label을 받을 수 있으므로, smoke 실패가 edge formatting이 아니라 node-side checker/parser 문제인지 먼저 증명한다.
- `configs/edge.yaml`의 `target: claude` dirty 상태는 smoke 전제로만 사용한다. 새 변경이 필요하면 이유를 `CODE_REVIEW`에 기록한다.
- 실제 계정 사용량이나 reset 시각은 테스트 fixture에 고정하지 않는다. raw fixture는 민감값을 synthetic 값으로 치환한다.
### 빌드 등급
- build lane: `cloud-G07` - 반복 Required 이슈이고, 실제 Claude TUI/PTY 관측, smoke 검증 신뢰성, 테스트 설계 판단이 모두 필요하므로 로컬 구현 등급으로는 부적절하다.
- review lane: `cloud-G07` - 반복 Required 이슈이며 verification trust가 핵심이다.
## 의존 관계 및 구현 순서
1. `REVIEW_REVIEW_REVIEW_API-1`에서 실제 Claude TUI 흐름을 진단하고 checker/parser를 수정한다.
2. `REVIEW_REVIEW_REVIEW_API-2`에서 이번 실패를 재현하는 regression tests를 추가한다.
3. `REVIEW_REVIEW_REVIEW_API-3`에서 smoke 검증을 실패 감지형 명령으로 다시 기록한다.
### [REVIEW_REVIEW_REVIEW_API-1] 실제 Claude TUI `/usage` 흐름 및 raw screen 기준으로 checker/parser 수정
#### 문제
`apps/node/internal/adapters/cli/status/claude.go:108`의 `fullUsageWait`는 `ParseStatusOutput(fullOutput)`가 `DailyLimit`과 `WeeklyLimit`을 모두 채우기만 기다린다. 그런데 실제 smoke에서는 TUI에 `Esc to cancel` 상태가 남아 있고 usage block이 파싱되지 않는다. 이는 `/usage` command가 실제 결과 화면까지 선택되지 않았거나, 결과 화면이 cursor repaint 형태라 line-oriented `cleanANSI`/regex가 놓치는 상황일 수 있다.
`apps/node/internal/adapters/cli/status/parser.go:14`의 regex와 `cleanANSI`는 실제 raw stream을 보지 않고 추정으로 보강된 상태다. 같은 추정 반복을 막기 위해 먼저 raw/sanitized/screen output을 비교해야 한다.
#### 해결 방법
1. 로컬 진단으로 실제 실패 raw를 확보한다.
- 임시 진단 코드는 repository에 남기지 않는다.
- 필요하면 `ClaudeChecker` timeout error를 일시적으로 넓혀 raw/sanitized tail을 확인하되, 최종 코드에는 과도한 debug print나 임시 파일 write를 남기지 않는다.
- 진단 결과로 `/usage`가 command palette에서 멈춘 것인지, 결과 screen repaint를 parser가 놓친 것인지 구분한다.
2. `/usage` 선택 흐름을 실제 TUI에 맞춘다.
- `sendText("\x15/usage\r")` 이후 command palette가 남아 있으면 필요한 추가 Enter 또는 선택 키를 명시적으로 처리한다.
- 추가 키 전송은 sentinel 기반으로 제한한다. usage result를 이미 파싱한 상태에서 불필요하게 Enter를 더 보내지 않는다.
3. parser가 raw stream과 최종 visible screen 중 하나에서 안정적으로 값을 찾게 한다.
- 단순 `\r -> \n` 변환만으로 부족하면 최소 terminal screen normalizer를 추가한다.
- normalizer는 OSC/SGR 제거, clear screen/clear line, cursor position, cursor up/down/left/right, carriage return을 처리해 visible text를 구성한다.
- 기존 Codex parser 경로가 깨지지 않도록 `ParseStatusOutput`은 Claude parsing 실패 시 Codex parsing을 유지한다.
#### 수정 파일 및 체크리스트
- [ ] `apps/node/internal/adapters/cli/status/claude.go` - 실제 `/usage` 선택 흐름이 결과 화면까지 도달하는지 sentinel로 검증
- [ ] `apps/node/internal/adapters/cli/status/claude.go` - timeout error가 리뷰 가능한 sanitized context를 충분히 제공하되 임시 debug artifact는 남기지 않음
- [ ] `apps/node/internal/adapters/cli/status/parser.go` - 실제 raw 또는 visible screen에서 `Current session`과 `Current week`를 모두 파싱
- [ ] `apps/node/internal/adapters/cli/status/parser.go` - Codex `5h limit`/`Weekly limit` parsing 회귀 없음
- [ ] 코드 전체 - 임시 진단 print, hard-coded reset time, 실제 계정값 fixture 없음
#### 테스트 작성
`REVIEW_REVIEW_REVIEW_API-2`에서 이번 smoke failure를 재현하는 test를 추가한다.
#### 중간 검증
```bash
go test ./apps/node/internal/adapters/cli/status/... -run 'TestParseStatusOutput_Claude|TestParseStatusOutput_CodexLimits|TestClaudeChecker' -count=1 -v
```
기대 결과: Claude checker/parser tests와 Codex parser test가 모두 통과한다.
### [REVIEW_REVIEW_REVIEW_API-2] 실패 재현 가능한 regression fixture/test 추가
#### 문제
`apps/node/internal/adapters/cli/status/parser_test.go:82`와 `apps/node/internal/adapters/cli/status/claude_test.go:172`의 현재 tests는 synthetic line stream을 통과시키지만 실제 smoke 실패를 잡지 못했다. 테스트가 통과해도 `bin` smoke가 실패하는 상태가 반복되고 있다.
#### 해결 방법
- 실제 진단에서 얻은 raw/sanitized 특징을 synthetic fixture로 축약한다.
- fixture는 반드시 현재 실패 모드를 한 번 재현해야 한다. 즉 수정 전에는 parser/checker 중 하나가 실패하고, 수정 후에만 통과해야 한다.
- fake Claude script는 단순 `printf 'Current session...\n'`가 아니라 실제 TUI처럼 command palette 또는 repaint stream을 흉내 낸다.
- `TestClaudeCheckerNotUsageExits`처럼 err nil을 허용하는 약한 테스트는 이번 성공조건으로 인정하지 않는다.
#### 수정 파일 및 체크리스트
- [ ] `apps/node/internal/adapters/cli/status/parser_test.go` - 실제 screen repaint/control sequence를 반영한 parser fixture 추가
- [ ] `apps/node/internal/adapters/cli/status/claude_test.go` - `/usage` 선택 흐름 또는 command palette 후 결과 화면 전환을 흉내 내는 fake TUI test 추가
- [ ] `apps/node/internal/adapters/cli/status/claude_test.go` - checker test가 `DailyLimit`, `DailyResetTime`, `WeeklyLimit`, `WeeklyResetTime`, metadata label을 모두 assert
- [ ] `apps/node/internal/adapters/cli/status/parser_test.go` - Codex fixture가 기존 `%`와 reset parsing을 유지하는지 assert
#### 테스트 작성
아래 이름과 동일하거나 더 구체적인 이름으로 추가한다.
- `TestParseStatusOutput_ClaudeUsageFromRepaintedScreen`
- `TestClaudeCheckerSelectsUsageAndParsesRepaintedScreen`
#### 중간 검증
```bash
go test ./apps/node/internal/adapters/cli/status/... -run 'TestParseStatusOutput_ClaudeUsageFromRepaintedScreen|TestClaudeCheckerSelectsUsageAndParsesRepaintedScreen|TestParseStatusOutput_CodexLimits' -count=1 -v
```
기대 결과: 새 regression tests와 Codex parser test가 모두 통과한다.
### [REVIEW_REVIEW_REVIEW_API-3] smoke 검증 신뢰성 복구
#### 문제
`agent-task/cli_claude_usage_status/code_review_cloud_G07_2.log:123`의 최종 smoke 기록은 성공처럼 되어 있지만 같은 명령 재실행에서 실패했다. 이번 라운드는 "기록된 출력"이 아니라 "실패 감지형 명령의 exit status"로 성공조건을 세워야 한다.
#### 해결 방법
최종 검증 smoke는 아래 조건을 모두 만족해야 PASS다.
- `edge.out`에 `node reported error`, `timeout waiting`, `raw output did not include parsed limits`가 없어야 한다.
- `edge.out`에 `[edge] sent command=status ... target=claude`가 있어야 한다.
- `edge.out`에 `[node-...-status] target=claude`가 있어야 한다.
- `edge.out`에 `Current session: <number>% remaining (resets ...)` 또는 Codex fallback `Daily limit: <number>% remaining (resets ...)`가 있어야 한다.
- `edge.out`에 `Current week: <number>% remaining (resets ...)` 또는 Codex fallback `Weekly limit: <number>% remaining (resets ...)`가 있어야 한다.
- `CODE_REVIEW-cloud-G07.md`의 smoke section에는 `tmpdir=...`, `sed` 출력, 두 `rg` 출력이 실제 stdout/stderr 그대로 있어야 한다.
- `(truncated)`, `(no output)` 같은 재구성 marker를 실제 명령 출력처럼 쓰지 않는다. 출력이 비어야 하는 negative check는 명령과 exit 의미를 함께 적는다.
#### 수정 파일 및 체크리스트
- [ ] `agent-task/cli_claude_usage_status/CODE_REVIEW-cloud-G07.md` - exact smoke command와 실제 output 기록
- [ ] `agent-task/cli_claude_usage_status/CODE_REVIEW-cloud-G07.md` - negative error check와 positive session/week `rg` output 기록
- [ ] `agent-task/cli_claude_usage_status/CODE_REVIEW-cloud-G07.md` - Go test, gofmt, diff check output이 실제 현재 코드와 일치
- [ ] smoke 후 orphan edge/node process가 남지 않았는지 확인
#### 테스트 작성
별도 Go test는 작성하지 않는다.
#### 중간 검증
```bash
command -v claude
```
기대 결과: 실제 Claude binary path가 출력된다.
## 수정 파일 요약
| 파일 | 항목 |
|------|------|
| `apps/node/internal/adapters/cli/status/claude.go` | REVIEW_REVIEW_REVIEW_API-1 |
| `apps/node/internal/adapters/cli/status/parser.go` | REVIEW_REVIEW_REVIEW_API-1 |
| `apps/node/internal/adapters/cli/status/parser_test.go` | REVIEW_REVIEW_REVIEW_API-2 |
| `apps/node/internal/adapters/cli/status/claude_test.go` | REVIEW_REVIEW_REVIEW_API-2 |
| `agent-task/cli_claude_usage_status/CODE_REVIEW-cloud-G07.md` | REVIEW_REVIEW_REVIEW_API-3 |
## 최종 검증
```bash
go test ./apps/node/internal/adapters/cli/status/... -count=1
go test ./apps/edge/cmd/edge -count=1
gofmt -l apps/node/internal/adapters/cli/status/parser.go apps/node/internal/adapters/cli/status/claude.go apps/node/internal/adapters/cli/status/parser_test.go apps/node/internal/adapters/cli/status/claude_test.go
git diff --check
command -v claude
```
기대 결과: Go tests가 통과하고 `gofmt -l` 출력이 비어 있으며 `git diff --check`가 성공한다.
```bash
set -euo pipefail
tmpdir="$(mktemp -d /tmp/iop-claude-status.XXXXXX)"
echo "tmpdir=$tmpdir"
mkfifo "$tmpdir/edge.in"
./bin/edge.sh < "$tmpdir/edge.in" > "$tmpdir/edge.out" 2>&1 &
edge_pid=$!
cleanup() {
kill "${node_pid:-}" "$edge_pid" 2>/dev/null || true
exec 9>&- 2>/dev/null || true
}
trap cleanup EXIT
exec 9>"$tmpdir/edge.in"
sleep 2
./bin/node.sh > "$tmpdir/node.out" 2>&1 &
node_pid=$!
sleep 5
printf '/status\n/exit\n' >&9
timeout 120 bash -c 'while kill -0 "$0" 2>/dev/null; do sleep 1; done' "$edge_pid"
kill "$node_pid" "$edge_pid" 2>/dev/null || true
exec 9>&-
trap - EXIT
sed -n '1,220p' "$tmpdir/edge.out"
sed -n '1,220p' "$tmpdir/node.out"
! rg --sort path -n 'node reported error|timeout waiting|raw output did not include parsed limits' "$tmpdir/edge.out"
rg --sort path -n '\\[edge\\] sent command=status .*target=claude' "$tmpdir/edge.out"
rg --sort path -n '\\[node-.*-status\\] target=claude' "$tmpdir/edge.out"
rg --sort path -n 'Current session: [0-9]+(\\.[0-9]+)?% remaining \\(resets .+\\)|Daily limit: [0-9]+(\\.[0-9]+)?% remaining \\(resets .+\\)' "$tmpdir/edge.out"
rg --sort path -n 'Current week: [0-9]+(\\.[0-9]+)?% remaining \\(resets .+\\)|Weekly limit: [0-9]+(\\.[0-9]+)?% remaining \\(resets .+\\)' "$tmpdir/edge.out"
```
기대 결과: negative error check는 매칭 없이 성공하고, 나머지 네 `rg`는 각각 실제 matching line을 출력한다. 이 smoke command가 non-zero로 끝나면 `REVIEW_REVIEW_REVIEW_API-3`는 미완료다.
모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 전체 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.