<!-- task=m-lemonade-provider-serving-validation/03+02_field_smoke plan=0 tag=TEST -->

# Plan - TEST

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

구현 완료 전 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 실제 구현 내용, 설계 결정, 검증 출력으로 반드시 채운다. 구현 중 사용자 결정, 사용자 소유 외부 환경, secret, 서비스 준비, 또는 범위 충돌 없이는 안전하게 진행할 수 없으면 review stub의 `사용자 리뷰 요청` 섹션에 정확한 증거를 남기고 멈춘다. 구현 에이전트는 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 검증 공백은 사용자 리뷰 요청이 아니다. 최종 판정, archive, `complete.log` 작성은 code-review-skill 전용이다.

## 배경

Lemonade provider의 최종 기준은 Edge OpenAI-compatible surface에서 모델 조회, non-streaming chat, streaming chat이 provider 경로로 수렴하는지 확인하는 것이다. 현재 repo에는 Ollama 전용 OpenAI e2e script와 generic `iop-edge smoke openai`만 있고, Lemonade/OpenAI-compatible provider 경로를 선택해 검증하는 field smoke가 없다. 이 plan은 선행 adapter 구현을 전제로 fake mode와 real mode를 모두 지원하는 smoke를 추가한다.

## 사용자 리뷰 요청 흐름

구현 중 차단이 생기면 active review stub의 `사용자 리뷰 요청` 섹션에 `agent-ops/skills/common/_templates/implementation-user-review-request-section.md` 형식으로 기록한다. 구현 에이전트가 채팅으로 직접 사용자에게 묻는 것은 금지되며, code-review가 요청의 타당성을 검증하고 실제 `USER_REVIEW.md` 작성 여부를 결정한다. 실제 Lemonade endpoint/model/auth 값이 준비되지 않아 real mode smoke를 실행할 수 없으면 이 섹션에 기록하고 PASS를 주장하지 않는다.

## Roadmap Targets

- Milestone: `agent-roadmap/phase/inference-provider-extension/milestones/lemonade-provider-serving-validation.md`
- Task ids:
  - `models-chat`: Edge OpenAI-compatible `/v1/models`와 non-streaming `/v1/chat/completions`가 Lemonade provider로 수렴하는 기준이 검증되어 있다.
  - `streaming`: streaming `/v1/chat/completions`에서 SSE chunk, finish reason, 종료 신호가 Lemonade provider 경로로 안정적으로 전달되는지 검증되어 있다.
  - `field-smoke`: split-host field smoke에서 Lemonade target/model을 선택해 모델 조회와 chat 왕복이 검증되어 있다.
  - `follow-up-scope`: 코드 수정이 필요한 항목은 이 Milestone의 추가 Task 또는 같은 Milestone task group의 후속 plan으로 정리되어 있다.
- Completion mode: check-on-pass

## 분석 결과

### 읽은 파일

- `agent-ops/skills/common/router.md`
- `agent-ops/skills/common/plan/SKILL.md`
- `agent-ops/rules/project/domain/node/rules.md`
- `agent-ops/rules/project/domain/edge/rules.md`
- `agent-ops/rules/project/domain/testing/rules.md`
- `agent-test/local/rules.md`
- `agent-test/local/node-smoke.md`
- `agent-test/local/edge-smoke.md`
- `agent-test/local/platform-common-smoke.md`
- `agent-roadmap/phase/inference-provider-extension/milestones/lemonade-provider-serving-validation.md`
- `scripts/e2e-openai-ollama.sh`
- `apps/edge/internal/edgecmd/smoke_openai.go`
- `Makefile`
- `configs/edge.yaml`
- `configs/node.yaml`
- `apps/edge/internal/openai/types.go`
- `apps/edge/internal/openai/chat_handler.go`
- `apps/edge/internal/openai/server_test.go`
- `apps/node/internal/adapters/vllm/vllm.go`
- `apps/node/internal/adapters/vllm/vllm_test.go`
- `apps/node/internal/adapters/adapters_blackbox_test.go`

### 테스트 환경 규칙

- `test_env=local`.
- `agent-test/local/rules.md`, `edge-smoke`, `node-smoke`, `platform-common-smoke`를 읽었다.
- 적용 명령: Edge OpenAI-compatible 경계 변경은 `iop-edge smoke openai` 또는 동등한 `/healthz`, `/v1/models`, `/v1/responses` 확인이 필요하다. 이 plan은 추가로 `/v1/chat/completions` non-streaming/streaming curl 검증을 포함한다.
- field runtime이나 외부 provider evidence는 원격 runner 또는 사용자 소유 host가 필요할 수 있다. 이 plan의 fake mode는 local deterministic check이고, Roadmap `field-smoke` PASS에는 real mode evidence가 필요하다.
- secret, token, 개인 endpoint 원문은 tracked 파일과 최종 보고에 기록하지 않는다.

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

- `scripts/e2e-openai-ollama.sh`는 Ollama fake server와 Ollama adapter만 검증한다. Lemonade/OpenAI-compatible adapter route는 없다.
- `apps/edge/internal/edgecmd/smoke_openai.go`는 `/healthz`, `/v1/models`, `/v1/responses`만 확인한다. `/v1/chat/completions` non-streaming/streaming 검증이 없다.
- real Lemonade endpoint/model/auth/streaming expectation은 아직 마일스톤 잠금에 남아 있으므로, 구현 시 environment가 없으면 사용자 리뷰 요청으로 멈춰야 한다.

### 심볼 참조

- rename/remove 없음.
- 새 script target `test-openai-lemonade` call site는 `Makefile`에 한정한다.

### 분할 판단

- split decision policy를 plan 작성 전에 평가했다.
- 공유 task group: `agent-task/m-lemonade-provider-serving-validation`.
- `01_config_contract`: config/proto contract.
- `02+01_openai_compatible_adapter`: Node adapter. 현재 active/archive `complete.log` 없음.
- `03+02_field_smoke`: 이 plan. predecessor `02`는 아직 만족되지 않았다. 구현 시작 전 같은 task group의 `02_*` 또는 `02+*` active/archive `complete.log`가 있어야 한다.

### 범위 결정 근거

- Lemonade 서버 설치, 모델 다운로드, 운영 제품화는 제외한다.
- `make test-e2e` 기본 흐름에는 real Lemonade dependency를 추가하지 않는다. 새 Makefile target은 명시 실행용으로 둔다.
- secret/header 값은 tracked script, config, docs, roadmap에 쓰지 않는다.
- Smoke 중 발견된 큰 코드 이슈는 이 plan에서 임의 확장하지 않는다. review stub에 증거를 남기고 code-review가 후속 plan을 만들게 한다.

### 빌드 등급

- `cloud-G07`: shell smoke orchestration, process control, real external endpoint evidence와 PASS gate가 핵심이며 terminal workflow 실패 분석이 필요할 수 있다.

## 구현 체크리스트

- [ ] 구현 시작 전 `02+01_openai_compatible_adapter`의 `complete.log`를 확인하고, 선행 adapter type/config 이름을 smoke config에 맞춘다.
- [ ] `scripts/e2e-openai-lemonade.sh`를 추가해 fake mode와 real mode에서 Edge/Node/OpenAI-compatible route를 검증한다.
- [ ] `Makefile`에 명시 실행 target `test-openai-lemonade`를 추가하되 `test-e2e` 기본 dependency에는 넣지 않는다.
- [ ] fake mode smoke, real mode smoke, 관련 Go package tests를 실행한다. real mode 환경값이 없으면 review stub의 `사용자 리뷰 요청`에 기록하고 PASS를 주장하지 않는다.
- [ ] smoke 결과 큰 코드 이슈가 발견되면 이 plan 범위를 넘겨 수정하지 말고 증거를 남겨 후속 plan으로 넘긴다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

## 의존 관계 및 구현 순서

- 디렉터리명 `03+02_field_smoke`의 predecessor는 `02`뿐이다.
- 구현 시작 전 `agent-task/m-lemonade-provider-serving-validation/02+01_openai_compatible_adapter/complete.log` 또는 archive의 matching `02_*`/`02+*` `complete.log`를 확인한다.
- 이 plan 본문은 추가 runtime dependency를 만들지 않는다.

## TEST-1 Lemonade smoke script

### 문제

`scripts/e2e-openai-ollama.sh:31-64`는 fake Ollama model/server만 만들고, Edge config도 `adapter: "ollama"`에 고정되어 있다. Lemonade/OpenAI-compatible provider를 선택해 `/v1/models`와 `/v1/chat/completions`를 검증하는 script가 없다.

```bash
31	MODEL="fake-ollama-model"
32	
33	FAKE_OLLAMA="$TMP_DIR/fake_ollama.go"
34	cat > "$FAKE_OLLAMA" <<'EOF'
...
60		mux.HandleFunc("/api/tags", func(w http.ResponseWriter, _ *http.Request) {
61			w.Header().Set("Content-Type", "application/json")
62			_, _ = w.Write([]byte(`{"models":[{"name":"fake-ollama-model"}]}`))
63		})
64		mux.HandleFunc("/api/chat", func(w http.ResponseWriter, r *http.Request) {
```

```bash
123	openai:
124	  enabled: true
125	  listen: "127.0.0.1:$OPENAI_PORT"
126	  adapter: "ollama"
127	  session_id: "cline"
128	  timeout_sec: 30
129	  strict_output: false
130	  model_routes:
131	    - model: "client-request-model"
132	      adapter: "ollama"
133	      target: "$MODEL"
```

### 해결 방법

- 새 `scripts/e2e-openai-lemonade.sh`를 추가한다.
- 기본 `IOP_LEMONADE_MODE`는 `fake`다. fake mode는 temp Go server가 `/v1/models`와 `/v1/chat/completions` SSE를 제공한다.
- real mode는 `IOP_LEMONADE_MODE=real`일 때 외부 Lemonade server를 사용한다. required env: `IOP_LEMONADE_ENDPOINT`, `IOP_LEMONADE_MODEL`. endpoint는 root 또는 trailing `/v1` 둘 다 허용하되 Node adapter config에는 root 형태로 넣는다.
- optional auth: `IOP_LEMONADE_AUTH_HEADER_NAME` + `IOP_LEMONADE_AUTH_HEADER_VALUE`가 모두 있을 때 generated temp YAML `headers`에만 쓴다. script stdout/stderr에는 header value를 출력하지 않는다.
- generated Edge config는 `openai.model_routes`에 client-facing model `"client-request-model"`을 두고 `adapter: "openai_compat"`, `target: "$MODEL"`, `node: "test-node"`로 보낸다.
- generated node adapter는 선행 contract의 `openai_compat_instances` name `"lemonade-local"`, provider `"lemonade"`, endpoint, capacity/queue timeout을 사용한다.
- 검증 단계:
  - Edge health `/healthz`.
  - Edge `/v1/models`에 client-facing model 노출.
  - Edge `/v1/chat/completions` non-streaming 응답에 sentinel 포함.
  - Edge `/v1/chat/completions` streaming 응답에 SSE content chunk와 `data: [DONE]`.
  - `go run ./apps/edge/cmd/edge smoke openai --model client-request-model ...`로 `/v1/responses` 경로도 확인.
- fake mode sentinel은 `IOP_OPENAI_LEMONADE_OK`로 고정한다. real mode는 empty response가 아니고 model route가 성공하면 통과하되, streaming `[DONE]`은 반드시 확인한다.

### 수정 파일 및 체크리스트

- [ ] `scripts/e2e-openai-lemonade.sh`: 새 smoke script 추가.
- [ ] script 내부 temp config에는 secret value를 출력하지 않는다.
- [ ] script cleanup은 Edge/Node/fake server process와 temp dir를 정리한다.

### 테스트 작성

- shell script 자체는 Go unit test 대신 `bash -n`과 fake mode 실행으로 검증한다.

### 중간 검증

```bash
bash -n scripts/e2e-openai-lemonade.sh
./scripts/e2e-openai-lemonade.sh
```

기대 결과: syntax check PASS, fake mode smoke PASS.

## TEST-2 Makefile target and command contract

### 문제

`Makefile:79-90`은 OpenAI-compatible e2e target으로 Ollama만 제공한다. Lemonade smoke는 명시적으로 실행할 target이 없다.

```makefile
79	test-e2e:
80		@echo "NOTE: test-e2e runs auxiliary smoke (Edge-Node + OpenAI) plus Control Plane-Edge wire smoke; completion still requires user-flow verification when changing runtime paths."
81		./scripts/e2e-smoke.sh
82		./scripts/e2e-openai-ollama.sh
83		./scripts/e2e-control-plane-edge-wire.sh
...
88	test-openai-ollama:
89		./scripts/e2e-openai-ollama.sh
```

### 해결 방법

- `.PHONY`에 `test-openai-lemonade`를 추가한다.
- `test-openai-lemonade` target은 `./scripts/e2e-openai-lemonade.sh`를 실행한다.
- `test-e2e`에는 추가하지 않는다. fake mode는 local check로 충분하지만 real Lemonade dependency를 기본 e2e에 숨기면 field smoke PASS 기준이 흐려진다.

### 수정 파일 및 체크리스트

- [ ] `Makefile`: `.PHONY`와 `test-openai-lemonade` target 추가.

### 테스트 작성

- Makefile target은 script fake mode 실행으로 검증한다.

### 중간 검증

```bash
make test-openai-lemonade
```

기대 결과: fake mode smoke PASS.

## TEST-3 Real Lemonade field gate and follow-up scope

### 문제

`apps/edge/internal/edgecmd/smoke_openai.go:118-124`는 generic OpenAI smoke 목적을 설명하지만 chat completions streaming을 직접 확인하지 않는다. 마일스톤은 실제 Lemonade target/model을 선택한 split-host field smoke와 streaming 안정성 확인을 요구한다.

```go
118		Long: `Validate the IOP Edge OpenAI-compatible HTTP API surface.
119		It runs a three-step diagnostics check:
120	  1. Checks GET /healthz (server status).
121	  2. Checks GET /v1/models (advertised model compatibility).
122	  3. Checks POST /v1/responses (non-streaming Responses API request processing).
123	
124	If --base-url is not provided, it auto-discovers the OpenAI URL from the effective config's listen address.`,
```

### 해결 방법

- `scripts/e2e-openai-lemonade.sh` real mode를 최종 PASS gate로 둔다.
- 구현 에이전트는 real mode를 아래 명령으로 실행한다. 환경값이 준비되어 있지 않으면 실행한 명령과 실패 출력을 `사용자 리뷰 요청`에 남기고 멈춘다.
- real mode가 provider-specific code issue를 찾았고 이 plan의 script/config 소규모 수정으로 해결할 수 없으면 PASS를 주장하지 않는다. review stub의 `계획 대비 변경 사항`과 `검증 결과`에 증거를 남겨 code-review가 후속 plan을 만들게 한다.

### 수정 파일 및 체크리스트

- [ ] `scripts/e2e-openai-lemonade.sh`: real mode validation branch 추가.
- [ ] `CODE_REVIEW-cloud-G07.md`: real mode 실행 결과 또는 사용자 리뷰 요청 근거 기록.

### 테스트 작성

- real Lemonade 환경은 unit test로 대체하지 않는다. Roadmap `field-smoke` PASS에는 real mode command output이 필요하다.

### 중간 검증

```bash
IOP_LEMONADE_MODE=real ./scripts/e2e-openai-lemonade.sh
```

기대 결과: env가 준비된 경우 real Lemonade smoke PASS. env가 없으면 사용자 리뷰 요청에 blocker evidence를 남기고 PASS 금지.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `scripts/e2e-openai-lemonade.sh` | TEST-1, TEST-3 |
| `Makefile` | TEST-2 |
| `CODE_REVIEW-cloud-G07.md` | TEST-3 |

## 최종 검증

```bash
bash -n scripts/e2e-openai-lemonade.sh
./scripts/e2e-openai-lemonade.sh
make test-openai-lemonade
IOP_LEMONADE_MODE=real ./scripts/e2e-openai-lemonade.sh
go test -count=1 ./apps/edge/internal/openai ./apps/edge/internal/edgecmd ./apps/node/internal/adapters/openai_compat ./apps/node/internal/adapters
git diff --check
```

기대 결과: 모든 명령이 exit 0. real mode 환경값이 없거나 endpoint/model/auth 준비가 안 되어 있으면 PASS 금지, `사용자 리뷰 요청` 작성.

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