diff --git a/adr/0007-scope-end-to-end-coverage-to-user-reachable-states.md b/adr/0007-scope-end-to-end-coverage-to-user-reachable-states.md index 63d39a56..aef957a8 100644 --- a/adr/0007-scope-end-to-end-coverage-to-user-reachable-states.md +++ b/adr/0007-scope-end-to-end-coverage-to-user-reachable-states.md @@ -76,6 +76,25 @@ Option 3. 4. **The floors do not move.** The browser lane keeps 100% source lines, 96% source regions and `--require-source-files`; the unit lane keeps 100% / 100%. +## Clarification: integration-boundary evidence (2026-10-21) + +Decision 3 requires surviving-mutant evidence for assertion gaps in production +logic covered by the configured mutation run. It does not require inventing a +hand-constructed mutation or another mutation harness to qualify an integration +boundary the configured mutation scope or runner does not exercise. + +For such boundaries, including root configuration, Actions event environments, +and dependency/compiler contracts, a finding may instead cite a reproducible +integration failure or a documented historical regression. It must identify the +production failure mode, explain why the mutation run does not exercise that +boundary, and add a regression test against the actual consumer or exported +configuration rather than only a helper or a duplicate parser. + +This clarification does not relax decisions 1, 2 or 4, or ADR 0006's +restrictions on test-tree guards. It does not authorize data-shape browser +fixture builds, change coverage floors, or replace surviving-mutant evidence for +assertion gaps within mutation-covered production logic. + ## Consequences - Hive `quality` findings that do not meet decision 2 are closed as not planned diff --git a/knip.config.js b/knip.config.js index 6a4e0367..22f19c7f 100644 --- a/knip.config.js +++ b/knip.config.js @@ -38,9 +38,6 @@ const config = { // Run through `npx` inside xargs pipelines in package.json scripts. 'markdown-link-check', 'markdownlint-cli', - // Type-only JSDoc import in docusaurus.config.js and sidebars.js; installed - // by @docusaurus/preset-classic. - '@docusaurus/plugin-content-docs', ], ignoreIssues: { // Tests load it through importSource(), a dynamic import knip cannot follow. diff --git a/package-lock.json b/package-lock.json index 8574a663..e62915ad 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,7 +28,9 @@ "devDependencies": { "@axe-core/playwright": "^4.14.0", "@docusaurus/faster": "^3.10.2", + "@docusaurus/mdx-loader": "3.10.2", "@docusaurus/module-type-aliases": "3.10.2", + "@docusaurus/plugin-content-docs": "3.10.2", "@docusaurus/types": "3.10.2", "@eslint/js": "^9.39.5", "@jridgewell/trace-mapping": "^0.3.31", diff --git a/package.json b/package.json index e9796e9b..f25aa789 100644 --- a/package.json +++ b/package.json @@ -111,7 +111,9 @@ "devDependencies": { "@axe-core/playwright": "^4.14.0", "@docusaurus/faster": "^3.10.2", + "@docusaurus/mdx-loader": "3.10.2", "@docusaurus/module-type-aliases": "3.10.2", + "@docusaurus/plugin-content-docs": "3.10.2", "@docusaurus/types": "3.10.2", "@eslint/js": "^9.39.5", "@jridgewell/trace-mapping": "^0.3.31", diff --git a/tests/mdx-active-content.test.mjs b/tests/mdx-active-content.test.mjs index 273db7a6..be41b970 100644 --- a/tests/mdx-active-content.test.mjs +++ b/tests/mdx-active-content.test.mjs @@ -1,5 +1,11 @@ import assert from 'node:assert/strict'; import test from 'node:test'; +import { fileURLToPath } from 'node:url'; +import { resolve } from 'node:path'; +import { loadSiteConfig } from '@docusaurus/core/lib/server/config.js'; +import { compileToJSX } from '@docusaurus/mdx-loader/lib/utils.js'; +import { DEFAULT_OPTIONS } from '@docusaurus/plugin-content-docs/lib/options.js'; +import { parseSync } from '@swc/core'; import { findActiveContent } from '../scripts/lib/mdx-active-content.mjs'; const reasons = (markdown) => findActiveContent(markdown).map((f) => f.reason); @@ -677,3 +683,138 @@ test('leaves an ordinary admonition and a fenced copy of the payload alone', () ].join('\n'); assert.deepEqual(reasons(body), []); }); + +const siteDir = fileURLToPath(new URL('../', import.meta.url)); +const { siteConfig } = await loadSiteConfig({ siteDir }); +const classicPreset = siteConfig.presets.find( + (preset) => Array.isArray(preset) && preset[0] === 'classic', +); +const compilerOptions = { + ...DEFAULT_OPTIONS, + ...classicPreset[1].docs, + siteDir, + staticDirs: siteConfig.staticDirectories.map((dir) => resolve(siteDir, dir)), + markdownConfig: siteConfig.markdown, +}; + +// Inspect the real compiler's JavaScript, not the gate's parser or rewrite +// helpers. Rejected MDX is compiled but never evaluated. +async function compiledContent(body, options = compilerOptions) { + const result = await compileToJSX({ + fileContent: body, + filePath: resolve(siteDir, 'docs/architectures/_security-regression.md'), + frontMatter: {}, + options, + compilerName: 'server', + }); + const urls = []; + const text = []; + function visit(node) { + if (!node || typeof node !== 'object') return; + if (node.type === 'StringLiteral') text.push(node.value); + if ( + node.type === 'KeyValueProperty' && + ['href', 'src'].includes(node.key.value) && + node.value.type === 'StringLiteral' + ) + urls.push([node.key.value, node.value.value]); + for (const value of Object.values(node)) { + if (Array.isArray(value)) value.forEach(visit); + else visit(value); + } + } + visit(parseSync(result.content, { syntax: 'ecmascript' })); + return { urls, text }; +} + +const COMPILER_REJECTED = [ + [ + 'whole-document fence pairing exposes a remote image', + '```mdx-code-block\n\n````\n\n```', + ['src', 'https://beacon.invalid/px.png'], + 'disallowed element ', + ], + [ + 'unwrapping exposes an encoded URL in an admonition below a heading', + '## Overview\n\n```mdx-code-block\n:::note[x](javascript:alert(1))\n```', + ['href', 'javascript:alert(1)'], + 'disallowed URL scheme javascript:', + ], + [ + 'GFM table delimiters expose an image inside an apparent code span', + '| a | b |\n| - | - |\n| `x | ` |', + ['src', 'https://beacon.invalid/px.png'], + 'disallowed element ', + ], + [ + 'duplicate definitions resolve to the first destination', + '[x][d]\n\n[d]: javascript:alert(1)\n[d]: https://www.cncf.io/', + ['href', 'javascript:alert(1)'], + 'disallowed URL scheme javascript:', + ], +]; + +for (const [name, body, url, reason] of COMPILER_REJECTED) { + test(`the publish gate rejects what Docusaurus makes active: ${name}`, async () => { + assert.deepEqual((await compiledContent(body)).urls, [url]); + assert.ok(reasons(body).includes(reason)); + }); +} + +const COMPILER_ACCEPTED = [ + [ + 'headings, modern admonitions and reference links', + '## Overview\n\n:::note[Safe title]\n\n[Read more][d]\n\n:::\n\n[d]: https://www.cncf.io/', + [['href', 'https://www.cncf.io/']], + 'Read more', + ], + [ + 'ordinary fences keep a legacy admonition payload inert', + '```\n:::note x](javascript:alert(1))\n```', + [], + ':::note x](javascript:alert(1))', + ], + [ + 'inline code preserves JSX as text', + 'Use `` as an example.', + [], + '', + ], + [ + 'YAML fences preserve braces and angle brackets', + '```yaml\nkey: {a: 1}\nexample: \n```', + [], + 'key: {a: 1}\nexample: ', + ], +]; + +for (const [name, body, urls, text] of COMPILER_ACCEPTED) { + test(`the publish gate and Docusaurus preserve valid content: ${name}`, async () => { + assert.deepEqual(reasons(body), []); + const compiled = await compiledContent(body); + assert.deepEqual(compiled.urls, urls); + assert.ok(compiled.text.some((value) => value.includes(text))); + }); +} + +test('the gate also rejects a composed bypass when legacy compatibility is enabled', async () => { + // future.v4 disables these rewrites on the current site. Exercise opt-in + // compatibility explicitly rather than claiming legacy titles are live now. + const options = { + ...compilerOptions, + markdownConfig: { + ...siteConfig.markdown, + mdx1Compat: { + ...siteConfig.markdown.mdx1Compat, + headingIds: true, + admonitions: true, + }, + }, + }; + const body = + '## Overview {#overview}\n\n```mdx-code-block\n:::note x](javascript:alert(1))\n````\n\n```'; + assert.deepEqual((await compiledContent(body, options)).urls, [ + ['href', 'javascript:alert(1)'], + ]); + assert.ok(reasons(body).includes('disallowed URL scheme javascript:')); +}); diff --git a/tests/stryker-config.test.mjs b/tests/stryker-config.test.mjs index b142b0e6..4ca3ad02 100644 --- a/tests/stryker-config.test.mjs +++ b/tests/stryker-config.test.mjs @@ -3,6 +3,7 @@ // nothing, so a stale list would quietly stop explaining the run. import assert from 'node:assert/strict'; +import { spawnSync } from 'node:child_process'; import { existsSync, readdirSync, readFileSync } from 'node:fs'; import test from 'node:test'; @@ -49,3 +50,30 @@ test('breakThreshold stands down for the full-tree report lanes', () => { assert.equal(breakThreshold('schedule'), null); assert.equal(breakThreshold('workflow_dispatch'), null); }); + +for (const [eventName, expected] of [ + [undefined, 83], + ['pull_request', 83], + ['merge_group', null], + ['schedule', null], + ['workflow_dispatch', null], +]) { + test(`exported mutation config selects the threshold for ${eventName ?? 'local runs'}`, () => { + const env = { ...process.env }; + delete env.GITHUB_EVENT_NAME; + if (eventName !== undefined) env.GITHUB_EVENT_NAME = eventName; + const run = spawnSync( + process.execPath, + [ + '--input-type=module', + '--eval', + `import config from ${JSON.stringify(new URL('../stryker.config.mjs', import.meta.url).href)}; +console.log(JSON.stringify(config.thresholds.break));`, + ], + { encoding: 'utf8', env }, + ); + assert.equal(run.error, undefined); + assert.equal(run.status, 0, run.stderr); + assert.equal(JSON.parse(run.stdout), expected); + }); +}