mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
test(mutation): сократить browser guards реестра (#659)
Issue: #659 User-Visible: no
This commit is contained in:
@@ -8,6 +8,10 @@
|
||||
import { readdirSync, readFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { METRIC_NAMES, collectMetrics, readBaseline } from './monolith-metrics.mjs';
|
||||
import { MUTANTS } from './mutation-registry.mjs';
|
||||
import {
|
||||
BROWSER_GUARD_LIMIT, browserGuardPolicy, readDocumentedBrowserGuards,
|
||||
} from './mutation-browser-policy.mjs';
|
||||
import { isMainModule } from './spawn-portable.mjs';
|
||||
|
||||
const count = (root, dir, match, re) =>
|
||||
@@ -39,6 +43,12 @@ if (isMainModule(import.meta.url)) {
|
||||
const w = Math.max(...rows.map(([n]) => n.length));
|
||||
for (const [name, n] of rows) console.log(`${name.padEnd(w)} ${n}`);
|
||||
|
||||
const browser = browserGuardPolicy(MUTANTS, readDocumentedBrowserGuards(process.cwd()));
|
||||
console.log(`\nmutation browser guards ${browser.count}/${BROWSER_GUARD_LIMIT}`);
|
||||
if (browser.missingReasons.length || browser.staleReasons.length) {
|
||||
console.log(`mutation browser inventory drift +${browser.missingReasons.length} / -${browser.staleReasons.length}`);
|
||||
}
|
||||
|
||||
// #624: связность монолита — те же шесть чисел и тот же модуль, что у гейта
|
||||
// `npm run lint:unused` («одно число — один источник»); рядом — база, чтобы
|
||||
// движение было видно без git blame. bundleBytes есть только после сборки.
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
// Browser mutation witnesses are the expensive part of the registry. This
|
||||
// module keeps the cap and the reviewed inventory independent from the CLI so
|
||||
// inventory, --check and unit tests ask the same question (#659).
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
|
||||
export const BROWSER_GUARD_LIMIT = 200;
|
||||
export const BROWSER_GUARD_INVENTORY = 'docs/testing-notes/mutation-browser-guards.md';
|
||||
|
||||
export const isBrowserGuard = (guard) => guard.includes('demo/') || guard.includes('bundle:sync');
|
||||
|
||||
export function documentedBrowserGuards(markdown) {
|
||||
return new Set([...markdown.matchAll(/^- `([^`]+)`$/gm)].map((match) => match[1]));
|
||||
}
|
||||
|
||||
export function readDocumentedBrowserGuards(root = process.cwd()) {
|
||||
return documentedBrowserGuards(readFileSync(join(root, BROWSER_GUARD_INVENTORY), 'utf8'));
|
||||
}
|
||||
|
||||
export function browserGuardPolicy(mutants, documented) {
|
||||
const browser = mutants.filter((mutant) => isBrowserGuard(mutant.guard));
|
||||
const current = new Set(browser.map((mutant) => mutant.id));
|
||||
return {
|
||||
browser,
|
||||
count: browser.length,
|
||||
overLimit: browser.length > BROWSER_GUARD_LIMIT,
|
||||
missingReasons: browser.filter((mutant) => !documented.has(mutant.id)),
|
||||
staleReasons: [...documented].filter((id) => !current.has(id)),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
// One clean frontend bundle can serve every browser mutant whose patch does
|
||||
// not touch a bundle input. Keep this policy outside the execution lifecycle:
|
||||
// the runner applies it, while tests can judge it without making worktrees.
|
||||
import { cpSync, existsSync, mkdtempSync, rmSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import { fingerprintCorpus } from './source-fingerprint.mjs';
|
||||
|
||||
const repoRoot = fileURLToPath(new URL('..', import.meta.url));
|
||||
const BUNDLE_CORPUS = new Set(fingerprintCorpus(repoRoot));
|
||||
const BUNDLE_TREES = ['dist', 'demo/srv/assets'];
|
||||
|
||||
export function guardNeedsBundle(guard) {
|
||||
return guard.includes('demo/') || guard.includes('bundle:sync');
|
||||
}
|
||||
|
||||
export function mutantPatchesNeedBundle(patches, corpus = BUNDLE_CORPUS) {
|
||||
const bundled = corpus instanceof Set ? corpus : new Set(corpus);
|
||||
return patches.some((patch) => bundled.has(patch.file.replaceAll('\\', '/')));
|
||||
}
|
||||
|
||||
export function mutantBundleStrategy(mutant, { seedAvailable = false, corpus } = {}) {
|
||||
if (!guardNeedsBundle(mutant.guard)) return 'none';
|
||||
if (seedAvailable && !mutantPatchesNeedBundle(mutant.patches, corpus)) return 'seed';
|
||||
return 'build';
|
||||
}
|
||||
|
||||
export const planNeedsBundleSeed = (mutants) => mutants.some((mutant) => (
|
||||
guardNeedsBundle(mutant.guard) && !mutantPatchesNeedBundle(mutant.patches)
|
||||
));
|
||||
|
||||
export const makeBundleSeed = () => mkdtempSync(join(tmpdir(), 'hp-mutant-bundle-'));
|
||||
|
||||
export function dropBundleSeed(dir) {
|
||||
if (dir) rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
export function captureBundleSeed(root, seed) {
|
||||
for (const name of BUNDLE_TREES) {
|
||||
const from = join(root, name);
|
||||
if (!existsSync(from)) throw new Error(`bundle seed source is missing: ${name}`);
|
||||
const to = join(seed, name);
|
||||
rmSync(to, { recursive: true, force: true });
|
||||
cpSync(from, to, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
export function restoreBundleSeed(seed, root) {
|
||||
for (const name of BUNDLE_TREES) {
|
||||
const from = join(seed, name);
|
||||
if (!existsSync(from)) throw new Error(`shared bundle seed is missing: ${name}`);
|
||||
const to = join(root, name);
|
||||
rmSync(to, { recursive: true, force: true });
|
||||
cpSync(from, to, { recursive: true });
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,14 @@ import { fileURLToPath } from 'node:url';
|
||||
import {
|
||||
MUTATION_OUTCOME, MUTATION_PROOF, runGuardPhases, runMutationLifecycle, setupFailureOwner,
|
||||
} from './mutation-guard-outcome.mjs';
|
||||
import {
|
||||
captureBundleSeed, guardNeedsBundle, mutantBundleStrategy, restoreBundleSeed,
|
||||
} from './mutation-bundle-cache.mjs';
|
||||
|
||||
export {
|
||||
dropBundleSeed, guardNeedsBundle, makeBundleSeed, mutantBundleStrategy,
|
||||
mutantPatchesNeedBundle, planNeedsBundleSeed,
|
||||
} from './mutation-bundle-cache.mjs';
|
||||
|
||||
const repoRoot = fileURLToPath(new URL('..', import.meta.url));
|
||||
|
||||
@@ -88,12 +96,6 @@ export function guardNeedsTestBuild(guard) {
|
||||
* самая дорогая часть прогона (255 мутантов × ~15-20 с), поэтому она
|
||||
* выполняется только там, где её результат кто-то откроет.
|
||||
*/
|
||||
export function guardNeedsBundle(guard) {
|
||||
// `bundle:sync` в гварде реестр больше не допускает (--check, #499), но
|
||||
// распознавание остаётся: чужой или старый гвард со сборкой всё равно
|
||||
// браузерный, и бандл ему нужен.
|
||||
return guard.includes('demo/') || guard.includes('bundle:sync');
|
||||
}
|
||||
|
||||
/**
|
||||
* Тёплый старт компиляции мутанта (#332): скопировать `test-build/` вместе с
|
||||
@@ -168,14 +170,16 @@ function printMutantOutcome(mutant, outcome) {
|
||||
if (outcome.detail) console.log(` ${outcome.detail}`);
|
||||
}
|
||||
|
||||
export function runMutant(mutant, { ref = 'HEAD' } = {}) {
|
||||
export function runMutant(mutant, { ref = 'HEAD', bundleSeed = null } = {}) {
|
||||
let dir;
|
||||
try {
|
||||
dir = makeWorktree(ref);
|
||||
const outcome = runMutationLifecycle({
|
||||
apply: () => applyPatches(dir, mutant.patches),
|
||||
prepare: () => {
|
||||
if (guardNeedsBundle(mutant.guard)) buildBundle(dir);
|
||||
const bundle = mutantBundleStrategy(mutant, { seedAvailable: Boolean(bundleSeed) });
|
||||
if (bundle === 'seed') restoreBundleSeed(bundleSeed, dir);
|
||||
else if (bundle === 'build') buildBundle(dir);
|
||||
if (guardNeedsTestBuild(mutant.guard)) buildTestBuild(dir);
|
||||
},
|
||||
guard: mutant.guard,
|
||||
@@ -199,12 +203,15 @@ export function runMutant(mutant, { ref = 'HEAD' } = {}) {
|
||||
|
||||
// Чистый прогон каждого guard ровно один раз: тест, красный и без мутанта,
|
||||
// «ловит» поломку тривиально и не доказывает ничего.
|
||||
export function runCleanGuards(mutants) {
|
||||
export function runCleanGuards(mutants, { bundleSeed = null } = {}) {
|
||||
const guards = [...new Set(mutants.map((m) => m.guard))];
|
||||
const dir = makeWorktree();
|
||||
try {
|
||||
try {
|
||||
if (guards.some(guardNeedsBundle)) buildBundle(dir);
|
||||
if (guards.some(guardNeedsBundle)) {
|
||||
buildBundle(dir);
|
||||
if (bundleSeed) captureBundleSeed(dir, bundleSeed);
|
||||
}
|
||||
// Один worktree на все чистые гварды — значит и компиляция одна.
|
||||
if (guards.some(guardNeedsTestBuild)) buildTestBuild(dir);
|
||||
} catch (error) {
|
||||
|
||||
+15
-43
@@ -10,9 +10,11 @@ import { fileURLToPath } from 'node:url';
|
||||
|
||||
import { MUTANTS } from './mutation-registry.mjs';
|
||||
import {
|
||||
applyPatches, buildBundle, dropWorktree, guardNeedsBundle, guardNeedsTestBuild,
|
||||
makeWorktree, runCleanGuards, runMutant,
|
||||
applyPatches, buildBundle, dropBundleSeed, dropWorktree, guardNeedsBundle, guardNeedsTestBuild,
|
||||
makeBundleSeed, makeWorktree, mutantBundleStrategy, mutantPatchesNeedBundle,
|
||||
planNeedsBundleSeed, runCleanGuards, runMutant,
|
||||
} from './mutation-execution.mjs';
|
||||
import { checkMutationRegistry } from './mutation-registry-check.mjs';
|
||||
import {
|
||||
ANCHOR_RADIUS_LINES, MUTATION_REGISTRY_FILES, anchorRegion, anchorSpan,
|
||||
baseRegistry, createGuardInputResolver, guardFiles, guardInputs,
|
||||
@@ -24,9 +26,7 @@ import {
|
||||
} from './mutation-evidence.mjs';
|
||||
import { attributeSetupFailure } from './mutation-attribution.mjs';
|
||||
import { guardEnvironment, planEnvironment, planEnvironmentLines } from './mutation-environment.mjs';
|
||||
import {
|
||||
MUTATION_OUTCOME, isProofOutcome, staticTestSelectionProblems,
|
||||
} from './mutation-guard-outcome.mjs';
|
||||
import { MUTATION_OUTCOME, isProofOutcome } from './mutation-guard-outcome.mjs';
|
||||
|
||||
const repoRoot = fileURLToPath(new URL('..', import.meta.url));
|
||||
|
||||
@@ -36,6 +36,7 @@ export {
|
||||
ANCHOR_RADIUS_LINES, LEDGER_SCHEMA, MUTANTS, MUTATION_REGISTRY_FILES,
|
||||
anchorRegion, anchorSpan, applyPatches, baseRegistry, createGuardInputResolver,
|
||||
guardFiles, guardInputs, guardNeedsBundle, guardNeedsTestBuild,
|
||||
mutantBundleStrategy, mutantPatchesNeedBundle,
|
||||
packageJsonRelevance, parseDiffRanges, patchTouched, readLedger, recordCaught,
|
||||
registryDelta, selectChangedMutants, selectForDiff, shardMutants,
|
||||
splitByLedger, witnessFingerprint, wrapperInputs,
|
||||
@@ -152,43 +153,9 @@ export async function main(argv) {
|
||||
}
|
||||
|
||||
if (argv.includes('--check')) {
|
||||
let stale = 0;
|
||||
let warned = 0;
|
||||
const readTest = (file) => {
|
||||
const path = join(repoRoot, file);
|
||||
return existsSync(path) ? readFileSync(path, 'utf8') : null;
|
||||
};
|
||||
for (const m of selected) {
|
||||
try {
|
||||
// #499: бандл мутанта собирает раннер (`buildBundle`, только rollup +
|
||||
// sync). Гвард с собственным `npm run bundle:sync` собирал бы его второй
|
||||
// раз — плюс `tsc --noEmit`, который на нестрогом мутанте падает сам и
|
||||
// красит гвард ещё до теста: «мутант пойман» без единого запуска смока.
|
||||
if (/bundle:sync|bundle-sync\.mjs|rollup -c/.test(m.guard)) {
|
||||
throw new Error('гвард сам собирает бандл — сборку делает раннер (#499)');
|
||||
}
|
||||
for (const patch of m.patches) {
|
||||
const source = readFileSync(join(repoRoot, patch.file), 'utf8');
|
||||
const hits = source.split(patch.find).length - 1;
|
||||
if (hits !== 1) throw new Error(`якорь найден ${hits} раз(а)`);
|
||||
}
|
||||
// #650: a name filter that matches nothing makes the witness vacuous.
|
||||
const selection = staticTestSelectionProblems(m.guard, readTest);
|
||||
const error = selection.find((problem) => problem.level === 'error');
|
||||
if (error) throw new Error(error.text);
|
||||
for (const problem of selection) {
|
||||
console.log(`WARN ${m.id}: ${problem.text}`);
|
||||
warned++;
|
||||
}
|
||||
console.log(`ok ${m.id}`);
|
||||
} catch (error) {
|
||||
console.log(`FAIL ${m.id}: ${error.message}`);
|
||||
stale++;
|
||||
}
|
||||
}
|
||||
if (warned) console.log(`предупреждений о шаблонах имён: ${warned}`);
|
||||
const result = checkMutationRegistry(selected, { allMutants: MUTANTS, root: repoRoot });
|
||||
reportPlanMetrics();
|
||||
return stale ? 2 : 0;
|
||||
return result;
|
||||
}
|
||||
|
||||
if (argv.includes('--build-only')) {
|
||||
@@ -241,7 +208,9 @@ export async function main(argv) {
|
||||
return 0;
|
||||
}
|
||||
reportPlanMetrics();
|
||||
if (!runCleanGuards(toRun)) return 2;
|
||||
const bundleSeed = planNeedsBundleSeed(toRun) ? makeBundleSeed() : null;
|
||||
try {
|
||||
if (!runCleanGuards(toRun, { bundleSeed })) return 2;
|
||||
let caught = 0;
|
||||
let unverifiable = false;
|
||||
// #568: свидетель, который не готовится к прогону, ломает гейт той задачи,
|
||||
@@ -252,7 +221,7 @@ export async function main(argv) {
|
||||
// прогона одного и того же мутанта, а не код с ожиданием.
|
||||
const preExisting = [];
|
||||
for (const entry of plan) {
|
||||
const outcome = runMutant(entry.mutant);
|
||||
const outcome = runMutant(entry.mutant, { bundleSeed });
|
||||
if (!isProofOutcome(outcome)) {
|
||||
if (outcome.kind === MUTATION_OUTCOME.SETUP) {
|
||||
const verdict = await attributeSetupFailure(entry.mutant, outcome, rangeBase);
|
||||
@@ -285,6 +254,9 @@ export async function main(argv) {
|
||||
}
|
||||
if (unverifiable) return 2;
|
||||
return caught === toRun.length - preExisting.length ? 0 : 1;
|
||||
} finally {
|
||||
dropBundleSeed(bundleSeed);
|
||||
}
|
||||
}
|
||||
|
||||
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
// Cheap structural validation for mutation declarations. Kept out of the CLI
|
||||
// so adding registry policy does not grow the execution coordinator (#558).
|
||||
import { existsSync, readFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
|
||||
import {
|
||||
BROWSER_GUARD_LIMIT, browserGuardPolicy, readDocumentedBrowserGuards,
|
||||
} from './mutation-browser-policy.mjs';
|
||||
import { staticTestSelectionProblems } from './mutation-guard-outcome.mjs';
|
||||
|
||||
export function checkMutationRegistry(selected, { allMutants, root, log = console.log }) {
|
||||
let stale = 0;
|
||||
let warned = 0;
|
||||
const readTest = (file) => {
|
||||
const path = join(root, file);
|
||||
return existsSync(path) ? readFileSync(path, 'utf8') : null;
|
||||
};
|
||||
for (const mutant of selected) {
|
||||
try {
|
||||
if (/bundle:sync|bundle-sync\.mjs|rollup -c/.test(mutant.guard)) {
|
||||
throw new Error('гвард сам собирает бандл — сборку делает раннер (#499)');
|
||||
}
|
||||
for (const patch of mutant.patches) {
|
||||
const source = readFileSync(join(root, patch.file), 'utf8');
|
||||
const hits = source.split(patch.find).length - 1;
|
||||
if (hits !== 1) throw new Error(`якорь найден ${hits} раз(а)`);
|
||||
}
|
||||
const selection = staticTestSelectionProblems(mutant.guard, readTest);
|
||||
const error = selection.find((problem) => problem.level === 'error');
|
||||
if (error) throw new Error(error.text);
|
||||
for (const problem of selection) {
|
||||
log(`WARN ${mutant.id}: ${problem.text}`);
|
||||
warned++;
|
||||
}
|
||||
log(`ok ${mutant.id}`);
|
||||
} catch (error) {
|
||||
log(`FAIL ${mutant.id}: ${error.message}`);
|
||||
stale++;
|
||||
}
|
||||
}
|
||||
try {
|
||||
const policy = browserGuardPolicy(allMutants, readDocumentedBrowserGuards(root));
|
||||
log(`browser guards: ${policy.count}/${BROWSER_GUARD_LIMIT}`);
|
||||
if (policy.overLimit) {
|
||||
log(`FAIL browser guards: лимит ${BROWSER_GUARD_LIMIT} превышен`);
|
||||
stale++;
|
||||
}
|
||||
for (const mutant of policy.missingReasons) {
|
||||
log(`WARN ${mutant.id}: browser guard не размечен — добавьте причину в `
|
||||
+ '`docs/testing-notes/mutation-browser-guards.md` и конкретику в `because`');
|
||||
warned++;
|
||||
}
|
||||
for (const id of policy.staleReasons) {
|
||||
log(`WARN ${id}: browser-разметка устарела — такого browser guard больше нет`);
|
||||
warned++;
|
||||
}
|
||||
} catch (error) {
|
||||
log(`FAIL browser guard policy: ${error.message}`);
|
||||
stale++;
|
||||
}
|
||||
if (warned) log(`предупреждений mutation registry: ${warned}`);
|
||||
return stale ? 2 : 0;
|
||||
}
|
||||
@@ -13058,8 +13058,175 @@ const MUTANT_DEFINITIONS = [
|
||||
|
||||
const mutationCardSource = readFileSync(join(repoRoot, 'src/houseplan-card.ts'), 'utf8');
|
||||
const mutationEditorSource = readFileSync(join(repoRoot, 'src/houseplan-editor-runtime.ts'), 'utf8');
|
||||
|
||||
// #659: these witnesses exercise pure state/geometry/CSS contracts and do not
|
||||
// need Chromium. Keep the grouping explicit: every promoted mutant is proven
|
||||
// by `mutation-gate`, not merely reclassified to avoid a bundle build.
|
||||
const unitGuard = (...files) => `node --test ${files.join(' ')} test/mutation-browser-offload.test.mjs`;
|
||||
const UNIT_GUARD_GROUPS = [
|
||||
[unitGuard('test/stairs.test.mjs'), [
|
||||
'stairs-fixed-floor-still-navigates',
|
||||
'stairs-link-auto-creates-target-object',
|
||||
'stairs-broken-targets-become-active',
|
||||
]],
|
||||
[unitGuard('test/device-hit-owner.test.mjs'), [
|
||||
'device-hit-scroll-observer-disabled',
|
||||
]],
|
||||
[unitGuard('test/summary-panel-runtime.test.mjs'), [
|
||||
'summary-hide-unmounts-before-animation',
|
||||
'summary-first-paint-shows-unavailable',
|
||||
'summary-metrics-block-first-frame',
|
||||
]],
|
||||
[unitGuard('test/form-shell-consumers.test.mjs'), [
|
||||
'discard-confirm-action-icon-falls-back-to-lock',
|
||||
]],
|
||||
[unitGuard('test/form-kit.test.mjs'), [
|
||||
'range-line-clamps-every-keystroke',
|
||||
]],
|
||||
[unitGuard('test/dialog-baseline.test.mjs', 'test/styles-split.test.mjs'), [
|
||||
'ha-form-shell-width-falls-back-to-generic',
|
||||
'ha-form-shell-loses-canvas',
|
||||
'ha-form-shell-mobile-keeps-desktop-inset',
|
||||
]],
|
||||
[unitGuard('test/summary-panel.test.mjs'), [
|
||||
'summary-dialog-loses-wide-shell',
|
||||
'summary-mobile-ignores-local-off',
|
||||
]],
|
||||
[unitGuard('test/styles-split.test.mjs'), [
|
||||
'marker-shadow-animates-again',
|
||||
]],
|
||||
[unitGuard('test/device-inbox.test.mjs'), [
|
||||
'device-inbox-batch-single-write',
|
||||
]],
|
||||
[unitGuard('test/houseplan-panel.test.mjs'), [
|
||||
'panel-ignores-pre-upgrade-properties',
|
||||
'panel-host-height-from-parent',
|
||||
]],
|
||||
[unitGuard('test/version-recovery-card.test.mjs'), [
|
||||
'version-recovery-delays-config-capability-adoption',
|
||||
]],
|
||||
[unitGuard('test/vacuum-calibration-write.test.mjs'), [
|
||||
'vacuum-manual-fit-after-proposal-uses-the-dock',
|
||||
]],
|
||||
[unitGuard('test/devices.test.mjs'), [
|
||||
'room-climate-ignores-marker-placement',
|
||||
]],
|
||||
[unitGuard('test/device-inbox.test.mjs'), [
|
||||
'device-tombstone-blocks-child-picker',
|
||||
]],
|
||||
[unitGuard('test/room-fit.test.mjs'), [
|
||||
'room-fit-interactive-owner-leaks-through',
|
||||
]],
|
||||
[unitGuard('test/iso-stage6.test.mjs', 'test/styles-split.test.mjs'), [
|
||||
'iso-tile-ring-visible',
|
||||
'iso-tile-edge-zero',
|
||||
'iso-tile-badge-gap-dropped',
|
||||
'iso-tile-shadow-layer-above',
|
||||
'iso-frame-selected-beats-focus',
|
||||
'iso-shadow-in-forced-colors',
|
||||
'iso-sun-length-ignores-elevation',
|
||||
'iso-sun-streaks-on-dark-floor',
|
||||
'iso-wall-top-constant',
|
||||
]],
|
||||
[unitGuard('test/backdrop-probe.test.mjs'), [
|
||||
'backdrop-hard-demoted-to-warn',
|
||||
]],
|
||||
[unitGuard('test/bundle-assets.test.mjs'), [
|
||||
'css-minifier-eats-required-space',
|
||||
]],
|
||||
[unitGuard('test/wall-thickness.test.mjs', 'test/wall-union-isolation.test.mjs'), [
|
||||
'wall-component-failure-kills-primary',
|
||||
]],
|
||||
[unitGuard('test/glow-scene.test.mjs'), [
|
||||
'union-failure-silent',
|
||||
'glow-fail-dark-weakened',
|
||||
'opening-cut-degenerate',
|
||||
'column-shadow-removed',
|
||||
'feather-20px',
|
||||
]],
|
||||
[unitGuard('test/visual-continuity.test.mjs'), [
|
||||
'continuity-long-resume-noop',
|
||||
]],
|
||||
[unitGuard('test/ha-binding-status.test.mjs'), [
|
||||
'registryless-opening-requires-registry-row',
|
||||
]],
|
||||
[unitGuard('test/header-menu.test.mjs'), [
|
||||
'header-menu-item-keeps-menu-open',
|
||||
'header-menu-escape-ignored',
|
||||
'active-tab-not-revealed',
|
||||
]],
|
||||
[unitGuard('test/iso-scene-render.test.mjs', 'test/iso-stage6.test.mjs'), [
|
||||
'grid-scale-iso-height-unscaled',
|
||||
'stage3-w8-material-defs-created-per-face',
|
||||
]],
|
||||
[unitGuard('test/support-feedback.test.mjs'), [
|
||||
'support-edited-retry-reuses-old-idempotency-key',
|
||||
]],
|
||||
[unitGuard('test/touch-gesture-click-guard.test.mjs'), [
|
||||
'touch-pinch-contextmenu-guard-removed',
|
||||
]],
|
||||
[unitGuard('test/live-editor.test.mjs'), [
|
||||
'live-editor-devices-drops-align-guides',
|
||||
'live-editor-decor-drops-align-guides',
|
||||
'live-editor-plan-drops-align-guides',
|
||||
'live-editor-keeps-the-settled-guides-visible',
|
||||
]],
|
||||
[unitGuard('test/live-viewport.test.mjs'), [
|
||||
'live-pan-incoming-edge-clipped',
|
||||
'live-pan-rewrites-viewbox-every-frame',
|
||||
]],
|
||||
[unitGuard('test/summary-runtime-loader.test.mjs'), [
|
||||
'summary-runtime-attaches-after-first-render',
|
||||
]],
|
||||
[unitGuard('test/config-reload-authority.test.mjs'), [
|
||||
'config-reload-drops-asset-gap-ownership',
|
||||
]],
|
||||
[unitGuard('test/summary-panel.test.mjs'), [
|
||||
'summary-picker-renders-unbounded-results',
|
||||
]],
|
||||
[unitGuard('test/form-shell-consumers.test.mjs', 'test/dialog-baseline.test.mjs',
|
||||
'test/space-dialog.test.mjs'), [
|
||||
'same-binding-click-resets-source',
|
||||
'state-callout-hidden-under-help',
|
||||
'dialog-card-loses-its-heading',
|
||||
'form-kit-writes-to-a-neighbour-key',
|
||||
'space-fill-segment-writes-a-neighbour-key',
|
||||
'space-save-enabled-without-changes',
|
||||
'space-discard-without-asking',
|
||||
'space-layer-toggle-loses-inversion',
|
||||
'general-save-enabled-without-changes',
|
||||
'general-tile-opacity-writes-a-neighbour-key',
|
||||
'general-north-clear-writes-zero',
|
||||
'topology-embedded-draws-its-own-heading',
|
||||
'room-save-enabled-without-changes',
|
||||
'room-discard-without-asking',
|
||||
'room-fill-segment-starts-at-none',
|
||||
'room-temp-min-writes-max',
|
||||
'marker-save-enabled-without-changes',
|
||||
'marker-save-click-forgets-baseline-before-result',
|
||||
'marker-discard-without-asking',
|
||||
'marker-confirm-row-shown-for-do-nothing',
|
||||
'marker-never-keeps-glow-block-live',
|
||||
'marker-badge-position-forgets-touch',
|
||||
]],
|
||||
[unitGuard('test/color-picker.test.mjs'), [
|
||||
'M-615-tile',
|
||||
'M-615-plate',
|
||||
]],
|
||||
[unitGuard('test/form-kit.test.mjs', 'test/styles-split.test.mjs'), [
|
||||
'form-kit-segment-breaks-words',
|
||||
]],
|
||||
[unitGuard('test/room-gear-drag.test.mjs'), [
|
||||
'room-gear-drag-reopens-settings',
|
||||
]],
|
||||
];
|
||||
const UNIT_GUARD_OVERRIDES = new Map(UNIT_GUARD_GROUPS.flatMap(([guard, ids]) => (
|
||||
ids.map((id) => [id, guard])
|
||||
)));
|
||||
|
||||
export const MUTANTS = MUTANT_DEFINITIONS.map((mutant) => ({
|
||||
...mutant,
|
||||
guard: UNIT_GUARD_OVERRIDES.get(mutant.id) ?? mutant.guard,
|
||||
patches: mutant.patches.map((patch) => relocateEditorPatch(
|
||||
patch, mutationCardSource, mutationEditorSource,
|
||||
)),
|
||||
|
||||
Reference in New Issue
Block a user