@nanobpm/nano-workforce 0.167.1 → 0.167.3

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.
@@ -1,93 +0,0 @@
1
- // Tests for the POST /app/api/actions/acknowledge-delivery-graph operation `acknowledgeDeliveryGraph`
2
- // (issue #641). The nwf UI's "Dismiss" (Done ✓) affordance for a TERMINAL delivery-graph run. It stamps
3
- // `acknowledged_at`, which the `delivery_graph_read_model` VIEW (096) derives into `list_bucket` =
4
- // 'history' and `ack_open` = 0, dropping the finished run from the Active list into History. The
5
- // delivery-graph twin of acknowledge-done / acknowledge-epic / acknowledge-pr.
6
- //
7
- // The op's only write is the `acknowledged_at` (+ `updated_at`) stamp, gated on the base `status`
8
- // belonging to the terminal tier (`DELIVERY_GRAPH_TERMINAL_STATUSES`). These tests seed an in-memory
9
- // `delivery_graph_runs` store and assert the 400/404/409/200 gate + the stamp; the bucket derivation
10
- // itself is proven in app/deliveryGraphReadModel.test.ts.
11
- import { test } from "node:test";
12
- import { assertEquals } from "#test-assert";
13
- import type { AppApi } from "@nanobpm/urban";
14
- import { noopLog } from "../test/log.ts";
15
- import { DELIVERY_GRAPH_TERMINAL_STATUSES } from "../app/deliveryGraphRun.ts";
16
- import handler from "./acknowledgeDeliveryGraph.ts";
17
-
18
- // biome-ignore lint/suspicious/noExplicitAny: test-only fake data layer over dynamic row shapes.
19
- function memApp(seed: any[]): { app: AppApi; rows: any[] } {
20
- // biome-ignore lint/suspicious/noExplicitAny: test-only store.
21
- const stores: Record<string, any[]> = { delivery_graph_runs: seed };
22
- function tbl(name: string, pk = "id") {
23
- // biome-ignore lint/suspicious/noExplicitAny: test-only.
24
- const rows = (stores[name] ??= [] as any[]);
25
- return {
26
- // biome-ignore lint/suspicious/noExplicitAny: test-only.
27
- async get(id: any) {
28
- return rows.find((r) => r[pk] === id);
29
- },
30
- // biome-ignore lint/suspicious/noExplicitAny: test-only.
31
- async update(id: any, patch: any) {
32
- const r = rows.find((row) => row[pk] === id);
33
- if (r) Object.assign(r, patch);
34
- return r ? 1 : 0;
35
- },
36
- };
37
- }
38
- const app = {
39
- data: { table: (n: string, pk?: string) => tbl(n, pk) },
40
- log: noopLog(),
41
- } as unknown as AppApi;
42
- return { app, rows: stores.delivery_graph_runs };
43
- }
44
-
45
- async function call(app: AppApi, body: unknown) {
46
- // biome-ignore lint/suspicious/noExplicitAny: test-only op invocation.
47
- return (await handler({ req: {} as any, params: {}, query: {}, body } as any, app)) as any;
48
- }
49
-
50
- test("acknowledge-delivery-graph: stamps acknowledged_at on a terminal run and returns 200", async () => {
51
- const { app, rows } = memApp([{ run_key: "run-1", status: "done", acknowledged_at: null }]);
52
- const res = await call(app, { run_key: "run-1" });
53
- assertEquals(res.status, 200);
54
- assertEquals(res.body.ok, true);
55
- assertEquals(typeof rows[0].acknowledged_at, "string");
56
- assertEquals(typeof rows[0].updated_at, "string");
57
- });
58
-
59
- test("acknowledge-delivery-graph: every terminal status is dismissable", async () => {
60
- for (const status of DELIVERY_GRAPH_TERMINAL_STATUSES) {
61
- const { app, rows } = memApp([{ run_key: "run-1", status, acknowledged_at: null }]);
62
- const res = await call(app, { run_key: "run-1" });
63
- assertEquals(res.status, 200, `status ${status} must be dismissable`);
64
- assertEquals(typeof rows[0].acknowledged_at, "string");
65
- }
66
- });
67
-
68
- test("acknowledge-delivery-graph: a live (running) run is rejected (409) and stays unstamped", async () => {
69
- const { app, rows } = memApp([{ run_key: "run-2", status: "running", acknowledged_at: null }]);
70
- const res = await call(app, { run_key: "run-2" });
71
- assertEquals(res.status, 409);
72
- assertEquals(res.body.ok, false);
73
- assertEquals(rows[0].acknowledged_at, null);
74
- });
75
-
76
- test("acknowledge-delivery-graph: a missing run_key → 400", async () => {
77
- const { app } = memApp([]);
78
- assertEquals((await call(app, {})).status, 400);
79
- assertEquals((await call(app, { run_key: " " })).status, 400);
80
- });
81
-
82
- test("acknowledge-delivery-graph: no matching run → 404", async () => {
83
- const { app } = memApp([]);
84
- assertEquals((await call(app, { run_key: "run-404" })).status, 404);
85
- });
86
-
87
- test("acknowledge-delivery-graph: idempotent — re-acknowledging a terminal run re-stamps and returns 200", async () => {
88
- const { app, rows } = memApp([{ run_key: "run-5", status: "failed", acknowledged_at: null }]);
89
- assertEquals((await call(app, { run_key: "run-5" })).status, 200);
90
- assertEquals(typeof rows[0].acknowledged_at, "string");
91
- assertEquals((await call(app, { run_key: "run-5" })).status, 200);
92
- assertEquals(typeof rows[0].acknowledged_at, "string");
93
- });
@@ -1,110 +0,0 @@
1
- // Tests for the POST /app/api/actions/acknowledge-done operation `acknowledgeDone` (issue #254 §5).
2
- // The nwf UI's "tick off" affordance for a TERMINAL feature run: it stamps `acknowledged_at`, which
3
- // the `feature_read_model` VIEW (073, issue #439) derives into `list_bucket` = 'history', dropping the
4
- // run from Active into History. Unlike acknowledgeBlocked it completes NO user task (a terminal run is
5
- // not parked). Since `list_bucket` is now a VIEW over `status`/`acknowledged_at` (no stored column),
6
- // these tests assert the operation's real write — the `acknowledged_at` stamp — and cross-check the
7
- // resulting bucket through the pure `deriveListBucket` oracle the VIEW mirrors.
8
- import { test } from "node:test";
9
- import { assertEquals } from "#test-assert";
10
- import type { AppApi } from "@nanobpm/urban";
11
- import { deriveListBucket } from "../app/stage.ts";
12
- import { noopLog } from "../test/log.ts";
13
- import handler from "./acknowledgeDone.ts";
14
-
15
- // An in-memory data layer wired through the `featureRuns` gateway (now a plain record table), so the
16
- // test exercises the operation's write path exactly as production does.
17
- function memApp(seed: any[]): { app: AppApi; rows: any[] } {
18
- const stores: Record<string, any[]> = { feature_runs: seed };
19
- function tbl(name: string, pk = "id") {
20
- const rows = (stores[name] ??= [] as any[]);
21
- const match = (r: any, where: any) => Object.entries(where).every(([k, v]) => r[k] === v);
22
- return {
23
- async all() {
24
- return rows.slice();
25
- },
26
- async get(id: any) {
27
- return rows.find((r) => r[pk] === id);
28
- },
29
- async find(where: any = {}) {
30
- return rows.filter((r) => match(r, where));
31
- },
32
- async insert(row: any) {
33
- rows.push({ ...row });
34
- return row[pk];
35
- },
36
- async update(id: any, patch: any) {
37
- const r = rows.find((row) => row[pk] === id);
38
- if (r) Object.assign(r, patch);
39
- return r ? 1 : 0;
40
- },
41
- };
42
- }
43
- const app = {
44
- data: { table: (n: string, pk?: string) => tbl(n, pk) },
45
- log: noopLog(),
46
- } as any as AppApi;
47
- return { app, rows: stores.feature_runs };
48
- }
49
-
50
- async function call(app: AppApi, body: unknown) {
51
- return (await handler({ req: {} as any, params: {}, query: {}, body } as any, app)) as any;
52
- }
53
-
54
- test("acknowledge-done: stamps acknowledged_at and (via the VIEW) buckets a terminal row into History", async () => {
55
- const { app, rows } = memApp([{ feature_key: "o/r#1", status: "merged", converge: 1, auto_merge: 1, acknowledged_at: null }]);
56
- // Before dismissal a terminal-but-unacknowledged run reads as Active through the VIEW.
57
- assertEquals(deriveListBucket(rows[0].status, rows[0].acknowledged_at), "active");
58
-
59
- const res = await call(app, { feature_key: "o/r#1" });
60
-
61
- assertEquals(res.status, 200);
62
- assertEquals(res.body.ok, true);
63
- assertEquals(typeof rows[0].acknowledged_at, "string");
64
- // The op's only write is the stamp; the VIEW derives 'history' from (terminal status + acknowledged).
65
- assertEquals(deriveListBucket(rows[0].status, rows[0].acknowledged_at), "history");
66
- });
67
-
68
- test("acknowledge-done: idempotent-safe — re-acknowledging keeps the row in History", async () => {
69
- const { app, rows } = memApp([{ feature_key: "o/r#2", status: "failed", converge: 1, auto_merge: 1, acknowledged_at: null }]);
70
- const first = await call(app, { feature_key: "o/r#2" });
71
- assertEquals(first.status, 200);
72
- assertEquals(deriveListBucket(rows[0].status, rows[0].acknowledged_at), "history");
73
- const firstStamp = rows[0].acknowledged_at;
74
- // Re-acknowledge — still 200, still history.
75
- const second = await call(app, { feature_key: "o/r#2" });
76
- assertEquals(second.status, 200);
77
- assertEquals(deriveListBucket(rows[0].status, rows[0].acknowledged_at), "history");
78
- assertEquals(typeof firstStamp, "string");
79
- });
80
-
81
- test("acknowledge-done: a missing feature_key → 400", async () => {
82
- const { app } = memApp([]);
83
- const res = await call(app, {});
84
- assertEquals(res.status, 400);
85
- assertEquals(res.body.ok, false);
86
- });
87
-
88
- test("acknowledge-done: no such feature run → 404", async () => {
89
- const { app } = memApp([]);
90
- const res = await call(app, { feature_key: "o/r#gone" });
91
- assertEquals(res.status, 404);
92
- assertEquals(res.body.ok, false);
93
- });
94
-
95
- test("acknowledge-done: a non-terminal run → 409, no acknowledged_at stamped", async () => {
96
- const { app, rows } = memApp([{ feature_key: "o/r#live", status: "running", converge: 1, auto_merge: 1, acknowledged_at: null }]);
97
- const res = await call(app, { feature_key: "o/r#live" });
98
- assertEquals(res.status, 409);
99
- assertEquals(res.body.ok, false);
100
- assertEquals(rows[0].acknowledged_at, null);
101
- // A live run stays Active through the VIEW (no stamp → not History).
102
- assertEquals(deriveListBucket(rows[0].status, rows[0].acknowledged_at), "active");
103
- });
104
-
105
- test("acknowledge-done: a converging (redispatch-terminal but live) run → 409", async () => {
106
- const { app, rows } = memApp([{ feature_key: "o/r#conv", status: "converging", converge: 1, auto_merge: 1, acknowledged_at: null }]);
107
- const res = await call(app, { feature_key: "o/r#conv" });
108
- assertEquals(res.status, 409);
109
- assertEquals(rows[0].acknowledged_at, null);
110
- });
@@ -1,173 +0,0 @@
1
- // Tests for the POST /app/api/actions/acknowledge-epic operation `acknowledgeEpic` (issue #298).
2
- // The nwf UI's "Dismiss" affordance for a RESOLVED epic — landed (delivery=landed) or
3
- // resolved-not-landed (delivery=null); only still-`converging` epics are rejected. It stamps
4
- // `acknowledged_at`, which the `plan_read_model` VIEW (074, issue #439) derives into `list_bucket` =
5
- // 'history' and `ack_open` = 0, dropping the resolved epic from Active into History. Unlike
6
- // acknowledge-blocked it completes NO user task (a resolved epic is not parked). The epic twin of
7
- // acknowledge-done.
8
- //
9
- // Since epic #412 retired the stored `plans.delivery` column, the op derives the delivery signal at
10
- // READ TIME (`derivePlanDelivery` → the pure `deriveDelivery`) by joining the epic's slice
11
- // `plan_tasks.pr_key` → `pull_requests.status`. So these tests seed `plan_tasks` + `pull_requests`
12
- // (not a `plans.delivery` column) to model a landed / converging / resolved-not-landed epic. Because
13
- // `list_bucket`/`ack_open` are now a VIEW (no stored column), the tests assert the operation's real
14
- // write — the `acknowledged_at` stamp and the 200/409 gate — and cross-check the resulting bucket
15
- // through the pure `deriveEpicBucket` / `epicIsAcknowledgeable` oracles the VIEW mirrors.
16
- import { test } from "node:test";
17
- import { assertEquals } from "#test-assert";
18
- import type { AppApi } from "@nanobpm/urban";
19
- import { deriveEpicBucket, epicIsAcknowledgeable } from "../app/delivery.ts";
20
- import { noopLog } from "../test/log.ts";
21
- import { withTrackingViews } from "../test/trackingViews.ts";
22
- import handler from "./acknowledgeEpic.ts";
23
-
24
- // An in-memory data layer wired through the `plans` gateway (now a plain record table). `extra` seeds
25
- // the join surfaces (`plan_tasks` / `pull_requests`) the read-time delivery derivation reads.
26
- function memApp(
27
- seed: any[],
28
- extra: Record<string, any[]> = {},
29
- ): { app: AppApi; rows: any[] } {
30
- const stores: Record<string, any[]> = { plans: seed, ...extra };
31
- function tbl(name: string, pk = "id") {
32
- const rows = (stores[name] ??= [] as any[]);
33
- const match = (r: any, where: any) => Object.entries(where).every(([k, v]) => r[k] === v);
34
- return {
35
- async all() {
36
- return rows.slice();
37
- },
38
- async get(id: any) {
39
- return rows.find((r) => r[pk] === id);
40
- },
41
- async find(where: any = {}) {
42
- return rows.filter((r) => match(r, where));
43
- },
44
- async insert(row: any) {
45
- rows.push({ ...row });
46
- return row[pk];
47
- },
48
- async update(id: any, patch: any) {
49
- const r = rows.find((row) => row[pk] === id);
50
- if (r) Object.assign(r, patch);
51
- return r ? 1 : 0;
52
- },
53
- };
54
- }
55
- const app = {
56
- data: { table: withTrackingViews((n: string, pk?: string) => tbl(n, pk)) },
57
- log: noopLog(),
58
- } as any as AppApi;
59
- return { app, rows: stores.plans };
60
- }
61
-
62
- async function call(app: AppApi, body: unknown) {
63
- return (await handler({ req: {} as any, params: {}, query: {}, body } as any, app)) as any;
64
- }
65
-
66
- test("acknowledge-epic: stamps acknowledged_at and (via the VIEW) buckets a landed epic into History", async () => {
67
- const { app, rows } = memApp(
68
- [{ plan_key: "o/r#1", status: "done", acknowledged_at: null }],
69
- {
70
- plan_tasks: [{ id: 1, plan_key: "o/r#1", pr_key: "o/r#100" }],
71
- pull_requests: [{ pr_key: "o/r#100", status: "merged" }], // landed
72
- },
73
- );
74
- // Before dismissal a landed-but-unacknowledged epic reads as Active with Dismiss open through the VIEW.
75
- assertEquals(deriveEpicBucket("done", "landed", rows[0].acknowledged_at), "active");
76
- assertEquals(epicIsAcknowledgeable("done", "landed"), true);
77
-
78
- const res = await call(app, { plan_key: "o/r#1" });
79
-
80
- assertEquals(res.status, 200);
81
- assertEquals(res.body.ok, true);
82
- assertEquals(typeof rows[0].acknowledged_at, "string");
83
- // The op's only write is the stamp; the VIEW derives 'history' + ack_open 0 from the acknowledged row.
84
- assertEquals(deriveEpicBucket("done", "landed", rows[0].acknowledged_at), "history");
85
- });
86
-
87
- test("acknowledge-epic: a still-converging epic is rejected (409) and stays Active", async () => {
88
- const { app, rows } = memApp(
89
- [{ plan_key: "o/r#2", status: "done", acknowledged_at: null }],
90
- {
91
- plan_tasks: [
92
- { id: 1, plan_key: "o/r#2", pr_key: "o/r#200" },
93
- { id: 2, plan_key: "o/r#2", pr_key: "o/r#201" },
94
- ],
95
- pull_requests: [
96
- { pr_key: "o/r#200", status: "merged" },
97
- { pr_key: "o/r#201", status: "converging" }, // still in flight → converging
98
- ],
99
- },
100
- );
101
-
102
- const res = await call(app, { plan_key: "o/r#2" });
103
-
104
- assertEquals(res.status, 409);
105
- assertEquals(res.body.ok, false);
106
- // Untouched: no premature acknowledged_at; still Active through the VIEW (converging → active).
107
- assertEquals(rows[0].acknowledged_at, null);
108
- assertEquals(deriveEpicBucket("done", "converging", rows[0].acknowledged_at), "active");
109
- });
110
-
111
- test("acknowledge-epic: a resolved-not-landed epic (delivery=null) is accepted (200) and flips to History", async () => {
112
- const { app, rows } = memApp(
113
- [{ plan_key: "o/r#2b", status: "done", acknowledged_at: null }],
114
- {
115
- plan_tasks: [
116
- { id: 1, plan_key: "o/r#2b", pr_key: "o/r#210" },
117
- { id: 2, plan_key: "o/r#2b", pr_key: "o/r#211" },
118
- ],
119
- pull_requests: [
120
- { pr_key: "o/r#210", status: "merged" },
121
- { pr_key: "o/r#211", status: "abandoned" }, // all terminal, not all merged → delivery null
122
- ],
123
- },
124
- );
125
- assertEquals(deriveEpicBucket("done", null, rows[0].acknowledged_at), "active");
126
- assertEquals(epicIsAcknowledgeable("done", null), true);
127
-
128
- const res = await call(app, { plan_key: "o/r#2b" });
129
-
130
- assertEquals(res.status, 200);
131
- assertEquals(res.body.ok, true);
132
- assertEquals(typeof rows[0].acknowledged_at, "string");
133
- assertEquals(deriveEpicBucket("done", null, rows[0].acknowledged_at), "history");
134
- });
135
-
136
- test("acknowledge-epic: a live (dispatched) epic is rejected (409)", async () => {
137
- const { app } = memApp([{ plan_key: "o/r#3", status: "dispatched", acknowledged_at: null }]);
138
- const res = await call(app, { plan_key: "o/r#3" });
139
- assertEquals(res.status, 409);
140
- });
141
-
142
- test("acknowledge-epic: a missing plan_key → 400", async () => {
143
- const { app } = memApp([]);
144
- const res = await call(app, {});
145
- assertEquals(res.status, 400);
146
- });
147
-
148
- test("acknowledge-epic: no matching epic → 404", async () => {
149
- const { app } = memApp([]);
150
- const res = await call(app, { plan_key: "o/r#404" });
151
- assertEquals(res.status, 404);
152
- });
153
-
154
- test("acknowledge-epic: idempotent — re-acknowledging a landed epic keeps it in History", async () => {
155
- const { app, rows } = memApp(
156
- [{ plan_key: "o/r#5", status: "done", acknowledged_at: null }],
157
- {
158
- plan_tasks: [{ id: 1, plan_key: "o/r#5", pr_key: "o/r#500" }],
159
- pull_requests: [{ pr_key: "o/r#500", status: "merged" }], // landed
160
- },
161
- );
162
-
163
- assertEquals((await call(app, { plan_key: "o/r#5" })).status, 200);
164
- const firstStamp = rows[0].acknowledged_at;
165
- assertEquals(deriveEpicBucket("done", "landed", rows[0].acknowledged_at), "history");
166
-
167
- const res2 = await call(app, { plan_key: "o/r#5" });
168
- assertEquals(res2.status, 200);
169
- assertEquals(deriveEpicBucket("done", "landed", rows[0].acknowledged_at), "history");
170
- // Re-stamped (a fresh timestamp) but still resolved.
171
- assertEquals(typeof rows[0].acknowledged_at, "string");
172
- void firstStamp;
173
- });
@@ -1,94 +0,0 @@
1
- // Tests for the POST /app/api/actions/acknowledge-pr operation `acknowledgePr` (issue #641).
2
- // The nwf UI's "Dismiss" (Done ✓) affordance for a TERMINAL pull request. It stamps `acknowledged_at`,
3
- // which the `pull_requests_read_model` VIEW (094) derives into `list_bucket` = 'history' and `ack_open`
4
- // = 0, dropping the finished PR from the Active convergence list into History. Unlike acknowledge-
5
- // blocked it completes NO user task (a terminal PR is not parked). The PR twin of acknowledge-done /
6
- // acknowledge-epic.
7
- //
8
- // The op's only write is the `acknowledged_at` (+ `updated_at`) stamp, gated on the base `status`
9
- // belonging to the terminal tier (`PR_TERMINAL_STATUSES`). These tests seed an in-memory `pull_requests`
10
- // store and assert the 400/404/409/200 gate + the stamp; the bucket derivation itself is proven in
11
- // app/pullRequestReadModel.test.ts.
12
- import { test } from "node:test";
13
- import { assertEquals } from "#test-assert";
14
- import type { AppApi } from "@nanobpm/urban";
15
- import { noopLog } from "../test/log.ts";
16
- import { PR_TERMINAL_STATUSES } from "../app/pullRequestReadModel.ts";
17
- import handler from "./acknowledgePr.ts";
18
-
19
- // biome-ignore lint/suspicious/noExplicitAny: test-only fake data layer over dynamic row shapes.
20
- function memApp(seed: any[]): { app: AppApi; rows: any[] } {
21
- // biome-ignore lint/suspicious/noExplicitAny: test-only store.
22
- const stores: Record<string, any[]> = { pull_requests: seed };
23
- function tbl(name: string, pk = "id") {
24
- // biome-ignore lint/suspicious/noExplicitAny: test-only.
25
- const rows = (stores[name] ??= [] as any[]);
26
- return {
27
- // biome-ignore lint/suspicious/noExplicitAny: test-only.
28
- async get(id: any) {
29
- return rows.find((r) => r[pk] === id);
30
- },
31
- // biome-ignore lint/suspicious/noExplicitAny: test-only.
32
- async update(id: any, patch: any) {
33
- const r = rows.find((row) => row[pk] === id);
34
- if (r) Object.assign(r, patch);
35
- return r ? 1 : 0;
36
- },
37
- };
38
- }
39
- const app = {
40
- data: { table: (n: string, pk?: string) => tbl(n, pk) },
41
- log: noopLog(),
42
- } as unknown as AppApi;
43
- return { app, rows: stores.pull_requests };
44
- }
45
-
46
- async function call(app: AppApi, body: unknown) {
47
- // biome-ignore lint/suspicious/noExplicitAny: test-only op invocation.
48
- return (await handler({ req: {} as any, params: {}, query: {}, body } as any, app)) as any;
49
- }
50
-
51
- test("acknowledge-pr: stamps acknowledged_at on a terminal PR and returns 200", async () => {
52
- const { app, rows } = memApp([{ pr_key: "o/r#1", status: "merged", acknowledged_at: null }]);
53
- const res = await call(app, { pr_key: "o/r#1" });
54
- assertEquals(res.status, 200);
55
- assertEquals(res.body.ok, true);
56
- assertEquals(typeof rows[0].acknowledged_at, "string");
57
- assertEquals(typeof rows[0].updated_at, "string");
58
- });
59
-
60
- test("acknowledge-pr: every terminal status is dismissable", async () => {
61
- for (const status of PR_TERMINAL_STATUSES) {
62
- const { app, rows } = memApp([{ pr_key: "o/r#1", status, acknowledged_at: null }]);
63
- const res = await call(app, { pr_key: "o/r#1" });
64
- assertEquals(res.status, 200, `status ${status} must be dismissable`);
65
- assertEquals(typeof rows[0].acknowledged_at, "string");
66
- }
67
- });
68
-
69
- test("acknowledge-pr: a live (still-converging) PR is rejected (409) and stays unstamped", async () => {
70
- const { app, rows } = memApp([{ pr_key: "o/r#2", status: "converging", acknowledged_at: null }]);
71
- const res = await call(app, { pr_key: "o/r#2" });
72
- assertEquals(res.status, 409);
73
- assertEquals(res.body.ok, false);
74
- assertEquals(rows[0].acknowledged_at, null);
75
- });
76
-
77
- test("acknowledge-pr: a missing pr_key → 400", async () => {
78
- const { app } = memApp([]);
79
- assertEquals((await call(app, {})).status, 400);
80
- assertEquals((await call(app, { pr_key: " " })).status, 400);
81
- });
82
-
83
- test("acknowledge-pr: no matching PR → 404", async () => {
84
- const { app } = memApp([]);
85
- assertEquals((await call(app, { pr_key: "o/r#404" })).status, 404);
86
- });
87
-
88
- test("acknowledge-pr: idempotent — re-acknowledging a terminal PR re-stamps and returns 200", async () => {
89
- const { app, rows } = memApp([{ pr_key: "o/r#5", status: "converged", acknowledged_at: null }]);
90
- assertEquals((await call(app, { pr_key: "o/r#5" })).status, 200);
91
- assertEquals(typeof rows[0].acknowledged_at, "string");
92
- assertEquals((await call(app, { pr_key: "o/r#5" })).status, 200);
93
- assertEquals(typeof rows[0].acknowledged_at, "string");
94
- });