From 95a32d0abea1cef4dec97fd93326452d5498d1fe Mon Sep 17 00:00:00 2001 From: Henry Stern Date: Sun, 24 May 2026 01:03:38 -0300 Subject: [PATCH] plane/testdata: pin to Plane CE v1.3.1; replace work_item_created with verbatim capture work_item_created.json is now a verbatim webhook delivery from plane.stern.ca (2026-05-24), exercising object-form state (PFB-24), past-tense action (PFB-22), the description_*/description_json fields the bridge ignores, and the full activity.actor object Plane ships beyond the {id, display_name} subset the bridge models. The other fixtures (work_item_updated/deleted, comment_*) keep their synthetic UUIDs but are structurally normalized to match the real v1.3.1 envelope. README documents which fixtures are verbatim vs structurally-derived, the capture procedure (psql webhook_logs), and which event types still need verbatim captures. Real-Plane-container e2e (the durable fix for testdata drift) tracked as PFB-28. Co-Authored-By: Claude Opus 4.7 (1M context) --- internal/plane/parse_test.go | 25 ++++-- internal/plane/testdata/README.md | 81 +++++++++++++++++++ internal/plane/testdata/comment_created.json | 19 ++++- internal/plane/testdata/comment_updated.json | 19 ++++- .../plane/testdata/work_item_created.json | 66 ++++++++++----- .../plane/testdata/work_item_deleted.json | 16 +++- .../plane/testdata/work_item_updated.json | 35 +++++++- 7 files changed, 225 insertions(+), 36 deletions(-) create mode 100644 internal/plane/testdata/README.md diff --git a/internal/plane/parse_test.go b/internal/plane/parse_test.go index 039fed0..35d9233 100644 --- a/internal/plane/parse_test.go +++ b/internal/plane/parse_test.go @@ -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, @@ -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) } }, }, diff --git a/internal/plane/testdata/README.md b/internal/plane/testdata/README.md new file mode 100644 index 0000000..601af30 --- /dev/null +++ b/internal/plane/testdata/README.md @@ -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 = '';" \ + | jq . > internal/plane/testdata/.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. diff --git a/internal/plane/testdata/comment_created.json b/internal/plane/testdata/comment_created.json index e708538..8d1b776 100644 --- a/internal/plane/testdata/comment_created.json +++ b/internal/plane/testdata/comment_created.json @@ -9,14 +9,29 @@ "project": "77777777-7777-7777-7777-777777777777", "workspace": "22222222-2222-2222-2222-222222222222", "comment_html": "

Could not reproduce locally. Will retry on the runner.

", + "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 } } diff --git a/internal/plane/testdata/comment_updated.json b/internal/plane/testdata/comment_updated.json index 63b8472..d601ae7 100644 --- a/internal/plane/testdata/comment_updated.json +++ b/internal/plane/testdata/comment_updated.json @@ -9,14 +9,29 @@ "project": "77777777-7777-7777-7777-777777777777", "workspace": "22222222-2222-2222-2222-222222222222", "comment_html": "

Reproduced. It is the parallel test in forge_test.go.

", + "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": "

Reproduced. It is the parallel test in forge_test.go.

", + "old_value": "

Could not reproduce locally. Will retry on the runner.

", "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 } } diff --git a/internal/plane/testdata/work_item_created.json b/internal/plane/testdata/work_item_created.json index b1447e8..0c7ea8d 100644 --- a/internal/plane/testdata/work_item_created.json +++ b/internal/plane/testdata/work_item_created.json @@ -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": "

Race detector trips intermittently on the forge package.

", + "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": "

sample

", + "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 } } diff --git a/internal/plane/testdata/work_item_deleted.json b/internal/plane/testdata/work_item_deleted.json index 0ac0c29..cd65cdc 100644 --- a/internal/plane/testdata/work_item_deleted.json +++ b/internal/plane/testdata/work_item_deleted.json @@ -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 } } diff --git a/internal/plane/testdata/work_item_updated.json b/internal/plane/testdata/work_item_updated.json index d420d3b..51e0302 100644 --- a/internal/plane/testdata/work_item_updated.json +++ b/internal/plane/testdata/work_item_updated.json @@ -6,7 +6,11 @@ "data": { "id": "33333333-3333-3333-3333-333333333333", "name": "Investigate flaky CI runner (in progress)", + "description": {}, "description_html": "

Race detector trips intermittently. Bisecting.

", + "description_json": {}, + "description_stripped": "Race detector trips intermittently. Bisecting.", + "description_binary": null, "state": { "id": "99999999-9999-9999-9999-999999999999", "name": "In Progress", @@ -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 } }