@crvouga/mockingbird-service-plane 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ # Changelog — @crvouga/mockingbird-service-plane
2
+
3
+ ## 0.1.0 (2026-09-22)
4
+
5
+ Initial release.
package/README.md ADDED
@@ -0,0 +1,120 @@
1
+ # @crvouga/mockingbird-service-plane
2
+
3
+ Stateful mock of the **Plane** REST API (v1) for test suites, covering what our bug-report
4
+ dedup and resolution jobs call on one project: work items (Plane's cursor-paginated list, get,
5
+ create, patch), comments, links, states and labels, with Plane's rate limit and error shapes.
6
+ Projects are provisioned on first use with Plane's default workflow (Backlog, Todo, In
7
+ Progress, Done, Cancelled), so no setup is needed.
8
+
9
+ - Operation coverage: [SUPPORT.md](https://github.com/crvouga/mockingbird/blob/main/packages/service/plane/SUPPORT.md)
10
+ - The contract (`openapi.yaml`) is hand-authored from Plane's API reference and the consumer's
11
+ zod schemas (`plane-response.ts`).
12
+
13
+ ## Install
14
+
15
+ ```bash
16
+ npm install -D @crvouga/mockingbird-service-plane
17
+ ```
18
+
19
+ ESM only. Node >= 22 or Bun >= 1.2. No native dependencies. Serve it with
20
+ `npx mockingbird-plane serve`, `createServer` from `./server` (Node), or `createRuntime` with
21
+ any Fetch server.
22
+
23
+ ## Usage
24
+
25
+ The backend hardcodes `https://api.plane.so` (`plane-http-client.ts`, seam **G-Y1**: make it
26
+ env-driven). Point it at the mock; `PLANE_ACCESS_TOKEN`, `PLANE_WORKSPACE_SLUG` and
27
+ `PLANE_BUGS_PROJECT_ID` can be any values (the project id must be a UUID, as our config
28
+ validates).
29
+
30
+ ```bash
31
+ npx mockingbird-plane serve --port 8821 --rate-limit 60
32
+ ```
33
+
34
+ ```ts
35
+ import { createRuntime } from "@crvouga/mockingbird-service-plane"
36
+
37
+ const plane = createRuntime()
38
+ const base = "http://plane.test/api/v1/workspaces/geviti/projects/33333333-3333-4333-8333-333333333333"
39
+ const headers = { "x-api-key": "plane_api_test", "content-type": "application/json" }
40
+
41
+ const created = await plane.fetch(
42
+ new Request(`${base}/work-items/`, {
43
+ method: "POST",
44
+ headers,
45
+ body: JSON.stringify({ name: "Checkout fails on Safari" }),
46
+ }),
47
+ )
48
+ const item = (await created.json()) as { id: string }
49
+ // Resolve it the way a teammate would; the resolution watcher then sees group "completed".
50
+ await plane.fetch(
51
+ new Request(`http://plane.test/__admin/work-items/${item.id}/state`, {
52
+ method: "POST",
53
+ headers,
54
+ body: JSON.stringify({ state: "Done" }),
55
+ }),
56
+ )
57
+ ```
58
+
59
+ ### Routes
60
+
61
+ All under `/api/v1/workspaces/{slug}/projects/{project_id}/`, with `X-API-Key`.
62
+
63
+ | Route | Behaviour |
64
+ | --- | --- |
65
+ | `GET work-items/` | `per_page` (≤ 100), `cursor=<per_page>:<page>:<is_prev>`, `order_by` (default `-created_at`). Plane's envelope: `results`, `next_cursor`, `prev_cursor`, `next_page_results`, `prev_page_results`, `count`, `total_count`, `total_pages`, `total_results`, `grouped_by`, `sub_grouped_by`, `extra_stats`. A malformed cursor is 400. |
66
+ | `POST work-items/` | `{name, description_html?, state?, labels?, priority?}` → 201 with a UUID `id`, per-project `sequence_id`, `state` (default Backlog), `labels`, `created_at`… Unknown state / label ids are DRF 400s (`{"state": ["Invalid pk \"…\" - object does not exist."]}`); a missing name is `{"name": ["This field is required."]}`. |
67
+ | `GET` / `PATCH work-items/{id}/` | Read or partially update (`state`, `labels`, `name`, `description_html`, `priority`). `completed_at` follows the state's group. |
68
+ | `GET` / `POST work-items/{id}/comments/` | `{comment_html, access?}` → 201 comment. |
69
+ | `GET` / `POST work-items/{id}/links/` | `{url, title?}` → 201 link; the same URL twice is 409 `{error, id}`. |
70
+ | `GET states/` | The project's workflow states (`id`, `name`, `group`, `color`, `sequence`, `default`). |
71
+ | `GET` / `POST labels/` | `{name, color?, description?}` → 201 label; a duplicate name is 409 `{error, id: <existing>}`. |
72
+
73
+ Errors: no key 401 `{"detail": "Authentication credentials were not provided."}`, a key outside
74
+ `apiKeys` 401 `{"detail": "Given API token is not valid"}`, unknown item/project 404
75
+ `{"error": "The requested resource does not exist."}`, throttled 429 `{"detail": "Request was
76
+ throttled. Expected available in N seconds."}` with `x-ratelimit-*` headers.
77
+
78
+ ### Admin (beyond the standard contract)
79
+
80
+ | Route | Effect |
81
+ | --- | --- |
82
+ | `POST /__admin/work-items/:id/state` | `{state: "<id or name>"}`: move an item (e.g. to `Done`) as a teammate would. |
83
+ | `GET /__admin/work-items` | The namespace's work items. |
84
+ | `POST /__admin/projects` | `{workspace, project}`: provision a project now; answers its states and labels. |
85
+ | `GET/PUT /__admin/settings` | `{apiKeys?, rateLimitPerMinute?: number \| null, projects?: ["<slug>/<uuid>"]}`. `rateLimitPerMinute: 60` reproduces Plane's limit on the mock clock; `projects` pins which projects exist (others 404). |
86
+
87
+ Fault presets (`POST /__admin/faults {"preset": "<name>", "count"?: n}`): `rate_limited` (429),
88
+ `server_error` (500), `bad_gateway` (502 HTML), `unauthorized` (401), `invalid_json` (200
89
+ non-JSON on reads), `network_drop`, `slow` (15 s, past our 10 s timeout),
90
+ `pagination_missing_cursor`, `pagination_repeated_cursor`. Our client retries GETs at 0, 2 and
91
+ 8 s, so `count: 2` recovers on the third attempt and `count: 3` exhausts the budget; writes are
92
+ never retried.
93
+
94
+ ### Namespaces
95
+
96
+ `x-mockingbird-namespace`, a `/ns/<name>` prefix on the base URL, or by API key:
97
+ `PUT /__admin/credentials {"credentials": {"<PLANE_ACCESS_TOKEN>": "<namespace>"}}`.
98
+
99
+ ### Deliberately not modelled
100
+
101
+ - Plane webhooks (our app polls), cycles, modules, pages, intake, attachments, members,
102
+ estimates, worklogs, and `expand=`.
103
+ - Deleting work items, comments, links or labels; archiving.
104
+ - Rich-text processing: `description_stripped` / `comment_stripped` are tag-stripped text.
105
+
106
+ ## API
107
+
108
+ | Export | Kind | Description |
109
+ | --- | --- | --- |
110
+ | `PlaneAPI` | class | The in-process mock: `fetch(request)`, `reset()`, `ensureProject(slug, id)`, `statesOf(id)`, `labelsOf(id)`, `moveToState(itemId, stateIdOrName)`, `applyPatch(item, patch)`, `workItems()`. Options: `sqlite`, `now`, `namespace`, `settings`. |
111
+ | `createRuntime` | function | The mock with the full service contract (health, admin, namespaces, credentials, presets). Options: `settings`, `clock`, `seed`, `adminKey`, `onLog`, `sqlite`. |
112
+ | `PLANE_PRESETS` | object | Every named fault preset. |
113
+ | `PLANE_NAMESPACE` | string | The service name, `"plane"`. |
114
+ | `DEFAULT_STATES` | array | The workflow a new project starts with. |
115
+ | `apiKeyCredential` | function | The `X-API-Key` a request carries (how credentials map to namespaces). |
116
+ | `uuidFrom` | function | The deterministic v4-shaped UUID the mock derives from a string. |
117
+ | `document`, `operationIds`, `supportedOperationIds` | values | The OpenAPI contract and its operation ids. |
118
+ | `createServer`, `serveTarget`, `DEFAULT_PORT` (`./server`) | Node | Serve over `node:http`; the `serve` CLI target (`--api-key`, `--rate-limit`); port 8821. |
119
+
120
+ Part of [mockingbird](https://github.com/crvouga/mockingbird).