230 lines
11 KiB
Text
230 lines
11 KiB
Text
<!-- task=cli_gemini_usage_status plan=1 tag=REVIEW_API -->
|
|
|
|
# Gemini Usage Status Follow-Up Plan
|
|
|
|
## 이 파일을 읽는 구현 에이전트에게
|
|
|
|
**구현 완료 후 `CODE_REVIEW-*-G??.md`의 모든 섹션을 채우는 것이 필수 최종 단계입니다. 이 파일을 채우기 전까지 작업은 완료가 아닙니다.** 아래 체크리스트와 중간/최종 검증을 실제로 실행하고, 구현 내용과 명령 출력을 `CODE_REVIEW-cloud-G07.md`에 기록하세요. `CODE_REVIEW-cloud-G07.md`의 `이 파일을 읽는 리뷰 에이전트에게` 섹션에 있는 아카이브 지시(`*.log` 변경, `complete.log` 작성)는 구현 에이전트가 수행하지 않습니다.
|
|
|
|
## 배경
|
|
|
|
1차 구현은 Gemini `/stats model` quota parser와 checker를 추가했지만, code review에서 두 Required 이슈가 확인되었다. Gemini checker는 `Gemini CLI` 배너만 보고 command를 보낼 수 있어 실제 TUI readiness가 불안정하다. 또한 계획된 `./bin/edge.sh` + `./bin/node.sh` smoke가 실패했고, 보조 `go run node` smoke로 대체되어 사용자 요청의 bin 기준 검증을 만족하지 못했다.
|
|
|
|
## 분석 결과
|
|
|
|
### 읽은 파일
|
|
|
|
- `agent-ops/skills/common/code-review/SKILL.md`
|
|
- `agent-task/cli_gemini_usage_status/plan_cloud_G07_0.log`
|
|
- `agent-task/cli_gemini_usage_status/code_review_cloud_G07_0.log`
|
|
- `apps/node/internal/adapters/cli/status/gemini.go`
|
|
- `apps/node/internal/adapters/cli/status/gemini_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`
|
|
|
|
### 테스트 커버리지 공백
|
|
|
|
- Gemini readiness: 기존 fake tests는 `Gemini CLI` 배너와 ready prompt를 거의 동시에 출력한다. 배너 후 prompt 전 입력이 발생하면 실패하는 테스트가 필요하다.
|
|
- Bin smoke: 기존 fixed smoke가 실패했다. `bin/node.sh`가 임시 node config를 실제 `node serve`에 전달하는지 검증하는 shell smoke가 필요하다.
|
|
|
|
### 심볼 참조
|
|
|
|
- renamed/removed symbol 없음.
|
|
- 참조 확인 명령: `rg --sort path -n "NewGeminiChecker|GeminiChecker|IOP_NODE_CONFIG|IOP_EDGE_CONFIG|gemini status not implemented|NewGeminiChecker\\(\\)" apps/node/internal/adapters/cli/status bin agent-task/cli_gemini_usage_status`
|
|
|
|
### 범위 결정 근거
|
|
|
|
- Gemini `/model`의 모델별 quota는 계속 제외한다. 이번 follow-up은 review Required 이슈만 처리한다.
|
|
- proto와 config schema는 수정하지 않는다. Shell wrapper override와 TUI readiness 안정화로 충분하다.
|
|
- 기존 `IOP_EDGE_ADDR` 동작은 유지한다. `bin/node.sh`에는 config file override만 추가해 기본 동작을 바꾸지 않는다.
|
|
|
|
### 빌드 등급
|
|
|
|
- build lane: `cloud-G07` — TUI/PTY readiness와 bin shell orchestration 검증이 핵심이다.
|
|
- review lane: `cloud-G07` — fixed smoke 출력이 실제 재실행과 일치하는지 확인해야 한다.
|
|
|
|
## 의존 관계 및 구현 순서
|
|
|
|
1. `REVIEW_API-1`에서 Gemini readiness 조건과 회귀 테스트를 먼저 고친다.
|
|
2. `REVIEW_API-2`에서 `bin/node.sh` config override와 bin smoke를 복구한다.
|
|
|
|
### [REVIEW_API-1] Gemini checker readiness를 prompt-ready 조건으로 제한
|
|
|
|
#### 문제
|
|
|
|
`apps/node/internal/adapters/cli/status/gemini.go:97-105`는 `Gemini CLI` 배너만 봐도 ready로 판단하고 300ms 뒤 `/stats model`을 전송한다. 실제 Gemini CLI는 이 배너가 authentication/welcome 단계에서 먼저 보이고 입력 composer는 `? for shortcuts` 또는 prompt/footer 이후 열린다. 이 상태에서는 command가 너무 일찍 보내져 TUI가 무시하거나 다른 상태에 소비할 수 있다.
|
|
|
|
Before (`apps/node/internal/adapters/cli/status/gemini.go:97`):
|
|
|
|
```go
|
|
// 1. Wait for startup ready prompt.
|
|
readyRegex := regexp.MustCompile(`for shortcuts|Gemini CLI|Tips for getting started`)
|
|
if _, err := waitForAny([]waitPattern{{name: "ready", pattern: readyRegex}}, 20*time.Second); err != nil {
|
|
return nil, fmt.Errorf("gemini startup failed: %w", err)
|
|
}
|
|
time.Sleep(300 * time.Millisecond)
|
|
|
|
// 2. Request /stats model
|
|
sendText("\x15/stats model\r")
|
|
```
|
|
|
|
#### 해결 방법
|
|
|
|
`Gemini CLI` banner-only match를 제거하고 input composer가 열린 뒤에만 매치되는 문구로 제한한다. 우선 후보는 `for shortcuts`, `Type your message`, `workspace (/directory)`다. fake TUI는 배너를 먼저 출력하고 500ms 동안 early stdin을 감시한 뒤 ready prompt를 출력하게 만들어, 기존 구현에서는 실패하고 수정 후 통과하게 한다.
|
|
|
|
After:
|
|
|
|
```go
|
|
readyRegex := regexp.MustCompile(`for shortcuts|Type your message|workspace \(/directory\)`)
|
|
```
|
|
|
|
#### 수정 파일 및 체크리스트
|
|
|
|
- [ ] `apps/node/internal/adapters/cli/status/gemini.go` - `readyRegex`에서 `Gemini CLI` 제거
|
|
- [ ] `apps/node/internal/adapters/cli/status/gemini.go` - 필요 시 ready wait timeout 에러 tail 개선
|
|
- [ ] `apps/node/internal/adapters/cli/status/gemini_test.go` - `TestGeminiCheckerWaitsForInputReady` 추가
|
|
|
|
#### 테스트 작성
|
|
|
|
- `apps/node/internal/adapters/cli/status/gemini_test.go` - `TestGeminiCheckerWaitsForInputReady`: fake bash script가 `Gemini CLI v0.42.0` 출력 후 `read -t 0.5 early`로 prompt 전 입력이 오면 exit 4를 낸다. 그 다음 `? for shortcuts`를 출력하고 `/stats model`을 읽어 quota를 반환한다. checker가 early input 없이 정상 quota를 파싱하는지 검증한다.
|
|
|
|
#### 중간 검증
|
|
|
|
```bash
|
|
go test ./apps/node/internal/adapters/cli/status/... -run 'TestGeminiChecker(WaitsForInputReady|RequestsStatsModel|NoAPICallsReturnsRawUnparsed)' -count=1
|
|
```
|
|
|
|
기대 결과: Gemini checker fake TUI tests가 모두 통과한다. 캐시 출력은 허용하지 않는다.
|
|
|
|
### [REVIEW_API-2] `bin/node.sh` config override와 bin-only smoke 복구
|
|
|
|
#### 문제
|
|
|
|
`bin/node.sh:6`은 항상 `configs/node.yaml`을 사용한다. `bin/node.sh:11`의 `IOP_EDGE_ADDR`은 reachability check에만 쓰이고, `bin/node.sh:36`의 실제 `node serve --config "$CONFIG_FILE"`에는 반영되지 않는다. 그래서 1차 계획의 fixed smoke는 임시 edge `127.0.0.1:19090`에 reachable check만 통과한 뒤 node가 기본 config의 `localhost:9090`으로 연결해 `error: no nodes connected`로 실패했다.
|
|
|
|
Before (`bin/node.sh:5`):
|
|
|
|
```bash
|
|
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
CONFIG_FILE="$REPO_ROOT/configs/node.yaml"
|
|
```
|
|
|
|
#### 해결 방법
|
|
|
|
`bin/edge.sh`와 같은 패턴으로 `IOP_NODE_CONFIG` override를 추가한다. 최종 smoke는 `/tmp`에 임시 edge config와 node config를 만들고, edge는 `IOP_EDGE_CONFIG`, node는 `IOP_NODE_CONFIG`로 실행한다. 이로써 `./bin/edge.sh`와 `./bin/node.sh`만으로 fake Gemini `/status`를 검증한다.
|
|
|
|
After:
|
|
|
|
```bash
|
|
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
CONFIG_FILE="${IOP_NODE_CONFIG:-$REPO_ROOT/configs/node.yaml}"
|
|
```
|
|
|
|
#### 수정 파일 및 체크리스트
|
|
|
|
- [ ] `bin/node.sh` - `IOP_NODE_CONFIG` override 추가
|
|
- [ ] `agent-task/cli_gemini_usage_status/CODE_REVIEW-cloud-G07.md` - 최종 검증에 fixed bin-only smoke 실제 출력 기록
|
|
|
|
#### 테스트 작성
|
|
|
|
별도 unit test는 작성하지 않는다. 변경은 shell wrapper fallback 1줄이며, 최종 bin-only smoke가 동작을 검증한다.
|
|
|
|
#### 중간 검증
|
|
|
|
```bash
|
|
IOP_NODE_CONFIG=/tmp/non-existent-iop-node.yaml ./bin/node.sh
|
|
```
|
|
|
|
기대 결과: config 파일이 없어서 edge address parsing 단계에서 invalid edge_addr 또는 file read 기반 error로 실패한다. 즉 `bin/node.sh`가 `IOP_NODE_CONFIG` 값을 사용한다. 이 명령은 실패가 기대 결과이므로 실제 stdout/stderr와 exit status를 기록한다.
|
|
|
|
## 수정 파일 요약
|
|
|
|
| 파일 | 항목 |
|
|
|------|------|
|
|
| `apps/node/internal/adapters/cli/status/gemini.go` | REVIEW_API-1 |
|
|
| `apps/node/internal/adapters/cli/status/gemini_test.go` | REVIEW_API-1 |
|
|
| `bin/node.sh` | REVIEW_API-2 |
|
|
| `agent-task/cli_gemini_usage_status/CODE_REVIEW-cloud-G07.md` | REVIEW_API-2 |
|
|
|
|
## 최종 검증
|
|
|
|
```bash
|
|
gofmt -l apps/node/internal/adapters/cli/status/gemini.go apps/node/internal/adapters/cli/status/gemini_test.go
|
|
```
|
|
|
|
기대 결과: 출력이 없어야 한다.
|
|
|
|
```bash
|
|
go test ./apps/node/internal/adapters/cli/status/... ./apps/node/internal/adapters/cli/... ./apps/edge/cmd/edge/... -count=1
|
|
```
|
|
|
|
기대 결과: status, cli, edge console tests가 모두 통과한다. 캐시 출력은 허용하지 않는다.
|
|
|
|
```bash
|
|
if rg --sort path -n 'gemini status not implemented|NewGeminiChecker\(\)|readyRegex := regexp.MustCompile\(`.*Gemini CLI' apps/node/internal/adapters/cli/status; then exit 1; fi
|
|
```
|
|
|
|
기대 결과: 출력 없이 exit 0이어야 한다.
|
|
|
|
```bash
|
|
tmpdir="$(mktemp -d)"
|
|
mkfifo "$tmpdir/edge.in"
|
|
cat > "$tmpdir/gemini" <<'SH'
|
|
#!/usr/bin/env sh
|
|
printf 'Gemini CLI v0.42.0\n'
|
|
printf '? for shortcuts\n'
|
|
IFS= read -r command
|
|
cmd="${command#?}"
|
|
if [ "$cmd" != "/stats model" ] && [ "$command" != "/stats model" ]; then
|
|
printf 'unexpected command: %s\n' "$command"
|
|
exit 3
|
|
fi
|
|
printf 'Auto (Gemini 3) Stats For Nerds\n'
|
|
printf '0%% used (Limit resets in 24h)\n'
|
|
printf 'Usage limit: 200\n'
|
|
printf 'Usage limits span all sessions and reset daily.\n'
|
|
IFS= read -r exit_command
|
|
exit 0
|
|
SH
|
|
chmod +x "$tmpdir/gemini"
|
|
perl -0pe 's/listen: "0\.0\.0\.0:9090"/listen: "127.0.0.1:19190"/; s/port: 9092/port: 19192/; s/target: "claude"/target: "gemini"/; s#command: "gemini"#command: "'"$tmpdir"'/gemini"#; s/token: "changeme"/token: "smoketoken"/' configs/edge.yaml > "$tmpdir/edge.yaml"
|
|
cat > "$tmpdir/node.yaml" <<'YAML'
|
|
transport:
|
|
edge_addr: "127.0.0.1:19190"
|
|
token: "smoketoken"
|
|
logging:
|
|
level: "info"
|
|
pretty: true
|
|
metrics:
|
|
port: 19191
|
|
YAML
|
|
IOP_EDGE_CONFIG="$tmpdir/edge.yaml" ./bin/edge.sh < "$tmpdir/edge.in" > "$tmpdir/edge.out" 2> "$tmpdir/edge.err" &
|
|
edge_pid=$!
|
|
exec 3> "$tmpdir/edge.in"
|
|
IOP_NODE_CONFIG="$tmpdir/node.yaml" ./bin/node.sh > "$tmpdir/node.out" 2> "$tmpdir/node.err" &
|
|
node_pid=$!
|
|
sleep 12
|
|
printf '/nodes\n' >&3
|
|
sleep 2
|
|
printf '/status\n' >&3
|
|
sleep 20
|
|
printf '/exit\n' >&3
|
|
wait "$edge_pid"
|
|
kill "$node_pid" 2>/dev/null || true
|
|
sed -n '1,220p' "$tmpdir/edge.out"
|
|
sed -n '1,120p' "$tmpdir/edge.err"
|
|
sed -n '1,160p' "$tmpdir/node.out"
|
|
sed -n '1,160p' "$tmpdir/node.err"
|
|
rg -n '\[node-local-node-status\] target=gemini session=default|Daily quota: 100% remaining \(resets 24h\)|Used: 0%|Usage limit: 200' "$tmpdir/edge.out"
|
|
if rg -n 'unexpected command|error: no nodes connected' "$tmpdir/edge.out" "$tmpdir/edge.err" "$tmpdir/node.out" "$tmpdir/node.err"; then exit 1; fi
|
|
```
|
|
|
|
기대 결과: `./bin/edge.sh`와 `./bin/node.sh`만으로 edge output에 `[node-local-node-status] target=gemini session=default`, `Daily quota: 100% remaining (resets 24h)`, `Used: 0%`, `Usage limit: 200`이 출력된다. `unexpected command`와 `error: no nodes connected`는 없어야 한다.
|
|
|
|
모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 전체 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.
|