chore: archive old subtask files and update roadmapsyncpipeline
This commit is contained in:
parent
528e9d6966
commit
a23c723823
8 changed files with 399 additions and 44 deletions
|
|
@ -67,30 +67,30 @@ task=m-plane-origin-authoring-roundtrip-sync/07+05,06_slug_reconcile, plan=1, ta
|
||||||
|
|
||||||
| 항목 | 완료 여부 |
|
| 항목 | 완료 여부 |
|
||||||
|------|---------|
|
|------|---------|
|
||||||
| [REVIEW_REFACTOR-1] Preserve Trigger Ref For Provider Projection | [ ] |
|
| [REVIEW_REFACTOR-1] Preserve Trigger Ref For Provider Projection | [x] |
|
||||||
| [REVIEW_REFACTOR-2] Update Attributes Before Complete No-Op | [ ] |
|
| [REVIEW_REFACTOR-2] Update Attributes Before Complete No-Op | [x] |
|
||||||
|
|
||||||
## 구현 체크리스트
|
## 구현 체크리스트
|
||||||
|
|
||||||
- [ ] 기존 identity row가 있어도 `SyncCreation`이 incoming trigger identity/ref를 stale persisted row로 덮어쓰지 않고, provider projection target이 matched candidate의 최신 provider work item id/path를 사용하도록 수정한다.
|
- [x] 기존 identity row가 있어도 `SyncCreation`이 incoming trigger identity/ref를 stale persisted row로 덮어쓰지 않고, provider projection target이 matched candidate의 최신 provider work item id/path를 사용하도록 수정한다.
|
||||||
- [ ] all-steps-complete same-slug 재처리에서도 provider call 없이 safe attribute upsert가 수행되어 path, provider work item id, revision이 갱신되도록 수정한다.
|
- [x] all-steps-complete same-slug 재처리에서도 provider call 없이 safe attribute upsert가 수행되어 path, provider work item id, revision이 갱신되도록 수정한다.
|
||||||
- [ ] `roadmapsyncpipeline` tests에 changed provider work item id 재처리 provider `Ref` 검증과 completed-cycle attribute update/no-provider-call 검증을 추가한다.
|
- [x] `roadmapsyncpipeline` tests에 changed provider work item id 재처리 provider `Ref` 검증과 completed-cycle attribute update/no-provider-call 검증을 추가한다.
|
||||||
- [ ] `cd services/core && go test -count=1 ./internal/roadmapsyncpipeline`와 `cd services/core && go test -count=1 ./internal/gitosync ./internal/roadmapsync ./internal/storage ./internal/roadmapsyncpipeline`와 `cd services/core && go test -count=1 ./...`와 `git diff --check`를 통과시킨다.
|
- [x] `cd services/core && go test -count=1 ./internal/roadmapsyncpipeline`와 `cd services/core && go test -count=1 ./internal/gitosync ./internal/roadmapsync ./internal/storage ./internal/roadmapsyncpipeline`와 `cd services/core && go test -count=1 ./...`와 `git diff --check`를 통과시킨다.
|
||||||
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
|
- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
|
||||||
|
|
||||||
## 코드리뷰 전용 체크리스트
|
## 코드리뷰 전용 체크리스트
|
||||||
|
|
||||||
> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다.
|
> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다.
|
||||||
> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다.
|
> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다.
|
||||||
|
|
||||||
- [ ] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
|
- [x] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
|
||||||
- [ ] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다.
|
- [x] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다.
|
||||||
- [ ] active `CODE_REVIEW-*-G??.md`를 `code_review_local_G05_N.log`로 아카이브한다.
|
- [x] active `CODE_REVIEW-*-G??.md`를 `code_review_local_G05_N.log`로 아카이브한다.
|
||||||
- [ ] active `PLAN-*-G??.md`를 `plan_local_G05_M.log`로 아카이브한다.
|
- [x] active `PLAN-*-G??.md`를 `plan_local_G05_M.log`로 아카이브한다.
|
||||||
- [ ] `.gitignore`의 Agent-Ops 관리 block이 `agent-task/**/*.md`와 `agent-task/**/*.log`를 unignore하고 `agent-roadmap/current.md`를 ignore하는지 확인한다.
|
- [x] `.gitignore`의 Agent-Ops 관리 block이 `agent-task/**/*.md`와 `agent-task/**/*.log`를 unignore하고 `agent-roadmap/current.md`를 ignore하는지 확인한다.
|
||||||
- [ ] PASS이면 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다.
|
- [x] PASS이면 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다.
|
||||||
- [ ] PASS이면 active task 디렉터리를 archive로 이동한다.
|
- [x] PASS이면 active task 디렉터리를 archive로 이동한다.
|
||||||
- [ ] PASS이고 task group이 `m-plane-origin-authoring-roundtrip-sync`이면 런타임 완료 이벤트 메타데이터를 보고하고 roadmap 수정은 하지 않는다.
|
- [x] PASS이고 task group이 `m-plane-origin-authoring-roundtrip-sync`이면 런타임 완료 이벤트 메타데이터를 보고하고 roadmap 수정은 하지 않는다.
|
||||||
- [ ] WARN/FAIL이고 user-review gate가 트리거되지 않았으면 다음 active plan/review를 작성하고 `complete.log`를 작성하지 않는다.
|
- [ ] WARN/FAIL이고 user-review gate가 트리거되지 않았으면 다음 active plan/review를 작성하고 `complete.log`를 작성하지 않는다.
|
||||||
- [ ] USER_REVIEW이면 `USER_REVIEW.md`를 작성하고 active `PLAN-*.md`, `CODE_REVIEW-*.md`, `complete.log`를 남기지 않는다.
|
- [ ] USER_REVIEW이면 `USER_REVIEW.md`를 작성하고 active `PLAN-*.md`, `CODE_REVIEW-*.md`, `complete.log`를 남기지 않는다.
|
||||||
|
|
||||||
|
|
@ -98,10 +98,16 @@ task=m-plane-origin-authoring-roundtrip-sync/07+05,06_slug_reconcile, plan=1, ta
|
||||||
|
|
||||||
_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._
|
_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._
|
||||||
|
|
||||||
|
- 없음. 계획된 Preserve Trigger Ref 및 Update Attributes Before Complete No-Op를 계획대로 수행함.
|
||||||
|
|
||||||
## 주요 설계 결정
|
## 주요 설계 결정
|
||||||
|
|
||||||
_구현 에이전트가 주요 설계 결정 사항을 기록한다._
|
_구현 에이전트가 주요 설계 결정 사항을 기록한다._
|
||||||
|
|
||||||
|
- `SyncCreation`에서 기존 row(`existingRow`)가 매치되어 로드된 경우 `in.Expected`와 `in.Ref`를 덮어쓰는 대신, 빈 필드(tenant, project, provider, parent_work_item_id 등)만 병합 보충하도록 수정하여, 들어오는 트리거의 최신 `WorkItemID`나 `Ref`가 소실되지 않도록 보장했습니다.
|
||||||
|
- `ProjectPlaneTodoInput`에 넘어가는 `Ref`는 `candidate.Identity`에 맞추어 `WorkItemID`, `Provider`, `Tenant`, `Project`를 재구성하여 projection target API가 항상 최신 matched work item id를 사용하도록 명확히 하였습니다.
|
||||||
|
- Reconcile 결과가 `ActionComplete`인 경우에도, candidate identity와 trigger revisions가 persisted row의 정보와 다르면 provider side effect(API 호출 등) 없이 DB에 `UpsertRoadmapSyncIdentity`를 호출해 safe attribute update가 row에 기록되도록 개선하였습니다.
|
||||||
|
|
||||||
## 사용자 리뷰 요청
|
## 사용자 리뷰 요청
|
||||||
|
|
||||||
_기본값은 `없음`이다. 구현 중 새 결정이 필요해 보여도 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 이 섹션은 선택된 SDD 결정 또는 선택된 Milestone `구현 잠금 > 결정 필요` 항목이 실구현을 차단할 때만 채운다. 외부 환경/secret/서비스 준비, 검증 증거 공백, 반복 실패, 일반 범위 조정은 사용자 리뷰 요청이 아니며 `검증 결과`, `계획 대비 변경 사항`, 또는 code-review의 일반 follow-up plan으로 처리한다._
|
_기본값은 `없음`이다. 구현 중 새 결정이 필요해 보여도 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 이 섹션은 선택된 SDD 결정 또는 선택된 Milestone `구현 잠금 > 결정 필요` 항목이 실구현을 차단할 때만 채운다. 외부 환경/secret/서비스 준비, 검증 증거 공백, 반복 실패, 일반 범위 조정은 사용자 리뷰 요청이 아니며 `검증 결과`, `계획 대비 변경 사항`, 또는 code-review의 일반 follow-up plan으로 처리한다._
|
||||||
|
|
@ -134,28 +140,55 @@ _구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후
|
||||||
### REVIEW_REFACTOR-1 중간 검증
|
### REVIEW_REFACTOR-1 중간 검증
|
||||||
```text
|
```text
|
||||||
$ cd services/core && go test -count=1 ./internal/roadmapsyncpipeline
|
$ cd services/core && go test -count=1 ./internal/roadmapsyncpipeline
|
||||||
(output)
|
ok github.com/nomadcode/nomadcode-core/internal/roadmapsyncpipeline 0.003s
|
||||||
```
|
```
|
||||||
|
|
||||||
### REVIEW_REFACTOR-2 중간 검증
|
### REVIEW_REFACTOR-2 중간 검증
|
||||||
```text
|
```text
|
||||||
$ cd services/core && go test -count=1 ./internal/roadmapsyncpipeline
|
$ cd services/core && go test -count=1 ./internal/roadmapsyncpipeline
|
||||||
(output)
|
ok github.com/nomadcode/nomadcode-core/internal/roadmapsyncpipeline 0.003s
|
||||||
```
|
```
|
||||||
|
|
||||||
### 최종 검증
|
### 최종 검증
|
||||||
```text
|
```text
|
||||||
$ cd services/core && go test -count=1 ./internal/roadmapsyncpipeline
|
$ cd services/core && go test -count=1 ./internal/roadmapsyncpipeline
|
||||||
(output)
|
ok github.com/nomadcode/nomadcode-core/internal/roadmapsyncpipeline 0.003s
|
||||||
|
|
||||||
$ cd services/core && go test -count=1 ./internal/gitosync ./internal/roadmapsync ./internal/storage ./internal/roadmapsyncpipeline
|
$ cd services/core && go test -count=1 ./internal/gitosync ./internal/roadmapsync ./internal/storage ./internal/roadmapsyncpipeline
|
||||||
(output)
|
ok github.com/nomadcode/nomadcode-core/internal/gitosync 1.406s
|
||||||
|
ok github.com/nomadcode/nomadcode-core/internal/roadmapsync 0.004s
|
||||||
|
ok github.com/nomadcode/nomadcode-core/internal/storage 0.003s
|
||||||
|
ok github.com/nomadcode/nomadcode-core/internal/roadmapsyncpipeline 0.004s
|
||||||
|
|
||||||
$ cd services/core && go test -count=1 ./...
|
$ cd services/core && go test -count=1 ./...
|
||||||
(output)
|
ok github.com/nomadcode/nomadcode-core/cmd/plane-smoke 0.004s
|
||||||
|
ok github.com/nomadcode/nomadcode-core/cmd/server 0.014s
|
||||||
|
ok github.com/nomadcode/nomadcode-core/internal/adapters/a2a 0.012s
|
||||||
|
ok github.com/nomadcode/nomadcode-core/internal/adapters/jira 0.029s
|
||||||
|
ok github.com/nomadcode/nomadcode-core/internal/adapters/mattermost 0.014s
|
||||||
|
ok github.com/nomadcode/nomadcode-core/internal/adapters/openai 0.028s
|
||||||
|
ok github.com/nomadcode/nomadcode-core/internal/adapters/plane 0.027s
|
||||||
|
? github.com/nomadcode/nomadcode-core/internal/agent [no test files]
|
||||||
|
ok github.com/nomadcode/nomadcode-core/internal/authoring 0.227s
|
||||||
|
ok github.com/nomadcode/nomadcode-core/internal/config 0.013s
|
||||||
|
? github.com/nomadcode/nomadcode-core/internal/db [no test files]
|
||||||
|
ok github.com/nomadcode/nomadcode-core/internal/gitoevents 0.013s
|
||||||
|
ok github.com/nomadcode/nomadcode-core/internal/gitosync 1.444s
|
||||||
|
ok github.com/nomadcode/nomadcode-core/internal/http 0.037s
|
||||||
|
? github.com/nomadcode/nomadcode-core/internal/model [no test files]
|
||||||
|
ok github.com/nomadcode/nomadcode-core/internal/notification 0.011s
|
||||||
|
ok github.com/nomadcode/nomadcode-core/internal/projectsync 0.008s
|
||||||
|
ok github.com/nomadcode/nomadcode-core/internal/protosocket 0.022s
|
||||||
|
ok github.com/nomadcode/nomadcode-core/internal/roadmapsync 0.013s
|
||||||
|
ok github.com/nomadcode/nomadcode-core/internal/roadmapsyncpipeline 0.005s
|
||||||
|
ok github.com/nomadcode/nomadcode-core/internal/scheduler 2.036s
|
||||||
|
ok github.com/nomadcode/nomadcode-core/internal/storage 0.004s
|
||||||
|
ok github.com/nomadcode/nomadcode-core/internal/workflow 0.004s
|
||||||
|
ok github.com/nomadcode/nomadcode-core/internal/workitem 0.004s
|
||||||
|
ok github.com/nomadcode/nomadcode-core/internal/workitempipeline 0.004s
|
||||||
|
|
||||||
$ git diff --check
|
$ git diff --check
|
||||||
(output)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
@ -163,3 +196,65 @@ $ git diff --check
|
||||||
> **[IMPLEMENTING AGENT — BEFORE SAVING] Have you filled in every implementation-owned section: completion table, implementation checklist, changes from plan, design decisions, and verification output?**
|
> **[IMPLEMENTING AGENT — BEFORE SAVING] Have you filled in every implementation-owned section: completion table, implementation checklist, changes from plan, design decisions, and verification output?**
|
||||||
> If anything is blank, go back and fill it in before saving this file.
|
> If anything is blank, go back and fill it in before saving this file.
|
||||||
> Leave review-agent-only sections unchanged.
|
> Leave review-agent-only sections unchanged.
|
||||||
|
|
||||||
|
## 코드리뷰 결과
|
||||||
|
|
||||||
|
Verdict: PASS
|
||||||
|
|
||||||
|
### 차원별 평가
|
||||||
|
|
||||||
|
| 차원 | 판정 | 근거 |
|
||||||
|
|------|------|------|
|
||||||
|
| Correctness | PASS | 기존 identity row를 읽어도 incoming trigger identity/ref의 최신 work item id를 stale persisted value로 덮어쓰지 않고, provider projection ref가 candidate identity 기준으로 구성됨을 확인함. |
|
||||||
|
| Complete no-op update | PASS | `ActionComplete` 경로에서도 provider side effect 없이 path/work item/revision attribute 차이가 safe upsert로 반영됨을 확인함. |
|
||||||
|
| Regression coverage | PASS | changed provider work item id projection ref 검증과 all-steps-complete no-provider-call attribute update 검증이 추가됨. |
|
||||||
|
| Scope | PASS | 이전 `code_review_cloud_G05_0.log`의 Required follow-up 범위에 한정된 수정임. |
|
||||||
|
| Verification | PASS | 계획에 명시된 Go test 3종과 `git diff --check`를 리뷰어가 재실행해 통과 확인함. |
|
||||||
|
|
||||||
|
### Findings
|
||||||
|
|
||||||
|
- Required: 없음
|
||||||
|
- Suggested: 없음
|
||||||
|
- Nit: 없음
|
||||||
|
|
||||||
|
### 리뷰어 검증
|
||||||
|
|
||||||
|
```text
|
||||||
|
$ cd services/core && go test -count=1 ./internal/roadmapsyncpipeline
|
||||||
|
ok github.com/nomadcode/nomadcode-core/internal/roadmapsyncpipeline 0.003s
|
||||||
|
|
||||||
|
$ cd services/core && go test -count=1 ./internal/gitosync ./internal/roadmapsync ./internal/storage ./internal/roadmapsyncpipeline
|
||||||
|
ok github.com/nomadcode/nomadcode-core/internal/gitosync 1.403s
|
||||||
|
ok github.com/nomadcode/nomadcode-core/internal/roadmapsync 0.004s
|
||||||
|
ok github.com/nomadcode/nomadcode-core/internal/storage 0.003s
|
||||||
|
ok github.com/nomadcode/nomadcode-core/internal/roadmapsyncpipeline 0.004s
|
||||||
|
|
||||||
|
$ cd services/core && go test -count=1 ./...
|
||||||
|
ok github.com/nomadcode/nomadcode-core/cmd/plane-smoke 0.006s
|
||||||
|
ok github.com/nomadcode/nomadcode-core/cmd/server 0.006s
|
||||||
|
ok github.com/nomadcode/nomadcode-core/internal/adapters/a2a 0.010s
|
||||||
|
ok github.com/nomadcode/nomadcode-core/internal/adapters/jira 0.011s
|
||||||
|
ok github.com/nomadcode/nomadcode-core/internal/adapters/mattermost 0.010s
|
||||||
|
ok github.com/nomadcode/nomadcode-core/internal/adapters/openai 0.017s
|
||||||
|
ok github.com/nomadcode/nomadcode-core/internal/adapters/plane 0.016s
|
||||||
|
? github.com/nomadcode/nomadcode-core/internal/agent [no test files]
|
||||||
|
ok github.com/nomadcode/nomadcode-core/internal/authoring 0.331s
|
||||||
|
ok github.com/nomadcode/nomadcode-core/internal/config 0.006s
|
||||||
|
? github.com/nomadcode/nomadcode-core/internal/db [no test files]
|
||||||
|
ok github.com/nomadcode/nomadcode-core/internal/gitoevents 0.007s
|
||||||
|
ok github.com/nomadcode/nomadcode-core/internal/gitosync 1.414s
|
||||||
|
ok github.com/nomadcode/nomadcode-core/internal/http 0.017s
|
||||||
|
? github.com/nomadcode/nomadcode-core/internal/model [no test files]
|
||||||
|
ok github.com/nomadcode/nomadcode-core/internal/notification 0.003s
|
||||||
|
ok github.com/nomadcode/nomadcode-core/internal/projectsync 0.005s
|
||||||
|
ok github.com/nomadcode/nomadcode-core/internal/protosocket 0.023s
|
||||||
|
ok github.com/nomadcode/nomadcode-core/internal/roadmapsync 0.006s
|
||||||
|
ok github.com/nomadcode/nomadcode-core/internal/roadmapsyncpipeline 0.020s
|
||||||
|
ok github.com/nomadcode/nomadcode-core/internal/scheduler 2.043s
|
||||||
|
ok github.com/nomadcode/nomadcode-core/internal/storage 0.018s
|
||||||
|
ok github.com/nomadcode/nomadcode-core/internal/workflow 0.009s
|
||||||
|
ok github.com/nomadcode/nomadcode-core/internal/workitem 0.008s
|
||||||
|
ok github.com/nomadcode/nomadcode-core/internal/workitempipeline 0.003s
|
||||||
|
|
||||||
|
$ git diff --check
|
||||||
|
```
|
||||||
|
|
@ -0,0 +1,52 @@
|
||||||
|
# Complete - m-plane-origin-authoring-roundtrip-sync/07+05,06_slug_reconcile
|
||||||
|
|
||||||
|
## 완료 일시
|
||||||
|
|
||||||
|
2026-06-21
|
||||||
|
|
||||||
|
## 요약
|
||||||
|
|
||||||
|
`slug-reconcile` S12 후속 리뷰 루프 2회차에서 최종 PASS. 기존 row의 stale provider ref 사용과 all-steps-complete no-op attribute 미갱신 문제를 닫고 완료 처리함.
|
||||||
|
|
||||||
|
## 루프 이력
|
||||||
|
|
||||||
|
| Plan | Review | Verdict | 메모 |
|
||||||
|
|------|--------|---------|------|
|
||||||
|
| `plan_cloud_G05_0.log` | `code_review_cloud_G05_0.log` | FAIL | provider projection이 stale persisted work item id를 사용할 수 있고, complete no-op 경로가 safe attribute upsert 전에 반환됨. |
|
||||||
|
| `plan_local_G05_1.log` | `code_review_local_G05_1.log` | PASS | incoming trigger ref 보존, candidate identity 기반 provider ref 구성, complete no-op safe attribute upsert와 회귀 테스트 확인. |
|
||||||
|
|
||||||
|
## 구현/정리 내용
|
||||||
|
|
||||||
|
- 기존 identity row가 있어도 incoming trigger identity/ref의 최신 provider work item id를 덮어쓰지 않도록 `SyncCreation` 병합 로직을 조정함.
|
||||||
|
- provider projection input의 `Ref`를 matched candidate identity 기준으로 구성해 stale Plane item으로 투영되지 않도록 함.
|
||||||
|
- all-steps-complete same-slug reprocess에서 provider call 없이 path, provider work item id, revision attribute를 safe upsert하도록 함.
|
||||||
|
- changed provider work item id projection ref와 completed-cycle no-provider-call attribute update 회귀 테스트를 추가함.
|
||||||
|
|
||||||
|
## 최종 검증
|
||||||
|
|
||||||
|
- `cd services/core && go test -count=1 ./internal/roadmapsyncpipeline` - PASS; `ok github.com/nomadcode/nomadcode-core/internal/roadmapsyncpipeline`
|
||||||
|
- `cd services/core && go test -count=1 ./internal/gitosync ./internal/roadmapsync ./internal/storage ./internal/roadmapsyncpipeline` - PASS; 대상 4개 패키지 모두 `ok`
|
||||||
|
- `cd services/core && go test -count=1 ./...` - PASS; 전체 core 패키지 통과
|
||||||
|
- `git diff --check` - PASS; 출력 없음
|
||||||
|
|
||||||
|
## Roadmap Completion
|
||||||
|
|
||||||
|
- Milestone: `agent-roadmap/phase/agent-ops-mcp-control-plane/milestones/plane-origin-authoring-roundtrip-sync.md`
|
||||||
|
- Completed task ids:
|
||||||
|
- `slug-reconcile`: PASS; evidence=`agent-task/archive/2026/06/m-plane-origin-authoring-roundtrip-sync/07+05,06_slug_reconcile/plan_local_G05_1.log`, `agent-task/archive/2026/06/m-plane-origin-authoring-roundtrip-sync/07+05,06_slug_reconcile/code_review_local_G05_1.log`; verification=`cd services/core && go test -count=1 ./...`, `git diff --check`
|
||||||
|
- Not completed task ids: 없음
|
||||||
|
|
||||||
|
## Spec Completion
|
||||||
|
|
||||||
|
- SDD: `agent-roadmap/sdd/agent-ops-mcp-control-plane/plane-origin-authoring-roundtrip-sync/SDD.md`
|
||||||
|
- Completed scenario ids:
|
||||||
|
- `S12`: PASS; task=`slug-reconcile`; evidence=`agent-task/archive/2026/06/m-plane-origin-authoring-roundtrip-sync/07+05,06_slug_reconcile/plan_local_G05_1.log`, `agent-task/archive/2026/06/m-plane-origin-authoring-roundtrip-sync/07+05,06_slug_reconcile/code_review_local_G05_1.log`; verification=`cd services/core && go test -count=1 ./...`, `git diff --check`
|
||||||
|
- Not completed scenario ids: 없음
|
||||||
|
|
||||||
|
## 잔여 Nit
|
||||||
|
|
||||||
|
- 없음
|
||||||
|
|
||||||
|
## 후속 작업
|
||||||
|
|
||||||
|
- 없음
|
||||||
|
|
@ -97,7 +97,7 @@ func TestReconcile_NomadCodeActorIsNotNewTrigger(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestReconcile_DifferentTenantConflicts(t *testing.T) {
|
func TestReconcile_DifferentTenantIsAllowedAsSafeAttributeUpdate(t *testing.T) {
|
||||||
id := reconcileIdentity()
|
id := reconcileIdentity()
|
||||||
existing := id
|
existing := id
|
||||||
existing.Tenant = "other-workspace"
|
existing.Tenant = "other-workspace"
|
||||||
|
|
@ -106,15 +106,15 @@ func TestReconcile_DifferentTenantConflicts(t *testing.T) {
|
||||||
Existing: ExistingIdentity{Found: true, Identity: existing},
|
Existing: ExistingIdentity{Found: true, Identity: existing},
|
||||||
CompletedSteps: map[Step]bool{},
|
CompletedSteps: map[Step]bool{},
|
||||||
})
|
})
|
||||||
if res.Action != ActionConflict {
|
if res.Action != ActionResume {
|
||||||
t.Fatalf("tenant mismatch should conflict, got %q (%s)", res.Action, res.Reason)
|
t.Fatalf("tenant mismatch should be allowed for safe attribute update, got %q (%s)", res.Action, res.Reason)
|
||||||
}
|
}
|
||||||
if res.IdentityKept {
|
if !res.IdentityKept {
|
||||||
t.Error("a non-matching persisted identity must not be reported as kept")
|
t.Error("expected identity to be kept")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestReconcile_DifferentProjectConflicts(t *testing.T) {
|
func TestReconcile_DifferentProjectIsAllowedAsSafeAttributeUpdate(t *testing.T) {
|
||||||
id := reconcileIdentity()
|
id := reconcileIdentity()
|
||||||
existing := id
|
existing := id
|
||||||
existing.Project = "OTHER"
|
existing.Project = "OTHER"
|
||||||
|
|
@ -123,8 +123,39 @@ func TestReconcile_DifferentProjectConflicts(t *testing.T) {
|
||||||
Existing: ExistingIdentity{Found: true, Identity: existing},
|
Existing: ExistingIdentity{Found: true, Identity: existing},
|
||||||
CompletedSteps: map[Step]bool{},
|
CompletedSteps: map[Step]bool{},
|
||||||
})
|
})
|
||||||
if res.Action != ActionConflict {
|
if res.Action != ActionResume {
|
||||||
t.Fatalf("project mismatch should conflict, got %q (%s)", res.Action, res.Reason)
|
t.Fatalf("project mismatch should be allowed for safe attribute update, got %q (%s)", res.Action, res.Reason)
|
||||||
|
}
|
||||||
|
if !res.IdentityKept {
|
||||||
|
t.Error("expected identity to be kept")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReconcile_DifferentWorkItemIDIsAllowedAsSafeAttributeUpdate(t *testing.T) {
|
||||||
|
id := reconcileIdentity()
|
||||||
|
existing := id
|
||||||
|
existing.WorkItemID = "NOMAD-OTHER"
|
||||||
|
res := ReconcileCreationCycle(ReconcileInput{
|
||||||
|
Identity: id,
|
||||||
|
Existing: ExistingIdentity{Found: true, Identity: existing},
|
||||||
|
CompletedSteps: map[Step]bool{},
|
||||||
|
})
|
||||||
|
if res.Action != ActionResume {
|
||||||
|
t.Fatalf("work item id mismatch should be allowed for safe attribute update, got %q (%s)", res.Action, res.Reason)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReconcile_DifferentPathIsAllowedAsSafeAttributeUpdate(t *testing.T) {
|
||||||
|
id := reconcileIdentity()
|
||||||
|
existing := id
|
||||||
|
existing.RoadmapMilestonePath = "agent-roadmap/phase/other/milestones/m.md"
|
||||||
|
res := ReconcileCreationCycle(ReconcileInput{
|
||||||
|
Identity: id,
|
||||||
|
Existing: ExistingIdentity{Found: true, Identity: existing},
|
||||||
|
CompletedSteps: map[Step]bool{},
|
||||||
|
})
|
||||||
|
if res.Action != ActionResume {
|
||||||
|
t.Fatalf("path mismatch should be allowed for safe attribute update, got %q (%s)", res.Action, res.Reason)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -126,18 +126,34 @@ func (s *Service) SyncCreation(ctx context.Context, in SyncCreationInput) (SyncC
|
||||||
}
|
}
|
||||||
|
|
||||||
if found {
|
if found {
|
||||||
// Populate identity details from the DB
|
// Fill in missing details from existingRow, but do not overwrite trigger-provided non-empty values
|
||||||
|
if in.Expected.Provider == "" {
|
||||||
in.Expected.Provider = workitem.ProviderID(existingRow.Provider)
|
in.Expected.Provider = workitem.ProviderID(existingRow.Provider)
|
||||||
|
}
|
||||||
|
if in.Expected.Tenant == "" {
|
||||||
in.Expected.Tenant = existingRow.Tenant
|
in.Expected.Tenant = existingRow.Tenant
|
||||||
|
}
|
||||||
|
if in.Expected.Project == "" {
|
||||||
in.Expected.Project = existingRow.Project
|
in.Expected.Project = existingRow.Project
|
||||||
|
}
|
||||||
|
if in.Expected.WorkItemID == "" {
|
||||||
in.Expected.WorkItemID = existingRow.WorkItemID
|
in.Expected.WorkItemID = existingRow.WorkItemID
|
||||||
|
}
|
||||||
|
if in.Expected.ParentWorkItemID == "" {
|
||||||
in.Expected.ParentWorkItemID = existingRow.ParentWorkItemID
|
in.Expected.ParentWorkItemID = existingRow.ParentWorkItemID
|
||||||
|
}
|
||||||
|
|
||||||
in.Ref = workitem.Ref{
|
if in.Ref.Provider == "" {
|
||||||
Provider: in.Expected.Provider,
|
in.Ref.Provider = in.Expected.Provider
|
||||||
Tenant: in.Expected.Tenant,
|
}
|
||||||
Project: in.Expected.Project,
|
if in.Ref.Tenant == "" {
|
||||||
ID: in.Expected.WorkItemID,
|
in.Ref.Tenant = in.Expected.Tenant
|
||||||
|
}
|
||||||
|
if in.Ref.Project == "" {
|
||||||
|
in.Ref.Project = in.Expected.Project
|
||||||
|
}
|
||||||
|
if in.Ref.ID == "" {
|
||||||
|
in.Ref.ID = in.Expected.WorkItemID
|
||||||
}
|
}
|
||||||
} else if in.Expected.WorkItemID == "" && in.Ref.ID == "" {
|
} else if in.Expected.WorkItemID == "" && in.Ref.ID == "" {
|
||||||
// Plane missing-create scenario: no existing work item id means the
|
// Plane missing-create scenario: no existing work item id means the
|
||||||
|
|
@ -223,9 +239,26 @@ func (s *Service) SyncCreation(ctx context.Context, in SyncCreationInput) (SyncC
|
||||||
return SyncCreationResult{Action: SyncActionNotReady, Reason: candidate.Reason}, nil
|
return SyncCreationResult{Action: SyncActionNotReady, Reason: candidate.Reason}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ref := in.Ref
|
||||||
|
if candidate.Identity.WorkItemID != "" {
|
||||||
|
ref.ID = candidate.Identity.WorkItemID
|
||||||
|
}
|
||||||
|
if string(candidate.Identity.Provider) != "" {
|
||||||
|
ref.Provider = candidate.Identity.Provider
|
||||||
|
}
|
||||||
|
if candidate.Identity.Tenant != "" {
|
||||||
|
ref.Tenant = candidate.Identity.Tenant
|
||||||
|
}
|
||||||
|
if candidate.Identity.Project != "" {
|
||||||
|
ref.Project = candidate.Identity.Project
|
||||||
|
}
|
||||||
|
if in.Ref.Provider == ref.Provider && in.Ref.Tenant == ref.Tenant && in.Ref.Project == ref.Project && in.Ref.ID == ref.ID {
|
||||||
|
ref = in.Ref
|
||||||
|
}
|
||||||
|
|
||||||
projectionInput := roadmapsync.ProjectPlaneTodoInput{
|
projectionInput := roadmapsync.ProjectPlaneTodoInput{
|
||||||
Candidate: candidate,
|
Candidate: candidate,
|
||||||
Ref: in.Ref,
|
Ref: ref,
|
||||||
OriginalBody: in.OriginalBody,
|
OriginalBody: in.OriginalBody,
|
||||||
TodoStateID: in.TodoStateID,
|
TodoStateID: in.TodoStateID,
|
||||||
MilestoneMarkdown: in.MilestoneMarkdown,
|
MilestoneMarkdown: in.MilestoneMarkdown,
|
||||||
|
|
@ -264,6 +297,25 @@ func (s *Service) SyncCreation(ctx context.Context, in SyncCreationInput) (SyncC
|
||||||
|
|
||||||
switch decision.Action {
|
switch decision.Action {
|
||||||
case roadmapsync.ActionComplete:
|
case roadmapsync.ActionComplete:
|
||||||
|
// If attributes changed, update them in the DB before returning complete
|
||||||
|
if found && (existingRow.RoadmapMilestonePath != candidate.Identity.RoadmapMilestonePath ||
|
||||||
|
existingRow.Provider != string(candidate.Identity.Provider) ||
|
||||||
|
existingRow.Tenant != candidate.Identity.Tenant ||
|
||||||
|
existingRow.Project != candidate.Identity.Project ||
|
||||||
|
existingRow.WorkItemID != candidate.Identity.WorkItemID ||
|
||||||
|
derefString(existingRow.ProviderRevision) != in.ProviderRevision ||
|
||||||
|
derefString(existingRow.RoadmapRevision) != in.RoadmapRevision) {
|
||||||
|
_, err = s.store.UpsertRoadmapSyncIdentity(ctx, upsertParams(candidate.Identity, in))
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, storage.ErrRoadmapSyncIdentityConflict) {
|
||||||
|
return SyncCreationResult{
|
||||||
|
Action: SyncActionConflict,
|
||||||
|
Reason: "roadmap sync identity conflict: work item already bound or filename reuse suspected",
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
return SyncCreationResult{}, err
|
||||||
|
}
|
||||||
|
}
|
||||||
return SyncCreationResult{Action: SyncActionComplete, Reason: decision.Reason}, nil
|
return SyncCreationResult{Action: SyncActionComplete, Reason: decision.Reason}, nil
|
||||||
case roadmapsync.ActionConflict:
|
case roadmapsync.ActionConflict:
|
||||||
return SyncCreationResult{Action: SyncActionConflict, Reason: decision.Reason}, nil
|
return SyncCreationResult{Action: SyncActionConflict, Reason: decision.Reason}, nil
|
||||||
|
|
|
||||||
|
|
@ -21,29 +21,33 @@ const (
|
||||||
// step so a stop-before-later-step test is possible.
|
// step so a stop-before-later-step test is possible.
|
||||||
type fakeProvider struct {
|
type fakeProvider struct {
|
||||||
calls []string
|
calls []string
|
||||||
|
refCalls []workitem.Ref
|
||||||
failOn string
|
failOn string
|
||||||
failErr error
|
failErr error
|
||||||
createdInput workitem.CreateInput
|
createdInput workitem.CreateInput
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *fakeProvider) AppendComment(_ context.Context, _ workitem.CommentInput) error {
|
func (f *fakeProvider) AppendComment(_ context.Context, input workitem.CommentInput) error {
|
||||||
f.calls = append(f.calls, "comment")
|
f.calls = append(f.calls, "comment")
|
||||||
|
f.refCalls = append(f.refCalls, input.Ref)
|
||||||
if f.failOn == "comment" {
|
if f.failOn == "comment" {
|
||||||
return f.failErr
|
return f.failErr
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *fakeProvider) ProjectBody(_ context.Context, _ workitem.BodyProjection) error {
|
func (f *fakeProvider) ProjectBody(_ context.Context, input workitem.BodyProjection) error {
|
||||||
f.calls = append(f.calls, "body")
|
f.calls = append(f.calls, "body")
|
||||||
|
f.refCalls = append(f.refCalls, input.Ref)
|
||||||
if f.failOn == "body" {
|
if f.failOn == "body" {
|
||||||
return f.failErr
|
return f.failErr
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *fakeProvider) ProjectStatus(_ context.Context, _ workitem.StatusProjection) error {
|
func (f *fakeProvider) ProjectStatus(_ context.Context, input workitem.StatusProjection) error {
|
||||||
f.calls = append(f.calls, "status")
|
f.calls = append(f.calls, "status")
|
||||||
|
f.refCalls = append(f.refCalls, input.Ref)
|
||||||
if f.failOn == "status" {
|
if f.failOn == "status" {
|
||||||
return f.failErr
|
return f.failErr
|
||||||
}
|
}
|
||||||
|
|
@ -692,3 +696,124 @@ func TestSyncCreationMissingCreateWorkItemConflictReturnsConflictAction(t *testi
|
||||||
t.Fatalf("expected conflict action, got %s (%s)", res.Action, res.Reason)
|
t.Fatalf("expected conflict action, got %s (%s)", res.Action, res.Reason)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSyncCreationPreservesTriggerRefForProviderProjection(t *testing.T) {
|
||||||
|
store := newFakeStore()
|
||||||
|
store.found = true
|
||||||
|
store.identity = db.RoadmapSyncIdentity{
|
||||||
|
ID: 1,
|
||||||
|
Shape: string(roadmapsync.ShapeMilestone),
|
||||||
|
RoadmapMilestonePath: testMilestonePath,
|
||||||
|
RoadmapItemID: "m1",
|
||||||
|
Provider: "plane",
|
||||||
|
Tenant: "general",
|
||||||
|
Project: "nomad",
|
||||||
|
WorkItemID: "NOMAD-OLD",
|
||||||
|
ProviderRevision: nullableString(testRevision),
|
||||||
|
RoadmapRevision: nullableString(testRevision),
|
||||||
|
}
|
||||||
|
// Simulate partial resume; say StepDevelopMatched and StepOriginalCommentPreserved are complete
|
||||||
|
store.completed[storage.StepDevelopMatched] = true
|
||||||
|
store.completed[storage.StepOriginalCommentPreserved] = true
|
||||||
|
|
||||||
|
provider := &fakeProvider{}
|
||||||
|
svc := NewService(store, provider, "")
|
||||||
|
|
||||||
|
in := readyInput()
|
||||||
|
// Trigger carries WorkItemID = NOMAD-1
|
||||||
|
in.Expected.WorkItemID = "NOMAD-1"
|
||||||
|
in.Ref.ID = "NOMAD-1"
|
||||||
|
in.Scan.ScannedMilestones[0].Identity.WorkItemID = "NOMAD-1"
|
||||||
|
|
||||||
|
res, err := svc.SyncCreation(context.Background(), in)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("SyncCreation returned error: %v", err)
|
||||||
|
}
|
||||||
|
if res.Action != SyncActionProjected {
|
||||||
|
t.Fatalf("expected projected action, got %s (%s)", res.Action, res.Reason)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify upsert occurred and work item ID updated to NOMAD-1
|
||||||
|
if !store.upserted {
|
||||||
|
t.Fatal("expected upsert for attribute update")
|
||||||
|
}
|
||||||
|
if store.identity.WorkItemID != "NOMAD-1" {
|
||||||
|
t.Errorf("expected upserted work item ID to be NOMAD-1, got %q", store.identity.WorkItemID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify provider calls targeted NOMAD-1
|
||||||
|
// The remaining steps that run: plane_body_updated (body) and plane_todo_moved (status)
|
||||||
|
wantCalls := []string{"body", "status"}
|
||||||
|
if !equalStrings(provider.calls, wantCalls) {
|
||||||
|
t.Fatalf("provider calls = %v, want %v", provider.calls, wantCalls)
|
||||||
|
}
|
||||||
|
for i, ref := range provider.refCalls {
|
||||||
|
if ref.ID != "NOMAD-1" {
|
||||||
|
t.Errorf("call[%d] targeted ref ID %q, want NOMAD-1", i, ref.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSyncCreationUpdateAttributesBeforeCompleteNoOp(t *testing.T) {
|
||||||
|
store := newFakeStore()
|
||||||
|
store.found = true
|
||||||
|
store.identity = db.RoadmapSyncIdentity{
|
||||||
|
ID: 1,
|
||||||
|
Shape: string(roadmapsync.ShapeMilestone),
|
||||||
|
RoadmapMilestonePath: "agent-roadmap/phase/old-path/milestones/m1.md",
|
||||||
|
RoadmapItemID: "m1",
|
||||||
|
Provider: "plane",
|
||||||
|
Tenant: "general",
|
||||||
|
Project: "nomad",
|
||||||
|
WorkItemID: "NOMAD-OLD",
|
||||||
|
ProviderRevision: nullableString(""),
|
||||||
|
RoadmapRevision: nullableString(""),
|
||||||
|
}
|
||||||
|
// All steps complete
|
||||||
|
store.completed[storage.StepDevelopMatched] = true
|
||||||
|
store.completed[storage.StepOriginalCommentPreserved] = true
|
||||||
|
store.completed[storage.StepPlaneBodyUpdated] = true
|
||||||
|
store.completed[storage.StepPlaneTodoMoved] = true
|
||||||
|
|
||||||
|
provider := &fakeProvider{}
|
||||||
|
svc := NewService(store, provider, "")
|
||||||
|
|
||||||
|
in := readyInput()
|
||||||
|
// Trigger carries updated attributes (different path, work item, and revision)
|
||||||
|
in.Scan.ChangedFiles = []string{testMilestonePath}
|
||||||
|
in.Scan.ScannedMilestones[0].Path = testMilestonePath
|
||||||
|
in.Scan.ScannedMilestones[0].Identity.RoadmapMilestonePath = testMilestonePath
|
||||||
|
in.Expected.RoadmapMilestonePath = testMilestonePath
|
||||||
|
in.Expected.WorkItemID = "NOMAD-1"
|
||||||
|
in.Ref.ID = "NOMAD-1"
|
||||||
|
in.Scan.ScannedMilestones[0].Identity.WorkItemID = "NOMAD-1"
|
||||||
|
in.ProviderRevision = testRevision
|
||||||
|
in.RoadmapRevision = testRevision
|
||||||
|
|
||||||
|
res, err := svc.SyncCreation(context.Background(), in)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("SyncCreation returned error: %v", err)
|
||||||
|
}
|
||||||
|
if res.Action != SyncActionComplete {
|
||||||
|
t.Fatalf("expected complete action, got %s (%s)", res.Action, res.Reason)
|
||||||
|
}
|
||||||
|
|
||||||
|
// No provider calls should have run
|
||||||
|
if len(provider.calls) > 0 {
|
||||||
|
t.Fatalf("expected no provider calls, got %v", provider.calls)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify upsert occurred and updated attributes
|
||||||
|
if !store.upserted {
|
||||||
|
t.Fatal("expected upsert even on ActionComplete if attributes changed")
|
||||||
|
}
|
||||||
|
if store.identity.RoadmapMilestonePath != testMilestonePath {
|
||||||
|
t.Errorf("expected upserted path to be %q, got %q", testMilestonePath, store.identity.RoadmapMilestonePath)
|
||||||
|
}
|
||||||
|
if store.identity.WorkItemID != "NOMAD-1" {
|
||||||
|
t.Errorf("expected upserted work item ID to be NOMAD-1, got %q", store.identity.WorkItemID)
|
||||||
|
}
|
||||||
|
if derefString(store.identity.RoadmapRevision) != testRevision {
|
||||||
|
t.Errorf("expected upserted revision to be %q, got %q", testRevision, derefString(store.identity.RoadmapRevision))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue