mandrel-platform 0.29.1 → 1.0.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mandrel-platform",
3
- "version": "0.29.1",
3
+ "version": "1.0.1",
4
4
  "description": "Shared CI/deploy workflows, composite toolchain action, npm config package, Renovate preset, and operator runbook templates.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -0,0 +1,494 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * check-cancelled-provenance.test.mjs — regression guard for provenance-aware
4
+ * handling of a cancelled tier in the `ci-required` aggregator (Story #333).
5
+ *
6
+ * WHY THIS EXISTS
7
+ * ---------------
8
+ * The aggregator failed the required gate on ANY `needs.*.result == 'cancelled'`
9
+ * with no statement of cause, so three very different situations were
10
+ * indistinguishable at the gate:
11
+ *
12
+ * - a sibling tier genuinely failed and fail-fast cancelled the rest;
13
+ * - a self-hosted runner's provisioning hook hung and the job was cancelled
14
+ * having never executed a step (Beestera/swarm-os#928: a job sat ~6min with
15
+ * its `Checkout` step SKIPPED, then died — no test signal at all);
16
+ * - a newer push superseded the run via the concurrency groups.
17
+ *
18
+ * There is no cancellation-reason field to read back — verified 2026-07-25,
19
+ * `gh run view --json` exposes only attempt/conclusion/createdAt/databaseId/
20
+ * displayTitle/event/headBranch/headSha/jobs/name/number/startedAt/status/
21
+ * updatedAt/url/workflowDatabaseId/workflowName, and
22
+ * `GET /repos/{owner}/{repo}/actions/runs/{id}` carries none either. Provenance
23
+ * is therefore INFERRED from observable run state, and this suite pins both the
24
+ * inference and the gate policy it feeds.
25
+ *
26
+ * The load-bearing negative: classification can never turn a red gate green.
27
+ * Every lookup failure yields `unknown`, and `unknown` neutralizes nothing —
28
+ * so the worst case is exactly today's behaviour. A `never-started` (infra)
29
+ * cancel is likewise never neutralized under any policy: that tier produced no
30
+ * signal, and passing on a tier that never ran is a vacuous pass.
31
+ *
32
+ * Run: node --test scripts/check-cancelled-provenance.test.mjs
33
+ */
34
+
35
+ import assert from "node:assert/strict";
36
+ import { test } from "node:test";
37
+ import {
38
+ readFileSync,
39
+ mkdtempSync,
40
+ mkdirSync,
41
+ writeFileSync,
42
+ chmodSync,
43
+ rmSync,
44
+ } from "node:fs";
45
+ import { execFileSync, spawnSync } from "node:child_process";
46
+ import { join, resolve, dirname } from "node:path";
47
+ import { fileURLToPath } from "node:url";
48
+ import { tmpdir } from "node:os";
49
+
50
+ const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
51
+ const WORKFLOW = ".github/workflows/pr-quality.yml";
52
+ const RUN_ID = "30179418666";
53
+
54
+ // ---------------------------------------------------------------------------
55
+ // Extract the aggregator's run script (same approach as the sibling suite).
56
+ // ---------------------------------------------------------------------------
57
+
58
+ function extractRunScript() {
59
+ const lines = readFileSync(join(repoRoot, WORKFLOW), "utf8").split("\n");
60
+ const start = lines.findIndex((l) => l === " ci-required:");
61
+ assert.notEqual(start, -1, "`ci-required` job not found");
62
+ const runIdx = lines.findIndex(
63
+ (l, i) => i > start && /^\s+run:\s*\|\s*$/.test(l)
64
+ );
65
+ assert.notEqual(runIdx, -1, "`run: |` block not found");
66
+ const runIndent = lines[runIdx].match(/^(\s*)/)[1].length;
67
+ const body = [];
68
+ for (let i = runIdx + 1; i < lines.length; i++) {
69
+ if (/^\s*$/.test(lines[i])) {
70
+ body.push("");
71
+ continue;
72
+ }
73
+ if (lines[i].match(/^(\s*)/)[1].length <= runIndent) break;
74
+ body.push(lines[i].slice(runIndent + 2));
75
+ }
76
+ return body.join("\n");
77
+ }
78
+
79
+ const script = extractRunScript();
80
+
81
+ // ---------------------------------------------------------------------------
82
+ // Actions-API fixtures. `steps[].conclusion` is what separates a job that ran
83
+ // from one that never started.
84
+ // ---------------------------------------------------------------------------
85
+
86
+ const ranSteps = [
87
+ { name: "Set up job", conclusion: "success" },
88
+ { name: "Checkout", conclusion: "success" },
89
+ { name: "Typecheck", conclusion: "success" },
90
+ ];
91
+
92
+ /** The swarm-os#928 shape: queued, then cancelled, having run nothing. */
93
+ const neverStartedSteps = [
94
+ { name: "Set up job", conclusion: "skipped" },
95
+ { name: "Checkout", conclusion: "skipped" },
96
+ ];
97
+
98
+ function runFixture(jobs, { createdAt = "2026-07-25T10:00:00Z" } = {}) {
99
+ return {
100
+ jobs,
101
+ workflowDatabaseId: 4242,
102
+ headBranch: "story-333",
103
+ createdAt,
104
+ };
105
+ }
106
+
107
+ const FIXTURES = {
108
+ // A sibling genuinely failed; the rest are fail-fast collateral.
109
+ failFast: runFixture([
110
+ { name: "Accessibility (2/3)", conclusion: "failure", steps: ranSteps },
111
+ { name: "Typecheck", conclusion: "cancelled", steps: ranSteps },
112
+ { name: "Lint & format", conclusion: "cancelled", steps: ranSteps },
113
+ ]),
114
+ // Nothing failed; one job never executed a step. Infra hang.
115
+ neverStarted: runFixture([
116
+ { name: "Unit (1/2)", conclusion: "success", steps: ranSteps },
117
+ { name: "Detect web-*", conclusion: "cancelled", steps: neverStartedSteps },
118
+ ]),
119
+ // Nothing failed, every cancelled job had started — a concurrency cancel.
120
+ superseded: runFixture([
121
+ { name: "Unit (1/2)", conclusion: "success", steps: ranSteps },
122
+ { name: "E2E / Smoke (1/1)", conclusion: "cancelled", steps: ranSteps },
123
+ ]),
124
+ // Same shape, but the cancelled job is the ONLY non-skipped one — nothing
125
+ // actually passed, so neutralizing would be a vacuous pass.
126
+ supersededNoPass: runFixture([
127
+ { name: "E2E / Smoke (1/1)", conclusion: "cancelled", steps: ranSteps },
128
+ ]),
129
+ };
130
+
131
+ const NEWER_RUNS = [
132
+ { databaseId: 30179418666, createdAt: "2026-07-25T10:00:00Z" },
133
+ { databaseId: 30179999999, createdAt: "2026-07-25T10:31:00Z" },
134
+ ];
135
+ const NO_NEWER_RUNS = [
136
+ { databaseId: 30179418666, createdAt: "2026-07-25T10:00:00Z" },
137
+ ];
138
+
139
+ // ---------------------------------------------------------------------------
140
+ // Harness: run the extracted script with a stubbed `gh` that answers
141
+ // `run view` and `run list` from the fixtures above.
142
+ // ---------------------------------------------------------------------------
143
+
144
+ function jqAvailable() {
145
+ try {
146
+ execFileSync("jq", ["--version"], { stdio: "ignore" });
147
+ return true;
148
+ } catch {
149
+ return false;
150
+ }
151
+ }
152
+
153
+ const semantics = {
154
+ skip: jqAvailable() ? false : "jq not available on this host",
155
+ };
156
+
157
+ function runAggregator(
158
+ needsResults,
159
+ { policy = "strict", runJson = null, runList = null, ghFails = false } = {}
160
+ ) {
161
+ const dir = mkdtempSync(join(tmpdir(), "cancelled-provenance-"));
162
+ try {
163
+ const bin = join(dir, "bin");
164
+ mkdirSync(bin);
165
+
166
+ // The stub dispatches on `gh run view` vs `gh run list`, so a test can
167
+ // pin the classification inputs without any network access.
168
+ const viewFile = join(dir, "view.json");
169
+ const listFile = join(dir, "list.json");
170
+ writeFileSync(viewFile, runJson ? JSON.stringify(runJson) : "");
171
+ writeFileSync(listFile, runList ? JSON.stringify(runList) : "");
172
+ writeFileSync(
173
+ join(bin, "gh"),
174
+ ghFails
175
+ ? "#!/usr/bin/env bash\nexit 1\n"
176
+ : [
177
+ "#!/usr/bin/env bash",
178
+ 'if [ "$1" = "run" ] && [ "$2" = "view" ]; then',
179
+ ` cat ${JSON.stringify(viewFile)}`,
180
+ ' exit 0',
181
+ 'fi',
182
+ 'if [ "$1" = "run" ] && [ "$2" = "list" ]; then',
183
+ ` cat ${JSON.stringify(listFile)}`,
184
+ ' exit 0',
185
+ 'fi',
186
+ "exit 1",
187
+ "",
188
+ ].join("\n")
189
+ );
190
+ chmodSync(join(bin, "gh"), 0o755);
191
+
192
+ const summary = join(dir, "summary.md");
193
+ writeFileSync(summary, "");
194
+ const file = join(dir, "aggregate.sh");
195
+ writeFileSync(file, script);
196
+
197
+ const needsJson = Object.fromEntries(
198
+ Object.entries(needsResults).map(([k, result]) => [
199
+ k,
200
+ { result, outputs: {} },
201
+ ])
202
+ );
203
+
204
+ const r = spawnSync("bash", [file], {
205
+ encoding: "utf8",
206
+ env: {
207
+ ...process.env,
208
+ PATH: `${bin}:${process.env.PATH}`,
209
+ NEEDS_JSON: JSON.stringify(needsJson),
210
+ CANCELLED_POLICY: policy,
211
+ GH_TOKEN: "stub-token",
212
+ RUN_ID,
213
+ REPO: "Beestera/swarm-os",
214
+ GITHUB_STEP_SUMMARY: summary,
215
+ },
216
+ });
217
+ return { ...r, summary: readFileSync(summary, "utf8") };
218
+ } finally {
219
+ rmSync(dir, { recursive: true, force: true });
220
+ }
221
+ }
222
+
223
+ // A cancelled tier alongside a passing one — the shape every test below uses
224
+ // unless it needs something different.
225
+ const ONE_CANCELLED = { unit: "success", e2e: "cancelled" };
226
+
227
+ // ---------------------------------------------------------------------------
228
+ // 1. CLASSIFICATION — each provenance class is inferred from run state.
229
+ // ---------------------------------------------------------------------------
230
+
231
+ test("classifies a run with a failed sibling as fail-fast", semantics, () => {
232
+ const r = runAggregator(ONE_CANCELLED, { runJson: FIXTURES.failFast });
233
+ assert.equal(r.status, 1);
234
+ assert.match(r.stderr, /Cancelled provenance: fail-fast/);
235
+ });
236
+
237
+ test(
238
+ "classifies a job that executed no step as never-started (infra)",
239
+ semantics,
240
+ () => {
241
+ const r = runAggregator(ONE_CANCELLED, { runJson: FIXTURES.neverStarted });
242
+ assert.equal(r.status, 1);
243
+ assert.match(r.stderr, /Cancelled provenance: never-started/);
244
+ // The swarm-os#928 job is named, so a triager sees WHICH job hung.
245
+ assert.match(r.stderr, /Detect web-\*: never-started/);
246
+ }
247
+ );
248
+
249
+ test("classifies a run with a newer sibling run as superseded", semantics, () => {
250
+ const r = runAggregator(ONE_CANCELLED, {
251
+ runJson: FIXTURES.superseded,
252
+ runList: NEWER_RUNS,
253
+ });
254
+ assert.match(r.stderr, /Cancelled provenance: superseded/);
255
+ });
256
+
257
+ test("stays unknown when no newer run exists", semantics, () => {
258
+ const r = runAggregator(ONE_CANCELLED, {
259
+ runJson: FIXTURES.superseded,
260
+ runList: NO_NEWER_RUNS,
261
+ });
262
+ assert.equal(r.status, 1);
263
+ assert.match(r.stderr, /Cancelled provenance: unknown/);
264
+ });
265
+
266
+ test("a failed sibling outranks a never-started job", semantics, () => {
267
+ // Precedence matters: a job cancelled while still queued has executed no
268
+ // step either, so checking `never-started` first would report an infra hang
269
+ // for every fail-fast run.
270
+ const r = runAggregator(ONE_CANCELLED, {
271
+ runJson: runFixture([
272
+ { name: "Accessibility (2/3)", conclusion: "failure", steps: ranSteps },
273
+ { name: "E2E / Smoke (1/1)", conclusion: "cancelled", steps: neverStartedSteps },
274
+ ]),
275
+ });
276
+ assert.match(r.stderr, /Cancelled provenance: fail-fast/);
277
+ assert.doesNotMatch(r.stderr, /provenance: never-started/);
278
+ });
279
+
280
+ test("classification is reported on the job summary too", semantics, () => {
281
+ const r = runAggregator(ONE_CANCELLED, { runJson: FIXTURES.neverStarted });
282
+ assert.match(r.summary, /Provenance: `never-started`/);
283
+ assert.match(r.summary, /INFRA fault/);
284
+ });
285
+
286
+ test("a green run performs no provenance lookup at all", semantics, () => {
287
+ const r = runAggregator({ unit: "success", e2e: "success" });
288
+ assert.equal(r.status, 0, r.stderr);
289
+ assert.doesNotMatch(r.stderr, /provenance/);
290
+ });
291
+
292
+ // ---------------------------------------------------------------------------
293
+ // 2. POLICY — strict is unchanged; provenance-aware neutralizes ONE class.
294
+ // ---------------------------------------------------------------------------
295
+
296
+ test("strict is the default and still fails every cancel", semantics, () => {
297
+ for (const [name, runJson] of Object.entries(FIXTURES)) {
298
+ const r = runAggregator(ONE_CANCELLED, {
299
+ policy: "strict",
300
+ runJson,
301
+ runList: NEWER_RUNS,
302
+ });
303
+ assert.equal(r.status, 1, `${name} should stay red under strict`);
304
+ }
305
+ });
306
+
307
+ test("an absent policy env var defaults to strict", semantics, () => {
308
+ const r = runAggregator(ONE_CANCELLED, {
309
+ policy: "",
310
+ runJson: FIXTURES.superseded,
311
+ runList: NEWER_RUNS,
312
+ });
313
+ assert.equal(r.status, 1);
314
+ });
315
+
316
+ test(
317
+ "provenance-aware neutralizes a superseded run's cancels",
318
+ semantics,
319
+ () => {
320
+ const r = runAggregator(ONE_CANCELLED, {
321
+ policy: "provenance-aware",
322
+ runJson: FIXTURES.superseded,
323
+ runList: NEWER_RUNS,
324
+ });
325
+ assert.equal(r.status, 0, r.stderr);
326
+ assert.match(r.stderr, /Superseded run/);
327
+ assert.match(r.summary, /neutral \(superseded run\)/);
328
+ }
329
+ );
330
+
331
+ test(
332
+ "provenance-aware keeps a never-started (infra) cancel red",
333
+ semantics,
334
+ () => {
335
+ // The painful case from swarm-os#928 — and deliberately NOT cleared. The
336
+ // tier produced no test signal, so a green here is a vacuous pass.
337
+ const r = runAggregator(ONE_CANCELLED, {
338
+ policy: "provenance-aware",
339
+ runJson: FIXTURES.neverStarted,
340
+ runList: NEWER_RUNS,
341
+ });
342
+ assert.equal(r.status, 1);
343
+ assert.match(r.summary, /never-started/);
344
+ }
345
+ );
346
+
347
+ test("provenance-aware keeps a fail-fast collateral cancel red", semantics, () => {
348
+ const r = runAggregator(ONE_CANCELLED, {
349
+ policy: "provenance-aware",
350
+ runJson: FIXTURES.failFast,
351
+ runList: NEWER_RUNS,
352
+ });
353
+ assert.equal(r.status, 1);
354
+ });
355
+
356
+ test("provenance-aware never clears a run where a job failed", semantics, () => {
357
+ const r = runAggregator(
358
+ { unit: "failure", e2e: "cancelled" },
359
+ {
360
+ policy: "provenance-aware",
361
+ runJson: FIXTURES.superseded,
362
+ runList: NEWER_RUNS,
363
+ }
364
+ );
365
+ assert.equal(r.status, 1);
366
+ assert.match(r.stderr, /unit\(failure\)/);
367
+ });
368
+
369
+ test(
370
+ "provenance-aware refuses to neutralize when nothing passed",
371
+ semantics,
372
+ () => {
373
+ // Neutralizing here would pass the required gate on a run with zero
374
+ // successful tiers — the vacuous pass the all-skipped guard also refuses.
375
+ const r = runAggregator(
376
+ { e2e: "cancelled" },
377
+ {
378
+ policy: "provenance-aware",
379
+ runJson: FIXTURES.supersededNoPass,
380
+ runList: NEWER_RUNS,
381
+ }
382
+ );
383
+ assert.equal(r.status, 1);
384
+ }
385
+ );
386
+
387
+ // ---------------------------------------------------------------------------
388
+ // 3. DEGRADATION — no lookup failure can turn a red gate green, or fail the
389
+ // aggregator step itself.
390
+ // ---------------------------------------------------------------------------
391
+
392
+ test("a failing gh lookup degrades to unknown and stays red", semantics, () => {
393
+ for (const policy of ["strict", "provenance-aware"]) {
394
+ const r = runAggregator(ONE_CANCELLED, { policy, ghFails: true });
395
+ assert.equal(r.status, 1, `${policy} must stay red when gh fails`);
396
+ assert.match(r.stderr, /Cancelled provenance: unknown/);
397
+ }
398
+ });
399
+
400
+ test("an absent gh degrades to unknown and stays red", semantics, () => {
401
+ const dir = mkdtempSync(join(tmpdir(), "cancelled-provenance-nogh-"));
402
+ try {
403
+ const empty = join(dir, "bin");
404
+ mkdirSync(empty);
405
+ const summary = join(dir, "summary.md");
406
+ writeFileSync(summary, "");
407
+ const file = join(dir, "aggregate.sh");
408
+ writeFileSync(file, script);
409
+ // PATH keeps the real jq (the script needs it) but no gh.
410
+ const jqDir = dirname(execFileSync("which", ["jq"], { encoding: "utf8" }).trim());
411
+ const r = spawnSync("/bin/bash", [file], {
412
+ encoding: "utf8",
413
+ env: {
414
+ PATH: `${empty}:${jqDir}`,
415
+ NEEDS_JSON: JSON.stringify({
416
+ unit: { result: "success" },
417
+ e2e: { result: "cancelled" },
418
+ }),
419
+ CANCELLED_POLICY: "provenance-aware",
420
+ RUN_ID,
421
+ REPO: "Beestera/swarm-os",
422
+ GITHUB_STEP_SUMMARY: summary,
423
+ },
424
+ });
425
+ assert.equal(r.status, 1, r.stderr);
426
+ assert.match(r.stderr, /Cancelled provenance: unknown/);
427
+ } finally {
428
+ rmSync(dir, { recursive: true, force: true });
429
+ }
430
+ });
431
+
432
+ test("malformed API output degrades to unknown and stays red", semantics, () => {
433
+ const dir = mkdtempSync(join(tmpdir(), "cancelled-provenance-bad-"));
434
+ try {
435
+ const bin = join(dir, "bin");
436
+ mkdirSync(bin);
437
+ writeFileSync(
438
+ join(bin, "gh"),
439
+ "#!/usr/bin/env bash\nprintf 'not json at all'\nexit 0\n"
440
+ );
441
+ chmodSync(join(bin, "gh"), 0o755);
442
+ const summary = join(dir, "summary.md");
443
+ writeFileSync(summary, "");
444
+ const file = join(dir, "aggregate.sh");
445
+ writeFileSync(file, script);
446
+ const r = spawnSync("bash", [file], {
447
+ encoding: "utf8",
448
+ env: {
449
+ ...process.env,
450
+ PATH: `${bin}:${process.env.PATH}`,
451
+ NEEDS_JSON: JSON.stringify({
452
+ unit: { result: "success" },
453
+ e2e: { result: "cancelled" },
454
+ }),
455
+ CANCELLED_POLICY: "provenance-aware",
456
+ RUN_ID,
457
+ REPO: "Beestera/swarm-os",
458
+ GITHUB_STEP_SUMMARY: summary,
459
+ },
460
+ });
461
+ assert.equal(r.status, 1, r.stderr);
462
+ assert.match(r.stderr, /Cancelled provenance: unknown/);
463
+ } finally {
464
+ rmSync(dir, { recursive: true, force: true });
465
+ }
466
+ });
467
+
468
+ // ---------------------------------------------------------------------------
469
+ // 4. INPUT SURFACE — the policy is a declared, defaulted workflow_call input.
470
+ // ---------------------------------------------------------------------------
471
+
472
+ test("cancelled-policy is declared with a strict default", () => {
473
+ // Scan the input's own block by indentation rather than matching it with a
474
+ // multi-line regex: the obvious `(?: {8}.*\n|\s*\n)*?` formulation has
475
+ // overlapping alternatives and backtracks exponentially on a file with many
476
+ // newlines (CodeQL js/redos, high). A line walk has no such failure mode and
477
+ // reads more like the other extractors in this suite.
478
+ const lines = readFileSync(join(repoRoot, WORKFLOW), "utf8").split("\n");
479
+ const start = lines.findIndex((l) => l === " cancelled-policy:");
480
+ assert.notEqual(start, -1, "`cancelled-policy:` input not declared");
481
+
482
+ const block = [];
483
+ for (let i = start + 1; i < lines.length; i++) {
484
+ if (/^\s*$/.test(lines[i])) continue;
485
+ if (lines[i].match(/^(\s*)/)[1].length <= 6) break;
486
+ block.push(lines[i].trim());
487
+ }
488
+
489
+ assert.ok(
490
+ block.includes("default: strict"),
491
+ "`cancelled-policy` must default to `strict` so existing consumers are unaffected; " +
492
+ `declared instead: ${JSON.stringify(block)}`
493
+ );
494
+ });