@nanobpm/nano-workforce 0.193.1 → 0.194.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.
@@ -15,6 +15,34 @@ function memData(seed: Record<string, any[]> = {}): { data: DataLayer; stores: R
15
15
  const stores: Record<string, any[]> = {};
16
16
  for (const [k, v] of Object.entries(seed)) stores[k] = v.map((r) => ({ ...r }));
17
17
  function tbl(name: string, pk = "id") {
18
+ // The ADR-0065 derived tracking VIEW (`<base>__tracking`) is modelled as a read-only projection
19
+ // over its base store, augmenting each row with `derived_status` (Copilot review of #829 — the PR
20
+ // self-heal now reads candidates through `prsTracking` and filters on `derived_status="escalated"`).
21
+ // Default `derived_status` to the base `status`, so a fixture that does NOT model out-of-band
22
+ // termination reads live (base status passes through). A test that wants a row the reconciler has
23
+ // already folded terminal seeds an explicit `derived_status` (e.g. `"abandoned"`) on the base row.
24
+ const trackingMatch = /^(.*)__tracking$/.exec(name);
25
+ if (trackingMatch) {
26
+ const base = (stores[trackingMatch[1]] ??= [] as any[]);
27
+ // biome-ignore lint/suspicious/noExplicitAny: see above
28
+ const project = (r: any) => ({ ...r, derived_status: r.derived_status ?? r.status });
29
+ // biome-ignore lint/suspicious/noExplicitAny: see above
30
+ const match = (r: any, where: any) => Object.entries(where).every(([k, v]) => r[k] === v);
31
+ return {
32
+ async all() {
33
+ return base.map(project);
34
+ },
35
+ // biome-ignore lint/suspicious/noExplicitAny: see above
36
+ async get(id: any) {
37
+ const r = base.find((row) => row[pk] === id);
38
+ return r ? project(r) : undefined;
39
+ },
40
+ // biome-ignore lint/suspicious/noExplicitAny: see above
41
+ async find(where: any = {}) {
42
+ return base.map(project).filter((r) => match(r, where));
43
+ },
44
+ };
45
+ }
18
46
  // biome-ignore lint/suspicious/noExplicitAny: see above
19
47
  const rows = (stores[name] ??= [] as any[]);
20
48
  // biome-ignore lint/suspicious/noExplicitAny: see above
@@ -48,26 +76,107 @@ function memData(seed: Record<string, any[]> = {}): { data: DataLayer; stores: R
48
76
  },
49
77
  };
50
78
  }
51
- const data = { table: (n: string, pk?: string) => tbl(n, pk) } as unknown as DataLayer;
79
+ const data = openableData(stores, tbl);
52
80
  return { data, stores };
53
81
  }
54
82
 
83
+ /** Wrap the in-memory `stores` as a `DataLayer` that ALSO exposes `open()` → a `DataSource` with the
84
+ * `exec` (guarded UPDATE) + `tx` (snapshot/rollback) surface the escalated-PR self-heal now uses for its
85
+ * conditional, atomic repair (Copilot review of #829). `exec` interprets ONLY the two guarded statements
86
+ * the heal issues — the snapshot-fenced `pull_requests` CAS and the by-id `escalations` retirement —
87
+ * mutating the same store objects `table()` reads, so both surfaces stay consistent within a test. The
88
+ * SQL shape itself is exercised against real SQLite by the app's integration/e2e suites; here the fake
89
+ * need only honour the guards so a race assertion can observe the CAS refusing a moved snapshot. */
90
+ // biome-ignore lint/suspicious/noExplicitAny: in-memory rows are untyped fixtures
91
+ function openableData(stores: Record<string, any[]>, tbl: (name: string, pk?: string) => any): DataLayer {
92
+ const norm = (v: unknown) => v ?? null;
93
+ const exec = async (sql: string, params: unknown[] = []) => {
94
+ const flip =
95
+ /UPDATE "pull_requests" SET "status" = 'converging', "updated_at" = \? WHERE "pr_key" = \? AND "status" = 'escalated' AND "process_key" IS \? AND "updated_at" IS \?/.exec(
96
+ sql,
97
+ );
98
+ if (flip) {
99
+ const [at, prKey, processKey, updatedAt] = params;
100
+ const row = (stores.pull_requests ?? []).find((r) => r.pr_key === prKey);
101
+ if (row && row.status === "escalated" && norm(row.process_key) === norm(processKey) && norm(row.updated_at) === norm(updatedAt)) {
102
+ row.status = "converging";
103
+ row.updated_at = at;
104
+ return { changed: 1 };
105
+ }
106
+ return { changed: 0 };
107
+ }
108
+ const retire = /UPDATE "escalations" SET "status" = 'stale' WHERE "id" = \? AND "status" = 'open'/.exec(sql);
109
+ if (retire) {
110
+ const row = (stores.escalations ?? []).find((r) => r.id === params[0]);
111
+ if (row && row.status === "open") {
112
+ row.status = "stale";
113
+ return { changed: 1 };
114
+ }
115
+ return { changed: 0 };
116
+ }
117
+ throw new Error(`unexpected exec sql: ${sql}`);
118
+ };
119
+ const source: any = {
120
+ exec,
121
+ table: (n: string, pk?: string) => tbl(n, pk),
122
+ tx: async (fn: (t: any) => Promise<unknown>) => {
123
+ const snap = JSON.parse(JSON.stringify(stores));
124
+ try {
125
+ return await fn(source);
126
+ } catch (e) {
127
+ for (const [n, rows] of Object.entries(stores)) {
128
+ rows.length = 0;
129
+ rows.push(...(snap[n] ?? []));
130
+ }
131
+ throw e;
132
+ }
133
+ },
134
+ };
135
+ return { table: (n: string, pk?: string) => tbl(n, pk), open: () => source } as unknown as DataLayer;
136
+ }
137
+
55
138
  /** A single engine-reported user task in the fixture. `state` mirrors the engine lifecycle; it
56
139
  * defaults to `"CREATED"` (the only open/answerable state) so existing fixtures read as live tasks.
57
140
  * A looping instance holds multiple tasks for one element (COMPLETED from prior rounds + the live one). */
58
141
  type FakeTask = { userTaskKey: string; elementId?: string; state?: "CREATED" | "COMPLETED" | "CANCELED"; formKey?: string };
59
142
 
143
+ /** A single engine-reported element instance in the fixture — the answer-recording service task the
144
+ * escalation self-heal probes for (issue #829). `state` mirrors the engine lifecycle; `"ACTIVE"` is the
145
+ * in-flight marker that an answer is being recorded. */
146
+ type FakeElement = { elementId: string; state: string };
147
+
60
148
  /** A fake engine whose user tasks are keyed by processInstanceKey (the only field the poller queries on
61
149
  * for plan / PR instances). It models the real engine's two accessors from ONE fixture so a test
62
150
  * genuinely exercises the lifecycle-state filtering: `searchUserTasks` returns tasks in ANY state
63
- * (COMPLETED first, as the live API does — issue #294), while `openUserTasks` pins `state:"CREATED"`. */
64
- function fakeEngine(byInstance: Record<string, FakeTask[]>): EngineClient {
151
+ * (COMPLETED first, as the live API does — issue #294), while `openUserTasks` pins `state:"CREATED"`.
152
+ * `searchElementInstances` is fed from an OPTIONAL second fixture (default empty) so the PR escalation
153
+ * self-heal can see an ACTIVE answer-recording task (issue #829). `searchProcessInstances` is fed from an
154
+ * OPTIONAL third fixture keyed by processInstanceKey: any key NOT listed defaults to reporting the
155
+ * instance `ACTIVE` (so existing heal fixtures, which model a genuinely-resumed live loop, stay green); a
156
+ * value of `null` models an instance ABSENT from the read model; a string overrides its lifecycle state
157
+ * (e.g. `"TERMINATED"`) — the positive-ACTIVE liveness gate the self-heal requires (issue #829). */
158
+ function fakeEngine(
159
+ byInstance: Record<string, FakeTask[]>,
160
+ elementsByInstance: Record<string, FakeElement[]> = {},
161
+ instanceStateByInstance: Record<string, string | null> = {},
162
+ ): EngineClient {
65
163
  const all = (filter?: { processInstanceKey?: string }) =>
66
164
  filter?.processInstanceKey ? (byInstance[filter.processInstanceKey] ?? []) : [];
67
165
  return {
68
166
  searchUserTasks: (filter?: { processInstanceKey?: string }) => Promise.resolve(all(filter)),
69
167
  openUserTasks: (filter?: { processInstanceKey?: string }) =>
70
168
  Promise.resolve(all(filter).filter((t) => (t.state ?? "CREATED") === "CREATED")),
169
+ searchElementInstances: (filter?: { processInstanceKey?: string }) =>
170
+ Promise.resolve(filter?.processInstanceKey ? (elementsByInstance[filter.processInstanceKey] ?? []) : []),
171
+ searchProcessInstances: (filter?: { processInstanceKeys?: string[] }) =>
172
+ Promise.resolve(
173
+ (filter?.processInstanceKeys ?? [])
174
+ .map((k) => {
175
+ const state = k in instanceStateByInstance ? instanceStateByInstance[k] : "ACTIVE";
176
+ return state === null ? null : { processInstanceKey: k, state };
177
+ })
178
+ .filter((it): it is { processInstanceKey: string; state: string } => it != null),
179
+ ),
71
180
  } as unknown as EngineClient;
72
181
  }
73
182
 
@@ -1072,3 +1181,463 @@ test("pollUserTasks (typed-seam fallback): self-heals an escalated run with no o
1072
1181
  const byKey = Object.fromEntries((stores.feature_runs ?? []).map((r) => [r.feature_key, r]));
1073
1182
  assertEquals(byKey["o/r#632"].status, "running", "the stranded escalated run is healed to running");
1074
1183
  });
1184
+
1185
+ test("pollUserTasks (engine-first): self-heals an escalated PR stranded off its parked task, sparing a genuinely parked one (issue #828)", async () => {
1186
+ // The convergence-path twin of the #642 feature heal. A PR's `status="escalated"` must hold ONLY while
1187
+ // a `wait-answer` / `wait-merge-answer` task is open. A PR whose instance the engine no longer reports
1188
+ // parked (its escalation was answered, but the write-side `answer-escalation` flip was lost around an
1189
+ // app restart — the #828 tear) is reconciled to `converging`; a PR whose task IS still open is left
1190
+ // escalated. The engine's open set is the authority, not the raw `pull_requests.status` column. The
1191
+ // orphaned `open` escalation rows are retired to `stale` so the audit trail matches.
1192
+ const { data, stores } = memData({
1193
+ pull_requests: [
1194
+ { pr_key: "o/r#828", status: "escalated", process_key: "rp-828", url: "https://github.com/o/r/pull/828", title: "stranded" },
1195
+ { pr_key: "o/r#77", status: "escalated", process_key: "rp-77", url: "https://github.com/o/r/pull/77", title: "still parked" },
1196
+ ],
1197
+ escalations: [
1198
+ { id: 1, pr_key: "o/r#828", status: "open", question: "orphaned — never answered", answer: null, answered_at: null },
1199
+ { id: 2, pr_key: "o/r#77", status: "open", question: "genuinely open", answer: null, answered_at: null },
1200
+ ],
1201
+ });
1202
+ const restore = stubUserTaskSearch([
1203
+ // Only rp-77 is genuinely parked; the engine reports NO open task on rp-828.
1204
+ { userTaskKey: "ut-parked", elementId: "wait-answer", processInstanceKey: "rp-77", state: "CREATED" },
1205
+ ]);
1206
+ try {
1207
+ await pollUserTasks(data, fakeEngine({ "rp-77": [{ userTaskKey: "ut-parked", elementId: "wait-answer" }] }), REST);
1208
+ } finally {
1209
+ restore();
1210
+ }
1211
+
1212
+ const byKey = Object.fromEntries((stores.pull_requests ?? []).map((r) => [r.pr_key, r]));
1213
+ assertEquals(byKey["o/r#828"].status, "converging", "the stranded escalated PR is healed to converging");
1214
+ assertEquals(byKey["o/r#77"].status, "escalated", "the genuinely parked PR stays escalated");
1215
+ const escById = Object.fromEntries((stores.escalations ?? []).map((r) => [r.id, r]));
1216
+ assertEquals(escById[1].status, "stale", "the healed PR's orphaned open escalation row is retired to stale");
1217
+ assertEquals(escById[2].status, "open", "the genuinely parked PR's escalation row stays open");
1218
+ });
1219
+
1220
+ test("pollUserTasks (engine-first): self-heals an escalated MERGE-loop PR off its parked wait-merge-answer task (issue #828)", async () => {
1221
+ // Both loops write `pr.persist-escalation` (`status="escalated"`) and are answered by the same
1222
+ // `answer-escalation` flip, so the heal must confirm against BOTH PR escalation elements — a merge
1223
+ // escalation parks on `wait-merge-answer`.
1224
+ const { data, stores } = memData({
1225
+ pull_requests: [
1226
+ { pr_key: "o/r#828m", status: "escalated", process_key: "mp-828", url: "https://github.com/o/r/pull/828", title: "stranded merge" },
1227
+ ],
1228
+ escalations: [{ id: 1, pr_key: "o/r#828m", status: "open", question: "not mergeable", answer: null, answered_at: null }],
1229
+ });
1230
+ const restore = stubUserTaskSearch([]); // engine reports NO open task on mp-828
1231
+ const engine = fakeEngine({ "mp-828": [] });
1232
+ try {
1233
+ await pollUserTasks(data, engine, REST);
1234
+ } finally {
1235
+ restore();
1236
+ }
1237
+ const byKey = Object.fromEntries((stores.pull_requests ?? []).map((r) => [r.pr_key, r]));
1238
+ assertEquals(byKey["o/r#828m"].status, "converging", "the stranded escalated merge-loop PR is healed to converging");
1239
+ });
1240
+
1241
+ test("pollUserTasks (engine-first): does NOT heal an escalated PR while its answer-recording job is in flight (issue #829)", async () => {
1242
+ // The dangerous race: both BPMN models flow the completed `wait-answer` / `wait-merge-answer` user task
1243
+ // DIRECTLY to the `record-answer` / `record-merge-answer` service task (`pr.answer-escalation`). Between
1244
+ // the operator completing the task and that job running, the user task is already gone (openUserTasks
1245
+ // returns none) yet the answer is being recorded RIGHT NOW. The escalation can be arbitrarily old (past
1246
+ // the raise-time grace), so the grace window does not cover this. Healing here would flip the row to
1247
+ // `converging` and retire the still-`open` escalation to `stale`, and the in-flight worker would then
1248
+ // find no target and silently drop the operator's answer. An ACTIVE answer-recording element instance is
1249
+ // positive evidence the answer is in flight, so the heal must skip it — exactly like an open task.
1250
+ const stale = new Date(Date.now() - 60 * 60_000).toISOString();
1251
+ const { data, stores } = memData({
1252
+ pull_requests: [
1253
+ { pr_key: "o/r#829r", status: "escalated", process_key: "rp-829r", updated_at: stale, url: "https://github.com/o/r/pull/829", title: "answer in flight (review)" },
1254
+ { pr_key: "o/r#829m", status: "escalated", process_key: "rp-829m", updated_at: stale, url: "https://github.com/o/r/pull/8290", title: "answer in flight (merge)" },
1255
+ ],
1256
+ escalations: [
1257
+ { id: 1, pr_key: "o/r#829r", status: "open", question: "answered, recording", answer: "do it", answered_at: stale },
1258
+ { id: 2, pr_key: "o/r#829m", status: "open", question: "answered, recording", answer: "rebase", answered_at: stale },
1259
+ ],
1260
+ });
1261
+ const restore = stubUserTaskSearch([]); // the user task has already completed on both instances
1262
+ // Each instance's answer-recording service task is ACTIVE — the operator's answer is being written.
1263
+ const engine = fakeEngine(
1264
+ { "rp-829r": [], "rp-829m": [] },
1265
+ {
1266
+ "rp-829r": [{ elementId: "record-answer", state: "ACTIVE" }],
1267
+ "rp-829m": [{ elementId: "record-merge-answer", state: "ACTIVE" }],
1268
+ },
1269
+ );
1270
+ try {
1271
+ await pollUserTasks(data, engine, REST);
1272
+ } finally {
1273
+ restore();
1274
+ }
1275
+ const byKey = Object.fromEntries((stores.pull_requests ?? []).map((r) => [r.pr_key, r]));
1276
+ assertEquals(byKey["o/r#829r"].status, "escalated", "a PR with an in-flight review answer-recorder is spared the heal");
1277
+ assertEquals(byKey["o/r#829m"].status, "escalated", "a PR with an in-flight merge answer-recorder is spared the heal");
1278
+ const escById = Object.fromEntries((stores.escalations ?? []).map((r) => [r.id, r]));
1279
+ assertEquals(escById[1].status, "open", "the in-flight review escalation row is left for the worker to answer");
1280
+ assertEquals(escById[2].status, "open", "the in-flight merge escalation row is left for the worker to answer");
1281
+ });
1282
+
1283
+ test("pollUserTasks (engine-first): DOES heal an escalated PR once its answer-recorder has completed (issue #829)", async () => {
1284
+ // The genuine #828 lost-write tear: the answer-recording job COMPLETED (its element instance is no
1285
+ // longer ACTIVE) but the durable `status="converging"` flip was lost around an app restart. With no open
1286
+ // task AND no ACTIVE answer-recorder, the PR is genuinely stranded and must heal — the in-flight guard
1287
+ // must not over-fire and wedge it permanently.
1288
+ const stale = new Date(Date.now() - 60 * 60_000).toISOString();
1289
+ const { data, stores } = memData({
1290
+ pull_requests: [{ pr_key: "o/r#829c", status: "escalated", process_key: "rp-829c", updated_at: stale, url: "https://github.com/o/r/pull/8291", title: "recorder completed, write lost" }],
1291
+ escalations: [{ id: 1, pr_key: "o/r#829c", status: "open", question: "orphaned", answer: null, answered_at: null }],
1292
+ });
1293
+ const restore = stubUserTaskSearch([]);
1294
+ const engine = fakeEngine({ "rp-829c": [] }, { "rp-829c": [{ elementId: "record-answer", state: "COMPLETED" }] });
1295
+ try {
1296
+ await pollUserTasks(data, engine, REST);
1297
+ } finally {
1298
+ restore();
1299
+ }
1300
+ const byKey = Object.fromEntries((stores.pull_requests ?? []).map((r) => [r.pr_key, r]));
1301
+ assertEquals(byKey["o/r#829c"].status, "converging", "a stranded PR whose answer-recorder already completed is healed");
1302
+ const escById = Object.fromEntries((stores.escalations ?? []).map((r) => [r.id, r]));
1303
+ assertEquals(escById[1].status, "stale", "the orphaned open escalation row is retired to stale");
1304
+ });
1305
+
1306
+ test("pollUserTasks (engine-first): does NOT heal an escalated PR whose process instance is TERMINAL (issue #829)", async () => {
1307
+ // Copilot review of the #828 heal: "no open task and no ACTIVE escalation element" is ALSO true for a
1308
+ // TERMINAL instance (cancelled / completed / failed), whose loop never resumed. Flipping such a frozen
1309
+ // `escalated` row to `converging` and retiring its audit rows fabricates a live loop the engine will
1310
+ // never advance; terminal PRs are owned by tracking/reconciliation (they read `abandoned`/settled on
1311
+ // `derived_status`). The heal now requires POSITIVE engine-truth that the instance is `ACTIVE`, so a
1312
+ // TERMINATED (and, below, an absent) instance is spared. One case per known terminal state.
1313
+ const stale = new Date(Date.now() - 60 * 60_000).toISOString();
1314
+ const { data, stores } = memData({
1315
+ pull_requests: [
1316
+ { pr_key: "o/r#829tt", status: "escalated", process_key: "rp-829tt", updated_at: stale, url: "https://github.com/o/r/pull/8297", title: "terminated instance" },
1317
+ { pr_key: "o/r#829tc", status: "escalated", process_key: "rp-829tc", updated_at: stale, url: "https://github.com/o/r/pull/8298", title: "cancelled instance" },
1318
+ ],
1319
+ escalations: [
1320
+ { id: 1, pr_key: "o/r#829tt", status: "open", question: "loop gone", answer: null, answered_at: null },
1321
+ { id: 2, pr_key: "o/r#829tc", status: "open", question: "loop gone", answer: null, answered_at: null },
1322
+ ],
1323
+ });
1324
+ const restore = stubUserTaskSearch([]); // no open task — but the loop is dead, not merely mid-transition
1325
+ // No open task, no ACTIVE escalation element, but the instance itself is a known terminal state.
1326
+ const engine = fakeEngine(
1327
+ { "rp-829tt": [], "rp-829tc": [] },
1328
+ {},
1329
+ { "rp-829tt": "TERMINATED", "rp-829tc": "CANCELED" },
1330
+ );
1331
+ try {
1332
+ await pollUserTasks(data, engine, REST);
1333
+ } finally {
1334
+ restore();
1335
+ }
1336
+ const byKey = Object.fromEntries((stores.pull_requests ?? []).map((r) => [r.pr_key, r]));
1337
+ assertEquals(byKey["o/r#829tt"].status, "escalated", "a TERMINATED instance is left to tracking/reconciliation, not healed");
1338
+ assertEquals(byKey["o/r#829tc"].status, "escalated", "a CANCELED instance is left to tracking/reconciliation, not healed");
1339
+ const escById = Object.fromEntries((stores.escalations ?? []).map((r) => [r.id, r]));
1340
+ assertEquals(escById[1].status, "open", "the terminal instance's escalation row is not retired");
1341
+ assertEquals(escById[2].status, "open", "the cancelled instance's escalation row is not retired");
1342
+ });
1343
+
1344
+ test("pollUserTasks (engine-first): does NOT heal an escalated PR whose process instance is ABSENT / unknown-state (issue #829)", async () => {
1345
+ // The other halves of the tri-state (Copilot review of #828): an instance genuinely ABSENT from the
1346
+ // read model (engine answered, no match) is treated like a terminal — its loop is gone, so leave it to
1347
+ // tracking rather than fabricate a resumed loop. An instance present with an UNKNOWN/empty `state` is a
1348
+ // partial read this app cannot interpret and is SPARED (never healed off a wire shape we misread). Both
1349
+ // stay `escalated`. One case each.
1350
+ const stale = new Date(Date.now() - 60 * 60_000).toISOString();
1351
+ const { data, stores } = memData({
1352
+ pull_requests: [
1353
+ { pr_key: "o/r#829ab", status: "escalated", process_key: "rp-829ab", updated_at: stale, url: "https://github.com/o/r/pull/8299", title: "absent instance" },
1354
+ { pr_key: "o/r#829uk", status: "escalated", process_key: "rp-829uk", updated_at: stale, url: "https://github.com/o/r/pull/82990", title: "unknown-state instance" },
1355
+ ],
1356
+ escalations: [
1357
+ { id: 1, pr_key: "o/r#829ab", status: "open", question: "loop gone", answer: null, answered_at: null },
1358
+ { id: 2, pr_key: "o/r#829uk", status: "open", question: "partial read", answer: null, answered_at: null },
1359
+ ],
1360
+ });
1361
+ const restore = stubUserTaskSearch([]);
1362
+ // `null` models an instance absent from the read model; a blank string models an unknown/partial state.
1363
+ const engine = fakeEngine({ "rp-829ab": [], "rp-829uk": [] }, {}, { "rp-829ab": null, "rp-829uk": "" });
1364
+ try {
1365
+ await pollUserTasks(data, engine, REST);
1366
+ } finally {
1367
+ restore();
1368
+ }
1369
+ const byKey = Object.fromEntries((stores.pull_requests ?? []).map((r) => [r.pr_key, r]));
1370
+ assertEquals(byKey["o/r#829ab"].status, "escalated", "an ABSENT instance is left to tracking/reconciliation, not healed");
1371
+ assertEquals(byKey["o/r#829uk"].status, "escalated", "an UNKNOWN-state instance is spared, not healed off a misread wire shape");
1372
+ const escById = Object.fromEntries((stores.escalations ?? []).map((r) => [r.id, r]));
1373
+ assertEquals(escById[1].status, "open", "the absent instance's escalation row is not retired");
1374
+ assertEquals(escById[2].status, "open", "the unknown-state instance's escalation row is not retired");
1375
+ });
1376
+
1377
+ test("pollUserTasks (engine-first): a terminated-while-escalated PR is EXCLUDED by the derived tracking VIEW — never probed (issue #829)", async () => {
1378
+ // Copilot review of #829: the candidate scan reads through `prsTracking` and requires
1379
+ // `derived_status="escalated"`, so a PR TERMINATED while its base status was `escalated` — whose VIEW
1380
+ // folds `derived_status` to `abandoned` — is excluded BEFORE any per-instance probe. Without the VIEW
1381
+ // filter this historical row would be reselected every poll and burn engine RPCs forever. We seed an
1382
+ // explicit `derived_status: "abandoned"` (the reconciler's terminal fold) and assert the heal issues NO
1383
+ // engine RPC for it at all, and its frozen base row / escalation are left untouched.
1384
+ const stale = new Date(Date.now() - 60 * 60_000).toISOString();
1385
+ const { data, stores } = memData({
1386
+ pull_requests: [
1387
+ { pr_key: "o/r#829term", status: "escalated", derived_status: "abandoned", process_key: "rp-829term", updated_at: stale, url: "https://github.com/o/r/pull/82991", title: "terminated-while-escalated (VIEW-folded)" },
1388
+ ],
1389
+ escalations: [{ id: 1, pr_key: "o/r#829term", status: "open", question: "loop gone", answer: null, answered_at: null }],
1390
+ });
1391
+ const restore = stubUserTaskSearch([]);
1392
+ const probed: string[] = [];
1393
+ const engine = {
1394
+ searchUserTasks: () => Promise.resolve([]),
1395
+ openUserTasks: (f?: { processInstanceKey?: string }) => {
1396
+ if (f?.processInstanceKey) probed.push(f.processInstanceKey);
1397
+ return Promise.resolve([]);
1398
+ },
1399
+ searchElementInstances: (f?: { processInstanceKey?: string }) => {
1400
+ if (f?.processInstanceKey) probed.push(f.processInstanceKey);
1401
+ return Promise.resolve([]);
1402
+ },
1403
+ searchProcessInstances: (f?: { processInstanceKeys?: string[] }) => {
1404
+ for (const k of f?.processInstanceKeys ?? []) probed.push(k);
1405
+ return Promise.resolve([]);
1406
+ },
1407
+ } as unknown as EngineClient;
1408
+ try {
1409
+ await pollUserTasks(data, engine, REST);
1410
+ } finally {
1411
+ restore();
1412
+ }
1413
+ assertEquals(probed.includes("rp-829term"), false, "a VIEW-folded terminal row is excluded from the scan — no engine RPC is issued for it");
1414
+ const byKey = Object.fromEntries((stores.pull_requests ?? []).map((r) => [r.pr_key, r]));
1415
+ assertEquals(byKey["o/r#829term"].status, "escalated", "the terminated row's frozen base status is left to tracking, not rewritten");
1416
+ const escById = Object.fromEntries((stores.escalations ?? []).map((r) => [r.id, r]));
1417
+ assertEquals(escById[1].status, "open", "the terminated row's escalation is not retired");
1418
+ });
1419
+
1420
+ test("pollUserTasks (engine-first): does NOT heal an escalated PR while its persist-escalation producer is still ACTIVE (issue #829)", async () => {
1421
+ // The producer-side twin of the answer-recorder race. `pr.persist-escalation` COMMITS the `open` row and
1422
+ // `status="escalated"` BEFORE the engine creates the `wait-answer` user task, so there is a window with an
1423
+ // `escalated` PR, an `open` escalation, and NO user task yet. The raise-time grace normally covers it, but
1424
+ // if the producer service task stays ACTIVE past the grace (an app restart / lease delay between the DB
1425
+ // commit and the job completing) the row is old enough to heal while the escalation is being raised RIGHT
1426
+ // NOW. Healing here would retire the just-raised escalation to `stale` and the question would never surface.
1427
+ // An ACTIVE persist-escalation element is positive evidence, so the heal must skip it — one case per loop.
1428
+ const stale = new Date(Date.now() - 60 * 60_000).toISOString();
1429
+ const { data, stores } = memData({
1430
+ pull_requests: [
1431
+ { pr_key: "o/r#829pr", status: "escalated", process_key: "rp-829pr", updated_at: stale, url: "https://github.com/o/r/pull/8293", title: "producer active (review)" },
1432
+ { pr_key: "o/r#829pm", status: "escalated", process_key: "rp-829pm", updated_at: stale, url: "https://github.com/o/r/pull/8294", title: "producer active (merge)" },
1433
+ ],
1434
+ escalations: [
1435
+ { id: 1, pr_key: "o/r#829pr", status: "open", question: "being raised", answer: null, answered_at: null },
1436
+ { id: 2, pr_key: "o/r#829pm", status: "open", question: "being raised", answer: null, answered_at: null },
1437
+ ],
1438
+ });
1439
+ const restore = stubUserTaskSearch([]); // the user task does not exist yet — the producer hasn't completed
1440
+ const engine = fakeEngine(
1441
+ { "rp-829pr": [], "rp-829pm": [] },
1442
+ {
1443
+ "rp-829pr": [{ elementId: "persist-escalation-blockedcomments", state: "ACTIVE" }],
1444
+ "rp-829pm": [{ elementId: "merge-esc-conflict", state: "ACTIVE" }],
1445
+ },
1446
+ );
1447
+ try {
1448
+ await pollUserTasks(data, engine, REST);
1449
+ } finally {
1450
+ restore();
1451
+ }
1452
+ const byKey = Object.fromEntries((stores.pull_requests ?? []).map((r) => [r.pr_key, r]));
1453
+ assertEquals(byKey["o/r#829pr"].status, "escalated", "a PR with an ACTIVE review persist-escalation producer is spared the heal");
1454
+ assertEquals(byKey["o/r#829pm"].status, "escalated", "a PR with an ACTIVE merge persist-escalation producer is spared the heal");
1455
+ const escById = Object.fromEntries((stores.escalations ?? []).map((r) => [r.id, r]));
1456
+ assertEquals(escById[1].status, "open", "the just-raised review escalation row is left open");
1457
+ assertEquals(escById[2].status, "open", "the just-raised merge escalation row is left open");
1458
+ });
1459
+
1460
+ test("pollUserTasks (engine-first): does NOT heal an escalated PR when the producer→user-task transition lands BETWEEN the two engine reads (issue #829)", async () => {
1461
+ // The producer→user-task TOCTOU the prior two guards miss. `openUserTasks()` (read 1) returns empty while
1462
+ // a long-running persist producer is still ACTIVE; that producer then COMPLETES and the engine creates the
1463
+ // `wait-answer` / `wait-merge-answer` user task BEFORE `searchElementInstances()` (read 2). Read 2 therefore
1464
+ // sees an ACTIVE wait element — NOT the producer. Because user-task creation does not re-stamp the PR row's
1465
+ // `updated_at` (only `pr.persist-escalation` does, before the task exists), the row is past the raise-time
1466
+ // grace, so neither the grace nor the producer/answer-recorder predicate covers it: a predicate that ignores
1467
+ // the wait elements lets the CAS win and stale a live escalation. Treat an ACTIVE `wait-answer` /
1468
+ // `wait-merge-answer` element as positive evidence in the second snapshot too — one case per loop.
1469
+ const stale = new Date(Date.now() - 60 * 60_000).toISOString();
1470
+ const { data, stores } = memData({
1471
+ pull_requests: [
1472
+ { pr_key: "o/r#829wr", status: "escalated", process_key: "rp-829wr", updated_at: stale, url: "https://github.com/o/r/pull/8295", title: "producer→wait (review)" },
1473
+ { pr_key: "o/r#829wm", status: "escalated", process_key: "rp-829wm", updated_at: stale, url: "https://github.com/o/r/pull/8296", title: "producer→wait (merge)" },
1474
+ ],
1475
+ escalations: [
1476
+ { id: 1, pr_key: "o/r#829wr", status: "open", question: "raised mid-read", answer: null, answered_at: null },
1477
+ { id: 2, pr_key: "o/r#829wm", status: "open", question: "raised mid-read", answer: null, answered_at: null },
1478
+ ],
1479
+ });
1480
+ // The typed `openUserTasks` seam reports NO open task (the producer was still ACTIVE at read 1), while the
1481
+ // element search (read 2) now sees the freshly-created wait element ACTIVE — the transition landed mid-read.
1482
+ const restore = stubUserTaskSearch([]);
1483
+ const engine = fakeEngine(
1484
+ { "rp-829wr": [], "rp-829wm": [] },
1485
+ {
1486
+ "rp-829wr": [{ elementId: "wait-answer", state: "ACTIVE" }],
1487
+ "rp-829wm": [{ elementId: "wait-merge-answer", state: "ACTIVE" }],
1488
+ },
1489
+ );
1490
+ try {
1491
+ await pollUserTasks(data, engine, REST);
1492
+ } finally {
1493
+ restore();
1494
+ }
1495
+ const byKey = Object.fromEntries((stores.pull_requests ?? []).map((r) => [r.pr_key, r]));
1496
+ assertEquals(byKey["o/r#829wr"].status, "escalated", "a PR whose wait-answer appeared mid-read is spared the heal");
1497
+ assertEquals(byKey["o/r#829wm"].status, "escalated", "a PR whose wait-merge-answer appeared mid-read is spared the heal");
1498
+ const escById = Object.fromEntries((stores.escalations ?? []).map((r) => [r.id, r]));
1499
+ assertEquals(escById[1].status, "open", "the live review escalation row is left open");
1500
+ assertEquals(escById[2].status, "open", "the live merge escalation row is left open");
1501
+ });
1502
+
1503
+ test("pollUserTasks (engine-first): a re-escalation in the read→write window is NOT clobbered — the guarded CAS refuses the moved snapshot (issue #829)", async () => {
1504
+ // The TOCTOU race Copilot flagged: the heal reads the PR, then does two remote engine RPCs, THEN writes.
1505
+ // If the SAME instance re-escalates in that window (`pr.persist-escalation` re-stamps `updated_at` and
1506
+ // INSERTs a fresh open escalation), a blind write would clobber the fresh `escalated` status and stale
1507
+ // the fresh escalation — dropping the operator's next answer. We reproduce the concurrent re-escalation
1508
+ // by mutating the store from inside `searchElementInstances` (the last read before the write), then
1509
+ // assert the snapshot-fenced CAS makes NO change: the PR stays `escalated` and the fresh escalation open.
1510
+ // NB the mutation advances `updated_at` AND inserts the fresh escalation TOGETHER, modelling the now-atomic
1511
+ // `pr.persist-escalation` producer (its insert + PR re-stamp share one transaction, issue #829) — so a
1512
+ // captured snapshot can never see the new escalation under the OLD generation; the covering red/green for
1513
+ // that producer atomicity lives in app/persist-escalation.test.ts.
1514
+ const stale = new Date(Date.now() - 60 * 60_000).toISOString();
1515
+ const { data, stores } = memData({
1516
+ pull_requests: [{ pr_key: "o/r#829race", status: "escalated", process_key: "rp-829race", updated_at: stale, url: "https://github.com/o/r/pull/8292", title: "re-escalates mid-heal" }],
1517
+ escalations: [{ id: 1, pr_key: "o/r#829race", status: "open", question: "first", answer: null, answered_at: null }],
1518
+ });
1519
+ const restore = stubUserTaskSearch([]);
1520
+ let reEscalated = false;
1521
+ const engine = {
1522
+ searchUserTasks: () => Promise.resolve([]),
1523
+ openUserTasks: () => Promise.resolve([]),
1524
+ // Report the instance ACTIVE so the heal reaches its guarded CAS (Copilot review of #829): without a
1525
+ // `searchProcessInstances` response the positive-liveness probe throws, sets `queryErrored`, and the
1526
+ // row would be spared for the WRONG reason — never exercising the snapshot-fenced CAS this test asserts.
1527
+ searchProcessInstances: () => Promise.resolve([{ processInstanceKey: "rp-829race", state: "ACTIVE" }]),
1528
+ searchElementInstances: (filter?: { processInstanceKey?: string }) => {
1529
+ // Simulate the concurrent re-escalation landing AFTER the heal captured its snapshot but BEFORE its
1530
+ // write: the row's generation advances (`updated_at`) and a brand-new open escalation is inserted.
1531
+ if (filter?.processInstanceKey === "rp-829race" && !reEscalated) {
1532
+ reEscalated = true;
1533
+ const row = (stores.pull_requests ?? []).find((r) => r.pr_key === "o/r#829race");
1534
+ if (row) row.updated_at = new Date().toISOString();
1535
+ (stores.escalations ?? []).push({ id: 2, pr_key: "o/r#829race", status: "open", question: "second", answer: null, answered_at: null });
1536
+ }
1537
+ return Promise.resolve([{ elementId: "record-answer", state: "COMPLETED" }]);
1538
+ },
1539
+ } as unknown as EngineClient;
1540
+ try {
1541
+ await pollUserTasks(data, engine, REST);
1542
+ } finally {
1543
+ restore();
1544
+ }
1545
+ const byKey = Object.fromEntries((stores.pull_requests ?? []).map((r) => [r.pr_key, r]));
1546
+ assertEquals(byKey["o/r#829race"].status, "escalated", "the re-escalated PR is left escalated — the CAS refused the moved snapshot");
1547
+ const escById = Object.fromEntries((stores.escalations ?? []).map((r) => [r.id, r]));
1548
+ assertEquals(escById[2].status, "open", "the fresh escalation opened in the window is NOT retired");
1549
+ assertEquals(escById[1].status, "open", "the snapshot's own escalation is left untouched too — the whole tx rolled to a no-op");
1550
+ });
1551
+
1552
+ test("pollUserTasks (engine-first): skips the per-instance open-task RPC for an escalated PR already seen parked in this pass's sweep (issue #828)", async () => {
1553
+ // best-effort sweep may truncate (drop tasks) but never invents one. Re-confirming with a per-instance
1554
+ // `openUserTasks` RPC is a redundant N+1 query; the self-heal must skip it. Only a PR NOT confirmed
1555
+ // parked by the sweep still needs the per-instance check.
1556
+ const { data, stores } = memData({
1557
+ pull_requests: [
1558
+ { pr_key: "o/r#parked", status: "escalated", process_key: "rp-parked", url: "https://github.com/o/r/pull/1", title: "parked" },
1559
+ { pr_key: "o/r#stranded", status: "escalated", process_key: "rp-stranded", url: "https://github.com/o/r/pull/2", title: "stranded" },
1560
+ ],
1561
+ escalations: [{ id: 1, pr_key: "o/r#parked", status: "open", question: "open", answer: null, answered_at: null }],
1562
+ });
1563
+ const restore = stubUserTaskSearch([
1564
+ { userTaskKey: "ut-parked", elementId: "wait-answer", processInstanceKey: "rp-parked", state: "CREATED" },
1565
+ ]);
1566
+ const openUserTasksCalls: string[] = [];
1567
+ const engine = {
1568
+ searchUserTasks: () => Promise.resolve([]),
1569
+ openUserTasks: (filter?: { processInstanceKey?: string }) => {
1570
+ if (filter?.processInstanceKey) openUserTasksCalls.push(filter.processInstanceKey);
1571
+ return Promise.resolve([]);
1572
+ },
1573
+ searchElementInstances: () => Promise.resolve([]),
1574
+ // The stranded instance genuinely resumed (ACTIVE) — so the sweep-miss falls through to the
1575
+ // per-instance confirm AND the positive-liveness gate, and heals (issue #829).
1576
+ searchProcessInstances: (filter?: { processInstanceKeys?: string[] }) =>
1577
+ Promise.resolve((filter?.processInstanceKeys ?? []).map((k) => ({ processInstanceKey: k, state: "ACTIVE" }))),
1578
+ } as unknown as EngineClient;
1579
+ try {
1580
+ await pollUserTasks(data, engine, REST);
1581
+ } finally {
1582
+ restore();
1583
+ }
1584
+ const byKey = Object.fromEntries((stores.pull_requests ?? []).map((r) => [r.pr_key, r]));
1585
+ assertEquals(byKey["o/r#parked"].status, "escalated", "the swept-parked PR stays escalated without a per-instance query");
1586
+ assertEquals(byKey["o/r#stranded"].status, "converging", "the PR absent from the sweep is still confirmed per-instance and healed");
1587
+ assertEquals(openUserTasksCalls.includes("rp-parked"), false, "no redundant per-instance RPC for the already-parked PR");
1588
+ assertEquals(openUserTasksCalls.includes("rp-stranded"), true, "the unconfirmed PR still needs the per-instance RPC");
1589
+ });
1590
+
1591
+ test("pollUserTasks (engine-first): does NOT heal an escalated PR when the per-instance open-task query errors (issue #828)", async () => {
1592
+ // A per-instance query error is not proof the PR is unparked — mutating on that negative evidence would
1593
+ // steal a parked escalation. On query error the PR must be left `escalated` for a later pass.
1594
+ const { data, stores } = memData({
1595
+ pull_requests: [{ pr_key: "o/r#err", status: "escalated", process_key: "rp-err", url: "https://github.com/o/r/pull/9", title: "query errors" }],
1596
+ escalations: [{ id: 1, pr_key: "o/r#err", status: "open", question: "open", answer: null, answered_at: null }],
1597
+ });
1598
+ const restore = stubUserTaskSearch([]);
1599
+ const engine = {
1600
+ searchUserTasks: () => Promise.resolve([]),
1601
+ openUserTasks: (filter?: { processInstanceKey?: string }) =>
1602
+ filter?.processInstanceKey === "rp-err" ? Promise.reject(new Error("engine down")) : Promise.resolve([]),
1603
+ searchElementInstances: () => Promise.resolve([]),
1604
+ } as unknown as EngineClient;
1605
+ try {
1606
+ await pollUserTasks(data, engine, REST);
1607
+ } finally {
1608
+ restore();
1609
+ }
1610
+ const byKey = Object.fromEntries((stores.pull_requests ?? []).map((r) => [r.pr_key, r]));
1611
+ assertEquals(byKey["o/r#err"].status, "escalated", "a failed per-instance query leaves the PR escalated");
1612
+ const escById = Object.fromEntries((stores.escalations ?? []).map((r) => [r.id, r]));
1613
+ assertEquals(escById[1].status, "open", "the escalation row is untouched when the heal is skipped");
1614
+ });
1615
+
1616
+ test("pollUserTasks (engine-first): does NOT heal a JUST-escalated PR inside the grace window before its user task exists (issue #828)", async () => {
1617
+ // `pr.persist-escalation` stamps `status="escalated"` (and `updated_at`) IMMEDIATELY BEFORE the engine
1618
+ // creates the `wait-answer` user task. A poll landing in that window sees `openUserTasks` return none
1619
+ // and would wrongly flip the fresh escalation back to `converging`. A short grace window on `updated_at`
1620
+ // spares a just-written escalation while still healing genuinely-stranded (old) rows.
1621
+ const fresh = new Date().toISOString();
1622
+ const stale = new Date(Date.now() - 60 * 60_000).toISOString();
1623
+ const { data, stores } = memData({
1624
+ pull_requests: [
1625
+ { pr_key: "o/r#fresh", status: "escalated", process_key: "rp-fresh", updated_at: fresh, url: "https://github.com/o/r/pull/3", title: "just escalated" },
1626
+ { pr_key: "o/r#old", status: "escalated", process_key: "rp-old", updated_at: stale, url: "https://github.com/o/r/pull/4", title: "genuinely stranded" },
1627
+ ],
1628
+ escalations: [
1629
+ { id: 1, pr_key: "o/r#fresh", status: "open", question: "fresh", answer: null, answered_at: null },
1630
+ { id: 2, pr_key: "o/r#old", status: "open", question: "old", answer: null, answered_at: null },
1631
+ ],
1632
+ });
1633
+ const restore = stubUserTaskSearch([]);
1634
+ const engine = fakeEngine({});
1635
+ try {
1636
+ await pollUserTasks(data, engine, REST);
1637
+ } finally {
1638
+ restore();
1639
+ }
1640
+ const byKey = Object.fromEntries((stores.pull_requests ?? []).map((r) => [r.pr_key, r]));
1641
+ assertEquals(byKey["o/r#fresh"].status, "escalated", "a just-escalated PR inside the grace window is spared the heal");
1642
+ assertEquals(byKey["o/r#old"].status, "converging", "a genuinely-stranded (old) PR is still healed");
1643
+ });