@nanobpm/nano-workforce 0.188.1 → 0.189.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.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,9 @@
1
+ ## [0.189.0](https://github.com/nanobpm/nano-workforce/compare/v0.188.1...v0.189.0) (2026-09-17)
2
+
3
+ ### Features
4
+
5
+ * **agentic:** gate stale worker harnesses via persisted protocol ([#804](https://github.com/nanobpm/nano-workforce/issues/804)) ([e5ba2c3](https://github.com/nanobpm/nano-workforce/commit/e5ba2c39e0f76a2654099c54e37e0d3a97f20473)), closes [#802](https://github.com/nanobpm/nano-workforce/issues/802) [#802](https://github.com/nanobpm/nano-workforce/issues/802) [#802](https://github.com/nanobpm/nano-workforce/issues/802) [#802](https://github.com/nanobpm/nano-workforce/issues/802) [#802](https://github.com/nanobpm/nano-workforce/issues/802) [#802](https://github.com/nanobpm/nano-workforce/issues/802)
6
+
1
7
  ## [0.188.1](https://github.com/nanobpm/nano-workforce/compare/v0.188.0...v0.188.1) (2026-09-17)
2
8
 
3
9
  ### Bug Fixes
@@ -111,6 +111,56 @@ test("the rendered transcript region sits directly beneath the Workers — suppl
111
111
  }
112
112
  });
113
113
 
114
+ // #802 — the DEPLOYED browser twin (mount.js) must surface a STALE harness as a distinct badge, and
115
+ // a healthy one with none. The typed renderer (`supply-render.ts`) has its own test, but the twin is
116
+ // hand-maintained and previously had no non-empty supply-row coverage, so it could silently stop
117
+ // surfacing stale harnesses while the typed test stayed green.
118
+ test("#802: mount.js renders a stale-harness badge for a stale worker and none for a healthy one", async () => {
119
+ const worker = (instance: string, harnessStale: boolean, harnessProtocol?: number) => ({
120
+ instance,
121
+ identity: "senior",
122
+ stream: instance,
123
+ family: "senior",
124
+ host: "h1",
125
+ jobKeys: [],
126
+ live: true,
127
+ staleMs: 0,
128
+ harnessStale,
129
+ ...(harnessProtocol !== undefined ? { harnessProtocol } : {}),
130
+ });
131
+ const workers = [worker("wk-stale", true, 0), worker("wk-ok", false, 3)];
132
+ const report = { count: workers.length, workers, leaves: [{ token: "senior", workers }], correlations: [] };
133
+ const restore = installEnv((url) => {
134
+ const ok = (body: unknown) => Promise.resolve({ ok: true, status: 200, json: async () => body });
135
+ if (url.includes("/supply")) return ok(report);
136
+ if (url.includes("/agent-instances")) return ok({ count: 0, instances: [] });
137
+ if (url.includes("/transcripts")) return ok({ sessions: [] });
138
+ return Promise.resolve({ ok: false, status: 404, json: async () => ({}) });
139
+ });
140
+ try {
141
+ const { mountCockpit } = await import("../../../pages/cockpit/mount.js");
142
+ const handle = mountCockpit(document.getElementById("root"), OPTS);
143
+ try {
144
+ await handle.refresh();
145
+ const staleRow = document.querySelector('.cockpit-supply-worker[data-worker="wk-stale"]');
146
+ const okRow = document.querySelector('.cockpit-supply-worker[data-worker="wk-ok"]');
147
+ assert(staleRow != null && okRow != null, "both worker rows rendered");
148
+ const badge = staleRow?.querySelector('.cockpit-supply-harness-stale[data-harness-stale="true"]');
149
+ assert(badge != null, "the stale worker carries the harness-stale badge");
150
+ assertEquals(badge?.textContent, "stale harness (v0)", "the badge shows the advertised protocol");
151
+ assertEquals(
152
+ okRow?.querySelector(".cockpit-supply-harness-stale"),
153
+ null,
154
+ "the healthy worker carries no harness-stale badge",
155
+ );
156
+ } finally {
157
+ handle.dispose();
158
+ }
159
+ } finally {
160
+ restore();
161
+ }
162
+ });
163
+
114
164
  test("live drill renders the transcript — a nwfTranscriptEvent chunk is never surfaced verbatim", async () => {
115
165
  const restore = installEnv(fetchStub());
116
166
  try {
@@ -140,3 +140,23 @@ test("H6: a worker with no correlation renders an em-dash process cell", () => {
140
140
  assert.equal(cell?.text(), "—");
141
141
  assert.equal(cell?.getAttribute("data-correlations"), "0");
142
142
  });
143
+
144
+ test("#802: renders a stale-harness badge for a worker below the minimum / with no advertised protocol", () => {
145
+ const host = new FakeElement("body");
146
+ const staleWorker = { instance: "wk-old", identity: "leaf-1", stream: "wk-old", family: "senior", host: "h1", jobKeys: [], live: true, staleMs: 0, harnessStale: true };
147
+ const okWorker = { instance: "wk-ok", identity: "leaf-1", stream: "wk-ok", family: "senior", host: "h2", jobKeys: [], live: true, staleMs: 0, harnessStale: false, harnessProtocol: 3 };
148
+ const report: SupplyReport = {
149
+ count: 2,
150
+ workers: [staleWorker, okWorker],
151
+ leaves: [{ token: "leaf-1", workers: [staleWorker, okWorker] }],
152
+ };
153
+ renderSupply(host, doc, supplyView(report));
154
+
155
+ const rowOld = host.byData("worker", "wk-old")[0];
156
+ assert.equal(rowOld?.getAttribute("data-harness-stale"), "true");
157
+ assert.equal(rowOld?.byClass("cockpit-supply-harness-stale").length, 1, "stale harness badge rendered");
158
+
159
+ const rowOk = host.byData("worker", "wk-ok")[0];
160
+ assert.equal(rowOk?.getAttribute("data-harness-stale"), "false");
161
+ assert.equal(rowOk?.byClass("cockpit-supply-harness-stale").length, 0, "no badge for a healthy harness");
162
+ });
@@ -51,6 +51,7 @@ function workerRow(doc: DocumentLike, worker: SupplyWorkerView, options: RenderS
51
51
  row.setAttribute("data-worker", worker.instance);
52
52
  row.setAttribute("data-liveness", worker.liveness);
53
53
  row.setAttribute("data-stream", worker.stream);
54
+ row.setAttribute("data-harness-stale", String(worker.harnessStale));
54
55
 
55
56
  const nameCell = el(doc, "td", "cockpit-td cockpit-supply-name");
56
57
  nameCell.appendChild(dot(doc, worker.liveness));
@@ -82,6 +83,20 @@ function workerRow(doc: DocumentLike, worker: SupplyWorkerView, options: RenderS
82
83
  }
83
84
  nameCell.appendChild(drill);
84
85
  }
86
+ // A stale harness silently swallows machine-readable artifacts (issue #802) — surface it as a
87
+ // distinct badge so the operator can drain/upgrade the worker. Separate from the liveness dot,
88
+ // which grades heartbeat recency, not harness capability.
89
+ if (worker.harnessStale) {
90
+ const badge = el(
91
+ doc,
92
+ "span",
93
+ "cockpit-supply-harness-stale",
94
+ worker.harnessProtocol === undefined ? "stale harness" : `stale harness (v${worker.harnessProtocol})`,
95
+ );
96
+ badge.setAttribute("data-harness-stale", "true");
97
+ badge.setAttribute("title", "Harness protocol below the configured minimum (or none advertised); jobs may dead-end.");
98
+ nameCell.appendChild(badge);
99
+ }
85
100
  row.appendChild(nameCell);
86
101
 
87
102
  row.appendChild(el(doc, "td", "cockpit-td cockpit-supply-family", worker.family));
@@ -38,9 +38,16 @@ export interface SupplyWorkerReport {
38
38
  readonly live: boolean;
39
39
  /** How long since the worker's last liveness refresh, in ms. */
40
40
  readonly staleMs: number;
41
+ /**
42
+ * Whether the worker's HARNESS is stale — its advertised harness protocol is below the configured
43
+ * minimum, or it advertised none at all (issue #802). Distinct from {@link Liveness} `"stale"`,
44
+ * which grades heartbeat recency. A stale harness silently swallows machine-readable artifacts
45
+ * (AgentInstance / transcript / result envelope), so its jobs dead-end at human escalations.
46
+ */
47
+ readonly harnessStale?: boolean;
48
+ /** The harness protocol version the worker advertised at enrolment, if any (issue #802). */
49
+ readonly harnessProtocol?: number;
41
50
  }
42
-
43
- /** The supply registered under one leaf token. */
44
51
  export interface SupplyLeafReport {
45
52
  readonly token: string;
46
53
  readonly workers: readonly SupplyWorkerReport[];
@@ -128,6 +135,10 @@ export interface SupplyWorkerView {
128
135
  readonly liveness: Liveness;
129
136
  /** How long since the last liveness refresh, in ms. */
130
137
  readonly staleMs: number;
138
+ /** Whether the worker's harness protocol is stale (below minimum / absent) — issue #802. */
139
+ readonly harnessStale: boolean;
140
+ /** The advertised harness protocol version, if any (issue #802). */
141
+ readonly harnessProtocol?: number;
131
142
  }
132
143
 
133
144
  /** One leaf-token section in the renderable supply view. */
@@ -212,6 +223,12 @@ function workerView(
212
223
  correlations,
213
224
  liveness: liveness(worker, staleAfterMs),
214
225
  staleMs: worker.staleMs,
226
+ // Fail loud: a report without a harness verdict (an older/cached response) has no trustworthy
227
+ // protocol assessment, so keep the worker visible as STALE until the server supplies a healthy one
228
+ // — mirrors the server's `harness?.stale ?? true` (issue #802). Defaulting to `false` here would
229
+ // let a cached response hide exactly the workers this surface is meant to expose.
230
+ harnessStale: worker.harnessStale ?? true,
231
+ ...(worker.harnessProtocol !== undefined ? { harnessProtocol: worker.harnessProtocol } : {}),
215
232
  };
216
233
  }
217
234
 
@@ -5,8 +5,11 @@ import { test } from "node:test";
5
5
  import { assert, assertEquals } from "#test-assert";
6
6
  import type { TaskDefinitionLeaf } from "@nanobpm/agentic/demand";
7
7
  import type { RegisteredWorker } from "@nanobpm/agentic/vocab";
8
+ import { HarnessProtocolRegistry } from "../../harnessProtocol.ts";
9
+ import { noopLog } from "../../../test/log.ts";
10
+ import { memDataFor } from "../../../test/worldDb.ts";
8
11
  import { CREW_VOCAB_VERSION } from "./crew-vocab.ts";
9
- import { buildRegistryReport, engineRestAddress, toWireReport } from "./demand-report.ts";
12
+ import { buildRegistryReport, computeRegistryReport, engineRestAddress, toWireReport } from "./demand-report.ts";
10
13
 
11
14
  const NOW = new Date(0);
12
15
  // A demanded taskDefinition leaf. `agentic` is the structural signal the engine reads from a task's
@@ -142,3 +145,65 @@ test("engineRestAddress strips trailing slashes from the derived NANOBPMN_BASE_U
142
145
  else process.env.NANOBPMN_BASE_URL = prevBase;
143
146
  }
144
147
  });
148
+
149
+ test("#802: toWireReport carries staleWorkers through, omitting harnessProtocol when absent", () => {
150
+ const base = buildRegistryReport({ taskDefinitions: [], workers: [seniorImpl] });
151
+ const report = {
152
+ ...base,
153
+ staleWorkers: [
154
+ { instance: "wk-old", stale: true },
155
+ { instance: "wk-low", stale: true, harnessProtocol: 0 },
156
+ ],
157
+ };
158
+ const wire = toWireReport(report);
159
+ assertEquals(wire.staleWorkers, [
160
+ { instance: "wk-old", stale: true },
161
+ { instance: "wk-low", stale: true, harnessProtocol: 0 },
162
+ ]);
163
+ });
164
+
165
+ test("#802: toWireReport omits staleWorkers entirely when the report has none (supply-only build)", () => {
166
+ const report = buildRegistryReport({ taskDefinitions: [], workers: [seniorImpl] });
167
+ assertEquals("staleWorkers" in toWireReport(report), false);
168
+ });
169
+
170
+ const HARNESS_MIGRATIONS = ["107_worker_harness_protocol.sql"];
171
+
172
+ test("#802: computeRegistryReport threads live workers through the registry into staleWorkers", async () => {
173
+ // Exercises the real assessment→staleWorkers wiring end-to-end against a mounted registry (migration
174
+ // 107) rather than hand-building `staleWorkers` before `toWireReport`: a regression in passing the
175
+ // live workers through `assessWorkersWithAvailability` would otherwise stay green.
176
+ const { data } = memDataFor(HARNESS_MIGRATIONS);
177
+ const reg = new HarnessProtocolRegistry(data);
178
+ await reg.recordEnrolment("w-front", 5); // healthy (>= default min 1)
179
+ await reg.recordEnrolment("w-kimi", 0); // below minimum → stale
180
+ // seniorImpl ("w-senior") never enrolled a protocol → absent → stale.
181
+ const report = await computeRegistryReport(noopLog(), data, [plannerFrontier, plannerKimi, seniorImpl]);
182
+ assertEquals(report.staleWorkers, [
183
+ { instance: "w-kimi", stale: true, harnessProtocol: 0 },
184
+ { instance: "w-senior", stale: true },
185
+ ]);
186
+ // The stale-harness condition is folded into the OVERALL status so the board's status pill (which
187
+ // renders only `report.status`, not `staleWorkers`) turns RED rather than staying green (issue #802).
188
+ assertEquals(report.status, "red");
189
+ });
190
+
191
+ test("#802: computeRegistryReport keeps status green when every enrolled harness is healthy", async () => {
192
+ // Guards the fold's other edge: a mounted registry with NO stale workers must NOT force `red` — the
193
+ // overall status stays whatever demand×supply produced (here green, empty demand / no missing).
194
+ const { data } = memDataFor(HARNESS_MIGRATIONS);
195
+ const reg = new HarnessProtocolRegistry(data);
196
+ await reg.recordEnrolment("w-front", 5);
197
+ await reg.recordEnrolment("w-senior", 5);
198
+ const report = await computeRegistryReport(noopLog(), data, [plannerFrontier, seniorImpl]);
199
+ assertEquals(report.staleWorkers, []);
200
+ assertEquals(report.status, "green");
201
+ });
202
+
203
+ test("#802: computeRegistryReport OMITS staleWorkers when the registry cannot be consulted", async () => {
204
+ // A legacy DB predating migration 107: the bounded read throws → registryAvailable=false, so the
205
+ // report must omit staleWorkers rather than mislabel an outage as a fleet-wide drain signal.
206
+ const { data } = memDataFor([]);
207
+ const report = await computeRegistryReport(noopLog(), data, [plannerFrontier, seniorImpl]);
208
+ assertEquals("staleWorkers" in report, false, "an outage omits staleWorkers, not report every worker stale");
209
+ });
@@ -18,9 +18,10 @@ import {
18
18
  type TaskDefinitionLeaf,
19
19
  } from "@nanobpm/agentic/demand";
20
20
  import type { RegisteredWorker } from "@nanobpm/agentic/vocab";
21
- import type { Logger } from "@nanobpm/urban";
22
- import type { RegistryReport as WireRegistryReport } from "../../../nano-generated/api-io.d.ts";
21
+ import type { DataLayer, Logger } from "@nanobpm/urban";
22
+ import type { StaleWorker, RegistryReport as WireRegistryReport } from "../../../nano-generated/api-io.d.ts";
23
23
  import { resolveEngineAddress } from "../../enginePreflight.ts";
24
+ import { assessWorkersWithAvailability, type HarnessAssessment } from "../../harnessProtocol.ts";
24
25
  import { envVar } from "../../version.ts";
25
26
  import { currentPresenceRegistry } from "../families/presence.family.ts";
26
27
  import { CREW_VOCAB_VERSION, crewResolver } from "./crew-vocab.ts";
@@ -38,6 +39,15 @@ export interface RegistryReport extends DemandSupplyReport {
38
39
  * demand is unavailable rather than silently showing "no demand".
39
40
  */
40
41
  readonly demandUnavailable: boolean;
42
+ /**
43
+ * The enrolled workers whose harness is STALE (issue #802) — below the configured minimum protocol,
44
+ * or advertising no version at all — so they may silently swallow AgentInstance / transcript /
45
+ * result-envelope artifacts and should be drained. Empty when every supplied worker is healthy;
46
+ * omitted only when the harness-protocol registry could not be consulted. When non-empty, the
47
+ * overall {@link DemandSupplyReport.status} is folded to `red` (a drain signal), because the board
48
+ * renders only `status` and does not surface this list on its own.
49
+ */
50
+ readonly staleWorkers?: readonly HarnessAssessment[];
41
51
  }
42
52
 
43
53
  /**
@@ -159,15 +169,53 @@ export function toWireReport(report: RegistryReport): WireRegistryReport {
159
169
  })),
160
170
  },
161
171
  status: report.status,
172
+ ...(report.staleWorkers !== undefined
173
+ ? {
174
+ staleWorkers: report.staleWorkers.map((w): StaleWorker => {
175
+ const out: StaleWorker = { instance: w.instance, stale: w.stale };
176
+ if (w.harnessProtocol !== undefined) out.harnessProtocol = w.harnessProtocol;
177
+ return out;
178
+ }),
179
+ }
180
+ : {}),
162
181
  };
163
182
  }
164
183
 
165
184
  /**
166
185
  * The composition path the `getAgenticRegistry` operation calls: read demand from the engine, read
167
- * supply from the presence registry, and build the report. Never throws for an engine outage — it
168
- * degrades to a supply-only report.
186
+ * supply from the presence registry, assess harness staleness (issue #802), and build the report.
187
+ * Never throws for an engine outage — it degrades to a supply-only report; the staleness assessment
188
+ * is best-effort and omitted when no data layer is mounted.
189
+ *
190
+ * `workers` defaults to the live presence feed ({@link supplyWorkers}); it is injectable so a test can
191
+ * drive the full assessment→`staleWorkers` wiring against a real registry without mounting the global
192
+ * presence family.
169
193
  */
170
- export async function computeRegistryReport(log?: Logger): Promise<RegistryReport> {
194
+ export async function computeRegistryReport(
195
+ log?: Logger,
196
+ data?: DataLayer,
197
+ workers: readonly RegisteredWorker[] = supplyWorkers(),
198
+ ): Promise<RegistryReport> {
171
199
  const taskDefinitions = await readDemand(log);
172
- return buildRegistryReport({ taskDefinitions, workers: supplyWorkers() });
200
+ const report = buildRegistryReport({ taskDefinitions, workers });
201
+ if (!data) return report;
202
+ const { registryAvailable, assessments } = await assessWorkersWithAvailability(
203
+ data,
204
+ workers.map((w) => w.instance),
205
+ );
206
+ // The registry could not be consulted (read outage / legacy DB): OMIT `staleWorkers` per the report
207
+ // contract, so an operator cannot mistake "the registry is unavailable" for "every harness is stale"
208
+ // and treat an outage as a fleet-wide drain signal (issue #802).
209
+ if (!registryAvailable) return report;
210
+ const staleWorkers = [...assessments.values()]
211
+ .filter((a) => a.stale)
212
+ .sort((a, b) => a.instance.localeCompare(b.instance));
213
+ // Fold the stale-harness condition into the OVERALL status (issue #802). The board renders only
214
+ // `report.status` as its overall pill and does not surface `staleWorkers`, so without this a
215
+ // healthy-demand report (status `green`/`amber`) would still show green while enrolled harnesses are
216
+ // stale — silently swallowing AgentInstance / transcript / result-envelope artifacts. A stale
217
+ // harness is a drain signal, so any stale worker forces the overall status to `red` (`red` is
218
+ // already the worst grade, so this never downgrades an existing `red`).
219
+ const status = staleWorkers.length > 0 ? "red" : report.status;
220
+ return { ...report, status, staleWorkers };
173
221
  }
package/app/contracts.ts CHANGED
@@ -233,6 +233,22 @@ export const ENV_CONTRACTS = {
233
233
  semantics:
234
234
  "Feature flag for the agentic supply endpoint; a value of 0/off/false/no disables it (enabled when unset).",
235
235
  },
236
+ NANO_AGENTIC_MIN_HARNESS_PROTOCOL: {
237
+ category: "env",
238
+ name: "NANO_AGENTIC_MIN_HARNESS_PROTOCOL",
239
+ owner: "app/harnessProtocol.ts",
240
+ semantics:
241
+ "Minimum worker-harness protocol version a worker must advertise at enrolment to be considered healthy (issue #802). A worker advertising a version below this — or advertising NO version at all (absent = stale) — is flagged stale in getAgenticSupply / the registry and, under NANO_AGENTIC_STALE_HARNESS_POLICY=refuse, is refused agent-job routing. Non-integer/blank degrades to the default.",
242
+ default: "1",
243
+ },
244
+ NANO_AGENTIC_STALE_HARNESS_POLICY: {
245
+ category: "env",
246
+ name: "NANO_AGENTIC_STALE_HARNESS_POLICY",
247
+ owner: "app/harnessProtocol.ts",
248
+ semantics:
249
+ "How the app treats a stale worker harness (issue #802): 'flag' (default) only marks it stale for observability/drain; 'refuse' additionally withholds its SERVE tokens at enrol so it wins no job leases. Anything other than the exact token 'refuse' is treated as 'flag' so a typo never silently drains the fleet.",
250
+ default: "flag",
251
+ },
236
252
  NANO_WORKFORCE_GIT_SHA: {
237
253
  category: "env",
238
254
  name: "NANO_WORKFORCE_GIT_SHA",
@@ -562,6 +578,14 @@ export const TYPE_CONTRACTS = {
562
578
  "The `durable-resume` ENROLMENT GATE (issue #325, ADR 0062 Slice 5/5, the INTEGRATION slice). `durable-resume` is a worker attribute declared at enrolment (ADR 0056 §7 — capability gates enrolment, NEVER the routing token `network.role#seat`), recorded per worker instance in `worker_durable_resume` (migration 052). The enrol door (`operations/enrolAgenticWorker.ts`) records it via `recordEnrolment`; `app/service.ts` consults `fleetSupportsDurableResume` before emitting the world-restore `commitSha` (the `io.nanobpm.agentTask.repository` envelope) so a re-leased `senior:pr-review` round RESUMES only on a participating fleet and gracefully DEGRADES (redriven from scratch) otherwise. Consume this ONE module for the durable-resume gate — do not re-declare a synonym or read the flag off a second store.",
563
579
  module: "app/durableResume.ts",
564
580
  },
581
+ HarnessProtocolRegistry: {
582
+ category: "type",
583
+ name: "HarnessProtocolRegistry",
584
+ owner: "app/harnessProtocol.ts",
585
+ semantics:
586
+ "The durable registry of per-worker advertised harness protocol version (issue #802), over `worker_harness_protocol` (migration 107) through the RAD `Table<T>` surface — mirroring {@link DurableResumeRegistry}. `harness-protocol` is a worker ATTRIBUTE advertised at enrolment (ADR 0056 §7 — capability gates enrolment, NEVER the routing token `network.role#seat`), recorded per worker instance by `recordEnrolment` from the enrol door (`operations/enrolAgenticWorker.ts`); a MISSING version is first-class STALE. It is the ONE shared source consumed by enrolment (record), supply (`getAgenticSupply` staleness verdict) and registry reporting (`computeRegistryReport` → `staleWorkers`, which folds a non-empty stale set into the overall red drain signal). The bounded `protocolsFor(instances)` read scopes to the live presence keys via a single `WHERE instance IN (…)` query — never an N+1 per-worker `findOne`. Consume this ONE module for the harness-protocol gate — do not re-declare a synonym or read the version off a second store.",
587
+ module: "app/harnessProtocol.ts",
588
+ },
565
589
  } as const satisfies Record<string, TypeContract>;
566
590
 
567
591
  export const CAPABILITY_URL_CONTRACTS = {
@@ -0,0 +1,170 @@
1
+ // Tests for the harness-protocol enrolment gate (issue #802) — the env knobs, the staleness
2
+ // derivation, and the durable registry over `worker_harness_protocol` (migration 107).
3
+ import { test } from "node:test";
4
+ import { assert, assertEquals } from "#test-assert";
5
+ import {
6
+ assessWorkers,
7
+ assessWorkersWithAvailability,
8
+ HarnessProtocolRegistry,
9
+ isStaleProtocol,
10
+ minHarnessProtocol,
11
+ staleHarnessPolicy,
12
+ } from "./harnessProtocol.ts";
13
+ import { memDataFor } from "../test/worldDb.ts";
14
+
15
+ const MIGRATIONS = ["107_worker_harness_protocol.sql"];
16
+
17
+ test("minHarnessProtocol defaults to 1 and reads the declared env knob", () => {
18
+ assertEquals(minHarnessProtocol({}), 1);
19
+ assertEquals(minHarnessProtocol({ NANO_AGENTIC_MIN_HARNESS_PROTOCOL: "3" }), 3);
20
+ // A malformed/blank value degrades to the default rather than NaN-poisoning the gate.
21
+ assertEquals(minHarnessProtocol({ NANO_AGENTIC_MIN_HARNESS_PROTOCOL: "nonsense" }), 1);
22
+ });
23
+
24
+ test("minHarnessProtocol rejects parseInt-lenient values (Copilot #802): '3junk'/'1.9'/blank → default", () => {
25
+ // `Number.parseInt` would accept "3junk" (→ 3) and truncate "1.9" (→ 1); a strict integer parse
26
+ // degrades all of these to the registered default so a malformed knob never silently shifts the gate.
27
+ assertEquals(minHarnessProtocol({ NANO_AGENTIC_MIN_HARNESS_PROTOCOL: "3junk" }), 1);
28
+ assertEquals(minHarnessProtocol({ NANO_AGENTIC_MIN_HARNESS_PROTOCOL: "1.9" }), 1);
29
+ assertEquals(minHarnessProtocol({ NANO_AGENTIC_MIN_HARNESS_PROTOCOL: "" }), 1);
30
+ assertEquals(minHarnessProtocol({ NANO_AGENTIC_MIN_HARNESS_PROTOCOL: " " }), 1);
31
+ assertEquals(minHarnessProtocol({ NANO_AGENTIC_MIN_HARNESS_PROTOCOL: "-2" }), 1);
32
+ // A clean integer (with surrounding whitespace) still parses.
33
+ assertEquals(minHarnessProtocol({ NANO_AGENTIC_MIN_HARNESS_PROTOCOL: " 4 " }), 4);
34
+ assertEquals(minHarnessProtocol({ NANO_AGENTIC_MIN_HARNESS_PROTOCOL: "0" }), 0);
35
+ });
36
+
37
+ test("staleHarnessPolicy defaults to flag; only the exact 'refuse' token opts into refusal", () => {
38
+ assertEquals(staleHarnessPolicy({}), "flag");
39
+ assertEquals(staleHarnessPolicy({ NANO_AGENTIC_STALE_HARNESS_POLICY: "refuse" }), "refuse");
40
+ assertEquals(staleHarnessPolicy({ NANO_AGENTIC_STALE_HARNESS_POLICY: " REFUSE " }), "refuse");
41
+ assertEquals(staleHarnessPolicy({ NANO_AGENTIC_STALE_HARNESS_POLICY: "flagg" }), "flag");
42
+ });
43
+
44
+ test("isStaleProtocol: absent version is stale; below-minimum is stale; at-or-above is healthy", () => {
45
+ assertEquals(isStaleProtocol(undefined, 1), true, "no version advertised = stale");
46
+ assertEquals(isStaleProtocol(null, 1), true, "null version = stale");
47
+ assertEquals(isStaleProtocol(0, 1), true, "below minimum = stale");
48
+ assertEquals(isStaleProtocol(1, 1), false, "at minimum = healthy");
49
+ assertEquals(isStaleProtocol(5, 1), false, "above minimum = healthy");
50
+ });
51
+
52
+ test("registry records the advertised protocol and reads it back (idempotent upsert)", async () => {
53
+ const { data } = memDataFor(MIGRATIONS);
54
+ const reg = new HarnessProtocolRegistry(data);
55
+ await reg.recordEnrolment("wk-1", 2);
56
+ assertEquals(await reg.protocolFor("wk-1"), 2);
57
+ // Re-enrol overwrites (upsert keyed by instance).
58
+ await reg.recordEnrolment("wk-1", 4);
59
+ assertEquals(await reg.protocolFor("wk-1"), 4);
60
+ });
61
+
62
+ test("a downgrade re-enrol WITHOUT a version clears a stale-healthy value to absent (stale)", async () => {
63
+ const { data } = memDataFor(MIGRATIONS);
64
+ const reg = new HarnessProtocolRegistry(data);
65
+ await reg.recordEnrolment("wk-1", 3);
66
+ assertEquals(await reg.protocolFor("wk-1"), 3);
67
+ await reg.recordEnrolment("wk-1", undefined);
68
+ assertEquals(await reg.protocolFor("wk-1"), undefined, "stale-healthy value cleared to NULL");
69
+ });
70
+
71
+ test("a blank/whitespace instance is a no-op (no unreachable/colliding row)", async () => {
72
+ const { data } = memDataFor(MIGRATIONS);
73
+ const reg = new HarnessProtocolRegistry(data);
74
+ await reg.recordEnrolment(" ", 2);
75
+ assertEquals((await reg.all()).size, 0);
76
+ });
77
+
78
+ test("assessWorkers: flags absent/below-min as stale, at-or-above as healthy (canonical derivation)", async () => {
79
+ const { data } = memDataFor(MIGRATIONS);
80
+ const reg = new HarnessProtocolRegistry(data);
81
+ await reg.recordEnrolment("healthy", 2);
82
+ await reg.recordEnrolment("old", 0);
83
+ await reg.recordEnrolment("versionless", undefined);
84
+ // "never-enrolled" has no row at all.
85
+ const out = await assessWorkers(data, ["healthy", "old", "versionless", "never-enrolled"], {
86
+ NANO_AGENTIC_MIN_HARNESS_PROTOCOL: "1",
87
+ });
88
+ assertEquals(out.get("healthy"), { instance: "healthy", harnessProtocol: 2, stale: false });
89
+ assertEquals(out.get("old"), { instance: "old", harnessProtocol: 0, stale: true });
90
+ assertEquals(out.get("versionless"), { instance: "versionless", stale: true });
91
+ assertEquals(out.get("never-enrolled"), { instance: "never-enrolled", stale: true });
92
+ });
93
+
94
+ test("protocolsFor: one bounded IN query maps each ORIGINAL key back, skips blanks, short-circuits empty", async () => {
95
+ // The hot-path read is a single bounded `WHERE instance IN (…)` over the live set (not a per-worker
96
+ // findOne / O(history) scan): assert it maps each key back correctly and handles the edge sets.
97
+ const { data } = memDataFor(MIGRATIONS);
98
+ const reg = new HarnessProtocolRegistry(data);
99
+ await reg.recordEnrolment("a", 3);
100
+ await reg.recordEnrolment("b", undefined); // NULL row → undefined
101
+ // "c" never enrolled.
102
+ const got = await reg.protocolsFor(["a", "b", "c", " "]);
103
+ assertEquals(got.get("a"), 3);
104
+ assertEquals(got.get("b"), undefined);
105
+ assertEquals(got.get("c"), undefined);
106
+ assertEquals(got.has(" "), false, "a blank instance keys no row and is skipped");
107
+ // An empty (or all-blank) set short-circuits without issuing an `IN ()` query.
108
+ assertEquals((await reg.protocolsFor([])).size, 0);
109
+ assertEquals((await reg.protocolsFor([" "])).size, 0);
110
+ });
111
+
112
+ test("protocolsFor chunks past SQLite's host-parameter cap (a large fleet does not overflow one IN query)", async () => {
113
+ // A fleet larger than a single IN(…) batch must still resolve every key: the read is chunked under
114
+ // SQLite's ~999 host-parameter floor, so scale never throws (which the caller would mislabel as a
115
+ // fleet-wide outage marking everyone stale). Exercise > 900 (two batches) plus a boundary key.
116
+ const { data } = memDataFor(MIGRATIONS);
117
+ const reg = new HarnessProtocolRegistry(data);
118
+ const instances: string[] = [];
119
+ for (let i = 0; i < 1500; i++) {
120
+ const id = `wk-${i}`;
121
+ instances.push(id);
122
+ if (i % 2 === 0) await reg.recordEnrolment(id, 2); // even = enrolled@2, odd = never enrolled
123
+ }
124
+ const got = await reg.protocolsFor(instances);
125
+ assertEquals(got.size, 1500, "every requested key is mapped back across batches");
126
+ assertEquals(got.get("wk-0"), 2);
127
+ assertEquals(got.get("wk-900"), 2, "a key in the second batch still resolves");
128
+ assertEquals(got.get("wk-1"), undefined, "a never-enrolled key reads back undefined");
129
+ assertEquals(got.get("wk-1499"), undefined);
130
+ });
131
+
132
+ test("assessWorkers with no data layer treats every worker as stale (fail loud)", async () => {
133
+ const out = await assessWorkers(undefined, ["a", "b"]);
134
+ assertEquals(out.get("a")?.stale, true);
135
+ assertEquals(out.get("b")?.stale, true);
136
+ assert(!("harnessProtocol" in (out.get("a") ?? {})), "no protocol known without a registry");
137
+ });
138
+
139
+ test("assessWorkersWithAvailability: registryAvailable is true on a successful read, false without a data layer", async () => {
140
+ const { data } = memDataFor(MIGRATIONS);
141
+ await new HarnessProtocolRegistry(data).recordEnrolment("healthy", 2);
142
+ const ok = await assessWorkersWithAvailability(data, ["healthy"], { NANO_AGENTIC_MIN_HARNESS_PROTOCOL: "1" });
143
+ assertEquals(ok.registryAvailable, true);
144
+ assertEquals(ok.assessments.get("healthy")?.stale, false);
145
+
146
+ const noData = await assessWorkersWithAvailability(undefined, ["healthy"]);
147
+ assertEquals(noData.registryAvailable, false, "no data layer = registry could not be consulted");
148
+ assertEquals(noData.assessments.get("healthy")?.stale, true, "still fails loud per-worker");
149
+ });
150
+
151
+ test("assessWorkersWithAvailability: a registry read outage reports registryAvailable=false (not silent all-stale)", async () => {
152
+ // A legacy DB predating migration 107: the table is absent, so the bounded read throws and is caught.
153
+ const { data } = memDataFor([]);
154
+ const res = await assessWorkersWithAvailability(data, ["a", "b"]);
155
+ assertEquals(res.registryAvailable, false, "read failure surfaces as unavailable, distinct from all-healthy");
156
+ assertEquals(res.assessments.get("a")?.stale, true, "assessments still fail loud");
157
+ });
158
+
159
+ test("protocolsFor reads only the requested instances (bounded), not the whole history", async () => {
160
+ const { data } = memDataFor(MIGRATIONS);
161
+ const reg = new HarnessProtocolRegistry(data);
162
+ await reg.recordEnrolment("live-1", 2);
163
+ await reg.recordEnrolment("live-2", undefined);
164
+ await reg.recordEnrolment("disconnected-history", 3);
165
+ const scoped = await reg.protocolsFor(["live-1", "live-2", "never-enrolled"]);
166
+ assertEquals(scoped.get("live-1"), 2);
167
+ assertEquals(scoped.get("live-2"), undefined, "an enrolled-but-versionless row reads as undefined");
168
+ assertEquals(scoped.get("never-enrolled"), undefined, "an absent row reads as undefined");
169
+ assertEquals(scoped.has("disconnected-history"), false, "a historical instance outside the live set is never read");
170
+ });