<!-- 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`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.
