feat: milestone work-item creation sync 관련 변경사항 커밋
- projectsync: checkout, config 모듈 개선 및 테스트 추가 - workitempipeline: service 개선 및 테스트 추가 - http handlers 테스트 업데이트 - client: proto_socket_branch_event_service.dart 추가 및 테스트 - contracts notes 업데이트 - agent-task: milestone 하위 subtask PLAN/CODE_REVIEW 문서 추가
This commit is contained in:
parent
296c430039
commit
f9c6be03ef
21 changed files with 2523 additions and 17 deletions
|
|
@ -0,0 +1,208 @@
|
||||||
|
<!-- task=m-milestone-work-item-creation-sync/01_workspace_topology 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-13
|
||||||
|
task=m-milestone-work-item-creation-sync/01_workspace_topology, plan=0, tag=API
|
||||||
|
|
||||||
|
## Roadmap Targets
|
||||||
|
|
||||||
|
- Milestone: `agent-roadmap/phase/agent-ops-mcp-control-plane/milestones/milestone-work-item-creation-sync.md`
|
||||||
|
- Task ids:
|
||||||
|
- `cycle-workspace-topology`: project workspace root 아래 `branches/main`, `branches/develop`, `slots/000+` 구조를 준비하고 branch workspace와 agent slot 상태를 분리한다.
|
||||||
|
- Completion mode: check-on-pass
|
||||||
|
|
||||||
|
## 이 파일을 읽는 리뷰 에이전트에게
|
||||||
|
|
||||||
|
> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다.
|
||||||
|
|
||||||
|
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
|
||||||
|
리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다.
|
||||||
|
|
||||||
|
1. 판정을 append한다.
|
||||||
|
2. `CODE_REVIEW-local-G06.md` → `code_review_local_G06_N.log`, `PLAN-local-G06.md` → `plan_local_G06_M.log`로 아카이브한다.
|
||||||
|
3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/m-milestone-work-item-creation-sync/01_workspace_topology/`로 이동한다. WARN/FAIL이면 user-review gate를 확인한 뒤 다음 active plan/review 파일 또는 `USER_REVIEW.md`를 작성한다.
|
||||||
|
4. PASS이고 task group이 `m-<milestone-slug>`이면 완료 이벤트 메타데이터를 보고한다. roadmap 상태 체크와 `update-roadmap` 호출은 런타임 책임이다.
|
||||||
|
5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 코드리뷰 결과
|
||||||
|
|
||||||
|
- 종합 판정: PASS
|
||||||
|
- 차원별 평가:
|
||||||
|
- correctness: Pass
|
||||||
|
- completeness: Pass
|
||||||
|
- test coverage: Pass
|
||||||
|
- API contract: Pass
|
||||||
|
- code quality: Pass
|
||||||
|
- plan deviation: Pass
|
||||||
|
- verification trust: Pass
|
||||||
|
- 발견된 문제: 없음
|
||||||
|
- 다음 단계: PASS 종결. `complete.log`를 작성하고 active plan/review 파일을 로그로 아카이브한 뒤 split task directory를 `agent-task/archive/YYYY/MM/m-milestone-work-item-creation-sync/01_workspace_topology/`로 이동한다.
|
||||||
|
|
||||||
|
### 리뷰 검증
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ cd services/core && go test ./internal/projectsync ./internal/workitempipeline ./internal/http
|
||||||
|
ok github.com/nomadcode/nomadcode-core/internal/projectsync (cached)
|
||||||
|
ok github.com/nomadcode/nomadcode-core/internal/workitempipeline (cached)
|
||||||
|
ok github.com/nomadcode/nomadcode-core/internal/http (cached)
|
||||||
|
|
||||||
|
$ cd services/core && 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/config (cached)
|
||||||
|
? github.com/nomadcode/nomadcode-core/internal/db [no test files]
|
||||||
|
ok github.com/nomadcode/nomadcode-core/internal/http (cached)
|
||||||
|
? github.com/nomadcode/nomadcode-core/internal/model [no test files]
|
||||||
|
ok github.com/nomadcode/nomadcode-core/internal/notification (cached)
|
||||||
|
ok github.com/nomadcode/nomadcode-core/internal/projectsync (cached)
|
||||||
|
ok github.com/nomadcode/nomadcode-core/internal/protosocket (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)
|
||||||
|
|
||||||
|
$ cd services/core && go test -count=1 ./internal/projectsync ./internal/workitempipeline ./internal/http
|
||||||
|
ok github.com/nomadcode/nomadcode-core/internal/projectsync 0.006s
|
||||||
|
ok github.com/nomadcode/nomadcode-core/internal/workitempipeline 0.003s
|
||||||
|
ok github.com/nomadcode/nomadcode-core/internal/http 0.004s
|
||||||
|
|
||||||
|
$ git diff --check
|
||||||
|
# 출력 없음
|
||||||
|
```
|
||||||
|
|
||||||
|
## 구현 항목별 완료 여부
|
||||||
|
|
||||||
|
| 항목 | 완료 여부 |
|
||||||
|
|------|---------|
|
||||||
|
| [API-1] Branch Workspace Topology | [x] |
|
||||||
|
| [API-2] Provision Policy | [x] |
|
||||||
|
| [API-3] Metadata Carries Branch And Slot Topology | [x] |
|
||||||
|
|
||||||
|
## 구현 체크리스트
|
||||||
|
|
||||||
|
- [x] branch workspace path helper와 branch sync state 타입을 추가하고 `main`, `develop` 고정 workspace를 agent slot과 분리한다.
|
||||||
|
- [x] checkout/provision policy가 `branches/main`, `branches/develop`, 최소 `slots/000` 준비 계획을 만들고 missing workspace를 임의 cwd로 대체하지 않도록 오류를 반환한다.
|
||||||
|
- [x] work item pipeline checkout metadata 또는 project sync metadata에 branch workspace root와 slot path가 함께 남도록 필요한 typed field를 추가한다.
|
||||||
|
- [x] `cd services/core && go test ./...`와 `git diff --check`를 실행한다.
|
||||||
|
- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
|
||||||
|
|
||||||
|
## 코드리뷰 전용 체크리스트
|
||||||
|
|
||||||
|
> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다.
|
||||||
|
> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다.
|
||||||
|
|
||||||
|
- [x] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
|
||||||
|
- [x] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다.
|
||||||
|
- [x] active `CODE_REVIEW-*-G??.md`를 `code_review_local_G06_N.log`로 아카이브한다.
|
||||||
|
- [x] active `PLAN-*-G??.md`를 `plan_local_G06_M.log`로 아카이브한다.
|
||||||
|
- [x] `.gitignore`의 Agent-Ops 관리 block이 `agent-task/**/*.md`와 `agent-task/**/*.log`를 unignore하고 `agent-roadmap/current.md`를 ignore하는지 확인한다.
|
||||||
|
- [x] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다.
|
||||||
|
- [x] PASS이면 active task 디렉터리 `agent-task/m-milestone-work-item-creation-sync/01_workspace_topology/`를 `agent-task/archive/YYYY/MM/m-milestone-work-item-creation-sync/01_workspace_topology/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다.
|
||||||
|
- [x] PASS이고 task group이 `m-<milestone-slug>`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다.
|
||||||
|
- [x] PASS split 작업이면 이동 후 빈 active parent `agent-task/m-milestone-work-item-creation-sync/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다.
|
||||||
|
- [ ] WARN/FAIL이고 user-review gate가 트리거되지 않았으면 다음 active `PLAN-{build_lane}-GNN.md`와 `CODE_REVIEW-{review_lane}-GNN.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로 이동한다.
|
||||||
|
|
||||||
|
## 계획 대비 변경 사항
|
||||||
|
|
||||||
|
- `handlers_test.go`는 `CheckoutMetadata`를 struct literal로 직접 구성하지 않아 신규 필드에 의한 코드 변경이 불필요했다. plan에 "handlers_test.go 수정"이 명시되어 있었으나 실제 영향이 없어 변경 없이 통과함을 확인하고 완료로 처리했다.
|
||||||
|
- `checkoutTaskMetadata` 함수 시그니처에 `provision projectsync.ProvisionPlan` 인자를 추가했다. plan 예시 코드에는 명시되지 않았으나 `BuildProvisionPlan` 결과를 전달하는 자연스러운 분리 방식으로 판단했다.
|
||||||
|
|
||||||
|
## 주요 설계 결정
|
||||||
|
|
||||||
|
- `BranchName`을 `string` 기반 타입으로 정의하고 `reservedBranchNames` map으로 closed set을 선언했다. `BranchWorkspacePath`는 이 set에 없는 값을 즉시 `ErrInvalidConfig`로 거부해 agent slot과 혼용이 구조적으로 불가능하다.
|
||||||
|
- `BranchSyncState`는 `SlotState`와 별도 타입으로 분리했다. slot과 branch의 lifecycle이 다르며(`available/in_use/dirty` vs `synced/stale/error`) 타입 수준에서 혼용을 방지한다.
|
||||||
|
- `BuildProvisionPlan`은 `BuildCheckoutPlan`과 독립적으로 호출된다. 두 함수 모두 동일한 normalized config에서 파생되므로 결과가 일관되고, 실제 git 실행은 양쪽 모두 포함하지 않는다.
|
||||||
|
- `CheckoutMetadata`에 `MainBranchPath`, `DevelopBranchPath`를 additive field로 추가했다. 기존 슬롯 필드(`SlotID`, `SlotIndex`, `SlotPath`)와 분리된 위치에 배치해 downstream sync layer가 두 경로를 구분하기 쉽게 했다.
|
||||||
|
|
||||||
|
## 사용자 리뷰 요청
|
||||||
|
|
||||||
|
_기본값은 `없음`이다. 구현 중 사용자 결정, 사용자 소유 외부 환경/secret/서비스 준비, 또는 계획 범위 변경 없이는 안전하게 진행할 수 없으면 아래 항목을 실제 내용으로 교체하고, 구현을 중단한 뒤 active 파일을 그대로 둔 채 리뷰를 요청한다. 구현 에이전트는 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 검증 증거 공백만으로는 사용자 리뷰 요청을 작성하지 않는다._
|
||||||
|
|
||||||
|
- 상태: 없음
|
||||||
|
- 사유 유형: 없음
|
||||||
|
- 결정 필요: 없음
|
||||||
|
- 차단 근거: 없음
|
||||||
|
- 실행한 검증/명령: 없음
|
||||||
|
- 자동 후속 불가 이유: 없음
|
||||||
|
- 재개 조건: 없음
|
||||||
|
|
||||||
|
## 리뷰어를 위한 체크포인트
|
||||||
|
|
||||||
|
- branch workspace path가 `branches/main`, `branches/develop`으로 고정되고 slot path와 섞이지 않는지 확인한다.
|
||||||
|
- work item pipeline metadata가 후속 sync layer에서 develop branch와 slot local path를 구분할 수 있는지 확인한다.
|
||||||
|
- 실제 git 실행을 넣지 않고 policy/helper 경계에 머물렀는지 확인한다.
|
||||||
|
|
||||||
|
## 검증 결과
|
||||||
|
|
||||||
|
_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._
|
||||||
|
|
||||||
|
필수 규칙:
|
||||||
|
- 검증 명령은 고정된 계약이다. 임의로 대체하지 않는다.
|
||||||
|
- 대체가 필요하면 `계획 대비 변경 사항`에 이유와 대체 명령을 기록한다.
|
||||||
|
- `검증 결과`에는 실제 stdout/stderr를 붙여 넣는다.
|
||||||
|
- 사용자 리뷰 요청으로 명령을 끝까지 실행하지 못했다면 `사용자 리뷰 요청`에 실행한 명령, 실제 출력, 미실행 명령의 사유를 기록한다.
|
||||||
|
- mobile/UI hang, timeout, 또는 2분 무진행은 blind retry를 중단하고 focused rerun 명령과 screenshot/window/UI-tree evidence path를 남기며, 불가능하면 정확한 사유를 남긴다.
|
||||||
|
|
||||||
|
### API-1 중간 검증
|
||||||
|
```bash
|
||||||
|
$ cd services/core && go test ./internal/projectsync
|
||||||
|
ok github.com/nomadcode/nomadcode-core/internal/projectsync 0.003s
|
||||||
|
```
|
||||||
|
|
||||||
|
### API-3 중간 검증
|
||||||
|
```bash
|
||||||
|
$ cd services/core && go test ./internal/workitempipeline ./internal/http
|
||||||
|
ok github.com/nomadcode/nomadcode-core/internal/workitempipeline 0.003s
|
||||||
|
ok github.com/nomadcode/nomadcode-core/internal/http 0.003s
|
||||||
|
```
|
||||||
|
|
||||||
|
### 최종 검증
|
||||||
|
```bash
|
||||||
|
$ cd services/core && go test ./... && git diff --check
|
||||||
|
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/config (cached)
|
||||||
|
? github.com/nomadcode/nomadcode-core/internal/db [no test files]
|
||||||
|
ok github.com/nomadcode/nomadcode-core/internal/http (cached)
|
||||||
|
? github.com/nomadcode/nomadcode-core/internal/model [no test files]
|
||||||
|
ok github.com/nomadcode/nomadcode-core/internal/notification (cached)
|
||||||
|
ok github.com/nomadcode/nomadcode-core/internal/projectsync (cached)
|
||||||
|
ok github.com/nomadcode/nomadcode-core/internal/protosocket 0.011s
|
||||||
|
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)
|
||||||
|
# git diff --check: 출력 없음 (whitespace 이상 없음)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
# Complete - m-milestone-work-item-creation-sync/01_workspace_topology
|
||||||
|
|
||||||
|
## 완료 일시
|
||||||
|
|
||||||
|
2026-06-13
|
||||||
|
|
||||||
|
## 요약
|
||||||
|
|
||||||
|
Workspace topology foundation 구현을 1회 리뷰 루프로 검토했고 최종 판정은 PASS다.
|
||||||
|
|
||||||
|
## 루프 이력
|
||||||
|
|
||||||
|
| Plan | Review | Verdict | 메모 |
|
||||||
|
|------|--------|---------|------|
|
||||||
|
| `plan_local_G06_0.log` | `code_review_local_G06_0.log` | PASS | branch workspace topology, provision plan, checkout metadata 구현과 검증 증거 확인 완료 |
|
||||||
|
|
||||||
|
## 구현/정리 내용
|
||||||
|
|
||||||
|
- `projectsync`에 reserved branch workspace path, branch sync state, provision plan을 추가했다.
|
||||||
|
- slot checkout path를 `slots/NNN` 구조로 고정하고 branch workspace와 분리했다.
|
||||||
|
- work item pipeline checkout metadata에 `main_branch_path`, `develop_branch_path`를 추가했다.
|
||||||
|
- 관련 core 단위 테스트와 HTTP binder 테스트 fixture를 새 topology에 맞춰 갱신했다.
|
||||||
|
|
||||||
|
## 최종 검증
|
||||||
|
|
||||||
|
- `cd services/core && go test ./internal/projectsync ./internal/workitempipeline ./internal/http` - PASS; 대상 패키지 테스트 통과, 최초 리뷰 실행은 cache 출력.
|
||||||
|
- `cd services/core && go test ./...` - PASS; 전체 core 테스트 통과, 패키지 대부분 cache 출력.
|
||||||
|
- `cd services/core && go test -count=1 ./internal/projectsync ./internal/workitempipeline ./internal/http` - PASS; 변경 패키지 실제 재실행 통과.
|
||||||
|
- `git diff --check` - PASS; 출력 없음.
|
||||||
|
|
||||||
|
## Roadmap Completion
|
||||||
|
|
||||||
|
- Milestone: `agent-roadmap/phase/agent-ops-mcp-control-plane/milestones/milestone-work-item-creation-sync.md`
|
||||||
|
- Completed task ids:
|
||||||
|
- `cycle-workspace-topology`: PASS; evidence=`agent-task/archive/2026/06/m-milestone-work-item-creation-sync/01_workspace_topology/plan_local_G06_0.log`, `agent-task/archive/2026/06/m-milestone-work-item-creation-sync/01_workspace_topology/code_review_local_G06_0.log`; verification=`cd services/core && go test ./...`, `cd services/core && go test -count=1 ./internal/projectsync ./internal/workitempipeline ./internal/http`, `git diff --check`
|
||||||
|
- Not completed task ids: 없음
|
||||||
|
|
||||||
|
## 잔여 Nit
|
||||||
|
|
||||||
|
- 없음
|
||||||
|
|
||||||
|
## 후속 작업
|
||||||
|
|
||||||
|
- 없음
|
||||||
|
|
@ -0,0 +1,238 @@
|
||||||
|
<!-- task=m-milestone-work-item-creation-sync/01_workspace_topology plan=0 tag=API -->
|
||||||
|
|
||||||
|
# Plan - API Workspace Topology
|
||||||
|
|
||||||
|
## 이 파일을 읽는 구현 에이전트에게
|
||||||
|
|
||||||
|
`CODE_REVIEW-local-G06.md`의 구현 에이전트 소유 섹션을 반드시 채운다. 검증 명령을 실행하고 실제 출력과 구현 메모를 남긴 뒤 active 파일을 유지한 채 리뷰 준비를 보고한다. 사용자 전용 결정, 사용자 소유 외부 환경, 범위 충돌로 막히면 review stub의 `사용자 리뷰 요청`에 근거를 기록하고 멈춘다. 구현 중 사용자에게 직접 질문하거나 `request_user_input`을 호출하지 않는다. 후속 에이전트가 명령 재실행으로 닫을 수 있는 증거 공백은 사용자 리뷰 요청이 아니다.
|
||||||
|
|
||||||
|
## 배경
|
||||||
|
|
||||||
|
현재 Milestone은 `branches/main`, `branches/develop`, `slots/000+`를 분리해야 한다. 방금 `SlotWorkspacePath`는 `root/slots/NNN`으로 바로 수정했지만 branch workspace helper, branch sync state, checkout/provision policy는 아직 코드에 없다. 이 plan은 `cycle-workspace-topology` 완료에 필요한 남은 topology foundation만 닫는다.
|
||||||
|
|
||||||
|
## 사용자 리뷰 요청 흐름
|
||||||
|
|
||||||
|
구현 중 차단은 `agent-ops/skills/common/_templates/implementation-user-review-request-section.md` 형식으로 active review stub의 `사용자 리뷰 요청`에 기록한다. 직접 사용자 프롬프트는 금지이며, code-review가 요청 타당성과 `USER_REVIEW.md` 생성을 판단한다.
|
||||||
|
|
||||||
|
## Roadmap Targets
|
||||||
|
|
||||||
|
- Milestone: `agent-roadmap/phase/agent-ops-mcp-control-plane/milestones/milestone-work-item-creation-sync.md`
|
||||||
|
- Task ids:
|
||||||
|
- `cycle-workspace-topology`: project workspace root 아래 `branches/main`, `branches/develop`, `slots/000+` 구조를 준비하고 branch workspace와 agent slot 상태를 분리한다.
|
||||||
|
- Completion mode: check-on-pass
|
||||||
|
|
||||||
|
## 분석 결과
|
||||||
|
|
||||||
|
### 읽은 파일
|
||||||
|
|
||||||
|
- `agent-ops/rules/project/domain/core/rules.md`
|
||||||
|
- `agent-ops/rules/project/domain/workspace-ops/rules.md`
|
||||||
|
- `agent-test/local/rules.md`
|
||||||
|
- `agent-test/local/core-smoke.md`
|
||||||
|
- `agent-test/local/workspace-ops-smoke.md`
|
||||||
|
- `agent-roadmap/current.md`
|
||||||
|
- `agent-roadmap/phase/agent-ops-mcp-control-plane/PHASE.md`
|
||||||
|
- `agent-roadmap/phase/agent-ops-mcp-control-plane/milestones/milestone-work-item-creation-sync.md`
|
||||||
|
- `services/core/internal/projectsync/config.go`
|
||||||
|
- `services/core/internal/projectsync/config_test.go`
|
||||||
|
- `services/core/internal/projectsync/checkout.go`
|
||||||
|
- `services/core/internal/projectsync/checkout_test.go`
|
||||||
|
- `services/core/internal/workitempipeline/service.go`
|
||||||
|
- `services/core/internal/workitempipeline/service_test.go`
|
||||||
|
- `services/core/internal/http/handlers_test.go`
|
||||||
|
- `services/core/internal/storage/workspace_slots.go`
|
||||||
|
- `services/core/internal/storage/workspace_slots_test.go`
|
||||||
|
|
||||||
|
### 테스트 환경 규칙
|
||||||
|
|
||||||
|
- test_env: `local`.
|
||||||
|
- `agent-test/local/rules.md` 읽음. core 변경은 `cd services/core && go test ./...`가 필수다.
|
||||||
|
- matched profiles: `agent-test/local/core-smoke.md`, `agent-test/local/workspace-ops-smoke.md`.
|
||||||
|
- 적용 명령: `cd services/core && go test ./...`, 문서/plan 변경 preflight로 `git diff --check`.
|
||||||
|
- `<확인 필요>` 값 없음. shell helper 변경이 아니므로 `bash -n bin/test bin/lint bin/build bin/dev`는 제외한다.
|
||||||
|
|
||||||
|
### 테스트 커버리지 공백
|
||||||
|
|
||||||
|
- branch workspace path/state helper는 새 단위 테스트가 필요하다.
|
||||||
|
- workspace provision policy는 실제 git clone/fetch를 실행하지 않는 policy object와 fake executor 테스트로 한정한다. 실제 remote checkout e2e는 이번 plan 범위에서 제외한다.
|
||||||
|
|
||||||
|
### 심볼 참조
|
||||||
|
|
||||||
|
- renamed/removed symbols: none.
|
||||||
|
|
||||||
|
### 분할 판단
|
||||||
|
|
||||||
|
- split decision policy를 평가했다.
|
||||||
|
- shared task group: `agent-task/m-milestone-work-item-creation-sync/`.
|
||||||
|
- `01_workspace_topology`: 독립 선행 foundation, dependencies: none.
|
||||||
|
- `02+01_iop_authoring_bridge`: predecessor `01` missing.
|
||||||
|
- `03+02_develop_match`: predecessor `02` missing.
|
||||||
|
- `04+03_plane_todo_projection`: predecessor `03` missing.
|
||||||
|
- `05+03,04_idempotency_retry`: predecessors `03`, `04` missing.
|
||||||
|
|
||||||
|
### 범위 결정 근거
|
||||||
|
|
||||||
|
- IOP `/v1/responses` 호출, authoring run 상태 저장, Plane projection, identity map migration은 후속 split plan으로 제외한다.
|
||||||
|
- 실제 git process 실행과 원격 push는 deterministic unit test로 다루기 어렵고 다음 단계 authoring/sync와 결합되므로 이번 plan에서는 interface와 policy 검증까지만 포함한다.
|
||||||
|
|
||||||
|
### 빌드 등급
|
||||||
|
|
||||||
|
- build: `local-G06`. core package 경계가 작고 테스트로 검증 가능하지만 후속 split의 foundation이라 보수적으로 G06으로 둔다.
|
||||||
|
|
||||||
|
## 구현 체크리스트
|
||||||
|
|
||||||
|
- [ ] branch workspace path helper와 branch sync state 타입을 추가하고 `main`, `develop` 고정 workspace를 agent slot과 분리한다.
|
||||||
|
- [ ] checkout/provision policy가 `branches/main`, `branches/develop`, 최소 `slots/000` 준비 계획을 만들고 missing workspace를 임의 cwd로 대체하지 않도록 오류를 반환한다.
|
||||||
|
- [ ] work item pipeline checkout metadata 또는 project sync metadata에 branch workspace root와 slot path가 함께 남도록 필요한 typed field를 추가한다.
|
||||||
|
- [ ] `cd services/core && go test ./...`와 `git diff --check`를 실행한다.
|
||||||
|
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
|
||||||
|
|
||||||
|
### [API-1] Branch Workspace Topology
|
||||||
|
|
||||||
|
#### 문제
|
||||||
|
|
||||||
|
[services/core/internal/projectsync/config.go](/config/workspace/nomadcode/services/core/internal/projectsync/config.go:140)는 project root와 slot path만 제공한다. Milestone은 `branches/main`, `branches/develop`, `slots/000+` 분리를 요구한다([milestone-work-item-creation-sync.md](/config/workspace/nomadcode/agent-roadmap/phase/agent-ops-mcp-control-plane/milestones/milestone-work-item-creation-sync.md:53)).
|
||||||
|
|
||||||
|
#### 해결 방법
|
||||||
|
|
||||||
|
`projectsync`에 `BranchName`, `BranchSyncState`, `BranchWorkspacePath`, `RequiredBranchWorkspaces`를 추가한다.
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Before: services/core/internal/projectsync/config.go:140
|
||||||
|
func ProjectWorkspaceRoot(workspaceBasePath, repoDirName string) (string, error) {
|
||||||
|
```
|
||||||
|
|
||||||
|
```go
|
||||||
|
// After
|
||||||
|
type BranchName string
|
||||||
|
const (
|
||||||
|
BranchNameMain BranchName = "main"
|
||||||
|
BranchNameDevelop BranchName = "develop"
|
||||||
|
)
|
||||||
|
func BranchWorkspacePath(workspaceBasePath, repoDirName string, branch BranchName) (string, error) {
|
||||||
|
...
|
||||||
|
return filepath.Join(root, "branches", string(branch)), nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 수정 파일 및 체크리스트
|
||||||
|
|
||||||
|
- [ ] `services/core/internal/projectsync/config.go`
|
||||||
|
- [ ] `services/core/internal/projectsync/config_test.go`
|
||||||
|
|
||||||
|
#### 테스트 작성
|
||||||
|
|
||||||
|
- `TestBranchWorkspacePathsUseReservedBranches` 추가: `main`, `develop` 경로와 invalid branch 거부를 검증한다.
|
||||||
|
- `TestBranchSyncStateValidation` 추가: `synced`, `stale`, `error`만 유효하게 둔다.
|
||||||
|
|
||||||
|
#### 중간 검증
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd services/core && go test ./internal/projectsync
|
||||||
|
```
|
||||||
|
|
||||||
|
### [API-2] Provision Policy
|
||||||
|
|
||||||
|
#### 문제
|
||||||
|
|
||||||
|
[services/core/internal/projectsync/checkout.go](/config/workspace/nomadcode/services/core/internal/projectsync/checkout.go:20)는 single slot checkout plan만 만든다. `branches/develop` scan 기준과 `branches/main` release 기준을 준비하는 plan이 없다.
|
||||||
|
|
||||||
|
#### 해결 방법
|
||||||
|
|
||||||
|
`TopologyPlan` 또는 `ProvisionPlan`을 추가해 root, branch workspaces, default slot, source branch를 한 번에 반환한다. 실제 git 실행은 하지 않는다.
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Before: services/core/internal/projectsync/checkout.go:42
|
||||||
|
return CheckoutPlan{
|
||||||
|
```
|
||||||
|
|
||||||
|
```go
|
||||||
|
// After
|
||||||
|
type ProvisionPlan struct {
|
||||||
|
ProjectWorkspaceRoot string
|
||||||
|
MainBranchPath string
|
||||||
|
DevelopBranchPath string
|
||||||
|
DefaultSlotPath string
|
||||||
|
}
|
||||||
|
func BuildProvisionPlan(config Config) (ProvisionPlan, error) { ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 수정 파일 및 체크리스트
|
||||||
|
|
||||||
|
- [ ] `services/core/internal/projectsync/checkout.go`
|
||||||
|
- [ ] `services/core/internal/projectsync/checkout_test.go`
|
||||||
|
|
||||||
|
#### 테스트 작성
|
||||||
|
|
||||||
|
- `TestBuildProvisionPlanIncludesBranchAndDefaultSlotPaths` 추가.
|
||||||
|
- `TestBuildProvisionPlanRejectsInvalidConfig` 추가.
|
||||||
|
|
||||||
|
#### 중간 검증
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd services/core && go test ./internal/projectsync
|
||||||
|
```
|
||||||
|
|
||||||
|
### [API-3] Metadata Carries Branch And Slot Topology
|
||||||
|
|
||||||
|
#### 문제
|
||||||
|
|
||||||
|
[services/core/internal/workitempipeline/service.go](/config/workspace/nomadcode/services/core/internal/workitempipeline/service.go:49)는 checkout metadata에 project root와 slot만 기록한다. 후속 sync layer가 `branches/develop` 기준과 slot local path를 구분하려면 branch workspace metadata가 필요하다.
|
||||||
|
|
||||||
|
#### 해결 방법
|
||||||
|
|
||||||
|
`CheckoutMetadata`에 `DevelopBranchPath`, `MainBranchPath`를 additive field로 추가하고 `BuildProvisionPlan` 결과를 metadata에 포함한다.
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Before: services/core/internal/workitempipeline/service.go:53
|
||||||
|
type CheckoutMetadata struct {
|
||||||
|
```
|
||||||
|
|
||||||
|
```go
|
||||||
|
// After
|
||||||
|
type CheckoutMetadata struct {
|
||||||
|
...
|
||||||
|
MainBranchPath string `json:"main_branch_path"`
|
||||||
|
DevelopBranchPath string `json:"develop_branch_path"`
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 수정 파일 및 체크리스트
|
||||||
|
|
||||||
|
- [ ] `services/core/internal/workitempipeline/service.go`
|
||||||
|
- [ ] `services/core/internal/workitempipeline/service_test.go`
|
||||||
|
- [ ] `services/core/internal/http/handlers_test.go`
|
||||||
|
|
||||||
|
#### 테스트 작성
|
||||||
|
|
||||||
|
- 기존 `TestCreateTaskFromWorkItemFetchesMapsAndRecordsCheckout`에 branch path assertions를 추가한다.
|
||||||
|
- project별 slot isolation test에도 `slots/<index>`와 branch paths가 섞이지 않음을 검증한다.
|
||||||
|
|
||||||
|
#### 중간 검증
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd services/core && go test ./internal/workitempipeline ./internal/http
|
||||||
|
```
|
||||||
|
|
||||||
|
## 수정 파일 요약
|
||||||
|
|
||||||
|
| 파일 | 항목 |
|
||||||
|
|------|------|
|
||||||
|
| `services/core/internal/projectsync/config.go` | API-1 |
|
||||||
|
| `services/core/internal/projectsync/config_test.go` | API-1 |
|
||||||
|
| `services/core/internal/projectsync/checkout.go` | API-2 |
|
||||||
|
| `services/core/internal/projectsync/checkout_test.go` | API-2 |
|
||||||
|
| `services/core/internal/workitempipeline/service.go` | API-3 |
|
||||||
|
| `services/core/internal/workitempipeline/service_test.go` | API-3 |
|
||||||
|
| `services/core/internal/http/handlers_test.go` | API-3 |
|
||||||
|
|
||||||
|
## 최종 검증
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd services/core && go test ./...
|
||||||
|
git diff --check
|
||||||
|
```
|
||||||
|
|
||||||
|
Go test cache output is acceptable for unchanged packages; changed packages must appear in output or be rerun directly as 중간 검증.
|
||||||
|
|
||||||
|
모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.
|
||||||
|
|
@ -0,0 +1,120 @@
|
||||||
|
<!-- task=m-milestone-work-item-creation-sync/02+01_iop_authoring_bridge 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`.
|
||||||
|
> Do not ask the user directly, present choices in chat, or call `request_user_input` during implementation.
|
||||||
|
> Finalization is review-agent-only.
|
||||||
|
|
||||||
|
## 개요
|
||||||
|
|
||||||
|
date=2026-06-13
|
||||||
|
task=m-milestone-work-item-creation-sync/02+01_iop_authoring_bridge, plan=0, tag=API
|
||||||
|
|
||||||
|
## Roadmap Targets
|
||||||
|
|
||||||
|
- Milestone: `agent-roadmap/phase/agent-ops-mcp-control-plane/milestones/milestone-work-item-creation-sync.md`
|
||||||
|
- Task ids:
|
||||||
|
- `cycle-iop-http-ready`: IOP Edge OpenAI-compatible HTTP(`/v1/responses`) workspace agent 실행 통로를 선행 조건으로 확인한다.
|
||||||
|
- `cycle-authoring-run`: Plane 티켓 제목/본문과 provider work item identity를 입력으로 `slots/<index>` workspace에서 1회 authoring run을 실행한다.
|
||||||
|
- Completion mode: check-on-pass
|
||||||
|
|
||||||
|
## 이 파일을 읽는 리뷰 에이전트에게
|
||||||
|
|
||||||
|
> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다.
|
||||||
|
|
||||||
|
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
|
||||||
|
|
||||||
|
## 구현 항목별 완료 여부
|
||||||
|
|
||||||
|
| 항목 | 완료 여부 |
|
||||||
|
|------|---------|
|
||||||
|
| [API-1] Responses Metadata Workspace Shape | [ ] |
|
||||||
|
| [API-2] Authoring Run Builder | [ ] |
|
||||||
|
| [API-3] Authoring State Boundary | [ ] |
|
||||||
|
|
||||||
|
## 구현 체크리스트
|
||||||
|
|
||||||
|
- [ ] `model.GenerateInput.Metadata`가 `metadata.workspace` object를 표현할 수 있게 additive typed metadata 구조를 도입하고 기존 flat metadata 호환을 유지한다.
|
||||||
|
- [ ] IOP workspace authoring request builder를 추가해 slot path, Plane ticket title/body, provider identity, project sync 설정, roadmap 작성 지시를 `/v1/responses` payload에 담는다.
|
||||||
|
- [ ] authoring run 상태 모델 `in_progress`, `succeeded`, `failed`와 stale 판단 기준을 저장 가능한 typed metadata 또는 새 persistence 경계로 남긴다.
|
||||||
|
- [ ] scheduler 또는 work item pipeline에서 Plane-origin authoring mode task가 일반 model completion과 구분되어 IOP HTTP bridge를 호출하도록 연결한다.
|
||||||
|
- [ ] `cd services/core && go test ./...`와 `git diff --check`를 실행한다.
|
||||||
|
- [ ] 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-milestone-work-item-creation-sync/02+01_iop_authoring_bridge/`를 archive로 이동한다.
|
||||||
|
- [ ] PASS이고 task group이 `m-<milestone-slug>`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다.
|
||||||
|
- [ ] PASS split 작업이면 이동 후 빈 active parent를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다.
|
||||||
|
- [ ] WARN/FAIL이면 다음 active plan/review 또는 `USER_REVIEW.md`를 작성한다.
|
||||||
|
|
||||||
|
## 계획 대비 변경 사항
|
||||||
|
|
||||||
|
_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._
|
||||||
|
|
||||||
|
## 주요 설계 결정
|
||||||
|
|
||||||
|
_구현 에이전트가 주요 설계 결정 사항을 기록한다._
|
||||||
|
|
||||||
|
## 사용자 리뷰 요청
|
||||||
|
|
||||||
|
_기본값은 `없음`이다. 구현 중 사용자 결정, 사용자 소유 외부 환경/secret/서비스 준비, 또는 계획 범위 변경 없이는 안전하게 진행할 수 없으면 아래 항목을 실제 내용으로 교체하고, 구현을 중단한 뒤 active 파일을 그대로 둔 채 리뷰를 요청한다._
|
||||||
|
|
||||||
|
- 상태: 없음
|
||||||
|
- 사유 유형: 없음
|
||||||
|
- 결정 필요: 없음
|
||||||
|
- 차단 근거: 없음
|
||||||
|
- 실행한 검증/명령: 없음
|
||||||
|
- 자동 후속 불가 이유: 없음
|
||||||
|
- 재개 조건: 없음
|
||||||
|
|
||||||
|
## 리뷰어를 위한 체크포인트
|
||||||
|
|
||||||
|
- `/v1/responses` request에 `metadata.workspace` object가 실제 JSON object로 들어가는지 확인한다.
|
||||||
|
- 일반 model task와 Plane-origin authoring task가 섞이지 않는지 확인한다.
|
||||||
|
- IOP CLI 실행이나 A2A fallback이 이 plan에 들어오지 않았는지 확인한다.
|
||||||
|
|
||||||
|
## 검증 결과
|
||||||
|
|
||||||
|
_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._
|
||||||
|
|
||||||
|
### API-1 중간 검증
|
||||||
|
```bash
|
||||||
|
$ cd services/core && go test ./internal/adapters/openai
|
||||||
|
(output)
|
||||||
|
```
|
||||||
|
|
||||||
|
### API-2 중간 검증
|
||||||
|
```bash
|
||||||
|
$ cd services/core && go test ./internal/authoring ./internal/scheduler
|
||||||
|
(output)
|
||||||
|
```
|
||||||
|
|
||||||
|
### API-3 중간 검증
|
||||||
|
```bash
|
||||||
|
$ cd services/core && go test ./internal/workflow ./internal/scheduler
|
||||||
|
(output)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 최종 검증
|
||||||
|
```bash
|
||||||
|
$ cd services/core && go test ./...
|
||||||
|
$ git diff --check
|
||||||
|
(output)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
@ -0,0 +1,226 @@
|
||||||
|
<!-- task=m-milestone-work-item-creation-sync/02+01_iop_authoring_bridge plan=0 tag=API -->
|
||||||
|
|
||||||
|
# Plan - API IOP Authoring Bridge
|
||||||
|
|
||||||
|
## 이 파일을 읽는 구현 에이전트에게
|
||||||
|
|
||||||
|
`CODE_REVIEW-cloud-G07.md`의 구현 에이전트 소유 섹션을 반드시 채운다. 검증 명령을 실행하고 실제 출력과 구현 메모를 남긴 뒤 active 파일을 유지한 채 리뷰 준비를 보고한다. 사용자 전용 결정, 사용자 소유 외부 환경, 범위 충돌로 막히면 review stub의 `사용자 리뷰 요청`에 근거를 기록하고 멈춘다. 구현 중 사용자에게 직접 질문하거나 `request_user_input`을 호출하지 않는다.
|
||||||
|
|
||||||
|
## 배경
|
||||||
|
|
||||||
|
현재 OpenAI-compatible client는 `/v1/responses` 호출과 flat string metadata를 지원한다. Milestone은 IOP CLI 직접 실행이 아니라 `model`, `input`, `metadata.workspace`만으로 workspace-bound authoring run을 시작해야 한다. 이 plan은 01 topology 완료 후 Plane 티켓을 slot workspace의 1회 authoring run으로 연결한다.
|
||||||
|
|
||||||
|
## 사용자 리뷰 요청 흐름
|
||||||
|
|
||||||
|
구현 중 차단은 active review stub의 `사용자 리뷰 요청`에 기록한다. 직접 사용자 프롬프트는 금지이며, code-review가 `USER_REVIEW.md` 작성 여부를 판단한다.
|
||||||
|
|
||||||
|
## Roadmap Targets
|
||||||
|
|
||||||
|
- Milestone: `agent-roadmap/phase/agent-ops-mcp-control-plane/milestones/milestone-work-item-creation-sync.md`
|
||||||
|
- Task ids:
|
||||||
|
- `cycle-iop-http-ready`: IOP Edge OpenAI-compatible HTTP(`/v1/responses`) workspace agent 실행 통로를 선행 조건으로 확인한다.
|
||||||
|
- `cycle-authoring-run`: Plane 티켓 제목/본문과 provider work item identity를 입력으로 `slots/<index>` workspace에서 1회 authoring run을 실행한다.
|
||||||
|
- Completion mode: check-on-pass
|
||||||
|
|
||||||
|
## 분석 결과
|
||||||
|
|
||||||
|
### 읽은 파일
|
||||||
|
|
||||||
|
- `agent-ops/rules/project/domain/core/rules.md`
|
||||||
|
- `agent-ops/rules/project/domain/contracts/rules.md`
|
||||||
|
- `agent-test/local/rules.md`
|
||||||
|
- `agent-test/local/core-smoke.md`
|
||||||
|
- `agent-roadmap/phase/agent-ops-mcp-control-plane/milestones/milestone-work-item-creation-sync.md`
|
||||||
|
- `services/core/internal/model/model.go`
|
||||||
|
- `services/core/internal/adapters/openai/client.go`
|
||||||
|
- `services/core/internal/adapters/openai/client_test.go`
|
||||||
|
- `services/core/internal/scheduler/jobs.go`
|
||||||
|
- `services/core/internal/scheduler/jobs_test.go`
|
||||||
|
- `services/core/internal/workitempipeline/service.go`
|
||||||
|
- `services/core/internal/workitem/provider.go`
|
||||||
|
- `services/core/cmd/server/main.go`
|
||||||
|
- `services/core/internal/config/config.go`
|
||||||
|
- `packages/contracts/notes/flutter-core-api-candidates.md`
|
||||||
|
|
||||||
|
### 테스트 환경 규칙
|
||||||
|
|
||||||
|
- test_env: `local`.
|
||||||
|
- `agent-test/local/rules.md`, `agent-test/local/core-smoke.md` 읽음.
|
||||||
|
- core 코드 변경 필수 검증: `cd services/core && go test ./...`.
|
||||||
|
- protocol/contract note 변경 시 `git diff --check`도 실행한다.
|
||||||
|
- 외부 IOP credential 없이 unit test와 httptest 기반 request-shape 검증으로 닫는다.
|
||||||
|
|
||||||
|
### 테스트 커버리지 공백
|
||||||
|
|
||||||
|
- 실제 IOP Edge 원격 실행은 credential/외부 서비스가 필요하므로 unit test에서 mock HTTP server로 request shape와 failure mapping을 검증한다.
|
||||||
|
- agent가 실제 Milestone 파일을 작성하고 push하는 e2e는 03 develop match plan과 연결되며 여기서는 authoring request/result state까지만 검증한다.
|
||||||
|
|
||||||
|
### 심볼 참조
|
||||||
|
|
||||||
|
- renamed/removed symbols: none.
|
||||||
|
|
||||||
|
### 분할 판단
|
||||||
|
|
||||||
|
- split decision policy를 평가했다.
|
||||||
|
- 이 plan은 `02+01_iop_authoring_bridge`이며 predecessor `01_workspace_topology` complete.log가 필요하다. 현재 missing.
|
||||||
|
- 03/04/05는 이 plan의 authoring result와 metadata를 소비하므로 별도 dependent plan으로 분리했다.
|
||||||
|
|
||||||
|
### 범위 결정 근거
|
||||||
|
|
||||||
|
- Plane Todo projection, identity map persistence, idempotency retry는 후속 plan으로 제외한다.
|
||||||
|
- A2A path는 현 Milestone의 1차 통로가 아니므로 변경하지 않는다.
|
||||||
|
|
||||||
|
### 빌드 등급
|
||||||
|
|
||||||
|
- build: `cloud-G07`. 외부 protocol shape, scheduler execution boundary, metadata contract가 맞물려 있어 더 넓은 reasoning과 review가 필요하다.
|
||||||
|
|
||||||
|
## 구현 체크리스트
|
||||||
|
|
||||||
|
- [ ] `model.GenerateInput.Metadata`가 `metadata.workspace` object를 표현할 수 있게 additive typed metadata 구조를 도입하고 기존 flat metadata 호환을 유지한다.
|
||||||
|
- [ ] IOP workspace authoring request builder를 추가해 slot path, Plane ticket title/body, provider identity, project sync 설정, roadmap 작성 지시를 `/v1/responses` payload에 담는다.
|
||||||
|
- [ ] authoring run 상태 모델 `in_progress`, `succeeded`, `failed`와 stale 판단 기준을 저장 가능한 typed metadata 또는 새 persistence 경계로 남긴다.
|
||||||
|
- [ ] scheduler 또는 work item pipeline에서 Plane-origin authoring mode task가 일반 model completion과 구분되어 IOP HTTP bridge를 호출하도록 연결한다.
|
||||||
|
- [ ] `cd services/core && go test ./...`와 `git diff --check`를 실행한다.
|
||||||
|
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
|
||||||
|
|
||||||
|
## 의존 관계 및 구현 순서
|
||||||
|
|
||||||
|
- `01_workspace_topology`의 `complete.log`가 필요하다. 현재 active predecessor만 있고 complete.log는 missing이다.
|
||||||
|
|
||||||
|
### [API-1] Responses Metadata Workspace Shape
|
||||||
|
|
||||||
|
#### 문제
|
||||||
|
|
||||||
|
[services/core/internal/model/model.go](/config/workspace/nomadcode/services/core/internal/model/model.go:12)는 `Metadata map[string]string`만 제공한다. Milestone은 `metadata.workspace` object를 요구한다([milestone-work-item-creation-sync.md](/config/workspace/nomadcode/agent-roadmap/phase/agent-ops-mcp-control-plane/milestones/milestone-work-item-creation-sync.md:54)).
|
||||||
|
|
||||||
|
#### 해결 방법
|
||||||
|
|
||||||
|
기존 callers를 깨지 않도록 `Metadata map[string]string`은 유지하고, `ExtraMetadata map[string]any` 또는 typed `WorkspaceMetadata`를 추가한다. OpenAI adapter는 둘을 병합하되 key 충돌은 typed workspace가 이기게 한다.
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Before: services/core/internal/adapters/openai/client.go:144
|
||||||
|
type responsesRequest struct {
|
||||||
|
Metadata map[string]string `json:"metadata,omitempty"`
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```go
|
||||||
|
// After
|
||||||
|
type responsesRequest struct {
|
||||||
|
Metadata map[string]any `json:"metadata,omitempty"`
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 수정 파일 및 체크리스트
|
||||||
|
|
||||||
|
- [ ] `services/core/internal/model/model.go`
|
||||||
|
- [ ] `services/core/internal/adapters/openai/client.go`
|
||||||
|
- [ ] `services/core/internal/adapters/openai/client_test.go`
|
||||||
|
|
||||||
|
#### 테스트 작성
|
||||||
|
|
||||||
|
- `TestGenerateCallsResponsesAPI`에 `metadata.workspace.path`, `metadata.workspace.source_branch`, provider identity assertion을 추가한다.
|
||||||
|
- 기존 flat `task_id`, `source` metadata가 유지되는지 검증한다.
|
||||||
|
|
||||||
|
#### 중간 검증
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd services/core && go test ./internal/adapters/openai
|
||||||
|
```
|
||||||
|
|
||||||
|
### [API-2] Authoring Run Builder
|
||||||
|
|
||||||
|
#### 문제
|
||||||
|
|
||||||
|
[services/core/internal/scheduler/jobs.go](/config/workspace/nomadcode/services/core/internal/scheduler/jobs.go:210)는 task payload를 generic prompt로 바꿀 뿐 Plane ticket/workspace authoring request를 만들지 않는다.
|
||||||
|
|
||||||
|
#### 해결 방법
|
||||||
|
|
||||||
|
`internal/authoring` 또는 `internal/workitempipeline` 하위에 request builder를 추가한다. 입력은 Plane work item title/body, `CheckoutMetadata`, provider identity이며 출력은 `model.GenerateInput`이다. prompt에는 roadmap Milestone 생성/갱신 지시와 provider identity 보존 지시를 넣는다.
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Before: services/core/internal/scheduler/jobs.go:225
|
||||||
|
return model.GenerateInput{
|
||||||
|
Input: input,
|
||||||
|
Instructions: instructions,
|
||||||
|
Metadata: map[string]string{...},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```go
|
||||||
|
// After
|
||||||
|
if authoring, ok := BuildAuthoringGenerateInput(task); ok {
|
||||||
|
return authoring
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 수정 파일 및 체크리스트
|
||||||
|
|
||||||
|
- [ ] `services/core/internal/authoring/request.go`
|
||||||
|
- [ ] `services/core/internal/authoring/request_test.go`
|
||||||
|
- [ ] `services/core/internal/scheduler/jobs.go`
|
||||||
|
- [ ] `services/core/internal/scheduler/jobs_test.go`
|
||||||
|
|
||||||
|
#### 테스트 작성
|
||||||
|
|
||||||
|
- Plane-origin metadata와 payload가 있을 때 workspace-bound generate input이 만들어지는지 검증한다.
|
||||||
|
- 일반 task는 기존 generic prompt behavior를 유지하는지 검증한다.
|
||||||
|
|
||||||
|
#### 중간 검증
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd services/core && go test ./internal/authoring ./internal/scheduler
|
||||||
|
```
|
||||||
|
|
||||||
|
### [API-3] Authoring State Boundary
|
||||||
|
|
||||||
|
#### 문제
|
||||||
|
|
||||||
|
Milestone은 authoring run 최소 상태와 stale/failed 기준을 요구하지만([milestone-work-item-creation-sync.md](/config/workspace/nomadcode/agent-roadmap/phase/agent-ops-mcp-control-plane/milestones/milestone-work-item-creation-sync.md:88)), 현재 workflow status는 generic task status뿐이다.
|
||||||
|
|
||||||
|
#### 해결 방법
|
||||||
|
|
||||||
|
첫 slice에서는 별도 attempt table을 만들지 않고 task metadata에 `authoring_run_state`, `authoring_updated_at`, `authoring_failure_type`를 기록한다. scheduler completion은 IOP HTTP call 성공만으로 Plane projection까지 가지 않는다.
|
||||||
|
|
||||||
|
#### 수정 파일 및 체크리스트
|
||||||
|
|
||||||
|
- [ ] `services/core/internal/workflow/model.go`
|
||||||
|
- [ ] `services/core/internal/workflow/lifecycle.go`
|
||||||
|
- [ ] `services/core/internal/scheduler/jobs.go`
|
||||||
|
- [ ] `services/core/internal/scheduler/jobs_test.go`
|
||||||
|
|
||||||
|
#### 테스트 작성
|
||||||
|
|
||||||
|
- 성공 시 `authoring_run_state=succeeded`, 실패 시 `failed` metadata가 남는지 lifecycle fake로 검증한다.
|
||||||
|
- timeout은 별도 상태가 아니라 `updated_at` 기준 stale 판정 helper로 검증한다.
|
||||||
|
|
||||||
|
#### 중간 검증
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd services/core && go test ./internal/workflow ./internal/scheduler
|
||||||
|
```
|
||||||
|
|
||||||
|
## 수정 파일 요약
|
||||||
|
|
||||||
|
| 파일 | 항목 |
|
||||||
|
|------|------|
|
||||||
|
| `services/core/internal/model/model.go` | API-1 |
|
||||||
|
| `services/core/internal/adapters/openai/client.go` | API-1 |
|
||||||
|
| `services/core/internal/adapters/openai/client_test.go` | API-1 |
|
||||||
|
| `services/core/internal/authoring/request.go` | API-2 |
|
||||||
|
| `services/core/internal/authoring/request_test.go` | API-2 |
|
||||||
|
| `services/core/internal/scheduler/jobs.go` | API-2, API-3 |
|
||||||
|
| `services/core/internal/scheduler/jobs_test.go` | API-2, API-3 |
|
||||||
|
| `services/core/internal/workflow/model.go` | API-3 |
|
||||||
|
| `services/core/internal/workflow/lifecycle.go` | API-3 |
|
||||||
|
|
||||||
|
## 최종 검증
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd services/core && go test ./...
|
||||||
|
git diff --check
|
||||||
|
```
|
||||||
|
|
||||||
|
Go test cache output is acceptable for unchanged packages; changed packages must appear in output or be rerun directly as 중간 검증.
|
||||||
|
|
||||||
|
모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.
|
||||||
|
|
@ -0,0 +1,86 @@
|
||||||
|
<!-- task=m-milestone-work-item-creation-sync/03+02_develop_match plan=0 tag=API -->
|
||||||
|
|
||||||
|
# Code Review Reference - API
|
||||||
|
|
||||||
|
> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is the mandatory final step of implementation.**
|
||||||
|
> Fill implementation-owned sections, run verification, keep active files in place, and report ready for review. Do not prompt the user directly during implementation.
|
||||||
|
|
||||||
|
## 개요
|
||||||
|
|
||||||
|
date=2026-06-13
|
||||||
|
task=m-milestone-work-item-creation-sync/03+02_develop_match, plan=0, tag=API
|
||||||
|
|
||||||
|
## Roadmap Targets
|
||||||
|
|
||||||
|
- Milestone: `agent-roadmap/phase/agent-ops-mcp-control-plane/milestones/milestone-work-item-creation-sync.md`
|
||||||
|
- Task ids:
|
||||||
|
- `cycle-develop-sync`: sync layer가 `branches/develop` 또는 remote `develop`에서 pushed Milestone을 감지하고 원래 Plane 티켓 identity와 매칭한다.
|
||||||
|
- Completion mode: check-on-pass
|
||||||
|
|
||||||
|
## 이 파일을 읽는 리뷰 에이전트에게
|
||||||
|
|
||||||
|
> **[REVIEW AGENT ONLY]** 종결 절차는 코드리뷰 에이전트 전용이다.
|
||||||
|
|
||||||
|
## 구현 항목별 완료 여부
|
||||||
|
|
||||||
|
| 항목 | 완료 여부 |
|
||||||
|
|------|---------|
|
||||||
|
| [API-1] Roadmap Sync Identity Model | [ ] |
|
||||||
|
| [API-2] Milestone Identity Parser | [ ] |
|
||||||
|
| [API-3] Develop Projection Candidate Gate | [ ] |
|
||||||
|
|
||||||
|
## 구현 체크리스트
|
||||||
|
|
||||||
|
- [ ] `develop` branch workspace 또는 remote revision scan 결과를 표현하는 `roadmapsync` domain model을 추가한다.
|
||||||
|
- [ ] Milestone 파일에서 provider, tenant, project, work item id, milestone path/id를 추출하는 parser를 추가한다.
|
||||||
|
- [ ] pushed/scanned Milestone이 원래 Plane work item identity와 일치할 때만 projection-ready result를 반환한다.
|
||||||
|
- [ ] slot local dirty path나 push 실패 result는 projection-ready가 아니도록 테스트한다.
|
||||||
|
- [ ] `cd services/core && go test ./...`와 `git diff --check`를 실행한다.
|
||||||
|
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
|
||||||
|
|
||||||
|
## 코드리뷰 전용 체크리스트
|
||||||
|
|
||||||
|
- [ ] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
|
||||||
|
- [ ] active 파일을 `.log`로 아카이브하고 PASS이면 `complete.log` 작성 후 task directory를 archive로 이동한다.
|
||||||
|
- [ ] PASS이고 task group이 `m-<milestone-slug>`이면 런타임 완료 이벤트 메타데이터를 보고한다.
|
||||||
|
|
||||||
|
## 계획 대비 변경 사항
|
||||||
|
|
||||||
|
_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._
|
||||||
|
|
||||||
|
## 주요 설계 결정
|
||||||
|
|
||||||
|
_구현 에이전트가 주요 설계 결정 사항을 기록한다._
|
||||||
|
|
||||||
|
## 사용자 리뷰 요청
|
||||||
|
|
||||||
|
- 상태: 없음
|
||||||
|
- 사유 유형: 없음
|
||||||
|
- 결정 필요: 없음
|
||||||
|
- 차단 근거: 없음
|
||||||
|
- 실행한 검증/명령: 없음
|
||||||
|
- 자동 후속 불가 이유: 없음
|
||||||
|
- 재개 조건: 없음
|
||||||
|
|
||||||
|
## 리뷰어를 위한 체크포인트
|
||||||
|
|
||||||
|
- slot local file이나 push 실패만으로 projection-ready가 되지 않는지 확인한다.
|
||||||
|
- Milestone parser가 provider identity 누락을 조용히 성공 처리하지 않는지 확인한다.
|
||||||
|
- sync identity field가 contract note와 충돌하지 않는지 확인한다.
|
||||||
|
|
||||||
|
## 검증 결과
|
||||||
|
|
||||||
|
### API-1 중간 검증
|
||||||
|
```bash
|
||||||
|
$ cd services/core && go test ./internal/roadmapsync
|
||||||
|
(output)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 최종 검증
|
||||||
|
```bash
|
||||||
|
$ cd services/core && go test ./...
|
||||||
|
$ git diff --check
|
||||||
|
(output)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
@ -0,0 +1,184 @@
|
||||||
|
<!-- task=m-milestone-work-item-creation-sync/03+02_develop_match plan=0 tag=API -->
|
||||||
|
|
||||||
|
# Plan - API Develop Milestone Match
|
||||||
|
|
||||||
|
## 이 파일을 읽는 구현 에이전트에게
|
||||||
|
|
||||||
|
`CODE_REVIEW-cloud-G07.md`의 구현 에이전트 소유 섹션을 반드시 채운다. 검증 명령을 실행하고 실제 출력과 구현 메모를 남긴 뒤 active 파일을 유지한 채 리뷰 준비를 보고한다. 사용자 전용 결정이나 외부 환경 차단은 review stub의 `사용자 리뷰 요청`에 근거를 기록하고 멈춘다. 구현 중 사용자에게 직접 질문하지 않는다.
|
||||||
|
|
||||||
|
## 배경
|
||||||
|
|
||||||
|
Plane Todo projection은 slot local 변경이나 push 실패만으로 진행하면 안 된다. source of truth는 `branches/develop` 또는 remote `develop`에 반영된 `agent-roadmap` Milestone이다. 이 plan은 pushed Milestone scan과 provider work item identity match를 구현한다.
|
||||||
|
|
||||||
|
## 사용자 리뷰 요청 흐름
|
||||||
|
|
||||||
|
구현 중 차단은 active review stub의 `사용자 리뷰 요청`에 기록한다. code-review가 요청 타당성을 검증한다.
|
||||||
|
|
||||||
|
## Roadmap Targets
|
||||||
|
|
||||||
|
- Milestone: `agent-roadmap/phase/agent-ops-mcp-control-plane/milestones/milestone-work-item-creation-sync.md`
|
||||||
|
- Task ids:
|
||||||
|
- `cycle-develop-sync`: sync layer가 `branches/develop` 또는 remote `develop`에서 pushed Milestone을 감지하고 원래 Plane 티켓 identity와 매칭한다.
|
||||||
|
- Completion mode: check-on-pass
|
||||||
|
|
||||||
|
## 분석 결과
|
||||||
|
|
||||||
|
### 읽은 파일
|
||||||
|
|
||||||
|
- `agent-ops/rules/project/domain/core/rules.md`
|
||||||
|
- `agent-ops/rules/project/domain/contracts/rules.md`
|
||||||
|
- `agent-test/local/rules.md`
|
||||||
|
- `agent-test/local/core-smoke.md`
|
||||||
|
- `agent-roadmap/phase/agent-ops-mcp-control-plane/milestones/milestone-work-item-creation-sync.md`
|
||||||
|
- `services/core/internal/projectsync/config.go`
|
||||||
|
- `services/core/internal/projectsync/checkout.go`
|
||||||
|
- `services/core/internal/workitempipeline/service.go`
|
||||||
|
- `services/core/internal/workitem/provider.go`
|
||||||
|
- `services/core/internal/storage/project_sync_settings.go`
|
||||||
|
- `services/core/internal/storage/workspace_slots.go`
|
||||||
|
- `services/core/migrations/00004_create_project_sync_settings.sql`
|
||||||
|
- `services/core/migrations/00005_create_workspace_slots.sql`
|
||||||
|
- `packages/contracts/notes/flutter-core-api-candidates.md`
|
||||||
|
|
||||||
|
### 테스트 환경 규칙
|
||||||
|
|
||||||
|
- test_env: `local`.
|
||||||
|
- `agent-test/local/rules.md`, `agent-test/local/core-smoke.md` 읽음.
|
||||||
|
- core 코드 변경 필수 검증: `cd services/core && go test ./...`.
|
||||||
|
- migration/query 변경이 있으면 SQLC 생성 파일과 storage tests를 함께 갱신한다.
|
||||||
|
|
||||||
|
### 테스트 커버리지 공백
|
||||||
|
|
||||||
|
- 실제 remote `develop` fetch/push e2e는 제외하고 fake git scanner로 branch/path/revision matching을 검증한다.
|
||||||
|
- Milestone Markdown parsing은 parser unit test를 추가해야 한다.
|
||||||
|
|
||||||
|
### 심볼 참조
|
||||||
|
|
||||||
|
- renamed/removed symbols: none.
|
||||||
|
|
||||||
|
### 분할 판단
|
||||||
|
|
||||||
|
- 이 plan은 `03+02_develop_match`이며 predecessor `02_iop_authoring_bridge` complete.log가 필요하다. 현재 missing.
|
||||||
|
- Plane mutation과 retry 정책은 04/05에서 분리한다.
|
||||||
|
|
||||||
|
### 범위 결정 근거
|
||||||
|
|
||||||
|
- Plane comment/body/status update는 제외한다.
|
||||||
|
- duplicate 재처리 정책은 identity persistence의 최소 unique constraint까지만 포함하고 full retry는 05에서 처리한다.
|
||||||
|
|
||||||
|
### 빌드 등급
|
||||||
|
|
||||||
|
- build: `cloud-G07`. git/roadmap scan, provider identity, DB shape가 맞물리는 cross-module 작업이다.
|
||||||
|
|
||||||
|
## 구현 체크리스트
|
||||||
|
|
||||||
|
- [ ] `develop` branch workspace 또는 remote revision scan 결과를 표현하는 `roadmapsync` domain model을 추가한다.
|
||||||
|
- [ ] Milestone 파일에서 provider, tenant, project, work item id, milestone path/id를 추출하는 parser를 추가한다.
|
||||||
|
- [ ] pushed/scanned Milestone이 원래 Plane work item identity와 일치할 때만 projection-ready result를 반환한다.
|
||||||
|
- [ ] slot local dirty path나 push 실패 result는 projection-ready가 아니도록 테스트한다.
|
||||||
|
- [ ] `cd services/core && go test ./...`와 `git diff --check`를 실행한다.
|
||||||
|
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
|
||||||
|
|
||||||
|
## 의존 관계 및 구현 순서
|
||||||
|
|
||||||
|
- `02_iop_authoring_bridge`의 `complete.log`가 필요하다. 현재 missing.
|
||||||
|
|
||||||
|
### [API-1] Roadmap Sync Identity Model
|
||||||
|
|
||||||
|
#### 문제
|
||||||
|
|
||||||
|
contract note는 sync identity 후보를 정의하지만([flutter-core-api-candidates.md](/config/workspace/nomadcode/packages/contracts/notes/flutter-core-api-candidates.md:347)), core에는 이를 담는 typed domain이 없다.
|
||||||
|
|
||||||
|
#### 해결 방법
|
||||||
|
|
||||||
|
`services/core/internal/roadmapsync` 패키지를 추가하고 `Identity`, `Revision`, `ScanResult`, `ProjectionCandidate`를 정의한다. provider-neutral shape는 contract note의 필드를 따른다.
|
||||||
|
|
||||||
|
#### 수정 파일 및 체크리스트
|
||||||
|
|
||||||
|
- [ ] `services/core/internal/roadmapsync/identity.go`
|
||||||
|
- [ ] `services/core/internal/roadmapsync/identity_test.go`
|
||||||
|
|
||||||
|
#### 테스트 작성
|
||||||
|
|
||||||
|
- valid milestone identity normalize test.
|
||||||
|
- missing provider/work item id reject test.
|
||||||
|
|
||||||
|
#### 중간 검증
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd services/core && go test ./internal/roadmapsync
|
||||||
|
```
|
||||||
|
|
||||||
|
### [API-2] Milestone Identity Parser
|
||||||
|
|
||||||
|
#### 문제
|
||||||
|
|
||||||
|
Milestone은 provider와 Plane work item id가 Milestone 작업 컨텍스트에 남아야 match 가능하다고 한다([milestone-work-item-creation-sync.md](/config/workspace/nomadcode/agent-roadmap/phase/agent-ops-mcp-control-plane/milestones/milestone-work-item-creation-sync.md:89)). 현재 Milestone Markdown에서 identity를 읽는 parser가 없다.
|
||||||
|
|
||||||
|
#### 해결 방법
|
||||||
|
|
||||||
|
첫 slice에서는 명시 섹션 또는 bullet 형식의 `Provider identity` block을 읽는 작고 엄격한 parser를 만든다. ad hoc string scan은 parser 내부로 제한하고, 외부 API는 typed result만 반환한다.
|
||||||
|
|
||||||
|
#### 수정 파일 및 체크리스트
|
||||||
|
|
||||||
|
- [ ] `services/core/internal/roadmapsync/milestone_parser.go`
|
||||||
|
- [ ] `services/core/internal/roadmapsync/milestone_parser_test.go`
|
||||||
|
|
||||||
|
#### 테스트 작성
|
||||||
|
|
||||||
|
- provider/work item id가 있으면 identity 반환.
|
||||||
|
- identity가 없거나 mismatched provider면 projection candidate를 만들지 않음.
|
||||||
|
|
||||||
|
#### 중간 검증
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd services/core && go test ./internal/roadmapsync
|
||||||
|
```
|
||||||
|
|
||||||
|
### [API-3] Develop Projection Candidate Gate
|
||||||
|
|
||||||
|
#### 문제
|
||||||
|
|
||||||
|
[services/core/internal/workitempipeline/service.go](/config/workspace/nomadcode/services/core/internal/workitempipeline/service.go:89)는 slot을 예약하고 task metadata를 남길 뿐, `branches/develop` 반영 여부를 확인하지 않는다.
|
||||||
|
|
||||||
|
#### 해결 방법
|
||||||
|
|
||||||
|
`roadmapsync.MatchDevelopMilestone`을 추가한다. 입력은 develop workspace path/revision, changed files, expected work item identity이며 출력은 projection-ready 여부와 reason이다. slot local path만 들어온 경우 false를 반환한다.
|
||||||
|
|
||||||
|
#### 수정 파일 및 체크리스트
|
||||||
|
|
||||||
|
- [ ] `services/core/internal/roadmapsync/develop_match.go`
|
||||||
|
- [ ] `services/core/internal/roadmapsync/develop_match_test.go`
|
||||||
|
|
||||||
|
#### 테스트 작성
|
||||||
|
|
||||||
|
- changed file이 `agent-roadmap/phase/*/milestones/*.md`이고 identity가 맞으면 ready.
|
||||||
|
- slot path, feature branch, identity mismatch, missing push revision은 not ready.
|
||||||
|
|
||||||
|
#### 중간 검증
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd services/core && go test ./internal/roadmapsync
|
||||||
|
```
|
||||||
|
|
||||||
|
## 수정 파일 요약
|
||||||
|
|
||||||
|
| 파일 | 항목 |
|
||||||
|
|------|------|
|
||||||
|
| `services/core/internal/roadmapsync/identity.go` | API-1 |
|
||||||
|
| `services/core/internal/roadmapsync/identity_test.go` | API-1 |
|
||||||
|
| `services/core/internal/roadmapsync/milestone_parser.go` | API-2 |
|
||||||
|
| `services/core/internal/roadmapsync/milestone_parser_test.go` | API-2 |
|
||||||
|
| `services/core/internal/roadmapsync/develop_match.go` | API-3 |
|
||||||
|
| `services/core/internal/roadmapsync/develop_match_test.go` | API-3 |
|
||||||
|
|
||||||
|
## 최종 검증
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd services/core && go test ./...
|
||||||
|
git diff --check
|
||||||
|
```
|
||||||
|
|
||||||
|
Go test cache output is acceptable for unchanged packages; changed packages must appear in output or be rerun directly as 중간 검증.
|
||||||
|
|
||||||
|
모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.
|
||||||
|
|
@ -0,0 +1,92 @@
|
||||||
|
<!-- task=m-milestone-work-item-creation-sync/04+03_plane_todo_projection plan=0 tag=API -->
|
||||||
|
|
||||||
|
# Code Review Reference - API
|
||||||
|
|
||||||
|
> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is the mandatory final step of implementation.**
|
||||||
|
> Fill implementation-owned sections, run verification, keep active files in place, and report ready for review. Do not prompt the user directly during implementation.
|
||||||
|
|
||||||
|
## 개요
|
||||||
|
|
||||||
|
date=2026-06-13
|
||||||
|
task=m-milestone-work-item-creation-sync/04+03_plane_todo_projection, plan=0, tag=API
|
||||||
|
|
||||||
|
## Roadmap Targets
|
||||||
|
|
||||||
|
- Milestone: `agent-roadmap/phase/agent-ops-mcp-control-plane/milestones/milestone-work-item-creation-sync.md`
|
||||||
|
- Task ids:
|
||||||
|
- `cycle-plane-todo`: 원본 Plane 본문을 `사용자 요청:` 댓글로 보존하고, `develop`의 Milestone 내용으로 Plane 본문/제목을 갱신한 뒤 `Todo`로 이동한다.
|
||||||
|
- Completion mode: check-on-pass
|
||||||
|
|
||||||
|
## 이 파일을 읽는 리뷰 에이전트에게
|
||||||
|
|
||||||
|
> **[REVIEW AGENT ONLY]** 종결 절차는 코드리뷰 에이전트 전용이다.
|
||||||
|
|
||||||
|
## 구현 항목별 완료 여부
|
||||||
|
|
||||||
|
| 항목 | 완료 여부 |
|
||||||
|
|------|---------|
|
||||||
|
| [API-1] Plane Body And Title Patch | [ ] |
|
||||||
|
| [API-2] Projection Service Ordering | [ ] |
|
||||||
|
| [API-3] Plane Body Formatter | [ ] |
|
||||||
|
|
||||||
|
## 구현 체크리스트
|
||||||
|
|
||||||
|
- [ ] Plane adapter에 work item body/title update capability를 additive로 추가한다.
|
||||||
|
- [ ] Projection service가 댓글 보존 -> body/title patch -> Todo status 이동 순서로 실행하고 중간 실패 시 다음 단계를 건너뛰게 한다.
|
||||||
|
- [ ] Milestone 내용을 Plane body로 변환하는 작은 formatter를 추가하고 제목은 `[milestone-id] 제목`으로 만든다.
|
||||||
|
- [ ] develop projection candidate가 없으면 no-op 또는 explicit not-ready error를 반환한다.
|
||||||
|
- [ ] `cd services/core && go test ./...`와 `git diff --check`를 실행한다.
|
||||||
|
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
|
||||||
|
|
||||||
|
## 코드리뷰 전용 체크리스트
|
||||||
|
|
||||||
|
- [ ] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
|
||||||
|
- [ ] active 파일을 `.log`로 아카이브하고 PASS이면 `complete.log` 작성 후 task directory를 archive로 이동한다.
|
||||||
|
- [ ] PASS이고 task group이 `m-<milestone-slug>`이면 런타임 완료 이벤트 메타데이터를 보고한다.
|
||||||
|
|
||||||
|
## 계획 대비 변경 사항
|
||||||
|
|
||||||
|
_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._
|
||||||
|
|
||||||
|
## 주요 설계 결정
|
||||||
|
|
||||||
|
_구현 에이전트가 주요 설계 결정 사항을 기록한다._
|
||||||
|
|
||||||
|
## 사용자 리뷰 요청
|
||||||
|
|
||||||
|
- 상태: 없음
|
||||||
|
- 사유 유형: 없음
|
||||||
|
- 결정 필요: 없음
|
||||||
|
- 차단 근거: 없음
|
||||||
|
- 실행한 검증/명령: 없음
|
||||||
|
- 자동 후속 불가 이유: 없음
|
||||||
|
- 재개 조건: 없음
|
||||||
|
|
||||||
|
## 리뷰어를 위한 체크포인트
|
||||||
|
|
||||||
|
- comment 보존 실패가 body/status mutation을 막는지 확인한다.
|
||||||
|
- Todo 이동이 develop projection candidate 없이 실행되지 않는지 확인한다.
|
||||||
|
- Plane custom field 전제를 넣지 않았는지 확인한다.
|
||||||
|
|
||||||
|
## 검증 결과
|
||||||
|
|
||||||
|
### API-1 중간 검증
|
||||||
|
```bash
|
||||||
|
$ cd services/core && go test ./internal/adapters/plane ./internal/workitem
|
||||||
|
(output)
|
||||||
|
```
|
||||||
|
|
||||||
|
### API-2 중간 검증
|
||||||
|
```bash
|
||||||
|
$ cd services/core && go test ./internal/roadmapsync ./internal/adapters/plane
|
||||||
|
(output)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 최종 검증
|
||||||
|
```bash
|
||||||
|
$ cd services/core && go test ./...
|
||||||
|
$ git diff --check
|
||||||
|
(output)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
@ -0,0 +1,180 @@
|
||||||
|
<!-- task=m-milestone-work-item-creation-sync/04+03_plane_todo_projection plan=0 tag=API -->
|
||||||
|
|
||||||
|
# Plan - API Plane Todo Projection
|
||||||
|
|
||||||
|
## 이 파일을 읽는 구현 에이전트에게
|
||||||
|
|
||||||
|
`CODE_REVIEW-cloud-G07.md`의 구현 에이전트 소유 섹션을 반드시 채운다. 검증 명령을 실행하고 실제 출력과 구현 메모를 남긴 뒤 active 파일을 유지한 채 리뷰 준비를 보고한다. 사용자 전용 결정이나 외부 환경 차단은 review stub의 `사용자 리뷰 요청`에 근거를 기록하고 멈춘다.
|
||||||
|
|
||||||
|
## 배경
|
||||||
|
|
||||||
|
Develop match가 완료된 뒤 Plane은 사용자 검토 UI로만 갱신된다. 원본 Plane 본문은 `사용자 요청:` 댓글로 먼저 보존해야 하며, develop 반영 전에는 본문 치환이나 Todo 이동을 하면 안 된다.
|
||||||
|
|
||||||
|
## 사용자 리뷰 요청 흐름
|
||||||
|
|
||||||
|
구현 중 차단은 active review stub의 `사용자 리뷰 요청`에 기록한다. 직접 사용자 프롬프트는 금지다.
|
||||||
|
|
||||||
|
## Roadmap Targets
|
||||||
|
|
||||||
|
- Milestone: `agent-roadmap/phase/agent-ops-mcp-control-plane/milestones/milestone-work-item-creation-sync.md`
|
||||||
|
- Task ids:
|
||||||
|
- `cycle-plane-todo`: 원본 Plane 본문을 `사용자 요청:` 댓글로 보존하고, `develop`의 Milestone 내용으로 Plane 본문/제목을 갱신한 뒤 `Todo`로 이동한다.
|
||||||
|
- Completion mode: check-on-pass
|
||||||
|
|
||||||
|
## 분석 결과
|
||||||
|
|
||||||
|
### 읽은 파일
|
||||||
|
|
||||||
|
- `agent-ops/rules/project/domain/core/rules.md`
|
||||||
|
- `agent-test/local/rules.md`
|
||||||
|
- `agent-test/local/core-smoke.md`
|
||||||
|
- `agent-roadmap/phase/agent-ops-mcp-control-plane/milestones/milestone-work-item-creation-sync.md`
|
||||||
|
- `services/core/internal/adapters/plane/client.go`
|
||||||
|
- `services/core/internal/adapters/plane/client_test.go`
|
||||||
|
- `services/core/internal/workitem/provider.go`
|
||||||
|
- `services/core/internal/http/handlers.go`
|
||||||
|
- `packages/contracts/notes/flutter-core-api-candidates.md`
|
||||||
|
|
||||||
|
### 테스트 환경 규칙
|
||||||
|
|
||||||
|
- test_env: `local`.
|
||||||
|
- `agent-test/local/core-smoke.md` 기준으로 `cd services/core && go test ./...` 실행.
|
||||||
|
- Plane token이 필요한 real API smoke는 unit test 대체로 충분하다. raw token은 기록하지 않는다.
|
||||||
|
|
||||||
|
### 테스트 커버리지 공백
|
||||||
|
|
||||||
|
- 실제 Plane dev server Todo 이동 smoke는 이 plan의 필수 검증에서 제외한다. adapter httptest와 projection service fake로 보존 순서와 body/status patch를 검증한다.
|
||||||
|
|
||||||
|
### 심볼 참조
|
||||||
|
|
||||||
|
- renamed/removed symbols: none.
|
||||||
|
|
||||||
|
### 분할 판단
|
||||||
|
|
||||||
|
- 이 plan은 `04+03_plane_todo_projection`이며 predecessor `03_develop_match` complete.log가 필요하다. 현재 missing.
|
||||||
|
- idempotency/retry는 05에서 분리한다.
|
||||||
|
|
||||||
|
### 범위 결정 근거
|
||||||
|
|
||||||
|
- Plane `Todo -> In Progress`, child tickets, completion states는 Milestone 범위 제외이므로 구현하지 않는다.
|
||||||
|
- provider custom fields는 free/self-hosted 전제와 맞지 않아 사용하지 않는다.
|
||||||
|
|
||||||
|
### 빌드 등급
|
||||||
|
|
||||||
|
- build: `cloud-G07`. 외부 adapter mutation 순서와 failure semantics가 중요하다.
|
||||||
|
|
||||||
|
## 구현 체크리스트
|
||||||
|
|
||||||
|
- [ ] Plane adapter에 work item body/title update capability를 additive로 추가한다.
|
||||||
|
- [ ] Projection service가 댓글 보존 -> body/title patch -> Todo status 이동 순서로 실행하고 중간 실패 시 다음 단계를 건너뛰게 한다.
|
||||||
|
- [ ] Milestone 내용을 Plane body로 변환하는 작은 formatter를 추가하고 제목은 `[milestone-id] 제목`으로 만든다.
|
||||||
|
- [ ] develop projection candidate가 없으면 no-op 또는 explicit not-ready error를 반환한다.
|
||||||
|
- [ ] `cd services/core && go test ./...`와 `git diff --check`를 실행한다.
|
||||||
|
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
|
||||||
|
|
||||||
|
## 의존 관계 및 구현 순서
|
||||||
|
|
||||||
|
- `03_develop_match`의 `complete.log`가 필요하다. 현재 missing.
|
||||||
|
|
||||||
|
### [API-1] Plane Body And Title Patch
|
||||||
|
|
||||||
|
#### 문제
|
||||||
|
|
||||||
|
[services/core/internal/adapters/plane/client.go](/config/workspace/nomadcode/services/core/internal/adapters/plane/client.go:226)는 comment 추가와 status update만 제공한다. Milestone 내용으로 body/title을 치환할 adapter capability가 없다.
|
||||||
|
|
||||||
|
#### 해결 방법
|
||||||
|
|
||||||
|
`UpdateWorkItem` 또는 `UpdateIssue` input에 `Title`, `DescriptionHTML`을 추가하고 PATCH body에 non-empty field만 담는다. 기존 `UpdateIssueStatus`는 새 method를 재사용하거나 wrapper로 유지한다.
|
||||||
|
|
||||||
|
#### 수정 파일 및 체크리스트
|
||||||
|
|
||||||
|
- [ ] `services/core/internal/adapters/plane/client.go`
|
||||||
|
- [ ] `services/core/internal/adapters/plane/client_test.go`
|
||||||
|
- [ ] `services/core/internal/workitem/provider.go`
|
||||||
|
|
||||||
|
#### 테스트 작성
|
||||||
|
|
||||||
|
- PATCH body가 `name`, `description_html`, `state`를 정확히 보내는지 httptest로 검증한다.
|
||||||
|
- blank title/body가 의도치 않게 삭제되지 않도록 non-empty field만 patch한다.
|
||||||
|
|
||||||
|
#### 중간 검증
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd services/core && go test ./internal/adapters/plane ./internal/workitem
|
||||||
|
```
|
||||||
|
|
||||||
|
### [API-2] Projection Service Ordering
|
||||||
|
|
||||||
|
#### 문제
|
||||||
|
|
||||||
|
Milestone은 댓글 보존 실패 시 본문 치환으로 넘어가지 말라고 한다([milestone-work-item-creation-sync.md](/config/workspace/nomadcode/agent-roadmap/phase/agent-ops-mcp-control-plane/milestones/milestone-work-item-creation-sync.md:102)). 현재 이 순서를 소유하는 service가 없다.
|
||||||
|
|
||||||
|
#### 해결 방법
|
||||||
|
|
||||||
|
`roadmapsync.ProjectPlaneTodo`를 추가한다. 입력은 develop match candidate, fetched Plane original body, Todo state id, provider client facets다. 순서는 comment -> update body/title -> project status다.
|
||||||
|
|
||||||
|
#### 수정 파일 및 체크리스트
|
||||||
|
|
||||||
|
- [ ] `services/core/internal/roadmapsync/plane_projection.go`
|
||||||
|
- [ ] `services/core/internal/roadmapsync/plane_projection_test.go`
|
||||||
|
|
||||||
|
#### 테스트 작성
|
||||||
|
|
||||||
|
- comment 실패 시 update/status 미호출.
|
||||||
|
- body update 실패 시 status 미호출.
|
||||||
|
- success 시 comment body가 `사용자 요청:` prefix를 포함하고 status가 Todo state로 이동.
|
||||||
|
|
||||||
|
#### 중간 검증
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd services/core && go test ./internal/roadmapsync ./internal/adapters/plane
|
||||||
|
```
|
||||||
|
|
||||||
|
### [API-3] Plane Body Formatter
|
||||||
|
|
||||||
|
#### 문제
|
||||||
|
|
||||||
|
Develop Milestone Markdown을 Plane 본문으로 옮기는 formatter가 없다. 제목도 `[milestone-id] 제목` 형식으로 만들어야 한다([milestone-work-item-creation-sync.md](/config/workspace/nomadcode/agent-roadmap/phase/agent-ops-mcp-control-plane/milestones/milestone-work-item-creation-sync.md:57)).
|
||||||
|
|
||||||
|
#### 해결 방법
|
||||||
|
|
||||||
|
Milestone title, 목표, 범위, 기능 요약을 HTML-safe Markdown/HTML로 변환하는 최소 formatter를 둔다. raw token/secret은 포함하지 않는다.
|
||||||
|
|
||||||
|
#### 수정 파일 및 체크리스트
|
||||||
|
|
||||||
|
- [ ] `services/core/internal/roadmapsync/plane_format.go`
|
||||||
|
- [ ] `services/core/internal/roadmapsync/plane_format_test.go`
|
||||||
|
|
||||||
|
#### 테스트 작성
|
||||||
|
|
||||||
|
- title format `[milestone-slug] Milestone Title`.
|
||||||
|
- body includes Milestone goal and excludes provider secret-like metadata.
|
||||||
|
|
||||||
|
#### 중간 검증
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd services/core && go test ./internal/roadmapsync
|
||||||
|
```
|
||||||
|
|
||||||
|
## 수정 파일 요약
|
||||||
|
|
||||||
|
| 파일 | 항목 |
|
||||||
|
|------|------|
|
||||||
|
| `services/core/internal/adapters/plane/client.go` | API-1 |
|
||||||
|
| `services/core/internal/adapters/plane/client_test.go` | API-1 |
|
||||||
|
| `services/core/internal/workitem/provider.go` | API-1 |
|
||||||
|
| `services/core/internal/roadmapsync/plane_projection.go` | API-2 |
|
||||||
|
| `services/core/internal/roadmapsync/plane_projection_test.go` | API-2 |
|
||||||
|
| `services/core/internal/roadmapsync/plane_format.go` | API-3 |
|
||||||
|
| `services/core/internal/roadmapsync/plane_format_test.go` | API-3 |
|
||||||
|
|
||||||
|
## 최종 검증
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd services/core && go test ./...
|
||||||
|
git diff --check
|
||||||
|
```
|
||||||
|
|
||||||
|
Go test cache output is acceptable for unchanged packages; changed packages must appear in output or be rerun directly as 중간 검증.
|
||||||
|
|
||||||
|
모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.
|
||||||
|
|
@ -0,0 +1,92 @@
|
||||||
|
<!-- task=m-milestone-work-item-creation-sync/05+03,04_idempotency_retry plan=0 tag=API -->
|
||||||
|
|
||||||
|
# Code Review Reference - API
|
||||||
|
|
||||||
|
> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is the mandatory final step of implementation.**
|
||||||
|
> Fill implementation-owned sections, run verification, keep active files in place, and report ready for review. Do not prompt the user directly during implementation.
|
||||||
|
|
||||||
|
## 개요
|
||||||
|
|
||||||
|
date=2026-06-13
|
||||||
|
task=m-milestone-work-item-creation-sync/05+03,04_idempotency_retry, plan=0, tag=API
|
||||||
|
|
||||||
|
## Roadmap Targets
|
||||||
|
|
||||||
|
- Milestone: `agent-roadmap/phase/agent-ops-mcp-control-plane/milestones/milestone-work-item-creation-sync.md`
|
||||||
|
- Task ids:
|
||||||
|
- `cycle-idempotency`: 같은 Plane 티켓 또는 같은 Milestone path 재처리 시 중복 생성 없이 남은 단계만 재시도한다.
|
||||||
|
- Completion mode: check-on-pass
|
||||||
|
|
||||||
|
## 이 파일을 읽는 리뷰 에이전트에게
|
||||||
|
|
||||||
|
> **[REVIEW AGENT ONLY]** 종결 절차는 코드리뷰 에이전트 전용이다.
|
||||||
|
|
||||||
|
## 구현 항목별 완료 여부
|
||||||
|
|
||||||
|
| 항목 | 완료 여부 |
|
||||||
|
|------|---------|
|
||||||
|
| [API-1] Identity Map Persistence | [ ] |
|
||||||
|
| [API-2] Step Ledger | [ ] |
|
||||||
|
| [API-3] Retry Decision Service | [ ] |
|
||||||
|
|
||||||
|
## 구현 체크리스트
|
||||||
|
|
||||||
|
- [ ] Milestone path, provider, tenant, project, work item id, provider revision, roadmap revision을 저장하는 identity map migration/query/store를 추가한다.
|
||||||
|
- [ ] provider work item 또는 Milestone path 재처리 시 existing identity를 찾고 중복 Milestone 생성/authoring을 막는다.
|
||||||
|
- [ ] develop match, comment preserve, body/title update, Todo status move의 step ledger를 저장하고 성공한 step은 재시도하지 않는다.
|
||||||
|
- [ ] actor guard와 revision mismatch가 silent overwrite 대신 conflict/retryable result를 반환하게 한다.
|
||||||
|
- [ ] `cd services/core && go test ./...`와 `git diff --check`를 실행한다.
|
||||||
|
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
|
||||||
|
|
||||||
|
## 코드리뷰 전용 체크리스트
|
||||||
|
|
||||||
|
- [ ] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
|
||||||
|
- [ ] active 파일을 `.log`로 아카이브하고 PASS이면 `complete.log` 작성 후 task directory를 archive로 이동한다.
|
||||||
|
- [ ] PASS이고 task group이 `m-<milestone-slug>`이면 런타임 완료 이벤트 메타데이터를 보고한다.
|
||||||
|
|
||||||
|
## 계획 대비 변경 사항
|
||||||
|
|
||||||
|
_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._
|
||||||
|
|
||||||
|
## 주요 설계 결정
|
||||||
|
|
||||||
|
_구현 에이전트가 주요 설계 결정 사항을 기록한다._
|
||||||
|
|
||||||
|
## 사용자 리뷰 요청
|
||||||
|
|
||||||
|
- 상태: 없음
|
||||||
|
- 사유 유형: 없음
|
||||||
|
- 결정 필요: 없음
|
||||||
|
- 차단 근거: 없음
|
||||||
|
- 실행한 검증/명령: 없음
|
||||||
|
- 자동 후속 불가 이유: 없음
|
||||||
|
- 재개 조건: 없음
|
||||||
|
|
||||||
|
## 리뷰어를 위한 체크포인트
|
||||||
|
|
||||||
|
- provider work item id와 Milestone path 양쪽 unique path가 중복 생성을 막는지 확인한다.
|
||||||
|
- completed step이 재시도에서 다시 호출되지 않는지 확인한다.
|
||||||
|
- actor guard가 NomadCode self mutation을 새 user trigger로 처리하지 않는지 확인한다.
|
||||||
|
|
||||||
|
## 검증 결과
|
||||||
|
|
||||||
|
### API-1 중간 검증
|
||||||
|
```bash
|
||||||
|
$ cd services/core && go test ./internal/storage ./internal/db
|
||||||
|
(output)
|
||||||
|
```
|
||||||
|
|
||||||
|
### API-3 중간 검증
|
||||||
|
```bash
|
||||||
|
$ cd services/core && go test ./internal/roadmapsync ./internal/storage
|
||||||
|
(output)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 최종 검증
|
||||||
|
```bash
|
||||||
|
$ cd services/core && go test ./...
|
||||||
|
$ git diff --check
|
||||||
|
(output)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
@ -0,0 +1,196 @@
|
||||||
|
<!-- task=m-milestone-work-item-creation-sync/05+03,04_idempotency_retry plan=0 tag=API -->
|
||||||
|
|
||||||
|
# Plan - API Idempotency Retry
|
||||||
|
|
||||||
|
## 이 파일을 읽는 구현 에이전트에게
|
||||||
|
|
||||||
|
`CODE_REVIEW-cloud-G07.md`의 구현 에이전트 소유 섹션을 반드시 채운다. 검증 명령을 실행하고 실제 출력과 구현 메모를 남긴 뒤 active 파일을 유지한 채 리뷰 준비를 보고한다. 사용자 전용 결정이나 외부 환경 차단은 review stub의 `사용자 리뷰 요청`에 근거를 기록하고 멈춘다.
|
||||||
|
|
||||||
|
## 배경
|
||||||
|
|
||||||
|
Plane-origin 생성 sync는 중간 단계가 일부만 성공해도 같은 Plane 티켓 또는 Milestone path 재처리 시 중복 생성 없이 남은 단계만 재시도해야 한다. 03은 develop match, 04는 Plane projection을 만들며, 이 plan은 그 둘을 identity map과 step ledger로 묶어 idempotent cycle로 만든다.
|
||||||
|
|
||||||
|
## 사용자 리뷰 요청 흐름
|
||||||
|
|
||||||
|
구현 중 차단은 active review stub의 `사용자 리뷰 요청`에 기록한다. 직접 사용자 프롬프트는 금지다.
|
||||||
|
|
||||||
|
## Roadmap Targets
|
||||||
|
|
||||||
|
- Milestone: `agent-roadmap/phase/agent-ops-mcp-control-plane/milestones/milestone-work-item-creation-sync.md`
|
||||||
|
- Task ids:
|
||||||
|
- `cycle-idempotency`: 같은 Plane 티켓 또는 같은 Milestone path 재처리 시 중복 생성 없이 남은 단계만 재시도한다.
|
||||||
|
- Completion mode: check-on-pass
|
||||||
|
|
||||||
|
## 분석 결과
|
||||||
|
|
||||||
|
### 읽은 파일
|
||||||
|
|
||||||
|
- `agent-ops/rules/project/domain/core/rules.md`
|
||||||
|
- `agent-test/local/rules.md`
|
||||||
|
- `agent-test/local/core-smoke.md`
|
||||||
|
- `agent-roadmap/phase/agent-ops-mcp-control-plane/milestones/milestone-work-item-creation-sync.md`
|
||||||
|
- `services/core/internal/workitem/provider.go`
|
||||||
|
- `services/core/internal/workitempipeline/service.go`
|
||||||
|
- `services/core/internal/storage/project_sync_settings.go`
|
||||||
|
- `services/core/internal/storage/workspace_slots.go`
|
||||||
|
- `services/core/migrations/00004_create_project_sync_settings.sql`
|
||||||
|
- `services/core/migrations/00005_create_workspace_slots.sql`
|
||||||
|
- `packages/contracts/notes/flutter-core-api-candidates.md`
|
||||||
|
|
||||||
|
### 테스트 환경 규칙
|
||||||
|
|
||||||
|
- test_env: `local`.
|
||||||
|
- `agent-test/local/core-smoke.md` 기준으로 `cd services/core && go test ./...` 실행.
|
||||||
|
- DB migration/query 변경은 generated db code와 storage tests를 함께 갱신한다.
|
||||||
|
|
||||||
|
### 테스트 커버리지 공백
|
||||||
|
|
||||||
|
- 실제 Plane/network retry는 fake projector와 fake store로 검증한다.
|
||||||
|
- DB race는 unique index/upsert query shape와 unit test로 검증하고 multi-process DB e2e는 제외한다.
|
||||||
|
|
||||||
|
### 심볼 참조
|
||||||
|
|
||||||
|
- renamed/removed symbols: none.
|
||||||
|
|
||||||
|
### 분할 판단
|
||||||
|
|
||||||
|
- 이 plan은 `05+03,04_idempotency_retry`이며 predecessors `03_develop_match`, `04_plane_todo_projection` complete.log가 필요하다. 현재 both missing.
|
||||||
|
|
||||||
|
### 범위 결정 근거
|
||||||
|
|
||||||
|
- Plane child work item, `Todo -> In Progress`, `Done/User Review/Cancelled` lifecycle은 Milestone 범위 제외이므로 구현하지 않는다.
|
||||||
|
- full saga/orchestrator framework는 과하고, first slice는 identity map + step ledger + retry decision으로 제한한다.
|
||||||
|
|
||||||
|
### 빌드 등급
|
||||||
|
|
||||||
|
- build: `cloud-G07`. persistence, uniqueness, retry ordering의 회귀 위험이 높다.
|
||||||
|
|
||||||
|
## 구현 체크리스트
|
||||||
|
|
||||||
|
- [ ] Milestone path, provider, tenant, project, work item id, provider revision, roadmap revision을 저장하는 identity map migration/query/store를 추가한다.
|
||||||
|
- [ ] provider work item 또는 Milestone path 재처리 시 existing identity를 찾고 중복 Milestone 생성/authoring을 막는다.
|
||||||
|
- [ ] develop match, comment preserve, body/title update, Todo status move의 step ledger를 저장하고 성공한 step은 재시도하지 않는다.
|
||||||
|
- [ ] actor guard와 revision mismatch가 silent overwrite 대신 conflict/retryable result를 반환하게 한다.
|
||||||
|
- [ ] `cd services/core && go test ./...`와 `git diff --check`를 실행한다.
|
||||||
|
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
|
||||||
|
|
||||||
|
## 의존 관계 및 구현 순서
|
||||||
|
|
||||||
|
- `03_develop_match`의 `complete.log`가 필요하다. 현재 missing.
|
||||||
|
- `04_plane_todo_projection`의 `complete.log`가 필요하다. 현재 missing.
|
||||||
|
|
||||||
|
### [API-1] Identity Map Persistence
|
||||||
|
|
||||||
|
#### 문제
|
||||||
|
|
||||||
|
contract note는 identity shape와 idempotency key를 요구하지만([flutter-core-api-candidates.md](/config/workspace/nomadcode/packages/contracts/notes/flutter-core-api-candidates.md:421)), core DB에는 project sync setting과 workspace slot만 있고 Milestone/work item identity map이 없다.
|
||||||
|
|
||||||
|
#### 해결 방법
|
||||||
|
|
||||||
|
`roadmap_sync_identities` migration과 SQLC queries를 추가한다. unique constraints는 `(provider, tenant, project, work_item_id)`와 `(roadmap_milestone_path)`를 둔다. revision fields는 nullable text로 둔다.
|
||||||
|
|
||||||
|
#### 수정 파일 및 체크리스트
|
||||||
|
|
||||||
|
- [ ] `services/core/migrations/00006_create_roadmap_sync_identities.sql`
|
||||||
|
- [ ] `services/core/queries/roadmap_sync_identities.sql`
|
||||||
|
- [ ] `services/core/internal/db/roadmap_sync_identities.sql.go`
|
||||||
|
- [ ] `services/core/internal/storage/roadmap_sync_identities.go`
|
||||||
|
- [ ] `services/core/internal/storage/roadmap_sync_identities_test.go`
|
||||||
|
|
||||||
|
#### 테스트 작성
|
||||||
|
|
||||||
|
- upsert by provider work item returns same row.
|
||||||
|
- upsert by milestone path returns same row.
|
||||||
|
- mismatched provider work item/path pair returns conflict sentinel.
|
||||||
|
|
||||||
|
#### 중간 검증
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd services/core && go test ./internal/storage ./internal/db
|
||||||
|
```
|
||||||
|
|
||||||
|
### [API-2] Step Ledger
|
||||||
|
|
||||||
|
#### 문제
|
||||||
|
|
||||||
|
Milestone은 부분 실패 후 남은 단계만 재시도해야 한다([milestone-work-item-creation-sync.md](/config/workspace/nomadcode/agent-roadmap/phase/agent-ops-mcp-control-plane/milestones/milestone-work-item-creation-sync.md:105)). 현재 어느 projection step이 성공했는지 저장하지 않는다.
|
||||||
|
|
||||||
|
#### 해결 방법
|
||||||
|
|
||||||
|
`roadmap_sync_steps` migration/query/store를 추가한다. steps는 `develop_matched`, `original_comment_preserved`, `plane_body_updated`, `plane_todo_moved`로 제한한다. completed step은 idempotently skip한다.
|
||||||
|
|
||||||
|
#### 수정 파일 및 체크리스트
|
||||||
|
|
||||||
|
- [ ] `services/core/migrations/00007_create_roadmap_sync_steps.sql`
|
||||||
|
- [ ] `services/core/queries/roadmap_sync_steps.sql`
|
||||||
|
- [ ] `services/core/internal/db/roadmap_sync_steps.sql.go`
|
||||||
|
- [ ] `services/core/internal/storage/roadmap_sync_steps.go`
|
||||||
|
- [ ] `services/core/internal/storage/roadmap_sync_steps_test.go`
|
||||||
|
|
||||||
|
#### 테스트 작성
|
||||||
|
|
||||||
|
- completed step 재적용은 no-op.
|
||||||
|
- failed step 이후 재시도는 해당 step부터 재개.
|
||||||
|
- unknown step은 reject.
|
||||||
|
|
||||||
|
#### 중간 검증
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd services/core && go test ./internal/storage
|
||||||
|
```
|
||||||
|
|
||||||
|
### [API-3] Retry Decision Service
|
||||||
|
|
||||||
|
#### 문제
|
||||||
|
|
||||||
|
03/04가 각각 match와 projection을 수행해도 재처리 시 어떤 단계를 건너뛰고 어디서 재개할지 결정하는 service가 없다.
|
||||||
|
|
||||||
|
#### 해결 방법
|
||||||
|
|
||||||
|
`roadmapsync.ReconcileCreationCycle`을 추가해 identity map과 step ledger를 읽고 next action을 반환한다. actor가 NomadCode self mutation이면 trigger로 처리하지 않는 actor guard를 둔다.
|
||||||
|
|
||||||
|
#### 수정 파일 및 체크리스트
|
||||||
|
|
||||||
|
- [ ] `services/core/internal/roadmapsync/retry.go`
|
||||||
|
- [ ] `services/core/internal/roadmapsync/retry_test.go`
|
||||||
|
|
||||||
|
#### 테스트 작성
|
||||||
|
|
||||||
|
- same Plane ticket 재처리 시 identity reuse.
|
||||||
|
- same Milestone path 재처리 시 identity reuse.
|
||||||
|
- provider revision mismatch는 conflict.
|
||||||
|
- NomadCode actor update는 new authoring trigger가 아님.
|
||||||
|
|
||||||
|
#### 중간 검증
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd services/core && go test ./internal/roadmapsync ./internal/storage
|
||||||
|
```
|
||||||
|
|
||||||
|
## 수정 파일 요약
|
||||||
|
|
||||||
|
| 파일 | 항목 |
|
||||||
|
|------|------|
|
||||||
|
| `services/core/migrations/00006_create_roadmap_sync_identities.sql` | API-1 |
|
||||||
|
| `services/core/queries/roadmap_sync_identities.sql` | API-1 |
|
||||||
|
| `services/core/internal/db/roadmap_sync_identities.sql.go` | API-1 |
|
||||||
|
| `services/core/internal/storage/roadmap_sync_identities.go` | API-1 |
|
||||||
|
| `services/core/internal/storage/roadmap_sync_identities_test.go` | API-1 |
|
||||||
|
| `services/core/migrations/00007_create_roadmap_sync_steps.sql` | API-2 |
|
||||||
|
| `services/core/queries/roadmap_sync_steps.sql` | API-2 |
|
||||||
|
| `services/core/internal/db/roadmap_sync_steps.sql.go` | API-2 |
|
||||||
|
| `services/core/internal/storage/roadmap_sync_steps.go` | API-2 |
|
||||||
|
| `services/core/internal/storage/roadmap_sync_steps_test.go` | API-2 |
|
||||||
|
| `services/core/internal/roadmapsync/retry.go` | API-3 |
|
||||||
|
| `services/core/internal/roadmapsync/retry_test.go` | API-3 |
|
||||||
|
|
||||||
|
## 최종 검증
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd services/core && go test ./...
|
||||||
|
git diff --check
|
||||||
|
```
|
||||||
|
|
||||||
|
Go test cache output is acceptable for unchanged packages; changed packages must appear in output or be rerun directly as 중간 검증.
|
||||||
|
|
||||||
|
모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.
|
||||||
|
|
@ -0,0 +1,193 @@
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'proto_socket_envelope.dart';
|
||||||
|
import 'proto_socket_lifecycle.dart';
|
||||||
|
|
||||||
|
class ChangedFile {
|
||||||
|
final String path;
|
||||||
|
final String changeType;
|
||||||
|
|
||||||
|
const ChangedFile({
|
||||||
|
required this.path,
|
||||||
|
required this.changeType,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory ChangedFile.fromMap(Map<String, Object?> map) {
|
||||||
|
final path = map['path'];
|
||||||
|
final changeType = map['change_type'];
|
||||||
|
if (path is! String || changeType is! String) {
|
||||||
|
throw const FormatException('Invalid ChangedFile payload');
|
||||||
|
}
|
||||||
|
return ChangedFile(path: path, changeType: changeType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class BranchUpdatedEvent {
|
||||||
|
final String id;
|
||||||
|
final String type;
|
||||||
|
final String provider;
|
||||||
|
final String deliveryId;
|
||||||
|
final String repoId;
|
||||||
|
final String branch;
|
||||||
|
final String before;
|
||||||
|
final String after;
|
||||||
|
final List<ChangedFile> changedFiles;
|
||||||
|
final String observedAt;
|
||||||
|
final String createdAt;
|
||||||
|
|
||||||
|
const BranchUpdatedEvent({
|
||||||
|
required this.id,
|
||||||
|
required this.type,
|
||||||
|
required this.provider,
|
||||||
|
required this.deliveryId,
|
||||||
|
required this.repoId,
|
||||||
|
required this.branch,
|
||||||
|
required this.before,
|
||||||
|
required this.after,
|
||||||
|
required this.changedFiles,
|
||||||
|
required this.observedAt,
|
||||||
|
required this.createdAt,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory BranchUpdatedEvent.fromMap(Map<String, Object?> map) {
|
||||||
|
final id = map['id'];
|
||||||
|
final type = map['type'];
|
||||||
|
final provider = map['provider'];
|
||||||
|
final deliveryId = map['delivery_id'];
|
||||||
|
final repoId = map['repo_id'];
|
||||||
|
final branch = map['branch'];
|
||||||
|
final before = map['before'];
|
||||||
|
final after = map['after'];
|
||||||
|
final rawFiles = map['changed_files'];
|
||||||
|
final observedAt = map['observed_at'];
|
||||||
|
final createdAt = map['created_at'];
|
||||||
|
|
||||||
|
if (id is! String ||
|
||||||
|
type is! String ||
|
||||||
|
provider is! String ||
|
||||||
|
deliveryId is! String ||
|
||||||
|
repoId is! String ||
|
||||||
|
branch is! String ||
|
||||||
|
before is! String ||
|
||||||
|
after is! String ||
|
||||||
|
rawFiles is! List ||
|
||||||
|
observedAt is! String ||
|
||||||
|
createdAt is! String) {
|
||||||
|
throw const FormatException('Invalid BranchUpdatedEvent payload');
|
||||||
|
}
|
||||||
|
|
||||||
|
final changedFiles = <ChangedFile>[];
|
||||||
|
for (final file in rawFiles) {
|
||||||
|
if (file is! Map) {
|
||||||
|
throw const FormatException('Invalid ChangedFile item');
|
||||||
|
}
|
||||||
|
changedFiles.add(ChangedFile.fromMap(Map<String, Object?>.from(file)));
|
||||||
|
}
|
||||||
|
|
||||||
|
return BranchUpdatedEvent(
|
||||||
|
id: id,
|
||||||
|
type: type,
|
||||||
|
provider: provider,
|
||||||
|
deliveryId: deliveryId,
|
||||||
|
repoId: repoId,
|
||||||
|
branch: branch,
|
||||||
|
before: before,
|
||||||
|
after: after,
|
||||||
|
changedFiles: changedFiles,
|
||||||
|
observedAt: observedAt,
|
||||||
|
createdAt: createdAt,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ProtoSocketBranchEventException implements Exception {
|
||||||
|
final String code;
|
||||||
|
final String message;
|
||||||
|
final bool retryable;
|
||||||
|
|
||||||
|
const ProtoSocketBranchEventException({
|
||||||
|
required this.code,
|
||||||
|
required this.message,
|
||||||
|
required this.retryable,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory ProtoSocketBranchEventException.fromError(ProtoSocketEnvelopeError error) {
|
||||||
|
return ProtoSocketBranchEventException(
|
||||||
|
code: error.code,
|
||||||
|
message: error.message,
|
||||||
|
retryable: error.retryable,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() =>
|
||||||
|
'ProtoSocketBranchEventException(code: $code, message: $message, '
|
||||||
|
'retryable: $retryable)';
|
||||||
|
}
|
||||||
|
|
||||||
|
class ProtoSocketBranchEventService {
|
||||||
|
final ProtoSocketTransport _transport;
|
||||||
|
final StreamController<BranchUpdatedEvent> _updates =
|
||||||
|
StreamController<BranchUpdatedEvent>.broadcast();
|
||||||
|
final Set<String> _filters = {};
|
||||||
|
StreamSubscription<ProtoSocketEnvelope>? _subscription;
|
||||||
|
int _messageCounter = 0;
|
||||||
|
|
||||||
|
ProtoSocketBranchEventService(this._transport) {
|
||||||
|
_subscription = _transport.events.listen(
|
||||||
|
_handleEnvelope,
|
||||||
|
onError: _updates.addError,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Stream<BranchUpdatedEvent> get branchUpdates => _updates.stream;
|
||||||
|
|
||||||
|
Future<void> subscribe({required String repoId, required String branch}) async {
|
||||||
|
final response = await _transport.sendRequest(ProtoSocketEnvelope(
|
||||||
|
id: _newMessageId(),
|
||||||
|
type: 'request',
|
||||||
|
channel: 'event',
|
||||||
|
action: 'event.subscribe',
|
||||||
|
payload: {
|
||||||
|
'events': ['branch.updated'],
|
||||||
|
'repo_id': repoId,
|
||||||
|
'branch': branch,
|
||||||
|
},
|
||||||
|
));
|
||||||
|
|
||||||
|
final error = response.error;
|
||||||
|
if (error != null) {
|
||||||
|
throw ProtoSocketBranchEventException.fromError(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
_filters.add('$repoId:$branch');
|
||||||
|
}
|
||||||
|
|
||||||
|
void dispose() {
|
||||||
|
_subscription?.cancel();
|
||||||
|
_updates.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleEnvelope(ProtoSocketEnvelope envelope) {
|
||||||
|
if (envelope.type == 'event' &&
|
||||||
|
envelope.channel == 'event' &&
|
||||||
|
envelope.action == 'branch.updated') {
|
||||||
|
try {
|
||||||
|
final event = BranchUpdatedEvent.fromMap(envelope.payload);
|
||||||
|
final key = '${event.repoId}:${event.branch}';
|
||||||
|
if (_filters.contains(key)) {
|
||||||
|
if (!_updates.isClosed) {
|
||||||
|
_updates.add(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
if (!_updates.isClosed) {
|
||||||
|
_updates.addError(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _newMessageId() =>
|
||||||
|
'msg-${DateTime.now().microsecondsSinceEpoch}-${_messageCounter++}';
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,424 @@
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:nomadcode_app/src/integrations/proto_socket/proto_socket_branch_event_service.dart';
|
||||||
|
import 'package:nomadcode_app/src/integrations/proto_socket/proto_socket_envelope.dart';
|
||||||
|
import 'package:nomadcode_app/src/integrations/proto_socket/proto_socket_lifecycle.dart';
|
||||||
|
|
||||||
|
class _FakeTransport implements ProtoSocketTransport {
|
||||||
|
final List<ProtoSocketEnvelope> sent = [];
|
||||||
|
final List<ProtoSocketEnvelope> _responses = [];
|
||||||
|
final StreamController<ProtoSocketEnvelope> _events =
|
||||||
|
StreamController<ProtoSocketEnvelope>.broadcast();
|
||||||
|
|
||||||
|
void enqueueResponse(ProtoSocketEnvelope response) {
|
||||||
|
_responses.add(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
void emitEvent(ProtoSocketEnvelope event) {
|
||||||
|
_events.add(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<ProtoSocketEnvelope> sendRequest(
|
||||||
|
ProtoSocketEnvelope envelope, {
|
||||||
|
Duration timeout = const Duration(seconds: 30),
|
||||||
|
}) async {
|
||||||
|
sent.add(envelope);
|
||||||
|
if (_responses.isEmpty) {
|
||||||
|
return ProtoSocketEnvelope(
|
||||||
|
id: 'resp-${envelope.id}',
|
||||||
|
correlationId: envelope.id,
|
||||||
|
type: 'response',
|
||||||
|
channel: envelope.channel,
|
||||||
|
action: envelope.action,
|
||||||
|
payload: const {'status': 'subscribed'},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return _responses.removeAt(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Stream<ProtoSocketEnvelope> get events => _events.stream;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> close() async {
|
||||||
|
await _events.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('ProtoSocketBranchEventService', () {
|
||||||
|
late _FakeTransport transport;
|
||||||
|
late ProtoSocketBranchEventService service;
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
transport = _FakeTransport();
|
||||||
|
service = ProtoSocketBranchEventService(transport);
|
||||||
|
});
|
||||||
|
|
||||||
|
tearDown(() async {
|
||||||
|
service.dispose();
|
||||||
|
await transport.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('subscribe sends event.subscribe request with correct shape', () async {
|
||||||
|
await service.subscribe(repoId: 'nomadcode', branch: 'develop');
|
||||||
|
|
||||||
|
expect(transport.sent, hasLength(1));
|
||||||
|
final request = transport.sent.first;
|
||||||
|
expect(request.type, equals('request'));
|
||||||
|
expect(request.channel, equals('event'));
|
||||||
|
expect(request.action, equals('event.subscribe'));
|
||||||
|
expect(request.payload['events'], equals(['branch.updated']));
|
||||||
|
expect(request.payload['repo_id'], equals('nomadcode'));
|
||||||
|
expect(request.payload['branch'], equals('develop'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('emits branch.updated as wakeup signal when event is received', () async {
|
||||||
|
// Must subscribe first to pass the repo/branch filter
|
||||||
|
await service.subscribe(repoId: 'nomadcode', branch: 'develop');
|
||||||
|
|
||||||
|
final eventsFuture = service.branchUpdates.first;
|
||||||
|
|
||||||
|
transport.emitEvent(const ProtoSocketEnvelope(
|
||||||
|
id: 'event-1',
|
||||||
|
type: 'event',
|
||||||
|
channel: 'event',
|
||||||
|
action: 'branch.updated',
|
||||||
|
payload: {
|
||||||
|
'id': 'event-1',
|
||||||
|
'type': 'branch.updated',
|
||||||
|
'provider': 'forgejo',
|
||||||
|
'delivery_id': 'delivery-1',
|
||||||
|
'repo_id': 'nomadcode',
|
||||||
|
'branch': 'develop',
|
||||||
|
'before': 'old-sha',
|
||||||
|
'after': 'new-sha',
|
||||||
|
'changed_files': [
|
||||||
|
{'path': 'README.md', 'change_type': 'modified'}
|
||||||
|
],
|
||||||
|
'observed_at': '2026-06-13T00:00:00Z',
|
||||||
|
'created_at': '2026-06-13T00:00:00Z'
|
||||||
|
},
|
||||||
|
));
|
||||||
|
|
||||||
|
final event = await eventsFuture;
|
||||||
|
expect(event.id, equals('event-1'));
|
||||||
|
expect(event.type, equals('branch.updated'));
|
||||||
|
expect(event.provider, equals('forgejo'));
|
||||||
|
expect(event.deliveryId, equals('delivery-1'));
|
||||||
|
expect(event.repoId, equals('nomadcode'));
|
||||||
|
expect(event.branch, equals('develop'));
|
||||||
|
expect(event.before, equals('old-sha'));
|
||||||
|
expect(event.after, equals('new-sha'));
|
||||||
|
expect(event.changedFiles, hasLength(1));
|
||||||
|
expect(event.changedFiles.first.path, equals('README.md'));
|
||||||
|
expect(event.changedFiles.first.changeType, equals('modified'));
|
||||||
|
expect(event.observedAt, equals('2026-06-13T00:00:00Z'));
|
||||||
|
expect(event.createdAt, equals('2026-06-13T00:00:00Z'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('ignores non-matching event channel or actions', () async {
|
||||||
|
await service.subscribe(repoId: 'nomadcode', branch: 'develop');
|
||||||
|
|
||||||
|
final events = <BranchUpdatedEvent>[];
|
||||||
|
final subscription = service.branchUpdates.listen(events.add);
|
||||||
|
|
||||||
|
// Non-matching action
|
||||||
|
transport.emitEvent(const ProtoSocketEnvelope(
|
||||||
|
id: 'event-2',
|
||||||
|
type: 'event',
|
||||||
|
channel: 'event',
|
||||||
|
action: 'branch.created',
|
||||||
|
payload: {},
|
||||||
|
));
|
||||||
|
|
||||||
|
// Non-matching channel
|
||||||
|
transport.emitEvent(const ProtoSocketEnvelope(
|
||||||
|
id: 'event-3',
|
||||||
|
type: 'event',
|
||||||
|
channel: 'task',
|
||||||
|
action: 'branch.updated',
|
||||||
|
payload: {},
|
||||||
|
));
|
||||||
|
|
||||||
|
// Non-matching type
|
||||||
|
transport.emitEvent(const ProtoSocketEnvelope(
|
||||||
|
id: 'event-4',
|
||||||
|
type: 'response',
|
||||||
|
channel: 'event',
|
||||||
|
action: 'branch.updated',
|
||||||
|
payload: {},
|
||||||
|
));
|
||||||
|
|
||||||
|
await Future.delayed(const Duration(milliseconds: 10));
|
||||||
|
|
||||||
|
expect(events, isEmpty);
|
||||||
|
await subscription.cancel();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('emits error on stream when event payload is malformed', () async {
|
||||||
|
await service.subscribe(repoId: 'nomadcode', branch: 'develop');
|
||||||
|
|
||||||
|
final events = <BranchUpdatedEvent>[];
|
||||||
|
final errors = [];
|
||||||
|
|
||||||
|
final subscription = service.branchUpdates.listen(
|
||||||
|
events.add,
|
||||||
|
onError: errors.add,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Missing 'repo_id' in payload
|
||||||
|
transport.emitEvent(const ProtoSocketEnvelope(
|
||||||
|
id: 'event-5',
|
||||||
|
type: 'event',
|
||||||
|
channel: 'event',
|
||||||
|
action: 'branch.updated',
|
||||||
|
payload: {
|
||||||
|
'id': 'event-5',
|
||||||
|
'type': 'branch.updated',
|
||||||
|
'provider': 'forgejo',
|
||||||
|
'delivery_id': 'delivery-1',
|
||||||
|
'branch': 'develop',
|
||||||
|
'before': 'old-sha',
|
||||||
|
'after': 'new-sha',
|
||||||
|
'changed_files': [],
|
||||||
|
'observed_at': '2026-06-13T00:00:00Z',
|
||||||
|
'created_at': '2026-06-13T00:00:00Z'
|
||||||
|
},
|
||||||
|
));
|
||||||
|
|
||||||
|
await Future.delayed(const Duration(milliseconds: 10));
|
||||||
|
|
||||||
|
expect(events, isEmpty);
|
||||||
|
expect(errors, hasLength(1));
|
||||||
|
expect(errors.first, isA<FormatException>());
|
||||||
|
|
||||||
|
await subscription.cancel();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('subscribe throws when Core returns an error envelope', () async {
|
||||||
|
transport.enqueueResponse(const ProtoSocketEnvelope(
|
||||||
|
id: 'resp-msg-1',
|
||||||
|
correlationId: 'msg-1',
|
||||||
|
type: 'error',
|
||||||
|
channel: 'event',
|
||||||
|
action: 'event.subscribe',
|
||||||
|
error: ProtoSocketEnvelopeError(
|
||||||
|
code: 'event.missing_connection',
|
||||||
|
message: 'active connection missing',
|
||||||
|
retryable: false,
|
||||||
|
),
|
||||||
|
));
|
||||||
|
|
||||||
|
await expectLater(
|
||||||
|
service.subscribe(repoId: 'nomadcode', branch: 'develop'),
|
||||||
|
throwsA(
|
||||||
|
isA<ProtoSocketBranchEventException>()
|
||||||
|
.having((e) => e.code, 'code', 'event.missing_connection')
|
||||||
|
.having((e) => e.message, 'message', 'active connection missing')
|
||||||
|
.having((e) => e.retryable, 'retryable', isFalse),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Verify that filter was not added, and subsequent event is ignored
|
||||||
|
final events = <BranchUpdatedEvent>[];
|
||||||
|
final subscription = service.branchUpdates.listen(events.add);
|
||||||
|
|
||||||
|
transport.emitEvent(const ProtoSocketEnvelope(
|
||||||
|
id: 'event-6',
|
||||||
|
type: 'event',
|
||||||
|
channel: 'event',
|
||||||
|
action: 'branch.updated',
|
||||||
|
payload: {
|
||||||
|
'id': 'event-6',
|
||||||
|
'type': 'branch.updated',
|
||||||
|
'provider': 'forgejo',
|
||||||
|
'delivery_id': 'delivery-1',
|
||||||
|
'repo_id': 'nomadcode',
|
||||||
|
'branch': 'develop',
|
||||||
|
'before': 'old-sha',
|
||||||
|
'after': 'new-sha',
|
||||||
|
'changed_files': [],
|
||||||
|
'observed_at': '2026-06-13T00:00:00Z',
|
||||||
|
'created_at': '2026-06-13T00:00:00Z'
|
||||||
|
},
|
||||||
|
));
|
||||||
|
|
||||||
|
await Future.delayed(const Duration(milliseconds: 10));
|
||||||
|
expect(events, isEmpty);
|
||||||
|
await subscription.cancel();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('emits only subscribed repo and branch events', () async {
|
||||||
|
// 1. Before subscribe: no events should be emitted (filter set is empty)
|
||||||
|
final eventsBefore = <BranchUpdatedEvent>[];
|
||||||
|
final subBefore = service.branchUpdates.listen(eventsBefore.add);
|
||||||
|
|
||||||
|
transport.emitEvent(const ProtoSocketEnvelope(
|
||||||
|
id: 'event-before',
|
||||||
|
type: 'event',
|
||||||
|
channel: 'event',
|
||||||
|
action: 'branch.updated',
|
||||||
|
payload: {
|
||||||
|
'id': 'event-before',
|
||||||
|
'type': 'branch.updated',
|
||||||
|
'provider': 'forgejo',
|
||||||
|
'delivery_id': 'delivery-1',
|
||||||
|
'repo_id': 'nomadcode',
|
||||||
|
'branch': 'develop',
|
||||||
|
'before': 'old-sha',
|
||||||
|
'after': 'new-sha',
|
||||||
|
'changed_files': [],
|
||||||
|
'observed_at': '2026-06-13T00:00:00Z',
|
||||||
|
'created_at': '2026-06-13T00:00:00Z'
|
||||||
|
},
|
||||||
|
));
|
||||||
|
|
||||||
|
await Future.delayed(const Duration(milliseconds: 10));
|
||||||
|
expect(eventsBefore, isEmpty);
|
||||||
|
await subBefore.cancel();
|
||||||
|
|
||||||
|
// 2. Subscribe to nomadcode/develop
|
||||||
|
await service.subscribe(repoId: 'nomadcode', branch: 'develop');
|
||||||
|
|
||||||
|
final eventsAfter = <BranchUpdatedEvent>[];
|
||||||
|
final subAfter = service.branchUpdates.listen(eventsAfter.add);
|
||||||
|
|
||||||
|
// Event for other repo
|
||||||
|
transport.emitEvent(const ProtoSocketEnvelope(
|
||||||
|
id: 'event-other-repo',
|
||||||
|
type: 'event',
|
||||||
|
channel: 'event',
|
||||||
|
action: 'branch.updated',
|
||||||
|
payload: {
|
||||||
|
'id': 'event-other-repo',
|
||||||
|
'type': 'branch.updated',
|
||||||
|
'provider': 'forgejo',
|
||||||
|
'delivery_id': 'delivery-1',
|
||||||
|
'repo_id': 'other-repo',
|
||||||
|
'branch': 'develop',
|
||||||
|
'before': 'old-sha',
|
||||||
|
'after': 'new-sha',
|
||||||
|
'changed_files': [],
|
||||||
|
'observed_at': '2026-06-13T00:00:00Z',
|
||||||
|
'created_at': '2026-06-13T00:00:00Z'
|
||||||
|
},
|
||||||
|
));
|
||||||
|
|
||||||
|
// Event for other branch
|
||||||
|
transport.emitEvent(const ProtoSocketEnvelope(
|
||||||
|
id: 'event-other-branch',
|
||||||
|
type: 'event',
|
||||||
|
channel: 'event',
|
||||||
|
action: 'branch.updated',
|
||||||
|
payload: {
|
||||||
|
'id': 'event-other-branch',
|
||||||
|
'type': 'branch.updated',
|
||||||
|
'provider': 'forgejo',
|
||||||
|
'delivery_id': 'delivery-1',
|
||||||
|
'repo_id': 'nomadcode',
|
||||||
|
'branch': 'main',
|
||||||
|
'before': 'old-sha',
|
||||||
|
'after': 'new-sha',
|
||||||
|
'changed_files': [],
|
||||||
|
'observed_at': '2026-06-13T00:00:00Z',
|
||||||
|
'created_at': '2026-06-13T00:00:00Z'
|
||||||
|
},
|
||||||
|
));
|
||||||
|
|
||||||
|
// Matching event
|
||||||
|
transport.emitEvent(const ProtoSocketEnvelope(
|
||||||
|
id: 'event-matching',
|
||||||
|
type: 'event',
|
||||||
|
channel: 'event',
|
||||||
|
action: 'branch.updated',
|
||||||
|
payload: {
|
||||||
|
'id': 'event-matching',
|
||||||
|
'type': 'branch.updated',
|
||||||
|
'provider': 'forgejo',
|
||||||
|
'delivery_id': 'delivery-1',
|
||||||
|
'repo_id': 'nomadcode',
|
||||||
|
'branch': 'develop',
|
||||||
|
'before': 'old-sha',
|
||||||
|
'after': 'new-sha',
|
||||||
|
'changed_files': [],
|
||||||
|
'observed_at': '2026-06-13T00:00:00Z',
|
||||||
|
'created_at': '2026-06-13T00:00:00Z'
|
||||||
|
},
|
||||||
|
));
|
||||||
|
|
||||||
|
await Future.delayed(const Duration(milliseconds: 10));
|
||||||
|
|
||||||
|
expect(eventsAfter, hasLength(1));
|
||||||
|
expect(eventsAfter.first.id, equals('event-matching'));
|
||||||
|
await subAfter.cancel();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('emits error when changed_files contains a malformed item', () async {
|
||||||
|
await service.subscribe(repoId: 'nomadcode', branch: 'develop');
|
||||||
|
|
||||||
|
final events = <BranchUpdatedEvent>[];
|
||||||
|
final errors = [];
|
||||||
|
|
||||||
|
final subscription = service.branchUpdates.listen(
|
||||||
|
events.add,
|
||||||
|
onError: errors.add,
|
||||||
|
);
|
||||||
|
|
||||||
|
// changed_files contains a string (not a map)
|
||||||
|
transport.emitEvent(const ProtoSocketEnvelope(
|
||||||
|
id: 'event-malformed-files-1',
|
||||||
|
type: 'event',
|
||||||
|
channel: 'event',
|
||||||
|
action: 'branch.updated',
|
||||||
|
payload: {
|
||||||
|
'id': 'event-malformed-files-1',
|
||||||
|
'type': 'branch.updated',
|
||||||
|
'provider': 'forgejo',
|
||||||
|
'delivery_id': 'delivery-1',
|
||||||
|
'repo_id': 'nomadcode',
|
||||||
|
'branch': 'develop',
|
||||||
|
'before': 'old-sha',
|
||||||
|
'after': 'new-sha',
|
||||||
|
'changed_files': ['not-a-map'],
|
||||||
|
'observed_at': '2026-06-13T00:00:00Z',
|
||||||
|
'created_at': '2026-06-13T00:00:00Z'
|
||||||
|
},
|
||||||
|
));
|
||||||
|
|
||||||
|
// changed_files contains map but missing field 'change_type'
|
||||||
|
transport.emitEvent(const ProtoSocketEnvelope(
|
||||||
|
id: 'event-malformed-files-2',
|
||||||
|
type: 'event',
|
||||||
|
channel: 'event',
|
||||||
|
action: 'branch.updated',
|
||||||
|
payload: {
|
||||||
|
'id': 'event-malformed-files-2',
|
||||||
|
'type': 'branch.updated',
|
||||||
|
'provider': 'forgejo',
|
||||||
|
'delivery_id': 'delivery-1',
|
||||||
|
'repo_id': 'nomadcode',
|
||||||
|
'branch': 'develop',
|
||||||
|
'before': 'old-sha',
|
||||||
|
'after': 'new-sha',
|
||||||
|
'changed_files': [
|
||||||
|
{'path': 'README.md'} // change_type is missing
|
||||||
|
],
|
||||||
|
'observed_at': '2026-06-13T00:00:00Z',
|
||||||
|
'created_at': '2026-06-13T00:00:00Z'
|
||||||
|
},
|
||||||
|
));
|
||||||
|
|
||||||
|
await Future.delayed(const Duration(milliseconds: 10));
|
||||||
|
|
||||||
|
expect(events, isEmpty);
|
||||||
|
expect(errors, hasLength(2));
|
||||||
|
expect(errors[0], isA<FormatException>());
|
||||||
|
expect(errors[1], isA<FormatException>());
|
||||||
|
|
||||||
|
await subscription.cancel();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -161,8 +161,15 @@
|
||||||
- Core 구현은 기존 REST smoke/compat 표면을 제거하지 않고, 내부 UI/런타임 호출만 proto-socket 우선 경로로 전환한다.
|
- Core 구현은 기존 REST smoke/compat 표면을 제거하지 않고, 내부 UI/런타임 호출만 proto-socket 우선 경로로 전환한다.
|
||||||
- 검증은 최소한 channel/action dispatch, auth boundary, error envelope, event broadcast 또는 reconnect 영향 중 해당되는 항목을 테스트로 고정한다.
|
- 검증은 최소한 channel/action dispatch, auth boundary, error envelope, event broadcast 또는 reconnect 영향 중 해당되는 항목을 테스트로 고정한다.
|
||||||
|
|
||||||
|
### 1.8 외부 Gito Branch Event Consumer
|
||||||
|
- **설명**: NomadCode 클라이언트는 Gito의 branch event를 proto-socket을 통해 수신하는 소비자입니다.
|
||||||
|
- **원문 계약**: Gito `agent-contract/provided/gito-forgejo-branch-events-v1.md` (계약 ID: `gito.forgejo-branch-events.v1`)
|
||||||
|
- **소비 액션**: `event.subscribe` (구독 요청) 및 `branch.updated` (브로드캐스트 이벤트)
|
||||||
|
- **책임 경계**: NomadCode는 `branch.updated` 이벤트를 단순 wakeup signal로만 해석하고 자체적인 Plane/roadmap mutation은 실행하지 않으며, 이벤트 수신 후 target branch revision에 대한 재검증 및 후속 동기화 여부를 직접 확인해야 합니다. 계약 본문 전체는 Gito 원문 계약을 따릅니다.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
||||||
## 2. 공통 및 인프라 API
|
## 2. 공통 및 인프라 API
|
||||||
|
|
||||||
### 2.1 `GET /healthz`
|
### 2.1 `GET /healthz`
|
||||||
|
|
|
||||||
|
|
@ -546,7 +546,7 @@ func TestNewProjectBinderReservesSlotBySettingID(t *testing.T) {
|
||||||
ProjectSyncSettingID: 7,
|
ProjectSyncSettingID: 7,
|
||||||
SlotIndex: 1,
|
SlotIndex: 1,
|
||||||
State: string(projectsync.SlotStateInUse),
|
State: string(projectsync.SlotStateInUse),
|
||||||
Path: "/home/user/workspace/nomadcode/001",
|
Path: "/home/user/workspace/nomadcode/slots/001",
|
||||||
}}
|
}}
|
||||||
binder := NewProjectBinder(store)
|
binder := NewProjectBinder(store)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,46 @@
|
||||||
package projectsync
|
package projectsync
|
||||||
|
|
||||||
|
// ProvisionPlan describes the complete workspace topology that must be prepared
|
||||||
|
// for a project before any slot checkout or sync operation can run. Building it
|
||||||
|
// never executes a git process; it only resolves paths from a normalized Config.
|
||||||
|
type ProvisionPlan struct {
|
||||||
|
ProjectWorkspaceRoot string
|
||||||
|
MainBranchPath string
|
||||||
|
DevelopBranchPath string
|
||||||
|
DefaultSlotPath string
|
||||||
|
}
|
||||||
|
|
||||||
|
// BuildProvisionPlan derives the workspace topology plan from a project sync
|
||||||
|
// Config. It returns ErrInvalidConfig when the Config cannot be normalized.
|
||||||
|
func BuildProvisionPlan(config Config) (ProvisionPlan, error) {
|
||||||
|
normalized, err := config.Normalize()
|
||||||
|
if err != nil {
|
||||||
|
return ProvisionPlan{}, err
|
||||||
|
}
|
||||||
|
root, err := ProjectWorkspaceRoot(normalized.WorkspaceBasePath, normalized.RepoDirName)
|
||||||
|
if err != nil {
|
||||||
|
return ProvisionPlan{}, err
|
||||||
|
}
|
||||||
|
mainPath, err := BranchWorkspacePath(normalized.WorkspaceBasePath, normalized.RepoDirName, BranchNameMain)
|
||||||
|
if err != nil {
|
||||||
|
return ProvisionPlan{}, err
|
||||||
|
}
|
||||||
|
developPath, err := BranchWorkspacePath(normalized.WorkspaceBasePath, normalized.RepoDirName, BranchNameDevelop)
|
||||||
|
if err != nil {
|
||||||
|
return ProvisionPlan{}, err
|
||||||
|
}
|
||||||
|
defaultSlot, err := SlotWorkspacePath(normalized.WorkspaceBasePath, normalized.RepoDirName, DefaultSlotIndex)
|
||||||
|
if err != nil {
|
||||||
|
return ProvisionPlan{}, err
|
||||||
|
}
|
||||||
|
return ProvisionPlan{
|
||||||
|
ProjectWorkspaceRoot: root,
|
||||||
|
MainBranchPath: mainPath,
|
||||||
|
DevelopBranchPath: developPath,
|
||||||
|
DefaultSlotPath: defaultSlot,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
// CheckoutPlan is the policy object describing where a single workspace slot
|
// CheckoutPlan is the policy object describing where a single workspace slot
|
||||||
// should be checked out for a project sync target. It is derived purely from a
|
// should be checked out for a project sync target. It is derived purely from a
|
||||||
// normalized Config and a reserved WorkspaceSlot; building it never runs a git
|
// normalized Config and a reserved WorkspaceSlot; building it never runs a git
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,40 @@ func validCheckoutConfig() Config {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBuildProvisionPlanIncludesBranchAndDefaultSlotPaths(t *testing.T) {
|
||||||
|
plan, err := BuildProvisionPlan(validCheckoutConfig())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("BuildProvisionPlan returned error: %v", err)
|
||||||
|
}
|
||||||
|
if plan.ProjectWorkspaceRoot != "/home/user/workspace/nomadcode" {
|
||||||
|
t.Errorf("project root: got %q", plan.ProjectWorkspaceRoot)
|
||||||
|
}
|
||||||
|
if plan.MainBranchPath != "/home/user/workspace/nomadcode/branches/main" {
|
||||||
|
t.Errorf("main branch path: got %q", plan.MainBranchPath)
|
||||||
|
}
|
||||||
|
if plan.DevelopBranchPath != "/home/user/workspace/nomadcode/branches/develop" {
|
||||||
|
t.Errorf("develop branch path: got %q", plan.DevelopBranchPath)
|
||||||
|
}
|
||||||
|
if plan.DefaultSlotPath != "/home/user/workspace/nomadcode/slots/000" {
|
||||||
|
t.Errorf("default slot path: got %q", plan.DefaultSlotPath)
|
||||||
|
}
|
||||||
|
// Branch paths and slot path must be distinct.
|
||||||
|
if plan.MainBranchPath == plan.DefaultSlotPath || plan.DevelopBranchPath == plan.DefaultSlotPath {
|
||||||
|
t.Error("branch paths and slot path must not collide")
|
||||||
|
}
|
||||||
|
if plan.MainBranchPath == plan.DevelopBranchPath {
|
||||||
|
t.Error("main and develop branch paths must be distinct")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildProvisionPlanRejectsInvalidConfig(t *testing.T) {
|
||||||
|
bad := validCheckoutConfig()
|
||||||
|
bad.GitRemoteURL = ""
|
||||||
|
if _, err := BuildProvisionPlan(bad); !errors.Is(err, ErrInvalidConfig) {
|
||||||
|
t.Fatalf("expected ErrInvalidConfig, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestBuildCheckoutPlanDefaultSlotUsesZeroPaddedPath(t *testing.T) {
|
func TestBuildCheckoutPlanDefaultSlotUsesZeroPaddedPath(t *testing.T) {
|
||||||
plan, err := BuildCheckoutPlan(validCheckoutConfig(), WorkspaceSlot{Index: DefaultSlotIndex})
|
plan, err := BuildCheckoutPlan(validCheckoutConfig(), WorkspaceSlot{Index: DefaultSlotIndex})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -34,7 +68,7 @@ func TestBuildCheckoutPlanDefaultSlotUsesZeroPaddedPath(t *testing.T) {
|
||||||
if plan.ProjectWorkspaceRoot != "/home/user/workspace/nomadcode" {
|
if plan.ProjectWorkspaceRoot != "/home/user/workspace/nomadcode" {
|
||||||
t.Errorf("project root: got %q", plan.ProjectWorkspaceRoot)
|
t.Errorf("project root: got %q", plan.ProjectWorkspaceRoot)
|
||||||
}
|
}
|
||||||
if plan.SlotPath != "/home/user/workspace/nomadcode/000" {
|
if plan.SlotPath != "/home/user/workspace/nomadcode/slots/000" {
|
||||||
t.Errorf("slot path: got %q", plan.SlotPath)
|
t.Errorf("slot path: got %q", plan.SlotPath)
|
||||||
}
|
}
|
||||||
if plan.SlotIndex != DefaultSlotIndex {
|
if plan.SlotIndex != DefaultSlotIndex {
|
||||||
|
|
@ -54,7 +88,7 @@ func TestBuildCheckoutPlanParallelSlotKeepsRemoteAndIncrementsPath(t *testing.T)
|
||||||
if plan.SourceBranch != "develop" {
|
if plan.SourceBranch != "develop" {
|
||||||
t.Errorf("source branch: got %q", plan.SourceBranch)
|
t.Errorf("source branch: got %q", plan.SourceBranch)
|
||||||
}
|
}
|
||||||
if plan.SlotPath != "/home/user/workspace/nomadcode/001" {
|
if plan.SlotPath != "/home/user/workspace/nomadcode/slots/001" {
|
||||||
t.Errorf("slot path: got %q", plan.SlotPath)
|
t.Errorf("slot path: got %q", plan.SlotPath)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,62 @@ const (
|
||||||
MaxSlotIndex SlotIndex = 999
|
MaxSlotIndex SlotIndex = 999
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type BranchName string
|
||||||
|
|
||||||
|
const (
|
||||||
|
BranchNameMain BranchName = "main"
|
||||||
|
BranchNameDevelop BranchName = "develop"
|
||||||
|
)
|
||||||
|
|
||||||
|
// reservedBranchNames is the closed set of branch workspace names. Only these
|
||||||
|
// branches get a fixed directory under branches/; agent slots use slots/NNN.
|
||||||
|
var reservedBranchNames = map[BranchName]struct{}{
|
||||||
|
BranchNameMain: {},
|
||||||
|
BranchNameDevelop: {},
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b BranchName) Valid() bool {
|
||||||
|
_, ok := reservedBranchNames[b]
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
type BranchSyncState string
|
||||||
|
|
||||||
|
const (
|
||||||
|
BranchSyncStateSynced BranchSyncState = "synced"
|
||||||
|
BranchSyncStateStale BranchSyncState = "stale"
|
||||||
|
BranchSyncStateError BranchSyncState = "error"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s BranchSyncState) Valid() bool {
|
||||||
|
switch s {
|
||||||
|
case BranchSyncStateSynced, BranchSyncStateStale, BranchSyncStateError:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RequiredBranchWorkspaces returns the fixed branch names every project
|
||||||
|
// workspace must provision under branches/.
|
||||||
|
func RequiredBranchWorkspaces() []BranchName {
|
||||||
|
return []BranchName{BranchNameMain, BranchNameDevelop}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BranchWorkspacePath returns the filesystem path for a named branch workspace
|
||||||
|
// under the project workspace root. Only reserved branch names are accepted;
|
||||||
|
// agent slots must use SlotWorkspacePath instead.
|
||||||
|
func BranchWorkspacePath(workspaceBasePath, repoDirName string, branch BranchName) (string, error) {
|
||||||
|
if !branch.Valid() {
|
||||||
|
return "", ErrInvalidConfig
|
||||||
|
}
|
||||||
|
root, err := ProjectWorkspaceRoot(workspaceBasePath, repoDirName)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return filepath.Join(root, "branches", string(branch)), nil
|
||||||
|
}
|
||||||
|
|
||||||
type ProviderProjectTarget struct {
|
type ProviderProjectTarget struct {
|
||||||
Provider workitem.ProviderID
|
Provider workitem.ProviderID
|
||||||
Tenant string
|
Tenant string
|
||||||
|
|
@ -158,7 +214,7 @@ func SlotWorkspacePath(workspaceBasePath, repoDirName string, slot SlotIndex) (s
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
return filepath.Join(root, slotName), nil
|
return filepath.Join(root, "slots", slotName), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func normalizePath(value string) (string, error) {
|
func normalizePath(value string) (string, error) {
|
||||||
|
|
|
||||||
|
|
@ -102,7 +102,7 @@ func TestWorkspacePathsUseRepoRootAndZeroPaddedSlot(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("SlotWorkspacePath returned error: %v", err)
|
t.Fatalf("SlotWorkspacePath returned error: %v", err)
|
||||||
}
|
}
|
||||||
if slotPath != "/home/user/workspace/nomadcode/000" {
|
if slotPath != "/home/user/workspace/nomadcode/slots/000" {
|
||||||
t.Fatalf("slot path: got %q", slotPath)
|
t.Fatalf("slot path: got %q", slotPath)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -158,7 +158,7 @@ func TestSlotFromDBRecordPreservesTypedFields(t *testing.T) {
|
||||||
ProjectSyncSettingID: 5,
|
ProjectSyncSettingID: 5,
|
||||||
SlotIndex: 2,
|
SlotIndex: 2,
|
||||||
State: string(SlotStateInUse),
|
State: string(SlotStateInUse),
|
||||||
Path: "/home/user/workspace/nomadcode/002",
|
Path: "/home/user/workspace/nomadcode/slots/002",
|
||||||
CreatedAt: now,
|
CreatedAt: now,
|
||||||
UpdatedAt: now,
|
UpdatedAt: now,
|
||||||
}
|
}
|
||||||
|
|
@ -173,17 +173,17 @@ func TestSlotFromDBRecordPreservesTypedFields(t *testing.T) {
|
||||||
if slot.State != SlotStateInUse {
|
if slot.State != SlotStateInUse {
|
||||||
t.Errorf("state: got %q", slot.State)
|
t.Errorf("state: got %q", slot.State)
|
||||||
}
|
}
|
||||||
if slot.Path != "/home/user/workspace/nomadcode/002" {
|
if slot.Path != "/home/user/workspace/nomadcode/slots/002" {
|
||||||
t.Errorf("path: got %q", slot.Path)
|
t.Errorf("path: got %q", slot.Path)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestToUpsertWorkspaceSlotParamsValidatesIndex(t *testing.T) {
|
func TestToUpsertWorkspaceSlotParamsValidatesIndex(t *testing.T) {
|
||||||
params, err := ToUpsertWorkspaceSlotParams(5, DefaultSlotIndex, "/ws/nomadcode/000")
|
params, err := ToUpsertWorkspaceSlotParams(5, DefaultSlotIndex, "/ws/nomadcode/slots/000")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unexpected error: %v", err)
|
t.Fatalf("unexpected error: %v", err)
|
||||||
}
|
}
|
||||||
if params.ProjectSyncSettingID != 5 || params.SlotIndex != 0 || params.Path != "/ws/nomadcode/000" {
|
if params.ProjectSyncSettingID != 5 || params.SlotIndex != 0 || params.Path != "/ws/nomadcode/slots/000" {
|
||||||
t.Errorf("unexpected params: %+v", params)
|
t.Errorf("unexpected params: %+v", params)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -303,6 +303,65 @@ func TestConfigFromDBRecord(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBranchWorkspacePathsUseReservedBranches(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
branch BranchName
|
||||||
|
wantPath string
|
||||||
|
}{
|
||||||
|
{BranchNameMain, "/home/user/workspace/nomadcode/branches/main"},
|
||||||
|
{BranchNameDevelop, "/home/user/workspace/nomadcode/branches/develop"},
|
||||||
|
}
|
||||||
|
for _, tc := range tests {
|
||||||
|
path, err := BranchWorkspacePath("/home/user/workspace", "nomadcode", tc.branch)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("BranchWorkspacePath(%q): unexpected error: %v", tc.branch, err)
|
||||||
|
}
|
||||||
|
if path != tc.wantPath {
|
||||||
|
t.Errorf("BranchWorkspacePath(%q): got %q, want %q", tc.branch, path, tc.wantPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Non-reserved branch names must be rejected.
|
||||||
|
for _, bad := range []BranchName{"feature/x", "release", "main-fork", ""} {
|
||||||
|
if _, err := BranchWorkspacePath("/home/user/workspace", "nomadcode", bad); !errors.Is(err, ErrInvalidConfig) {
|
||||||
|
t.Errorf("BranchWorkspacePath(%q): expected ErrInvalidConfig, got %v", bad, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Branch path must not overlap with slot path.
|
||||||
|
slotPath, _ := SlotWorkspacePath("/home/user/workspace", "nomadcode", DefaultSlotIndex)
|
||||||
|
branchPath, _ := BranchWorkspacePath("/home/user/workspace", "nomadcode", BranchNameDevelop)
|
||||||
|
if slotPath == branchPath {
|
||||||
|
t.Errorf("branch and slot paths must not collide: both %q", slotPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBranchSyncStateValidation(t *testing.T) {
|
||||||
|
valid := []BranchSyncState{BranchSyncStateSynced, BranchSyncStateStale, BranchSyncStateError}
|
||||||
|
for _, s := range valid {
|
||||||
|
if !s.Valid() {
|
||||||
|
t.Errorf("state %q should be valid", s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if BranchSyncState("unknown").Valid() {
|
||||||
|
t.Error("unknown branch sync state should be invalid")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRequiredBranchWorkspacesContainsMainAndDevelop(t *testing.T) {
|
||||||
|
required := RequiredBranchWorkspaces()
|
||||||
|
found := map[BranchName]bool{}
|
||||||
|
for _, b := range required {
|
||||||
|
found[b] = true
|
||||||
|
}
|
||||||
|
if !found[BranchNameMain] {
|
||||||
|
t.Error("RequiredBranchWorkspaces must include main")
|
||||||
|
}
|
||||||
|
if !found[BranchNameDevelop] {
|
||||||
|
t.Error("RequiredBranchWorkspaces must include develop")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TestConfigRoundTrip verifies Config → DB params → Config preserves fields.
|
// TestConfigRoundTrip verifies Config → DB params → Config preserves fields.
|
||||||
func TestConfigRoundTrip(t *testing.T) {
|
func TestConfigRoundTrip(t *testing.T) {
|
||||||
original := Config{
|
original := Config{
|
||||||
|
|
|
||||||
|
|
@ -49,13 +49,16 @@ type ProjectBinder interface {
|
||||||
// CheckoutMetadata is the stable JSON contract recorded on a created task under
|
// CheckoutMetadata is the stable JSON contract recorded on a created task under
|
||||||
// the "checkout" key. git remote and source branch always come from the project
|
// the "checkout" key. git remote and source branch always come from the project
|
||||||
// sync config; slot id/index/path come from the reserved slot and the checkout
|
// sync config; slot id/index/path come from the reserved slot and the checkout
|
||||||
// plan, so each task carries the independent workspace it was bound to.
|
// plan; branch paths come from the provision plan so downstream sync layers can
|
||||||
|
// distinguish the develop scanning base from the slot's independent checkout.
|
||||||
type CheckoutMetadata struct {
|
type CheckoutMetadata struct {
|
||||||
ProjectSyncSettingID int64 `json:"project_sync_setting_id"`
|
ProjectSyncSettingID int64 `json:"project_sync_setting_id"`
|
||||||
WorkspaceID string `json:"workspace_id"`
|
WorkspaceID string `json:"workspace_id"`
|
||||||
GitRemoteURL string `json:"git_remote_url"`
|
GitRemoteURL string `json:"git_remote_url"`
|
||||||
SourceBranch string `json:"source_branch"`
|
SourceBranch string `json:"source_branch"`
|
||||||
ProjectWorkspaceRoot string `json:"project_workspace_root"`
|
ProjectWorkspaceRoot string `json:"project_workspace_root"`
|
||||||
|
MainBranchPath string `json:"main_branch_path"`
|
||||||
|
DevelopBranchPath string `json:"develop_branch_path"`
|
||||||
SlotID int64 `json:"slot_id"`
|
SlotID int64 `json:"slot_id"`
|
||||||
SlotIndex int `json:"slot_index"`
|
SlotIndex int `json:"slot_index"`
|
||||||
SlotPath string `json:"slot_path"`
|
SlotPath string `json:"slot_path"`
|
||||||
|
|
@ -94,6 +97,10 @@ func (s *Service) CreateTaskFromWorkItem(ctx context.Context, input CreateTaskIn
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return storage.Task{}, err
|
return storage.Task{}, err
|
||||||
}
|
}
|
||||||
|
provision, err := projectsync.BuildProvisionPlan(config)
|
||||||
|
if err != nil {
|
||||||
|
return storage.Task{}, err
|
||||||
|
}
|
||||||
|
|
||||||
item, err := s.reader.FetchWorkItem(ctx, input.Ref)
|
item, err := s.reader.FetchWorkItem(ctx, input.Ref)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -107,14 +114,14 @@ func (s *Service) CreateTaskFromWorkItem(ctx context.Context, input CreateTaskIn
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return storage.Task{}, err
|
return storage.Task{}, err
|
||||||
}
|
}
|
||||||
createInput.Metadata, err = checkoutTaskMetadata(binding.SettingID, config, slot, plan)
|
createInput.Metadata, err = checkoutTaskMetadata(binding.SettingID, config, slot, plan, provision)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return storage.Task{}, err
|
return storage.Task{}, err
|
||||||
}
|
}
|
||||||
return s.tasks.CreateTask(ctx, createInput)
|
return s.tasks.CreateTask(ctx, createInput)
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkoutTaskMetadata(settingID int64, config projectsync.Config, slot projectsync.WorkspaceSlot, plan projectsync.CheckoutPlan) (json.RawMessage, error) {
|
func checkoutTaskMetadata(settingID int64, config projectsync.Config, slot projectsync.WorkspaceSlot, plan projectsync.CheckoutPlan, provision projectsync.ProvisionPlan) (json.RawMessage, error) {
|
||||||
return json.Marshal(map[string]any{
|
return json.Marshal(map[string]any{
|
||||||
"checkout": CheckoutMetadata{
|
"checkout": CheckoutMetadata{
|
||||||
ProjectSyncSettingID: settingID,
|
ProjectSyncSettingID: settingID,
|
||||||
|
|
@ -122,6 +129,8 @@ func checkoutTaskMetadata(settingID int64, config projectsync.Config, slot proje
|
||||||
GitRemoteURL: plan.RemoteURL,
|
GitRemoteURL: plan.RemoteURL,
|
||||||
SourceBranch: plan.SourceBranch,
|
SourceBranch: plan.SourceBranch,
|
||||||
ProjectWorkspaceRoot: plan.ProjectWorkspaceRoot,
|
ProjectWorkspaceRoot: plan.ProjectWorkspaceRoot,
|
||||||
|
MainBranchPath: provision.MainBranchPath,
|
||||||
|
DevelopBranchPath: provision.DevelopBranchPath,
|
||||||
SlotID: slot.ID,
|
SlotID: slot.ID,
|
||||||
SlotIndex: int(plan.SlotIndex),
|
SlotIndex: int(plan.SlotIndex),
|
||||||
SlotPath: plan.SlotPath,
|
SlotPath: plan.SlotPath,
|
||||||
|
|
|
||||||
|
|
@ -107,7 +107,7 @@ func reservedSlot() projectsync.WorkspaceSlot {
|
||||||
ProjectSyncSettingID: 5,
|
ProjectSyncSettingID: 5,
|
||||||
Index: projectsync.DefaultSlotIndex,
|
Index: projectsync.DefaultSlotIndex,
|
||||||
State: projectsync.SlotStateInUse,
|
State: projectsync.SlotStateInUse,
|
||||||
Path: "/home/user/workspace/nomadcode/000",
|
Path: "/home/user/workspace/nomadcode/slots/000",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -185,7 +185,8 @@ func TestCreateTaskFromWorkItemFetchesMapsAndRecordsCheckout(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// The checkout binding is recorded on the task metadata: remote/branch from
|
// The checkout binding is recorded on the task metadata: remote/branch from
|
||||||
// config, slot id/index/path from the reserved slot and checkout plan.
|
// config, slot id/index/path from the reserved slot and checkout plan,
|
||||||
|
// branch paths from the provision plan.
|
||||||
checkout := decodeCheckout(t, creator.calledWith.Metadata)
|
checkout := decodeCheckout(t, creator.calledWith.Metadata)
|
||||||
want := CheckoutMetadata{
|
want := CheckoutMetadata{
|
||||||
ProjectSyncSettingID: 5,
|
ProjectSyncSettingID: 5,
|
||||||
|
|
@ -193,9 +194,11 @@ func TestCreateTaskFromWorkItemFetchesMapsAndRecordsCheckout(t *testing.T) {
|
||||||
GitRemoteURL: "git@example.com:acme/nomadcode.git",
|
GitRemoteURL: "git@example.com:acme/nomadcode.git",
|
||||||
SourceBranch: "develop",
|
SourceBranch: "develop",
|
||||||
ProjectWorkspaceRoot: "/home/user/workspace/nomadcode",
|
ProjectWorkspaceRoot: "/home/user/workspace/nomadcode",
|
||||||
|
MainBranchPath: "/home/user/workspace/nomadcode/branches/main",
|
||||||
|
DevelopBranchPath: "/home/user/workspace/nomadcode/branches/develop",
|
||||||
SlotID: 11,
|
SlotID: 11,
|
||||||
SlotIndex: 0,
|
SlotIndex: 0,
|
||||||
SlotPath: "/home/user/workspace/nomadcode/000",
|
SlotPath: "/home/user/workspace/nomadcode/slots/000",
|
||||||
}
|
}
|
||||||
if checkout != want {
|
if checkout != want {
|
||||||
t.Errorf("checkout metadata:\n got %+v\nwant %+v", checkout, want)
|
t.Errorf("checkout metadata:\n got %+v\nwant %+v", checkout, want)
|
||||||
|
|
@ -367,9 +370,19 @@ func TestCreateTaskFromWorkItemBindsConfigAndSlotPerProject(t *testing.T) {
|
||||||
if checkout.ProjectSyncSettingID != 5 || checkout.GitRemoteURL != "git@example.com:acme/nomadcode.git" {
|
if checkout.ProjectSyncSettingID != 5 || checkout.GitRemoteURL != "git@example.com:acme/nomadcode.git" {
|
||||||
t.Errorf("same-project ticket %q checkout: %+v", id, checkout)
|
t.Errorf("same-project ticket %q checkout: %+v", id, checkout)
|
||||||
}
|
}
|
||||||
if checkout.SlotID != 11 || checkout.SlotIndex != 0 || checkout.SlotPath != "/home/user/workspace/nomadcode/000" {
|
if checkout.SlotID != 11 || checkout.SlotIndex != 0 || checkout.SlotPath != "/home/user/workspace/nomadcode/slots/000" {
|
||||||
t.Errorf("same-project ticket %q slot: %+v", id, checkout)
|
t.Errorf("same-project ticket %q slot: %+v", id, checkout)
|
||||||
}
|
}
|
||||||
|
// Branch paths must be present and must not overlap with the slot path.
|
||||||
|
if checkout.MainBranchPath != "/home/user/workspace/nomadcode/branches/main" {
|
||||||
|
t.Errorf("same-project ticket %q main branch path: got %q", id, checkout.MainBranchPath)
|
||||||
|
}
|
||||||
|
if checkout.DevelopBranchPath != "/home/user/workspace/nomadcode/branches/develop" {
|
||||||
|
t.Errorf("same-project ticket %q develop branch path: got %q", id, checkout.DevelopBranchPath)
|
||||||
|
}
|
||||||
|
if checkout.SlotPath == checkout.MainBranchPath || checkout.SlotPath == checkout.DevelopBranchPath {
|
||||||
|
t.Errorf("same-project ticket %q: slot path collides with branch path: %+v", id, checkout)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// A different project resolves to its own config and an independent slot.
|
// A different project resolves to its own config and an independent slot.
|
||||||
|
|
@ -385,9 +398,13 @@ func TestCreateTaskFromWorkItemBindsConfigAndSlotPerProject(t *testing.T) {
|
||||||
if checkoutB.ProjectSyncSettingID != 7 || checkoutB.GitRemoteURL != "git@example.com:acme/other.git" {
|
if checkoutB.ProjectSyncSettingID != 7 || checkoutB.GitRemoteURL != "git@example.com:acme/other.git" {
|
||||||
t.Errorf("different-project checkout not isolated: %+v", checkoutB)
|
t.Errorf("different-project checkout not isolated: %+v", checkoutB)
|
||||||
}
|
}
|
||||||
if checkoutB.SlotID != 22 || checkoutB.SlotIndex != 1 || checkoutB.SlotPath != "/home/user/workspace/nomadcode/001" {
|
if checkoutB.SlotID != 22 || checkoutB.SlotIndex != 1 || checkoutB.SlotPath != "/home/user/workspace/nomadcode/slots/001" {
|
||||||
t.Errorf("different-project slot not independent: %+v", checkoutB)
|
t.Errorf("different-project slot not independent: %+v", checkoutB)
|
||||||
}
|
}
|
||||||
|
// Branch paths must not overlap with the slot path for the different project.
|
||||||
|
if checkoutB.SlotPath == checkoutB.MainBranchPath || checkoutB.SlotPath == checkoutB.DevelopBranchPath {
|
||||||
|
t.Errorf("different-project: slot path collides with branch path: %+v", checkoutB)
|
||||||
|
}
|
||||||
|
|
||||||
// An unconfigured project is isolated and blocked.
|
// An unconfigured project is isolated and blocked.
|
||||||
creator.calledWith = nil
|
creator.calledWith = nil
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue