archive completed subtask and update related artifacts

This commit is contained in:
toki 2026-06-17 07:49:01 +09:00
parent f4959a3bab
commit 48901b11d5
9 changed files with 678 additions and 25 deletions

View file

@ -6,7 +6,7 @@
## 목표
로드맵 기반 agent-ops 운영 루프, Plane/Jira 같은 work item provider와 Milestone item의 양방향 동기화 도메인, 외부 agent 제어 표면을 Core action과 MCP tool 계층으로 분리한다. 현재는 Milestone Work Item Creation Sync, Plane Work Item Webhook Intake, Gito Branch Event Creation Sync Bridge를 완료/archive했고, Gito Proto-Socket Consumer Wire Readiness를 다음 계획 후보로 둔다.
로드맵 기반 agent-ops 운영 루프, Plane/Jira 같은 work item provider와 Milestone item의 양방향 동기화 도메인, 외부 agent 제어 표면을 Core action과 MCP tool 계층으로 분리한다. 현재는 Milestone Work Item Creation Sync, Plane Work Item Webhook Intake, Gito Branch Event Creation Sync Bridge를 완료/archive했고, Gito Proto-Socket Consumer Wire Readiness를 진행 중이다.
## Milestone 흐름
@ -30,9 +30,9 @@
- 경로: `agent-roadmap/archive/phase/agent-ops-mcp-control-plane/milestones/gito-branch-event-creation-sync-bridge.md`
- 요약: Gito `branch.updated` wakeup을 NomadCode core가 소비하고 target branch 재검증/스캔 뒤 creation sync job으로 enqueue하는 bridge를 구현했다. 코드 레벨 종료 검토에서 roadmap milestone path가 없는 event drop을 보강하고 archive했다.
- [계획] Gito Proto-Socket Consumer Wire Readiness
- [진행중] Gito Proto-Socket Consumer Wire Readiness
- 경로: `agent-roadmap/phase/agent-ops-mcp-control-plane/milestones/gito-protosocket-consumer-wire-readiness.md`
- 요약: NomadCode/Gito 연동 MVP의 다음 구현 slice로, 기존 Gito event semantic bridge를 실제 Gito `/proto-socket` binary protocol과 cross-project smoke 기준으로 닫는다.
- 요약: NomadCode/Gito 연동 MVP의 다음 구현 slice로, Gito `/proto-socket` binary transport/decode와 bridge e2e 검증을 완료했고 failure/reconnect 운영 기준 작업을 진행 중이다.
- [계획] Roadmap Driven Agent-Ops Automation
- 경로: `agent-roadmap/phase/agent-ops-mcp-control-plane/milestones/roadmap-driven-agent-ops-automation.md`

View file

@ -11,7 +11,7 @@ NomadCode core의 Gito branch event consumer를 실제 Gito `/proto-socket` wire
## 상태
[계획]
[진행중]
## 승격 조건
@ -37,10 +37,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으로 돌려주는 테스트가 통과한다.
- [x] [transport-adapter] JSON text websocket transport를 실제 proto-socket binary/protobuf transport 또는 동등 adapter로 교체한다. 검증: 실제 proto-socket test server가 `event.subscribe`를 받고 `branch.updated`를 binary frame으로 돌려주는 테스트가 통과한다.
- [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으로 수렴한다.
- [x] [envelope-decode] binary frame에서 받은 `structpb.Struct` envelope을 기존 `gitoevents.DecodeBranchUpdatedEnvelope` 입력으로 안전하게 변환한다. 검증: malformed frame, 다른 channel/action, payload 누락, 알 수 없는 metadata/custom field가 side effect 없이 drop 또는 ignore된다.
- [x] [bridge-e2e] wire-compatible consumer, branch scan, `EnqueueRoadmapCreationSync`까지 이어지는 integration test 또는 dev smoke를 추가한다. 검증: Gito `branch.updated` 한 건이 target branch 재검증 후 정확히 하나의 creation sync job으로 수렴한다.
- [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

@ -42,43 +42,48 @@ task=m-gito-protosocket-consumer-wire-readiness/02+01_bridge_e2e, plan=0, tag=TE
| 항목 | 완료 여부 |
|------|---------|
| [TEST-1] Add wire-to-bridge integration test | [ ] |
| [TEST-2] Keep side-effect boundaries visible in e2e fixture | [ ] |
| [TEST-1] Add wire-to-bridge integration test | [x] |
| [TEST-2] Keep side-effect boundaries visible in e2e fixture | [x] |
## 구현 체크리스트
- [ ] 선행 `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의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
- [x] 선행 `01_wire_transport_decode`의 active 또는 archived `complete.log`를 확인한 뒤 구현을 시작한다.
- [x] binary proto-socket test server/client 흐름에서 Gito `event.subscribe` 후 `branch.updated` event를 발행하는 integration-style test를 추가한다.
- [x] fake scanner/reader/enqueuer를 조합해 target branch 재검증 후 `EnqueueRoadmapCreationSync`가 정확히 한 번 호출되는지 assert한다.
- [x] duplicate event나 no-milestone event가 같은 test fixture에서 추가 enqueue로 번지지 않는지 최소 한 케이스를 포함한다.
- [x] `git diff --check`와 `cd services/core && go test ./...`를 실행한다.
- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
## 코드리뷰 전용 체크리스트
> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다.
> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다.
- [ ] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
- [ ] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다.
- [ ] active `CODE_REVIEW-*-G??.md`를 `code_review_cloud_G07_N.log`로 아카이브한다.
- [ ] active `PLAN-*-G??.md`를 `plan_cloud_G07_M.log`로 아카이브한다.
- [ ] `.gitignore`의 Agent-Ops 관리 block이 `agent-task/**/*.md`와 `agent-task/**/*.log`를 unignore하고 `agent-roadmap/current.md`를 ignore하는지 확인한다.
- [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/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`를 작성하지 않는다.
- [x] WARN/FAIL이고 user-review gate가 트리거되지 않았으면 다음 active `PLAN-cloud-G07.md`와 `CODE_REVIEW-cloud-G07.md`를 작성하고 `complete.log`를 작성하지 않는다.
- [ ] USER_REVIEW이면 `agent-ops/skills/common/code-review/templates/user-review-template.md` 기준으로 `USER_REVIEW.md`를 작성하고 active `PLAN-*.md`, `CODE_REVIEW-*.md`, `complete.log`를 남기지 않는다.
- [ ] USER_REVIEW가 사용자 결정으로 완료/PASS 해소되면 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log`를 작성한 뒤 task directory를 archive로 이동한다.
## 계획 대비 변경 사항
_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._
- PLAN `runner_test.go 또는 새 test file`에 대해 기존 `runner_test.go`를 확장했다. 새 파일 생성 없이 기존 파일에 추가하는 것이 더 적합하다고 판단.
- PLAN은 `bridge_test.go`에서 `TEST-2` fake helper를 재사용할 수 있다고 언급했고, 실제로 기존 `fakeScanner`, `fakeReader`, `matchedScanOutput()`, `milestoneMarkdownWithIdentity` 상수를 재사용했다. 추가 helper는 `runner_test.go`에만 추가.
- 서버 helper `gotoWireServer`를 별도 함수로 추출해 두 테스트가 공유하도록 했다. PLAN에는 명시되지 않았지만 중복 제거를 위해 적용.
- `gotoWireServer`는 이벤트 슬라이스를 받아 각 이벤트 사이에 100ms delay를 줘 Bridge.Handle → MarkProcessed가 완료된 뒤 다음 이벤트를 전송한다. 이를 통해 duplicate test의 dedup이 결정적으로 동작한다.
## 주요 설계 결정
_구현 에이전트가 주요 설계 결정 사항을 기록한다._
- `channelEnqueuer`: 버퍼 채널 기반 fake enqueuer. 테스트가 채널 receive로 enqueue 완료를 대기할 수 있고, 버퍼 크기 4로 back-pressure 없이 빠른 처리가 가능하다.
- `gotoWireServer` 공유 helper: subscribe request → response 후 이벤트 시퀀스를 100ms 간격으로 전송. 간격은 Bridge.Handle 동기 실행(scanner + reader + enqueuer + MarkProcessed)보다 길어야 하며, fake들이 즉시 반환하므로 100ms는 충분히 보수적이다.
- `TestWireConsumerDuplicateEventDoesNotReEnqueue`의 500ms 대기: 두 번째 이벤트가 서버에서 전송된 뒤(100ms) Bridge에서 처리(즉시)되고 jobCh에 아무것도 없음을 확인한다. 500ms는 wire + handler 경로를 충분히 커버한다.
## 사용자 리뷰 요청
@ -113,12 +118,20 @@ _구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후
cd services/core && go test ./internal/gitosync -run TestWireConsumerEventEnqueuesCreationSyncOnce -count=1
```
```
ok github.com/nomadcode/nomadcode-core/internal/gitosync 0.061s
```
### TEST-2 중간 검증
```bash
cd services/core && go test ./internal/gitosync -count=1
```
```
ok github.com/nomadcode/nomadcode-core/internal/gitosync 0.972s
```
### 최종 검증
```bash
@ -126,6 +139,33 @@ git diff --check
cd services/core && go test ./...
```
```
# git diff --check: no output (PASS)
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)
ok github.com/nomadcode/nomadcode-core/internal/authoring (cached)
ok github.com/nomadcode/nomadcode-core/internal/config (cached)
ok github.com/nomadcode/nomadcode-core/internal/gitoevents (cached)
ok github.com/nomadcode/nomadcode-core/internal/gitosync 0.972s
ok github.com/nomadcode/nomadcode-core/internal/http (cached)
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?**
@ -145,3 +185,34 @@ cd services/core && go test ./...
| 사용자 리뷰 요청 | Implementing agent | Keep `상태: 없음` unless user input is required |
| 리뷰어를 위한 체크포인트 | Fixed at stub creation | Review focus |
| 검증 결과 | Implementing agent | Paste actual output |
## 코드리뷰 결과
- 종합 판정: FAIL
- 차원별 평가:
- correctness: Fail
- completeness: Fail
- test coverage: Fail
- API contract: Pass
- code quality: Pass
- plan deviation: Fail
- verification trust: Pass
- 발견된 문제:
- Required: `services/core/internal/gitosync/runner_test.go:456`에서 새 e2e가 `fakeScanner{out: matchedScanOutput()}`를 그대로 재사용하고, `services/core/internal/gitosync/runner_test.go:495`에서 `RoadmapRevision == "bbbb"`를 기대합니다. 하지만 wire fixture의 `branch.updated.payload.after`는 `"new-sha"`이고, 계획은 fake scanner가 event `After` revision을 읽어 `ScanOutput`을 반환하도록 요구했습니다(`PLAN-cloud-G07.md:100`). 현재 테스트는 decoded wire event의 `after`가 `Bridge.Handle` -> scanner -> `EnqueueRoadmapCreationSync`까지 전달되는지 검증하지 않으므로, revision handoff가 끊겨도 정적 scanner output 때문에 통과할 수 있습니다. 수정: `runner_test.go`에 e2e 전용 recording scanner를 두어 `ev.RepoID`, `ev.Branch`, `ev.Before`, `ev.After`, milestone `ChangedFiles`를 검증하고, `ScanOutput.Result.Revision.Revision`을 `ev.After`로 구성한 뒤 `job.RoadmapRevision == "new-sha"`를 assert하세요.
- 다음 단계: FAIL 후속 `PLAN-cloud-G07.md`와 `CODE_REVIEW-cloud-G07.md`를 작성해 위 Required만 좁게 수정/검증한다.
리뷰 검증:
```bash
git diff --check
cd services/core && go test ./internal/gitosync -run TestWireConsumer -count=1
cd services/core && go test ./...
```
결과:
```text
git diff --check: PASS (no output)
go test ./internal/gitosync -run TestWireConsumer -count=1: PASS
go test ./...: PASS
```

View file

@ -0,0 +1,227 @@
<!-- task=m-gito-protosocket-consumer-wire-readiness/02+01_bridge_e2e plan=1 tag=REVIEW_TEST -->
# Code Review Reference - REVIEW_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-17
task=m-gito-protosocket-consumer-wire-readiness/02+01_bridge_e2e, plan=1, tag=REVIEW_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
## Archive Evidence Snapshot
- Archived plan: `agent-task/m-gito-protosocket-consumer-wire-readiness/02+01_bridge_e2e/plan_cloud_G07_0.log`
- Archived review: `agent-task/m-gito-protosocket-consumer-wire-readiness/02+01_bridge_e2e/code_review_cloud_G07_0.log`
- Verdict: FAIL
- Required summary:
- `services/core/internal/gitosync/runner_test.go`의 새 e2e가 정적 `fakeScanner{out: matchedScanOutput()}`와 `RoadmapRevision == "bbbb"` 기대값을 사용해, wire event `after == "new-sha"`가 scanner/enqueue 경로까지 전달되는지 검증하지 못한다.
- Suggested summary: 없음
- Nit summary: 없음
- Affected files:
- `services/core/internal/gitosync/runner_test.go`
- Verification evidence:
- `git diff --check` - PASS; no output
- `cd services/core && go test ./internal/gitosync -run TestWireConsumer -count=1` - PASS
- `cd services/core && go test ./...` - PASS
- Allowed archive reread:
- `agent-task/m-gito-protosocket-consumer-wire-readiness/02+01_bridge_e2e/plan_cloud_G07_0.log`
- `agent-task/m-gito-protosocket-consumer-wire-readiness/02+01_bridge_e2e/code_review_cloud_G07_0.log`
## 이 파일을 읽는 리뷰 에이전트에게
> **[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/`로 이동한다. 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_TEST-1] Tie e2e scanner output to wire event revision | [x] |
## 구현 체크리스트
- [x] `services/core/internal/gitosync/runner_test.go`에 e2e 전용 scanner fake를 추가하거나 기존 fake를 감싸 `BranchUpdatedEvent`의 `RepoID`, `Branch`, `Before`, `After`, milestone `ChangedFiles`를 검증한다.
- [x] scanner fake가 `ev.After`를 `ScanOutput.Result.Revision.Revision`으로 반영하게 하고 `TestWireConsumerEventEnqueuesCreationSyncOnce`에서 `job.RoadmapRevision == "new-sha"`를 assert한다.
- [x] `TestWireConsumerDuplicateEventDoesNotReEnqueue`가 새 scanner fixture와 함께 duplicate event를 추가 enqueue로 번지지 않게 유지한다.
- [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하는지 확인한다.
- [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/02+01_bridge_e2e/`를 `agent-task/archive/YYYY/MM/m-gito-protosocket-consumer-wire-readiness/02+01_bridge_e2e/`로 이동하고 최종 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`와 `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로 이동한다.
## 계획 대비 변경 사항
계획 그대로 구현했습니다. 계획 대비 달라진 범위 없음.
## 주요 설계 결정
- `recordingScanner`: `eventCh chan gitoevents.BranchUpdatedEvent` (buffer=4)를 보유하고 `Scan`이 호출될 때 이벤트를 채널에 보낸 뒤 `matchedScanOutput()`에서 `Result.Revision.Revision = ev.After`를 덮어씌워 반환한다. 채널을 통해 비동기 goroutine 안의 Scan 호출을 test goroutine에서 안전하게 읽을 수 있다.
- 이벤트 필드 검증 시점: `jobCh`에서 job을 받은 직후 `scanner.eventCh`를 논블로킹으로 읽는다. job이 도착했다는 것은 `Scan → enqueueDoc → EnqueueRoadmapCreationSync` 순서가 이미 완료됐다는 뜻이므로 `eventCh`에 이미 이벤트가 있다.
- `TestWireConsumerDuplicateEventDoesNotReEnqueue`도 `recordingScanner`로 교체해 두 테스트의 scanner 구현을 통일했다. duplicate 케이스에서 `Scan`은 1회만 호출되므로 `eventCh` buffer 충돌 없음.
## 사용자 리뷰 요청
_기본값은 `없음`이다. 구현 중 사용자 결정, 사용자 소유 외부 환경/secret/서비스 준비, 또는 계획 범위 변경 없이는 안전하게 진행할 수 없으면 아래 항목을 실제 내용으로 교체하고, 구현을 중단한 뒤 active 파일을 그대로 둔 채 리뷰를 요청한다. 구현 에이전트는 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 검증 증거 공백만으로는 사용자 리뷰 요청을 작성하지 않는다._
- 상태: 없음
- 사유 유형: 없음
- 결정 필요: 없음
- 차단 근거: 없음
- 실행한 검증/명령: 없음
- 자동 후속 불가 이유: 없음
- 재개 조건: 없음
## 리뷰어를 위한 체크포인트
- scanner fake가 실제 `BranchUpdatedEvent.After`를 사용해 `ScanOutput.Result.Revision.Revision`을 만든다.
- `TestWireConsumerEventEnqueuesCreationSyncOnce`가 `job.RoadmapRevision == "new-sha"`를 검증한다.
- duplicate event test가 same-after 중복을 추가 enqueue 없이 유지한다.
## 검증 결과
_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._
필수 규칙:
- 검증 명령은 고정된 계약이다. 임의로 대체하지 않는다.
- 대체가 필요하면 `계획 대비 변경 사항`에 이유와 대체 명령을 기록한다.
- `검증 결과`에는 실제 stdout/stderr를 붙여 넣는다.
### REVIEW_TEST-1 중간 검증
```bash
cd services/core && go test ./internal/gitosync -run 'TestWireConsumer(EventEnqueuesCreationSyncOnce|DuplicateEventDoesNotReEnqueue)' -count=1
```
```
=== RUN TestWireConsumerEventEnqueuesCreationSyncOnce
--- PASS: TestWireConsumerEventEnqueuesCreationSyncOnce (0.06s)
=== RUN TestWireConsumerDuplicateEventDoesNotReEnqueue
--- PASS: TestWireConsumerDuplicateEventDoesNotReEnqueue (0.56s)
PASS
ok github.com/nomadcode/nomadcode-core/internal/gitosync 0.618s
```
### 최종 검증
```bash
git diff --check
cd services/core && go test ./...
```
```
# git diff --check: no output (PASS)
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)
ok github.com/nomadcode/nomadcode-core/internal/authoring (cached)
ok github.com/nomadcode/nomadcode-core/internal/config (cached)
ok github.com/nomadcode/nomadcode-core/internal/gitoevents (cached)
ok github.com/nomadcode/nomadcode-core/internal/gitosync 0.971s
ok github.com/nomadcode/nomadcode-core/internal/http (cached)
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 plan | Narrow prior-loop context allowed for this follow-up |
| 구현 항목별 완료 여부 | 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
- completeness: Pass
- test coverage: Pass
- API contract: Pass
- code quality: Pass
- plan deviation: Pass
- verification trust: Pass
- 발견된 문제: 없음
- 다음 단계: PASS이므로 `complete.log` 작성 후 active task directory를 archive로 이동한다.
리뷰 검증:
```bash
git diff --check
cd services/core && go test ./internal/gitosync -run 'TestWireConsumer(EventEnqueuesCreationSyncOnce|DuplicateEventDoesNotReEnqueue)' -count=1 -v
cd services/core && go test ./...
```
결과:
```text
git diff --check: PASS (no output)
go test ./internal/gitosync -run 'TestWireConsumer(EventEnqueuesCreationSyncOnce|DuplicateEventDoesNotReEnqueue)' -count=1 -v: PASS
go test ./...: PASS
```

View file

@ -0,0 +1,43 @@
# Complete - m-gito-protosocket-consumer-wire-readiness/02+01_bridge_e2e
## 완료 일시
2026-06-16
## 요약
Gito wire-to-bridge e2e 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 missing wire event `after` revision handoff assertion in the e2e scanner fixture. |
| `plan_cloud_G07_1.log` | `code_review_cloud_G07_1.log` | PASS | Recording scanner ties `BranchUpdatedEvent.After` to `RoadmapRevision`, and focused/full core tests passed. |
## 구현/정리 내용
- Added a wire-to-bridge integration-style test that drives binary proto-socket `event.subscribe` and `branch.updated` through `NewRunner`, `gitoevents.Client`, `Bridge.Handle`, and `EnqueueRoadmapCreationSync`.
- Added duplicate event coverage to prove same `(repo, branch, after)` wire events do not enqueue a second creation sync job.
- Follow-up review tightened the e2e scanner fixture so it records the decoded wire event and reflects `ev.After` into `ScanOutput.Result.Revision.Revision`; the job now asserts `RoadmapRevision == "new-sha"`.
## 최종 검증
- `git diff --check` - PASS; no output.
- `cd services/core && go test ./internal/gitosync -run 'TestWireConsumer(EventEnqueuesCreationSyncOnce|DuplicateEventDoesNotReEnqueue)' -count=1 -v` - PASS; both focused wire consumer tests passed.
- `cd services/core && go test ./...` - PASS; all core packages passed.
## Roadmap Completion
- Milestone: `agent-roadmap/phase/agent-ops-mcp-control-plane/milestones/gito-protosocket-consumer-wire-readiness.md`
- Completed task ids:
- `bridge-e2e`: PASS; evidence=`agent-task/archive/2026/06/m-gito-protosocket-consumer-wire-readiness/02+01_bridge_e2e/plan_cloud_G07_1.log`, `agent-task/archive/2026/06/m-gito-protosocket-consumer-wire-readiness/02+01_bridge_e2e/code_review_cloud_G07_1.log`; verification=`cd services/core && go test ./internal/gitosync -run 'TestWireConsumer(EventEnqueuesCreationSyncOnce|DuplicateEventDoesNotReEnqueue)' -count=1 -v`, `cd services/core && go test ./...`
- Not completed task ids: 없음
## 잔여 Nit
- 없음
## 후속 작업
- 없음

View file

@ -0,0 +1,89 @@
<!-- task=m-gito-protosocket-consumer-wire-readiness/02+01_bridge_e2e plan=1 tag=REVIEW_TEST -->
# Plan - REVIEW_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`를 만들지 않습니다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 닫을 수 있는 증거 공백은 사용자 리뷰가 아닙니다.
## 배경
이 후속 계획은 첫 리뷰에서 발견된 Required 하나만 닫습니다. 새 wire-to-bridge e2e 테스트가 binary proto-socket event를 받기는 하지만, scanner fake가 event `After` revision을 읽지 않아 wire payload의 revision handoff를 검증하지 못합니다.
## 사용자 리뷰 요청 흐름
구현 중 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
## Archive Evidence Snapshot
- Archived plan: `agent-task/m-gito-protosocket-consumer-wire-readiness/02+01_bridge_e2e/plan_cloud_G07_0.log`
- Archived review: `agent-task/m-gito-protosocket-consumer-wire-readiness/02+01_bridge_e2e/code_review_cloud_G07_0.log`
- Verdict: FAIL
- Required summary:
- `services/core/internal/gitosync/runner_test.go`의 새 e2e가 정적 `fakeScanner{out: matchedScanOutput()}`와 `RoadmapRevision == "bbbb"` 기대값을 사용해, wire event `after == "new-sha"`가 scanner/enqueue 경로까지 전달되는지 검증하지 못한다.
- Suggested summary: 없음
- Nit summary: 없음
- Affected files:
- `services/core/internal/gitosync/runner_test.go`
- Verification evidence:
- `git diff --check` - PASS; no output
- `cd services/core && go test ./internal/gitosync -run TestWireConsumer -count=1` - PASS
- `cd services/core && go test ./...` - PASS
- Allowed archive reread:
- `agent-task/m-gito-protosocket-consumer-wire-readiness/02+01_bridge_e2e/plan_cloud_G07_0.log`
- `agent-task/m-gito-protosocket-consumer-wire-readiness/02+01_bridge_e2e/code_review_cloud_G07_0.log`
## 범위 결정 근거
Production code, proto-socket transport, bridge behavior, scheduler args type은 변경하지 않습니다. 이미 존재하는 e2e test fixture만 보강해 계획의 revision handoff 검증 요구를 만족시킵니다.
## 빌드 등급
`cloud-G07`입니다. binary proto-socket, async event delivery, scanner seam, scheduler job args가 함께 걸린 기존 리뷰 경로를 유지합니다.
## 구현 체크리스트
- [ ] `services/core/internal/gitosync/runner_test.go`에 e2e 전용 scanner fake를 추가하거나 기존 fake를 감싸 `BranchUpdatedEvent`의 `RepoID`, `Branch`, `Before`, `After`, milestone `ChangedFiles`를 검증한다.
- [ ] scanner fake가 `ev.After`를 `ScanOutput.Result.Revision.Revision`으로 반영하게 하고 `TestWireConsumerEventEnqueuesCreationSyncOnce`에서 `job.RoadmapRevision == "new-sha"`를 assert한다.
- [ ] `TestWireConsumerDuplicateEventDoesNotReEnqueue`가 새 scanner fixture와 함께 duplicate event를 추가 enqueue로 번지지 않게 유지한다.
- [ ] `git diff --check`와 `cd services/core && go test ./...`를 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
## [REVIEW_TEST-1] Tie e2e scanner output to wire event revision
문제: 현재 e2e는 `branchUpdatedEnvelope()`의 `after` 값이 scanner까지 전달되는지 확인하지 않습니다. 정적 `matchedScanOutput()`이 `"bbbb"` revision을 반환하기 때문에 event decode 또는 handoff가 깨져도 test가 통과할 수 있습니다.
해결 방법: `runner_test.go`에 recording scanner를 두어 받은 `gitoevents.BranchUpdatedEvent`를 검사하고, `matchedScanOutput()` 기반 output의 `Result.Revision.Revision`을 `ev.After`로 바꿔 반환합니다. happy path test는 `job.RoadmapRevision == "new-sha"`를 기대해야 합니다.
수정 파일 및 체크리스트:
- [ ] `runner_test.go`에 recording scanner helper를 추가한다.
- [ ] helper가 `RepoID`, `Branch`, `Before`, `After`, milestone changed file presence를 검증한다.
- [ ] helper가 반환하는 `ScanOutput.Result.Revision.Revision`과 최종 `job.RoadmapRevision`이 wire event `after`와 일치하게 한다.
- [ ] duplicate test가 same-after duplicate를 계속 한 번만 enqueue한다고 검증한다.
테스트 작성: 필수. 기존 테스트를 보강하고 새 production API는 만들지 않습니다.
중간 검증:
```bash
cd services/core && go test ./internal/gitosync -run 'TestWireConsumer(EventEnqueuesCreationSyncOnce|DuplicateEventDoesNotReEnqueue)' -count=1
```
## 최종 검증
```bash
git diff --check
cd services/core && go test ./...
```
모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.

View file

@ -64,7 +64,7 @@ none expected. If implementing reconnect introduces new unexported types, no exp
### 분할 판단
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가 생긴 뒤 운영 실패 기준을 보강할 수 있습니다.
split decision policy를 평가했습니다. 이 plan은 `03+01_failure_ops`라서 predecessor index `01`에 의존합니다. 현재 predecessor는 `agent-task/archive/2026/06/m-gito-protosocket-consumer-wire-readiness/01_wire_transport_decode/complete.log`로 PASS 완료 근거가 있습니다. `bridge-e2e`와는 독립적으로, binary transport가 생긴 뒤 운영 실패 기준을 보강할 수 있습니다.
### 범위 결정 근거
@ -85,7 +85,7 @@ split decision policy를 평가했습니다. 이 plan은 `03+01_failure_ops`라
## 의존 관계 및 구현 순서
이 subtask directory의 `+01`이 runtime dependency source of truth입니다. `01_wire_transport_decode``complete.log`를 만들기 전에는 구현하지 않습니다. 현재 확인 시점에는 predecessor complete evidence가 없습니다.
이 subtask directory의 `+01`이 runtime dependency source of truth입니다. `01_wire_transport_decode``agent-task/archive/2026/06/m-gito-protosocket-consumer-wire-readiness/01_wire_transport_decode/complete.log`에서 PASS 완료 확인되었습니다.
### [REFACTOR-1] Define runner shutdown and reconnect behavior

View file

@ -13,6 +13,8 @@ import (
toki "git.toki-labs.com/toki/proto-socket/go"
"github.com/nomadcode/nomadcode-core/internal/gitoevents"
"github.com/nomadcode/nomadcode-core/internal/protosocket"
"github.com/nomadcode/nomadcode-core/internal/scheduler"
"github.com/nomadcode/nomadcode-core/internal/workitem"
"google.golang.org/protobuf/types/known/structpb"
"nhooyr.io/websocket"
)
@ -385,3 +387,224 @@ func TestProtoSocketTransportClientRunDropsMalformedAndMismatchedStructs(t *test
// success
}
}
// channelEnqueuer captures EnqueueRoadmapCreationSync calls on a buffered
// channel so e2e tests can wait for enqueue completion without polling.
type channelEnqueuer struct {
ch chan scheduler.RoadmapCreationSyncJobArgs
}
// recordingScanner records the BranchUpdatedEvent it receives and returns
// matchedScanOutput() with Result.Revision.Revision overridden to ev.After.
// This ties the e2e job's RoadmapRevision to the wire event's actual after
// value, so a revision handoff break would be caught by the test assertion.
type recordingScanner struct {
eventCh chan gitoevents.BranchUpdatedEvent
}
func newRecordingScanner() *recordingScanner {
return &recordingScanner{eventCh: make(chan gitoevents.BranchUpdatedEvent, 4)}
}
func (s *recordingScanner) Scan(_ context.Context, ev gitoevents.BranchUpdatedEvent) (ScanOutput, error) {
s.eventCh <- ev
out := matchedScanOutput()
out.Result.Revision.Revision = ev.After
return out, nil
}
func (e *channelEnqueuer) EnqueueRoadmapCreationSync(_ context.Context, args scheduler.RoadmapCreationSyncJobArgs) error {
e.ch <- args
return nil
}
// gotoWireServer returns an httptest.Server that speaks binary proto-socket,
// responds to the event.subscribe request, and broadcasts each env in events
// after a 50 ms delay. The caller owns closing the server.
func gotoWireServer(t *testing.T, events []protosocket.Envelope) *httptest.Server {
t.Helper()
return 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 != gitoevents.EventChannel || env.Action != gitoevents.SubscribeAction {
return nil, errors.New("unexpected request")
}
resEnv := protosocket.Envelope{
ProtocolVersion: protosocket.ProtocolVersion,
ID: "res-1",
CorrelationID: env.ID,
Type: "response",
Channel: gitoevents.EventChannel,
Action: gitoevents.SubscribeAction,
}
resStruct, _ := resEnv.ToStruct()
go func() {
time.Sleep(50 * time.Millisecond)
for _, ev := range events {
s, _ := ev.ToStruct()
_ = serverClient.Send(s)
// Give the bridge time to finish Handle (MarkProcessed) before
// sending the next event so dedup is observable.
time.Sleep(100 * time.Millisecond)
}
}()
return resStruct, nil
})
<-r.Context().Done()
}))
}
// TestWireConsumerEventEnqueuesCreationSyncOnce verifies the full wire path:
// binary proto-socket transport → gitoevents.Client → Bridge.Handle → Enqueuer.
// EnqueueRoadmapCreationSync must be called exactly once with the expected args.
// The recordingScanner reflects ev.After into RoadmapRevision, so this test
// also verifies that the wire event's revision is forwarded through the path.
func TestWireConsumerEventEnqueuesCreationSyncOnce(t *testing.T) {
jobCh := make(chan scheduler.RoadmapCreationSyncJobArgs, 4)
enq := &channelEnqueuer{ch: jobCh}
reader := &fakeReader{item: workitem.WorkItem{DescriptionHTML: "<p>html body</p>"}}
scanner := newRecordingScanner()
bridge, err := NewBridge(scanner, reader, enq,
NewInMemoryRevisionStore(), BridgeConfig{
RepoID: "nomadcode",
Branch: "develop",
TodoStateID: "state-todo",
}, nil)
if err != nil {
t.Fatalf("NewBridge: %v", err)
}
srv := gotoWireServer(t, []protosocket.Envelope{branchUpdatedEnvelope()})
defer srv.Close()
wsURL := strings.Replace(srv.URL, "http", "ws", 1)
client, err := NewRunner(wsURL, "nomadcode", "develop", bridge, nil)
if err != nil {
t.Fatalf("NewRunner: %v", err)
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() { _ = client.Run(ctx) }()
var job scheduler.RoadmapCreationSyncJobArgs
select {
case job = <-jobCh:
case <-time.After(3 * time.Second):
t.Fatal("timed out waiting for EnqueueRoadmapCreationSync")
}
// Validate the scanner received the wire event with expected fields.
// By the time jobCh has a value, scanner.eventCh already has the event.
var scannedEv gitoevents.BranchUpdatedEvent
select {
case scannedEv = <-scanner.eventCh:
default:
t.Fatal("recordingScanner was not called with the wire event")
}
if scannedEv.RepoID != "nomadcode" || scannedEv.Branch != "develop" {
t.Fatalf("scanner event repo/branch: got %q/%q", scannedEv.RepoID, scannedEv.Branch)
}
if scannedEv.Before != "old-sha" || scannedEv.After != "new-sha" {
t.Fatalf("scanner event before/after: got %q/%q", scannedEv.Before, scannedEv.After)
}
if len(scannedEv.MilestoneChangedFiles()) == 0 {
t.Fatal("scanner event: expected at least one milestone changed file")
}
// Job args assertions.
if job.Expected.WorkItemID != "wi-123" {
t.Fatalf("Expected.WorkItemID: got %q, want wi-123", job.Expected.WorkItemID)
}
if job.Ref.Provider != "plane" || job.Ref.ID != "wi-123" || job.Ref.Project != "proj-1" {
t.Fatalf("Ref: got %+v", job.Ref)
}
if job.TodoStateID != "state-todo" {
t.Fatalf("TodoStateID: got %q", job.TodoStateID)
}
// recordingScanner sets Revision.Revision to ev.After; the bridge copies it
// into RoadmapRevision, so this asserts the wire revision is forwarded.
if job.RoadmapRevision != "new-sha" {
t.Fatalf("RoadmapRevision: got %q, want new-sha (wire event after)", job.RoadmapRevision)
}
if job.MilestoneMarkdown != milestoneMarkdownWithIdentity {
t.Fatalf("MilestoneMarkdown mismatch")
}
if job.OriginalBody != "<p>html body</p>" {
t.Fatalf("OriginalBody: got %q", job.OriginalBody)
}
if job.ExternalSource != "nomadcode" || job.ExternalID != "wi-123" {
t.Fatalf("ExternalTags: source=%q id=%q", job.ExternalSource, job.ExternalID)
}
if job.ProviderRevision != "" {
t.Fatalf("ProviderRevision: got %q, want empty (mutable Plane state must not be a revision)", job.ProviderRevision)
}
// One event was sent; verify no second enqueue occurred.
select {
case extra := <-jobCh:
t.Fatalf("unexpected second enqueue: %+v", extra)
default:
}
}
// TestWireConsumerDuplicateEventDoesNotReEnqueue sends the same branch.updated
// event twice over the wire. The bridge's InMemoryRevisionStore must deduplicate
// at the (repo, branch, after) key so only one creation sync job is enqueued.
func TestWireConsumerDuplicateEventDoesNotReEnqueue(t *testing.T) {
jobCh := make(chan scheduler.RoadmapCreationSyncJobArgs, 4)
enq := &channelEnqueuer{ch: jobCh}
reader := &fakeReader{item: workitem.WorkItem{DescriptionText: "body"}}
bridge, err := NewBridge(newRecordingScanner(), reader, enq,
NewInMemoryRevisionStore(), BridgeConfig{
RepoID: "nomadcode",
Branch: "develop",
TodoStateID: "state-todo",
}, nil)
if err != nil {
t.Fatalf("NewBridge: %v", err)
}
ev := branchUpdatedEnvelope()
srv := gotoWireServer(t, []protosocket.Envelope{ev, ev})
defer srv.Close()
wsURL := strings.Replace(srv.URL, "http", "ws", 1)
client, err := NewRunner(wsURL, "nomadcode", "develop", bridge, nil)
if err != nil {
t.Fatalf("NewRunner: %v", err)
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() { _ = client.Run(ctx) }()
// First event must produce exactly one enqueue.
select {
case <-jobCh:
case <-time.After(3 * time.Second):
t.Fatal("timed out waiting for first EnqueueRoadmapCreationSync")
}
// The duplicate (same after="new-sha") must be silently dropped by the
// InMemoryRevisionStore. Wait long enough for the wire + bridge path to
// complete, then assert no second job arrived.
select {
case extra := <-jobCh:
t.Fatalf("duplicate event must not re-enqueue: %+v", extra)
case <-time.After(500 * time.Millisecond):
// No second enqueue — expected.
}
}