<!-- task=m-control-plane-operator-actions/02+01_execution_actions plan=1 tag=REVIEW_EXEC_ACTIONS -->

# Plan - REVIEW_EXEC_ACTIONS

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

구현의 마지막 단계는 반드시 active `CODE_REVIEW-local-G06.md`에서 구현 에이전트 소유 섹션을 실제 변경 내용, 설계 결정, 검증 출력으로 채우는 것이다. 검증 명령을 실행하고 실제 stdout/stderr를 붙인 뒤 active 파일을 그대로 두고 리뷰 준비를 보고한다. 최종 판정, log rename, `complete.log` 작성, archive 이동은 code-review-skill 전용이다.

선택된 SDD 결정 또는 선택된 Milestone `구현 잠금 > 결정 필요` 항목 때문에 구현이 막히면 active review stub의 `사용자 리뷰 요청` 섹션에 정확한 연결 대상, 증거, 실행한 명령, 재개 조건을 기록하고 멈춘다. 구현 중 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 환경/secret/service 차단, 일반 범위 조정, 검증 증거 공백처럼 후속 agent가 닫을 수 있는 문제는 사용자 리뷰 요청이 아니라 `검증 결과` 또는 `계획 대비 변경 사항`에 기록한다.

## 배경

이 follow-up은 직전 리뷰의 Required 이슈만 닫는다. 현재 execution action adapter는 성공 상태를 반환하지만 앱은 `void` 결과를 실패로 해석하고, 실행 row에는 실제 runner id 대신 execution id가 들어가 runner-scoped endpoint를 잘못 호출한다. SDD S03/S04 충족을 위해 성공 refresh와 runner id 전파를 회귀 테스트로 고정한다.

## 사용자 리뷰 요청 흐름

구현 중 사용자 리뷰가 필요한 경우는 선택된 SDD 결정 또는 선택된 Milestone lock 결정이 실구현을 차단할 때뿐이다. 구현 에이전트는 `agent-ops/skills/common/_templates/implementation-user-review-request-section.md`에서 복사된 active review stub의 `사용자 리뷰 요청` 섹션만 채우고 멈춘다. 직접 사용자 프롬프트는 금지되며, code-review가 요청 타당성 검증과 실제 `USER_REVIEW.md` 작성을 소유한다.

## Archive Evidence Snapshot

- Current archived plan: `agent-task/m-control-plane-operator-actions/02+01_execution_actions/plan_local_G07_0.log`
- Current archived review: `agent-task/m-control-plane-operator-actions/02+01_execution_actions/code_review_local_G07_0.log`
- Verdict: FAIL
- Required summary:
  - `OtoCoreWriteResult<void>.succeeded(null, ...)` is not accepted by `result.isSuccess`, so successful execution actions skip refresh and show failed state.
  - `OtoExecutionViewModel.runnerID` is populated with `record.id`; runner-scoped actions use execution id as runner id.
- Suggested/Nit summary: 없음
- Affected files: `apps/client/lib/src/app/core_connection_client.dart`, `apps/client/lib/src/app/oto_client_app.dart`, `apps/client/test/widget_test.dart`
- Verification evidence from failed loop: `cd apps/client && flutter test` PASS, `cd packages/flutter/oto_console && flutter test` PASS, `git diff --check` PASS, but tests did not cover success refresh or real runner id propagation.
- Roadmap/spec carryover: Roadmap task `execution-actions`; SDD scenarios `S03`, `S04`.
- Narrow reread allowed: the two archived files above and predecessor evidence `agent-task/archive/2026/06/m-control-plane-operator-actions/01_job_create/complete.log`.

## Roadmap Targets

- Milestone: `agent-roadmap/phase/control-plane-product-surface/milestones/control-plane-operator-actions.md`
- Task ids:
  - `execution-actions`: 기존 execution/runner write endpoint를 사용하는 취소, report/log/artifact action 진입 상태를 연결한다.
- Completion mode: check-on-pass

## Spec Targets

- SDD: `agent-roadmap/sdd/control-plane-product-surface/control-plane-operator-actions/SDD.md`
- Acceptance scenarios:
  - `S03`: task=`execution-actions`; evidence=`cd apps/client && flutter test`, execution action runner id request/draft tests
  - `S04`: task=`execution-actions`; evidence=success-path UI refresh test proving write success triggers read/detail reload without optimistic terminal state
- Completion mode: spec-check-on-pass

## 분석 결과

### 읽은 파일

- `agent-ops/skills/common/code-review/SKILL.md`
- `agent-ops/skills/common/plan/SKILL.md`
- `agent-ops/skills/common/_templates/implementation-user-review-request-section.md`
- `agent-test/local/rules.md`
- `agent-task/m-control-plane-operator-actions/02+01_execution_actions/plan_local_G07_0.log`
- `agent-task/m-control-plane-operator-actions/02+01_execution_actions/code_review_local_G07_0.log`
- `agent-task/archive/2026/06/m-control-plane-operator-actions/01_job_create/complete.log`
- `agent-roadmap/sdd/control-plane-product-surface/control-plane-operator-actions/SDD.md`
- `apps/client/lib/src/app/core_connection_client.dart`
- `apps/client/lib/src/app/oto_client_app.dart`
- `apps/client/test/widget_test.dart`
- `packages/flutter/oto_console/lib/src/oto_console_contract.dart`
- `services/core/internal/httpserver/routes.go`
- `services/core/internal/httpserver/dto.go`
- `services/core/internal/httpserver/runner_cicd_handlers.go`

### 테스트 환경 규칙

- 선택한 test_env: `local`.
- `agent-test/local/rules.md`가 존재하며 전체를 읽었다.
- 적용 명령: `cd apps/client && flutter test`, `cd packages/flutter/oto_console && flutter test`, `git diff --check`.
- 이번 follow-up은 `apps/client` 회귀가 핵심이다. `packages/flutter/oto_console` public contract를 건드리면 console test도 실행한다.
- `<확인 필요>` 값은 없었다.

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

- 성공한 execution action이 `_refreshCoreStatus()`와 expanded detail reload를 호출하는 앱 테스트가 없다.
- execution action draft/request가 실제 `runner-1`을 사용하는지 검증하는 앱 테스트가 없다.
- adapter test는 `result.state`만 확인하고, 앱 성공 분기에서 사용하는 success 판정과 연결하지 않는다.

### 심볼 참조

- renamed/removed symbol: none.
- `OtoCoreWriteResult.isSuccess` call sites: `apps/client/lib/src/app/oto_client_app.dart` job create path and execution action result path.

### 분할 판단

- split decision policy를 재평가했다.
- shared task group: `agent-task/m-control-plane-operator-actions/`.
- selected subtask: `02+01_execution_actions`.
- predecessor `01_job_create`는 `agent-task/archive/2026/06/m-control-plane-operator-actions/01_job_create/complete.log`로 충족됨.
- follow-up은 같은 subtask의 Required 이슈 수습이며 새 split을 만들 정도의 독립 경계가 없다.

### 범위 결정 근거

- 신규 Core route, proto field, persistence schema는 추가하지 않는다.
- `services/core/internal/httpserver/**`는 runner id source of truth 확인용으로만 참조했다. 구현은 우선 앱 read state에서 실제 runner id를 유도하거나, 알 수 없으면 action을 disabled로 유지한다.
- console action surface의 버튼 레이아웃과 copy는 이번 Required 이슈 범위가 아니다.

### 빌드 등급

- 결정: `local-G06`.
- 근거: 앱 상태 분기와 runner id 전파, 회귀 테스트 보강으로 bounded하며 deterministic Flutter tests로 검증 가능하다.

## 구현 체크리스트

- [ ] execution action 성공 판정을 `void` data와 분리해 success/deferred 결과가 action success state와 read/detail refresh를 트리거하게 하고 `cd apps/client && flutter test --plain-name "OtoClientApp wires execution actions and refresh"`가 통과하게 한다.
- [ ] execution action draft가 실제 runner id를 사용하도록 앱 read state mapping을 수정하고, runner id를 알 수 없으면 runner-scoped action을 disabled로 유지하며 `cd apps/client && flutter test --plain-name "OtoClientApp wires execution actions and refresh"`가 통과하게 한다.
- [ ] `cd apps/client && flutter test`, 필요 시 `cd packages/flutter/oto_console && flutter test`, `git diff --check`를 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [REVIEW_EXEC_ACTIONS-1] Fix Void Success Refresh Handling

문제: `apps/client/lib/src/app/core_connection_client.dart:146`의 `isSuccess`는 `data != null`을 요구한다. 하지만 `apps/client/lib/src/app/core_connection_client.dart:529`는 execution action success를 `OtoCoreWriteResult<void>.succeeded(null, ...)`로 반환하고, `apps/client/lib/src/app/oto_client_app.dart:198`은 그 값을 실패로 처리한다.

해결 방법: job create의 data-bearing success semantics는 깨지지 않게 유지한다. Execution action 전용 result handling에서는 `result.state == OtoCoreWriteState.succeeded || result.state == OtoCoreWriteState.deferred`를 성공/지연 완료로 판단하고, 성공 메시지를 표시한 뒤 `_refreshCoreStatus()`와 expanded `_loadExecutionDetail(executionID)`를 호출한다.

Before:

```dart
// apps/client/lib/src/app/oto_client_app.dart:197
setState(() {
  if (result.isSuccess) {
    _executionActionStates[executionID] = OtoActionViewState.succeeded(message: result.message);
    _refreshCoreStatus();
    if (_expandedExecutionID == executionID) {
      _loadExecutionDetail(executionID);
    }
  } else {
    _executionActionStates[executionID] = OtoActionViewState.failed(message: result.message);
  }
});
```

After:

```dart
final accepted = result.state == OtoCoreWriteState.succeeded ||
    result.state == OtoCoreWriteState.deferred;
setState(() {
  _executionActionStates[executionID] = accepted
      ? OtoActionViewState.succeeded(message: result.message)
      : OtoActionViewState.failed(message: result.message);
});
if (accepted) {
  _refreshCoreStatus();
  if (_expandedExecutionID == executionID) {
    _loadExecutionDetail(executionID);
  }
}
```

수정 파일 및 체크리스트:

- [ ] `apps/client/lib/src/app/oto_client_app.dart`: execution action success/deferred branch 수정.
- [ ] `apps/client/test/widget_test.dart`: success result에서 read refresh와 expanded logs/artifacts reload call count를 검증.

테스트 작성:

- 작성: `apps/client/test/widget_test.dart`
- 테스트명: `OtoClientApp wires execution actions and refresh`
- assertion goal: report success calls write client, shows success state, triggers `fetchExecution`, `fetchLogs`, `fetchArtifacts` again, and failure path still does not refresh.
- fixture: `_FakeCoreWriteClient` returns `OtoCoreWriteResult<void>.succeeded(null, message: 'report accepted')`; `_FakeCoreReadClient` records fetch counts.

중간 검증:

```bash
cd apps/client && flutter test --plain-name "OtoClientApp wires execution actions and refresh"
```

예상 결과: success refresh assertion이 포함된 targeted test가 통과한다.

### [REVIEW_EXEC_ACTIONS-2] Use Real Runner ID For Execution Actions

문제: `apps/client/lib/src/app/oto_client_app.dart:449`가 `runnerID: record.id`를 사용한다. Core route는 `services/core/internal/httpserver/routes.go:111-118`의 runner-scoped endpoint이며, `services/core/internal/httpserver/runner_cicd_handlers.go:306-313`이 path/body runner id mismatch와 unknown runner를 거부한다. execution id를 runner id로 보내면 S03 action이 실패한다.

해결 방법: `OtoExecutionViewModel.runnerID`에는 실제 runner id만 넣는다. 앱이 `widget.runnerIDs`와 `fetchRunnerStatus(...).currentExecutionID`를 알고 있으면 `executionID -> runnerID` map을 만들어 execution view model에 반영한다. 실제 runner id를 알 수 없으면 빈 문자열을 유지해 `OtoExecutionsSurface`가 action을 disabled로 렌더링하게 한다. Core/proto/schema는 이번 follow-up에서 변경하지 않는다.

Before:

```dart
// apps/client/lib/src/app/oto_client_app.dart:445
OtoExecutionViewModel(
  executionID: record.id,
  jobID: record.jobID,
  state: record.state,
  runnerID: record.id,
  createdAt: record.createdAt?.toIso8601String() ?? '',
  updatedAt: record.updatedAt?.toIso8601String() ?? '',
)
```

After:

```dart
OtoExecutionViewModel(
  executionID: record.id,
  jobID: record.jobID,
  state: record.state,
  runnerID: runnerIDByExecution[record.id] ?? '',
  createdAt: record.createdAt?.toIso8601String() ?? '',
  updatedAt: record.updatedAt?.toIso8601String() ?? '',
)
```

수정 파일 및 체크리스트:

- [ ] `apps/client/lib/src/app/oto_client_app.dart`: execution read mapping에 actual runner id lookup 추가.
- [ ] `apps/client/test/widget_test.dart`: action draft가 `runner-1`과 `exec-action-test`를 각각 담는지 검증.
- [ ] `apps/client/test/widget_test.dart`: runner id를 알 수 없는 execution action은 callback이 호출되지 않거나 disabled 상태임을 검증.

테스트 작성:

- 작성: `apps/client/test/widget_test.dart`
- 테스트명: `OtoClientApp wires execution actions and refresh`
- assertion goal: `onReportExecution` receives `draft.runnerID == 'runner-1'`, not `'exec-action-test'`; unknown runner id keeps action disabled.
- fixture: `runnerIDs: ['runner-1']`, `_FakeCoreReadClient.onFetchRunnerStatus` returns `currentExecutionID: 'exec-action-test'`.

중간 검증:

```bash
cd apps/client && flutter test --plain-name "OtoClientApp wires execution actions and refresh"
```

예상 결과: real runner id draft assertion과 unknown-runner disabled assertion이 통과한다.

## 의존 관계 및 구현 순서

1. `REVIEW_EXEC_ACTIONS-2`에서 runner id source를 먼저 고정한다.
2. `REVIEW_EXEC_ACTIONS-1`에서 success refresh branch를 고치고 같은 앱 테스트에서 두 Required 이슈를 함께 검증한다.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `apps/client/lib/src/app/oto_client_app.dart` | REVIEW_EXEC_ACTIONS-1, REVIEW_EXEC_ACTIONS-2 |
| `apps/client/test/widget_test.dart` | REVIEW_EXEC_ACTIONS-1, REVIEW_EXEC_ACTIONS-2 |

## 최종 검증

```bash
cd apps/client && flutter test
cd packages/flutter/oto_console && flutter test
git diff --check
```

예상 결과: 모든 명령이 exit 0이다.

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