<!-- task=m-agent-readable-repository-refactor/03+02_openai_provider_tools plan=1 tag=REVIEW_TEST -->

# OpenAI 테스트 double 책임 분리와 dispatch 배치 보정

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

> 구현 후 검증을 실행하고 CODE_REVIEW 파일의 구현 에이전트 소유 섹션에 실제 메모와 출력을 채운 뒤 active 파일을 유지한 채 리뷰 준비 완료를 보고한다. 종결 판정, log rename, complete.log, archive 이동은 code-review 전용이다. 선택된 Milestone의 미해결 `구현 잠금 > 결정 필요` 항목이 실구현을 차단할 때만 CODE_REVIEW의 `사용자 리뷰 요청`에 연결 근거를 기록하고 중단한다. 구현 중 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하거나 `USER_REVIEW.md`/`complete.log`를 만들지 않는다. 환경·secret·service, 일반 범위 조정, 후속 에이전트가 보완할 수 있는 증거 공백은 사용자 리뷰 사유가 아니다.

## 배경

첫 리뷰에서 226개 테스트 본문, fresh package test, LOC gate는 통과했다. 그러나 Roadmap `openai-tests`가 요구한 feature-local/function-field test double 축소가 완료되지 않았고, 일반 Chat validation 테스트 하나가 provider dispatch 파일에 배치되었다. 이 후속은 두 구조적 Required만 보정하고 production 동작과 테스트 assertion은 유지한다.

## 사용자 리뷰 요청 흐름

사용자 리뷰 요청은 선택된 Milestone의 구현 잠금 결정만 active review stub의 `사용자 리뷰 요청` 섹션에 기록한다. 섹션 형식은 `agent-ops/skills/common/_templates/implementation-user-review-request-section.md`를 따른다. 구현 중 직접 사용자 prompt는 금지하며, 요청의 타당성 검증과 `USER_REVIEW.md` 작성은 code-review가 소유한다.

## Roadmap Targets

- Milestone: `agent-roadmap/phase/automation-runtime-bridge/milestones/agent-readable-repository-refactor.md`
- Milestone link: [Milestone 문서](agent-roadmap/phase/automation-runtime-bridge/milestones/agent-readable-repository-refactor.md)
- Task ids:
  - `openai-tests`: OpenAI 서버 테스트를 기능 시나리오별 파일과 feature-local double로 분해
- Completion mode: check-on-pass

## Archive Evidence Snapshot

- Prior task: `agent-task/m-agent-readable-repository-refactor/03+02_openai_provider_tools`
- Archived plan: `agent-task/m-agent-readable-repository-refactor/03+02_openai_provider_tools/plan_local_G08_0.log`
- Archived review: `agent-task/m-agent-readable-repository-refactor/03+02_openai_provider_tools/code_review_local_G08_0.log`
- Verdict: FAIL
- Findings: Required 2, Suggested 0, Nit 0
- Required summary:
  - `server_test_support_test.go:16`: package-wide `fakeRunService`가 provider tunnel/pool과 normalized/cancel/Ollama 상태를 함께 소유해 Roadmap test-double 축소 조건을 충족하지 못함.
  - `provider_dispatch_test.go:425`: 일반 Chat unsupported-field validation 테스트가 provider dispatch 책임 파일에 배치됨.
- Affected files: `apps/edge/internal/openai/server_test_support_test.go`, 신규 `apps/edge/internal/openai/provider_test_support_test.go`, provider 시나리오 테스트 7개, `identity_metering_test.go`, `usage_metrics_test.go`, `chat_handler_test.go`.
- Verification evidence: HEAD/current `Test*` 226개 이름·본문 동일, `go test -count=1 ./apps/edge/internal/openai` PASS, LOC >1000 목록 `[]`, gofmt/`git diff --check` PASS.
- Roadmap carryover: `openai-tests` check-on-pass를 유지한다. SDD는 불필요다.
- Narrow reread allowed: 위 `plan_local_G08_0.log`, `code_review_local_G08_0.log`, predecessor `agent-task/archive/2026/07/m-agent-readable-repository-refactor/02+01_openai_responses_workspace/complete.log`만 필요할 때 재확인한다.

## 분석 결과

### 읽은 파일

- `agent-roadmap/phase/automation-runtime-bridge/milestones/agent-readable-repository-refactor.md`
- `agent-spec/input/openai-compatible-surface.md`
- `agent-contract/outer/openai-compatible-api.md`
- `agent-contract/inner/edge-node-runtime-wire.md`
- `agent-test/local/rules.md`
- `agent-test/local/edge-smoke.md`
- `agent-test/local/testing-smoke.md`
- `apps/edge/internal/openai/server_test_support_test.go`
- `apps/edge/internal/openai/chat_handler_test.go`
- `apps/edge/internal/openai/chat_tool_synthesis_test.go`
- `apps/edge/internal/openai/provider_dispatch_test.go`
- `apps/edge/internal/openai/provider_tunnel_test.go`
- `apps/edge/internal/openai/provider_tunnel_auth_test.go`
- `apps/edge/internal/openai/provider_policy_test.go`
- `apps/edge/internal/openai/provider_tool_validation_test.go`
- `apps/edge/internal/openai/provider_observability_test.go`
- `apps/edge/internal/openai/provider_selection_test.go`
- `apps/edge/internal/openai/identity_metering_test.go`
- `apps/edge/internal/openai/usage_metrics_test.go`
- `agent-task/m-agent-readable-repository-refactor/03+02_openai_provider_tools/plan_local_G08_0.log`
- `agent-task/m-agent-readable-repository-refactor/03+02_openai_provider_tools/code_review_local_G08_0.log`
- `agent-task/archive/2026/07/m-agent-readable-repository-refactor/02+01_openai_responses_workspace/complete.log`

### SDD 기준

선택 Milestone은 `SDD: 불필요`다. 외부 API/proto/config schema와 runtime 책임 경계를 바꾸지 않는 동작 보존형 테스트 재배치라는 기록 사유를 계속 적용한다.

### 테스트 환경 규칙

`test_env=local`이며 `agent-test/local/rules.md`, `edge-smoke.md`, `testing-smoke.md`를 읽었다. 동작 변경 없는 테스트 harness 리팩터링이므로 fresh package test, Test 226개, 테스트 파일 1,000줄 gate, gofmt, `git diff --check`를 필수 검증으로 삼는다. 실행 경로를 바꾸지 않으므로 repo 내부 edge-node 진단, 보조 E2E smoke, full-cycle 실제 구동은 비적용이다. Go test cache는 허용하지 않고 `-count=1`을 사용한다.

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

- production 동작 변경은 없다.
- test double 분리는 기존 226개 테스트의 컴파일·assertion·fresh 실행으로 검증한다.
- 테스트 위치 이동은 유일 함수 위치와 Test 개수로 누락/중복을 검증한다.
- 추가 동작 테스트 공백은 없다.

### 심볼 참조

- `fakeRunService` provider control 필드 참조는 `provider_dispatch_test.go`, `provider_tunnel_test.go`, `provider_tunnel_auth_test.go`, `provider_policy_test.go`, `provider_tool_validation_test.go`, `provider_observability_test.go`, `provider_selection_test.go`, `identity_metering_test.go`, `usage_metrics_test.go`에 있다.
- `TestChatCompletionsRejectsUnsupportedFields`는 `provider_dispatch_test.go:425`에 한 번만 존재한다.
- production symbol rename/remove는 없다.

### 분할 판단

공유 task group은 `agent-task/m-agent-readable-repository-refactor`이고 현 follow-up은 기존 split `03+02_openai_provider_tools`에 남는다. predecessor `02+01_openai_responses_workspace`는 `agent-task/archive/2026/07/m-agent-readable-repository-refactor/02+01_openai_responses_workspace/complete.log`로 충족되었다. Required 2건은 동일 package의 테스트 topology와 공용 fixture 변경을 하나의 226개 회귀로 판정해야 하므로 후속 plan을 다시 분할하지 않는다.

### 범위 결정 근거

`apps/edge/internal/openai` 테스트 harness와 파일 배치만 바꾼다. production OpenAI 구현, API/wire/config 계약, agent-spec/contract evidence pointer는 수정하지 않는다. 삭제된 `server_test.go` 포인터 동기화는 별도 Roadmap `evidence-links` Task가 소유한다.

### 빌드 등급

`local-G09`: 한 Go package에 한정되고 결과가 226개 fresh test로 결정적이지만, 9개 테스트 파일의 provider fixture 생성과 642줄 공용 double 구조를 함께 재편한다.

## 구현 체크리스트

- [ ] [REVIEW_TEST-1] normalized 기본 fake와 provider tunnel/pool 전용 fake를 분리하고 기존 226개 테스트·assertion·LOC gate·fresh package test를 보존한다.
- [ ] [REVIEW_TEST-2] `TestChatCompletionsRejectsUnsupportedFields`를 본문 변경 없이 `chat_handler_test.go`로 옮기고 provider dispatch 파일의 책임을 복구한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [REVIEW_TEST-1] provider 전용 test double 분리

- 문제: `apps/edge/internal/openai/server_test_support_test.go:16`의 `fakeRunService`가 normalized run, provider tunnel/pool, cancel, Ollama fixture 상태를 모두 소유한다. `SubmitProviderPool`/`buildPoolResult`는 동일 파일 `161~447`행에 남아 Roadmap이 요구한 feature-local/function-field double 축소를 충족하지 못한다.
- 해결 방법:

~~~text
Before (server_test_support_test.go:16)
fakeRunService
  - normalized SubmitRun/cancel/Ollama state
  - provider tunnel state and relay
  - provider-pool selection/result state

After
server_test_support_test.go
  - fakeRunService: normalized SubmitRun/cancel/Ollama의 작은 기본 double
  - provider method은 오설정을 즉시 실패시키는 최소 default만 유지
provider_test_support_test.go
  - providerFakeRunService: fakeRunService를 embed
  - tunnel/pool state, handles, relay, snapshots와 SubmitProviderTunnel/SubmitProviderPool 구현 소유
~~~

- 수정 파일 및 체크리스트:
  - [ ] `apps/edge/internal/openai/server_test_support_test.go`: normalized/cancel/Ollama 기본 double만 남기고 provider-specific state/구현을 제거한다.
  - [ ] `apps/edge/internal/openai/provider_test_support_test.go`: `providerFakeRunService`, tunnel/pool handle/result/relay/snapshot helper를 두고 기본 double을 embed한다.
  - [ ] `apps/edge/internal/openai/provider_dispatch_test.go`, `provider_tunnel_test.go`, `provider_tunnel_auth_test.go`, `provider_policy_test.go`, `provider_tool_validation_test.go`, `provider_observability_test.go`, `provider_selection_test.go`: provider control field를 쓰는 fixture만 provider 전용 double로 전환하고 테스트 이름/assertion은 바꾸지 않는다.
  - [ ] `apps/edge/internal/openai/identity_metering_test.go`, `usage_metrics_test.go`: provider tunnel metric/identity fixture만 provider 전용 double로 전환하고 normalized fixture는 기본 double을 유지한다.
- 테스트 작성: 추가 동작 테스트는 작성하지 않는다. 이는 internal test harness 리팩터링이며 기존 226개 시나리오가 모든 interface 경로와 assertion을 커버한다.
- 중간 검증:

~~~bash
gofmt -w apps/edge/internal/openai/*_test.go
rg --sort path -n '^type (fakeRunService|providerFakeRunService) struct' apps/edge/internal/openai/*_test.go
python3 - <<'PY'
from pathlib import Path
s = Path('apps/edge/internal/openai/server_test_support_test.go').read_text()
block = s.split('type fakeRunService struct {', 1)[1].split('\n}', 1)[0]
bad = [name for name in ('tunnel', 'pool') if name in block]
print(bad)
raise SystemExit(bool(bad))
PY
GOCACHE=/tmp/iop-go-build-cache go test -count=1 ./apps/edge/internal/openai
~~~

기대 결과: 기본 fake struct에 provider tunnel/pool control field가 없고, provider 전용 double이 별도 파일에 존재하며 fresh package test가 통과한다.

### [REVIEW_TEST-2] Chat unsupported-field validation 책임 배치 복구

- 문제: `apps/edge/internal/openai/provider_dispatch_test.go:425`의 `TestChatCompletionsRejectsUnsupportedFields`는 provider catalog/pool dispatch를 사용하지 않고 normalized Ollama Chat request validation만 검증한다.
- 해결 방법:

~~~text
Before (provider_dispatch_test.go:425)
TestChatCompletionsRejectsUnsupportedFields

After
chat_handler_test.go
  TestChatCompletionsRejectsUnsupportedFields
provider_dispatch_test.go
  provider catalog/pool dispatch 시나리오만 유지
~~~

- 수정 파일 및 체크리스트:
  - [ ] `apps/edge/internal/openai/provider_dispatch_test.go`: 대상 함수를 제거하고 import를 정리한다.
  - [ ] `apps/edge/internal/openai/chat_handler_test.go`: 대상 함수 본문과 assertion을 그대로 추가하고 import를 정리한다.
- 테스트 작성: 새 테스트는 작성하지 않고 기존 테스트를 이동한다. Test 이름, subtest case, body, assertion을 보존한다.
- 중간 검증:

~~~bash
test "$(rg -l --sort path '^func TestChatCompletionsRejectsUnsupportedFields' apps/edge/internal/openai/*_test.go)" = "apps/edge/internal/openai/chat_handler_test.go"
test -z "$(gofmt -l apps/edge/internal/openai/chat_handler_test.go apps/edge/internal/openai/provider_dispatch_test.go)"
~~~

기대 결과: 대상 Test가 `chat_handler_test.go`에만 존재하고 두 파일에 gofmt 차이가 없다.

## 의존 관계 및 구현 순서

1. predecessor `02+01_openai_responses_workspace`는 `agent-task/archive/2026/07/m-agent-readable-repository-refactor/02+01_openai_responses_workspace/complete.log`로 충족되었다.
2. provider test double을 분리한 뒤 Chat validation 테스트 위치를 보정하고 최종 회귀를 실행한다.

## 수정 파일 요약

| 파일 | 항목 |
|---|---|
| `apps/edge/internal/openai/server_test_support_test.go` | REVIEW_TEST-1 |
| `apps/edge/internal/openai/provider_test_support_test.go` | REVIEW_TEST-1 |
| `apps/edge/internal/openai/provider_dispatch_test.go` | REVIEW_TEST-1, REVIEW_TEST-2 |
| `apps/edge/internal/openai/provider_tunnel_test.go` | REVIEW_TEST-1 |
| `apps/edge/internal/openai/provider_tunnel_auth_test.go` | REVIEW_TEST-1 |
| `apps/edge/internal/openai/provider_policy_test.go` | REVIEW_TEST-1 |
| `apps/edge/internal/openai/provider_tool_validation_test.go` | REVIEW_TEST-1 |
| `apps/edge/internal/openai/provider_observability_test.go` | REVIEW_TEST-1 |
| `apps/edge/internal/openai/provider_selection_test.go` | REVIEW_TEST-1 |
| `apps/edge/internal/openai/identity_metering_test.go` | REVIEW_TEST-1 |
| `apps/edge/internal/openai/usage_metrics_test.go` | REVIEW_TEST-1 |
| `apps/edge/internal/openai/chat_handler_test.go` | REVIEW_TEST-2 |

## 최종 검증

~~~bash
gofmt -w apps/edge/internal/openai/*_test.go
test ! -e apps/edge/internal/openai/server_test.go
test "$(rg -l --sort path '^func TestChatCompletionsRejectsUnsupportedFields' apps/edge/internal/openai/*_test.go)" = "apps/edge/internal/openai/chat_handler_test.go"
python3 - <<'PY'
from pathlib import Path
s = Path('apps/edge/internal/openai/server_test_support_test.go').read_text()
block = s.split('type fakeRunService struct {', 1)[1].split('\n}', 1)[0]
bad = [name for name in ('tunnel', 'pool') if name in block]
print(bad)
raise SystemExit(bool(bad))
PY
GOCACHE=/tmp/iop-go-build-cache go test -count=1 ./apps/edge/internal/openai
[ "$(GOCACHE=/tmp/iop-go-build-cache go test -list '^Test' ./apps/edge/internal/openai | sed '/^ok[[:space:]]/d' | wc -l)" -eq 226 ]
python3 -c "from pathlib import Path; p=Path('apps/edge/internal/openai'); bad=sorted((sum(1 for _ in f.open()),str(f)) for f in p.glob('*_test.go') if sum(1 for _ in f.open())>1000); print(bad); raise SystemExit(bool(bad))"
git diff --check
~~~

기대 결과: provider 전용 test double 분리, 일반 Chat validation 책임 배치 복구, fresh package test PASS, Test 226개, 모든 OpenAI test 파일 1,000줄 이하, whitespace 오류 없음.

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