iop/agent-task/cli_claude_usage_status/plan_local_G06_1.log

251 lines
12 KiB
Text

<!-- task=cli_claude_usage_status plan=1 tag=REVIEW_API -->
# Claude Usage Status Review Follow-up Plan
## 이 파일을 읽는 구현 에이전트에게
**구현 완료 후 `CODE_REVIEW-*-G??.md`의 모든 섹션을 채우는 것이 필수 최종 단계입니다. 이 파일을 채우기 전까지 작업은 완료가 아닙니다.** 아래 체크리스트와 중간/최종 검증을 실제로 실행하고, 구현 내용과 명령 출력을 `CODE_REVIEW-cloud-G07.md`에 기록하세요. `CODE_REVIEW-cloud-G07.md`의 `이 파일을 읽는 리뷰 에이전트에게` 섹션에 있는 아카이브 지시(`*.log` 변경, `complete.log` 작성)는 구현 에이전트가 수행하지 않습니다.
## 배경
1차 구현은 Claude `/usage` 파서와 PTY checker를 추가했지만, 리뷰 중 실제 `./bin/edge.sh` + `./bin/node.sh` smoke에서 `/status` 출력이 `Current week`만 표시되고 `Current session`이 누락됐다. 원인은 `ClaudeChecker`가 session/week 둘 중 하나만 나오면 usage block이 준비됐다고 판단하는 조기 반환 조건이다. 이번 후속 작업은 checker가 두 usage section을 모두 기다리도록 고정하고, 지연 출력 fake TUI와 실제 bin smoke 검증으로 회귀를 막는다.
## 분석 결과
### 읽은 파일
- `agent-ops/skills/common/code-review/SKILL.md`
- `agent-task/cli_claude_usage_status/plan_local_G06_0.log`
- `agent-task/cli_claude_usage_status/code_review_cloud_G07_0.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/node/internal/adapters/cli/cli.go`
- `apps/node/internal/node/node.go`
- `apps/edge/cmd/edge/console.go`
- `apps/edge/cmd/edge/console_test.go`
- `bin/edge.sh`
- `bin/node.sh`
- `configs/edge.yaml`
### 테스트 커버리지 공백
- `ClaudeChecker` complete-wait behavior: 기존 `TestClaudeCheckerRequestsUsage`는 session/week를 즉시 연속 출력하므로 조기 반환을 잡지 못한다.
- 실제 bin smoke: 1차 `CODE_REVIEW`는 SKIP을 기록했지만, 리뷰 환경에서 `command -v claude`와 bin smoke가 실행 가능했고 실제로 `Current session` 누락을 발견했다.
- Edge formatting 자체는 단위 테스트가 있으나, node checker가 `DailyLimit`을 누락할 때 `/status` 결과가 불완전해지는 end-to-end 검증이 필요하다.
### 심볼 참조
- renamed/removed symbol 없음.
- 참조 확인 명령: `rg --sort path -n "ClaudeChecker|NewClaudeChecker|ParseStatusOutput|TestClaudeChecker|formatUsageStatus" apps/node/internal/adapters/cli apps/edge/cmd/edge`
- `ClaudeChecker.Check`는 `status.CheckUsage` → `CLI.HandleCommand` → node command → edge `/status`로 연결된다.
### 범위 결정 근거
- `proto/**`, `runtime/types.go`, node command transport는 수정하지 않는다. metadata와 command response 경로는 이미 동작한다.
- `configs/edge.yaml`은 이번 후속에서 수정하지 않는다. 현재 dirty diff는 `target: claude`이며 smoke 전제로만 사용한다.
- Codex checker와 Gemini checker는 수정하지 않는다. 다만 status package tests는 함께 실행해 Codex 회귀를 확인한다.
- parser 자체는 현재 tests를 통과하므로 우선 `ClaudeChecker`의 wait predicate를 고친다. 실제 bin smoke 후 parser가 여전히 `Current session`을 못 읽으면 raw output 근거와 함께 parser 보강을 `계획 대비 변경 사항`에 기록한다.
### 빌드 등급
- build lane: `local-G06` — 변경은 Claude checker wait 조건과 tests에 국한되며 실패가 재현 가능하다.
- review lane: `cloud-G07` — 실제 TUI/bin smoke 검증 신뢰가 핵심이므로 동일 review 등급을 유지한다.
## 의존 관계 및 구현 순서
1. `REVIEW_API-1`에서 checker가 both section parsed 상태를 기다리도록 고친다.
2. `REVIEW_API-2`에서 지연 출력 fake TUI regression test를 추가한다.
3. `REVIEW_API-3`에서 실제 bin smoke를 stricter assertion으로 실행해 review file에 기록한다.
### [REVIEW_API-1] ClaudeChecker가 session/week 모두 파싱될 때까지 기다리게 수정
#### 문제
`apps/node/internal/adapters/cli/status/claude.go:112-115`는 주석상 “both Current session and Current week”를 기다린다고 되어 있지만 regex가 alternation이라 둘 중 하나만 나오면 통과한다. 리뷰 중 실제 smoke에서 edge output은 `Current week: 100% remaining (resets May 16, 6pm (Asia/Seoul))`만 출력했고 `Current session`이 누락됐다.
Before (`apps/node/internal/adapters/cli/status/claude.go:112`):
```go
// 3. Wait for usage block output (both Current session and Current week)
usageRegex := regexp.MustCompile(`(?s)(?:Current session).*?Resets|Current week.*?Resets`)
if err := waitFor(usageRegex, 20*time.Second); err != nil {
return nil, fmt.Errorf("failed waiting for usage output: %w", err)
}
```
#### 해결 방법
`waitFor` regex 대신 predicate 기반 대기 helper를 추가한다. 매 chunk마다 `ParseStatusOutput(fullOutput)`을 호출하고 `DailyLimit`과 `WeeklyLimit`이 모두 채워진 경우에만 성공한다. timeout error에는 clean output의 마지막 일부를 포함해 실제 TUI 변경 시 parser 보강 근거를 남긴다.
After:
```go
if err := waitForUsageStatus(20 * time.Second); err != nil {
return nil, fmt.Errorf("failed waiting for usage output: %w", err)
}
```
```go
waitForUsageStatus := func(timeout time.Duration) error {
timeoutCh := time.After(timeout)
for {
if st, _ := ParseStatusOutput(fullOutput); st != nil && st.DailyLimit != "" && st.WeeklyLimit != "" {
return nil
}
select {
case <-ctx.Done():
return ctx.Err()
case <-timeoutCh:
return fmt.Errorf("timeout waiting for complete Claude usage block: %s", usageOutputTail(cleanANSI(fullOutput)))
case chunk, ok := <-chunks:
if !ok {
return io.EOF
}
fullOutput += chunk
}
}
}
```
#### 수정 파일 및 체크리스트
- [ ] `apps/node/internal/adapters/cli/status/claude.go` - regex-only `usageRegex` wait 제거
- [ ] `apps/node/internal/adapters/cli/status/claude.go` - parsed status predicate로 `DailyLimit`과 `WeeklyLimit` 모두 대기
- [ ] `apps/node/internal/adapters/cli/status/claude.go` - timeout error에 clean output tail 포함
- [ ] `apps/node/internal/adapters/cli/status/claude.go` - 불필요해진 `regexp` import가 있으면 제거하지 말고 startup regex 사용 여부를 확인해 gofmt/go test로 정리
#### 테스트 작성
`REVIEW_API-2`에서 regression test를 추가하므로 이 항목 자체에는 별도 테스트 파일을 만들지 않는다.
#### 중간 검증
```bash
go test ./apps/node/internal/adapters/cli/status/... -run 'TestClaudeChecker' -count=1
```
기대 결과: Claude checker 관련 tests가 통과하고 조기 반환 regression이 재현되지 않는다.
### [REVIEW_API-2] 지연 출력 fake TUI로 조기 반환 회귀 테스트 추가
#### 문제
`apps/node/internal/adapters/cli/status/claude_test.go:31-36`의 fake TUI는 session/week block을 즉시 연속 출력한다. 현재처럼 checker가 session만 보고 성공 처리해도 500ms flush 안에 week가 들어와 test가 통과할 수 있어 실제 smoke 누락을 잡지 못한다.
Before (`apps/node/internal/adapters/cli/status/claude_test.go:31`):
```sh
# Print Claude /usage output
printf 'Current session: [████████░░░░░░░░░░░░] 2%% used\n'
printf 'Resets 10am (Asia/Seoul)\n'
printf '\n'
printf 'Current week (all models): [███████████████░░░░░] 50%% used\n'
printf 'Resets May 16, 6pm (Asia/Seoul)\n'
```
#### 해결 방법
새 테스트 `TestClaudeCheckerWaitsForDelayedUsageWeek`를 추가한다. fake TUI는 session block을 출력한 뒤 900ms 이상 sleep하고 week block을 출력한다. 현재 구현의 500ms flush 방식이면 `WeeklyLimit`이 비어 실패해야 하며, `REVIEW_API-1` 수정 후 통과해야 한다.
After:
```go
func TestClaudeCheckerWaitsForDelayedUsageWeek(t *testing.T) {
// fake Claude prints Current session, sleeps longer than the old flush,
// then prints Current week.
}
```
#### 수정 파일 및 체크리스트
- [ ] `apps/node/internal/adapters/cli/status/claude_test.go` - `TestClaudeCheckerWaitsForDelayedUsageWeek` 추가
- [ ] `apps/node/internal/adapters/cli/status/claude_test.go` - 새 test가 `DailyLimit`, `DailyResetTime`, `WeeklyLimit`, `WeeklyResetTime`을 모두 assertion
- [ ] `apps/node/internal/adapters/cli/status/claude_test.go` - 기존 `TestClaudeCheckerRequestsUsage`는 유지하되 중복 fixture가 과하면 작은 helper로만 정리
#### 테스트 작성
위 테스트를 반드시 작성한다. timeout은 기존 5초 내로 충분해야 하며 fake script sleep은 900ms 이상으로 둔다.
#### 중간 검증
```bash
go test ./apps/node/internal/adapters/cli/status/... -run 'TestClaudeCheckerWaitsForDelayedUsageWeek|TestClaudeCheckerRequestsUsage' -count=1
```
기대 결과: 두 테스트가 모두 통과한다.
### [REVIEW_API-3] 실제 bin smoke를 실행하고 Current session/week 양쪽을 검증
#### 문제
`agent-task/cli_claude_usage_status/code_review_cloud_G07_0.log:206-208`은 bin smoke를 SKIP으로 기록했다. 하지만 `command -v claude`가 성공했고, 리뷰 중 같은 smoke를 실행해 실제 `/status` 결과를 얻었다. 계획의 final verification contract와 사용자 요청은 `bin` shell 기반 `/status` 테스트까지 포함한다.
#### 해결 방법
후속 구현 완료 후 실제 bin smoke를 실행하고 `CODE_REVIEW-cloud-G07.md`에 stdout/stderr를 붙인다. `rg` 검증은 기존처럼 session/week 중 하나만 있어도 통과하는 pattern이 아니라, 두 개 명령으로 `Current session`과 `Current week`를 각각 요구한다.
#### 수정 파일 및 체크리스트
- [ ] `agent-task/cli_claude_usage_status/CODE_REVIEW-cloud-G07.md` - bin smoke 실제 출력 기록
- [ ] `agent-task/cli_claude_usage_status/CODE_REVIEW-cloud-G07.md` - `command -v claude` 실패 시에만 SKIP 사유 기록
- [ ] `agent-task/cli_claude_usage_status/CODE_REVIEW-cloud-G07.md` - smoke output에 `Current session`과 `Current week` 양쪽 line이 있음을 별도 `rg` 출력으로 기록
#### 테스트 작성
별도 Go test는 작성하지 않는다. 이 항목은 실제 bin smoke 검증 신뢰 회복이 목적이다.
#### 중간 검증
```bash
command -v claude
```
기대 결과: Claude binary path가 출력된다. 실패하면 bin smoke를 실행하지 않고 실패 stdout/stderr를 review file에 기록한다.
## 수정 파일 요약
| 파일 | 항목 |
|------|------|
| `apps/node/internal/adapters/cli/status/claude.go` | REVIEW_API-1 |
| `apps/node/internal/adapters/cli/status/claude_test.go` | REVIEW_API-2 |
| `agent-task/cli_claude_usage_status/CODE_REVIEW-cloud-G07.md` | 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/claude.go apps/node/internal/adapters/cli/status/claude_test.go
git diff --check
command -v claude
```
기대 결과: Go tests가 통과하고 `gofmt -l` 출력이 비어 있으며 `git diff --check`가 성공한다. `command -v claude`는 bin smoke 전제 확인이다.
```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`의 전체 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.