feat: gito-protosocket-consumer-wire-readiness milestone completion

- Update roadmap milestone status
- Add gitosync runner and tests
- Update gitoevents tests
- Adjust docker-compose configuration
- Update core and project READMEs
- Add agent-task for consumer wire readiness
This commit is contained in:
toki 2026-06-16 22:26:44 +09:00
parent 0109c89eb9
commit f4959a3bab
16 changed files with 1973 additions and 30 deletions

View file

@ -145,6 +145,7 @@ IOP 경계가 걸린 작업은 같은 workspace에 sibling IOP repository가 있
| Core | `WORKFLOW_TASK_TIMEOUT_SEC` | 선택 | workflow task timeout 기본값을 조정합니다. |
| Core | `MATTERMOST_BASE_URL`, `MATTERMOST_TOKEN`, `MATTERMOST_CHANNEL_ID` | 선택 | Mattermost task completion message 발송용 설정입니다. token 값은 문서에 기록하지 않습니다. |
| Core | `PLANE_BASE_URL`, `PLANE_TOKEN` | 선택 | Plane work item 조회, comment, state update 연동용 설정입니다. token 값은 문서에 기록하지 않습니다. |
| Core | `GITO_PROTO_SOCKET_URL`, `GITO_REPO_ID`, `GITO_DEVELOP_REPO_PATH`, `GITO_BRANCH`, `GITO_REMOTE_NAME`, `ROADMAP_CREATION_TODO_STATE_ID`, `PLANE_TODO_STATE_ID` | 선택 | Gito `branch.updated` proto-socket consumer 설정입니다. endpoint, repo id, local develop checkout, Todo state id가 모두 있을 때만 consumer가 켜집니다. `ROADMAP_CREATION_TODO_STATE_ID``PLANE_TODO_STATE_ID`보다 우선합니다. |
| Core | `JIRA_BASE_URL`, `JIRA_EMAIL`, `JIRA_API_TOKEN` | 선택 | Jira issue 조회, comment, status transition 연동용 설정입니다. Jira Cloud Basic auth(`Email:APIToken`) 형식을 사용합니다. |
## 참고 문서

View file

@ -38,10 +38,10 @@ NomadCode core의 Gito branch event consumer를 실제 Gito `/proto-socket` wire
NomadCode가 Gito branch event를 논리 DTO가 아니라 실제 proto-socket wire frame으로 소비하되, Gito의 base event schema를 NomadCode 내부 모델로 오염시키지 않는다.
- [ ] [transport-adapter] JSON text websocket transport를 실제 proto-socket binary/protobuf transport 또는 동등 adapter로 교체한다. 검증: 실제 proto-socket test server가 `event.subscribe`를 받고 `branch.updated`를 binary frame으로 돌려주는 테스트가 통과한다.
- [ ] [schema-boundary] Gito `branch.updated` base payload를 repo/branch/revision/change hint 중심으로 해석하고 NomadCode 전용 값은 optional metadata/custom field가 있을 때만 소비한다. 검증: NomadCode 전용 필드 없이도 branch scan/enqueue wakeup이 동작한다.
- [x] [schema-boundary] Gito `branch.updated` base payload를 repo/branch/revision/change hint 중심으로 해석하고 NomadCode 전용 값은 optional metadata/custom field가 있을 때만 소비한다. 검증: NomadCode 전용 필드 없이도 branch scan/enqueue wakeup이 동작한다.
- [ ] [envelope-decode] binary frame에서 받은 `structpb.Struct` envelope을 기존 `gitoevents.DecodeBranchUpdatedEnvelope` 입력으로 안전하게 변환한다. 검증: malformed frame, 다른 channel/action, payload 누락, 알 수 없는 metadata/custom field가 side effect 없이 drop 또는 ignore된다.
- [ ] [bridge-e2e] wire-compatible consumer, branch scan, `EnqueueRoadmapCreationSync`까지 이어지는 integration test 또는 dev smoke를 추가한다. 검증: Gito `branch.updated` 한 건이 target branch 재검증 후 정확히 하나의 creation sync job으로 수렴한다.
- [ ] [config-docs] `GITO_PROTO_SOCKET_URL`, `GITO_REPO_ID`, `GITO_DEVELOP_REPO_PATH`, `GITO_BRANCH`, `GITO_REMOTE_NAME`, `ROADMAP_CREATION_TODO_STATE_ID` 또는 `PLANE_TODO_STATE_ID` 설정을 README, compose, env sample 중 적절한 위치에 문서화한다. 검증: secret 없이 local/dev consumer를 켤 수 있는 설정 경로가 남는다.
- [x] [config-docs] `GITO_PROTO_SOCKET_URL`, `GITO_REPO_ID`, `GITO_DEVELOP_REPO_PATH`, `GITO_BRANCH`, `GITO_REMOTE_NAME`, `ROADMAP_CREATION_TODO_STATE_ID` 또는 `PLANE_TODO_STATE_ID` 설정을 README, compose, env sample 중 적절한 위치에 문서화한다. 검증: secret 없이 local/dev consumer를 켤 수 있는 설정 경로가 남는다.
- [ ] [failure-ops] reconnect, not-ready revision, duplicate event, malformed frame 처리와 로그 기준을 정리한다. 검증: 장애 입력이 core server shutdown이나 중복 Plane mutation으로 이어지지 않는다.
## 완료 리뷰

View file

@ -0,0 +1,221 @@
<!-- task=m-gito-protosocket-consumer-wire-readiness/01_wire_transport_decode 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.
> Do not ask the user directly, present choices in chat, or call `request_user_input` during implementation; record the needed decision in `사용자 리뷰 요청` and stop for code-review.
> Finalization (`코드리뷰 결과`, log rename, `complete.log`, archive moves, `코드리뷰 전용 체크리스트`) is review-agent-only, even after compaction/resume.
> Follow the ownership table at the bottom of this file for which sections you own.
## 개요
date=2026-06-16
task=m-gito-protosocket-consumer-wire-readiness/01_wire_transport_decode, plan=0, tag=API
## Roadmap Targets
- Milestone: `agent-roadmap/phase/agent-ops-mcp-control-plane/milestones/gito-protosocket-consumer-wire-readiness.md`
- Task ids:
- `transport-adapter`: JSON text websocket transport를 실제 proto-socket binary/protobuf transport 또는 동등 adapter로 교체
- `envelope-decode`: binary frame에서 받은 `structpb.Struct` envelope을 기존 `gitoevents.DecodeBranchUpdatedEnvelope` 입력으로 안전하게 변환
- Completion mode: check-on-pass
## 이 파일을 읽는 리뷰 에이전트에게
> **[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-gito-protosocket-consumer-wire-readiness/01_wire_transport_decode/`로 이동한다. WARN/FAIL이면 user-review gate를 확인한 뒤 다음 active plan/review 파일 또는 `USER_REVIEW.md`를 작성한다.
4. PASS이고 task group이 `m-gito-protosocket-consumer-wire-readiness`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다.
5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다.
---
## 구현 항목별 완료 여부
| 항목 | 완료 여부 |
|------|---------|
| [API-1] Replace JSON websocket transport with binary proto-socket transport | [x] |
| [API-2] Convert inbound Struct envelopes through the shared envelope boundary | [x] |
| [API-3] Prove `event.subscribe` request and `branch.updated` binary event interop | [x] |
## 구현 체크리스트
- [x] `gitosync` production transport가 `git.toki-labs.com/toki/proto-socket/go` `WsClient`와 `protosocket.ParserMap()`으로 binary `structpb.Struct` frame을 송수신하도록 바꾼다.
- [x] `structpb.Struct` 수신값을 `protosocket.EnvelopeFromStruct`로 변환하고 malformed struct, non-Struct frame, wrong channel/action, payload 누락을 side effect 없이 drop 또는 error path로 처리하는 테스트를 추가한다.
- [x] 실제 proto-socket test server가 `event.subscribe`를 받고 `branch.updated`를 binary frame으로 돌려주는 focused test를 추가한다.
- [x] `git diff --check`와 `cd services/core && go test ./...`를 실행한다.
- [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하고 `agent-roadmap/current.md`를 ignore하는지 확인한다.
- [ ] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다.
- [ ] PASS이면 active task 디렉터리 `agent-task/m-gito-protosocket-consumer-wire-readiness/01_wire_transport_decode/`를 `agent-task/archive/YYYY/MM/m-gito-protosocket-consumer-wire-readiness/01_wire_transport_decode/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다.
- [ ] PASS이고 task group이 `m-gito-protosocket-consumer-wire-readiness`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다.
- [ ] PASS split 작업이면 이동 후 빈 active parent `agent-task/m-gito-protosocket-consumer-wire-readiness/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다.
- [x] WARN/FAIL이고 user-review gate가 트리거되지 않았으면 다음 active `PLAN-cloud-G07.md` and `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로 이동한다.
## 계획 대비 변경 사항
- `TestRunnerTransportUsesBinaryProtoSocketSubscribeAndEvent` 에서 toki `WsClient`를 통한 binary `proto-socket` 통신 시, subscribe 요청(responseNonce가 있는 response 패킷)은 `SendRequest`가 아닌 `Send`로 전달되므로 클라이언트 측에서 pending 상태가 없어 toki 내부적으로 drop됩니다. 따라서 transport.Read에서 첫 번째로 읽히는 패킷이 subscribe response가 아닌 `branch.updated` event 패킷이 됨을 반영하여 테스트 검증 부분을 수정했습니다.
## 주요 설계 결정
- 기존 JSON text websocket 통신 방식을 `git.toki-labs.com/toki/proto-socket/go` 라이브러리의 `WsClient`를 이용한 binary protobuf 패킷 통신 방식으로 전환하였습니다.
- WsClient 비동기 콜백 리스너에서 수신한 `*structpb.Struct` 데이터를 `EnvelopeFromStruct`를 통해 디코딩하여 내부 채널로 안전하게 전달하는 방식을 적용했습니다.
- WsClient 연결 종료 이벤트 시 `io.EOF` 에러를 내부 수신 채널로 전달하여 Read 루프가 안전하게 종료되도록 하였습니다.
## 사용자 리뷰 요청
- 상태: 없음
- 사유 유형: 없음
- 결정 필요: 없음
- 차단 근거: 없음
- 실행한 검증/명령: 없음
- 자동 후속 불가 이유: 없음
- 재개 조건: 없음
## 리뷰어를 위한 체크포인트
- `runner.go`가 JSON text frame을 더 이상 쓰지 않는지 확인한다.
- `structpb.Struct` decode failure가 handler side effect로 이어지지 않는지 확인한다.
- binary proto-socket subscribe/event test가 실제 `toki.WsClient` 경로를 통과하는지 확인한다.
## 검증 결과
### API-1 중간 검증
```bash
cd services/core && go test ./internal/gitosync ./internal/protosocket
```
```
ok github.com/nomadcode/nomadcode-core/internal/gitosync 0.160s
ok github.com/nomadcode/nomadcode-core/internal/protosocket (cached)
```
### API-2 중간 검증
```bash
cd services/core && go test ./internal/gitoevents ./internal/gitosync
```
```
ok github.com/nomadcode/nomadcode-core/internal/gitoevents (cached)
ok github.com/nomadcode/nomadcode-core/internal/gitosync (cached)
```
### API-3 중간 검증
```bash
cd services/core && go test ./internal/gitosync -run TestRunnerTransportUsesBinaryProtoSocketSubscribeAndEvent -count=1
```
```
ok github.com/nomadcode/nomadcode-core/internal/gitosync 0.058s
```
### 최종 검증
```bash
git diff --check
cd services/core && go test ./...
```
```
# git diff --check
(No output, exited with code 0)
# go test ./...
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)
ok github.com/nomadcode/nomadcode-core/internal/adapters/jira (cached)
ok github.com/nomadcode/nomadcode-core/internal/adapters/mattermost (cached)
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/authoring (cached)
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/gitoevents (cached)
ok github.com/nomadcode/nomadcode-core/internal/gitosync (cached)
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/projectsync (cached)
ok github.com/nomadcode/nomadcode-core/internal/protosocket (cached)
ok github.com/nomadcode/nomadcode-core/internal/roadmapsync (cached)
ok github.com/nomadcode/nomadcode-core/internal/roadmapsyncpipeline (cached)
ok github.com/nomadcode/nomadcode-core/internal/scheduler (cached)
ok github.com/nomadcode/nomadcode-core/internal/storage (cached)
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)
```
---
> **[IMPLEMENTING AGENT — BEFORE SAVING] Have you filled in every implementation-owned section: completion table, implementation checklist, changes from plan, design decisions, and verification output?**
> If anything is blank, go back and fill it in before saving this file.
> Leave review-agent-only sections unchanged.
## 소유권
| Section | Owner | Note |
|---------|-------|------|
| Header comment, 개요, 리뷰 에이전트 지시 | Fixed at stub creation | Implementing agent must not modify or execute these |
| Roadmap Targets | Fixed at stub creation from plan | Implementing agent must not modify |
| 구현 항목별 완료 여부 | Implementing agent | Check `[ ]` to `[x]` only |
| 구현 체크리스트 | Implementing agent | Check `[ ]` to `[x]` only |
| 코드리뷰 전용 체크리스트 | Review agent only | Implementing agent must not modify |
| 계획 대비 변경 사항, 주요 설계 결정 | Implementing agent | Replace placeholder text with actual content |
| 사용자 리뷰 요청 | Implementing agent | Keep `상태: 없음` unless user input is required |
| 리뷰어를 위한 체크포인트 | Fixed at stub creation | Review focus |
| 검증 결과 | Implementing agent | Paste actual output |
## 코드리뷰 결과
### 종합 판정
FAIL
### 차원별 평가
| 차원 | 평가 | 근거 |
|------|------|------|
| Correctness | Fail | inbound queue overflow/disconnect paths can silently drop branch events or EOF signals. |
| Completeness | Fail | API-3 and malformed/drop-path verification required by the plan are not fully covered by the implemented tests. |
| Test coverage | Fail | The focused binary interop test exercises `protoSocketTransport` directly, not the production `gitoevents.Client.Run` handler path. |
| API contract | Pass | The transport now uses binary `PacketBase` with `google.protobuf.Struct` and the subscribe envelope shape matches the Gito contract. |
| Code quality | Pass | I applied `gofmt` to `runner.go` and `runner_test.go`; no remaining formatting issue was found. |
| Plan deviation | Pass | The selected task's core transport/decode scope is judgeable; unrelated active sibling/workspace changes were not used for this verdict. |
| Verification trust | Pass | Re-ran `git diff --check`, `go test ./internal/gitosync ./internal/gitoevents ./internal/protosocket`, `go test ./...`, and the focused interop test with `-count=1`. |
### 발견된 문제
- Required: `services/core/internal/gitosync/runner.go:64` and `services/core/internal/gitosync/runner.go:74` use non-blocking sends with a `default` case for both decoded envelopes and disconnect EOF. If more than 100 branch events arrive before `Read` drains the channel, events are silently discarded; if the disconnect signal is also dropped, `Read` can drain buffered items and then block until context cancellation instead of observing stream end. Fix by making overflow explicit rather than lossy: apply backpressure, or transition the transport to a terminal overflow error/close state that `Read` must return. Add a focused test that proves branch events are not silently lost and close/overflow is observable.
- Required: `services/core/internal/gitosync/runner_test.go:123` verifies `protoSocketTransport.Write` and `Read` manually, but the plan's API-3 acceptance requires the real client subscription path to deliver `branch.updated` to the `gitoevents.Client` handler exactly once. Fix by driving `gitoevents.NewClient` (or `NewRunner` with fakes) against the proto-socket test server, asserting the server receives `event.subscribe`, then asserting the handler receives exactly one on-target `branch.updated` event.
- Required: `services/core/internal/gitosync/runner_test.go:89` only sends invalid packet bytes; it does not cover the plan's valid-Struct malformed/drop cases such as missing payload and wrong channel/action through the real transport/client side-effect boundary. Fix by adding a real proto-socket test that sends malformed/mismatched `structpb.Struct` envelopes before a valid event and asserts no handler side effect occurs until the valid event.
### 다음 단계
WARN/FAIL follow-up: archive this review/plan pair and create the next active `PLAN-cloud-G07.md` and `CODE_REVIEW-cloud-G07.md` focused on lossless/error-observable inbound handling and the missing real-client interop tests.

View file

@ -0,0 +1,249 @@
<!-- task=m-gito-protosocket-consumer-wire-readiness/01_wire_transport_decode plan=1 tag=REVIEW_API -->
# Code Review Reference - REVIEW_API
> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is the mandatory final step of implementation.**
> The task is NOT complete until every implementation-owned section below is filled in.
> Complete the `구현 체크리스트`; the final checklist item is mandatory before saving.
> Fill implementation-owned sections, then stop with active files in place and report ready for review.
> If implementation is blocked by a user-only decision, user-owned external environment prerequisite, or scope conflict, fill `사용자 리뷰 요청` with evidence and stop with active files in place; code-review decides whether to write `USER_REVIEW.md`. Evidence gaps that a follow-up agent can close by rerunning commands or collecting artifacts are normal follow-up issues, not user-review blockers by themselves.
> Do not ask the user directly, present choices in chat, or call `request_user_input` during implementation; record the needed decision in `사용자 리뷰 요청` and stop for code-review.
> Finalization (`코드리뷰 결과`, log rename, `complete.log`, archive moves, `코드리뷰 전용 체크리스트`) is review-agent-only, even after compaction/resume.
> Follow the ownership table at the bottom of this file for which sections you own.
## 개요
date=2026-06-16
task=m-gito-protosocket-consumer-wire-readiness/01_wire_transport_decode, plan=1, tag=REVIEW_API
## Roadmap Targets
- Milestone: `agent-roadmap/phase/agent-ops-mcp-control-plane/milestones/gito-protosocket-consumer-wire-readiness.md`
- Task ids:
- `transport-adapter`: JSON text websocket transport를 실제 proto-socket binary/protobuf transport 또는 동등 adapter로 교체
- `envelope-decode`: binary frame에서 받은 `structpb.Struct` envelope을 기존 `gitoevents.DecodeBranchUpdatedEnvelope` 입력으로 안전하게 변환
- Completion mode: check-on-pass
## Archive Evidence Snapshot
- Current archived plan: `agent-task/m-gito-protosocket-consumer-wire-readiness/01_wire_transport_decode/plan_cloud_G07_0.log`
- Current archived review: `agent-task/m-gito-protosocket-consumer-wire-readiness/01_wire_transport_decode/code_review_cloud_G07_0.log`
- Verdict: FAIL
- Required summary:
- `services/core/internal/gitosync/runner.go:64` and `services/core/internal/gitosync/runner.go:74` silently drop inbound decoded envelopes and disconnect EOF when the buffered channel is full.
- `services/core/internal/gitosync/runner_test.go:123` verifies transport `Write`/`Read` manually but does not prove `gitoevents.Client.Run` delivers the binary `branch.updated` event to the handler exactly once.
- `services/core/internal/gitosync/runner_test.go:89` covers invalid packet bytes but not valid-Struct malformed/mismatched envelope side-effect behavior through the real transport/client path.
- Affected files:
- `services/core/internal/gitosync/runner.go`
- `services/core/internal/gitosync/runner_test.go`
- `services/core/internal/gitoevents/events_test.go` if an extra decoder regression is needed
- Verification evidence from prior loop:
- `git diff --check` passed after review-applied `gofmt`.
- `cd services/core && go test ./internal/gitosync ./internal/gitoevents ./internal/protosocket` passed.
- `cd services/core && go test ./...` passed.
- `cd services/core && go test ./internal/gitosync -run TestRunnerTransportUsesBinaryProtoSocketSubscribeAndEvent -count=1 -v` passed.
- Roadmap carryover: keep the same `transport-adapter` and `envelope-decode` targets; do not mark roadmap tasks directly.
- Allowed archive reread: only the two archived files listed above if exact prior wording is needed.
## 이 파일을 읽는 리뷰 에이전트에게
> **[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-gito-protosocket-consumer-wire-readiness/01_wire_transport_decode/`로 이동한다. WARN/FAIL이면 user-review gate를 확인한 뒤 다음 active plan/review 파일 또는 `USER_REVIEW.md`를 작성한다.
4. PASS이고 task group이 `m-gito-protosocket-consumer-wire-readiness`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다.
5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다.
---
## 구현 항목별 완료 여부
| 항목 | 완료 여부 |
|------|---------|
| [REVIEW_API-1] Make inbound queue overflow and disconnect observable | [x] |
| [REVIEW_API-2] Prove the production client handler path over binary proto-socket | [x] |
| [REVIEW_API-3] Cover malformed and mismatched Struct frames through the side-effect boundary | [x] |
## 구현 체크리스트
- [x] inbound decoded envelopes and disconnect/error signals are never silently dropped; overflow or close is either backpressured or returned as an explicit `Read` error.
- [x] a real proto-socket server plus `gitoevents.Client.Run` test proves `event.subscribe` is sent and exactly one on-target binary `branch.updated` event reaches the handler.
- [x] valid-Struct malformed/mismatched frames, including wrong channel/action and missing payload, are covered through the real transport/client side-effect boundary before a valid event.
- [x] `gofmt`, `git diff --check`, `cd services/core && go test ./internal/gitosync ./internal/gitoevents ./internal/protosocket`, and `cd services/core && go test ./...` are executed.
- [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하고 `agent-roadmap/current.md`를 ignore하는지 확인한다.
- [x] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다.
- [x] PASS이면 active task 디렉터리 `agent-task/m-gito-protosocket-consumer-wire-readiness/01_wire_transport_decode/`를 `agent-task/archive/YYYY/MM/m-gito-protosocket-consumer-wire-readiness/01_wire_transport_decode/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다.
- [x] PASS이고 task group이 `m-gito-protosocket-consumer-wire-readiness`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다.
- [x] PASS split 작업이면 이동 후 빈 active parent `agent-task/m-gito-protosocket-consumer-wire-readiness/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다.
- [ ] WARN/FAIL이고 user-review gate가 트리거되지 않았으면 다음 active `PLAN-cloud-G07.md` and `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로 이동한다.
## 계획 대비 변경 사항
- 없음. 계획에 제시된 설계 요구사항(Overflow 및 Disconnect 방지, client.Run 연동 검증, malformed/mismatched struct drop 검증)을 지연 없이 모두 완벽하게 이행하였습니다.
## 주요 설계 결정
- `protoSocketTransport` 내부에서 비동기 리스너 콜백(AddListenerTyped, AddDisconnectListener)이 inbound 채널로 메시지를 인입할 때 무손실 처리를 위해 backpressure를 적용하도록 select/enqueue 방식을 도입하였습니다.
- transport가 Close() 될 때 blocking 상태를 해제하기 위해 `closed chan struct{}` 시그널링 채널을 추가하여 안전하게 루프가 종료되도록 하였습니다.
- 테스트의 유연성을 위해 transport에 `inboundCap` 필드를 추가하여 버퍼 오버플로우 상황을 시뮬레이션할 수 있게 하였습니다.
## 사용자 리뷰 요청
- 상태: 없음
- 사유 유형: 없음
- 결정 필요: 없음
- 차단 근거: 없음
- 실행한 검증/명령: 없음
- 자동 후속 불가 이유: 없음
- 재개 조건: 없음
## 리뷰어를 위한 체크포인트
- `protoSocketTransport`가 branch event 또는 terminal error/EOF를 silent drop하지 않는지 확인한다.
- `gitoevents.Client.Run`이 real proto-socket server와 production transport 조합에서 handler를 정확히 한 번 호출하는지 확인한다.
- malformed or mismatched `structpb.Struct` frames do not cause handler side effects and do not prevent a later valid event from being consumed.
## 검증 결과
### REVIEW_API-1 중간 검증
```bash
cd services/core && go test ./internal/gitosync -run 'TestProtoSocketTransport.*(Overflow|Disconnect|Drop)' -count=1
```
```
ok github.com/nomadcode/nomadcode-core/internal/gitosync 0.313s
```
### REVIEW_API-2 중간 검증
```bash
cd services/core && go test ./internal/gitosync -run TestProtoSocketTransportClientRunDeliversBinaryBranchUpdatedEvent -count=1
```
```
ok github.com/nomadcode/nomadcode-core/internal/gitosync 0.063s
```
### REVIEW_API-3 중간 검증
```bash
cd services/core && go test ./internal/gitosync -run TestProtoSocketTransportClientRunDropsMalformedAndMismatchedStructs -count=1
```
```
ok github.com/nomadcode/nomadcode-core/internal/gitosync 0.111s
```
### 최종 검증
```bash
gofmt -w services/core/internal/gitosync/runner.go services/core/internal/gitosync/runner_test.go services/core/internal/gitoevents/events_test.go
git diff --check
cd services/core && go test ./internal/gitosync ./internal/gitoevents ./internal/protosocket
cd services/core && go test ./...
```
```
# gofmt -w services/core/internal/gitosync/runner.go services/core/internal/gitosync/runner_test.go services/core/internal/gitoevents/events_test.go
(No output, exited with code 0)
# git diff --check
(No output, exited with code 0)
# go test ./internal/gitosync ./internal/gitoevents ./internal/protosocket
ok github.com/nomadcode/nomadcode-core/internal/gitosync 0.367s
ok github.com/nomadcode/nomadcode-core/internal/gitoevents (cached)
ok github.com/nomadcode/nomadcode-core/internal/protosocket (cached)
# go test ./...
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)
ok github.com/nomadcode/nomadcode-core/internal/adapters/jira (cached)
ok github.com/nomadcode/nomadcode-core/internal/adapters/mattermost (cached)
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/authoring (cached)
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/gitoevents (cached)
ok github.com/nomadcode/nomadcode-core/internal/gitosync (cached)
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/projectsync (cached)
ok github.com/nomadcode/nomadcode-core/internal/protosocket (cached)
ok github.com/nomadcode/nomadcode-core/internal/roadmapsync (cached)
ok github.com/nomadcode/nomadcode-core/internal/roadmapsyncpipeline (cached)
ok github.com/nomadcode/nomadcode-core/internal/scheduler (cached)
ok github.com/nomadcode/nomadcode-core/internal/storage (cached)
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)
```
---
> **[IMPLEMENTING AGENT — BEFORE SAVING] Have you filled in every implementation-owned section: completion table, implementation checklist, changes from plan, design decisions, and verification output?**
> If anything is blank, go back and fill it in before saving this file.
> Leave review-agent-only sections unchanged.
## 소유권
| Section                 | Owner | Note                                    |
| ------------------------------------------| --------------------------------------------| ----------------------------------------------------------------------------|
| Header comment, 개요, 리뷰 에이전트 지시 | Fixed at stub creation | Implementing agent must not modify or execute these            |
| Roadmap Targets             | Fixed at stub creation from plan | Implementing agent must not modify                     |
| Archive Evidence Snapshot        | Fixed at stub creation from follow-up plan | Implementing agent may use only the listed archive paths for narrow reread |
| 구현 항목별 완료 여부          | Implementing agent | Check `[ ]` to `[x]` only                         |
| 구현 체크리스트             | Implementing agent | Check `[ ]` to `[x]` only                         |
| 코드리뷰 전용 체크리스트         | Review agent only | Implementing agent must not modify                     |
| 계획 대비 변경 사항, 주요 설계 결정   | Implementing agent | Replace placeholder text with actual content                |
| 사용자 리뷰 요청             | Implementing agent | Keep `상태: 없음` unless user input is required              |
| 리뷰어를 위한 체크포인트         | Fixed at stub creation | Review focus                                |
| 검증 결과                | Implementing agent | Paste actual output                            |
## 코드리뷰 결과
### 종합 판정
PASS
### 차원별 평가
| 차원 | 평가 | 근거 |
|------|------|------|
| Correctness | Pass | `protoSocketTransport` no longer uses lossy `default` sends; inbound events and disconnect EOF are backpressured or unblocked by `Close`. |
| Completeness | Pass | All three follow-up items are implemented and the review stub checklist is complete. |
| Test coverage | Pass | Added focused tests for overflow/disconnect observability, real `gitoevents.Client.Run` binary interop, and malformed/mismatched struct drop behavior. |
| API contract | Pass | The transport remains on binary proto-socket `PacketBase` + `structpb.Struct`, and the subscribe/event path matches the Gito contract. |
| Code quality | Pass | `gofmt -l` returned no files; no debug prints, stale JSON text transport, or TODOs were found in the touched gitosync/gitoevents files. |
| Plan deviation | Pass | Follow-up scope stayed within `runner.go` and focused tests, with prior wider docs/roadmap changes left outside this verdict. |
| Verification trust | Pass | Re-ran focused tests, `git diff --check`, `go test ./internal/gitosync ./internal/gitoevents ./internal/protosocket`, and `go test ./...`; all passed. |
### 발견된 문제
없음
### 다음 단계
PASS: archive this review/plan pair, write `complete.log`, move the split task directory to archive, and report milestone completion event metadata for the runtime.

View file

@ -0,0 +1,48 @@
# Complete - m-gito-protosocket-consumer-wire-readiness/01_wire_transport_decode
## 완료 일시
2026-06-16
## 요약
Gito consumer wire transport/decode readiness split task completed after 2 review loops; final verdict PASS.
## 루프 이력
| Plan | Review | Verdict | 메모 |
|------|--------|---------|------|
| `plan_cloud_G07_0.log` | `code_review_cloud_G07_0.log` | FAIL | Required follow-up for inbound silent drop risk and missing real-client interop/drop-path tests. |
| `plan_cloud_G07_1.log` | `code_review_cloud_G07_1.log` | PASS | Backpressure/EOF observability and real `gitoevents.Client.Run` binary proto-socket tests completed. |
## 구현/정리 내용
- Replaced the Gito consumer's JSON text websocket transport with binary proto-socket `PacketBase`/`structpb.Struct` transport.
- Routed inbound `structpb.Struct` messages through `protosocket.EnvelopeFromStruct` and made inbound event/error delivery non-lossy via backpressure plus close signaling.
- Added focused tests for binary subscribe/event interop, real `gitoevents.Client.Run` handler delivery, malformed/mismatched struct drops, and overflow/disconnect observability.
## 최종 검증
- `gofmt -l services/core/internal/gitosync/runner.go services/core/internal/gitosync/runner_test.go services/core/internal/gitoevents/events_test.go` - PASS; no files listed.
- `git diff --check` - PASS; no output.
- `cd services/core && go test ./internal/gitosync -run 'TestProtoSocketTransport.*(Overflow|Disconnect|Drop)' -count=1 -v` - PASS; overflow/disconnect and malformed/drop focused tests passed.
- `cd services/core && go test ./internal/gitosync -run TestProtoSocketTransportClientRunDeliversBinaryBranchUpdatedEvent -count=1 -v` - PASS.
- `cd services/core && go test ./internal/gitosync -run TestProtoSocketTransportClientRunDropsMalformedAndMismatchedStructs -count=1 -v` - PASS.
- `cd services/core && go test ./internal/gitosync ./internal/gitoevents ./internal/protosocket` - PASS.
- `cd services/core && go test ./...` - PASS.
## Roadmap Completion
- Milestone: `agent-roadmap/phase/agent-ops-mcp-control-plane/milestones/gito-protosocket-consumer-wire-readiness.md`
- Completed task ids:
- `transport-adapter`: PASS; evidence=`agent-task/archive/2026/06/m-gito-protosocket-consumer-wire-readiness/01_wire_transport_decode/plan_cloud_G07_1.log`, `agent-task/archive/2026/06/m-gito-protosocket-consumer-wire-readiness/01_wire_transport_decode/code_review_cloud_G07_1.log`; verification=`cd services/core && go test ./internal/gitosync -run TestProtoSocketTransportClientRunDeliversBinaryBranchUpdatedEvent -count=1 -v`, `cd services/core && go test ./internal/gitosync ./internal/gitoevents ./internal/protosocket`, `cd services/core && go test ./...`
- `envelope-decode`: PASS; evidence=`agent-task/archive/2026/06/m-gito-protosocket-consumer-wire-readiness/01_wire_transport_decode/plan_cloud_G07_1.log`, `agent-task/archive/2026/06/m-gito-protosocket-consumer-wire-readiness/01_wire_transport_decode/code_review_cloud_G07_1.log`; verification=`cd services/core && go test ./internal/gitosync -run TestProtoSocketTransportClientRunDropsMalformedAndMismatchedStructs -count=1 -v`, `cd services/core && go test ./internal/gitosync -run 'TestProtoSocketTransport.*(Overflow|Disconnect|Drop)' -count=1 -v`, `cd services/core && go test ./...`
- Not completed task ids: 없음
## 잔여 Nit
- 없음
## 후속 작업
- 없음

View file

@ -0,0 +1,191 @@
<!-- task=m-gito-protosocket-consumer-wire-readiness/01_wire_transport_decode plan=0 tag=API -->
# Plan - API
## 이 파일을 읽는 구현 에이전트에게
`CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채우는 것이 필수입니다. 구현 후 검증을 실행하고 실제 stdout/stderr를 붙인 뒤 active 파일을 그대로 두고 리뷰 준비를 보고하세요. 최종 판정, log rename, `complete.log`, archive 이동은 code-review-skill 전용입니다.
구현 중 사용자만 결정할 수 있는 범위 변경, 사용자 소유 외부 환경/secret, 또는 scope conflict가 없이는 안전하게 진행할 수 없으면 review stub의 `사용자 리뷰 요청` 섹션에 근거와 재개 조건을 채우고 멈추세요. 구현 에이전트는 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않으며, `USER_REVIEW.md`, archive log, `complete.log`를 만들지 않습니다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 닫을 수 있는 증거 공백은 사용자 리뷰가 아닙니다.
## 배경
현재 NomadCode Gito consumer의 production transport는 `nhooyr.io/websocket` text frame에 JSON envelope을 싣습니다. Gito 계약은 `/proto-socket`이 binary protobuf `PacketBase` frame과 `google.protobuf.Struct` payload만 지원한다고 명시하므로, 실제 wire와 맞지 않습니다. 이 계획은 transport 교체와 binary frame에서 온 `structpb.Struct`를 기존 `gitoevents.DecodeBranchUpdatedEnvelope` 입력으로 안전하게 변환하는 작업을 먼저 닫습니다.
## 사용자 리뷰 요청 흐름
구현 중 blocker는 active review stub의 `사용자 리뷰 요청` 섹션에 기록합니다. 구현 에이전트의 직접 사용자 질문은 금지되며, code-review가 해당 요청을 검증하고 실제 `USER_REVIEW.md` 작성 여부를 결정합니다.
## Roadmap Targets
- Milestone: `agent-roadmap/phase/agent-ops-mcp-control-plane/milestones/gito-protosocket-consumer-wire-readiness.md`
- Task ids:
- `transport-adapter`: JSON text websocket transport를 실제 proto-socket binary/protobuf transport 또는 동등 adapter로 교체
- `envelope-decode`: binary frame에서 받은 `structpb.Struct` envelope을 기존 `gitoevents.DecodeBranchUpdatedEnvelope` 입력으로 안전하게 변환
- Completion mode: check-on-pass
## 분석 결과
### 읽은 파일
- `agent-roadmap/current.md`
- `agent-roadmap/phase/agent-ops-mcp-control-plane/PHASE.md`
- `agent-roadmap/phase/agent-ops-mcp-control-plane/milestones/gito-protosocket-consumer-wire-readiness.md`
- `/config/workspace/gito/agent-contract/index.md`
- `/config/workspace/gito/agent-contract/provided/gito-forgejo-branch-events-v1.md`
- `agent-test/local/rules.md`
- `agent-test/local/core-smoke.md`
- `agent-test/local/workspace-ops-smoke.md`
- `services/core/internal/gitosync/runner.go`
- `services/core/internal/gitoevents/client.go`
- `services/core/internal/gitoevents/events.go`
- `services/core/internal/gitoevents/client_test.go`
- `services/core/internal/gitoevents/events_test.go`
- `services/core/internal/protosocket/envelope.go`
- `services/core/internal/protosocket/envelope_test.go`
- `services/core/internal/protosocket/server.go`
- `services/core/internal/protosocket/server_test.go`
- `services/core/internal/protosocket/tasks_test.go`
- `services/core/go.mod`
- `/config/go/pkg/mod/git.toki-labs.com/toki/proto-socket/go@v0.0.0-20260530100557-fed0da37fa1a/ws_client.go`
- `/config/go/pkg/mod/git.toki-labs.com/toki/proto-socket/go@v0.0.0-20260530100557-fed0da37fa1a/communicator.go`
### 테스트 환경 규칙
`test_env=local`입니다. `agent-test/local/rules.md`와 `agent-test/local/core-smoke.md`, roadmap/doc touch 때문에 `agent-test/local/workspace-ops-smoke.md`를 읽었습니다. 적용 명령은 core 변경 최소 `cd services/core && go test ./...`와 문서/roadmap preflight `git diff --check`입니다. remote runner profile은 standard runner를 기준으로 하지만, 구현 task의 deterministic contract는 현재 checkout에서 같은 Go test와 diff check를 실행하는 것으로 둡니다.
### 테스트 커버리지 공백
`gitosync/runner.go`의 text JSON transport는 현재 직접 테스트가 없습니다. `protosocket/server_test.go`와 `tasks_test.go`는 binary proto-socket client/server 사용 예시를 검증하지만 Gito consumer transport에는 적용되지 않습니다. 이 계획에서 production transport 단위/route 테스트를 추가해야 합니다.
### 심볼 참조
none. 기존 exported symbol rename/remove 없이 `websocketTransport` 내부 구현 또는 새 unexported transport helper만 바꿉니다.
### 분할 판단
split decision policy를 먼저 평가했습니다. Wire transport와 binary struct decode는 같은 protocol boundary이며 함께 리뷰해야 합니다. `bridge-e2e`와 `failure-ops`는 이 작업의 complete.log 뒤에 검증하는 downstream behavior라서 별도 dependent subtasks로 분리했습니다. 공유 task group은 `m-gito-protosocket-consumer-wire-readiness`이고, 이 plan은 `01_wire_transport_decode`로 선행 의존성이 없습니다.
### 범위 결정 근거
이 plan은 `services/core/internal/gitosync/runner.go`, 필요한 경우 `services/core/internal/gitoevents/*`, 해당 tests만 수정합니다. branch scan/enqueue 의미, config/env wiring, Plane job semantics, Gito producer 구현은 제외합니다.
### 빌드 등급
`cloud-G07`입니다. 실제 binary protobuf wire, async listener/request 흐름, malformed frame handling이 protocol/schema risk를 갖고 기존 JSON path를 교체합니다.
## 구현 체크리스트
- [ ] `gitosync` production transport가 `git.toki-labs.com/toki/proto-socket/go` `WsClient`와 `protosocket.ParserMap()`으로 binary `structpb.Struct` frame을 송수신하도록 바꾼다.
- [ ] `structpb.Struct` 수신값을 `protosocket.EnvelopeFromStruct`로 변환하고 malformed struct, non-Struct frame, wrong channel/action, payload 누락을 side effect 없이 drop 또는 error path로 처리하는 테스트를 추가한다.
- [ ] 실제 proto-socket test server가 `event.subscribe`를 받고 `branch.updated`를 binary frame으로 돌려주는 focused test를 추가한다.
- [ ] `git diff --check`와 `cd services/core && go test ./...`를 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
### [API-1] Replace JSON websocket transport with binary proto-socket transport
문제: [runner.go](/config/workspace/nomadcode/services/core/internal/gitosync/runner.go:32)의 `websocketTransport` 설명과 [runner.go](/config/workspace/nomadcode/services/core/internal/gitosync/runner.go:49)의 `Write`, [runner.go](/config/workspace/nomadcode/services/core/internal/gitosync/runner.go:60)의 `Read`가 JSON text frame을 사용합니다. Gito contract는 JSON text frame 미지원, binary protobuf `PacketBase` + `structpb.Struct`만 지원합니다.
해결 방법: `websocket.Dial` 이후 `toki.NewWsClient(conn, 0, 0, protosocket.ParserMap())` 또는 configurable heartbeat helper를 사용합니다. `Write`는 `env.ToStruct()` 후 `client.Communicator.Send(structMsg)`로 binary frame을 보내고, `Read`는 `toki.AddListenerTyped[*structpb.Struct]` callback이 `EnvelopeFromStruct` 결과를 channel에 넣도록 구현합니다. `Close`는 `WsClient.Close()`와 websocket close를 idempotent하게 처리합니다.
Before:
```go
// services/core/internal/gitosync/runner.go:49
func (t *websocketTransport) Write(ctx context.Context, env protosocket.Envelope) error {
if t.conn == nil {
return errors.New("gitosync: transport not connected")
}
data, err := json.Marshal(env)
if err != nil {
return err
}
return t.conn.Write(ctx, websocket.MessageText, data)
}
```
After direction:
```go
func (t *protoSocketTransport) Write(ctx context.Context, env protosocket.Envelope) error {
if t.client == nil {
return errors.New("gitosync: transport not connected")
}
msg, err := env.ToStruct()
if err != nil {
return err
}
return t.client.Communicator.Send(msg)
}
```
수정 파일 및 체크리스트:
- [ ] `services/core/internal/gitosync/runner.go`에서 JSON marshal/unmarshal transport를 제거하고 binary transport를 추가한다.
- [ ] `encoding/json` import가 더 이상 필요 없으면 제거한다.
- [ ] `google.golang.org/protobuf/types/known/structpb` import가 필요하면 추가한다.
테스트 작성: `services/core/internal/gitosync/runner_test.go`를 새로 만들거나 기존 test 파일에 transport-focused tests를 추가합니다. test server는 `protosocket.NewServer` 또는 local `httptest` + `toki.NewWsClient`를 사용합니다.
중간 검증:
```bash
cd services/core && go test ./internal/gitosync ./internal/protosocket
```
### [API-2] Convert inbound Struct envelopes through the shared envelope boundary
문제: [protosocket/envelope.go](/config/workspace/nomadcode/services/core/internal/protosocket/envelope.go:67)에 이미 `EnvelopeFromStruct`가 있고 [runner.go](/config/workspace/nomadcode/services/core/internal/gitosync/runner.go:68)는 JSON unmarshal로 `Envelope`을 만듭니다. binary path에서 decode boundary를 공유하지 않으면 `structpb.Struct` malformed handling과 event decode가 갈라집니다.
해결 방법: inbound `*structpb.Struct` listener callback에서 `protosocket.EnvelopeFromStruct`를 호출하고 성공한 envelope만 `Read` queue에 넣습니다. nil struct 또는 malformed payload는 log/error queue로 처리하되 `gitoevents.Client.handleEnvelope`에 partial envelope을 전달하지 않습니다.
수정 파일 및 체크리스트:
- [ ] `services/core/internal/gitosync/runner.go`에 inbound channel과 decode error path를 둔다.
- [ ] `Read(ctx)`는 context cancel, transport close, decode error를 구분해 반환한다.
- [ ] wrong channel/action은 transport가 아니라 `gitoevents.Client`의 기존 drop path에 맡긴다.
테스트 작성: malformed `structpb.Struct`, wrong action, missing payload frame을 보내 handler가 호출되지 않는 테스트를 추가합니다.
중간 검증:
```bash
cd services/core && go test ./internal/gitoevents ./internal/gitosync
```
### [API-3] Prove `event.subscribe` request and `branch.updated` binary event interop
문제: [gitoevents/client.go](/config/workspace/nomadcode/services/core/internal/gitoevents/client.go:89)는 subscribe envelope을 보내지만 현재 production transport test는 fake transport뿐입니다. 실제 proto-socket test server가 binary request를 받는 검증이 없습니다.
해결 방법: test server가 `toki.AddRequestListenerTyped[*structpb.Struct,*structpb.Struct]`로 `event.subscribe`를 받고, subscription response 후 `branch.updated` event를 `WsClient.Send` 또는 server broadcast로 전송합니다. `gitoevents.Client` handler가 정확히 한 번 호출되는지 검증합니다.
수정 파일 및 체크리스트:
- [ ] `services/core/internal/gitosync/runner_test.go`에 binary subscribe/event test를 추가한다.
- [ ] request envelope의 `channel=event`, `action=event.subscribe`, payload `events=["branch.updated"]`, `repo_id`, `branch`를 assert한다.
- [ ] broadcast event는 Gito contract base payload를 사용한다.
테스트 작성: 필수. 테스트명 예: `TestRunnerTransportUsesBinaryProtoSocketSubscribeAndEvent`.
중간 검증:
```bash
cd services/core && go test ./internal/gitosync -run TestRunnerTransportUsesBinaryProtoSocketSubscribeAndEvent -count=1
```
## 수정 파일 요약
| 파일 | 항목 |
| --- | --- |
| `services/core/internal/gitosync/runner.go` | API-1, API-2 |
| `services/core/internal/gitosync/runner_test.go` | API-1, API-2, API-3 |
| `services/core/internal/gitoevents/events_test.go` | API-2 if extra decode regression is needed |
## 최종 검증
```bash
git diff --check
cd services/core && go test ./...
```
모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.

View file

@ -0,0 +1,126 @@
<!-- task=m-gito-protosocket-consumer-wire-readiness/01_wire_transport_decode plan=1 tag=REVIEW_API -->
# Plan - REVIEW_API
## 이 파일을 읽는 구현 에이전트에게
이 계획은 이전 리뷰에서 나온 Required 이슈만 닫는 follow-up입니다. 구현 중 사용자 결정, 사용자 소유 외부 환경/secret, 또는 scope conflict 없이는 안전하게 진행할 수 없을 때만 `CODE_REVIEW-*-G??.md`의 `사용자 리뷰 요청` 섹션을 채우고 멈추세요. 구현 에이전트는 사용자에게 직접 질문하거나 선택지를 제시하지 않습니다.
## Roadmap Targets
- Milestone: `agent-roadmap/phase/agent-ops-mcp-control-plane/milestones/gito-protosocket-consumer-wire-readiness.md`
- Task ids:
- `transport-adapter`: JSON text websocket transport를 실제 proto-socket binary/protobuf transport 또는 동등 adapter로 교체
- `envelope-decode`: binary frame에서 받은 `structpb.Struct` envelope을 기존 `gitoevents.DecodeBranchUpdatedEnvelope` 입력으로 안전하게 변환
- Completion mode: check-on-pass
## Archive Evidence Snapshot
- Current archived plan: `agent-task/m-gito-protosocket-consumer-wire-readiness/01_wire_transport_decode/plan_cloud_G07_0.log`
- Current archived review: `agent-task/m-gito-protosocket-consumer-wire-readiness/01_wire_transport_decode/code_review_cloud_G07_0.log`
- Verdict: FAIL
- Required summary:
- `services/core/internal/gitosync/runner.go:64` and `services/core/internal/gitosync/runner.go:74` silently drop inbound decoded envelopes and disconnect EOF when the buffered channel is full.
- `services/core/internal/gitosync/runner_test.go:123` verifies transport `Write`/`Read` manually but does not prove `gitoevents.Client.Run` delivers the binary `branch.updated` event to the handler exactly once.
- `services/core/internal/gitosync/runner_test.go:89` covers invalid packet bytes but not valid-Struct malformed/mismatched envelope side-effect behavior through the real transport/client path.
- Affected files:
- `services/core/internal/gitosync/runner.go`
- `services/core/internal/gitosync/runner_test.go`
- `services/core/internal/gitoevents/events_test.go` if an extra decoder regression is needed
- Verification evidence from prior loop:
- `git diff --check` passed after review-applied `gofmt`.
- `cd services/core && go test ./internal/gitosync ./internal/gitoevents ./internal/protosocket` passed.
- `cd services/core && go test ./...` passed.
- `cd services/core && go test ./internal/gitosync -run TestRunnerTransportUsesBinaryProtoSocketSubscribeAndEvent -count=1 -v` passed.
- Roadmap carryover: keep the same `transport-adapter` and `envelope-decode` targets; do not mark roadmap tasks directly.
- Allowed archive reread: only the two archived files listed above if exact prior wording is needed.
## 범위 결정 근거
이 follow-up은 active `01_wire_transport_decode`의 실패 원인만 수정합니다. README, compose, roadmap 상태 전환, bridge e2e, reconnect policy, Plane mutation semantics는 범위 밖입니다.
## 구현 체크리스트
- [ ] inbound decoded envelopes and disconnect/error signals are never silently dropped; overflow or close is either backpressured or returned as an explicit `Read` error.
- [ ] a real proto-socket server plus `gitoevents.Client.Run` test proves `event.subscribe` is sent and exactly one on-target binary `branch.updated` event reaches the handler.
- [ ] valid-Struct malformed/mismatched frames, including wrong channel/action and missing payload, are covered through the real transport/client side-effect boundary before a valid event.
- [ ] `gofmt`, `git diff --check`, `cd services/core && go test ./internal/gitosync ./internal/gitoevents ./internal/protosocket`, and `cd services/core && go test ./...` are executed.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
### [REVIEW_API-1] Make inbound queue overflow and disconnect observable
문제: `protoSocketTransport` currently uses non-blocking sends with `default` in the typed listener and disconnect listener. This can silently lose `branch.updated` wakeups or lose the terminal EOF/error signal.
해결 방법:
- Add a small helper for enqueueing inbound results so decoded envelopes and terminal errors share one policy.
- Prefer backpressure over silent loss, or close/mark the transport with an explicit overflow error that `Read` returns deterministically.
- Ensure `Read(ctx)` cannot block forever after the websocket has disconnected just because an EOF/error notification was dropped.
- Keep the fix local to `services/core/internal/gitosync/runner.go`.
테스트 작성:
- Add a focused test in `services/core/internal/gitosync/runner_test.go` that would fail against the prior lossy `default` path.
- The test should prove either all queued branch events are eventually observable, or overflow is surfaced as a deterministic error rather than silent data loss.
- Include a disconnect-after-buffered-events case if the chosen implementation can otherwise lose EOF.
중간 검증:
```bash
cd services/core && go test ./internal/gitosync -run 'TestProtoSocketTransport.*(Overflow|Disconnect|Drop)' -count=1
```
### [REVIEW_API-2] Prove the production client handler path over binary proto-socket
문제: `TestRunnerTransportUsesBinaryProtoSocketSubscribeAndEvent` drives `protoSocketTransport` directly. It does not prove `gitoevents.Client.Run` sends the subscription and forwards the binary broadcast to the handler exactly once.
해결 방법:
- Use a real `httptest` websocket server with `toki.NewWsClient` and `protosocket.ParserMap()`.
- Build a `gitoevents.Client` with `&protoSocketTransport{url: wsURL}` and a handler channel.
- Assert the server receives `channel=event`, `action=event.subscribe`, `events=["branch.updated"]`, `repo_id`, and `branch`.
- Send one binary `branch.updated` event from the server and assert exactly one handler invocation with the expected repo/branch/revision.
- Cancel the context and assert `Run` exits cleanly.
테스트 작성:
- Suggested name: `TestProtoSocketTransportClientRunDeliversBinaryBranchUpdatedEvent`.
- This may replace or extend `TestRunnerTransportUsesBinaryProtoSocketSubscribeAndEvent`, but the final test must exercise `gitoevents.Client.Run`.
중간 검증:
```bash
cd services/core && go test ./internal/gitosync -run TestProtoSocketTransportClientRunDeliversBinaryBranchUpdatedEvent -count=1
```
### [REVIEW_API-3] Cover malformed and mismatched Struct frames through the side-effect boundary
문제: The current malformed test sends invalid packet bytes. The plan also required valid `structpb.Struct` envelopes with malformed or mismatched semantic content to be dropped or routed to an error path without handler side effects.
해결 방법:
- Through the same real proto-socket transport/client path, send frames such as wrong channel/action, missing payload, malformed `changed_files`, and optional non-Struct or unknown type behavior if feasible.
- Assert the handler is not called for those frames.
- Then send a valid on-target `branch.updated` event and assert the handler is called once, proving the stream remains usable after ignored semantic frames.
테스트 작성:
- Suggested name: `TestProtoSocketTransportClientRunDropsMalformedAndMismatchedStructs`.
- If a non-Struct packet cannot be observed by this parser map, document it in `계획 대비 변경 사항` and cover the explicit parser/drop behavior with the closest deterministic test.
중간 검증:
```bash
cd services/core && go test ./internal/gitosync -run TestProtoSocketTransportClientRunDropsMalformedAndMismatchedStructs -count=1
```
## 수정 파일 요약
| 파일 | 항목 |
| --- | --- |
| `services/core/internal/gitosync/runner.go` | REVIEW_API-1 |
| `services/core/internal/gitosync/runner_test.go` | REVIEW_API-1, REVIEW_API-2, REVIEW_API-3 |
| `services/core/internal/gitoevents/events_test.go` | REVIEW_API-3 if an extra decoder regression is clearer there |
## 최종 검증
```bash
gofmt -w services/core/internal/gitosync/runner.go services/core/internal/gitosync/runner_test.go services/core/internal/gitoevents/events_test.go
git diff --check
cd services/core && go test ./internal/gitosync ./internal/gitoevents ./internal/protosocket
cd services/core && go test ./...
```

View file

@ -0,0 +1,147 @@
<!-- task=m-gito-protosocket-consumer-wire-readiness/02+01_bridge_e2e plan=0 tag=TEST -->
# Code Review Reference - TEST
> **[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.
> Do not ask the user directly, present choices in chat, or call `request_user_input` during implementation; record the needed decision in `사용자 리뷰 요청` and stop for code-review.
> Finalization (`코드리뷰 결과`, log rename, `complete.log`, archive moves, `코드리뷰 전용 체크리스트`) is review-agent-only, even after compaction/resume.
> Follow the ownership table at the bottom of this file for which sections you own.
## 개요
date=2026-06-16
task=m-gito-protosocket-consumer-wire-readiness/02+01_bridge_e2e, plan=0, tag=TEST
## Roadmap Targets
- Milestone: `agent-roadmap/phase/agent-ops-mcp-control-plane/milestones/gito-protosocket-consumer-wire-readiness.md`
- Task ids:
- `bridge-e2e`: wire-compatible consumer, branch scan, `EnqueueRoadmapCreationSync`까지 이어지는 integration test 또는 dev smoke 추가
- Completion mode: check-on-pass
## 이 파일을 읽는 리뷰 에이전트에게
> **[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-gito-protosocket-consumer-wire-readiness/02+01_bridge_e2e/`로 이동한다.
4. PASS이고 task group이 `m-gito-protosocket-consumer-wire-readiness`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다.
5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다.
---
## 구현 항목별 완료 여부
| 항목 | 완료 여부 |
|------|---------|
| [TEST-1] Add wire-to-bridge integration test | [ ] |
| [TEST-2] Keep side-effect boundaries visible in e2e fixture | [ ] |
## 구현 체크리스트
- [ ] 선행 `01_wire_transport_decode`의 active 또는 archived `complete.log`를 확인한 뒤 구현을 시작한다.
- [ ] binary proto-socket test server/client 흐름에서 Gito `event.subscribe``branch.updated` event를 발행하는 integration-style test를 추가한다.
- [ ] fake scanner/reader/enqueuer를 조합해 target branch 재검증 후 `EnqueueRoadmapCreationSync`가 정확히 한 번 호출되는지 assert한다.
- [ ] duplicate event나 no-milestone event가 같은 test fixture에서 추가 enqueue로 번지지 않는지 최소 한 케이스를 포함한다.
- [ ] `git diff --check``cd services/core && go test ./...`를 실행한다.
- [ ] 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하고 `agent-roadmap/current.md`를 ignore하는지 확인한다.
- [ ] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다.
- [ ] PASS이면 active task 디렉터리 `agent-task/m-gito-protosocket-consumer-wire-readiness/02+01_bridge_e2e/``agent-task/archive/YYYY/MM/m-gito-protosocket-consumer-wire-readiness/02+01_bridge_e2e/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다.
- [ ] PASS이고 task group이 `m-gito-protosocket-consumer-wire-readiness`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다.
- [ ] PASS split 작업이면 이동 후 빈 active parent `agent-task/m-gito-protosocket-consumer-wire-readiness/`를 제거하거나, 남은 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로 이동한다.
## 계획 대비 변경 사항
_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._
## 주요 설계 결정
_구현 에이전트가 주요 설계 결정 사항을 기록한다._
## 사용자 리뷰 요청
_기본값은 `없음`이다. 구현 중 사용자 결정, 사용자 소유 외부 환경/secret/서비스 준비, 또는 계획 범위 변경 없이는 안전하게 진행할 수 없으면 아래 항목을 실제 내용으로 교체하고, 구현을 중단한 뒤 active 파일을 그대로 둔 채 리뷰를 요청한다. 구현 에이전트는 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 검증 증거 공백만으로는 사용자 리뷰 요청을 작성하지 않는다._
- 상태: 없음
- 사유 유형: 없음
- 결정 필요: 없음
- 차단 근거: 없음
- 실행한 검증/명령: 없음
- 자동 후속 불가 이유: 없음
- 재개 조건: 없음
## 리뷰어를 위한 체크포인트
- `02+01` predecessor completion이 구현 전에 충족됐는지 확인한다.
- test가 실제 binary proto-socket path를 통과하고 fake transport만 반복하지 않는지 확인한다.
- `EnqueueRoadmapCreationSync`가 정확히 한 번 호출되는 assertion이 있는지 확인한다.
## 검증 결과
_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._
필수 규칙:
- 검증 명령은 고정된 계약이다. 임의로 대체하지 않는다.
- 대체가 필요하면 `계획 대비 변경 사항`에 이유와 대체 명령을 기록한다.
- `검증 결과`에는 실제 stdout/stderr를 붙여 넣는다.
### TEST-1 중간 검증
```bash
cd services/core && go test ./internal/gitosync -run TestWireConsumerEventEnqueuesCreationSyncOnce -count=1
```
### TEST-2 중간 검증
```bash
cd services/core && go test ./internal/gitosync -count=1
```
### 최종 검증
```bash
git diff --check
cd services/core && go test ./...
```
---
> **[IMPLEMENTING AGENT — BEFORE SAVING] Have you filled in every implementation-owned section: completion table, implementation checklist, changes from plan, design decisions, and verification output?**
> If anything is blank, go back and fill it in before saving this file.
> Leave review-agent-only sections unchanged.
## 소유권
| Section | Owner | Note |
|---------|-------|------|
| Header comment, 개요, 리뷰 에이전트 지시 | Fixed at stub creation | Implementing agent must not modify or execute these |
| Roadmap Targets | Fixed at stub creation from plan | Implementing agent must not modify |
| 구현 항목별 완료 여부 | Implementing agent | Check `[ ]` to `[x]` only |
| 구현 체크리스트 | Implementing agent | Check `[ ]` to `[x]` only |
| 코드리뷰 전용 체크리스트 | Review agent only | Implementing agent must not modify |
| 계획 대비 변경 사항, 주요 설계 결정 | Implementing agent | Replace placeholder text with actual content |
| 사용자 리뷰 요청 | Implementing agent | Keep `상태: 없음` unless user input is required |
| 리뷰어를 위한 체크포인트 | Fixed at stub creation | Review focus |
| 검증 결과 | Implementing agent | Paste actual output |

View file

@ -0,0 +1,146 @@
<!-- task=m-gito-protosocket-consumer-wire-readiness/02+01_bridge_e2e plan=0 tag=TEST -->
# Plan - TEST
## 이 파일을 읽는 구현 에이전트에게
`CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채우는 것이 필수입니다. 구현 후 검증을 실행하고 실제 stdout/stderr를 붙인 뒤 active 파일을 그대로 두고 리뷰 준비를 보고하세요. 최종 판정, log rename, `complete.log`, archive 이동은 code-review-skill 전용입니다.
구현 중 사용자만 결정할 수 있는 범위 변경, 사용자 소유 외부 환경/secret, 또는 scope conflict가 없이는 안전하게 진행할 수 없으면 review stub의 `사용자 리뷰 요청` 섹션에 근거와 재개 조건을 채우고 멈추세요. 구현 에이전트는 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않으며, `USER_REVIEW.md`, archive log, `complete.log`를 만들지 않습니다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 닫을 수 있는 증거 공백은 사용자 리뷰가 아닙니다.
## 배경
`gitoevents``gitosync`는 각각 단위 테스트가 있지만, 실제 binary proto-socket consumer에서 branch scan과 `EnqueueRoadmapCreationSync`까지 이어지는 한 흐름의 검증은 없습니다. 이 계획은 선행 binary transport가 완료된 뒤, Gito `branch.updated` 한 건이 target branch 재검증 후 정확히 하나의 creation sync job으로 수렴함을 확인하는 integration-style test를 추가합니다.
## 사용자 리뷰 요청 흐름
구현 중 blocker는 active review stub의 `사용자 리뷰 요청` 섹션에 기록합니다. 구현 에이전트의 직접 사용자 질문은 금지되며, code-review가 해당 요청을 검증하고 실제 `USER_REVIEW.md` 작성 여부를 결정합니다.
## Roadmap Targets
- Milestone: `agent-roadmap/phase/agent-ops-mcp-control-plane/milestones/gito-protosocket-consumer-wire-readiness.md`
- Task ids:
- `bridge-e2e`: wire-compatible consumer, branch scan, `EnqueueRoadmapCreationSync`까지 이어지는 integration test 또는 dev smoke 추가
- Completion mode: check-on-pass
## 분석 결과
### 읽은 파일
- `agent-roadmap/current.md`
- `agent-roadmap/phase/agent-ops-mcp-control-plane/PHASE.md`
- `agent-roadmap/phase/agent-ops-mcp-control-plane/milestones/gito-protosocket-consumer-wire-readiness.md`
- `/config/workspace/gito/agent-contract/index.md`
- `/config/workspace/gito/agent-contract/provided/gito-forgejo-branch-events-v1.md`
- `agent-test/local/rules.md`
- `agent-test/local/core-smoke.md`
- `agent-test/local/workspace-ops-smoke.md`
- `services/core/internal/gitoevents/client.go`
- `services/core/internal/gitoevents/events.go`
- `services/core/internal/gitoevents/client_test.go`
- `services/core/internal/gitoevents/events_test.go`
- `services/core/internal/gitosync/runner.go`
- `services/core/internal/gitosync/bridge.go`
- `services/core/internal/gitosync/bridge_test.go`
- `services/core/internal/gitosync/scanner.go`
- `services/core/internal/gitosync/scanner_test.go`
- `services/core/internal/protosocket/server.go`
- `services/core/internal/protosocket/server_test.go`
- `services/core/internal/protosocket/tasks_test.go`
- `services/core/internal/scheduler/roadmap_sync_jobs.go`
- `services/core/internal/workitem/provider.go`
- `services/core/go.mod`
### 테스트 환경 규칙
`test_env=local`입니다. `agent-test/local/rules.md`, `agent-test/local/core-smoke.md`, `agent-test/local/workspace-ops-smoke.md`를 읽었습니다. 적용 명령은 `cd services/core && go test ./...``git diff --check`입니다. 이 plan은 core integration-style unit test를 추가하므로 Go test cache보다 fresh run이 더 낫고, focused command에는 `-count=1`을 사용합니다.
### 테스트 커버리지 공백
현재 `bridge_test.go`는 fake scanner/reader/enqueuer로 `Bridge.Handle` semantics를 검증합니다. `scanner_test.go`는 git command sequence를 검증합니다. 하지만 실제 wire consumer -> scanner -> bridge -> enqueuer를 한 테스트에서 연결하지 않습니다.
### 심볼 참조
none. Exported API rename/remove 없이 test-only fakes나 helper를 추가합니다.
### 분할 판단
split decision policy를 평가했습니다. 이 plan은 `02+01_bridge_e2e`라서 predecessor index `01`에 의존합니다. 현재 active predecessor는 `agent-task/m-gito-protosocket-consumer-wire-readiness/01_wire_transport_decode/`이고 아직 `complete.log`가 없습니다. 구현은 해당 predecessor가 PASS하여 active 또는 archive `complete.log`가 생긴 뒤 시작해야 합니다.
### 범위 결정 근거
이 plan은 e2e 성격의 테스트와 필요한 test seam만 다룹니다. Production transport 구현은 `01_wire_transport_decode` 범위이고, reconnect/backoff/log 정책은 `03+01_failure_ops` 범위입니다. 실제 Plane API나 real River queue는 사용하지 않고 fake enqueuer로 job args를 검증합니다.
### 빌드 등급
`cloud-G07`입니다. binary protocol, async event delivery, git scan fake, scheduler job args를 한 번에 검증하는 넓은 context가 필요합니다.
## 구현 체크리스트
- [ ] 선행 `01_wire_transport_decode`의 active 또는 archived `complete.log`를 확인한 뒤 구현을 시작한다.
- [ ] binary proto-socket test server/client 흐름에서 Gito `event.subscribe``branch.updated` event를 발행하는 integration-style test를 추가한다.
- [ ] fake scanner/reader/enqueuer를 조합해 target branch 재검증 후 `EnqueueRoadmapCreationSync`가 정확히 한 번 호출되는지 assert한다.
- [ ] duplicate event나 no-milestone event가 같은 test fixture에서 추가 enqueue로 번지지 않는지 최소 한 케이스를 포함한다.
- [ ] `git diff --check``cd services/core && go test ./...`를 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
## 의존 관계 및 구현 순서
이 subtask directory의 `+01`이 runtime dependency source of truth입니다. `01_wire_transport_decode``complete.log`를 만들기 전에는 구현하지 않습니다. 현재 확인 시점에는 predecessor complete evidence가 없습니다.
### [TEST-1] Add wire-to-bridge integration test
문제: [gitosync/bridge.go](/config/workspace/nomadcode/services/core/internal/gitosync/bridge.go:100)는 event를 scan/enqueue로 넘기고 [gitosync/runner.go](/config/workspace/nomadcode/services/core/internal/gitosync/runner.go:88)는 runner를 조립하지만, 둘이 실제 binary event stream과 함께 검증되지 않습니다.
해결 방법: `services/core/internal/gitosync/runner_test.go` 또는 새 test file에서 in-process proto-socket server를 띄우고, `NewRunner`가 연결하게 합니다. server는 subscribe request를 확인한 뒤 contract-shaped `branch.updated` broadcast를 보냅니다. Bridge에는 fake scanner/reader/enqueuer를 연결하고 enqueued job count와 args를 검증합니다.
수정 파일 및 체크리스트:
- [ ] `services/core/internal/gitosync/runner_test.go`에 e2e-style test를 추가한다.
- [ ] fake scanner는 event `After` revision을 읽어 `ScanOutput`을 반환한다.
- [ ] fake enqueuer는 `RoadmapCreationSyncJobArgs`를 capture한다.
- [ ] expected `Ref`, `TodoStateID`, `RoadmapRevision`, `MilestoneMarkdown`, `ExternalSource/ExternalID`를 assert한다.
테스트 작성: 필수. 테스트명 예: `TestWireConsumerEventEnqueuesCreationSyncOnce`.
중간 검증:
```bash
cd services/core && go test ./internal/gitosync -run TestWireConsumerEventEnqueuesCreationSyncOnce -count=1
```
### [TEST-2] Keep side-effect boundaries visible in e2e fixture
문제: [gitoevents/client.go](/config/workspace/nomadcode/services/core/internal/gitoevents/client.go:133)는 milestone path가 없으면 handler를 호출하지 않고, [gitosync/bridge.go](/config/workspace/nomadcode/services/core/internal/gitosync/bridge.go:106)는 duplicate revision을 skip합니다. e2e fixture가 happy path만 검증하면 중복 Plane mutation 방지 근거가 약합니다.
해결 방법: same test file에 duplicate branch.updated event 또는 no-milestone event를 추가하고 enqueuer call count가 1로 유지되는지 확인합니다. 기존 unit tests를 중복하지 않도록 wire path에서 최소 케이스만 둡니다.
수정 파일 및 체크리스트:
- [ ] duplicate event 또는 no-milestone event case를 추가한다.
- [ ] timeout이 있는 channel receive를 써서 async test가 hang하지 않도록 한다.
- [ ] test helper는 1초 안팎 timeout으로 deterministic하게 실패하도록 한다.
테스트 작성: 필수. `TEST-1`과 같은 test 또는 별도 test로 작성합니다.
중간 검증:
```bash
cd services/core && go test ./internal/gitosync -count=1
```
## 수정 파일 요약
| 파일 | 항목 |
| --- | --- |
| `services/core/internal/gitosync/runner_test.go` | TEST-1, TEST-2 |
| `services/core/internal/gitosync/bridge_test.go` | TEST-2 if existing fake helpers are reused |
## 최종 검증
```bash
git diff --check
cd services/core && go test ./...
```
모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.

View file

@ -0,0 +1,147 @@
<!-- task=m-gito-protosocket-consumer-wire-readiness/03+01_failure_ops plan=0 tag=REFACTOR -->
# Code Review Reference - REFACTOR
> **[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.
> Do not ask the user directly, present choices in chat, or call `request_user_input` during implementation; record the needed decision in `사용자 리뷰 요청` and stop for code-review.
> Finalization (`코드리뷰 결과`, log rename, `complete.log`, archive moves, `코드리뷰 전용 체크리스트`) is review-agent-only, even after compaction/resume.
> Follow the ownership table at the bottom of this file for which sections you own.
## 개요
date=2026-06-16
task=m-gito-protosocket-consumer-wire-readiness/03+01_failure_ops, plan=0, tag=REFACTOR
## Roadmap Targets
- Milestone: `agent-roadmap/phase/agent-ops-mcp-control-plane/milestones/gito-protosocket-consumer-wire-readiness.md`
- Task ids:
- `failure-ops`: reconnect, not-ready revision, duplicate event, malformed frame 처리와 로그 기준 정리
- Completion mode: check-on-pass
## 이 파일을 읽는 리뷰 에이전트에게
> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다.
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다.
1. 판정을 append한다.
2. `CODE_REVIEW-cloud-G06.md` -> `code_review_cloud_G06_N.log`, `PLAN-cloud-G06.md` -> `plan_cloud_G06_M.log`로 아카이브한다.
3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/m-gito-protosocket-consumer-wire-readiness/03+01_failure_ops/`로 이동한다.
4. PASS이고 task group이 `m-gito-protosocket-consumer-wire-readiness`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다.
5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다.
---
## 구현 항목별 완료 여부
| 항목 | 완료 여부 |
|------|---------|
| [REFACTOR-1] Define runner shutdown and reconnect behavior | [ ] |
| [REFACTOR-2] Pin malformed frame and duplicate/no-ready side-effect behavior | [ ] |
## 구현 체크리스트
- [ ] 선행 `01_wire_transport_decode`의 active 또는 archived `complete.log`를 확인한 뒤 구현을 시작한다.
- [ ] runner transport disconnect/normal shutdown/malformed binary frame behavior를 분류하고, core server shutdown을 error spam으로 만들지 않는 기준을 구현한다.
- [ ] reconnect 또는 retry policy를 최소 MVP로 정한다. 자동 reconnect를 넣는다면 context cancel과 backoff 상한을 테스트한다. 넣지 않는다면 runner stop/error reporting 기준을 명시하고 test로 고정한다.
- [ ] 기존 not-ready revision과 duplicate event tests가 binary path 변경 뒤에도 중복 Plane mutation을 막는지 보강한다.
- [ ] `git diff --check``cd services/core && go test ./...`를 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
## 코드리뷰 전용 체크리스트
> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다.
> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다.
- [ ] `코드리뷰 결과``PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
- [ ] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다.
- [ ] active `CODE_REVIEW-*-G??.md``code_review_cloud_G06_N.log`로 아카이브한다.
- [ ] active `PLAN-*-G??.md``plan_cloud_G06_M.log`로 아카이브한다.
- [ ] `.gitignore`의 Agent-Ops 관리 block이 `agent-task/**/*.md``agent-task/**/*.log`를 unignore하고 `agent-roadmap/current.md`를 ignore하는지 확인한다.
- [ ] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다.
- [ ] PASS이면 active task 디렉터리 `agent-task/m-gito-protosocket-consumer-wire-readiness/03+01_failure_ops/``agent-task/archive/YYYY/MM/m-gito-protosocket-consumer-wire-readiness/03+01_failure_ops/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다.
- [ ] PASS이고 task group이 `m-gito-protosocket-consumer-wire-readiness`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다.
- [ ] PASS split 작업이면 이동 후 빈 active parent `agent-task/m-gito-protosocket-consumer-wire-readiness/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다.
- [ ] WARN/FAIL이고 user-review gate가 트리거되지 않았으면 다음 active `PLAN-cloud-G06.md``CODE_REVIEW-cloud-G06.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로 이동한다.
## 계획 대비 변경 사항
_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._
## 주요 설계 결정
_구현 에이전트가 주요 설계 결정 사항을 기록한다._
## 사용자 리뷰 요청
_기본값은 `없음`이다. 구현 중 사용자 결정, 사용자 소유 외부 환경/secret/서비스 준비, 또는 계획 범위 변경 없이는 안전하게 진행할 수 없으면 아래 항목을 실제 내용으로 교체하고, 구현을 중단한 뒤 active 파일을 그대로 둔 채 리뷰를 요청한다. 구현 에이전트는 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 검증 증거 공백만으로는 사용자 리뷰 요청을 작성하지 않는다._
- 상태: 없음
- 사유 유형: 없음
- 결정 필요: 없음
- 차단 근거: 없음
- 실행한 검증/명령: 없음
- 자동 후속 불가 이유: 없음
- 재개 조건: 없음
## 리뷰어를 위한 체크포인트
- `03+01` predecessor completion이 구현 전에 충족됐는지 확인한다.
- normal context cancellation이 noisy error로 보고되지 않는지 확인한다.
- malformed frame/duplicate/not-ready 입력이 Plane mutation이나 process shutdown으로 번지지 않는지 확인한다.
## 검증 결과
_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._
필수 규칙:
- 검증 명령은 고정된 계약이다. 임의로 대체하지 않는다.
- 대체가 필요하면 `계획 대비 변경 사항`에 이유와 대체 명령을 기록한다.
- `검증 결과`에는 실제 stdout/stderr를 붙여 넣는다.
### REFACTOR-1 중간 검증
```bash
cd services/core && go test ./internal/gitosync -run 'TestRunner(StopsCleanly|Reconnects)' -count=1
```
### REFACTOR-2 중간 검증
```bash
cd services/core && go test ./internal/gitoevents ./internal/gitosync -count=1
```
### 최종 검증
```bash
git diff --check
cd services/core && go test ./...
```
---
> **[IMPLEMENTING AGENT — BEFORE SAVING] Have you filled in every implementation-owned section: completion table, implementation checklist, changes from plan, design decisions, and verification output?**
> If anything is blank, go back and fill it in before saving this file.
> Leave review-agent-only sections unchanged.
## 소유권
| Section | Owner | Note |
|---------|-------|------|
| Header comment, 개요, 리뷰 에이전트 지시 | Fixed at stub creation | Implementing agent must not modify or execute these |
| Roadmap Targets | Fixed at stub creation from plan | Implementing agent must not modify |
| 구현 항목별 완료 여부 | Implementing agent | Check `[ ]` to `[x]` only |
| 구현 체크리스트 | Implementing agent | Check `[ ]` to `[x]` only |
| 코드리뷰 전용 체크리스트 | Review agent only | Implementing agent must not modify |
| 계획 대비 변경 사항, 주요 설계 결정 | Implementing agent | Replace placeholder text with actual content |
| 사용자 리뷰 요청 | Implementing agent | Keep `상태: 없음` unless user input is required |
| 리뷰어를 위한 체크포인트 | Fixed at stub creation | Review focus |
| 검증 결과 | Implementing agent | Paste actual output |

View file

@ -0,0 +1,146 @@
<!-- task=m-gito-protosocket-consumer-wire-readiness/03+01_failure_ops plan=0 tag=REFACTOR -->
# Plan - REFACTOR
## 이 파일을 읽는 구현 에이전트에게
`CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채우는 것이 필수입니다. 구현 후 검증을 실행하고 실제 stdout/stderr를 붙인 뒤 active 파일을 그대로 두고 리뷰 준비를 보고하세요. 최종 판정, log rename, `complete.log`, archive 이동은 code-review-skill 전용입니다.
구현 중 사용자만 결정할 수 있는 범위 변경, 사용자 소유 외부 환경/secret, 또는 scope conflict가 없이는 안전하게 진행할 수 없으면 review stub의 `사용자 리뷰 요청` 섹션에 근거와 재개 조건을 채우고 멈추세요. 구현 에이전트는 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않으며, `USER_REVIEW.md`, archive log, `complete.log`를 만들지 않습니다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 닫을 수 있는 증거 공백은 사용자 리뷰가 아닙니다.
## 배경
Gito consumer는 장기 실행 subscription입니다. malformed frame, not-ready revision, duplicate revision은 이미 일부 단위 테스트가 있지만, binary transport 교체 후 reconnect/normal shutdown/logging 기준이 명확하지 않으면 core server shutdown이나 중복 Plane mutation으로 이어질 수 있습니다. 이 계획은 선행 binary transport 위에서 장애 입력이 safe drop/retry/no-op으로 수렴하는 운영 기준을 정리합니다.
## 사용자 리뷰 요청 흐름
구현 중 blocker는 active review stub의 `사용자 리뷰 요청` 섹션에 기록합니다. 구현 에이전트의 직접 사용자 질문은 금지되며, code-review가 해당 요청을 검증하고 실제 `USER_REVIEW.md` 작성 여부를 결정합니다.
## Roadmap Targets
- Milestone: `agent-roadmap/phase/agent-ops-mcp-control-plane/milestones/gito-protosocket-consumer-wire-readiness.md`
- Task ids:
- `failure-ops`: reconnect, not-ready revision, duplicate event, malformed frame 처리와 로그 기준 정리
- Completion mode: check-on-pass
## 분석 결과
### 읽은 파일
- `agent-roadmap/current.md`
- `agent-roadmap/phase/agent-ops-mcp-control-plane/PHASE.md`
- `agent-roadmap/phase/agent-ops-mcp-control-plane/milestones/gito-protosocket-consumer-wire-readiness.md`
- `/config/workspace/gito/agent-contract/index.md`
- `/config/workspace/gito/agent-contract/provided/gito-forgejo-branch-events-v1.md`
- `agent-test/local/rules.md`
- `agent-test/local/core-smoke.md`
- `agent-test/local/workspace-ops-smoke.md`
- `services/core/internal/gitoevents/client.go`
- `services/core/internal/gitoevents/client_test.go`
- `services/core/internal/gitoevents/events.go`
- `services/core/internal/gitoevents/events_test.go`
- `services/core/internal/gitosync/runner.go`
- `services/core/internal/gitosync/bridge.go`
- `services/core/internal/gitosync/bridge_test.go`
- `services/core/internal/gitosync/scanner.go`
- `services/core/internal/gitosync/scanner_test.go`
- `services/core/cmd/server/main.go`
- `services/core/internal/config/config.go`
- `services/core/internal/config/config_test.go`
- `/config/go/pkg/mod/git.toki-labs.com/toki/proto-socket/go@v0.0.0-20260530100557-fed0da37fa1a/ws_client.go`
- `/config/go/pkg/mod/git.toki-labs.com/toki/proto-socket/go@v0.0.0-20260530100557-fed0da37fa1a/communicator.go`
### 테스트 환경 규칙
`test_env=local`입니다. `agent-test/local/rules.md`, `agent-test/local/core-smoke.md`, `agent-test/local/workspace-ops-smoke.md`를 읽었습니다. 적용 명령은 `cd services/core && go test ./...``git diff --check`입니다. failure/reconnect tests should use `-count=1` for fresh async behavior evidence.
### 테스트 커버리지 공백
`Bridge.Handle` duplicate/not-ready behavior는 [bridge_test.go](/config/workspace/nomadcode/services/core/internal/gitosync/bridge_test.go:209)와 [bridge_test.go](/config/workspace/nomadcode/services/core/internal/gitosync/bridge_test.go:226)에 있습니다. `gitoevents.Client` drops malformed/off-target envelopes in [client_test.go](/config/workspace/nomadcode/services/core/internal/gitoevents/client_test.go:113). Missing coverage: binary transport disconnect/reconnect behavior, normal shutdown error classification, malformed binary frame logging after `01_wire_transport_decode`.
### 심볼 참조
none expected. If implementing reconnect introduces new unexported types, no exported call-site update is needed. If `NewRunner` signature changes, grep all references before editing.
### 분할 판단
split decision policy를 평가했습니다. 이 plan은 `03+01_failure_ops`라서 predecessor index `01`에 의존합니다. 현재 active predecessor는 `agent-task/m-gito-protosocket-consumer-wire-readiness/01_wire_transport_decode/`이고 아직 `complete.log`가 없습니다. `bridge-e2e`와는 독립적으로, binary transport가 생긴 뒤 운영 실패 기준을 보강할 수 있습니다.
### 범위 결정 근거
이 plan은 reconnect/shutdown/malformed/drop/log criteria만 다룹니다. Producer-side Gito dedupe, durable listener persistence, Plane lifecycle state transition은 마일스톤 범위 제외 또는 후속 범위입니다. Full external dev smoke는 `bridge-e2e`와 별도입니다.
### 빌드 등급
`cloud-G06`입니다. 비동기 transport lifecycle과 log/error classification이 있지만 scope는 `gitoevents`/`gitosync`로 제한됩니다.
## 구현 체크리스트
- [ ] 선행 `01_wire_transport_decode`의 active 또는 archived `complete.log`를 확인한 뒤 구현을 시작한다.
- [ ] runner transport disconnect/normal shutdown/malformed binary frame behavior를 분류하고, core server shutdown을 error spam으로 만들지 않는 기준을 구현한다.
- [ ] reconnect 또는 retry policy를 최소 MVP로 정한다. 자동 reconnect를 넣는다면 context cancel과 backoff 상한을 테스트한다. 넣지 않는다면 runner stop/error reporting 기준을 명시하고 test로 고정한다.
- [ ] 기존 not-ready revision과 duplicate event tests가 binary path 변경 뒤에도 중복 Plane mutation을 막는지 보강한다.
- [ ] `git diff --check``cd services/core && go test ./...`를 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
## 의존 관계 및 구현 순서
이 subtask directory의 `+01`이 runtime dependency source of truth입니다. `01_wire_transport_decode``complete.log`를 만들기 전에는 구현하지 않습니다. 현재 확인 시점에는 predecessor complete evidence가 없습니다.
### [REFACTOR-1] Define runner shutdown and reconnect behavior
문제: [main.go](/config/workspace/nomadcode/services/core/cmd/server/main.go:129)는 runner를 goroutine으로 시작하고 returned error를 로그합니다. [gitoevents/client.go](/config/workspace/nomadcode/services/core/internal/gitoevents/client.go:96)는 context cancel이면 nil을 반환하지만 transport disconnect와 malformed binary frame은 runner error로 전파될 수 있습니다. 기준이 없으면 정상 shutdown과 장애가 같은 로그로 보일 수 있습니다.
해결 방법: context cancel/normal close는 nil 또는 non-error log로 정리합니다. unexpected disconnect는 either bounded reconnect loop or explicit retryable error log로 고정합니다. 자동 reconnect를 넣으면 `NewRunner` 내부가 아니라 runner wrapper/helper로 분리해 testable하게 둡니다.
수정 파일 및 체크리스트:
- [ ] `services/core/internal/gitosync/runner.go`에서 close/disconnect error classification을 정리한다.
- [ ] `services/core/internal/gitosync/runner_test.go`에 normal shutdown과 unexpected close test를 추가한다.
- [ ] 필요 시 `cmd/server/main.go` log message가 secret/payload 없이 운영 필드만 남기는지 확인한다.
테스트 작성: 필수. 테스트명 예: `TestRunnerStopsCleanlyOnContextCancel`와 reconnect를 구현한다면 `TestRunnerReconnectsAfterUnexpectedDisconnect`.
중간 검증:
```bash
cd services/core && go test ./internal/gitosync -run 'TestRunner(StopsCleanly|Reconnects)' -count=1
```
### [REFACTOR-2] Pin malformed frame and duplicate/no-ready side-effect behavior
문제: [client_test.go](/config/workspace/nomadcode/services/core/internal/gitoevents/client_test.go:113)는 logical malformed envelope을 drop합니다. binary decode가 들어오면 malformed frame은 transport/decode boundary에서 발생하므로 별도 regression이 필요합니다.
해결 방법: malformed binary frame, wrong channel/action, missing payload, unknown metadata/custom field가 handler/enqueuer를 호출하지 않는 tests를 둡니다. 기존 duplicate/not-ready tests는 유지하고, binary path와 결합되는 최소 regression을 추가합니다.
수정 파일 및 체크리스트:
- [ ] `services/core/internal/gitosync/runner_test.go`에 malformed binary frame no-side-effect test를 추가한다.
- [ ] `services/core/internal/gitosync/bridge_test.go` 기존 duplicate/not-ready tests를 유지하고 필요한 assertion만 보강한다.
- [ ] logs에 payload 원문이나 secret이 남지 않도록 assert 가능한 부분만 확인한다.
테스트 작성: 필수. 테스트명 예: `TestRunnerDropsMalformedBinaryFrameWithoutEnqueue`.
중간 검증:
```bash
cd services/core && go test ./internal/gitoevents ./internal/gitosync -count=1
```
## 수정 파일 요약
| 파일 | 항목 |
| --- | --- |
| `services/core/internal/gitosync/runner.go` | REFACTOR-1, REFACTOR-2 |
| `services/core/internal/gitosync/runner_test.go` | REFACTOR-1, REFACTOR-2 |
| `services/core/internal/gitosync/bridge_test.go` | REFACTOR-2 if duplicate/not-ready assertions need expansion |
| `services/core/cmd/server/main.go` | REFACTOR-1 if runner error log needs classification |
## 최종 검증
```bash
git diff --check
cd services/core && go test ./...
```
모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.

View file

@ -102,6 +102,18 @@ Jira 연동은 Jira Cloud Basic auth(`Email:APIToken`) 형식을 사용합니다
Mattermost task completion notification은 `MATTERMOST_BASE_URL`, `MATTERMOST_TOKEN`, `MATTERMOST_CHANNEL_ID` 환경 변수를 사용해 `POST /api/v4/posts`로 발송합니다. 설정이 누락되거나 Mattermost가 non-2xx를 반환하면 core는 notification projection 오류를 기록하되 canonical task completion 상태를 롤백하지 않습니다.
Gito branch event consumer는 Gito `/proto-socket` endpoint에서 `branch.updated` wakeup을 받고, local develop checkout을 fetch/scan한 뒤 Plane-origin roadmap creation sync job으로 넘깁니다. `GITO_PROTO_SOCKET_URL`, `GITO_REPO_ID`, `GITO_DEVELOP_REPO_PATH`, `ROADMAP_CREATION_TODO_STATE_ID` 또는 `PLANE_TODO_STATE_ID`가 모두 있어야 켜지며, `GITO_BRANCH` 기본값은 `develop`, `GITO_REMOTE_NAME` 기본값은 `origin`입니다. `ROADMAP_CREATION_TODO_STATE_ID`를 설정하면 `PLANE_TODO_STATE_ID`보다 우선합니다.
```bash
GITO_PROTO_SOCKET_URL="ws://<gito-host>/proto-socket" \
GITO_REPO_ID="<gito-repo-id>" \
GITO_DEVELOP_REPO_PATH="/path/to/develop-checkout" \
GITO_BRANCH="develop" \
GITO_REMOTE_NAME="origin" \
ROADMAP_CREATION_TODO_STATE_ID="<plane-todo-state-id>" \
./bin/run
```
```bash
PLANE_BASE_URL="https://plane.toki-labs.com" \
PLANE_TOKEN="<plane-api-key>" \
@ -174,6 +186,8 @@ DB 접근 코드는 `migrations/`의 스키마와 `queries/`의 SQL을 기준으
Docker Compose 실행은 dev 배포용입니다. compose는 외부 Docker 네트워크 `net_nginx`에 붙고, 같은 네트워크의 `code-server-postgres``nomad-core-dev` DB로, `code-server-redis`의 Redis DB 4번과 `nomadcode-core:dev` key prefix로 접속합니다. dev Redis 주소나 prefix를 바꿀 때는 local 실행용 `REDIS_URL`, `REDIS_KEY_PREFIX`와 분리된 `DEV_REDIS_URL`, `DEV_REDIS_KEY_PREFIX`를 사용합니다. 외부 노출 테스트에서는 `AUTH_PASSWORD`를 함께 지정합니다.
Compose도 Gito consumer env를 빈 기본값으로 전달하므로, 필요한 값과 local checkout volume이 준비된 경우에만 consumer가 활성화됩니다. `GITO_DEVELOP_REPO_PATH`는 container에서 접근 가능한 경로여야 합니다.
```bash
AUTH_PASSWORD="change-me" ./bin/docker-up
```

View file

@ -30,6 +30,13 @@ services:
PLANE_CREATION_BACKLOG_STATE_ID: ${PLANE_CREATION_BACKLOG_STATE_ID:-62d4c50c-0cea-4a76-a0ed-ec97498b2d5f}
PLANE_AGENT_ASSIGNEE_ID: ${PLANE_AGENT_ASSIGNEE_ID:-5d116a77-d3df-4f54-80bf-eca61e0118c4}
PLANE_SELF_ACTOR_ID: ${PLANE_SELF_ACTOR_ID:-}
GITO_PROTO_SOCKET_URL: ${GITO_PROTO_SOCKET_URL:-}
GITO_REPO_ID: ${GITO_REPO_ID:-}
GITO_DEVELOP_REPO_PATH: ${GITO_DEVELOP_REPO_PATH:-}
GITO_BRANCH: ${GITO_BRANCH:-develop}
GITO_REMOTE_NAME: ${GITO_REMOTE_NAME:-origin}
ROADMAP_CREATION_TODO_STATE_ID: ${ROADMAP_CREATION_TODO_STATE_ID:-}
PLANE_TODO_STATE_ID: ${PLANE_TODO_STATE_ID:-}
ports:
- "${NOMADCODE_CORE_HOST_PORT:-18010}:8080"
volumes:

View file

@ -98,6 +98,59 @@ func TestDecodeBranchUpdatedEnvelope(t *testing.T) {
}
}
func TestDecodeBranchUpdatedEnvelopeAcceptsConsumerNeutralBasePayload(t *testing.T) {
env := protosocket.Envelope{
ProtocolVersion: protosocket.ProtocolVersion,
ID: "event-id",
Type: "event",
Channel: EventChannel,
Action: BranchUpdatedAction,
Payload: map[string]any{
"id": "event-id",
"type": BranchUpdatedAction,
"repo_id": "nomadcode",
"branch": "develop",
"before": "old-sha",
"after": "new-sha",
"changed_files": []any{
map[string]any{
"path": "agent-roadmap/phase/example/milestones/example.md",
"change_type": "modified",
},
},
},
}
ev, err := DecodeBranchUpdatedEnvelope(env)
if err != nil {
t.Fatalf("decode consumer-neutral payload: unexpected error: %v", err)
}
if ev.RepoID != "nomadcode" || ev.Branch != "develop" || ev.After != "new-sha" {
t.Fatalf("decoded event: got %+v", ev)
}
if len(ev.MilestoneChangedFiles()) != 1 {
t.Fatalf("milestone wakeup hint: got %+v", ev.MilestoneChangedFiles())
}
}
func TestDecodeBranchUpdatedEnvelopeIgnoresOptionalConsumerExtensions(t *testing.T) {
env := branchUpdatedEnvelope()
env.Payload["metadata"] = map[string]any{
"nomadcode": map[string]any{"ignored": true},
}
env.Payload["custom"] = map[string]any{
"consumer": "nomadcode",
}
ev, err := DecodeBranchUpdatedEnvelope(env)
if err != nil {
t.Fatalf("decode with optional extensions: unexpected error: %v", err)
}
if ev.RepoID != "nomadcode" || ev.Branch != "develop" {
t.Fatalf("decoded event: got %+v", ev)
}
}
func TestDecodeBranchUpdatedEnvelopeRejectsWrongAction(t *testing.T) {
env := branchUpdatedEnvelope()
env.Action = "task.status.changed"

View file

@ -2,16 +2,19 @@ package gitosync
import (
"context"
"encoding/json"
"errors"
"io"
"log/slog"
"os/exec"
"strings"
"sync"
"nhooyr.io/websocket"
toki "git.toki-labs.com/toki/proto-socket/go"
"github.com/nomadcode/nomadcode-core/internal/gitoevents"
"github.com/nomadcode/nomadcode-core/internal/protosocket"
"google.golang.org/protobuf/types/known/structpb"
)
// ExecCommandRunner is the production CommandRunner backed by os/exec. It runs
@ -29,54 +32,111 @@ func (ExecCommandRunner) Run(ctx context.Context, dir, name string, args ...stri
return string(out), nil
}
// websocketTransport is the production gitoevents.Transport that dials the Gito
// proto-socket endpoint over a websocket and exchanges JSON envelopes. It is
// kept unexported; callers wire it through NewRunner.
type websocketTransport struct {
url string
conn *websocket.Conn
// protoSocketTransport is the production gitoevents.Transport that dials the Gito
// proto-socket endpoint over a websocket and exchanges binary protobuf envelopes.
// It is kept unexported; callers wire it through NewRunner.
type protoSocketTransport struct {
url string
conn *websocket.Conn
client *toki.WsClient
inbound chan struct {
env protosocket.Envelope
err error
}
closeOnce sync.Once
closed chan struct{}
inboundCap int
}
func (t *websocketTransport) Connect(ctx context.Context) error {
func (t *protoSocketTransport) Connect(ctx context.Context) error {
conn, _, err := websocket.Dial(ctx, t.url, nil)
if err != nil {
return err
}
t.conn = conn
t.closed = make(chan struct{})
cap := t.inboundCap
if cap <= 0 {
cap = 100
}
t.client = toki.NewWsClient(conn, 0, 0, protosocket.ParserMap())
t.inbound = make(chan struct {
env protosocket.Envelope
err error
}, cap)
toki.AddListenerTyped[*structpb.Struct](&t.client.Communicator, func(msg *structpb.Struct) {
env, err := protosocket.EnvelopeFromStruct(msg)
t.enqueue(struct {
env protosocket.Envelope
err error
}{env: env, err: err})
})
t.client.AddDisconnectListener(func(c *toki.WsClient) {
t.enqueue(struct {
env protosocket.Envelope
err error
}{err: io.EOF})
})
return nil
}
func (t *websocketTransport) Write(ctx context.Context, env protosocket.Envelope) error {
if t.conn == nil {
func (t *protoSocketTransport) enqueue(item struct {
env protosocket.Envelope
err error
}) {
select {
case <-t.closed:
case t.inbound <- item:
}
}
func (t *protoSocketTransport) Write(ctx context.Context, env protosocket.Envelope) error {
if t.client == nil {
return errors.New("gitosync: transport not connected")
}
data, err := json.Marshal(env)
msg, err := env.ToStruct()
if err != nil {
return err
}
return t.conn.Write(ctx, websocket.MessageText, data)
return t.client.Communicator.Send(msg)
}
func (t *websocketTransport) Read(ctx context.Context) (protosocket.Envelope, error) {
if t.conn == nil {
func (t *protoSocketTransport) Read(ctx context.Context) (protosocket.Envelope, error) {
if t.client == nil {
return protosocket.Envelope{}, errors.New("gitosync: transport not connected")
}
_, data, err := t.conn.Read(ctx)
if err != nil {
return protosocket.Envelope{}, err
select {
case <-ctx.Done():
return protosocket.Envelope{}, ctx.Err()
case <-t.closed:
return protosocket.Envelope{}, io.EOF
case item, ok := <-t.inbound:
if !ok {
return protosocket.Envelope{}, io.EOF
}
if item.err != nil {
return protosocket.Envelope{}, item.err
}
return item.env, nil
}
var env protosocket.Envelope
if err := json.Unmarshal(data, &env); err != nil {
return protosocket.Envelope{}, err
}
return env, nil
}
func (t *websocketTransport) Close() error {
if t.conn == nil {
return nil
}
return t.conn.Close(websocket.StatusNormalClosure, "")
func (t *protoSocketTransport) Close() error {
var err error
t.closeOnce.Do(func() {
if t.closed != nil {
close(t.closed)
}
if t.client != nil {
err = t.client.Close()
}
})
return err
}
// NewRunner builds the gitoevents client wired to forward on-target branch
@ -92,7 +152,7 @@ func NewRunner(url, repoID, branch string, bridge *Bridge, logger *slog.Logger)
if bridge == nil {
return nil, errors.New("gitosync: bridge is nil")
}
transport := &websocketTransport{url: url}
transport := &protoSocketTransport{url: url}
handler := func(ctx context.Context, ev gitoevents.BranchUpdatedEvent) {
if err := bridge.Handle(ctx, ev); err != nil {
if logger != nil {

View file

@ -0,0 +1,387 @@
package gitosync
import (
"context"
"errors"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
toki "git.toki-labs.com/toki/proto-socket/go"
"github.com/nomadcode/nomadcode-core/internal/gitoevents"
"github.com/nomadcode/nomadcode-core/internal/protosocket"
"google.golang.org/protobuf/types/known/structpb"
"nhooyr.io/websocket"
)
// branchUpdatedEnvelope builds a contract-shaped broadcast envelope fixture.
func branchUpdatedEnvelope() protosocket.Envelope {
return protosocket.Envelope{
ProtocolVersion: protosocket.ProtocolVersion,
ID: "event-id",
Type: "event",
Channel: "event",
Action: "branch.updated",
Payload: map[string]any{
"id": "event-id",
"type": "branch.updated",
"provider": "forgejo",
"delivery_id": "delivery-id",
"repo_id": "nomadcode",
"branch": "develop",
"before": "old-sha",
"after": "new-sha",
"changed_files": []any{
map[string]any{
"path": "agent-roadmap/phase/example/milestones/example.md",
"change_type": "modified",
},
map[string]any{
"path": "README.md",
"change_type": "modified",
},
},
"observed_at": "2026-06-13T00:00:00Z",
"created_at": "2026-06-13T00:00:00Z",
},
}
}
// TestProtoSocketTransportConnectAndClose verifies basic connect/close using toki.WsClient.
func TestProtoSocketTransportConnectAndClose(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
conn, err := websocket.Accept(w, r, nil)
if err != nil {
return
}
defer conn.Close(websocket.StatusInternalError, "")
ctx := r.Context()
for {
_, _, err := conn.Read(ctx)
if err != nil {
break
}
}
}))
defer srv.Close()
wsURL := strings.Replace(srv.URL, "http", "ws", 1)
transport := &protoSocketTransport{url: wsURL}
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
if err := transport.Connect(ctx); err != nil {
t.Fatalf("Connect failed: %v", err)
}
if transport.client == nil || !transport.client.IsAlive() {
t.Fatal("client should be alive")
}
if err := transport.Close(); err != nil {
t.Fatalf("Close failed: %v", err)
}
}
// TestProtoSocketTransportOverflowAndDisconnect implements REVIEW_API-1.
// It proves backpressure on overflow and that close/disconnect terminal signals
// are not silently dropped.
func TestProtoSocketTransportOverflowAndDisconnect(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
conn, err := websocket.Accept(w, r, nil)
if err != nil {
return
}
serverClient := toki.NewWsClient(conn, 0, 0, protosocket.ParserMap())
defer serverClient.Close()
// Send 3 events.
// Since buffer capacity is 1, the client will block (backpressure) during the second or third send.
eventEnv := branchUpdatedEnvelope()
eventStruct, _ := eventEnv.ToStruct()
_ = serverClient.Send(eventStruct)
_ = serverClient.Send(eventStruct)
_ = serverClient.Send(eventStruct)
// Wait briefly then close the server-side client to trigger a disconnect
time.Sleep(100 * time.Millisecond)
}))
defer srv.Close()
wsURL := strings.Replace(srv.URL, "http", "ws", 1)
transport := &protoSocketTransport{
url: wsURL,
inboundCap: 1, // Set small buffer capacity to test overflow/backpressure
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := transport.Connect(ctx); err != nil {
t.Fatalf("Connect failed: %v", err)
}
defer transport.Close()
// Wait to let server send events and then disconnect
time.Sleep(200 * time.Millisecond)
// We expect to read events successfully, and finally observe the disconnect EOF without getting stuck.
// 1st Read
env1, err := transport.Read(ctx)
if err != nil {
t.Fatalf("Read 1 failed: %v", err)
}
if env1.Action != "branch.updated" {
t.Errorf("expected branch.updated event, got: %s", env1.Action)
}
// 2nd Read
env2, err := transport.Read(ctx)
if err != nil {
t.Fatalf("Read 2 failed: %v", err)
}
if env2.Action != "branch.updated" {
t.Errorf("expected branch.updated event, got: %s", env2.Action)
}
// 3rd Read (Could either be 3rd event or EOF, depending on toki read timing, but must return either)
_, err = transport.Read(ctx)
// Eventually it must return EOF. We do another read just to ensure EOF.
_, err = transport.Read(ctx)
if err == nil {
t.Fatal("expected terminal EOF error, but got nil")
}
if !errors.Is(err, io.EOF) && !strings.Contains(err.Error(), "context") {
t.Fatalf("expected EOF error, got: %v", err)
}
}
// TestProtoSocketTransportClientRunDeliversBinaryBranchUpdatedEvent implements REVIEW_API-2.
// It verifies that gitoevents.Client.Run works with the production transport and mock server.
func TestProtoSocketTransportClientRunDeliversBinaryBranchUpdatedEvent(t *testing.T) {
subscribeCh := make(chan *structpb.Struct, 1)
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
conn, err := websocket.Accept(w, r, nil)
if err != nil {
return
}
serverClient := toki.NewWsClient(conn, 0, 0, protosocket.ParserMap())
defer serverClient.Close()
toki.AddRequestListenerTyped[*structpb.Struct, *structpb.Struct](&serverClient.Communicator, func(req *structpb.Struct) (*structpb.Struct, error) {
env, err := protosocket.EnvelopeFromStruct(req)
if err != nil {
return nil, err
}
if env.Channel == "event" && env.Action == "event.subscribe" {
subscribeCh <- req
resEnv := protosocket.Envelope{
ProtocolVersion: protosocket.ProtocolVersion,
ID: "res-id",
CorrelationID: env.ID,
Type: "response",
Channel: "event",
Action: "event.subscribe",
}
resStruct, _ := resEnv.ToStruct()
go func() {
time.Sleep(50 * time.Millisecond)
eventEnv := branchUpdatedEnvelope()
eventStruct, _ := eventEnv.ToStruct()
_ = serverClient.Send(eventStruct)
}()
return resStruct, nil
}
return nil, errors.New("unexpected request")
})
<-r.Context().Done()
}))
defer srv.Close()
wsURL := strings.Replace(srv.URL, "http", "ws", 1)
transport := &protoSocketTransport{url: wsURL}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
handlerCh := make(chan gitoevents.BranchUpdatedEvent, 1)
handler := func(ctx context.Context, ev gitoevents.BranchUpdatedEvent) {
handlerCh <- ev
}
client, err := gitoevents.NewClient(transport, gitoevents.Options{
RepoID: "nomadcode",
Branch: "develop",
Handler: handler,
})
if err != nil {
t.Fatalf("NewClient failed: %v", err)
}
runErrCh := make(chan error, 1)
go func() {
runErrCh <- client.Run(ctx)
}()
// Verify subscribe request was received on the server
select {
case reqStruct := <-subscribeCh:
env, err := protosocket.EnvelopeFromStruct(reqStruct)
if err != nil {
t.Fatalf("invalid subscribe request: %v", err)
}
if env.Channel != "event" || env.Action != "event.subscribe" {
t.Fatalf("unexpected subscribe request: %+v", env)
}
events, ok := env.Payload["events"].([]any)
if !ok || len(events) != 1 || events[0] != "branch.updated" {
t.Fatalf("unexpected subscribe request events: %v", env.Payload["events"])
}
case <-time.After(2 * time.Second):
t.Fatal("timed out waiting for subscribe request")
}
// Verify event was received by the handler
select {
case ev := <-handlerCh:
if ev.RepoID != "nomadcode" || ev.Branch != "develop" || ev.After != "new-sha" {
t.Fatalf("unexpected branch updated event: %+v", ev)
}
case <-time.After(2 * time.Second):
t.Fatal("timed out waiting for branch updated event")
}
// Cancel context and verify client exits cleanly
cancel()
select {
case runErr := <-runErrCh:
if runErr != nil {
t.Fatalf("expected nil error on clean exit, got: %v", runErr)
}
case <-time.After(2 * time.Second):
t.Fatal("timed out waiting for Client.Run to exit")
}
}
// TestProtoSocketTransportClientRunDropsMalformedAndMismatchedStructs implements REVIEW_API-3.
// It verifies that malformed or mismatched structpb.Struct envelopes are safely ignored
// without causing handler side effects or breaking the stream.
func TestProtoSocketTransportClientRunDropsMalformedAndMismatchedStructs(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
conn, err := websocket.Accept(w, r, nil)
if err != nil {
return
}
serverClient := toki.NewWsClient(conn, 0, 0, protosocket.ParserMap())
defer serverClient.Close()
toki.AddRequestListenerTyped[*structpb.Struct, *structpb.Struct](&serverClient.Communicator, func(req *structpb.Struct) (*structpb.Struct, error) {
env, _ := protosocket.EnvelopeFromStruct(req)
if env.Action == "event.subscribe" {
resEnv := protosocket.Envelope{
ProtocolVersion: protosocket.ProtocolVersion,
ID: "res-id",
CorrelationID: env.ID,
Type: "response",
Channel: "event",
Action: "event.subscribe",
}
resStruct, _ := resEnv.ToStruct()
go func() {
time.Sleep(50 * time.Millisecond)
// 1. Wrong channel/action
wrongEnv := branchUpdatedEnvelope()
wrongEnv.Channel = "wrong"
wrongEnv.Action = "wrong"
wrongStruct, _ := wrongEnv.ToStruct()
_ = serverClient.Send(wrongStruct)
// 2. Missing payload
missingEnv := branchUpdatedEnvelope()
missingEnv.Payload = nil
missingStruct, _ := missingEnv.ToStruct()
_ = serverClient.Send(missingStruct)
// 3. Malformed changed_files
malformedEnv := branchUpdatedEnvelope()
malformedEnv.Payload["changed_files"] = "not-a-list"
malformedStruct, _ := malformedEnv.ToStruct()
_ = serverClient.Send(malformedStruct)
// Wait a bit to ensure Client.handleEnvelope processes drops
time.Sleep(50 * time.Millisecond)
// 4. Send valid target event
validEnv := branchUpdatedEnvelope()
validStruct, _ := validEnv.ToStruct()
_ = serverClient.Send(validStruct)
}()
return resStruct, nil
}
return nil, errors.New("unexpected request")
})
<-r.Context().Done()
}))
defer srv.Close()
wsURL := strings.Replace(srv.URL, "http", "ws", 1)
transport := &protoSocketTransport{url: wsURL}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
handlerCh := make(chan gitoevents.BranchUpdatedEvent, 10)
handler := func(ctx context.Context, ev gitoevents.BranchUpdatedEvent) {
handlerCh <- ev
}
client, err := gitoevents.NewClient(transport, gitoevents.Options{
RepoID: "nomadcode",
Branch: "develop",
Handler: handler,
})
if err != nil {
t.Fatalf("NewClient failed: %v", err)
}
go func() {
_ = client.Run(ctx)
}()
// We expect exactly one event (the valid on-target one) to reach the handler.
select {
case ev := <-handlerCh:
if ev.RepoID != "nomadcode" || ev.Branch != "develop" {
t.Fatalf("unexpected event: %+v", ev)
}
case <-time.After(2 * time.Second):
t.Fatal("timed out waiting for valid event")
}
// Verify no other events were received
select {
case extra := <-handlerCh:
t.Fatalf("received unexpected extra event: %+v", extra)
default:
// success
}
}