<!-- task=m-agent-readable-repository-refactor/07_client_runtime_panel plan=2 tag=REVIEW_REVIEW_REFACTOR_CLIENT -->

# Flutter runtime panel readability·pending-state 검증 종결 계획

## 이 파일을 읽는 구현 에이전트에게

아래 세 Required 항목만 구현하고 지정 검증을 그대로 실행한다. 실제 구현 내용과 stdout/stderr를 active `CODE_REVIEW-local-G06.md`의 구현 에이전트 소유 섹션에 채운 뒤 active 파일을 유지하고 리뷰 준비 상태로 보고한다. 최종 판정, 로그 rename, `complete.log`, archive 이동은 code-review 전용이다.

선택된 Milestone `구현 잠금 > 결정 필요` 항목이 구현을 차단할 때만 review stub의 `사용자 리뷰 요청`에 정확한 연결 대상과 근거를 기록하고 멈춘다. 구현 중 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하거나, `USER_REVIEW.md`·로그·`complete.log`를 직접 만들지 않는다. 환경/secret/서비스 차단, 일반 범위 조정, 재실행으로 채울 수 있는 검증 증거 공백은 사용자 리뷰 사유가 아니다.

## 배경

두 번째 구현은 production history 상태 분기를 바로잡았지만 고정된 read-set 상한 검증을 실패한 채 baseline을 2010으로 올렸고, test `main`의 `split_review`도 남겼다. 또한 command pending 회귀 테스트가 즉시 완료되는 fake를 사용해 이름과 달리 pending 상태를 관찰하지 않는다. UI와 repository 계약은 유지하면서 중복 command 실행 흐름, test 등록 구조, deterministic fake만 좁게 보완한다.

## 사용자 리뷰 요청 흐름

선택된 Milestone lock 결정만 active review stub의 `사용자 리뷰 요청`에 기록하며, 형식은 `agent-ops/skills/common/_templates/implementation-user-review-request-section.md`를 따른다. 구현 중 직접 사용자 prompt는 금지하고 실제 `USER_REVIEW.md` 생성 여부 판단과 파일 작성은 code-review가 소유한다.

## Archive Evidence Snapshot

- Task: `agent-task/m-agent-readable-repository-refactor/07_client_runtime_panel`
- Prior plan: `agent-task/m-agent-readable-repository-refactor/07_client_runtime_panel/plan_local_G06_1.log`
- Prior review: `agent-task/m-agent-readable-repository-refactor/07_client_runtime_panel/code_review_local_G06_1.log`
- Verdict: `FAIL`
- Findings: Required 3, Suggested 0, Nit 0
  - actual `client-ui-readability` total 2008 LOC가 계획 상한 1969를 넘지만 baseline 2010이 이를 허용한다.
  - command pending 테스트가 command future를 지연시키지 않아 완료 후 상태만 검증한다.
  - `runtime_panel_test.dart`의 `main`이 191 LOC `split_review`이고 baseline은 235 LOC를 허용한다.
- Affected files: `apps/client/lib/widgets/runtime_panel.dart`, `apps/client/test/runtime_panel_test.dart`, `apps/client/test/support/client_test_harness.dart`, `scripts/readability_baseline.json`
- Verification evidence:
  - `flutter test test/runtime_panel_test.dart`: PASS, 5 tests.
  - `make client-test`: PASS, 40 tests.
  - `flutter analyze`: PASS.
  - actual audit: `runtime_panel.dart` file violation 없음, `client-ui-readability=2008`, test `main=191 split_review`.
  - `make readability-audit`: PASS지만 baseline이 위 두 부채를 허용한다.
  - `git diff --check`: PASS.
- Roadmap carryover: 이전 계획에 `Roadmap Targets`가 없으므로 이 follow-up PASS도 Milestone Task id를 직접 체크하지 않는다.
- 추가 상세가 꼭 필요할 때만 위 `plan_local_G06_1.log`와 `code_review_local_G06_1.log`를 좁게 읽는다.

## 분석 결과

### 읽은 파일

- `apps/client/lib/widgets/runtime_panel.dart`
- `apps/client/lib/widgets/runtime_panel_sections.dart`
- `apps/client/lib/client_home_page.dart`
- `apps/client/lib/control_plane_status_repository.dart`
- `apps/client/lib/control_plane_status_dto.dart`
- `apps/client/test/runtime_panel_test.dart`
- `apps/client/test/support/client_test_harness.dart`
- `scripts/readability_read_sets.json`
- `scripts/readability_baseline.json`
- `agent-test/local/rules.md`
- `agent-test/local/client-smoke.md`
- `agent-roadmap/phase/automation-runtime-bridge/PHASE.md`
- `agent-roadmap/phase/automation-runtime-bridge/milestones/agent-readable-repository-refactor.md`
- `agent-spec/control/control-plane-operations.md`

### SDD 기준

- 선택 Milestone은 `SDD: 불필요`다. 외부 API/proto/config schema나 runtime 책임 경계를 바꾸지 않는 동일 client domain의 동작 보존형 책임 분리다.

### 테스트 환경 규칙

- `test_env=local`.
- `agent-test/local/rules.md`와 `agent-test/local/client-smoke.md`를 읽었다.
- runner는 현재 Codex local runner, repo root는 `/config/workspace/iop`, branch는 `main`, 분석 시 HEAD는 `57f0eaa`, worktree는 병렬 subtask 변경을 포함한 dirty 상태다.
- 필수 검증은 `make client-test`, 보조 검증은 `flutter analyze`, 범위 smoke는 `flutter test test/runtime_panel_test.dart`다.
- 외부 endpoint, Docker, emulator/device, credential은 필요하지 않는다. `<확인 필요>` 값과 test-rule 유지보수 필요는 없다.

### 테스트 커버리지 공백

- loaded-empty/not-loaded production 분기는 코드와 empty/error/populated test로 확인된다.
- `RuntimePanel keeps loaded empty history visible while a command is pending`는 fake command future가 즉시 완료되어 pending 구간을 검증하지 않는다.
- read-set 상한과 test `main` violation은 actual audit의 deterministic jq 검사로 닫을 수 있다.

### 심볼 참조

- 통합 대상 `_sendCommand`, `_sendAgentCommand`, `_sendAgentStatus`는 `runtime_panel.dart` 내부 button/dialog callback에서만 참조된다.
- `RuntimePanelDomainAgentsSection`과 `RuntimePanelOperationsHistorySection` call site는 `runtime_panel.dart` 한 곳이며 이름/API를 바꾸지 않는다.
- `RuntimePanel` public 생성자와 repository/DTO symbol은 변경하지 않는다.

### 분할 판단

- split decision policy를 먼저 확인했다.
- 기존 `07_client_runtime_panel` 단일 follow-up을 유지한다. 세 항목은 같은 Flutter feature와 같은 targeted/full client·readability 검증을 사용하고, 별도 API foundation이나 도메인 경계가 없다.
- subtask 이름에 predecessor dependency가 없으며 병렬 task의 source/baseline 항목은 수정하지 않는다.

### 범위 결정 근거

- layout, 문구, 색상, key, dialog/command operation, repository/DTO 계약을 변경하지 않는다.
- `runtime_panel_sections.dart`, `client_home_page.dart`, 다른 client panel과 다른 readability baseline/read-set 항목은 현재 동작이 맞으므로 수정하지 않는다.
- generic widget/helper package, agent-ui, agent-spec, roadmap 갱신을 만들지 않는다.
- Spec update not needed: Control Plane HTTP operations를 소비하는 현재 기능과 계약은 바뀌지 않는다.

### 빌드 등급

- `local-G06`: Required 3건이 한 Flutter feature의 중복 제거, deterministic fake, test 등록 구조와 exact audit로 제한되고 로컬에서 완전히 검증 가능하다.

## 구현 체크리스트

- [ ] 중복 command 실행 흐름을 하나의 private helper로 통합해 `client-ui-readability` actual total과 baseline을 1969 LOC 이하로 낮춘다.
- [ ] command response completer를 fake에 추가하고 loaded-empty test가 command 완료 전 empty/no-spinner 상태를 결정적으로 검증하게 한다.
- [ ] runtime panel test scenario를 등록 helper로 분리해 `main` function violation을 제거하고 해당 baseline entry를 삭제한다.
- [ ] targeted test, client 전체 test/analyze, exact readability 검사와 ratchet, `git diff --check`를 모두 통과시킨다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [REVIEW_REVIEW_REFACTOR_CLIENT-1] Read-set 상한을 실제 코드 축소로 충족

문제: `apps/client/lib/widgets/runtime_panel.dart:69`, `:157`, `:240`의 세 command method가 loading, repository call, response 분류, status banner, refresh, catch를 중복한다. actual read set은 2008 LOC인데 `scripts/readability_baseline.json:1457`을 2010으로 올려 계획 상한 실패를 가린다.

해결 방법:

Before (`apps/client/lib/widgets/runtime_panel.dart:69`):

```dart
Future<void> _sendCommand(String operation) async { /* full state/send/result flow */ }
Future<void> _sendAgentCommand(String selector, String command) async { /* duplicated flow */ }
Future<void> _sendAgentStatus(String selector) async { /* duplicated flow */ }
```

After:

```dart
Future<void> _sendCommand(String operation) =>
    _executeCommand(operation);

Future<void> _sendAgentCommand(String selector, String command) =>
    _executeCommand(
      'agent.command',
      targetSelector: selector,
      parameters: {'command': command},
    );

Future<void> _executeCommand(
  String operation, {
  String? targetSelector,
  Map<String, String>? parameters,
}) async {
  // one preserved loading/send/result/refresh/error path
}
```

empty selector/command guard는 기존 wrapper에 유지하고, error status 집합과 banner 문자열 및 `_fetchOperations()` 호출 시점은 바꾸지 않는다. actual audit가 1969 이하가 될 때까지 이 중복 경계 안에서만 축소하며 layout section을 재합치지 않는다. `scripts/readability_baseline.json`의 `client-ui-readability` 값은 최종 actual total과 정확히 일치시킨다.

수정 파일 및 체크리스트:

- [ ] `apps/client/lib/widgets/runtime_panel.dart`: 세 command path의 공통 state/repository/result 처리를 하나의 80 LOC 이하 helper로 통합한다.
- [ ] `scripts/readability_baseline.json`: `client-ui-readability` 값을 actual total과 일치하는 1969 이하 값으로 낮춘다.

테스트 작성: skip. 공개 동작을 바꾸지 않는 중복 제거이며 기존 health/status/agent command widget test와 전체 client test를 사용한다.

중간 검증:

```bash
python3 scripts/readability_audit.py --input-mode worktree --output /tmp/iop-client-readability.json
jq -e '[.task_read_sets[] | select(.task_id == "client-ui-readability") | .total_loc] | length == 1 and .[0] <= 1969' /tmp/iop-client-readability.json
actual="$(jq -r '.task_read_sets[] | select(.task_id == "client-ui-readability") | .total_loc' /tmp/iop-client-readability.json)"
jq -e --argjson actual "$actual" '[.task_read_set_totals[] | select(.task_id == "client-ui-readability") | .value] == [$actual]' scripts/readability_baseline.json
```

기대 결과: 모든 명령이 exit 0이고 baseline read-set 값이 1969 이하 actual total과 정확히 같다.

### [REVIEW_REVIEW_REFACTOR_CLIENT-2] 실제 command pending 상태 검증

문제: `apps/client/test/runtime_panel_test.dart:135`는 pending을 주장하지만 `apps/client/test/support/client_test_harness.dart:376`의 `sendEdgeCommand`가 즉시 완료된다. 현재 assertion은 command와 후속 operations fetch가 끝난 뒤 empty 상태만 확인한다.

해결 방법:

Before (`apps/client/test/support/client_test_harness.dart:376`):

```dart
Future<EdgeCommandResponseView> sendEdgeCommand(...) async {
  // validation
  return EdgeCommandResponseView(...);
}
```

After:

```dart
Completer<EdgeCommandResponseView>? commandResponseCompleter;

Future<EdgeCommandResponseView> sendEdgeCommand(...) async {
  // validation and captured arguments
  if (commandResponseCompleter != null) {
    return commandResponseCompleter!.future;
  }
  return EdgeCommandResponseView(...);
}
```

pending test는 empty response가 보인 뒤 completer를 설정하고 Health Check를 시작한다. completer가 미완료인 상태에서 `No operation executions recorded.` 1개, `CircularProgressIndicator` 0개, command 호출 인자를 확인한다. 그 뒤 command response를 완료하고 `pumpAndSettle`하여 test에 미완료 future를 남기지 않는다.

수정 파일 및 체크리스트:

- [ ] `apps/client/test/support/client_test_harness.dart`: 선택적으로 command 응답을 지연하는 completer hook을 추가한다.
- [ ] `apps/client/test/runtime_panel_test.dart`: command 완료 전/후를 분리해 pending empty-state assertion을 실행한다.

테스트 작성:

- `apps/client/test/runtime_panel_test.dart` — 기존 `RuntimePanel keeps loaded empty history visible while a command is pending`을 실제 deferred command로 보강한다.
- assertion: pending 동안 empty 문구 유지, spinner 없음, `health.check`가 `edge-a`로 호출됨, completer 완료 후 settle 가능.

중간 검증:

```bash
cd apps/client && flutter test test/runtime_panel_test.dart --plain-name 'RuntimePanel keeps loaded empty history visible while a command is pending'
```

기대 결과: 단일 test가 exit 0이고 완료되지 않은 command future 구간의 assertion을 통과한다.

### [REVIEW_REVIEW_REFACTOR_CLIENT-3] Test main split-review 제거

문제: `apps/client/test/runtime_panel_test.dart:8`의 `main`이 191 LOC이고 actual audit에서 `split_review`다. `scripts/readability_baseline.json:1447`은 더 큰 235 LOC를 계속 허용한다.

해결 방법:

Before (`apps/client/test/runtime_panel_test.dart:8`):

```dart
void main() {
  testWidgets('...', (...) async { ... });
  testWidgets('...', (...) async { ... });
  // five scenarios inline
}
```

After:

```dart
void main() {
  _registerPanelRenderingScenario();
  _registerCommandErrorScenario();
  _registerCommandGatingScenario();
  _registerPendingEmptyHistoryScenario();
  _registerOperationsFetchStateScenario();
}
```

각 registration helper는 기존 test 이름과 body를 그대로 소유하고 80 LOC 이하로 유지한다. test case를 합치거나 assertion을 삭제하지 않는다. actual audit에서 `runtime_panel_test.dart` violation이 사라지면 해당 baseline entry를 삭제한다.

수정 파일 및 체크리스트:

- [ ] `apps/client/test/runtime_panel_test.dart`: 다섯 test 등록을 명명된 top-level helper로 분리하고 각각 80 LOC 이하로 유지한다.
- [ ] `scripts/readability_baseline.json`: `runtime_panel_test.dart` `main` function entry만 삭제한다.

테스트 작성: 새 scenario는 추가하지 않는다. 기존 5개 이름과 assertion을 유지한 채 등록 구조만 분리한다.

중간 검증:

```bash
python3 scripts/readability_audit.py --input-mode worktree --output /tmp/iop-client-readability.json
jq -e '[.violations[] | select(.path == "apps/client/test/runtime_panel_test.dart")] | length == 0' /tmp/iop-client-readability.json
jq -e '[.function_thresholds[] | select(.path == "apps/client/test/runtime_panel_test.dart")] | length == 0' scripts/readability_baseline.json
```

기대 결과: actual audit와 baseline 모두 runtime panel test function violation이 없고 두 jq가 exit 0이다.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `apps/client/lib/widgets/runtime_panel.dart` | REVIEW_REVIEW_REFACTOR_CLIENT-1 |
| `apps/client/test/support/client_test_harness.dart` | REVIEW_REVIEW_REFACTOR_CLIENT-2 |
| `apps/client/test/runtime_panel_test.dart` | REVIEW_REVIEW_REFACTOR_CLIENT-2, REVIEW_REVIEW_REFACTOR_CLIENT-3 |
| `scripts/readability_baseline.json` | REVIEW_REVIEW_REFACTOR_CLIENT-1, REVIEW_REVIEW_REFACTOR_CLIENT-3 |

## 최종 검증

```bash
(cd apps/client && flutter test test/runtime_panel_test.dart)
make client-test
(cd apps/client && flutter analyze)
python3 scripts/readability_audit.py --input-mode worktree --output /tmp/iop-client-readability.json
jq -e '[.violations[] | select(.path == "apps/client/lib/widgets/runtime_panel.dart" and (.level == "split_review" or .level == "exception"))] | length == 0' /tmp/iop-client-readability.json
jq -e '[.violations[] | select(.path == "apps/client/test/runtime_panel_test.dart")] | length == 0' /tmp/iop-client-readability.json
jq -e '[.task_read_sets[] | select(.task_id == "client-ui-readability") | .total_loc] | length == 1 and .[0] <= 1969' /tmp/iop-client-readability.json
jq -e '.tasks[] | select(.task_id == "client-ui-readability") | (.files | index("apps/client/lib/widgets/runtime_panel_sections.dart") != null)' scripts/readability_read_sets.json
jq -e '[.function_thresholds[] | select(.path == "apps/client/test/runtime_panel_test.dart")] | length == 0' scripts/readability_baseline.json
actual="$(jq -r '.task_read_sets[] | select(.task_id == "client-ui-readability") | .total_loc' /tmp/iop-client-readability.json)"
jq -e --argjson actual "$actual" '[.task_read_set_totals[] | select(.task_id == "client-ui-readability") | .value] == [$actual]' scripts/readability_baseline.json
make readability-audit
git diff --check
```

모든 명령이 exit 0이어야 한다. pending test는 command 완료 전 상태를 관찰해야 하며, actual/baseline 모두 runtime panel test violation이 없고 `client-ui-readability` actual total과 baseline이 같은 1969 이하 값이어야 한다.

모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.
