@nanobpm/nano-workforce 0.167.2 → 0.167.4

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,139 +0,0 @@
1
- // Tests for the POST /app/api/actions/acknowledge-pr operation `acknowledgePr` (issue #641, #652).
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 on the base `pull_requests` row,
9
- // gated on the TERMINAL tier (`PR_TERMINAL_STATUSES`) — but read off the `pull_requests_read_model`
10
- // VIEW's FOLDED/effective `status` (issue #652), the SAME source of truth the Dismiss affordance
11
- // (`ack_open`) derives from, NOT the frozen base column. These tests seed an in-memory read model +
12
- // base store and assert the 400/404/409/200 gate + the stamp; the bucket derivation itself is proven
13
- // in app/pullRequestReadModel.test.ts.
14
- import { test } from "node:test";
15
- import { assertEquals } from "#test-assert";
16
- import type { AppApi } from "@nanobpm/urban";
17
- import { noopLog } from "../test/log.ts";
18
- import { PR_TERMINAL_STATUSES, PULL_REQUEST_READ_MODEL_NAME } from "../app/pullRequestReadModel.ts";
19
- import handler from "./acknowledgePr.ts";
20
-
21
- // A read model row (folded/effective `status` the op gates on) and its base `pull_requests` row (the
22
- // stamp target). `derivedStatus` folds over `status` exactly as `COALESCE(pr.derived_status, pr.status)`
23
- // does in the 094 VIEW, so a seed can model an out-of-band-terminated PR (base `escalated`, folded
24
- // `abandoned`) — the #652 drift case.
25
- type Seed = { prKey: string; status: string; derivedStatus?: string; acknowledgedAt?: string | null };
26
-
27
- // biome-ignore lint/suspicious/noExplicitAny: test-only fake data layer over dynamic row shapes.
28
- function memApp(seeds: Seed[]): { app: AppApi; base: any[] } {
29
- // biome-ignore lint/suspicious/noExplicitAny: test-only store.
30
- const base: any[] = seeds.map((s) => ({
31
- pr_key: s.prKey,
32
- status: s.status,
33
- acknowledged_at: s.acknowledgedAt ?? null,
34
- }));
35
- // The read model VIEW exposes the folded effective status as `status` (COALESCE(derived_status, status)).
36
- // biome-ignore lint/suspicious/noExplicitAny: test-only store.
37
- const readModel: any[] = seeds.map((s) => ({
38
- pr_key: s.prKey,
39
- status: s.derivedStatus ?? s.status,
40
- }));
41
- // biome-ignore lint/suspicious/noExplicitAny: test-only store.
42
- const stores: Record<string, any[]> = { pull_requests: base, [PULL_REQUEST_READ_MODEL_NAME]: readModel };
43
- function tbl(name: string, pk = "id") {
44
- // biome-ignore lint/suspicious/noExplicitAny: test-only.
45
- const rows = (stores[name] ??= [] as any[]);
46
- return {
47
- // biome-ignore lint/suspicious/noExplicitAny: test-only.
48
- async get(id: any) {
49
- return rows.find((r) => r[pk] === id);
50
- },
51
- // biome-ignore lint/suspicious/noExplicitAny: test-only.
52
- async update(id: any, patch: any) {
53
- const r = rows.find((row) => row[pk] === id);
54
- if (r) Object.assign(r, patch);
55
- return r ? 1 : 0;
56
- },
57
- };
58
- }
59
- const app = {
60
- data: { table: (n: string, pk?: string) => tbl(n, pk) },
61
- log: noopLog(),
62
- } as unknown as AppApi;
63
- return { app, base };
64
- }
65
-
66
- async function call(app: AppApi, body: unknown) {
67
- // biome-ignore lint/suspicious/noExplicitAny: test-only op invocation.
68
- return (await handler({ req: {} as any, params: {}, query: {}, body } as any, app)) as any;
69
- }
70
-
71
- test("acknowledge-pr: stamps acknowledged_at on a terminal PR and returns 200", async () => {
72
- const { app, base } = memApp([{ prKey: "o/r#1", status: "merged" }]);
73
- const res = await call(app, { pr_key: "o/r#1" });
74
- assertEquals(res.status, 200);
75
- assertEquals(res.body.ok, true);
76
- assertEquals(typeof base[0].acknowledged_at, "string");
77
- assertEquals(typeof base[0].updated_at, "string");
78
- });
79
-
80
- test("acknowledge-pr: every terminal status is dismissable", async () => {
81
- for (const status of PR_TERMINAL_STATUSES) {
82
- const { app, base } = memApp([{ prKey: "o/r#1", status }]);
83
- const res = await call(app, { pr_key: "o/r#1" });
84
- assertEquals(res.status, 200, `status ${status} must be dismissable`);
85
- assertEquals(typeof base[0].acknowledged_at, "string");
86
- }
87
- });
88
-
89
- // Issue #652 — the drift case. A PR terminated OUT OF BAND leaves base `status` frozen at its last
90
- // transient (`escalated`) while the tracking VIEW folds `derived_status` to `abandoned`, so the read
91
- // model reports the effective `status = abandoned` and lights `ack_open`. Gating on the base column
92
- // (the old bug) 409'd "pull request is not terminal" on exactly the row the UI shows Dismiss on;
93
- // gating on the folded read-model status accepts it.
94
- test("acknowledge-pr: an out-of-band-terminated PR (base escalated, folded abandoned) is dismissable (200)", async () => {
95
- const { app, base } = memApp([{ prKey: "o/r#1716", status: "escalated", derivedStatus: "abandoned" }]);
96
- const res = await call(app, { pr_key: "o/r#1716" });
97
- assertEquals(res.status, 200);
98
- assertEquals(res.body.ok, true);
99
- assertEquals(typeof base[0].acknowledged_at, "string");
100
- // The base row's frozen transient is NOT rewritten — only the acknowledgement is stamped; the VIEW
101
- // folds it to History (list_bucket = 'history', ack_open = 0) off derived_status + acknowledged_at.
102
- assertEquals(base[0].status, "escalated");
103
- });
104
-
105
- test("acknowledge-pr: a live (still-converging) PR is rejected (409) and stays unstamped", async () => {
106
- const { app, base } = memApp([{ prKey: "o/r#2", status: "converging" }]);
107
- const res = await call(app, { pr_key: "o/r#2" });
108
- assertEquals(res.status, 409);
109
- assertEquals(res.body.ok, false);
110
- assertEquals(base[0].acknowledged_at, null);
111
- });
112
-
113
- // A genuinely live PR whose engine instance is still ACTIVE has `derived_status === status` (a live PR
114
- // has no terminal fold), so the read model reports the live transient — still not terminal → 409.
115
- test("acknowledge-pr: a live PR awaiting review (instance active, folded == base) is rejected (409)", async () => {
116
- const { app, base } = memApp([{ prKey: "o/r#3", status: "waiting_review", derivedStatus: "waiting_review" }]);
117
- const res = await call(app, { pr_key: "o/r#3" });
118
- assertEquals(res.status, 409);
119
- assertEquals(base[0].acknowledged_at, null);
120
- });
121
-
122
- test("acknowledge-pr: a missing pr_key → 400", async () => {
123
- const { app } = memApp([]);
124
- assertEquals((await call(app, {})).status, 400);
125
- assertEquals((await call(app, { pr_key: " " })).status, 400);
126
- });
127
-
128
- test("acknowledge-pr: no matching PR → 404", async () => {
129
- const { app } = memApp([]);
130
- assertEquals((await call(app, { pr_key: "o/r#404" })).status, 404);
131
- });
132
-
133
- test("acknowledge-pr: idempotent — re-acknowledging a terminal PR re-stamps and returns 200", async () => {
134
- const { app, base } = memApp([{ prKey: "o/r#5", status: "converged" }]);
135
- assertEquals((await call(app, { pr_key: "o/r#5" })).status, 200);
136
- assertEquals(typeof base[0].acknowledged_at, "string");
137
- assertEquals((await call(app, { pr_key: "o/r#5" })).status, 200);
138
- assertEquals(typeof base[0].acknowledged_at, "string");
139
- });