97 lines
17 KiB
Markdown
97 lines
17 KiB
Markdown
# Headless Validation Handoff
|
|
|
|
이 문서는 `operator-headless-workflow-validation` 마일스톤의 handoff table 기준
|
|
(`agent-roadmap/phase/operator-surface/milestones/operator-headless-workflow-validation.md`)
|
|
을 충족하는 검증 evidence를 한곳에 모은다. 후속 `Flutter Operator Console MVP`는
|
|
실화면을 만들기 전에 이 matrix로 각 scenario의 command/input/expected output/exit
|
|
code/protobuf field를 확인한다.
|
|
|
|
ALT 운영 UI 게이트(`agent-ops/rules/project/rules.md`의 "운영 UI 구현 게이트")에 따라
|
|
모든 검증은 화면 없이 CLI와 machine-readable 출력으로 먼저 수행한다.
|
|
|
|
## 공통 실행 형식
|
|
|
|
- validate(offline, 소켓 없이 dry-run): `alt operator scenario validate --file <yaml>`
|
|
- run(실 API 연결): `alt operator scenario run --file <yaml> --api-url ws://127.0.0.1:18030/socket --output jsonl`
|
|
- 로컬 개발에서는 `cd apps/cli && go run ./cmd/alt <위 인자>`로 실행한다(`bin/dev` 참고).
|
|
- exit code 계약: `0` 전 step 기대 일치, `1` typed error/expectation mismatch, `2` parse/validation/flag 오류, `3` transport 실패.
|
|
- expected output fixture는 `--output jsonl` 기준의 step/summary line이다. `run_id`,
|
|
`count`, `starting_cash`, `ending_equity`, `total_return`, `trade_count`,
|
|
`error_message` 값은 seed 데이터에 따라 달라지는 illustrative 값이며, 후속 MVP가
|
|
계약으로 의존하는 부분은 line별 `scenario`/`step`/`action`/`status` key와 step별로
|
|
검증하는 protobuf field의 존재 여부다.
|
|
|
|
### command-first workflow handoff matrix
|
|
|
|
이 matrix는 import/status/backtest workflow별로 command, expected output keys,
|
|
반복 운영 여부, UI candidate/defer reason을 명시한다. 화면 레이아웃·위젯·차트는
|
|
범위 밖이며 Flutter Operator Console MVP phase에서 별도로 정의한다.
|
|
|
|
| Scenario | Command | Input fixture | Expected output fixture | Expected output keys | Expected exit code | Repeatable operation | UI candidate | Checked protobuf/view-model field | UI defer reason |
|
|
|-----|-|---|-|-|--|-|-|-|-|
|
|
| `api_connection_smoke` | `alt operator scenario run --file testdata/operator/api_connection_smoke.yaml --api-url ws://127.0.0.1:18030/socket --output jsonl` | `testdata/operator/api_connection_smoke.yaml` | `testdata/operator/expected/api_connection_smoke.jsonl` | `scenario`, `status`, `type`, `action` | `0` (성공), `3` (transport 실패 시) | yes | connection status display | `HelloResponse.capabilities`; summary `status`/`exit_code` | Flutter capability chip layout 미정 |
|
|
| `market_data_status_query` | `alt operator scenario run --file testdata/operator/market_data_status_query.yaml --api-url ws://127.0.0.1:18030/socket --output jsonl` | `testdata/operator/market_data_status_query.yaml` | `testdata/operator/expected/market_data_status_query.jsonl` | `scenario`, `status`, `type`, `action`, `provider`, `instrument_count`, `bar_count`, `count`, `universe`, `freshness_status`, `missing_count`, `latest_yyyymmdd`, `gap_status`, `gap_count`, `duplicate_count`, `provider_delay_days` | `0` (성공/empty), `1` (mismatch) | yes | market data status card (종목수·바개수·최신도) | `import_daily_bars` → `ImportDailyBarsResponse.instrument_count`/`bar_count`; `ListInstrumentsResponse.instruments`, `ListBarsResponse.bars`; `collection_freshness` → `freshness_status`, `missing_count`, `latest_yyyymmdd`; `ErrorInfo.code` | 차트/바 시각화 레이아웃 미정 |
|
|
| `us_market_data_status_query` | `alt operator scenario run --file testdata/operator/us_market_data_status_query.yaml --api-url ws://127.0.0.1:18030/socket --output jsonl` | `testdata/operator/us_market_data_status_query.yaml` | `testdata/operator/expected/us_market_data_status_query.jsonl` | `scenario`, `status`, `type`, `action`, `provider`, `instrument_count`, `bar_count`, `count`, `universe`, `freshness_status`, `missing_count`, `latest_yyyymmdd`, `gap_status`, `gap_count`, `duplicate_count`, `provider_delay_days` | `0` (성공/empty), `1` (mismatch) | yes | market data status card (종목수·바개수·최신도) | `import_daily_bars` → `ImportDailyBarsResponse.instrument_count`/`bar_count`; `ListInstrumentsResponse.instruments`, `ListBarsResponse.bars`; `collection_freshness` → `freshness_status`, `missing_count`, `latest_yyyymmdd`; `ErrorInfo.code` | 차트/바 시각화 레이아웃 미정 |
|
|
| `backtest_run_request` | `alt operator scenario run --file testdata/operator/backtest_run_request.yaml --api-url ws://127.0.0.1:18030/socket --output jsonl` | `testdata/operator/backtest_run_request.yaml` | `testdata/operator/expected/backtest_run_request.jsonl` | `scenario`, `status`, `type`, `action`, `run_id`, `run_status` | `0` (성공), `1` (typed error/mismatch) | yes | backtest 생성 form (parameter 입력/검증) | `StartBacktestRequest.spec.selector`; `StartBacktestResponse.run.id`, `BacktestRun.status` | form validation layout 미정 |
|
|
| `backtest_run_polling` | `alt operator scenario run --file testdata/operator/backtest_run_polling.yaml --api-url ws://127.0.0.1:18030/socket --output jsonl` | `testdata/operator/backtest_run_polling.yaml` | `testdata/operator/expected/backtest_run_polling.jsonl` | `scenario`, `status`, `type`, `action`, `provider`, `instrument_count`, `bar_count`, `run_id`, `run_status` | `0` (terminal 도달), `3` (polling timeout) | yes | backtest 진행 상태 표시 (상태 칩/진행바) | named universe `import_daily_bars` → `ImportDailyBarsResponse.provider`/`instrument_count`/`bar_count`; `StartBacktestRequest.spec.selector`; `StartBacktestResponse.run.id`; `GetBacktestRunResponse.run.status` 전이 (pending→running→succeeded/failed/canceled) | 진행바/상태 전이 애니메이션 미정 |
|
|
| `backtest_result_summary` | `alt operator scenario run --file testdata/operator/backtest_result_summary.yaml --api-url ws://127.0.0.1:18030/socket --output jsonl` | `testdata/operator/backtest_result_summary.yaml` | `testdata/operator/expected/backtest_result_summary.jsonl` | `scenario`, `status`, `type`, `action`, `run_id`, `starting_cash`, `ending_equity`, `total_return`, `trade_count` | `0` (성공), `1` (missing run/typed error) | yes | backtest 결과 요약 (metrics chips) | `GetBacktestResultResponse.result.summary` | 차트/메트릭 레이아웃 미정 |
|
|
| `backtest_matrix_success` | `alt operator scenario run --file testdata/operator/backtest_matrix_success.yaml --api-url ws://127.0.0.1:18030/socket --output jsonl` | `testdata/operator/backtest_matrix_success.yaml` | `testdata/operator/expected/backtest_matrix_success.jsonl` | `scenario`, `status`, `type`, `action`, `matrix_run_id`, `universe`, `strategy_id`, `timeframe`, `period_id`, `run_id`, `run_status`, `exit_code` | `0` (성공) | yes | backtest matrix batch 성공 상태 표시 | `StartBacktestResponse.run.id`, `BacktestRun.status`; summary `exit_code` | batch 결과 화면 레이아웃 미정 |
|
|
| `backtest_matrix_failed` | `alt operator scenario run --file testdata/operator/backtest_matrix_failed.yaml --api-url ws://127.0.0.1:18030/socket --output jsonl` | `testdata/operator/backtest_matrix_failed.yaml` | `testdata/operator/expected/backtest_matrix_failed.jsonl` | `scenario`, `status`, `type`, `action`, `matrix_run_id`, `universe`, `strategy_id`, `timeframe`, `period_id`, `run_id`, `run_status`, `exit_code` | `0` (terminal failed) | yes | backtest matrix batch 실패 상태 표시 | `StartBacktestResponse.run.id`, `BacktestRun.status`; summary `exit_code` | batch 결과 화면 레이아웃 미정 |
|
|
| `backtest_matrix_timeout` | `alt operator scenario run --file testdata/operator/backtest_matrix_timeout.yaml --api-url ws://127.0.0.1:18030/socket --output jsonl` | `testdata/operator/backtest_matrix_timeout.yaml` | `testdata/operator/expected/backtest_matrix_timeout.jsonl` | `scenario`, `status`, `type`, `action`, `matrix_run_id`, `universe`, `strategy_id`, `timeframe`, `period_id`, `run_id`, `run_status`, `exit_code` | `3` (timeout) | yes | backtest matrix batch 타임아웃 표시 | `StartBacktestResponse.run.id`; summary `exit_code` | batch 결과 화면 레이아웃 미정 |
|
|
| `backtest_matrix_mismatch` | `alt operator scenario run --file testdata/operator/backtest_matrix_mismatch.yaml --api-url ws://127.0.0.1:18030/socket --output jsonl` | `testdata/operator/backtest_matrix_mismatch.yaml` | `testdata/operator/expected/backtest_matrix_mismatch.jsonl` | `scenario`, `status`, `type`, `action`, `matrix_run_id`, `universe`, `strategy_id`, `timeframe`, `period_id`, `run_id`, `run_status`, `exit_code` | `1` (mismatch) | yes | backtest matrix batch 기대 불일치 표시 | `StartBacktestResponse.run.id`, `BacktestRun.status`; summary `exit_code` | batch 결과 화면 레이아웃 미정 |
|
|
| `invalid_request_matrix` | `alt operator scenario run --file testdata/operator/invalid_request_matrix.yaml --api-url ws://127.0.0.1:18030/socket --output jsonl` | `testdata/operator/invalid_request_matrix.yaml` | `testdata/operator/expected/invalid_request_matrix.jsonl` | `scenario`, `status`, `type`, `action`, `error_code`, `error_message` | `0` (기대한 typed error 도달), `2` (malformed scenario) | no (개발/검증 전용) | error/message 노출 (검증용) | `ErrorInfo.code` (`invalid_request`, `not_found`) | error 메시지 UI 미정 |
|
|
| `kis_daily_import_smoke` | `alt operator scenario run --file testdata/operator/kis_daily_import_smoke.yaml --api-url ws://127.0.0.1:18030/socket --output jsonl` | `testdata/operator/kis_daily_import_smoke.yaml` | `testdata/operator/expected/kis_daily_import_smoke.jsonl` | `scenario`, `status`, `type`, `action`, `provider`, `instrument_count`, `bar_count` | `0` (성공), `1` (mismatch/typed error) | yes | KIS import status (진행률/결과) | `ImportDailyBarsResponse.provider`, `instrument_count`, `bar_count` | import 진행 UI 미정 |
|
|
| `paper_trading_state` | `alt operator scenario run --file testdata/operator/paper_trading_state.yaml --api-url ws://127.0.0.1:18030/socket --output jsonl` | `testdata/operator/paper_trading_state.yaml` | `testdata/operator/expected/paper_trading_state.jsonl` | `scenario`, `status`, `type`, `action`, `account_id`, `run_id`, `run_status`, `cash`, `equity_point_count`, `fill_count`, `latest_equity`, `position_count`, `risk` | `0` (성공/terminal 도착), `1` (typed error/mismatch) | yes | paper trading dashboard (account 상태/포지션/평가금액/리스크) | `StartPaperTradingResponse.run.id`, `run_status`; `GetPaperTradingStateResponse.account_id`, `cash`, `equity_point_count`, `fill_count`, `latest_equity`, `position_count`, `risk_rejections`; summary `status`/`exit_code` | Flutter dashboard 레이아웃 미정 |
|
|
| `paper_order_lifecycle` | `alt operator scenario run --file testdata/operator/paper_order_lifecycle.yaml --api-url ws://127.0.0.1:18030/socket --output jsonl` | `testdata/operator/paper_order_lifecycle.yaml` | `testdata/operator/expected/paper_order_lifecycle.jsonl` | `scenario`, `status`, `type`, `action`, `account_id`, `run_id`, `run_status`, `cash`, `equity_point_count`, `fill_count`, `latest_equity`, `position_count`, `risk`, `order_id`, `order_status`, `fill_price` | `0` (성공/terminal 도착), `1` (typed error/mismatch) | yes | paper order management list (주문 상태/체결가/취소) | `StartPaperTradingResponse.run.id`, `run_status`; `SubmitPaperOrderResponse.order.id`, `order_status`; `FillPaperOrderResponse.fill_price`, `order_status`; `CancelPaperOrderResponse.order_status`; `GetPaperTradingStateResponse.account_id`, `cash`, `fill_count`; summary `status`/`exit_code` | Flutter 주문 목록/상세 레이아웃 미정 |
|
|
| `live_order_lifecycle` | `alt operator scenario run --file testdata/operator/live_order_lifecycle.yaml --api-url ws://127.0.0.1:18030/socket --output jsonl` | `testdata/operator/live_order_lifecycle.yaml` | `testdata/operator/expected/live_order_lifecycle.jsonl` | `scenario`, `status`, `type`, `action`, `live_order_id`, `live_order_status`, `broker_order_id`, `broker_status`, `operator_confirmed` | `0` (성공/operator gate 통과), `1` (typed error/confirmation missing/mismatch) | yes | live order management (실주문 상태/취소/브로커 상태) | `SubmitLiveOrderResponse.order.id`, `order.status`, `order.broker_id`, `order.broker_status`; `OperatorConfirmation.confirmed`; `CancelLiveOrderResponse.order.status`; `GetLiveOrderResponse.order.status`; summary `status`/`exit_code` | Flutter 실주문 목록/상세 레이아웃 미정 |
|
|
| `live_risk_kill_switch` | `alt operator scenario run --file testdata/operator/live_risk_kill_switch.yaml --api-url ws://127.0.0.1:18030/socket --output jsonl` | `testdata/operator/live_risk_kill_switch.yaml` | `testdata/operator/expected/live_risk_kill_switch.jsonl` | `scenario`, `status`, `type`, `action`, `max_daily_orders`, `max_open_orders`, `allow_short_selling`, `kill_switch_halted`, `kill_switch_reason` | `0` (성공/정책 조회·킬스위치 해제), `1` (typed error/mismatch) | yes | live risk 제어 패널 (정책 표시·킬스위치 토글) | `GetLiveRiskPolicyResponse.policy.max_daily_orders`, `max_open_orders`, `allow_short_selling`; `GetLiveKillSwitchResponse.state.halted`, `state.reason`; `SetLiveKillSwitchResponse.state.halted`, `state.reason`; summary `status`/`exit_code` | Flutter 킬스위치 토글·정책 표시 레이아웃 미정 |
|
|
| `live_account_sync` | `alt operator scenario run --file testdata/operator/live_account_sync.yaml --api-url ws://127.0.0.1:18030/socket --output jsonl` | `testdata/operator/live_account_sync.yaml` | `testdata/operator/expected/live_account_sync.jsonl` | `scenario`, `status`, `type`, `action`, `live_account_id`, `live_broker`, `cash_count`, `live_position_count`, `synced_at_unix_ms`, `live_stale` | `0` (sync 성공·snapshot 조회 성공·not_found 기대 충족), `1` (typed error/mismatch) | yes | live 계좌 잔고·포지션 스냅샷 (계좌 현황 패널) | `SyncLiveAccountResponse.snapshot.account_id`, `broker`, `cash[]`, `positions[]`, `synced_at_unix_ms`, `stale`; `GetLiveAccountSnapshotResponse.snapshot`; summary `status`/`exit_code` | Flutter 계좌 잔고·포지션 목록 레이아웃 미정 |
|
|
| `live_audit_query` | `alt operator scenario run --file testdata/operator/live_audit_query.yaml --api-url ws://127.0.0.1:18030/socket --output jsonl` | `testdata/operator/live_audit_query.yaml` | `testdata/operator/expected/live_audit_query.jsonl` | `scenario`, `status`, `type`, `action`, `audit_event_count` | `0` (조회 성공·필터 조회 성공), `1` (typed error/mismatch) | yes | live 감사 이력 조회 (운영 감사 패널) | `ListLiveAuditEventsResponse.events[]`; `LiveAuditEvent.event_id`, `type`, `account_id`, `order_id`, `timestamp_unix_ms`; summary `status`/`exit_code` | Flutter 감사 이력 목록 레이아웃 미정 |
|
|
|
|
## Exit code 계약 보강 노트
|
|
|
|
- `invalid_request_matrix.yaml`은 typed error가 "도착하는지"를 검증하므로, 의도한
|
|
error code가 오면 step은 통과하고 run은 exit `0`으로 끝난다. 이는 API가 올바른
|
|
typed `ErrorInfo`를 돌려준다는 evidence다.
|
|
- 잘못된 fixture 자체(필수 step id 누락 등)의 non-zero exit는
|
|
`testdata/operator/malformed_scenario.yaml`을 `alt operator scenario validate`로
|
|
돌리면 exit code `2`로 재현된다.
|
|
|
|
## 남은 wireframe 의존성 요약
|
|
|
|
마일스톤 비범위(out-of-scope) 항목과 직접 연결된다:
|
|
|
|
- Flutter 실화면 구현 및 dashboard layout/navigation/card/chart/form 시각 디자인 확정.
|
|
- 위 matrix의 scenario별 "UI defer reason" 열에 적힌 화면 정의.
|
|
- push notification, production 운영 자동화는 이번 마일스톤 handoff 범위 밖이다.
|
|
|
|
이 화면 정의는 `Flutter Operator Console UX Plan` / `Flutter Operator Console MVP`에서
|
|
wireframe 또는 동등한 화면 정의 산출물과 사용자 승인 기준이 준비되어야 구현 잠금이
|
|
풀린다(운영 UI 구현 게이트).
|
|
|
|
## 원격 환경 동기화 및 마이그레이션 확인 절차
|
|
|
|
원격 runner(macOS host) 및 `code-server` 컨테이너 네트워크 내의 DB/Redis 인프라 정합성과 마이그레이션 확인은 아래 절차를 통해 검증한다. 환경 정보(IP, 비밀번호 등)는 노출하지 않는다.
|
|
|
|
1. **원격 호스트 및 코드 정합성 확인**
|
|
- macOS host와 로컬 code-server 컨테이너는 동일한 workspace 디렉토리를 마운트하여 사용하므로 코드는 동기화된다.
|
|
- git commit hash 및 dirty 파일 상태가 일치하는지 확인한다:
|
|
```bash
|
|
ssh <ALT_REMOTE_SSH> "cd <ALT_REMOTE_ALT_ROOT> && git rev-parse HEAD && git status --short"
|
|
```
|
|
2. **인프라(PostgreSQL / Redis) 헬스체크**
|
|
- docker compose를 통해 실행 중인 DB 및 Redis 서비스 컨테이너의 준비 상태를 점검한다:
|
|
```bash
|
|
ssh <ALT_REMOTE_SSH> "docker exec code-server-postgres pg_isready -U <DB_USER> -d <DB_NAME>"
|
|
ssh <ALT_REMOTE_SSH> "docker exec code-server-redis redis-cli ping"
|
|
```
|
|
|
|
3. **마이그레이션 적용 및 버전 확인**
|
|
- 현재 checkout 기준으로 데이터베이스 마이그레이션을 실행한다:
|
|
```bash
|
|
ssh <ALT_REMOTE_SSH> "docker exec code-server bash -lc 'cd <CONTAINER_ALT_ROOT>/services/worker && DATABASE_URL=<DATABASE_URL> REDIS_URL=<REDIS_URL> go run ./cmd/alt-worker-migrate'"
|
|
```
|
|
- schema_migrations 테이블에서 적용된 최신 마이그레이션 버전을 확인하여 일치하는지 점검한다:
|
|
```bash
|
|
ssh <ALT_REMOTE_SSH> "docker exec code-server-postgres psql -U <DB_USER> -d <DB_NAME> -t -A -c 'select max(version) from schema_migrations;'"
|
|
```
|