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

# Plan - OPS Backtest Workflow Scenarios

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

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

## 배경

Market scenario runner가 준비되면 backtest list/detail/result/start/polling과 invalid request matrix를 같은 headless contract로 검증해야 한다. API는 `services/api/internal/socket/backtest.go:74`부터 start/list/detail/result/compare handlers를 갖고, worker는 `services/worker/internal/socket/backtest.go:112`부터 worker-owned execution/read handlers를 갖고 있다. 이 작업은 서비스 내부를 바꾸지 않고 CLI runner에 backtest actions, polling, summary output, typed error matrix를 추가한다.

## 사용자 리뷰 요청 흐름

구현 중 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/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-ops/skills/common/plan/SKILL.md`
- `agent-roadmap/current.md`
- `agent-roadmap/phase/operator-surface/PHASE.md`
- `agent-roadmap/phase/operator-surface/milestones/operator-headless-workflow-validation.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`
- `apps/cli/go.mod`
- `apps/cli/cmd/alt/main.go`
- `packages/contracts/proto/alt/v1/common.proto`
- `packages/contracts/proto/alt/v1/market.proto`
- `packages/contracts/proto/alt/v1/backtest.proto`
- `services/api/internal/socket/backtest.go`
- `services/api/internal/socket/backtest_test.go`
- `services/api/internal/socket/market.go`
- `services/api/internal/socket/market_test.go`
- `services/api/internal/contracts/parser_map.go`
- `services/api/internal/contracts/parser_map_test.go`
- `services/api/internal/workerclient/client.go`
- `services/api/internal/workerclient/client_test.go`
- `services/worker/internal/socket/backtest.go`
- `services/worker/internal/socket/backtest_test.go`
- `services/worker/internal/socket/backtest_mapping.go`
- `services/worker/internal/socket/backtest_mapping_test.go`
- `services/worker/internal/storage/ports.go`
- `services/worker/internal/contracts/parser_map.go`
- `services/worker/internal/contracts/parser_map_test.go`
- `../proto-socket/go/ws_client.go`
- `../proto-socket/go/communicator.go`

### 테스트 환경 규칙

- `test_env=local`.
- `agent-test/local/rules.md`는 존재하고 전문을 읽었다. 현재 shell 직접 테스트 금지, 원격 ALT checkout root에서 실행한다.
- 적용 profile: `agent-test/local/operations-smoke.md` because `apps/cli/**` 변경.
- 참고 profile: `api-smoke`, `worker-smoke`, `contracts-smoke`. Backtest runner가 API/worker contract surface를 호출하므로 final `bin/test`로 baseline 회귀를 확인한다.
- focused command `(cd apps/cli && go test ./...)`는 `bin/test`의 CLI module subset fallback이다.

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

- Existing API/worker tests cover backtest handler registration, validation, forwarding, unavailable/not_found/internal typed errors.
- CLI runner does not yet build backtest protobuf requests, poll run status, summarize result metrics, or print invalid request matrix output. New tests must cover these.
- Real infra-backed completed backtest result is not required in this subtask; fake proto-socket server tests are the deterministic verification path.

### 심볼 참조

- renamed/removed symbol: none.

### 분할 판단

- split decision policy를 plan 파일 선택 전에 평가했다.
- shared task group: `agent-task/m-operator-headless-workflow-validation/`.
- 이 plan의 directory `03+02_backtest_workflows`는 predecessor index `02`를 요구한다.
- predecessor `02`: active `agent-task/m-operator-headless-workflow-validation/02+01_api_market/complete.log` 없음, archive complete.log 없음. runtime은 `02+01_api_market` PASS 후 이 plan을 구현해야 한다.

### 범위 결정 근거

- `services/api/**`, `services/worker/**`, `packages/contracts/**`는 수정하지 않는다. Existing handlers already cover start/list/detail/result/compare.
- Strategy execution correctness, storage migration, provider import, real PostgreSQL/Redis smoke는 worker milestone 범위로 보며 이 CLI runner subtask에서는 fake socket 검증으로 제한한다.
- Flutter UI handoff 문서는 `04+02,03_evidence_handoff`로 분리한다.

### 빌드 등급

- build lane: `cloud-G07`, review lane: `cloud-G07`.
- 이유: terminal scenario execution, polling timeout, stdout/exit-status/error contract가 있는 CLI workflow 작업이다.

## 구현 체크리스트

- [ ] `02+01_api_market`의 `complete.log`가 active 또는 archive에 있는지 확인하고, 없으면 구현을 시작하지 않고 review stub에 blocker를 기록한다.
- [ ] Backtest start/list/detail/result/compare/poll actions를 scenario schema와 runner에 추가한다.
- [ ] `backtest_run_request`, `backtest_run_polling`, `backtest_result_summary`, `invalid_request_matrix` fixture를 추가한다. 검증: fixture 또는 local smoke가 성공/실패 결과를 확인한다.
- [ ] typed error, unavailable, disconnected, expectation mismatch 출력이 grep 가능한 field로 남는지 테스트한다. 검증: typed error 또는 상태 코드 출력 테스트가 통과한다.
- [ ] 원격 ALT checkout root에서 focused CLI test와 operations smoke 검증을 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

## 의존 관계 및 구현 순서

- 이 directory 이름의 runtime dependency는 `03+02_backtest_workflows`이다.
- 구현 시작 전 `agent-task/m-operator-headless-workflow-validation/02+01_api_market/complete.log` 또는 `agent-task/archive/*/*/m-operator-headless-workflow-validation/02+01_api_market/complete.log`가 있어야 한다.
- 추가 hidden dependency를 만들지 않는다.

### [OPS-1] Backtest Request Actions And Polling

#### 문제

`packages/contracts/proto/alt/v1/backtest.proto:35` defines `StartBacktestRequest`; `:98`, `:107`, `:89`, `:117` define list/detail/result/compare requests. Existing API handlers expose those messages, but the CLI runner from prior subtasks only handles hello/market actions.

#### 해결 방법

- Extend scenario step actions:
  - `start_backtest`
  - `list_backtest_runs`
  - `get_backtest_run_detail`
  - `get_backtest_result`
  - `compare_backtest_runs`
  - `poll_backtest_run`
- Support `save_as` and interpolation from prior step response for run id, using explicit `{{steps.<id>.run.id}}` paths only.
- Poll action repeats detail/list request until status is in expected terminal set or timeout expires.

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

- [ ] `apps/cli/internal/operator/scenario.go`: backtest fields, run spec, polling config, expected status/result fields 추가.
- [ ] `apps/cli/internal/operator/runner.go`: backtest action dispatch와 saved value lookup 추가.
- [ ] `apps/cli/internal/operator/output.go`: backtest run/result summary fields 추가.
- [ ] `apps/cli/internal/operator/runner_backtest_test.go`: fake server start/list/detail/result/compare/poll tests 추가.
- [ ] `apps/cli/testdata/operator/backtest_run_request.yaml`: start fixture 추가.
- [ ] `apps/cli/testdata/operator/backtest_run_polling.yaml`: polling fixture 추가.
- [ ] `apps/cli/testdata/operator/backtest_result_summary.yaml`: result fixture 추가.

#### 테스트 작성

- 작성: `apps/cli/internal/operator/runner_backtest_test.go`
  - `TestRunBacktestRequestOutputsRunID`: start response run id/status output.
  - `TestRunBacktestPollingStopsOnSucceeded`: polling uses saved run id and terminal status.
  - `TestRunBacktestResultSummaryOutputsMetrics`: summary fields output.
  - `TestRunBacktestMissingRunIDTypedError`: missing run id expected error path.

#### 중간 검증

원격 ALT checkout root 기준:

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

기대 결과: exit code 0. Fake API server tests prove request build, polling, and summary output.

### [OPS-2] Invalid Request Matrix And Error Output

#### 문제

Milestone requires unavailable/error/disconnected to remain machine-readable without a UI. Existing API/worker handlers return typed `ErrorInfo`, but CLI must normalize typed errors and transport failures into stable terminal output and exit codes.

#### 해결 방법

- Add scenario expectation fields:
  - `expect.error_code`
  - `expect.transport_status`
  - `expect.exit_code`
- Add invalid matrix fixture with missing spec, missing run id, empty compare id, invalid market/timeframe/date range.
- Output JSONL must include at least `scenario`, `step`, `action`, `status`, `error.code`, `error.message`, `exit_code` in summary.

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

- [ ] `apps/cli/internal/operator/scenario.go`: expected error/status validation 추가.
- [ ] `apps/cli/internal/operator/runner.go`: typed error expectation and matrix execution behavior 추가.
- [ ] `apps/cli/internal/operator/output.go`: transport/typed/expectation status fields 고정.
- [ ] `apps/cli/testdata/operator/invalid_request_matrix.yaml`: full invalid matrix로 확장.
- [ ] `apps/cli/internal/operator/runner_error_test.go`: typed error expected/unexpected, transport failure, disconnected test 추가.

#### 테스트 작성

- 작성: `apps/cli/internal/operator/runner_error_test.go`
  - `TestExpectedTypedErrorExitsZero`
  - `TestUnexpectedTypedErrorExitsOne`
  - `TestTransportFailureExitsThree`
  - `TestExpectationMismatchExitsOne`

#### 중간 검증

원격 ALT checkout root 기준:

```bash
(cd apps/cli && go test -count=1 ./internal/operator -run 'Test(ExpectedTypedError|UnexpectedTypedError|TransportFailure|ExpectationMismatch)')
```

기대 결과: exit code 0.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `apps/cli/internal/operator/scenario.go` | OPS-1, OPS-2 |
| `apps/cli/internal/operator/runner.go` | OPS-1, OPS-2 |
| `apps/cli/internal/operator/output.go` | OPS-1, OPS-2 |
| `apps/cli/internal/operator/runner_backtest_test.go` | OPS-1 |
| `apps/cli/internal/operator/runner_error_test.go` | OPS-2 |
| `apps/cli/testdata/operator/backtest_run_request.yaml` | OPS-1 |
| `apps/cli/testdata/operator/backtest_run_polling.yaml` | OPS-1 |
| `apps/cli/testdata/operator/backtest_result_summary.yaml` | OPS-1 |
| `apps/cli/testdata/operator/invalid_request_matrix.yaml` | OPS-2 |

## 최종 검증

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

```bash
(cd apps/cli && go test -count=1 ./...)
bin/test
```

기대 결과: 모든 명령 exit code 0. 원격 host나 checkout 접근 불가 시 pass가 아니라 차단 사유와 미실행 명령을 review stub에 남긴다.

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