feat: operator runtime refactor - API lifecycle capabilities, client bootstrap, and socket handler updates

This commit is contained in:
toki 2026-05-31 09:08:03 +09:00
parent c4a457c5c4
commit 24983ad72a
12 changed files with 1450 additions and 15 deletions

View file

@ -7,7 +7,7 @@
## 활성 Milestone
- [계획] Operator Runtime Reliability Refactor
- [진행중] Operator Runtime Reliability Refactor
- Phase: `agent-roadmap/phase/operator-surface/PHASE.md`
- 경로: `agent-roadmap/phase/operator-surface/milestones/operator-runtime-refactor.md`

View file

@ -16,7 +16,7 @@ Flutter client를 ALT의 공식 operator surface로 삼아 market data 상태와
- [완료] API-Centered Proto-Socket Rail
- 경로: `agent-roadmap/archive/phase/operator-surface/milestones/api-centered-proto-socket-rail.md`
- 요약: API 서버를 control plane으로 두고 client-api, api-worker 내부 통신을 proto-socket과 ALT protobuf contracts로 통일한다.
- [계획] Operator Runtime Reliability Refactor
- [진행중] Operator Runtime Reliability Refactor
- 경로: `agent-roadmap/phase/operator-surface/milestones/operator-runtime-refactor.md`
- 요약: Flutter operator console/headless validation 전에 API-worker lifecycle, worker execution wiring, capability reporting, client bootstrap, 원격 테스트 룰을 정리한다.
- [계획] Flutter Operator Console UX Plan

View file

@ -11,7 +11,7 @@ Flutter operator console 작업을 더 진행하기 전에 API-worker runtime ra
## 상태
[계획]
[진행중]
## 구현 잠금
@ -24,7 +24,8 @@ Flutter operator console 작업을 더 진행하기 전에 API-worker runtime ra
- worker의 backtest start/execution wiring 또는 명시적 unavailable 상태 정리
- hello capability와 handler registration test를 실제 surface와 일치
- `../nexo/agent-test/local/rules.md`의 원격 실행 기준을 참고해 ALT local test profile을 원격 테스트 host 기준으로 정리
- Flutter app bootstrap에서 operator shell과 push/Firebase/Mattermost 초기화를 분리
- Flutter app
- bootstrap에서 operator shell과 push/Firebase/Mattermost 초기화를 분리
- remote verification evidence, generated drift check, skipped integration 조건을 plan/review 루프에서 재현 가능하게 정리
## 기능
@ -38,13 +39,13 @@ Operator console과 headless validation이 의존할 API-worker-client rail을 s
- [x] [test-rules] `agent-test/local` 규칙을 `../nexo/agent-test/local/rules.md`의 원격 실행 방식에 맞춰 `ssh toki@toki-labs.com` 기준으로 정리하고, ALT smoke profile이 local shell 실행을 암시하지 않게 한다.
- [ ] [capabilities] API/worker `HelloResponse.capabilities`와 handler coverage test가 market/backtest/worker availability를 실제 등록 surface와 일치하게 검증한다.
- [ ] [client-bootstrap] Flutter operator shell이 push/Firebase/Mattermost 초기화 없이도 시작 가능한 경계로 분리되고, push-specific integration은 후속 push Milestone 범위로 남는다.
- [ ] [verification-handoff] 원격 ALT checkout에서 실행할 검증 명령, `bin/contracts-check`, `bin/worker-storage-check`, 통합 테스트 skip 조건과 실제 stdout/stderr 기록 기준을 plan/review 산출물에 남길 수 있게 정리한다.
- [x] [verification-handoff] 원격 ALT checkout에서 실행할 검증 명령, `bin/contracts-check`, `bin/worker-storage-check`, 통합 테스트 skip 조건과 실제 stdout/stderr 기록 기준을 plan/review 산출물에 남길 수 있게 정리한다.
## 완료 리뷰
- 상태: 없음
- 요청일: 없음
- 완료 근거: `test-rules`는 Milestone 생성 시점에 반영했다. runtime/client refactor 기능 Task는 아직 충족되지 않았다.
- 완료 근거: `test-rules`는 Milestone 생성 시점에 반영했고, `verification-handoff`는 2026-05-31에 `agent-test/local` handoff 문구와 `m-operator-runtime-refactor` plan/review 산출물로 반영했다. runtime/client refactor 기능 Task는 아직 충족되지 않았다.
- 리뷰 필요:
- [ ] 사용자가 완료 결과를 확인했다
- [ ] archive 이동을 승인했다
@ -62,6 +63,7 @@ Operator console과 headless validation이 의존할 API-worker-client rail을 s
- 관련 경로: `services/api/`, `services/worker/`, `apps/client/`, `agent-test/local/`, `bin/`
- 표준선(선택): API는 thin control plane으로 유지하고 worker execution/storage는 worker에 둔다. 로컬 테스트 profile의 명령은 로컬 shell이 아니라 원격 테스트 host의 ALT checkout에서 실행한다. Client operator shell은 push integration 없이도 기본 실행 가능해야 한다.
- 초기 반영: 2026-05-31에 `../nexo/agent-test/local/rules.md`를 참고해 `agent-test/local/rules.md`와 smoke profile의 host/blocker 문구를 원격 테스트 host 기준으로 갱신했다.
- 진행 반영: 2026-05-31에 `agent-test/local` handoff 문구를 보강하고 `agent-task/m-operator-runtime-refactor/` 아래 worker/API/client split plan을 작성했다.
- 선행 작업: API-Centered Proto-Socket Rail
- 후속 작업: Flutter Operator Console UX Plan, Operator Headless Workflow Validation, Flutter Operator Console MVP
- 확인 필요: 없음

View file

@ -0,0 +1,150 @@
<!-- task=m-operator-runtime-refactor/01_worker_execution plan=0 tag=WORKER -->
# Code Review Reference - WORKER
> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is the mandatory final step of implementation.**
> The task is NOT complete until every implementation-owned section below is filled in.
> Complete the `구현 체크리스트`; the final checklist item is mandatory before saving.
> Fill implementation-owned sections, then stop with active files in place and report ready for review.
> If implementation is blocked by a user-only decision, user-owned external environment prerequisite, or scope conflict, fill `사용자 리뷰 요청` with evidence and stop with active files in place; code-review decides whether to write `USER_REVIEW.md`. Evidence gaps that a follow-up agent can close by rerunning commands or collecting artifacts are normal follow-up issues, not user-review blockers by themselves.
> Finalization (`코드리뷰 결과`, log rename, `complete.log`, archive moves, `코드리뷰 전용 체크리스트`) is review-agent-only, even after compaction/resume.
> Follow the ownership table at the bottom of this file for which sections you own.
## 개요
date=2026-05-31
task=m-operator-runtime-refactor/01_worker_execution, plan=0, tag=WORKER
## Roadmap Targets
- Milestone: `agent-roadmap/phase/operator-surface/milestones/operator-runtime-refactor.md`
- Task ids:
- `worker-exec`: `services/worker` main에서 `BacktestStarter`, concrete `KindRunBacktest` handler, engine/store dependency를 연결하거나, 아직 실행 불가인 부분은 capability와 typed error로 명시한다.
- Completion mode: check-on-pass
## 이 파일을 읽는 리뷰 에이전트에게
> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다.
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다.
1. 판정을 append한다.
2. `CODE_REVIEW-cloud-G07.md` -> `code_review_cloud_G07_N.log`, `PLAN-cloud-G07.md` -> `plan_cloud_G07_M.log`로 아카이브한다.
3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/m-operator-runtime-refactor/01_worker_execution/`로 이동한다. WARN/FAIL이면 user-review gate를 확인한 뒤 다음 active plan/review 파일 또는 `USER_REVIEW.md`를 작성한다. `USER_REVIEW.md`가 사용자 결정으로 완료/PASS 해소되면 code-review가 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log` 작성 후 archive 이동한다.
4. PASS이고 task group이 `m-<milestone-slug>`이면 완료 이벤트 메타데이터를 보고한다. roadmap 상태 체크와 `update-roadmap` 호출은 런타임 책임이다.
5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다.
---
## 구현 항목별 완료 여부
| 항목 | 완료 여부 |
|------|---------|
| [WORKER-1] Worker Capability Truth Source | [x] |
| [WORKER-2] Typed Unavailable And Handler Coverage | [x] |
## 구현 체크리스트
- [x] [WORKER-1] worker hello capabilities를 `Deps` 기반으로 계산해 start surface가 unavailable일 때 execution capability를 광고하지 않게 한다.
- [x] [WORKER-2] worker tests를 보강해 nil starter unavailable response와 hello capability/handler coverage가 실제 registered surface와 일치함을 검증한다.
- [x] 원격 ALT checkout root에서 `go test -count=1 ./services/worker/...`를 실행하고 실제 stdout/stderr를 review stub에 기록한다.
- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
## 코드리뷰 전용 체크리스트
> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다.
> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다.
- [x] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
- [x] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다.
- [x] active `CODE_REVIEW-*-G??.md`를 `code_review_cloud_G07_N.log`로 아카이브한다.
- [x] active `PLAN-*-G??.md`를 `plan_cloud_G07_M.log`로 아카이브한다.
- [x] `.gitignore`의 Agent-Ops 관리 block이 `agent-task/**/*.md`와 `agent-task/**/*.log`를 unignore하여 plan/review/archive 산출물이 추적 가능한지 확인한다.
- [x] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다.
- [x] PASS이면 active task 디렉터리 `agent-task/m-operator-runtime-refactor/01_worker_execution/`를 `agent-task/archive/YYYY/MM/m-operator-runtime-refactor/01_worker_execution/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다.
- [x] PASS이고 task group이 `m-operator-runtime-refactor`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다.
- [x] PASS split 작업이면 이동 후 빈 active parent `agent-task/m-operator-runtime-refactor/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다.
- [ ] WARN/FAIL이고 user-review gate가 트리거되지 않았으면 다음 active `PLAN-cloud-G07.md`와 `CODE_REVIEW-cloud-G07.md`를 작성하고 `complete.log`를 작성하지 않는다.
- [ ] USER_REVIEW(이면 `agent-ops/skills/common/code-review/templates/user-review-template.md` 기준으로 `USER_REVIEW.md`를 작성하고 active `PLAN-*.md`, `CODE_REVIEW-*.md`, `complete.log`를 남기지 않는다.
- [ ] USER_REVIEW가 사용자 결정으로 완료/PASS 해소되면 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log`를 작성한 뒤 task directory를 archive로 이동한다.
## 계획 대비 변경 사항
- 없음. 계획된 범위와 명세에 맞춰 정확하게 구현을 완료하였습니다.
## 주요 설계 결정
- `socket.Deps` 객체를 `helloHandler`에 클로저 형태로 주입하여 `protoSocket` 패키지의 형식이 지정된 리퀘스트 리스너 시그니처를 깨뜨리지 않으면서, `helloHandler`와 `handleHello`가 dynamic하게 `Deps` 필드들의 non-nil 여부에 따라 worker의 capabilities 목록을 필터링하도록 설계하였습니다.
- `capabilitiesForDeps` 헬퍼 함수를 추가하여, standard capabilities인 `hello`를 기본으로 포함하고, `Instruments`와 `Bars`가 모두 존재할 때 `market-read`, `Analysis`와 `Results`가 모두 존재할 때 `backtest-read`, `Starter`가 존재할 때 `backtest-start` 및 `worker-execution`을 추가하도록 매핑하였습니다.
## 사용자 리뷰 요청
- 상태: 없음
- 사유 유형: 없음
- 결정 필요: 없음
- 차단 근거: 없음
- 실행한 검증/명령: 없음
- 자동 후속 불가 이유: 없음
- 재개 조건: 없음
## 리뷰어를 위한 체크포인트
- Worker hello capability가 `Deps` 실제 availability에서 계산되는지 확인한다.
- `StartBacktestRequest` handler는 계속 등록되지만 nil starter에서는 typed unavailable을 반환하는지 확인한다.
- production engine/store wiring을 억지로 연결하지 않고 unavailable path를 명시했다는 범위 결정을 유지했는지 확인한다.
## 검증 결과
### WORKER-1 중간 검증
```text
$ go test -count=1 ./services/worker/internal/socket
ok git.toki-labs.com/toki/alt/services/worker/internal/socket 0.060s
```
### WORKER-2 중간 검증
```text
$ go test -count=1 ./services/worker/internal/socket ./services/worker/internal/jobs
ok git.toki-labs.com/toki/alt/services/worker/internal/socket 0.057s
ok git.toki-labs.com/toki/alt/services/worker/internal/jobs 0.002s
```
### 최종 검증
```text
$ go test -count=1 ./services/worker/...
? git.toki-labs.com/toki/alt/services/worker/cmd/alt-worker [no test files]
? git.toki-labs.com/toki/alt/services/worker/cmd/alt-worker-data-check [no test files]
ok git.toki-labs.com/toki/alt/services/worker/cmd/alt-worker-migrate 0.003s
ok git.toki-labs.com/toki/alt/services/worker/internal/backtest 0.002s
ok git.toki-labs.com/toki/alt/services/worker/internal/config 0.004s
ok git.toki-labs.com/toki/alt/services/worker/internal/contracts 0.006s
ok git.toki-labs.com/toki/alt/services/worker/internal/jobs 0.004s
ok git.toki-labs.com/toki/alt/services/worker/internal/marketdata/datacheck0.004s
ok git.toki-labs.com/toki/alt/services/worker/internal/marketdata/importer0.004s
ok git.toki-labs.com/toki/alt/services/worker/internal/providers/kis 0.008s
ok git.toki-labs.com/toki/alt/services/worker/internal/rediskeys 0.004s
ok git.toki-labs.com/toki/alt/services/worker/internal/socket 0.058s
? git.toki-labs.com/toki/alt/services/worker/internal/storage [no test files]
ok git.toki-labs.com/toki/alt/services/worker/internal/storage/postgres 0.096s
? git.toki-labs.com/toki/alt/services/worker/internal/storage/postgres/sqlc [no test files]
```
---
> **[IMPLEMENTING AGENT — BEFORE SAVING] Have you filled in every implementation-owned section: completion table, implementation checklist, changes from plan, design decisions, and verification output?**
> If anything is blank, go back and fill it in before saving this file.
> Leave review-agent-only sections unchanged.
## 코드리뷰 결과
- 종합 판정: PASS
- 차원별 평가:
- correctness: Pass
- completeness: Pass
- test coverage: Pass
- API contract: Pass
- code quality: Pass
- plan deviation: Pass
- verification trust: Pass
- 발견된 문제: 없음
- 다음 단계: PASS이므로 `complete.log`를 작성하고 active task 디렉터리를 archive로 이동한다. `m-operator-runtime-refactor` 완료 이벤트 메타데이터를 보고하되 roadmap 수정은 런타임 책임으로 남긴다.

View file

@ -0,0 +1,40 @@
# Complete - m-operator-runtime-refactor/01_worker_execution
## 완료 일시
2026-05-31
## 요약
Worker hello capability truth source를 `socket.Deps` 기반으로 정리하고 capability/test coverage를 보강했다. loop=1, final verdict=PASS.
## 루프 이력
| Plan | Review | Verdict | 메모 |
|------|--------|---------|------|
| `plan_cloud_G07_0.log` | `code_review_cloud_G07_0.log` | PASS | worker capability가 unavailable start를 광고하지 않고 원격 worker test가 통과함 |
## 구현/정리 내용
- `services/worker/internal/socket/handlers.go`에서 hello handler가 `Deps`를 받아 `hello`, `market-read`, `backtest-read`, `backtest-start` capability를 실제 dependency availability 기준으로 계산한다.
- `services/worker/internal/socket/server_test.go`에 capability helper와 worker socket hello regression coverage를 추가했다.
## 최종 검증
- `go test -count=1 ./services/worker/...` - PASS; 원격 ALT checkout에서 재실행했고 worker 전체 패키지가 exit code 0으로 통과했다.
- `git diff --check -- services/worker/internal/socket/handlers.go services/worker/internal/socket/server_test.go` - PASS; whitespace error 없음.
## Roadmap Completion
- Milestone: `agent-roadmap/phase/operator-surface/milestones/operator-runtime-refactor.md`
- Completed task ids:
- `worker-exec`: PASS; evidence=`agent-task/archive/2026/05/m-operator-runtime-refactor/01_worker_execution/plan_cloud_G07_0.log`, `agent-task/archive/2026/05/m-operator-runtime-refactor/01_worker_execution/code_review_cloud_G07_0.log`; verification=`go test -count=1 ./services/worker/...`
- Not completed task ids: 없음
## 잔여 Nit
- 없음
## 후속 작업
- 없음

View file

@ -0,0 +1,255 @@
<!-- task=m-operator-runtime-refactor/01_worker_execution plan=0 tag=WORKER -->
# PLAN - WORKER
## 이 파일을 읽는 구현 에이전트에게
`CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채우는 것이 필수다. 구현 후 검증을 실행하고, 실제 변경 내용과 stdout/stderr를 review stub에 남긴 뒤 active 파일을 유지하고 리뷰 준비 상태로 보고한다. 사용자 결정, 사용자 소유 외부 환경, 범위 충돌 없이는 진행할 수 없을 때만 review stub의 `사용자 리뷰 요청`을 채우고 멈춘다. 단순 증거 공백은 후속 에이전트가 재실행으로 메울 수 있으므로 사용자 리뷰 요청이 아니다.
## 배경
현재 worker process는 `KindRunBacktest` placeholder를 등록하고 store-backed read surface만 연결한다. `StartBacktest`는 `Deps.Starter == nil`이면 typed unavailable을 반환하지만, worker hello capability는 항상 `worker-execution`을 광고한다. production `StrategyPort`와 실행 대상 universe adapter가 아직 없으므로, 이번 단계는 실행 가능하다고 속이지 않고 start surface unavailable을 capability와 테스트로 고정한다.
## 사용자 리뷰 요청 흐름
구현 중 차단 사유는 active `CODE_REVIEW-*-G??.md`의 `사용자 리뷰 요청` 섹션에 기록한다. 이 섹션은 `agent-ops/skills/common/_templates/implementation-user-review-request-section.md` 형식을 따르며, 실제 `USER_REVIEW.md` 작성 여부는 code-review가 판단한다.
## Roadmap Targets
- Milestone: `agent-roadmap/phase/operator-surface/milestones/operator-runtime-refactor.md`
- Task ids:
- `worker-exec`: `services/worker` main에서 `BacktestStarter`, concrete `KindRunBacktest` handler, engine/store dependency를 연결하거나, 아직 실행 불가인 부분은 capability와 typed error로 명시한다.
- 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-ops/rules/project/domain/worker/rules.md`
- `agent-ops/rules/project/domain/operations/rules.md`
- `agent-test/local/rules.md`
- `agent-test/local/worker-smoke.md`
- `agent-test/local/operations-smoke.md`
- `agent-roadmap/current.md`
- `agent-roadmap/phase/operator-surface/PHASE.md`
- `agent-roadmap/phase/operator-surface/milestones/operator-runtime-refactor.md`
- `services/worker/cmd/alt-worker/main.go`
- `services/worker/internal/socket/server.go`
- `services/worker/internal/socket/handlers.go`
- `services/worker/internal/socket/backtest.go`
- `services/worker/internal/socket/market.go`
- `services/worker/internal/socket/server_test.go`
- `services/worker/internal/socket/backtest_test.go`
- `services/worker/internal/socket/market_test.go`
- `services/worker/internal/socket/backtest_mapping.go`
- `services/worker/internal/socket/backtest_mapping_test.go`
- `services/worker/internal/jobs/builtin.go`
- `services/worker/internal/jobs/runner.go`
- `services/worker/internal/jobs/backtest_starter.go`
- `services/worker/internal/jobs/backtest_jobs.go`
- `services/worker/internal/jobs/runner_test.go`
- `services/worker/internal/jobs/backtest_starter_test.go`
- `services/worker/internal/jobs/backtest_jobs_test.go`
- `services/worker/internal/backtest/engine.go`
- `services/worker/internal/backtest/engine_test.go`
- `services/worker/internal/backtest/fixture_test.go`
- `services/worker/internal/storage/ports.go`
- `services/worker/internal/storage/postgres/store.go`
- `services/worker/internal/storage/postgres/store_test.go`
- `services/worker/go.mod`
- `packages/domain/backtest/types.go`
### 테스트 환경 규칙
- `test_env=local`.
- `agent-test/local/rules.md`는 존재하고 읽었다. local 검증은 현재 shell에서 직접 실행 금지이며, `ssh toki@toki-labs.com` 접속 후 원격 ALT checkout root에서 profile 명령을 실행한다.
- 매칭 profile은 `agent-test/local/worker-smoke.md`다. worker-only 변경의 기본 명령은 원격 checkout root 기준 `go test ./services/worker/...`이며, fresh run이 필요하므로 plan에서는 `go test -count=1 ./services/worker/...`를 사용한다.
- storage migration/query/generated behavior를 바꾸지 않으면 `bin/worker-storage-check`는 필수 검증이 아니다. 실행이 필요해졌는데 infra나 원격 host가 막히면 pass가 아니라 blocker/skip 근거와 미실행 명령을 기록한다.
- `agent-test/local/operations-smoke.md`는 이번에 작은 verification-handoff 문서 변경을 확인하기 위해 읽었지만, 이 worker plan의 code path 검증 명령은 worker profile을 따른다.
### 테스트 커버리지 공백
- `handleStartBacktest`의 nil starter typed unavailable은 `services/worker/internal/socket/backtest_test.go:112`에서 이미 직접 검증한다.
- worker hello capability가 실제 `Deps`와 일치하는지는 현재 coverage가 없다. `services/worker/internal/socket/server_test.go:59`는 server name/protocol만 확인하고 capability 내용은 검증하지 않는다.
- `KindRunBacktest` concrete handler는 `services/worker/internal/jobs/backtest_jobs_test.go:308`에서 placeholder override를 검증하지만, process main에서는 production engine dependency가 없어서 연결되어 있지 않다.
### 심볼 참조
- renamed/removed symbol: none.
- `handleHello` reference: `services/worker/internal/socket/handlers.go:48`.
- `helloHandler` reference: `services/worker/internal/socket/handlers.go:19`.
- `RegisterBuiltins` reference: `services/worker/cmd/alt-worker/main.go:25`, `services/worker/internal/jobs/runner_test.go:104`, `services/worker/internal/jobs/backtest_jobs_test.go:310`.
### 분할 판단
- split decision policy를 먼저 평가했다.
- shared task group: `m-operator-runtime-refactor`.
- sibling plans:
- `01_worker_execution`: worker execution availability and worker capability truth source. no predecessors.
- `02+01_api_lifecycle_capabilities`: API lifecycle/capability work. depends on `01_worker_execution` so API capability names can align with worker truth.
- `03_client_bootstrap`: Flutter bootstrap separation. independent of worker/API implementation.
- 이 plan은 worker domain 안에서 끝나며 API/client call-site rollout을 포함하지 않는다. storage SQL or schema 변경도 하지 않는다.
### 범위 결정 근거
- production `BacktestStarter`/`Engine` wiring is intentionally excluded because `backtest.Engine` needs a `StrategyPort` and `BarSource` (`services/worker/internal/backtest/engine.go:20`, `services/worker/internal/backtest/engine.go:31`), while current `RunSpec` has no instrument/universe field and `storage.Store.GetBars` reads by instrument id (`services/worker/internal/storage/ports.go:22`).
- The acceptable milestone path is explicit unavailable: keep `Deps.Starter == nil`, keep typed `unavailable` response, and ensure hello capabilities no longer advertise execution.
- API lifecycle and client bootstrap are covered by sibling plans.
### 빌드 등급
- build lane: `cloud-G07`; review lane: `cloud-G07`.
- Worker runtime capability reporting affects proto-socket session semantics and milestone completion, while integration evidence may depend on remote-only verification.
## 구현 체크리스트
- [ ] [WORKER-1] worker hello capabilities를 `Deps` 기반으로 계산해 start surface가 unavailable일 때 execution capability를 광고하지 않게 한다.
- [ ] [WORKER-2] worker tests를 보강해 nil starter unavailable response와 hello capability/handler coverage가 실제 registered surface와 일치함을 검증한다.
- [ ] 원격 ALT checkout root에서 `go test -count=1 ./services/worker/...`를 실행하고 실제 stdout/stderr를 review stub에 기록한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
### [WORKER-1] Worker Capability Truth Source
#### 문제
`services/worker/internal/socket/handlers.go:58` always returns `worker-execution`, even when `services/worker/cmd/alt-worker/main.go:37` builds `socket.Deps` without `Starter`. This conflicts with `services/worker/internal/socket/backtest.go:117`, where start requests return typed unavailable when no starter exists.
#### 해결 방법
Make hello registration close over `Deps`, and compute capabilities from actual dependency availability.
Before:
```go
// services/worker/internal/socket/handlers.go:18
handlers := []sessionHandler{
helloHandler(),
}
```
After:
```go
handlers := []sessionHandler{
helloHandler(deps),
}
```
Before:
```go
// services/worker/internal/socket/handlers.go:53
func handleHello(req *altv1.HelloRequest) (*altv1.HelloResponse, error) {
```
After:
```go
func handleHello(deps Deps, req *altv1.HelloRequest) (*altv1.HelloResponse, error) {
```
Capabilities should always include `hello`. Add `market-read` only when both `deps.Instruments` and `deps.Bars` are non-nil. Add `backtest-read` only when `deps.Analysis` and `deps.Results` are non-nil. Add `backtest-start` only when `deps.Starter` is non-nil. Do not add `worker-execution` while start is unavailable.
#### 수정 파일 및 체크리스트
- [ ] `services/worker/internal/socket/handlers.go`: change `helloHandler` and `handleHello` to accept `Deps`.
- [ ] `services/worker/internal/socket/handlers.go`: add a small `capabilitiesForDeps(deps Deps) []string` helper.
- [ ] `services/worker/cmd/alt-worker/main.go`: leave `Starter` nil and keep existing typed unavailable path; no fake starter or placeholder execution should be wired.
#### 테스트 작성
Write tests in `services/worker/internal/socket/server_test.go`:
- `TestWorkerHelloCapabilitiesReflectDeps`: no deps => only `hello`; market deps => `market-read`; backtest read deps => `backtest-read`; starter => `backtest-start`.
- Keep existing socket hello test and assert it does not advertise `backtest-start` when `BacktestDeps{}` is used.
#### 중간 검증
Run from the remote ALT checkout root:
```bash
go test -count=1 ./services/worker/internal/socket
```
Expected: exit code 0, no missing handler/capability assertions.
### [WORKER-2] Typed Unavailable And Handler Coverage
#### 문제
`services/worker/internal/socket/backtest_test.go:112` covers the direct nil-starter response, but `services/worker/internal/socket/server_test.go:67` only checks handler uniqueness/hello presence. It does not tie the handler registry to capability claims.
#### 해결 방법
Extend registry tests to compare actual request handlers with capability names. Keep `StartBacktestRequest` registered, because registered start requests should return typed unavailable while `Deps.Starter` is nil. The distinction is capability truth: registered handler exists, execution capability does not.
Before:
```go
// services/worker/internal/socket/server_test.go:67
func TestSessionHandlers(t *testing.T) {
```
After:
```go
func TestSessionHandlers(t *testing.T) {
// keep requestType uniqueness checks
}
func TestWorkerCapabilitiesDoNotClaimUnavailableStart(t *testing.T) {
// validate capability names from Deps{}
}
```
#### 수정 파일 및 체크리스트
- [ ] `services/worker/internal/socket/server_test.go`: assert capabilities for `Deps{}` omit `backtest-start`.
- [ ] `services/worker/internal/socket/backtest_test.go`: keep `TestHandleStartBacktestUnavailable`; update only if helper names change.
- [ ] `services/worker/internal/socket/market_test.go`: no change expected unless capability helper uses fake stores from this file.
#### 테스트 작성
Tests are required because this is the regression that completes the roadmap task: the worker must not advertise unavailable execution.
#### 중간 검증
Run from the remote ALT checkout root:
```bash
go test -count=1 ./services/worker/internal/socket ./services/worker/internal/jobs
```
Expected: exit code 0. Existing job tests should still show `RegisterRunBacktestHandler` can override the built-in placeholder when a future production adapter is ready.
## 수정 파일 요약
| 파일 | 항목 |
|------|------|
| `services/worker/internal/socket/handlers.go` | WORKER-1 |
| `services/worker/internal/socket/server_test.go` | WORKER-1, WORKER-2 |
| `services/worker/internal/socket/backtest_test.go` | WORKER-2 |
| `services/worker/cmd/alt-worker/main.go` | WORKER-1 scope check only |
## 최종 검증
Run from the remote ALT checkout root:
```bash
go test -count=1 ./services/worker/...
```
Expected: exit code 0. Go test cache output is not acceptable for this plan because capability assertions are new.
If implementation changes storage SQL, migrations, or generated sqlc output, also run from the remote ALT checkout root:
```bash
bin/worker-storage-check
```
Expected: exit code 0, or a recorded blocker/skip with actual stdout/stderr if remote infra is unavailable.
모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.

View file

@ -0,0 +1,140 @@
<!-- task=m-operator-runtime-refactor/02+01_api_lifecycle_capabilities plan=0 tag=API -->
# Code Review Reference - API
> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is the mandatory final step of implementation.**
> The task is NOT complete until every implementation-owned section below is filled in.
> Complete the `구현 체크리스트`; the final checklist item is mandatory before saving.
> Fill implementation-owned sections, then stop with active files in place and report ready for review.
> If implementation is blocked by a user-only decision, user-owned external environment prerequisite, or scope conflict, fill `사용자 리뷰 요청` with evidence and stop with active files in place; code-review decides whether to write `USER_REVIEW.md`. Evidence gaps that a follow-up agent can close by rerunning commands or collecting artifacts are normal follow-up issues, not user-review blockers by themselves.
> Finalization (`코드리뷰 결과`, log rename, `complete.log`, archive moves, `코드리뷰 전용 체크리스트`) is review-agent-only, even after compaction/resume.
> Follow the ownership table at the bottom of this file for which sections you own.
## 개요
date=2026-05-31
task=m-operator-runtime-refactor/02+01_api_lifecycle_capabilities, plan=0, tag=API
## Roadmap Targets
- Milestone: `agent-roadmap/phase/operator-surface/milestones/operator-runtime-refactor.md`
- Task ids:
- `api-worker-lifecycle`: `services/api`의 worker client 전역 상태와 one-shot connect 흐름을 주입 가능한 lifecycle로 정리하고, connect failure, timeout, disconnect behavior를 테스트한다.
- `capabilities`: API/worker `HelloResponse.capabilities`와 handler coverage test가 market/backtest/worker availability를 실제 등록 surface와 일치하게 검증한다.
- Completion mode: check-on-pass
## 이 파일을 읽는 리뷰 에이전트에게
> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다.
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다.
1. 판정을 append한다.
2. `CODE_REVIEW-cloud-G07.md` -> `code_review_cloud_G07_N.log`, `PLAN-cloud-G07.md` -> `plan_cloud_G07_M.log`로 아카이브한다.
3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/m-operator-runtime-refactor/02+01_api_lifecycle_capabilities/`로 이동한다. WARN/FAIL이면 user-review gate를 확인한 뒤 다음 active plan/review 파일 또는 `USER_REVIEW.md`를 작성한다.
4. PASS이고 task group이 `m-operator-runtime-refactor`이면 완료 이벤트 메타데이터를 보고한다. roadmap 상태 체크와 `update-roadmap` 호출은 런타임 책임이다.
5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다.
---
## 구현 항목별 완료 여부
| 항목 | 완료 여부 |
|------|---------|
| [API-1] Inject Worker Lifecycle | [ ] |
| [API-2] Reconnect And Error Mapping | [ ] |
| [API-3] API Capability Alignment | [ ] |
## 구현 체크리스트
- [ ] [API-1] API server construction에서 package global `socket.Worker`를 제거하고 worker client를 `Server`/handler dependency로 주입한다.
- [ ] [API-2] worker connect failure, timeout, disconnect/reconnect behavior를 handler/lifecycle tests로 고정한다.
- [ ] [API-3] API `HelloResponse.capabilities`를 registered handlers와 worker availability 상태에 맞춰 검증한다.
- [ ] 원격 ALT checkout root에서 `go test -count=1 ./services/api/...`를 실행하고 실제 stdout/stderr를 review stub에 기록한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
## 코드리뷰 전용 체크리스트
> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다.
> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다.
- [ ] `코드리뷰 결과``PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
- [ ] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다.
- [ ] active `CODE_REVIEW-*-G??.md``code_review_cloud_G07_N.log`로 아카이브한다.
- [ ] active `PLAN-*-G??.md``plan_cloud_G07_M.log`로 아카이브한다.
- [ ] `.gitignore`의 Agent-Ops 관리 block이 `agent-task/**/*.md``agent-task/**/*.log`를 unignore하여 plan/review/archive 산출물이 추적 가능한지 확인한다.
- [ ] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다.
- [ ] PASS이면 active task 디렉터리 `agent-task/m-operator-runtime-refactor/02+01_api_lifecycle_capabilities/``agent-task/archive/YYYY/MM/m-operator-runtime-refactor/02+01_api_lifecycle_capabilities/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다.
- [ ] PASS이고 task group이 `m-operator-runtime-refactor`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다.
- [ ] PASS split 작업이면 이동 후 빈 active parent `agent-task/m-operator-runtime-refactor/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다.
- [ ] WARN/FAIL이고 user-review gate가 트리거되지 않았으면 다음 active `PLAN-cloud-G07.md``CODE_REVIEW-cloud-G07.md`를 작성하고 `complete.log`를 작성하지 않는다.
- [ ] USER_REVIEW이면 `agent-ops/skills/common/code-review/templates/user-review-template.md` 기준으로 `USER_REVIEW.md`를 작성하고 active `PLAN-*.md`, `CODE_REVIEW-*.md`, `complete.log`를 남기지 않는다.
- [ ] USER_REVIEW가 사용자 결정으로 완료/PASS 해소되면 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log`를 작성한 뒤 task directory를 archive로 이동한다.
## 계획 대비 변경 사항
_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._
## 주요 설계 결정
_구현 에이전트가 주요 설계 결정 사항을 기록한다._
## 사용자 리뷰 요청
_기본값은 `없음`이다. 구현 중 사용자 결정, 사용자 소유 외부 환경/secret/서비스 준비, 또는 계획 범위 변경 없이는 안전하게 진행할 수 없으면 아래 항목을 실제 내용으로 교체하고, 구현을 중단한 뒤 active 파일을 그대로 둔 채 리뷰를 요청한다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 검증 증거 공백만으로는 사용자 리뷰 요청을 작성하지 않는다._
- 상태: 없음
- 사유 유형: 없음
- 결정 필요: 없음
- 차단 근거: 없음
- 실행한 검증/명령: 없음
- 자동 후속 불가 이유: 없음
- 재개 조건: 없음
## 리뷰어를 위한 체크포인트
- `socket.Worker` package global이 제거되거나 더 이상 runtime state source가 아닌지 확인한다.
- Worker connect failure/timeout/disconnect가 typed response로 매핑되고 reconnect path가 테스트되는지 확인한다.
- API hello capabilities가 registered API handler surface와 worker availability를 반영하는지 확인한다.
- `01_worker_execution`의 completed capability naming과 불일치가 없는지 확인한다.
## 검증 결과
_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._
필수 규칙:
- 검증 명령은 고정된 계약이다. 임의로 대체하지 않는다.
- 대체가 필요하면 `계획 대비 변경 사항`에 이유와 대체 명령을 기록한다.
- `검증 결과`에는 실제 stdout/stderr를 붙여 넣는다.
- 사용자 리뷰 요청으로 명령을 끝까지 실행하지 못했다면 `사용자 리뷰 요청`에 실행한 명령, 실제 출력, 미실행 명령의 사유를 기록한다.
### API-1 중간 검증
```text
$ go test -count=1 ./services/api/internal/socket
(output)
```
### API-2 중간 검증
```text
$ go test -count=1 ./services/api/internal/socket ./services/api/internal/workerclient
(output)
```
### API-3 중간 검증
```text
$ go test -count=1 ./services/api/internal/socket
(output)
```
### 최종 검증
```text
$ go test -count=1 ./services/api/...
(output)
```
---
> **[IMPLEMENTING AGENT — BEFORE SAVING] Have you filled in every implementation-owned section: completion table, implementation checklist, changes from plan, design decisions, and verification output?**
> If anything is blank, go back and fill it in before saving this file.
> Leave review-agent-only sections unchanged.

View file

@ -0,0 +1,327 @@
<!-- task=m-operator-runtime-refactor/02+01_api_lifecycle_capabilities plan=0 tag=API -->
# PLAN - API
## 이 파일을 읽는 구현 에이전트에게
`CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 반드시 채운다. 구현 후 검증 명령의 실제 stdout/stderr를 붙이고, active plan/review 파일을 그대로 둔 채 리뷰 준비 상태로 보고한다. 사용자 결정, 사용자 소유 외부 환경, 범위 충돌 없이는 안전하게 진행할 수 없을 때만 review stub의 `사용자 리뷰 요청`을 채우고 멈춘다. 후속 에이전트가 명령 재실행으로 해소할 수 있는 증거 공백은 사용자 리뷰 요청이 아니다.
## 배경
현재 API server construction이 package global `socket.Worker`를 덮어쓰고, process main은 worker connect error를 goroutine 안에서 버린다. Handler tests도 global swap에 의존해 병렬성/격리가 약하다. 이 계획은 API를 thin control plane으로 유지하면서 worker client lifecycle을 주입 가능하게 만들고, hello capability가 registered handler와 worker availability를 반영하게 한다.
## 사용자 리뷰 요청 흐름
구현 중 차단 사유는 active `CODE_REVIEW-*-G??.md``사용자 리뷰 요청` 섹션에 기록한다. code-review가 정당성을 확인한 뒤에만 `USER_REVIEW.md`를 작성한다.
## Roadmap Targets
- Milestone: `agent-roadmap/phase/operator-surface/milestones/operator-runtime-refactor.md`
- Task ids:
- `api-worker-lifecycle`: `services/api`의 worker client 전역 상태와 one-shot connect 흐름을 주입 가능한 lifecycle로 정리하고, connect failure, timeout, disconnect behavior를 테스트한다.
- `capabilities`: API/worker `HelloResponse.capabilities`와 handler coverage test가 market/backtest/worker availability를 실제 등록 surface와 일치하게 검증한다.
- 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-ops/rules/project/domain/api/rules.md`
- `agent-ops/rules/project/domain/worker/rules.md`
- `agent-ops/rules/project/domain/operations/rules.md`
- `agent-test/local/rules.md`
- `agent-test/local/api-smoke.md`
- `agent-test/local/worker-smoke.md`
- `agent-test/local/contracts-smoke.md`
- `agent-roadmap/current.md`
- `agent-roadmap/phase/operator-surface/PHASE.md`
- `agent-roadmap/phase/operator-surface/milestones/operator-runtime-refactor.md`
- `services/api/cmd/alt-api/main.go`
- `services/api/internal/config/config.go`
- `services/api/internal/config/config_test.go`
- `services/api/internal/socket/server.go`
- `services/api/internal/socket/handlers.go`
- `services/api/internal/socket/backtest.go`
- `services/api/internal/socket/market.go`
- `services/api/internal/socket/server_test.go`
- `services/api/internal/socket/backtest_test.go`
- `services/api/internal/socket/market_test.go`
- `services/api/internal/workerclient/client.go`
- `services/api/internal/workerclient/client_test.go`
- `services/api/go.mod`
- `services/worker/internal/socket/handlers.go`
- `services/worker/internal/socket/server_test.go`
### 테스트 환경 규칙
- `test_env=local`.
- `agent-test/local/rules.md`는 존재하고 읽었다. 검증 명령은 현재 shell에서 직접 실행하지 않고 remote host에서 ALT checkout root 기준으로 실행한다.
- 매칭 profile은 `agent-test/local/api-smoke.md`다. API-only 변경 기본 명령은 `go test ./services/api/...`; fresh run이 필요하므로 `go test -count=1 ./services/api/...`를 사용한다.
- Parser/contract schema를 바꾸지 않으므로 `agent-test/local/contracts-smoke.md``bin/contracts-check`는 필수 검증이 아니다. Protobuf schema/generated output을 건드리면 이 plan 범위 이탈로 기록하고 contracts check를 추가해야 한다.
- Worker capability names depend on predecessor `01_worker_execution`; this dependent task must not start before that subtask has `complete.log`.
### 테스트 커버리지 공백
- `services/api/internal/workerclient/client_test.go:17` covers unavailable connect, and `services/api/internal/workerclient/client_test.go:209` covers request timeout.
- Disconnect/reconnect behavior is not covered. `sendTyped` returns `ErrUnavailable` when `wsClient` is nil/dead (`services/api/internal/workerclient/client.go:163`) but no API handler tries to reconnect before forwarding.
- `services/api/internal/socket/server_test.go:60` only asserts capabilities are non-empty; it does not verify registered API handlers or worker availability capability.
- Handler tests mutate global `Worker` through `withWorker` (`services/api/internal/socket/backtest_test.go:71`), so dependency injection/lifecycle behavior is not isolated.
### 심볼 참조
- removed/renamed symbol candidate: package global `socket.Worker`.
- References found:
- `services/api/internal/socket/server.go:18`
- `services/api/internal/socket/server.go:21`
- `services/api/cmd/alt-api/main.go:23`
- `services/api/cmd/alt-api/main.go:38`
- `services/api/internal/socket/backtest.go:68`, `84`, `103`, `122`, `146`
- `services/api/internal/socket/market.go:43`, `76`
- tests: `services/api/internal/socket/backtest_test.go:74`, `75`, `76`
### 분할 판단
- split decision policy를 먼저 평가했다.
- shared task group: `m-operator-runtime-refactor`.
- this subtask: `02+01_api_lifecycle_capabilities`.
- predecessor `01`: expected active path `agent-task/m-operator-runtime-refactor/01_worker_execution/complete.log`; currently missing because the predecessor plan was just created. Runtime dependency comes from the directory name, so implementation must wait until predecessor PASS writes `complete.log`.
- API work is split from worker because it removes package global state and changes lifecycle semantics across handlers/tests. Client bootstrap is independent and remains in `03_client_bootstrap`.
### 범위 결정 근거
- Do not change protobuf schema or generated code. `HelloResponse.capabilities` already exists in generated contracts.
- Do not make API run backtest execution or import storage dependencies; API remains a thin forwarder.
- Do not implement Flutter UI changes here.
- Do not make worker availability a hard API startup requirement. API should start and report worker unavailable when connect fails.
### 빌드 등급
- build lane: `cloud-G07`; review lane: `cloud-G07`.
- This changes lifecycle/concurrency boundaries, removes global state, and affects client-visible capability semantics.
## 구현 체크리스트
- [ ] [API-1] API server construction에서 package global `socket.Worker`를 제거하고 worker client를 `Server`/handler dependency로 주입한다.
- [ ] [API-2] worker connect failure, timeout, disconnect/reconnect behavior를 handler/lifecycle tests로 고정한다.
- [ ] [API-3] API `HelloResponse.capabilities`를 registered handlers와 worker availability 상태에 맞춰 검증한다.
- [ ] 원격 ALT checkout root에서 `go test -count=1 ./services/api/...`를 실행하고 실제 stdout/stderr를 review stub에 기록한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
## 의존 관계 및 구현 순서
`02+01_api_lifecycle_capabilities``01_worker_execution``complete.log`가 있어야 구현을 시작한다. worker side capability names and unavailable/start semantics must be known before API hello capability assertions are finalized.
### [API-1] Inject Worker Lifecycle
#### 문제
`services/api/internal/socket/server.go:18` declares package global `Worker`, and `NewServer` overwrites it at `services/api/internal/socket/server.go:21`. `services/api/cmd/alt-api/main.go:22` starts a goroutine that ignores `socket.Worker.Connect(ctx)` errors. This hides connect failures and makes tests mutate shared state.
#### 해결 방법
Introduce an API `Server` wrapper and dependency injection.
Before:
```go
// services/api/internal/socket/server.go:18
var Worker workerclient.WorkerClient
func NewServer(cfg config.Config) *protoSocket.WsServer {
Worker = workerclient.New(cfg.WorkerSocketURL)
```
After:
```go
type Server struct {
wsServer *protoSocket.WsServer
worker workerclient.WorkerClient
}
func NewServer(cfg config.Config) *Server {
return NewServerWithWorker(cfg, workerclient.New(cfg.WorkerSocketURL))
}
```
Register handlers with the injected worker:
```go
server.wsServer.OnClientConnected = registerSessionHandlers(worker)
```
Move close ownership into `Server.Stop`, and remove `socket.Worker.Connect/Close` from `cmd/alt-api/main.go`. API startup may attempt eager connect and log failure, but handlers must still operate when worker is unavailable.
#### 수정 파일 및 체크리스트
- [ ] `services/api/internal/socket/server.go`: add `Server`, `NewServerWithWorker`, `Start`, `Stop`.
- [ ] `services/api/internal/socket/handlers.go`: make `sessionHandlers(worker)` and `registerSessionHandlers(worker)` close over injected worker.
- [ ] `services/api/internal/socket/backtest.go`: pass `workerclient.WorkerClient` into handlers instead of reading global `Worker`.
- [ ] `services/api/internal/socket/market.go`: same injection change.
- [ ] `services/api/cmd/alt-api/main.go`: remove direct global connect/close usage.
#### 테스트 작성
Update tests to use `NewServerWithWorker(cfg, fake)` and remove `withWorker` global swap where possible. Add a test that two servers can be built with different fake workers without cross-talk.
#### 중간 검증
Run from the remote ALT checkout root:
```bash
go test -count=1 ./services/api/internal/socket
```
Expected: exit code 0 and no package global worker dependency.
### [API-2] Reconnect And Error Mapping
#### 문제
`services/api/internal/workerclient/client.go:56` has idempotent `Connect`, but API handlers never call it after startup. If a worker disconnects, `sendTyped` returns `ErrUnavailable` (`services/api/internal/workerclient/client.go:163`) until an external reconnect happens. There is no test for reconnect behavior.
#### 해결 방법
Before each forwarded worker request, ensure the injected worker is connected with the request timeout context. Since `Connect` is already idempotent, calling it in the request path handles initial connect failure and later disconnects without a second lifecycle manager.
Before:
```go
// services/api/internal/socket/backtest.go:71
ctx, cancel := context.WithTimeout(context.Background(), workerRequestTimeout)
defer cancel()
res, err := Worker.StartBacktest(ctx, req)
```
After:
```go
ctx, cancel := context.WithTimeout(context.Background(), workerRequestTimeout)
defer cancel()
if err := worker.Connect(ctx); err != nil {
return &altv1.StartBacktestResponse{Error: workerErrorInfo(err)}, nil
}
res, err := worker.StartBacktest(ctx, req)
```
Apply the same helper path for market and backtest forwarding to keep error mapping consistent.
#### 수정 파일 및 체크리스트
- [ ] `services/api/internal/socket/backtest.go`: add shared worker connect/forward helper or call `worker.Connect(ctx)` consistently.
- [ ] `services/api/internal/socket/market.go`: same behavior.
- [ ] `services/api/internal/workerclient/client.go`: add `IsConnected() bool` only if API hello capability needs it; keep `Connect` idempotent.
- [ ] `services/api/internal/workerclient/client_test.go`: add reconnect after close/dead connection coverage if implementation changes client state methods.
#### 테스트 작성
Add fake worker fields for connect count and connect error. Required tests:
- connect failure returns typed unavailable without forwarding request.
- context timeout maps to timeout.
- after fake disconnect state, next valid handler calls `Connect` again before forwarding.
#### 중간 검증
Run from the remote ALT checkout root:
```bash
go test -count=1 ./services/api/internal/socket ./services/api/internal/workerclient
```
Expected: exit code 0; timeout/unavailable mapping remains typed response payloads.
### [API-3] API Capability Alignment
#### 문제
`services/api/internal/socket/handlers.go:66` returns static capabilities with `hello`, `request-response`, and `market-read`. The API now registers backtest handlers (`services/api/internal/socket/backtest_test.go:442`) but does not advertise them, and it cannot represent worker availability.
#### 해결 방법
Build capabilities from registered handler groups and worker connection state. Minimum expected capability set:
- always: `hello`, `request-response`
- registered API surface: `market-read`, `backtest-read`, `backtest-start`
- worker state: `worker-available` when connected/alive, otherwise `worker-unavailable`
Keep names aligned with `01_worker_execution`. If predecessor chose different exact names, use those names here and record the change in review.
Before:
```go
// services/api/internal/socket/handlers.go:70
Capabilities: []string{
"hello",
"request-response",
"market-read",
},
```
After:
```go
Capabilities: capabilitiesForSession(worker)
```
#### 수정 파일 및 체크리스트
- [ ] `services/api/internal/socket/handlers.go`: make `helloHandler(worker)` and `handleHello(worker, req)`.
- [ ] `services/api/internal/socket/server_test.go`: assert hello capabilities include registered market/backtest API surface and worker availability state.
- [ ] `services/api/internal/socket/backtest_test.go` and `market_test.go`: keep handler coverage tests aligned with capability helper.
#### 테스트 작성
Tests are required:
- `TestServerRespondsToHelloRequest` should assert exact capability set for an unavailable fake worker.
- A unit test should assert capability names and handler registration stay in sync.
- If `workerclient.IsConnected()` is added, test both true and false fake states.
#### 중간 검증
Run from the remote ALT checkout root:
```bash
go test -count=1 ./services/api/internal/socket
```
Expected: exit code 0 and deterministic capability assertions.
## 수정 파일 요약
| 파일 | 항목 |
|------|------|
| `services/api/internal/socket/server.go` | API-1 |
| `services/api/internal/socket/handlers.go` | API-1, API-3 |
| `services/api/internal/socket/backtest.go` | API-1, API-2 |
| `services/api/internal/socket/market.go` | API-1, API-2 |
| `services/api/cmd/alt-api/main.go` | API-1 |
| `services/api/internal/workerclient/client.go` | API-2, API-3 if needed |
| `services/api/internal/socket/server_test.go` | API-1, API-3 |
| `services/api/internal/socket/backtest_test.go` | API-1, API-2, API-3 |
| `services/api/internal/socket/market_test.go` | API-1, API-2, API-3 |
| `services/api/internal/workerclient/client_test.go` | API-2 |
## 최종 검증
Run from the remote ALT checkout root:
```bash
go test -count=1 ./services/api/...
```
Expected: exit code 0. Go test cache output is not acceptable because lifecycle and capability tests are new.
If implementation changes protobuf schema or generated contract outputs, stop and record scope change; then run from the remote ALT checkout root:
```bash
bin/contracts-check
```
Expected: exit code 0 with actual stdout/stderr, or documented blocker. Schema/generated changes are not expected in this plan.
모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.

View file

@ -0,0 +1,138 @@
<!-- task=m-operator-runtime-refactor/03_client_bootstrap plan=0 tag=CLIENT -->
# Code Review Reference - CLIENT
> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is the mandatory final step of implementation.**
> The task is NOT complete until every implementation-owned section below is filled in.
> Complete the `구현 체크리스트`; the final checklist item is mandatory before saving.
> Fill implementation-owned sections, then stop with active files in place and report ready for review.
> If implementation is blocked by a user-only decision, user-owned external environment prerequisite, or scope conflict, fill `사용자 리뷰 요청` with evidence and stop with active files in place; code-review decides whether to write `USER_REVIEW.md`. Evidence gaps that a follow-up agent can close by rerunning commands or collecting artifacts are normal follow-up issues, not user-review blockers by themselves.
> Finalization (`코드리뷰 결과`, log rename, `complete.log`, archive moves, `코드리뷰 전용 체크리스트`) is review-agent-only, even after compaction/resume.
> Follow the ownership table at the bottom of this file for which sections you own.
## 개요
date=2026-05-31
task=m-operator-runtime-refactor/03_client_bootstrap, plan=0, tag=CLIENT
## Roadmap Targets
- Milestone: `agent-roadmap/phase/operator-surface/milestones/operator-runtime-refactor.md`
- Task ids:
- `client-bootstrap`: Flutter operator shell이 push/Firebase/Mattermost 초기화 없이도 시작 가능한 경계로 분리되고, push-specific integration은 후속 push Milestone 범위로 남는다.
- Completion mode: check-on-pass
## 이 파일을 읽는 리뷰 에이전트에게
> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다.
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다.
1. 판정을 append한다.
2. `CODE_REVIEW-cloud-G07.md` -> `code_review_cloud_G07_N.log`, `PLAN-cloud-G07.md` -> `plan_cloud_G07_M.log`로 아카이브한다.
3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/m-operator-runtime-refactor/03_client_bootstrap/`로 이동한다. WARN/FAIL이면 user-review gate를 확인한 뒤 다음 active plan/review 파일 또는 `USER_REVIEW.md`를 작성한다.
4. PASS이고 task group이 `m-operator-runtime-refactor`이면 완료 이벤트 메타데이터를 보고한다. roadmap 상태 체크와 `update-roadmap` 호출은 런타임 책임이다.
5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다.
---
## 구현 항목별 완료 여부
| 항목 | 완료 여부 |
|------|---------|
| [CLIENT-1] Core Shell Bootstrap | [ ] |
| [CLIENT-2] App Shell Starts Without Push | [ ] |
| [CLIENT-3] Push Opt-In Boundary | [ ] |
## 구현 체크리스트
- [ ] [CLIENT-1] core operator shell bootstrap을 Firebase/Mattermost push initialization과 분리한다.
- [ ] [CLIENT-2] bootstrap tests를 추가해 기본 `runAltClient` path가 push/Firebase 없이 app shell을 시작할 수 있음을 검증한다.
- [ ] [CLIENT-3] push-specific initialization은 explicit opt-in API로 남기고 후속 push Milestone 범위임을 코드 경계로 드러낸다.
- [ ] 원격 ALT checkout root에서 `cd apps/client && flutter test`를 실행하고 실제 stdout/stderr를 review stub에 기록한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
## 코드리뷰 전용 체크리스트
> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다.
> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다.
- [ ] `코드리뷰 결과``PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
- [ ] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다.
- [ ] active `CODE_REVIEW-*-G??.md``code_review_cloud_G07_N.log`로 아카이브한다.
- [ ] active `PLAN-*-G??.md``plan_cloud_G07_M.log`로 아카이브한다.
- [ ] `.gitignore`의 Agent-Ops 관리 block이 `agent-task/**/*.md``agent-task/**/*.log`를 unignore하여 plan/review/archive 산출물이 추적 가능한지 확인한다.
- [ ] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다.
- [ ] PASS이면 active task 디렉터리 `agent-task/m-operator-runtime-refactor/03_client_bootstrap/``agent-task/archive/YYYY/MM/m-operator-runtime-refactor/03_client_bootstrap/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다.
- [ ] PASS이고 task group이 `m-operator-runtime-refactor`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다.
- [ ] PASS split 작업이면 이동 후 빈 active parent `agent-task/m-operator-runtime-refactor/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다.
- [ ] WARN/FAIL이고 user-review gate가 트리거되지 않았으면 다음 active `PLAN-cloud-G07.md``CODE_REVIEW-cloud-G07.md`를 작성하고 `complete.log`를 작성하지 않는다.
- [ ] USER_REVIEW이면 `agent-ops/skills/common/code-review/templates/user-review-template.md` 기준으로 `USER_REVIEW.md`를 작성하고 active `PLAN-*.md`, `CODE_REVIEW-*.md`, `complete.log`를 남기지 않는다.
- [ ] USER_REVIEW가 사용자 결정으로 완료/PASS 해소되면 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log`를 작성한 뒤 task directory를 archive로 이동한다.
## 계획 대비 변경 사항
_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._
## 주요 설계 결정
_구현 에이전트가 주요 설계 결정 사항을 기록한다._
## 사용자 리뷰 요청
_기본값은 `없음`이다. 구현 중 사용자 결정, 사용자 소유 외부 환경/secret/서비스 준비, 또는 계획 범위 변경 없이는 안전하게 진행할 수 없으면 아래 항목을 실제 내용으로 교체하고, 구현을 중단한 뒤 active 파일을 그대로 둔 채 리뷰를 요청한다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 검증 증거 공백만으로는 사용자 리뷰 요청을 작성하지 않는다._
- 상태: 없음
- 사유 유형: 없음
- 결정 필요: 없음
- 차단 근거: 없음
- 실행한 검증/명령: 없음
- 자동 후속 불가 이유: 없음
- 재개 조건: 없음
## 리뷰어를 위한 체크포인트
- Default `runAltClient`/`bootstrapAltClient` path does not require Firebase or Mattermost push readiness.
- Push initialization is explicit opt-in and remains outside dashboard/widget presentation code.
- New tests avoid platform Firebase initialization by using injected hooks/fakes.
## 검증 결과
_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._
필수 규칙:
- 검증 명령은 고정된 계약이다. 임의로 대체하지 않는다.
- 대체가 필요하면 `계획 대비 변경 사항`에 이유와 대체 명령을 기록한다.
- `검증 결과`에는 실제 stdout/stderr를 붙여 넣는다.
- 사용자 리뷰 요청으로 명령을 끝까지 실행하지 못했다면 `사용자 리뷰 요청`에 실행한 명령, 실제 출력, 미실행 명령의 사유를 기록한다.
### CLIENT-1 중간 검증
```text
$ cd apps/client && flutter test test/app/bootstrap_test.dart
(output)
```
### CLIENT-2 중간 검증
```text
$ cd apps/client && flutter test test/widget_test.dart test/app/bootstrap_test.dart
(output)
```
### CLIENT-3 중간 검증
```text
$ cd apps/client && flutter test test/integrations/mattermost_push_host_integration_test.dart
(output)
```
### 최종 검증
```text
$ cd apps/client && flutter test
(output)
```
---
> **[IMPLEMENTING AGENT — BEFORE SAVING] Have you filled in every implementation-owned section: completion table, implementation checklist, changes from plan, design decisions, and verification output?**
> If anything is blank, go back and fill it in before saving this file.
> Leave review-agent-only sections unchanged.

View file

@ -0,0 +1,290 @@
<!-- task=m-operator-runtime-refactor/03_client_bootstrap plan=0 tag=CLIENT -->
# PLAN - CLIENT
## 이 파일을 읽는 구현 에이전트에게
`CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 구현이 막히면 active review stub의 `사용자 리뷰 요청`에 정확한 결정/근거/실행 명령을 기록하고 멈춘다. 후속 에이전트가 재실행으로 메울 수 있는 검증 증거 공백은 사용자 리뷰 요청이 아니다.
## 배경
Flutter app bootstrap currently initializes Firebase and Mattermost push before rendering the operator shell. The current Milestone requires the operator shell to start without push/Firebase/Mattermost readiness, while push behavior remains a later Milestone. This plan separates core shell bootstrap from optional push integration without changing dashboard UI or generated contracts.
## 사용자 리뷰 요청 흐름
구현 중 사용자 결정이 필요한 경우 active `CODE_REVIEW-*-G??.md``사용자 리뷰 요청` 섹션을 채우고 멈춘다. code-review가 검증한 경우에만 `USER_REVIEW.md`를 작성한다.
## Roadmap Targets
- Milestone: `agent-roadmap/phase/operator-surface/milestones/operator-runtime-refactor.md`
- Task ids:
- `client-bootstrap`: Flutter operator shell이 push/Firebase/Mattermost 초기화 없이도 시작 가능한 경계로 분리되고, push-specific integration은 후속 push Milestone 범위로 남는다.
- 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-ops/rules/project/domain/client/rules.md`
- `agent-ops/rules/project/domain/operations/rules.md`
- `agent-test/local/rules.md`
- `agent-test/local/client-smoke.md`
- `agent-roadmap/current.md`
- `agent-roadmap/phase/operator-surface/PHASE.md`
- `agent-roadmap/phase/operator-surface/milestones/operator-runtime-refactor.md`
- `apps/client/lib/main.dart`
- `apps/client/lib/src/app/bootstrap.dart`
- `apps/client/lib/src/app/app.dart`
- `apps/client/test/widget_test.dart`
- `apps/client/test/integrations/mattermost_push_host_integration_test.dart`
- `apps/client/integration_test/socket_runtime_smoke_test.dart`
- `apps/client/pubspec.yaml`
### 테스트 환경 규칙
- `test_env=local`.
- `agent-test/local/rules.md`는 존재하고 읽었다. local shell에서 테스트를 직접 실행하지 않고 remote host의 ALT checkout root에서 실행한다.
- 매칭 profile은 `agent-test/local/client-smoke.md`다. client-only 변경 기본 명령은 `cd apps/client && flutter test`.
- Socket runtime/integration smoke는 API socket/platform runtime이 필요하면 실행 가능 여부를 확인하고, 실행할 수 없으면 pass가 아니라 skip/blocker와 미실행 명령을 기록한다.
### 테스트 커버리지 공백
- `apps/client/test/widget_test.dart:18` already proves `AltClientApp` can render without a `mattermostHost`, but it bypasses `runAltClient` and `bootstrapAltClient`.
- `apps/client/lib/src/app/bootstrap.dart:25` has no unit test proving Firebase/push are optional.
- Mattermost host integration tests cover host initialization behavior in isolation (`apps/client/test/integrations/mattermost_push_host_integration_test.dart:91`) but not app bootstrap separation.
### 심볼 참조
- renamed/removed symbol candidate: none required.
- `bootstrapAltClient` references: `apps/client/lib/src/app/bootstrap.dart:25`, `apps/client/lib/src/app/bootstrap.dart:33`.
- `runAltClient` references: `apps/client/lib/main.dart:3`, `apps/client/lib/src/app/bootstrap.dart:31`.
- `MattermostPushHostIntegration` app references: `apps/client/lib/src/app/bootstrap.dart:6`, `apps/client/lib/src/app/app.dart:6`.
### 분할 판단
- split decision policy를 먼저 평가했다.
- shared task group: `m-operator-runtime-refactor`.
- this subtask: `03_client_bootstrap`, no predecessors.
- It is independent because it touches Flutter bootstrap/test boundaries only. It must not wait for worker/API runtime plans and must not implement push notification behavior itself.
### 범위 결정 근거
- Do not change dashboard layout, navigation depth, chart/form wireframe, or production operator screens.
- Do not edit generated Dart protobuf files.
- Do not remove Mattermost/Firebase integration packages; only make them optional at app bootstrap.
- Do not implement push notification behavior. Push-specific integration remains in the later push Milestone.
### 빌드 등급
- build lane: `cloud-G07`; review lane: `cloud-G07`.
- Flutter bootstrap touches platform/Firebase startup and optional external integration boundaries; tests are runnable but runtime smoke may be environment-gated.
## 구현 체크리스트
- [ ] [CLIENT-1] core operator shell bootstrap을 Firebase/Mattermost push initialization과 분리한다.
- [ ] [CLIENT-2] bootstrap tests를 추가해 기본 `runAltClient` path가 push/Firebase 없이 app shell을 시작할 수 있음을 검증한다.
- [ ] [CLIENT-3] push-specific initialization은 explicit opt-in API로 남기고 후속 push Milestone 범위임을 코드 경계로 드러낸다.
- [ ] 원격 ALT checkout root에서 `cd apps/client && flutter test`를 실행하고 실제 stdout/stderr를 review stub에 기록한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
### [CLIENT-1] Core Shell Bootstrap
#### 문제
`apps/client/lib/src/app/bootstrap.dart:25` calls `Firebase.initializeApp()` and `_mattermostHost.initialize()` before `runApp`. This makes optional push readiness a prerequisite for the operator shell, contrary to the client domain rule and current Milestone.
#### 해결 방법
Split bootstrap into core shell setup and optional push setup. Keep `applyFullscreenMode` in the core path. Return or pass a nullable `MattermostPushHostIntegration` only when push is explicitly enabled.
Before:
```dart
// apps/client/lib/src/app/bootstrap.dart:25
Future<void> bootstrapAltClient() async {
await applyFullscreenMode();
await Firebase.initializeApp();
await _mattermostHost.initialize();
}
```
After:
```dart
class AltClientBootstrapOptions {
const AltClientBootstrapOptions({this.enablePush = false});
final bool enablePush;
}
Future<MattermostPushHostIntegration?> bootstrapAltClient({
AltClientBootstrapOptions options = const AltClientBootstrapOptions(),
}) async {
await applyFullscreenMode();
if (!options.enablePush) return null;
await Firebase.initializeApp();
await _mattermostHost.initialize();
return _mattermostHost;
}
```
Allow injectable functions or host factory if needed for tests; do not call Firebase in the default test path.
#### 수정 파일 및 체크리스트
- [ ] `apps/client/lib/src/app/bootstrap.dart`: introduce bootstrap options/result and default push-off core bootstrap.
- [ ] `apps/client/lib/main.dart`: keep `main` simple; default `runAltClient()` should render the core shell.
- [ ] `apps/client/lib/src/app/app.dart`: no change expected unless constructor docs or nullable host behavior needs tightening.
#### 테스트 작성
Add `apps/client/test/app/bootstrap_test.dart` or equivalent:
- default `bootstrapAltClient` does not call injected Firebase/push functions.
- opt-in push path calls injected Firebase and host initialize once.
#### 중간 검증
Run from the remote ALT checkout root:
```bash
cd apps/client && flutter test test/app/bootstrap_test.dart
```
Expected: exit code 0.
### [CLIENT-2] App Shell Starts Without Push
#### 문제
`apps/client/test/widget_test.dart:19` instantiates `AltClientApp` directly, so it does not protect the `runAltClient` bootstrap path.
#### 해결 방법
Make `runAltClient` accept injectable `runApp`/bootstrap hooks or a testable `buildAltClientApp` helper. Keep tests simple and avoid launching platform Firebase.
Before:
```dart
// apps/client/lib/src/app/bootstrap.dart:31
Future<void> runAltClient() async {
WidgetsFlutterBinding.ensureInitialized();
await bootstrapAltClient();
runApp(ProviderScope(child: AltClientApp(mattermostHost: _mattermostHost)));
}
```
After:
```dart
Future<void> runAltClient({
AltClientBootstrapOptions options = const AltClientBootstrapOptions(),
void Function(Widget app)? runAppOverride,
}) async {
WidgetsFlutterBinding.ensureInitialized();
final mattermostHost = await bootstrapAltClient(options: options);
(runAppOverride ?? runApp)(
ProviderScope(child: AltClientApp(mattermostHost: mattermostHost)),
);
}
```
If `runAppOverride` creates awkward production API, prefer a private testable helper that returns the root widget after bootstrap.
#### 수정 파일 및 체크리스트
- [ ] `apps/client/lib/src/app/bootstrap.dart`: make default run path pass `mattermostHost: null`.
- [ ] `apps/client/test/widget_test.dart`: keep direct shell tests passing.
- [ ] `apps/client/test/app/bootstrap_test.dart`: assert default run path builds `AltClientApp` without a Mattermost host.
#### 테스트 작성
Required. Use injected callbacks/fakes so no Firebase platform initialization is needed.
#### 중간 검증
Run from the remote ALT checkout root:
```bash
cd apps/client && flutter test test/widget_test.dart test/app/bootstrap_test.dart
```
Expected: exit code 0.
### [CLIENT-3] Push Opt-In Boundary
#### 문제
`apps/client/lib/src/app/bootstrap.dart:10` creates a package-level `_mattermostHost` and immediately uses it during default bootstrap. This hides the optional boundary and invites later push work to leak into the operator shell.
#### 해결 방법
Keep the host factory private but only instantiate or initialize it when push is explicitly enabled. Existing `MattermostPushHostIntegration` tests remain the behavior owner for push-specific logic.
Before:
```dart
// apps/client/lib/src/app/bootstrap.dart:10
final _mattermostHost = MattermostPushHostIntegration(
pushClient: MattermostPushPluginClient(),
);
```
After:
```dart
MattermostPushHostIntegration createMattermostPushHost() {
return MattermostPushHostIntegration(
pushClient: MattermostPushPluginClient(),
);
}
```
Use the factory only in opt-in push bootstrap. If a singleton is retained, ensure it is not initialized on the default path.
#### 수정 파일 및 체크리스트
- [ ] `apps/client/lib/src/app/bootstrap.dart`: isolate push host creation/initialization behind explicit opt-in.
- [ ] `apps/client/test/integrations/mattermost_push_host_integration_test.dart`: no behavior change expected; run to confirm.
- [ ] Do not edit `apps/client/lib/src/integrations/mattermost/*` unless a compile error requires a narrow import/API update.
#### 테스트 작성
No new Mattermost behavior test is needed beyond existing host tests, but run them because imports/factory usage may change.
#### 중간 검증
Run from the remote ALT checkout root:
```bash
cd apps/client && flutter test test/integrations/mattermost_push_host_integration_test.dart
```
Expected: exit code 0.
## 수정 파일 요약
| 파일 | 항목 |
|------|------|
| `apps/client/lib/src/app/bootstrap.dart` | CLIENT-1, CLIENT-2, CLIENT-3 |
| `apps/client/lib/main.dart` | CLIENT-2 |
| `apps/client/lib/src/app/app.dart` | CLIENT-1 if nullable host docs/types need tightening |
| `apps/client/test/app/bootstrap_test.dart` | CLIENT-1, CLIENT-2 |
| `apps/client/test/widget_test.dart` | CLIENT-2 |
| `apps/client/test/integrations/mattermost_push_host_integration_test.dart` | CLIENT-3 verification only |
## 최종 검증
Run from the remote ALT checkout root:
```bash
cd apps/client && flutter test
```
Expected: exit code 0. Flutter test cache is not accepted as evidence; record actual stdout/stderr.
If runtime socket smoke is attempted, use the client profile rules and record actual skip/blocker conditions instead of treating skipped integration as pass.
모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.

View file

@ -16,7 +16,7 @@ type sessionHandler struct {
// through the same registry instead of bespoke registration paths.
func sessionHandlers(deps Deps) []sessionHandler {
handlers := []sessionHandler{
helloHandler(),
helloHandler(deps),
}
handlers = append(handlers, marketHandlers(deps)...)
handlers = append(handlers, backtestHandlers(deps)...)
@ -41,16 +41,21 @@ func registerHandlers(client *protoSocket.WsClient, handlers []sessionHandler) {
}
}
func helloHandler() sessionHandler {
func helloHandler(deps Deps) sessionHandler {
return sessionHandler{
requestType: protoSocket.TypeNameOf(&altv1.HelloRequest{}),
register: func(client *protoSocket.WsClient) {
protoSocket.AddRequestListenerTyped[*altv1.HelloRequest, *altv1.HelloResponse](&client.Communicator, handleHello)
protoSocket.AddRequestListenerTyped[*altv1.HelloRequest, *altv1.HelloResponse](
&client.Communicator,
func(req *altv1.HelloRequest) (*altv1.HelloResponse, error) {
return handleHello(deps, req)
},
)
},
}
}
func handleHello(req *altv1.HelloRequest) (*altv1.HelloResponse, error) {
func handleHello(deps Deps, req *altv1.HelloRequest) (*altv1.HelloResponse, error) {
protocolVersion := req.GetAltProtocolVersion()
if protocolVersion == "" {
protocolVersion = defaultAltProtocolVersion
@ -59,10 +64,20 @@ func handleHello(req *altv1.HelloRequest) (*altv1.HelloResponse, error) {
ServerName: serverName,
ServerVersion: serverVersion,
AltProtocolVersion: protocolVersion,
Capabilities: []string{
"hello",
"market-read",
"worker-execution",
},
Capabilities: capabilitiesForDeps(deps),
}, nil
}
func capabilitiesForDeps(deps Deps) []string {
caps := []string{"hello"}
if deps.Instruments != nil && deps.Bars != nil {
caps = append(caps, "market-read")
}
if deps.Analysis != nil && deps.Results != nil {
caps = append(caps, "backtest-read")
}
if deps.Starter != nil {
caps = append(caps, "backtest-start", "worker-execution")
}
return caps
}

View file

@ -62,6 +62,13 @@ func TestWorkerSocketServerHello(t *testing.T) {
if res.AltProtocolVersion != "alt.v1" {
t.Errorf("expected AltProtocolVersion to be %q, got %q", "alt.v1", res.AltProtocolVersion)
}
// Assert no backtest-start or worker-execution since BacktestDeps{} has no Starter.
for _, cap := range res.Capabilities {
if cap == "backtest-start" || cap == "worker-execution" {
t.Errorf("expected capability %q to be omitted when Starter is nil", cap)
}
}
}
func TestSessionHandlers(t *testing.T) {
@ -104,3 +111,74 @@ func TestRegisterHandlers(t *testing.T) {
registerHandlers(nil, handlers)
})
}
func TestWorkerHelloCapabilitiesReflectDeps(t *testing.T) {
tests := []struct {
name string
deps Deps
expected []string
}{
{
name: "no deps",
deps: Deps{},
expected: []string{"hello"},
},
{
name: "market deps",
deps: Deps{
Instruments: &fakeInstrumentStore{},
Bars: &fakeBarStore{},
},
expected: []string{"hello", "market-read"},
},
{
name: "backtest read deps",
deps: Deps{
Analysis: &fakeAnalysisStore{},
Results: &fakeResultStore{},
},
expected: []string{"hello", "backtest-read"},
},
{
name: "starter deps",
deps: Deps{
Starter: &fakeStarter{},
},
expected: []string{"hello", "backtest-start", "worker-execution"},
},
{
name: "all deps",
deps: Deps{
Instruments: &fakeInstrumentStore{},
Bars: &fakeBarStore{},
Analysis: &fakeAnalysisStore{},
Results: &fakeResultStore{},
Starter: &fakeStarter{},
},
expected: []string{"hello", "market-read", "backtest-read", "backtest-start", "worker-execution"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
caps := capabilitiesForDeps(tt.deps)
if len(caps) != len(tt.expected) {
t.Fatalf("expected caps %v, got %v", tt.expected, caps)
}
for i, v := range tt.expected {
if caps[i] != v {
t.Errorf("expected cap at %d to be %q, got %q", i, v, caps[i])
}
}
})
}
}
func TestWorkerCapabilitiesDoNotClaimUnavailableStart(t *testing.T) {
caps := capabilitiesForDeps(Deps{})
for _, cap := range caps {
if cap == "backtest-start" || cap == "worker-execution" {
t.Errorf("capabilities for empty Deps{} should not claim unavailable start, but got %q", cap)
}
}
}