@nanobpm/nano-workforce 0.36.0 → 0.38.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 +7 -17
  2. package/AGENTS.md +8 -7
  3. package/CHANGELOG.md +14 -0
  4. package/README.md +6 -27
  5. package/SPEC.md +15 -13
  6. package/app/abandon.test.ts +14 -19
  7. package/app/abandon.ts +2 -2
  8. package/app/baseGuard.test.ts +7 -6
  9. package/app/blackboard.test.ts +25 -35
  10. package/app/blackboard.ts +2 -2
  11. package/app/ensure-pr.test.ts +10 -12
  12. package/app/github.test.ts +9 -8
  13. package/app/github.ts +1 -21
  14. package/app/instance-tracking.test.ts +8 -6
  15. package/app/mergeExclusion.test.ts +11 -20
  16. package/app/mergeProtocol.test.ts +14 -13
  17. package/app/mergeRebaseArm.test.ts +8 -6
  18. package/app/mergeTrain.test.ts +10 -9
  19. package/app/persist-escalation.test.ts +10 -31
  20. package/app/persist-round.test.ts +7 -20
  21. package/app/plan.test.ts +19 -42
  22. package/app/record-plan-review.test.ts +8 -7
  23. package/app/retro.test.ts +24 -48
  24. package/app/reviewWait.test.ts +12 -11
  25. package/app/rounds.test.ts +13 -12
  26. package/app/service.test.ts +14 -27
  27. package/app/service.ts +1 -1
  28. package/app/taskDelta.test.ts +8 -17
  29. package/app/trialMerge.test.ts +4 -3
  30. package/app/version.ts +6 -21
  31. package/app/waves.test.ts +17 -16
  32. package/biome.json +0 -2
  33. package/main.ts +3 -3
  34. package/nano.app.json +2 -25
  35. package/openapi.yaml +649 -0
  36. package/operations/answerFeatureEscalation.test.ts +100 -0
  37. package/operations/answerFeatureEscalation.ts +49 -0
  38. package/operations/appendBlackboard.ts +61 -0
  39. package/{actions → operations}/blackboard.test.ts +28 -30
  40. package/{actions/abandon.test.ts → operations/checkAbandon.test.ts} +12 -26
  41. package/{actions/abandon.ts → operations/checkAbandon.ts} +8 -7
  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 +3 -4
  46. package/operations/postMessage.ts +1 -1
  47. package/operations/readBlackboard.ts +29 -0
  48. package/operations/startAndMessage.test.ts +11 -15
  49. package/operations/startConvergenceLoop.ts +17 -11
  50. package/operations/startPlanFanout.ts +13 -7
  51. package/package.json +9 -7
  52. package/pages/epic.page.json +1 -1
  53. package/pages/home.page.json +1 -1
  54. package/scripts/check-agent-prompts.test.ts +15 -11
  55. package/scripts/layout-bpmn.ts +6 -28
  56. package/scripts/pages-contract.test.ts +14 -13
  57. package/scripts/purge-db.ts +1 -1
  58. package/scripts/upgrade-from-pack.ts +1 -1
  59. package/test/assert.ts +74 -0
  60. package/tsconfig.json +4 -1
  61. package/workers/persist-task-escalation/worker.ts +1 -1
  62. package/workers/record-plan-review/worker.test.ts +6 -9
  63. package/workers/record-results/worker.test.ts +5 -8
  64. package/workers/record-trial-merge/worker.test.ts +7 -18
  65. package/workers/record-wave/worker.test.ts +13 -20
  66. package/workers/retro-gather/worker.test.ts +4 -17
  67. package/workers/retro-record/worker.test.ts +8 -27
  68. package/workers/select-wave/worker.test.ts +5 -8
  69. package/actions/blackboard.ts +0 -77
  70. package/actions/feature-answer-hook.ts +0 -45
  71. package/actions/plan-hook.ts +0 -20
  72. package/actions/webhook-submit.ts +0 -22
  73. package/deno.json +0 -24
  74. package/deno.lock +0 -1777
  75. package/openapi.json +0 -248
@@ -6,7 +6,8 @@
6
6
  // passes `recordRound=false`, which must suppress the round insert while still opening the
7
7
  // escalation. The agent-raised / max-rounds arms omit the flag (no prior round row) and must
8
8
  // still record the round.
9
- import { assert, assertEquals } from "jsr:@std/assert@1";
9
+ import { test } from "node:test";
10
+ import { assert, assertEquals } from "#test-assert";
10
11
  import handler from "../workers/persist-escalation/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 === "escalations" ? "id" : name === "rounds" ? "id" : "pr_key";
29
- // deno-lint-ignore no-explicit-any
30
28
  store.set((row as any)[pk], row);
31
29
  return name === "escalations" ? 42 : 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
  },
@@ -41,33 +38,29 @@ function fakeApp() {
41
38
  return { app, inserts, updates, rows };
42
39
  }
43
40
 
44
- Deno.test("stalled arm (recordRound=false) does not insert a duplicate rounds row", async () => {
41
+ test("stalled arm (recordRound=false) does not insert a duplicate rounds row", async () => {
45
42
  const { app, inserts } = fakeApp();
46
43
  const job = {
47
44
  variables: { prKey: "o/r#1", round: 3, status: "blocked", question: "stalled", recordRound: false },
48
45
  };
49
- // deno-lint-ignore no-explicit-any
50
46
  const out = await handler(job as any, app as any);
51
47
  assertEquals(inserts.rounds.length, 0, "no round row when the round was already recorded");
52
48
  assertEquals(inserts.escalations.length, 1, "escalation is still opened");
53
- // deno-lint-ignore no-explicit-any
54
49
  assertEquals((out as any).escalationId, 42);
55
50
  });
56
51
 
57
- Deno.test("escalation arm without the flag still records the round", async () => {
52
+ test("escalation arm without the flag still records the round", async () => {
58
53
  const { app, inserts } = fakeApp();
59
54
  const job = { variables: { prKey: "o/r#1", round: 3, status: "blocked", question: "max rounds" } };
60
- // deno-lint-ignore no-explicit-any
61
55
  await handler(job as any, app as any);
62
56
  assertEquals(inserts.rounds.length, 1);
63
- // deno-lint-ignore no-explicit-any
64
57
  assertEquals((inserts.rounds[0] as any).round_no, 3);
65
58
  });
66
59
 
67
60
  // When the convergence-loop passes repo/prNumber and the FK parent is missing (engine/app.db
68
61
  // desync), persist-escalation reconstructs the `pull_requests` row before the rounds/escalations
69
62
  // inserts so opening an escalation never dies with an opaque FOREIGN KEY constraint failure.
70
- Deno.test("persist-escalation heals a missing pull_requests parent before recording", async () => {
63
+ test("persist-escalation heals a missing pull_requests parent before recording", async () => {
71
64
  const { app, inserts, updates } = fakeApp();
72
65
  const job = {
73
66
  variables: {
@@ -79,32 +72,26 @@ Deno.test("persist-escalation heals a missing pull_requests parent before record
79
72
  prNumber: 8,
80
73
  },
81
74
  };
82
- // deno-lint-ignore no-explicit-any
83
75
  await handler(job as any, app as any);
84
76
  assertEquals(inserts.pull_requests?.length, 1, "the missing parent is reconstructed");
85
77
  // Assert against the reconstruction insert payload (ensurePr) rather than the stored row: the
86
78
  // fake update() doesn't apply patches, so the row would otherwise still read the insert's
87
79
  // "converging" status and mask the worker's real final state.
88
- // deno-lint-ignore no-explicit-any
89
80
  const healed = inserts.pull_requests![0] as any;
90
81
  assertEquals(healed.status, "converging", "the healed parent starts in the converging aggregate");
91
82
  assertEquals(inserts.rounds.length, 1, "the round is still recorded");
92
83
  assertEquals(inserts.escalations.length, 1, "the escalation is still opened");
93
84
  // And the worker still moves the (now-present) PR to escalated as its final state.
94
85
  assertEquals(updates.pull_requests!.length, 1, "the PR is updated once after the heal");
95
- // deno-lint-ignore no-explicit-any
96
86
  assertEquals((updates.pull_requests![0] as any).patch.status, "escalated");
97
87
  });
98
88
 
99
89
 
100
- Deno.test("a padded question is persisted trimmed (no whitespace drift)", async () => {
90
+ test("a padded question is persisted trimmed (no whitespace drift)", async () => {
101
91
  const { app, inserts, updates } = fakeApp();
102
92
  const job = { variables: { prKey: "o/r#1", round: 4, status: "needs_input", question: " needs a decision " } };
103
- // deno-lint-ignore no-explicit-any
104
93
  await handler(job as any, app as any);
105
- // deno-lint-ignore no-explicit-any
106
94
  assertEquals((inserts.escalations[0] as any).question, "needs a decision", "escalation stores the trimmed question");
107
- // deno-lint-ignore no-explicit-any
108
95
  assertEquals((updates.pull_requests![0] as any).patch.open_escalation_question, "needs a decision", "denormalised question is trimmed too");
109
96
  });
110
97
 
@@ -113,7 +100,7 @@ Deno.test("a padded question is persisted trimmed (no whitespace drift)", async
113
100
  // on Magikcraft/nano-bpm #597/#599) must NOT throw (which parked an un-remediable JobNoRetries
114
101
  // incident). It now opens an *answerable* escalation with a fabricated, concrete question and the
115
102
  // agent's transcript attached, so a human can unblock the loop entirely from the UI.
116
- Deno.test("blank question fabricates an answerable escalation (no throw, no incident)", async () => {
103
+ test("blank question fabricates an answerable escalation (no throw, no incident)", async () => {
117
104
  for (const question of [undefined, "", " "]) {
118
105
  const { app, inserts, updates } = fakeApp();
119
106
  const job = {
@@ -124,12 +111,9 @@ Deno.test("blank question fabricates an answerable escalation (no throw, no inci
124
111
  "io.nanobpm.agentResult": { output: "the agent's prose review, no result file" },
125
112
  },
126
113
  };
127
- // deno-lint-ignore no-explicit-any
128
114
  const out = await handler(job as any, app as any);
129
- // deno-lint-ignore no-explicit-any
130
115
  assertEquals((out as any).escalationId, 42, "an escalation is opened, not refused");
131
116
  assertEquals(inserts.escalations.length, 1, "escalation row written");
132
- // deno-lint-ignore no-explicit-any
133
117
  const esc = inserts.escalations[0] as any;
134
118
  assert(esc.question.trim().length > 0, "fabricated question is concrete/non-blank");
135
119
  assert(
@@ -139,7 +123,6 @@ Deno.test("blank question fabricates an answerable escalation (no throw, no inci
139
123
  assertEquals(esc.transcript, "the agent's prose review, no result file", "transcript attached");
140
124
  // Default status for an unclassified escalation is a question needing input.
141
125
  assertEquals(esc.kind, "question");
142
- // deno-lint-ignore no-explicit-any
143
126
  const pr = updates.pull_requests![0] as any;
144
127
  assertEquals(pr.patch.open_escalation_question, esc.question, "denormalised question set");
145
128
  }
@@ -147,12 +130,10 @@ Deno.test("blank question fabricates an answerable escalation (no throw, no inci
147
130
 
148
131
  // When a non-empty-but-unclassified status arrives with no question, the fabricated question
149
132
  // names the status so the human sees what the agent reported.
150
- Deno.test("unclassified status without a question names the status in the fabricated question", async () => {
133
+ test("unclassified status without a question names the status in the fabricated question", async () => {
151
134
  const { app, inserts } = fakeApp();
152
135
  const job = { variables: { prKey: "o/r#1", round: 3, status: "in_progress" } };
153
- // deno-lint-ignore no-explicit-any
154
136
  await handler(job as any, app as any);
155
- // deno-lint-ignore no-explicit-any
156
137
  const esc = inserts.escalations[0] as any;
157
138
  assert(esc.question.includes("in_progress"), "fabricated question references the raw status");
158
139
  assertEquals(esc.kind, "blocker", "a non needs_input status is a blocker escalation");
@@ -160,7 +141,7 @@ Deno.test("unclassified status without a question names the status in the fabric
160
141
 
161
142
  // When repo/prNumber process variables are absent the heal still runs by parsing the canonical
162
143
  // `owner/repo#N` prKey, so the escalation's FK parent is never left unguarded.
163
- Deno.test("persist-escalation heals from the prKey when repo/prNumber are absent", async () => {
144
+ test("persist-escalation heals from the prKey when repo/prNumber are absent", async () => {
164
145
  const { app, inserts } = fakeApp();
165
146
  const job = {
166
147
  variables: {
@@ -168,13 +149,11 @@ Deno.test("persist-escalation heals from the prKey when repo/prNumber are absent
168
149
  round: 2,
169
150
  status: "needs_input",
170
151
  question: "decide",
171
- abandonUrl: "https://host/hooks/abandon?token=TOK-en_123",
152
+ abandonUrl: "https://host/app/api/hooks/abandon?token=TOK-en_123",
172
153
  },
173
154
  };
174
- // deno-lint-ignore no-explicit-any
175
155
  await handler(job as any, app as any);
176
156
  assertEquals(inserts.pull_requests?.length, 1, "the parent is reconstructed from the prKey");
177
- // deno-lint-ignore no-explicit-any
178
157
  const healed = inserts.pull_requests![0] as any;
179
158
  assertEquals(healed.repo, "o/r");
180
159
  assertEquals(healed.number, 12);
@@ -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,51 +70,44 @@ 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: {
114
103
  prKey: "o/r#12",
115
104
  round: 2,
116
105
  status: "addressed",
117
- abandonUrl: "https://host/hooks/abandon?token=TOK-en_123",
106
+ abandonUrl: "https://host/app/api/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);
@@ -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
  });