<!-- task=m-backtest-engine-baseline/01_worker_lifecycle plan=1 tag=REVIEW_API -->

# Backtest Worker Lifecycle Follow-up Plan

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

이 plan은 이전 리뷰의 Required 이슈만 좁게 해결한다. 새 기능 범위를 넓히지 말고, 기존 worker lifecycle skeleton 안에서 run lookup semantics, pending lifecycle 기록, built-in override 검증을 정리한다. 구현 후 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 실제 변경 내용과 검증 출력으로 채운 뒤 active 파일을 그대로 두고 리뷰 준비를 보고한다.

## 배경

이전 구현은 `run_backtest` handler와 executor skeleton을 추가했지만, `BacktestRunStore.GetRun`의 모든 error를 run 없음으로 취급해 `pending` 상태를 건너뛰고 곧바로 `running`을 기록했다. 또한 tests가 빈 runner에 handler만 등록해서 built-in placeholder override 경로를 검증하지 못했다.

## 구현 체크리스트

- [ ] [REVIEW_API-1] `BacktestRunStore.GetRun` not-found 계약과 pending lifecycle을 분리한다.
- [ ] [REVIEW_API-2] `run_backtest` live handler가 built-in placeholder를 override하는 경로를 테스트한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [REVIEW_API-1] Not-found contract and pending lifecycle

#### 문제

`services/worker/internal/jobs/backtest_jobs.go`가 `store.GetRun`의 모든 error를 새 run 생성으로 처리한다. 이 때문에 실제 storage 조회 실패와 not-found가 구분되지 않고, 새 run의 첫 persisted state가 `pending`이 아니라 `running`이 된다.

#### 해결 방법

`services/worker/internal/storage/ports.go`에 `ErrRunNotFound` sentinel을 추가한다. `services/worker/internal/storage/postgres/store.go`는 `pgx.ErrNoRows`만 이 sentinel로 변환하고, 다른 error는 그대로 wrapping해 반환한다.

`RegisterRunBacktestHandler`는 다음 흐름을 따른다.

1. `GetRun`이 성공하면 기존 `CreatedAt`을 보존하고 spec/status/updated_at만 `running`으로 갱신한다.
2. `GetRun`이 `storage.ErrRunNotFound`이면 새 run을 먼저 `pending`으로 `UpsertRun`한 뒤 같은 run을 `running`으로 전환한다.
3. 그 외 `GetRun` error는 executor 실행과 `UpsertRun` 없이 반환한다.
4. executor 성공/실패 후 기존처럼 `succeeded` 또는 `failed`를 기록하되 원 executor error를 보존한다.

#### 수정 파일

- `services/worker/internal/storage/ports.go`
- `services/worker/internal/storage/postgres/store.go`
- `services/worker/internal/jobs/backtest_jobs.go`
- `services/worker/internal/jobs/backtest_jobs_test.go`

#### 테스트 작성

- 새 run success: `pending -> running -> succeeded` upsert 순서와 executor 호출을 검증한다.
- executor failure: `pending -> running -> failed` upsert 순서와 원 error 반환을 검증한다.
- `GetRun`이 `storage.ErrRunNotFound`가 아닌 error를 반환하면 executor가 호출되지 않고 upsert도 발생하지 않음을 검증한다.

#### 중간 검증

```bash
go test -count=1 ./services/worker/internal/jobs ./services/worker/internal/storage/postgres
```

기대 결과: exit code 0.

### [REVIEW_API-2] Built-in placeholder override coverage

#### 문제

`run_backtest` tests가 빈 runner에 live handler만 등록한다. 따라서 `RegisterBuiltins`가 등록한 `KindRunBacktest` placeholder를 live lifecycle handler가 실제로 override하는지 검증하지 못한다.

#### 해결 방법

`services/worker/internal/jobs/backtest_jobs_test.go`에 daily import test와 같은 형태의 override test를 추가한다.

1. `runner := NewRunner()`
2. `RegisterBuiltins(runner)`
3. `RegisterRunBacktestHandler(runner, store, executor, now)`
4. `runner.Len()`이 `3`으로 유지되는지 확인한다.
5. valid `KindRunBacktest` job 실행 시 placeholder no-op이 아니라 executor/store lifecycle이 호출되는지 확인한다.

#### 수정 파일

- `services/worker/internal/jobs/backtest_jobs_test.go`

#### 테스트 작성

- `TestRegisterRunBacktestHandlerOverridesBuiltinPlaceholder`

#### 중간 검증

```bash
go test -count=1 ./services/worker/internal/jobs
```

기대 결과: exit code 0.

## 리뷰어를 위한 체크포인트

- `GetRun`의 not-found와 실제 storage error가 구분되는지 확인한다.
- 새 run일 때 `pending` 상태가 실제로 persisted된 뒤 `running`과 final state로 전환되는지 확인한다.
- built-in placeholder override test가 `RegisterBuiltins` 이후 live handler 등록을 검증하는지 확인한다.
- result persistence, API/client 조회 연결, fixture full-cycle 작업이 이 follow-up에 섞이지 않았는지 확인한다.

## 최종 검증

```bash
go test -count=1 ./services/worker/...
```

기대 결과: exit code 0.

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