---
test_env: dev
test_profile: edge-smoke
domain: edge
verification_type: smoke
last_rule_updated_at: 2026-08-13
---

# edge-smoke dev 테스트

## 읽기 조건

- `apps/edge/**` 변경 또는 edge registry, transport, service, console, OpenAI-compatible/A2A 입력 표면 dev 검증 판단이 필요한 경우

## 적용 범위

- `apps/edge/cmd/edge/**`
- `apps/edge/internal/bootstrap/**`
- `apps/edge/internal/transport/**`
- `apps/edge/internal/service/**`
- `apps/edge/internal/events/**`
- `apps/edge/internal/input/**`
- `apps/edge/internal/openai/**`
- `apps/edge/internal/opsconsole/**`
- `apps/edge/internal/node/**`

## 분류

- domain: edge
- verification_type: smoke
- scope: edge 실행 그룹, node registry, input surface baseline

## 환경

- host: local checkout. dev runtime, external CLI, shared port evidence가 필요하면 원격 runner를 사용한다.
- port: compose Edge-Node TCP transport `19003`; dev artifact/bootstrap HTTP 후보 `18082`, dev Edge OpenAI-compatible HTTP 후보 `18083`, dev Edge metrics 후보 `19101`.
- runtime: Go `1.24`
- package manager: Go modules / Makefile
- docker: unit/smoke quick check는 Docker를 요구하지 않는다. compose dev 검증은 `docker compose --env-file .env.dev.example ...`로 수행한다.
- external service: dev artifact/base URL 후보 `http://toki-labs.com:18082`, compose Edge runtime 주소 후보 `toki-labs.com:19003`, dev-runtime provider pool native Edge 주소 후보 `toki-labs.com:18084`
- model endpoint: dev OpenAI-compatible base URL 후보 `http://toki-labs.com:18083/v1`
- credential: token/secret 원문은 문서에 기록하지 않는다.

## dev-runtime provider pool 인벤토리

dev-runtime provider pool과 4-node 연결 상태를 점검할 때는 `agent-test/inventory-dev.yaml`의 machine-readable 값을 우선하고, 원격 runner `ssh toki@toki-labs.com`의 `/Users/toki/agent-work/iop-dev` checkout을 기준으로 한다.

Claude Anthropic-compatible 단일 요청 Agent 실행을 검증할 때는 Claude가 보낸 실제 Edge `/v1/messages` ingress POST 수를 계수한다. PASS 기준은 정확히 1회이며, 같은 endpoint·사용자 요청·Claude 세션 또는 logical request id 하나는 이를 대체하지 않는다. Harness는 supervised child에만 `CLAUDE_CODE_MAX_RETRIES=0`과 `CLAUDE_CODE_DISABLE_TERMINAL_TITLE=1`을 고정해 SDK retry와 별도 session-title Messages 요청을 제거하고 parent/user Claude 설정은 변경하지 않는다. IOP 내부 Plan/Work/Review provider 응답은 선택적 문자열 `message.reasoning_content`를 검증 후 폐기하며 stage 결과, artifact, caller 응답, 관측 로그에 보존하지 않는다. Gemini `extra_content.google.thought_signature`는 Plan/Review의 정확한 비공개 구조에서만 허용한다. terminal text signature는 폐기하고 Review tool-call signature는 같은 요청의 다음 Gemini assistant tool-call message에만 그대로 되돌려 보내며 Work, artifact, 결과, 로그에는 남기지 않는다. plan/work/review를 caller나 외부 test harness가 각각 호출하거나 Claude-facing `tool_use`/tool result continuation으로 이어 간 과거 다중 요청 실험은 protocol bridge와 model/provider 연결 evidence로만 보존하고 단일 요청 acceptance로 재사용하지 않는다. 이 경로의 stage와 workspace tool loop는 IOP Edge와 operator가 승인한 IOP Node가 소유하며 Agent-Ops dispatcher와 Pi를 실행 경로 또는 test harness로 사용하지 않는다. 현재 구현은 `darwin|linux` Node catalog와 exact host matching만 지원하고 Windows는 fail-closed다; 선택된 dev runner의 OS는 실행 evidence일 뿐 caller-visible 기능 selector가 아니다.

- Edge config: `build/dev-runtime/edge.yaml`
- Edge id: `edge-toki-labs-dev`
- Control Plane HTTP: `http://127.0.0.1:18001`
- bootstrap HTTP: `http://toki-labs.com:18082`
- Edge OpenAI-compatible base URL: `http://toki-labs.com:18083/v1`
- Edge-Node TCP transport: `toki-labs.com:18084`
- active model aliases: `laguna-s:2.1`(GX10), `ornith:35b`/`ornith-fast`(OneXPlayer/RTX5090), `qwen3.6:35b`(mac-mlx-vllm)
- host Pi dispatcher profile: `agent-test/inventory-agent.yaml`의 `environments.dev.agents.pi` 기준. 현재 기본 provider/model/thinking level은 `iop` / `glm-5.2` / `high`이고, dispatcher local-model route는 `iop/ornith:35b` / `high`다. 두 모델 모두 IOP Edge `http://toki-labs.com:18083/v1`을 사용하며 Pi local direct providers는 제거된 상태다. credential 원문 대신 같은 inventory의 SOPS `token_ref`만 기준으로 삼는다.
- provider/model separation: Laguna는 GX10 `poolside_v1`, Qwen은 mac `qwen`/`qwen3`, Ornith는 Lemonade runtime profile을 사용한다. dev-corp `gemma4:26b`와 stopped DiffusionGemma 설정을 이들 profile에 섞지 않는다.

노드 후보:

- mac CLI + MLX provider node: `mac-codex-node` / `mac-codex`
  - SSH/user: `ssh toki@toki-labs.com`
  - 목적: `cli` adapter, `codex app-server` + `mac-mlx-vllm` provider resource
  - workspace: `/Users/toki/agent-work/iop-workspace/nomadcode`
  - MLX vLLM provider: `mac-mlx-vllm`
    - provider endpoint: `http://127.0.0.1:8002/v1`
    - served model: `mlx-community/Qwen3.6-35B-A3B-4bit`
    - capacity baseline: `2`
    - priority baseline: `2`
    - workdir: `/Users/toki/agent-work/iop-mlx-vllm`
    - runtime baseline: `vllm-mlx`, `--max-num-seqs 2`, `--max-kv-size 262144`, `--max-request-tokens 262144`, `--use-paged-cache --paged-cache-block-size 64 --max-cache-blocks 4096`, `--enable-auto-tool-choice --tool-call-parser qwen --reasoning-parser qwen3`, `--default-chat-template-kwargs {"enable_thinking": true}`
    - KV policy: per-call window bound `262144`, one full context-size setting
    - long-context admission baseline: `total_context_tokens=262144`, `long_context_capacity=1` (KV budget `262144` fits one full 262144-window request; long slot count is not the normal capacity `2`)
- GX10 vLLM node: `gx10-vllm-node` / `gx10-vllm`
  - SSH/user: `ssh toki@192.168.0.91`
  - provider endpoint: `http://192.168.0.91:8001/v1`
  - served model: `laguna-s:2.1`
  - capacity baseline: `4`
  - priority baseline: `1`
  - runtime baseline: custom image `iop-vllm-laguna-s21:v0.25.1-cu130`, model `poolside/Laguna-S-2.1-NVFP4` revision `07614121b31898586430f189d27a25a0be310843`, `--served-model-name laguna-s:2.1`, `--max-model-len 262144`, `--max-num-seqs 4`, `--gpu-memory-utilization 0.70`, `--enable-prefix-caching`, `--enable-auto-tool-choice --tool-call-parser poolside_v1`, `--reasoning-parser poolside_v1`, `--chat-template /run/iop/laguna-s-2.1-thinking.jinja`, `--default-chat-template-kwargs '{"enable_thinking":true}'`, `--override-generation-config '{"temperature":0.7,"top_p":0.95}'`
  - thinking template baseline: host `/home/toki/iop-gx10-vllm/laguna-s-2.1-thinking.jinja`를 container에 read-only bind하고 generation prefix를 `<think>\n`으로 둔다. stock `<think>` prefix가 첫 토큰 `</think>`를 유도해 reasoning이 비는 현상을 방지한다.
  - speculative decoding baseline: `poolside/Laguna-S-2.1-DFlash-NVFP4` revision `723794750422b3efbf3a7b3af76dffb4ba035943`, `method=dflash`, `num_speculative_tokens=7`
  - long-context admission baseline: observed GPU KV cache `373711` tokens, `long_context_capacity=1` (vLLM max concurrency `1.43x` for a 262144-token request)
  - reasoning baseline: provider-native `reasoning` delta를 Edge passthrough가 보존하고 Pi가 `thinking_delta`로 소비한다. Pi Laguna compat는 thinking level을 `chat_template_kwargs.enable_thinking`으로 전달하고 `preserve_thinking=true`를 유지한다.
  - workspace: `/home/toki/iop-gx10-vllm`
- OneXPlayer Lemonade node: `onexplayer-lemonade-node` / `onexplayer-lemonade`
  - SSH/user: `ssh r0bin@192.168.0.59`
  - 접속 기준: 현재 작업 호스트에서 직접 SSH
  - provider endpoint: `http://192.168.0.59:13305/v1`
  - served model: `Ornith-1.0-35B-GGUF-llamacpp-tp1-Q5_K_M`
  - capacity baseline: `1` (agent 장문 요청의 provider 독점 실행 기준)
  - priority baseline: `2`
  - load baseline: checkpoint `LordNeel/Ornith-1.0-35B-GGUF-llamacpp-tp1:Q5_K_M`, backend `vulkan`, ctx size `262144`, `llamacpp_args="--spec-type none -np 1 -cb -fa on -b 4096 -ub 1024 --kv-unified --temp 0.6 --top-p 0.95 --top-k 20"`, `save_options=true`
  - long-context admission baseline: `total_context_tokens=262144`, `long_context_capacity=1` (IOP의 provider 직렬 admission과 llama-server의 `-np 1`을 일치시켜 agent turn별 prefix cache가 서로 다른 slot에 분산되지 않게 한다. `/slots`는 slot 1개와 `n_ctx=262144`를 보고한다)
  - workspace: `C:/Users/r0bin/iop-field`
- RTX5090 Lemonade node: `rtx5090-lemonade-node` / `rtx5090-lemonade`
  - SSH/user: `ssh iop-dev-rtx5090`
  - 접속 기준: 현재 작업 호스트의 local SSH config alias와 public-key batch 인증
  - provider endpoint: `http://192.168.0.111:13305/v1`
  - served model: `Ornith-1.0-35B-GGUF-llamacpp-tp1-Q5_K_M`
  - capacity baseline: `1`
  - priority baseline: `0`
  - load baseline: backend `cuda`, Q5 GGUF, Q8 KV, ctx size `262144`, `llamacpp_args="--chat-template-kwargs '{\"preserve_thinking\":true}' --kv-unified --min-p 0.00 --repeat-penalty 1.0 --spec-type none --temp 0.6 --top-k 20 --top-p 0.95 -b 512 -cb -ctk q8_0 -ctv q8_0 -fa on -np 1 -ub 256"`, Lemonade `host=0.0.0.0`
  - long-context admission baseline: `total_context_tokens=262144`, `long_context_capacity=1`
  - process baseline: IOP 관련 Windows 부팅 자동 실행은 비활성이고, 수동 `remote-llm-toggle.ps1`이 Lemonade Server, Ornith, IOP Node를 순차 UP/DOWN한다. Startup shortcut, Run entry, Task Scheduler, Windows service는 IOP dev 배포가 생성·재생성하지 않는다.
  - workspace: `C:/Users/r0bin/iop-field`

GX10은 Laguna S 2.1 NVFP4 + DFlash NVFP4를 사용한다. OneXPlayer와 RTX5090 Lemonade는 Ornith Q5 GGUF를 사용하고 runtime speculative decoding은 `--spec-type none`으로 끈다. provider family별 parser/template을 섞지 않는다.

OneXPlayer Lemonade Node는 원격 runner나 Edge host에서 다시 SSH하거나 proxy process로 띄우지 않는다. 현재 작업 호스트에서 OneXPlayer Windows host에 `ssh r0bin@192.168.0.59`로 직접 접속한 뒤 generated PowerShell bootstrap을 실행한다.

RTX5090 Lemonade Node도 원격 runner나 Edge host를 경유하지 않는다. 현재 작업 호스트에서 `ssh iop-dev-rtx5090`으로 public-key batch 접속한다. Windows 부팅 owner는 없으며 `RemoteLLM_mode.ahk`의 유일한 `Run`은 수동 `remote-llm-toggle.ps1` 호출이다. 기본 Toggle은 operator 동작에만 쓰고 자동 검증은 `-Action Status|Up|Down`을 명시한다. UP 완료는 Lemonade `0.0.0.0:13305`, 정확한 Ornith profile, llama slot `1`/`n_ctx=262144`, Node의 Edge 연결을 모두 확인하고, DOWN 완료는 Node/Lemonade/llama process와 port가 모두 사라진 상태로 판정한다.

mac-mlx-vllm provider는 mac-codex-node 소속 resource로, Edge host와 같은 macOS host에서 vllm-mlx process로 실행한다. vllm-mlx API는 외부에 직접 노출하지 않고 `127.0.0.1:8002`에 bind하며, Edge OpenAI-compatible adapter가 local bearer header로 호출한다. 운영 파일은 `/Users/toki/agent-work/iop-mlx-vllm/vllm-mlx.pid`, `logs/vllm-mlx.stdout.log`, `logs/vllm-mlx.stderr.log`를 기준으로 한다. Docker와 macOS 여유 메모리를 고려해 capacity는 `2`를 기본선으로 유지한다.

GX10 Laguna 공식 vLLM baseline은 `temperature=0.7`, `top_p=0.95`, model `generation_config`의 `top_k=20`, `tool_call_parser=poolside_v1`, `reasoning_parser=poolside_v1`, `enable_thinking=true`다. 현재 dev는 reasoning 출력을 위해 generation prefix를 `<think>\n`으로 보정한 로컬 템플릿을 사용한다. Edge/Pi smoke는 `high`에서 thinking event, `off`에서 thinking event 0개와 최종 text를 대조하고 agentic multi-turn에서는 tool-call 전후 reasoning과 최종 text까지 확인한다. OneXPlayer/RTX5090 Ornith sampling은 기존 `temperature=0.6`, `top_p=0.95`, `top_k=20` 기준을 유지한다.

## 명령

- setup:
- lint:
- unit: `go test ./apps/edge/...`
- smoke: `./scripts/e2e-smoke.sh`는 기본 포트 임시 설정을 쓰는 보조 smoke이므로 dev 포트 override 필요 여부를 먼저 확인한다.
- e2e: `make test-e2e`는 보조 smoke이며 full-cycle 실제 구동을 대체하지 않는다.
- model: dev OpenAI-compatible profile을 띄운 경우 `iop-edge smoke openai --base-url http://127.0.0.1:18083`
- full-cycle: repo 내부 edge-node 진단, `iop-edge smoke openai`, OpenAI-compatible 입력 표면 수동 검증

## 필수 검증

- 변경한 edge 패키지 또는 `go test ./apps/edge/...`를 실행한다.
- registry, service, transport, console, HTTP/A2A 입력 표면을 바꾼 경우 edge-node 메시지 2회 왕복과 command 응답을 확인한다.
- OpenAI-compatible 경계를 바꾼 경우 dev `18083` 기준 `iop-edge smoke openai` 또는 동등한 `/healthz`, `/v1/models`, `/v1/responses` 확인으로 edge service와 node adapter 경로 수렴을 확인한다.
- runtime `edge.yaml`에서 `openai.enabled=true`이면 `openai.provider_id`가 현재 CLI/provider identity와 일치하는지 `config check`로 검증한다. 이 값이 빠진 candidate는 배포하지 않는다.
- OpenAI-compatible tool-call 경계(`apps/edge/internal/openai/**`의 text tool-call 합성/validation)를 바꾼 경우 dev `18083` `/v1/chat/completions`에 Pi/Cline형 `tools[]` 요청을 non-stream/stream 각각 최소 1회 보내 raw text tool-call boundary smoke를 수행한다. token은 원격 환경 변수에서 주입하고 원문을 명령/로그/보고에 남기지 않는다.
- dev provider-pool tool-call drift를 확인할 때는 active model group별 provider만 섞는다. Laguna group은 `gx10-vllm` direct와 Edge `laguna-s:2.1`, Ornith group은 `onexplayer-lemonade`/`rtx5090-lemonade` direct와 Edge `ornith:35b`, Qwen group은 `mac-mlx-vllm` direct와 Edge `qwen3.6:35b`를 각각 검증한다.
- bootstrap/artifact 경계를 바꾼 경우 dev artifact/base URL 후보 `18082`가 local/test `18080` field baseline을 덮어쓰지 않는지 확인한다.

## 보조 검증

- `./scripts/e2e-smoke.sh`는 edge-node 최소 생존 확인에 사용한다.
- `./scripts/e2e-openai-ollama.sh`는 OpenAI-compatible Ollama 입력 표면 보조 확인에 사용한다.

## 판정 기준

- node 등록, `/nodes`, console 메시지 전송, 기대 payload를 포함한 `[node-*-message]` 출력, 같은 run의 complete event가 확인된다.
- node 로컬 `[node-message]` payload 라인 목록과 edge `[node-*-message]` payload 라인 목록이 run별로 내용/순서까지 동일해야 한다.
- edge complete는 같은 run의 마지막 `[node-*-message]` 이후에만 정상이다.
- OpenAI-compatible smoke에서 `/healthz`, `/v1/models`, `/v1/responses`가 기대 상태로 응답한다.
- raw text tool-call boundary smoke: Pi/Cline형 `tools[]` 요청에서 응답 body와 SSE delta 어디에도 `<tool_call>`, `{{`, `<|mask_end|>` 원문이 성공 content로 남지 않는다. 요청 `tools[]`에 있는 valid raw text tool-call은 `message.tool_calls`(또는 stream `delta.tool_calls`)와 `finish_reason: "tool_calls"`로 정규화되고, unknown tool hallucination이나 malformed 블록은 성공 content가 아니라 non-stream `tool_validation_error`(HTTP 502) 또는 SSE `tool_validation_error` 이벤트로 끝난다. non-stream/strict buffered stream은 bounded retry 후 차단을 확인한다. 세부 payload와 계약은 `docs/edge-local-dev-guide.md`의 Raw text tool-call boundary smoke와 `agent-contract/outer/openai-compatible-api.md`를 따른다.
- raw boundary smoke evidence는 tracked 문서가 아니라 ignored run 위치(`agent-test/runs/**`)나 code-review output path에 저장하고, 저장물에도 token 원문을 남기지 않는다.
- provider-pool dispatch는 `in_flight >= capacity`인 provider를 후보에서 제외하고, 남은 후보 중 가장 낮은 `in_flight` 레벨을 먼저 선택한다. 같은 `in_flight` 레벨 안에서는 낮은 priority 값과 rotation으로 분산한다.
- dev-runtime managed capacity smoke는 `scripts/e2e-openai-managed-capacity-smoke.sh`를 사용한다. 같은 principal token으로 authenticated route alias를 먼저 조회하고, exact active route의 `resource_selector`, profile, upstream model과 일치하는 healthy provider snapshot 하나만 eligible pool로 본다. `ornith:35b`는 `onexplayer-lemonade=1`, `ornith-fast`는 `rtx5090-lemonade=1`로 각각 분리하며, 현재 projected route에서 제외된 두 provider의 capacity를 더하지 않는다. Chat/Responses를 alias별로 따로 실행해 각각 2개 요청에서 selected `in_flight=1`, `queued>=1`을 확인한다.
- managed smoke는 실제 emitted request JSON의 Unicode rune 수와 `runes/4 + runes/16` estimate를 계산하고 `long_context_threshold_tokens`와 대조한다. normal qualification은 `capacity`, 별도 long-context 시나리오는 `long_context_capacity`를 사용한다. 장문 반복/liveness payload로 normal capacity를 증명하거나 normal 요청으로 long slot을 증명하지 않는다. unprojected/legacy Laguna `laguna-s:2.1`과 Qwen `qwen3.6:35b`는 기존 model-group capacity `4`/`2` 기준을 유지한다.
- capacity smoke 완료 후 대상 provider의 `in_flight=0`, `queued=0` 회복을 확인한다.
- long-context admission 시나리오(normal 10-way, mixed long/normal, all-long-slot-full)와 최종 회복 근거는 `agent-test/dev/long-context-admission-smoke.md`와 `scripts/e2e-long-context-admission-smoke.sh`를 사용한다.
- `/v1/responses`와 `/v1/chat/completions`는 각각 짧은 입력에서 700~1200 token 수준의 구조화된 응답을 유도하고 provider-native thinking을 제한하며 50~100ms 간격으로 status를 polling한다. 각 요청의 HTTP 200, endpoint-native success terminal 정확히 1개, `[DONE]` 정확히 1개, selected provider의 eligible capacity 미초과, exact peak와 queue, 최종 0 회복을 별도 증거로 남긴다.
- managed smoke invocation마다 ignored `agent-test/runs/**` 아래 mode `0700` unique directory와 immutable run id를 생성한다. manifest가 소유하고 현재 run/dispatch 이후 생성된 request/result/status만 판정하며 stale/missing/foreign-run body는 실패한다. tracked/review evidence에는 sanitized summary의 run id, script hash, alias, selected provider, endpoint, computed shape/class, terminal count, peak/queue/final counter, outcome만 허용하고 token/header, route/slot id, prompt, response body와 출력은 기록하지 않는다.
- Qwen provider-pool smoke는 thinking/reasoning 텍스트가 포함될 수 있다. 추론 출력 자체를 실패로 보지 말고 HTTP 성공, model alias, final marker 포함 여부, provider node log/run count 증가로 판정한다. 응답 전체가 특정 token과 정확히 같은지 비교하는 strict exact-match는 이 profile의 기본 판정으로 쓰지 않는다.
- Qwen provider를 agent/tool-call 용도로 검증할 때는 일반 chat smoke와 별도로 forced tool call, auto tool call, streaming `delta.tool_calls`, multi-turn tool result 후 최종 답변을 확인한다. raw native marker나 reasoning text가 assistant content로 새면 해당 model/runtime의 parser/template profile 미확정으로 보고한다.
- Qwen runtime에는 Qwen 전용 parser/template 검증값만 사용한다. dev-corp Gemma 계열의 `tool_call_parser=gemma4`, `reasoning_parser=gemma4`, Gemma4 chat template/profile을 Qwen provider에 복사하지 않는다.
- bootstrap 사용자 명령은 완성된 URL과 positional token 하나만 포함한다.

## 기준 출력 예시

```text
edge> /nodes
[edge] sent run_id=...
[node-*-message] IOP_EXPECTED_REPLY
[node-*-event] complete
```

## 차단 기준

- dev host 또는 Edge-Node TCP port 접근이 불가능하다.
- 외부 CLI profile 검증에 필요한 CLI 설치, 계정, provider 상태가 없다.
- dev artifact/model/metrics 포트가 기존 field baseline과 충돌한다.
- Node 등록 시 `openai_compat adapter instance key "<provider-id>" conflicts with provider id`가 발생한다. 이는 agent orchestration 차단이 아니라 Edge config mapper 또는 stale binary 문제다. provider/adapter 정의를 삭제하지 말고 candidate source identity와 `go test -race ./apps/edge/internal/node`를 확인한 뒤 Edge를 먼저 교체한다.

## 보고 항목

- 실행한 명령:
- 성공한 검증:
- 실패/차단된 검증:
- 생략 사유:
- 남은 위험:

## 금지 사항

- secret, token, 개인 endpoint 원문은 tracked 파일에 기록하지 않는다.
- field baseline 포트(`18080`, `18081`, `19090`, `19092`)를 dev 전용 포트로 재사용하지 않는다.
