feat: provider catalog device status milestone completion and catalog view implementation

- Add provider catalog device status milestone tracking
- Implement catalog status HTTP endpoints
- Add client catalog view implementation
- Update control-plane HTTP views and tests
- Update edge cmd nodes and tests
- Update roadmap and phase documentation
This commit is contained in:
toki 2026-06-20 19:51:45 +09:00
parent 06f1d36fa3
commit 3f8da3354c
15 changed files with 1644 additions and 43 deletions

View file

@ -1,6 +1,6 @@
# IOP 로드맵 현황 및 진행 계획
작성 기준: 2026-06-18
작성 기준: 2026-06-20
# 1. 월별 로드맵
@ -21,11 +21,11 @@
| 구분 | 내용 |
| --- | --- |
| 목표 | 같은 Edge 안에서 동일 `model` 요청을 model group queue로 묶고, 여러 Node 후보에 순차 dispatch하는 구조를 정리합니다. Lemonade 이후 vLLM과 SGLang provider 검증을 이어가며 1차 KPI 범위의 속도/디바이스 효율 축을 닫습니다. |
| 목표 | 같은 Edge 안에서 동일 `model` 요청을 model group queue로 묶고, 여러 Node 후보에 순차 dispatch하는 구조를 정리합니다. Lemonade 이후 vLLM과 SGLang provider 검증 결과를 반영해 1차 KPI 범위의 속도/디바이스 효율 축을 닫습니다. |
| 예상 투입인원 | 1명 |
| 현재 구현 상태 | Node-local admission/capacity 기반 완료<br>Edge model group queue scheduling과 같은 Edge 내부의 다중 Node 후보 순차 dispatch 기반 완료<br>OpenAI-compatible `model` route queue 정책 정합화 완료<br>vLLM provider serving validation 완료: provider boundary, DGX Spark vLLM container, config contract, 모델 조회/chat, streaming, split-host field smoke 검증 PASS 근거와 코드 레벨 완료 리뷰까지 정리하고 archive함<br>SGLang provider serving validation을 다음 활성 마일스톤으로 지정함. 현재 계획/잠금 상태이며 SDD 승인과 provider 경계·실테스트 endpoint/model/auth/streaming 결정이 필요함 |
| 주요 작업 | 완료: OpenAI-compatible `model` 값을 queue group key로 사용하는 Edge-owned FIFO 정리<br>완료: Node 후보별 capacity, in-flight, queued snapshot을 기준으로 dispatch 흐름 정리<br>완료: Model route queue 정책 원천 정합화<br>완료: vLLM OpenAI-compatible provider의 config contract, 모델 조회, chat, streaming, split-host field smoke 검증<br>다음: SGLang provider SDD gate 해소와 최소 serving 경로 검증<br>계획: Provider별 adapter/config/target/model 매핑 기준을 vLLM/SGLang 검증 결과로 보강 |
| 완성 예정 기능 | vLLM provider 최소 serving 검증<br>SGLang provider 최소 serving 검증<br>Provider별 adapter/config/target/model 매핑 기준 보강 |
| 현재 구현 상태 | Node-local admission/capacity 기반 완료<br>Edge model group queue scheduling과 같은 Edge 내부의 다중 Node 후보 순차 dispatch 기반 완료<br>OpenAI-compatible `model` route queue 정책 정합화 완료<br>vLLM provider serving validation 완료: provider boundary, DGX Spark vLLM container, config contract, 모델 조회/chat, streaming, split-host field smoke 검증 PASS 근거와 코드 레벨 완료 리뷰까지 정리하고 archive함<br>SGLang provider serving validation은 GX10 + Qwen3.6 35B NVFP4 field validation 결과 NVIDIA checkpoint 직접 로딩 실패와 vLLM 대비 낮은 throughput/KV 여유가 확인되어 현 조건에서 provider 채택을 폐기함<br>다음 활성 작업은 운영 관측 Phase의 `Model Alias Provider Pool과 Provider Catalog` 계획 마일스톤으로 전환됨 |
| 주요 작업 | 완료: OpenAI-compatible `model` 값을 queue group key로 사용하는 Edge-owned FIFO 정리<br>완료: Node 후보별 capacity, in-flight, queued snapshot을 기준으로 dispatch 흐름 정리<br>완료: Model route Queue 정책 원천 정합화<br>완료: vLLM OpenAI-compatible provider의 config contract, 모델 조회, chat, streaming, split-host field smoke 검증<br>정리: SGLang provider는 현 field 조건에서 채택하지 않고 Provider Catalog/Qualification 후속 판단 후보로 분리<br>다음: `models[]` 기반 provider pool과 read-only Provider Catalog 계약 구현 계획 |
| 완성 예정 기능 | vLLM provider 최소 serving 검증 완료<br>SGLang provider field 판단 결과 정리<br>Provider별 adapter/config/target/model 매핑 기준을 Provider Catalog 계약으로 승계 |
---
@ -35,21 +35,21 @@
| --- | --- |
| 목표 | IOP를 여러 Edge, Node, CLI Agent, local inference provider와 함께 운영하기 위해 필요한 사용자/토큰/사용량/로그/provider 상태 관찰 기준을 1차 feature MVP 단위로 정리합니다. |
| 예상 투입인원 | 1명 |
| 현재 구현 상태 | Edge, Node, Control Plane, Client의 기본 상태 관찰 경로 구축<br>Provider availability와 capacity snapshot 기반 완료<br>사용자/토큰/사용량/로그 추적과 Provider Catalog는 스케치 상태이며, 사용자 검토 후 계획으로 승격 |
| 주요 작업 | 사용자 단위 API/CLI/local inference 사용량 집계 범위 정리<br>Token, request, execution, provider usage 로그의 최소 수집 항목 정의<br>Control Plane/Client 또는 CLI에서 확인할 운영 상태 항목 정리<br>API, CLI, local inference provider를 구분하는 Provider Catalog 기준 정리<br>vLLM, vLLM-MLX, Lemonade, SGLang 등 로컬 디바이스 provider 상태 표현 정리 |
| 완성 예정 기능 | 사용량, 토큰, 로그 운영 추적 MVP<br>Provider Catalog와 로컬 디바이스 상태 관리 MVP<br>운영자가 확인할 수 있는 provider/status/usage 최소 화면 또는 명령 표면 |
| 현재 구현 상태 | Edge, Node, Control Plane, Client의 기본 상태 관찰 경로 구축<br>Provider availability와 capacity snapshot 기반 완료<br>`Model Alias Provider Pool과 Provider Catalog`가 현재 활성 계획 마일스톤이며 SDD는 승인됨/잠금 해제 상태<br>사용자/토큰/사용량/로그 추적과 Provider-Device-Model Qualification 리포트는 후속 스케치 상태 |
| 주요 작업 | `models[]`를 OpenAI-compatible model key이자 provider pool canonical routing key로 정리<br>`nodes[].providers[]` 기반 provider 정의와 provider별 served model list 계약 정리<br>Edge-owned target rewrite와 `in_flight / capacity` load-ratio provider 선택 정책 구현 계획<br>API, CLI, local inference provider를 구분하는 read-only Provider Catalog 기준 정리<br>사용자 단위 API/CLI/local inference 사용량, token, request, execution 로그의 최소 수집 항목은 후속 MVP로 구체화 |
| 완성 예정 기능 | Model Alias Provider Pool과 Provider Catalog MVP<br>Provider Catalog와 로컬 디바이스 상태 관리 MVP<br>사용량, 토큰, 로그 운영 추적 MVP<br>운영자가 확인할 수 있는 provider/status/usage 최소 화면 또는 명령 표면 |
---
## 8월 ~ 9월: CLI Agent 알림, 원격 작업 환경, 단계 호출 검증 MVP
## 8월 ~ 9월: CLI Agent 알림, 원격 작업 환경, Update Plane, 단계 호출 검증 MVP
| 구분 | 내용 |
| --- | --- |
| 목표 | CLI Agent 사용량 limit 감지와 알림, 작업 자동 이어받기, workspace-bound execution 기반 원격 작업 환경을 feature MVP로 정리합니다. 동시에 planner/generator/verifier 형태의 단계 호출runtime schema 검증은 최소 실행 모드로 스케치합니다. |
| 목표 | CLI Agent 사용량 limit 감지와 알림, 작업 자동 이어받기, workspace-bound execution 기반 원격 작업 환경을 feature MVP로 정리합니다. 동시에 Edge/Node 자체 업데이트 기반과 planner/generator/verifier 형태의 단계 호출/runtime schema 검증은 최소 실행 모드로 스케치합니다. |
| 예상 투입인원 | 1명 |
| 현재 구현 상태 | CLI runtime과 usage checker 계열 기반 마련<br>알림 자체는 `nexo`의 messaging/notification 기반을 활용할 수 있는 상태<br>socket/protocol 기반은 `proto-socket`의 안정화 결과를 활용할 수 있는 상태<br>CLI Agent 알림/자동 이어받기, 원격 작업 환경, 단계 호출 검증 MVP는 스케치 상태이며 사용자 검토 후 계획으로 승격 |
| 주요 작업 | CLI Agent limit 도달 이벤트와 사용자 알림 trigger 기준 정리<br>자동 이어받기 허용 조건, 중단 조건, 사용자 승인 경계 정리<br>workspace-bound execution을 이용한 원격 코딩/유지보수 작업 환경의 최소 운영 흐름 정리<br>요청 의도 분석, 실제 작업, 검증/schema 강제, 오류 시 retry/fallback의 최소 단계 실행 흐름 정리<br>Nexo 알림 기반과 IOP 실행 이벤트의 연동 경계 정리 |
| 완성 예정 기능 | CLI Agent 사용량 알림 MVP<br>CLI Agent 자동 이어받기 MVP<br>원격 코딩/유지보수 작업 환경 MVP<br>단계 호출과 runtime schema 검증 MVP |
| 현재 구현 상태 | CLI runtime과 usage checker 계열 기반 마련<br>OpenAI Workspace Agent Execution Contract 완료<br>알림 자체는 `nexo`의 messaging/notification 기반을 활용할 수 있는 상태<br>socket/protocol 기반은 `proto-socket`의 안정화 결과를 활용할 수 있는 상태<br>CLI Agent 알림/자동 이어받기, 원격 작업 환경, Update Plane, 단계 호출 검증 MVP는 스케치/계획 상태이며 사용자 검토 후 구체화 |
| 주요 작업 | CLI Agent limit 도달 이벤트와 사용자 알림 trigger 기준 정리<br>자동 이어받기 허용 조건, 중단 조건, 사용자 승인 경계 정리<br>workspace-bound execution을 이용한 원격 코딩/유지보수 작업 환경의 최소 운영 흐름 정리<br>Control Plane release view, Edge/Node update protocol, host-local manager, rollout/recovery 정책 스케치<br>요청 의도 분석, 실제 작업, 검증/schema 강제, 오류 시 retry/fallback의 최소 단계 실행 흐름 정리<br>Nexo 알림 기반과 IOP 실행 이벤트의 연동 경계 정리 |
| 완성 예정 기능 | CLI Agent 사용량 알림 MVP<br>CLI Agent 자동 이어받기 MVP<br>원격 코딩/유지보수 작업 환경 MVP<br>Update Plane 안정 프로토콜과 자체 업데이트 기반 스케치<br>단계 호출과 runtime schema 검증 MVP |
---
@ -80,9 +80,9 @@
| 구분      | 주요 산출물                                                                                                                           |
| ----------------| -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 5월 ~ 6월 중순 | Edge-Node 실행 기반, Ollama serving 안정화, Control Plane/Client 운영 기반, CLI Automation Runtime, Domain Agent registry/bootstrap, OpenAI Responses input surface, Workspace Agent Execution Contract, Workspace port/env 표준화, Lemonade provider 검증 결과 |
| 6월 중순 ~ 7월 | Edge model group queue scheduling과 model route queue 정책 정합화 결과, vLLM/SGLang provider 검증 결과, provider별 adapter/config/target/model 매핑 기준                                                                    |
| 7월 ~ 8월   | 사용량/토큰/로그 추적 MVP 정의, Provider Catalog 초안, 로컬 디바이스 provider 상태 관리 기준, 운영 확인 표면 초안                                                                        |
| 8월 ~ 9월   | CLI Agent 사용량 알림/자동 이어받기 MVP, 원격 작업 환경 MVP, 단계 호출과 runtime schema 검증 MVP, Nexo 알림 연동 경계                                                                      |
| 6월 중순 ~ 7월 | Edge model group queue scheduling과 model route queue 정책 정합화 결과, vLLM provider 검증 결과, SGLang provider field 판단 결과, provider별 adapter/config/target/model 매핑 기준                                                                    |
| 7월 ~ 8월   | Model Alias Provider Pool과 Provider Catalog MVP, 사용량/토큰/로그 추적 MVP 정의, 로컬 디바이스 provider 상태 관리 기준, 운영 확인 표면 초안                                                                        |
| 8월 ~ 9월   | CLI Agent 사용량 알림/자동 이어받기 MVP, 원격 작업 환경 MVP, Update Plane 안정 프로토콜/자체 업데이트 기반, 단계 호출과 runtime schema 검증 MVP, Nexo 알림 연동 경계                                                                      |
| 9월 ~ 10월   | 1차 KPI 범위 feature MVP 통합 검증 결과, E2E smoke 기준, 운영 전환 체크리스트, known limitation, 2차 범위 분리 결과                                                                       |
| 10월 이후   | 장기 기억/RAG, advisor/context hook, 원격 터널링, oto scheduler/CI-CD, cloud fallback/품질 평가 고도화 검토 결과                                                                        |
@ -92,7 +92,7 @@
| 리스크 및 고려사항 | 내용 |
| --- | --- |
| Provider 실장비 검증 | vLLM, SGLang, vLLM-MLX 등 local inference provider는 adapter 구현보다 실제 장비와 런타임별 동작 검증이 일정 변수입니다. 1차 범위에서는 provider별 전체 최적화보다 최소 serving 경로와 상태 표현을 우선합니다. |
| Provider 실장비 검증 | vLLM, SGLang, vLLM-MLX 등 local inference provider는 adapter 구현보다 실제 장비와 런타임별 동작 검증이 일정 변수입니다. SGLang은 현 field 조건에서 채택하지 않기로 정리했으며, 1차 범위에서는 provider별 전체 최적화보다 최소 serving 경로와 상태 표현을 우선합니다. |
| 사용량/토큰/로그 정책 결정 | 사용자 단위 usage, token, log 추적은 구현보다 보존 범위, 노출 범위, 제한 정책 결정이 중요합니다. Billing, chargeback, 조직 IAM, 장기 retention은 1차 범위에서 완성하지 않고 후속 구체화로 둡니다. |
| CLI Agent 자동 이어받기 안전 경계 | 자동 이어받기는 편의성이 크지만, 무제한 재시도나 의도하지 않은 실행 지속으로 이어지면 위험합니다. 1차에서는 limit 감지, 알림, 명시된 조건에서의 이어받기만 MVP로 다룹니다. |
| Nexo와 Proto Socket 기반 활용 | 알림 기반은 `nexo`, socket/protocol 기반은 `proto-socket`의 거의 완료된 기반을 활용하는 전제로 봅니다. IOP에서는 새 인프라를 다시 만들기보다 실행 이벤트, 알림 trigger, protocol compatibility, 운영 표면 고정에 집중합니다. |
@ -104,10 +104,10 @@
IOP는 현재 Control Plane - Edge - Node 기반의 실행 오케스트레이션 구조와 OpenAI-compatible 실행 표면을 상당 부분 구축한 상태입니다. Edge-Node 실행 경로, Ollama serving, Control Plane/Client 운영 기반, CLI Automation Runtime, Domain Agent registry/bootstrap, OpenAI Responses input surface, workspace-bound agent execution contract, Workspace port/env 표준화, Node multi-target serving, provider availability/capacity queue, Lemonade provider 검증, Edge model group queue scheduling, model route queue 정책 정합화까지 완료되었습니다.
현재 vLLM provider serving validation은 완료 및 archive 처리되었습니다. Edge model group queue scheduling과 model route queue 정책 정합화는 완료된 기반으로 보고, 다음 활성 마일스톤은 SGLang provider serving validation입니다. SGLang은 현재 계획/잠금 상태이며, SDD 승인과 provider 경계·실테스트 endpoint/model/auth/streaming 결정이 다음 진입 조건입니다.
현재 vLLM provider serving validation은 완료 및 archive 처리되었습니다. SGLang provider serving validation은 field validation 결과를 반영해 현 조건에서는 provider 채택을 폐기했습니다. Edge model group queue scheduling과 model route queue 정책 정합화는 완료된 기반으로 보고, 다음 활성 마일스톤은 운영 관측 Phase의 `Model Alias Provider Pool과 Provider Catalog`입니다.
이후 7월부터 8월까지는 사용자/토큰/사용량/로그 추적과 Provider Catalog, 로컬 디바이스 provider 상태 관리를 정리합니다. 이 단계는 완성된 billing이나 enterprise IAM이 아니라, 운영자가 IOP의 사용량과 provider 상태를 확인할 수 있는 feature MVP를 목표로 합니다.
이후 7월부터 8월까지는 `models[]` 기반 provider pool, Edge-owned target rewrite, load-ratio provider 선택, read-only Provider Catalog, 로컬 디바이스 provider 상태 관리를 정리합니다. 이 단계는 완성된 billing이나 enterprise IAM이 아니라, 운영자가 IOP의 사용량과 provider 상태를 확인할 수 있는 feature MVP를 목표로 합니다. 사용자/토큰/사용량/로그 추적과 Provider-Device-Model Qualification 리포트는 후속 마일스톤으로 이어집니다.
8월부터 9월까지는 CLI Agent 사용량 limit 알림, 자동 이어받기, 원격 코딩/유지보수 작업 환경, 단계 호출과 runtime schema 검증 MVP를 구체화합니다. 알림 기반은 `nexo`, socket/protocol 기반은 `proto-socket`의 거의 완료된 기반을 활용하고, IOP는 실행 이벤트와 운영 정책, E2E 흐름 고정에 집중합니다.
8월부터 9월까지는 CLI Agent 사용량 limit 알림, 자동 이어받기, 원격 코딩/유지보수 작업 환경, Update Plane 자체 업데이트 기반, 단계 호출과 runtime schema 검증 MVP를 구체화합니다. 알림 기반은 `nexo`, socket/protocol 기반은 `proto-socket`의 거의 완료된 기반을 활용하고, IOP는 실행 이벤트와 운영 정책, 업데이트 수렴, E2E 흐름 고정에 집중합니다.
9월부터 10월까지는 1차 KPI 범위에 포함된 feature MVP를 통합 검증하고 안정화합니다. 10월 이후에는 장기 기억/RAG, advisor, context hook, 원격 터널링, oto scheduler/CI-CD, cross-Edge/cloud fallback 같은 2차 고도화 후보를 별도로 검토합니다.

View file

@ -47,7 +47,7 @@ provider/device/model별 qualification report와 모델 lifecycle 관리는 prov
- 경로: `agent-roadmap/phase/inference-provider-extension/PHASE.md`
- 요약: 1차 MVP의 속도 향상 축으로, 같은 Edge 안의 model group queue와 여러 Node 후보 순차 dispatch를 기준으로 다중 디바이스 효율을 높이고 Lemonade/vLLM/SGLang 같은 provider 검증을 이어가는 단계다.
- [계획] 운영 관측과 Provider 관리
- [진행중] 운영 관측과 Provider 관리
- 경로: `agent-roadmap/phase/operational-observability-provider-management/PHASE.md`
- 요약: 사용자/토큰/사용량/로그 추적과 API/CLI/local inference provider catalog, 로컬 디바이스 provider 상태 관리, provider/device/model qualification report와 모델 lifecycle 관리 방향을 MVP 운영 축과 후속 심화 축으로 스케치한다.

View file

@ -2,7 +2,7 @@
## 상태
[계획]
[진행중]
## 목표
@ -16,7 +16,7 @@ provider 확장 Phase에서 검증한 Ollama, vLLM, SGLang, Lemonade 같은 추
완료, 검토중, 진행중, 계획, 스케치 순서로 두어 아래로 갈수록 미래 작업에 가까워지게 정렬한다.
스케치 Milestone은 아직 구현 가능한 계획이 아니므로 사용자 검토와 구체화 후 `[계획]`으로 승격한다.
- [계획] Model Alias Provider Pool과 Provider Catalog
- [진행중] Model Alias Provider Pool과 Provider Catalog
- 경로: `agent-roadmap/phase/operational-observability-provider-management/milestones/provider-catalog-device-status.md`
- 요약: `models[]`를 외부 model key이자 운영 catalog model로 두고, Node 하위 provider와 served model list를 기준으로 Edge가 provider 선택, load-ratio routing, target rewrite, read-only catalog 상태를 소유하는 MVP 계약을 구현 계획 대상으로 정리한다.

View file

@ -14,7 +14,7 @@ provider/device/model별 qualification report와 benchmark/품질 비교는 이
## 상태
[계획]
[진행중]
## 승격 조건
@ -52,27 +52,27 @@ provider/device/model별 qualification report와 benchmark/품질 비교는 이
`models[]` catalog model을 중심으로 provider 후보를 묶고 Edge가 실행 후보를 고르는 최소 산출물을 구현한다.
- [ ] [alias-contract] `models[].id`가 OpenAI-compatible `model` key이자 provider pool의 canonical routing key가 되며, `models[].providers`가 provider id -> served model name map으로 검증된다.
- [ ] [candidate-schema] `nodes[].providers[]`가 provider id, type, category, served `models[]`, health, capacity, in-flight, queued, coarse lifecycle capability를 표현한다.
- [ ] [target-rewrite] Edge가 선택한 provider id의 `models[].providers[provider_id]` 값을 concrete target으로 rewrite해 Node에 전달하고, Node/provider adapter는 rewrite하지 않는다.
- [ ] [selection-policy] Edge가 available provider 중 `in_flight / capacity` load ratio가 가장 낮은 후보를 우선 선택하고, 동률은 deterministic tie-break로 처리하며, queue timeout을 적용한다.
- [ ] [pool-compat] 기존 `openai.model_routes`, `adapter + target`, Node provider instance 설정과 새 `models[]`/provider catalog 계약의 호환/마이그레이션 방향이 정리되어 있다.
- [x] [alias-contract] `models[].id`가 OpenAI-compatible `model` key이자 provider pool의 canonical routing key가 되며, `models[].providers`가 provider id -> served model name map으로 검증된다.
- [x] [candidate-schema] `nodes[].providers[]`가 provider id, type, category, served `models[]`, health, capacity, in-flight, queued, coarse lifecycle capability를 표현한다.
- [x] [target-rewrite] Edge가 선택한 provider id의 `models[].providers[provider_id]` 값을 concrete target으로 rewrite해 Node에 전달하고, Node/provider adapter는 rewrite하지 않는다.
- [x] [selection-policy] Edge가 available provider 중 `in_flight / capacity` load ratio가 가장 낮은 후보를 우선 선택하고, 동률은 deterministic tie-break로 처리하며, queue timeout을 적용한다.
- [x] [pool-compat] 기존 `openai.model_routes`, `adapter + target`, Node provider instance 설정과 새 `models[]`/provider catalog 계약의 호환/마이그레이션 방향이 정리되어 있다.
### Epic: [provider-catalog] Provider Operations Catalog
운영자가 model provider pool과 provider 상태를 같은 기준으로 비교하기 위한 read-only catalog 산출물을 묶는다.
- [ ] [category-map] MVP provider category가 `api`, `cli`, `local_inference`로 정리되고 read-only catalog 표시 필드에 반영된다.
- [x] [category-map] MVP provider category가 `api`, `cli`, `local_inference`로 정리되고 read-only catalog 표시 필드에 반영된다.
- [ ] [device-status] provider health, capacity, in-flight, derived load ratio, served models, queued 상태가 catalog/status 표면에 정리된다.
- [ ] [lifecycle-cap] Ollama, Lemonade, vLLM, SGLang 등의 lifecycle 차이가 coarse capability flags로 표시되고 상세 qualification report는 후속 범위로 분리된다.
- [ ] [boundary] provider adapter 구현, Edge-owned provider pool routing, read-only operations catalog, 후속 runtime control의 책임 경계가 정리되어 있다.
- [x] [lifecycle-cap] Ollama, Lemonade, vLLM, SGLang 등의 lifecycle 차이가 coarse capability flags로 표시되고 상세 qualification report는 후속 범위로 분리된다.
- [x] [boundary] provider adapter 구현, Edge-owned provider pool routing, read-only operations catalog, 후속 runtime control의 책임 경계가 정리되어 있다.
- [ ] [ops-review] Edge config와 Edge-owned read-only catalog/status를 기준으로 `iop-edge` CLI, Control Plane, Client가 어떤 정보를 소비하는지 확인할 수 있다.
## 완료 리뷰
- 상태: 없음
- 요청일: 없음
- 완료 근거: 계획 Milestone이며 model provider pool과 catalog 기능 Task가 아직 충족되지 않았다.
- 완료 근거: 진행중 Milestone이며 `agent-task/archive/2026/06/m-provider-catalog-device-status/01_pool_schema/complete.log``agent-task/archive/2026/06/m-provider-catalog-device-status/02+01_edge_dispatch/complete.log`의 Roadmap/Spec Completion 근거로 `alias-contract`, `candidate-schema`, `target-rewrite`, `selection-policy`를 완료 반영했다. `pool-compat``01_pool_schema` 계획의 API-3 compatibility 산출물, `apps/edge/README.md``configs/edge.yaml`의 migration/compat note, `TestLoadEdge_ProviderPoolLegacyCompatibility` 및 OpenAI provider-pool legacy fallback tests를 S05 보조 근거로 완료 반영했다. Provider Operations Catalog는 `packages/go/config`, `proto/iop/runtime.proto`, `apps/edge/internal/service/model_queue.go`, `apps/edge/README.md`, `configs/edge.yaml`, `apps/edge/internal/edgecmd/nodes.go` 근거로 `category-map`, `lifecycle-cap`, `boundary`를 완료 반영했으며, `device-status``ops-review`는 Control Plane/Client 표면 계획이 남아 있다.
- 리뷰 필요:
- [ ] 사용자가 완료 결과를 확인했다
- [ ] archive 이동을 승인했다
@ -97,4 +97,7 @@ provider/device/model별 qualification report와 benchmark/품질 비교는 이
- 선행 작업: Node provider 상태와 Capacity Queue 기반, Edge 모델 그룹 Queue 스케줄링 전환
- 후속 작업: provider pool scheduler 구현, Provider-Device-Model Qualification 리포트와 Lifecycle 관리, provider enable/disable, benchmark/품질 평가
- 작업 메모: [pool-compat] `apps/edge/README.md``configs/edge.yaml`에 기존 `openai.model_routes`/`adapter + target`/Node provider instance 설정을 새 `models[]` provider pool의 1:1 migration 입력 및 fallback/compat 경로로 유지하는 방향을 정리했다. SDD 대상 Milestone의 공식 Task 체크는 `complete.log`의 Roadmap/Spec Completion 근거로 처리한다.
- 작업 메모: [sync] `01_pool_schema` 완료 로그로 `alias-contract`, `candidate-schema`를, `02+01_edge_dispatch` 완료 로그로 `target-rewrite`, `selection-policy`를 Roadmap/Spec Completion 기준 완료 처리했다.
- 작업 메모: [sync-correction] `pool-compat``01_pool_schema`의 공식 Roadmap/Spec Targets에는 빠졌지만, SDD S05 Evidence Map이 요구한 migration/compat note와 기존 `model_routes` regression evidence가 `apps/edge/README.md`, `configs/edge.yaml`, `packages/go/config/config_test.go`, `apps/edge/internal/openai/server_test.go`에 남아 있어 완료로 보정했다.
- 작업 메모: [provider-catalog-small] `api`, `cli`, `local_inference` category는 `config.Category*``ProviderSnapshot.category`에 반영되어 있고, coarse lifecycle capability는 `nodes[].providers[].lifecycle_capabilities``ProviderSnapshot.lifecycle_capabilities``iop-edge nodes list` configured provider 요약으로 확인 가능하다. 상세 lifecycle/qualification과 runtime control은 SDD/범위 제외 기준에 따라 후속 Milestone으로 분리한다.
- 확인 필요: 없음

View file

@ -0,0 +1,198 @@
<!-- task=m-provider-catalog-device-status/03_catalog_status_http plan=1 tag=REVIEW_API -->
# Code Review Reference - REVIEW_API
> **[IMPLEMENTING AGENT - READ FIRST] Filling in this file is the mandatory final step of implementation.**
> The task is NOT complete until every implementation-owned section below is filled in.
> Complete the `구현 체크리스트`; the final checklist item is mandatory before saving.
> Fill implementation-owned sections, then stop with active files in place and report ready for review.
> If implementation is blocked by a selected SDD decision or selected Milestone `구현 잠금 > 결정 필요` item, fill `사용자 리뷰 요청` with linked evidence and stop with active files in place; code-review decides whether to write `USER_REVIEW.md`. Environment/secret/service blockers, generic scope changes, repeated failures, and evidence gaps that a follow-up agent can close are normal follow-up issues, not user-review blockers by themselves.
> Do not ask the user directly, present choices in chat, or call `request_user_input` during implementation; record only SDD/Milestone lock decisions in `사용자 리뷰 요청` and stop for code-review.
> Finalization (`코드리뷰 결과`, log rename, `complete.log`, archive moves, `코드리뷰 전용 체크리스트`) is review-agent-only, even after compaction/resume.
> Follow the ownership table at the bottom of this file for which sections you own.
## 개요
date=2026-06-20
task=m-provider-catalog-device-status/03_catalog_status_http, plan=1, tag=REVIEW_API
## Roadmap Targets
- Milestone: `agent-roadmap/phase/operational-observability-provider-management/milestones/provider-catalog-device-status.md`
- Task ids:
- `device-status`: provider health, capacity, in-flight, derived load ratio, served models, queued 상태가 catalog/status 표면에 정리된다.
- Completion mode: check-on-pass
## Spec Targets
- SDD: `agent-roadmap/sdd/operational-observability-provider-management/provider-catalog-device-status/SDD.md`
- Acceptance scenarios:
- `S07`: task=`device-status`; evidence=`Control Plane HTTP status JSON exposes provider health, capacity, in_flight, derived load_ratio, served_models, queued without runtime/resource telemetry.`
- Completion mode: spec-check-on-pass
## Archive Evidence Snapshot
- Archived plan: `agent-task/m-provider-catalog-device-status/03_catalog_status_http/plan_local_G04_0.log`
- Archived review: `agent-task/m-provider-catalog-device-status/03_catalog_status_http/code_review_local_G04_0.log`
- Verdict: FAIL
- Required summary:
- `apps/control-plane/cmd/control-plane/http_views.go:158` uses `json:"load_ratio,omitempty"`, so a valid idle provider with `load_ratio=0` omits the required status field from `/edges/{id}/status` JSON.
- `apps/control-plane/cmd/control-plane/main_test.go:638` already has a second provider fixture with implicit zero load ratio, but raw JSON assertions do not verify that `load_ratio` is present and equals `0`.
- Affected files:
- `apps/control-plane/cmd/control-plane/http_views.go`
- `apps/control-plane/cmd/control-plane/main_test.go`
- Prior verification evidence:
- Implementer recorded `go test ./apps/control-plane/cmd/control-plane -run TestEdgeRegistryHTTPHandlersGetLiveStatus -count=1`, `go test ./apps/control-plane/... -count=1`, and `git diff --check` as passing.
- Review rerun confirmed `go test ./apps/control-plane/cmd/control-plane -run TestEdgeRegistryHTTPHandlersGetLiveStatus -count=1`, `go test ./apps/control-plane/... -count=1`, and `git diff --check` pass before this follow-up.
- Carryover:
- Roadmap Targets and Spec Targets remain unchanged.
- This follow-up still works toward SDD `S07` and Milestone task `device-status`.
- Narrow archive reread allowed if needed:
- `agent-task/m-provider-catalog-device-status/03_catalog_status_http/plan_local_G04_0.log`
- `agent-task/m-provider-catalog-device-status/03_catalog_status_http/code_review_local_G04_0.log`
## 이 파일을 읽는 리뷰 에이전트에게
> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다.
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다.
1. 판정을 append한다.
2. `CODE_REVIEW-local-G03.md` -> `code_review_local_G03_1.log`, `PLAN-local-G03.md` -> `plan_local_G03_1.log`로 아카이브한다.
3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/m-provider-catalog-device-status/03_catalog_status_http/`로 이동한다. WARN/FAIL이면 user-review gate를 확인한 뒤 다음 active plan/review 파일 또는 `USER_REVIEW.md`를 작성한다.
4. PASS이고 task group이 `m-<milestone-slug>`이면 완료 이벤트 메타데이터를 보고한다. roadmap 상태 체크와 `update-roadmap` 호출은 런타임 책임이다.
5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다.
---
## 구현 항목별 완료 여부
| 항목 | 완료 여부 |
|------|---------|
| [REVIEW_API-1] Zero Load Ratio JSON 보존 | [x] |
## 구현 체크리스트
- [x] `apps/control-plane/cmd/control-plane/http_views.go`에서 `providerSnapshotView.LoadRatio` JSON tag를 `json:"load_ratio"`로 바꿔 zero value도 직렬화한다.
- [x] `apps/control-plane/cmd/control-plane/main_test.go`에서 second provider raw JSON assertion에 `load_ratio` key 존재 여부와 `float64(0)` 값을 추가한다.
- [x] `go test ./apps/control-plane/cmd/control-plane -run TestEdgeRegistryHTTPHandlersGetLiveStatus -count=1`를 실행한다.
- [x] `go test ./apps/control-plane/... -count=1`와 `git diff --check`를 실행한다.
- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
## 코드리뷰 전용 체크리스트
> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다.
> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다.
- [x] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
- [x] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다.
- [x] active `CODE_REVIEW-*-G??.md`를 `code_review_local_G03_1.log`로 아카이브한다.
- [x] active `PLAN-*-G??.md`를 `plan_local_G03_1.log`로 아카이브한다.
- [x] `.gitignore`의 Agent-Ops 관리 block이 `agent-task/**/*.md`와 `agent-task/**/*.log`를 unignore하고 `agent-roadmap/current.md`를 ignore하는지 확인한다.
- [x] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다.
- [x] PASS이면 active task 디렉터리 `agent-task/m-provider-catalog-device-status/03_catalog_status_http/`를 `agent-task/archive/YYYY/MM/m-provider-catalog-device-status/03_catalog_status_http/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다.
- [x] PASS이고 task group이 `m-<milestone-slug>`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다.
- [x] PASS split 작업이면 이동 후 빈 active parent `agent-task/m-provider-catalog-device-status/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다.
- [ ] WARN/FAIL이고 user-review gate가 트리거되지 않았으면 다음 active `PLAN-*-G??.md`와 `CODE_REVIEW-*-G??.md`를 작성하고 `complete.log`를 작성하지 않는다.
- [ ] USER_REVIEW이면 `agent-ops/skills/common/code-review/templates/user-review-template.md` 기준으로 `USER_REVIEW.md`를 작성하고 active `PLAN-*.md`, `CODE_REVIEW-*.md`, `complete.log`를 남기지 않는다.
- [ ] USER_REVIEW가 연결된 SDD/Milestone 결정으로 완료/PASS 해소되면 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log`를 작성한 뒤 task directory를 archive로 이동한다.
## 계획 대비 변경 사항
계획에 따라 정확한 2개 파일만 수정했다. 추가 변경 사항 없음.
1. `apps/control-plane/cmd/control-plane/http_views.go`: `LoadRatio` JSON tag에서 `omitempty` 제거
2. `apps/control-plane/cmd/control-plane/main_test.go`: second provider raw JSON에 `load_ratio: 0` assertion 추가
## 주요 설계 결정
없음. 단일 JSON tag 조정만 수행.
## 사용자 리뷰 요청
_기본값은 `없음`이다. 구현 중 새 결정이 필요해 보여도 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 이 섹션은 선택된 SDD 결정 또는 선택된 Milestone `구현 잠금 > 결정 필요` 항목이 실구현을 차단할 때만 채운다. 외부 환경/secret/서비스 준비, 검증 증거 공백, 반복 실패, 일반 범위 조정은 사용자 리뷰 요청이 아니며 `검증 결과`, `계획 대비 변경 사항`, 또는 code-review의 일반 follow-up plan으로 처리한다._
- 상태: 없음
- 사유 유형: 없음
- 연결 대상: 없음
- 결정 필요: 없음
- 차단 근거: 없음
- 실행한 검증/명령: 없음
- 자동 후속 불가 이유: 없음
- 재개 조건: 없음
## 리뷰어를 위한 체크포인트
- `LoadRatio` JSON tag가 `omitempty` 없이 zero value를 직렬화하는지 확인한다.
- raw JSON assertion이 second provider의 `load_ratio: 0` 존재 여부를 검증하는지 확인한다.
- SDD `S07`의 status JSON evidence가 계속 충족되는지 확인한다.
## 검증 결과
_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._
필수 규칙:
- 검증 명령은 고정된 계약이다. 임의로 대체하지 않는다.
- 대체가 필요하면 `계획 대비 변경 사항`에 이유와 대체 명령을 기록한다.
- `검증 결과`에는 실제 stdout/stderr를 붙여 넣는다.
- 사용자 리뷰 요청으로 명령을 끝까지 실행하지 못했다면 `사용자 리뷰 요청`에 실행한 명령, 실제 출력, 미실행 명령의 사유를 기록한다.
- mobile/UI hang, timeout, 또는 2분 무진행은 blind retry를 중단하고 focused rerun 명령과 screenshot/window/UI-tree evidence path를 남기며, 불가능하면 정확한 사유를 남긴다.
### REVIEW_API-1 중간 검증
```text
$ go test ./apps/control-plane/cmd/control-plane -run TestEdgeRegistryHTTPHandlersGetLiveStatus -count=1
ok iop/apps/control-plane/cmd/control-plane 0.008s
```
### 최종 검증
```text
$ go test ./apps/control-plane/... -count=1
ok iop/apps/control-plane/cmd/control-plane 0.148s
ok iop/apps/control-plane/internal/wire 1.487s
$ git diff --check
(no output - no whitespace errors)
```
---
> **[IMPLEMENTING AGENT - BEFORE SAVING] Have you filled in every implementation-owned section: completion table, implementation checklist, changes from plan, design decisions, and verification output?**
> If anything is blank, go back and fill it in before saving this file.
> Leave review-agent-only sections unchanged.
## 섹션 소유권
| Section | Owner | Note |
|---------|-------|------|
| Header comment, 개요, 리뷰 에이전트 지시 | Fixed at stub creation | Implementing agent must not modify or execute these |
| Roadmap Targets | Fixed at stub creation from plan when present | Implementing agent must not modify |
| Spec Targets | Fixed at stub creation from plan when present | Implementing agent must not modify |
| Archive Evidence Snapshot | Fixed at stub creation from follow-up plan | Implementing agent may narrowly reread only the listed archive paths if needed |
| 구현 항목별 완료 여부 | Implementing agent | Check `[ ]` to `[x]` only |
| 구현 체크리스트 | Implementing agent | Check `[ ]` to `[x]` only |
| 코드리뷰 전용 체크리스트 | Review agent only | Implementing agent must not modify |
| 계획 대비 변경 사항, 주요 설계 결정 | Implementing agent | Replace placeholder text with actual content |
| 사용자 리뷰 요청 | Implementing agent | Keep `상태: 없음` unless a selected SDD/Milestone lock decision blocks implementation |
| 리뷰어를 위한 체크포인트 | Fixed at stub creation | Review focus |
| 검증 결과 | Implementing agent | Paste actual command output |
## 코드리뷰 결과
- 종합 판정: PASS
- 차원별 평가:
- Correctness: Pass
- Completeness: Pass
- Test coverage: Pass
- API contract: Pass
- Code quality: Pass
- Plan deviation: Pass
- Verification trust: Pass
- Spec conformance: Pass
- 발견된 문제:
- 없음
- 다음 단계:
- PASS complete.log 작성 후 task directory archive 이동.

View file

@ -0,0 +1,206 @@
<!-- task=m-provider-catalog-device-status/03_catalog_status_http plan=0 tag=API -->
# Code Review Reference - API
> **[IMPLEMENTING AGENT - READ FIRST] Filling in this file is the mandatory final step of implementation.**
> The task is NOT complete until every implementation-owned section below is filled in.
> Complete the `구현 체크리스트`; the final checklist item is mandatory before saving.
> Fill implementation-owned sections, then stop with active files in place and report ready for review.
> If implementation is blocked by a selected SDD decision or selected Milestone `구현 잠금 > 결정 필요` item, fill `사용자 리뷰 요청` with linked evidence and stop with active files in place; code-review decides whether to write `USER_REVIEW.md`. Environment/secret/service blockers, generic scope changes, repeated failures, and evidence gaps that a follow-up agent can close are normal follow-up issues, not user-review blockers by themselves.
> Do not ask the user directly, present choices in chat, or call `request_user_input` during implementation; record only SDD/Milestone lock decisions in `사용자 리뷰 요청` and stop for code-review.
> Finalization (`코드리뷰 결과`, log rename, `complete.log`, archive moves, `코드리뷰 전용 체크리스트`) is review-agent-only, even after compaction/resume.
> Follow the ownership table at the bottom of this file for which sections you own.
## 개요
date=2026-06-20
task=m-provider-catalog-device-status/03_catalog_status_http, plan=0, tag=API
## Roadmap Targets
- Milestone: `agent-roadmap/phase/operational-observability-provider-management/milestones/provider-catalog-device-status.md`
- Task ids:
- `device-status`: provider health, capacity, in-flight, derived load ratio, served models, queued 상태가 catalog/status 표면에 정리된다.
- Completion mode: check-on-pass
## Spec Targets
- SDD: `agent-roadmap/sdd/operational-observability-provider-management/provider-catalog-device-status/SDD.md`
- Acceptance scenarios:
- `S07`: task=`device-status`; evidence=`Control Plane HTTP status JSON exposes provider health, capacity, in_flight, derived load_ratio, served_models, queued without runtime/resource telemetry.`
- Completion mode: spec-check-on-pass
## 이 파일을 읽는 리뷰 에이전트에게
> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다.
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다.
1. 판정을 append한다.
2. `CODE_REVIEW-local-G04.md` -> `code_review_local_G04_0.log`, `PLAN-local-G04.md` -> `plan_local_G04_0.log`로 아카이브한다.
3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/m-provider-catalog-device-status/03_catalog_status_http/`로 이동한다. WARN/FAIL이면 user-review gate를 확인한 뒤 다음 active plan/review 파일 또는 `USER_REVIEW.md`를 작성한다. `USER_REVIEW.md`가 연결된 SDD/Milestone 결정으로 완료/PASS 해소되면 code-review가 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log` 작성 후 archive 이동한다.
4. PASS이고 task group이 `m-<milestone-slug>`이면 완료 이벤트 메타데이터를 보고한다. roadmap 상태 체크와 `update-roadmap` 호출은 런타임 책임이다.
5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다.
---
## 구현 항목별 완료 여부
| 항목 | 완료 여부 |
|------|---------|
| [API-1] Provider Snapshot JSON DTO 추가 | [x] |
| [API-2] Status JSON 회귀 테스트 보강 | [x] |
## 구현 체크리스트
- [x] Control Plane HTTP status DTO에 provider snapshot read-only fields를 추가하고 `EdgeNodeSnapshot.ProviderSnapshots`를 JSON으로 전달한다.
- [x] `/edges/{id}/status` 테스트에 provider snapshot fixture와 JSON assertion을 추가해 `health`, `capacity`, `in_flight`, `load_ratio`, `served_models`, `queued`, `category`, `lifecycle_capabilities`가 보존되는지 확인한다.
- [x] raw config secret non-leak 검증이 provider snapshot 추가 후에도 유지되는지 확인한다.
- [x] `go test ./apps/control-plane/...`를 실행한다.
- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
## 코드리뷰 전용 체크리스트
> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다.
> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다.
- [x] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
- [x] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다.
- [x] active `CODE_REVIEW-*-G??.md`를 `code_review_local_G04_0.log`로 아카이브한다.
- [x] active `PLAN-*-G??.md`를 `plan_local_G04_0.log`로 아카이브한다.
- [x] `.gitignore`의 Agent-Ops 관리 block이 `agent-task/**/*.md`와 `agent-task/**/*.log`를 unignore하고 `agent-roadmap/current.md`를 ignore하는지 확인한다.
- [ ] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다.
- [ ] PASS이면 active task 디렉터리 `agent-task/m-provider-catalog-device-status/03_catalog_status_http/`를 `agent-task/archive/YYYY/MM/m-provider-catalog-device-status/03_catalog_status_http/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다.
- [ ] PASS이고 task group이 `m-<milestone-slug>`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다.
- [ ] PASS split 작업이면 이동 후 빈 active parent `agent-task/m-provider-catalog-device-status/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다.
- [x] WARN/FAIL이고 user-review gate가 트리거되지 않았으면 다음 active `PLAN-local-G03.md`와 `CODE_REVIEW-local-G03.md`를 작성하고 `complete.log`를 작성하지 않는다.
- [ ] USER_REVIEW이면 `agent-ops/skills/common/code-review/templates/user-review-template.md` 기준으로 `USER_REVIEW.md`를 작성하고 active `PLAN-*.md`, `CODE_REVIEW-*.md`, `complete.log`를 남기지 않는다.
- [ ] USER_REVIEW가 연결된 SDD/Milestone 결정으로 완료/PASS 해소되면 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log`를 작성한 뒤 task directory를 archive로 이동한다.
## 계획 대비 변경 사항
계획에 명시된 구현을 그대로 따랐습니다. 추가 변경 사항은 없습니다.
- `providerSnapshotView` struct는 plan의 목표 코드와 동일하게 12개 필드로 추가
- `providerSnapshotViews` helper는 proto getter만 사용하며 nil/empty slice는 nil 반환
- `edgeStatusResponseViewFromProto`는 `n.GetProviderSnapshots()`를 `providerSnapshotViews()`로 변환하여 매핑
- 테스트 fixture는 plan에 명시된 2개 provider snapshot (vllm-gpu, ollama-cpu) 추가
- typed DTO assertion과 raw JSON assertion 모두 plan에 명시된 필드 검증 포함
## 주요 설계 결정
- `providerSnapshotView`는 pointer가 아닌 value slice (`[]providerSnapshotView`)로 정의하여, 빈 경우 `null`이 아닌 `nil` JSON으로 직렬화되도록 함 (omitempty 태그와 함께)
- `ServedModels`와 `LifecycleCapabilities`는 `append([]string(nil), ...)`로 복사하여 원본 슬라이스 수정 방지
- `providerSnapshotViews` helper는 nil 입력 시 nil 반환하여 `json:"provider_snapshots,omitempty"`이 작동하도록 함
## 사용자 리뷰 요청
_기본값은 `없음`이다. 구현 중 새 결정이 필요해 보여도 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 이 섹션은 선택된 SDD 결정 또는 선택된 Milestone `구현 잠금 > 결정 필요` 항목이 실구현을 차단할 때만 채운다. 외부 환경/secret/서비스 준비, 검증 증거 공백, 반복 실패, 일반 범위 조정은 사용자 리뷰 요청이 아니며 `검증 결과`, `계획 대비 변경 사항`, 또는 code-review의 일반 follow-up plan으로 처리한다._
- 상태: 없음
- 사유 유형: 없음
- 연결 대상: 없음
- 결정 필요: 없음
- 차단 근거: 없음
- 실행한 검증/명령: 없음
- 자동 후속 불가 이유: 없음
- 재개 조건: 없음
## 리뷰어를 위한 체크포인트
- `edgeStatusResponseViewFromProto`가 `n.GetProviderSnapshots()`를 누락하지 않는지 확인한다.
- `provider_snapshots` JSON은 read-only status/catalog 필드만 포함하고 Node token/address/raw adapter settings를 노출하지 않는지 확인한다.
- test fixture가 `load_ratio`, `served_models`, `lifecycle_capabilities`까지 검증하는지 확인한다.
## 검증 결과
_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._
필수 규칙:
- 검증 명령은 고정된 계약이다. 임의로 대체하지 않는다.
- 대체가 필요하면 `계획 대비 변경 사항`에 이유와 대체 명령을 기록한다.
- `검증 결과`에는 실제 stdout/stderr를 붙여 넣는다.
- 사용자 리뷰 요청으로 명령을 끝까지 실행하지 못했다면 `사용자 리뷰 요청`에 실행한 명령, 실제 출력, 미실행 명령의 사유를 기록한다.
- mobile/UI hang, timeout, 또는 2분 무진행은 blind retry를 중단하고 focused rerun 명령과 screenshot/window/UI-tree evidence path를 남기며, 불가능하면 정확한 사유를 남긴다.
### API-1 중간 검증
```text
$ go test ./apps/control-plane/cmd/control-plane -run TestEdgeRegistryHTTPHandlersGetLiveStatus -count=1
=== RUN TestEdgeRegistryHTTPHandlersGetLiveStatus
=== RUN TestEdgeRegistryHTTPHandlersGetLiveStatus/Success
=== RUN TestEdgeRegistryHTTPHandlersGetLiveStatus/MissingEdge
=== RUN TestEdgeRegistryHTTPHandlersGetLiveStatus/UpstreamErrorOrTimeout
--- PASS: TestEdgeRegistryHTTPHandlersGetLiveStatus (0.00s)
--- PASS: TestEdgeRegistryHTTPHandlersGetLiveStatus/Success (0.00s)
--- PASS: TestEdgeRegistryHTTPHandlersGetLiveStatus/MissingEdge (0.00s)
--- PASS: TestEdgeRegistryHTTPHandlersGetLiveStatus/UpstreamErrorOrTimeout (0.00s)
PASS
ok iop/apps/control-plane/cmd/control-plane 0.008s
```
### API-2 중간 검증
```text
$ go test ./apps/control-plane/cmd/control-plane -run TestEdgeRegistryHTTPHandlersGetLiveStatus -count=1
=== RUN TestEdgeRegistryHTTPHandlersGetLiveStatus
=== RUN TestEdgeRegistryHTTPHandlersGetLiveStatus/Success
=== RUN TestEdgeRegistryHTTPHandlersGetLiveStatus/MissingEdge
=== RUN TestEdgeRegistryHTTPHandlersGetLiveStatus/UpstreamErrorOrTimeout
--- PASS: TestEdgeRegistryHTTPHandlersGetLiveStatus (0.00s)
--- PASS: TestEdgeRegistryHTTPHandlersGetLiveStatus/Success (0.00s)
--- PASS: TestEdgeRegistryHTTPHandlersGetLiveStatus/MissingEdge (0.00s)
--- PASS: TestEdgeRegistryHTTPHandlersGetLiveStatus/UpstreamErrorOrTimeout (0.00s)
PASS
ok iop/apps/control-plane/cmd/control-plane 0.008s
```
### 최종 검증
```text
$ go test ./apps/control-plane/... -count=1
ok iop/apps/control-plane/cmd/control-plane 0.143s
ok iop/apps/control-plane/internal/wire 1.478s
$ git diff --check
(no output - no whitespace errors)
```
---
> **[IMPLEMENTING AGENT - BEFORE SAVING] Have you filled in every implementation-owned section: completion table, implementation checklist, changes from plan, design decisions, and verification output?**
> If anything is blank, go back and fill it in before saving this file.
> Leave review-agent-only sections unchanged.
## 섹션 소유권
| Section | Owner | Note |
|---------|-------|------|
| Header comment, 개요, 리뷰 에이전트 지시 | Fixed at stub creation | Implementing agent must not modify or execute these |
| Roadmap Targets | Fixed at stub creation from plan when present | Implementing agent must not modify |
| Spec Targets | Fixed at stub creation from plan when present | Implementing agent must not modify |
| 구현 항목별 완료 여부 | Implementing agent | Check `[ ]` to `[x]` only |
| 구현 체크리스트 | Implementing agent | Check `[ ]` to `[x]` only |
| 코드리뷰 전용 체크리스트 | Review agent only | Implementing agent must not modify |
| 계획 대비 변경 사항, 주요 설계 결정 | Implementing agent | Replace placeholder text with actual content |
| 사용자 리뷰 요청 | Implementing agent | Keep `상태: 없음` unless a selected SDD/Milestone lock decision blocks implementation |
| 리뷰어를 위한 체크포인트 | Fixed at stub creation | Review focus |
| 검증 결과 | Implementing agent | Paste actual command output |
## 코드리뷰 결과
- 종합 판정: FAIL
- 차원별 평가:
- Correctness: Fail
- Completeness: Fail
- Test coverage: Fail
- API contract: Fail
- Code quality: Pass
- Plan deviation: Pass
- Verification trust: Pass
- Spec conformance: Fail
- 발견된 문제:
- Required: [apps/control-plane/cmd/control-plane/http_views.go](/config/workspace/iop/apps/control-plane/cmd/control-plane/http_views.go:158)의 `load_ratio,omitempty` 때문에 유효한 idle 상태값인 `load_ratio=0`이 `/edges/{id}/status` JSON에서 누락됩니다. SDD `S07`과 이 plan의 `Spec Targets`는 derived `load_ratio`가 status JSON에 노출되는 것을 요구하므로, `LoadRatio` JSON tag에서 `omitempty`를 제거하고 zero value가 직렬화되는지 보장해야 합니다. [apps/control-plane/cmd/control-plane/main_test.go](/config/workspace/iop/apps/control-plane/cmd/control-plane/main_test.go:638)의 두 번째 provider fixture가 이미 zero-load 케이스지만 raw JSON에서 `load_ratio` 존재 여부를 검증하지 않아 회귀가 잡히지 않습니다. `rawP1["load_ratio"]`가 존재하고 `float64(0)`인지 확인하는 assertion을 추가하세요.
- 다음 단계:
- FAIL 후속 plan/review를 작성한다.

View file

@ -0,0 +1,51 @@
# Complete - m-provider-catalog-device-status/03_catalog_status_http
## 완료 일시
2026-06-20
## 요약
Control Plane provider status JSON 노출 작업을 2개 루프로 완료했다. 최종 판정: PASS.
## 루프 이력
| Plan | Review | Verdict | 메모 |
|------|--------|---------|------|
| `plan_local_G04_0.log` | `code_review_local_G04_0.log` | FAIL | provider snapshot JSON 추가와 테스트 보강은 완료됐으나, `load_ratio=0`이 `omitempty`로 누락되는 API 계약 문제가 발견됐다. |
| `plan_local_G03_1.log` | `code_review_local_G03_1.log` | PASS | `load_ratio` zero value를 JSON에 보존하고 raw JSON regression assertion을 추가했다. |
## 구현/정리 내용
- Control Plane `/edges/{id}/status` node JSON에 `provider_snapshots`를 추가해 provider health, capacity, in-flight, queued, served models, category, lifecycle capabilities, load ratio를 전달한다.
- provider snapshot 변환은 proto getter만 사용하고 string slice는 복사해 HTTP DTO로 격리했다.
- status handler test에 provider snapshot fixture, typed DTO assertion, raw JSON assertion, config/provider snapshot secret non-leak assertion을 추가했다.
- `load_ratio` JSON tag에서 `omitempty`를 제거해 idle provider의 `load_ratio=0`도 명시적으로 노출되도록 했다.
## 최종 검증
- `go test ./apps/control-plane/cmd/control-plane -run TestEdgeRegistryHTTPHandlersGetLiveStatus -count=1` - PASS; `ok iop/apps/control-plane/cmd/control-plane 0.005s`
- `go test ./apps/control-plane/... -count=1` - PASS; `ok iop/apps/control-plane/cmd/control-plane 0.141s`, `ok iop/apps/control-plane/internal/wire 1.487s`
- `git diff --check` - PASS; no output.
## Roadmap Completion
- Milestone: `agent-roadmap/phase/operational-observability-provider-management/milestones/provider-catalog-device-status.md`
- Completed task ids:
- `device-status`: PASS; evidence=`agent-task/archive/2026/06/m-provider-catalog-device-status/03_catalog_status_http/plan_local_G03_1.log`, `agent-task/archive/2026/06/m-provider-catalog-device-status/03_catalog_status_http/code_review_local_G03_1.log`; verification=`go test ./apps/control-plane/cmd/control-plane -run TestEdgeRegistryHTTPHandlersGetLiveStatus -count=1`, `go test ./apps/control-plane/... -count=1`, `git diff --check`
- Not completed task ids: 없음
## Spec Completion
- SDD: `agent-roadmap/sdd/operational-observability-provider-management/provider-catalog-device-status/SDD.md`
- Completed scenario ids:
- `S07`: PASS; task=`device-status`; evidence=`agent-task/archive/2026/06/m-provider-catalog-device-status/03_catalog_status_http/plan_local_G03_1.log`, `agent-task/archive/2026/06/m-provider-catalog-device-status/03_catalog_status_http/code_review_local_G03_1.log`; verification=`go test ./apps/control-plane/cmd/control-plane -run TestEdgeRegistryHTTPHandlersGetLiveStatus -count=1`, `go test ./apps/control-plane/... -count=1`, `git diff --check`
- Not completed scenario ids: 없음
## 잔여 Nit
- 없음
## 후속 작업
- 없음

View file

@ -0,0 +1,97 @@
<!-- task=m-provider-catalog-device-status/03_catalog_status_http plan=1 tag=REVIEW_API -->
# PLAN REVIEW_API - Preserve Zero Load Ratio In Status JSON
## 이 파일을 읽는 구현 에이전트에게
이 follow-up plan은 직전 코드리뷰의 Required 1건만 해결한다. 구현은 active `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션 작성까지 끝나야 완료다. 코드 변경 후 검증 명령을 실행하고 실제 stdout/stderr를 review stub에 붙인 뒤 active 파일을 그대로 두고 리뷰 준비를 보고한다.
구현 중 선택된 SDD 결정 또는 선택된 Milestone `구현 잠금 > 결정 필요` 항목이 막으면 review stub의 `사용자 리뷰 요청` 섹션에 연결 대상, 차단 근거, 실행한 명령, 재개 조건을 기록하고 멈춘다. 직접 사용자에게 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 환경, secret, service 준비, 일반 범위 변경, 증거 공백은 사용자 리뷰 요청이 아니라 검증 결과 또는 후속 plan 대상으로 기록한다.
## Roadmap Targets
- Milestone: `agent-roadmap/phase/operational-observability-provider-management/milestones/provider-catalog-device-status.md`
- Task ids:
- `device-status`: provider health, capacity, in-flight, derived load ratio, served models, queued 상태가 catalog/status 표면에 정리된다.
- Completion mode: check-on-pass
## Spec Targets
- SDD: `agent-roadmap/sdd/operational-observability-provider-management/provider-catalog-device-status/SDD.md`
- Acceptance scenarios:
- `S07`: task=`device-status`; evidence=`Control Plane HTTP status JSON exposes provider health, capacity, in_flight, derived load_ratio, served_models, queued without runtime/resource telemetry.`
- Completion mode: spec-check-on-pass
## Archive Evidence Snapshot
- Archived plan: `agent-task/m-provider-catalog-device-status/03_catalog_status_http/plan_local_G04_0.log`
- Archived review: `agent-task/m-provider-catalog-device-status/03_catalog_status_http/code_review_local_G04_0.log`
- Verdict: FAIL
- Required summary:
- `apps/control-plane/cmd/control-plane/http_views.go:158` uses `json:"load_ratio,omitempty"`, so a valid idle provider with `load_ratio=0` omits the required status field from `/edges/{id}/status` JSON.
- `apps/control-plane/cmd/control-plane/main_test.go:638` already has a second provider fixture with implicit zero load ratio, but raw JSON assertions do not verify that `load_ratio` is present and equals `0`.
- Affected files:
- `apps/control-plane/cmd/control-plane/http_views.go`
- `apps/control-plane/cmd/control-plane/main_test.go`
- Prior verification evidence:
- Implementer recorded `go test ./apps/control-plane/cmd/control-plane -run TestEdgeRegistryHTTPHandlersGetLiveStatus -count=1`, `go test ./apps/control-plane/... -count=1`, and `git diff --check` as passing.
- Review rerun confirmed `go test ./apps/control-plane/cmd/control-plane -run TestEdgeRegistryHTTPHandlersGetLiveStatus -count=1`, `go test ./apps/control-plane/... -count=1`, and `git diff --check` pass before this follow-up.
- Carryover:
- Roadmap Targets and Spec Targets remain unchanged.
- This follow-up still works toward SDD `S07` and Milestone task `device-status`.
- Narrow archive reread allowed if needed:
- `agent-task/m-provider-catalog-device-status/03_catalog_status_http/plan_local_G04_0.log`
- `agent-task/m-provider-catalog-device-status/03_catalog_status_http/code_review_local_G04_0.log`
## 분석 결과
### 빌드 등급
- lane/grade: `local-G03`.
- 근거: 단일 JSON tag 조정과 기존 HTTP handler test assertion 보강이다. 로컬 Go test와 `git diff --check`로 결정적으로 검증 가능하다.
### 범위 결정 근거
- 포함: `LoadRatio` zero value가 Control Plane HTTP JSON에서 보존되도록 DTO tag를 수정한다.
- 포함: 기존 status handler success fixture의 second provider를 사용해 raw JSON에 `load_ratio: 0`이 존재하는지 확인한다.
- 제외: provider queue/status 계산, proto 원본/생성물, Client DTO/UI, Edge CLI 표시. 이번 Required와 직접 관련이 없다.
## 구현 체크리스트
- [ ] `apps/control-plane/cmd/control-plane/http_views.go`에서 `providerSnapshotView.LoadRatio` JSON tag를 `json:"load_ratio"`로 바꿔 zero value도 직렬화한다.
- [ ] `apps/control-plane/cmd/control-plane/main_test.go`에서 second provider raw JSON assertion에 `load_ratio` key 존재 여부와 `float64(0)` 값을 추가한다.
- [ ] `go test ./apps/control-plane/cmd/control-plane -run TestEdgeRegistryHTTPHandlersGetLiveStatus -count=1`를 실행한다.
- [ ] `go test ./apps/control-plane/... -count=1`와 `git diff --check`를 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
### [REVIEW_API-1] Zero Load Ratio JSON 보존
#### 문제
`ProviderSnapshot.LoadRatio`의 `0`은 idle provider의 정상 상태값이다. 현재 Control Plane HTTP DTO는 `json:"load_ratio,omitempty"`를 사용해 `0`을 JSON에서 생략한다. 이러면 SDD `S07`이 요구한 derived `load_ratio` 노출이 idle provider에서 깨지고, Client는 `0`과 필드 없음/unknown을 구분할 수 없다.
#### 해결 방법
`LoadRatio` JSON tag에서 `omitempty`를 제거한다. 기존 테스트 fixture의 second provider는 `LoadRatio`를 명시하지 않아 zero value 케이스로 쓸 수 있으므로, raw JSON assertion에서 `rawP1["load_ratio"]`가 존재하고 `float64(0)`인지 검증한다.
#### 수정 파일 및 체크리스트
- [ ] `apps/control-plane/cmd/control-plane/http_views.go`: `LoadRatio float32 `json:"load_ratio"``
- [ ] `apps/control-plane/cmd/control-plane/main_test.go`: `rawP1` assertion에 `load_ratio` 존재/값 검증 추가.
#### 중간 검증
```bash
go test ./apps/control-plane/cmd/control-plane -run TestEdgeRegistryHTTPHandlersGetLiveStatus -count=1
```
기대 결과: status handler success/missing/upstream error subtest가 모두 통과하고, zero `load_ratio` raw JSON assertion도 통과한다.
## 최종 검증
```bash
go test ./apps/control-plane/... -count=1
git diff --check
```
기대 결과: Control Plane Go tests가 통과하고 공백 오류가 없다. Go test cache output은 허용하지 않는다.

View file

@ -0,0 +1,293 @@
<!-- task=m-provider-catalog-device-status/03_catalog_status_http plan=0 tag=API -->
# PLAN API - Control Plane Provider Status JSON
## 이 파일을 읽는 구현 에이전트에게
이 plan의 구현은 active `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션 작성까지 끝나야 완료다. 코드 변경 후 검증 명령을 실행하고 실제 stdout/stderr를 review stub에 붙인 뒤 active 파일을 그대로 두고 리뷰 준비를 보고한다. 최종 판정, log rename, `complete.log`, archive 이동은 code-review 스킬 전용이다.
구현 중 선택된 SDD 결정 또는 선택된 Milestone `구현 잠금 > 결정 필요` 항목이 막으면 review stub의 `사용자 리뷰 요청` 섹션에 연결 대상, 차단 근거, 실행한 명령, 재개 조건을 기록하고 멈춘다. 직접 사용자에게 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 환경, secret, service 준비, 일반 범위 변경, 증거 공백은 사용자 리뷰 요청이 아니라 검증 결과 또는 후속 plan 대상으로 기록한다.
## 배경
Edge wire/proto에는 `EdgeNodeSnapshot.provider_snapshots`와 `ProviderSnapshot`의 catalog/status 필드가 이미 있다. 그러나 Control Plane HTTP `/edges/{id}/status` 변환은 node의 config summary만 내보내고 provider snapshots를 버린다. 이 때문에 Edge-owned read-only provider status가 Control Plane과 Client 표면으로 전달되지 않아 `device-status`를 닫을 수 없다.
## 사용자 리뷰 요청 흐름
사용자 리뷰 요청은 선택된 SDD 결정 또는 선택된 Milestone lock decision만 active review stub의 `사용자 리뷰 요청` 섹션에 기록한다. 구현 중 직접 사용자 프롬프트는 금지하며, code-review가 정당성을 검증하고 실제 `USER_REVIEW.md` 작성 여부를 결정한다.
## Roadmap Targets
- Milestone: `agent-roadmap/phase/operational-observability-provider-management/milestones/provider-catalog-device-status.md`
- Task ids:
- `device-status`: provider health, capacity, in-flight, derived load ratio, served models, queued 상태가 catalog/status 표면에 정리된다.
- Completion mode: check-on-pass
## Spec Targets
- SDD: `agent-roadmap/sdd/operational-observability-provider-management/provider-catalog-device-status/SDD.md`
- Acceptance scenarios:
- `S07`: task=`device-status`; evidence=`Control Plane HTTP status JSON exposes provider health, capacity, in_flight, derived load_ratio, served_models, queued without runtime/resource telemetry.`
- Completion mode: spec-check-on-pass
## 분석 결과
### 읽은 파일
- `AGENTS.md`
- `agent-ops/rules/project/rules.md`
- `agent-ops/rules/common/rules-roadmap.md`
- `agent-ops/skills/common/router.md`
- `agent-ops/skills/common/plan/SKILL.md`
- `agent-ops/skills/common/update-roadmap/SKILL.md`
- `agent-ops/skills/common/_templates/implementation-user-review-request-section.md`
- `agent-roadmap/current.md`
- `agent-roadmap/phase/operational-observability-provider-management/PHASE.md`
- `agent-roadmap/phase/operational-observability-provider-management/milestones/provider-catalog-device-status.md`
- `agent-roadmap/sdd/operational-observability-provider-management/provider-catalog-device-status/SDD.md`
- `agent-ops/rules/project/domain/control-plane/rules.md`
- `agent-ops/rules/project/domain/platform-common/rules.md`
- `agent-ops/rules/project/domain/testing/rules.md`
- `agent-test/local/rules.md`
- `agent-test/local/control-plane-smoke.md`
- `agent-test/local/platform-common-smoke.md`
- `proto/iop/control.proto`
- `proto/gen/iop/control.pb.go`
- `proto/iop/runtime.proto`
- `proto/gen/iop/runtime.pb.go`
- `apps/control-plane/cmd/control-plane/http_views.go`
- `apps/control-plane/cmd/control-plane/main_test.go`
- `apps/edge/internal/controlplane/connector.go`
- `apps/edge/internal/service/status_provider.go`
- `apps/edge/internal/service/model_queue.go`
- `apps/edge/internal/service/status_provider_test.go`
### 테스트 환경 규칙
- test_env: `local`.
- `agent-test/local/rules.md`를 읽었다. Go quick check는 현재 local checkout에서 실행하고, Docker compose가 필요한 검증은 원격 runner가 필요하다는 규칙을 확인했다.
- 매칭 profile: `agent-test/local/control-plane-smoke.md`를 읽었다. 적용 명령은 `go test ./apps/control-plane/...`다.
- 참고 profile: `agent-test/local/platform-common-smoke.md`를 읽었다. 이번 plan은 proto 원본/생성물 변경 없이 기존 generated field를 소비하므로 `make proto`는 요구하지 않는다.
- HTTP lifecycle 자체를 새 endpoint로 확장하지 않고 기존 `/edges/{id}/status` JSON DTO를 보강한다. `/healthz`, `/readyz`, `make test-control-plane-edge-wire`는 smoke 후보지만 이 plan의 필수 최종 검증은 대상 Go 패키지 테스트로 둔다.
### 테스트 커버리지 공백
- 현재 `TestEdgeRegistryHTTPHandlersGetLiveStatus`는 node config summary와 secret non-leak만 검증하고 provider snapshots JSON을 검증하지 않는다.
- `edgeStatusResponseViewFromProto`가 `EdgeNodeSnapshot.ProviderSnapshots`를 누락해도 현재 테스트가 실패하지 않는다.
- Edge internal status coverage는 `apps/edge/internal/service/status_provider_test.go`에서 provider `Capacity`, `InFlight`, `Queued`, `LoadRatio`까지 검증되어 있다. 이번 plan은 Control Plane JSON 변환의 회귀 테스트만 추가한다.
### 심볼 참조
- renamed/removed symbols: none.
- 새 internal DTO 후보: `providerSnapshotView`.
- 변경 call site: `edgeStatusResponseViewFromProto` 내부 node mapping only.
### 분할 판단
- split decision policy를 평가했다.
- 공유 task group: `agent-task/m-provider-catalog-device-status/`.
- `03_catalog_status_http`: Control Plane HTTP DTO/API test. dependency: none.
- `04+03_client_catalog_view`: Client DTO/UI 소비. dependency: `03_catalog_status_http` complete.log.
- 분할 이유: Control Plane JSON 계약이 Client UI의 입력 계약이므로 API surface를 먼저 닫고, Client는 그 JSON shape에 의존해 별도 검증한다. Go HTTP tests와 Flutter widget tests는 서로 다른 runner/리스크를 가진다.
### 범위 결정 근거
- 포함: `apps/control-plane/cmd/control-plane/http_views.go`, `apps/control-plane/cmd/control-plane/main_test.go`.
- 제외: `proto/iop/*.proto`와 `proto/gen/**`. 이미 `EdgeNodeSnapshot.provider_snapshots`와 `ProviderSnapshot` generated getters가 있으므로 계약 변경이 아니다.
- 제외: Edge queue/status 계산. `apps/edge/internal/service/model_queue.go`와 status tests가 이미 provider stats를 만든다.
- 제외: Client DTO/UI. dependent plan `04+03_client_catalog_view`에서 처리한다.
- 제외: runtime control, resource telemetry, qualification report. SDD D05-D06과 Milestone 범위 제외에 따라 후속 범위다.
### 빌드 등급
- lane/grade: `local-G04`.
- 근거: 단일 Go app HTTP DTO 변환과 대상 테스트 보강이다. 기존 proto/generated 타입이 있어 public wire contract 변경은 없고, `go test ./apps/control-plane/...`로 결정적으로 검증 가능하다.
## 구현 체크리스트
- [ ] Control Plane HTTP status DTO에 provider snapshot read-only fields를 추가하고 `EdgeNodeSnapshot.ProviderSnapshots`를 JSON으로 전달한다.
- [ ] `/edges/{id}/status` 테스트에 provider snapshot fixture와 JSON assertion을 추가해 `health`, `capacity`, `in_flight`, `load_ratio`, `served_models`, `queued`, `category`, `lifecycle_capabilities`가 보존되는지 확인한다.
- [ ] raw config secret non-leak 검증이 provider snapshot 추가 후에도 유지되는지 확인한다.
- [ ] `go test ./apps/control-plane/...`를 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
### [API-1] Provider Snapshot JSON DTO 추가
#### 문제
[apps/control-plane/cmd/control-plane/http_views.go](/config/workspace/iop/apps/control-plane/cmd/control-plane/http_views.go:138)에서 `edgeNodeSnapshotView`가 `NodeID`, `Alias`, `Label`, `Connected`, `Config`만 가진다. [apps/control-plane/cmd/control-plane/http_views.go](/config/workspace/iop/apps/control-plane/cmd/control-plane/http_views.go:191)의 `edgeStatusResponseViewFromProto`도 `n.GetProviderSnapshots()`를 읽지 않아 Edge가 보낸 provider status가 HTTP JSON에서 사라진다.
현재 코드:
```go
138 type edgeNodeSnapshotView struct {
139 NodeID string `json:"node_id"`
140 Alias string `json:"alias"`
141 Label string `json:"label"`
142 Connected bool `json:"connected"`
143 Config *nodeConfigSummaryView `json:"config,omitempty"`
144 }
...
191 func edgeStatusResponseViewFromProto(resp *iop.EdgeStatusResponse) edgeStatusResponseView {
192 nodes := make([]edgeNodeSnapshotView, 0, len(resp.GetNodes()))
193 for _, n := range resp.GetNodes() {
194 nodes = append(nodes, edgeNodeSnapshotView{
195 NodeID: n.GetNodeId(),
196 Alias: n.GetAlias(),
197 Label: n.GetLabel(),
198 Connected: n.GetConnected(),
199 Config: newNodeConfigSummaryView(n.GetConfig()),
200 })
201 }
```
#### 해결 방법
`providerSnapshotView`를 `http_views.go`에 추가하고 `edgeNodeSnapshotView`에 `ProviderSnapshots []providerSnapshotView 'json:"provider_snapshots,omitempty"'`를 둔다. 변환 helper는 proto getters만 사용하고 nil/empty는 nil을 반환한다.
목표 코드:
```go
type providerSnapshotView struct {
Adapter string `json:"adapter,omitempty"`
Status string `json:"status,omitempty"`
Capacity int32 `json:"capacity"`
InFlight int32 `json:"in_flight"`
Queued int32 `json:"queued"`
ID string `json:"id,omitempty"`
Type string `json:"type,omitempty"`
Category string `json:"category,omitempty"`
ServedModels []string `json:"served_models,omitempty"`
Health string `json:"health,omitempty"`
LoadRatio float32 `json:"load_ratio,omitempty"`
LifecycleCapabilities []string `json:"lifecycle_capabilities,omitempty"`
}
func providerSnapshotViews(snaps []*iop.ProviderSnapshot) []providerSnapshotView {
if len(snaps) == 0 {
return nil
}
views := make([]providerSnapshotView, 0, len(snaps))
for _, p := range snaps {
views = append(views, providerSnapshotView{
Adapter: p.GetAdapter(),
Status: p.GetStatus(),
Capacity: p.GetCapacity(),
InFlight: p.GetInFlight(),
Queued: p.GetQueued(),
ID: p.GetId(),
Type: p.GetType(),
Category: p.GetCategory(),
ServedModels: append([]string(nil), p.GetServedModels()...),
Health: p.GetHealth(),
LoadRatio: p.GetLoadRatio(),
LifecycleCapabilities: append([]string(nil), p.GetLifecycleCapabilities()...),
})
}
return views
}
```
`edgeStatusResponseViewFromProto`의 node append에는 `ProviderSnapshots: providerSnapshotViews(n.GetProviderSnapshots())`를 추가한다.
#### 수정 파일 및 체크리스트
- [ ] `apps/control-plane/cmd/control-plane/http_views.go`: DTO struct와 helper 추가.
- [ ] `apps/control-plane/cmd/control-plane/http_views.go`: `edgeStatusResponseViewFromProto` node mapping에 provider snapshots 추가.
- [ ] `apps/control-plane/cmd/control-plane/main_test.go`: JSON shape assertion 추가.
#### 테스트 작성
- 작성: `apps/control-plane/cmd/control-plane/main_test.go`의 `TestEdgeRegistryHTTPHandlersGetLiveStatus/Success`에 provider snapshot fixture와 typed/raw assertion을 추가한다.
#### 중간 검증
```bash
go test ./apps/control-plane/cmd/control-plane -run TestEdgeRegistryHTTPHandlersGetLiveStatus -count=1
```
기대 결과: provider snapshot JSON assertion과 기존 secret non-leak assertion이 모두 통과한다.
### [API-2] Status JSON 회귀 테스트 보강
#### 문제
[apps/control-plane/cmd/control-plane/main_test.go](/config/workspace/iop/apps/control-plane/cmd/control-plane/main_test.go:444)의 fixture는 `EdgeStatusResponse.Nodes[0]`에 config만 넣고 provider snapshots를 넣지 않는다. 그래서 `provider_snapshots` 누락이 회귀로 잡히지 않는다.
현재 fixture 일부:
```go
444 return &iop.EdgeStatusResponse{
445 RequestId: "req-123",
446 EdgeId: "edge-a",
447 EdgeName: "Edge A",
448 ObservedTimeUnixNano: connectedAt.UnixNano(),
449 Nodes: []*iop.EdgeNodeSnapshot{
450 {
451 NodeId: "node-1",
452 Alias: "alpha",
453 Label: "node0",
454 Connected: true,
455 Config: &iop.NodeConfigPayload{
```
#### 해결 방법
fixture에 provider snapshot을 추가하고 typed DTO와 raw JSON 모두 확인한다. raw assertion은 `provider_snapshots` 키가 있고 secret/config internals가 들어가지 않는다는 기존 검증과 공존해야 한다.
목표 fixture:
```go
ProviderSnapshots: []*iop.ProviderSnapshot{
{
Adapter: "vllm-gpu",
Status: "available",
Capacity: 4,
InFlight: 1,
Queued: 2,
Id: "provider-vllm-primary",
Type: "vllm",
Category: "api",
ServedModels: []string{"nvidia/Qwen3.6-35B-A3B-NVFP4"},
Health: "available",
LoadRatio: 0.25,
LifecycleCapabilities: []string{"list_models", "load_model"},
},
},
```
Assert `node.ProviderSnapshots[0]` preserves all fields, and raw JSON has `provider_snapshots[0].load_ratio == 0.25`, `served_models[0]`, `lifecycle_capabilities[0]`.
#### 수정 파일 및 체크리스트
- [ ] `apps/control-plane/cmd/control-plane/main_test.go`: fixture에 provider snapshot 추가.
- [ ] `apps/control-plane/cmd/control-plane/main_test.go`: typed DTO assertion 추가.
- [ ] `apps/control-plane/cmd/control-plane/main_test.go`: raw JSON key assertion 추가.
#### 테스트 작성
- 작성: 위 test에 직접 추가한다. 별도 새 test보다 기존 status handler 성공 케이스에 붙이는 편이 HTTP view 계약을 한 곳에서 검증한다.
#### 중간 검증
```bash
go test ./apps/control-plane/cmd/control-plane -run TestEdgeRegistryHTTPHandlersGetLiveStatus -count=1
```
기대 결과: status handler 성공/누락/502 subtest가 모두 통과한다.
## 수정 파일 요약
| 파일 | 항목 |
|------|------|
| `apps/control-plane/cmd/control-plane/http_views.go` | API-1 |
| `apps/control-plane/cmd/control-plane/main_test.go` | API-2 |
## 최종 검증
```bash
go test ./apps/control-plane/... -count=1
git diff --check
```
기대 결과: Go control-plane tests가 통과하고 공백 오류가 없다. Go test cache output은 허용하지 않는다. 모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.

View file

@ -0,0 +1,172 @@
<!-- task=m-provider-catalog-device-status/04+03_client_catalog_view plan=0 tag=API -->
# Code Review Reference - API
> **[IMPLEMENTING AGENT - READ FIRST] Filling in this file is the mandatory final step of implementation.**
> The task is NOT complete until every implementation-owned section below is filled in.
> Complete the `구현 체크리스트`; the final checklist item is mandatory before saving.
> Fill implementation-owned sections, then stop with active files in place and report ready for review.
> If implementation is blocked by a selected SDD decision or selected Milestone `구현 잠금 > 결정 필요` item, fill `사용자 리뷰 요청` with linked evidence and stop with active files in place; code-review decides whether to write `USER_REVIEW.md`. Environment/secret/service blockers, generic scope changes, repeated failures, and evidence gaps that a follow-up agent can close are normal follow-up issues, not user-review blockers by themselves.
> Do not ask the user directly, present choices in chat, or call `request_user_input` during implementation; record only SDD/Milestone lock decisions in `사용자 리뷰 요청` and stop for code-review.
> Finalization (`코드리뷰 결과`, log rename, `complete.log`, archive moves, `코드리뷰 전용 체크리스트`) is review-agent-only, even after compaction/resume.
> Follow the ownership table at the bottom of this file for which sections you own.
## 개요
date=2026-06-20
task=m-provider-catalog-device-status/04+03_client_catalog_view, plan=0, tag=API
## Roadmap Targets
- Milestone: `agent-roadmap/phase/operational-observability-provider-management/milestones/provider-catalog-device-status.md`
- Task ids:
- `ops-review`: Edge config와 Edge-owned read-only catalog/status를 기준으로 `iop-edge` CLI, Control Plane, Client가 어떤 정보를 소비하는지 확인할 수 있다.
- Completion mode: check-on-pass
## Spec Targets
- SDD: `agent-roadmap/sdd/operational-observability-provider-management/provider-catalog-device-status/SDD.md`
- Acceptance scenarios:
- `S10`: task=`ops-review`; evidence=`Edge CLI configured-provider summary, Control Plane provider status JSON, Client Nodes panel provider catalog/status rendering, and resolved user_review_0.log are present.`
- Completion mode: spec-check-on-pass
## 이 파일을 읽는 리뷰 에이전트에게
> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다.
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다.
1. 판정을 append한다.
2. `CODE_REVIEW-local-G05.md` -> `code_review_local_G05_0.log`, `PLAN-local-G05.md` -> `plan_local_G05_0.log`로 아카이브한다.
3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/m-provider-catalog-device-status/04+03_client_catalog_view/`로 이동한다. WARN/FAIL이면 user-review gate를 확인한 뒤 다음 active plan/review 파일 또는 `USER_REVIEW.md`를 작성한다. `USER_REVIEW.md`가 연결된 SDD/Milestone 결정으로 완료/PASS 해소되면 code-review가 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log` 작성 후 archive 이동한다.
4. PASS이고 task group이 `m-<milestone-slug>`이면 완료 이벤트 메타데이터를 보고한다. roadmap 상태 체크와 `update-roadmap` 호출은 런타임 책임이다.
5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다.
---
## 구현 항목별 완료 여부
| 항목 | 완료 여부 |
|------|---------|
| [API-1] Client Provider Snapshot DTO 추가 | [ ] |
| [API-2] Nodes Panel Provider Catalog 표시 | [ ] |
| [API-3] Ops Review Evidence 연결 | [ ] |
## 구현 체크리스트
- [ ] 선행 task `agent-task/m-provider-catalog-device-status/03_catalog_status_http/complete.log` 또는 matching archive complete.log가 존재하는지 확인하고, 없으면 구현하지 말고 dependency missing으로 기록한다.
- [ ] Client HTTP DTO에 `ProviderSnapshotView``EdgeNodeSnapshotView.providerSnapshots` parsing을 추가한다.
- [ ] Nodes panel에 read-only Provider Catalog/Status 섹션을 추가해 provider id/type/category, health/status, capacity, in-flight, queued, load ratio, served models, lifecycle capabilities를 표시한다.
- [ ] Fake status repository와 widget test fixture에 provider snapshot을 추가하고 Nodes panel test가 provider catalog/status 표시를 검증하도록 보강한다.
- [ ] `cd apps/client && flutter test`를 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
## 코드리뷰 전용 체크리스트
> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다.
> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다.
- [ ] `코드리뷰 결과``PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
- [ ] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다.
- [ ] active `CODE_REVIEW-*-G??.md``code_review_local_G05_0.log`로 아카이브한다.
- [ ] active `PLAN-*-G??.md``plan_local_G05_0.log`로 아카이브한다.
- [ ] `.gitignore`의 Agent-Ops 관리 block이 `agent-task/**/*.md``agent-task/**/*.log`를 unignore하고 `agent-roadmap/current.md`를 ignore하는지 확인한다.
- [ ] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다.
- [ ] PASS이면 active task 디렉터리 `agent-task/m-provider-catalog-device-status/04+03_client_catalog_view/``agent-task/archive/YYYY/MM/m-provider-catalog-device-status/04+03_client_catalog_view/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다.
- [ ] PASS이고 task group이 `m-<milestone-slug>`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다.
- [ ] PASS split 작업이면 이동 후 빈 active parent `agent-task/m-provider-catalog-device-status/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다.
- [ ] WARN/FAIL이고 user-review gate가 트리거되지 않았으면 다음 active `PLAN-local-G05.md``CODE_REVIEW-local-G05.md`를 작성하고 `complete.log`를 작성하지 않는다.
- [ ] USER_REVIEW이면 `agent-ops/skills/common/code-review/templates/user-review-template.md` 기준으로 `USER_REVIEW.md`를 작성하고 active `PLAN-*.md`, `CODE_REVIEW-*.md`, `complete.log`를 남기지 않는다.
- [ ] USER_REVIEW가 연결된 SDD/Milestone 결정으로 완료/PASS 해소되면 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log`를 작성한 뒤 task directory를 archive로 이동한다.
## 계획 대비 변경 사항
_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._
## 주요 설계 결정
_구현 에이전트가 주요 설계 결정 사항을 기록한다._
## 사용자 리뷰 요청
_기본값은 `없음`이다. 구현 중 새 결정이 필요해 보여도 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 이 섹션은 선택된 SDD 결정 또는 선택된 Milestone `구현 잠금 > 결정 필요` 항목이 실구현을 차단할 때만 채운다. 외부 환경/secret/서비스 준비, 검증 증거 공백, 반복 실패, 일반 범위 조정은 사용자 리뷰 요청이 아니며 `검증 결과`, `계획 대비 변경 사항`, 또는 code-review의 일반 follow-up plan으로 처리한다._
- 상태: 없음
- 사유 유형: 없음
- 연결 대상: 없음
- 결정 필요: 없음
- 차단 근거: 없음
- 실행한 검증/명령: 없음
- 자동 후속 불가 이유: 없음
- 재개 조건: 없음
## 리뷰어를 위한 체크포인트
- 구현 전 `03_catalog_status_http` complete.log가 확인되었는지 본다.
- DTO parsing이 `capacity`, `in_flight`, `queued`, `load_ratio` numeric 값을 안전하게 처리하는지 확인한다.
- Nodes panel이 provider catalog/status를 표시하되 runtime control UI를 추가하지 않았는지 확인한다.
- `ops-review` evidence에 Edge CLI, Control Plane, Client, `user_review_0.log` 연결이 남았는지 확인한다.
## 검증 결과
_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._
필수 규칙:
- 검증 명령은 고정된 계약이다. 임의로 대체하지 않는다.
- 대체가 필요하면 `계획 대비 변경 사항`에 이유와 대체 명령을 기록한다.
- `검증 결과`에는 실제 stdout/stderr를 붙여 넣는다.
- 사용자 리뷰 요청으로 명령을 끝까지 실행하지 못했다면 `사용자 리뷰 요청`에 실행한 명령, 실제 출력, 미실행 명령의 사유를 기록한다.
- mobile/UI hang, timeout, 또는 2분 무진행은 blind retry를 중단하고 focused rerun 명령과 screenshot/window/UI-tree evidence path를 남기며, 불가능하면 정확한 사유를 남긴다.
### API-1 중간 검증
```text
$ cd apps/client && flutter test test/widget_test.dart
(output)
```
### API-2 중간 검증
```text
$ cd apps/client && flutter test test/widget_test.dart
(output)
```
### API-3 중간 검증
```text
$ test -f agent-task/m-provider-catalog-device-status/03_catalog_status_http/complete.log || find agent-task/archive -path '*/m-provider-catalog-device-status/03_catalog_status_http/complete.log' -print -quit
(output)
```
### 최종 검증
```text
$ cd apps/client && flutter test
(output)
$ git diff --check
(output)
```
---
> **[IMPLEMENTING AGENT - BEFORE SAVING] Have you filled in every implementation-owned section: completion table, implementation checklist, changes from plan, design decisions, and verification output?**
> If anything is blank, go back and fill it in before saving this file.
> Leave review-agent-only sections unchanged.
## 섹션 소유권
| Section | Owner | Note |
|---------|-------|------|
| Header comment, 개요, 리뷰 에이전트 지시 | Fixed at stub creation | Implementing agent must not modify or execute these |
| Roadmap Targets | Fixed at stub creation from plan when present | Implementing agent must not modify |
| Spec Targets | Fixed at stub creation from plan when present | Implementing agent must not modify |
| 구현 항목별 완료 여부 | Implementing agent | Check `[ ]` to `[x]` only |
| 구현 체크리스트 | Implementing agent | Check `[ ]` to `[x]` only |
| 코드리뷰 전용 체크리스트 | Review agent only | Implementing agent must not modify |
| 계획 대비 변경 사항, 주요 설계 결정 | Implementing agent | Replace placeholder text with actual content |
| 사용자 리뷰 요청 | Implementing agent | Keep `상태: 없음` unless a selected SDD/Milestone lock decision blocks implementation |
| 리뷰어를 위한 체크포인트 | Fixed at stub creation | Review focus |
| 검증 결과 | Implementing agent | Paste actual command output |

View file

@ -0,0 +1,335 @@
<!-- task=m-provider-catalog-device-status/04+03_client_catalog_view plan=0 tag=API -->
# PLAN API - Client Provider Catalog View
## 이 파일을 읽는 구현 에이전트에게
이 plan은 `04+03_client_catalog_view` dependent split task다. 같은 task group의 `03_catalog_status_http``complete.log`를 만들기 전에는 구현을 시작하지 않는다. 구현이 끝나도 active `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션 작성까지 끝나야 완료다. 검증 명령의 실제 stdout/stderr를 review stub에 붙이고 active 파일을 그대로 둔 뒤 리뷰 준비를 보고한다. 최종 판정, log rename, `complete.log`, archive 이동은 code-review 스킬 전용이다.
구현 중 선택된 SDD 결정 또는 선택된 Milestone `구현 잠금 > 결정 필요` 항목이 막으면 review stub의 `사용자 리뷰 요청` 섹션에 연결 대상, 차단 근거, 실행한 명령, 재개 조건을 기록하고 멈춘다. 직접 사용자에게 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 환경, secret, service 준비, 일반 범위 변경, 증거 공백은 사용자 리뷰 요청이 아니라 검증 결과 또는 후속 plan 대상으로 기록한다.
## 배경
Control Plane Client는 `/edges/{id}/status` JSON을 소비하지만 `EdgeNodeSnapshotView`가 provider snapshots를 파싱하지 않는다. Nodes panel도 node config adapters만 보여 주므로 운영자가 provider category, status, load, served models, lifecycle capability를 Client에서 확인할 수 없다. 03번 plan이 Control Plane HTTP JSON 계약을 닫으면, 이 plan은 Client DTO와 Nodes view에서 같은 read-only catalog/status를 표시한다.
## 사용자 리뷰 요청 흐름
사용자 리뷰 요청은 선택된 SDD 결정 또는 선택된 Milestone lock decision만 active review stub의 `사용자 리뷰 요청` 섹션에 기록한다. 구현 중 직접 사용자 프롬프트는 금지하며, code-review가 정당성을 검증하고 실제 `USER_REVIEW.md` 작성 여부를 결정한다.
## Roadmap Targets
- Milestone: `agent-roadmap/phase/operational-observability-provider-management/milestones/provider-catalog-device-status.md`
- Task ids:
- `ops-review`: Edge config와 Edge-owned read-only catalog/status를 기준으로 `iop-edge` CLI, Control Plane, Client가 어떤 정보를 소비하는지 확인할 수 있다.
- Completion mode: check-on-pass
## Spec Targets
- SDD: `agent-roadmap/sdd/operational-observability-provider-management/provider-catalog-device-status/SDD.md`
- Acceptance scenarios:
- `S10`: task=`ops-review`; evidence=`Edge CLI configured-provider summary, Control Plane provider status JSON, Client Nodes panel provider catalog/status rendering, and resolved user_review_0.log are present.`
- Completion mode: spec-check-on-pass
## 분석 결과
### 읽은 파일
- `AGENTS.md`
- `agent-ops/rules/project/rules.md`
- `agent-ops/rules/common/rules-roadmap.md`
- `agent-ops/skills/common/router.md`
- `agent-ops/skills/common/plan/SKILL.md`
- `agent-ops/skills/common/update-roadmap/SKILL.md`
- `agent-ops/skills/common/_templates/implementation-user-review-request-section.md`
- `agent-roadmap/current.md`
- `agent-roadmap/phase/operational-observability-provider-management/PHASE.md`
- `agent-roadmap/phase/operational-observability-provider-management/milestones/provider-catalog-device-status.md`
- `agent-roadmap/sdd/operational-observability-provider-management/provider-catalog-device-status/SDD.md`
- `agent-ops/rules/project/domain/client/rules.md`
- `agent-ops/rules/project/domain/control-plane/rules.md`
- `agent-ops/rules/project/domain/testing/rules.md`
- `agent-test/local/rules.md`
- `agent-test/local/client-smoke.md`
- `agent-test/local/control-plane-smoke.md`
- `apps/client/lib/control_plane_status_client.dart`
- `apps/client/lib/control_plane_status_dto.dart`
- `apps/client/lib/control_plane_status_repository.dart`
- `apps/client/lib/widgets/nodes_panel.dart`
- `apps/client/lib/widgets/edges_panel.dart`
- `apps/client/test/widget_test.dart`
- `apps/client/test/control_plane_status_controller_test.dart`
- `apps/control-plane/cmd/control-plane/http_views.go`
- `proto/iop/control.proto`
- `proto/iop/runtime.proto`
### 테스트 환경 규칙
- test_env: `local`.
- `agent-test/local/rules.md`를 읽었다. Flutter client 포함 검증은 원격 runner 또는 동기화된 Flutter 환경 기준이며, 현재 checkout의 변경분이 원격에 없으면 원격 검증 완료로 보지 않는다고 확인했다.
- 매칭 profile: `agent-test/local/client-smoke.md`를 읽었다. 필수 명령은 `cd apps/client && flutter test`다.
- 관련 upstream profile: `agent-test/local/control-plane-smoke.md`를 읽었다. 이 plan은 Control Plane 코드를 수정하지 않으며 03번 plan의 complete.log를 선행 조건으로 둔다.
- Docker compose, web dev server, analyzer는 이번 DTO/widget 범위의 필수 검증이 아니다. Flutter analyzer 영향이 확인되면 구현 에이전트가 `cd apps/client && flutter analyze --no-fatal-infos`를 추가 실행하고 review stub에 기록한다.
### 테스트 커버리지 공백
- `EdgeNodeSnapshotView.fromJson``provider_snapshots`를 파싱하지 않는다. HTTP repository가 JSON을 받아도 DTO에서 버린다.
- `NodesPanel`은 node config adapters만 표시한다. provider status, category, served models, load ratio, lifecycle capabilities가 UI에 없다.
- `FakeControlPlaneStatusRepository.fetchEdgeStatus` fixture와 `Client App opens Nodes panel...` widget test는 provider catalog/status가 표시되어야 한다는 회귀 조건을 갖고 있지 않다.
### 심볼 참조
- renamed/removed symbols: none.
- 새 DTO 후보: `ProviderSnapshotView`.
- 새 field 후보: `EdgeNodeSnapshotView.providerSnapshots`.
- 변경 call sites: `EdgeNodeSnapshotView` constructor usages in `apps/client/test/widget_test.dart` and `apps/client/test/control_plane_status_controller_test.dart` may need `providerSnapshots: []` only if constructor is kept `required`; prefer optional default list to minimize call-site churn.
### 분할 판단
- split decision policy를 평가했다.
- 공유 task group: `agent-task/m-provider-catalog-device-status/`.
- predecessor: `03_catalog_status_http` - 현재 active plan으로 존재하며 complete.log는 아직 없음.
- current task: `04+03_client_catalog_view` - `03` complete.log가 필요하다.
- 분할 이유: Client DTO/UI는 Control Plane HTTP JSON shape에 의존한다. Go API 변환과 Flutter widget 검증은 서로 다른 ownership/test runner를 가지므로 별도 review unit으로 둔다.
### 범위 결정 근거
- 포함: `apps/client/lib/control_plane_status_dto.dart`, `apps/client/lib/widgets/nodes_panel.dart`, `apps/client/test/widget_test.dart`. 필요 시 DTO parse 단위 test를 기존 client test 파일에 추가할 수 있다.
- 제외: `apps/control-plane/**`. 03번 plan의 PASS가 선행 조건이다.
- 제외: generated Dart proto. Client는 Control Plane HTTP JSON DTO를 소비하므로 proto binding 수정이 아니다.
- 제외: Edge CLI. 작은 직접 작업으로 `iop-edge nodes list` configured provider summary가 추가되어 있다.
- 제외: runtime enable/disable, drain, fallback 우선순위, capacity override. SDD D06과 Milestone 범위 제외에 따라 후속 범위다.
### 빌드 등급
- lane/grade: `local-G05`.
- 근거: Flutter DTO와 widget 표시 변경이며 UI test로 검증 가능하다. 다만 Client runner/Flutter SDK 상태와 layout 회귀 위험이 있어 G04보다 한 단계 높였다.
## 구현 체크리스트
- [ ] 선행 task `agent-task/m-provider-catalog-device-status/03_catalog_status_http/complete.log` 또는 matching archive complete.log가 존재하는지 확인하고, 없으면 구현하지 말고 dependency missing으로 기록한다.
- [ ] Client HTTP DTO에 `ProviderSnapshotView``EdgeNodeSnapshotView.providerSnapshots` parsing을 추가한다.
- [ ] Nodes panel에 read-only Provider Catalog/Status 섹션을 추가해 provider id/type/category, health/status, capacity, in-flight, queued, load ratio, served models, lifecycle capabilities를 표시한다.
- [ ] Fake status repository와 widget test fixture에 provider snapshot을 추가하고 Nodes panel test가 provider catalog/status 표시를 검증하도록 보강한다.
- [ ] `cd apps/client && flutter test`를 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
## 의존 관계 및 구현 순서
- 이 subtask dir 이름 `04+03_client_catalog_view`는 predecessor index `03`만 요구한다.
- 구현 시작 전 다음 중 하나를 확인한다.
- active: `agent-task/m-provider-catalog-device-status/03_catalog_status_http/complete.log`
- archive: `agent-task/archive/*/*/m-provider-catalog-device-status/03_catalog_status_http/complete.log`
- 현재 계획 작성 시점에는 predecessor complete.log가 없다. 구현 에이전트는 03 PASS 이후 진행한다.
### [API-1] Client Provider Snapshot DTO 추가
#### 문제
[apps/client/lib/control_plane_status_dto.dart](/config/workspace/iop/apps/client/lib/control_plane_status_dto.dart:65)의 `EdgeNodeSnapshotView`는 provider snapshots field가 없다. [apps/client/lib/control_plane_status_dto.dart](/config/workspace/iop/apps/client/lib/control_plane_status_dto.dart:80)의 `fromJson``provider_snapshots`를 읽지 않는다.
현재 코드:
```dart
65 class EdgeNodeSnapshotView {
66 final String nodeId;
67 final String alias;
68 final String label;
69 final bool connected;
70 final NodeConfigSummaryView? config;
...
80 factory EdgeNodeSnapshotView.fromJson(Map<String, dynamic> json) {
81 return EdgeNodeSnapshotView(
82 nodeId: json['node_id'] as String? ?? '',
83 alias: json['alias'] as String? ?? '',
84 label: json['label'] as String? ?? '',
85 connected: json['connected'] as bool? ?? false,
86 config: json['config'] != null
87 ? NodeConfigSummaryView.fromJson(json['config'] as Map<String, dynamic>)
88 : null,
89 );
90 }
91 }
```
#### 해결 방법
`ProviderSnapshotView` DTO를 추가한다. Numeric JSON은 Go에서 `int`/`float`로 올 수 있으므로 `capacity`, `in_flight`, `queued``as num?``toInt()`, `load_ratio``toDouble()`로 파싱한다. `EdgeNodeSnapshotView` 생성자에는 `List<ProviderSnapshotView> providerSnapshots = const []` default를 둬 기존 fixture churn을 줄인다.
목표 코드:
```dart
class ProviderSnapshotView {
final String id;
final String adapter;
final String type;
final String category;
final String status;
final String health;
final int capacity;
final int inFlight;
final int queued;
final double loadRatio;
final List<String> servedModels;
final List<String> lifecycleCapabilities;
ProviderSnapshotView({
required this.id,
required this.adapter,
required this.type,
required this.category,
required this.status,
required this.health,
required this.capacity,
required this.inFlight,
required this.queued,
required this.loadRatio,
required this.servedModels,
required this.lifecycleCapabilities,
});
factory ProviderSnapshotView.fromJson(Map<String, dynamic> json) {
return ProviderSnapshotView(
id: json['id'] as String? ?? '',
adapter: json['adapter'] as String? ?? '',
type: json['type'] as String? ?? '',
category: json['category'] as String? ?? '',
status: json['status'] as String? ?? '',
health: json['health'] as String? ?? '',
capacity: (json['capacity'] as num?)?.toInt() ?? 0,
inFlight: (json['in_flight'] as num?)?.toInt() ?? 0,
queued: (json['queued'] as num?)?.toInt() ?? 0,
loadRatio: (json['load_ratio'] as num?)?.toDouble() ?? 0,
servedModels: List<String>.from(json['served_models'] ?? []),
lifecycleCapabilities: List<String>.from(json['lifecycle_capabilities'] ?? []),
);
}
}
```
#### 수정 파일 및 체크리스트
- [ ] `apps/client/lib/control_plane_status_dto.dart`: DTO 추가.
- [ ] `apps/client/lib/control_plane_status_dto.dart`: `EdgeNodeSnapshotView.providerSnapshots` 추가 및 JSON parsing.
- [ ] `apps/client/test/widget_test.dart` 또는 기존 client unit test: JSON parse assertion 추가 또는 widget fixture에서 파싱 경로 검증.
#### 테스트 작성
- 작성: `apps/client/test/widget_test.dart`의 fake repository fixture에 provider snapshots를 추가한다. 별도 pure DTO test가 더 간결하면 같은 test file 또는 `control_plane_status_controller_test.dart`에 추가해도 된다.
#### 중간 검증
```bash
cd apps/client && flutter test test/widget_test.dart
```
기대 결과: 기존 app/widget tests와 provider DTO 표시 assertion이 통과한다.
### [API-2] Nodes Panel Provider Catalog 표시
#### 문제
[apps/client/lib/widgets/nodes_panel.dart](/config/workspace/iop/apps/client/lib/widgets/nodes_panel.dart:211)은 config가 있을 때 concurrency와 adapters만 표시한다. provider snapshots가 추가되어도 UI에서 볼 수 없다.
현재 코드:
```dart
211 if (node.config != null) ...[
212 const SizedBox(height: 16),
213 const Divider(color: Color(0xFF334155)),
214 const SizedBox(height: 12),
215 Row(
216 children: [
217 const Icon(Icons.bolt, size: 16, color: Color(0xFF6366F1)),
218 const SizedBox(width: 6),
219 Text(
220 'Concurrency Limit: ${node.config!.concurrency}',
...
226 const Text(
227 'Adapters',
...
234 Wrap(
235 spacing: 8,
236 runSpacing: 8,
237 children: node.config!.adapters.map((adapter) {
```
#### 해결 방법
Node card 안에 `Provider Catalog` 섹션을 추가한다. Cards 안에 별도 `Card`를 중첩하지 말고 기존 dark panel 안에서 `Column`, `Wrap`, `Container`로 compact rows를 만든다. 표시 필드는 다음을 포함한다.
- provider label: `id`가 있으면 id, 없으면 adapter.
- type/category: `type``category`.
- state: `health` 우선, 없으면 `status`.
- load: `inFlight/capacity`, `queued`, `loadRatio`.
- served models: comma-separated first line, 길면 wrap.
- lifecycle capabilities: coarse flags만 표시.
UI text는 기존 Nodes panel 스타일에 맞춰 작고 조밀하게 둔다. 새 marketing/설명 문구는 추가하지 않는다.
#### 수정 파일 및 체크리스트
- [ ] `apps/client/lib/widgets/nodes_panel.dart`: provider section helper 추가.
- [ ] `apps/client/lib/widgets/nodes_panel.dart`: node card에 provider snapshots가 있을 때만 section 렌더링.
- [ ] `apps/client/test/widget_test.dart`: `Provider Catalog`, provider id/type/category/status/load/models/lifecycle text assertion 추가.
#### 테스트 작성
- 작성: `Client App opens Nodes panel and displays active Nodes and configurations`에 provider display assertion을 추가한다.
#### 중간 검증
```bash
cd apps/client && flutter test test/widget_test.dart
```
기대 결과: Nodes panel test가 provider catalog/status 표시까지 검증한다.
### [API-3] Ops Review Evidence 연결
#### 문제
Milestone `ops-review``iop-edge` CLI, Control Plane, Client가 어떤 정보를 소비하는지 확인할 수 있어야 한다. 작은 직접 작업으로 `iop-edge nodes list` configured provider summary는 추가됐지만, Client plan이 PASS될 때 전체 소비 경로 근거가 review/complete log에 남아야 한다.
#### 해결 방법
review stub의 `주요 설계 결정` 또는 `검증 결과`에 다음 증거를 남긴다.
- Edge CLI: `apps/edge/internal/edgecmd/nodes.go`가 configured provider category/type/models/capacity/lifecycle summary를 출력하고 `TestNodesListPrintsConfiguredProviders`가 통과했다.
- Control Plane: predecessor 03 complete.log가 `/edges/{id}/status` provider snapshots JSON을 검증했다.
- Client: 이 plan의 widget test가 provider catalog/status 표시를 검증했다.
- SDD review: `agent-roadmap/sdd/operational-observability-provider-management/provider-catalog-device-status/user_review_0.log`가 D01-D06 해결 이력으로 존재한다.
#### 수정 파일 및 체크리스트
- [ ] `CODE_REVIEW-local-G05.md`: 구현 후 주요 설계 결정 또는 검증 결과에 위 evidence 연결을 기록한다.
- [ ] source code는 API-1/API-2 범위를 넘기지 않는다.
#### 테스트 작성
- 별도 테스트 없음. API-1/API-2 테스트와 predecessor 03 complete.log가 evidence다.
#### 중간 검증
```bash
test -f agent-task/m-provider-catalog-device-status/03_catalog_status_http/complete.log || find agent-task/archive -path '*/m-provider-catalog-device-status/03_catalog_status_http/complete.log' -print -quit
```
기대 결과: 03 predecessor complete.log 경로가 출력된다.
## 수정 파일 요약
| 파일 | 항목 |
|------|------|
| `apps/client/lib/control_plane_status_dto.dart` | API-1 |
| `apps/client/lib/widgets/nodes_panel.dart` | API-2 |
| `apps/client/test/widget_test.dart` | API-1, API-2 |
| `agent-task/m-provider-catalog-device-status/04+03_client_catalog_view/CODE_REVIEW-local-G05.md` | API-3 |
## 최종 검증
```bash
cd apps/client && flutter test
git diff --check
```
기대 결과: Flutter tests가 통과하고 공백 오류가 없다. Go test cache output은 해당 없음. 모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.

View file

@ -136,11 +136,27 @@ type fleetCommandResponse struct {
}
type edgeNodeSnapshotView struct {
NodeID string `json:"node_id"`
Alias string `json:"alias"`
Label string `json:"label"`
Connected bool `json:"connected"`
Config *nodeConfigSummaryView `json:"config,omitempty"`
NodeID string `json:"node_id"`
Alias string `json:"alias"`
Label string `json:"label"`
Connected bool `json:"connected"`
Config *nodeConfigSummaryView `json:"config,omitempty"`
ProviderSnapshots []providerSnapshotView `json:"provider_snapshots,omitempty"`
}
type providerSnapshotView struct {
Adapter string `json:"adapter,omitempty"`
Status string `json:"status,omitempty"`
Capacity int32 `json:"capacity"`
InFlight int32 `json:"in_flight"`
Queued int32 `json:"queued"`
ID string `json:"id,omitempty"`
Type string `json:"type,omitempty"`
Category string `json:"category,omitempty"`
ServedModels []string `json:"served_models,omitempty"`
Health string `json:"health,omitempty"`
LoadRatio float32 `json:"load_ratio"`
LifecycleCapabilities []string `json:"lifecycle_capabilities,omitempty"`
}
type nodeConfigSummaryView struct {
@ -176,6 +192,30 @@ func newNodeConfigSummaryView(payload *iop.NodeConfigPayload) *nodeConfigSummary
}
}
func providerSnapshotViews(snaps []*iop.ProviderSnapshot) []providerSnapshotView {
if len(snaps) == 0 {
return nil
}
views := make([]providerSnapshotView, 0, len(snaps))
for _, p := range snaps {
views = append(views, providerSnapshotView{
Adapter: p.GetAdapter(),
Status: p.GetStatus(),
Capacity: p.GetCapacity(),
InFlight: p.GetInFlight(),
Queued: p.GetQueued(),
ID: p.GetId(),
Type: p.GetType(),
Category: p.GetCategory(),
ServedModels: append([]string(nil), p.GetServedModels()...),
Health: p.GetHealth(),
LoadRatio: p.GetLoadRatio(),
LifecycleCapabilities: append([]string(nil), p.GetLifecycleCapabilities()...),
})
}
return views
}
func edgeRegistryViewFromState(state wire.EdgeConnectionState) edgeRegistryView {
return edgeRegistryView{
EdgeID: state.EdgeID,
@ -192,11 +232,12 @@ func edgeStatusResponseViewFromProto(resp *iop.EdgeStatusResponse) edgeStatusRes
nodes := make([]edgeNodeSnapshotView, 0, len(resp.GetNodes()))
for _, n := range resp.GetNodes() {
nodes = append(nodes, edgeNodeSnapshotView{
NodeID: n.GetNodeId(),
Alias: n.GetAlias(),
Label: n.GetLabel(),
Connected: n.GetConnected(),
Config: newNodeConfigSummaryView(n.GetConfig()),
NodeID: n.GetNodeId(),
Alias: n.GetAlias(),
Label: n.GetLabel(),
Connected: n.GetConnected(),
Config: newNodeConfigSummaryView(n.GetConfig()),
ProviderSnapshots: providerSnapshotViews(n.GetProviderSnapshots()),
})
}
return edgeStatusResponseView{

View file

@ -464,6 +464,32 @@ func TestEdgeRegistryHTTPHandlersGetLiveStatus(t *testing.T) {
Concurrency: 4,
},
},
ProviderSnapshots: []*iop.ProviderSnapshot{
{
Adapter: "vllm-gpu",
Status: "available",
Capacity: 4,
InFlight: 1,
Queued: 2,
Id: "provider-vllm-primary",
Type: "vllm",
Category: "api",
ServedModels: []string{"nvidia/Qwen3.6-35B-A3B-NVFP4"},
Health: "available",
LoadRatio: 0.25,
LifecycleCapabilities: []string{"list_models", "load_model"},
},
{
Adapter: "ollama-cpu",
Status: "backlog",
Capacity: 2,
InFlight: 2,
Queued: 5,
Id: "provider-ollama-default",
Type: "ollama",
Health: "unavailable",
},
},
},
},
}, nil
@ -505,6 +531,41 @@ func TestEdgeRegistryHTTPHandlersGetLiveStatus(t *testing.T) {
t.Fatalf("unexpected adapter summary: %+v", adapter)
}
// Provider snapshots assertions
if len(node.ProviderSnapshots) != 2 {
t.Fatalf("expected 2 provider snapshots, got %d", len(node.ProviderSnapshots))
}
// First provider snapshot (vllm-gpu)
p0 := node.ProviderSnapshots[0]
if p0.Adapter != "vllm-gpu" || p0.Status != "available" || p0.Capacity != 4 {
t.Fatalf("unexpected provider snapshot[0]: %+v", p0)
}
if p0.InFlight != 1 || p0.Queued != 2 {
t.Fatalf("unexpected in_flight/queued: in_flight=%d queued=%d", p0.InFlight, p0.Queued)
}
if p0.ID != "provider-vllm-primary" || p0.Type != "vllm" || p0.Category != "api" {
t.Fatalf("unexpected id/type/category: id=%q type=%q category=%q", p0.ID, p0.Type, p0.Category)
}
if len(p0.ServedModels) != 1 || p0.ServedModels[0] != "nvidia/Qwen3.6-35B-A3B-NVFP4" {
t.Fatalf("unexpected served_models: %+v", p0.ServedModels)
}
if p0.Health != "available" || p0.LoadRatio != 0.25 {
t.Fatalf("unexpected health/load_ratio: health=%q load_ratio=%f", p0.Health, p0.LoadRatio)
}
if len(p0.LifecycleCapabilities) != 2 || p0.LifecycleCapabilities[0] != "list_models" || p0.LifecycleCapabilities[1] != "load_model" {
t.Fatalf("unexpected lifecycle_capabilities: %+v", p0.LifecycleCapabilities)
}
// Second provider snapshot (ollama-cpu)
p1 := node.ProviderSnapshots[1]
if p1.Adapter != "ollama-cpu" || p1.Status != "backlog" || p1.Health != "unavailable" {
t.Fatalf("unexpected provider snapshot[1]: %+v", p1)
}
if p1.Capacity != 2 || p1.InFlight != 2 || p1.Queued != 5 {
t.Fatalf("unexpected capacity/in_flight/queued for ollama: capacity=%d in_flight=%d queued=%d", p1.Capacity, p1.InFlight, p1.Queued)
}
bodyStr := resp.Body.String()
if strings.Contains(bodyStr, "super-secret-token") || strings.Contains(bodyStr, "secret_key") || strings.Contains(bodyStr, "settings") {
t.Fatalf("leaked raw config settings in response: %s", bodyStr)
@ -536,6 +597,64 @@ func TestEdgeRegistryHTTPHandlersGetLiveStatus(t *testing.T) {
t.Fatalf("config summary missing key %q: %#v", required, rawConfig)
}
}
// Provider snapshots raw JSON assertions
rawProviderSnapshots, ok := rawNode["provider_snapshots"].([]any)
if !ok {
t.Fatalf("provider_snapshots key missing or not an array: %#v", rawNode)
}
if len(rawProviderSnapshots) != 2 {
t.Fatalf("expected 2 provider_snapshots in raw JSON, got %d", len(rawProviderSnapshots))
}
rawP0, ok := rawProviderSnapshots[0].(map[string]any)
if !ok {
t.Fatalf("provider_snapshots[0] is not an object: %#v", rawProviderSnapshots[0])
}
if rawP0["adapter"] != "vllm-gpu" {
t.Fatalf("provider_snapshots[0].adapter: got %v", rawP0["adapter"])
}
if rawP0["capacity"] != float64(4) {
t.Fatalf("provider_snapshots[0].capacity: got %v", rawP0["capacity"])
}
if rawP0["load_ratio"] != float64(0.25) {
t.Fatalf("provider_snapshots[0].load_ratio: got %v", rawP0["load_ratio"])
}
servedModels, ok := rawP0["served_models"].([]any)
if !ok || len(servedModels) != 1 {
t.Fatalf("provider_snapshots[0].served_models: got %#v", rawP0["served_models"])
}
if servedModels[0] != "nvidia/Qwen3.6-35B-A3B-NVFP4" {
t.Fatalf("provider_snapshots[0].served_models[0]: got %v", servedModels[0])
}
lifecycleCaps, ok := rawP0["lifecycle_capabilities"].([]any)
if !ok || len(lifecycleCaps) != 2 {
t.Fatalf("provider_snapshots[0].lifecycle_capabilities: got %#v", rawP0["lifecycle_capabilities"])
}
if lifecycleCaps[0] != "list_models" || lifecycleCaps[1] != "load_model" {
t.Fatalf("provider_snapshots[0].lifecycle_capabilities: got %v", lifecycleCaps)
}
// Second provider snapshot raw check
rawP1, ok := rawProviderSnapshots[1].(map[string]any)
if !ok {
t.Fatalf("provider_snapshots[1] is not an object: %#v", rawProviderSnapshots[1])
}
if rawP1["adapter"] != "ollama-cpu" || rawP1["health"] != "unavailable" {
t.Fatalf("provider_snapshots[1] adapter/health: got %#v", rawP1)
}
// Zero load_ratio must be present in raw JSON (omitempty 제거 검증)
if rawP1["load_ratio"] != float64(0) {
t.Fatalf("provider_snapshots[1].load_ratio: expected 0 for idle provider, got %v", rawP1["load_ratio"])
}
// Ensure raw JSON does not leak config internals into provider_snapshots
providerSnapshotsStr, _ := json.Marshal(rawP0)
for _, forbidden := range []string{"runtime", "settings", "secret"} {
if strings.Contains(string(providerSnapshotsStr), forbidden) {
t.Fatalf("provider_snapshot leaked raw config key %q: %s", forbidden, providerSnapshotsStr)
}
}
})
t.Run("MissingEdge", func(t *testing.T) {

View file

@ -179,6 +179,56 @@ func TestNodeRegisterUpsertsYAMLAndOutputsBootstrap(t *testing.T) {
}
}
func TestNodesListPrintsConfiguredProviders(t *testing.T) {
dir := t.TempDir()
cfgPath := filepath.Join(dir, "edge.yaml")
yaml := `
server:
listen: "0.0.0.0:9090"
nodes:
- id: "node-gpu-01"
alias: "gpu-node"
token: "token-1"
providers:
- id: "vllm-gpu"
type: "vllm"
category: "api"
adapter: "vllm-gpu"
models:
- "nvidia/Qwen3.6-35B-A3B-NVFP4"
capacity: 4
lifecycle_capabilities:
- "list_models"
- "load_model"
`
if err := os.WriteFile(cfgPath, []byte(yaml), 0o600); err != nil {
t.Fatalf("write %s: %v", cfgPath, err)
}
root := NewRoot(Options{})
var out bytes.Buffer
root.SetOut(&out)
root.SetErr(&out)
root.SetArgs([]string{"--config", cfgPath, "nodes", "list"})
if err := root.Execute(); err != nil {
t.Fatalf("execute nodes list: %v\n%s", err, out.String())
}
got := out.String()
for _, want := range []string{
"node-gpu-01",
"providers: vllm-gpu",
"type=vllm/api",
"models=nvidia/Qwen3.6-35B-A3B-NVFP4",
"capacity=4",
"lifecycle=list_models,load_model",
} {
if !strings.Contains(got, want) {
t.Errorf("nodes list output missing %q:\n%s", want, got)
}
}
}
func TestPatchYAMLPreservesArtifactBootstrapField(t *testing.T) {
origYAML := `server:
listen: "0.0.0.0:9090"

View file

@ -48,8 +48,44 @@ When the server is offline or this command is run standalone, all nodes will sho
fmt.Fprintf(cmd.OutOrStdout(), "%s\n", strings.Repeat("-", 100))
for _, n := range nodes {
fmt.Fprintf(cmd.OutOrStdout(), "%-25s %-20s %-40s %-12s\n", n.ID, n.Alias, n.Token, "offline (configured)")
if summary := formatConfiguredProviders(n.Providers); summary != "" {
fmt.Fprintf(cmd.OutOrStdout(), " providers: %s\n", summary)
}
}
return nil
},
}
}
func formatConfiguredProviders(providers []config.NodeProviderConf) string {
if len(providers) == 0 {
return ""
}
summaries := make([]string, 0, len(providers))
for _, p := range providers {
if p.ID == "" {
continue
}
parts := []string{p.ID}
if p.Type != "" || p.Category != "" {
typeCategory := string(p.Category)
if p.Type != "" && typeCategory != "" {
typeCategory = p.Type + "/" + typeCategory
} else if p.Type != "" {
typeCategory = p.Type
}
parts = append(parts, "type="+typeCategory)
}
if len(p.Models) > 0 {
parts = append(parts, "models="+strings.Join(p.Models, ","))
}
if p.Capacity > 0 {
parts = append(parts, fmt.Sprintf("capacity=%d", p.Capacity))
}
if len(p.LifecycleCapabilities) > 0 {
parts = append(parts, "lifecycle="+strings.Join(p.LifecycleCapabilities, ","))
}
summaries = append(summaries, strings.Join(parts, " "))
}
return strings.Join(summaries, "; ")
}