@nanobpm/nano-workforce 0.36.0 → 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 (51) hide show
  1. package/.github/workflows/ci.yml +7 -17
  2. package/AGENTS.md +8 -7
  3. package/CHANGELOG.md +7 -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/app/abandon.test.ts +10 -15
  8. package/app/baseGuard.test.ts +7 -6
  9. package/app/blackboard.test.ts +22 -32
  10. package/app/ensure-pr.test.ts +10 -12
  11. package/app/github.test.ts +9 -8
  12. package/app/github.ts +1 -21
  13. package/app/instance-tracking.test.ts +8 -6
  14. package/app/mergeExclusion.test.ts +11 -20
  15. package/app/mergeProtocol.test.ts +14 -13
  16. package/app/mergeRebaseArm.test.ts +8 -6
  17. package/app/mergeTrain.test.ts +10 -9
  18. package/app/persist-escalation.test.ts +9 -30
  19. package/app/persist-round.test.ts +6 -19
  20. package/app/plan.test.ts +19 -42
  21. package/app/record-plan-review.test.ts +8 -7
  22. package/app/retro.test.ts +24 -48
  23. package/app/reviewWait.test.ts +12 -11
  24. package/app/rounds.test.ts +13 -12
  25. package/app/service.test.ts +14 -27
  26. package/app/taskDelta.test.ts +8 -17
  27. package/app/trialMerge.test.ts +4 -3
  28. package/app/version.ts +6 -21
  29. package/app/waves.test.ts +17 -16
  30. package/operations/getVersion.test.ts +8 -12
  31. package/operations/getVersion.ts +2 -3
  32. package/operations/listActivePrs.test.ts +8 -14
  33. package/operations/listActivePrs.ts +2 -3
  34. package/operations/startAndMessage.test.ts +6 -12
  35. package/package.json +6 -4
  36. package/scripts/check-agent-prompts.test.ts +15 -11
  37. package/scripts/layout-bpmn.ts +6 -28
  38. package/scripts/pages-contract.test.ts +14 -13
  39. package/scripts/purge-db.ts +1 -1
  40. package/scripts/upgrade-from-pack.ts +1 -1
  41. package/test/assert.ts +74 -0
  42. package/tsconfig.json +4 -1
  43. package/workers/record-plan-review/worker.test.ts +6 -9
  44. package/workers/record-results/worker.test.ts +5 -8
  45. package/workers/record-trial-merge/worker.test.ts +7 -18
  46. package/workers/record-wave/worker.test.ts +13 -20
  47. package/workers/retro-gather/worker.test.ts +4 -17
  48. package/workers/retro-record/worker.test.ts +8 -27
  49. package/workers/select-wave/worker.test.ts +5 -8
  50. package/deno.json +0 -24
  51. package/deno.lock +0 -1777
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);
@@ -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
  });
package/app/retro.test.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  // Unit tests for the epic retrospective stage (app/retro.ts, 016_plan_retro.sql).
2
- import { assert, assertEquals, assertStringIncludes } from "jsr:@std/assert@1";
2
+ import { test } from "node:test";
3
+ import { assert, assertEquals, assertStringIncludes } from "#test-assert";
3
4
  import type { DataLayer, EngineClient } from "@nanobpm/urban";
4
5
  import { appendEntry } from "./blackboard.ts";
5
6
  import { recordTaskDelta } from "./taskDelta.ts";
@@ -15,18 +16,13 @@ import {
15
16
  } from "./retro.ts";
16
17
 
17
18
  // In-memory record gateway matching the Table<T> subset retro.ts uses: insert/find/findOne/get/update.
18
- // deno-lint-ignore no-explicit-any
19
19
  function memData(): { data: DataLayer; stores: Record<string, any[]> } {
20
- // deno-lint-ignore no-explicit-any
21
20
  const stores: Record<string, any[]> = {};
22
21
  const seq: Record<string, number> = {};
23
22
  function tbl(name: string, pk = "id") {
24
- // deno-lint-ignore no-explicit-any
25
23
  const rows = (stores[name] ??= [] as any[]);
26
- // deno-lint-ignore no-explicit-any
27
24
  const match = (r: any, where: any) => Object.entries(where).every(([k, v]) => r[k] === v);
28
25
  return {
29
- // deno-lint-ignore no-explicit-any require-await
30
26
  async insert(row: any) {
31
27
  if (pk !== "id" && rows.some((r) => r[pk] === row[pk])) {
32
28
  throw new Error(`UNIQUE constraint failed: ${name}.${pk}`);
@@ -35,26 +31,21 @@ function memData(): { data: DataLayer; stores: Record<string, any[]> } {
35
31
  rows.push(pk === "id" ? { id, ...row } : { ...row });
36
32
  return pk === "id" ? id : row[pk];
37
33
  },
38
- // deno-lint-ignore no-explicit-any require-await
39
34
  async find(where: any = {}) {
40
35
  return rows.filter((r) => match(r, where));
41
36
  },
42
- // deno-lint-ignore no-explicit-any require-await
43
37
  async findOne(where: any = {}) {
44
38
  return rows.find((r) => match(r, where));
45
39
  },
46
- // deno-lint-ignore no-explicit-any require-await
47
40
  async get(id: any) {
48
41
  return rows.find((row) => row[pk] === id);
49
42
  },
50
- // deno-lint-ignore no-explicit-any require-await
51
43
  async update(id: any, patch: any) {
52
44
  const r = rows.find((row) => row[pk] === id);
53
45
  if (r) Object.assign(r, patch);
54
46
  },
55
47
  };
56
48
  }
57
- // deno-lint-ignore no-explicit-any
58
49
  const data = { table: (n: string, pk?: string) => tbl(n, pk) } as any as DataLayer;
59
50
  return { data, stores };
60
51
  }
@@ -62,21 +53,17 @@ function memData(): { data: DataLayer; stores: Record<string, any[]> } {
62
53
  // A fake engine recording createInstance calls.
63
54
  function fakeEngine(): { engine: EngineClient; started: { processDefinitionId: string; variables: Record<string, unknown> }[] } {
64
55
  const started: { processDefinitionId: string; variables: Record<string, unknown> }[] = [];
65
- // deno-lint-ignore no-explicit-any
66
56
  const engine = {
67
- // deno-lint-ignore no-explicit-any require-await
68
57
  async createInstance(req: any) {
69
58
  started.push({ processDefinitionId: req.processDefinitionId, variables: req.variables });
70
59
  return { processInstanceKey: `PI-${started.length}` };
71
60
  },
72
- // deno-lint-ignore no-explicit-any require-await
73
61
  } as any as EngineClient;
74
62
  return { engine, started };
75
63
  }
76
64
 
77
65
  const PLAN = "acme/widgets#7";
78
66
 
79
- // deno-lint-ignore no-explicit-any
80
67
  function seedPlan(stores: Record<string, any[]>, over: Record<string, unknown> = {}) {
81
68
  stores["plans"] = [{
82
69
  plan_key: PLAN,
@@ -89,20 +76,17 @@ function seedPlan(stores: Record<string, any[]>, over: Record<string, unknown> =
89
76
  }];
90
77
  }
91
78
 
92
- // deno-lint-ignore no-explicit-any
93
79
  function seedTask(stores: Record<string, any[]>, task: Record<string, unknown>) {
94
80
  (stores["plan_tasks"] ??= []).push({ plan_key: PLAN, ...task });
95
81
  }
96
- // deno-lint-ignore no-explicit-any
97
82
  function seedPr(stores: Record<string, any[]>, pr_key: string, status: string) {
98
83
  (stores["pull_requests"] ??= []).push({ pr_key, status });
99
84
  }
100
- // deno-lint-ignore no-explicit-any
101
85
  function seedReview(stores: Record<string, any[]>, round: number, approved: number, findings: string | null) {
102
86
  (stores["plan_reviews"] ??= []).push({ plan_key: PLAN, round, approved, findings, created_at: `t${round}`, job_key: null });
103
87
  }
104
88
 
105
- Deno.test("autoRetroEnabled: on by default; disabled by 0/false/off/no", () => {
89
+ test("autoRetroEnabled: on by default; disabled by 0/false/off/no", () => {
106
90
  const prev = process.env.NANO_AUTO_RETRO;
107
91
  try {
108
92
  delete process.env.NANO_AUTO_RETRO;
@@ -119,7 +103,7 @@ Deno.test("autoRetroEnabled: on by default; disabled by 0/false/off/no", () => {
119
103
  }
120
104
  });
121
105
 
122
- Deno.test("planKeyForPr: resolves the plan a PR's task belongs to; undefined when unlinked", async () => {
106
+ test("planKeyForPr: resolves the plan a PR's task belongs to; undefined when unlinked", async () => {
123
107
  const { data, stores } = memData();
124
108
  seedTask(stores, { id: "t1", status: "opened", pr_key: "acme/widgets#10" });
125
109
  assertEquals(await planKeyForPr(data, "acme/widgets#10"), PLAN);
@@ -127,7 +111,7 @@ Deno.test("planKeyForPr: resolves the plan a PR's task belongs to; undefined whe
127
111
  assertEquals(await planKeyForPr(data, ""), undefined);
128
112
  });
129
113
 
130
- Deno.test("isPlanComplete: false while any task is still in flight", async () => {
114
+ test("isPlanComplete: false while any task is still in flight", async () => {
131
115
  const { data, stores } = memData();
132
116
  seedTask(stores, { id: "t1", status: "opened", pr_key: "acme/widgets#10" });
133
117
  seedTask(stores, { id: "t2", status: "pending", pr_key: null });
@@ -136,14 +120,14 @@ Deno.test("isPlanComplete: false while any task is still in flight", async () =>
136
120
  assertEquals(await isPlanComplete(data, PLAN), false);
137
121
  });
138
122
 
139
- Deno.test("isPlanComplete: false when an opened task's PR is not yet terminal", async () => {
123
+ test("isPlanComplete: false when an opened task's PR is not yet terminal", async () => {
140
124
  const { data, stores } = memData();
141
125
  seedTask(stores, { id: "t1", status: "opened", pr_key: "acme/widgets#10" });
142
126
  seedPr(stores, "acme/widgets#10", "waiting_deps"); // in the merge stage, not terminal
143
127
  assertEquals(await isPlanComplete(data, PLAN), false);
144
128
  });
145
129
 
146
- Deno.test("isPlanComplete: true when every task is settled (terminal PR or skipped/blocked)", async () => {
130
+ test("isPlanComplete: true when every task is settled (terminal PR or skipped/blocked)", async () => {
147
131
  const { data, stores } = memData();
148
132
  seedTask(stores, { id: "t1", status: "opened", pr_key: "acme/widgets#10" });
149
133
  seedTask(stores, { id: "t2", status: "skipped", pr_key: null });
@@ -153,12 +137,12 @@ Deno.test("isPlanComplete: true when every task is settled (terminal PR or skipp
153
137
  assertEquals(await isPlanComplete(data, PLAN), true);
154
138
  });
155
139
 
156
- Deno.test("isPlanComplete: an empty plan has nothing to retrospect", async () => {
140
+ test("isPlanComplete: an empty plan has nothing to retrospect", async () => {
157
141
  const { data } = memData();
158
142
  assertEquals(await isPlanComplete(data, PLAN), false);
159
143
  });
160
144
 
161
- Deno.test("gatherRetro: separates learnings from notes and folds in deltas", async () => {
145
+ test("gatherRetro: separates learnings from notes and folds in deltas", async () => {
162
146
  const { data, stores } = memData();
163
147
  seedPlan(stores);
164
148
  await appendEntry(data, PLAN, { author_task: "t1", kind: "learning", body: "regen the API surface before building" });
@@ -181,7 +165,7 @@ Deno.test("gatherRetro: separates learnings from notes and folds in deltas", asy
181
165
  assertEquals(d.repo, "acme/widgets");
182
166
  });
183
167
 
184
- Deno.test("gatherRetro: folds in the plan-review trace and task-outcome shape", async () => {
168
+ test("gatherRetro: folds in the plan-review trace and task-outcome shape", async () => {
185
169
  const { data, stores } = memData();
186
170
  seedPlan(stores);
187
171
  seedTask(stores, { id: "t1", task_id: "t1", status: "opened", pr_key: "acme/widgets#10" });
@@ -201,7 +185,7 @@ Deno.test("gatherRetro: folds in the plan-review trace and task-outcome shape",
201
185
  assertEquals(d.taskOutcomes.byStatus, { opened: 2, skipped: 1 });
202
186
  });
203
187
 
204
- Deno.test("gatherRetro: no reviews → zero rounds, not approved, empty rejections", async () => {
188
+ test("gatherRetro: no reviews → zero rounds, not approved, empty rejections", async () => {
205
189
  const { data, stores } = memData();
206
190
  seedPlan(stores);
207
191
  const d = await gatherRetro(data, PLAN);
@@ -211,7 +195,7 @@ Deno.test("gatherRetro: no reviews → zero rounds, not approved, empty rejectio
211
195
  assertEquals(d.taskOutcomes, { total: 0, byStatus: {} });
212
196
  });
213
197
 
214
- Deno.test("renderRetroBrief: renders learnings + constraints; states 'none' with no learnings", () => {
198
+ test("renderRetroBrief: renders learnings + constraints; states 'none' with no learnings", () => {
215
199
  const empty = renderRetroBrief({
216
200
  planKey: PLAN, repo: "acme/widgets", issueUrl: "", title: null,
217
201
  learnings: [], touchedFiles: [], contractChanges: [], constraints: [], notes: [],
@@ -243,7 +227,7 @@ Deno.test("renderRetroBrief: renders learnings + constraints; states 'none' with
243
227
  assertStringIncludes(brief, "Task outcomes");
244
228
  });
245
229
 
246
- Deno.test("isDigestEmpty: empty only with no learnings/deltas/notes AND no review rejections", () => {
230
+ test("isDigestEmpty: empty only with no learnings/deltas/notes AND no review rejections", () => {
247
231
  const base = { planKey: PLAN, repo: "", issueUrl: "", title: null, learnings: [], touchedFiles: [], contractChanges: [], constraints: [], notes: [], reviewRounds: 0, reviewRejections: [], planApproved: false, taskOutcomes: { total: 0, byStatus: {} } };
248
232
  assert(isDigestEmpty({ ...base, counts: { learnings: 0, deltas: 0, notes: 0 } }));
249
233
  assert(!isDigestEmpty({ ...base, counts: { learnings: 1, deltas: 0, notes: 0 } }));
@@ -255,7 +239,7 @@ Deno.test("isDigestEmpty: empty only with no learnings/deltas/notes AND no revie
255
239
  assert(isDigestEmpty({ ...base, reviewRounds: 1, planApproved: true, taskOutcomes: { total: 3, byStatus: { opened: 3 } }, counts: { learnings: 0, deltas: 0, notes: 0 } }));
256
240
  });
257
241
 
258
- Deno.test("recordRetro: inserts then updates the same plan_key row in place", async () => {
242
+ test("recordRetro: inserts then updates the same plan_key row in place", async () => {
259
243
  const { data, stores } = memData();
260
244
  await recordRetro(data, PLAN, { status: "filed", prKey: "acme/widgets#20", learnings: 3, summary: "promoted 2" });
261
245
  assertEquals(stores["plan_retros"].length, 1);
@@ -268,25 +252,21 @@ Deno.test("recordRetro: inserts then updates the same plan_key row in place", as
268
252
  assertEquals(stores["plan_retros"][0].pr_key, null);
269
253
  });
270
254
 
271
- Deno.test("recordRetro: rethrows a non-unique (FOREIGN KEY) constraint error instead of swallowing it", async () => {
255
+ test("recordRetro: rethrows a non-unique (FOREIGN KEY) constraint error instead of swallowing it", async () => {
272
256
  // A FK failure (e.g. plan_key missing in plans) must NOT be treated as a benign duplicate and
273
257
  // fall through to a silent update — that would make the write look successful while doing nothing.
274
258
  let updated = false;
275
259
  const table = {
276
- // deno-lint-ignore require-await
277
260
  async insert() {
278
261
  throw new Error("FOREIGN KEY constraint failed");
279
262
  },
280
- // deno-lint-ignore require-await
281
263
  async update() {
282
264
  updated = true;
283
265
  },
284
- // deno-lint-ignore require-await
285
266
  async get() {
286
267
  return undefined;
287
268
  },
288
269
  };
289
- // deno-lint-ignore no-explicit-any
290
270
  const data = { table: () => table } as any as DataLayer;
291
271
  let threw = false;
292
272
  try {
@@ -299,7 +279,7 @@ Deno.test("recordRetro: rethrows a non-unique (FOREIGN KEY) constraint error ins
299
279
  assertEquals(updated, false, "must not silently fall back to update on a non-unique error");
300
280
  });
301
281
 
302
- Deno.test("maybeStartRetro: starts the retro exactly once when the last PR lands with material", async () => {
282
+ test("maybeStartRetro: starts the retro exactly once when the last PR lands with material", async () => {
303
283
  const { data, stores } = memData();
304
284
  seedPlan(stores);
305
285
  seedTask(stores, { id: "t1", status: "opened", pr_key: "acme/widgets#10" });
@@ -325,13 +305,12 @@ Deno.test("maybeStartRetro: starts the retro exactly once when the last PR lands
325
305
  assertEquals(started.length, 1, "fire-once guard");
326
306
  });
327
307
 
328
- Deno.test("maybeStartRetro: a createInstance failure records a blocked retro (fire-once guard already consumed)", async () => {
308
+ test("maybeStartRetro: a createInstance failure records a blocked retro (fire-once guard already consumed)", async () => {
329
309
  const { data, stores } = memData();
330
310
  seedPlan(stores);
331
311
  seedTask(stores, { id: "t1", status: "opened", pr_key: "acme/widgets#10" });
332
312
  seedPr(stores, "acme/widgets#10", "merged");
333
313
  await appendEntry(data, PLAN, { author_task: "t1", kind: "learning", body: "regen first" });
334
- // deno-lint-ignore no-explicit-any require-await
335
314
  const engine = { async createInstance() { throw new Error("gateway down"); } } as any as EngineClient;
336
315
 
337
316
  const r = await maybeStartRetro(data, engine, "acme/widgets#10");
@@ -346,19 +325,16 @@ Deno.test("maybeStartRetro: a createInstance failure records a blocked retro (fi
346
325
  assertStringIncludes(String(stores["plan_retros"][0].summary), "gateway down");
347
326
  });
348
327
 
349
- Deno.test("maybeStartRetro: a secondary blocked-retro persistence failure still returns start-failed (not error)", async () => {
328
+ test("maybeStartRetro: a secondary blocked-retro persistence failure still returns start-failed (not error)", async () => {
350
329
  const { data, stores } = memData();
351
330
  seedPlan(stores);
352
331
  seedTask(stores, { id: "t1", status: "opened", pr_key: "acme/widgets#10" });
353
332
  seedPr(stores, "acme/widgets#10", "merged");
354
333
  await appendEntry(data, PLAN, { author_task: "t1", kind: "learning", body: "regen first" });
355
- // deno-lint-ignore no-explicit-any require-await
356
334
  const engine = { async createInstance() { throw new Error("gateway down"); } } as any as EngineClient;
357
335
  // recordRetro rethrows non-unique DB errors; simulate the blocked-retro insert hitting a
358
336
  // FOREIGN KEY failure so the persistence in the createInstance-failure handler throws.
359
- // deno-lint-ignore no-explicit-any
360
337
  const failingData = {
361
- // deno-lint-ignore no-explicit-any
362
338
  table: (name: string, pk?: string) => {
363
339
  const t = (data as any).table(name, pk);
364
340
  if (name !== "plan_retros") return t;
@@ -374,7 +350,7 @@ Deno.test("maybeStartRetro: a secondary blocked-retro persistence failure still
374
350
  assertEquals(stores["plan_retros"].length, 0);
375
351
  });
376
352
 
377
- Deno.test("maybeStartRetro: a pre-claimed retro start does not start a duplicate process", async () => {
353
+ test("maybeStartRetro: a pre-claimed retro start does not start a duplicate process", async () => {
378
354
  const { data, stores } = memData();
379
355
  seedPlan(stores);
380
356
  seedTask(stores, { id: "t1", status: "opened", pr_key: "acme/widgets#10" });
@@ -389,7 +365,7 @@ Deno.test("maybeStartRetro: a pre-claimed retro start does not start a duplicate
389
365
  assertEquals(stores["plans"][0].retro_started_at, null);
390
366
  });
391
367
 
392
- Deno.test("maybeStartRetro: bails while the plan is incomplete", async () => {
368
+ test("maybeStartRetro: bails while the plan is incomplete", async () => {
393
369
  const { data, stores } = memData();
394
370
  seedPlan(stores);
395
371
  seedTask(stores, { id: "t1", status: "opened", pr_key: "acme/widgets#10" });
@@ -404,7 +380,7 @@ Deno.test("maybeStartRetro: bails while the plan is incomplete", async () => {
404
380
  assertEquals(stores["plans"][0].retro_started_at, null, "must not stamp an incomplete plan");
405
381
  });
406
382
 
407
- Deno.test("maybeStartRetro: complete but empty → records a skipped retro, does not start the process", async () => {
383
+ test("maybeStartRetro: complete but empty → records a skipped retro, does not start the process", async () => {
408
384
  const { data, stores } = memData();
409
385
  seedPlan(stores);
410
386
  seedTask(stores, { id: "t1", status: "opened", pr_key: "acme/widgets#10" });
@@ -419,7 +395,7 @@ Deno.test("maybeStartRetro: complete but empty → records a skipped retro, does
419
395
  assertEquals(stores["plan_retros"][0].status, "skipped");
420
396
  });
421
397
 
422
- Deno.test("maybeStartRetro: a rejected review round alone is enough to fire the retro", async () => {
398
+ test("maybeStartRetro: a rejected review round alone is enough to fire the retro", async () => {
423
399
  const { data, stores } = memData();
424
400
  seedPlan(stores);
425
401
  seedTask(stores, { id: "t1", status: "opened", pr_key: "acme/widgets#10" });
@@ -436,7 +412,7 @@ Deno.test("maybeStartRetro: a rejected review round alone is enough to fire the
436
412
  assert(stores["plans"][0].retro_started_at, "retro_started_at must be stamped");
437
413
  });
438
414
 
439
- Deno.test("maybeStartRetro: a PR not part of any plan is a no-op", async () => {
415
+ test("maybeStartRetro: a PR not part of any plan is a no-op", async () => {
440
416
  const { data } = memData();
441
417
  const { engine, started } = fakeEngine();
442
418
  const r = await maybeStartRetro(data, engine, "acme/widgets#99");
@@ -445,7 +421,7 @@ Deno.test("maybeStartRetro: a PR not part of any plan is a no-op", async () => {
445
421
  assertEquals(started.length, 0);
446
422
  });
447
423
 
448
- Deno.test("maybeStartRetro: honours NANO_AUTO_RETRO=0", async () => {
424
+ test("maybeStartRetro: honours NANO_AUTO_RETRO=0", async () => {
449
425
  const prev = process.env.NANO_AUTO_RETRO;
450
426
  process.env.NANO_AUTO_RETRO = "0";
451
427
  try {