iop/agent-task/cli_claude_usage_status/plan_local_G06_2.log

227 lines
13 KiB
Text

<!-- task=cli_claude_usage_status plan=2 tag=REVIEW_REVIEW_API -->
# Claude Usage Status Parser Follow-up Plan
## 이 파일을 읽는 구현 에이전트에게
**구현 완료 후 `CODE_REVIEW-*-G??.md`의 모든 섹션을 채우는 것이 필수 최종 단계입니다. 이 파일을 채우기 전까지 작업은 완료가 아닙니다.** 아래 체크리스트와 중간/최종 검증을 실제로 실행하고, 구현 내용과 명령 출력을 `CODE_REVIEW-cloud-G07.md`에 기록하세요. `CODE_REVIEW-cloud-G07.md`의 `이 파일을 읽는 리뷰 에이전트에게` 섹션에 있는 아카이브 지시(`*.log` 변경, `complete.log` 작성)는 구현 에이전트가 수행하지 않습니다.
## 배경
2차 구현은 `ClaudeChecker`가 `DailyLimit`과 `WeeklyLimit`을 모두 기다리도록 고쳤지만, 실제 `./bin/edge.sh` + `./bin/node.sh` smoke에서 Claude `/usage` 화면을 열고도 parser가 complete usage block을 못 찾아 timeout 됐다. 실패 output tail에는 `Last 24h`, `Extra usage`, `Esc to cancel` 등 usage 화면 텍스트가 보여 TUI 조작은 성공했고, 문제는 실제 PTY raw stream의 carriage return/cursor-control 기반 화면을 line-oriented parser가 안정적으로 정규화하지 못하는 데 있다. 이번 작업은 실제 TUI stream 형태를 반영해 sanitizer/parser를 보강하고, smoke 검증 출력 신뢰를 회복한다.
## 분석 결과
### 읽은 파일
- `agent-ops/skills/common/code-review/SKILL.md`
- `agent-task/cli_claude_usage_status/plan_local_G06_1.log`
- `agent-task/cli_claude_usage_status/code_review_cloud_G07_1.log`
- `apps/node/internal/adapters/cli/status/claude.go`
- `apps/node/internal/adapters/cli/status/claude_test.go`
- `apps/node/internal/adapters/cli/status/parser.go`
- `apps/node/internal/adapters/cli/status/parser_test.go`
- `apps/node/internal/adapters/cli/status/status.go`
- `apps/node/internal/adapters/cli/status/status_test.go`
- `apps/edge/cmd/edge/console.go`
- `apps/edge/cmd/edge/console_test.go`
- `bin/edge.sh`
- `bin/node.sh`
- `configs/edge.yaml`
### 테스트 커버리지 공백
- 실제 PTY output normalization: 현재 tests는 `\n` sample과 `\x1b[1C` cursor-forward만 다루며, smoke 실패에 보인 `\r`, cursor movement, broken spacing을 포함하지 않는다.
- Parser/checker integration: delayed week fake test는 complete wait를 검증하지만, 실제 Claude usage screen raw stream과 비슷한 control sequence fixture를 검증하지 않는다.
- Verification trust: 이전 review file에는 smoke 성공 출력이 있었지만, 같은 환경에서 정확한 plan command를 재실행하면 실패했다. 최종 검증에는 `sed` output과 두 `rg` output을 실제 stdout/stderr 그대로 기록해야 한다.
### 심볼 참조
- renamed/removed symbol 없음.
- 참조 확인 명령: `rg --sort path -n "cleanANSI|ParseStatusOutput|claudeSessionRegex|claudeWeekRegex|remainingPercent|ClaudeChecker|TestParseStatusOutput_Claude|TestClaudeChecker" apps/node/internal/adapters/cli/status apps/edge/cmd/edge`
- `ParseStatusOutput`는 Codex checker도 사용하므로 Codex parser regression을 함께 실행해야 한다.
### 범위 결정 근거
- `proto/**`, `runtime/types.go`, node command transport는 수정하지 않는다.
- `apps/edge/cmd/edge` 출력 포맷은 이미 양쪽 label을 표시할 수 있으므로 이번 핵심 범위가 아니다. 단, smoke 검증 때문에 edge console tests는 함께 실행한다.
- `configs/edge.yaml`은 새로 수정하지 않는다. 현재 `target: claude` dirty 상태는 smoke 전제로만 사용한다.
- 외부 Claude TUI가 계속 변할 수 있으므로 fixture는 실제 smoke failure의 특징(`\r`, cursor-control, broken spacing)을 반영하되 인증/계정 값에 의존하지 않는 synthetic raw string으로 둔다.
### 빌드 등급
- build lane: `local-G06` — parser/checker 보강과 tests 중심이며 실패가 현재 환경에서 재현된다.
- review lane: `cloud-G07` — 실제 TUI smoke 신뢰가 핵심이라 동일 review 등급을 유지한다.
## 의존 관계 및 구현 순서
1. `REVIEW_REVIEW_API-1`에서 sanitizer/parser를 실제 PTY stream에 맞게 보강한다.
2. `REVIEW_REVIEW_API-2`에서 realistic raw fixture tests를 추가한다.
3. `REVIEW_REVIEW_API-3`에서 정확한 smoke 검증을 재실행하고 출력 신뢰를 회복한다.
### [REVIEW_REVIEW_API-1] Claude usage sanitizer/parser를 실제 PTY raw stream에 맞게 보강
#### 문제
`apps/node/internal/adapters/cli/status/parser.go:20-21`의 Claude regex는 `% used`와 `Resets`가 `\n` 기반 line으로 정리된다고 가정한다. 하지만 실제 smoke 실패 output에는 carriage return과 cursor-control이 섞인 화면 업데이트가 남아 있었고, `ClaudeChecker`는 `/usage` 화면에 진입했지만 complete block을 찾지 못해 timeout 됐다. `cleanANSI`도 `apps/node/internal/adapters/cli/status/parser.go:116-128`에서 `\r`와 비-출력 control character를 line-oriented text로 정규화하지 않는다.
Before (`apps/node/internal/adapters/cli/status/parser.go:20`):
```go
claudeSessionRegex = regexp.MustCompile(`(?s)(?:Current\s+session).*?(?:(\d+)%\s*used|(\d+(?:\.\d+)?)% used)\s*\n\s*Resets\s+([^\n]+)`)
claudeWeekRegex = regexp.MustCompile(`(?s)(?:Current\s+week(?:\s+\(all models\))?).*?(?:(\d+)%\s*used|(\d+(?:\.\d+)?)% used)\s*\n\s*Resets\s+([^\n]+)`)
```
Before (`apps/node/internal/adapters/cli/status/parser.go:116`):
```go
func cleanANSI(str string) string {
// Replace cursor-forward escapes with spaces to prevent text overlap
str = regexp.MustCompile(`\x1b\[\d+C`).ReplaceAllString(str, " ")
```
#### 해결 방법
`cleanANSI` 또는 별도 `normalizeTerminalText`에서 아래를 처리한다.
- OSC 제거를 CSI 제거보다 먼저 수행한다.
- `\r\n`, 단독 `\r`을 `\n`으로 정규화한다.
- cursor-forward는 가능하면 count만큼 공백으로 변환한다.
- 남은 C0 control character는 `\n`, `\t`를 제외하고 공백으로 치환한다.
- 반복 공백은 regex가 처리할 수 있게 유지하되, broken spacing이 있어도 label/used/reset을 찾도록 Claude regex를 완화한다.
Claude regex는 `\n`에만 의존하지 말고 `\s+Resets\s+`를 허용한다. decimal percentage가 들어와도 `remainingPercent`가 `%` suffix를 잃지 않게 `strconv.ParseFloat` 기반으로 처리한다.
After:
```go
claudeSessionRegex = regexp.MustCompile(`(?is)Current\s+session.*?(\d+(?:\.\d+)?)%\s+used\s+Resets\s+([^\n]+)`)
claudeWeekRegex = regexp.MustCompile(`(?is)Current\s+week(?:\s+\(all\s+models\))?.*?(\d+(?:\.\d+)?)%\s+used\s+Resets\s+([^\n]+)`)
```
#### 수정 파일 및 체크리스트
- [ ] `apps/node/internal/adapters/cli/status/parser.go` - `cleanANSI`가 carriage return과 remaining controls를 line-oriented text로 정규화
- [ ] `apps/node/internal/adapters/cli/status/parser.go` - cursor-forward count를 가능한 만큼 공백으로 반영
- [ ] `apps/node/internal/adapters/cli/status/parser.go` - Claude regex가 `\n` adjacency에만 의존하지 않도록 완화
- [ ] `apps/node/internal/adapters/cli/status/parser.go` - `remainingPercent`를 decimal-safe하게 수정하고 `%` suffix 유지
- [ ] `apps/node/internal/adapters/cli/status/claude.go` - timeout tail slicing이 6줄 이하일 때도 output을 비우지 않도록 `start := 0; if len(lines) > 6 { start = len(lines)-6 }` 형태로 수정
#### 테스트 작성
`REVIEW_REVIEW_API-2`에서 parser/checker regression tests를 추가한다.
#### 중간 검증
```bash
go test ./apps/node/internal/adapters/cli/status/... -run 'TestParseStatusOutput_Claude|TestParseStatusOutput_CodexLimits' -count=1
```
기대 결과: Claude realistic parser tests와 Codex parser test가 모두 통과한다.
### [REVIEW_REVIEW_API-2] 실제 TUI stream 형태를 반영한 parser/checker regression tests 추가
#### 문제
`apps/node/internal/adapters/cli/status/parser_test.go:59`의 ANSI test는 cursor-forward 1종만 포함한다. 실제 실패는 `/usage` 화면이 열렸지만 carriage return과 cursor updates 때문에 parser가 line-oriented block을 못 찾는 형태였고, 이 회귀를 잡는 fixture가 없다.
#### 해결 방법
다음 tests를 추가하거나 기존 test를 확장한다.
- `TestParseStatusOutput_ClaudeUsageWithCarriageReturns`: `Current session\r...2% used\rResets ...\rCurrent week ...0% used\rResets ...` 형태를 파싱한다.
- `TestParseStatusOutput_ClaudeUsageWithTerminalControls`: cursor-forward/back, line clear, CR, broken spacing을 섞은 synthetic raw stream에서 양쪽 limit/reset을 파싱한다.
- `TestClaudeCheckerParsesCarriageReturnUsageScreen`: fake Claude script가 `\r` 중심으로 usage screen을 출력해 checker까지 통과하는지 검증한다.
#### 수정 파일 및 체크리스트
- [ ] `apps/node/internal/adapters/cli/status/parser_test.go` - carriage return 기반 Claude parser test 추가
- [ ] `apps/node/internal/adapters/cli/status/parser_test.go` - terminal control 기반 Claude parser test 추가
- [ ] `apps/node/internal/adapters/cli/status/claude_test.go` - fake TUI가 `\r`/cursor-control usage screen을 출력하는 checker test 추가
- [ ] 기존 `TestParseStatusOutput_CodexLimits`, `TestClaudeCheckerWaitsForDelayedUsageWeek` 유지
#### 테스트 작성
위 테스트들을 반드시 작성한다. fixture에는 계정명, 실제 reset 시각 같은 환경값을 넣지 말고 synthetic 값으로 고정한다.
#### 중간 검증
```bash
go test ./apps/node/internal/adapters/cli/status/... -run 'TestParseStatusOutput_ClaudeUsageWithCarriageReturns|TestParseStatusOutput_ClaudeUsageWithTerminalControls|TestClaudeCheckerParsesCarriageReturnUsageScreen|TestClaudeCheckerWaitsForDelayedUsageWeek' -count=1
```
기대 결과: 추가 regression tests와 기존 delayed-week test가 통과한다.
### [REVIEW_REVIEW_API-3] 실제 bin smoke를 정확한 명령과 출력으로 재검증
#### 문제
`agent-task/cli_claude_usage_status/code_review_cloud_G07_1.log:119-207`에는 smoke 성공 출력이 기록됐지만, 리뷰에서 계획의 정확한 smoke command를 재실행한 결과 node가 timeout error를 반환했고 두 `rg` 명령 모두 실패했다. verification trust를 회복하려면 실제 성공 출력과 `rg` 결과를 그대로 기록해야 한다.
#### 해결 방법
최종 검증의 smoke command를 그대로 실행한다. `mktemp` template는 plan의 `XXXXXX`를 사용하고, `sed -n` 출력에 `(truncated)` 같은 재구성 marker를 넣지 않는다. `rg`는 session line과 week line을 별도 명령으로 확인한다.
#### 수정 파일 및 체크리스트
- [ ] `agent-task/cli_claude_usage_status/CODE_REVIEW-cloud-G07.md` - exact smoke command 출력 기록
- [ ] `agent-task/cli_claude_usage_status/CODE_REVIEW-cloud-G07.md` - `rg session`/`rg week` 실제 출력 기록
- [ ] smoke 후 orphan node/edge process가 남지 않았는지 확인하고 필요하면 종료
#### 테스트 작성
별도 Go test는 작성하지 않는다.
#### 중간 검증
```bash
command -v claude
```
기대 결과: Claude binary path가 출력된다.
## 수정 파일 요약
| 파일 | 항목 |
|------|------|
| `apps/node/internal/adapters/cli/status/parser.go` | REVIEW_REVIEW_API-1 |
| `apps/node/internal/adapters/cli/status/claude.go` | REVIEW_REVIEW_API-1 |
| `apps/node/internal/adapters/cli/status/parser_test.go` | REVIEW_REVIEW_API-2 |
| `apps/node/internal/adapters/cli/status/claude_test.go` | REVIEW_REVIEW_API-2 |
| `agent-task/cli_claude_usage_status/CODE_REVIEW-cloud-G07.md` | 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
tmpdir="$(mktemp -d /tmp/iop-claude-status.XXXXXX)"
mkfifo "$tmpdir/edge.in"
./bin/edge.sh < "$tmpdir/edge.in" > "$tmpdir/edge.out" 2>&1 &
edge_pid=$!
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" || true
kill "$node_pid" "$edge_pid" 2>/dev/null || true
exec 9>&-
sed -n '1,220p' "$tmpdir/edge.out"
sed -n '1,220p' "$tmpdir/node.out"
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"
```
기대 결과: `edge.out`에 `[edge] sent command=status ... target=claude`, `[node-...-status] target=claude`, `Current session` remaining/reset line, `Current week` remaining/reset line이 모두 출력된다.
모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 전체 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.