<!-- task=m-operator-headless-workflow-validation/03+02_backtest_workflows plan=1 tag=REVIEW_OPS -->

# Plan - REVIEW_OPS Backtest Workflow Review Fixes

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

`CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채우는 것이 필수다. 구현 후 검증을 실행하고 실제 stdout/stderr를 붙여 넣은 뒤 active 파일을 그대로 두고 리뷰 준비를 보고한다. 사용자 결정, 사용자 소유 외부 환경/secret/서비스 준비, 또는 계획 범위 충돌로 막히면 review stub의 `사용자 리뷰 요청` 섹션에 근거를 채우고 멈춘다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 검증 증거 공백은 사용자 리뷰 요청이 아니다.

## 배경

첫 리뷰에서 CLI backtest runner 테스트는 통과했지만 필수 headless contract 산출물이 빠져 있었다. `invalid_request_matrix`는 여전히 예전 unknown-action parser fixture이고, JSONL summary에는 계획에서 요구한 `exit_code`가 없으며, `polling_timeout` schema field가 실제 poll loop에 적용되지 않는다. 또한 compare run ids의 empty value validation이 API/worker 계약과 맞지 않는다.

## 사용자 리뷰 요청 흐름

구현 중 blocker는 active review stub의 `사용자 리뷰 요청` 섹션에 `agent-ops/skills/common/_templates/implementation-user-review-request-section.md` 형식으로 기록한다. code-review가 해당 요청을 검증하고 실제 `USER_REVIEW.md` 작성 여부를 결정한다.

## Roadmap Targets

- Milestone: `agent-roadmap/phase/operator-surface/milestones/operator-headless-workflow-validation.md`
- Task ids:
  - `backtest-check`: backtest list/detail/result/start 관련 결과를 화면 없이 확인할 수 있다.
  - `error-output`: unavailable/error/disconnected 상태가 화면 없이도 판별 가능한 출력으로 남는다.
- Completion mode: check-on-pass

## 분석 결과

### 읽은 파일

- `agent-ops/rules/project/rules.md`
- `agent-ops/rules/private/rules.md`
- `agent-ops/rules/common/rules-roadmap.md`
- `agent-roadmap/current.md`
- `agent-roadmap/phase/operator-surface/milestones/operator-headless-workflow-validation.md`
- `agent-ops/skills/common/router.md`
- `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-ops/rules/project/domain/operations/rules.md`
- `agent-ops/rules/project/domain/api/rules.md`
- `agent-ops/rules/project/domain/worker/rules.md`
- `agent-ops/rules/project/domain/contracts/rules.md`
- `agent-test/local/rules.md`
- `agent-test/local/operations-smoke.md`
- `agent-test/local/api-smoke.md`
- `agent-test/local/worker-smoke.md`
- `agent-test/local/contracts-smoke.md`
- `agent-ops/rules/private/testing-env.md`
- `agent-task/m-operator-headless-workflow-validation/03+02_backtest_workflows/plan_cloud_G07_0.log`
- `agent-task/m-operator-headless-workflow-validation/03+02_backtest_workflows/code_review_cloud_G07_0.log`
- `agent-task/archive/2026/06/m-operator-headless-workflow-validation/02+01_api_market/complete.log`
- `apps/cli/internal/operator/scenario.go`
- `apps/cli/internal/operator/runner.go`
- `apps/cli/internal/operator/output.go`
- `apps/cli/internal/operator/client.go`
- `apps/cli/internal/operator/client_test.go`
- `apps/cli/internal/operator/runner_backtest_test.go`
- `apps/cli/internal/operator/runner_error_test.go`
- `apps/cli/internal/operator/runner_market_test.go`
- `apps/cli/internal/operator/scenario_test.go`
- `apps/cli/internal/operator/parser_map.go`
- `apps/cli/internal/operator/parser_map_test.go`
- `apps/cli/internal/cli/cli.go`
- `apps/cli/internal/cli/cli_test.go`
- `apps/cli/testdata/operator/backtest_run_request.yaml`
- `apps/cli/testdata/operator/backtest_run_polling.yaml`
- `apps/cli/testdata/operator/backtest_result_summary.yaml`
- `apps/cli/testdata/operator/invalid_request_matrix.yaml`
- `apps/client/pubspec.lock`
- `packages/contracts/proto/alt/v1/backtest.proto`
- `services/api/internal/socket/backtest.go`
- `services/worker/internal/socket/backtest.go`
- `.gitignore`

### 테스트 환경 규칙

- `test_env=local`.
- `agent-test/local/rules.md`를 읽었다. 현재 shell 직접 테스트는 금지이며, 검증은 원격 테스트 host의 ALT checkout root에서 실행한다.
- 적용 profile: `agent-test/local/operations-smoke.md` because `apps/cli/**` 변경.
- 참고 profile: `api-smoke`, `worker-smoke`, `contracts-smoke` because runner가 API/worker/contracts backtest payload를 호출한다.
- 리뷰 중 원격 code-server 컨테이너에서 focused operator test와 `bin/test`를 재실행했고 exit code 0을 확인했다. 그러나 이 테스트들은 아래 Required 결함을 커버하지 못했다.

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

- `invalid_request_matrix.yaml`은 full invalid matrix가 아니며, `scenario_test.go`와 `cli_test.go`는 여전히 unknown action parse failure만 확인한다.
- JSONL summary `exit_code` 출력 테스트가 없다.
- `polling_timeout`이 poll loop에 적용되는지 확인하는 테스트가 없다.
- `compare_backtest_runs`의 empty run id validation 테스트가 없다.

### 심볼 참조

- renamed/removed symbol: none.

### 분할 판단

- split decision policy를 평가했다.
- shared task group: `agent-task/m-operator-headless-workflow-validation/`.
- 이 follow-up은 기존 split subtask `03+02_backtest_workflows` 안의 리뷰 보완이며, 새 sibling subtask를 만들지 않는다.
- predecessor `02`는 `agent-task/archive/2026/06/m-operator-headless-workflow-validation/02+01_api_market/complete.log`로 충족되어 있다.

### 범위 결정 근거

- `services/api/**`, `services/worker/**`, `packages/contracts/**`는 수정하지 않는다. API/worker backtest handlers and protobuf request/response contracts already exist.
- `apps/client/pubspec.lock`은 이 follow-up의 구현 범위가 아니다. 이 변경이 task-owned이면 제거하고, 사용자 소유/pre-existing 변경으로 확인되면 건드리지 말고 review stub에 제외 근거만 기록한다.
- Flutter UI, real PostgreSQL/Redis-backed backtest execution, provider integration은 범위 밖이다.

### 빌드 등급

- build lane: `cloud-G07`, review lane: `cloud-G07`.
- 이유: terminal scenario runner, stdout/JSONL contract, exit-status contract, polling timeout behavior를 수정한다.

## 구현 체크리스트

- [ ] `invalid_request_matrix.yaml`을 full invalid matrix로 교체하고 이를 검증하는 tests를 추가한다.
- [ ] JSONL summary에 `exit_code`를 포함하고 runner completion paths가 이를 채우도록 수정한다.
- [ ] `request.polling_timeout`이 poll loop의 effective timeout으로 동작하도록 수정한다.
- [ ] `compare_backtest_runs.request.run_ids`의 empty value를 dry-run validation에서 거부한다.
- [ ] 원격 ALT checkout root에서 focused CLI test와 `bin/test`를 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

## 의존 관계 및 구현 순서

- 이 directory 이름의 runtime dependency는 `03+02_backtest_workflows`이고 predecessor index `02`는 archive `complete.log`로 충족되어 있다.
- 먼저 scenario validation과 fixtures를 고친 뒤 output/polling behavior tests를 추가한다.

### [REVIEW_OPS-1] Invalid Request Matrix Fixture And Tests

#### 문제

`apps/cli/testdata/operator/invalid_request_matrix.yaml:1` is still a validation-only unknown-action fixture. `PLAN-cloud-G07.md` required a full invalid matrix for missing spec, missing run id, empty compare id, invalid market/timeframe/date range, and grep-able error output.

#### 해결 방법

- Replace the old `send_unsupported_request` fixture with supported scenario actions that exercise invalid requests.
- Keep parser/validation-error tests for malformed YAML as inline test data instead of overloading the matrix fixture.
- Add tests that load/run the matrix with fake API typed errors where needed and assert stable `status`, `error.code`, `error.message`, and expected exit behavior.

#### 수정 파일 및 체크리스트

- [ ] `apps/cli/testdata/operator/invalid_request_matrix.yaml`: full invalid matrix로 교체.
- [ ] `apps/cli/internal/operator/scenario_test.go`: unknown-action fixture dependency 제거 또는 inline YAML로 변경.
- [ ] `apps/cli/internal/cli/cli_test.go`: parse-error tests가 full matrix fixture를 실패 fixture로 쓰지 않도록 변경.
- [ ] `apps/cli/internal/operator/runner_error_test.go`: invalid matrix 실행/출력 assertions 추가.

#### 테스트 작성

- 작성: `apps/cli/internal/operator/runner_error_test.go`
  - `TestInvalidRequestMatrixOutputsExpectedErrors`: matrix run emits expected typed/mismatch/transport fields and expected exit behavior.
- 작성/수정: `apps/cli/internal/operator/scenario_test.go`
  - `TestLoadScenarioRejectsUnknownAction`: inline YAML로 unknown action validation 유지.

#### 중간 검증

원격 ALT checkout root 기준:

```bash
(cd apps/cli && go test -count=1 ./internal/operator -run 'TestInvalidRequestMatrix|TestLoadScenarioRejectsUnknownAction')
```

기대 결과: exit code 0.

### [REVIEW_OPS-2] JSONL Summary Exit Code

#### 문제

`apps/cli/internal/operator/output.go:42` defines `RunSummary` without `ExitCode`, and `apps/cli/internal/operator/output.go:139` writes JSONL summary without `exit_code`. The plan explicitly required `exit_code` in summary output.

#### 해결 방법

- Add `ExitCode int` to `RunSummary`.
- Set `ExitCode` in both dial-failure and normal completion summary writes in `RunScenario`.
- Render `exit_code` in JSONL summary. Text output may also include `exit_code` if it stays grep-friendly.

#### 수정 파일 및 체크리스트

- [ ] `apps/cli/internal/operator/output.go`: `RunSummary.ExitCode` and summary rendering 추가.
- [ ] `apps/cli/internal/operator/runner.go`: summary construction paths에서 exit code set.
- [ ] `apps/cli/internal/operator/runner_error_test.go`: JSONL summary `exit_code` assertion 추가.

#### 테스트 작성

- 작성: `apps/cli/internal/operator/runner_error_test.go`
  - `TestJSONLSummaryIncludesExitCode`: JSONL summary contains `exit_code` for success and failure path.

#### 중간 검증

원격 ALT checkout root 기준:

```bash
(cd apps/cli && go test -count=1 ./internal/operator -run 'TestJSONLSummaryIncludesExitCode')
```

기대 결과: exit code 0.

### [REVIEW_OPS-3] Polling Timeout Behavior

#### 문제

`apps/cli/internal/operator/scenario.go:164` exposes `polling_timeout`, but `apps/cli/internal/operator/runner.go:284` ignores it and uses only the generic per-step timeout. Scenario authors cannot set a polling-specific timeout.

#### 해결 방법

- In `ActionPollBacktestRun`, derive an effective poll timeout from `step.Request.PollingTimeout` when it is greater than zero.
- Keep existing `timeout` fallback when `polling_timeout` is absent.
- Avoid sleeping past the effective context deadline; wait on either interval timer or context done.

#### 수정 파일 및 체크리스트

- [ ] `apps/cli/internal/operator/runner.go`: poll loop timeout and sleep behavior 수정.
- [ ] `apps/cli/internal/operator/runner_backtest_test.go`: short `polling_timeout` regression 추가.
- [ ] `apps/cli/testdata/operator/backtest_run_polling.yaml`: 필요하면 `polling_timeout` 예시 추가.

#### 테스트 작성

- 작성: `apps/cli/internal/operator/runner_backtest_test.go`
  - `TestRunBacktestPollingHonorsPollingTimeout`: non-terminal fake response exits with transport timeout according to short `polling_timeout`.

#### 중간 검증

원격 ALT checkout root 기준:

```bash
(cd apps/cli && go test -count=1 ./internal/operator -run 'TestRunBacktestPolling')
```

기대 결과: exit code 0.

### [REVIEW_OPS-4] Compare Empty Run ID Validation

#### 문제

`apps/cli/internal/operator/scenario.go:311` validates only `len(run_ids) > 0`. `run_ids: [""]` passes dry-run validation even though API/worker handlers return `invalid_request` for empty compare ids.

#### 해결 방법

- Reject empty or whitespace-only values in `compare_backtest_runs.request.run_ids`.
- Include the empty-id case in the invalid matrix fixture and tests.

#### 수정 파일 및 체크리스트

- [ ] `apps/cli/internal/operator/scenario.go`: per-value validation 추가.
- [ ] `apps/cli/internal/operator/scenario_test.go`: empty compare id validation test 추가.
- [ ] `apps/cli/testdata/operator/invalid_request_matrix.yaml`: empty compare id case 포함.

#### 테스트 작성

- 작성: `apps/cli/internal/operator/scenario_test.go`
  - `TestValidateRejectsEmptyCompareRunID`: `run_ids: [""]` is rejected before socket dial.

#### 중간 검증

원격 ALT checkout root 기준:

```bash
(cd apps/cli && go test -count=1 ./internal/operator -run 'TestValidateRejectsEmptyCompareRunID|TestInvalidRequestMatrix')
```

기대 결과: exit code 0.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `apps/cli/internal/operator/scenario.go` | REVIEW_OPS-4 |
| `apps/cli/internal/operator/runner.go` | REVIEW_OPS-2, REVIEW_OPS-3 |
| `apps/cli/internal/operator/output.go` | REVIEW_OPS-2 |
| `apps/cli/internal/operator/runner_backtest_test.go` | REVIEW_OPS-3 |
| `apps/cli/internal/operator/runner_error_test.go` | REVIEW_OPS-1, REVIEW_OPS-2 |
| `apps/cli/internal/operator/scenario_test.go` | REVIEW_OPS-1, REVIEW_OPS-4 |
| `apps/cli/internal/cli/cli_test.go` | REVIEW_OPS-1 |
| `apps/cli/testdata/operator/invalid_request_matrix.yaml` | REVIEW_OPS-1, REVIEW_OPS-4 |
| `apps/cli/testdata/operator/backtest_run_polling.yaml` | REVIEW_OPS-3 |

## 최종 검증

원격 ALT checkout root 기준으로 실행한다. CLI output/exit code tests should be fresh; use `-count=1`.

```bash
(cd apps/cli && go test -count=1 ./internal/operator -run 'Test(InvalidRequestMatrix|LoadScenarioRejectsUnknownAction|JSONLSummaryIncludesExitCode|RunBacktestPolling|ValidateRejectsEmptyCompareRunID)')
(cd apps/cli && go test -count=1 ./...)
bin/test
```

기대 결과: 모든 명령 exit code 0. `apps/client/pubspec.lock` 변경은 이 task가 만든 변경이면 제거하고, 사용자 소유/pre-existing 변경이면 건드리지 않고 review stub에 제외 근거를 기록한다.

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