From 03f9e9890636088060ba8c4c4cf411642f33a50a Mon Sep 17 00:00:00 2001 From: toki Date: Sun, 31 May 2026 05:50:18 +0900 Subject: [PATCH] feat: proto socket infrastructure communication rail - client diagnostics & archive M12 core diagnostics - Archive 12+11_core_diagnostics to agent-task/archive/2026/05 - Update client app initialization with proto socket lifecycle - Add workspace home page with diagnostic capabilities - Integrate proto socket lifecycle management - Update tests for proto socket integration - Update core server tests - Update roadmap milestone tracking --- ...ocket-infrastructure-communication-rail.md | 7 +- .../code_review_cloud_G07_0.log | 0 .../code_review_cloud_G07_1.log} | 132 +++++++++++--- .../12+11_core_diagnostics/complete.log | 36 ++++ .../plan_cloud_G07_0.log | 0 .../plan_cloud_G07_1.log} | 0 .../CODE_REVIEW-cloud-G07.md | 36 ++-- .../lib/src/app/nomadcode_client_app.dart | 1 + .../presentation/workspace_home_page.dart | 164 +++++++++++++++++- .../proto_socket/proto_socket_lifecycle.dart | 125 ++++++++++++- apps/client/pubspec.lock | 8 +- .../proto_socket_lifecycle_test.dart | 76 +++++++- apps/client/test/widget_test.dart | 63 ++++++- .../core/internal/protosocket/server_test.go | 2 + 14 files changed, 594 insertions(+), 56 deletions(-) rename agent-task/{ => archive/2026/05}/m-proto-socket-infrastructure-communication-rail/12+11_core_diagnostics/code_review_cloud_G07_0.log (100%) rename agent-task/{m-proto-socket-infrastructure-communication-rail/12+11_core_diagnostics/CODE_REVIEW-cloud-G07.md => archive/2026/05/m-proto-socket-infrastructure-communication-rail/12+11_core_diagnostics/code_review_cloud_G07_1.log} (56%) create mode 100644 agent-task/archive/2026/05/m-proto-socket-infrastructure-communication-rail/12+11_core_diagnostics/complete.log rename agent-task/{ => archive/2026/05}/m-proto-socket-infrastructure-communication-rail/12+11_core_diagnostics/plan_cloud_G07_0.log (100%) rename agent-task/{m-proto-socket-infrastructure-communication-rail/12+11_core_diagnostics/PLAN-cloud-G07.md => archive/2026/05/m-proto-socket-infrastructure-communication-rail/12+11_core_diagnostics/plan_cloud_G07_1.log} (100%) diff --git a/agent-roadmap/phase/workflow-core/milestones/proto-socket-infrastructure-communication-rail.md b/agent-roadmap/phase/workflow-core/milestones/proto-socket-infrastructure-communication-rail.md index 36d0fa8..c2d0bbb 100644 --- a/agent-roadmap/phase/workflow-core/milestones/proto-socket-infrastructure-communication-rail.md +++ b/agent-roadmap/phase/workflow-core/milestones/proto-socket-infrastructure-communication-rail.md @@ -61,7 +61,7 @@ Flutter client가 no-op proto-socket facade를 실제 Core 연결 레일로 전 내부 통신 레일을 운영 가능하게 만들고 예외 프로토콜이 다시 기본값으로 새지 않게 한다. -- [ ] [diagnostics] connection id, protocol version, channel name, error code를 로그와 디버그 표면에서 확인할 수 있게 한다. +- [ ] [diagnostics] connection id, protocol version, channel name, error code를 로그와 디버그 표면에서 확인할 수 있게 한다. 검증: client diagnostics 구현은 반영되었고, 허용된 검증 환경에서 Flutter test/analyze와 code-review PASS가 필요하다. - [x] [rollout-docs] 새 내부 통신을 추가할 때 proto-socket 우선 여부와 REST/HTTP 예외 사유를 확인하는 운영 체크를 문서화한다. ## 완료 리뷰 @@ -88,10 +88,11 @@ Flutter client가 no-op proto-socket facade를 실제 Core 연결 레일로 전 - 표준선(선택): 내부 런타임 간 신규 통신은 `proto-socket`을 기본 표준으로 삼고, REST/HTTP는 명시된 예외 조건에서만 유지한다. - 선행 작업: Mattermost Nexo Messaging Alignment, Client Integration Standardization, 프로젝트 룰의 proto-socket 표준 규약 - 후속 작업: Workflow Core, External Integration, Project Workspace Management UX -- 현재 지점: Core proto-socket endpoint, task channel, Flutter concrete connector, client task service boundary, workspace/task 화면 소비 경계는 반영되었고, 다음 작업은 diagnostics 로그와 디버그 표면을 여는 것이다. +- 현재 지점: Core proto-socket endpoint, task channel, Flutter concrete connector, client task service boundary, workspace/task 화면 소비 경계, client diagnostics snapshot/debug surface 구현은 반영되었고, 다음 작업은 허용된 검증 환경에서 Flutter test/analyze 실행과 code-review PASS로 diagnostics 항목을 닫는 것이다. - 반영 근거: `packages/contracts/notes/flutter-core-api-candidates.md`에 내부 통신 원칙, proto-socket semantic envelope 후보, REST compatibility map 후보를 추가했다. - Core 반영 근거: `services/core/internal/protosocket/`, `services/core/internal/http/router.go`, `services/core/cmd/server/main.go`에 proto-socket server, task channel, task status event broadcaster, authenticated route 연결과 contract tests가 추가되었다. - Client connector 반영 근거: `apps/client/lib/src/integrations/proto_socket/proto_socket_client.dart`, `apps/client/lib/src/app/bootstrap.dart`, `apps/client/lib/src/app/nomadcode_client_app.dart`에 `RealProtoSocketConnector`, endpoint-config 기반 connect, task service wiring이 추가되었다. - Client task/test 반영 근거: `apps/client/lib/src/integrations/proto_socket/proto_socket_task_service.dart`, `apps/client/lib/src/features/workspaces/presentation/workspace_home_page.dart`, `apps/client/test/integrations/proto_socket_*_test.dart`, `apps/client/test/widget_test.dart`에 task channel 소비 경계와 endpoint/lifecycle/task service/widget 검증이 추가되었다. +- Client diagnostics 반영 근거: `apps/client/lib/src/integrations/proto_socket/proto_socket_lifecycle.dart`에 `ProtoSocketDiagnostics` snapshot/stream과 response/event diagnostics recording을 추가했고, `apps/client/lib/src/app/nomadcode_client_app.dart`, `apps/client/lib/src/features/workspaces/presentation/workspace_home_page.dart`, `apps/client/test/integrations/proto_socket_lifecycle_test.dart`, `apps/client/test/widget_test.dart`에 payload/auth/raw error message 없이 connection id, protocol version, channel/action, error code, timestamp를 확인하는 debug surface와 테스트를 추가했다. - 운영 체크 근거: `packages/contracts/notes/flutter-core-api-candidates.md`에 새 내부 통신 추가 시 proto-socket 우선 여부, REST/HTTP 예외 기록, channel/action 문서화, 검증 항목을 확인하는 운영 체크를 추가했다. -- 확인 필요: 없음 +- 확인 필요: 허용된 원격/검증 환경에서 `cd apps/client && flutter test`, `cd apps/client && flutter analyze --no-fatal-infos`를 실행하고, `agent-task/m-proto-socket-infrastructure-communication-rail/13+11,12_client_diagnostics/` code-review PASS 후 `[diagnostics]` 완료 처리 여부를 반영한다. diff --git a/agent-task/m-proto-socket-infrastructure-communication-rail/12+11_core_diagnostics/code_review_cloud_G07_0.log b/agent-task/archive/2026/05/m-proto-socket-infrastructure-communication-rail/12+11_core_diagnostics/code_review_cloud_G07_0.log similarity index 100% rename from agent-task/m-proto-socket-infrastructure-communication-rail/12+11_core_diagnostics/code_review_cloud_G07_0.log rename to agent-task/archive/2026/05/m-proto-socket-infrastructure-communication-rail/12+11_core_diagnostics/code_review_cloud_G07_0.log diff --git a/agent-task/m-proto-socket-infrastructure-communication-rail/12+11_core_diagnostics/CODE_REVIEW-cloud-G07.md b/agent-task/archive/2026/05/m-proto-socket-infrastructure-communication-rail/12+11_core_diagnostics/code_review_cloud_G07_1.log similarity index 56% rename from agent-task/m-proto-socket-infrastructure-communication-rail/12+11_core_diagnostics/CODE_REVIEW-cloud-G07.md rename to agent-task/archive/2026/05/m-proto-socket-infrastructure-communication-rail/12+11_core_diagnostics/code_review_cloud_G07_1.log index 1ca957a..ad4ae59 100644 --- a/agent-task/m-proto-socket-infrastructure-communication-rail/12+11_core_diagnostics/CODE_REVIEW-cloud-G07.md +++ b/agent-task/archive/2026/05/m-proto-socket-infrastructure-communication-rail/12+11_core_diagnostics/code_review_cloud_G07_1.log @@ -34,45 +34,48 @@ task=m-proto-socket-infrastructure-communication-rail/12+11_core_diagnostics, pl | 항목 | 완료 여부 | |------|---------| -| [REVIEW_API-1] Checklist contract recovery | [ ] | -| [REVIEW_API-2] Core test evidence recovery | [ ] | -| [REVIEW_API-3] Core final verification recovery | [ ] | +| [REVIEW_API-1] Checklist contract recovery | [x] | +| [REVIEW_API-2] Core test evidence recovery | [x] | +| [REVIEW_API-3] Core final verification recovery | [x] | ## 구현 체크리스트 -- [ ] 이번 plan/review의 `구현 체크리스트` 항목 텍스트와 순서를 동일하게 유지하고, 실행 사유나 메모는 체크리스트 문구가 아닌 `계획 대비 변경 사항` 또는 `검증 결과`에 기록한다. -- [ ] `cd services/core && go test ./internal/protosocket -run 'TestServer.*Diagnostics|TestServerRespondsWithErrorEnvelopeForUnsupportedAction' -count=1`를 허용된 원격/검증 환경에서 실행하고 실제 stdout/stderr를 기록한다. -- [ ] `cd services/core && go test ./internal/protosocket -run 'TestRouterProtoSocketTaskListRequiresAuth|TestProtoSocketTaskErrorEnvelopeCodes|Test.*Broadcast.*' -count=1`를 허용된 원격/검증 환경에서 실행하고 실제 stdout/stderr를 기록한다. -- [ ] `cd services/core && go test ./...`를 허용된 원격/검증 환경에서 실행하고 실제 stdout/stderr를 기록한다. -- [ ] `cd services/core && go vet ./...`를 허용된 원격/검증 환경에서 실행하고 실제 stdout/stderr를 기록한다. -- [ ] 검증 명령이 실패하면 repo-owned 원인을 최소 범위로 수정하고, 실패 출력과 수정 후 통과 출력을 모두 기록한다. -- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. +- [x] 이번 plan/review의 `구현 체크리스트` 항목 텍스트와 순서를 동일하게 유지하고, 실행 사유나 메모는 체크리스트 문구가 아닌 `계획 대비 변경 사항` 또는 `검증 결과`에 기록한다. +- [x] `cd services/core && go test ./internal/protosocket -run 'TestServer.*Diagnostics|TestServerRespondsWithErrorEnvelopeForUnsupportedAction' -count=1`를 허용된 원격/검증 환경에서 실행하고 실제 stdout/stderr를 기록한다. +- [x] `cd services/core && go test ./internal/protosocket -run 'TestRouterProtoSocketTaskListRequiresAuth|TestProtoSocketTaskErrorEnvelopeCodes|Test.*Broadcast.*' -count=1`를 허용된 원격/검증 환경에서 실행하고 실제 stdout/stderr를 기록한다. +- [x] `cd services/core && go test ./...`를 허용된 원격/검증 환경에서 실행하고 실제 stdout/stderr를 기록한다. +- [x] `cd services/core && go vet ./...`를 허용된 원격/검증 환경에서 실행하고 실제 stdout/stderr를 기록한다. +- [x] 검증 명령이 실패하면 repo-owned 원인을 최소 범위로 수정하고, 실패 출력과 수정 후 통과 출력을 모두 기록한다. +- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. ## 코드리뷰 전용 체크리스트 > **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다. > 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다. -- [ ] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다. -- [ ] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다. -- [ ] active `CODE_REVIEW-*-G??.md`를 `code_review_cloud_G07_N.log`로 아카이브한다. -- [ ] active `PLAN-*-G??.md`를 `plan_cloud_G07_M.log`로 아카이브한다. -- [ ] `.gitignore`의 Agent-Ops 관리 block이 `agent-task/**/*.md`와 `agent-task/**/*.log`를 unignore하여 plan/review/archive 산출물이 추적 가능한지 확인한다. -- [ ] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다. -- [ ] PASS이면 active task 디렉터리 `agent-task/m-proto-socket-infrastructure-communication-rail/12+11_core_diagnostics/`를 `agent-task/archive/YYYY/MM/m-proto-socket-infrastructure-communication-rail/12+11_core_diagnostics/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다. -- [ ] PASS이고 task group이 `m-`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다. -- [ ] PASS split 작업이면 이동 후 빈 active parent `agent-task/m-proto-socket-infrastructure-communication-rail/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다. +- [x] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다. +- [x] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다. +- [x] active `CODE_REVIEW-*-G??.md`를 `code_review_cloud_G07_N.log`로 아카이브한다. +- [x] active `PLAN-*-G??.md`를 `plan_cloud_G07_M.log`로 아카이브한다. +- [x] `.gitignore`의 Agent-Ops 관리 block이 `agent-task/**/*.md`와 `agent-task/**/*.log`를 unignore하여 plan/review/archive 산출물이 추적 가능한지 확인한다. +- [x] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다. +- [x] PASS이면 active task 디렉터리 `agent-task/m-proto-socket-infrastructure-communication-rail/12+11_core_diagnostics/`를 `agent-task/archive/YYYY/MM/m-proto-socket-infrastructure-communication-rail/12+11_core_diagnostics/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다. +- [x] PASS이고 task group이 `m-`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다. +- [x] PASS split 작업이면 이동 후 빈 active parent `agent-task/m-proto-socket-infrastructure-communication-rail/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다. - [ ] WARN/FAIL이고 user-review gate가 트리거되지 않았으면 다음 active `PLAN-cloud-G07.md`와 `CODE_REVIEW-cloud-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로 이동한다. ## 계획 대비 변경 사항 -_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._ +- 원격 검증은 기존 원격 `~/works/nomadcode`가 다른 커밋과 사용자 변경을 가진 상태라 건드리지 않고, 현재 작업 트리를 `/tmp/nomadcode-verify-382b5f1`에 임시 전개해 실행했다. +- `go test ./...` 첫 실행에서 `TestServerAcceptsWebSocketAndClosesClients`가 server-side client 등록 전에 `srv.Close()`를 호출할 수 있는 race로 실패했다. `server_test.go`에서 기존 helper `waitForServerClients(t, srv, 1)`를 close 직전에 호출하도록 최소 수정했다. ## 주요 설계 결정 -_구현 에이전트가 주요 설계 결정 사항을 기록한다._ +- checklist contract 회복은 active `PLAN-cloud-G07.md`와 `CODE_REVIEW-cloud-G07.md`의 체크리스트 문구를 바꾸지 않고 체크 상태만 갱신했다. +- 소스 변경은 production code가 아니라 `services/core/internal/protosocket/server_test.go`의 기존 close lifecycle test 안정화에 한정했다. +- remote verification은 `toki@toki-labs.com:/tmp/nomadcode-verify-382b5f1`에서 수행했다. ## 사용자 리뷰 요청 @@ -106,34 +109,93 @@ _구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 ### REVIEW_API-1 중간 검증 ``` $ diff -u <(awk '/^## 구현 체크리스트$/{flag=1; next} /^## 의존 관계 및 구현 순서$/{flag=0} flag && /^- \[[ x]\] /{sub(/^- \[[ x]\] /,"- [ ] "); print}' PLAN-cloud-G07.md) <(awk '/^## 구현 체크리스트$/{flag=1; next} /^## 코드리뷰 전용 체크리스트$/{flag=0} flag && /^- \[[ x]\] /{sub(/^- \[[ x]\] /,"- [ ] "); print}' CODE_REVIEW-cloud-G07.md) -(output) +(no stdout/stderr; exit 0) ``` ### REVIEW_API-2 중간 검증 ``` $ cd services/core && go test ./internal/protosocket -run 'TestServer.*Diagnostics|TestServerRespondsWithErrorEnvelopeForUnsupportedAction' -count=1 -(output) +ok github.com/nomadcode/nomadcode-core/internal/protosocket 0.572s $ cd services/core && go test ./internal/protosocket -run 'TestRouterProtoSocketTaskListRequiresAuth|TestProtoSocketTaskErrorEnvelopeCodes|Test.*Broadcast.*' -count=1 -(output) +ok github.com/nomadcode/nomadcode-core/internal/protosocket 0.368s ``` ### REVIEW_API-3 중간 검증 ``` $ cd services/core && go test ./... -(output) +첫 실행 실패: +ok github.com/nomadcode/nomadcode-core/cmd/plane-smoke 0.518s +? github.com/nomadcode/nomadcode-core/cmd/server [no test files] +ok github.com/nomadcode/nomadcode-core/internal/adapters/a2a 0.469s +? github.com/nomadcode/nomadcode-core/internal/adapters/mattermost [no test files] +ok github.com/nomadcode/nomadcode-core/internal/adapters/openai 0.925s +ok github.com/nomadcode/nomadcode-core/internal/adapters/plane 1.660s +? github.com/nomadcode/nomadcode-core/internal/agent [no test files] +ok github.com/nomadcode/nomadcode-core/internal/config 2.879s +? github.com/nomadcode/nomadcode-core/internal/db [no test files] +ok github.com/nomadcode/nomadcode-core/internal/http 2.488s +? github.com/nomadcode/nomadcode-core/internal/model [no test files] +ok github.com/nomadcode/nomadcode-core/internal/notification 2.054s +--- FAIL: TestServerAcceptsWebSocketAndClosesClients (1.00s) + server_test.go:135: timed out waiting for client to disconnect after server close +FAIL +FAIL github.com/nomadcode/nomadcode-core/internal/protosocket 2.234s +ok github.com/nomadcode/nomadcode-core/internal/scheduler 5.458s +? github.com/nomadcode/nomadcode-core/internal/storage [no test files] +ok github.com/nomadcode/nomadcode-core/internal/workflow 3.117s +ok github.com/nomadcode/nomadcode-core/internal/workitem 3.531s +ok github.com/nomadcode/nomadcode-core/internal/workitempipeline 4.084s +FAIL + +수정 후 재실행: +ok github.com/nomadcode/nomadcode-core/cmd/plane-smoke (cached) +? github.com/nomadcode/nomadcode-core/cmd/server [no test files] +ok github.com/nomadcode/nomadcode-core/internal/adapters/a2a (cached) +? github.com/nomadcode/nomadcode-core/internal/adapters/mattermost [no test files] +ok github.com/nomadcode/nomadcode-core/internal/adapters/openai (cached) +ok github.com/nomadcode/nomadcode-core/internal/adapters/plane (cached) +? github.com/nomadcode/nomadcode-core/internal/agent [no test files] +ok github.com/nomadcode/nomadcode-core/internal/config (cached) +? github.com/nomadcode/nomadcode-core/internal/db [no test files] +ok github.com/nomadcode/nomadcode-core/internal/http (cached) +? github.com/nomadcode/nomadcode-core/internal/model [no test files] +ok github.com/nomadcode/nomadcode-core/internal/notification (cached) +ok github.com/nomadcode/nomadcode-core/internal/protosocket 0.397s +ok github.com/nomadcode/nomadcode-core/internal/scheduler (cached) +? github.com/nomadcode/nomadcode-core/internal/storage [no test files] +ok github.com/nomadcode/nomadcode-core/internal/workflow (cached) +ok github.com/nomadcode/nomadcode-core/internal/workitem (cached) +ok github.com/nomadcode/nomadcode-core/internal/workitempipeline (cached) $ cd services/core && go vet ./... -(output) +(no stdout/stderr; exit 0) ``` ### 최종 검증 ``` $ cd services/core && go test ./... -(output) +ok github.com/nomadcode/nomadcode-core/cmd/plane-smoke (cached) +? github.com/nomadcode/nomadcode-core/cmd/server [no test files] +ok github.com/nomadcode/nomadcode-core/internal/adapters/a2a (cached) +? github.com/nomadcode/nomadcode-core/internal/adapters/mattermost [no test files] +ok github.com/nomadcode/nomadcode-core/internal/adapters/openai (cached) +ok github.com/nomadcode/nomadcode-core/internal/adapters/plane (cached) +? github.com/nomadcode/nomadcode-core/internal/agent [no test files] +ok github.com/nomadcode/nomadcode-core/internal/config (cached) +? github.com/nomadcode/nomadcode-core/internal/db [no test files] +ok github.com/nomadcode/nomadcode-core/internal/http (cached) +? github.com/nomadcode/nomadcode-core/internal/model [no test files] +ok github.com/nomadcode/nomadcode-core/internal/notification (cached) +ok github.com/nomadcode/nomadcode-core/internal/protosocket (cached) +ok github.com/nomadcode/nomadcode-core/internal/scheduler (cached) +? github.com/nomadcode/nomadcode-core/internal/storage [no test files] +ok github.com/nomadcode/nomadcode-core/internal/workflow (cached) +ok github.com/nomadcode/nomadcode-core/internal/workitem (cached) +ok github.com/nomadcode/nomadcode-core/internal/workitempipeline (cached) $ cd services/core && go vet ./... -(output) +(no stdout/stderr; exit 0) ``` --- @@ -141,3 +203,17 @@ $ cd services/core && go vet ./... > **[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. + +## 코드리뷰 결과 + +- 종합 판정: PASS +- 차원별 평가: + - Correctness: Pass + - Completeness: Pass + - Test coverage: Pass + - API contract: Pass + - Code quality: Pass + - Plan deviation: Pass + - Verification trust: Pass +- 발견된 문제: 없음 +- 다음 단계: PASS 완료 처리한다. `complete.log`를 작성하고 active task directory를 archive로 이동한다. diff --git a/agent-task/archive/2026/05/m-proto-socket-infrastructure-communication-rail/12+11_core_diagnostics/complete.log b/agent-task/archive/2026/05/m-proto-socket-infrastructure-communication-rail/12+11_core_diagnostics/complete.log new file mode 100644 index 0000000..14633ca --- /dev/null +++ b/agent-task/archive/2026/05/m-proto-socket-infrastructure-communication-rail/12+11_core_diagnostics/complete.log @@ -0,0 +1,36 @@ +# Complete - m-proto-socket-infrastructure-communication-rail/12+11_core_diagnostics + +## 완료 일시 + +2026-05-30 + +## 요약 + +Core proto-socket diagnostics implementation and verification recovery completed after 2 review loops; final verdict PASS. + +## 루프 이력 + +| Plan | Review | Verdict | 메모 | +|------|--------|---------|------| +| `plan_cloud_G07_0.log` | `code_review_cloud_G07_0.log` | FAIL | Core diagnostics source changes were present, but checklist contract and required verification evidence were incomplete. | +| `plan_cloud_G07_1.log` | `code_review_cloud_G07_1.log` | PASS | Verification evidence was recovered in the allowed remote environment and the flaky close lifecycle test was stabilized. | + +## 구현/정리 내용 + +- Core proto-socket response/event diagnostics meta and structured log coverage from the original implementation loop were retained. +- Follow-up loop preserved plan/review checklist text, recorded actual remote stdout/stderr, and stabilized `TestServerAcceptsWebSocketAndClosesClients` with `waitForServerClients(t, srv, 1)` before server close. + +## 최종 검증 + +- `cd services/core && go test ./internal/protosocket -run 'TestServer.*Diagnostics|TestServerRespondsWithErrorEnvelopeForUnsupportedAction' -count=1` - PASS; remote rerun output: `ok github.com/nomadcode/nomadcode-core/internal/protosocket 0.853s`. +- `cd services/core && go test ./internal/protosocket -run 'TestRouterProtoSocketTaskListRequiresAuth|TestProtoSocketTaskErrorEnvelopeCodes|Test.*Broadcast.*' -count=1` - PASS; remote rerun output: `ok github.com/nomadcode/nomadcode-core/internal/protosocket 0.541s`. +- `cd services/core && go test ./...` - PASS; remote rerun completed with all packages passing or cached and no failures. +- `cd services/core && go vet ./...` - PASS; remote rerun exited 0 with no stdout/stderr. + +## 잔여 Nit + +- 없음 + +## 후속 작업 + +- 없음 diff --git a/agent-task/m-proto-socket-infrastructure-communication-rail/12+11_core_diagnostics/plan_cloud_G07_0.log b/agent-task/archive/2026/05/m-proto-socket-infrastructure-communication-rail/12+11_core_diagnostics/plan_cloud_G07_0.log similarity index 100% rename from agent-task/m-proto-socket-infrastructure-communication-rail/12+11_core_diagnostics/plan_cloud_G07_0.log rename to agent-task/archive/2026/05/m-proto-socket-infrastructure-communication-rail/12+11_core_diagnostics/plan_cloud_G07_0.log diff --git a/agent-task/m-proto-socket-infrastructure-communication-rail/12+11_core_diagnostics/PLAN-cloud-G07.md b/agent-task/archive/2026/05/m-proto-socket-infrastructure-communication-rail/12+11_core_diagnostics/plan_cloud_G07_1.log similarity index 100% rename from agent-task/m-proto-socket-infrastructure-communication-rail/12+11_core_diagnostics/PLAN-cloud-G07.md rename to agent-task/archive/2026/05/m-proto-socket-infrastructure-communication-rail/12+11_core_diagnostics/plan_cloud_G07_1.log diff --git a/agent-task/m-proto-socket-infrastructure-communication-rail/13+11,12_client_diagnostics/CODE_REVIEW-cloud-G07.md b/agent-task/m-proto-socket-infrastructure-communication-rail/13+11,12_client_diagnostics/CODE_REVIEW-cloud-G07.md index e59d626..e80d9f8 100644 --- a/agent-task/m-proto-socket-infrastructure-communication-rail/13+11,12_client_diagnostics/CODE_REVIEW-cloud-G07.md +++ b/agent-task/m-proto-socket-infrastructure-communication-rail/13+11,12_client_diagnostics/CODE_REVIEW-cloud-G07.md @@ -41,19 +41,19 @@ task=m-proto-socket-infrastructure-communication-rail/13+11,12_client_diagnostic | 항목 | 완료 여부 | |------|---------| -| [API-1] Client diagnostics state | [ ] | -| [API-2] Workspace debug surface | [ ] | +| [API-1] Client diagnostics state | [x] | +| [API-2] Workspace debug surface | [x] | ## 구현 체크리스트 - [ ] `11_contracts_diagnostics`와 `12+11_core_diagnostics`가 PASS되어 각 `complete.log`가 생겼는지 확인하고 경로를 review stub에 기록한다. -- [ ] `ProtoSocketLifecycle` 또는 dedicated diagnostics controller가 connection state, connection id, protocol version, recent channel/action/error code를 비밀 없이 유지하게 한다. -- [ ] `ProtoSocketTaskService`가 response/error envelope meta를 diagnostics sink에 전달하고 기존 error mapping을 유지한다. -- [ ] `WorkspaceHomePage` 또는 app shell에 compact debug surface를 추가해 connection id, protocol version, channel, error code를 확인할 수 있게 한다. +- [x] `ProtoSocketLifecycle` 또는 dedicated diagnostics controller가 connection state, connection id, protocol version, recent channel/action/error code를 비밀 없이 유지하게 한다. +- [x] `ProtoSocketTaskService`가 response/error envelope meta를 diagnostics sink에 전달하고 기존 error mapping을 유지한다. +- [x] `WorkspaceHomePage` 또는 app shell에 compact debug surface를 추가해 connection id, protocol version, channel, error code를 확인할 수 있게 한다. - [ ] Flutter tests로 endpoint 설정, reconnect/failure lifecycle, task service diagnostics, widget debug surface를 검증한다. - [ ] `cd apps/client && flutter test`를 허용된 원격/검증 환경에서 실행한다. - [ ] `cd apps/client && flutter analyze --no-fatal-infos`를 허용된 원격/검증 환경에서 실행한다. -- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. +- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. ## 코드리뷰 전용 체크리스트 @@ -75,11 +75,19 @@ task=m-proto-socket-infrastructure-communication-rail/13+11,12_client_diagnostic ## 계획 대비 변경 사항 -_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._ +- 선행 `11_contracts_diagnostics`, `12+11_core_diagnostics`의 active `complete.log`는 아직 확인되지 않았다. 사용자의 직접 요청으로 client diagnostics 구현과 문서 동기화를 먼저 진행했으며, 최종 PASS/complete 처리는 code-review와 런타임 완료 이벤트 뒤에 남긴다. +- 새 `proto_socket_diagnostics.dart` 파일을 만들지 않고 `ProtoSocketDiagnostics`를 `proto_socket_lifecycle.dart`에 두었다. 현재 diagnostics 소비자가 lifecycle과 workspace UI에 한정되어 있어 파일 추가보다 기존 facade 안에 보관하는 편이 범위를 작게 유지한다. +- `ProtoSocketTaskService`에 sink를 직접 주입하지 않고, `ProtoSocketLifecycle`이 연결 후 반환하는 transport를 diagnostics wrapper로 감싸 response envelope를 기록하게 했다. 이 방식은 기존 task service error mapping을 바꾸지 않으면서 task service의 response/error envelope meta를 같은 diagnostics state로 전달한다. +- app shell이 별도 diagnostics subscription을 소유하지 않고 `WorkspaceHomePage`에 lifecycle을 넘겨 `StreamBuilder`가 `diagnosticsStream`을 직접 구독한다. state 중복 저장을 피하고 dispose 책임은 기존 lifecycle 소유권에 둔다. +- `agent-test/local/rules.md`가 로컬 테스트/검증 실행 금지를 명시하므로 이번 세션에서는 Flutter test/analyze 명령을 실행하지 않았다. 허용된 원격/검증 환경에서 후속 실행이 필요하다. ## 주요 설계 결정 -_구현 에이전트가 주요 설계 결정 사항을 기록한다._ +- diagnostics snapshot에는 `state`, `connection_id`, `protocol_version`, `channel`, `action`, `error_code`, `timestamp`만 보관한다. +- response/event의 `payload`, `auth`, raw error message, secret 계열 값은 diagnostics state와 UI에 올리지 않는다. +- error가 없는 envelope는 `errorCode`를 `null`로 유지하고 UI에서는 `none`으로 표시한다. +- Core가 이미 envelope meta와 structured log diagnostics를 제공하므로 client 작업은 수신/표시 표면에만 한정했다. +- workspace debug surface는 `ACTIVE TASKS` 아래의 compact `PROTO-SOCKET` section으로 추가했고, 긴 값은 ellipsis 처리해 기존 화면 레이아웃을 흔들지 않게 했다. ## 사용자 리뷰 요청 @@ -113,22 +121,26 @@ _구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 ### API-1 중간 검증 ``` $ cd apps/client && flutter test test/integrations/proto_socket_lifecycle_test.dart test/integrations/proto_socket_task_service_test.dart -(output) +미실행. `agent-test/local/rules.md`가 로컬 테스트/검증 실행 금지를 명시한다. +후속으로 허용된 원격/검증 환경에서 실행해야 한다. ``` ### API-2 중간 검증 ``` $ cd apps/client && flutter test test/widget_test.dart -(output) +미실행. `agent-test/local/rules.md`가 로컬 테스트/검증 실행 금지를 명시한다. +후속으로 허용된 원격/검증 환경에서 실행해야 한다. ``` ### 최종 검증 ``` $ cd apps/client && flutter test -(output) +미실행. `agent-test/local/rules.md`가 로컬 테스트/검증 실행 금지를 명시한다. +후속으로 허용된 원격/검증 환경에서 실행해야 한다. $ cd apps/client && flutter analyze --no-fatal-infos -(output) +미실행. `agent-test/local/rules.md`가 로컬 테스트/검증 실행 금지를 명시한다. +후속으로 허용된 원격/검증 환경에서 실행해야 한다. ``` --- diff --git a/apps/client/lib/src/app/nomadcode_client_app.dart b/apps/client/lib/src/app/nomadcode_client_app.dart index ff7ed9a..183bc9c 100644 --- a/apps/client/lib/src/app/nomadcode_client_app.dart +++ b/apps/client/lib/src/app/nomadcode_client_app.dart @@ -144,6 +144,7 @@ class _NomadCodeClientHomeState extends State<_NomadCodeClientHome> { return WorkspaceHomePage( onLaunchCodeServer: (project) => _launcher.openCodeServer(project), loadTasks: _taskLoader, + protoSocketLifecycle: widget.protoSocketLifecycle, ); } } diff --git a/apps/client/lib/src/features/workspaces/presentation/workspace_home_page.dart b/apps/client/lib/src/features/workspaces/presentation/workspace_home_page.dart index 23f7c8d..a1e41e2 100644 --- a/apps/client/lib/src/features/workspaces/presentation/workspace_home_page.dart +++ b/apps/client/lib/src/features/workspaces/presentation/workspace_home_page.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import '../../../integrations/proto_socket/proto_socket_lifecycle.dart'; import '../domain/project_workspace.dart'; import '../domain/workspace_task.dart'; @@ -13,8 +14,14 @@ typedef WorkspaceTaskLoader = class WorkspaceHomePage extends StatefulWidget { final Future Function(ProjectWorkspace)? onLaunchCodeServer; final WorkspaceTaskLoader? loadTasks; + final ProtoSocketLifecycle? protoSocketLifecycle; - const WorkspaceHomePage({super.key, this.onLaunchCodeServer, this.loadTasks}); + const WorkspaceHomePage({ + super.key, + this.onLaunchCodeServer, + this.loadTasks, + this.protoSocketLifecycle, + }); @override State createState() => _WorkspaceHomePageState(); @@ -53,9 +60,7 @@ class _WorkspaceHomePageState extends State { void _reloadTasks() { final loader = widget.loadTasks; final project = _selectedProject; - _tasksFuture = (loader != null && project != null) - ? loader(project) - : null; + _tasksFuture = (loader != null && project != null) ? loader(project) : null; } @override @@ -366,6 +371,7 @@ class _WorkspaceHomePageState extends State { ), const SizedBox(height: 16), _buildTaskSection(project), + _buildProtoSocketDiagnosticsSection(), ], ), ); @@ -479,6 +485,138 @@ class _WorkspaceHomePageState extends State { ); } + Widget _buildProtoSocketDiagnosticsSection() { + final lifecycle = widget.protoSocketLifecycle; + if (lifecycle == null) return const SizedBox.shrink(); + return StreamBuilder( + stream: lifecycle.diagnosticsStream, + initialData: lifecycle.diagnostics, + builder: (context, snapshot) { + final diagnostics = snapshot.data ?? lifecycle.diagnostics; + final stateColor = _diagnosticsStateColor(diagnostics.state); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 32), + const Text( + 'PROTO-SOCKET', + style: TextStyle( + color: Colors.white38, + fontSize: 12, + fontWeight: FontWeight.bold, + letterSpacing: 1.0, + ), + ), + const SizedBox(height: 16), + Container( + width: double.infinity, + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: const Color(0xFF14142B), + borderRadius: BorderRadius.circular(8), + border: Border.all(color: const Color(0xFF222244)), + ), + child: Wrap( + spacing: 10, + runSpacing: 10, + children: [ + _buildDiagnosticsChip( + Icons.wifi_tethering, + 'state', + diagnostics.state.name, + color: stateColor, + ), + _buildDiagnosticsChip( + Icons.link, + 'connection_id', + _diagnosticValue(diagnostics.connectionId), + ), + _buildDiagnosticsChip( + Icons.device_hub, + 'protocol_version', + _diagnosticValue(diagnostics.protocolVersion), + ), + _buildDiagnosticsChip( + Icons.label_outline, + 'channel', + _diagnosticValue(diagnostics.channel), + ), + _buildDiagnosticsChip( + Icons.flash_on, + 'action', + _diagnosticValue(diagnostics.action), + ), + _buildDiagnosticsChip( + Icons.error_outline, + 'error_code', + _diagnosticValue(diagnostics.errorCode, empty: 'none'), + color: diagnostics.errorCode == null + ? Colors.greenAccent + : Colors.redAccent, + ), + _buildDiagnosticsChip( + Icons.schedule, + 'timestamp', + _diagnosticValue(diagnostics.timestamp), + ), + ], + ), + ), + ], + ); + }, + ); + } + + Widget _buildDiagnosticsChip( + IconData icon, + String label, + String value, { + Color color = Colors.cyanAccent, + }) { + return Container( + constraints: const BoxConstraints(minWidth: 132, maxWidth: 280), + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), + decoration: BoxDecoration( + color: color.withValues(alpha: 0.08), + borderRadius: BorderRadius.circular(6), + border: Border.all(color: color.withValues(alpha: 0.22)), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(icon, color: color, size: 16), + const SizedBox(width: 8), + Flexible( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + label, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle(color: Colors.white38, fontSize: 10), + ), + const SizedBox(height: 2), + Text( + value, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: color, + fontSize: 12, + fontWeight: FontWeight.w700, + ), + ), + ], + ), + ), + ], + ), + ); + } + Widget _buildStatusChip(String status) { return Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), @@ -579,4 +717,22 @@ class _WorkspaceHomePageState extends State { }, ); } + + Color _diagnosticsStateColor(ProtoSocketConnectionState state) { + switch (state) { + case ProtoSocketConnectionState.connected: + return Colors.greenAccent; + case ProtoSocketConnectionState.connecting: + return Colors.amberAccent; + case ProtoSocketConnectionState.failed: + return Colors.redAccent; + case ProtoSocketConnectionState.disconnected: + return Colors.white54; + } + } + + String _diagnosticValue(String? value, {String empty = 'n/a'}) { + if (value == null || value.isEmpty) return empty; + return value; + } } diff --git a/apps/client/lib/src/integrations/proto_socket/proto_socket_lifecycle.dart b/apps/client/lib/src/integrations/proto_socket/proto_socket_lifecycle.dart index 6df6fe0..d351e76 100644 --- a/apps/client/lib/src/integrations/proto_socket/proto_socket_lifecycle.dart +++ b/apps/client/lib/src/integrations/proto_socket/proto_socket_lifecycle.dart @@ -5,6 +5,62 @@ import 'proto_socket_envelope.dart'; enum ProtoSocketConnectionState { disconnected, connecting, connected, failed } +class ProtoSocketDiagnostics { + final ProtoSocketConnectionState state; + final String? connectionId; + final String? protocolVersion; + final String? channel; + final String? action; + final String? errorCode; + final String? timestamp; + + const ProtoSocketDiagnostics({ + required this.state, + this.connectionId, + this.protocolVersion, + this.channel, + this.action, + this.errorCode, + this.timestamp, + }); + + ProtoSocketDiagnostics copyWithState(ProtoSocketConnectionState next) { + return ProtoSocketDiagnostics( + state: next, + connectionId: connectionId, + protocolVersion: protocolVersion, + channel: channel, + action: action, + errorCode: errorCode, + timestamp: timestamp, + ); + } + + ProtoSocketDiagnostics fromEnvelope(ProtoSocketEnvelope envelope) { + return ProtoSocketDiagnostics( + state: state, + connectionId: + _stringValue(envelope.meta['connection_id']) ?? connectionId, + protocolVersion: + _nonEmptyString(envelope.protocolVersion) ?? + _stringValue(envelope.meta['protocol_version']) ?? + protocolVersion, + channel: + _nonEmptyString(envelope.channel) ?? + _stringValue(envelope.meta['channel']) ?? + channel, + action: + _nonEmptyString(envelope.action) ?? + _stringValue(envelope.meta['action']) ?? + action, + errorCode: + _nonEmptyString(envelope.error?.code) ?? + _nonEmptyString(_stringValue(envelope.meta['error_code'])), + timestamp: _stringValue(envelope.meta['timestamp']) ?? timestamp, + ); + } +} + abstract interface class ProtoSocketTransport { Future sendRequest( ProtoSocketEnvelope envelope, { @@ -32,16 +88,25 @@ class ProtoSocketLifecycle { final ProtoSocketConnector _connector; final StreamController _stateController = StreamController.broadcast(); + final StreamController _diagnosticsController = + StreamController.broadcast(); ProtoSocketConnectionState _state = ProtoSocketConnectionState.disconnected; + ProtoSocketDiagnostics _diagnostics = const ProtoSocketDiagnostics( + state: ProtoSocketConnectionState.disconnected, + ); Object? _lastError; ProtoSocketTransport? _transport; + StreamSubscription? _eventDiagnosticsSub; ProtoSocketLifecycle({required ProtoSocketConnector connector}) : _connector = connector; ProtoSocketConnectionState get state => _state; Stream get stateStream => _stateController.stream; + ProtoSocketDiagnostics get diagnostics => _diagnostics; + Stream get diagnosticsStream => + _diagnosticsController.stream; Object? get lastError => _lastError; ProtoSocketTransport? get transport => _transport; @@ -53,11 +118,18 @@ class ProtoSocketLifecycle { _emit(ProtoSocketConnectionState.connecting); try { final transport = await _connector.connect(config); - _transport = transport; + await _eventDiagnosticsSub?.cancel(); + _eventDiagnosticsSub = transport.events.listen(_recordDiagnostics); + _transport = _DiagnosticsProtoSocketTransport( + delegate: transport, + onEnvelope: _recordDiagnostics, + ); _lastError = null; _emit(ProtoSocketConnectionState.connected); } catch (e) { _transport = null; + await _eventDiagnosticsSub?.cancel(); + _eventDiagnosticsSub = null; _lastError = e; _emit(ProtoSocketConnectionState.failed); rethrow; @@ -67,20 +139,64 @@ class ProtoSocketLifecycle { Future disconnect() async { if (_state == ProtoSocketConnectionState.disconnected) return; await _connector.disconnect(); + await _eventDiagnosticsSub?.cancel(); + _eventDiagnosticsSub = null; _transport = null; _emit(ProtoSocketConnectionState.disconnected); } Future dispose() async { + await _eventDiagnosticsSub?.cancel(); + _eventDiagnosticsSub = null; + await _diagnosticsController.close(); await _stateController.close(); } void _emit(ProtoSocketConnectionState next) { _state = next; + _emitDiagnostics(_diagnostics.copyWithState(next)); if (!_stateController.isClosed) { _stateController.add(next); } } + + void _recordDiagnostics(ProtoSocketEnvelope envelope) { + _emitDiagnostics(_diagnostics.fromEnvelope(envelope)); + } + + void _emitDiagnostics(ProtoSocketDiagnostics next) { + _diagnostics = next; + if (!_diagnosticsController.isClosed) { + _diagnosticsController.add(next); + } + } +} + +class _DiagnosticsProtoSocketTransport implements ProtoSocketTransport { + final ProtoSocketTransport _delegate; + final void Function(ProtoSocketEnvelope envelope) _onEnvelope; + + _DiagnosticsProtoSocketTransport({ + required ProtoSocketTransport delegate, + required void Function(ProtoSocketEnvelope envelope) onEnvelope, + }) : _delegate = delegate, + _onEnvelope = onEnvelope; + + @override + Stream get events => _delegate.events; + + @override + Future sendRequest( + ProtoSocketEnvelope envelope, { + Duration timeout = const Duration(seconds: 30), + }) async { + final response = await _delegate.sendRequest(envelope, timeout: timeout); + _onEnvelope(response); + return response; + } + + @override + Future close() => _delegate.close(); } /// Test/fallback connector for disabled or fake scenarios. @@ -112,3 +228,10 @@ class _NoopProtoSocketTransport implements ProtoSocketTransport { @override Future close() => _events.close(); } + +String? _stringValue(Object? value) => value is String ? value : null; + +String? _nonEmptyString(String? value) { + if (value == null || value.isEmpty) return null; + return value; +} diff --git a/apps/client/pubspec.lock b/apps/client/pubspec.lock index 6e64f4a..e87151e 100644 --- a/apps/client/pubspec.lock +++ b/apps/client/pubspec.lock @@ -212,10 +212,10 @@ packages: dependency: transitive description: name: meta - sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349" + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" url: "https://pub.dev" source: hosted - version: "1.18.0" + version: "1.17.0" nexo_messaging: dependency: "direct main" description: @@ -303,10 +303,10 @@ packages: dependency: transitive description: name: test_api - sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e" + sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a" url: "https://pub.dev" source: hosted - version: "0.7.11" + version: "0.7.10" typed_data: dependency: transitive description: diff --git a/apps/client/test/integrations/proto_socket_lifecycle_test.dart b/apps/client/test/integrations/proto_socket_lifecycle_test.dart index 60ea6e9..04de2ba 100644 --- a/apps/client/test/integrations/proto_socket_lifecycle_test.dart +++ b/apps/client/test/integrations/proto_socket_lifecycle_test.dart @@ -104,7 +104,7 @@ void main() { await lifecycle.connect(config); expect(lifecycle.state, ProtoSocketConnectionState.connected); - expect(lifecycle.transport, same(fake.transport)); + expect(lifecycle.transport, isNotNull); expect(fake.connectCalls, equals(1)); await lifecycle.disconnect(); @@ -123,6 +123,80 @@ void main() { ]); }); + test('records response diagnostics from transport requests', () async { + final fake = _FakeConnector(); + final lifecycle = ProtoSocketLifecycle(connector: fake); + fake.transport.response = const ProtoSocketEnvelope( + id: 'resp-1', + correlationId: 'req-1', + type: 'response', + channel: 'task', + action: 'task.list', + meta: { + 'connection_id': 'conn-response', + 'timestamp': '2026-05-30T12:00:00Z', + }, + payload: {'secret': 'not-for-diagnostics'}, + ); + + await lifecycle.connect(config); + await lifecycle.transport!.sendRequest( + const ProtoSocketEnvelope( + id: 'req-1', + type: 'request', + channel: 'task', + action: 'task.list', + ), + ); + + expect(lifecycle.diagnostics.state, ProtoSocketConnectionState.connected); + expect(lifecycle.diagnostics.connectionId, equals('conn-response')); + expect( + lifecycle.diagnostics.protocolVersion, + equals(protoSocketProtocolVersion), + ); + expect(lifecycle.diagnostics.channel, equals('task')); + expect(lifecycle.diagnostics.action, equals('task.list')); + expect(lifecycle.diagnostics.errorCode, isNull); + expect(lifecycle.diagnostics.timestamp, equals('2026-05-30T12:00:00Z')); + + await lifecycle.dispose(); + }); + + test('records event diagnostics with error codes', () async { + final fake = _FakeConnector(); + final lifecycle = ProtoSocketLifecycle(connector: fake); + + await lifecycle.connect(config); + fake.transport.controller.add( + const ProtoSocketEnvelope( + id: 'event-1', + type: 'event', + channel: 'task', + action: 'task.status.changed', + error: ProtoSocketEnvelopeError( + code: 'task.conflict', + message: 'payload detail is private', + retryable: false, + ), + meta: { + 'connection_id': 'conn-event', + 'timestamp': '2026-05-30T12:01:00Z', + }, + payload: {'token': 'do-not-display'}, + ), + ); + await Future.delayed(Duration.zero); + + expect(lifecycle.diagnostics.connectionId, equals('conn-event')); + expect(lifecycle.diagnostics.channel, equals('task')); + expect(lifecycle.diagnostics.action, equals('task.status.changed')); + expect(lifecycle.diagnostics.errorCode, equals('task.conflict')); + expect(lifecycle.diagnostics.timestamp, equals('2026-05-30T12:01:00Z')); + + await lifecycle.dispose(); + }); + test('connect failure transitions to failed and records error', () async { final fake = _FakeConnector()..throwOnConnect = StateError('boom'); final lifecycle = ProtoSocketLifecycle(connector: fake); diff --git a/apps/client/test/widget_test.dart b/apps/client/test/widget_test.dart index b3bcebd..4f3bfc0 100644 --- a/apps/client/test/widget_test.dart +++ b/apps/client/test/widget_test.dart @@ -24,6 +24,8 @@ const _disabledProtoSocketConfig = ProtoSocketEndpointConfig( ); class _FakeProtoSocketTransport implements ProtoSocketTransport { + final StreamController controller = + StreamController.broadcast(); final List> taskMaps = const [ { 'id': 'task-service-1', @@ -35,7 +37,7 @@ class _FakeProtoSocketTransport implements ProtoSocketTransport { final List sent = []; @override - Stream get events => const Stream.empty(); + Stream get events => controller.stream; @override Future sendRequest( @@ -53,11 +55,15 @@ class _FakeProtoSocketTransport implements ProtoSocketTransport { channel: 'task', action: envelope.action, payload: {'tasks': taskMaps}, + meta: const { + 'connection_id': 'conn-widget', + 'timestamp': '2026-05-30T12:02:00Z', + }, ); } @override - Future close() async {} + Future close() => controller.close(); } class _FakeProtoSocketConnector implements ProtoSocketConnector { @@ -174,7 +180,8 @@ void main() { await tester.pumpWidget( MaterialApp( home: WorkspaceHomePage( - loadTasks: (project) async => throw Exception('secret transport boom'), + loadTasks: (project) async => + throw Exception('secret transport boom'), ), ), ); @@ -186,6 +193,54 @@ void main() { expect(find.textContaining('secret transport boom'), findsNothing); }); + testWidgets('WorkspaceHomePage shows proto-socket diagnostics safely', ( + WidgetTester tester, + ) async { + final connector = _FakeProtoSocketConnector(); + final lifecycle = ProtoSocketLifecycle(connector: connector); + await lifecycle.connect(_enabledProtoSocketConfig); + + await tester.pumpWidget( + MaterialApp(home: WorkspaceHomePage(protoSocketLifecycle: lifecycle)), + ); + await tester.pumpAndSettle(); + + connector.transport.controller.add( + const ProtoSocketEnvelope( + id: 'event-secret', + type: 'event', + channel: 'task', + action: 'task.status.changed', + error: ProtoSocketEnvelopeError( + code: 'task.conflict', + message: 'secret server detail', + retryable: false, + ), + payload: {'secret': 'do-not-render'}, + meta: { + 'connection_id': 'conn-debug', + 'timestamp': '2026-05-30T12:03:00Z', + }, + ), + ); + await tester.pump(); + + expect(find.text('PROTO-SOCKET'), findsOneWidget); + expect(find.text('connected'), findsOneWidget); + expect(find.text('conn-debug'), findsOneWidget); + expect(find.text(protoSocketProtocolVersion), findsOneWidget); + expect(find.text('task'), findsOneWidget); + expect(find.text('task.status.changed'), findsOneWidget); + expect(find.text('task.conflict'), findsOneWidget); + expect(find.text('2026-05-30T12:03:00Z'), findsOneWidget); + expect(find.textContaining('do-not-render'), findsNothing); + expect(find.textContaining('secret server detail'), findsNothing); + + await tester.pumpWidget(const SizedBox.shrink()); + await lifecycle.disconnect(); + await lifecycle.dispose(); + }); + testWidgets('reloads tasks when loader is attached after initial build', ( WidgetTester tester, ) async { @@ -272,6 +327,8 @@ void main() { expect(connector.transport.sent.single.action, equals('task.list')); expect(find.text('Service task from Core'), findsOneWidget); expect(find.text('running'), findsOneWidget); + expect(find.text('conn-widget'), findsOneWidget); + expect(find.text('task.list'), findsOneWidget); await tester.pumpWidget(const SizedBox.shrink()); await tester.pump(); diff --git a/services/core/internal/protosocket/server_test.go b/services/core/internal/protosocket/server_test.go index 29f3346..46075f1 100644 --- a/services/core/internal/protosocket/server_test.go +++ b/services/core/internal/protosocket/server_test.go @@ -124,6 +124,8 @@ func TestServerAcceptsWebSocketAndClosesClients(t *testing.T) { disconnected <- struct{}{} }) + waitForServerClients(t, srv, 1) + if err := srv.Close(); err != nil { t.Errorf("failed to close server: %v", err) }