Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 17 additions & 8 deletions internal/plane/parse_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@ func TestParse_Fixtures(t *testing.T) {
check func(t *testing.T, ev *Event)
}{
{
// Verbatim capture from Plane CE v1.3.1 (plane.stern.ca,
// 2026-05-24). The fixture exercises: object-form state
// (PFB-24), past-tense action verb (PFB-22), extra
// description_*/description_json fields the bridge ignores,
// and the full activity.actor object Plane ships (the bridge
// only reads .id + .display_name today).
name: "work item created",
fixture: "work_item_created.json",
eventHeader: planeEventIssue,
Expand All @@ -49,23 +55,26 @@ func TestParse_Fixtures(t *testing.T) {
if ev.WorkItem == nil {
t.Fatal("WorkItem nil")
}
if got := ev.WorkItem.Name; got != "Investigate flaky CI runner" {
if got := ev.WorkItem.Name; got != "pfb-25 capture sample" {
t.Errorf("Name = %q", got)
}
if got := ev.WorkItem.SequenceID; got != 42 {
if got := ev.WorkItem.SequenceID; got != 35 {
t.Errorf("SequenceID = %d", got)
}
if got := len(ev.WorkItem.Assignees); got != 1 {
t.Errorf("Assignees len = %d", got)
if got := ev.WorkItem.State.ID; got != "e931d389-7080-4612-9f6a-05b535ac3afa" {
t.Errorf("State.ID = %q", got)
}
if ev.WorkItem.Priority != "high" {
t.Errorf("Priority = %q", ev.WorkItem.Priority)
if got := ev.WorkItem.State.Name; got != "Backlog" {
t.Errorf("State.Name = %q", got)
}
if got := ev.WorkItem.Priority; got != "none" {
t.Errorf("Priority = %q", got)
}
if ev.Comment != nil {
t.Errorf("Comment should be nil")
}
if ev.Actor.DisplayName != "Henry" {
t.Errorf("Actor.DisplayName = %q", ev.Actor.DisplayName)
if got := ev.Actor.DisplayName; got != "henry" {
t.Errorf("Actor.DisplayName = %q", got)
}
},
},
Expand Down
81 changes: 81 additions & 0 deletions internal/plane/testdata/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# internal/plane/testdata

Fixtures used by `parse_test.go`. Each file is a single webhook delivery
body, the exact bytes Plane sends to a `webhook_url` over HTTP.

## Pinned Plane version

**Plane CE v1.3.1** (`makeplane/plane-backend:v1.3.1`). Wire-shape drift
between Plane versions has burned us three times — PFB-22 (action verb
tense), PFB-24 (object-form state/labels/assignees), and PFB-25 (REST
response uses bare UUIDs while webhooks use objects). Every captured
payload below is locked to this version.

Pinning the testdata to v1.3.1 is a snapshot, not a guarantee — when
Plane upstream ships a new minor, recapture the payloads against the new
version, bump this line, and run the suite to surface any decode breaks
before they hit production.

## Provenance

| File | Source | Notes |
|---|---|---|
| `work_item_created.json` | verbatim capture from `plane.stern.ca` (2026-05-24) | Real Plane CE v1.3.1 webhook delivery body. Captured via `psql webhook_logs.request_body`. Exercises object-form state (PFB-24), past-tense action verb (PFB-22), full activity.actor (richer than the bridge models), and the `description`/`description_json`/`description_stripped` fields the bridge currently ignores. |
| `work_item_updated.json` | **structurally derived** — original synthetic shape, augmented to match the real v1.3.1 envelope | Awaiting verbatim capture. Uses the same field layout as the verbatim `work_item_created.json` but with synthetic UUIDs and content for "Investigate flaky CI runner". |
| `work_item_deleted.json` | **structurally derived** — minimal | Awaiting verbatim capture. Deletes carry only `data.id` in the original modelling; verify against a real capture whether Plane v1.3.1 ships richer delete payloads (it likely does — at minimum `project`, `workspace`, `created_by`). |
| `comment_created.json`, `comment_updated.json` | **synthetic** — pre-PFB-24, never recaptured | Awaiting verbatim capture. The shape currently checks only into the fields the bridge reads (`id`, `issue`, `comment_html`, `actor`, `created_by`, `access`); verify whether real Plane comment payloads carry the same actor-as-string + activity.actor-as-object split as work item payloads. |

`comment_deleted.json` is intentionally absent — the bridge doesn't
parse comment deletes from the webhook side today (the Plane→forge
direction is read-only for now), so there's no fixture-decoded path
to pin.

## Capture procedure

Plane stores every delivered webhook in postgres `webhook_logs`. To
recapture for a new version (or to fill one of the synthetic slots above
with a real capture):

```bash
# On the Plane host:
PGPW=$(sudo grep '^POSTGRES_PASSWORD=' /etc/plane/plane.env | cut -d= -f2)

# 1. Trigger the event on Plane (UI or REST). The webhook delivers immediately.
# 2. Find the delivery (most recent matching event):
sudo podman exec -e PGPASSWORD="$PGPW" plane-postgres psql -U plane -d plane -P pager=off -c \
"SELECT id, event_type, created_at
FROM webhook_logs
WHERE event_type = 'issue' OR event_type = 'issue_comment'
ORDER BY created_at DESC LIMIT 5;"

# 3. Pull the verbatim body (cast bytea → text and pipe to jq):
sudo podman exec -e PGPASSWORD="$PGPW" plane-postgres psql -U plane -d plane -tA -P pager=off -c \
"SELECT convert_from(request_body, 'UTF-8') FROM webhook_logs WHERE id = '<delivery-uuid>';" \
| jq . > internal/plane/testdata/<event>.json
```

The capture is purely structural — no secrets, but it does contain
workspace/project/user UUIDs from the source environment. Sanitize if
the source workspace isn't already public.

## What the fixtures pin

`parse_test.go::TestParse_Fixtures` exercises each fixture against the
bridge's `Parse` function and asserts the high-value fields decoded
successfully. The PFB-22, PFB-24, and PFB-25 regressions all manifested
as decode errors that this layer would catch — adding a recaptured
fixture for each event type is the durable defence against the same
class of bug.

For deeper coverage, two inline tests pin specific known-real shapes:

- `TestParse_RealPlaneWorkItemPayload_PFB24` — object-form state + label
in a webhook body
- `TestWorkItem_UnmarshalJSON_RESTShape_PFB25` /
`TestCreateIssue_RealPlaneRESTShape_PFB25` — bare-UUID state /
labels / assignees in a REST POST response (the opposite shape from
webhooks; see PFB-25 for the trap)

A real-Plane-container end-to-end test is tracked separately as the
next phase of the audit, so future contract drift is caught in CI
against the real backend rather than against captured fixtures alone.
19 changes: 17 additions & 2 deletions internal/plane/testdata/comment_created.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,29 @@
"project": "77777777-7777-7777-7777-777777777777",
"workspace": "22222222-2222-2222-2222-222222222222",
"comment_html": "<p>Could not reproduce locally. Will retry on the runner.</p>",
"comment_stripped": "Could not reproduce locally. Will retry on the runner.",
"comment_json": {},
"actor": "88888888-8888-8888-8888-888888888888",
"created_by": "88888888-8888-8888-8888-888888888888",
"access": "INTERNAL"
"updated_by": null,
"access": "INTERNAL",
"created_at": "2026-05-24T01:30:00.000000Z",
"updated_at": "2026-05-24T01:30:00.000000Z"
},
"activity": {
"field": null,
"new_value": null,
"old_value": null,
"actor": {
"id": "88888888-8888-8888-8888-888888888888",
"first_name": "Henry",
"last_name": "Stern",
"email": "henry@stern.ca",
"avatar": "",
"avatar_url": null,
"display_name": "Henry"
}
},
"old_identifier": null,
"new_identifier": null
}
}
19 changes: 17 additions & 2 deletions internal/plane/testdata/comment_updated.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,29 @@
"project": "77777777-7777-7777-7777-777777777777",
"workspace": "22222222-2222-2222-2222-222222222222",
"comment_html": "<p>Reproduced. It is the parallel test in forge_test.go.</p>",
"comment_stripped": "Reproduced. It is the parallel test in forge_test.go.",
"comment_json": {},
"actor": "88888888-8888-8888-8888-888888888888",
"created_by": "88888888-8888-8888-8888-888888888888",
"access": "INTERNAL"
"updated_by": "88888888-8888-8888-8888-888888888888",
"access": "INTERNAL",
"created_at": "2026-05-24T01:30:00.000000Z",
"updated_at": "2026-05-24T02:00:00.000000Z"
},
"activity": {
"field": "comment_html",
"new_value": "<p>Reproduced. It is the parallel test in forge_test.go.</p>",
"old_value": "<p>Could not reproduce locally. Will retry on the runner.</p>",
"actor": {
"id": "88888888-8888-8888-8888-888888888888",
"first_name": "Henry",
"last_name": "Stern",
"email": "henry@stern.ca",
"avatar": "",
"avatar_url": null,
"display_name": "Henry"
}
},
"old_identifier": null,
"new_identifier": null
}
}
66 changes: 46 additions & 20 deletions internal/plane/testdata/work_item_created.json
Original file line number Diff line number Diff line change
@@ -1,34 +1,60 @@
{
"event": "issue",
"action": "created",
"webhook_id": "11111111-1111-1111-1111-111111111111",
"workspace_id": "22222222-2222-2222-2222-222222222222",
"webhook_id": "3eda452e-8761-416d-86e0-02962a11a06c",
"workspace_id": "f0ebd07e-6540-4876-8b07-cdc15659e2b1",
"data": {
"id": "33333333-3333-3333-3333-333333333333",
"name": "Investigate flaky CI runner",
"description_html": "<p>Race detector trips intermittently on the forge package.</p>",
"id": "2d048fe5-c172-42f2-ab92-d5d26f4d7e96",
"labels": [],
"assignees": [],
"state": {
"id": "44444444-4444-4444-4444-444444444444",
"id": "e931d389-7080-4612-9f6a-05b535ac3afa",
"name": "Backlog",
"color": "#60646C",
"group": "backlog"
},
"priority": "high",
"assignees": [
{ "id": "55555555-5555-5555-5555-555555555555", "display_name": "Alice" }
],
"labels": [
{ "id": "66666666-6666-6666-6666-666666666666", "name": "bug", "color": "#ff0000" }
],
"project": "77777777-7777-7777-7777-777777777777",
"workspace": "22222222-2222-2222-2222-222222222222",
"created_by": "88888888-8888-8888-8888-888888888888",
"sequence_id": 42
"description": {},
"created_at": "2026-05-24T03:44:46.654905Z",
"updated_at": "2026-05-24T03:44:46.591386Z",
"deleted_at": null,
"point": null,
"name": "pfb-25 capture sample",
"description_json": {},
"description_html": "<p>sample</p>",
"description_stripped": "sample",
"description_binary": null,
"priority": "none",
"start_date": null,
"target_date": null,
"sequence_id": 35,
"sort_order": 255535.0,
"completed_at": null,
"archived_at": null,
"is_draft": false,
"external_source": null,
"external_id": null,
"created_by": "00a3fa45-f4f8-4a19-b7cf-f86a648f717d",
"updated_by": null,
"project": "ede7e196-e408-47b6-883b-d2188f101dd0",
"workspace": "f0ebd07e-6540-4876-8b07-cdc15659e2b1",
"parent": null,
"estimate_point": null,
"type": null
},
"activity": {
"field": null,
"new_value": null,
"old_value": null,
"actor": {
"id": "88888888-8888-8888-8888-888888888888",
"display_name": "Henry"
}
"id": "00a3fa45-f4f8-4a19-b7cf-f86a648f717d",
"first_name": "Henry",
"last_name": "Stern",
"email": "henry@stern.ca",
"avatar": "",
"avatar_url": null,
"display_name": "henry"
},
"old_identifier": null,
"new_identifier": null
}
}
16 changes: 14 additions & 2 deletions internal/plane/testdata/work_item_deleted.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,24 @@
"webhook_id": "11111111-1111-1111-1111-111111111111",
"workspace_id": "22222222-2222-2222-2222-222222222222",
"data": {
"id": "33333333-3333-3333-3333-333333333333"
"id": "33333333-3333-3333-3333-333333333333",
"project": "77777777-7777-7777-7777-777777777777",
"workspace": "22222222-2222-2222-2222-222222222222"
},
"activity": {
"field": null,
"new_value": null,
"old_value": null,
"actor": {
"id": "88888888-8888-8888-8888-888888888888",
"first_name": "Henry",
"last_name": "Stern",
"email": "henry@stern.ca",
"avatar": "",
"avatar_url": null,
"display_name": "Henry"
}
},
"old_identifier": null,
"new_identifier": null
}
}
35 changes: 33 additions & 2 deletions internal/plane/testdata/work_item_updated.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@
"data": {
"id": "33333333-3333-3333-3333-333333333333",
"name": "Investigate flaky CI runner (in progress)",
"description": {},
"description_html": "<p>Race detector trips intermittently. Bisecting.</p>",
"description_json": {},
"description_stripped": "Race detector trips intermittently. Bisecting.",
"description_binary": null,
"state": {
"id": "99999999-9999-9999-9999-999999999999",
"name": "In Progress",
Expand All @@ -25,12 +29,39 @@
"project": "77777777-7777-7777-7777-777777777777",
"workspace": "22222222-2222-2222-2222-222222222222",
"created_by": "88888888-8888-8888-8888-888888888888",
"sequence_id": 42
"updated_by": "88888888-8888-8888-8888-888888888888",
"sequence_id": 42,
"sort_order": 65535.0,
"completed_at": null,
"archived_at": null,
"is_draft": false,
"external_source": null,
"external_id": null,
"parent": null,
"estimate_point": null,
"type": null,
"type_id": null,
"point": null,
"start_date": null,
"target_date": null,
"deleted_at": null,
"created_at": "2026-05-24T01:00:00.000000Z",
"updated_at": "2026-05-24T02:00:00.000000Z"
},
"activity": {
"field": "state",
"new_value": "99999999-9999-9999-9999-999999999999",
"old_value": "44444444-4444-4444-4444-444444444444",
"actor": {
"id": "88888888-8888-8888-8888-888888888888",
"first_name": "Henry",
"last_name": "Stern",
"email": "henry@stern.ca",
"avatar": "",
"avatar_url": null,
"display_name": "Henry"
}
},
"old_identifier": null,
"new_identifier": null
}
}
Loading