test(multi-edge): Edge별 이력 상태 검증을 보강한다
Edge별 command audit과 domain agent 상태가 서로 섞이지 않도록 control-plane/client fixture와 Runtime panel widget 검증을 보강한다. 로드맵 상태와 Agent-Ops 완료 로그를 함께 정리한다.
This commit is contained in:
parent
a43d2172ae
commit
ea949daa3b
13 changed files with 1559 additions and 312 deletions
|
|
@ -49,7 +49,7 @@
|
|||
- 경로: `agent-roadmap/archive/phase/control-plane-portal-ops/milestones/policy-history-audit.md`
|
||||
- 요약: 권한, 정책, 실행 이력, 감사 로그를 제품 운영에 필요한 수준으로 확장하고, 공통 audit schema evidence까지 확정했다.
|
||||
|
||||
- [계획] Multi-Edge 운영
|
||||
- [진행중] Multi-Edge 운영
|
||||
- 경로: `agent-roadmap/phase/control-plane-portal-ops/milestones/multi-edge-operations.md`
|
||||
- 요약: 여러 Edge group을 관찰하고 운영하는 fleet-level 기능을 구축한다.
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ Control Plane은 Edge를 제어하기 쉽게 연결하는 레이어이며, Edge
|
|||
|
||||
## 상태
|
||||
|
||||
[계획]
|
||||
[진행중]
|
||||
|
||||
## 구현 잠금
|
||||
|
||||
|
|
@ -36,7 +36,7 @@ Control Plane은 Edge를 제어하기 쉽게 연결하는 레이어이며, Edge
|
|||
### Epic: [fleet-ops-boundary] Fleet Operations Boundary
|
||||
|
||||
- [x] [native-fleet] Multi-edge 운영 명령과 이벤트 relay는 IOP native protocol을 기준으로 설계한다.
|
||||
- [ ] [edge-status-view] Control Plane은 여러 Edge 상태를 구분해 조회하고 표시할 수 있다. 검증: 두 개 이상의 Edge 상태가 구분되는 조회/표시 경로를 확인한다.
|
||||
- [x] [edge-status-view] Control Plane은 여러 Edge 상태를 구분해 조회하고 표시할 수 있다. 검증: 두 개 이상의 Edge 상태가 구분되는 조회/표시 경로를 확인한다.
|
||||
- [ ] [history-agent-state] Edge별 실행/명령 결과와 domain agent capability/status/command summary가 운영 화면에서 구분된다.
|
||||
- [x] [openai-routing] OpenAI-compatible 표면은 특정 Edge/adapter로 라우팅되는 inference 호환 경로로 제한한다.
|
||||
- [x] [a2a-routing] A2A 표면은 특정 Edge/adapter로 위임되는 agent task 경로로 제한한다.
|
||||
|
|
@ -73,3 +73,4 @@ Control Plane은 Edge를 제어하기 쉽게 연결하는 레이어이며, Edge
|
|||
- 완료 근거: [a2a-routing] `apps/edge/internal/input/a2a/server.go`는 Edge-local `SubmitRun`/`CancelRun`을 통해 특정 `adapter + target` agent task 위임만 처리한다. 검증: `go test ./apps/edge/internal/input/a2a` 통과.
|
||||
- 완료 근거: [native-fleet] `proto/iop/control.proto`에 native command 요청/응답/이벤트 relay와 capability/status 요약 메시지를 추가하고, Control Plane과 Edge의 parser map을 갱신했다. 검증: `make proto proto-dart` 및 `go test ./apps/control-plane/internal/wire` 통과.
|
||||
- 완료 근거: [edge-ownership] Control Plane native contract/registry에 Edge config, Node registry, agent lifecycle detail 원본이 노출되지 않도록 descriptor 및 reflect 기반 소유권 가드 테스트(`TestProtoOwnershipGuard`, `TestControlPlaneRegistryOwnershipGuard`)를 추가했다. 검증: `go test ./apps/control-plane/internal/wire` 통과.
|
||||
- 완료 근거: [edge-status-view] `apps/client/test/widget_test.dart`에서 두 Edge의 fleet status가 선택에 따라 `Edge Alpha`/`Edge Beta`, `edge-a`/`edge-b`, connected/disconnected, health 값을 구분해 표시되는 경로를 고정했다. 검증: `cd apps/client && flutter test test/widget_test.dart` 통과.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,157 @@
|
|||
<!-- task=m-multi-edge-operations/01_history_agent_contract 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 user-only decision, user-owned external environment prerequisite, or scope conflict, fill `사용자 리뷰 요청` with evidence and stop with active files in place; code-review decides whether to write `USER_REVIEW.md`. Evidence gaps that a follow-up agent can close by rerunning commands or collecting artifacts are normal follow-up issues, not user-review blockers by themselves.
|
||||
> 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-04
|
||||
task=m-multi-edge-operations/01_history_agent_contract, plan=0, tag=API
|
||||
|
||||
## 이 파일을 읽는 리뷰 에이전트에게
|
||||
|
||||
> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다.
|
||||
|
||||
각 항목의 구현을 실제 소스 파일과 대리조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
|
||||
리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다.
|
||||
|
||||
1. 판정을 append한다.
|
||||
2. `CODE_REVIEW-local-G05.md` -> `code_review_local_G05_N.log`, `PLAN-local-G05.md` -> `plan_local_G05_M.log`로 아카이브한다.
|
||||
3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/m-multi-edge-operations/01_history_agent_contract/`로 이동한다. WARN/FAIL이면 user-review gate를 확인한 뒤 다음 active plan/review 파일 또는 `USER_REVIEW.md`를 작성한다.
|
||||
4. PASS이고 task group이 `m-multi-edge-operations`이면 완료 이벤트 메타데이터를 보고한다. 이 plan에는 Roadmap Targets가 없으므로 roadmap Task 체크는 no-op이어야 한다.
|
||||
5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다.
|
||||
|
||||
---
|
||||
|
||||
## 구현 항목별 완료 여부
|
||||
|
||||
| 항목 | 완료 여부 |
|
||||
|------|---------|
|
||||
| [API-1] Fleet Status Domain Agent Isolation | [x] |
|
||||
| [API-2] Edge Operations Audit Isolation | [x] |
|
||||
|
||||
## 구현 체크리스트
|
||||
|
||||
- [x] `/fleet/status`가 두 connected Edge의 서로 다른 domain agent capability/status/command summary를 구분해 반환하는 focused test를 추가한다.
|
||||
- [x] `/edges/{edge_id}/operations`가 Edge별 command audit을 섞지 않는 focused test를 추가한다.
|
||||
- [x] 필요 시 backend mapper/registry 구현을 테스트 기대에 맞게 보강하되 Control Plane이 Edge-owned 원본 데이터를 소유하지 않도록 유지한다.
|
||||
- [x] `go test ./apps/control-plane/cmd/control-plane ./apps/control-plane/internal/wire ./apps/edge/internal/service -count=1`를 실행한다.
|
||||
- [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_G05_N.log`로 아카이브한다.
|
||||
- [x] active `PLAN-*-G??.md`를 `plan_local_G05_M.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-multi-edge-operations/01_history_agent_contract/`를 `agent-task/archive/YYYY/MM/m-multi-edge-operations/01_history_agent_contract/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다.
|
||||
- [x] PASS이고 task group이 `m-multi-edge-operations`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다.
|
||||
- [x] PASS split 작업이면 이동 후 빈 active parent `agent-task/m-multi-edge-operations/`를 제거하거나, 남은 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가 사용자 결정으로 완료/PASS 해소되면 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log`를 작성한 뒤 task directory를 archive로 이동한다.
|
||||
|
||||
## 계획 대비 변경 사항
|
||||
|
||||
- `TestFleetStatusHTTPHandlerCombinesConnectionAndCapabilities`에서 `edge-offline`을 disconnect 처리할 때, token 값을 하드코딩 `3`에서 `MarkConnected` 시 반환받은 토큰인 `offlineToken`으로 대체하였습니다. `edge-other`를 테스트에 새로 추가함에 따라 token의 시퀀스 ID가 변하는 것을 방지하여 보다 안정적인 disconnect 상태 관찰이 가능하게 했습니다.
|
||||
|
||||
## 주요 설계 결정
|
||||
|
||||
- Control Plane이 Edge가 소유하는 상태(Domain Agent status 등)의 원본을 소유하지 않도록, `registry.RequestStatus` mock handler를 통해서만 해당 정보를 전달받고 status response mapping 레이어를 검증하는 방식을 유지했습니다.
|
||||
- cross-edge isolation 검증을 위해 `TestEdgeOperationsHTTPHandlerListsAudit`에서 `edge-a`와 `edge-b`에 각각 서로 다른 `RecordCommandRequest`/`RecordCommandEvent`/`RecordCommandResult`를 등록하여 각 엔드포인트가 서로의 감사 기록을 노출하지 않는지 엄격하게 assertion을 추가했습니다.
|
||||
|
||||
## 사용자 리뷰 요청
|
||||
|
||||
_기본값은 `없음`이다. 구현 중 사용자 결정, 사용자 소유 외부 환경/secret/서비스 준비, 또는 계획 범위 변경 없이는 안전하게 진행할 수 없으면 아래 항목을 실제 내용으로 교체하고, 구현을 중단한 뒤 active 파일을 그대로 둔 채 리뷰를 요청한다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 검증 증거 공백만으로는 사용자 리뷰 요청을 작성하지 않는다._
|
||||
|
||||
- 상태: 없음
|
||||
- 사유 유형: 없음
|
||||
- 결정 필요: 없음
|
||||
- 차단 근거: 없음
|
||||
- 실행한 검증/명령: 없음
|
||||
- 자동 후속 불가 이유: 없음
|
||||
- 재개 조건: 없음
|
||||
|
||||
## 리뷰어를 위한 체크포인트
|
||||
|
||||
- `/fleet/status`가 Edge별 `domain_agents`를 분리해 반환하는지 확인한다.
|
||||
- `/edges/{edge_id}/operations`가 다른 Edge command를 섞지 않는지 확인한다.
|
||||
- Control Plane response가 Node address/token/raw config settings를 노출하지 않는지 확인한다.
|
||||
|
||||
## 검증 결과
|
||||
|
||||
_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._
|
||||
|
||||
필수 규칙:
|
||||
- 검증 명령은 고정된 계약이다. 임의로 대체하지 않는다.
|
||||
- 대체가 필요하면 `계획 대비 변경 사항`에 이유와 대체 명령을 기록한다.
|
||||
- `검증 결과`에는 실제 stdout/stderr를 붙여 넣는다.
|
||||
- 사용자 리뷰 요청으로 명령을 끝까지 실행하지 못했다면 `사용자 리뷰 요청`에 실행한 명령, 실제 출력, 미실행 명령의 사유를 기록한다.
|
||||
- mobile/UI hang, timeout, 또는 2분 무진행은 blind retry를 중단하고 focused rerun 명령과 screenshot/window/UI-tree evidence path를 남기며, 불가능하면 정확한 사유를 남긴다.
|
||||
|
||||
### API-1 중간 검증
|
||||
```bash
|
||||
$ go test ./apps/control-plane/cmd/control-plane -run 'TestFleetStatusHTTPHandlerCombinesConnectionAndCapabilities' -count=1
|
||||
ok iop/apps/control-plane/cmd/control-plane 0.006s
|
||||
```
|
||||
|
||||
### API-2 중간 검증
|
||||
```bash
|
||||
$ go test ./apps/control-plane/cmd/control-plane -run 'TestEdgeOperationsHTTPHandlerListsAudit' -count=1
|
||||
ok iop/apps/control-plane/cmd/control-plane 0.008s
|
||||
```
|
||||
|
||||
### 최종 검증
|
||||
```bash
|
||||
$ go test ./apps/control-plane/cmd/control-plane ./apps/control-plane/internal/wire ./apps/edge/internal/service -count=1
|
||||
ok iop/apps/control-plane/cmd/control-plane 0.017s
|
||||
ok iop/apps/control-plane/internal/wire 1.483s
|
||||
ok iop/apps/edge/internal/service 0.018s
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
> **[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.
|
||||
|
||||
Sections and their ownership:
|
||||
|
||||
| Section | Owner | Note |
|
||||
|---------|-------|------|
|
||||
| Header comment, 개요, 리뷰 에이전트 지시 | Fixed at stub creation | Implementing agent must not modify or execute these |
|
||||
| 구현 항목별 완료 여부 | Implementing agent | Check `[ ]` -> `[x]` only |
|
||||
| 구현 체크리스트 | Implementing agent | Check `[ ]` -> `[x]` only |
|
||||
| 코드리뷰 전용 체크리스트 | Review agent only | Implementing agent must not modify |
|
||||
| 계획 대비 변경 사항, 주요 설계 결정 | Implementing agent | Replace placeholders |
|
||||
| 사용자 리뷰 요청 | Implementing agent | Keep `상태: 없음` unless blocked |
|
||||
| 리뷰어를 위한 체크포인트 | Fixed at stub creation | Review focus |
|
||||
| 검증 결과 | Implementing agent | Paste actual output |
|
||||
|
||||
## 코드리뷰 결과
|
||||
|
||||
- 종합 판정: PASS
|
||||
- 차원별 평가:
|
||||
- correctness: Pass
|
||||
- completeness: Pass
|
||||
- test coverage: Pass
|
||||
- API contract: Pass
|
||||
- code quality: Pass
|
||||
- plan deviation: Pass
|
||||
- verification trust: Pass
|
||||
- 발견된 문제:
|
||||
- Nit: `apps/control-plane/cmd/control-plane/main_test.go:764` blank line has trailing whitespace. `gofmt -w apps/control-plane/cmd/control-plane/main_test.go` removes it.
|
||||
- Nit: `apps/control-plane/cmd/control-plane/main_test.go:787` blank line has trailing whitespace. `gofmt -w apps/control-plane/cmd/control-plane/main_test.go` removes it.
|
||||
- 다음 단계: PASS이므로 `complete.log`를 작성하고 active task 디렉터리를 archive로 이동한다.
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
# Complete - m-multi-edge-operations/01_history_agent_contract
|
||||
|
||||
## 완료 일시
|
||||
|
||||
2026-06-04
|
||||
|
||||
## 요약
|
||||
|
||||
History agent state backend HTTP contract review loop 1 completed with PASS.
|
||||
|
||||
## 루프 이력
|
||||
|
||||
| Plan | Review | Verdict | 메모 |
|
||||
|------|--------|---------|------|
|
||||
| `plan_local_G05_0.log` | `code_review_local_G05_0.log` | PASS | Edge별 domain agent summary와 command audit isolation focused tests가 추가되었고 검증이 통과했다. |
|
||||
|
||||
## 구현/정리 내용
|
||||
|
||||
- `/fleet/status` handler test가 두 connected Edge의 서로 다른 `domain_agents` summary를 분리해 반환하는지 확인하도록 보강되었다.
|
||||
- `/edges/{edge_id}/operations` handler test가 `edge-a`와 `edge-b` command audit을 서로 섞지 않는지 양방향으로 확인하도록 보강되었다.
|
||||
- `edge-offline` disconnect fixture가 `MarkConnected` 반환 token을 사용하도록 안정화되었다.
|
||||
|
||||
## 최종 검증
|
||||
|
||||
- `go test ./apps/control-plane/cmd/control-plane -run 'TestFleetStatusHTTPHandlerCombinesConnectionAndCapabilities' -count=1` - PASS; `ok iop/apps/control-plane/cmd/control-plane 0.012s`.
|
||||
- `go test ./apps/control-plane/cmd/control-plane -run 'TestEdgeOperationsHTTPHandlerListsAudit' -count=1` - PASS; `ok iop/apps/control-plane/cmd/control-plane 0.008s`.
|
||||
- `go test ./apps/control-plane/cmd/control-plane ./apps/control-plane/internal/wire ./apps/edge/internal/service -count=1` - PASS; all three packages passed.
|
||||
- `git diff --check -- apps/control-plane/cmd/control-plane/main_test.go` - FAIL; Nit-only trailing whitespace reported at lines 764 and 787.
|
||||
|
||||
## 잔여 Nit
|
||||
|
||||
- `apps/control-plane/cmd/control-plane/main_test.go:764` blank line has trailing whitespace.
|
||||
- `apps/control-plane/cmd/control-plane/main_test.go:787` blank line has trailing whitespace.
|
||||
|
||||
## 후속 작업
|
||||
|
||||
- 없음
|
||||
|
|
@ -0,0 +1,144 @@
|
|||
<!-- task=m-multi-edge-operations/01_history_agent_contract plan=0 tag=API -->
|
||||
|
||||
# Plan - History Agent State Backend Contract
|
||||
|
||||
## 이 파일을 읽는 구현 에이전트에게
|
||||
|
||||
구현 완료는 active `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션까지 실제 구현 내용과 검증 출력으로 채운 뒤에만 성립한다. 검증 명령을 실행하고 실제 stdout/stderr를 review stub에 붙인다. active 파일은 그대로 두고 리뷰 준비 상태를 보고하며, `complete.log` 작성, `.log` rename, archive 이동은 code-review 스킬 전용이다. 구현 중 사용자 결정, 사용자 소유 외부 환경/secret/서비스 준비, 또는 계획 범위 충돌로 막히면 review stub의 `사용자 리뷰 요청`에 정확한 근거를 남기고 멈춘다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 증거 공백은 사용자 리뷰 요청이 아니다.
|
||||
|
||||
## 배경
|
||||
|
||||
`history-agent-state`는 Edge별 실행/명령 결과와 domain agent summary가 섞이지 않는 운영면을 요구한다. Control Plane에는 이미 `/fleet/status`, `/edges/{id}/operations`, command relay가 있지만, Edge별로 서로 다른 domain agent 상태와 command audit이 분리되는 HTTP 계약 검증이 약하다. 이 선행 작업은 client 작업이 의존할 backend JSON 계약을 작고 결정적으로 고정한다.
|
||||
|
||||
## 사용자 리뷰 요청 흐름
|
||||
|
||||
구현 중 blocker는 active review stub의 `사용자 리뷰 요청` 섹션에 기록한다. 해당 섹션은 `agent-ops/skills/common/_templates/implementation-user-review-request-section.md` 기준이며, 실제 `USER_REVIEW.md` 작성 여부 판단은 code-review가 소유한다.
|
||||
|
||||
## 분석 결과
|
||||
|
||||
### 읽은 파일
|
||||
|
||||
- `agent-roadmap/current.md`
|
||||
- `agent-roadmap/phase/control-plane-portal-ops/PHASE.md`
|
||||
- `agent-roadmap/phase/control-plane-portal-ops/milestones/multi-edge-operations.md`
|
||||
- `agent-test/local/rules.md`
|
||||
- `agent-test/local/control-plane-smoke.md`
|
||||
- `agent-test/local/edge-smoke.md`
|
||||
- `agent-test/local/client-smoke.md`
|
||||
- `agent-test/local/platform-common-smoke.md`
|
||||
- `agent-ops/rules/project/domain/control-plane/rules.md`
|
||||
- `agent-ops/rules/project/domain/edge/rules.md`
|
||||
- `agent-ops/rules/project/domain/client/rules.md`
|
||||
- `agent-ops/rules/project/domain/testing/rules.md`
|
||||
- `apps/control-plane/cmd/control-plane/main.go`
|
||||
- `apps/control-plane/cmd/control-plane/main_test.go`
|
||||
- `apps/control-plane/internal/wire/edge_registry.go`
|
||||
- `apps/control-plane/internal/wire/edge_server.go`
|
||||
- `apps/edge/internal/service/service.go`
|
||||
- `apps/edge/internal/service/service_test.go`
|
||||
- `proto/iop/control.proto`
|
||||
|
||||
### 테스트 환경 규칙
|
||||
|
||||
- test_env: `local`.
|
||||
- `agent-test/local/rules.md` present/read. Matching profiles read: `control-plane-smoke`, `edge-smoke`, `client-smoke`, `platform-common-smoke`.
|
||||
- 적용 명령: control-plane 변경은 `go test ./apps/control-plane/...`; Edge wire/registry lifecycle가 바뀌면 `make test-control-plane-edge-wire`; edge service가 바뀌면 `go test ./apps/edge/...`.
|
||||
- 이 subtask는 backend HTTP contract/test 고정이므로 primary verification은 focused Go tests다. Proto 원본 변경은 계획 범위 밖이며, proto를 바꾸면 별도 `make proto proto-dart`가 필요하다.
|
||||
- `<확인 필요>` 값 없음.
|
||||
|
||||
### 테스트 커버리지 공백
|
||||
|
||||
- `/fleet/status`는 `TestFleetStatusHTTPHandlerCombinesConnectionAndCapabilities`에서 online/degraded/offline과 capability를 확인하지만, 두 connected Edge의 서로 다른 `domain_agents` 구분은 고정하지 않는다.
|
||||
- `/edges/{id}/operations`는 `TestEdgeOperationsHTTPHandlerListsAudit`에서 단일 Edge audit만 확인한다. 다른 Edge command가 섞이지 않는지, 각 Edge 조회가 자기 command만 반환하는지 고정하지 않는다.
|
||||
- Edge service는 `TestServiceCapabilitiesAndDomainAgents`와 `TestServiceExecuteCommand...`로 domain agent summary와 command event를 확인한다. Control Plane HTTP aggregation과 Edge별 isolation을 함께 검증하지는 않는다.
|
||||
|
||||
### 심볼 참조
|
||||
|
||||
- none. Rename/remove 없음.
|
||||
|
||||
### 분할 판단
|
||||
|
||||
- split decision policy 적용 완료.
|
||||
- shared task group: `agent-task/m-multi-edge-operations`.
|
||||
- `01_history_agent_contract`: backend HTTP/registry contract 검증. 선행 없음.
|
||||
- `02+01_history_agent_client`: Flutter 운영 화면 구현/검증. `01_history_agent_contract`의 `complete.log`가 필요하다.
|
||||
- split 이유: backend JSON contract와 Flutter UI state는 ownership, verification command, failure profile이 다르다.
|
||||
|
||||
### 범위 결정 근거
|
||||
|
||||
- 포함: `apps/control-plane/cmd/control-plane/main_test.go`; 테스트 실패가 실제 contract gap을 드러낼 때만 `apps/control-plane/cmd/control-plane/main.go`.
|
||||
- 포함 가능: Edge domain agent summary 생성에 gap이 있으면 `apps/edge/internal/service/service.go`와 `apps/edge/internal/service/service_test.go`.
|
||||
- 제외: proto schema 변경, durable audit/history 저장소, OTO artifact/log 원본 저장, Flutter UI 변경, docs/README 갱신.
|
||||
- Control Plane은 Edge-owned summary만 관찰한다. Node token/address/config raw settings를 HTTP response에 추가하지 않는다.
|
||||
|
||||
### 빌드 등급
|
||||
|
||||
- build lane/grade: `local-G05`. Go HTTP contract와 focused tests로 검증 가능한 bounded backend 작업이다.
|
||||
|
||||
## 구현 체크리스트
|
||||
|
||||
- [ ] `/fleet/status`가 두 connected Edge의 서로 다른 domain agent capability/status/command summary를 구분해 반환하는 focused test를 추가한다.
|
||||
- [ ] `/edges/{edge_id}/operations`가 Edge별 command audit을 섞지 않는 focused test를 추가한다.
|
||||
- [ ] 필요 시 backend mapper/registry 구현을 테스트 기대에 맞게 보강하되 Control Plane이 Edge-owned 원본 데이터를 소유하지 않도록 유지한다.
|
||||
- [ ] `go test ./apps/control-plane/cmd/control-plane ./apps/control-plane/internal/wire ./apps/edge/internal/service -count=1`를 실행한다.
|
||||
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
|
||||
|
||||
### [API-1] Fleet Status Domain Agent Isolation
|
||||
|
||||
문제: `apps/control-plane/cmd/control-plane/main.go:501`의 `/fleet/status`는 registry snapshot별로 `fleetEdgeViewFromState`를 호출하고, `main.go:690`에서 live status의 domain agents를 view에 복사한다. 기존 `apps/control-plane/cmd/control-plane/main_test.go:675`는 capability와 health만 고정하고 두 Edge의 서로 다른 `domain_agents` 구분을 검증하지 않는다.
|
||||
|
||||
해결 방법: `TestFleetStatusHTTPHandlerCombinesConnectionAndCapabilities`에 connected Edge를 하나 더 추가하거나 subtest를 추가한다. `requestStatus`는 Edge ID별로 서로 다른 `EdgeDomainAgentSummary`를 반환하게 한다. JSON decode 후 edge map에서 `edge-online`과 새 Edge의 `DomainAgents`가 각각 기대 `agent_kind`, `lifecycle_state`, `active_command_id`, `summary`를 갖는지 확인한다. 기존 node internals leak guard는 유지한다.
|
||||
|
||||
수정 파일 및 체크리스트:
|
||||
|
||||
- [ ] `apps/control-plane/cmd/control-plane/main_test.go`: fleet status test fixture에 distinct domain agent summary 추가.
|
||||
- [ ] `apps/control-plane/cmd/control-plane/main_test.go`: Edge별 `DomainAgents` assertion 추가.
|
||||
- [ ] `apps/control-plane/cmd/control-plane/main.go`: 테스트가 드러낸 mapper gap이 있을 때만 수정.
|
||||
|
||||
테스트 작성: 작성. `TestFleetStatusHTTPHandlerCombinesConnectionAndCapabilities` 또는 새 focused test에서 Edge별 domain agent summary를 검증한다.
|
||||
|
||||
중간 검증:
|
||||
|
||||
```bash
|
||||
go test ./apps/control-plane/cmd/control-plane -run 'TestFleetStatusHTTPHandlerCombinesConnectionAndCapabilities' -count=1
|
||||
```
|
||||
|
||||
### [API-2] Edge Operations Audit Isolation
|
||||
|
||||
문제: `apps/control-plane/cmd/control-plane/main.go:450`의 `/edges/{id}/operations`는 `registry.Commands(edgeID)`를 반환하고, `apps/control-plane/internal/wire/edge_registry.go:328`은 Edge ID로 command records를 필터링한다. 기존 `apps/control-plane/cmd/control-plane/main_test.go:643`은 edge-a command 하나만 seed해 cross-edge 섞임을 검증하지 않는다.
|
||||
|
||||
해결 방법: `TestEdgeOperationsHTTPHandlerListsAudit`에 `edge-b` command request/event/result를 추가한다. `/edges/edge-a/operations` 응답에 edge-b command가 없는지 확인하고, `/edges/edge-b/operations`도 조회해 edge-b command만 반환되는지 확인한다.
|
||||
|
||||
수정 파일 및 체크리스트:
|
||||
|
||||
- [ ] `apps/control-plane/cmd/control-plane/main_test.go`: edge-b command record fixture 추가.
|
||||
- [ ] `apps/control-plane/cmd/control-plane/main_test.go`: edge-a/edge-b 양방향 operations isolation assertion 추가.
|
||||
- [ ] `apps/control-plane/internal/wire/edge_registry.go`: 테스트가 기존 filtering gap을 드러낼 때만 수정.
|
||||
|
||||
테스트 작성: 작성. HTTP handler 레벨에서 Edge별 operations isolation을 검증한다.
|
||||
|
||||
중간 검증:
|
||||
|
||||
```bash
|
||||
go test ./apps/control-plane/cmd/control-plane -run 'TestEdgeOperationsHTTPHandlerListsAudit' -count=1
|
||||
```
|
||||
|
||||
## 수정 파일 요약
|
||||
|
||||
| 파일 | 항목 |
|
||||
|------|------|
|
||||
| `apps/control-plane/cmd/control-plane/main_test.go` | API-1, API-2 |
|
||||
| `apps/control-plane/cmd/control-plane/main.go` | API-1, API-2 only if tests reveal mapper gap |
|
||||
| `apps/control-plane/internal/wire/edge_registry.go` | API-2 only if filtering gap appears |
|
||||
| `apps/edge/internal/service/service.go` | API-1 only if domain agent source summary gap appears |
|
||||
| `apps/edge/internal/service/service_test.go` | API-1 only if edge service behavior changes |
|
||||
|
||||
## 최종 검증
|
||||
|
||||
```bash
|
||||
go test ./apps/control-plane/cmd/control-plane ./apps/control-plane/internal/wire ./apps/edge/internal/service -count=1
|
||||
```
|
||||
|
||||
기대 결과: all packages pass. Go test cache output은 허용하지 않는다.
|
||||
|
||||
모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.
|
||||
|
|
@ -0,0 +1,188 @@
|
|||
<!-- task=m-multi-edge-operations/02+01_history_agent_client 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 user-only decision, user-owned external environment prerequisite, or scope conflict, fill `사용자 리뷰 요청` with evidence and stop with active files in place; code-review decides whether to write `USER_REVIEW.md`. Evidence gaps that a follow-up agent can close by rerunning commands or collecting artifacts are normal follow-up issues, not user-review blockers by themselves.
|
||||
> 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-04
|
||||
task=m-multi-edge-operations/02+01_history_agent_client, plan=0, tag=API
|
||||
|
||||
## Roadmap Targets
|
||||
|
||||
- Milestone: `agent-roadmap/phase/control-plane-portal-ops/milestones/multi-edge-operations.md`
|
||||
- Task ids:
|
||||
- `history-agent-state`: Edge별 실행/명령 결과와 domain agent capability/status/command summary가 운영 화면에서 구분된다.
|
||||
- Completion mode: check-on-pass
|
||||
|
||||
## 이 파일을 읽는 리뷰 에이전트에게
|
||||
|
||||
> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다.
|
||||
|
||||
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
|
||||
리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다.
|
||||
|
||||
1. 판정을 append한다.
|
||||
2. `CODE_REVIEW-local-G06.md` -> `code_review_local_G06_N.log`, `PLAN-local-G06.md` -> `plan_local_G06_M.log`로 아카이브한다.
|
||||
3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/m-multi-edge-operations/02+01_history_agent_client/`로 이동한다. WARN/FAIL이면 user-review gate를 확인한 뒤 다음 active plan/review 파일 또는 `USER_REVIEW.md`를 작성한다.
|
||||
4. PASS이고 task group이 `m-multi-edge-operations`이면 완료 이벤트 메타데이터를 보고한다. roadmap 상태 체크와 `update-roadmap` 호출은 런타임 책임이다.
|
||||
5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다.
|
||||
|
||||
---
|
||||
|
||||
## 구현 항목별 완료 여부
|
||||
|
||||
| 항목 | 완료 여부 |
|
||||
|------|---------|
|
||||
| [API-1] Runtime Panel Clears Stale Edge Operations | [x] |
|
||||
| [API-2] Edge-Specific Runtime Fixture And Assertions | [x] |
|
||||
|
||||
## 구현 체크리스트
|
||||
|
||||
- [x] `01_history_agent_contract` predecessor complete.log가 있는지 확인한다.
|
||||
- [x] Runtime panel이 Edge 선택 변경과 느린 fetch 중 stale operations/domain agent state를 섞지 않도록 보강한다.
|
||||
- [x] Fake repository가 Edge별로 서로 다른 domain agents, active command id, operations history, command target Edge를 반환/기록하도록 만든다.
|
||||
- [x] widget test에서 Runtime panel of Edge switch 후 domain agents와 operations history가 Edge별로 구분되는지 검증한다.
|
||||
- [x] `cd apps/client && flutter test test/widget_test.dart`를 실행한다.
|
||||
- [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_G06_N.log`로 아카이브한다.
|
||||
- [x] active `PLAN-*-G??.md`를 `plan_local_G06_M.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-multi-edge-operations/02+01_history_agent_client/`를 `agent-task/archive/YYYY/MM/m-multi-edge-operations/02+01_history_agent_client/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다.
|
||||
- [ ] PASS이고 task group이 `m-multi-edge-operations`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다.
|
||||
- [ ] PASS split 작업이면 이동 후 빈 active parent `agent-task/m-multi-edge-operations/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다.
|
||||
- [x] WARN/FAIL이고 user-review gate가 트리거되지 않았으면 다음 active `PLAN-local-G07.md`와 `CODE_REVIEW-local-G07.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가 사용자 결정으로 완료/PASS 해소되면 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log`를 작성한 뒤 task directory를 archive로 이동한다.
|
||||
|
||||
## 계획 대비 변경 사항
|
||||
|
||||
없음.
|
||||
|
||||
## 주요 설계 결정
|
||||
|
||||
1. **Stale Operations Clear & Response Guard**:
|
||||
`RuntimePanel`이 selected Edge 변경 시 기존 operations 데이터를 즉시 `null`로 비우도록 하여, 새 Edge의 데이터 로딩 중 이전 Edge의 operations 내역이 잘못 노출되는 UX 결함을 수정했습니다. 또한 비동기 응답 처리 완료 시점의 selected Edge ID와 응답 대상 Edge ID를 비교하여, 고속 Edge 전환 시 stale response가 반영되지 않도록 guard하였습니다.
|
||||
2. **Edge별 Domain Agent & Operations 이력 분리**:
|
||||
`FakeControlPlaneStatusRepository`에서 `edge-a`와 `edge-b`에 대해 서로 다른 `domainAgents` 상태(`READY`/`READY` vs `BUSY`/`READY`) 및 `operations` 내역(`oto-agent.sync` vs `build-deploy.deploy`)을 반환하도록 설정하고, command 호출 시 target Edge ID to `lastCommandEdgeId`로 캡처하여 검증하도록 설계했습니다.
|
||||
|
||||
## 사용자 리뷰 요청
|
||||
|
||||
- 상태: 없음
|
||||
- 사유 유형: 없음
|
||||
- 결정 필요: 없음
|
||||
- 차단 근거: 없음
|
||||
- 실행한 검증/명령: 없음
|
||||
- 자동 후속 불가 이유: 없음
|
||||
- 재개 조건: 없음
|
||||
|
||||
## 리뷰어를 위한 체크포인트
|
||||
|
||||
- predecessor `01_history_agent_contract` 완료 근거가 확인됐는지 본다.
|
||||
- Runtime panel이 선택 Edge 변경 중 stale operations를 보여주지 않는지 본다.
|
||||
- widget test가 alpha-only/beta-only domain agent와 operations text를 모두 검증하는지 본다.
|
||||
- Client가 Edge/Node transport에 직접 붙지 않고 Control Plane repository boundary만 소비하는지 본다.
|
||||
|
||||
## 검증 결과
|
||||
|
||||
### API-1 중간 검증
|
||||
```bash
|
||||
$ cd apps/client && flutter test test/widget_test.dart -r expanded --plain-name 'Client App opens Operations & Domain Agents panel and verifies agents, operations history, and command triggering'
|
||||
00:00 +0: loading /config/workspace/iop/apps/client/test/widget_test.dart
|
||||
00:00 +0: Client App opens Operations & Domain Agents panel and verifies agents,
|
||||
operations history, and command triggering
|
||||
00:01 +1: All tests passed!
|
||||
```
|
||||
|
||||
### API-2 중간 검증
|
||||
```bash
|
||||
$ cd apps/client && flutter test test/widget_test.dart -r expanded --plain-name 'Client App opens Operations & Domain Agents panel and verifies agents, operations history, and command triggering'
|
||||
00:00 +0: loading /config/workspace/iop/apps/client/test/widget_test.dart
|
||||
00:00 +0: Client App opens Operations & Domain Agents panel and verifies agents,
|
||||
operations history, and command triggering
|
||||
00:01 +1: All tests passed!
|
||||
```
|
||||
|
||||
### 최종 검증
|
||||
```bash
|
||||
$ cd apps/client && flutter test test/widget_test.dart
|
||||
00:00 +0: loading /config/workspace/iop/apps/client/test/widget_test.dart
|
||||
00:01 +0: loading /config/workspace/iop/apps/client/test/widget_test.dart
|
||||
00:02 +0: loading /config/workspace/iop/apps/client/test/widget_test.dart
|
||||
00:03 +0: loading /config/workspace/iop/apps/client/test/widget_test.dart
|
||||
00:04 +0: loading /config/workspace/iop/apps/client/test/widget_test.dart
|
||||
00:04 +0: Client App basic rendering and success handshake test
|
||||
00:05 +0: Client App basic rendering and success handshake test
|
||||
00:05 +1: Client App connection error state test
|
||||
00:05 +2: Client App opens IOP agent panel from the left rail
|
||||
00:05 +3: Client App opens IOP agent panel from the left rail
|
||||
00:05 +3: Client App opens Edges panel and displays Edge details
|
||||
00:05 +4: Client App opens Edges panel and displays Edge details
|
||||
00:05 +4: Client ... Nodes panel and displays active Nodes and configurations
|
||||
00:06 +4: Client ... Nodes panel and displays active Nodes and configurations
|
||||
00:06 +5: Client App opens Execution/Logs panel and displays lifecycle events
|
||||
00:06 +6: Client App opens Execution/Logs panel and displays lifecycle events
|
||||
00:06 +6: Client App refresh behavior when selected edge disappears
|
||||
00:06 +7: Client ... agents, operations history, and command triggering
|
||||
00:07 +7: Client ... agents, operations history, and command triggering
|
||||
00:07 +8: Client ... agents, operations history, and command triggering
|
||||
00:07 +8: Client ... or error command responses and shows error banner
|
||||
00:07 +9: Client ... agent.status and agent.command without required inputs
|
||||
00:08 +9: Client ... agent.status and agent.command without required inputs
|
||||
00:08 +10: Client ... agent.status and agent.command without required inputs
|
||||
00:08 +10: Client App mobile screen layout verification for layout and overflow
|
||||
00:08 +11: Client App mobile screen layout verification for layout and overflow
|
||||
00:08 +11: All tests passed!
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
> **[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.
|
||||
|
||||
Sections and their ownership:
|
||||
|
||||
| Section | Owner | Note |
|
||||
|---------|-------|------|
|
||||
| Header comment, 개요, 리뷰 에이전트 지시 | Fixed at stub creation | Implementing agent must not modify or execute these |
|
||||
| Roadmap Targets | Fixed at stub creation | Do not modify; code-review copies to `complete.log` on PASS |
|
||||
| 구현 항목별 완료 여부 | Implementing agent | Check `[ ]` -> `[x]` only |
|
||||
| 구현 체크리스트 | Implementing agent | Check `[ ]` -> `[x]` only |
|
||||
| 코드리뷰 전용 체크리스트 | Review agent only | Implementing agent must not modify |
|
||||
| 계획 대비 변경 사항, 주요 설계 결정 | Implementing agent | Replace placeholders |
|
||||
| 사용자 리뷰 요청 | Implementing agent | Keep `상태: 없음` unless blocked |
|
||||
| 리뷰어를 위한 체크포인트 | Fixed at stub creation | Review focus |
|
||||
| 검증 결과 | Implementing agent | Paste actual output |
|
||||
|
||||
## 코드리뷰 결과
|
||||
|
||||
- 종합 판정: FAIL
|
||||
- 차원별 평가:
|
||||
- correctness: Pass
|
||||
- completeness: Fail
|
||||
- test coverage: Fail
|
||||
- API contract: Pass
|
||||
- code quality: Pass
|
||||
- plan deviation: Fail
|
||||
- verification trust: Pass
|
||||
- 발견된 문제:
|
||||
- Required: `apps/client/test/widget_test.dart:660`의 Edge Beta 전환 테스트가 `pumpAndSettle()`로 즉시 최종 상태만 확인하고, 이전 Edge의 operations가 새 Edge 로딩 중 사라지는지 검증하지 않습니다. 이 상태에서는 `_fetchOperations()`의 fetch-start clear가 제거되어도 빠른 fake response 때문에 테스트가 통과할 수 있습니다. `fetchEdgeOperations('edge-b')`를 지연/completer로 제어하는 fixture를 추가하고, Edge 전환 직후 beta response를 완료하기 전 `oto-agent.sync`와 `Synced 5 models`가 화면에서 사라졌는지 확인하세요.
|
||||
- Required: `apps/client/test/widget_test.dart:666`의 Edge Beta 검증은 `BUSY`/`READY`와 beta operation presence만 확인하고, 계획에서 요구한 alpha-only domain agent command summary/operation text의 부재를 확인하지 않습니다. 전환 전 `Active Command: cmd-alpha-active`를 확인하고, 전환 후 `Active Command: cmd-alpha-active`, `oto-agent.sync`, `Synced 5 models`는 `findsNothing`, `Active Command: cmd-beta-active`, `build-deploy.deploy`, `Deploy queued for edge beta`는 표시되도록 assertion을 추가하세요.
|
||||
- 다음 단계: WARN/FAIL 후속 plan/review 파일을 작성한다.
|
||||
|
|
@ -0,0 +1,185 @@
|
|||
<!-- task=m-multi-edge-operations/02+01_history_agent_client 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 user-only decision, user-owned external environment prerequisite, or scope conflict, fill `사용자 리뷰 요청` with evidence and stop with active files in place; code-review decides whether to write `USER_REVIEW.md`. Evidence gaps that a follow-up agent can close by rerunning commands or collecting artifacts are normal follow-up issues, not user-review blockers by themselves.
|
||||
> 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-04
|
||||
task=m-multi-edge-operations/02+01_history_agent_client, plan=1, tag=REVIEW_API
|
||||
|
||||
## Roadmap Targets
|
||||
|
||||
- Milestone: `agent-roadmap/phase/control-plane-portal-ops/milestones/multi-edge-operations.md`
|
||||
- Task ids:
|
||||
- `history-agent-state`: Edge별 실행/명령 결과와 domain agent capability/status/command summary가 운영 화면에서 구분된다.
|
||||
- Completion mode: check-on-pass
|
||||
|
||||
## 이 파일을 읽는 리뷰 에이전트에게
|
||||
|
||||
> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다.
|
||||
|
||||
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
|
||||
리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다.
|
||||
|
||||
1. 판정을 append한다.
|
||||
2. `CODE_REVIEW-local-G07.md` -> `code_review_local_G07_N.log`, `PLAN-local-G07.md` -> `plan_local_G07_M.log`로 아카이브한다.
|
||||
3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/m-multi-edge-operations/02+01_history_agent_client/`로 이동한다. WARN/FAIL이면 user-review gate를 확인한 뒤 다음 active plan/review 파일 또는 `USER_REVIEW.md`를 작성한다.
|
||||
4. PASS이고 task group이 `m-multi-edge-operations`이면 완료 이벤트 메타데이터를 보고한다. roadmap 상태 체크와 `update-roadmap` 호출은 런타임 책임이다.
|
||||
5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다.
|
||||
|
||||
---
|
||||
|
||||
## 구현 항목별 완료 여부
|
||||
|
||||
| 항목 | 완료 여부 |
|
||||
|------|---------|
|
||||
| [REVIEW_API-1] Delayed Operations Fetch Assertion | [x] |
|
||||
| [REVIEW_API-2] Edge-Specific Command Summary Negative Assertions | [x] |
|
||||
|
||||
## 구현 체크리스트
|
||||
|
||||
- [x] Fake repository에 Edge별 operations 응답을 지연/completer로 제어할 수 있는 테스트 helper를 추가한다.
|
||||
- [x] Runtime panel widget test가 Edge Beta operations 응답 완료 전 alpha operation text가 사라지는지 검증한다.
|
||||
- [x] Runtime panel widget test가 Edge 전환 전/후 alpha-only와 beta-only active command summary 및 operations text의 presence/absence를 모두 검증한다.
|
||||
- [x] `cd apps/client && flutter test test/widget_test.dart -r expanded --plain-name 'Client App opens Operations & Domain Agents panel and verifies agents, operations history, and command triggering'`를 실행한다.
|
||||
- [x] `cd apps/client && flutter test test/widget_test.dart`를 실행한다.
|
||||
- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
|
||||
|
||||
## 코드리뷰 전용 체크리스트
|
||||
|
||||
> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다.
|
||||
> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다.
|
||||
|
||||
- [x] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
|
||||
- [x] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다.
|
||||
- [x] active `CODE_REVIEW-*-G07.md`를 `code_review_local_G07_N.log`로 아카이브한다.
|
||||
- [x] active `PLAN-*-G07.md`를 `plan_local_G07_M.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-multi-edge-operations/02+01_history_agent_client/`를 `agent-task/archive/2026/06/m-multi-edge-operations/02+01_history_agent_client/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다.
|
||||
- [x] PASS이고 task group이 `m-multi-edge-operations`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다.
|
||||
- [x] PASS split 작업이면 이동 후 빈 active parent `agent-task/m-multi-edge-operations/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다.
|
||||
- [ ] WARN/FAIL이고 user-review gate가 트리거되지 않았으면 다음 active `PLAN-local-G07.md`와 `CODE_REVIEW-local-G07.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가 사용자 결정으로 완료/PASS 해소되면 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log`를 작성한 뒤 task directory를 archive로 이동한다.
|
||||
|
||||
## 계획 대비 변경 사항
|
||||
|
||||
없음.
|
||||
|
||||
## 주요 설계 결정
|
||||
|
||||
1. **지연 및 과도기 상태 검증 (Completer 기반)**:
|
||||
`FakeControlPlaneStatusRepository` 내부에 `Completer<EdgeOperationsResponseView>` 필드를 도입하여 Edge Beta의 operations 데이터 fetching 시점을 테스트 코드에서 완전히 제어할 수 있도록 하였습니다. 이를 통해 `Edge Beta`를 선택한 프레임 직후(fetch 완료 전 과도기 상태)에서 로딩 인디케이터(`CircularProgressIndicator`) 노출 여부와 이전 Edge Alpha의 이력이 즉각적으로 소거(clear)되었는지를 단언(assertion)할 수 있게 검증 환경을 강화했습니다.
|
||||
2. **Negative Assertions (비존재 검증) 보강**:
|
||||
단순히 최종 상태에서 필요한 위젯이 노출되는지만 체크하는 기존의 취약성을 개선하여, Edge Alpha 상태에서는 Alpha 관련 항목만 보이고 Beta 관련 항목이 없는지, 그리고 Edge Beta 상태로 전환된 후에는 Alpha 관련 데이터(`cmd-alpha-active`, `oto-agent.sync`, `Synced 5 models`)가 화면상에서 완벽히 사라졌는지를 명시적으로 검증하는 `findsNothing` 단언을 앞뒤로 촘촘하게 추가하여 검증 공백을 메웠습니다.
|
||||
|
||||
## 사용자 리뷰 요청
|
||||
|
||||
- 상태: 없음
|
||||
- 사유 유형: 없음
|
||||
- 결정 필요: 없음
|
||||
- 차단 근거: 없음
|
||||
- 실행한 검증/명령: 없음
|
||||
- 자동 후속 불가 이유: 없음
|
||||
- 재개 조건: 없음
|
||||
|
||||
## 리뷰어를 위한 체크포인트
|
||||
|
||||
- edge-b operations fetch가 test에서 실제로 지연되고, beta response 완료 전 alpha operation text가 사라지는지 본다.
|
||||
- Edge 전환 전/후 `Active Command: cmd-alpha-active`와 `Active Command: cmd-beta-active`의 presence/absence가 모두 검증되는지 본다.
|
||||
- beta operation 표시 검증 and alpha operation 부재 검증이 같은 Runtime panel test 안에서 연결되어 있는지 본다.
|
||||
- 후속 변경이 `apps/client/test/widget_test.dart` 중심으로 좁게 유지됐는지 본다.
|
||||
|
||||
## 검증 결과
|
||||
|
||||
### REVIEW_API-1 중간 검증
|
||||
```bash
|
||||
$ cd apps/client && flutter test test/widget_test.dart -r expanded --plain-name 'Client App opens Operations & Domain Agents panel and verifies agents, operations history, and command triggering'
|
||||
00:00 +0: loading /config/workspace/iop/apps/client/test/widget_test.dart
|
||||
00:00 +0: Client App opens Operations & Domain Agents panel and verifies agents,
|
||||
operations history, and command triggering
|
||||
00:01 +1: All tests passed!
|
||||
```
|
||||
|
||||
### REVIEW_API-2 중간 검증
|
||||
```bash
|
||||
$ cd apps/client && flutter test test/widget_test.dart -r expanded --plain-name 'Client App opens Operations & Domain Agents panel and verifies agents, operations history, and command triggering'
|
||||
00:00 +0: loading /config/workspace/iop/apps/client/test/widget_test.dart
|
||||
00:00 +0: Client App opens Operations & Domain Agents panel and verifies agents,
|
||||
operations history, and command triggering
|
||||
00:01 +1: All tests passed!
|
||||
```
|
||||
|
||||
### 최종 검증
|
||||
```bash
|
||||
$ cd apps/client && flutter test test/widget_test.dart
|
||||
00:00 +0: loading /config/workspace/iop/apps/client/test/widget_test.dart
|
||||
00:01 +0: loading /config/workspace/iop/apps/client/test/widget_test.dart
|
||||
00:02 +0: loading /config/workspace/iop/apps/client/test/widget_test.dart
|
||||
00:02 +0: Client App basic rendering and success handshake test
|
||||
00:03 +0: Client App basic rendering and success handshake test
|
||||
00:03 +1: Client App connection error state test
|
||||
00:03 +2: Client App connection error state test
|
||||
00:03 +2: Client App opens IOP agent panel from the left rail
|
||||
00:03 +3: Client App opens IOP agent panel from the left rail
|
||||
00:03 +3: Client App opens Edges panel and displays Edge details
|
||||
00:04 +3: Client App opens Edges panel and displays Edge details
|
||||
00:04 +4: Client App opens Edges panel and displays Edge details
|
||||
00:04 +4: Client ... Nodes panel and displays active Nodes and configurations
|
||||
00:04 +5: Client App opens Execution/Logs panel and displays lifecycle events
|
||||
00:04 +6: Client App opens Execution/Logs panel and displays lifecycle events
|
||||
00:04 +6: Client App refresh behavior when selected edge disappears
|
||||
00:04 +7: Client App refresh behavior when selected edge disappears
|
||||
00:04 +7: Client ... agents, operations history, and command triggering
|
||||
00:05 +7: Client ... agents, operations history, and command triggering
|
||||
00:05 +8: Client ... or error command responses and shows error banner
|
||||
00:05 +9: Client ... or error command responses and shows error banner
|
||||
00:05 +9: Client ... agent.status and agent.command without required inputs
|
||||
00:05 +10: Client App mobile screen layout verification for layout and overflow
|
||||
00:06 +10: Client App mobile screen layout verification for layout and overflow
|
||||
00:06 +11: Client App mobile screen layout verification for layout and overflow
|
||||
00:06 +11: All tests passed!
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
> **[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.
|
||||
|
||||
Sections and their ownership:
|
||||
|
||||
| Section | Owner | Note |
|
||||
|---------|-------|------|
|
||||
| Header comment, 개요, 리뷰 에이전트 지시 | Fixed at stub creation | Implementing agent must not modify or execute these |
|
||||
| Roadmap Targets | Fixed at stub creation | Do not modify; code-review copies to `complete.log` on PASS |
|
||||
| 구현 항목별 완료 여부 | Implementing agent | Check `[ ]` -> `[x]` only |
|
||||
| 구현 체크리스트 | Implementing agent | Check `[ ]` -> `[x]` only |
|
||||
| 코드리뷰 전용 체크리스트 | Review agent only | Implementing agent must not modify |
|
||||
| 계획 대비 변경 사항, 주요 설계 결정 | Implementing agent | Replace placeholders |
|
||||
| 사용자 리뷰 요청 | Implementing agent | Keep `상태: 없음` unless blocked |
|
||||
| 리뷰어를 위한 체크포인트 | Fixed at stub creation | Review focus |
|
||||
| 검증 결과 | Implementing agent | Paste actual output |
|
||||
|
||||
## 코드리뷰 결과
|
||||
|
||||
- 종합 판정: PASS
|
||||
- 차원별 평가:
|
||||
- correctness: Pass
|
||||
- completeness: Pass
|
||||
- test coverage: Pass
|
||||
- API contract: Pass
|
||||
- code quality: Pass
|
||||
- plan deviation: Pass
|
||||
- verification trust: Pass
|
||||
- 발견된 문제: 없음
|
||||
- 다음 단계: PASS 종결 절차를 수행한다.
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
# Complete - m-multi-edge-operations/02+01_history_agent_client
|
||||
|
||||
## 완료 일시
|
||||
|
||||
2026-06-04
|
||||
|
||||
## 요약
|
||||
|
||||
History agent state client view review loop 2 completed with PASS.
|
||||
|
||||
## 루프 이력
|
||||
|
||||
| Plan | Review | Verdict | 메모 |
|
||||
|------|--------|---------|------|
|
||||
| `plan_local_G06_0.log` | `code_review_local_G06_0.log` | FAIL | Edge 전환 중 stale operations 제거와 alpha-only command/operation 부재를 테스트가 직접 검증하지 못해 follow-up이 생성되었다. |
|
||||
| `plan_local_G07_1.log` | `code_review_local_G07_1.log` | PASS | Runtime panel widget test가 delayed edge-b operations fetch, alpha/beta active command summary, operations presence/absence를 모두 검증하도록 보강되었다. |
|
||||
|
||||
## 구현/정리 내용
|
||||
|
||||
- Runtime panel operations fetch가 Edge 선택 변경 시 이전 operations를 즉시 비우고 stale async response를 무시하도록 유지되었다.
|
||||
- `FakeControlPlaneStatusRepository`가 Edge별 domain agent state, active command id, operations history, command dispatch target Edge를 분리해 반환/기록한다.
|
||||
- Runtime panel widget test가 Edge Beta operations fetch 완료 전 Alpha operations text가 사라지는지 검증하고, 전환 전후 alpha-only/beta-only command summary와 operations text의 presence/absence를 모두 확인한다.
|
||||
|
||||
## 최종 검증
|
||||
|
||||
- `cd apps/client && flutter test test/widget_test.dart -r expanded --plain-name 'Client App opens Operations & Domain Agents panel and verifies agents, operations history, and command triggering'` - PASS; `All tests passed!`.
|
||||
- `cd apps/client && flutter test test/widget_test.dart` - PASS; `All tests passed!`.
|
||||
- `git diff --check -- apps/client/test/widget_test.dart apps/client/lib/control_plane_status_widgets.dart` - PASS; whitespace errors 없음.
|
||||
|
||||
## Roadmap Completion
|
||||
|
||||
- Milestone: `agent-roadmap/phase/control-plane-portal-ops/milestones/multi-edge-operations.md`
|
||||
- Completed task ids:
|
||||
- `history-agent-state`: PASS; evidence=`plan_local_G07_1.log`, `code_review_local_G07_1.log`; verification=`cd apps/client && flutter test test/widget_test.dart`
|
||||
- Not completed task ids: 없음
|
||||
|
||||
## 잔여 Nit
|
||||
|
||||
- 없음
|
||||
|
||||
## 후속 작업
|
||||
|
||||
- 없음
|
||||
|
|
@ -0,0 +1,159 @@
|
|||
<!-- task=m-multi-edge-operations/02+01_history_agent_client plan=0 tag=API -->
|
||||
|
||||
# Plan - History Agent State Client View
|
||||
|
||||
## 이 파일을 읽는 구현 에이전트에게
|
||||
|
||||
구현 완료는 active `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션까지 실제 구현 내용과 검증 출력으로 채운 뒤에만 성립한다. 검증 명령을 실행하고 실제 stdout/stderr를 review stub에 붙인다. active 파일은 그대로 두고 리뷰 준비 상태를 보고하며, `complete.log` 작성, `.log` rename, archive 이동은 code-review 스킬 전용이다. 구현 중 사용자 결정, 사용자 소유 외부 환경/secret/서비스 준비, 또는 계획 범위 충돌로 막히면 review stub의 `사용자 리뷰 요청`에 정확한 근거를 남기고 멈춘다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 증거 공백은 사용자 리뷰 요청이 아니다.
|
||||
|
||||
## 배경
|
||||
|
||||
Runtime panel은 domain agent summary와 operations history를 운영자가 보는 1차 화면이다. 현재 test fixture는 모든 Edge에 같은 domain agents와 operations를 돌려주므로, Edge 선택 변경 시 history/agent state가 실제로 분리 표시되는지 검증하지 못한다. 이 작업은 backend 계약 subtask가 통과한 뒤 Flutter 화면과 repository fixture를 Edge별로 분리해 `history-agent-state` 완료 근거를 만든다.
|
||||
|
||||
## 사용자 리뷰 요청 흐름
|
||||
|
||||
구현 중 blocker는 active review stub의 `사용자 리뷰 요청` 섹션에 기록한다. 해당 섹션은 `agent-ops/skills/common/_templates/implementation-user-review-request-section.md` 기준이며, 실제 `USER_REVIEW.md` 작성 여부 판단은 code-review가 소유한다.
|
||||
|
||||
## Roadmap Targets
|
||||
|
||||
- Milestone: `agent-roadmap/phase/control-plane-portal-ops/milestones/multi-edge-operations.md`
|
||||
- Task ids:
|
||||
- `history-agent-state`: Edge별 실행/명령 결과와 domain agent capability/status/command summary가 운영 화면에서 구분된다.
|
||||
- Completion mode: check-on-pass
|
||||
|
||||
## 분석 결과
|
||||
|
||||
### 읽은 파일
|
||||
|
||||
- `agent-roadmap/current.md`
|
||||
- `agent-roadmap/phase/control-plane-portal-ops/PHASE.md`
|
||||
- `agent-roadmap/phase/control-plane-portal-ops/milestones/multi-edge-operations.md`
|
||||
- `agent-test/local/rules.md`
|
||||
- `agent-test/local/client-smoke.md`
|
||||
- `agent-test/local/control-plane-smoke.md`
|
||||
- `agent-test/local/edge-smoke.md`
|
||||
- `agent-test/local/platform-common-smoke.md`
|
||||
- `agent-ops/rules/project/domain/client/rules.md`
|
||||
- `agent-ops/rules/project/domain/control-plane/rules.md`
|
||||
- `agent-ops/rules/project/domain/edge/rules.md`
|
||||
- `agent-ops/rules/project/domain/testing/rules.md`
|
||||
- `apps/client/lib/control_plane_status_client.dart`
|
||||
- `apps/client/lib/control_plane_status_widgets.dart`
|
||||
- `apps/client/lib/main.dart`
|
||||
- `apps/client/test/widget_test.dart`
|
||||
- `apps/control-plane/cmd/control-plane/main.go`
|
||||
- `apps/control-plane/cmd/control-plane/main_test.go`
|
||||
- `apps/control-plane/internal/wire/edge_registry.go`
|
||||
- `apps/control-plane/internal/wire/edge_server.go`
|
||||
- `apps/edge/internal/service/service.go`
|
||||
- `apps/edge/internal/service/service_test.go`
|
||||
- `proto/iop/control.proto`
|
||||
|
||||
### 테스트 환경 규칙
|
||||
|
||||
- test_env: `local`.
|
||||
- `agent-test/local/rules.md` present/read. Matching profiles read: `client-smoke`, `control-plane-smoke`, `edge-smoke`, `platform-common-smoke`.
|
||||
- 적용 명령: client 변경은 최소 `cd apps/client && flutter test`; analyzer 영향이 있으면 `cd apps/client && flutter analyze --no-fatal-infos`.
|
||||
- 현재 작업 컨테이너에서 `cd apps/client && flutter test test/widget_test.dart`가 이미 성공했다. 원격 runtime evidence는 web/dev-server 또는 Docker compose 변경이 있을 때만 필요하다.
|
||||
- `<확인 필요>` 값 없음.
|
||||
|
||||
### 테스트 커버리지 공백
|
||||
|
||||
- `apps/client/test/widget_test.dart:199`의 `fetchFleetStatus`는 모든 Edge에 동일한 domain agents를 반환한다.
|
||||
- `apps/client/test/widget_test.dart:247`의 `fetchEdgeOperations`는 어떤 Edge ID에도 같은 operation을 반환한다.
|
||||
- `apps/client/test/widget_test.dart:553`의 Runtime panel test는 기본 선택 Edge 하나만 검증한다. Edge 선택 변경 후 domain agent summary, active command, operations history가 바뀌는지 확인하지 않는다.
|
||||
- `apps/client/lib/control_plane_status_widgets.dart:856`의 `_fetchOperations`는 새 Edge fetch 시작 시 기존 `_operations`를 유지한다. 느린 요청이나 선택 변경 중 stale operation이 보일 수 있는 위험이 있다.
|
||||
|
||||
### 심볼 참조
|
||||
|
||||
- none. Rename/remove 없음.
|
||||
|
||||
### 분할 판단
|
||||
|
||||
- split decision policy 적용 완료.
|
||||
- shared task group: `agent-task/m-multi-edge-operations`.
|
||||
- predecessor: `01_history_agent_contract` is required by directory name `02+01_history_agent_client`; active predecessor exists but `complete.log` is missing. Implementation must wait until `agent-task/m-multi-edge-operations/01_history_agent_contract/complete.log` or archived equivalent exists.
|
||||
- 이 subtask는 backend contract 고정 후 client UI/state/test를 다룬다. Backend 수정은 predecessor 범위로 남긴다.
|
||||
|
||||
### 범위 결정 근거
|
||||
|
||||
- 포함: `apps/client/lib/control_plane_status_widgets.dart`, `apps/client/test/widget_test.dart`.
|
||||
- 포함 가능: repository model parsing gap이 있으면 `apps/client/lib/control_plane_status_client.dart`; selected-edge state propagation gap이 있으면 `apps/client/lib/main.dart`.
|
||||
- 제외: Go backend contract 수정, proto 변경, durable audit/history 저장, domain-agent artifact/log 상세 표시, NomadCode-specific shell UX.
|
||||
- UI는 Control Plane HTTP 표면을 소비하며 Edge/Node TCP transport에 직접 붙지 않는다.
|
||||
|
||||
### 빌드 등급
|
||||
|
||||
- build lane/grade: `local-G06`. Flutter state/UI test가 핵심이고 predecessor contract에 의존하지만, bounded widget tests로 검증 가능하다.
|
||||
|
||||
## 구현 체크리스트
|
||||
|
||||
- [ ] `01_history_agent_contract` predecessor complete.log가 있는지 확인한다.
|
||||
- [ ] Runtime panel이 Edge 선택 변경과 느린 fetch 중 stale operations/domain agent state를 섞지 않도록 보강한다.
|
||||
- [ ] Fake repository가 Edge별로 서로 다른 domain agents, active command id, operations history, command target Edge를 반환/기록하도록 만든다.
|
||||
- [ ] widget test에서 Runtime panel의 Edge 선택 변경 후 domain agents와 operations history가 Edge별로 구분되는지 검증한다.
|
||||
- [ ] `cd apps/client && flutter test test/widget_test.dart`를 실행한다.
|
||||
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
|
||||
|
||||
## 의존 관계 및 구현 순서
|
||||
|
||||
이 subtask 디렉터리명은 `02+01_history_agent_client`다. predecessor index `01`은 `agent-task/m-multi-edge-operations/01_history_agent_contract`이며, 구현 시작 전 active 또는 archived `complete.log`가 필요하다. 현재 작성 시점에는 predecessor complete.log가 없다.
|
||||
|
||||
### [API-1] Runtime Panel Clears Stale Edge Operations
|
||||
|
||||
문제: `apps/client/lib/control_plane_status_widgets.dart:849`는 selected Edge가 바뀌면 `_fetchOperations()`를 호출하지만, `control_plane_status_widgets.dart:856`은 fetch 시작 시 기존 `_operations`를 지우지 않는다. `_buildOperationsHistory`는 `control_plane_status_widgets.dart:1379`에서 `_operations`가 nil일 때만 loading을 보여주므로, 새 Edge 로딩 중 이전 Edge operations가 보일 수 있다.
|
||||
|
||||
해결 방법: `_RuntimePanelState`에 `_operationsEdgeId` 같은 owner field를 두거나, `_fetchOperations` 시작 시 `_operations = null`로 초기화한다. async completion 시 `mounted && widget.selectedEdgeId == edgeId`를 확인한 뒤에만 `_operations`를 set한다. 이 변경은 selected Edge가 빠르게 바뀌어도 stale result가 화면에 남지 않게 한다.
|
||||
|
||||
수정 파일 및 체크리스트:
|
||||
|
||||
- [ ] `apps/client/lib/control_plane_status_widgets.dart`: operations owner Edge ID tracking 또는 fetch-start clear 추가.
|
||||
- [ ] `apps/client/lib/control_plane_status_widgets.dart`: async response guard 추가.
|
||||
|
||||
테스트 작성: widget test에서 Edge switch 후 이전 Edge operation 텍스트가 사라지는 assertion으로 검증한다.
|
||||
|
||||
중간 검증:
|
||||
|
||||
```bash
|
||||
cd apps/client && flutter test test/widget_test.dart -r expanded --plain-name 'Client App opens Operations & Domain Agents panel and verifies agents, operations history, and command triggering'
|
||||
```
|
||||
|
||||
### [API-2] Edge-Specific Runtime Fixture And Assertions
|
||||
|
||||
문제: `apps/client/test/widget_test.dart:199`은 fleet status fixture를 `mockEdges.map`으로 만들며 모든 Edge에 같은 domain agents를 붙인다. `apps/client/test/widget_test.dart:247`은 edgeId를 받지만 operation 내용은 항상 `oto-agent.sync`/`Synced 5 models`다. 따라서 `history-agent-state`의 Edge별 구분 요구를 테스트가 보장하지 않는다.
|
||||
|
||||
해결 방법: `FakeControlPlaneStatusRepository`에 Edge별 fixture helpers를 추가한다. 예: edge-a는 `oto-agent READY`, `build-deploy READY`, `cmd-alpha-active`, `oto-agent.sync`, `Synced 5 models`; edge-b는 `oto-agent BUSY` 또는 `ERROR`, `cmd-beta-active`, `build-deploy.deploy`, `Deploy queued for edge beta`. Runtime panel test에서 dropdown으로 edge-b를 선택하고 alpha-only texts가 사라지고 beta-only texts가 나타나는지 검증한다. `sendEdgeCommand`에는 `lastCommandEdgeId`를 기록해 선택된 Edge로 command가 나가는지도 확인한다.
|
||||
|
||||
수정 파일 및 체크리스트:
|
||||
|
||||
- [ ] `apps/client/test/widget_test.dart`: Edge별 domain agent fixture 분리.
|
||||
- [ ] `apps/client/test/widget_test.dart`: Edge별 operations fixture 분리.
|
||||
- [ ] `apps/client/test/widget_test.dart`: Runtime panel Edge switch assertion 추가.
|
||||
- [ ] `apps/client/test/widget_test.dart`: selected Edge command dispatch assertion 추가.
|
||||
|
||||
테스트 작성: 작성. 기존 Runtime panel widget test를 확장하거나 새 focused widget test를 추가한다.
|
||||
|
||||
중간 검증:
|
||||
|
||||
```bash
|
||||
cd apps/client && flutter test test/widget_test.dart -r expanded --plain-name 'Client App opens Operations & Domain Agents panel and verifies agents, operations history, and command triggering'
|
||||
```
|
||||
|
||||
## 수정 파일 요약
|
||||
|
||||
| 파일 | 항목 |
|
||||
|------|------|
|
||||
| `apps/client/lib/control_plane_status_widgets.dart` | API-1 |
|
||||
| `apps/client/test/widget_test.dart` | API-1, API-2 |
|
||||
| `apps/client/lib/control_plane_status_client.dart` | API-2 only if parsing/model gap appears |
|
||||
| `apps/client/lib/main.dart` | API-1 only if selected-edge state propagation gap appears |
|
||||
|
||||
## 최종 검증
|
||||
|
||||
```bash
|
||||
cd apps/client && flutter test test/widget_test.dart
|
||||
```
|
||||
|
||||
기대 결과: all tests passed. Flutter widget test cache output is not applicable; record actual command output.
|
||||
|
||||
모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
<!-- task=m-multi-edge-operations/02+01_history_agent_client plan=1 tag=REVIEW_API -->
|
||||
|
||||
# Plan - Runtime Panel Edge Switch Test Hardening
|
||||
|
||||
## 이 파일을 읽는 구현 에이전트에게
|
||||
|
||||
구현 완료는 active `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션까지 실제 구현 내용과 검증 출력으로 채운 뒤에만 성립한다. 검증 명령을 실행하고 실제 stdout/stderr를 review stub에 붙인다. active 파일은 그대로 두고 리뷰 준비 상태를 보고하며, `complete.log` 작성, `.log` rename, archive 이동은 code-review 스킬 전용이다. 구현 중 사용자 결정, 사용자 소유 외부 환경/secret/서비스 준비, 또는 계획 범위 충돌로 막히면 review stub의 `사용자 리뷰 요청`에 정확한 근거를 남기고 멈춘다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 증거 공백은 사용자 리뷰 요청이 아니다.
|
||||
|
||||
## 배경
|
||||
|
||||
첫 구현 루프는 Runtime panel의 Edge별 domain agent와 operations fixture를 분리하고 `flutter test test/widget_test.dart`를 통과했다. 그러나 리뷰에서 widget test가 빠른 fake response의 최종 상태만 확인해, 새 Edge 로딩 중 이전 Edge operation이 남는 회귀와 alpha-only command summary가 계속 보이는 회귀를 잡지 못하는 것이 확인됐다. 이 후속 작업은 구현 코드를 넓히지 않고 해당 검증 공백을 닫는다.
|
||||
|
||||
## 사용자 리뷰 요청 흐름
|
||||
|
||||
구현 중 blocker는 active review stub의 `사용자 리뷰 요청` 섹션에 기록한다. 해당 섹션은 `agent-ops/skills/common/_templates/implementation-user-review-request-section.md` 기준이며, 실제 `USER_REVIEW.md` 작성 여부 판단은 code-review가 소유한다.
|
||||
|
||||
## Roadmap Targets
|
||||
|
||||
- Milestone: `agent-roadmap/phase/control-plane-portal-ops/milestones/multi-edge-operations.md`
|
||||
- Task ids:
|
||||
- `history-agent-state`: Edge별 실행/명령 결과와 domain agent capability/status/command summary가 운영 화면에서 구분된다.
|
||||
- Completion mode: check-on-pass
|
||||
|
||||
## 리뷰 결과 요약
|
||||
|
||||
- Required: Edge Beta 전환 테스트가 빠른 fake response 최종 상태만 확인해서 fetch-start clear 회귀를 잡지 못한다.
|
||||
- Required: Edge 전환 후 alpha-only active command/operation text가 사라지는지 검증하지 않는다.
|
||||
|
||||
## 범위 결정 근거
|
||||
|
||||
- 포함: `apps/client/test/widget_test.dart`.
|
||||
- 포함 가능: stale-loading assertion을 통과시키기 위해 꼭 필요한 경우에만 `apps/client/lib/control_plane_status_widgets.dart`.
|
||||
- 제외: backend contract, proto, Control Plane/Edge Go 코드, roadmap 문서 수정.
|
||||
- 후속은 검증 공백 보강이 목적이므로 UI 구조 변경이나 새 feature를 추가하지 않는다.
|
||||
|
||||
## 빌드 등급
|
||||
|
||||
- build lane/grade: `local-G07`. 실패 원인은 명시적이고 deterministic한 widget test coverage 공백이며, bounded Flutter test로 검증 가능하다.
|
||||
|
||||
## 구현 체크리스트
|
||||
|
||||
- [ ] Fake repository에 Edge별 operations 응답을 지연/completer로 제어할 수 있는 테스트 helper를 추가한다.
|
||||
- [ ] Runtime panel widget test가 Edge Beta operations 응답 완료 전 alpha operation text가 사라지는지 검증한다.
|
||||
- [ ] Runtime panel widget test가 Edge 전환 전/후 alpha-only와 beta-only active command summary 및 operations text의 presence/absence를 모두 검증한다.
|
||||
- [ ] `cd apps/client && flutter test test/widget_test.dart -r expanded --plain-name 'Client App opens Operations & Domain Agents panel and verifies agents, operations history, and command triggering'`를 실행한다.
|
||||
- [ ] `cd apps/client && flutter test test/widget_test.dart`를 실행한다.
|
||||
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
|
||||
|
||||
### [REVIEW_API-1] Delayed Operations Fetch Assertion
|
||||
|
||||
문제: `apps/client/test/widget_test.dart`의 Runtime panel test는 Edge Beta 선택 뒤 `pumpAndSettle()`로 최종 상태만 본다. fake `fetchEdgeOperations('edge-b')`가 즉시 완료되므로 `_fetchOperations()`의 `_operations = null`이 제거되어도 테스트가 통과할 수 있다.
|
||||
|
||||
해결 방법: `FakeControlPlaneStatusRepository`에 edge-b operations future를 test에서 지연시킬 수 있는 helper를 둔다. 예를 들어 `Completer<EdgeOperationsResponseView>? delayedEdgeBOperations` 또는 edgeId별 delay map을 사용한다. 테스트에서는 Edge Beta 선택 직후 beta operations future를 완료하기 전에 한 프레임을 pump하고, alpha operation인 `oto-agent.sync`와 `Synced 5 models`가 `findsNothing`인지 확인한다. 그 뒤 beta future를 완료하고 beta operation이 표시되는지 확인한다.
|
||||
|
||||
수정 파일 및 체크리스트:
|
||||
|
||||
- [ ] `apps/client/test/widget_test.dart`: edge-b operations fetch를 지연 제어하는 fixture helper 추가.
|
||||
- [ ] `apps/client/test/widget_test.dart`: Edge Beta 선택 직후 alpha operation absence assertion 추가.
|
||||
|
||||
중간 검증:
|
||||
|
||||
```bash
|
||||
cd apps/client && flutter test test/widget_test.dart -r expanded --plain-name 'Client App opens Operations & Domain Agents panel and verifies agents, operations history, and command triggering'
|
||||
```
|
||||
|
||||
### [REVIEW_API-2] Edge-Specific Command Summary Negative Assertions
|
||||
|
||||
문제: 현재 test는 Edge Beta 전환 후 `BUSY`, `READY`, `build-deploy.deploy`가 보이는지만 확인한다. `Active Command: cmd-alpha-active`, `oto-agent.sync`, `Synced 5 models`가 남아 있어도 테스트가 명확히 실패하지 않는다.
|
||||
|
||||
해결 방법: Edge Alpha 상태에서 `Active Command: cmd-alpha-active`, `oto-agent.sync`, `Synced 5 models`가 표시되는지 먼저 확인한다. Edge Beta 전환 후에는 `Active Command: cmd-beta-active`, `build-deploy.deploy`, `Deploy queued for edge beta`가 표시되고, alpha-only texts인 `Active Command: cmd-alpha-active`, `oto-agent.sync`, `Synced 5 models`가 모두 `findsNothing`인지 확인한다.
|
||||
|
||||
수정 파일 및 체크리스트:
|
||||
|
||||
- [ ] `apps/client/test/widget_test.dart`: alpha active command summary presence assertion 추가.
|
||||
- [ ] `apps/client/test/widget_test.dart`: beta active command summary presence assertion 추가.
|
||||
- [ ] `apps/client/test/widget_test.dart`: alpha-only active command/operation absence assertion 추가.
|
||||
|
||||
중간 검증:
|
||||
|
||||
```bash
|
||||
cd apps/client && flutter test test/widget_test.dart -r expanded --plain-name 'Client App opens Operations & Domain Agents panel and verifies agents, operations history, and command triggering'
|
||||
```
|
||||
|
||||
## 수정 파일 요약
|
||||
|
||||
| 파일 | 항목 |
|
||||
|------|------|
|
||||
| `apps/client/test/widget_test.dart` | REVIEW_API-1, REVIEW_API-2 |
|
||||
| `apps/client/lib/control_plane_status_widgets.dart` | REVIEW_API-1 only if assertion exposes an implementation gap |
|
||||
|
||||
## 최종 검증
|
||||
|
||||
```bash
|
||||
cd apps/client && flutter test test/widget_test.dart
|
||||
```
|
||||
|
||||
기대 결과: all tests passed. 검증 결과에는 실제 stdout/stderr를 기록한다.
|
||||
|
|
@ -859,17 +859,18 @@ class _RuntimePanelState extends State<RuntimePanel> {
|
|||
setState(() {
|
||||
_isLoading = true;
|
||||
_error = null;
|
||||
_operations = null;
|
||||
});
|
||||
try {
|
||||
final ops = await widget.statusRepository.fetchEdgeOperations(edgeId);
|
||||
if (mounted) {
|
||||
if (mounted && widget.selectedEdgeId == edgeId) {
|
||||
setState(() {
|
||||
_operations = ops;
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
if (mounted && widget.selectedEdgeId == edgeId) {
|
||||
setState(() {
|
||||
_error = e.toString();
|
||||
_isLoading = false;
|
||||
|
|
|
|||
|
|
@ -71,6 +71,8 @@ class FakeControlPlaneStatusRepository implements ControlPlaneStatusRepository {
|
|||
String? lastCommandOperation;
|
||||
String? lastCommandTargetSelector;
|
||||
Map<String, String>? lastCommandParameters;
|
||||
String? lastCommandEdgeId;
|
||||
Completer<EdgeOperationsResponseView>? edgeBOperationsCompleter;
|
||||
|
||||
List<EdgeRegistryView> mockEdges = [
|
||||
EdgeRegistryView(
|
||||
|
|
@ -125,33 +127,58 @@ class FakeControlPlaneStatusRepository implements ControlPlaneStatusRepository {
|
|||
label: 'CPU-only',
|
||||
connected: false,
|
||||
config: NodeConfigSummaryView(
|
||||
adapters: [
|
||||
AdapterSummaryView(type: 'python-cli', enabled: true),
|
||||
],
|
||||
adapters: [AdapterSummaryView(type: 'python-cli', enabled: true)],
|
||||
concurrency: 2,
|
||||
),
|
||||
),
|
||||
],
|
||||
capabilities: [
|
||||
EdgeCapabilitySummaryView(kind: 'Serving', available: true, status: 'ready', summary: 'Active'),
|
||||
EdgeCapabilitySummaryView(kind: 'Automation', available: true, status: 'ready', summary: 'Active'),
|
||||
],
|
||||
domainAgents: [
|
||||
EdgeDomainAgentSummaryView(
|
||||
agentKind: 'oto-agent',
|
||||
EdgeCapabilitySummaryView(
|
||||
kind: 'Serving',
|
||||
available: true,
|
||||
lifecycleState: 'ready',
|
||||
activeCommandId: '',
|
||||
status: 'ready',
|
||||
summary: 'Active',
|
||||
),
|
||||
EdgeDomainAgentSummaryView(
|
||||
agentKind: 'build-deploy',
|
||||
EdgeCapabilitySummaryView(
|
||||
kind: 'Automation',
|
||||
available: true,
|
||||
lifecycleState: 'ready',
|
||||
activeCommandId: '',
|
||||
summary: 'Idle',
|
||||
status: 'ready',
|
||||
summary: 'Active',
|
||||
),
|
||||
],
|
||||
domainAgents: edgeId == 'edge-a'
|
||||
? [
|
||||
EdgeDomainAgentSummaryView(
|
||||
agentKind: 'oto-agent',
|
||||
available: true,
|
||||
lifecycleState: 'ready',
|
||||
activeCommandId: 'cmd-alpha-active',
|
||||
summary: 'Active',
|
||||
),
|
||||
EdgeDomainAgentSummaryView(
|
||||
agentKind: 'build-deploy',
|
||||
available: true,
|
||||
lifecycleState: 'ready',
|
||||
activeCommandId: '',
|
||||
summary: 'Idle',
|
||||
),
|
||||
]
|
||||
: [
|
||||
EdgeDomainAgentSummaryView(
|
||||
agentKind: 'oto-agent',
|
||||
available: true,
|
||||
lifecycleState: 'busy',
|
||||
activeCommandId: 'cmd-beta-active',
|
||||
summary: 'Processing',
|
||||
),
|
||||
EdgeDomainAgentSummaryView(
|
||||
agentKind: 'build-deploy',
|
||||
available: true,
|
||||
lifecycleState: 'ready',
|
||||
activeCommandId: '',
|
||||
summary: 'Idle',
|
||||
),
|
||||
],
|
||||
metadata: {'region': 'us-west'},
|
||||
error: '',
|
||||
);
|
||||
|
|
@ -191,53 +218,103 @@ class FakeControlPlaneStatusRepository implements ControlPlaneStatusRepository {
|
|||
Future<FleetStatusResponseView> fetchFleetStatus() async {
|
||||
return FleetStatusResponseView(
|
||||
generatedAt: DateTime.now(),
|
||||
edges: mockEdges.map((e) => FleetEdgeView(
|
||||
edgeId: e.edgeId,
|
||||
edgeName: e.edgeName,
|
||||
version: e.version,
|
||||
protocol: e.protocol,
|
||||
connected: e.connected,
|
||||
health: e.connected ? 'healthy' : 'unhealthy',
|
||||
lastSeen: e.lastSeen,
|
||||
nodeCount: 2,
|
||||
capabilities: e.capabilities.map((c) => EdgeCapabilitySummaryView(kind: c, available: true, status: 'ready', summary: '')).toList(),
|
||||
domainAgents: [
|
||||
EdgeDomainAgentSummaryView(
|
||||
agentKind: 'oto-agent',
|
||||
available: true,
|
||||
lifecycleState: 'ready',
|
||||
activeCommandId: '',
|
||||
summary: 'Active',
|
||||
),
|
||||
EdgeDomainAgentSummaryView(
|
||||
agentKind: 'build-deploy',
|
||||
available: true,
|
||||
lifecycleState: 'ready',
|
||||
activeCommandId: '',
|
||||
summary: 'Idle',
|
||||
),
|
||||
],
|
||||
error: '',
|
||||
)).toList(),
|
||||
edges: mockEdges
|
||||
.map(
|
||||
(e) => FleetEdgeView(
|
||||
edgeId: e.edgeId,
|
||||
edgeName: e.edgeName,
|
||||
version: e.version,
|
||||
protocol: e.protocol,
|
||||
connected: e.connected,
|
||||
health: e.connected ? 'healthy' : 'unhealthy',
|
||||
lastSeen: e.lastSeen,
|
||||
nodeCount: 2,
|
||||
capabilities: e.capabilities
|
||||
.map(
|
||||
(c) => EdgeCapabilitySummaryView(
|
||||
kind: c,
|
||||
available: true,
|
||||
status: 'ready',
|
||||
summary: '',
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
domainAgents: e.edgeId == 'edge-a'
|
||||
? [
|
||||
EdgeDomainAgentSummaryView(
|
||||
agentKind: 'oto-agent',
|
||||
available: true,
|
||||
lifecycleState: 'ready',
|
||||
activeCommandId: 'cmd-alpha-active',
|
||||
summary: 'Active',
|
||||
),
|
||||
EdgeDomainAgentSummaryView(
|
||||
agentKind: 'build-deploy',
|
||||
available: true,
|
||||
lifecycleState: 'ready',
|
||||
activeCommandId: '',
|
||||
summary: 'Idle',
|
||||
),
|
||||
]
|
||||
: [
|
||||
EdgeDomainAgentSummaryView(
|
||||
agentKind: 'oto-agent',
|
||||
available: true,
|
||||
lifecycleState: 'busy',
|
||||
activeCommandId: 'cmd-beta-active',
|
||||
summary: 'Processing',
|
||||
),
|
||||
EdgeDomainAgentSummaryView(
|
||||
agentKind: 'build-deploy',
|
||||
available: true,
|
||||
lifecycleState: 'ready',
|
||||
activeCommandId: '',
|
||||
summary: 'Idle',
|
||||
),
|
||||
],
|
||||
error: '',
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<EdgeOperationsResponseView> fetchEdgeOperations(String edgeId) async {
|
||||
return EdgeOperationsResponseView(
|
||||
edgeId: edgeId,
|
||||
operations: [
|
||||
EdgeCommandRecordView(
|
||||
edgeId: edgeId,
|
||||
commandId: 'cmd-1',
|
||||
operation: 'oto-agent.sync',
|
||||
targetSelector: '',
|
||||
status: 'success',
|
||||
summary: 'Synced 5 models',
|
||||
error: '',
|
||||
),
|
||||
],
|
||||
);
|
||||
if (edgeId == 'edge-a') {
|
||||
return EdgeOperationsResponseView(
|
||||
edgeId: edgeId,
|
||||
operations: [
|
||||
EdgeCommandRecordView(
|
||||
edgeId: edgeId,
|
||||
commandId: 'cmd-alpha-active',
|
||||
operation: 'oto-agent.sync',
|
||||
targetSelector: '',
|
||||
status: 'success',
|
||||
summary: 'Synced 5 models',
|
||||
error: '',
|
||||
),
|
||||
],
|
||||
);
|
||||
} else {
|
||||
if (edgeBOperationsCompleter != null) {
|
||||
return edgeBOperationsCompleter!.future;
|
||||
}
|
||||
return EdgeOperationsResponseView(
|
||||
edgeId: edgeId,
|
||||
operations: [
|
||||
EdgeCommandRecordView(
|
||||
edgeId: edgeId,
|
||||
commandId: 'cmd-beta-active',
|
||||
operation: 'build-deploy.deploy',
|
||||
targetSelector: '',
|
||||
status: 'success',
|
||||
summary: 'Deploy queued for edge beta',
|
||||
error: '',
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -250,8 +327,10 @@ class FakeControlPlaneStatusRepository implements ControlPlaneStatusRepository {
|
|||
lastCommandOperation = operation;
|
||||
lastCommandTargetSelector = targetSelector;
|
||||
lastCommandParameters = parameters;
|
||||
lastCommandEdgeId = edgeId;
|
||||
|
||||
if (operation == 'agent.status' && (targetSelector == null || targetSelector.isEmpty)) {
|
||||
if (operation == 'agent.status' &&
|
||||
(targetSelector == null || targetSelector.isEmpty)) {
|
||||
return EdgeCommandResponseView(
|
||||
requestId: 'req-111',
|
||||
commandId: 'cmd-failed',
|
||||
|
|
@ -263,13 +342,17 @@ class FakeControlPlaneStatusRepository implements ControlPlaneStatusRepository {
|
|||
}
|
||||
if (operation == 'agent.command') {
|
||||
final cmd = parameters?['command'];
|
||||
if (targetSelector == null || targetSelector.isEmpty || cmd == null || cmd.isEmpty) {
|
||||
if (targetSelector == null ||
|
||||
targetSelector.isEmpty ||
|
||||
cmd == null ||
|
||||
cmd.isEmpty) {
|
||||
return EdgeCommandResponseView(
|
||||
requestId: 'req-111',
|
||||
commandId: 'cmd-failed',
|
||||
edgeId: edgeId,
|
||||
status: 'error',
|
||||
summary: 'Failed: agent.command requires target_selector and parameters.command',
|
||||
summary:
|
||||
'Failed: agent.command requires target_selector and parameters.command',
|
||||
error: 'target_selector and parameters.command are required',
|
||||
);
|
||||
}
|
||||
|
|
@ -293,10 +376,9 @@ void main() {
|
|||
final fakeClient = FakeClientWireClient(shouldSuccess: true);
|
||||
final fakeStatusRepo = FakeControlPlaneStatusRepository();
|
||||
|
||||
await tester.pumpWidget(IopClientApp(
|
||||
testClient: fakeClient,
|
||||
statusRepository: fakeStatusRepo,
|
||||
));
|
||||
await tester.pumpWidget(
|
||||
IopClientApp(testClient: fakeClient, statusRepository: fakeStatusRepo),
|
||||
);
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
|
||||
|
|
@ -324,10 +406,9 @@ void main() {
|
|||
);
|
||||
final fakeStatusRepo = FakeControlPlaneStatusRepository();
|
||||
|
||||
await tester.pumpWidget(IopClientApp(
|
||||
testClient: fakeClient,
|
||||
statusRepository: fakeStatusRepo,
|
||||
));
|
||||
await tester.pumpWidget(
|
||||
IopClientApp(testClient: fakeClient, statusRepository: fakeStatusRepo),
|
||||
);
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
|
||||
|
|
@ -340,10 +421,9 @@ void main() {
|
|||
final fakeClient = FakeClientWireClient(shouldSuccess: true);
|
||||
final fakeStatusRepo = FakeControlPlaneStatusRepository();
|
||||
|
||||
await tester.pumpWidget(IopClientApp(
|
||||
testClient: fakeClient,
|
||||
statusRepository: fakeStatusRepo,
|
||||
));
|
||||
await tester.pumpWidget(
|
||||
IopClientApp(testClient: fakeClient, statusRepository: fakeStatusRepo),
|
||||
);
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
|
||||
|
|
@ -362,10 +442,9 @@ void main() {
|
|||
final fakeClient = FakeClientWireClient(shouldSuccess: true);
|
||||
final fakeStatusRepo = FakeControlPlaneStatusRepository();
|
||||
|
||||
await tester.pumpWidget(IopClientApp(
|
||||
testClient: fakeClient,
|
||||
statusRepository: fakeStatusRepo,
|
||||
));
|
||||
await tester.pumpWidget(
|
||||
IopClientApp(testClient: fakeClient, statusRepository: fakeStatusRepo),
|
||||
);
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
|
||||
|
|
@ -382,63 +461,78 @@ void main() {
|
|||
expect(find.text('1.2.0'), findsOneWidget);
|
||||
expect(find.text('Serving'), findsOneWidget);
|
||||
expect(find.text('Automation'), findsOneWidget);
|
||||
|
||||
// Verify selecting another Edge switches the detail pane instead of
|
||||
// reusing the first Edge's status view.
|
||||
await tester.tap(find.text('Edge Beta'));
|
||||
await tester.pump();
|
||||
|
||||
expect(find.text('Edge Alpha'), findsOneWidget);
|
||||
expect(find.text('Edge Beta'), findsNWidgets(2));
|
||||
expect(find.text('edge-b'), findsOneWidget);
|
||||
expect(find.text('1.2.1'), findsOneWidget);
|
||||
expect(find.text('DISCONNECTED'), findsOneWidget);
|
||||
expect(find.text('unhealthy'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('Client App opens Nodes panel and displays active Nodes and configurations', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
final fakeClient = FakeClientWireClient(shouldSuccess: true);
|
||||
final fakeStatusRepo = FakeControlPlaneStatusRepository();
|
||||
testWidgets(
|
||||
'Client App opens Nodes panel and displays active Nodes and configurations',
|
||||
(WidgetTester tester) async {
|
||||
final fakeClient = FakeClientWireClient(shouldSuccess: true);
|
||||
final fakeStatusRepo = FakeControlPlaneStatusRepository();
|
||||
|
||||
await tester.pumpWidget(IopClientApp(
|
||||
testClient: fakeClient,
|
||||
statusRepository: fakeStatusRepo,
|
||||
));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
await tester.pumpWidget(
|
||||
IopClientApp(testClient: fakeClient, statusRepository: fakeStatusRepo),
|
||||
);
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
|
||||
await tester.tap(find.byTooltip('Nodes'));
|
||||
await tester.pump();
|
||||
await tester.tap(find.byTooltip('Nodes'));
|
||||
await tester.pump();
|
||||
|
||||
// Verify Nodes view header
|
||||
expect(find.text('Nodes View'), findsOneWidget);
|
||||
// Verify Nodes view header
|
||||
expect(find.text('Nodes View'), findsOneWidget);
|
||||
|
||||
// Verify nodes listed
|
||||
expect(find.text('Node One'), findsOneWidget);
|
||||
expect(find.text('Node Two'), findsOneWidget);
|
||||
// Verify nodes listed
|
||||
expect(find.text('Node One'), findsOneWidget);
|
||||
expect(find.text('Node Two'), findsOneWidget);
|
||||
|
||||
// Verify node properties and adapters
|
||||
expect(find.text('Label: GPU-T4'), findsOneWidget);
|
||||
expect(find.text('Concurrency Limit: 4'), findsOneWidget);
|
||||
expect(find.text('ollama'), findsOneWidget);
|
||||
expect(find.text('custom'), findsOneWidget);
|
||||
});
|
||||
// Verify node properties and adapters
|
||||
expect(find.text('Label: GPU-T4'), findsOneWidget);
|
||||
expect(find.text('Concurrency Limit: 4'), findsOneWidget);
|
||||
expect(find.text('ollama'), findsOneWidget);
|
||||
expect(find.text('custom'), findsOneWidget);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets('Client App opens Execution/Logs panel and displays lifecycle events', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
final fakeClient = FakeClientWireClient(shouldSuccess: true);
|
||||
final fakeStatusRepo = FakeControlPlaneStatusRepository();
|
||||
testWidgets(
|
||||
'Client App opens Execution/Logs panel and displays lifecycle events',
|
||||
(WidgetTester tester) async {
|
||||
final fakeClient = FakeClientWireClient(shouldSuccess: true);
|
||||
final fakeStatusRepo = FakeControlPlaneStatusRepository();
|
||||
|
||||
await tester.pumpWidget(IopClientApp(
|
||||
testClient: fakeClient,
|
||||
statusRepository: fakeStatusRepo,
|
||||
));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
await tester.pumpWidget(
|
||||
IopClientApp(testClient: fakeClient, statusRepository: fakeStatusRepo),
|
||||
);
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
|
||||
await tester.tap(find.byTooltip('Execution & Logs'));
|
||||
await tester.pump();
|
||||
await tester.tap(find.byTooltip('Execution & Logs'));
|
||||
await tester.pump();
|
||||
|
||||
// Verify logs header
|
||||
expect(find.text('Lifecycle Events & Logs'), findsOneWidget);
|
||||
// Verify logs header
|
||||
expect(find.text('Lifecycle Events & Logs'), findsOneWidget);
|
||||
|
||||
// Verify captured events
|
||||
expect(find.text('ONLINE'), findsOneWidget);
|
||||
expect(find.text('WARN'), findsOneWidget);
|
||||
expect(find.text('Node connected and registered successfully'), findsOneWidget);
|
||||
expect(find.text('Execution concurrency limit reached'), findsOneWidget);
|
||||
});
|
||||
// Verify captured events
|
||||
expect(find.text('ONLINE'), findsOneWidget);
|
||||
expect(find.text('WARN'), findsOneWidget);
|
||||
expect(
|
||||
find.text('Node connected and registered successfully'),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(find.text('Execution concurrency limit reached'), findsOneWidget);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets('Client App refresh behavior when selected edge disappears', (
|
||||
WidgetTester tester,
|
||||
|
|
@ -446,10 +540,9 @@ void main() {
|
|||
final fakeClient = FakeClientWireClient(shouldSuccess: true);
|
||||
final fakeStatusRepo = FakeControlPlaneStatusRepository();
|
||||
|
||||
await tester.pumpWidget(IopClientApp(
|
||||
testClient: fakeClient,
|
||||
statusRepository: fakeStatusRepo,
|
||||
));
|
||||
await tester.pumpWidget(
|
||||
IopClientApp(testClient: fakeClient, statusRepository: fakeStatusRepo),
|
||||
);
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
|
||||
|
|
@ -475,16 +568,16 @@ void main() {
|
|||
await tester.tap(find.byTooltip('Overview'));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(seconds: 1));
|
||||
|
||||
|
||||
// Verify route has navigated back to Overview successfully
|
||||
expect(find.text('Operations Overview'), findsOneWidget);
|
||||
|
||||
|
||||
// Drag the ListView upward to scroll down and mount the bottom refresh button
|
||||
final listViewFinder = find.byType(ListView);
|
||||
await tester.drag(listViewFinder.first, const Offset(0.0, -400.0));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 200));
|
||||
|
||||
|
||||
// Tap the refresh button safely by its text label
|
||||
await tester.tap(find.text('Refresh Connection'));
|
||||
await tester.pump();
|
||||
|
|
@ -495,7 +588,10 @@ void main() {
|
|||
await tester.pump();
|
||||
|
||||
expect(find.text('Edge Alpha'), findsNothing);
|
||||
expect(find.text('Edge Gamma'), findsNWidgets(2)); // list & details (auto-selected)
|
||||
expect(
|
||||
find.text('Edge Gamma'),
|
||||
findsNWidgets(2),
|
||||
); // list & details (auto-selected)
|
||||
|
||||
// 4. Switch to Nodes panel, verify dropdown Value works without assert crash
|
||||
await tester.tap(find.byTooltip('Nodes'));
|
||||
|
|
@ -511,193 +607,268 @@ void main() {
|
|||
expect(find.text('Edge Gamma'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('Client App opens Operations & Domain Agents panel and verifies agents, operations history, and command triggering', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
final fakeClient = FakeClientWireClient(shouldSuccess: true);
|
||||
final fakeStatusRepo = FakeControlPlaneStatusRepository();
|
||||
testWidgets(
|
||||
'Client App opens Operations & Domain Agents panel and verifies agents, operations history, and command triggering',
|
||||
(WidgetTester tester) async {
|
||||
final fakeClient = FakeClientWireClient(shouldSuccess: true);
|
||||
final fakeStatusRepo = FakeControlPlaneStatusRepository();
|
||||
|
||||
await tester.pumpWidget(IopClientApp(
|
||||
testClient: fakeClient,
|
||||
statusRepository: fakeStatusRepo,
|
||||
));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
await tester.pumpWidget(
|
||||
IopClientApp(testClient: fakeClient, statusRepository: fakeStatusRepo),
|
||||
);
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
|
||||
await tester.tap(find.byTooltip('Runtime'));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
await tester.tap(find.byTooltip('Runtime'));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
|
||||
// Verify panel header and sections
|
||||
expect(find.text('Operations & Domain Agents'), findsOneWidget);
|
||||
expect(find.text('Domain Agents'), findsOneWidget);
|
||||
expect(find.text('Operations Execution History'), findsOneWidget);
|
||||
expect(find.text('System Gated Operations'), findsOneWidget);
|
||||
// Verify panel header and sections
|
||||
expect(find.text('Operations & Domain Agents'), findsOneWidget);
|
||||
expect(find.text('Domain Agents'), findsOneWidget);
|
||||
expect(find.text('Operations Execution History'), findsOneWidget);
|
||||
expect(find.text('System Gated Operations'), findsOneWidget);
|
||||
|
||||
// Verify domain agents summary listed
|
||||
expect(find.text('OTO-AGENT'), findsOneWidget);
|
||||
expect(find.text('BUILD-DEPLOY'), findsOneWidget);
|
||||
expect(find.text('READY'), findsNWidgets(2)); // oto-agent and build-deploy are both ready
|
||||
// Verify domain agents summary listed
|
||||
expect(find.text('OTO-AGENT'), findsOneWidget);
|
||||
expect(find.text('BUILD-DEPLOY'), findsOneWidget);
|
||||
expect(
|
||||
find.text('READY'),
|
||||
findsNWidgets(2),
|
||||
); // oto-agent and build-deploy are both ready
|
||||
|
||||
// Verify operations history item
|
||||
expect(find.text('oto-agent.sync'), findsOneWidget);
|
||||
expect(find.text('SUCCESS'), findsOneWidget);
|
||||
expect(find.textContaining('Synced 5 models'), findsOneWidget);
|
||||
// [REVIEW_API-2] Verify domain agents active command summary for Edge Alpha (presence)
|
||||
expect(find.text('Active Command: cmd-alpha-active'), findsOneWidget);
|
||||
|
||||
// Verify Trigger Sync button is not present
|
||||
expect(find.text('Trigger Sync'), findsNothing);
|
||||
// [REVIEW_API-2] Verify beta active command summary is NOT showing for Edge Alpha
|
||||
expect(find.text('Active Command: cmd-beta-active'), findsNothing);
|
||||
expect(find.text('build-deploy.deploy'), findsNothing);
|
||||
expect(find.textContaining('Deploy queued for edge beta'), findsNothing);
|
||||
|
||||
// Verify System Gated buttons are present
|
||||
expect(find.text('Health Check'), findsOneWidget);
|
||||
expect(find.text('Agent Status'), findsOneWidget);
|
||||
expect(find.text('Agent Command'), findsOneWidget);
|
||||
// Verify operations history item
|
||||
expect(find.text('oto-agent.sync'), findsOneWidget);
|
||||
expect(find.text('SUCCESS'), findsOneWidget);
|
||||
expect(find.textContaining('Synced 5 models'), findsOneWidget);
|
||||
|
||||
// Verify Health Check button works and shows success banner
|
||||
await tester.tap(find.text('Health Check'));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
// Verify Trigger Sync button is not present
|
||||
expect(find.text('Trigger Sync'), findsNothing);
|
||||
|
||||
// Check for success status banner message
|
||||
expect(find.textContaining('Success: Command accepted'), findsOneWidget);
|
||||
});
|
||||
// Verify System Gated buttons are present
|
||||
expect(find.text('Health Check'), findsOneWidget);
|
||||
expect(find.text('Agent Status'), findsOneWidget);
|
||||
expect(find.text('Agent Command'), findsOneWidget);
|
||||
|
||||
testWidgets('Client App handles unsupported or error command responses and shows error banner', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
final fakeClient = FakeClientWireClient(shouldSuccess: true);
|
||||
final fakeStatusRepo = FakeControlPlaneStatusRepository();
|
||||
// Verify Health Check button works and shows success banner
|
||||
await tester.tap(find.text('Health Check'));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
|
||||
// Configure repository to simulate a failed/unsupported command response
|
||||
fakeStatusRepo.nextCommandStatus = 'unsupported';
|
||||
fakeStatusRepo.nextCommandError = 'Operation not supported by Edge';
|
||||
fakeStatusRepo.nextCommandSummary = 'Error summary';
|
||||
// Check for success status banner message
|
||||
expect(find.textContaining('Success: Command accepted'), findsOneWidget);
|
||||
|
||||
await tester.pumpWidget(IopClientApp(
|
||||
testClient: fakeClient,
|
||||
statusRepository: fakeStatusRepo,
|
||||
));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
// Verify command target edge ID was edge-a
|
||||
expect(fakeStatusRepo.lastCommandEdgeId, equals('edge-a'));
|
||||
|
||||
await tester.tap(find.byTooltip('Runtime'));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
// Setup delayed fetch for edge-b
|
||||
fakeStatusRepo.edgeBOperationsCompleter = Completer<EdgeOperationsResponseView>();
|
||||
|
||||
// Verify Agent Status button works, opens dialog, and then shows error banner
|
||||
await tester.tap(find.text('Agent Status'));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
// Switch to Edge Beta via Dropdown
|
||||
await tester.tap(find.byType(DropdownButton<String>));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Edge Beta').last);
|
||||
// Pump one frame to trigger edge selection but wait before operations fetch resolves
|
||||
await tester.pump();
|
||||
|
||||
expect(find.text('Get Agent Status'), findsOneWidget);
|
||||
await tester.enterText(find.byType(TextField), 'test-node');
|
||||
await tester.tap(find.text('Send'));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
// [REVIEW_API-1] Verify that before the operations fetch completes:
|
||||
// 1. The domain agents section has updated immediately (so cmd-alpha-active is gone, cmd-beta-active is present)
|
||||
expect(find.text('Active Command: cmd-alpha-active'), findsNothing);
|
||||
expect(find.text('Active Command: cmd-beta-active'), findsOneWidget);
|
||||
// 2. The operations history of the previous edge (edge-a) is cleared immediately upon fetch start.
|
||||
expect(find.text('oto-agent.sync'), findsNothing);
|
||||
expect(find.textContaining('Synced 5 models'), findsNothing);
|
||||
// 3. The loading indicator is shown since _operations is null and _isLoading is true
|
||||
expect(find.byType(CircularProgressIndicator), findsOneWidget);
|
||||
|
||||
// Check for failure status banner message
|
||||
expect(find.textContaining('Failed: unsupported - Operation not supported by Edge'), findsOneWidget);
|
||||
});
|
||||
// Now complete the operations fetch for edge-b
|
||||
fakeStatusRepo.edgeBOperationsCompleter!.complete(EdgeOperationsResponseView(
|
||||
edgeId: 'edge-b',
|
||||
operations: [
|
||||
EdgeCommandRecordView(
|
||||
edgeId: 'edge-b',
|
||||
commandId: 'cmd-beta-active',
|
||||
operation: 'build-deploy.deploy',
|
||||
targetSelector: '',
|
||||
status: 'success',
|
||||
summary: 'Deploy queued for edge beta',
|
||||
error: '',
|
||||
),
|
||||
],
|
||||
));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
testWidgets('Client App gates agent.status and agent.command without required inputs', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
final fakeClient = FakeClientWireClient(shouldSuccess: true);
|
||||
final fakeStatusRepo = FakeControlPlaneStatusRepository();
|
||||
// Verify domain agents for Edge Beta (oto-agent shows BUSY, build-deploy shows READY)
|
||||
expect(find.text('BUSY'), findsOneWidget);
|
||||
expect(find.text('READY'), findsOneWidget);
|
||||
|
||||
await tester.pumpWidget(IopClientApp(
|
||||
testClient: fakeClient,
|
||||
statusRepository: fakeStatusRepo,
|
||||
));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
// [REVIEW_API-2] Verify presence of beta active command and operations history
|
||||
expect(find.text('Active Command: cmd-beta-active'), findsOneWidget);
|
||||
expect(find.text('build-deploy.deploy'), findsOneWidget);
|
||||
expect(find.textContaining('Deploy queued for edge beta'), findsOneWidget);
|
||||
|
||||
await tester.tap(find.byTooltip('Runtime'));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
// [REVIEW_API-2] Verify absence of alpha-only active command and operations history
|
||||
expect(find.text('Active Command: cmd-alpha-active'), findsNothing);
|
||||
expect(find.text('oto-agent.sync'), findsNothing);
|
||||
expect(find.textContaining('Synced 5 models'), findsNothing);
|
||||
|
||||
// 1. Verify agent.status gating
|
||||
await tester.tap(find.text('Agent Status'));
|
||||
await tester.pump();
|
||||
expect(find.text('Get Agent Status'), findsOneWidget);
|
||||
// Verify command triggering on Edge Beta sends command to edge-b
|
||||
await tester.tap(find.text('Health Check'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(fakeStatusRepo.lastCommandEdgeId, equals('edge-b'));
|
||||
},
|
||||
);
|
||||
|
||||
// Try to send with empty target selector
|
||||
await tester.enterText(find.byType(TextField), '');
|
||||
await tester.tap(find.text('Send'));
|
||||
await tester.pump();
|
||||
testWidgets(
|
||||
'Client App handles unsupported or error command responses and shows error banner',
|
||||
(WidgetTester tester) async {
|
||||
final fakeClient = FakeClientWireClient(shouldSuccess: true);
|
||||
final fakeStatusRepo = FakeControlPlaneStatusRepository();
|
||||
|
||||
// The dialog should still be open (Send was gated/no-op)
|
||||
expect(find.text('Get Agent Status'), findsOneWidget);
|
||||
expect(fakeStatusRepo.lastCommandOperation, isNull);
|
||||
// Configure repository to simulate a failed/unsupported command response
|
||||
fakeStatusRepo.nextCommandStatus = 'unsupported';
|
||||
fakeStatusRepo.nextCommandError = 'Operation not supported by Edge';
|
||||
fakeStatusRepo.nextCommandSummary = 'Error summary';
|
||||
|
||||
// Cancel dialog
|
||||
await tester.tap(find.text('Cancel'));
|
||||
await tester.pump();
|
||||
await tester.pumpWidget(
|
||||
IopClientApp(testClient: fakeClient, statusRepository: fakeStatusRepo),
|
||||
);
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
|
||||
// 2. Verify agent.command gating
|
||||
await tester.tap(find.text('Agent Command'));
|
||||
await tester.pump();
|
||||
expect(find.text('Send Agent Command'), findsOneWidget);
|
||||
await tester.tap(find.byTooltip('Runtime'));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
|
||||
// Try to send with empty command name and empty selector
|
||||
await tester.tap(find.text('Send'));
|
||||
await tester.pump();
|
||||
expect(find.text('Send Agent Command'), findsOneWidget);
|
||||
expect(fakeStatusRepo.lastCommandOperation, isNull);
|
||||
// Verify Agent Status button works, opens dialog, and then shows error banner
|
||||
await tester.tap(find.text('Agent Status'));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
|
||||
// Enter only selector
|
||||
await tester.enterText(find.byType(TextField).at(0), 'node-1');
|
||||
await tester.tap(find.text('Send'));
|
||||
await tester.pump();
|
||||
expect(find.text('Send Agent Command'), findsOneWidget);
|
||||
expect(fakeStatusRepo.lastCommandOperation, isNull);
|
||||
expect(find.text('Get Agent Status'), findsOneWidget);
|
||||
await tester.enterText(find.byType(TextField), 'test-node');
|
||||
await tester.tap(find.text('Send'));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
|
||||
// Enter only command name (clear selector)
|
||||
await tester.enterText(find.byType(TextField).at(0), '');
|
||||
await tester.enterText(find.byType(TextField).at(1), 'sync');
|
||||
await tester.tap(find.text('Send'));
|
||||
await tester.pump();
|
||||
expect(find.text('Send Agent Command'), findsOneWidget);
|
||||
expect(fakeStatusRepo.lastCommandOperation, isNull);
|
||||
// Check for failure status banner message
|
||||
expect(
|
||||
find.textContaining(
|
||||
'Failed: unsupported - Operation not supported by Edge',
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
// Cancel
|
||||
await tester.tap(find.text('Cancel'));
|
||||
await tester.pump();
|
||||
});
|
||||
testWidgets(
|
||||
'Client App gates agent.status and agent.command without required inputs',
|
||||
(WidgetTester tester) async {
|
||||
final fakeClient = FakeClientWireClient(shouldSuccess: true);
|
||||
final fakeStatusRepo = FakeControlPlaneStatusRepository();
|
||||
|
||||
testWidgets('Client App mobile screen layout verification for layout and overflow', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
// Set a small mobile screen size
|
||||
tester.view.physicalSize = const Size(360, 640);
|
||||
tester.view.devicePixelRatio = 1.0;
|
||||
await tester.pumpWidget(
|
||||
IopClientApp(testClient: fakeClient, statusRepository: fakeStatusRepo),
|
||||
);
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
|
||||
final fakeClient = FakeClientWireClient(shouldSuccess: true);
|
||||
final fakeStatusRepo = FakeControlPlaneStatusRepository();
|
||||
await tester.tap(find.byTooltip('Runtime'));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
|
||||
await tester.pumpWidget(IopClientApp(
|
||||
testClient: fakeClient,
|
||||
statusRepository: fakeStatusRepo,
|
||||
));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
// 1. Verify agent.status gating
|
||||
await tester.tap(find.text('Agent Status'));
|
||||
await tester.pump();
|
||||
expect(find.text('Get Agent Status'), findsOneWidget);
|
||||
|
||||
// Overview
|
||||
expect(find.text('Operations Overview'), findsOneWidget);
|
||||
// Try to send with empty target selector
|
||||
await tester.enterText(find.byType(TextField), '');
|
||||
await tester.tap(find.text('Send'));
|
||||
await tester.pump();
|
||||
|
||||
// Switch to Edges panel
|
||||
await tester.tap(find.byTooltip('Edges'));
|
||||
await tester.pump();
|
||||
expect(find.text('Edge Alpha'), findsNWidgets(2));
|
||||
// The dialog should still be open (Send was gated/no-op)
|
||||
expect(find.text('Get Agent Status'), findsOneWidget);
|
||||
expect(fakeStatusRepo.lastCommandOperation, isNull);
|
||||
|
||||
// Switch to Runtime panel
|
||||
await tester.tap(find.byTooltip('Runtime'));
|
||||
await tester.pump();
|
||||
expect(find.text('Operations & Domain Agents'), findsOneWidget);
|
||||
// Cancel dialog
|
||||
await tester.tap(find.text('Cancel'));
|
||||
await tester.pump();
|
||||
|
||||
// Reset view size after test
|
||||
addTearDown(() {
|
||||
tester.view.resetPhysicalSize();
|
||||
tester.view.resetDevicePixelRatio();
|
||||
});
|
||||
});
|
||||
// 2. Verify agent.command gating
|
||||
await tester.tap(find.text('Agent Command'));
|
||||
await tester.pump();
|
||||
expect(find.text('Send Agent Command'), findsOneWidget);
|
||||
|
||||
// Try to send with empty command name and empty selector
|
||||
await tester.tap(find.text('Send'));
|
||||
await tester.pump();
|
||||
expect(find.text('Send Agent Command'), findsOneWidget);
|
||||
expect(fakeStatusRepo.lastCommandOperation, isNull);
|
||||
|
||||
// Enter only selector
|
||||
await tester.enterText(find.byType(TextField).at(0), 'node-1');
|
||||
await tester.tap(find.text('Send'));
|
||||
await tester.pump();
|
||||
expect(find.text('Send Agent Command'), findsOneWidget);
|
||||
expect(fakeStatusRepo.lastCommandOperation, isNull);
|
||||
|
||||
// Enter only command name (clear selector)
|
||||
await tester.enterText(find.byType(TextField).at(0), '');
|
||||
await tester.enterText(find.byType(TextField).at(1), 'sync');
|
||||
await tester.tap(find.text('Send'));
|
||||
await tester.pump();
|
||||
expect(find.text('Send Agent Command'), findsOneWidget);
|
||||
expect(fakeStatusRepo.lastCommandOperation, isNull);
|
||||
|
||||
// Cancel
|
||||
await tester.tap(find.text('Cancel'));
|
||||
await tester.pump();
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'Client App mobile screen layout verification for layout and overflow',
|
||||
(WidgetTester tester) async {
|
||||
// Set a small mobile screen size
|
||||
tester.view.physicalSize = const Size(360, 640);
|
||||
tester.view.devicePixelRatio = 1.0;
|
||||
|
||||
final fakeClient = FakeClientWireClient(shouldSuccess: true);
|
||||
final fakeStatusRepo = FakeControlPlaneStatusRepository();
|
||||
|
||||
await tester.pumpWidget(
|
||||
IopClientApp(testClient: fakeClient, statusRepository: fakeStatusRepo),
|
||||
);
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
|
||||
// Overview
|
||||
expect(find.text('Operations Overview'), findsOneWidget);
|
||||
|
||||
// Switch to Edges panel
|
||||
await tester.tap(find.byTooltip('Edges'));
|
||||
await tester.pump();
|
||||
expect(find.text('Edge Alpha'), findsNWidgets(2));
|
||||
|
||||
// Switch to Runtime panel
|
||||
await tester.tap(find.byTooltip('Runtime'));
|
||||
await tester.pump();
|
||||
expect(find.text('Operations & Domain Agents'), findsOneWidget);
|
||||
|
||||
// Reset view size after test
|
||||
addTearDown(() {
|
||||
tester.view.resetPhysicalSize();
|
||||
tester.view.resetDevicePixelRatio();
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -644,31 +644,61 @@ func TestEdgeOperationsHTTPHandlerListsAudit(t *testing.T) {
|
|||
registry := wire.NewEdgeRegistry()
|
||||
at := time.Unix(1780142200, 0).UTC()
|
||||
registry.MarkConnected(&iop.EdgeHelloRequest{EdgeId: "edge-a", EdgeName: "Edge A"}, at)
|
||||
registry.RecordCommandRequest("edge-a", &iop.EdgeCommandRequest{CommandId: "cmd-1", Operation: "restart-node", TargetSelector: "node-1"}, at)
|
||||
registry.RecordCommandEvent("edge-a", &iop.EdgeCommandEvent{CommandId: "cmd-1", Phase: "running", Summary: "executing"}, at.Add(time.Second))
|
||||
registry.RecordCommandResult("edge-a", &iop.EdgeCommandResponse{CommandId: "cmd-1", Status: "ok", Summary: "done"}, at.Add(2*time.Second))
|
||||
registry.MarkConnected(&iop.EdgeHelloRequest{EdgeId: "edge-b", EdgeName: "Edge B"}, at)
|
||||
|
||||
// Edge A command records
|
||||
registry.RecordCommandRequest("edge-a", &iop.EdgeCommandRequest{CommandId: "cmd-a-1", Operation: "restart-node", TargetSelector: "node-1"}, at)
|
||||
registry.RecordCommandEvent("edge-a", &iop.EdgeCommandEvent{CommandId: "cmd-a-1", Phase: "running", Summary: "executing"}, at.Add(time.Second))
|
||||
registry.RecordCommandResult("edge-a", &iop.EdgeCommandResponse{CommandId: "cmd-a-1", Status: "ok", Summary: "done"}, at.Add(2*time.Second))
|
||||
|
||||
// Edge B command records
|
||||
registry.RecordCommandRequest("edge-b", &iop.EdgeCommandRequest{CommandId: "cmd-b-1", Operation: "drain-node", TargetSelector: "node-2"}, at)
|
||||
registry.RecordCommandEvent("edge-b", &iop.EdgeCommandEvent{CommandId: "cmd-b-1", Phase: "draining", Summary: "evicting pods"}, at.Add(time.Second))
|
||||
registry.RecordCommandResult("edge-b", &iop.EdgeCommandResponse{CommandId: "cmd-b-1", Status: "failed", Summary: "timeout"}, at.Add(2*time.Second))
|
||||
|
||||
mux := http.NewServeMux()
|
||||
registerEdgeRegistryHandlers(mux, registry, nil, nil)
|
||||
|
||||
resp := httptest.NewRecorder()
|
||||
mux.ServeHTTP(resp, httptest.NewRequest(http.MethodGet, "/edges/edge-a/operations", nil))
|
||||
if resp.Code != http.StatusOK {
|
||||
t.Fatalf("GET operations status=%d body=%s", resp.Code, resp.Body.String())
|
||||
// Request Edge A operations
|
||||
respA := httptest.NewRecorder()
|
||||
mux.ServeHTTP(respA, httptest.NewRequest(http.MethodGet, "/edges/edge-a/operations", nil))
|
||||
if respA.Code != http.StatusOK {
|
||||
t.Fatalf("GET /edges/edge-a/operations status=%d body=%s", respA.Code, respA.Body.String())
|
||||
}
|
||||
var got edgeOperationsResponse
|
||||
if err := json.Unmarshal(resp.Body.Bytes(), &got); err != nil {
|
||||
t.Fatalf("decode operations: %v", err)
|
||||
var gotA edgeOperationsResponse
|
||||
if err := json.Unmarshal(respA.Body.Bytes(), &gotA); err != nil {
|
||||
t.Fatalf("decode operations A: %v", err)
|
||||
}
|
||||
if got.EdgeID != "edge-a" || len(got.Operations) != 1 {
|
||||
t.Fatalf("unexpected operations response: %+v", got)
|
||||
if gotA.EdgeID != "edge-a" || len(gotA.Operations) != 1 {
|
||||
t.Fatalf("unexpected operations A response: %+v", gotA)
|
||||
}
|
||||
op := got.Operations[0]
|
||||
if op.CommandID != "cmd-1" || op.Operation != "restart-node" || op.Status != "ok" || op.Summary != "done" {
|
||||
t.Fatalf("unexpected operation record: %+v", op)
|
||||
opA := gotA.Operations[0]
|
||||
if opA.CommandID != "cmd-a-1" || opA.Operation != "restart-node" || opA.Status != "ok" || opA.Summary != "done" {
|
||||
t.Fatalf("unexpected operation A record: %+v", opA)
|
||||
}
|
||||
if len(op.Events) != 1 || op.Events[0].Phase != "running" {
|
||||
t.Fatalf("unexpected operation events: %+v", op.Events)
|
||||
if len(opA.Events) != 1 || opA.Events[0].Phase != "running" {
|
||||
t.Fatalf("unexpected operation A events: %+v", opA.Events)
|
||||
}
|
||||
|
||||
// Request Edge B operations
|
||||
respB := httptest.NewRecorder()
|
||||
mux.ServeHTTP(respB, httptest.NewRequest(http.MethodGet, "/edges/edge-b/operations", nil))
|
||||
if respB.Code != http.StatusOK {
|
||||
t.Fatalf("GET /edges/edge-b/operations status=%d body=%s", respB.Code, respB.Body.String())
|
||||
}
|
||||
var gotB edgeOperationsResponse
|
||||
if err := json.Unmarshal(respB.Body.Bytes(), &gotB); err != nil {
|
||||
t.Fatalf("decode operations B: %v", err)
|
||||
}
|
||||
if gotB.EdgeID != "edge-b" || len(gotB.Operations) != 1 {
|
||||
t.Fatalf("unexpected operations B response: %+v", gotB)
|
||||
}
|
||||
opB := gotB.Operations[0]
|
||||
if opB.CommandID != "cmd-b-1" || opB.Operation != "drain-node" || opB.Status != "failed" || opB.Summary != "timeout" {
|
||||
t.Fatalf("unexpected operation B record: %+v", opB)
|
||||
}
|
||||
if len(opB.Events) != 1 || opB.Events[0].Phase != "draining" {
|
||||
t.Fatalf("unexpected operation B events: %+v", opB.Events)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -676,9 +706,10 @@ func TestFleetStatusHTTPHandlerCombinesConnectionAndCapabilities(t *testing.T) {
|
|||
registry := wire.NewEdgeRegistry()
|
||||
at := time.Unix(1780142200, 0).UTC()
|
||||
registry.MarkConnected(&iop.EdgeHelloRequest{EdgeId: "edge-online", EdgeName: "Online", Version: "1.0.0"}, at)
|
||||
registry.MarkConnected(&iop.EdgeHelloRequest{EdgeId: "edge-other", EdgeName: "Other", Version: "1.1.0"}, at)
|
||||
registry.MarkConnected(&iop.EdgeHelloRequest{EdgeId: "edge-degraded", EdgeName: "Degraded"}, at)
|
||||
registry.MarkConnected(&iop.EdgeHelloRequest{EdgeId: "edge-offline", EdgeName: "Offline"}, at)
|
||||
registry.MarkDisconnected("edge-offline", 3, at.Add(time.Second), proto_socket.DisconnectInfo{Reason: "closed"})
|
||||
offlineToken := registry.MarkConnected(&iop.EdgeHelloRequest{EdgeId: "edge-offline", EdgeName: "Offline"}, at)
|
||||
registry.MarkDisconnected("edge-offline", offlineToken, at.Add(time.Second), proto_socket.DisconnectInfo{Reason: "closed"})
|
||||
|
||||
requestStatus := func(edgeID string, timeout time.Duration) (*iop.EdgeStatusResponse, error) {
|
||||
switch edgeID {
|
||||
|
|
@ -689,6 +720,20 @@ func TestFleetStatusHTTPHandlerCombinesConnectionAndCapabilities(t *testing.T) {
|
|||
Capabilities: []*iop.EdgeCapabilitySummary{
|
||||
{Kind: "run-dispatch", Available: true, Status: "ready"},
|
||||
},
|
||||
DomainAgents: []*iop.EdgeDomainAgentSummary{
|
||||
{AgentKind: "oto", Available: true, LifecycleState: "ready", ActiveCommandId: "cmd-oto-1", Summary: "oto is ready"},
|
||||
},
|
||||
}, nil
|
||||
case "edge-other":
|
||||
return &iop.EdgeStatusResponse{
|
||||
EdgeId: edgeID,
|
||||
Nodes: []*iop.EdgeNodeSnapshot{{NodeId: "node-2", Alias: "beta", Connected: true}},
|
||||
Capabilities: []*iop.EdgeCapabilitySummary{
|
||||
{Kind: "run-dispatch", Available: true, Status: "ready"},
|
||||
},
|
||||
DomainAgents: []*iop.EdgeDomainAgentSummary{
|
||||
{AgentKind: "deployer", Available: true, LifecycleState: "busy", ActiveCommandId: "cmd-dep-2", Summary: "deployer is busy"},
|
||||
},
|
||||
}, nil
|
||||
case "edge-degraded":
|
||||
return nil, fmt.Errorf("status timeout")
|
||||
|
|
@ -709,19 +754,38 @@ func TestFleetStatusHTTPHandlerCombinesConnectionAndCapabilities(t *testing.T) {
|
|||
if err := json.Unmarshal(resp.Body.Bytes(), &got); err != nil {
|
||||
t.Fatalf("decode fleet status: %v", err)
|
||||
}
|
||||
if len(got.Edges) != 3 {
|
||||
t.Fatalf("expected 3 fleet edges, got %d", len(got.Edges))
|
||||
if len(got.Edges) != 4 {
|
||||
t.Fatalf("expected 4 fleet edges, got %d", len(got.Edges))
|
||||
}
|
||||
health := map[string]fleetEdgeView{}
|
||||
for _, e := range got.Edges {
|
||||
health[e.EdgeID] = e
|
||||
}
|
||||
if e := health["edge-online"]; e.Health != "online" || e.NodeCount != 1 || len(e.Capabilities) != 1 {
|
||||
t.Fatalf("unexpected online edge view: %+v", e)
|
||||
|
||||
// Verify edge-online
|
||||
eOnline := health["edge-online"]
|
||||
if eOnline.Health != "online" || eOnline.NodeCount != 1 || len(eOnline.Capabilities) != 1 {
|
||||
t.Fatalf("unexpected online edge view: %+v", eOnline)
|
||||
}
|
||||
if len(eOnline.DomainAgents) != 1 || eOnline.DomainAgents[0].AgentKind != "oto" || eOnline.DomainAgents[0].LifecycleState != "ready" || eOnline.DomainAgents[0].ActiveCommandID != "cmd-oto-1" || eOnline.DomainAgents[0].Summary != "oto is ready" {
|
||||
t.Fatalf("unexpected domain agents for edge-online: %+v", eOnline.DomainAgents)
|
||||
}
|
||||
|
||||
// Verify edge-other
|
||||
eOther := health["edge-other"]
|
||||
if eOther.Health != "online" || eOther.NodeCount != 1 || len(eOther.Capabilities) != 1 {
|
||||
t.Fatalf("unexpected edge-other view: %+v", eOther)
|
||||
}
|
||||
if len(eOther.DomainAgents) != 1 || eOther.DomainAgents[0].AgentKind != "deployer" || eOther.DomainAgents[0].LifecycleState != "busy" || eOther.DomainAgents[0].ActiveCommandID != "cmd-dep-2" || eOther.DomainAgents[0].Summary != "deployer is busy" {
|
||||
t.Fatalf("unexpected domain agents for edge-other: %+v", eOther.DomainAgents)
|
||||
}
|
||||
|
||||
// Verify edge-degraded
|
||||
if e := health["edge-degraded"]; e.Health != "degraded" || e.Error == "" {
|
||||
t.Fatalf("unexpected degraded edge view: %+v", e)
|
||||
}
|
||||
|
||||
// Verify edge-offline
|
||||
if e := health["edge-offline"]; e.Health != "offline" || e.Connected {
|
||||
t.Fatalf("unexpected offline edge view: %+v", e)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue