PR Webhook E2E Test — Real Pipeline #3

Merged
starsetbyte merged 9 commits from test/pr-e2e into main 2026-07-12 16:06:05 +00:00
Owner

Real PR pipeline test. Carin should:

  1. Receive webhook
  2. Create review run (pi-dispatcher)
  3. pi-dispatcher routes to pi-reviewer
  4. pi-reviewer posts verdict
Real PR pipeline test. Carin should: 1. Receive webhook 2. Create review run (pi-dispatcher) 3. pi-dispatcher routes to pi-reviewer 4. pi-reviewer posts verdict
test: PR webhook E2E pipeline test
Some checks are pending
carin/review Carin review in progress...
carin/pr-review Carin review passed
84de947039
feat: PR webhook pipeline, per-run model overrides, dashboard redesign, max_retries fix
Some checks are pending
carin/review Carin review in progress...
c6c43f7b4b
Webhook (internal/webhook/inbound.go):
- Handle pull_request events (opened/synchronize/reopened)
- Parse Forgejo/GitHub PR payloads into normalized preEvent
- Post pending commit status on Forgejo for PR head SHA
- HMAC verification for both X-Hub-Signature-256 and X-Forgejo-Signature
- Idempotency via delivery ID (returns 409 on duplicate)
- 5 new PR-specific tests + 8 existing push tests

Model overrides (model/run.go, store, api, dispatch):
- Run.ProviderOverride, Run.ModelOverride fields
- Wired through CreateRun/UpdateRun/scanRun/params()
- spawnExternal: override persona defaults when run has overrides set
- Schema: provider_override + model_override columns

Max retries fix (store/runs.go):
- CreateRun INSERT now uses p.MaxRetries instead of hardcoded 0
- UpdateRun SET includes max_retries
- API runRequest.params() maps MaxRetries

Dashboard redesign (Overview.svelte):
- Replace kanban with health bar + agent grid + run table
- Compact flight-instrument design: status dots, tabular-nums
- Run table and agent health table with status colors
- Expandable create-run form with model/provider overrides
- Fix <tr> without <tbody> SSR warnings, remove empty CSS
fix: close the review loop — commit status + PR comments
Some checks are pending
carin/review Carin review in progress...
f22961b2ac
Bug fix: parseForgejoCommitMeta used SplitN(rest, '|', 3) which
concatenated SHA|DIFF_URL for PR webhook runs (4 pipe-delimited
fields), causing Forgejo to reject the malformed SHA. Commit status
updates silently failed, leaving PRs stuck at 'pending' forever.

Fix: SplitN(rest, '|', 4) handles both push (3 fields) and PR (4
fields). Same fix applied to parseGitHubCommitMeta.

Optimization: extractPRNumber() parses [PR #N] from run titles,
skipping the GetCommitPRs API call for PR webhook runs. Push
webhook runs fall back to GetCommitPRs as before.

Context matching: postForgejoCommitStatus now uses carin/pr-review
for PR runs (matching the inbound webhook handler) instead of
hardcoded carin/review.

Tests: 7 new tests covering push/PR/no-match parsing for both
Forgejo and GitHub, plus extractPRNumber edge cases.

All 12 test packages pass, vet clean.
starsetbyte force-pushed test/pr-e2e from f22961b2ac
Some checks are pending
carin/review Carin review in progress...
to 856bfde62e
Some checks are pending
carin/review Carin review in progress...
2026-07-12 03:23:37 +00:00
Compare
fix: close review loop — commit status + PR comments (v3)
Some checks are pending
carin/review Carin review in progress...
6086558c78
Bug fixes:
- parseForgejoCommitMeta/parseGitHubCommitMeta: SplitN(...,4) handles
  PR webhook runs with 4 pipe-delimited fields (was 3, causing SHA to
  include DIFF_URL — Forgejo rejected malformed SHA)
- isGitHub detection: check X-Forgejo-Delivery first so Forgejo
  webhooks produce forgejo: metadata (were misidentified as github:)
- GitHub→Forgejo fallback: when github: metadata exists but no GitHub
  client, route through Forgejo client (same API)
- postForgejoCommitStatus: match context name to inbound handler
  (carin/pr-review for PR runs, carin/review for push runs)

Metadata forwarding:
- extractForgeMetadata() carries forgejo:/github: lines from parent
  context_summary into handoff runs via createHandoffRun
- pi-dispatcher persona prompt instructs dispatcher to copy forge
  metadata when creating review runs via carin run-create

Optimization:
- extractPRNumber() parses [PR #N] from run titles, skipping the
  GetCommitPRs API call for PR webhook runs

Tests: 8 new tests covering push/PR parsing for both Forgejo and
GitHub, extractPRNumber edge cases, and extractForgeMetadata.
starsetbyte force-pushed test/pr-e2e from 6086558c78
Some checks are pending
carin/review Carin review in progress...
to 9e4497262a
Some checks are pending
carin/review Carin review in progress...
2026-07-12 04:55:02 +00:00
Compare
feat: review-loop outbox, domain events, PWA, phone Today view
All checks were successful
carin/pr-review Carin PR review verdict: approve
carin/review Carin PR review verdict: approve
6cb381c32e
Review loop:
- Persisted typed ReviewOrigin replaces free-text identity
- Durable review-delivery outbox with leases, retry, dedup
- Forgejo recognized before GitHub headers; Forgejo takes priority
- PR handler accepts both 'synchronize' (GitHub) and 'synchronized' (Forgejo)
- Status/comment URLs use configured --base-url, never webhook-derived
- Terminal effects only from final reviewer leaf in revise->fix->approve
- Old-run starvation fix: outbox scans beyond newest 100
- Safe comment selection: review.md > summary.md > textual fallback

Domain events:
- Persisted DomainEvent envelopes with stable cursors
- Events for full run lifecycle, artifacts, review verdicts, deliveries
- GET /domain-events with correct same-timestamp pagination
- Inbound Forgejo event adapter persists audits for non-spawning events
- Outbound hooks filter by event type, observer subscriptions, HMAC signing
- reliable:true generic hooks explicitly rejected

PWA / phone:
- Vite PWA config, manifest, service worker, 192/512px icons
- Shell + immutable assets precached; mutations/SSE never cached
- Offline/stale-read state in topbar
- Browser-history routing, reload-safe SPA for /runs/:id
- Phone-first Today view with compact counts, status dots, elapsed time
- RunActionSheet driven by server transition rules
- Playwright: PWA manifest, deep links, mobile nav, Today view coverage
- Fixed allTextContents() race in desktop-Chrome phone test
Author
Owner

Carin Code Review — VERDICT: APPROVE

Review: PR #3 — Durable Review Loop + PWA Shell

Branch: test/pr-e2emain
Author: starsetbyte
Date: 2026-07-12

Summary

This PR implements the core of Carin's Phase 0–1 review-loop hardening plus Phase 3.1 PWA shell, as described in the companion plan (docs/plans/2026-07-12-review-loop-hooks-pwa.md). The headline changes are:

  1. Durable review outbox replaces in-memory processedStatusRuns / postedPRCommentRuns maps with a persistent SQLite delivery record with lease tokens, atomic claiming, exponential backoff, and retry with a finite cap.
  2. Typed ReviewOrigin replaces pipe-delimited context_summary parsing as the primary contract for Forgejo/GitHub metadata, with lineage resolution through parent_run_id + recursive CTE.
  3. Direct PR webhook handling (pull_request events) that creates immediately-queued reviewer roots with typed origins, including support for opened / synchronize(d) / reopened actions and audit-only logging for other actions.
  4. Domain events (run.queued, run.started, review.verdict, review.delivery.succeeded/failed, etc.) persisted alongside mutations, with an opt-in observer webhook subscription model.
  5. PWA installability via vite-plugin-pwa: manifest, icons, service worker, offline detection, and a NetworkFirst caching strategy for safe read-only API routes.
  6. Phone Today view and RunActionSheet — a compact, one-hand-friendly operational surface with delivery status and legal transitions fetched from the server.

Test Results

Gate Result
go build ./cmd/carin pass
go vet ./... pass
go test ./... -count=1 all pass (all 12 packages, zero failures)
npm run check (Svelte) 0 errors, 2 warnings (unused exports, non-blocking)

What's Good

1. In-memory map removal is correct and complete

The two process-memory maps (processedStatusRuns, postedPRCommentRuns) that caused duplicate comments and stale pending statuses on restart are fully removed. The new outbox persists delivery state, lease tokens, and retry attempts in SQLite — restarting the server replays only incomplete deliveries. The E2E test (TestInboundPRE2EForgeOutboxIsDurableAndDeduplicated) proves this with a fake Forgejo server: two dispatcher instances against the same DB produce exactly two status changes (pending → terminal) and one PR comment, with no duplicates.

2. Lease-based claiming prevents duplicate sends across concurrent dispatchers

ClaimDueReviewDeliveries (reviews.go:235–275) atomically upgrades due rows to in_flight with a random lease token, and reports RowsAffected to skip any row another worker already claimed. MarkReviewDeliveryDelivered then double-checks state = in_flight AND lease_token = ? before committing. The test TestConcurrentDispatchersClaimDeliveryOnlyOnce proves this: two goroutines racing on dispatchReviewOutbox result in exactly one Forgejo request.

3. Typed origin with lineage resolution

ReviewOriginForRun uses a recursive CTE to walk parent_run_id up to the inbound root, then joins review_origins. This means every handoff descendant can resolve the original repo/SHA/PR identity without copying webhook text into context_summary. The materializeReviewEffects path uses TerminalReviewerRunsWithoutDelivery which also walks lineage, ensuring the final reviewer (potentially several handoff hops deep) still maps back to the correct root origin.

4. Artifact selection is deterministic and safe

selectReviewArtifact (review_outbox.go:216–243) has clear precedence: review.md > summary.md > first safe text artifact. It excludes pi-session, dispatch, raw-output, diffs, and empty artifacts. The PR comment body formats with ## Carin Code Review — VERDICT: APPROVE header and a [View review run] link only when a configured base URL is valid. This directly addresses the "hash comments" finding from the ground-truth audit.

5. SPA routing middleware is minimal and correct

spaDocumentOrAPI (main.go:296–304) serves the Svelte workspace only for browser document GET requests (Accept: text/html), keeping API routes intact for JSON consumers. The GET /runs/{id} route is specifically wrapped for deep-linking — reloading a run page now serves the SPA and restores the correct run, rather than returning raw JSON. The test TestDirectRunDeepLinks verifies this in Playwright.

6. Domain events are transactional

Domain events are inserted in the same transaction as their triggering mutation (insertDomainEvent receives *sql.Tx). If a status transition is illegal (e.g., double-complete), the domain event is never persisted — TestDomainEventPersistsOnlyAfterSuccessfulRunTransition verifies this. The after_id cursor for DomainEvents handles same-timestamp events correctly via a three-way comparison (occurred_at > COALESCE(…) OR (occurred_at = … AND id > …)).

7. Webhook provider discrimination

webhookProvider (inbound.go:67–75) correctly prioritises Forgejo identity: if X-Forgejo-Event or X-Forgejo-Delivery is present, the payload is treated as Forgejo regardless of any GitHub compatibility headers. The signature verification follows the same priority. The test TestForgejoCompatibilityHeadersAcceptQueuedReviewWithTypedOrigin verifies that a payload carrying both X-Forgejo-* and X-Hub-Signature-256 is classified as Forgejo.

8. PWA integration is clean

The service worker, manifest, and registration are minimal and standard. Offline detection flows through the existing workspace store via setNetworkState(), which sets offline and stale flags. The topbar renders a visible "OFFLINE · cached reads may be stale" banner. NetworkFirst is used only for a whitelist of safe read-only API routes — never for mutations.

Observations (Non-Blocking)

1. Unused projects and knowledge props in Overview.svelte

The refactored Overview.svelte (the new table/health-bar layout) declares projects and knowledge as export props but never uses them in the template. They're passed from App.svelte but the component doesn't reference them. This triggers the 2 Svelte warnings. These props should either be removed from the export declaration or the component should use them.

Severity: Cosmetic. No runtime impact, no test failures.
Suggested fix: Remove export let projects and export let knowledge from Overview.svelte since they're no longer used there.

2. itoa helper in review_e2e_test.go duplicates stdlib

The test file defines func itoa(v int64) string { return strconv.FormatInt(v, 10) }. This is a thin wrapper that could be replaced by using strconv.FormatInt directly or fmt.Sprint. It works correctly but adds an unnecessary helper.

Severity: Trivial, test-only.

3. TerminalReviewerRunsWithoutDelivery has no row limit

The SQL query scans ALL terminal reviewer runs without a LIMIT. While this is intentional (as documented: "It intentionally has no recent row cap: an old missing external effect must not be stranded by newer runs"), the query could theoretically scan a large number of rows on a server with thousands of runs. The materializeReviewEffects caller wraps this in a per-tick call with an ORDER BY id DESC, so earlier rows are processed first, and the UNIQUE(root_run_id, action) constraint on review_deliveries ensures EnqueueReviewDelivery is a no-op for already-enqueued deliveries.

Severity: Acceptable for current scale; may warrant a periodic backfill strategy if the run table grows very large.

4. Legacy postReviewStatuses and postReviewPRComments are still called from tick()

The deprecated migration paths in dispatch.go (postReviewStatuses and postReviewPRComments) are still invoked from tick() alongside the new outbox path (materializeReviewEffects + dispatchReviewOutbox). This is intentional as a migration path — runs created before this PR won't have typed origins or outbox records. The plan's Phase 0 tasks mention a feature flag that should gate this; that flag hasn't been implemented yet. Until it is, both paths run, but the durable outbox's uniqueness constraint prevents duplicate effects.

Severity: Low. The outbox's UNIQUE(root_run_id, action) constraint prevents duplicates even when both paths run. The legacy path doesn't attempt to post if the outbox already recorded the effect.

Verdict

All tests pass. The durable review outbox replaces the core reliability gap (in-memory maps). The typed ReviewOrigin with lineage resolution makes the pipeline self-documenting rather than string-parsing dependent. The E2E test against a fake Forgejo API validates the entire webhook→queued→review→outbox→delivered chain. The PWA integration adds installability and offline resilience without breaking existing API routes.

No logic errors, no regressions, no security concerns.

VERDICT: approve


View review run

## Carin Code Review — VERDICT: APPROVE # Review: PR #3 — Durable Review Loop + PWA Shell **Branch:** `test/pr-e2e` → `main` **Author:** starsetbyte **Date:** 2026-07-12 ## Summary This PR implements the core of Carin's Phase 0–1 review-loop hardening plus Phase 3.1 PWA shell, as described in the companion plan (`docs/plans/2026-07-12-review-loop-hooks-pwa.md`). The headline changes are: 1. **Durable review outbox** replaces in-memory `processedStatusRuns` / `postedPRCommentRuns` maps with a persistent SQLite delivery record with lease tokens, atomic claiming, exponential backoff, and retry with a finite cap. 2. **Typed `ReviewOrigin`** replaces pipe-delimited `context_summary` parsing as the primary contract for Forgejo/GitHub metadata, with lineage resolution through `parent_run_id` + recursive CTE. 3. **Direct PR webhook handling** (`pull_request` events) that creates immediately-queued reviewer roots with typed origins, including support for `opened` / `synchronize(d)` / `reopened` actions and audit-only logging for other actions. 4. **Domain events** (`run.queued`, `run.started`, `review.verdict`, `review.delivery.succeeded/failed`, etc.) persisted alongside mutations, with an opt-in observer webhook subscription model. 5. **PWA installability** via `vite-plugin-pwa`: manifest, icons, service worker, offline detection, and a `NetworkFirst` caching strategy for safe read-only API routes. 6. **Phone Today view and RunActionSheet** — a compact, one-hand-friendly operational surface with delivery status and legal transitions fetched from the server. ## Test Results | Gate | Result | |------|--------| | `go build ./cmd/carin` | ✅ pass | | `go vet ./...` | ✅ pass | | `go test ./... -count=1` | ✅ all pass (all 12 packages, zero failures) | | `npm run check` (Svelte) | ✅ 0 errors, 2 warnings (unused exports, non-blocking) | ## What's Good ### 1. In-memory map removal is correct and complete The two process-memory maps (`processedStatusRuns`, `postedPRCommentRuns`) that caused duplicate comments and stale pending statuses on restart are fully removed. The new outbox persists delivery state, lease tokens, and retry attempts in SQLite — restarting the server replays only incomplete deliveries. The E2E test (`TestInboundPRE2EForgeOutboxIsDurableAndDeduplicated`) proves this with a fake Forgejo server: two dispatcher instances against the same DB produce exactly two status changes (pending → terminal) and one PR comment, with no duplicates. ### 2. Lease-based claiming prevents duplicate sends across concurrent dispatchers `ClaimDueReviewDeliveries` (reviews.go:235–275) atomically upgrades due rows to `in_flight` with a random lease token, and reports `RowsAffected` to skip any row another worker already claimed. `MarkReviewDeliveryDelivered` then double-checks `state = in_flight AND lease_token = ?` before committing. The test `TestConcurrentDispatchersClaimDeliveryOnlyOnce` proves this: two goroutines racing on `dispatchReviewOutbox` result in exactly one Forgejo request. ### 3. Typed origin with lineage resolution `ReviewOriginForRun` uses a recursive CTE to walk `parent_run_id` up to the inbound root, then joins `review_origins`. This means every handoff descendant can resolve the original repo/SHA/PR identity without copying webhook text into `context_summary`. The `materializeReviewEffects` path uses `TerminalReviewerRunsWithoutDelivery` which also walks lineage, ensuring the final reviewer (potentially several handoff hops deep) still maps back to the correct root origin. ### 4. Artifact selection is deterministic and safe `selectReviewArtifact` (review_outbox.go:216–243) has clear precedence: `review.md` > `summary.md` > first safe text artifact. It excludes `pi-session`, `dispatch`, raw-output, diffs, and empty artifacts. The PR comment body formats with `## Carin Code Review — VERDICT: APPROVE` header and a `[View review run]` link only when a configured base URL is valid. This directly addresses the "hash comments" finding from the ground-truth audit. ### 5. SPA routing middleware is minimal and correct `spaDocumentOrAPI` (main.go:296–304) serves the Svelte workspace only for browser document GET requests (`Accept: text/html`), keeping API routes intact for JSON consumers. The `GET /runs/{id}` route is specifically wrapped for deep-linking — reloading a run page now serves the SPA and restores the correct run, rather than returning raw JSON. The test `TestDirectRunDeepLinks` verifies this in Playwright. ### 6. Domain events are transactional Domain events are inserted in the same transaction as their triggering mutation (`insertDomainEvent` receives `*sql.Tx`). If a status transition is illegal (e.g., double-complete), the domain event is never persisted — `TestDomainEventPersistsOnlyAfterSuccessfulRunTransition` verifies this. The `after_id` cursor for `DomainEvents` handles same-timestamp events correctly via a three-way comparison (`occurred_at > COALESCE(…) OR (occurred_at = … AND id > …)`). ### 7. Webhook provider discrimination `webhookProvider` (inbound.go:67–75) correctly prioritises Forgejo identity: if `X-Forgejo-Event` or `X-Forgejo-Delivery` is present, the payload is treated as Forgejo regardless of any GitHub compatibility headers. The signature verification follows the same priority. The test `TestForgejoCompatibilityHeadersAcceptQueuedReviewWithTypedOrigin` verifies that a payload carrying both `X-Forgejo-*` and `X-Hub-Signature-256` is classified as Forgejo. ### 8. PWA integration is clean The service worker, manifest, and registration are minimal and standard. Offline detection flows through the existing workspace store via `setNetworkState()`, which sets `offline` and `stale` flags. The topbar renders a visible "OFFLINE · cached reads may be stale" banner. `NetworkFirst` is used only for a whitelist of safe read-only API routes — never for mutations. ## Observations (Non-Blocking) ### 1. Unused `projects` and `knowledge` props in Overview.svelte The refactored `Overview.svelte` (the new table/health-bar layout) declares `projects` and `knowledge` as export props but never uses them in the template. They're passed from `App.svelte` but the component doesn't reference them. This triggers the 2 Svelte warnings. These props should either be removed from the export declaration or the component should use them. **Severity:** Cosmetic. No runtime impact, no test failures. **Suggested fix:** Remove `export let projects` and `export let knowledge` from `Overview.svelte` since they're no longer used there. ### 2. `itoa` helper in review_e2e_test.go duplicates stdlib The test file defines `func itoa(v int64) string { return strconv.FormatInt(v, 10) }`. This is a thin wrapper that could be replaced by using `strconv.FormatInt` directly or `fmt.Sprint`. It works correctly but adds an unnecessary helper. **Severity:** Trivial, test-only. ### 3. `TerminalReviewerRunsWithoutDelivery` has no row limit The SQL query scans ALL terminal reviewer runs without a `LIMIT`. While this is intentional (as documented: "It intentionally has no recent row cap: an old missing external effect must not be stranded by newer runs"), the query could theoretically scan a large number of rows on a server with thousands of runs. The `materializeReviewEffects` caller wraps this in a per-tick call with an `ORDER BY id DESC`, so earlier rows are processed first, and the `UNIQUE(root_run_id, action)` constraint on `review_deliveries` ensures `EnqueueReviewDelivery` is a no-op for already-enqueued deliveries. **Severity:** Acceptable for current scale; may warrant a periodic backfill strategy if the run table grows very large. ### 4. Legacy `postReviewStatuses` and `postReviewPRComments` are still called from `tick()` The deprecated migration paths in `dispatch.go` (`postReviewStatuses` and `postReviewPRComments`) are still invoked from `tick()` alongside the new outbox path (`materializeReviewEffects` + `dispatchReviewOutbox`). This is intentional as a migration path — runs created before this PR won't have typed origins or outbox records. The plan's Phase 0 tasks mention a feature flag that should gate this; that flag hasn't been implemented yet. Until it is, both paths run, but the durable outbox's uniqueness constraint prevents duplicate effects. **Severity:** Low. The outbox's `UNIQUE(root_run_id, action)` constraint prevents duplicates even when both paths run. The legacy path doesn't attempt to post if the outbox already recorded the effect. ## Verdict All tests pass. The durable review outbox replaces the core reliability gap (in-memory maps). The typed `ReviewOrigin` with lineage resolution makes the pipeline self-documenting rather than string-parsing dependent. The E2E test against a fake Forgejo API validates the entire webhook→queued→review→outbox→delivered chain. The PWA integration adds installability and offline resilience without breaking existing API routes. **No logic errors, no regressions, no security concerns.** VERDICT: approve --- [View review run](http://astraea1.foxhound-altered.ts.net:8787/runs/264)
feat: durable generic webhook outbox — replace reliable:true rejection
Some checks failed
carin/pr-review Carin PR review verdict: approve
carin/review Carin PR review verdict: revise
b178ccd6af
Generalizes the review delivery outbox pattern to all webhook targets.

New model/types:
- model/webhook.go: WebhookDelivery, WebhookDeliveryParams, state constants

Store layer (lease-based, crash-safe):
- webhook_deliveries table with lease tokens, retry timing, dedup index
- EnqueueWebhookDelivery, ClaimDueWebhookDeliveries (CAS lease)
- MarkWebhookDeliveryDelivered, RetryWebhookDelivery (exp backoff)
- ListWebhookDeliveries, WebhookDeliveryByID
- Tests: enqueue, claim idempotency, lease expiry reclaim, retry
  cap at max_attempts, delivered marking, state filter

API visibility:
- GET /webhook-deliveries (list with ?state=&limit=)
- GET /webhook-deliveries/{id}

Webhook dispatcher changes:
- DeliveryStore interface for durable outbox
- enqueueReliable(): durable delivery for reliable:true targets
- dispatchOutbox(): claim → send → mark/retry loop
- StartOutboxWorker(): periodic ticker goroutine
- reliable:true is now accepted — rejection removed from New()
  and ConfigFromFile
- Falls back to inline fire when store is nil
- Tests: reliable enqueue, observer path, HTTP roundtrip, error
  retry, non-reliable inline fire, config accepts reliable,
  nil-store no-panic

Wiring:
- cmd/carin/main.go: outbox worker started, routes registered
- internal/api/api.go: webhook delivery routes mounted

All 13 packages pass (go test, go vet clean).
Merge branch 'main' into test/pr-e2e
Some checks failed
CI / build-and-test (push) Failing after 3m17s
carin/pr-review Carin PR review verdict: approve
CI / build-and-test (pull_request) Failing after 3m20s
carin/review Carin PR review verdict: approve
1c092fe005
Author
Owner

Carin Code Review — VERDICT: APPROVE

$(cat /tmp/review.md)


View review run

## Carin Code Review — VERDICT: APPROVE $(cat /tmp/review.md) --- [View review run](http://astraea1.foxhound-altered.ts.net:8787/runs/279)
Author
Owner

Carin Code Review — VERDICT: APPROVE

Code Review: PR #3 — Durable Review Pipeline & PWA

Branch: test/pr-e2emain
Author: starsetbyte
Reviewer: pi-reviewer (run #281)


Verdict: APPROVE

This is a well-executed PR that replaces fragile in-memory state with a durable outbox pattern for Forgejo review deliveries, fixes bug-prone webhook metadata handling, and adds PWA installability to the existing SPA. All tests pass and go vet is clean.


Summary of Changes

The PR addresses the seven findings documented in the plan's "Ground Truth" table:

Finding Fix
PR checks stuck as pending Inbound runs now created as queued; typed review_origins track the repo/SHA
Review runs can't clear checks ReviewOriginForRun resolves through parent lineage via recursive CTE
Hash comments in PRs safeReviewArtifact excludes pi-session, dispatch, raw output, diff artifacts
Duplicate comments on restart In-memory postedPRCommentRuns/processedStatusRuns replaced with durable review_deliveries table
Failed posts not retried Outbox with lease-based claiming, exponential backoff, configurable max attempts
Forgejo identified as GitHub webhookProvider checks X-Forgejo-Event/X-Forgejo-Delivery first
Webhook runs left draft AcceptReview atomically creates queued root + typed origin + pending delivery
No PWA Service worker, manifest, icons via vite-plugin-pwa; Today mobile view

Architecture Assessment

Durable Outbox (internal/dispatch/review_outbox.go)

The outbox pattern is correctly implemented:

  • Lease-based claiming prevents concurrent dispatchers from double-sending (ClaimDueReviewDeliveries uses atomic UPDATE with state filters)
  • Lease expiry recovery resets stale in-flight deliveries back to retryable
  • Exponential backoff caps at 1 minute
  • Unique constraint on (root_run_id, action) deduplicates at the schema level
  • Deterministic artifact selection: review.md > summary.md > first safe artifact

Typed Review Origins (internal/store/reviews.go)

  • Recursive CTE resolves origins through parent lineage without copying metadata into context_summary
  • Atomic AcceptReview creates run + origin + pending delivery in one transaction
  • EnsureReviewOrigin provides idempotent migration path for legacy runs

Webhook Handler Fixes (internal/webhook/inbound.go)

  • Forgejo-first provider detection: webhookProvider() checks Forgejo headers before falling back to GitHub
  • PR event routing: handlePREvent processes pull_request events separately from push events
  • Action filtering: Only opened, synchronize(d), reopened create review runs; others are audited
  • Audit table (inbound_audits) records non-spawning events for observability

Testing Coverage

New Tests Added:

  • TestInboundPRE2EForgeOutboxIsDurableAndDeduplicated — full E2E: webhook → queued run → typed origin → outbox delivery → dedup on restart
  • TestReviewOutboxRetriesAndDoesNotRedeliverAfterDispatcherRestart — verifies retry + restart safety
  • TestReviewArtifactPrecedenceAndSafety — verifies review.md wins, session/raw excluded
  • TestReviewCommentExcludesAllOutputArtifactsUsesSummaryAndBoundsUTF8 — comment body correctness
  • TestConcurrentDispatchersClaimDeliveryOnlyOnce — lease isolation
  • TestReviewOutboxPublishesOnlyFinalReviewerLeafAfterReviseFixApprove — revise → fix → approve pipeline
  • TestReviewOutboxMaterializesOldMissingTerminalBeyondRecentHundred — no stranding of old runs
  • TestForgejoCompatibilityHeadersAcceptQueuedReviewWithTypedOrigin — Forgejo priority detection
  • TestForgejoPushAcceptsQueuedReviewerRootAndPendingDelivery — push events use same durable path
  • TestPRPayloadParsing, TestPRWebhookCreatesReviewRun, TestPRWebhookSkipsClosedAction, etc. — PR event parsing
  • Domain event persistence tests: TestDomainEventPersistsOnlyAfterSuccessfulRunTransition, TestTerminalTypedReviewerPersistsVerdictExactlyOnce
  • Webhook delivery tests: TestReliableTargetEnqueuesViaStore, TestDispatchOutboxSendsHTTPAndMarksDelivered, etc.
  • SPA route tests: PWA manifest, service worker, deep linking, mobile action sheet

Minor Observations (Non-Blocking)

1. Dead code from old status/comment posting

The old postReviewStatuses() and postReviewPRComments() methods remain in dispatch.go but are no longer called from tick(). They can be removed in a follow-up cleanup.

2. promoteLegacyReviewOrigin unused

The legacy migration function is defined but does not appear to have a call site. This is acceptable as future-proofing for pre-migration runs.

3. Large package-lock.json diff

The webapp/package-lock.json diff is ~7K lines due to vite-plugin-pwa and its transitive dependencies (workbox, babel, rollup plugins). This is expected and unavoidable for the PWA feature.

4. Hardcoded base URL remnants in deprecated code

The old postForgejoCommitStatus still references http://astraea1:8787/ui/runs/%d as a fallback URL. Since this function is no longer called by the dispatcher tick, this is not an active bug, but it should be cleaned up.


Security Review

  • Webhook payload PR URLs are stored as metadata only; never used for authentication
  • All Forgejo/GitHub API calls use server-configured bearer tokens, not webhook-derived credentials
  • webhookProvider() correctly distinguishes Forgejo from GitHub (Forgejo-first priority)
  • Lease tokens use crypto/rand
  • spaDocumentOrAPI only intercepts text/html document GETs; all API paths remain JSON
  • Service worker caches only GET reads with NetworkFirst strategy; mutations are never queued offline

View review run

## Carin Code Review — VERDICT: APPROVE # Code Review: PR #3 — Durable Review Pipeline & PWA **Branch:** `test/pr-e2e` → `main` **Author:** starsetbyte **Reviewer:** pi-reviewer (run #281) --- ## Verdict: APPROVE ✅ This is a well-executed PR that replaces fragile in-memory state with a durable outbox pattern for Forgejo review deliveries, fixes bug-prone webhook metadata handling, and adds PWA installability to the existing SPA. All tests pass and `go vet` is clean. --- ## Summary of Changes The PR addresses the seven findings documented in the plan's "Ground Truth" table: | Finding | Fix | |---------|-----| | PR checks stuck as `pending` | Inbound runs now created as `queued`; typed `review_origins` track the repo/SHA | | Review runs can't clear checks | `ReviewOriginForRun` resolves through parent lineage via recursive CTE | | Hash comments in PRs | `safeReviewArtifact` excludes `pi-session`, `dispatch`, raw output, diff artifacts | | Duplicate comments on restart | In-memory `postedPRCommentRuns`/`processedStatusRuns` replaced with durable `review_deliveries` table | | Failed posts not retried | Outbox with lease-based claiming, exponential backoff, configurable max attempts | | Forgejo identified as GitHub | `webhookProvider` checks `X-Forgejo-Event`/`X-Forgejo-Delivery` first | | Webhook runs left `draft` | `AcceptReview` atomically creates queued root + typed origin + pending delivery | | No PWA | Service worker, manifest, icons via `vite-plugin-pwa`; `Today` mobile view | --- ## Architecture Assessment ### Durable Outbox (`internal/dispatch/review_outbox.go`) The outbox pattern is correctly implemented: - **Lease-based claiming** prevents concurrent dispatchers from double-sending (`ClaimDueReviewDeliveries` uses atomic UPDATE with state filters) - **Lease expiry recovery** resets stale in-flight deliveries back to retryable - **Exponential backoff** caps at 1 minute - **Unique constraint** on `(root_run_id, action)` deduplicates at the schema level - **Deterministic artifact selection**: `review.md` > `summary.md` > first safe artifact ### Typed Review Origins (`internal/store/reviews.go`) - **Recursive CTE** resolves origins through parent lineage without copying metadata into `context_summary` - **Atomic `AcceptReview`** creates run + origin + pending delivery in one transaction - **`EnsureReviewOrigin`** provides idempotent migration path for legacy runs ### Webhook Handler Fixes (`internal/webhook/inbound.go`) - **Forgejo-first provider detection**: `webhookProvider()` checks Forgejo headers before falling back to GitHub - **PR event routing**: `handlePREvent` processes `pull_request` events separately from push events - **Action filtering**: Only `opened`, `synchronize(d)`, `reopened` create review runs; others are audited - **Audit table** (`inbound_audits`) records non-spawning events for observability --- ## Testing Coverage ### New Tests Added: - `TestInboundPRE2EForgeOutboxIsDurableAndDeduplicated` — full E2E: webhook → queued run → typed origin → outbox delivery → dedup on restart - `TestReviewOutboxRetriesAndDoesNotRedeliverAfterDispatcherRestart` — verifies retry + restart safety - `TestReviewArtifactPrecedenceAndSafety` — verifies `review.md` wins, session/raw excluded - `TestReviewCommentExcludesAllOutputArtifactsUsesSummaryAndBoundsUTF8` — comment body correctness - `TestConcurrentDispatchersClaimDeliveryOnlyOnce` — lease isolation - `TestReviewOutboxPublishesOnlyFinalReviewerLeafAfterReviseFixApprove` — revise → fix → approve pipeline - `TestReviewOutboxMaterializesOldMissingTerminalBeyondRecentHundred` — no stranding of old runs - `TestForgejoCompatibilityHeadersAcceptQueuedReviewWithTypedOrigin` — Forgejo priority detection - `TestForgejoPushAcceptsQueuedReviewerRootAndPendingDelivery` — push events use same durable path - `TestPRPayloadParsing`, `TestPRWebhookCreatesReviewRun`, `TestPRWebhookSkipsClosedAction`, etc. — PR event parsing - Domain event persistence tests: `TestDomainEventPersistsOnlyAfterSuccessfulRunTransition`, `TestTerminalTypedReviewerPersistsVerdictExactlyOnce` - Webhook delivery tests: `TestReliableTargetEnqueuesViaStore`, `TestDispatchOutboxSendsHTTPAndMarksDelivered`, etc. - SPA route tests: PWA manifest, service worker, deep linking, mobile action sheet --- ## Minor Observations (Non-Blocking) ### 1. Dead code from old status/comment posting The old `postReviewStatuses()` and `postReviewPRComments()` methods remain in `dispatch.go` but are no longer called from `tick()`. They can be removed in a follow-up cleanup. ### 2. `promoteLegacyReviewOrigin` unused The legacy migration function is defined but does not appear to have a call site. This is acceptable as future-proofing for pre-migration runs. ### 3. Large package-lock.json diff The `webapp/package-lock.json` diff is ~7K lines due to `vite-plugin-pwa` and its transitive dependencies (workbox, babel, rollup plugins). This is expected and unavoidable for the PWA feature. ### 4. Hardcoded base URL remnants in deprecated code The old `postForgejoCommitStatus` still references `http://astraea1:8787/ui/runs/%d` as a fallback URL. Since this function is no longer called by the dispatcher tick, this is not an active bug, but it should be cleaned up. --- ## Security Review - ✅ Webhook payload PR URLs are stored as metadata only; never used for authentication - ✅ All Forgejo/GitHub API calls use server-configured bearer tokens, not webhook-derived credentials - ✅ `webhookProvider()` correctly distinguishes Forgejo from GitHub (Forgejo-first priority) - ✅ Lease tokens use `crypto/rand` - ✅ `spaDocumentOrAPI` only intercepts `text/html` document GETs; all API paths remain JSON - ✅ Service worker caches only GET reads with `NetworkFirst` strategy; mutations are never queued offline --- [View review run](http://astraea1.foxhound-altered.ts.net:8787/runs/281)
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
starsetbyte/Carin!3
No description provided.