@nanobpm/nano-workforce 0.167.0 → 0.167.2

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.
@@ -0,0 +1,57 @@
1
+ // Unit coverage for pr.record-feature-implementing (issue #642) — the twin of
2
+ // `record-feature-escalation`. It runs on BOTH edges into `implement-task` (first entry + answer
3
+ // re-entry) and must flip the run to the non-terminal `running` status so `escalated` holds ONLY
4
+ // while parked on the native `feature-escalation` user task. Without it, the answer loop-back left
5
+ // `feature_runs.status` a stale `escalated` through the whole re-implementation (the #632 tear).
6
+ import { test } from "node:test";
7
+ import { assertEquals } from "#test-assert";
8
+ import { noopLog } from "../../test/log.ts";
9
+ import handler from "./worker.ts";
10
+
11
+ // biome-ignore lint/suspicious/noExplicitAny: tiny in-memory app double, mirrors record-feature-escalation.worker.test
12
+ function fakeApp(rows: Record<string, unknown>[]): any {
13
+ const stores: Record<string, Record<string, unknown>[]> = { feature_runs: rows };
14
+ return {
15
+ stores,
16
+ data: {
17
+ table(name: string, key: string) {
18
+ const store = (stores[name] ??= []);
19
+ return {
20
+ // biome-ignore lint/suspicious/noExplicitAny: test double
21
+ get: (k: any) => Promise.resolve(store.find((r) => r[key] === k)),
22
+ // biome-ignore lint/suspicious/noExplicitAny: test double
23
+ update: (k: any, patch: any) => {
24
+ const row = store.find((r) => r[key] === k);
25
+ if (row) Object.assign(row, patch);
26
+ return Promise.resolve(row);
27
+ },
28
+ };
29
+ },
30
+ },
31
+ log: noopLog(),
32
+ };
33
+ }
34
+
35
+ test("record-feature-implementing: resets an escalated run back to running on the answer re-entry", async () => {
36
+ // The answer loop-back routes through this task before re-dispatching implement-task; it must clear
37
+ // the stale `escalated` the run parked on so the Overview no longer reads it as escalated while it
38
+ // re-implements (issue #642 — the #632 tear).
39
+ const rows = [{ feature_key: "owner/repo#7", status: "escalated", updated_at: "2025-01-01T00:00:00.000Z" }];
40
+ const app = fakeApp(rows);
41
+ const out = await handler({ jobKey: "job-1", variables: { featureKey: "owner/repo#7" } } as never, app);
42
+ assertEquals(out, {});
43
+ assertEquals(rows[0].status, "running");
44
+ assertEquals(rows[0].updated_at !== "2025-01-01T00:00:00.000Z", true, "updated_at was refreshed");
45
+ });
46
+
47
+ test("record-feature-implementing: a confirming write on the first entry (already running) keeps status running and still refreshes updated_at", async () => {
48
+ // On `f_toImplement` (first entry) the row is already `running` from dispatch — re-stamping is a
49
+ // harmless idempotent confirming write for the STATUS (a retried at-least-once job never regresses it),
50
+ // but the worker still refreshes `updated_at` on every invocation, so assert that timestamp write too
51
+ // (a future refactor must not silently stop stamping it — the self-heal grace window keys on it).
52
+ const rows = [{ feature_key: "owner/repo#8", status: "running", updated_at: "2025-01-01T00:00:00.000Z" }];
53
+ const app = fakeApp(rows);
54
+ await handler({ jobKey: "job-8", variables: { featureKey: "owner/repo#8" } } as never, app);
55
+ assertEquals(rows[0].status, "running");
56
+ assertEquals(rows[0].updated_at !== "2025-01-01T00:00:00.000Z", true, "updated_at was refreshed on the confirming write");
57
+ });
@@ -0,0 +1,31 @@
1
+ // pr.record-feature-implementing — the twin of `record-feature-escalation` (issue #642). This
2
+ // service task sits on BOTH edges into `implement-task`: the first entry (`f_toImplement`, off
3
+ // `ensure-base-branch`) AND the answer re-entry (`w_answerLoop`, off the `w_gw_answer` gateway).
4
+ // It stamps `feature_runs.status="running"` so the run is `escalated` ONLY while a token is parked
5
+ // on the native `feature-escalation` user task — honouring the invariant `record-feature-escalation`
6
+ // (the sole `escalated` writer) would otherwise violate on the answer loop-back: it had no symmetric
7
+ // reset, so `status` stayed a stale `escalated` through the ENTIRE post-answer re-implementation
8
+ // (the #632 tear). Parity with the PR `status="escalated"` contract, which holds only while parked.
9
+ //
10
+ // Idempotent-safe: re-stamping `running` is a no-op FOR THE STATUS, so the at-least-once job can retry
11
+ // freely; it does still refresh `updated_at` on every invocation (a confirming timestamp write), and
12
+ // stamping `running` on the very first entry (when the row is already `running` from dispatch) is a
13
+ // harmless confirming write.
14
+ import type { AppJobHandler } from "@nanobpm/urban";
15
+ import { featureRuns } from "../../app/feature.ts";
16
+ import type { WorkerInputs } from "../../nano-generated/worker-io.d.ts";
17
+
18
+ // Input typed off the model data envelope (`RecordFeatureImplementingIn` in feature.bpmn) — ADR 0040.
19
+ type In = WorkerInputs["pr.record-feature-implementing"];
20
+
21
+ const handler: AppJobHandler<In> = async (job, app) => {
22
+ const featureKey = job.variables.featureKey;
23
+ await featureRuns(app.data).update(featureKey, {
24
+ status: "running",
25
+ updated_at: new Date().toISOString(),
26
+ });
27
+ app.log.info("record-feature-implementing", { featureKey });
28
+ return {};
29
+ };
30
+
31
+ export default handler;