mandrel-platform 0.13.0 → 0.14.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/README.md +203 -7
- package/config/dependency-cruiser.base.json +121 -0
- package/config/edge-security/allowlist.mjs +138 -0
- package/config/edge-security/cors-astro.mjs +131 -0
- package/config/edge-security/cors-hono.mjs +111 -0
- package/config/edge-security/index.mjs +34 -0
- package/config/edge-security/rate-limit.mjs +219 -0
- package/config/edge-security/security-headers.mjs +148 -0
- package/config/knip.base.json +7 -0
- package/config/lighthouse.base.json +27 -0
- package/config/size-limit.base.json +6 -0
- package/config/stryker.base.json +15 -0
- package/package.json +23 -2
- package/scripts/check-action-pins.mjs +344 -0
- package/scripts/check-action-pins.test.mjs +240 -0
- package/scripts/check-coverage-threshold.mjs +300 -0
- package/scripts/check-coverage-threshold.test.mjs +350 -0
- package/scripts/check-destructive-migration.mjs +313 -0
- package/scripts/check-destructive-migration.test.mjs +183 -0
- package/scripts/check-pin-drift.mjs +361 -17
- package/scripts/check-pin-drift.test.mjs +344 -1
- package/scripts/edge-security.test.mjs +300 -0
- package/scripts/pin-drift-consumers.json +2 -0
- package/scripts/platform-repair.mjs +748 -0
- package/scripts/platform-repair.test.mjs +458 -0
- package/scripts/platform-sync.mjs +0 -0
- package/scripts/platform-sync.test.mjs +0 -0
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* platform-repair.test.mjs — node:test suite for the scheduled platform-sync
|
|
4
|
+
* repair-PR loop (Story #113).
|
|
5
|
+
*
|
|
6
|
+
* The loop reuses the detector (`check-pin-drift.mjs`) for drift classification
|
|
7
|
+
* and shells out to `git` / `gh` / `platform-sync.mjs` only through injectable
|
|
8
|
+
* seams, so the whole detect→repair pipeline is exercised offline with canned
|
|
9
|
+
* GitHub responses — no network, no `gh` auth, no clones.
|
|
10
|
+
*
|
|
11
|
+
* Coverage:
|
|
12
|
+
* 1. classifyRepairability — split/lagging/skew repairable; holding/error/
|
|
13
|
+
* no-drift/floating-only skipped.
|
|
14
|
+
* 2. renderRepairPrBody — explains what drifted AND links the dashboard run.
|
|
15
|
+
* 3. findOpenRepairPr / parsePrNumberFromUrl — idempotency primitives.
|
|
16
|
+
* 4. runRepair (full pipeline) — opens a PR for a drifting consumer, UPDATES
|
|
17
|
+
* the existing PR on re-run (no duplicate), defers `holding`, and is
|
|
18
|
+
* read-only without a token + non-mutating under --dry-run.
|
|
19
|
+
*
|
|
20
|
+
* Run: node scripts/platform-repair.test.mjs (or `node --test scripts/`)
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import assert from "node:assert/strict";
|
|
24
|
+
import { test } from "node:test";
|
|
25
|
+
|
|
26
|
+
import {
|
|
27
|
+
REPAIR_BRANCH,
|
|
28
|
+
classifyRepairability,
|
|
29
|
+
describeDrift,
|
|
30
|
+
findOpenRepairPr,
|
|
31
|
+
parseArgv,
|
|
32
|
+
parsePrNumberFromUrl,
|
|
33
|
+
renderRepairPrBody,
|
|
34
|
+
renderRepairReport,
|
|
35
|
+
runRepair,
|
|
36
|
+
} from "./platform-repair.mjs";
|
|
37
|
+
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
// parseArgv
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
test("parseArgv reads ref, dry-run, config, dashboard url, json", () => {
|
|
43
|
+
const o = parseArgv([
|
|
44
|
+
"--ref",
|
|
45
|
+
"mandrel-platform-v1.2.3",
|
|
46
|
+
"--dry-run",
|
|
47
|
+
"--config",
|
|
48
|
+
"alt.json",
|
|
49
|
+
"--dashboard-run-url",
|
|
50
|
+
"https://example/run/9",
|
|
51
|
+
"--json",
|
|
52
|
+
]);
|
|
53
|
+
assert.equal(o.ref, "mandrel-platform-v1.2.3");
|
|
54
|
+
assert.equal(o.dryRun, true);
|
|
55
|
+
assert.equal(o.config, "alt.json");
|
|
56
|
+
assert.equal(o.dashboardRunUrl, "https://example/run/9");
|
|
57
|
+
assert.equal(o.json, true);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("parseArgv defaults", () => {
|
|
61
|
+
const o = parseArgv([]);
|
|
62
|
+
assert.equal(o.ref, null);
|
|
63
|
+
assert.equal(o.dryRun, false);
|
|
64
|
+
assert.equal(o.config, "scripts/pin-drift-consumers.json");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
// classifyRepairability
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
test("classifyRepairability: split pin is repairable", () => {
|
|
72
|
+
const r = classifyRepairability({
|
|
73
|
+
drift: true,
|
|
74
|
+
verdict: { splitPinned: true, lagState: "unknown", distinctRefs: ["a", "b"] },
|
|
75
|
+
});
|
|
76
|
+
assert.equal(r.repairable, true);
|
|
77
|
+
assert.equal(r.reason, "drift");
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("classifyRepairability: lagging uses pin is repairable", () => {
|
|
81
|
+
const r = classifyRepairability({
|
|
82
|
+
drift: true,
|
|
83
|
+
verdict: { splitPinned: false, lagState: "lagging", pinnedSha: "f".repeat(40) },
|
|
84
|
+
});
|
|
85
|
+
assert.equal(r.repairable, true);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("classifyRepairability: surface skew is repairable", () => {
|
|
89
|
+
const r = classifyRepairability({
|
|
90
|
+
drift: true,
|
|
91
|
+
verdict: { splitPinned: false, lagState: "current" },
|
|
92
|
+
npm: { npmState: "lagging" },
|
|
93
|
+
surfaceSkew: true,
|
|
94
|
+
});
|
|
95
|
+
assert.equal(r.repairable, true);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("classifyRepairability: holding is deferred, not repaired", () => {
|
|
99
|
+
const r = classifyRepairability({
|
|
100
|
+
drift: false,
|
|
101
|
+
holding: true,
|
|
102
|
+
verdict: { lagState: "lagging" },
|
|
103
|
+
});
|
|
104
|
+
assert.equal(r.repairable, false);
|
|
105
|
+
assert.equal(r.reason, "holding");
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("classifyRepairability: detector error is not repairable", () => {
|
|
109
|
+
const r = classifyRepairability({ error: "boom", verdict: { lagState: "no-pins" } });
|
|
110
|
+
assert.equal(r.repairable, false);
|
|
111
|
+
assert.equal(r.reason, "error");
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("classifyRepairability: no drift is skipped", () => {
|
|
115
|
+
const r = classifyRepairability({ drift: false, verdict: { lagState: "current" } });
|
|
116
|
+
assert.equal(r.repairable, false);
|
|
117
|
+
assert.equal(r.reason, "no-drift");
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("classifyRepairability: floating-ref-only (unknown, not split) is not auto-repairable", () => {
|
|
121
|
+
const r = classifyRepairability({
|
|
122
|
+
drift: true,
|
|
123
|
+
verdict: { splitPinned: false, lagState: "unknown" },
|
|
124
|
+
npm: { npmState: "absent" },
|
|
125
|
+
surfaceSkew: false,
|
|
126
|
+
});
|
|
127
|
+
assert.equal(r.repairable, false);
|
|
128
|
+
assert.equal(r.reason, "unknown-ref");
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// ---------------------------------------------------------------------------
|
|
132
|
+
// describeDrift + renderRepairPrBody (Acceptance: body explains drift + links run)
|
|
133
|
+
// ---------------------------------------------------------------------------
|
|
134
|
+
|
|
135
|
+
test("describeDrift names split pin + surface skew", () => {
|
|
136
|
+
const lines = describeDrift({
|
|
137
|
+
verdict: { splitPinned: true, distinctRefs: ["a", "b", "c"] },
|
|
138
|
+
npm: { npmState: "lagging", version: "0.11.3" },
|
|
139
|
+
surfaceSkew: true,
|
|
140
|
+
});
|
|
141
|
+
assert.ok(lines.some((l) => l.includes("Split pin")));
|
|
142
|
+
assert.ok(lines.some((l) => l.includes("Surface skew")));
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("renderRepairPrBody explains what drifted and links the dashboard run", () => {
|
|
146
|
+
const body = renderRepairPrBody({
|
|
147
|
+
name: "domio",
|
|
148
|
+
repo: "dsj1984/domio",
|
|
149
|
+
result: {
|
|
150
|
+
verdict: { splitPinned: false, lagState: "lagging", pinnedSha: "a".repeat(40) },
|
|
151
|
+
npm: { npmState: "current", version: "1.2.3" },
|
|
152
|
+
surfaceSkew: false,
|
|
153
|
+
},
|
|
154
|
+
ref: "mandrel-platform-v1.2.3",
|
|
155
|
+
targetSha: "b".repeat(40),
|
|
156
|
+
dashboardRunUrl: "https://github.com/dsj1984/mandrel-platform/actions/runs/42",
|
|
157
|
+
});
|
|
158
|
+
// Criterion: body explains what drifted.
|
|
159
|
+
assert.ok(body.includes("Release lag"), "names the drift class");
|
|
160
|
+
assert.ok(body.includes("What drifted"));
|
|
161
|
+
// Criterion: body links the pin-drift dashboard run.
|
|
162
|
+
assert.ok(
|
|
163
|
+
body.includes("https://github.com/dsj1984/mandrel-platform/actions/runs/42"),
|
|
164
|
+
"links the dashboard run",
|
|
165
|
+
);
|
|
166
|
+
// Advisory posture: PR goes through consumer CI, no auto-merge.
|
|
167
|
+
assert.ok(body.includes("advisory"));
|
|
168
|
+
assert.ok(body.includes("mandrel-platform-v1.2.3"));
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test("renderRepairPrBody falls back to the dashboard workflow when no run url", () => {
|
|
172
|
+
const body = renderRepairPrBody({
|
|
173
|
+
name: "x",
|
|
174
|
+
repo: "o/x",
|
|
175
|
+
result: { verdict: { splitPinned: true, distinctRefs: ["a", "b"] } },
|
|
176
|
+
ref: "v1",
|
|
177
|
+
targetSha: null,
|
|
178
|
+
dashboardRunUrl: null,
|
|
179
|
+
});
|
|
180
|
+
assert.ok(body.includes("pin-drift.yml"));
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
// ---------------------------------------------------------------------------
|
|
184
|
+
// idempotency primitives
|
|
185
|
+
// ---------------------------------------------------------------------------
|
|
186
|
+
|
|
187
|
+
test("parsePrNumberFromUrl extracts the number from gh pr create output", () => {
|
|
188
|
+
assert.equal(parsePrNumberFromUrl("https://github.com/o/r/pull/77\n"), 77);
|
|
189
|
+
assert.equal(parsePrNumberFromUrl("no url here"), null);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test("findOpenRepairPr returns the open PR number for the repair head branch", () => {
|
|
193
|
+
const runGh = (args) => {
|
|
194
|
+
assert.deepEqual(
|
|
195
|
+
args.slice(0, 8),
|
|
196
|
+
["pr", "list", "--repo", "o/r", "--head", REPAIR_BRANCH, "--state", "open"],
|
|
197
|
+
);
|
|
198
|
+
return JSON.stringify([{ number: 12 }]);
|
|
199
|
+
};
|
|
200
|
+
assert.equal(findOpenRepairPr("o/r", runGh), 12);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test("findOpenRepairPr returns null when no open repair PR exists", () => {
|
|
204
|
+
assert.equal(findOpenRepairPr("o/r", () => "[]"), null);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
// ---------------------------------------------------------------------------
|
|
208
|
+
// Full pipeline (runRepair) with injected seams
|
|
209
|
+
// ---------------------------------------------------------------------------
|
|
210
|
+
|
|
211
|
+
const PLATFORM_REPO = "dsj1984/mandrel-platform";
|
|
212
|
+
const LATEST_SHA = "b".repeat(40);
|
|
213
|
+
const STALE_SHA = "a".repeat(40);
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Build an injectable `gh` runner that answers the detector's API calls plus the
|
|
217
|
+
* repair loop's PR list/create/edit. `openPrs` maps repo → existing open repair
|
|
218
|
+
* PR number (or undefined for none). Records create/edit calls in `calls`.
|
|
219
|
+
*/
|
|
220
|
+
function makeGh({ consumerWorkflow, npmVersion, openPrs = {}, calls }) {
|
|
221
|
+
return (args) => {
|
|
222
|
+
const path = args[1];
|
|
223
|
+
// ---- detector (check-pin-drift) API surface ----
|
|
224
|
+
if (args[0] === "api") {
|
|
225
|
+
if (path === `repos/${PLATFORM_REPO}/releases/latest`) {
|
|
226
|
+
return JSON.stringify({
|
|
227
|
+
tag_name: "mandrel-platform-v1.2.3",
|
|
228
|
+
published_at: "2020-01-01T00:00:00Z", // ancient → past the hold window
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
if (path === `repos/${PLATFORM_REPO}/git/ref/tags/mandrel-platform-v1.2.3`) {
|
|
232
|
+
return JSON.stringify({ object: { sha: LATEST_SHA, type: "commit" } });
|
|
233
|
+
}
|
|
234
|
+
if (/\/contents\/\.github\/workflows/.test(path)) {
|
|
235
|
+
return JSON.stringify([
|
|
236
|
+
{ type: "file", name: "ci.yml", content: Buffer.from(consumerWorkflow).toString("base64"), encoding: "base64" },
|
|
237
|
+
]);
|
|
238
|
+
}
|
|
239
|
+
if (/\/contents\/package\.json/.test(path)) {
|
|
240
|
+
const pkg = JSON.stringify({ devDependencies: { "mandrel-platform": npmVersion } });
|
|
241
|
+
return JSON.stringify({ encoding: "base64", content: Buffer.from(pkg).toString("base64") });
|
|
242
|
+
}
|
|
243
|
+
if (/^repos\/[^/]+\/[^/]+$/.test(path)) {
|
|
244
|
+
return JSON.stringify({ default_branch: "main" });
|
|
245
|
+
}
|
|
246
|
+
throw new Error(`unexpected gh api path: ${path}`);
|
|
247
|
+
}
|
|
248
|
+
// ---- repair loop PR surface ----
|
|
249
|
+
if (args[0] === "pr" && args[1] === "list") {
|
|
250
|
+
const repo = args[args.indexOf("--repo") + 1];
|
|
251
|
+
const n = openPrs[repo];
|
|
252
|
+
return JSON.stringify(n ? [{ number: n }] : []);
|
|
253
|
+
}
|
|
254
|
+
if (args[0] === "pr" && args[1] === "create") {
|
|
255
|
+
calls.push({ kind: "create", args });
|
|
256
|
+
const repo = args[args.indexOf("--repo") + 1];
|
|
257
|
+
return `https://github.com/${repo}/pull/101\n`;
|
|
258
|
+
}
|
|
259
|
+
if (args[0] === "pr" && args[1] === "edit") {
|
|
260
|
+
calls.push({ kind: "edit", args });
|
|
261
|
+
return "";
|
|
262
|
+
}
|
|
263
|
+
if (args[0] === "repo" && args[1] === "view") {
|
|
264
|
+
return "main\n";
|
|
265
|
+
}
|
|
266
|
+
throw new Error(`unexpected gh call: ${args.join(" ")}`);
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const noopGit = () => "";
|
|
271
|
+
|
|
272
|
+
function laggingConfig() {
|
|
273
|
+
return {
|
|
274
|
+
platformRepo: PLATFORM_REPO,
|
|
275
|
+
minimumReleaseAge: "3 days",
|
|
276
|
+
consumers: [{ name: "domio", repo: "dsj1984/domio" }],
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// A consumer workflow pinning the STALE platform SHA → lagging drift.
|
|
281
|
+
const STALE_WORKFLOW = [
|
|
282
|
+
"jobs:",
|
|
283
|
+
" q:",
|
|
284
|
+
" steps:",
|
|
285
|
+
` - uses: ${PLATFORM_REPO}/.github/workflows/pr-quality.yml@${STALE_SHA}`,
|
|
286
|
+
].join("\n");
|
|
287
|
+
|
|
288
|
+
test("runRepair opens a repair PR for a lagging consumer (live, token present)", () => {
|
|
289
|
+
const calls = [];
|
|
290
|
+
const runGh = makeGh({ consumerWorkflow: STALE_WORKFLOW, npmVersion: "1.2.3", calls });
|
|
291
|
+
const report = runRepair({
|
|
292
|
+
config: laggingConfig(),
|
|
293
|
+
ref: null,
|
|
294
|
+
dryRun: false,
|
|
295
|
+
dashboardRunUrl: "https://example/run/5",
|
|
296
|
+
token: "ghp_fake",
|
|
297
|
+
templates: null,
|
|
298
|
+
runGh,
|
|
299
|
+
runGit: noopGit,
|
|
300
|
+
runSync: () => ({ changed: true, pins: [{ file: ".github/workflows/ci.yml" }] }),
|
|
301
|
+
workRoot: "/tmp/does-not-matter", // noopGit never touches disk
|
|
302
|
+
});
|
|
303
|
+
const row = report.rows.find((r) => r.name === "domio");
|
|
304
|
+
assert.equal(row.action, "opened");
|
|
305
|
+
assert.equal(row.prNumber, 101);
|
|
306
|
+
assert.equal(calls.filter((c) => c.kind === "create").length, 1);
|
|
307
|
+
// The created PR body links the dashboard run.
|
|
308
|
+
const createCall = calls.find((c) => c.kind === "create");
|
|
309
|
+
const body = createCall.args[createCall.args.indexOf("--body") + 1];
|
|
310
|
+
assert.ok(body.includes("https://example/run/5"));
|
|
311
|
+
assert.ok(body.includes("Release lag"));
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
test("runRepair UPDATES an existing repair PR instead of opening a duplicate (idempotent)", () => {
|
|
315
|
+
const calls = [];
|
|
316
|
+
const runGh = makeGh({
|
|
317
|
+
consumerWorkflow: STALE_WORKFLOW,
|
|
318
|
+
npmVersion: "1.2.3",
|
|
319
|
+
openPrs: { "dsj1984/domio": 55 },
|
|
320
|
+
calls,
|
|
321
|
+
});
|
|
322
|
+
const report = runRepair({
|
|
323
|
+
config: laggingConfig(),
|
|
324
|
+
ref: null,
|
|
325
|
+
dryRun: false,
|
|
326
|
+
dashboardRunUrl: null,
|
|
327
|
+
token: "ghp_fake",
|
|
328
|
+
templates: null,
|
|
329
|
+
runGh,
|
|
330
|
+
runGit: noopGit,
|
|
331
|
+
runSync: () => ({ changed: true, pins: [{ file: "x" }] }),
|
|
332
|
+
workRoot: "/tmp/x",
|
|
333
|
+
});
|
|
334
|
+
const row = report.rows.find((r) => r.name === "domio");
|
|
335
|
+
assert.equal(row.action, "updated");
|
|
336
|
+
assert.equal(row.prNumber, 55);
|
|
337
|
+
assert.equal(calls.filter((c) => c.kind === "create").length, 0, "no duplicate PR created");
|
|
338
|
+
assert.equal(calls.filter((c) => c.kind === "edit").length, 1, "existing PR edited");
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
test("runRepair is read-only without a token (reports would-open, no git/gh mutation)", () => {
|
|
342
|
+
const calls = [];
|
|
343
|
+
const runGh = makeGh({ consumerWorkflow: STALE_WORKFLOW, npmVersion: "1.2.3", calls });
|
|
344
|
+
let gitCalled = false;
|
|
345
|
+
const report = runRepair({
|
|
346
|
+
config: laggingConfig(),
|
|
347
|
+
ref: null,
|
|
348
|
+
dryRun: false,
|
|
349
|
+
dashboardRunUrl: null,
|
|
350
|
+
token: null,
|
|
351
|
+
templates: null,
|
|
352
|
+
runGh,
|
|
353
|
+
runGit: () => {
|
|
354
|
+
gitCalled = true;
|
|
355
|
+
return "";
|
|
356
|
+
},
|
|
357
|
+
runSync: () => {
|
|
358
|
+
throw new Error("sync must not run without a token");
|
|
359
|
+
},
|
|
360
|
+
});
|
|
361
|
+
const row = report.rows.find((r) => r.name === "domio");
|
|
362
|
+
assert.equal(row.action, "skipped-no-token");
|
|
363
|
+
assert.equal(gitCalled, false);
|
|
364
|
+
assert.equal(report.hasToken, false);
|
|
365
|
+
assert.equal(calls.filter((c) => c.kind === "create").length, 0);
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
test("runRepair --dry-run plans without cloning, syncing, or opening a PR", () => {
|
|
369
|
+
const calls = [];
|
|
370
|
+
const runGh = makeGh({ consumerWorkflow: STALE_WORKFLOW, npmVersion: "1.2.3", calls });
|
|
371
|
+
let mutated = false;
|
|
372
|
+
const report = runRepair({
|
|
373
|
+
config: laggingConfig(),
|
|
374
|
+
ref: null,
|
|
375
|
+
dryRun: true,
|
|
376
|
+
dashboardRunUrl: null,
|
|
377
|
+
token: "ghp_fake",
|
|
378
|
+
templates: null,
|
|
379
|
+
runGh,
|
|
380
|
+
runGit: () => {
|
|
381
|
+
mutated = true;
|
|
382
|
+
return "";
|
|
383
|
+
},
|
|
384
|
+
runSync: () => {
|
|
385
|
+
mutated = true;
|
|
386
|
+
return { changed: true };
|
|
387
|
+
},
|
|
388
|
+
});
|
|
389
|
+
const row = report.rows.find((r) => r.name === "domio");
|
|
390
|
+
assert.equal(row.action, "planned");
|
|
391
|
+
assert.equal(mutated, false, "dry-run must not clone or sync");
|
|
392
|
+
assert.equal(calls.filter((c) => c.kind === "create").length, 0);
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
test("runRepair defers a holding consumer (fresh release inside the hold window)", () => {
|
|
396
|
+
// published_at is "now" → inside the 3-day window → holding, not drift.
|
|
397
|
+
const runGh = (args) => {
|
|
398
|
+
const path = args[1];
|
|
399
|
+
if (path === `repos/${PLATFORM_REPO}/releases/latest`) {
|
|
400
|
+
return JSON.stringify({
|
|
401
|
+
tag_name: "mandrel-platform-v1.2.3",
|
|
402
|
+
published_at: new Date().toISOString(),
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
if (path === `repos/${PLATFORM_REPO}/git/ref/tags/mandrel-platform-v1.2.3`) {
|
|
406
|
+
return JSON.stringify({ object: { sha: LATEST_SHA, type: "commit" } });
|
|
407
|
+
}
|
|
408
|
+
if (/\/contents\/\.github\/workflows/.test(path)) {
|
|
409
|
+
return JSON.stringify([
|
|
410
|
+
{ type: "file", name: "ci.yml", content: Buffer.from(STALE_WORKFLOW).toString("base64"), encoding: "base64" },
|
|
411
|
+
]);
|
|
412
|
+
}
|
|
413
|
+
if (/\/contents\/package\.json/.test(path)) {
|
|
414
|
+
const pkg = JSON.stringify({ devDependencies: { "mandrel-platform": "1.2.3" } });
|
|
415
|
+
return JSON.stringify({ encoding: "base64", content: Buffer.from(pkg).toString("base64") });
|
|
416
|
+
}
|
|
417
|
+
if (/^repos\/[^/]+\/[^/]+$/.test(path)) return JSON.stringify({ default_branch: "main" });
|
|
418
|
+
throw new Error(`unexpected: ${path}`);
|
|
419
|
+
};
|
|
420
|
+
const report = runRepair({
|
|
421
|
+
config: laggingConfig(),
|
|
422
|
+
ref: null,
|
|
423
|
+
dryRun: false,
|
|
424
|
+
dashboardRunUrl: null,
|
|
425
|
+
token: "ghp_fake",
|
|
426
|
+
templates: null,
|
|
427
|
+
runGh,
|
|
428
|
+
runGit: () => {
|
|
429
|
+
throw new Error("must not clone a holding consumer");
|
|
430
|
+
},
|
|
431
|
+
runSync: () => {
|
|
432
|
+
throw new Error("must not sync a holding consumer");
|
|
433
|
+
},
|
|
434
|
+
});
|
|
435
|
+
const row = report.rows.find((r) => r.name === "domio");
|
|
436
|
+
assert.equal(row.action, "holding");
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
// ---------------------------------------------------------------------------
|
|
440
|
+
// renderRepairReport
|
|
441
|
+
// ---------------------------------------------------------------------------
|
|
442
|
+
|
|
443
|
+
test("renderRepairReport tabulates outcomes and a repaired section", () => {
|
|
444
|
+
const text = renderRepairReport({
|
|
445
|
+
ref: "mandrel-platform-v1.2.3",
|
|
446
|
+
targetSha: LATEST_SHA,
|
|
447
|
+
dryRun: false,
|
|
448
|
+
hasToken: true,
|
|
449
|
+
rows: [
|
|
450
|
+
{ name: "domio", repo: "dsj1984/domio", action: "opened", prNumber: 101 },
|
|
451
|
+
{ name: "athportal", repo: "dsj1984/athportal", action: "holding", prNumber: null },
|
|
452
|
+
],
|
|
453
|
+
});
|
|
454
|
+
assert.ok(text.includes("platform-sync repair loop"));
|
|
455
|
+
assert.ok(text.includes("domio"));
|
|
456
|
+
assert.ok(text.includes("Repaired (1)"));
|
|
457
|
+
assert.ok(text.includes("#101"));
|
|
458
|
+
});
|
|
File without changes
|
|
File without changes
|