@nanobpm/nano-workforce 0.120.0 → 0.120.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.
- package/CHANGELOG.md +15 -0
- package/app/capabilityNeed.test.ts +4 -2
- package/app/capabilityNeed.ts +3 -1
- package/app/deliveryGraphCompiler.test.ts +72 -44
- package/app/deliveryGraphCompiler.ts +121 -19
- package/app/deliveryGraphRun.test.ts +2 -2
- package/app/deliveryRunner.test.ts +29 -17
- package/app/deliveryRunner.ts +31 -10
- package/app/feature.test.ts +3 -1
- package/app/feature.ts +9 -0
- package/app/featureReadiness.test.ts +7 -4
- package/app/featureReadiness.ts +8 -3
- package/app/plan.test.ts +1 -1
- package/app/plan.ts +9 -0
- package/app/planFanoutPreflight.test.ts +12 -12
- package/app/planLowering.test.ts +2 -0
- package/app/planLowering.ts +7 -2
- package/app/pollUserTasks.test.ts +49 -1
- package/app/readiness.test.ts +9 -0
- package/app/readiness.ts +25 -0
- package/app/service.ts +24 -6
- package/biome.json +24 -1
- package/e2e/delivery-graph.e2e.ts +2 -1
- package/e2e/feature-preflight.e2e.ts +2 -0
- package/e2e/inter-epic-dependency.e2e.ts +7 -1
- package/e2e/plan-fanout-preflight.e2e.ts +2 -0
- package/e2e/readiness-gate.e2e.ts +27 -11
- package/operations/compileDeliveryGraph.test.ts +3 -0
- package/operations/compileDeliveryGraph.ts +1 -1
- package/operations/dispatchDeliveryGraph.ts +1 -1
- package/operations/previewDeliveryGraph.ts +1 -1
- package/operations/startDeliveryGraph.ts +1 -1
- package/package.json +3 -2
- package/resources/processes/feature.bpmn +168 -52
- package/resources/processes/plan-fanout.bpmn +168 -52
- package/resources/processes/readiness-gate.bpmn +194 -84
- package/workers/readiness-probe/worker.test.ts +82 -238
- package/workers/readiness-probe/worker.ts +46 -128
|
@@ -1,32 +1,15 @@
|
|
|
1
|
-
// Unit coverage for the pr.readiness-probe
|
|
2
|
-
//
|
|
3
|
-
// The loop is factored out (`pollUntilReady`) with injectable I/O, clock, and publisher so the two
|
|
4
|
-
// load-bearing behaviours are proven without a network, a subprocess, or a real timer:
|
|
5
|
-
// • ready → it publishes the `readiness-ready` message exactly once and returns ready;
|
|
6
|
-
// • never-ready → it EXHAUSTS its local budget and returns not-ready WITHOUT publishing (the
|
|
7
|
-
// engine timer, not the worker, is the authoritative bound) — and it must not loop forever.
|
|
1
|
+
// Unit coverage for the pr.readiness-probe single-shot worker (workers/readiness-probe/worker.ts, #428).
|
|
8
2
|
import { test } from "node:test";
|
|
9
3
|
import { assert, assertEquals, assertRejects } from "#test-assert";
|
|
10
4
|
import type { CommandResult, HttpResponse, ProbeExec, ReadinessProbe } from "../../app/readiness.ts";
|
|
11
5
|
import { parseProbe } from "../../app/readiness.ts";
|
|
12
|
-
import handler, {
|
|
6
|
+
import handler, { probeSingleShot, READINESS_READY_MESSAGE, readGateVars, safeBind } from "./worker.ts";
|
|
13
7
|
|
|
14
|
-
|
|
15
|
-
// probe races to its deadline in zero real time (no setTimeout) and the test can never hang.
|
|
16
|
-
function fakeClock() {
|
|
17
|
-
let t = 0;
|
|
18
|
-
return {
|
|
19
|
-
now: () => t,
|
|
20
|
-
wait: async (ms: number) => {
|
|
21
|
-
t += ms;
|
|
22
|
-
},
|
|
23
|
-
};
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function execReturning(seq: Array<HttpResponse>): ProbeExec {
|
|
8
|
+
function execReturning(seq: Array<HttpResponse>, counts: { http: number } = { http: 0 }): ProbeExec {
|
|
27
9
|
let i = 0;
|
|
28
10
|
return {
|
|
29
11
|
async httpGet() {
|
|
12
|
+
counts.http += 1;
|
|
30
13
|
const r = seq[Math.min(i, seq.length - 1)];
|
|
31
14
|
i += 1;
|
|
32
15
|
return r;
|
|
@@ -37,8 +20,8 @@ function execReturning(seq: Array<HttpResponse>): ProbeExec {
|
|
|
37
20
|
};
|
|
38
21
|
}
|
|
39
22
|
|
|
40
|
-
const httpProbe = (poll
|
|
41
|
-
parseProbe({ kind: "http", target: "https://x/health", poll });
|
|
23
|
+
const httpProbe = (poll?: ReadinessProbe["poll"]): ReadinessProbe =>
|
|
24
|
+
parseProbe({ kind: "http", target: "https://x/health?token=s3cr3t", poll });
|
|
42
25
|
|
|
43
26
|
test("safeBind: strips reserved keys (ready/detail) so a bind can only ADD outputs, never shadow the payload", () => {
|
|
44
27
|
const cleaned = safeBind({ resolvedArtifact: "@nanobpm/urban@0.54.0", ready: "false", detail: "spoofed" });
|
|
@@ -48,99 +31,46 @@ test("safeBind: strips reserved keys (ready/detail) so a bind can only ADD outpu
|
|
|
48
31
|
assertEquals(Object.keys(safeBind(undefined)).length, 0, "an absent bind yields an empty object");
|
|
49
32
|
});
|
|
50
33
|
|
|
51
|
-
test("
|
|
52
|
-
const clock = fakeClock();
|
|
53
|
-
const seen: string[] = [];
|
|
54
|
-
let publishes = 0;
|
|
55
|
-
const res = await pollUntilReady({
|
|
56
|
-
probe: httpProbe({ everyMs: 5, timeoutMs: 30, backoff: "fixed" }),
|
|
57
|
-
gateKey: "gate-fallback-throws",
|
|
58
|
-
exec: execReturning([{ status: 503, body: "" }]),
|
|
59
|
-
env: {},
|
|
60
|
-
now: clock.now,
|
|
61
|
-
wait: clock.wait,
|
|
62
|
-
publish: async () => {
|
|
63
|
-
publishes += 1;
|
|
64
|
-
},
|
|
65
|
-
fallback: async () => {
|
|
66
|
-
throw new Error("boom at https://h/p?token=s3cr3t");
|
|
67
|
-
},
|
|
68
|
-
log: (msg) => seen.push(msg),
|
|
69
|
-
});
|
|
70
|
-
assert(!res.ready, "a throwing fallback keeps the not-ready outcome for the engine timer");
|
|
71
|
-
assertEquals(publishes, 0, "nothing is published when the fallback throws");
|
|
72
|
-
const all = seen.join("\n");
|
|
73
|
-
assert(all.includes("fallback error: Error"), "the fallback error is logged by class name");
|
|
74
|
-
assert(!all.includes("s3cr3t"), "the raw error message (with its secret) must not leak");
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
test("pollUntilReady: publishes readiness-ready once and returns ready when a probe goes green", async () => {
|
|
78
|
-
const clock = fakeClock();
|
|
34
|
+
test("probeSingleShot: publishes readiness-ready once and returns ready when a probe is green", async () => {
|
|
79
35
|
const published: Array<{ detail: string }> = [];
|
|
80
|
-
const
|
|
81
|
-
const res = await
|
|
82
|
-
probe: httpProbe({ everyMs:
|
|
83
|
-
|
|
84
|
-
exec,
|
|
36
|
+
const counts = { http: 0 };
|
|
37
|
+
const res = await probeSingleShot({
|
|
38
|
+
probe: httpProbe({ everyMs: 1, timeoutMs: 60_000, backoff: "fixed" }),
|
|
39
|
+
exec: execReturning([{ status: 200, body: "ok" }], counts),
|
|
85
40
|
env: {},
|
|
86
|
-
now: clock.now,
|
|
87
|
-
wait: clock.wait,
|
|
88
41
|
publish: async (detail) => {
|
|
89
42
|
published.push({ detail });
|
|
90
43
|
},
|
|
91
44
|
});
|
|
92
|
-
assert(res.ready, "the probe
|
|
45
|
+
assert(res.ready, "the probe reported ready");
|
|
46
|
+
assertEquals(counts.http, 1, "a single activation performs exactly one probe I/O");
|
|
93
47
|
assertEquals(published.length, 1, "exactly one readiness message was published");
|
|
94
48
|
});
|
|
95
49
|
|
|
96
|
-
test("
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
const exec: ProbeExec = {
|
|
103
|
-
async httpGet() {
|
|
104
|
-
return { status: 0, body: "" };
|
|
105
|
-
},
|
|
106
|
-
async run(): Promise<CommandResult> {
|
|
107
|
-
return { code: 0, stdout: payload, stderr: "" };
|
|
108
|
-
},
|
|
109
|
-
};
|
|
110
|
-
const res = await pollUntilReady({
|
|
111
|
-
probe: parseProbe({
|
|
112
|
-
kind: "capability",
|
|
113
|
-
target: "github-releases:nanobpm/nano-ide",
|
|
114
|
-
match: { capabilityRef: "nano-ide#274", package: "@nanobpm/urban" },
|
|
115
|
-
poll: { everyMs: 5, timeoutMs: 5000, backoff: "fixed" },
|
|
116
|
-
}),
|
|
117
|
-
gateKey: "gate-cap",
|
|
118
|
-
exec,
|
|
50
|
+
test("probeSingleShot: not-ready and not lastAttempt returns without publishing", async () => {
|
|
51
|
+
let publishes = 0;
|
|
52
|
+
const counts = { http: 0 };
|
|
53
|
+
const res = await probeSingleShot({
|
|
54
|
+
probe: httpProbe({ everyMs: 1, timeoutMs: 60_000, backoff: "fixed" }),
|
|
55
|
+
exec: execReturning([{ status: 503, body: "" }], counts),
|
|
119
56
|
env: {},
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
publish: async (detail, bind) => {
|
|
123
|
-
published.push({ detail, bind });
|
|
57
|
+
publish: async () => {
|
|
58
|
+
publishes += 1;
|
|
124
59
|
},
|
|
125
60
|
});
|
|
126
|
-
assert(res.ready, "the
|
|
127
|
-
assertEquals(
|
|
128
|
-
assertEquals(
|
|
61
|
+
assert(!res.ready, "the probe reported not-ready");
|
|
62
|
+
assertEquals(counts.http, 1, "regression guard: the worker has no wall-clock retry loop");
|
|
63
|
+
assertEquals(publishes, 0, "a not-ready non-final activation never publishes");
|
|
129
64
|
});
|
|
130
65
|
|
|
131
|
-
test("
|
|
132
|
-
// Deterministic provenance never resolves (no matching release), so the loop exhausts its budget —
|
|
133
|
-
// the gate boundary. The fallback thunk then verifies empirically and publishes a bound version.
|
|
134
|
-
const clock = fakeClock();
|
|
66
|
+
test("probeSingleShot: lastAttempt fallback fires once, can resolve ready, and publishes bound outputs", async () => {
|
|
135
67
|
const published: Array<{ bind?: Record<string, string> }> = [];
|
|
136
68
|
let fallbackCalls = 0;
|
|
137
|
-
const res = await
|
|
138
|
-
probe: httpProbe(
|
|
139
|
-
gateKey: "gate-fallback",
|
|
69
|
+
const res = await probeSingleShot({
|
|
70
|
+
probe: httpProbe(),
|
|
140
71
|
exec: execReturning([{ status: 503, body: "" }]),
|
|
141
72
|
env: {},
|
|
142
|
-
|
|
143
|
-
wait: clock.wait,
|
|
73
|
+
lastAttempt: true,
|
|
144
74
|
publish: async (_detail, bind) => {
|
|
145
75
|
published.push({ bind });
|
|
146
76
|
},
|
|
@@ -150,110 +80,103 @@ test("pollUntilReady: the gated fallback fires ONCE at budget exhaustion and can
|
|
|
150
80
|
},
|
|
151
81
|
});
|
|
152
82
|
assert(res.ready, "the boundary fallback resolved the edge");
|
|
153
|
-
assertEquals(fallbackCalls, 1, "the fallback fires exactly once
|
|
83
|
+
assertEquals(fallbackCalls, 1, "the fallback fires exactly once at the boundary");
|
|
84
|
+
assertEquals(published.length, 1, "the fallback ready result publishes once");
|
|
154
85
|
assertEquals(published[0]?.bind?.resolvedArtifact, "@nanobpm/urban@0.60.0");
|
|
155
86
|
});
|
|
156
87
|
|
|
157
|
-
test("
|
|
158
|
-
const
|
|
88
|
+
test("probeSingleShot: lastAttempt fallback throwing is caught by class name and stays not-ready", async () => {
|
|
89
|
+
const seen: string[] = [];
|
|
159
90
|
let publishes = 0;
|
|
160
|
-
const res = await
|
|
161
|
-
probe: httpProbe(
|
|
162
|
-
gateKey: "gate-fallback-noop",
|
|
91
|
+
const res = await probeSingleShot({
|
|
92
|
+
probe: httpProbe(),
|
|
163
93
|
exec: execReturning([{ status: 503, body: "" }]),
|
|
164
94
|
env: {},
|
|
165
|
-
|
|
166
|
-
wait: clock.wait,
|
|
95
|
+
lastAttempt: true,
|
|
167
96
|
publish: async () => {
|
|
168
97
|
publishes += 1;
|
|
169
98
|
},
|
|
170
|
-
fallback: async () =>
|
|
99
|
+
fallback: async () => {
|
|
100
|
+
throw new Error("boom at https://h/p?token=fallback-secret");
|
|
101
|
+
},
|
|
102
|
+
log: (msg) => seen.push(msg),
|
|
171
103
|
});
|
|
172
|
-
assert(!res.ready, "
|
|
173
|
-
assertEquals(publishes, 0, "
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
);
|
|
104
|
+
assert(!res.ready, "a throwing fallback keeps the not-ready outcome for the engine timer");
|
|
105
|
+
assertEquals(publishes, 0, "nothing is published when the fallback throws");
|
|
106
|
+
const all = [res.detail, ...seen].join("\n");
|
|
107
|
+
assert(all.includes("fallback error: Error"), "the fallback error is logged by class name");
|
|
108
|
+
assert(!all.includes("fallback-secret"), "the raw error message must not leak");
|
|
178
109
|
});
|
|
179
110
|
|
|
180
|
-
test("
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
const res = await pollUntilReady({
|
|
185
|
-
probe: httpProbe({ everyMs: 5, timeoutMs: 100, backoff: "fixed" }),
|
|
186
|
-
gateKey: "gate-2",
|
|
187
|
-
exec,
|
|
111
|
+
test("probeSingleShot: inconclusive lastAttempt fallback surfaces its redacted detail", async () => {
|
|
112
|
+
const res = await probeSingleShot({
|
|
113
|
+
probe: httpProbe(),
|
|
114
|
+
exec: execReturning([{ status: 503, body: "" }]),
|
|
188
115
|
env: {},
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
publishes += 1;
|
|
193
|
-
},
|
|
116
|
+
lastAttempt: true,
|
|
117
|
+
publish: async () => {},
|
|
118
|
+
fallback: async () => ({ ready: false, detail: "still nothing" }),
|
|
194
119
|
});
|
|
195
|
-
assert(!res.ready, "
|
|
196
|
-
|
|
197
|
-
assert(clock.now() <= 100, "the loop stopped at (or before) its declared budget");
|
|
120
|
+
assert(!res.ready, "an inconclusive fallback keeps not-ready");
|
|
121
|
+
assert(res.detail.includes("fallback: still nothing"), "the fallback diagnostic is preserved");
|
|
198
122
|
});
|
|
199
123
|
|
|
200
|
-
test("
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
124
|
+
test("probeSingleShot: forwards a matcher's bind through publish into the message variables (#274 Gap B)", async () => {
|
|
125
|
+
const published: Array<{ detail: string; bind?: Record<string, string> }> = [];
|
|
126
|
+
const payload = JSON.stringify([{ tag_name: "@nanobpm/urban@0.54.0", body: "## Provenance\n- #274\n" }]);
|
|
127
|
+
const exec: ProbeExec = {
|
|
128
|
+
async httpGet() {
|
|
129
|
+
return { status: 0, body: "" };
|
|
130
|
+
},
|
|
131
|
+
async run(): Promise<CommandResult> {
|
|
132
|
+
return { code: 0, stdout: payload, stderr: "" };
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
const res = await probeSingleShot({
|
|
136
|
+
probe: parseProbe({
|
|
137
|
+
kind: "capability",
|
|
138
|
+
target: "github-releases:nanobpm/nano-ide",
|
|
139
|
+
match: { capabilityRef: "nano-ide#274", package: "@nanobpm/urban" },
|
|
140
|
+
}),
|
|
215
141
|
exec,
|
|
216
142
|
env: {},
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
publish: async () => {
|
|
220
|
-
publishes += 1;
|
|
143
|
+
publish: async (detail, bind) => {
|
|
144
|
+
published.push({ detail, bind });
|
|
221
145
|
},
|
|
222
146
|
});
|
|
223
|
-
assert(res.ready, "the
|
|
224
|
-
assertEquals(
|
|
225
|
-
|
|
147
|
+
assert(res.ready, "the capability edge resolved");
|
|
148
|
+
assertEquals(published.length, 1, "exactly one readiness message was published");
|
|
149
|
+
assertEquals(published[0]?.bind?.resolvedArtifact, "@nanobpm/urban@0.54.0", "the resolved artifact flowed through the bind");
|
|
226
150
|
});
|
|
227
151
|
|
|
228
|
-
test("
|
|
229
|
-
const clock = fakeClock();
|
|
152
|
+
test("probeSingleShot: I/O throw is caught, raw message not leaked, and no retry loop runs", async () => {
|
|
230
153
|
const seen: string[] = [];
|
|
154
|
+
let calls = 0;
|
|
231
155
|
const exec: ProbeExec = {
|
|
232
156
|
async httpGet() {
|
|
157
|
+
calls += 1;
|
|
233
158
|
throw new Error("connection refused to https://h/p?token=s3cr3t");
|
|
234
159
|
},
|
|
235
160
|
async run(): Promise<CommandResult> {
|
|
236
161
|
return { code: 1, stdout: "", stderr: "" };
|
|
237
162
|
},
|
|
238
163
|
};
|
|
239
|
-
const res = await
|
|
240
|
-
probe: httpProbe({ everyMs:
|
|
241
|
-
gateKey: "gate-3",
|
|
164
|
+
const res = await probeSingleShot({
|
|
165
|
+
probe: httpProbe({ everyMs: 1, timeoutMs: 60_000, backoff: "fixed" }),
|
|
242
166
|
exec,
|
|
243
167
|
env: {},
|
|
244
|
-
now: clock.now,
|
|
245
|
-
wait: clock.wait,
|
|
246
168
|
publish: async () => {},
|
|
247
169
|
log: (msg) => seen.push(msg),
|
|
248
170
|
});
|
|
249
171
|
assert(!res.ready, "a transport failure is a transient not-ready, not a crash");
|
|
172
|
+
assertEquals(calls, 1, "regression guard: an I/O throw does not enter an internal retry loop");
|
|
250
173
|
const all = [res.detail, ...seen].join("\n");
|
|
251
174
|
assert(!all.includes("s3cr3t"), "the raw error message (with its secret) must not leak");
|
|
252
175
|
assert(!all.includes("connection refused"), "only the error class is surfaced, not the message");
|
|
253
176
|
});
|
|
254
177
|
|
|
255
178
|
test("handler: a blank gateKey fails fast (an empty correlationKey would never release the gate)", async () => {
|
|
256
|
-
const job = { variables: { probe: { kind: "http", target: "https://x/health" }, gateKey: " " } };
|
|
179
|
+
const job = { variables: { probe: { kind: "http", target: "https://x/health" }, gateKey: " ", probeTimeout: "PT30M" } };
|
|
257
180
|
await assertRejects(
|
|
258
181
|
// biome-ignore lint/suspicious/noExplicitAny: minimal job/app stub for the fail-fast guard.
|
|
259
182
|
() => handler(job as any, {} as any),
|
|
@@ -262,87 +185,11 @@ test("handler: a blank gateKey fails fast (an empty correlationKey would never r
|
|
|
262
185
|
);
|
|
263
186
|
});
|
|
264
187
|
|
|
265
|
-
test("pollUntilReady: the poll cadence reads NANO_READINESS_POLL_EVERY_MS from the injected env, not ambient process.env", async () => {
|
|
266
|
-
const clock = fakeClock();
|
|
267
|
-
// Descriptor omits everyMs, so the cadence falls back to the env contract. A small injected value
|
|
268
|
-
// (25ms) makes the loop step through its 100ms budget; the ambient default (15_000ms) would blow
|
|
269
|
-
// the budget on the first wait and bail at now()=0. Asserting the clock advanced proves the
|
|
270
|
-
// injected env — not process.env — drove the cadence.
|
|
271
|
-
const exec = execReturning([{ status: 503, body: "" }]);
|
|
272
|
-
const res = await pollUntilReady({
|
|
273
|
-
probe: httpProbe({ timeoutMs: 100, backoff: "fixed" }),
|
|
274
|
-
gateKey: "gate-env",
|
|
275
|
-
exec,
|
|
276
|
-
env: { NANO_READINESS_POLL_EVERY_MS: "25" },
|
|
277
|
-
now: clock.now,
|
|
278
|
-
wait: clock.wait,
|
|
279
|
-
publish: async () => {},
|
|
280
|
-
});
|
|
281
|
-
assert(!res.ready, "the never-green probe exhausted its budget");
|
|
282
|
-
assert(clock.now() > 0, "the injected env's 25ms cadence stepped the clock (ambient default would bail at 0)");
|
|
283
|
-
});
|
|
284
|
-
|
|
285
|
-
test("pollUntilReady: an omitted poll.timeoutMs takes the budget from NANO_READINESS_POLL_TIMEOUT, not the built-in 30m", async () => {
|
|
286
|
-
const clock = fakeClock();
|
|
287
|
-
// Regression for the worker/gate timeout drift: the gate's engine timer derives from
|
|
288
|
-
// NANO_READINESS_POLL_TIMEOUT when the descriptor omits poll.timeoutMs, but the worker used to
|
|
289
|
-
// fall back to the hard-coded 30m default. With a 45m env budget and no descriptor timeout, the
|
|
290
|
-
// never-green probe must keep polling PAST 30m (up to the 45m the gate itself waits) so it can't
|
|
291
|
-
// go silent while the gate is still waiting. everyMs is clamped to MAX_EVERY_MS (5m), so ~9 waits
|
|
292
|
-
// land the clock beyond 30m only if the env budget — not the 30m default — is in force.
|
|
293
|
-
const exec = execReturning([{ status: 503, body: "" }]);
|
|
294
|
-
const THIRTY_MIN = 30 * 60_000;
|
|
295
|
-
const res = await pollUntilReady({
|
|
296
|
-
probe: httpProbe({ everyMs: 5 * 60_000, backoff: "fixed" }),
|
|
297
|
-
gateKey: "gate-timeout-env",
|
|
298
|
-
exec,
|
|
299
|
-
env: { NANO_READINESS_POLL_TIMEOUT: "PT45M" },
|
|
300
|
-
now: clock.now,
|
|
301
|
-
wait: clock.wait,
|
|
302
|
-
publish: async () => {},
|
|
303
|
-
});
|
|
304
|
-
assert(!res.ready, "the never-green probe exhausted its budget");
|
|
305
|
-
assert(
|
|
306
|
-
clock.now() > THIRTY_MIN,
|
|
307
|
-
`the worker honored the 45m env budget (probed past 30m); stopped at ${clock.now()}ms`,
|
|
308
|
-
);
|
|
309
|
-
});
|
|
310
|
-
|
|
311
|
-
test("pollUntilReady: the seeded probeTimeout binds the worker to the gate per-instance, overriding ambient env", async () => {
|
|
312
|
-
const clock = fakeClock();
|
|
313
|
-
// Regression for the per-instance worker/gate drift: the gate's engine timers fire off the seeded
|
|
314
|
-
// `probeTimeout` process variable, so the worker must adopt THAT value, not recompute from the
|
|
315
|
-
// ambient env. Here the seeded bound (45m) is generous while the ambient env (1m) is stale/smaller.
|
|
316
|
-
// The env-recomputing code would bail after 1m — going silent while the engine still waits 45m —
|
|
317
|
-
// so the never-green probe must instead keep polling PAST 30m to prove the seeded value is in force.
|
|
318
|
-
const exec = execReturning([{ status: 503, body: "" }]);
|
|
319
|
-
const THIRTY_MIN = 30 * 60_000;
|
|
320
|
-
const res = await pollUntilReady({
|
|
321
|
-
probe: httpProbe({ everyMs: 5 * 60_000, backoff: "fixed" }),
|
|
322
|
-
gateKey: "gate-seeded",
|
|
323
|
-
probeTimeout: "PT45M",
|
|
324
|
-
exec,
|
|
325
|
-
env: { NANO_READINESS_POLL_TIMEOUT: "PT1M" },
|
|
326
|
-
now: clock.now,
|
|
327
|
-
wait: clock.wait,
|
|
328
|
-
publish: async () => {},
|
|
329
|
-
});
|
|
330
|
-
assert(!res.ready, "the never-green probe exhausted its (seeded) budget");
|
|
331
|
-
assert(
|
|
332
|
-
clock.now() > THIRTY_MIN,
|
|
333
|
-
`the worker honored the seeded 45m probeTimeout over the 1m env; stopped at ${clock.now()}ms`,
|
|
334
|
-
);
|
|
335
|
-
});
|
|
336
|
-
|
|
337
188
|
test("READINESS_READY_MESSAGE is the name the gate correlates", () => {
|
|
338
189
|
assertEquals(READINESS_READY_MESSAGE, "readiness-ready");
|
|
339
190
|
});
|
|
340
191
|
|
|
341
192
|
test("readGateVars: returns the RAW (untrimmed) gateKey so the publish key matches the gate's =gateKey subscription", () => {
|
|
342
|
-
// The gate's message subscription binds correlationKey="=gateKey" (the untrimmed process variable).
|
|
343
|
-
// If a caller seeds a whitespace-padded gateKey, trimming the publish key would desync it from the
|
|
344
|
-
// subscription — a green probe would then release the wait only via the timeout arm. So the key we
|
|
345
|
-
// publish on must be the raw value, byte-for-byte.
|
|
346
193
|
const { gateKey } = readGateVars({ gateKey: " gate-x ", probeTimeout: "PT30M" });
|
|
347
194
|
assertEquals(gateKey, " gate-x ", "the raw gateKey is preserved for byte-for-byte correlation");
|
|
348
195
|
});
|
|
@@ -361,9 +208,6 @@ test("readGateVars: a blank/whitespace gateKey fails fast (an empty correlationK
|
|
|
361
208
|
});
|
|
362
209
|
|
|
363
210
|
test("readGateVars: a missing/blank probeTimeout fails fast rather than silently falling back to env", () => {
|
|
364
|
-
// probeTimeout drives BOTH the gate's engine timers (=probeTimeout). A missing/non-string value used
|
|
365
|
-
// to fall back to the env-derived twin, breaking the per-instance bound and masking a mis-seeded
|
|
366
|
-
// instance until it escalated. It must now fail fast.
|
|
367
211
|
for (const probeTimeout of [undefined, "", " ", 42, null]) {
|
|
368
212
|
let threw = false;
|
|
369
213
|
try {
|