mandrel-platform 0.17.2 → 0.18.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/README.md +220 -14
- package/config/commitlint.base.mjs +36 -0
- package/config/repo-settings.schema.json +78 -0
- package/package.json +2 -1
- package/scripts/apply-uptime-monitors.mjs +378 -0
- package/scripts/apply-uptime-monitors.test.mjs +372 -0
- package/scripts/check-repo-settings.mjs +363 -0
- package/scripts/check-repo-settings.test.mjs +320 -0
- package/scripts/check-required-contexts.mjs +247 -129
- package/scripts/check-required-contexts.test.mjs +137 -0
- package/scripts/check-ruleset.mjs +435 -0
- package/scripts/check-ruleset.test.mjs +439 -0
- package/scripts/check-wrangler-baseline.mjs +514 -0
- package/scripts/check-wrangler-baseline.test.mjs +454 -0
- package/scripts/platform-sync.mjs +533 -5
- package/scripts/platform-sync.test.mjs +477 -0
- package/templates/workflows/deploy-staging.yml +86 -0
- package/templates/workflows/uptime-apply.yml +54 -0
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* check-ruleset.test.mjs — node:test suite for the Story #178 branch-ruleset
|
|
4
|
+
* drift dashboard.
|
|
5
|
+
*
|
|
6
|
+
* Exercises the pure classifiers (findBranchRuleset, mapRulesetToContract,
|
|
7
|
+
* diffRuleset) directly, and the full buildReport/runCli pipeline with an
|
|
8
|
+
* injected `runGh` seam (offline — no real GitHub calls).
|
|
9
|
+
*
|
|
10
|
+
* Run: node scripts/check-ruleset.test.mjs (or `node --test scripts/`)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import assert from "node:assert/strict";
|
|
14
|
+
import { mkdtempSync, writeFileSync, rmSync } from "node:fs";
|
|
15
|
+
import { tmpdir } from "node:os";
|
|
16
|
+
import { join } from "node:path";
|
|
17
|
+
import { afterEach, beforeEach, test } from "node:test";
|
|
18
|
+
import {
|
|
19
|
+
findBranchRuleset,
|
|
20
|
+
mapRulesetToContract,
|
|
21
|
+
diffRuleset,
|
|
22
|
+
buildReport,
|
|
23
|
+
hasDrift,
|
|
24
|
+
renderReport,
|
|
25
|
+
runCli,
|
|
26
|
+
parseArgv,
|
|
27
|
+
} from "./check-ruleset.mjs";
|
|
28
|
+
|
|
29
|
+
const CONTRACT = {
|
|
30
|
+
branch: "main",
|
|
31
|
+
requiredStatusChecks: ["ci-required"],
|
|
32
|
+
aggregatorJob: "ci-required",
|
|
33
|
+
upstreamJobs: ["lint", "typecheck", "unit"],
|
|
34
|
+
enforceAdmins: false,
|
|
35
|
+
requireLinearHistory: false,
|
|
36
|
+
allowForcePushes: false,
|
|
37
|
+
allowDeletions: false,
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
function compliantRuleset(overrides = {}) {
|
|
41
|
+
return {
|
|
42
|
+
id: 1,
|
|
43
|
+
name: "main-protection",
|
|
44
|
+
enforcement: "active",
|
|
45
|
+
conditions: { ref_name: { include: ["refs/heads/main"], exclude: [] } },
|
|
46
|
+
bypass_actors: [],
|
|
47
|
+
rules: [
|
|
48
|
+
{ type: "pull_request" },
|
|
49
|
+
{
|
|
50
|
+
type: "required_status_checks",
|
|
51
|
+
parameters: {
|
|
52
|
+
required_status_checks: [{ context: "ci-required" }],
|
|
53
|
+
strict_required_status_checks_policy: true,
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
{ type: "non_fast_forward" },
|
|
57
|
+
{ type: "deletion" },
|
|
58
|
+
],
|
|
59
|
+
...overrides,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
// findBranchRuleset
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
|
|
67
|
+
test("findBranchRuleset picks the active ruleset targeting refs/heads/<branch>", () => {
|
|
68
|
+
const rulesets = [
|
|
69
|
+
{ id: 1, enforcement: "active", conditions: { ref_name: { include: ["refs/heads/dev"] } } },
|
|
70
|
+
{ id: 2, enforcement: "active", conditions: { ref_name: { include: ["refs/heads/main"] } } },
|
|
71
|
+
];
|
|
72
|
+
const found = findBranchRuleset(rulesets, "main");
|
|
73
|
+
assert.equal(found.id, 2);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("findBranchRuleset matches ~DEFAULT_BRANCH as a stand-in for the target branch", () => {
|
|
77
|
+
const rulesets = [{ id: 5, enforcement: "active", conditions: { ref_name: { include: ["~DEFAULT_BRANCH"] } } }];
|
|
78
|
+
const found = findBranchRuleset(rulesets, "main");
|
|
79
|
+
assert.equal(found.id, 5);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("findBranchRuleset ignores disabled ('evaluate') rulesets", () => {
|
|
83
|
+
const rulesets = [{ id: 9, enforcement: "evaluate", conditions: { ref_name: { include: ["refs/heads/main"] } } }];
|
|
84
|
+
assert.equal(findBranchRuleset(rulesets, "main"), null);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("findBranchRuleset returns null when nothing targets the branch", () => {
|
|
88
|
+
const rulesets = [{ id: 1, enforcement: "active", conditions: { ref_name: { include: ["refs/heads/dev"] } } }];
|
|
89
|
+
assert.equal(findBranchRuleset(rulesets, "main"), null);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// ---------------------------------------------------------------------------
|
|
93
|
+
// mapRulesetToContract
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
|
|
96
|
+
test("mapRulesetToContract maps a fully compliant ruleset", () => {
|
|
97
|
+
const mapped = mapRulesetToContract(compliantRuleset());
|
|
98
|
+
assert.deepEqual(mapped, {
|
|
99
|
+
pullRequestRequired: true,
|
|
100
|
+
bypassActorsEmpty: true,
|
|
101
|
+
requiredStatusChecks: ["ci-required"],
|
|
102
|
+
strictRequiredStatusChecksPolicy: true,
|
|
103
|
+
allowForcePushes: false,
|
|
104
|
+
allowDeletions: false,
|
|
105
|
+
requireLinearHistory: false,
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("mapRulesetToContract reports bypass_actors present as bypassActorsEmpty=false", () => {
|
|
110
|
+
const ruleset = compliantRuleset({ bypass_actors: [{ actor_id: 1, actor_type: "Team" }] });
|
|
111
|
+
const mapped = mapRulesetToContract(ruleset);
|
|
112
|
+
assert.equal(mapped.bypassActorsEmpty, false);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("mapRulesetToContract reports missing non_fast_forward rule as allowForcePushes=true", () => {
|
|
116
|
+
const ruleset = compliantRuleset({ rules: compliantRuleset().rules.filter((r) => r.type !== "non_fast_forward") });
|
|
117
|
+
const mapped = mapRulesetToContract(ruleset);
|
|
118
|
+
assert.equal(mapped.allowForcePushes, true);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("mapRulesetToContract reports missing deletion rule as allowDeletions=true", () => {
|
|
122
|
+
const ruleset = compliantRuleset({ rules: compliantRuleset().rules.filter((r) => r.type !== "deletion") });
|
|
123
|
+
const mapped = mapRulesetToContract(ruleset);
|
|
124
|
+
assert.equal(mapped.allowDeletions, true);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("mapRulesetToContract detects required_linear_history when present", () => {
|
|
128
|
+
const ruleset = compliantRuleset({ rules: [...compliantRuleset().rules, { type: "required_linear_history" }] });
|
|
129
|
+
const mapped = mapRulesetToContract(ruleset);
|
|
130
|
+
assert.equal(mapped.requireLinearHistory, true);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("mapRulesetToContract handles a ruleset with no rules array", () => {
|
|
134
|
+
const mapped = mapRulesetToContract({ id: 1, bypass_actors: [] });
|
|
135
|
+
assert.equal(mapped.pullRequestRequired, false);
|
|
136
|
+
assert.deepEqual(mapped.requiredStatusChecks, []);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// ---------------------------------------------------------------------------
|
|
140
|
+
// diffRuleset
|
|
141
|
+
// ---------------------------------------------------------------------------
|
|
142
|
+
|
|
143
|
+
test("diffRuleset reports no mismatches for a fully compliant live ruleset", () => {
|
|
144
|
+
const live = mapRulesetToContract(compliantRuleset());
|
|
145
|
+
const { drifted, mismatches } = diffRuleset(live, CONTRACT);
|
|
146
|
+
assert.equal(drifted, false);
|
|
147
|
+
assert.deepEqual(mismatches, []);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test("diffRuleset flags a bypass actor added to the ruleset", () => {
|
|
151
|
+
const live = mapRulesetToContract(compliantRuleset({ bypass_actors: [{ actor_id: 42 }] }));
|
|
152
|
+
const { drifted, mismatches } = diffRuleset(live, CONTRACT);
|
|
153
|
+
assert.equal(drifted, true);
|
|
154
|
+
assert.deepEqual(mismatches, [{ field: "bypassActorsEmpty", expected: true, actual: false }]);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test("diffRuleset flags force-push re-enabled (non_fast_forward rule removed)", () => {
|
|
158
|
+
const ruleset = compliantRuleset({ rules: compliantRuleset().rules.filter((r) => r.type !== "non_fast_forward") });
|
|
159
|
+
const live = mapRulesetToContract(ruleset);
|
|
160
|
+
const { mismatches } = diffRuleset(live, CONTRACT);
|
|
161
|
+
assert.deepEqual(mismatches, [{ field: "allowForcePushes", expected: false, actual: true }]);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test("diffRuleset flags strict-status-checks-policy turned off", () => {
|
|
165
|
+
const ruleset = compliantRuleset();
|
|
166
|
+
ruleset.rules[1].parameters.strict_required_status_checks_policy = false;
|
|
167
|
+
const live = mapRulesetToContract(ruleset);
|
|
168
|
+
const { mismatches } = diffRuleset(live, CONTRACT);
|
|
169
|
+
assert.deepEqual(mismatches, [
|
|
170
|
+
{ field: "strictRequiredStatusChecksPolicy", expected: true, actual: false },
|
|
171
|
+
]);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test("diffRuleset flags required-status-checks context drift (order-independent set compare)", () => {
|
|
175
|
+
const ruleset = compliantRuleset();
|
|
176
|
+
ruleset.rules[1].parameters.required_status_checks = [{ context: "some-other-check" }];
|
|
177
|
+
const live = mapRulesetToContract(ruleset);
|
|
178
|
+
const { mismatches } = diffRuleset(live, CONTRACT);
|
|
179
|
+
const field = mismatches.find((m) => m.field === "requiredStatusChecks");
|
|
180
|
+
assert.ok(field);
|
|
181
|
+
assert.deepEqual(field.actual, ["some-other-check"]);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test("diffRuleset does not flag required-status-checks when the set matches regardless of order", () => {
|
|
185
|
+
const contract = { ...CONTRACT, requiredStatusChecks: ["a", "b"] };
|
|
186
|
+
const ruleset = compliantRuleset();
|
|
187
|
+
ruleset.rules[1].parameters.required_status_checks = [{ context: "b" }, { context: "a" }];
|
|
188
|
+
const live = mapRulesetToContract(ruleset);
|
|
189
|
+
const { mismatches } = diffRuleset(live, contract);
|
|
190
|
+
assert.equal(mismatches.some((m) => m.field === "requiredStatusChecks"), false);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test("diffRuleset flags PR-not-required", () => {
|
|
194
|
+
const ruleset = compliantRuleset({ rules: compliantRuleset().rules.filter((r) => r.type !== "pull_request") });
|
|
195
|
+
const live = mapRulesetToContract(ruleset);
|
|
196
|
+
const { mismatches } = diffRuleset(live, CONTRACT);
|
|
197
|
+
assert.deepEqual(mismatches, [{ field: "pullRequestRequired", expected: true, actual: false }]);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test("diffRuleset ignores contract keys the ruleset cannot encode ($schema, aggregatorJob, upstreamJobs, enforceAdmins, _note)", () => {
|
|
201
|
+
const contractWithExtras = { ...CONTRACT, $schema: "../../config/main-protection.schema.json", _note: "..." };
|
|
202
|
+
const live = mapRulesetToContract(compliantRuleset());
|
|
203
|
+
const { drifted } = diffRuleset(live, contractWithExtras);
|
|
204
|
+
assert.equal(drifted, false);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
// ---------------------------------------------------------------------------
|
|
208
|
+
// buildReport / hasDrift / renderReport
|
|
209
|
+
// ---------------------------------------------------------------------------
|
|
210
|
+
|
|
211
|
+
function fakeRunGh(responses) {
|
|
212
|
+
return (args) => {
|
|
213
|
+
const path = args[1]; // ["api", "<path>", ...]
|
|
214
|
+
if (!(path in responses)) {
|
|
215
|
+
throw new Error(`fakeRunGh: no stub for ${path}`);
|
|
216
|
+
}
|
|
217
|
+
return JSON.stringify(responses[path]);
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
test("buildReport: consumer matching the contract reports 'current'", () => {
|
|
222
|
+
const config = { consumers: [{ name: "swarm-os", repo: "Beestera/swarm-os" }] };
|
|
223
|
+
const runGh = fakeRunGh({
|
|
224
|
+
"repos/Beestera/swarm-os/rulesets": [{ id: 1 }],
|
|
225
|
+
"repos/Beestera/swarm-os/rulesets/1": compliantRuleset(),
|
|
226
|
+
});
|
|
227
|
+
const report = buildReport(config, CONTRACT, runGh);
|
|
228
|
+
assert.equal(report.consumers[0].status, "current");
|
|
229
|
+
assert.equal(hasDrift(report), false);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test("buildReport: consumer with a bypass actor reports 'drift'", () => {
|
|
233
|
+
const config = { consumers: [{ name: "domio", repo: "dsj1984/domio" }] };
|
|
234
|
+
const runGh = fakeRunGh({
|
|
235
|
+
"repos/dsj1984/domio/rulesets": [{ id: 7 }],
|
|
236
|
+
"repos/dsj1984/domio/rulesets/7": compliantRuleset({ bypass_actors: [{ actor_id: 1 }] }),
|
|
237
|
+
});
|
|
238
|
+
const report = buildReport(config, CONTRACT, runGh);
|
|
239
|
+
assert.equal(report.consumers[0].status, "drift");
|
|
240
|
+
assert.deepEqual(report.consumers[0].mismatches, [
|
|
241
|
+
{ field: "bypassActorsEmpty", expected: true, actual: false },
|
|
242
|
+
]);
|
|
243
|
+
assert.equal(hasDrift(report), true);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
test("buildReport: no ruleset targeting the branch reports 'missing' and counts as drift", () => {
|
|
247
|
+
const config = { consumers: [{ name: "athportal", repo: "dsj1984/athportal" }] };
|
|
248
|
+
const runGh = fakeRunGh({
|
|
249
|
+
"repos/dsj1984/athportal/rulesets": [{ id: 1 }],
|
|
250
|
+
"repos/dsj1984/athportal/rulesets/1": {
|
|
251
|
+
id: 1,
|
|
252
|
+
enforcement: "active",
|
|
253
|
+
conditions: { ref_name: { include: ["refs/heads/dev"] } },
|
|
254
|
+
},
|
|
255
|
+
});
|
|
256
|
+
const report = buildReport(config, CONTRACT, runGh);
|
|
257
|
+
assert.equal(report.consumers[0].status, "missing");
|
|
258
|
+
assert.equal(hasDrift(report), true);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
test("buildReport: empty rulesets list reports 'missing'", () => {
|
|
262
|
+
const config = { consumers: [{ name: "empty", repo: "owner/empty" }] };
|
|
263
|
+
const runGh = fakeRunGh({ "repos/owner/empty/rulesets": [] });
|
|
264
|
+
const report = buildReport(config, CONTRACT, runGh);
|
|
265
|
+
assert.equal(report.consumers[0].status, "missing");
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
test("buildReport: a gh failure for one consumer surfaces as 'error', not a thrown exception", () => {
|
|
269
|
+
const config = { consumers: [{ name: "broken", repo: "owner/broken" }] };
|
|
270
|
+
const runGh = () => {
|
|
271
|
+
throw new Error("gh: repository not found");
|
|
272
|
+
};
|
|
273
|
+
const report = buildReport(config, CONTRACT, runGh);
|
|
274
|
+
assert.equal(report.consumers[0].status, "error");
|
|
275
|
+
assert.match(report.consumers[0].error, /not found/);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
test("buildReport respects a per-consumer branch override", () => {
|
|
279
|
+
const config = { consumers: [{ name: "custom", repo: "owner/custom", branch: "trunk" }] };
|
|
280
|
+
const runGh = fakeRunGh({
|
|
281
|
+
"repos/owner/custom/rulesets": [{ id: 1 }],
|
|
282
|
+
"repos/owner/custom/rulesets/1": {
|
|
283
|
+
...compliantRuleset(),
|
|
284
|
+
conditions: { ref_name: { include: ["refs/heads/trunk"] } },
|
|
285
|
+
},
|
|
286
|
+
});
|
|
287
|
+
const report = buildReport(config, CONTRACT, runGh);
|
|
288
|
+
assert.equal(report.consumers[0].status, "current");
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
test("renderReport renders the non-blocking framing and per-consumer rows", () => {
|
|
292
|
+
const report = {
|
|
293
|
+
contract: CONTRACT,
|
|
294
|
+
consumers: [
|
|
295
|
+
{ name: "domio", repo: "dsj1984/domio", status: "current" },
|
|
296
|
+
{
|
|
297
|
+
name: "athportal",
|
|
298
|
+
repo: "dsj1984/athportal",
|
|
299
|
+
status: "drift",
|
|
300
|
+
mismatches: [{ field: "bypassActorsEmpty", expected: true, actual: false }],
|
|
301
|
+
},
|
|
302
|
+
{ name: "swarm-os", repo: "Beestera/swarm-os", status: "missing", error: "no active ruleset" },
|
|
303
|
+
{ name: "broken", repo: "owner/broken", status: "error", error: "boom" },
|
|
304
|
+
],
|
|
305
|
+
};
|
|
306
|
+
const text = renderReport(report);
|
|
307
|
+
assert.match(text, /never a hard gate/);
|
|
308
|
+
assert.match(text, /domio.*✅ current/);
|
|
309
|
+
assert.match(text, /athportal.*❌ drift/);
|
|
310
|
+
assert.match(text, /bypassActorsEmpty/);
|
|
311
|
+
assert.match(text, /swarm-os.*⚠️ missing/);
|
|
312
|
+
assert.match(text, /broken.*⚠️ error/);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
test("parseArgv reads --config/--contract/--json/--strict", () => {
|
|
316
|
+
const parsed = parseArgv(["--config", "foo.json", "--contract", "bar.json", "--json", "--strict"]);
|
|
317
|
+
assert.deepEqual(parsed, { config: "foo.json", contract: "bar.json", json: true, strict: true });
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
test("parseArgv defaults to the shared pin-drift consumer registry and the main-protection runbook", () => {
|
|
321
|
+
const parsed = parseArgv([]);
|
|
322
|
+
assert.equal(parsed.config, "scripts/pin-drift-consumers.json");
|
|
323
|
+
assert.equal(parsed.contract, "docs/runbooks/main-protection.json");
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
// ---------------------------------------------------------------------------
|
|
327
|
+
// runCli
|
|
328
|
+
// ---------------------------------------------------------------------------
|
|
329
|
+
|
|
330
|
+
let tmpDir;
|
|
331
|
+
|
|
332
|
+
beforeEach(() => {
|
|
333
|
+
tmpDir = mkdtempSync(join(tmpdir(), "check-ruleset-test-"));
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
afterEach(() => {
|
|
337
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
test("runCli --strict exits 1 when drift is present; exits 0 without --strict (non-blocking default)", () => {
|
|
341
|
+
const config = { consumers: [{ name: "domio", repo: "dsj1984/domio" }] };
|
|
342
|
+
const runGh = fakeRunGh({
|
|
343
|
+
"repos/dsj1984/domio/rulesets": [{ id: 1 }],
|
|
344
|
+
"repos/dsj1984/domio/rulesets/1": compliantRuleset({ bypass_actors: [{ actor_id: 1 }] }),
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
const stdout = { write: () => {} };
|
|
348
|
+
const stderr = { write: () => {} };
|
|
349
|
+
|
|
350
|
+
const configPath = join(tmpDir, "consumers.json");
|
|
351
|
+
const contractPath = join(tmpDir, "contract.json");
|
|
352
|
+
writeFileSync(configPath, JSON.stringify(config));
|
|
353
|
+
writeFileSync(contractPath, JSON.stringify(CONTRACT));
|
|
354
|
+
|
|
355
|
+
const exitNonStrict = runCli({
|
|
356
|
+
argv: ["--config", configPath, "--contract", contractPath],
|
|
357
|
+
cwd: tmpDir,
|
|
358
|
+
stdout,
|
|
359
|
+
stderr,
|
|
360
|
+
runGh,
|
|
361
|
+
summaryPath: undefined,
|
|
362
|
+
});
|
|
363
|
+
assert.equal(exitNonStrict, 0, "non-strict never fails on drift");
|
|
364
|
+
|
|
365
|
+
const exitStrict = runCli({
|
|
366
|
+
argv: ["--config", configPath, "--contract", contractPath, "--strict"],
|
|
367
|
+
cwd: tmpDir,
|
|
368
|
+
stdout,
|
|
369
|
+
stderr,
|
|
370
|
+
runGh,
|
|
371
|
+
summaryPath: undefined,
|
|
372
|
+
});
|
|
373
|
+
assert.equal(exitStrict, 1, "--strict fails on drift");
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
test("runCli --json emits a machine-readable envelope with the drift verdict", () => {
|
|
377
|
+
const config = { consumers: [{ name: "swarm-os", repo: "Beestera/swarm-os" }] };
|
|
378
|
+
const runGh = fakeRunGh({
|
|
379
|
+
"repos/Beestera/swarm-os/rulesets": [{ id: 1 }],
|
|
380
|
+
"repos/Beestera/swarm-os/rulesets/1": compliantRuleset(),
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
let out = "";
|
|
384
|
+
const stdout = { write: (s) => (out += s) };
|
|
385
|
+
const stderr = { write: () => {} };
|
|
386
|
+
|
|
387
|
+
const configPath = join(tmpDir, "consumers.json");
|
|
388
|
+
const contractPath = join(tmpDir, "contract.json");
|
|
389
|
+
writeFileSync(configPath, JSON.stringify(config));
|
|
390
|
+
writeFileSync(contractPath, JSON.stringify(CONTRACT));
|
|
391
|
+
|
|
392
|
+
const exit = runCli({
|
|
393
|
+
argv: ["--config", configPath, "--contract", contractPath, "--json"],
|
|
394
|
+
cwd: tmpDir,
|
|
395
|
+
stdout,
|
|
396
|
+
stderr,
|
|
397
|
+
runGh,
|
|
398
|
+
summaryPath: undefined,
|
|
399
|
+
});
|
|
400
|
+
assert.equal(exit, 0);
|
|
401
|
+
const parsed = JSON.parse(out);
|
|
402
|
+
assert.equal(parsed.kind, "ruleset-report");
|
|
403
|
+
assert.equal(parsed.drift, false);
|
|
404
|
+
assert.equal(parsed.consumers[0].status, "current");
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
test("runCli exits 1 on a fatal config-read error (missing file)", () => {
|
|
408
|
+
let errOut = "";
|
|
409
|
+
const exit = runCli({
|
|
410
|
+
argv: ["--config", join(tmpDir, "does-not-exist.json")],
|
|
411
|
+
cwd: tmpDir,
|
|
412
|
+
stdout: { write: () => {} },
|
|
413
|
+
stderr: { write: (s) => (errOut += s) },
|
|
414
|
+
runGh: () => {
|
|
415
|
+
throw new Error("should not be called");
|
|
416
|
+
},
|
|
417
|
+
summaryPath: undefined,
|
|
418
|
+
});
|
|
419
|
+
assert.equal(exit, 1);
|
|
420
|
+
assert.match(errOut, /failed to read config/);
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
test("runCli exits 1 on a fatal contract-read error (missing file)", () => {
|
|
424
|
+
const configPath = join(tmpDir, "consumers.json");
|
|
425
|
+
writeFileSync(configPath, JSON.stringify({ consumers: [] }));
|
|
426
|
+
let errOut = "";
|
|
427
|
+
const exit = runCli({
|
|
428
|
+
argv: ["--config", configPath, "--contract", join(tmpDir, "does-not-exist.json")],
|
|
429
|
+
cwd: tmpDir,
|
|
430
|
+
stdout: { write: () => {} },
|
|
431
|
+
stderr: { write: (s) => (errOut += s) },
|
|
432
|
+
runGh: () => {
|
|
433
|
+
throw new Error("should not be called");
|
|
434
|
+
},
|
|
435
|
+
summaryPath: undefined,
|
|
436
|
+
});
|
|
437
|
+
assert.equal(exit, 1);
|
|
438
|
+
assert.match(errOut, /failed to read contract/);
|
|
439
|
+
});
|