feat: core diagnostics - G07 code review and proto-socket improvements
- Add code review log for cloud G07 - Update plan and code review documents for core diagnostics - Improve protosocket server implementation and tests
This commit is contained in:
parent
d0a0da8157
commit
382b5f14ba
7 changed files with 804 additions and 171 deletions
|
|
@ -1,6 +1,6 @@
|
|||
<!-- task=m-proto-socket-infrastructure-communication-rail/12+11_core_diagnostics plan=0 tag=API -->
|
||||
<!-- task=m-proto-socket-infrastructure-communication-rail/12+11_core_diagnostics plan=1 tag=REVIEW_API -->
|
||||
|
||||
# Code Review Reference - 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.
|
||||
|
|
@ -13,7 +13,7 @@
|
|||
## 개요
|
||||
|
||||
date=2026-05-30
|
||||
task=m-proto-socket-infrastructure-communication-rail/12+11_core_diagnostics, plan=0, tag=API
|
||||
task=m-proto-socket-infrastructure-communication-rail/12+11_core_diagnostics, plan=1, tag=REVIEW_API
|
||||
|
||||
## 이 파일을 읽는 리뷰 에이전트에게
|
||||
|
||||
|
|
@ -23,7 +23,7 @@ task=m-proto-socket-infrastructure-communication-rail/12+11_core_diagnostics, pl
|
|||
리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다.
|
||||
|
||||
1. 판정을 append한다.
|
||||
2. `CODE_REVIEW-cloud-G07.md` → `code_review_cloud_G07_N.log`, `PLAN-cloud-G07.md` → `plan_cloud_G07_M.log`로 아카이브한다.
|
||||
2. `CODE_REVIEW-cloud-G07.md` -> `code_review_cloud_G07_N.log`, `PLAN-cloud-G07.md` -> `plan_cloud_G07_M.log`로 아카이브한다.
|
||||
3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/m-proto-socket-infrastructure-communication-rail/12+11_core_diagnostics/`로 이동한다. WARN/FAIL이면 user-review gate를 확인한 뒤 다음 active plan/review 파일 또는 `USER_REVIEW.md`를 작성한다. `USER_REVIEW.md`가 사용자 결정으로 완료/PASS 해소되면 code-review가 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log` 작성 후 archive 이동한다.
|
||||
4. PASS이고 task group이 `m-<milestone-slug>`이면 완료 이벤트 메타데이터를 보고한다. roadmap 상태 체크와 `update-roadmap` 호출은 런타임 책임이다.
|
||||
5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다.
|
||||
|
|
@ -34,17 +34,18 @@ task=m-proto-socket-infrastructure-communication-rail/12+11_core_diagnostics, pl
|
|||
|
||||
| 항목 | 완료 여부 |
|
||||
|------|---------|
|
||||
| [API-1] Core connection diagnostics | [ ] |
|
||||
| [API-2] Core envelope meta and error logs | [ ] |
|
||||
| [REVIEW_API-1] Checklist contract recovery | [ ] |
|
||||
| [REVIEW_API-2] Core test evidence recovery | [ ] |
|
||||
| [REVIEW_API-3] Core final verification recovery | [ ] |
|
||||
|
||||
## 구현 체크리스트
|
||||
|
||||
- [ ] `11_contracts_diagnostics`가 PASS되어 `complete.log`가 생겼는지 확인하고 경로를 review stub에 기록한다.
|
||||
- [ ] Core proto-socket 연결마다 stable `connection_id`를 만들고 연결/해제/request/broadcast 로그에 `connection_id`, `protocol_version`, `channel`, `action`, `error_code`를 구조화 속성으로 남긴다.
|
||||
- [ ] response/event envelope `meta`에 diagnostics 필드를 additive로 채운다.
|
||||
- [ ] Core 테스트로 diagnostics meta와 구조화 로그를 검증한다.
|
||||
- [ ] `cd services/core && go test ./...`를 허용된 원격/검증 환경에서 실행한다.
|
||||
- [ ] `cd services/core && go vet ./...`를 허용된 원격/검증 환경에서 실행한다.
|
||||
- [ ] 이번 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의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
|
||||
|
||||
## 코드리뷰 전용 체크리스트
|
||||
|
|
@ -87,10 +88,10 @@ _기본값은 `없음`이다. 구현 중 사용자 결정, 사용자 소유 외
|
|||
|
||||
## 리뷰어를 위한 체크포인트
|
||||
|
||||
- `connection_id` 생성과 clients map 변경이 race 없이 mutex 보호 아래 있는가.
|
||||
- response/event meta가 기존 payload shape와 REST compatibility를 깨지 않는 additive 변경인가.
|
||||
- 로그에 auth token, payload 원문, secret이 출력되지 않는가.
|
||||
- predecessor `11_contracts_diagnostics` complete evidence가 기록되었는가.
|
||||
- plan/review의 `구현 체크리스트` 항목 텍스트와 순서가 동일한가.
|
||||
- targeted tests, full core test, vet의 실제 stdout/stderr가 기록되었는가.
|
||||
- 명령 실패가 있었다면 repo-owned 원인을 최소 범위로 수정했고 실패/재실행 출력이 모두 남았는가.
|
||||
- 테스트 실행 환경 사유가 단순 증거 공백으로 남지 않고, 필요 시 사용자 리뷰 요청 요건을 충족하는 구체 증거로 기록되었는가.
|
||||
|
||||
## 검증 결과
|
||||
|
||||
|
|
@ -102,18 +103,30 @@ _구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후
|
|||
- `검증 결과`에는 실제 stdout/stderr를 붙여 넣는다.
|
||||
- 사용자 리뷰 요청으로 명령을 끝까지 실행하지 못했다면 `사용자 리뷰 요청`에 실행한 명령, 실제 출력, 미실행 명령의 사유를 기록한다.
|
||||
|
||||
### API-1 중간 검증
|
||||
### REVIEW_API-1 중간 검증
|
||||
```
|
||||
$ cd services/core && go test ./internal/protosocket -run 'TestServer.*Diagnostics|TestServerRespondsWithErrorEnvelopeForUnsupportedAction' -count=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)
|
||||
```
|
||||
|
||||
### API-2 중간 검증
|
||||
### REVIEW_API-2 중간 검증
|
||||
```
|
||||
$ cd services/core && go test ./internal/protosocket -run 'TestServer.*Diagnostics|TestServerRespondsWithErrorEnvelopeForUnsupportedAction' -count=1
|
||||
(output)
|
||||
|
||||
$ cd services/core && go test ./internal/protosocket -run 'TestRouterProtoSocketTaskListRequiresAuth|TestProtoSocketTaskErrorEnvelopeCodes|Test.*Broadcast.*' -count=1
|
||||
(output)
|
||||
```
|
||||
|
||||
### REVIEW_API-3 중간 검증
|
||||
```
|
||||
$ cd services/core && go test ./...
|
||||
(output)
|
||||
|
||||
$ cd services/core && go vet ./...
|
||||
(output)
|
||||
```
|
||||
|
||||
### 최종 검증
|
||||
```
|
||||
$ cd services/core && go test ./...
|
||||
|
|
|
|||
|
|
@ -1,18 +1,22 @@
|
|||
<!-- task=m-proto-socket-infrastructure-communication-rail/12+11_core_diagnostics plan=0 tag=API -->
|
||||
<!-- task=m-proto-socket-infrastructure-communication-rail/12+11_core_diagnostics plan=1 tag=REVIEW_API -->
|
||||
|
||||
# Plan - API Core Proto-Socket Diagnostics
|
||||
# Plan - REVIEW_API Core Diagnostics Verification Recovery
|
||||
|
||||
## 이 파일을 읽는 구현 에이전트에게
|
||||
|
||||
`CODE_REVIEW-cloud-G07.md`의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 반드시 채운다. 구현 후 검증을 실행하고 active 파일은 그대로 둔 채 리뷰 준비를 보고한다. 최종 판정, log rename, `complete.log`, archive 이동은 code-review-skill 전용이다. 사용자 결정, 사용자 소유 외부 환경, 범위 충돌로 막히면 review stub의 `사용자 리뷰 요청` 섹션에 정확한 근거와 재개 조건을 채우고 멈춘다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 닫을 수 있는 증거 공백은 사용자 리뷰 요청이 아니다.
|
||||
`CODE_REVIEW-cloud-G07.md`의 구현 에이전트 소유 섹션을 실제 검증 출력으로 반드시 채운다. 이번 루프의 기본 목표는 검증 증거 회복이며, 소스 코드는 명령 실패가 실제 repo-owned 문제를 드러낼 때만 최소 범위로 수정한다. 최종 판정, log rename, `complete.log`, archive 이동은 code-review-skill 전용이다. 사용자 결정, 사용자 소유 외부 환경, 범위 충돌로 막히면 review stub의 `사용자 리뷰 요청` 섹션에 정확한 근거와 재개 조건을 채우고 멈춘다. 단순 증거 공백은 사용자 리뷰 요청이 아니며, 허용된 원격/검증 환경에서 명령을 재실행해 닫는다.
|
||||
|
||||
## 배경
|
||||
|
||||
Core proto-socket server는 연결과 broadcast 실패를 로그로 남기지만 connection id, protocol version, channel, action, error code를 구조화된 속성으로 남기지 않는다. Client debug surface도 Core가 보내는 response/event meta를 소비해야 하므로 먼저 Core가 안정적인 diagnostics meta와 로그를 제공해야 한다. 이 subtask는 계약 문서 subtask 완료 후 진행한다.
|
||||
이전 루프 `code_review_cloud_G07_0.log`는 Core diagnostics 구현 자체보다 plan/review 루프 계약과 검증 신뢰 문제로 FAIL 처리되었다.
|
||||
|
||||
- review stub의 `구현 체크리스트` 항목 중 하나가 계획 파일 원문과 달라졌다.
|
||||
- 필수 최종 검증 `cd services/core && go test ./...`와 `cd services/core && go vet ./...`가 실행되지 않았고 실제 stdout/stderr가 없다.
|
||||
- local 테스트 규칙은 로컬 실행을 금지하므로, 이번 루프는 허용된 원격/검증 환경에서 명령 증거를 회복해야 한다.
|
||||
|
||||
## 사용자 리뷰 요청 흐름
|
||||
|
||||
구현 중 차단 사유는 active review stub의 `사용자 리뷰 요청` 섹션에 기록한다. 이 섹션은 `agent-ops/skills/common/_templates/implementation-user-review-request-section.md`를 기준으로 하며, code-review가 검증 후 실제 `USER_REVIEW.md` 작성 여부를 결정한다.
|
||||
구현 중 차단 사유는 active review stub의 `사용자 리뷰 요청` 섹션에 기록한다. 허용된 원격/검증 환경 자체가 사용자 소유 준비물이라 접근할 수 없다는 구체 증거가 있을 때만 사용자 리뷰 요청을 작성한다. 단순히 이전 루프처럼 "로컬 규칙상 미실행"만 기록하고 완료 처리하지 않는다.
|
||||
|
||||
## 분석 결과
|
||||
|
||||
|
|
@ -21,195 +25,140 @@ Core proto-socket server는 연결과 broadcast 실패를 로그로 남기지만
|
|||
- `agent-ops/rules/project/rules.md`
|
||||
- `agent-ops/rules/private/rules.md`
|
||||
- `agent-ops/rules/common/rules-roadmap.md`
|
||||
- `agent-ops/skills/common/router.md`
|
||||
- `agent-ops/skills/common/plan/SKILL.md`
|
||||
- `agent-roadmap/current.md`
|
||||
- `agent-roadmap/phase/workflow-core/PHASE.md`
|
||||
- `agent-roadmap/phase/workflow-core/milestones/proto-socket-infrastructure-communication-rail.md`
|
||||
- `agent-ops/rules/project/domain/core/rules.md`
|
||||
- `agent-ops/rules/project/domain/contracts/rules.md`
|
||||
- `agent-test/local/rules.md`
|
||||
- `agent-test/local/core-smoke.md`
|
||||
- `agent-test/local/contracts-smoke.md`
|
||||
- `agent-ops/skills/common/code-review/SKILL.md`
|
||||
- `agent-task/m-proto-socket-infrastructure-communication-rail/12+11_core_diagnostics/plan_cloud_G07_0.log`
|
||||
- `agent-task/m-proto-socket-infrastructure-communication-rail/12+11_core_diagnostics/code_review_cloud_G07_0.log`
|
||||
- `services/core/internal/protosocket/server.go`
|
||||
- `services/core/internal/protosocket/dispatcher.go`
|
||||
- `services/core/internal/protosocket/envelope.go`
|
||||
- `services/core/internal/protosocket/events.go`
|
||||
- `services/core/internal/protosocket/tasks.go`
|
||||
- `services/core/internal/http/router.go`
|
||||
- `services/core/internal/protosocket/server_test.go`
|
||||
- `services/core/internal/protosocket/tasks_test.go`
|
||||
- `services/core/internal/protosocket/envelope_test.go`
|
||||
- `packages/contracts/notes/flutter-core-api-candidates.md`
|
||||
|
||||
### 테스트 환경 규칙
|
||||
|
||||
`test_env=local`로 판단했고 `agent-test/local/rules.md`를 읽었다. 이 파일은 로컬 테스트 금지를 명시하므로 현 세션에서는 검증 명령을 실행하지 않는다. 적용 profile은 `core-smoke`이며 core 변경의 필수 검증은 `cd services/core && go test ./...`, 보조 검증은 `cd services/core && go vet ./...`다. 명령은 후속 구현/리뷰 에이전트가 허용된 원격/검증 환경에서 실행한다.
|
||||
|
||||
### 테스트 커버리지 공백
|
||||
|
||||
- connection id가 response/event meta에 들어가는 동작: 기존 테스트 없음. `server_test.go` 또는 `tasks_test.go`에 추가 필요.
|
||||
- protocol/channel/action/error_code 구조화 로그: 기존 테스트 없음. `slog` buffer 기반 테스트 추가 필요.
|
||||
- 기존 task action 의미 보존: `tasks_test.go`가 create/list/get/enqueue, auth, error code를 이미 검증한다.
|
||||
|
||||
### 심볼 참조
|
||||
|
||||
none. 기존 public symbol rename/remove 없이 server 내부 struct/helper를 추가한다.
|
||||
|
||||
### 분할 판단
|
||||
|
||||
split decision policy를 평가했다. 이 subtask는 `12+11_core_diagnostics`이며 predecessor `11_contracts_diagnostics`의 `complete.log`가 필요하다. 현재 active/archive predecessor completion은 없음으로 확인했다. 구현은 `11` PASS 후 시작해야 한다.
|
||||
`agent-test/local/rules.md`는 로컬 테스트/검증 실행을 금지한다. 이 plan의 `go test`/`go vet` 명령은 허용된 원격/검증 환경에서 실행하고, 실제 stdout/stderr를 review stub에 붙여 넣는다. 명령을 실행할 수 없으면 사용자 소유 환경 차단인지, repo-owned setup 문제인지 구분해 기록한다.
|
||||
|
||||
### 범위 결정 근거
|
||||
|
||||
Core proto-socket server, dispatcher/event helper, core tests만 수정한다. Flutter debug UI와 contracts note 수정은 각각 `13+11,12_client_diagnostics`, `11_contracts_diagnostics` 범위다. REST task API shape, auth policy, workflow service behavior, DB schema는 변경하지 않는다.
|
||||
이전 코드 리뷰에서 즉시 확인된 소스 결함은 없다. 따라서 기본 범위는 task artifact의 증거 회복이다. `go test` 또는 `go vet`가 실패하면 실패 원인을 기준으로 `services/core/internal/protosocket/**` 안에서 최소 수정하고, 실패 출력과 수정 후 재실행 출력을 모두 남긴다.
|
||||
|
||||
### 빌드 등급
|
||||
|
||||
`cloud-G07`: protocol diagnostics, websocket connection lifecycle, structured logging, concurrency-safe client registry를 건드리므로 cloud lane과 높은 등급이 필요하다.
|
||||
`cloud-G07`: verification trust recovery이며 이전 루프가 필수 core 검증 증거 없이 종료되었으므로 cloud lane과 G07을 유지한다.
|
||||
|
||||
## 구현 체크리스트
|
||||
|
||||
- [ ] `11_contracts_diagnostics`가 PASS되어 `complete.log`가 생겼는지 확인하고 경로를 review stub에 기록한다.
|
||||
- [ ] Core proto-socket 연결마다 stable `connection_id`를 만들고 연결/해제/request/broadcast 로그에 `connection_id`, `protocol_version`, `channel`, `action`, `error_code`를 구조화 속성으로 남긴다.
|
||||
- [ ] response/event envelope `meta`에 diagnostics 필드를 additive로 채운다.
|
||||
- [ ] Core 테스트로 diagnostics meta와 구조화 로그를 검증한다.
|
||||
- [ ] `cd services/core && go test ./...`를 허용된 원격/검증 환경에서 실행한다.
|
||||
- [ ] `cd services/core && go vet ./...`를 허용된 원격/검증 환경에서 실행한다.
|
||||
- [ ] 이번 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의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
|
||||
|
||||
## 의존 관계 및 구현 순서
|
||||
|
||||
이 directory name은 `12+11_core_diagnostics`이므로 predecessor는 같은 task group의 `11_*` subtask뿐이다. `agent-task/m-proto-socket-infrastructure-communication-rail/11_contracts_diagnostics/complete.log` 또는 matching archive `complete.log`가 생기기 전에는 구현을 시작하지 않는다.
|
||||
1. `plan_cloud_G07_0.log`와 `code_review_cloud_G07_0.log`의 FAIL 사유를 확인한다.
|
||||
2. follow-up `PLAN-cloud-G07.md`와 `CODE_REVIEW-cloud-G07.md`의 `구현 체크리스트` 텍스트/순서가 일치하는지 확인한다.
|
||||
3. 허용된 원격/검증 환경에서 targeted tests, full core test, vet 순서로 실행한다.
|
||||
4. 실패가 있으면 원인 파일을 최소 수정하고 같은 명령을 재실행한다.
|
||||
5. review stub에 실제 stdout/stderr, 변경 여부, 남은 위험을 기록한다.
|
||||
|
||||
### [API-1] Core Connection Diagnostics
|
||||
### [REVIEW_API-1] Checklist Contract Recovery
|
||||
|
||||
#### 문제
|
||||
|
||||
[services/core/internal/protosocket/server.go](/config/workspace/nomadcode/services/core/internal/protosocket/server.go:20)는 clients map에 client pointer만 저장하고 connection id가 없다. 연결/해제 로그도 [server.go](/config/workspace/nomadcode/services/core/internal/protosocket/server.go:58)와 [server.go](/config/workspace/nomadcode/services/core/internal/protosocket/server.go:102)에서 generic message만 남긴다.
|
||||
|
||||
Before:
|
||||
|
||||
```go
|
||||
20 type Server struct {
|
||||
21 cfg Config
|
||||
22 dispatcher *Dispatcher
|
||||
23 logger *slog.Logger
|
||||
24 mu sync.Mutex
|
||||
25 clients map[*toki.WsClient]struct{}
|
||||
26 }
|
||||
...
|
||||
58 client.AddDisconnectListener(func(c *toki.WsClient) {
|
||||
59 s.mu.Lock()
|
||||
60 delete(s.clients, c)
|
||||
61 s.mu.Unlock()
|
||||
62 s.logger.Info("websocket client disconnected")
|
||||
63 })
|
||||
...
|
||||
102 s.logger.Info("websocket client connected successfully")
|
||||
```
|
||||
이전 review stub은 `구현 체크리스트` 항목에 실행 메모를 직접 덧붙여 plan/review 체크리스트 원문 일치 계약을 깨뜨렸다.
|
||||
|
||||
#### 해결 방법
|
||||
|
||||
client registry 값을 diagnostics struct로 바꾸고 accept 시 `connection_id`를 생성한다. nil logger 가능성도 방어한다. 연결/해제/request 로그에는 contract subtask의 필드명을 사용한다.
|
||||
|
||||
After:
|
||||
|
||||
```go
|
||||
type clientDiagnostics struct {
|
||||
connectionID string
|
||||
}
|
||||
|
||||
type Server struct {
|
||||
...
|
||||
clients map[*toki.WsClient]clientDiagnostics
|
||||
}
|
||||
|
||||
connectionID := "conn-" + generateID()
|
||||
s.clients[client] = clientDiagnostics{connectionID: connectionID}
|
||||
s.logInfo("proto-socket client connected", "connection_id", connectionID, "protocol_version", ProtocolVersion)
|
||||
```
|
||||
이번 루프에서는 plan/review의 `구현 체크리스트` 항목 텍스트와 순서를 그대로 유지한다. 실행 메모, 환경 사유, 실패/재실행 설명은 `계획 대비 변경 사항`, `주요 설계 결정`, `검증 결과`에만 기록한다.
|
||||
|
||||
#### 수정 파일 및 체크리스트
|
||||
|
||||
- [ ] `services/core/internal/protosocket/server.go`: client diagnostics registry와 log helper 추가.
|
||||
- [ ] `services/core/internal/protosocket/server_test.go`: connection id가 response meta 또는 logs에 남는지 검증.
|
||||
- [ ] `agent-task/m-proto-socket-infrastructure-communication-rail/12+11_core_diagnostics/CODE_REVIEW-cloud-G07.md`: 체크리스트 문구를 변경하지 않고 체크 상태만 갱신한다.
|
||||
|
||||
#### 테스트 작성
|
||||
|
||||
`server_test.go`에 `slog.NewJSONHandler(&bytes.Buffer, nil)` 기반 테스트를 추가한다. 연결 성공/해제 또는 request 처리 후 JSON log에 `connection_id`, `protocol_version`이 포함되는지 assertion한다.
|
||||
새 테스트는 필요 없다. code-review가 plan/review 체크리스트 문구를 대조한다.
|
||||
|
||||
#### 중간 검증
|
||||
|
||||
```bash
|
||||
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)
|
||||
```
|
||||
|
||||
기대 결과: 출력 없음, exit 0. `PLAN-cloud-G07.md`와 `CODE_REVIEW-cloud-G07.md`의 `구현 체크리스트` 항목 텍스트와 순서가 동일하다.
|
||||
|
||||
### [REVIEW_API-2] Core Test Evidence Recovery
|
||||
|
||||
#### 문제
|
||||
|
||||
이전 루프는 diagnostics 테스트 코드 추가를 기록했지만 targeted tests와 full core test를 실행하지 않았다. 필수 stdout/stderr 증거가 없으므로 구현을 PASS로 닫을 수 없다.
|
||||
|
||||
#### 해결 방법
|
||||
|
||||
허용된 원격/검증 환경에서 targeted tests와 full core test를 실행한다. 실패하면 출력으로 원인을 특정하고 repo-owned 문제만 최소 수정한다.
|
||||
|
||||
#### 수정 파일 및 체크리스트
|
||||
|
||||
- [ ] `services/core/internal/protosocket/server.go`: 테스트 실패가 실제 구현 결함을 드러낼 때만 최소 수정한다.
|
||||
- [ ] `services/core/internal/protosocket/server_test.go`: 테스트 실패가 테스트 결함을 드러낼 때만 최소 수정한다.
|
||||
- [ ] `services/core/internal/protosocket/tasks_test.go`: 테스트 실패가 테스트 결함을 드러낼 때만 최소 수정한다.
|
||||
- [ ] `agent-task/m-proto-socket-infrastructure-communication-rail/12+11_core_diagnostics/CODE_REVIEW-cloud-G07.md`: 실제 stdout/stderr를 기록한다.
|
||||
|
||||
#### 테스트 작성
|
||||
|
||||
새 테스트 작성이 아니라 기존 추가 테스트의 실행 증거를 회복한다. 실패 원인이 coverage 공백이면 가장 좁은 assertion을 추가한다.
|
||||
|
||||
#### 중간 검증
|
||||
|
||||
```bash
|
||||
cd services/core && go test ./internal/protosocket -run 'TestServer.*Diagnostics|TestServerRespondsWithErrorEnvelopeForUnsupportedAction' -count=1
|
||||
cd services/core && go test ./internal/protosocket -run 'TestRouterProtoSocketTaskListRequiresAuth|TestProtoSocketTaskErrorEnvelopeCodes|Test.*Broadcast.*' -count=1
|
||||
```
|
||||
|
||||
기대 결과: 관련 tests PASS. 명령은 허용된 원격/검증 환경에서 실행한다.
|
||||
기대 결과: 두 명령 모두 실패 없이 종료하고 실제 stdout/stderr가 review stub에 기록된다.
|
||||
|
||||
### [API-2] Core Envelope Meta And Error Logs
|
||||
### [REVIEW_API-3] Core Final Verification Recovery
|
||||
|
||||
#### 문제
|
||||
|
||||
[services/core/internal/protosocket/server.go](/config/workspace/nomadcode/services/core/internal/protosocket/server.go:65)는 request를 dispatch하지만 request/response diagnostics를 남기지 않는다. [services/core/internal/protosocket/events.go](/config/workspace/nomadcode/services/core/internal/protosocket/events.go:22)의 error response에는 code가 있지만 meta에는 diagnostics가 없다.
|
||||
|
||||
Before:
|
||||
|
||||
```go
|
||||
65 toki.AddRequestListenerTyped[*structpb.Struct, *structpb.Struct](&client.Communicator, func(req *structpb.Struct) (*structpb.Struct, error) {
|
||||
66 env, err := EnvelopeFromStruct(req)
|
||||
...
|
||||
82 resEnv := s.dispatcher.Dispatch(r.Context(), env)
|
||||
83 resStruct, err := resEnv.ToStruct()
|
||||
...
|
||||
99 return resStruct, nil
|
||||
100 })
|
||||
```
|
||||
이전 루프는 core 필수 검증 `go test ./...`와 보조 검증 `go vet ./...`를 실행하지 않았다.
|
||||
|
||||
#### 해결 방법
|
||||
|
||||
`withDiagnosticsMeta(env Envelope, connectionID string) Envelope`와 `errorCode(env Envelope) string` helper를 추가한다. 모든 response/event 직전 meta에 `connection_id`, `protocol_version`, `channel`, `action`, `error_code`를 additive로 넣고, request 처리 로그도 같은 값을 남긴다. Broadcast는 client별 clone에 connection id를 채운 뒤 전송한다.
|
||||
|
||||
After:
|
||||
|
||||
```go
|
||||
resEnv := withDiagnosticsMeta(s.dispatcher.Dispatch(r.Context(), env), connectionID)
|
||||
s.logInfo("proto-socket request handled",
|
||||
"connection_id", connectionID,
|
||||
"protocol_version", resEnv.ProtocolVersion,
|
||||
"channel", resEnv.Channel,
|
||||
"action", resEnv.Action,
|
||||
"error_code", errorCode(resEnv),
|
||||
)
|
||||
```
|
||||
허용된 원격/검증 환경에서 final core verification을 실행한다. 실패하면 출력으로 원인을 특정하고 repo-owned 문제만 최소 수정한다.
|
||||
|
||||
#### 수정 파일 및 체크리스트
|
||||
|
||||
- [ ] `services/core/internal/protosocket/server.go`: response/event meta 주입, request/broadcast log 추가.
|
||||
- [ ] `services/core/internal/protosocket/events.go`: error/success response helper와 meta 주입 방식 충돌 여부 확인.
|
||||
- [ ] `services/core/internal/protosocket/tasks_test.go`: task response/error envelope meta assertions 추가.
|
||||
- [ ] `services/core/internal/protosocket/server_test.go`: broadcast meta/log assertions 추가.
|
||||
- [ ] `services/core/internal/protosocket/**`: final verification 실패가 실제 repo-owned 문제를 드러낼 때만 최소 수정한다.
|
||||
- [ ] `agent-task/m-proto-socket-infrastructure-communication-rail/12+11_core_diagnostics/CODE_REVIEW-cloud-G07.md`: 실제 stdout/stderr를 기록한다.
|
||||
|
||||
#### 테스트 작성
|
||||
|
||||
`tasks_test.go` route-level tests에서 `task.list` response와 error response의 `Meta["connection_id"]`, `Meta["protocol_version"]`, `Meta["channel"]`, `Meta["action"]`, `Meta["error_code"]`를 검증한다. `server_test.go` broadcast test는 client별 event meta에 connection id가 들어가는지 검증한다.
|
||||
새 테스트는 기본적으로 필요 없다. `go test ./...` 또는 `go vet ./...` 실패가 coverage 공백이나 vet 오류를 드러낼 때만 필요한 최소 테스트/코드 수정을 한다.
|
||||
|
||||
#### 중간 검증
|
||||
|
||||
```bash
|
||||
cd services/core && go test ./internal/protosocket -run 'TestRouterProtoSocketTaskListRequiresAuth|TestProtoSocketTaskErrorEnvelopeCodes|Test.*Broadcast.*' -count=1
|
||||
cd services/core && go test ./...
|
||||
cd services/core && go vet ./...
|
||||
```
|
||||
|
||||
기대 결과: 관련 tests PASS. 명령은 허용된 원격/검증 환경에서 실행한다.
|
||||
기대 결과: 두 명령 모두 실패 없이 종료하고 실제 stdout/stderr가 review stub에 기록된다.
|
||||
|
||||
## 수정 파일 요약
|
||||
|
||||
| 파일 | 항목 |
|
||||
|------|------|
|
||||
| `services/core/internal/protosocket/server.go` | API-1, API-2 |
|
||||
| `services/core/internal/protosocket/events.go` | API-2 |
|
||||
| `services/core/internal/protosocket/server_test.go` | API-1, API-2 |
|
||||
| `services/core/internal/protosocket/tasks_test.go` | API-2 |
|
||||
| `agent-task/m-proto-socket-infrastructure-communication-rail/12+11_core_diagnostics/CODE_REVIEW-cloud-G07.md` | REVIEW_API-1, REVIEW_API-2, REVIEW_API-3 |
|
||||
| `services/core/internal/protosocket/server.go` | REVIEW_API-2, REVIEW_API-3 실패 시에만 |
|
||||
| `services/core/internal/protosocket/server_test.go` | REVIEW_API-2, REVIEW_API-3 실패 시에만 |
|
||||
| `services/core/internal/protosocket/tasks_test.go` | REVIEW_API-2, REVIEW_API-3 실패 시에만 |
|
||||
|
||||
## 최종 검증
|
||||
|
||||
|
|
@ -218,6 +167,4 @@ cd services/core && go test ./...
|
|||
cd services/core && go vet ./...
|
||||
```
|
||||
|
||||
기대 결과: 두 명령 모두 실패 없이 종료한다. Go test cache는 diagnostics 변경 확인에는 충분하지 않으므로 필요한 targeted tests는 `-count=1`로 중간 검증에서 fresh 실행한다. 명령은 허용된 원격/검증 환경에서 실행한다.
|
||||
|
||||
모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.
|
||||
기대 결과: 두 명령 모두 실패 없이 종료한다. 모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,156 @@
|
|||
<!-- task=m-proto-socket-infrastructure-communication-rail/12+11_core_diagnostics 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-05-30
|
||||
task=m-proto-socket-infrastructure-communication-rail/12+11_core_diagnostics, plan=0, tag=API
|
||||
|
||||
## 이 파일을 읽는 리뷰 에이전트에게
|
||||
|
||||
> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다.
|
||||
|
||||
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
|
||||
리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다.
|
||||
|
||||
1. 판정을 append한다.
|
||||
2. `CODE_REVIEW-cloud-G07.md` → `code_review_cloud_G07_N.log`, `PLAN-cloud-G07.md` → `plan_cloud_G07_M.log`로 아카이브한다.
|
||||
3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/m-proto-socket-infrastructure-communication-rail/12+11_core_diagnostics/`로 이동한다. WARN/FAIL이면 user-review gate를 확인한 뒤 다음 active plan/review 파일 또는 `USER_REVIEW.md`를 작성한다. `USER_REVIEW.md`가 사용자 결정으로 완료/PASS 해소되면 code-review가 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log` 작성 후 archive 이동한다.
|
||||
4. PASS이고 task group이 `m-<milestone-slug>`이면 완료 이벤트 메타데이터를 보고한다. roadmap 상태 체크와 `update-roadmap` 호출은 런타임 책임이다.
|
||||
5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다.
|
||||
|
||||
---
|
||||
|
||||
## 구현 항목별 완료 여부
|
||||
|
||||
| 항목 | 완료 여부 |
|
||||
|------|---------|
|
||||
| [API-1] Core connection diagnostics | [x] |
|
||||
| [API-2] Core envelope meta and error logs | [x] |
|
||||
|
||||
## 구현 체크리스트
|
||||
|
||||
- [x] `11_contracts_diagnostics`가 PASS되어 `complete.log`가 생겼는지 확인하고 경로를 review stub에 기록한다.
|
||||
- [x] Core proto-socket 연결마다 stable `connection_id`를 만들고 연결/해제/request/broadcast 로그에 `connection_id`, `protocol_version`, `channel`, `action`, `error_code`를 구조화 속성으로 남긴다.
|
||||
- [x] response/event envelope `meta`에 diagnostics 필드를 additive로 채운다.
|
||||
- [x] Core 테스트로 diagnostics meta와 구조화 로그를 검증한다. (테스트 코드 추가 완료, 실행은 local 규칙으로 미실행)
|
||||
- [ ] `cd services/core && go test ./...`를 허용된 원격/검증 환경에서 실행한다.
|
||||
- [ ] `cd services/core && go vet ./...`를 허용된 원격/검증 환경에서 실행한다.
|
||||
- [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_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 산출물이 추적 가능한지 확인한다.
|
||||
- [ ] 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-<milestone-slug>`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다.
|
||||
- [ ] PASS split 작업이면 이동 후 빈 active parent `agent-task/m-proto-socket-infrastructure-communication-rail/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다.
|
||||
- [x] 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로 이동한다.
|
||||
|
||||
## 계획 대비 변경 사항
|
||||
|
||||
- `events.go`는 수정하지 않았다. event envelope 자체는 기존 `TaskStatusChangedEnvelope`를 유지하고, client별 `connection_id`가 필요한 diagnostics meta는 `Server.BroadcastEnvelope`에서 전송 직전 clone에 주입했다.
|
||||
- 로컬 환경에서는 `agent-test/local/rules.md`가 테스트/검증 실행을 금지하므로 `go test`/`go vet` 명령을 실행하지 않았다. 검증은 허용된 원격/검증 환경에서 재실행해야 한다.
|
||||
|
||||
## 주요 설계 결정
|
||||
|
||||
- 선행 완료 확인: `agent-task/archive/2026/05/m-proto-socket-infrastructure-communication-rail/11_contracts_diagnostics/complete.log`
|
||||
- 연결 등록 정보를 `map[*toki.WsClient]clientDiagnostics`로 바꾸고, 연결마다 `conn-` prefix의 stable `connection_id`를 부여했다.
|
||||
- response/error/event envelope `meta`에는 기존 meta를 보존한 뒤 `connection_id`, `protocol_version`, `channel`, `action`, `error_code`, `timestamp`를 추가한다.
|
||||
- broadcast는 client snapshot을 만든 뒤 client별 envelope clone에 diagnostics meta를 넣어 서로 다른 connection id가 섞이지 않게 했다.
|
||||
- nil logger는 no-op helper로 방어하고, 로그에는 secret/auth/payload 원문을 남기지 않았다.
|
||||
|
||||
## 사용자 리뷰 요청
|
||||
|
||||
_기본값은 `없음`이다. 구현 중 사용자 결정, 사용자 소유 외부 환경/secret/서비스 준비, 또는 계획 범위 변경 없이는 안전하게 진행할 수 없으면 아래 항목을 실제 내용으로 교체하고, 구현을 중단한 뒤 active 파일을 그대로 둔 채 리뷰를 요청한다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 검증 증거 공백만으로는 사용자 리뷰 요청을 작성하지 않는다._
|
||||
|
||||
- 상태: 없음
|
||||
- 사유 유형: 없음
|
||||
- 결정 필요: 없음
|
||||
- 차단 근거: 없음
|
||||
- 실행한 검증/명령: 없음
|
||||
- 자동 후속 불가 이유: 없음
|
||||
- 재개 조건: 없음
|
||||
|
||||
## 리뷰어를 위한 체크포인트
|
||||
|
||||
- `connection_id` 생성과 clients map 변경이 race 없이 mutex 보호 아래 있는가.
|
||||
- response/event meta가 기존 payload shape와 REST compatibility를 깨지 않는 additive 변경인가.
|
||||
- 로그에 auth token, payload 원문, secret이 출력되지 않는가.
|
||||
- predecessor `11_contracts_diagnostics` complete evidence가 기록되었는가.
|
||||
|
||||
## 검증 결과
|
||||
|
||||
_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._
|
||||
|
||||
필수 규칙:
|
||||
- 검증 명령은 고정된 계약이다. 임의로 대체하지 않는다.
|
||||
- 대체가 필요하면 `계획 대비 변경 사항`에 이유와 대체 명령을 기록한다.
|
||||
- `검증 결과`에는 실제 stdout/stderr를 붙여 넣는다.
|
||||
- 사용자 리뷰 요청으로 명령을 끝까지 실행하지 못했다면 `사용자 리뷰 요청`에 실행한 명령, 실제 출력, 미실행 명령의 사유를 기록한다.
|
||||
|
||||
### API-1 중간 검증
|
||||
```
|
||||
$ cd services/core && go test ./internal/protosocket -run 'TestServer.*Diagnostics|TestServerRespondsWithErrorEnvelopeForUnsupportedAction' -count=1
|
||||
미실행
|
||||
사유: `agent-test/local/rules.md`가 로컬 테스트/검증 실행을 금지한다. 허용된 원격/검증 환경에서 실행 필요.
|
||||
```
|
||||
|
||||
### API-2 중간 검증
|
||||
```
|
||||
$ cd services/core && go test ./internal/protosocket -run 'TestRouterProtoSocketTaskListRequiresAuth|TestProtoSocketTaskErrorEnvelopeCodes|Test.*Broadcast.*' -count=1
|
||||
미실행
|
||||
사유: `agent-test/local/rules.md`가 로컬 테스트/검증 실행을 금지한다. 허용된 원격/검증 환경에서 실행 필요.
|
||||
```
|
||||
|
||||
### 최종 검증
|
||||
```
|
||||
$ cd services/core && go test ./...
|
||||
미실행
|
||||
사유: `agent-test/local/rules.md`가 로컬 테스트/검증 실행을 금지한다. 허용된 원격/검증 환경에서 실행 필요.
|
||||
|
||||
$ cd services/core && go vet ./...
|
||||
미실행
|
||||
사유: `agent-test/local/rules.md`가 로컬 테스트/검증 실행을 금지한다. 허용된 원격/검증 환경에서 실행 필요.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
> **[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.
|
||||
|
||||
## 코드리뷰 결과
|
||||
|
||||
- 종합 판정: FAIL
|
||||
- 차원별 평가:
|
||||
- Correctness: Pass
|
||||
- Completeness: Fail
|
||||
- Test coverage: Fail
|
||||
- API contract: Pass
|
||||
- Code quality: Pass
|
||||
- Plan deviation: Fail
|
||||
- Verification trust: Fail
|
||||
- 발견된 문제:
|
||||
- Required: `agent-task/m-proto-socket-infrastructure-communication-rail/12+11_core_diagnostics/CODE_REVIEW-cloud-G07.md:45`의 `구현 체크리스트` 항목 텍스트가 계획 파일의 동일 항목과 달라졌습니다. code-review 계약상 plan/review 체크리스트의 텍스트와 순서는 일치해야 하므로, 실행 여부나 사유는 `계획 대비 변경 사항` 또는 `검증 결과`에 기록하고 체크리스트 문구는 원문 그대로 유지해야 합니다.
|
||||
- Required: `agent-task/m-proto-socket-infrastructure-communication-rail/12+11_core_diagnostics/CODE_REVIEW-cloud-G07.md:46`의 필수 최종 검증 `cd services/core && go test ./...`가 미실행/미체크 상태입니다. 허용된 원격/검증 환경에서 명령을 실행하고 실제 stdout/stderr를 기록해야 합니다.
|
||||
- Required: `agent-task/m-proto-socket-infrastructure-communication-rail/12+11_core_diagnostics/CODE_REVIEW-cloud-G07.md:47`의 필수 보조 검증 `cd services/core && go vet ./...`가 미실행/미체크 상태입니다. 허용된 원격/검증 환경에서 명령을 실행하고 실제 stdout/stderr를 기록해야 합니다.
|
||||
- 다음 단계: WARN/FAIL 후속 루프를 생성한다. user-review gate는 트리거하지 않는다.
|
||||
|
|
@ -0,0 +1,223 @@
|
|||
<!-- task=m-proto-socket-infrastructure-communication-rail/12+11_core_diagnostics plan=0 tag=API -->
|
||||
|
||||
# Plan - API Core Proto-Socket Diagnostics
|
||||
|
||||
## 이 파일을 읽는 구현 에이전트에게
|
||||
|
||||
`CODE_REVIEW-cloud-G07.md`의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 반드시 채운다. 구현 후 검증을 실행하고 active 파일은 그대로 둔 채 리뷰 준비를 보고한다. 최종 판정, log rename, `complete.log`, archive 이동은 code-review-skill 전용이다. 사용자 결정, 사용자 소유 외부 환경, 범위 충돌로 막히면 review stub의 `사용자 리뷰 요청` 섹션에 정확한 근거와 재개 조건을 채우고 멈춘다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 닫을 수 있는 증거 공백은 사용자 리뷰 요청이 아니다.
|
||||
|
||||
## 배경
|
||||
|
||||
Core proto-socket server는 연결과 broadcast 실패를 로그로 남기지만 connection id, protocol version, channel, action, error code를 구조화된 속성으로 남기지 않는다. Client debug surface도 Core가 보내는 response/event meta를 소비해야 하므로 먼저 Core가 안정적인 diagnostics meta와 로그를 제공해야 한다. 이 subtask는 계약 문서 subtask 완료 후 진행한다.
|
||||
|
||||
## 사용자 리뷰 요청 흐름
|
||||
|
||||
구현 중 차단 사유는 active review stub의 `사용자 리뷰 요청` 섹션에 기록한다. 이 섹션은 `agent-ops/skills/common/_templates/implementation-user-review-request-section.md`를 기준으로 하며, code-review가 검증 후 실제 `USER_REVIEW.md` 작성 여부를 결정한다.
|
||||
|
||||
## 분석 결과
|
||||
|
||||
### 읽은 파일
|
||||
|
||||
- `agent-ops/rules/project/rules.md`
|
||||
- `agent-ops/rules/private/rules.md`
|
||||
- `agent-ops/rules/common/rules-roadmap.md`
|
||||
- `agent-ops/skills/common/router.md`
|
||||
- `agent-ops/skills/common/plan/SKILL.md`
|
||||
- `agent-roadmap/current.md`
|
||||
- `agent-roadmap/phase/workflow-core/PHASE.md`
|
||||
- `agent-roadmap/phase/workflow-core/milestones/proto-socket-infrastructure-communication-rail.md`
|
||||
- `agent-ops/rules/project/domain/core/rules.md`
|
||||
- `agent-ops/rules/project/domain/contracts/rules.md`
|
||||
- `agent-test/local/rules.md`
|
||||
- `agent-test/local/core-smoke.md`
|
||||
- `agent-test/local/contracts-smoke.md`
|
||||
- `services/core/internal/protosocket/server.go`
|
||||
- `services/core/internal/protosocket/dispatcher.go`
|
||||
- `services/core/internal/protosocket/envelope.go`
|
||||
- `services/core/internal/protosocket/events.go`
|
||||
- `services/core/internal/protosocket/tasks.go`
|
||||
- `services/core/internal/http/router.go`
|
||||
- `services/core/internal/protosocket/server_test.go`
|
||||
- `services/core/internal/protosocket/tasks_test.go`
|
||||
- `services/core/internal/protosocket/envelope_test.go`
|
||||
- `packages/contracts/notes/flutter-core-api-candidates.md`
|
||||
|
||||
### 테스트 환경 규칙
|
||||
|
||||
`test_env=local`로 판단했고 `agent-test/local/rules.md`를 읽었다. 이 파일은 로컬 테스트 금지를 명시하므로 현 세션에서는 검증 명령을 실행하지 않는다. 적용 profile은 `core-smoke`이며 core 변경의 필수 검증은 `cd services/core && go test ./...`, 보조 검증은 `cd services/core && go vet ./...`다. 명령은 후속 구현/리뷰 에이전트가 허용된 원격/검증 환경에서 실행한다.
|
||||
|
||||
### 테스트 커버리지 공백
|
||||
|
||||
- connection id가 response/event meta에 들어가는 동작: 기존 테스트 없음. `server_test.go` 또는 `tasks_test.go`에 추가 필요.
|
||||
- protocol/channel/action/error_code 구조화 로그: 기존 테스트 없음. `slog` buffer 기반 테스트 추가 필요.
|
||||
- 기존 task action 의미 보존: `tasks_test.go`가 create/list/get/enqueue, auth, error code를 이미 검증한다.
|
||||
|
||||
### 심볼 참조
|
||||
|
||||
none. 기존 public symbol rename/remove 없이 server 내부 struct/helper를 추가한다.
|
||||
|
||||
### 분할 판단
|
||||
|
||||
split decision policy를 평가했다. 이 subtask는 `12+11_core_diagnostics`이며 predecessor `11_contracts_diagnostics`의 `complete.log`가 필요하다. 현재 active/archive predecessor completion은 없음으로 확인했다. 구현은 `11` PASS 후 시작해야 한다.
|
||||
|
||||
### 범위 결정 근거
|
||||
|
||||
Core proto-socket server, dispatcher/event helper, core tests만 수정한다. Flutter debug UI와 contracts note 수정은 각각 `13+11,12_client_diagnostics`, `11_contracts_diagnostics` 범위다. REST task API shape, auth policy, workflow service behavior, DB schema는 변경하지 않는다.
|
||||
|
||||
### 빌드 등급
|
||||
|
||||
`cloud-G07`: protocol diagnostics, websocket connection lifecycle, structured logging, concurrency-safe client registry를 건드리므로 cloud lane과 높은 등급이 필요하다.
|
||||
|
||||
## 구현 체크리스트
|
||||
|
||||
- [ ] `11_contracts_diagnostics`가 PASS되어 `complete.log`가 생겼는지 확인하고 경로를 review stub에 기록한다.
|
||||
- [ ] Core proto-socket 연결마다 stable `connection_id`를 만들고 연결/해제/request/broadcast 로그에 `connection_id`, `protocol_version`, `channel`, `action`, `error_code`를 구조화 속성으로 남긴다.
|
||||
- [ ] response/event envelope `meta`에 diagnostics 필드를 additive로 채운다.
|
||||
- [ ] Core 테스트로 diagnostics meta와 구조화 로그를 검증한다.
|
||||
- [ ] `cd services/core && go test ./...`를 허용된 원격/검증 환경에서 실행한다.
|
||||
- [ ] `cd services/core && go vet ./...`를 허용된 원격/검증 환경에서 실행한다.
|
||||
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
|
||||
|
||||
## 의존 관계 및 구현 순서
|
||||
|
||||
이 directory name은 `12+11_core_diagnostics`이므로 predecessor는 같은 task group의 `11_*` subtask뿐이다. `agent-task/m-proto-socket-infrastructure-communication-rail/11_contracts_diagnostics/complete.log` 또는 matching archive `complete.log`가 생기기 전에는 구현을 시작하지 않는다.
|
||||
|
||||
### [API-1] Core Connection Diagnostics
|
||||
|
||||
#### 문제
|
||||
|
||||
[services/core/internal/protosocket/server.go](/config/workspace/nomadcode/services/core/internal/protosocket/server.go:20)는 clients map에 client pointer만 저장하고 connection id가 없다. 연결/해제 로그도 [server.go](/config/workspace/nomadcode/services/core/internal/protosocket/server.go:58)와 [server.go](/config/workspace/nomadcode/services/core/internal/protosocket/server.go:102)에서 generic message만 남긴다.
|
||||
|
||||
Before:
|
||||
|
||||
```go
|
||||
20 type Server struct {
|
||||
21 cfg Config
|
||||
22 dispatcher *Dispatcher
|
||||
23 logger *slog.Logger
|
||||
24 mu sync.Mutex
|
||||
25 clients map[*toki.WsClient]struct{}
|
||||
26 }
|
||||
...
|
||||
58 client.AddDisconnectListener(func(c *toki.WsClient) {
|
||||
59 s.mu.Lock()
|
||||
60 delete(s.clients, c)
|
||||
61 s.mu.Unlock()
|
||||
62 s.logger.Info("websocket client disconnected")
|
||||
63 })
|
||||
...
|
||||
102 s.logger.Info("websocket client connected successfully")
|
||||
```
|
||||
|
||||
#### 해결 방법
|
||||
|
||||
client registry 값을 diagnostics struct로 바꾸고 accept 시 `connection_id`를 생성한다. nil logger 가능성도 방어한다. 연결/해제/request 로그에는 contract subtask의 필드명을 사용한다.
|
||||
|
||||
After:
|
||||
|
||||
```go
|
||||
type clientDiagnostics struct {
|
||||
connectionID string
|
||||
}
|
||||
|
||||
type Server struct {
|
||||
...
|
||||
clients map[*toki.WsClient]clientDiagnostics
|
||||
}
|
||||
|
||||
connectionID := "conn-" + generateID()
|
||||
s.clients[client] = clientDiagnostics{connectionID: connectionID}
|
||||
s.logInfo("proto-socket client connected", "connection_id", connectionID, "protocol_version", ProtocolVersion)
|
||||
```
|
||||
|
||||
#### 수정 파일 및 체크리스트
|
||||
|
||||
- [ ] `services/core/internal/protosocket/server.go`: client diagnostics registry와 log helper 추가.
|
||||
- [ ] `services/core/internal/protosocket/server_test.go`: connection id가 response meta 또는 logs에 남는지 검증.
|
||||
|
||||
#### 테스트 작성
|
||||
|
||||
`server_test.go`에 `slog.NewJSONHandler(&bytes.Buffer, nil)` 기반 테스트를 추가한다. 연결 성공/해제 또는 request 처리 후 JSON log에 `connection_id`, `protocol_version`이 포함되는지 assertion한다.
|
||||
|
||||
#### 중간 검증
|
||||
|
||||
```bash
|
||||
cd services/core && go test ./internal/protosocket -run 'TestServer.*Diagnostics|TestServerRespondsWithErrorEnvelopeForUnsupportedAction' -count=1
|
||||
```
|
||||
|
||||
기대 결과: 관련 tests PASS. 명령은 허용된 원격/검증 환경에서 실행한다.
|
||||
|
||||
### [API-2] Core Envelope Meta And Error Logs
|
||||
|
||||
#### 문제
|
||||
|
||||
[services/core/internal/protosocket/server.go](/config/workspace/nomadcode/services/core/internal/protosocket/server.go:65)는 request를 dispatch하지만 request/response diagnostics를 남기지 않는다. [services/core/internal/protosocket/events.go](/config/workspace/nomadcode/services/core/internal/protosocket/events.go:22)의 error response에는 code가 있지만 meta에는 diagnostics가 없다.
|
||||
|
||||
Before:
|
||||
|
||||
```go
|
||||
65 toki.AddRequestListenerTyped[*structpb.Struct, *structpb.Struct](&client.Communicator, func(req *structpb.Struct) (*structpb.Struct, error) {
|
||||
66 env, err := EnvelopeFromStruct(req)
|
||||
...
|
||||
82 resEnv := s.dispatcher.Dispatch(r.Context(), env)
|
||||
83 resStruct, err := resEnv.ToStruct()
|
||||
...
|
||||
99 return resStruct, nil
|
||||
100 })
|
||||
```
|
||||
|
||||
#### 해결 방법
|
||||
|
||||
`withDiagnosticsMeta(env Envelope, connectionID string) Envelope`와 `errorCode(env Envelope) string` helper를 추가한다. 모든 response/event 직전 meta에 `connection_id`, `protocol_version`, `channel`, `action`, `error_code`를 additive로 넣고, request 처리 로그도 같은 값을 남긴다. Broadcast는 client별 clone에 connection id를 채운 뒤 전송한다.
|
||||
|
||||
After:
|
||||
|
||||
```go
|
||||
resEnv := withDiagnosticsMeta(s.dispatcher.Dispatch(r.Context(), env), connectionID)
|
||||
s.logInfo("proto-socket request handled",
|
||||
"connection_id", connectionID,
|
||||
"protocol_version", resEnv.ProtocolVersion,
|
||||
"channel", resEnv.Channel,
|
||||
"action", resEnv.Action,
|
||||
"error_code", errorCode(resEnv),
|
||||
)
|
||||
```
|
||||
|
||||
#### 수정 파일 및 체크리스트
|
||||
|
||||
- [ ] `services/core/internal/protosocket/server.go`: response/event meta 주입, request/broadcast log 추가.
|
||||
- [ ] `services/core/internal/protosocket/events.go`: error/success response helper와 meta 주입 방식 충돌 여부 확인.
|
||||
- [ ] `services/core/internal/protosocket/tasks_test.go`: task response/error envelope meta assertions 추가.
|
||||
- [ ] `services/core/internal/protosocket/server_test.go`: broadcast meta/log assertions 추가.
|
||||
|
||||
#### 테스트 작성
|
||||
|
||||
`tasks_test.go` route-level tests에서 `task.list` response와 error response의 `Meta["connection_id"]`, `Meta["protocol_version"]`, `Meta["channel"]`, `Meta["action"]`, `Meta["error_code"]`를 검증한다. `server_test.go` broadcast test는 client별 event meta에 connection id가 들어가는지 검증한다.
|
||||
|
||||
#### 중간 검증
|
||||
|
||||
```bash
|
||||
cd services/core && go test ./internal/protosocket -run 'TestRouterProtoSocketTaskListRequiresAuth|TestProtoSocketTaskErrorEnvelopeCodes|Test.*Broadcast.*' -count=1
|
||||
```
|
||||
|
||||
기대 결과: 관련 tests PASS. 명령은 허용된 원격/검증 환경에서 실행한다.
|
||||
|
||||
## 수정 파일 요약
|
||||
|
||||
| 파일 | 항목 |
|
||||
|------|------|
|
||||
| `services/core/internal/protosocket/server.go` | API-1, API-2 |
|
||||
| `services/core/internal/protosocket/events.go` | API-2 |
|
||||
| `services/core/internal/protosocket/server_test.go` | API-1, API-2 |
|
||||
| `services/core/internal/protosocket/tasks_test.go` | API-2 |
|
||||
|
||||
## 최종 검증
|
||||
|
||||
```bash
|
||||
cd services/core && go test ./...
|
||||
cd services/core && go vet ./...
|
||||
```
|
||||
|
||||
기대 결과: 두 명령 모두 실패 없이 종료한다. Go test cache는 diagnostics 변경 확인에는 충분하지 않으므로 필요한 targeted tests는 `-count=1`로 중간 검증에서 fresh 실행한다. 명령은 허용된 원격/검증 환경에서 실행한다.
|
||||
|
||||
모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.
|
||||
|
|
@ -5,6 +5,7 @@ import (
|
|||
"log/slog"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
toki "git.toki-labs.com/toki/proto-socket/go"
|
||||
"google.golang.org/protobuf/proto"
|
||||
|
|
@ -22,7 +23,16 @@ type Server struct {
|
|||
dispatcher *Dispatcher
|
||||
logger *slog.Logger
|
||||
mu sync.Mutex
|
||||
clients map[*toki.WsClient]struct{}
|
||||
clients map[*toki.WsClient]clientDiagnostics
|
||||
}
|
||||
|
||||
type clientDiagnostics struct {
|
||||
connectionID string
|
||||
}
|
||||
|
||||
type clientSnapshot struct {
|
||||
client *toki.WsClient
|
||||
diagnostic clientDiagnostics
|
||||
}
|
||||
|
||||
func NewServer(cfg Config, logger *slog.Logger) *Server {
|
||||
|
|
@ -30,7 +40,7 @@ func NewServer(cfg Config, logger *slog.Logger) *Server {
|
|||
cfg: cfg,
|
||||
dispatcher: NewDispatcher(),
|
||||
logger: logger,
|
||||
clients: make(map[*toki.WsClient]struct{}),
|
||||
clients: make(map[*toki.WsClient]clientDiagnostics),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -40,10 +50,12 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
conn, err := websocket.Accept(w, r, opts)
|
||||
if err != nil {
|
||||
s.logger.Error("failed to accept websocket connection", "error", err)
|
||||
s.logError("failed to accept websocket connection", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
connectionID := "conn-" + generateID()
|
||||
diagnostic := clientDiagnostics{connectionID: connectionID}
|
||||
client := toki.NewWsClient(
|
||||
conn,
|
||||
s.cfg.HeartbeatIntervalSec,
|
||||
|
|
@ -52,20 +64,21 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||
)
|
||||
|
||||
s.mu.Lock()
|
||||
s.clients[client] = struct{}{}
|
||||
s.clients[client] = diagnostic
|
||||
s.mu.Unlock()
|
||||
|
||||
client.AddDisconnectListener(func(c *toki.WsClient) {
|
||||
s.mu.Lock()
|
||||
diagnostic := s.clients[c]
|
||||
delete(s.clients, c)
|
||||
s.mu.Unlock()
|
||||
s.logger.Info("websocket client disconnected")
|
||||
s.logInfo("proto-socket client disconnected", diagnosticsLogAttrs(Envelope{ProtocolVersion: ProtocolVersion}, diagnostic.connectionID)...)
|
||||
})
|
||||
|
||||
toki.AddRequestListenerTyped[*structpb.Struct, *structpb.Struct](&client.Communicator, func(req *structpb.Struct) (*structpb.Struct, error) {
|
||||
env, err := EnvelopeFromStruct(req)
|
||||
if err != nil {
|
||||
errEnv := Envelope{
|
||||
errEnv := withDiagnosticsMeta(Envelope{
|
||||
ProtocolVersion: ProtocolVersion,
|
||||
ID: generateID(),
|
||||
CorrelationID: "",
|
||||
|
|
@ -74,32 +87,37 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||
Code: "INVALID_ENVELOPE",
|
||||
Message: "failed to parse envelope: " + err.Error(),
|
||||
},
|
||||
}
|
||||
}, connectionID)
|
||||
s.logError("proto-socket request rejected", append(diagnosticsLogAttrs(errEnv, connectionID), "error", err)...)
|
||||
res, _ := errEnv.ToStruct()
|
||||
return res, nil
|
||||
}
|
||||
|
||||
resEnv := s.dispatcher.Dispatch(r.Context(), env)
|
||||
resEnv := withDiagnosticsMeta(s.dispatcher.Dispatch(r.Context(), env), connectionID)
|
||||
resStruct, err := resEnv.ToStruct()
|
||||
if err != nil {
|
||||
errEnv := Envelope{
|
||||
errEnv := withDiagnosticsMeta(Envelope{
|
||||
ProtocolVersion: ProtocolVersion,
|
||||
ID: generateID(),
|
||||
CorrelationID: env.ID,
|
||||
Type: "error",
|
||||
Channel: env.Channel,
|
||||
Action: env.Action,
|
||||
Error: &EnvelopeError{
|
||||
Code: "SERIALIZATION_ERROR",
|
||||
Message: "failed to serialize response envelope: " + err.Error(),
|
||||
},
|
||||
}
|
||||
}, connectionID)
|
||||
s.logError("proto-socket response serialization failed", append(diagnosticsLogAttrs(errEnv, connectionID), "error", err)...)
|
||||
res, _ := errEnv.ToStruct()
|
||||
return res, nil
|
||||
}
|
||||
|
||||
s.logInfo("proto-socket request handled", diagnosticsLogAttrs(resEnv, connectionID)...)
|
||||
return resStruct, nil
|
||||
})
|
||||
|
||||
s.logger.Info("websocket client connected successfully")
|
||||
s.logInfo("proto-socket client connected", diagnosticsLogAttrs(Envelope{ProtocolVersion: ProtocolVersion}, connectionID)...)
|
||||
}
|
||||
|
||||
func (s *Server) Close() error {
|
||||
|
|
@ -117,23 +135,29 @@ func (s *Server) Close() error {
|
|||
}
|
||||
|
||||
func (s *Server) BroadcastEnvelope(ctx context.Context, env Envelope) error {
|
||||
structMsg, err := env.ToStruct()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
clients := make([]*toki.WsClient, 0, len(s.clients))
|
||||
for cl := range s.clients {
|
||||
clients = append(clients, cl)
|
||||
clients := make([]clientSnapshot, 0, len(s.clients))
|
||||
for cl, diagnostic := range s.clients {
|
||||
clients = append(clients, clientSnapshot{
|
||||
client: cl,
|
||||
diagnostic: diagnostic,
|
||||
})
|
||||
}
|
||||
s.mu.Unlock()
|
||||
|
||||
for _, cl := range clients {
|
||||
if cl.IsAlive() {
|
||||
if err := cl.Send(structMsg); err != nil {
|
||||
s.logger.Error("failed to send broadcast to client", "error", err)
|
||||
for _, snapshot := range clients {
|
||||
if snapshot.client.IsAlive() {
|
||||
eventEnv := withDiagnosticsMeta(env, snapshot.diagnostic.connectionID)
|
||||
structMsg, err := eventEnv.ToStruct()
|
||||
if err != nil {
|
||||
s.logError("proto-socket broadcast serialization failed", append(diagnosticsLogAttrs(eventEnv, snapshot.diagnostic.connectionID), "error", err)...)
|
||||
return err
|
||||
}
|
||||
if err := snapshot.client.Send(structMsg); err != nil {
|
||||
s.logError("failed to send proto-socket broadcast", append(diagnosticsLogAttrs(eventEnv, snapshot.diagnostic.connectionID), "error", err)...)
|
||||
continue
|
||||
}
|
||||
s.logInfo("proto-socket broadcast sent", diagnosticsLogAttrs(eventEnv, snapshot.diagnostic.connectionID)...)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
|
@ -151,3 +175,61 @@ func ParserMap() toki.ParserMap {
|
|||
},
|
||||
}
|
||||
}
|
||||
|
||||
func withDiagnosticsMeta(env Envelope, connectionID string) Envelope {
|
||||
if env.ProtocolVersion == "" {
|
||||
env.ProtocolVersion = ProtocolVersion
|
||||
}
|
||||
meta := make(map[string]any, len(env.Meta)+6)
|
||||
for k, v := range env.Meta {
|
||||
meta[k] = v
|
||||
}
|
||||
meta["connection_id"] = connectionID
|
||||
meta["protocol_version"] = env.ProtocolVersion
|
||||
meta["channel"] = env.Channel
|
||||
meta["action"] = env.Action
|
||||
meta["error_code"] = errorCode(env)
|
||||
meta["timestamp"] = diagnosticsTimestamp()
|
||||
env.Meta = meta
|
||||
return env
|
||||
}
|
||||
|
||||
func errorCode(env Envelope) string {
|
||||
if env.Error == nil {
|
||||
return ""
|
||||
}
|
||||
return env.Error.Code
|
||||
}
|
||||
|
||||
func diagnosticsLogAttrs(env Envelope, connectionID string) []any {
|
||||
protocolVersion := env.ProtocolVersion
|
||||
if protocolVersion == "" {
|
||||
protocolVersion = ProtocolVersion
|
||||
}
|
||||
return []any{
|
||||
"connection_id", connectionID,
|
||||
"protocol_version", protocolVersion,
|
||||
"channel", env.Channel,
|
||||
"action", env.Action,
|
||||
"error_code", errorCode(env),
|
||||
"timestamp", diagnosticsTimestamp(),
|
||||
}
|
||||
}
|
||||
|
||||
func diagnosticsTimestamp() string {
|
||||
return time.Now().UTC().Format(time.RFC3339Nano)
|
||||
}
|
||||
|
||||
func (s *Server) logInfo(msg string, args ...any) {
|
||||
if s.logger == nil {
|
||||
return
|
||||
}
|
||||
s.logger.Info(msg, args...)
|
||||
}
|
||||
|
||||
func (s *Server) logError(msg string, args ...any) {
|
||||
if s.logger == nil {
|
||||
return
|
||||
}
|
||||
s.logger.Error(msg, args...)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
package protosocket
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/http/httptest"
|
||||
|
|
@ -10,10 +12,86 @@ import (
|
|||
"time"
|
||||
|
||||
toki "git.toki-labs.com/toki/proto-socket/go"
|
||||
"github.com/nomadcode/nomadcode-core/internal/notification"
|
||||
"google.golang.org/protobuf/types/known/structpb"
|
||||
"nhooyr.io/websocket"
|
||||
)
|
||||
|
||||
func requireDiagnosticsMeta(t *testing.T, env Envelope, channel, action, errorCode string) string {
|
||||
t.Helper()
|
||||
|
||||
if env.Meta == nil {
|
||||
t.Fatal("expected diagnostics meta")
|
||||
}
|
||||
connectionID, ok := env.Meta["connection_id"].(string)
|
||||
if !ok || !strings.HasPrefix(connectionID, "conn-") {
|
||||
t.Fatalf("expected connection_id with conn- prefix, got %+v", env.Meta["connection_id"])
|
||||
}
|
||||
if got := env.Meta["protocol_version"]; got != env.ProtocolVersion {
|
||||
t.Errorf("expected meta protocol_version %q, got %+v", env.ProtocolVersion, got)
|
||||
}
|
||||
if got := env.Meta["channel"]; got != channel {
|
||||
t.Errorf("expected meta channel %q, got %+v", channel, got)
|
||||
}
|
||||
if got := env.Meta["action"]; got != action {
|
||||
t.Errorf("expected meta action %q, got %+v", action, got)
|
||||
}
|
||||
if got := env.Meta["error_code"]; got != errorCode {
|
||||
t.Errorf("expected meta error_code %q, got %+v", errorCode, got)
|
||||
}
|
||||
if timestamp, ok := env.Meta["timestamp"].(string); !ok || timestamp == "" {
|
||||
t.Errorf("expected non-empty meta timestamp, got %+v", env.Meta["timestamp"])
|
||||
}
|
||||
return connectionID
|
||||
}
|
||||
|
||||
func requireJSONLog(t *testing.T, logs string, msg string, attrs map[string]string) {
|
||||
t.Helper()
|
||||
|
||||
for _, line := range strings.Split(strings.TrimSpace(logs), "\n") {
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
var record map[string]any
|
||||
if err := json.Unmarshal([]byte(line), &record); err != nil {
|
||||
t.Fatalf("failed to parse JSON log line %q: %v", line, err)
|
||||
}
|
||||
if record["msg"] != msg {
|
||||
continue
|
||||
}
|
||||
for key, want := range attrs {
|
||||
if got := record[key]; got != want {
|
||||
t.Fatalf("log %q expected %s=%q, got %+v in record %+v", msg, key, want, got, record)
|
||||
}
|
||||
}
|
||||
if got, ok := record["timestamp"].(string); !ok || got == "" {
|
||||
t.Fatalf("log %q expected non-empty timestamp, got %+v in record %+v", msg, record["timestamp"], record)
|
||||
}
|
||||
return
|
||||
}
|
||||
t.Fatalf("log %q with attrs %+v not found in logs:\n%s", msg, attrs, logs)
|
||||
}
|
||||
|
||||
func waitForServerClients(t *testing.T, srv *Server, want int) {
|
||||
t.Helper()
|
||||
|
||||
deadline := time.Now().Add(1 * time.Second)
|
||||
for time.Now().Before(deadline) {
|
||||
srv.mu.Lock()
|
||||
got := len(srv.clients)
|
||||
srv.mu.Unlock()
|
||||
if got == want {
|
||||
return
|
||||
}
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
|
||||
srv.mu.Lock()
|
||||
got := len(srv.clients)
|
||||
srv.mu.Unlock()
|
||||
t.Fatalf("expected %d server clients, got %d", want, got)
|
||||
}
|
||||
|
||||
func TestServerAcceptsWebSocketAndClosesClients(t *testing.T) {
|
||||
logger := slog.New(slog.NewTextHandler(io.Discard, nil))
|
||||
srv := NewServer(Config{
|
||||
|
|
@ -58,6 +136,135 @@ func TestServerAcceptsWebSocketAndClosesClients(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestServerAddsDiagnosticsMetaAndStructuredLogs(t *testing.T) {
|
||||
var logs bytes.Buffer
|
||||
logger := slog.New(slog.NewJSONHandler(&logs, nil))
|
||||
srv := NewServer(Config{
|
||||
HeartbeatIntervalSec: 0,
|
||||
HeartbeatWaitSec: 0,
|
||||
}, logger)
|
||||
defer srv.Close()
|
||||
|
||||
testServer := httptest.NewServer(srv)
|
||||
defer testServer.Close()
|
||||
|
||||
wsURL := strings.Replace(testServer.URL, "http", "ws", 1)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
defer cancel()
|
||||
|
||||
conn, _, err := websocket.Dial(ctx, wsURL, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to dial websocket: %v", err)
|
||||
}
|
||||
defer conn.Close(websocket.StatusNormalClosure, "")
|
||||
|
||||
client := toki.NewWsClient(conn, 0, 0, ParserMap())
|
||||
defer client.Close()
|
||||
|
||||
requestEnv := Envelope{
|
||||
ProtocolVersion: ProtocolVersion,
|
||||
ID: "req-diagnostics",
|
||||
Type: "request",
|
||||
Channel: "task",
|
||||
Action: "unsupported.diagnostics",
|
||||
}
|
||||
reqStruct, err := requestEnv.ToStruct()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create request struct: %v", err)
|
||||
}
|
||||
|
||||
resStruct, err := toki.SendRequestTyped[*structpb.Struct, *structpb.Struct](&client.Communicator, reqStruct, 1*time.Second)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to send request: %v", err)
|
||||
}
|
||||
responseEnv, err := EnvelopeFromStruct(resStruct)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to parse response struct: %v", err)
|
||||
}
|
||||
|
||||
connectionID := requireDiagnosticsMeta(t, responseEnv, "task", "unsupported.diagnostics", "UNSUPPORTED_ACTION")
|
||||
requireJSONLog(t, logs.String(), "proto-socket request handled", map[string]string{
|
||||
"connection_id": connectionID,
|
||||
"protocol_version": ProtocolVersion,
|
||||
"channel": "task",
|
||||
"action": "unsupported.diagnostics",
|
||||
"error_code": "UNSUPPORTED_ACTION",
|
||||
})
|
||||
}
|
||||
|
||||
func TestServerBroadcastAddsDiagnosticsMetaAndStructuredLogs(t *testing.T) {
|
||||
var logs bytes.Buffer
|
||||
logger := slog.New(slog.NewJSONHandler(&logs, nil))
|
||||
srv := NewServer(Config{
|
||||
HeartbeatIntervalSec: 0,
|
||||
HeartbeatWaitSec: 0,
|
||||
}, logger)
|
||||
defer srv.Close()
|
||||
|
||||
testServer := httptest.NewServer(srv)
|
||||
defer testServer.Close()
|
||||
|
||||
wsURL := strings.Replace(testServer.URL, "http", "ws", 1)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
defer cancel()
|
||||
|
||||
conn, _, err := websocket.Dial(ctx, wsURL, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to dial websocket: %v", err)
|
||||
}
|
||||
defer conn.Close(websocket.StatusNormalClosure, "")
|
||||
|
||||
client := toki.NewWsClient(conn, 0, 0, ParserMap())
|
||||
defer client.Close()
|
||||
|
||||
received := make(chan struct {
|
||||
env Envelope
|
||||
err error
|
||||
}, 1)
|
||||
toki.AddListenerTyped[*structpb.Struct](&client.Communicator, func(msg *structpb.Struct) {
|
||||
env, err := EnvelopeFromStruct(msg)
|
||||
received <- struct {
|
||||
env Envelope
|
||||
err error
|
||||
}{env: env, err: err}
|
||||
})
|
||||
|
||||
waitForServerClients(t, srv, 1)
|
||||
|
||||
err = srv.BroadcastEnvelope(ctx, TaskStatusChangedEnvelope(notification.TaskEvent{
|
||||
Type: notification.TaskEventRunning,
|
||||
TaskID: "task-diagnostics",
|
||||
Status: "running",
|
||||
Title: "Diagnostics",
|
||||
Message: "running",
|
||||
}))
|
||||
if err != nil {
|
||||
t.Fatalf("broadcast failed: %v", err)
|
||||
}
|
||||
|
||||
var got struct {
|
||||
env Envelope
|
||||
err error
|
||||
}
|
||||
select {
|
||||
case got = <-received:
|
||||
case <-time.After(1 * time.Second):
|
||||
t.Fatal("timed out waiting for broadcast envelope")
|
||||
}
|
||||
if got.err != nil {
|
||||
t.Fatalf("failed to parse broadcast envelope: %v", got.err)
|
||||
}
|
||||
|
||||
connectionID := requireDiagnosticsMeta(t, got.env, "task", "task.status.changed", "")
|
||||
requireJSONLog(t, logs.String(), "proto-socket broadcast sent", map[string]string{
|
||||
"connection_id": connectionID,
|
||||
"protocol_version": ProtocolVersion,
|
||||
"channel": "task",
|
||||
"action": "task.status.changed",
|
||||
"error_code": "",
|
||||
})
|
||||
}
|
||||
|
||||
func TestServerRespondsWithErrorEnvelopeForUnsupportedAction(t *testing.T) {
|
||||
logger := slog.New(slog.NewTextHandler(io.Discard, nil))
|
||||
srv := NewServer(Config{
|
||||
|
|
|
|||
|
|
@ -315,6 +315,7 @@ func TestRouterProtoSocketTaskListRequiresAuth(t *testing.T) {
|
|||
if _, ok := res.Payload["tasks"]; !ok {
|
||||
t.Errorf("expected tasks payload, got %+v", res.Payload)
|
||||
}
|
||||
requireDiagnosticsMeta(t, res, "task", "task.list", "")
|
||||
}
|
||||
|
||||
func TestProtoSocketTaskActionsMatchRestSemantics(t *testing.T) {
|
||||
|
|
@ -363,6 +364,7 @@ func TestProtoSocketTaskActionsMatchRestSemantics(t *testing.T) {
|
|||
if create.Type != "response" || create.Payload["id"] != "task-c" || create.Payload["status"] != "pending" {
|
||||
t.Errorf("unexpected create response: %+v (err=%+v)", create.Payload, create.Error)
|
||||
}
|
||||
requireDiagnosticsMeta(t, create, "task", "task.create", "")
|
||||
|
||||
get := send("task.get", map[string]any{"id": "task-g"})
|
||||
if get.Type != "response" {
|
||||
|
|
@ -371,11 +373,13 @@ func TestProtoSocketTaskActionsMatchRestSemantics(t *testing.T) {
|
|||
if task, ok := get.Payload["task"].(map[string]any); !ok || task["status"] != "running" {
|
||||
t.Errorf("unexpected get task payload: %+v", get.Payload)
|
||||
}
|
||||
requireDiagnosticsMeta(t, get, "task", "task.get", "")
|
||||
|
||||
enqueue := send("task.enqueue", map[string]any{"id": "task-e"})
|
||||
if enqueue.Type != "response" || enqueue.Payload["status"] != "queued" {
|
||||
t.Errorf("unexpected enqueue response: %+v", enqueue.Payload)
|
||||
}
|
||||
requireDiagnosticsMeta(t, enqueue, "task", "task.enqueue", "")
|
||||
}
|
||||
|
||||
func TestProtoSocketTaskErrorEnvelopeCodes(t *testing.T) {
|
||||
|
|
@ -427,6 +431,7 @@ func TestProtoSocketTaskErrorEnvelopeCodes(t *testing.T) {
|
|||
if res.Error == nil || res.Error.Code != tc.code {
|
||||
t.Errorf("%s: expected code %q, got %+v", tc.action, tc.code, res.Error)
|
||||
}
|
||||
requireDiagnosticsMeta(t, res, "task", tc.action, tc.code)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue