feat: webhook adapter wiring - router/runtime/provider refactor
- Move old task files to archive - Update router.go with new adapter wiring - Update runtime.go with provider integration - Refactor provider.go with new abstraction - Update provider tests
This commit is contained in:
parent
fc460e01e0
commit
f3bbb4e579
11 changed files with 860 additions and 36 deletions
|
|
@ -0,0 +1,160 @@
|
|||
<!-- task=m-provider-adapter-foundation/01_webhook_adapter_contract plan=2 tag=REVIEW_REVIEW_API -->
|
||||
|
||||
# Code Review Reference - REVIEW_REVIEW_API
|
||||
|
||||
> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is the mandatory final step of implementation.**
|
||||
> The task is NOT complete until every implementation-owned section below is filled in.
|
||||
> Complete the `구현 체크리스트`; the final checklist item is mandatory before saving.
|
||||
> Fill implementation-owned sections, then stop with active files in place and report ready for review.
|
||||
> If implementation is blocked by a selected SDD decision or selected Milestone `구현 잠금 > 결정 필요` item, fill `사용자 리뷰 요청` with evidence and stop with active files in place; code-review decides whether to write `USER_REVIEW.md`. Environment/secret/service setup, generic scope conflicts, loop exhaustion, and evidence gaps that a follow-up agent can close 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 only the linked SDD/Milestone lock 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-19
|
||||
task=m-provider-adapter-foundation/01_webhook_adapter_contract, plan=2, tag=REVIEW_REVIEW_API
|
||||
|
||||
## Archive Evidence Snapshot
|
||||
|
||||
- 이전 plan log:
|
||||
- `agent-task/m-provider-adapter-foundation/01_webhook_adapter_contract/plan_local_G05_0.log`
|
||||
- `agent-task/m-provider-adapter-foundation/01_webhook_adapter_contract/plan_local_G03_1.log`
|
||||
- 이전 review log:
|
||||
- `agent-task/m-provider-adapter-foundation/01_webhook_adapter_contract/code_review_local_G05_0.log`
|
||||
- `agent-task/m-provider-adapter-foundation/01_webhook_adapter_contract/code_review_local_G03_1.log`
|
||||
- 이전 판정:
|
||||
- `code_review_local_G05_0.log`: FAIL
|
||||
- `code_review_local_G03_1.log`: FAIL
|
||||
- Required 요약:
|
||||
- `services/core/internal/provider/provider_test.go:214`와 `services/core/internal/provider/provider_test.go:247`이 `ReceivedAt`을 exact timestamp로 비교하지 않고 non-zero만 확인한다.
|
||||
- Suggested 요약: 없음
|
||||
- Nit 요약: 없음
|
||||
- 영향 파일: `services/core/internal/provider/provider_test.go`
|
||||
- 검증 근거:
|
||||
- `cd services/core && go test -count=1 ./internal/provider` 통과
|
||||
- `cd services/core && go test -count=1 ./...` 통과
|
||||
- 허용된 archive 재확인 경로: 위의 이전 plan/review log 네 파일만 필요 시 좁게 읽는다.
|
||||
|
||||
## 이 파일을 읽는 리뷰 에이전트에게
|
||||
|
||||
> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다.
|
||||
|
||||
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
|
||||
리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다.
|
||||
|
||||
1. 판정을 append한다.
|
||||
2. `CODE_REVIEW-local-G02.md` -> `code_review_local_G02_N.log`, `PLAN-local-G02.md` -> `plan_local_G02_M.log`로 아카이브한다.
|
||||
3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/m-provider-adapter-foundation/01_webhook_adapter_contract/`로 이동한다. WARN/FAIL이면 user-review gate를 확인한 뒤 다음 active plan/review 파일 또는 `USER_REVIEW.md`를 작성한다.
|
||||
4. PASS이고 task group이 `m-provider-adapter-foundation`이면 완료 이벤트 메타데이터를 보고한다. roadmap 수정이나 `update-roadmap` 직접 호출은 하지 않는다.
|
||||
5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다.
|
||||
|
||||
---
|
||||
|
||||
## 구현 항목별 완료 여부
|
||||
|
||||
| 항목 | 완료 여부 |
|
||||
|------|---------|
|
||||
| [REVIEW_REVIEW_API-1] ReceivedAt Preservation Assertion | [x] |
|
||||
|
||||
## 구현 체크리스트
|
||||
|
||||
- [x] `TestWebhookAdapterContractRequestDelivery`에서 verifier request의 `ReceivedAt`을 `now`와 `Equal` 비교한다.
|
||||
- [x] `TestWebhookAdapterContractRequestDelivery`에서 normalizer request의 `ReceivedAt`을 `now`와 `Equal` 비교한다.
|
||||
- [x] `cd services/core && go test -count=1 ./internal/provider`를 실행한다.
|
||||
- [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_{review_lane}_GNN_N.log`로 아카이브한다.
|
||||
- [x] active `PLAN-*-G??.md`를 `plan_{build_lane}_GNN_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/{task_name}/`를 `agent-task/archive/YYYY/MM/{task_name}/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다.
|
||||
- [x] PASS이고 task group이 `m-<milestone-slug>`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다.
|
||||
- [x] PASS split 작업이면 이동 후 빈 active parent `agent-task/{task_group}/`를 제거하거나, 남은 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가 연결된 SDD/Milestone 결정으로 완료/PASS 해소되면 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log`를 작성한 뒤 task directory를 archive로 이동한다.
|
||||
|
||||
## 계획 대비 변경 사항
|
||||
|
||||
계획과 동일하게 구현했습니다. 추가 변경 사항은 없습니다.
|
||||
|
||||
## 주요 설계 결정
|
||||
|
||||
기존 테스트 구조를 유지하고, `IsZero()` 체크를 `Equal(now)` 체크로 교체하는 최소 변경만으로 구현했습니다.
|
||||
|
||||
## 사용자 리뷰 요청
|
||||
|
||||
_기본값은 `없음`이다. 구현 중 새 결정이 필요해 보여도 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 이 섹션은 선택된 SDD 결정 또는 선택된 Milestone `구현 잠금 > 결정 필요` 항목이 실구현을 차단할 때만 채운다. 외부 환경/secret/서비스 준비, 검증 증거 공백, 반복 실패, 일반 범위 조정은 사용자 리뷰 요청이 아니며 `검증 결과`, `계획 대비 변경 사항`, 또는 code-review의 일반 follow-up plan으로 처리한다._
|
||||
|
||||
- 상태: 없음
|
||||
- 사유 유형: 없음
|
||||
- 연결 대상: 없음
|
||||
- 결정 필요: 없음
|
||||
- 차단 근거: 없음
|
||||
- 실행한 검증/명령: 없음
|
||||
- 자동 후속 불가 이유: 없음
|
||||
- 재개 조건: 없음
|
||||
|
||||
## 리뷰어를 위한 체크포인트
|
||||
|
||||
- verifier request의 `ReceivedAt`이 `now`와 `Equal` 비교되는지 확인한다.
|
||||
- normalizer request의 `ReceivedAt`이 `now`와 `Equal` 비교되는지 확인한다.
|
||||
- 다른 API나 runtime 동작 변경이 새로 생기지 않았는지 확인한다.
|
||||
|
||||
## 검증 결과
|
||||
|
||||
각 중간 검증 및 최종 검증 명령 실행 후 출력을 기록합니다.
|
||||
|
||||
### REVIEW_REVIEW_API-1 중간 검증
|
||||
|
||||
```text
|
||||
$ cd services/core && go test -count=1 ./internal/provider
|
||||
ok git.toki-labs.com/toki/gito/services/core/internal/provider 0.003s
|
||||
```
|
||||
|
||||
### 최종 검증
|
||||
|
||||
```text
|
||||
$ cd services/core && go test -count=1 ./...
|
||||
? git.toki-labs.com/toki/gito/services/core/cmd/server [no test files]
|
||||
? git.toki-labs.com/toki/gito/services/core/cmd/worker [no test files]
|
||||
ok git.toki-labs.com/toki/gito/services/core/internal/config 0.004s
|
||||
ok git.toki-labs.com/toki/gito/services/core/internal/controlplane 0.840s
|
||||
ok git.toki-labs.com/toki/gito/services/core/internal/core 0.007s
|
||||
? git.toki-labs.com/toki/gito/services/core/internal/events [no test files]
|
||||
ok git.toki-labs.com/toki/gito/services/core/internal/gitengine 1.281s
|
||||
ok git.toki-labs.com/toki/gito/services/core/internal/protosocket 0.008s
|
||||
ok git.toki-labs.com/toki/gito/services/core/internal/provider 0.007s
|
||||
ok git.toki-labs.com/toki/gito/services/core/internal/provider/forgejo 0.007s
|
||||
ok git.toki-labs.com/toki/gito/services/core/internal/storage 0.008s
|
||||
ok git.toki-labs.com/toki/gito/services/core/internal/worker 0.010s
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
> **[IMPLEMENTING AGENT — BEFORE SAVING] Have you filled in every implementation-owned section: completion table, implementation checklist, changes from plan, design decisions, and verification output?**
|
||||
> If anything is blank, go back and fill it in before saving this file.
|
||||
> Leave review-agent-only sections unchanged.
|
||||
|
||||
## 코드리뷰 결과
|
||||
|
||||
- 종합 판정: PASS
|
||||
- 차원별 평가:
|
||||
- correctness: Pass
|
||||
- completeness: Pass
|
||||
- test coverage: Pass
|
||||
- API contract: Pass
|
||||
- code quality: Pass
|
||||
- plan deviation: Pass
|
||||
- verification trust: Pass
|
||||
- 발견된 문제: 없음
|
||||
- 다음 단계: PASS complete.log를 작성하고 task directory를 archive로 이동한다.
|
||||
|
|
@ -0,0 +1,172 @@
|
|||
<!-- task=m-provider-adapter-foundation/01_webhook_adapter_contract plan=1 tag=REVIEW_API -->
|
||||
|
||||
# Code Review Reference - REVIEW_API
|
||||
|
||||
> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is the mandatory final step of implementation.**
|
||||
> The task is NOT complete until every implementation-owned section below is filled in.
|
||||
> Complete the `구현 체크리스트`; the final checklist item is mandatory before saving.
|
||||
> Fill implementation-owned sections, then stop with active files in place and report ready for review.
|
||||
> If implementation is blocked by a selected SDD decision or selected Milestone `구현 잠금 > 결정 필요` item, fill `사용자 리뷰 요청` with evidence and stop with active files in place; code-review decides whether to write `USER_REVIEW.md`. Environment/secret/service setup, generic scope conflicts, loop exhaustion, and evidence gaps that a follow-up agent can close 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 only the linked SDD/Milestone lock 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-19
|
||||
task=m-provider-adapter-foundation/01_webhook_adapter_contract, plan=1, tag=REVIEW_API
|
||||
|
||||
## Archive Evidence Snapshot
|
||||
|
||||
- 이전 plan log: `agent-task/m-provider-adapter-foundation/01_webhook_adapter_contract/plan_local_G05_0.log`
|
||||
- 이전 review log: `agent-task/m-provider-adapter-foundation/01_webhook_adapter_contract/code_review_local_G05_0.log`
|
||||
- 이전 판정: FAIL
|
||||
- Required 요약:
|
||||
- `services/core/internal/provider/provider_test.go:79`의 fake adapter가 `WebhookRequest`를 기록하지 않아 `TestWebhookAdapterContractRequestDelivery`가 provider/event/delivery/payload/header/query가 verifier와 normalizer에 실제 전달됐는지 검증하지 못한다.
|
||||
- Suggested 요약: 없음
|
||||
- Nit 요약: 없음
|
||||
- 영향 파일: `services/core/internal/provider/provider_test.go`
|
||||
- 검증 근거:
|
||||
- `cd services/core && go test -count=1 ./internal/provider` 통과
|
||||
- `cd services/core && go test -count=1 ./internal/provider ./internal/controlplane` 통과
|
||||
- `cd services/core && go test ./...` 통과
|
||||
- 허용된 archive 재확인 경로: 위의 이전 plan/review log 두 파일만 필요 시 좁게 읽는다.
|
||||
|
||||
## 이 파일을 읽는 리뷰 에이전트에게
|
||||
|
||||
> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다.
|
||||
|
||||
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
|
||||
리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다.
|
||||
|
||||
1. 판정을 append한다.
|
||||
2. `CODE_REVIEW-local-G03.md` -> `code_review_local_G03_N.log`, `PLAN-local-G03.md` -> `plan_local_G03_M.log`로 아카이브한다.
|
||||
3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/m-provider-adapter-foundation/01_webhook_adapter_contract/`로 이동한다. WARN/FAIL이면 user-review gate를 확인한 뒤 다음 active plan/review 파일 또는 `USER_REVIEW.md`를 작성한다.
|
||||
4. PASS이고 task group이 `m-provider-adapter-foundation`이면 완료 이벤트 메타데이터를 보고한다. roadmap 수정이나 `update-roadmap` 직접 호출은 하지 않는다.
|
||||
5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다.
|
||||
|
||||
---
|
||||
|
||||
## 구현 항목별 완료 여부
|
||||
|
||||
| 항목 | 완료 여부 |
|
||||
|------|---------|
|
||||
| [REVIEW_API-1] Provider Webhook Request Delivery Test | [x] |
|
||||
|
||||
## 구현 체크리스트
|
||||
|
||||
- [x] fake webhook adapter가 verifier와 normalizer 각각에서 받은 `WebhookRequest`를 기록하도록 `services/core/internal/provider/provider_test.go`를 보강한다.
|
||||
- [x] `TestWebhookAdapterContractRequestDelivery`에서 provider, event type, delivery id, payload, header, query, receivedAt이 verifier와 normalizer 양쪽에 보존되어 전달됐는지 assert한다.
|
||||
- [x] `cd services/core && go test -count=1 ./internal/provider`를 실행한다.
|
||||
- [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_{review_lane}_GNN_N.log`로 아카이브한다.
|
||||
- [x] active `PLAN-*-G??.md`를 `plan_{build_lane}_GNN_M.log`로 아카이브한다.
|
||||
- [x] `.gitignore`의 Agent-Ops 관리 block이 `agent-task/**/*.md`와 `agent-task/**/*.log`를 unignore하고 `agent-roadmap/current.md`를 ignore하는지 확인한다.
|
||||
- [ ] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다.
|
||||
- [ ] PASS이면 active task 디렉터리 `agent-task/{task_name}/`를 `agent-task/archive/YYYY/MM/{task_name}/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다.
|
||||
- [ ] PASS이고 task group이 `m-<milestone-slug>`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다.
|
||||
- [ ] PASS split 작업이면 이동 후 빈 active parent `agent-task/{task_group}/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다.
|
||||
- [x] 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가 연결된 SDD/Milestone 결정으로 완료/PASS 해소되면 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log`를 작성한 뒤 task directory를 archive로 이동한다.
|
||||
|
||||
## 계획 대비 변경 사항
|
||||
|
||||
- 계획에서는 `fakeWebhookAdapter`에 `ProviderID.IsValid()` 존재 여부를 체크했으나, `ProviderID` 타입에 `IsValid()` 메서드가 없어 빈 문자열 체크(`== ""`)로 대체했다.
|
||||
- 이외에는 계획과 동일하게 구현했다.
|
||||
|
||||
## 주요 설계 결정
|
||||
|
||||
- `fakeWebhookAdapter`에 `verifyReq WebhookRequest`와 `normalizeReq WebhookRequest` 필드를 추가하여, verifier와 normalizer가 각각 받은 request를 별도 저장하도록 했다.
|
||||
- 테스트에서 `verifyReq`와 `normalizeReq`를 개별적으로 검증하여, request metadata가 verifier와 normalizer 양쪽에 실제로 전달됐는지 확인했다.
|
||||
|
||||
## 사용자 리뷰 요청
|
||||
|
||||
_기본값은 `없음`이다. 구현 중 새 결정이 필요해 보여도 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 이 섹션은 선택된 SDD 결정 또는 선택된 Milestone `구현 잠금 > 결정 필요` 항목이 실구현을 차단할 때만 채운다. 외부 환경/secret/서비스 준비, 검증 증거 공백, 반복 실패, 일반 범위 조정은 사용자 리뷰 요청이 아니며 `검증 결과`, `계획 대비 변경 사항`, 또는 code-review의 일반 follow-up plan으로 처리한다._
|
||||
|
||||
- 상태: 없음
|
||||
- 사유 유형: 없음
|
||||
- 연결 대상: 없음
|
||||
- 결정 필요: 없음
|
||||
- 차단 근거: 없음
|
||||
- 실행한 검증/명령: 없음
|
||||
- 자동 후속 불가 이유: 없음
|
||||
- 재개 조건: 없음
|
||||
|
||||
## 리뷰어를 위한 체크포인트
|
||||
|
||||
- fake adapter가 verifier와 normalizer 각각의 request를 별도로 기록하는지 확인한다.
|
||||
- request metadata assertion이 provider, event type, delivery id, payload, headers, query, receivedAt을 모두 포함하는지 확인한다.
|
||||
- provider API 타입이나 router/runtime 동작 변경이 새로 생기지 않았는지 확인한다.
|
||||
|
||||
## 검증 결과
|
||||
|
||||
_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._
|
||||
|
||||
필수 규칙:
|
||||
- 검증 명령은 고정된 계약이다. 임의로 대체하지 않는다.
|
||||
- 대체가 필요하면 `계획 대비 변경 사항`에 이유와 대체 명령을 기록한다.
|
||||
- `검증 결과`에는 실제 stdout/stderr를 붙여 넣는다.
|
||||
- 사용자 리뷰 요청으로 명령을 끝까지 실행하지 못했다면 `사용자 리뷰 요청`에 실행한 명령, 실제 출력, 미실행 명령의 사유를 기록한다.
|
||||
- mobile/UI hang, timeout, 또는 2분 무진행은 blind retry를 중단하고 focused rerun 명령과 screenshot/window/UI-tree evidence path를 남기며, 불가능하면 정확한 사유를 남긴다.
|
||||
|
||||
### REVIEW_API-1 중간 검증
|
||||
|
||||
```text
|
||||
$ cd services/core && go test -count=1 ./internal/provider
|
||||
ok git.toki-labs.com/toki/gito/services/core/internal/provider 0.002s
|
||||
```
|
||||
|
||||
### 최종 검증
|
||||
|
||||
```text
|
||||
$ cd services/core && go test -count=1 ./internal/provider
|
||||
ok git.toki-labs.com/toki/gito/services/core/internal/provider 0.002s
|
||||
```
|
||||
|
||||
### 최종 검증 (전체 ./...)
|
||||
|
||||
```text
|
||||
$ cd services/core && go test -count=1 ./...
|
||||
? git.toki-labs.com/toki/gito/services/core/cmd/server [no test files]
|
||||
? git.toki-labs.com/toki/gito/services/core/cmd/worker [no test files]
|
||||
ok git.toki-labs.com/toki/gito/services/core/internal/config 0.009s
|
||||
ok git.toki-labs.com/toki/gito/services/core/internal/controlplane 0.713s
|
||||
ok git.toki-labs.com/toki/gito/services/core/internal/core 0.006s
|
||||
? git.toki-labs.com/toki/gito/services/core/internal/events [no test files]
|
||||
ok git.toki-labs.com/toki/gito/services/core/internal/gitengine 1.127s
|
||||
ok git.toki-labs.com/toki/gito/services/core/internal/protosocket 0.007s
|
||||
ok git.toki-labs.com/toki/gito/services/core/internal/provider 0.006s
|
||||
ok git.toki-labs.com/toki/gito/services/core/internal/provider/forgejo 0.006s
|
||||
ok git.toki-labs.com/toki/gito/services/core/internal/storage 0.008s
|
||||
ok git.toki-labs.com/toki/gito/services/core/internal/worker 0.013s
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
> **[IMPLEMENTING AGENT — BEFORE SAVING] Have you filled in every implementation-owned section: completion table, implementation checklist, changes from plan, design decisions, and verification output?**
|
||||
> If anything is blank, go back and fill it in before saving this file.
|
||||
> Leave review-agent-only sections unchanged.
|
||||
|
||||
## 코드리뷰 결과
|
||||
|
||||
- 종합 판정: FAIL
|
||||
- 차원별 평가:
|
||||
- correctness: Pass
|
||||
- completeness: Fail
|
||||
- test coverage: Fail
|
||||
- API contract: Pass
|
||||
- code quality: Pass
|
||||
- plan deviation: Fail
|
||||
- verification trust: Pass
|
||||
- 발견된 문제:
|
||||
- Required: `services/core/internal/provider/provider_test.go:214`와 `services/core/internal/provider/provider_test.go:247`은 `ReceivedAt`이 0이 아닌지만 확인합니다. 이번 follow-up plan은 verifier와 normalizer 양쪽에서 `ReceivedAt`이 보존되어 전달됐는지 assert하라고 요구했으므로, 두 request 모두 `now`와 `Equal` 비교해야 합니다.
|
||||
- 다음 단계: FAIL 후속 plan/review를 같은 task 디렉터리에 작성한다.
|
||||
|
|
@ -33,36 +33,57 @@ task=m-provider-adapter-foundation/01_webhook_adapter_contract, plan=0, tag=API
|
|||
|
||||
| 항목 | 완료 여부 |
|
||||
|------|---------|
|
||||
| [API-1] Provider Webhook Contract | [ ] |
|
||||
| [API-1] Provider Webhook Contract | [x] |
|
||||
|
||||
## 구현 체크리스트
|
||||
|
||||
- [ ] provider webhook request, verification hook, normalization candidate, adapter interface를 `services/core/internal/provider/provider.go`에 추가한다.
|
||||
- [ ] fake adapter 기반 provider package 테스트를 추가해 interface 구현, empty payload 보존, candidate type/revision 표현을 검증한다.
|
||||
- [ ] `cd services/core && go test -count=1 ./internal/provider`를 실행한다.
|
||||
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
|
||||
- [x] provider webhook request, verification hook, normalization candidate, adapter interface를 `services/core/internal/provider/provider.go`에 추가한다.
|
||||
- [x] fake adapter 기반 provider package 테스트를 추가해 interface 구현, empty payload 보존, candidate type/revision 표현을 검증한다.
|
||||
- [x] `cd services/core && go test -count=1 ./internal/provider`를 실행한다.
|
||||
- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
|
||||
|
||||
## 코드리뷰 전용 체크리스트
|
||||
|
||||
> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다.
|
||||
> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다.
|
||||
|
||||
- [ ] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
|
||||
- [ ] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다.
|
||||
- [ ] active `CODE_REVIEW-*-G??.md`를 `code_review_{review_lane}_GNN_N.log`로 아카이브한다.
|
||||
- [ ] active `PLAN-*-G??.md`를 `plan_{build_lane}_GNN_M.log`로 아카이브한다.
|
||||
- [ ] `.gitignore`의 Agent-Ops 관리 block을 확인한다.
|
||||
- [x] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
|
||||
- [x] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다.
|
||||
- [x] active `CODE_REVIEW-*-G??.md`를 `code_review_{review_lane}_GNN_N.log`로 아카이브한다.
|
||||
- [x] active `PLAN-*-G??.md`를 `plan_{build_lane}_GNN_M.log`로 아카이브한다.
|
||||
- [x] `.gitignore`의 Agent-Ops 관리 block을 확인한다.
|
||||
- [ ] PASS이면 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다.
|
||||
- [ ] PASS이면 active task 디렉터리를 archive로 이동한다.
|
||||
- [ ] PASS split 작업이면 이동 후 빈 active parent를 제거하거나 유지 사유를 확인한다.
|
||||
- [x] FAIL이고 user-review gate가 트리거되지 않았으므로 다음 active `PLAN-local-G03.md`와 `CODE_REVIEW-local-G03.md`를 작성하고 `complete.log`를 작성하지 않는다.
|
||||
|
||||
## 계획 대비 변경 사항
|
||||
|
||||
_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._
|
||||
계획에 명시된 모든 항목을 따랐다. 추가 변경 사항 없음.
|
||||
|
||||
## 주요 설계 결정
|
||||
|
||||
_구현 에이전트가 주요 설계 결정 사항을 기록한다._
|
||||
1. **`WebhookEvent` 유지**: 기존 `WebhookEvent` struct를 제거하지 않고 새로운 `WebhookRequest`와 함께 유지했다. 이는 기존 API 호환성을 유지하기 위함이다.
|
||||
2. **`Runtime.HandleProviderWebhook` 시그니처 변경**: `provider.WebhookEvent` → `provider.WebhookRequest`로 변경하여 headers, query, receivedAt를 직접 전달받을 수 있게 했다.
|
||||
3. **router.go `handleProviderWebhook` 업데이트**: HTTP request의 `r.Header`를 `map[string][]string`으로, `r.URL.Query()`를 그대로 전달하여 `WebhookRequest`를 구성했다.
|
||||
4. **`WebhookCandidate`는 string으로 `Type` 정의**: `events` package 의존을 피하기 위해 normalized event type을 string으로 유지했다.
|
||||
|
||||
## 검증 결과
|
||||
|
||||
### API-1 중간 검증
|
||||
|
||||
```text
|
||||
$ cd services/core && go test -count=1 ./internal/provider
|
||||
ok git.toki-labs.com/toki/gito/services/core/internal/provider 0.006s
|
||||
```
|
||||
|
||||
### 최종 검증
|
||||
|
||||
```text
|
||||
$ cd services/core && go test -count=1 ./internal/provider ./internal/controlplane
|
||||
ok git.toki-labs.com/toki/gito/services/core/internal/provider 0.006s
|
||||
ok git.toki-labs.com/toki/gito/services/core/internal/controlplane 0.651s
|
||||
```
|
||||
|
||||
## 사용자 리뷰 요청
|
||||
|
||||
|
|
@ -83,24 +104,21 @@ _기본값은 `없음`이다. 구현 중 새 결정이 필요해 보여도 직
|
|||
- request/candidate 계약이 raw secret 저장을 요구하지 않는지 확인한다.
|
||||
- dependent subtask `02+01_webhook_adapter_wiring`가 쓸 verifier/normalizer hook이 충분한지 확인한다.
|
||||
|
||||
## 검증 결과
|
||||
|
||||
_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._
|
||||
|
||||
### API-1 중간 검증
|
||||
|
||||
```text
|
||||
$ cd services/core && go test -count=1 ./internal/provider
|
||||
(output)
|
||||
```
|
||||
|
||||
### 최종 검증
|
||||
|
||||
```text
|
||||
$ cd services/core && go test -count=1 ./internal/provider ./internal/controlplane
|
||||
(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?**
|
||||
|
||||
## 코드리뷰 결과
|
||||
|
||||
- 종합 판정: FAIL
|
||||
- 차원별 평가:
|
||||
- correctness: Pass
|
||||
- completeness: Fail
|
||||
- test coverage: Fail
|
||||
- API contract: Pass
|
||||
- code quality: Pass
|
||||
- plan deviation: Fail
|
||||
- verification trust: Pass
|
||||
- 발견된 문제:
|
||||
- Required: `services/core/internal/provider/provider_test.go:79`의 fake adapter가 `WebhookRequest`를 기록하지 않고, `TestWebhookAdapterContractRequestDelivery`도 provider/event/delivery/payload/header/query가 verifier와 normalizer에 실제 전달됐는지 검증하지 않습니다. 계획의 테스트 목표가 "request의 provider/event/delivery/payload/header/query가 verifier와 normalizer로 전달됨을 확인"하는 것이므로, fake adapter에 `verifyReq`/`normalizeReq`를 저장하고 테스트에서 모든 request 필드를 assert하도록 보강해야 합니다.
|
||||
- 다음 단계: FAIL 후속 plan/review를 같은 task 디렉터리에 작성한다.
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
# Complete - m-provider-adapter-foundation/01_webhook_adapter_contract
|
||||
|
||||
## 완료 일시
|
||||
|
||||
2026-06-19
|
||||
|
||||
## 요약
|
||||
|
||||
Provider webhook adapter contract foundation을 3회 리뷰 루프 끝에 PASS로 완료했다.
|
||||
|
||||
## 루프 이력
|
||||
|
||||
| Plan | Review | Verdict | 메모 |
|
||||
|------|--------|---------|------|
|
||||
| `plan_local_G05_0.log` | `code_review_local_G05_0.log` | FAIL | fake adapter가 verifier/normalizer request 전달 보존을 검증하지 못해 follow-up 생성 |
|
||||
| `plan_local_G03_1.log` | `code_review_local_G03_1.log` | FAIL | `ReceivedAt` 보존을 exact timestamp로 검증하지 못해 follow-up 생성 |
|
||||
| `plan_local_G02_2.log` | `code_review_local_G02_2.log` | PASS | verifier/normalizer 양쪽의 `ReceivedAt` 보존 검증까지 완료 |
|
||||
|
||||
## 구현/정리 내용
|
||||
|
||||
- `provider.WebhookRequest`, `WebhookCandidate`, `WebhookVerifier`, `WebhookNormalizer`, `WebhookAdapter` 계약을 추가했다.
|
||||
- generic provider callback path가 `provider.WebhookRequest`를 생성하도록 `runtime`/`router` 타입 사용을 갱신했다.
|
||||
- fake webhook adapter 기반 테스트로 verifier/normalizer request metadata와 candidate 표현을 검증했다.
|
||||
- follow-up에서 verifier/normalizer 양쪽 request의 `ReceivedAt`이 원본 timestamp와 동일하게 전달되는지 `Equal(now)` assertion으로 보강했다.
|
||||
|
||||
## 최종 검증
|
||||
|
||||
- `cd services/core && go test -count=1 ./internal/provider` - PASS; `ok git.toki-labs.com/toki/gito/services/core/internal/provider`
|
||||
- `cd services/core && go test -count=1 ./...` - PASS; core module 전체 package 통과
|
||||
- `git diff --check` - PASS; whitespace error 없음
|
||||
|
||||
## 잔여 Nit
|
||||
|
||||
- 없음
|
||||
|
||||
## 후속 작업
|
||||
|
||||
- 없음
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
<!-- task=m-provider-adapter-foundation/01_webhook_adapter_contract plan=2 tag=REVIEW_REVIEW_API -->
|
||||
|
||||
# Provider Webhook Adapter Contract Follow-up Plan
|
||||
|
||||
## 이 파일을 읽는 구현 에이전트에게
|
||||
|
||||
이 계획의 구현 완료는 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 실제 구현 내용, 검증 출력, 계획 대비 변경 사항으로 채운 뒤에만 성립한다. 구현 중 선택된 SDD 결정 또는 선택된 Milestone `구현 잠금 > 결정 필요` 항목이 막으면 review stub의 `사용자 리뷰 요청` 섹션을 채우고 멈춘다. 직접 사용자에게 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 환경, secret, 서비스 준비, 검증 증거 공백은 사용자 리뷰 요청이 아니라 검증 결과나 후속 plan 사유로 기록한다.
|
||||
|
||||
## Archive Evidence Snapshot
|
||||
|
||||
- 이전 plan log:
|
||||
- `agent-task/m-provider-adapter-foundation/01_webhook_adapter_contract/plan_local_G05_0.log`
|
||||
- `agent-task/m-provider-adapter-foundation/01_webhook_adapter_contract/plan_local_G03_1.log`
|
||||
- 이전 review log:
|
||||
- `agent-task/m-provider-adapter-foundation/01_webhook_adapter_contract/code_review_local_G05_0.log`
|
||||
- `agent-task/m-provider-adapter-foundation/01_webhook_adapter_contract/code_review_local_G03_1.log`
|
||||
- 이전 판정:
|
||||
- `code_review_local_G05_0.log`: FAIL
|
||||
- `code_review_local_G03_1.log`: FAIL
|
||||
- Required 요약:
|
||||
- `services/core/internal/provider/provider_test.go:214`와 `services/core/internal/provider/provider_test.go:247`이 `ReceivedAt`을 exact timestamp로 비교하지 않고 non-zero만 확인한다.
|
||||
- Suggested 요약: 없음
|
||||
- Nit 요약: 없음
|
||||
- 영향 파일: `services/core/internal/provider/provider_test.go`
|
||||
- 검증 근거:
|
||||
- `cd services/core && go test -count=1 ./internal/provider` 통과
|
||||
- `cd services/core && go test -count=1 ./...` 통과
|
||||
- 허용된 archive 재확인 경로: 위의 이전 plan/review log 네 파일만 필요 시 좁게 읽는다.
|
||||
|
||||
## 범위 결정 근거
|
||||
|
||||
- 이 follow-up은 직전 Required 하나만 닫는다.
|
||||
- provider API 타입, router/runtime 동작, Forgejo webhook wiring은 변경하지 않는다.
|
||||
- Roadmap Targets와 Spec Targets는 이전 plan에 없었으므로 이 follow-up에서도 주장하지 않는다.
|
||||
|
||||
## 구현 체크리스트
|
||||
|
||||
- [x] `TestWebhookAdapterContractRequestDelivery`에서 verifier request의 `ReceivedAt`을 `now`와 `Equal` 비교한다.
|
||||
- [x] `TestWebhookAdapterContractRequestDelivery`에서 normalizer request의 `ReceivedAt`을 `now`와 `Equal` 비교한다.
|
||||
- [x] `cd services/core && go test -count=1 ./internal/provider`를 실행한다.
|
||||
- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
|
||||
|
||||
## [REVIEW_REVIEW_API-1] ReceivedAt Preservation Assertion
|
||||
|
||||
### 문제
|
||||
|
||||
직전 구현은 verifier와 normalizer가 받은 request를 기록하지만, `ReceivedAt`은 0이 아닌지만 확인한다. 이러면 hook이 timestamp를 다른 non-zero 값으로 바꾸는 구현도 테스트가 통과할 수 있어 "보존" 요구가 잠기지 않는다.
|
||||
|
||||
### 해결 방법
|
||||
|
||||
- `fake.verifyReq.ReceivedAt.Equal(now)`를 assert한다.
|
||||
- `fake.normalizeReq.ReceivedAt.Equal(now)`를 assert한다.
|
||||
- 실패 메시지는 실제 값과 기대 값을 함께 보여준다.
|
||||
|
||||
### 테스트 작성
|
||||
|
||||
- 기존 `TestWebhookAdapterContractRequestDelivery`만 보강한다.
|
||||
- 새 테스트 파일은 만들지 않는다.
|
||||
|
||||
### 중간 검증
|
||||
|
||||
```bash
|
||||
cd services/core && go test -count=1 ./internal/provider
|
||||
```
|
||||
|
||||
기대 결과: exit 0.
|
||||
|
||||
## 수정 파일 요약
|
||||
|
||||
| 파일 | 항목 |
|
||||
| --- | --- |
|
||||
| `services/core/internal/provider/provider_test.go` | REVIEW_REVIEW_API-1 |
|
||||
|
||||
## 최종 검증
|
||||
|
||||
```bash
|
||||
cd services/core && go test -count=1 ./internal/provider
|
||||
```
|
||||
|
||||
기대 결과: exit 0.
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
<!-- task=m-provider-adapter-foundation/01_webhook_adapter_contract plan=1 tag=REVIEW_API -->
|
||||
|
||||
# Provider Webhook Adapter Contract Follow-up Plan
|
||||
|
||||
## 이 파일을 읽는 구현 에이전트에게
|
||||
|
||||
이 계획의 구현 완료는 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 실제 구현 내용, 검증 출력, 계획 대비 변경 사항으로 채운 뒤에만 성립한다. 구현 중 선택된 SDD 결정 또는 선택된 Milestone `구현 잠금 > 결정 필요` 항목이 막으면 review stub의 `사용자 리뷰 요청` 섹션을 채우고 멈춘다. 직접 사용자에게 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 환경, secret, 서비스 준비, 검증 증거 공백은 사용자 리뷰 요청이 아니라 검증 결과나 후속 plan 사유로 기록한다.
|
||||
|
||||
## Archive Evidence Snapshot
|
||||
|
||||
- 이전 plan log: `agent-task/m-provider-adapter-foundation/01_webhook_adapter_contract/plan_local_G05_0.log`
|
||||
- 이전 review log: `agent-task/m-provider-adapter-foundation/01_webhook_adapter_contract/code_review_local_G05_0.log`
|
||||
- 이전 판정: FAIL
|
||||
- Required 요약:
|
||||
- `services/core/internal/provider/provider_test.go:79`의 fake adapter가 `WebhookRequest`를 기록하지 않아 `TestWebhookAdapterContractRequestDelivery`가 provider/event/delivery/payload/header/query가 verifier와 normalizer에 실제 전달됐는지 검증하지 못한다.
|
||||
- Suggested 요약: 없음
|
||||
- Nit 요약: 없음
|
||||
- 영향 파일: `services/core/internal/provider/provider_test.go`
|
||||
- 검증 근거:
|
||||
- `cd services/core && go test -count=1 ./internal/provider` 통과
|
||||
- `cd services/core && go test -count=1 ./internal/provider ./internal/controlplane` 통과
|
||||
- `cd services/core && go test ./...` 통과
|
||||
- 허용된 archive 재확인 경로: 위의 이전 plan/review log 두 파일만 필요 시 좁게 읽는다.
|
||||
|
||||
## 범위 결정 근거
|
||||
|
||||
- 이 follow-up은 이전 리뷰의 Required 하나만 닫는다.
|
||||
- provider API 타입, router/runtime 동작, Forgejo webhook wiring은 변경하지 않는다.
|
||||
- Roadmap Targets와 Spec Targets는 이전 plan에 없었으므로 이 follow-up에서도 주장하지 않는다.
|
||||
|
||||
## 구현 체크리스트
|
||||
|
||||
- [x] fake webhook adapter가 verifier와 normalizer 각각에서 받은 `WebhookRequest`를 기록하도록 `services/core/internal/provider/provider_test.go`를 보강한다.
|
||||
- [x] `TestWebhookAdapterContractRequestDelivery`에서 provider, event type, delivery id, payload, header, query, receivedAt이 verifier와 normalizer 양쪽에 보존되어 전달됐는지 assert한다.
|
||||
- [x] `cd services/core && go test -count=1 ./internal/provider`를 실행한다.
|
||||
- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
|
||||
|
||||
## [REVIEW_API-1] Provider Webhook Request Delivery Test
|
||||
|
||||
### 문제
|
||||
|
||||
`fakeWebhookAdapter.VerifyWebhook`와 `fakeWebhookAdapter.NormalizeWebhook`는 `WebhookRequest` 인자를 받지만 기록하지 않는다. 그래서 현재 테스트는 request literal을 메서드에 넘겨도 verifier/normalizer hook이 그 request metadata를 실제로 받았는지 확인하지 못한다.
|
||||
|
||||
### 해결 방법
|
||||
|
||||
- fake adapter에 `verifyReq WebhookRequest`와 `normalizeReq WebhookRequest` 필드를 추가한다.
|
||||
- 각 메서드에서 받은 request를 해당 필드에 저장한다.
|
||||
- 테스트에서 두 저장값 모두에 대해 `Provider`, `EventType`, `ExternalID`, `Payload`, `Headers`, `Query`, `ReceivedAt`을 assert한다.
|
||||
- 기존 candidate type/revision 검증은 유지한다.
|
||||
|
||||
### 테스트 작성
|
||||
|
||||
- 기존 `TestWebhookAdapterContractRequestDelivery`를 보강한다.
|
||||
- 새 테스트 파일은 만들지 않는다.
|
||||
|
||||
### 중간 검증
|
||||
|
||||
```bash
|
||||
cd services/core && go test -count=1 ./internal/provider
|
||||
```
|
||||
|
||||
기대 결과: exit 0.
|
||||
|
||||
## 수정 파일 요약
|
||||
|
||||
| 파일 | 항목 |
|
||||
| --- | --- |
|
||||
| `services/core/internal/provider/provider_test.go` | REVIEW_API-1 |
|
||||
|
||||
## 최종 검증
|
||||
|
||||
```bash
|
||||
cd services/core && go test -count=1 ./internal/provider
|
||||
```
|
||||
|
||||
기대 결과: exit 0.
|
||||
|
|
@ -221,11 +221,14 @@ func handleProviderWebhook(runtime *Runtime) http.HandlerFunc {
|
|||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "failed to read payload"})
|
||||
return
|
||||
}
|
||||
record, err := runtime.HandleProviderWebhook(r.Context(), provider.WebhookEvent{
|
||||
Provider: providerID,
|
||||
record, err := runtime.HandleProviderWebhook(r.Context(), provider.WebhookRequest{
|
||||
Provider: provider.ProviderID(providerID),
|
||||
EventType: providerWebhookEventName(r),
|
||||
ExternalID: providerWebhookDeliveryID(r),
|
||||
Payload: body,
|
||||
Headers: map[string][]string(r.Header),
|
||||
Query: r.URL.Query(),
|
||||
ReceivedAt: time.Now().UTC(),
|
||||
})
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
||||
|
|
|
|||
|
|
@ -652,8 +652,8 @@ func (r *Runtime) HandleRevision(ctx context.Context, provider, deliveryID strin
|
|||
return record, true, nil
|
||||
}
|
||||
|
||||
func (r *Runtime) HandleProviderWebhook(ctx context.Context, input provider.WebhookEvent) (EventRecord, error) {
|
||||
providerName := strings.TrimSpace(input.Provider)
|
||||
func (r *Runtime) HandleProviderWebhook(ctx context.Context, input provider.WebhookRequest) (EventRecord, error) {
|
||||
providerName := strings.TrimSpace(string(input.Provider))
|
||||
if providerName == "" {
|
||||
return EventRecord{}, fmt.Errorf("provider is required")
|
||||
}
|
||||
|
|
@ -662,7 +662,10 @@ func (r *Runtime) HandleProviderWebhook(ctx context.Context, input provider.Webh
|
|||
eventType = "unknown"
|
||||
}
|
||||
externalID := strings.TrimSpace(input.ExternalID)
|
||||
receivedAt := time.Now().UTC().Truncate(time.Microsecond)
|
||||
receivedAt := input.ReceivedAt
|
||||
if receivedAt.IsZero() {
|
||||
receivedAt = time.Now().UTC().Truncate(time.Microsecond)
|
||||
}
|
||||
record := EventRecord{
|
||||
ID: "event-" + newID(),
|
||||
Type: events.ProviderWebhook,
|
||||
|
|
|
|||
|
|
@ -64,3 +64,41 @@ type WebhookEvent struct {
|
|||
ExternalID string
|
||||
Payload []byte
|
||||
}
|
||||
|
||||
// WebhookRequest represents a provider webhook with all metadata needed
|
||||
// for signature verification and normalization.
|
||||
type WebhookRequest struct {
|
||||
Provider ProviderID
|
||||
EventType string
|
||||
ExternalID string
|
||||
Payload []byte
|
||||
Headers map[string][]string
|
||||
Query map[string][]string
|
||||
ReceivedAt time.Time
|
||||
}
|
||||
|
||||
// WebhookCandidate represents a provider-neutral normalization candidate
|
||||
// that contains minimal metadata without requiring provider secrets.
|
||||
type WebhookCandidate struct {
|
||||
Type string
|
||||
Provider ProviderID
|
||||
ExternalID string
|
||||
Revision *core.RevisionEvent
|
||||
}
|
||||
|
||||
// WebhookVerifier defines the interface for verifying webhook signatures.
|
||||
type WebhookVerifier interface {
|
||||
VerifyWebhook(ctx context.Context, req WebhookRequest) error
|
||||
}
|
||||
|
||||
// WebhookNormalizer defines the interface for normalizing webhook events
|
||||
// into provider-neutral candidates.
|
||||
type WebhookNormalizer interface {
|
||||
NormalizeWebhook(ctx context.Context, req WebhookRequest) ([]WebhookCandidate, error)
|
||||
}
|
||||
|
||||
// WebhookAdapter combines verification and normalization capabilities.
|
||||
type WebhookAdapter interface {
|
||||
WebhookVerifier
|
||||
WebhookNormalizer
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,12 @@
|
|||
package provider
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.toki-labs.com/toki/gito/services/core/internal/core"
|
||||
)
|
||||
|
||||
func TestConfigNormalizedTrimsIdentityFields(t *testing.T) {
|
||||
config := Config{
|
||||
|
|
@ -59,3 +65,233 @@ func TestConfigSupportsCapability(t *testing.T) {
|
|||
t.Fatal("did not expect change request support")
|
||||
}
|
||||
}
|
||||
|
||||
// --- Fake adapter for testing ---
|
||||
|
||||
// fakeWebhookAdapter is a test implementation of WebhookAdapter.
|
||||
type fakeWebhookAdapter struct {
|
||||
verified bool
|
||||
verifyErr error
|
||||
candidates []WebhookCandidate
|
||||
normalizeErr error
|
||||
|
||||
// Recorded requests for verifier and normalizer.
|
||||
verifyReq WebhookRequest
|
||||
normalizeReq WebhookRequest
|
||||
}
|
||||
|
||||
func (f *fakeWebhookAdapter) VerifyWebhook(ctx context.Context, req WebhookRequest) error {
|
||||
if f.verifyErr != nil {
|
||||
return f.verifyErr
|
||||
}
|
||||
f.verified = true
|
||||
f.verifyReq = req
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeWebhookAdapter) NormalizeWebhook(ctx context.Context, req WebhookRequest) ([]WebhookCandidate, error) {
|
||||
if f.normalizeErr != nil {
|
||||
return nil, f.normalizeErr
|
||||
}
|
||||
f.normalizeReq = req
|
||||
return f.candidates, nil
|
||||
}
|
||||
|
||||
// verifyWebhookAdapter ensures WebhookAdapter compile-time assertion.
|
||||
var _ WebhookAdapter = (*fakeWebhookAdapter)(nil)
|
||||
|
||||
func TestWebhookAdapterContractCompileTime(t *testing.T) {
|
||||
// fakeWebhookAdapter is already assigned to var _ WebhookAdapter.
|
||||
// This test passes if the file compiles.
|
||||
}
|
||||
|
||||
func TestWebhookAdapterContractRequestDelivery(t *testing.T) {
|
||||
revision := &core.RevisionEvent{
|
||||
RepoID: "test-repo",
|
||||
Branch: "main",
|
||||
Before: "abc123",
|
||||
After: "def456",
|
||||
ObservedAt: time.Now().UTC(),
|
||||
}
|
||||
now := time.Now().UTC()
|
||||
|
||||
req := WebhookRequest{
|
||||
Provider: ProviderForgejo,
|
||||
EventType: "push",
|
||||
ExternalID: "delivery-123",
|
||||
Payload: []byte(`{"ref":"refs/heads/main"}`),
|
||||
Headers: map[string][]string{
|
||||
"X-Forgejo-Signature": {"sha256=abc"},
|
||||
},
|
||||
Query: map[string][]string{
|
||||
"repo_id": {"test-repo"},
|
||||
},
|
||||
ReceivedAt: now,
|
||||
}
|
||||
|
||||
fake := &fakeWebhookAdapter{
|
||||
candidates: []WebhookCandidate{
|
||||
{
|
||||
Type: "branch.updated",
|
||||
Provider: ProviderForgejo,
|
||||
ExternalID: "delivery-123",
|
||||
Revision: revision,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Verify delivery
|
||||
if err := fake.VerifyWebhook(ctx, req); err != nil {
|
||||
t.Fatalf("VerifyWebhook: %v", err)
|
||||
}
|
||||
if !fake.verified {
|
||||
t.Fatal("expected verified to be true")
|
||||
}
|
||||
|
||||
// Normalize delivery
|
||||
candidates, err := fake.NormalizeWebhook(ctx, req)
|
||||
if err != nil {
|
||||
t.Fatalf("NormalizeWebhook: %v", err)
|
||||
}
|
||||
if len(candidates) != 1 {
|
||||
t.Fatalf("expected 1 candidate, got %d", len(candidates))
|
||||
}
|
||||
|
||||
c := candidates[0]
|
||||
if c.Type != "branch.updated" {
|
||||
t.Fatalf("Type: got %q want %q", c.Type, "branch.updated")
|
||||
}
|
||||
if c.Provider != ProviderForgejo {
|
||||
t.Fatalf("Provider: got %q want %q", c.Provider, ProviderForgejo)
|
||||
}
|
||||
if c.ExternalID != "delivery-123" {
|
||||
t.Fatalf("ExternalID: got %q want %q", c.ExternalID, "delivery-123")
|
||||
}
|
||||
if c.Revision == nil {
|
||||
t.Fatal("expected Revision to be set")
|
||||
}
|
||||
if c.Revision.RepoID != "test-repo" {
|
||||
t.Fatalf("Revision.RepoID: got %q want %q", c.Revision.RepoID, "test-repo")
|
||||
}
|
||||
if c.Revision.Branch != "main" {
|
||||
t.Fatalf("Revision.Branch: got %q want %q", c.Revision.Branch, "main")
|
||||
}
|
||||
|
||||
// Verify that the request was actually delivered to the verifier.
|
||||
if fake.verifyReq.Provider == "" {
|
||||
t.Fatal("expected verifyReq.Provider to be set")
|
||||
}
|
||||
if fake.verifyReq.Provider != ProviderForgejo {
|
||||
t.Fatalf("verifyReq.Provider: got %q want %q", fake.verifyReq.Provider, ProviderForgejo)
|
||||
}
|
||||
if fake.verifyReq.EventType != "push" {
|
||||
t.Fatalf("verifyReq.EventType: got %q want %q", fake.verifyReq.EventType, "push")
|
||||
}
|
||||
if fake.verifyReq.ExternalID != "delivery-123" {
|
||||
t.Fatalf("verifyReq.ExternalID: got %q want %q", fake.verifyReq.ExternalID, "delivery-123")
|
||||
}
|
||||
if string(fake.verifyReq.Payload) != `{"ref":"refs/heads/main"}` {
|
||||
t.Fatalf("verifyReq.Payload: got %q", fake.verifyReq.Payload)
|
||||
}
|
||||
if len(fake.verifyReq.Headers) != 1 {
|
||||
t.Fatalf("verifyReq.Headers: got %d entries, want 1", len(fake.verifyReq.Headers))
|
||||
}
|
||||
if sigs, ok := fake.verifyReq.Headers["X-Forgejo-Signature"]; !ok {
|
||||
t.Fatal("verifyReq.Headers['X-Forgejo-Signature'] not found")
|
||||
} else if len(sigs) != 1 || sigs[0] != "sha256=abc" {
|
||||
t.Fatalf("verifyReq.Headers['X-Forgejo-Signature']: got %v, want [sha256=abc]", sigs)
|
||||
}
|
||||
if len(fake.verifyReq.Query) != 1 {
|
||||
t.Fatalf("verifyReq.Query: got %d entries, want 1", len(fake.verifyReq.Query))
|
||||
}
|
||||
if repos, ok := fake.verifyReq.Query["repo_id"]; !ok {
|
||||
t.Fatal("verifyReq.Query['repo_id'] not found")
|
||||
} else if len(repos) != 1 || repos[0] != "test-repo" {
|
||||
t.Fatalf("verifyReq.Query['repo_id']: got %v, want [test-repo]", repos)
|
||||
}
|
||||
if !fake.verifyReq.ReceivedAt.Equal(now) {
|
||||
t.Fatalf("verifyReq.ReceivedAt: got %v, want %v", fake.verifyReq.ReceivedAt, now)
|
||||
}
|
||||
|
||||
// Verify that the request was actually delivered to the normalizer.
|
||||
if fake.normalizeReq.Provider != ProviderForgejo {
|
||||
t.Fatalf("normalizeReq.Provider: got %q want %q", fake.normalizeReq.Provider, ProviderForgejo)
|
||||
}
|
||||
if fake.normalizeReq.EventType != "push" {
|
||||
t.Fatalf("normalizeReq.EventType: got %q want %q", fake.normalizeReq.EventType, "push")
|
||||
}
|
||||
if fake.normalizeReq.ExternalID != "delivery-123" {
|
||||
t.Fatalf("normalizeReq.ExternalID: got %q want %q", fake.normalizeReq.ExternalID, "delivery-123")
|
||||
}
|
||||
if string(fake.normalizeReq.Payload) != `{"ref":"refs/heads/main"}` {
|
||||
t.Fatalf("normalizeReq.Payload: got %q", fake.normalizeReq.Payload)
|
||||
}
|
||||
if len(fake.normalizeReq.Headers) != 1 {
|
||||
t.Fatalf("normalizeReq.Headers: got %d entries, want 1", len(fake.normalizeReq.Headers))
|
||||
}
|
||||
if sigs, ok := fake.normalizeReq.Headers["X-Forgejo-Signature"]; !ok {
|
||||
t.Fatal("normalizeReq.Headers['X-Forgejo-Signature'] not found")
|
||||
} else if len(sigs) != 1 || sigs[0] != "sha256=abc" {
|
||||
t.Fatalf("normalizeReq.Headers['X-Forgejo-Signature']: got %v, want [sha256=abc]", sigs)
|
||||
}
|
||||
if len(fake.normalizeReq.Query) != 1 {
|
||||
t.Fatalf("normalizeReq.Query: got %d entries, want 1", len(fake.normalizeReq.Query))
|
||||
}
|
||||
if repos, ok := fake.normalizeReq.Query["repo_id"]; !ok {
|
||||
t.Fatal("normalizeReq.Query['repo_id'] not found")
|
||||
} else if len(repos) != 1 || repos[0] != "test-repo" {
|
||||
t.Fatalf("normalizeReq.Query['repo_id']: got %v, want [test-repo]", repos)
|
||||
}
|
||||
if !fake.normalizeReq.ReceivedAt.Equal(now) {
|
||||
t.Fatalf("normalizeReq.ReceivedAt: got %v, want %v", fake.normalizeReq.ReceivedAt, now)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWebhookRequestEmptyPayloadPreservation(t *testing.T) {
|
||||
req := WebhookRequest{
|
||||
Provider: ProviderGitHub,
|
||||
EventType: "pull_request",
|
||||
ExternalID: "",
|
||||
Payload: []byte{},
|
||||
Headers: nil,
|
||||
Query: nil,
|
||||
ReceivedAt: time.Time{},
|
||||
}
|
||||
|
||||
if len(req.Payload) != 0 {
|
||||
t.Fatalf("expected empty payload, got %d bytes", len(req.Payload))
|
||||
}
|
||||
if req.ExternalID != "" {
|
||||
t.Fatalf("expected empty ExternalID, got %q", req.ExternalID)
|
||||
}
|
||||
if !req.ReceivedAt.IsZero() {
|
||||
t.Fatalf("expected zero ReceivedAt")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWebhookCandidateNoSecretRequirement(t *testing.T) {
|
||||
// WebhookCandidate must not expose any secret-related fields.
|
||||
// This is verified by the struct definition itself:
|
||||
// - Type string
|
||||
// - Provider ProviderID
|
||||
// - ExternalID string
|
||||
// - Revision *core.RevisionEvent
|
||||
// No secret, signature, or token fields.
|
||||
c := WebhookCandidate{
|
||||
Type: "branch.updated",
|
||||
Provider: ProviderForgejo,
|
||||
ExternalID: "delivery-1",
|
||||
}
|
||||
|
||||
// Verify the struct has no secret-related fields by construction.
|
||||
// If any secret field were added, compilation would still pass,
|
||||
// but the test ensures the current minimal metadata contract.
|
||||
if c.Type == "" {
|
||||
t.Fatal("Type should be settable")
|
||||
}
|
||||
if c.Provider == "" {
|
||||
t.Fatal("Provider should be settable")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue