mandrel-platform 1.13.0 → 1.13.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.
@@ -39,9 +39,14 @@ import {
39
39
  applyExceptions,
40
40
  buildClients,
41
41
  checkShape,
42
+ collectInfisicalSecrets,
42
43
  collectWorkflowReferences,
43
44
  computeExitCode,
45
+ createGitHubClient,
46
+ createInfisicalClient,
44
47
  isAbsentStatus,
48
+ isRetryableStatus,
49
+ linkNextUrl,
45
50
  parseCliArgs,
46
51
  parseDotenv,
47
52
  parseExceptions,
@@ -50,10 +55,12 @@ import {
50
55
  reconcileNames,
51
56
  redactUrl,
52
57
  renderReport,
58
+ resolveEnvironments,
53
59
  resolveScriptName,
54
60
  resolveSurfaceEnvironment,
55
61
  runDoctor,
56
62
  runOfflineChecks,
63
+ stripJsonc,
57
64
  } from "./env-doctor.mjs";
58
65
 
59
66
  const execFileAsync = promisify(execFile);
@@ -463,7 +470,7 @@ test("renderReport lists a suppressed exception and its revisit date in the summ
463
470
  });
464
471
  const text = renderReport({ ...applied, surfaces: [], strictOrphans: false, environments: ["staging"] });
465
472
  assert.match(text, /Suppressed by an active exception/);
466
- assert.match(text, /A \[github\/staging\] — revisit 2027-01-01: deferred pending rotation/);
473
+ assert.match(text, /A \[github\/staging\] \(fail\) — revisit 2027-01-01: deferred pending rotation/);
467
474
  assert.match(text, /1 suppressed/);
468
475
  });
469
476
 
@@ -941,8 +948,10 @@ test("CLI with no credentials prints one ::notice per unchecked surface and exit
941
948
  },
942
949
  });
943
950
  assert.equal(run.code, 0, run.stdout + run.stderr);
944
- const notices = run.stdout.split("\n").filter((l) => l.startsWith("::notice title=env-doctor surface unchecked::"));
945
- assert.equal(notices.length, 3, `expected one notice per absent surface, got:\n${run.stdout}`);
951
+ // On STDERR since Story #487: stdout is the machine channel in every mode.
952
+ const notices = run.stderr.split("\n").filter((l) => l.startsWith("::notice title=env-doctor surface unchecked::"));
953
+ assert.equal(notices.length, 3, `expected one notice per absent surface, got:\n${run.stderr}`);
954
+ assert.ok(!run.stdout.includes("::notice"), "no annotation may share the machine channel");
946
955
  assert.match(run.stdout, /offline: checked/);
947
956
  } finally {
948
957
  rmSync(root, { recursive: true, force: true });
@@ -1922,3 +1931,520 @@ test("CLI: the reusable workflow's env shape reaches the identifiers without any
1922
1931
  rmSync(root, { recursive: true, force: true });
1923
1932
  }
1924
1933
  });
1934
+
1935
+ // ---------------------------------------------------------------------------
1936
+ // Story #487 — the edges where the doctor answered wrongly, or unparseably
1937
+ // ---------------------------------------------------------------------------
1938
+
1939
+ /**
1940
+ * A minimal `Response` stand-in. `headers.get` is included because the GitHub
1941
+ * pagination reads `Link` off it, and a fake that omits headers is exactly the
1942
+ * shape the client must survive rather than throw on.
1943
+ *
1944
+ * @param {unknown} body
1945
+ * @param {{status?: number, headers?: Record<string, string>}} [opts]
1946
+ */
1947
+ function fakeResponse(body, { status = 200, headers = {} } = {}) {
1948
+ return {
1949
+ ok: status >= 200 && status < 300,
1950
+ status,
1951
+ statusText: `synthetic ${status}`,
1952
+ headers: { get: (name) => headers[name.toLowerCase()] ?? null },
1953
+ json: async () => body,
1954
+ };
1955
+ }
1956
+
1957
+ // --- AC-1: an undeclared environment fails closed --------------------------
1958
+
1959
+ test("resolveEnvironments refuses a slug the manifest does not declare, and defaults to the manifest's own", () => {
1960
+ const manifest = parseManifest(singleWorkerManifest());
1961
+ assert.deepEqual(resolveEnvironments({ requested: null, manifest }), ["staging", "production"]);
1962
+ assert.deepEqual(resolveEnvironments({ requested: "", manifest }), ["staging", "production"]);
1963
+ assert.deepEqual(resolveEnvironments({ requested: " production , staging ", manifest }), ["production", "staging"]);
1964
+ assert.throws(() => resolveEnvironments({ requested: "prodcution", manifest }), /prodcution/);
1965
+ });
1966
+
1967
+ test("CLI: an environment absent from the manifest exits 1 before any probe, naming both sides", async () => {
1968
+ // The defect this closes is not a crash — it is a PASS. Before Story #487
1969
+ // this exact argv exited 0 with every surface `checked` and zero findings,
1970
+ // because a slug no key claims narrows every reconcile to the empty set.
1971
+ const root = makeRepo(CONSISTENT_REPO);
1972
+ const manifestPath = join(root, "env.manifest.json");
1973
+ writeFileSync(manifestPath, JSON.stringify(singleWorkerManifest()));
1974
+ try {
1975
+ const run = await runCli([
1976
+ "--manifest",
1977
+ manifestPath,
1978
+ "--repo-root",
1979
+ root,
1980
+ "--environments",
1981
+ "prodcution",
1982
+ "--offline",
1983
+ ]);
1984
+ assert.equal(run.code, 1, `expected a usage failure, got:\n${run.stdout}${run.stderr}`);
1985
+ assert.ok(run.stderr.includes("prodcution"), `stderr must name the rejected slug:\n${run.stderr}`);
1986
+ assert.ok(
1987
+ run.stderr.includes("staging, production"),
1988
+ `stderr must name the declared environments:\n${run.stderr}`
1989
+ );
1990
+ assert.equal(run.stdout, "", "nothing was probed, so no report may be emitted");
1991
+ } finally {
1992
+ rmSync(root, { recursive: true, force: true });
1993
+ }
1994
+ });
1995
+
1996
+ // --- AC-2: imported Infisical secrets are resident where they were asked for
1997
+
1998
+ test("an imported secret counts in the folder that was ASKED for, is shape-checked there, and never prints", async () => {
1999
+ // A deliberately dull, obviously-non-credential placeholder: this string is
2000
+ // grepped out of the captured output, and uniqueness is all the assertion
2001
+ // needs — key-shaped entropy here would be a true positive for the secret
2002
+ // scan, which reads entropy beside a secret-shaped name.
2003
+ // The marker is asserted on, not the whole URL: a substring check against a
2004
+ // URL-shaped string CONSTANT is `js/incomplete-url-substring-sanitization`
2005
+ // (CodeQL, high — it cannot tell a leak assertion from a host check). The
2006
+ // sibling suites escape it only because they compare against a loop
2007
+ // variable. Splitting the marker out keeps the value a real URL, so the
2008
+ // `shape: "url"` stage is still exercised, and makes the leak assertion
2009
+ // STRONGER: any fragment of the value failing to be redacted now fails here.
2010
+ const IMPORTED_VALUE_MARKER = "imported-from-the-shared-folder";
2011
+ const IMPORTED_VALUE = `https://${IMPORTED_VALUE_MARKER}.test`;
2012
+ const requested = [];
2013
+ const fetchImpl = async (url) => {
2014
+ requested.push(url);
2015
+ const withValues = new URL(url).searchParams.get("viewSecretValue") === "true";
2016
+ return fakeResponse({
2017
+ // The queried folder defines NOTHING itself. Everything it resolves
2018
+ // arrives through the import — the shape that reported every key
2019
+ // missing while the deploy would have resolved all of them.
2020
+ secrets: [],
2021
+ imports: [
2022
+ {
2023
+ secretPath: "/shared",
2024
+ environment: "staging",
2025
+ folderId: "folder-shared",
2026
+ secrets: [{ secretKey: "PUBLIC_SITE_URL", ...(withValues ? { secretValue: IMPORTED_VALUE } : {}) }],
2027
+ },
2028
+ ],
2029
+ });
2030
+ };
2031
+ const infisical = createInfisicalClient({
2032
+ token: "test-access-token",
2033
+ projectId: "proj-1",
2034
+ siteUrl: "https://infisical.test",
2035
+ fetchImpl,
2036
+ });
2037
+ const manifest = parseManifest({
2038
+ environments: ["staging"],
2039
+ workers: {},
2040
+ keys: [
2041
+ {
2042
+ name: "PUBLIC_SITE_URL",
2043
+ kind: "var",
2044
+ sensitivity: "public",
2045
+ residency: { local: null, github: null, cloudflare: null },
2046
+ infisical: { folder: "/cloudflare", environments: ["staging"] },
2047
+ shape: "url",
2048
+ },
2049
+ ],
2050
+ });
2051
+
2052
+ const root = makeRepo({});
2053
+ try {
2054
+ const report = await runDoctor({ manifest, repoRoot: root, environments: ["staging"], infisical });
2055
+ const surface = report.surfaces.find((s) => s.surface === "infisical");
2056
+ assert.deepEqual(
2057
+ report.findings.filter((f) => f.surface === "infisical"),
2058
+ [],
2059
+ "an imported secret is resident at the folder that resolves it"
2060
+ );
2061
+ assert.equal(surface.status, "checked");
2062
+ assert.match(surface.notice, /1 value shape\(s\) verified/, "the imported value must reach the shape stage");
2063
+ assert.ok(requested.length > 0, "the client must have been called");
2064
+ for (const url of requested) {
2065
+ assert.ok(url.includes("includeImports=true"), `every listing must send includeImports explicitly: ${url}`);
2066
+ assert.ok(url.includes("secretPath=%2Fcloudflare"), `the REQUESTED folder is what is asked for: ${url}`);
2067
+ }
2068
+ const captured = renderReport(report) + JSON.stringify(report);
2069
+ assert.ok(!captured.includes(IMPORTED_VALUE_MARKER), "an imported VALUE reached the doctor's output");
2070
+ } finally {
2071
+ rmSync(root, { recursive: true, force: true });
2072
+ }
2073
+ });
2074
+
2075
+ test("collectInfisicalSecrets gives a directly-defined name precedence over an imported one", () => {
2076
+ const merged = collectInfisicalSecrets({
2077
+ secrets: [{ secretKey: "OVERRIDDEN", secretValue: "local-wins" }],
2078
+ imports: [
2079
+ { secretPath: "/shared", secrets: [{ secretKey: "OVERRIDDEN", secretValue: "import-loses" }] },
2080
+ { secretPath: "/base", secrets: [{ secretKey: "ONLY_IMPORTED", secretValue: "from-base" }] },
2081
+ ],
2082
+ });
2083
+ assert.deepEqual(
2084
+ merged.map((s) => s.secretKey).sort(),
2085
+ ["ONLY_IMPORTED", "OVERRIDDEN"],
2086
+ "each name appears once, whichever parts of the response carried it"
2087
+ );
2088
+ assert.equal(merged.find((s) => s.secretKey === "OVERRIDDEN").secretValue, "local-wins");
2089
+ });
2090
+
2091
+ test("collectInfisicalSecrets is total over a response missing either half", () => {
2092
+ assert.deepEqual(collectInfisicalSecrets({}), []);
2093
+ assert.deepEqual(collectInfisicalSecrets({ imports: null, secrets: undefined }), []);
2094
+ assert.deepEqual(collectInfisicalSecrets({ imports: [{ secrets: "not-an-array" }] }), []);
2095
+ });
2096
+
2097
+ // --- AC-3: the JSONC wrangler actually emits -------------------------------
2098
+
2099
+ test("parseWranglerVars reads the JSONC wrangler emits — trailing comma, block and inline comments", () => {
2100
+ const config = [
2101
+ "{",
2102
+ ' // Generated by create-cloudflare.',
2103
+ ' "name": "acme-site",',
2104
+ " /* A block comment,",
2105
+ ' which may contain "quotes" and , commas. */',
2106
+ ' "vars": {',
2107
+ ' "PUBLIC_SITE_URL": "https://example.test", // an inline comment after a value',
2108
+ ' "PUBLIC_BUILD_CHANNEL": "stable",',
2109
+ " },",
2110
+ "}",
2111
+ ].join("\n");
2112
+ assert.deepEqual(parseWranglerVars(config, "wrangler.jsonc"), ["PUBLIC_BUILD_CHANNEL", "PUBLIC_SITE_URL"]);
2113
+ });
2114
+
2115
+ test("stripJsonc leaves comment-looking and comma-looking text inside strings alone", () => {
2116
+ const parsed = JSON.parse(stripJsonc('{"url": "https://example.test/a//b", "trailing": "a, b",}'));
2117
+ assert.equal(parsed.url, "https://example.test/a//b", "a // inside a string is not a comment");
2118
+ assert.equal(parsed.trailing, "a, b");
2119
+ });
2120
+
2121
+ test("an unparseable wrangler config is ONE fail finding on the wrangler surface, never an empty var set", () => {
2122
+ const root = mkdtempSync(join(tmpdir(), "env-doctor-jsonc-"));
2123
+ writeFileSync(join(root, "wrangler.jsonc"), '{ "vars": { "PUBLIC_SITE_URL": } }');
2124
+ const manifest = parseManifest({
2125
+ environments: ["staging"],
2126
+ workers: { site: { config: "wrangler.jsonc", scriptName: "acme-site-{env}" } },
2127
+ keys: [
2128
+ {
2129
+ name: "PUBLIC_SITE_URL",
2130
+ kind: "var",
2131
+ sensitivity: "public",
2132
+ residency: { local: null, github: null, cloudflare: { workers: ["site"], kind: "var" } },
2133
+ infisical: "unmanaged",
2134
+ },
2135
+ ],
2136
+ });
2137
+ try {
2138
+ assert.throws(() => parseWranglerVars('{ "vars": }', "wrangler.jsonc"), /not parseable as JSON\/JSONC/);
2139
+ const { findings, checked } = runOfflineChecks({ manifest, repoRoot: root });
2140
+ const wrangler = findings.filter((f) => f.surface === "wrangler");
2141
+ assert.equal(wrangler.length, 1, `expected exactly one wrangler finding, got ${JSON.stringify(wrangler)}`);
2142
+ assert.equal(wrangler[0].severity, "fail");
2143
+ assert.match(wrangler[0].detail, /not parseable as JSON\/JSONC/);
2144
+ assert.ok(!checked.includes("wrangler:site"), "a file that could not be read was not checked");
2145
+ } finally {
2146
+ rmSync(root, { recursive: true, force: true });
2147
+ }
2148
+ });
2149
+
2150
+ // --- AC-4: GitHub listings complete past page one --------------------------
2151
+
2152
+ test("linkNextUrl reads rel=\"next\" out of an RFC 8288 header and ignores every other rel", () => {
2153
+ const header =
2154
+ '<https://api.github.test/x?page=2>; rel="next", <https://api.github.test/x?page=9>; rel="last"';
2155
+ assert.equal(linkNextUrl(header), "https://api.github.test/x?page=2");
2156
+ assert.equal(linkNextUrl('<https://api.github.test/x?page=1>; rel="prev"'), null);
2157
+ assert.equal(linkNextUrl(""), null);
2158
+ assert.equal(linkNextUrl(null), null);
2159
+ });
2160
+
2161
+ test("a GitHub listing follows Link pagination — 150 secrets across two pages, no false missing", async () => {
2162
+ const names = Array.from({ length: 150 }, (_, i) => `REPO_SECRET_${String(i + 1).padStart(3, "0")}`);
2163
+ const apiBase = "https://api.github.test";
2164
+ const nextUrl = `${apiBase}/repos/o/r/actions/secrets?per_page=100&page=2`;
2165
+ const fetched = [];
2166
+ const fetchImpl = async (url) => {
2167
+ fetched.push(url);
2168
+ if (!url.includes("/actions/secrets")) return fakeResponse({ secrets: [], variables: [] });
2169
+ if (url.includes("page=2")) {
2170
+ return fakeResponse({ total_count: 150, secrets: names.slice(100).map((name) => ({ name })) });
2171
+ }
2172
+ return fakeResponse(
2173
+ { total_count: 150, secrets: names.slice(0, 100).map((name) => ({ name })) },
2174
+ { headers: { link: `<${nextUrl}>; rel="next", <${nextUrl}>; rel="last"` } }
2175
+ );
2176
+ };
2177
+ const github = createGitHubClient({ token: "test-pat", repo: "o/r", fetchImpl, apiBase });
2178
+ const manifest = parseManifest({
2179
+ environments: ["staging"],
2180
+ keys: names.map((name) => ({
2181
+ name,
2182
+ kind: "secret",
2183
+ sensitivity: "secret",
2184
+ residency: { local: null, github: { scope: "repository", kind: "secret" } },
2185
+ infisical: "unmanaged",
2186
+ })),
2187
+ });
2188
+
2189
+ const root = makeRepo({});
2190
+ try {
2191
+ const report = await runDoctor({ manifest, repoRoot: root, environments: ["staging"], github });
2192
+ assert.equal(report.surfaces.find((s) => s.surface === "github").status, "checked");
2193
+ assert.deepEqual(
2194
+ report.findings.filter((f) => f.surface === "github"),
2195
+ [],
2196
+ "every name past the page boundary must be seen — page one alone invents 50 missing keys"
2197
+ );
2198
+ assert.ok(fetched.includes(nextUrl), "the rel=\"next\" page must actually be requested");
2199
+ } finally {
2200
+ rmSync(root, { recursive: true, force: true });
2201
+ }
2202
+ });
2203
+
2204
+ // --- AC-5: one JSON document on stdout -------------------------------------
2205
+
2206
+ test("CLI --json emits exactly one parseable document, with every annotation on stderr", async () => {
2207
+ const root = makeRepo(CONSISTENT_REPO);
2208
+ const manifestPath = join(root, "env.manifest.json");
2209
+ writeFileSync(manifestPath, JSON.stringify(singleWorkerManifest()));
2210
+ try {
2211
+ const run = await runCli(["--manifest", manifestPath, "--repo-root", root, "--json"], {
2212
+ env: {
2213
+ ENV_DRIFT_GITHUB_TOKEN: "",
2214
+ CLOUDFLARE_API_TOKEN: "",
2215
+ INFISICAL_TOKEN: "",
2216
+ INFISICAL_CLIENT_ID: "",
2217
+ INFISICAL_CLIENT_SECRET: "",
2218
+ },
2219
+ });
2220
+ assert.equal(run.code, 0, run.stdout + run.stderr);
2221
+ const report = JSON.parse(run.stdout);
2222
+ assert.equal(report.exitCode, 0);
2223
+ assert.equal(report.surfaces.filter((s) => s.status === "unchecked").length, 3);
2224
+ assert.ok(!run.stdout.includes("::notice"), "an annotation on stdout is what made JSON.parse throw");
2225
+ assert.ok(!run.stdout.includes("::error"), "an annotation on stdout is what made JSON.parse throw");
2226
+ const notices = run.stderr.split("\n").filter((l) => l.startsWith("::notice title=env-doctor surface unchecked::"));
2227
+ assert.equal(notices.length, 3, `every unchecked surface still annotates, on stderr:\n${run.stderr}`);
2228
+ } finally {
2229
+ rmSync(root, { recursive: true, force: true });
2230
+ }
2231
+ });
2232
+
2233
+ // --- AC-6: every probe is bounded and retried ------------------------------
2234
+
2235
+ /** Drive one GitHub client through `runDoctor` and return its surface record. */
2236
+ async function githubSurface(github, environments = ["staging"]) {
2237
+ const root = makeRepo({});
2238
+ try {
2239
+ const report = await runDoctor({
2240
+ manifest: githubOnlyManifest({ scope: "repository", kind: "secret" }),
2241
+ repoRoot: root,
2242
+ environments,
2243
+ github,
2244
+ });
2245
+ return report.surfaces.find((s) => s.surface === "github");
2246
+ } finally {
2247
+ rmSync(root, { recursive: true, force: true });
2248
+ }
2249
+ }
2250
+
2251
+ test("a fetchImpl that never settles AND ignores init.signal still fails its surface inside the budget", async () => {
2252
+ // The signal alone is a request to stop, honoured at the transport's
2253
+ // discretion. This fake accepts it and does nothing with it — the shape a
2254
+ // stub, a polyfill, or an init-rebuilding wrapper produces — so only the
2255
+ // raced timer can end the call.
2256
+ const timeoutMs = 150;
2257
+ let signalled = false;
2258
+ const fetchImpl = (_url, init) => {
2259
+ signalled = Boolean(init?.signal);
2260
+ return new Promise(() => {});
2261
+ };
2262
+ const github = createGitHubClient({ token: "test-pat", repo: "o/r", fetchImpl, timeoutMs, retryDelayMs: 1 });
2263
+ const startedAt = Date.now();
2264
+ const surface = await githubSurface(github);
2265
+ const elapsed = Date.now() - startedAt;
2266
+ assert.equal(surface.status, "error");
2267
+ assert.match(surface.notice, /timed out after 150ms/);
2268
+ assert.ok(signalled, "the signal is still passed — a real fetch uses it to release the socket");
2269
+ assert.ok(elapsed < timeoutMs * 2, `expected a failure inside twice the timeout, took ${elapsed}ms`);
2270
+ });
2271
+
2272
+ test("a 503 twice then a 200 reports checked — the retry is bounded, not absent", async () => {
2273
+ const attempts = new Map();
2274
+ const fetchImpl = async (url) => {
2275
+ const n = (attempts.get(url) ?? 0) + 1;
2276
+ attempts.set(url, n);
2277
+ if (n <= 2) return fakeResponse({ message: "unavailable" }, { status: 503 });
2278
+ return fakeResponse({ secrets: [{ name: "SHARED_TOKEN" }], variables: [] });
2279
+ };
2280
+ const github = createGitHubClient({ token: "test-pat", repo: "o/r", fetchImpl, retryDelayMs: 1 });
2281
+ const surface = await githubSurface(github);
2282
+ assert.equal(surface.status, "checked", surface.notice ?? "");
2283
+ for (const [url, n] of attempts) assert.equal(n, 3, `${url} should have taken three attempts`);
2284
+ });
2285
+
2286
+ test("a 401 is not retried — one attempt per request, and the surface errors", async () => {
2287
+ const attempts = new Map();
2288
+ const fetchImpl = async (url) => {
2289
+ attempts.set(url, (attempts.get(url) ?? 0) + 1);
2290
+ return fakeResponse({ message: "Bad credentials" }, { status: 401 });
2291
+ };
2292
+ const github = createGitHubClient({ token: "expired-pat", repo: "o/r", fetchImpl, retryDelayMs: 1 });
2293
+ const surface = await githubSurface(github);
2294
+ assert.equal(surface.status, "error");
2295
+ assert.match(surface.notice, /401/);
2296
+ assert.ok(attempts.size > 0, "the client must have been called");
2297
+ for (const [url, n] of attempts) assert.equal(n, 1, `${url} must not be retried on a 401`);
2298
+ });
2299
+
2300
+ test("isRetryableStatus covers 429 and the 5xx band, and nothing else", () => {
2301
+ assert.ok(isRetryableStatus(429));
2302
+ assert.ok(isRetryableStatus(500));
2303
+ assert.ok(isRetryableStatus(503));
2304
+ assert.ok(!isRetryableStatus(401));
2305
+ assert.ok(!isRetryableStatus(404));
2306
+ assert.ok(!isRetryableStatus(422));
2307
+ });
2308
+
2309
+ // --- AC-7: a dated orphan exception ----------------------------------------
2310
+
2311
+ test('a severity: "orphan" exception suppresses its orphan and clears --strict-orphans', () => {
2312
+ const findings = [
2313
+ {
2314
+ severity: "orphan",
2315
+ kind: "orphan",
2316
+ key: "BUILDER_SCRATCH_TOKEN",
2317
+ surface: "infisical",
2318
+ environment: "staging",
2319
+ detail: "present in infisical but declared by no manifest key",
2320
+ },
2321
+ ];
2322
+ const exceptions = parseExceptions([
2323
+ { key: "BUILDER_SCRATCH_TOKEN", severity: "orphan", "revisit-date": "2027-01-01", reason: "platform tooling" },
2324
+ ]);
2325
+ const applied = applyExceptions({ findings, exceptions, now: new Date("2026-09-10T00:00:00Z") });
2326
+ assert.deepEqual(applied.findings, [], "the orphan moves out of the reported set");
2327
+ assert.equal(applied.suppressed.length, 1);
2328
+ assert.equal(applied.suppressed[0].severity, "orphan");
2329
+ assert.equal(
2330
+ computeExitCode({ findings: applied.findings, expired: applied.expired, surfaces: [], strictOrphans: true }),
2331
+ 0,
2332
+ "the whole point: --strict-orphans passes without widening the manifest"
2333
+ );
2334
+ });
2335
+
2336
+ test('an orphan exception clears a whole --strict-orphans run end to end', async () => {
2337
+ // The unit above scores the exit CONTRACT; this one scores the run. The
2338
+ // manifest is deliberately left honest — BUILDER_SCRATCH_TOKEN is declared
2339
+ // nowhere in it — because widening the manifest to buy the same green is
2340
+ // precisely what this exception form exists to avoid.
2341
+ const manifest = infisicalOnlyManifest({ folder: "/", environments: ["staging"] });
2342
+ const root = makeRepo({});
2343
+ try {
2344
+ const report = await runDoctor({
2345
+ manifest,
2346
+ repoRoot: root,
2347
+ environments: ["staging"],
2348
+ strictOrphans: true,
2349
+ exceptions: parseExceptions([
2350
+ { key: "BUILDER_SCRATCH_TOKEN", severity: "orphan", "revisit-date": "2027-01-01" },
2351
+ ]),
2352
+ now: new Date("2026-09-10T00:00:00Z"),
2353
+ infisical: {
2354
+ listNames: async () => ["SHARED_TOKEN", "BUILDER_SCRATCH_TOKEN"],
2355
+ listValues: async () => new Map(),
2356
+ },
2357
+ });
2358
+ assert.equal(report.exitCode, 0, `--strict-orphans should pass:\n${renderReport(report)}`);
2359
+ assert.deepEqual(report.findings, [], "the suppressed orphan leaves the reported set");
2360
+ assert.equal(report.suppressed.length, 1);
2361
+ assert.equal(report.suppressed[0].key, "BUILDER_SCRATCH_TOKEN");
2362
+ assert.match(renderReport(report), /BUILDER_SCRATCH_TOKEN \[infisical\/staging\] \(orphan\) — revisit 2027-01-01/);
2363
+ } finally {
2364
+ rmSync(root, { recursive: true, force: true });
2365
+ }
2366
+ });
2367
+
2368
+ test("severity defaults to fail, and a fail exception never silences an orphan", () => {
2369
+ const [entry] = parseExceptions([{ key: "LEGACY_API_KEY", "revisit-date": "2027-01-01" }]);
2370
+ assert.equal(entry.severity, "fail");
2371
+ const orphan = { severity: "orphan", kind: "orphan", key: "LEGACY_API_KEY", surface: "github", environment: null };
2372
+ const applied = applyExceptions({
2373
+ findings: [orphan],
2374
+ exceptions: [entry],
2375
+ now: new Date("2026-09-10T00:00:00Z"),
2376
+ });
2377
+ assert.equal(applied.suppressed.length, 0, "a default exception defers a failure, not an orphan");
2378
+ assert.deepEqual(applied.findings, [orphan]);
2379
+ });
2380
+
2381
+ test("an expired orphan exception still fails the run, and an unknown severity is refused", () => {
2382
+ const exceptions = parseExceptions([
2383
+ { key: "BUILDER_SCRATCH_TOKEN", severity: "orphan", "revisit-date": "2026-01-01" },
2384
+ ]);
2385
+ const applied = applyExceptions({ findings: [], exceptions, now: new Date("2026-09-10T00:00:00Z") });
2386
+ assert.equal(applied.expired.length, 1);
2387
+ assert.equal(
2388
+ computeExitCode({ findings: [], expired: applied.expired, surfaces: [], strictOrphans: false }),
2389
+ 1,
2390
+ "an orphan exception expires as loudly as any other"
2391
+ );
2392
+ assert.throws(
2393
+ () => parseExceptions([{ key: "X", severity: "warn", "revisit-date": "2027-01-01" }]),
2394
+ /severity must be "fail" or "orphan"/
2395
+ );
2396
+ });
2397
+
2398
+ // --- AC-8: the docblock and the reference narrowing ------------------------
2399
+
2400
+ test("the docblock names .env.example as the local surface and claims no real .env is read", () => {
2401
+ const source = readFileSync(SCRIPT, "utf8");
2402
+ const docblock = source.slice(source.indexOf("/**"), source.indexOf("*/") + 2);
2403
+ assert.ok(docblock.includes("`.env.example` in the caller repo"), "the local surface must name .env.example");
2404
+ assert.ok(
2405
+ !docblock.includes("`.env` /") && !docblock.includes("`.env`\n"),
2406
+ "the docblock must not claim a developer's real .env is a probed surface"
2407
+ );
2408
+ });
2409
+
2410
+ test("collectWorkflowReferences ignores a `vars` reached as a property, and whole-line comment prose", () => {
2411
+ assert.deepEqual(collectWorkflowReferences("steps.x.outputs.vars.Y"), { secrets: [], vars: [] });
2412
+ assert.deepEqual(collectWorkflowReferences(" # supply secrets.LEGACY_TOKEN if you still have one"), {
2413
+ secrets: [],
2414
+ vars: [],
2415
+ });
2416
+ // Only a WHOLE-line comment is prose. A trailing `#` on a line that also
2417
+ // carries YAML is not worth a parser: narrowing to the start of the line is
2418
+ // the rule that cannot accidentally drop a real reference.
2419
+ assert.deepEqual(collectWorkflowReferences("SITE: ${{ vars.PUBLIC_SITE_URL }} # see vars.TRAILING_NOTE"), {
2420
+ secrets: [],
2421
+ vars: ["PUBLIC_SITE_URL", "TRAILING_NOTE"].sort(),
2422
+ });
2423
+ assert.deepEqual(collectWorkflowReferences(" run: echo ${{ secrets.TURSO_AUTH_TOKEN }}"), {
2424
+ secrets: ["TURSO_AUTH_TOKEN"],
2425
+ vars: [],
2426
+ });
2427
+ });
2428
+
2429
+ // --- AC-9: the documented contract -----------------------------------------
2430
+
2431
+ test("the env-drift docs carry the fail-closed environments rule and the orphan exception form", () => {
2432
+ const doc = readFileSync(join(HERE, "..", "docs", "reusable-workflows.md"), "utf8");
2433
+ // `includes` + a message, not `assert.match`: a regex miss here dumps the
2434
+ // whole document into the failure output and buries the reason.
2435
+ const start = doc.indexOf("## `env-drift.yml`");
2436
+ assert.notEqual(start, -1, "the env-drift section must exist");
2437
+ const section = doc.slice(start, doc.indexOf("\n## ", start + 1));
2438
+ assert.ok(
2439
+ section.includes("#### Requested environments fail closed"),
2440
+ "the env-drift section must document that an undeclared environment fails the run"
2441
+ );
2442
+ assert.ok(
2443
+ section.includes("| `environments` names a slug the manifest does not declare | 1 |"),
2444
+ "the exit-contract table must carry the undeclared-environment row"
2445
+ );
2446
+ assert.ok(
2447
+ section.includes('"severity": "orphan"'),
2448
+ "the exceptions section must show the orphan-suppressing entry form"
2449
+ );
2450
+ });
@@ -0,0 +1,66 @@
1
+ {
2
+ "auditReportVersion": 2,
3
+ "vulnerabilities": {
4
+ "fixture-lib": {
5
+ "name": "fixture-lib",
6
+ "severity": "high",
7
+ "isDirect": false,
8
+ "via": [
9
+ {
10
+ "source": 1105678,
11
+ "name": "fixture-lib",
12
+ "dependency": "fixture-lib",
13
+ "title": "Prototype pollution in fixture-lib",
14
+ "url": "https://github.com/advisories/GHSA-fixt-ure0-0001",
15
+ "severity": "high",
16
+ "cwe": ["CWE-1321"],
17
+ "cvss": {
18
+ "score": 8.2,
19
+ "vectorString": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:H"
20
+ },
21
+ "range": "<1.4.2"
22
+ }
23
+ ],
24
+ "effects": ["fixture-consumer"],
25
+ "range": "<1.4.2",
26
+ "nodes": ["node_modules/fixture-lib"],
27
+ "fixAvailable": {
28
+ "name": "fixture-lib",
29
+ "version": "1.4.2",
30
+ "isSemVerMajor": false
31
+ }
32
+ },
33
+ "fixture-consumer": {
34
+ "name": "fixture-consumer",
35
+ "severity": "high",
36
+ "isDirect": true,
37
+ "via": ["fixture-lib"],
38
+ "effects": [],
39
+ "range": "*",
40
+ "nodes": ["node_modules/fixture-consumer"],
41
+ "fixAvailable": {
42
+ "name": "fixture-lib",
43
+ "version": "1.4.2",
44
+ "isSemVerMajor": false
45
+ }
46
+ }
47
+ },
48
+ "metadata": {
49
+ "vulnerabilities": {
50
+ "info": 0,
51
+ "low": 0,
52
+ "moderate": 0,
53
+ "high": 2,
54
+ "critical": 0,
55
+ "total": 2
56
+ },
57
+ "dependencies": {
58
+ "prod": 214,
59
+ "dev": 0,
60
+ "optional": 0,
61
+ "peer": 0,
62
+ "peerOptional": 0,
63
+ "total": 214
64
+ }
65
+ }
66
+ }