@nanobpm/nano-workforce 0.122.0 → 0.123.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.
@@ -1,112 +0,0 @@
1
- // Tests for the `plans` gateway's write-time epic-bucket projection (issue #298) and its one-shot
2
- // backfill. The gateway wraps the plain table so EVERY writer — startPlan, the record workers, the
3
- // acknowledge-epic op — automatically gets a fresh `list_bucket`/`ack_open` projection without
4
- // passing them: the single write path is the only place `deriveEpicBucket` / `epicIsAcknowledgeable`
5
- // are applied. Mirrors app/featureGateway.test.ts.
6
- //
7
- // Since epic #412 retired the stored `plans.delivery` column, the gateway projects with `delivery`
8
- // treated as UNKNOWN (null): `list_bucket` is provably identical to the delivery-aware value for
9
- // every reachable state, and `ack_open` becomes a *candidate* (any unacknowledged `done` epic) that
10
- // the `pollPlanBucket` read-model pass (implemented in app/service.ts, tested in app/delivery.test.ts)
11
- // corrects with the read-time signal.
12
- import { test } from "node:test";
13
- import { assert, assertEquals } from "#test-assert";
14
- import type { DataLayer } from "@nanobpm/urban";
15
- import { backfillPlanBuckets, plans } from "./plan.ts";
16
-
17
- // In-memory record gateway with the same semantics the real Table exposes. The `plans` proxy wraps
18
- // whatever data.table returns, so this exercises the real proxy.
19
- function memData(): { data: DataLayer; rows: any[] } {
20
- const rows: any[] = [];
21
- function tbl(_name: string, pk = "id") {
22
- const match = (r: any, where: any) => Object.entries(where).every(([k, v]) => r[k] === v);
23
- return {
24
- async all() {
25
- return rows.slice();
26
- },
27
- async get(id: any) {
28
- return rows.find((r) => r[pk] === id);
29
- },
30
- async find(where: any = {}) {
31
- return rows.filter((r) => match(r, where));
32
- },
33
- async insert(row: any) {
34
- rows.push({ ...row });
35
- return row[pk];
36
- },
37
- async update(id: any, patch: any) {
38
- const r = rows.find((row) => row[pk] === id);
39
- if (r) Object.assign(r, patch);
40
- return r ? 1 : 0;
41
- },
42
- };
43
- }
44
- const data = { table: (n: string, pk?: string) => tbl(n, pk) } as any as DataLayer;
45
- return { data, rows };
46
- }
47
-
48
- test("insert projects list_bucket/ack_open from status without the caller passing them", async () => {
49
- const { data, rows } = memData();
50
- await plans(data).insert({ plan_key: "o/r#1", status: "planning" });
51
- assertEquals(rows[0].list_bucket, "active");
52
- assertEquals(rows[0].ack_open, 0);
53
- });
54
-
55
- test("status flip to done keeps the epic Active and marks a Dismiss candidate (delivery-free gateway)", async () => {
56
- const { data, rows } = memData();
57
- rows.push({ plan_key: "o/r#2", status: "dispatched" });
58
- // record-results marks the epic done. The delivery-free gateway keeps it Active and — not seeing
59
- // the read-time delivery signal — marks it a Dismiss candidate (ack_open=1); `pollPlanBucket`
60
- // clears ack_open while the epic is still converging.
61
- await plans(data).update("o/r#2", { status: "done" });
62
- assertEquals(rows[0].list_bucket, "active");
63
- assertEquals(rows[0].ack_open, 1);
64
- });
65
-
66
- test("acknowledging a done epic flips it to History and closes the Dismiss affordance", async () => {
67
- const { data, rows } = memData();
68
- await plans(data).insert({ plan_key: "o/r#3", status: "done" });
69
- assertEquals(rows[0].list_bucket, "active");
70
- assertEquals(rows[0].ack_open, 1);
71
- // Operator dismisses → gateway reprojects to History, ack_open closes.
72
- await plans(data).update("o/r#3", { acknowledged_at: "2024-01-01T00:00:00Z" });
73
- assertEquals(rows[0].list_bucket, "history");
74
- assertEquals(rows[0].ack_open, 0);
75
- });
76
-
77
- test("a projection-irrelevant patch (epic_phase only) does not disturb the stored bucket", async () => {
78
- const { data, rows } = memData();
79
- rows.push({ plan_key: "o/r#4", status: "done", list_bucket: "active", ack_open: 1 });
80
- await plans(data).update("o/r#4", { epic_phase: "Finalizing" });
81
- assertEquals(rows[0].list_bucket, "active");
82
- assertEquals(rows[0].ack_open, 1);
83
- });
84
-
85
- test("a direct write to list_bucket is overridden by the canonical derivation (no bypass)", async () => {
86
- const { data, rows } = memData();
87
- rows.push({ plan_key: "o/r#5", status: "done" });
88
- // A caller tries to force History; the gateway re-derives from status (delivery-free) and overrides it.
89
- await plans(data).update("o/r#5", { list_bucket: "history" });
90
- assertEquals(rows[0].list_bucket, "active");
91
- });
92
-
93
- test("backfillPlanBuckets stamps only legacy (NULL list_bucket) rows, idempotently", async () => {
94
- const { data, rows } = memData();
95
- rows.push({ plan_key: "o/r#legacy", status: "done", list_bucket: null, ack_open: null });
96
- rows.push({ plan_key: "o/r#fresh", status: "planning", list_bucket: "active", ack_open: 0 });
97
- const stamped = await backfillPlanBuckets(data);
98
- assertEquals(stamped, 1);
99
- assertEquals(rows[0].list_bucket, "active");
100
- // Delivery-free gateway marks the done epic a Dismiss candidate; pollPlanBucket corrects it later.
101
- assertEquals(rows[0].ack_open, 1);
102
- // Second pass is a no-op: every row is now projected.
103
- assertEquals(await backfillPlanBuckets(data), 0);
104
- });
105
-
106
- test("terminal failed epic buckets to History", async () => {
107
- const { data, rows } = memData();
108
- rows.push({ plan_key: "o/r#6", status: "dispatched" });
109
- await plans(data).update("o/r#6", { status: "failed" });
110
- assertEquals(rows[0].list_bucket, "history");
111
- assert(!rows[0].ack_open);
112
- });