@nanobpm/nano-workforce 0.35.2 → 0.37.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.
Files changed (75) hide show
  1. package/.github/workflows/ci.yml +10 -17
  2. package/AGENTS.md +8 -7
  3. package/CHANGELOG.md +14 -0
  4. package/README.md +1 -21
  5. package/actions/abandon.test.ts +8 -16
  6. package/actions/blackboard.test.ts +13 -21
  7. package/actions/blackboard.ts +1 -0
  8. package/actions/feature-answer-hook.ts +1 -0
  9. package/actions/plan-hook.ts +2 -1
  10. package/actions/webhook-submit.ts +2 -1
  11. package/app/abandon.test.ts +10 -15
  12. package/app/baseGuard.test.ts +7 -6
  13. package/app/blackboard.test.ts +22 -32
  14. package/app/blackboard.ts +7 -6
  15. package/app/ensure-pr.test.ts +10 -12
  16. package/app/github.test.ts +9 -8
  17. package/app/github.ts +24 -22
  18. package/app/instance-tracking.test.ts +8 -6
  19. package/app/mergeExclusion.test.ts +11 -20
  20. package/app/mergeExclusion.ts +8 -3
  21. package/app/mergeProtocol.test.ts +14 -13
  22. package/app/mergeProtocol.ts +1 -0
  23. package/app/mergeRebaseArm.test.ts +8 -6
  24. package/app/mergeTrain.test.ts +10 -9
  25. package/app/persist-escalation.test.ts +9 -30
  26. package/app/persist-round.test.ts +6 -19
  27. package/app/plan.test.ts +19 -42
  28. package/app/plan.ts +1 -1
  29. package/app/record-plan-review.test.ts +8 -7
  30. package/app/retro.test.ts +24 -48
  31. package/app/retro.ts +4 -2
  32. package/app/reviewWait.test.ts +12 -11
  33. package/app/rounds.test.ts +13 -12
  34. package/app/service.test.ts +14 -27
  35. package/app/service.ts +8 -4
  36. package/app/taskDelta.test.ts +8 -17
  37. package/app/taskDelta.ts +1 -0
  38. package/app/trialMerge.test.ts +4 -3
  39. package/app/version.ts +9 -21
  40. package/app/waves.test.ts +17 -16
  41. package/biome.json +143 -0
  42. package/operations/getVersion.test.ts +8 -12
  43. package/operations/getVersion.ts +2 -3
  44. package/operations/listActivePrs.test.ts +8 -14
  45. package/operations/listActivePrs.ts +2 -3
  46. package/operations/postMessage.ts +3 -3
  47. package/operations/startAndMessage.test.ts +6 -12
  48. package/package.json +9 -4
  49. package/plugins/no-unsafe-type-assertion.grit +12 -0
  50. package/scripts/check-agent-prompts.test.ts +15 -11
  51. package/scripts/check-agent-prompts.ts +4 -2
  52. package/scripts/layout-bpmn.ts +6 -26
  53. package/scripts/pages-contract.test.ts +14 -13
  54. package/scripts/purge-db.ts +3 -2
  55. package/scripts/upgrade-from-pack.ts +9 -5
  56. package/test/assert.ts +74 -0
  57. package/tsconfig.json +4 -1
  58. package/workers/finalize/worker.ts +2 -1
  59. package/workers/merge/worker.ts +3 -3
  60. package/workers/persist-escalation/worker.ts +2 -1
  61. package/workers/persist-round/worker.ts +2 -1
  62. package/workers/record-plan-review/worker.test.ts +6 -9
  63. package/workers/record-plan-review/worker.ts +2 -1
  64. package/workers/record-results/worker.test.ts +5 -8
  65. package/workers/record-results/worker.ts +2 -1
  66. package/workers/record-trial-merge/worker.test.ts +7 -18
  67. package/workers/record-trial-merge/worker.ts +6 -6
  68. package/workers/record-wave/worker.test.ts +13 -20
  69. package/workers/record-wave/worker.ts +7 -8
  70. package/workers/retro-gather/worker.test.ts +4 -17
  71. package/workers/retro-record/worker.test.ts +8 -27
  72. package/workers/retro-record/worker.ts +3 -3
  73. package/workers/select-wave/worker.test.ts +5 -8
  74. package/deno.json +0 -24
  75. package/deno.lock +0 -1776
@@ -6,7 +6,8 @@
6
6
  // PR in `waiting_review` so the deterministic poller (app/service.ts) starts soliciting a review.
7
7
  // A `waiting` round is what replaced the old failure mode where an agent with nothing to do
8
8
  // re-requested the review destructively and escalated `blocked`.
9
- import { assertEquals } from "jsr:@std/assert@1";
9
+ import { test } from "node:test";
10
+ import { assertEquals } from "#test-assert";
10
11
  import handler from "../workers/persist-round/worker.ts";
11
12
 
12
13
  function fakeApp() {
@@ -18,19 +19,15 @@ function fakeApp() {
18
19
  table(name: string, _key: string) {
19
20
  const store = (rows[name] ??= new Map());
20
21
  return {
21
- // deno-lint-ignore require-await
22
22
  async get(key: string) {
23
23
  return store.get(key);
24
24
  },
25
- // deno-lint-ignore require-await
26
25
  async insert(row: unknown) {
27
26
  (inserts[name] ??= []).push(row);
28
27
  const pk = name === "rounds" ? "id" : "pr_key";
29
- // deno-lint-ignore no-explicit-any
30
28
  store.set((row as any)[pk], row);
31
29
  return 1;
32
30
  },
33
- // deno-lint-ignore require-await
34
31
  async update(key: string, patch: unknown) {
35
32
  (updates[name] ??= []).push({ key, patch });
36
33
  },
@@ -42,20 +39,17 @@ function fakeApp() {
42
39
  }
43
40
 
44
41
  for (const status of ["addressed", "waiting"]) {
45
- Deno.test(`persist-round records a '${status}' round and parks the PR in waiting_review`, async () => {
42
+ test(`persist-round records a '${status}' round and parks the PR in waiting_review`, async () => {
46
43
  const { app, inserts, updates } = fakeApp();
47
44
  const job = { variables: { prKey: "o/r#1", round: 1, status, summary: `round was ${status}` } };
48
- // deno-lint-ignore no-explicit-any
49
45
  await handler(job as any, app as any);
50
46
 
51
47
  assertEquals(inserts.rounds.length, 1, "the round is recorded");
52
- // deno-lint-ignore no-explicit-any
53
48
  const round = inserts.rounds[0] as any;
54
49
  assertEquals(round.status, status, "the round carries the agent's status");
55
50
  assertEquals(round.round_no, 1);
56
51
 
57
52
  assertEquals(updates.pull_requests!.length, 1, "the PR is updated once");
58
- // deno-lint-ignore no-explicit-any
59
53
  const patch = (updates.pull_requests![0] as any).patch;
60
54
  assertEquals(patch.status, "waiting_review", "the PR parks in waiting_review for the poller");
61
55
  assertEquals(patch.current_round, 1);
@@ -65,7 +59,7 @@ for (const status of ["addressed", "waiting"]) {
65
59
  // When the convergence-loop passes repo/prNumber and the FK parent is missing (engine/app.db
66
60
  // desync), persist-round reconstructs the `pull_requests` row before recording the round so the
67
61
  // insert never dies with an opaque FOREIGN KEY constraint failure.
68
- Deno.test("persist-round heals a missing pull_requests parent before recording the round", async () => {
62
+ test("persist-round heals a missing pull_requests parent before recording the round", async () => {
69
63
  const { app, inserts, updates } = fakeApp();
70
64
  const job = {
71
65
  variables: {
@@ -76,38 +70,33 @@ Deno.test("persist-round heals a missing pull_requests parent before recording t
76
70
  prNumber: 7,
77
71
  },
78
72
  };
79
- // deno-lint-ignore no-explicit-any
80
73
  await handler(job as any, app as any);
81
74
 
82
75
  assertEquals(inserts.pull_requests?.length, 1, "the missing parent is reconstructed");
83
76
  // Assert against the reconstruction insert payload (ensurePr) rather than the stored row: the
84
77
  // fake update() doesn't apply patches, so the row would otherwise still read the insert's
85
78
  // "converging" status and mask the worker's real final state.
86
- // deno-lint-ignore no-explicit-any
87
79
  const healed = inserts.pull_requests![0] as any;
88
80
  assertEquals(healed.status, "converging", "the healed parent starts in the converging aggregate");
89
81
  assertEquals(healed.url, "https://github.com/o/r/pull/7", "URL is derived canonically");
90
82
  assertEquals(inserts.rounds.length, 1, "the round is still recorded after the heal");
91
83
  // And the worker still parks the (now-present) PR in waiting_review as its final state.
92
84
  assertEquals(updates.pull_requests!.length, 1, "the PR is updated once after the heal");
93
- // deno-lint-ignore no-explicit-any
94
85
  assertEquals((updates.pull_requests![0] as any).patch.status, "waiting_review");
95
86
  });
96
87
 
97
88
  // rather than writing a NULL status — the round history stays readable.
98
- Deno.test("persist-round defaults a missing status to 'addressed'", async () => {
89
+ test("persist-round defaults a missing status to 'addressed'", async () => {
99
90
  const { app, inserts } = fakeApp();
100
91
  const job = { variables: { prKey: "o/r#1", round: 2 } };
101
- // deno-lint-ignore no-explicit-any
102
92
  await handler(job as any, app as any);
103
- // deno-lint-ignore no-explicit-any
104
93
  assertEquals((inserts.rounds[0] as any).status, "addressed");
105
94
  });
106
95
 
107
96
  // When repo/prNumber process variables are absent (an older in-flight instance, or a regression)
108
97
  // the heal still runs by parsing the canonical `owner/repo#N` prKey, so the FK-child insert is
109
98
  // never left unguarded.
110
- Deno.test("persist-round heals from the prKey when repo/prNumber are absent", async () => {
99
+ test("persist-round heals from the prKey when repo/prNumber are absent", async () => {
111
100
  const { app, inserts } = fakeApp();
112
101
  const job = {
113
102
  variables: {
@@ -117,10 +106,8 @@ Deno.test("persist-round heals from the prKey when repo/prNumber are absent", as
117
106
  abandonUrl: "https://host/hooks/abandon?token=TOK-en_123",
118
107
  },
119
108
  };
120
- // deno-lint-ignore no-explicit-any
121
109
  await handler(job as any, app as any);
122
110
  assertEquals(inserts.pull_requests?.length, 1, "the parent is reconstructed from the prKey");
123
- // deno-lint-ignore no-explicit-any
124
111
  const healed = inserts.pull_requests![0] as any;
125
112
  assertEquals(healed.repo, "o/r");
126
113
  assertEquals(healed.number, 12);
package/app/plan.test.ts CHANGED
@@ -4,47 +4,48 @@
4
4
  // `NaN`/`0` (e.g. unset, "", "abc"), the cap check `round + 1 >= cap` would never fire and the
5
5
  // planner could revise forever. `positiveIntEnv` must fall back to the default on any value that
6
6
  // is not a positive integer, so the loop is always bounded.
7
- import { assertEquals } from "jsr:@std/assert@1";
7
+ import { test } from "node:test";
8
+ import { assertEquals } from "#test-assert";
8
9
  import { positiveIntEnv } from "./plan.ts";
9
10
 
10
11
  const KEY = "NANO_PLAN_REVIEW_ROUNDS_TEST";
11
12
 
12
13
  function withEnv(value: string | undefined, run: () => void) {
13
- const had = Object.prototype.hasOwnProperty.call(Deno.env.toObject(), KEY);
14
- const prev = Deno.env.get(KEY);
14
+ const had = Object.prototype.hasOwnProperty.call(process.env, KEY);
15
+ const prev = process.env[KEY];
15
16
  try {
16
- if (value === undefined) Deno.env.delete(KEY);
17
- else Deno.env.set(KEY, value);
17
+ if (value === undefined) delete process.env[KEY];
18
+ else process.env[KEY] = value;
18
19
  run();
19
20
  } finally {
20
- if (had && prev !== undefined) Deno.env.set(KEY, prev);
21
- else Deno.env.delete(KEY);
21
+ if (had && prev !== undefined) process.env[KEY] = prev;
22
+ else delete process.env[KEY];
22
23
  }
23
24
  }
24
25
 
25
- Deno.test("unset → fallback (bounded loop, never NaN)", () => {
26
+ test("unset → fallback (bounded loop, never NaN)", () => {
26
27
  withEnv(undefined, () => assertEquals(positiveIntEnv(KEY, 3), 3));
27
28
  });
28
29
 
29
- Deno.test("blank/whitespace → fallback, not 0", () => {
30
+ test("blank/whitespace → fallback, not 0", () => {
30
31
  withEnv("", () => assertEquals(positiveIntEnv(KEY, 3), 3));
31
32
  withEnv(" ", () => assertEquals(positiveIntEnv(KEY, 3), 3));
32
33
  });
33
34
 
34
- Deno.test("non-numeric → fallback, not NaN", () => {
35
+ test("non-numeric → fallback, not NaN", () => {
35
36
  withEnv("abc", () => assertEquals(positiveIntEnv(KEY, 3), 3));
36
37
  });
37
38
 
38
- Deno.test("zero and negatives → fallback (cap must be >= 1)", () => {
39
+ test("zero and negatives → fallback (cap must be >= 1)", () => {
39
40
  withEnv("0", () => assertEquals(positiveIntEnv(KEY, 3), 3));
40
41
  withEnv("-2", () => assertEquals(positiveIntEnv(KEY, 3), 3));
41
42
  });
42
43
 
43
- Deno.test("non-integer → fallback", () => {
44
+ test("non-integer → fallback", () => {
44
45
  withEnv("2.5", () => assertEquals(positiveIntEnv(KEY, 3), 3));
45
46
  });
46
47
 
47
- Deno.test("valid positive integer → honoured", () => {
48
+ test("valid positive integer → honoured", () => {
48
49
  withEnv("5", () => assertEquals(positiveIntEnv(KEY, 3), 5));
49
50
  withEnv("1", () => assertEquals(positiveIntEnv(KEY, 3), 1));
50
51
  });
@@ -58,28 +59,22 @@ Deno.test("valid positive integer → honoured", () => {
58
59
  // asserts the `plan_reviews` rows for the plan key are gone after a re-plan.
59
60
  import { startPlan } from "./plan.ts";
60
61
 
61
- // deno-lint-ignore no-explicit-any
62
62
  function memTable(rows: any[], key: string) {
63
63
  return {
64
- // deno-lint-ignore no-explicit-any
65
64
  get: (k: any) => Promise.resolve(rows.find((r) => r[key] === k) ?? null),
66
- // deno-lint-ignore no-explicit-any
67
65
  find: (q: any) =>
68
66
  Promise.resolve(
69
67
  rows.filter((r) => Object.entries(q).every(([f, v]) => r[f] === v)),
70
68
  ),
71
- // deno-lint-ignore no-explicit-any
72
69
  insert: (r: any) => {
73
70
  rows.push(r);
74
71
  return Promise.resolve(r);
75
72
  },
76
- // deno-lint-ignore no-explicit-any
77
73
  update: (k: any, patch: any) => {
78
74
  const r = rows.find((x) => x[key] === k);
79
75
  if (r) Object.assign(r, patch);
80
76
  return Promise.resolve(r);
81
77
  },
82
- // deno-lint-ignore no-explicit-any
83
78
  delete: (k: any) => {
84
79
  for (let i = rows.length - 1; i >= 0; i--) {
85
80
  if (rows[i][key] === k) rows.splice(i, 1);
@@ -89,7 +84,7 @@ function memTable(rows: any[], key: string) {
89
84
  };
90
85
  }
91
86
 
92
- Deno.test("re-plan of a finished issue clears stale plan_reviews rows", async () => {
87
+ test("re-plan of a finished issue clears stale plan_reviews rows", async () => {
93
88
  const PLAN_KEY = "owner/repo#7";
94
89
  const stores: Record<string, { rows: unknown[]; key: string }> = {
95
90
  plans: {
@@ -112,11 +107,9 @@ Deno.test("re-plan of a finished issue clears stale plan_reviews rows", async ()
112
107
  const data = {
113
108
  table: (name: string, key: string) =>
114
109
  memTable(stores[name]?.rows ?? [], stores[name]?.key ?? key),
115
- // deno-lint-ignore no-explicit-any
116
110
  } as any;
117
111
  const engine = {
118
112
  createInstance: () => Promise.resolve({ processInstanceKey: "PI-1" }),
119
- // deno-lint-ignore no-explicit-any
120
113
  } as any;
121
114
 
122
115
  await startPlan(data, engine, {
@@ -139,7 +132,7 @@ Deno.test("re-plan of a finished issue clears stale plan_reviews rows", async ()
139
132
  // `refreshOpenTaskEscalation` re-surfaces a dead question in the answer form — the same
140
133
  // stale-row class as `plan_reviews` above. This drives `startPlan` against the in-memory data
141
134
  // layer and asserts both the escalation rows and the denormalised pointer are cleared.
142
- Deno.test("re-plan of a finished issue clears stale open escalations and the denormalised open_task_* pointer", async () => {
135
+ test("re-plan of a finished issue clears stale open escalations and the denormalised open_task_* pointer", async () => {
143
136
  const PLAN_KEY = "owner/repo#8";
144
137
  const stores: Record<string, { rows: unknown[]; key: string }> = {
145
138
  plans: {
@@ -172,11 +165,9 @@ Deno.test("re-plan of a finished issue clears stale open escalations and the den
172
165
  const data = {
173
166
  table: (name: string, key: string) =>
174
167
  memTable(stores[name]?.rows ?? [], stores[name]?.key ?? key),
175
- // deno-lint-ignore no-explicit-any
176
168
  } as any;
177
169
  const engine = {
178
170
  createInstance: () => Promise.resolve({ processInstanceKey: "PI-1" }),
179
- // deno-lint-ignore no-explicit-any
180
171
  } as any;
181
172
 
182
173
  await startPlan(data, engine, {
@@ -214,16 +205,14 @@ function escalationStores(rows: unknown[]): Record<string, { rows: unknown[]; ke
214
205
  };
215
206
  }
216
207
 
217
- // deno-lint-ignore no-explicit-any
218
208
  function memData(stores: Record<string, { rows: any[]; key: string }>) {
219
209
  return {
220
210
  table: (name: string, key: string) =>
221
211
  memTable(stores[name]?.rows ?? [], stores[name]?.key ?? key),
222
- // deno-lint-ignore no-explicit-any
223
212
  } as any;
224
213
  }
225
214
 
226
- Deno.test("refreshOpenTaskEscalation surfaces the OLDEST open escalation, then clears when none remain", async () => {
215
+ test("refreshOpenTaskEscalation surfaces the OLDEST open escalation, then clears when none remain", async () => {
227
216
  const stores = escalationStores([
228
217
  { id: 2, plan_key: "owner/repo#9", task_id: "b", corr_key: "owner/repo#9:b", question: "Q-b", status: "open" },
229
218
  { id: 1, plan_key: "owner/repo#9", task_id: "a", corr_key: "owner/repo#9:a", question: "Q-a", status: "open" },
@@ -231,7 +220,6 @@ Deno.test("refreshOpenTaskEscalation surfaces the OLDEST open escalation, then c
231
220
  const data = memData(stores);
232
221
 
233
222
  await refreshOpenTaskEscalation(data, "owner/repo#9");
234
- // deno-lint-ignore no-explicit-any
235
223
  let plan = stores.plans.rows[0] as any;
236
224
  assertEquals(plan.open_task_escalation_id, 1);
237
225
  assertEquals(plan.open_task_question, "Q-a");
@@ -239,19 +227,15 @@ Deno.test("refreshOpenTaskEscalation surfaces the OLDEST open escalation, then c
239
227
  assertEquals(plan.open_task_id, "a");
240
228
 
241
229
  // Once the oldest is answered, the next-oldest is surfaced.
242
- // deno-lint-ignore no-explicit-any
243
230
  (stores.plan_escalations.rows.find((r: any) => r.id === 1) as any).status = "answered";
244
231
  await refreshOpenTaskEscalation(data, "owner/repo#9");
245
- // deno-lint-ignore no-explicit-any
246
232
  plan = stores.plans.rows[0] as any;
247
233
  assertEquals(plan.open_task_escalation_id, 2);
248
234
  assertEquals(plan.open_task_id, "b");
249
235
 
250
236
  // With nothing open the denormalised fields clear.
251
- // deno-lint-ignore no-explicit-any
252
237
  (stores.plan_escalations.rows.find((r: any) => r.id === 2) as any).status = "answered";
253
238
  await refreshOpenTaskEscalation(data, "owner/repo#9");
254
- // deno-lint-ignore no-explicit-any
255
239
  plan = stores.plans.rows[0] as any;
256
240
  assertEquals(plan.open_task_escalation_id, null);
257
241
  assertEquals(plan.open_task_question, null);
@@ -259,7 +243,7 @@ Deno.test("refreshOpenTaskEscalation surfaces the OLDEST open escalation, then c
259
243
  assertEquals(plan.open_task_id, null);
260
244
  });
261
245
 
262
- Deno.test("answerTaskEscalation records the answer, mirrors it onto the task, publishes the resume message, and re-surfaces the next escalation", async () => {
246
+ test("answerTaskEscalation records the answer, mirrors it onto the task, publishes the resume message, and re-surfaces the next escalation", async () => {
263
247
  const stores = escalationStores([
264
248
  { id: 1, plan_key: "owner/repo#9", task_id: "a", corr_key: "owner/repo#9:a", question: "Q-a", status: "open", answer: null },
265
249
  { id: 2, plan_key: "owner/repo#9", task_id: "b", corr_key: "owner/repo#9:b", question: "Q-b", status: "open", answer: null },
@@ -267,15 +251,12 @@ Deno.test("answerTaskEscalation records the answer, mirrors it onto the task, pu
267
251
  stores.plan_tasks.rows.push({ id: 10, plan_key: "owner/repo#9", task_id: "a", answer: null });
268
252
  const data = memData(stores);
269
253
 
270
- // deno-lint-ignore no-explicit-any
271
254
  const published: any[] = [];
272
255
  const engine = {
273
- // deno-lint-ignore no-explicit-any
274
256
  publishMessage: (m: any) => {
275
257
  published.push(m);
276
258
  return Promise.resolve();
277
259
  },
278
- // deno-lint-ignore no-explicit-any
279
260
  } as any;
280
261
 
281
262
  const r = await answerTaskEscalation(data, engine, "owner/repo#9:a", "do it");
@@ -285,13 +266,11 @@ Deno.test("answerTaskEscalation records the answer, mirrors it onto the task, pu
285
266
  assertEquals(r.taskId, "a");
286
267
 
287
268
  // Escalation row marked answered with the recorded answer.
288
- // deno-lint-ignore no-explicit-any
289
269
  const esc = stores.plan_escalations.rows.find((x: any) => x.id === 1) as any;
290
270
  assertEquals(esc.status, "answered");
291
271
  assertEquals(esc.answer, "do it");
292
272
 
293
273
  // Answer mirrored onto the task row.
294
- // deno-lint-ignore no-explicit-any
295
274
  assertEquals((stores.plan_tasks.rows[0] as any).answer, "do it");
296
275
 
297
276
  // Correlated resume message published on the shared constant channel.
@@ -301,16 +280,14 @@ Deno.test("answerTaskEscalation records the answer, mirrors it onto the task, pu
301
280
  assertEquals(published[0].variables.answer, "do it");
302
281
 
303
282
  // Next-oldest open escalation re-surfaced on the plan row.
304
- // deno-lint-ignore no-explicit-any
305
283
  assertEquals((stores.plans.rows[0] as any).open_task_escalation_id, 2);
306
284
  });
307
285
 
308
- Deno.test("answerTaskEscalation is a no-op when no open escalation matches the correlation key", async () => {
286
+ test("answerTaskEscalation is a no-op when no open escalation matches the correlation key", async () => {
309
287
  const stores = escalationStores([]);
310
288
  const data = memData(stores);
311
289
  const engine = {
312
290
  publishMessage: () => Promise.reject(new Error("should not publish")),
313
- // deno-lint-ignore no-explicit-any
314
291
  } as any;
315
292
  const r = await answerTaskEscalation(data, engine, "owner/repo#9:missing", "x");
316
293
  assertEquals(r.ok, false);
package/app/plan.ts CHANGED
@@ -11,8 +11,8 @@
11
11
  // hand-written SQL — matching app/service.ts.
12
12
  import type { DataLayer, EngineClient } from "@nanobpm/urban";
13
13
  import { blackboardUrl, mintBlackboardToken, renderCoordinationBrief } from "./blackboard.ts";
14
- import { clearTaskDeltas } from "./taskDelta.ts";
15
14
  import { clearExclusions } from "./mergeExclusion.ts";
15
+ import { clearTaskDeltas } from "./taskDelta.ts";
16
16
 
17
17
  /** The BPMN process this module drives (resources/processes/plan-fanout.bpmn). */
18
18
  export const PLAN_PROCESS_ID = "plan-fanout";
@@ -4,35 +4,36 @@
4
4
  // re-emitted as `planFindings`. `JSON.stringify` can THROW (BigInt, circular refs) or return
5
5
  // `undefined` (functions/symbols); either would fail the whole job and wedge the process in a
6
6
  // retry loop. `str()` must never throw and must always yield a string.
7
- import { assertEquals } from "jsr:@std/assert@1";
7
+ import { test } from "node:test";
8
+ import { assertEquals } from "#test-assert";
8
9
  import { str } from "../workers/record-plan-review/worker.ts";
9
10
 
10
- Deno.test("strings pass through unchanged", () => {
11
+ test("strings pass through unchanged", () => {
11
12
  assertEquals(str("hello"), "hello");
12
13
  assertEquals(str(""), "");
13
14
  });
14
15
 
15
- Deno.test("null/undefined → empty string", () => {
16
+ test("null/undefined → empty string", () => {
16
17
  assertEquals(str(null), "");
17
18
  assertEquals(str(undefined), "");
18
19
  });
19
20
 
20
- Deno.test("plain objects/arrays → JSON", () => {
21
+ test("plain objects/arrays → JSON", () => {
21
22
  assertEquals(str({ a: 1 }), '{"a":1}');
22
23
  assertEquals(str([1, 2]), "[1,2]");
23
24
  });
24
25
 
25
- Deno.test("BigInt does not throw (JSON.stringify would) → String fallback", () => {
26
+ test("BigInt does not throw (JSON.stringify would) → String fallback", () => {
26
27
  assertEquals(str(10n), "10");
27
28
  });
28
29
 
29
- Deno.test("circular structure does not throw → String fallback", () => {
30
+ test("circular structure does not throw → String fallback", () => {
30
31
  const circular: Record<string, unknown> = {};
31
32
  circular.self = circular;
32
33
  const out = str(circular);
33
34
  assertEquals(typeof out, "string");
34
35
  });
35
36
 
36
- Deno.test("value JSON.stringify renders as undefined → String fallback", () => {
37
+ test("value JSON.stringify renders as undefined → String fallback", () => {
37
38
  assertEquals(str(() => 1), String(() => 1));
38
39
  });