perf: lazy-load editor runtime

Issue: #337
User-Visible: yes
This commit is contained in:
Matysh
2026-08-28 08:29:44 +03:00
parent 4a61407282
commit 59be9c8565
83 changed files with 32499 additions and 26465 deletions
+135
View File
@@ -0,0 +1,135 @@
import assert from 'node:assert/strict';
import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import test from 'node:test';
import {
buildBundleManifest, buildFingerprintPlugin, editorRuntimeRetryUrlPlugin,
} from '../scripts/bundle-manifest.mjs';
import { assertBundleBudget } from '../scripts/bundle-budget.mjs';
import { compareBundleTrees, sha256Bytes, verifyBundleTree } from '../scripts/bundle-tree.mjs';
import {
minifyCssText, minifyStaticCssTemplates,
} from '../scripts/css-template-minifier.mjs';
test('CSS template minifier preserves semantic whitespace, strings and functions', () => {
const css = `
/* owner note */
.a .b, .c > .d { --pair: 1 2; width: calc(100% - 2px); }
.quoted { content: "a b /* text */"; background: url("a b.png"); }
.joined/**/.state { color: red; }
`;
assert.equal(
minifyCssText(css),
'.a .b,.c>.d{--pair:1 2;width:calc(100% - 2px);}.quoted{content:"a b /* text */";background:url("a b.png");}.joined.state{color:red;}',
);
assert.equal(
minifyStaticCssTemplates('const s = css` .a { content: "\\`"; } `;', 'fixture.ts'),
'const s = css`.a{content:"\\`";}`;',
);
});
test('CSS template minifier fails closed on interpolation and malformed input', () => {
assert.throws(
() => minifyStaticCssTemplates('const s = css`color:${value}`;', 'fixture.ts'),
/interpolated css template/,
);
assert.throws(() => minifyCssText('.a{/* nope'), /unclosed CSS comment/);
assert.throws(() => minifyCssText('.a{content:"nope}'), /unclosed CSS string/);
});
test('bundle manifest separates static initial graph from dynamic editor graph', () => {
const manifest = buildBundleManifest({
'houseplan-card.js': {
type: 'chunk', fileName: 'houseplan-card.js', code: 'entry', isEntry: true,
imports: ['shared.js'], dynamicImports: [
'houseplan-assets/editor.js', 'houseplan-assets/houseplan-onboarding-runtime-HASH.js',
],
},
'shared.js': {
type: 'chunk', fileName: 'shared.js', code: 'shared', isEntry: false,
imports: [], dynamicImports: [],
},
'houseplan-assets/editor.js': {
type: 'chunk', fileName: 'houseplan-assets/editor.js', code: 'editor', isEntry: false,
imports: ['shared.js'], dynamicImports: [],
},
'houseplan-assets/houseplan-onboarding-runtime-HASH.js': {
type: 'chunk', fileName: 'houseplan-assets/houseplan-onboarding-runtime-HASH.js',
code: 'onboarding', isEntry: false,
imports: ['shared.js'], dynamicImports: [],
},
}, 'fingerprint');
assert.deepEqual(manifest.initialViewFiles, ['houseplan-card.js', 'shared.js']);
assert.deepEqual(manifest.lazyEditorFiles, ['houseplan-assets/editor.js']);
assert.deepEqual(manifest.lazyOnboardingFiles, [
'houseplan-assets/houseplan-onboarding-runtime-HASH.js',
]);
assert.deepEqual(manifest.lazyFiles, [
'houseplan-assets/editor.js', 'houseplan-assets/houseplan-onboarding-runtime-HASH.js',
]);
assert.doesNotThrow(() => assertBundleBudget(manifest, 1_000_000));
assert.throws(() => assertBundleBudget(manifest, 1), /exceeds/);
});
test('build fingerprint is embedded for Windows and POSIX source ids', () => {
const plugin = buildFingerprintPlugin('exact-build');
for (const id of ['C:\\repo\\src\\houseplan-card.ts', '/repo/src/houseplan-editor-runtime.ts']) {
const transformed = plugin.transform(
'export const fingerprint = "__HOUSEPLAN_SOURCE_FINGERPRINT__";', id,
);
assert.match(transformed.code, /"exact-build"/);
}
});
test('retry URL points at the content-hashed runtime chunk after naming', () => {
const plugin = editorRuntimeRetryUrlPlugin();
const bundle = {
'houseplan-assets/houseplan-card.js': {
type: 'chunk', fileName: 'houseplan-assets/houseplan-card.js',
code: 'new URL("__HOUSEPLAN_EDITOR_RETRY_ASSET__", import.meta.url);'
+ 'new URL("__HOUSEPLAN_ONBOARDING_RETRY_ASSET__", import.meta.url)', modules: {},
},
'houseplan-assets/houseplan-editor-runtime-HASH.js': {
type: 'chunk', fileName: 'houseplan-assets/houseplan-editor-runtime-HASH.js', code: '',
modules: { '/repo/src/houseplan-editor-runtime.ts': {} },
},
'houseplan-assets/houseplan-onboarding-runtime-HASH.js': {
type: 'chunk', fileName: 'houseplan-assets/houseplan-onboarding-runtime-HASH.js', code: '',
modules: { '/repo/src/houseplan-onboarding-runtime.ts': {} },
},
};
plugin.generateBundle({}, bundle);
assert.equal(
bundle['houseplan-assets/houseplan-card.js'].code,
'new URL("./houseplan-editor-runtime-HASH.js", import.meta.url);'
+ 'new URL("./houseplan-onboarding-runtime-HASH.js", import.meta.url)',
);
});
test('bundle tree verification fails for a missing or tampered manifest asset', () => {
const temp = mkdtempSync(join(tmpdir(), 'houseplan-bundle-tree-'));
const source = join(temp, 'source');
const target = join(temp, 'target');
try {
for (const root of [source, target]) {
mkdirSync(join(root, 'houseplan-assets'), { recursive: true });
writeFileSync(join(root, 'houseplan-card.js'), 'entry');
writeFileSync(join(root, 'houseplan-assets', 'editor-hash.js'), 'editor');
const files = ['houseplan-card.js', 'houseplan-assets/editor-hash.js'].map((path) => ({
path, sha256: sha256Bytes(readFileSync(join(root, path))),
}));
writeFileSync(join(root, 'houseplan-assets.json'), `${JSON.stringify({
schema: 1, fingerprint: 'fixture', entry: 'houseplan-card.js', files,
})}\n`);
}
assert.doesNotThrow(() => compareBundleTrees(source, target));
writeFileSync(join(target, 'houseplan-assets', 'editor-hash.js'), 'tampered');
assert.throws(() => verifyBundleTree(target), /manifest hash mismatch/);
rmSync(join(target, 'houseplan-assets', 'editor-hash.js'));
assert.throws(() => verifyBundleTree(target), /manifest asset is missing/);
} finally {
rmSync(temp, { recursive: true, force: true });
}
});
+30
View File
@@ -1,4 +1,5 @@
import assert from 'node:assert/strict';
import { createHash } from 'node:crypto';
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { resolve } from 'node:path';
@@ -44,6 +45,35 @@ test('bundle freshness rejects a missing or stale fingerprint', async () => {
}
});
test('bundle freshness verifies every manifest-listed demo asset', async () => {
const root = fixtureRoot();
try {
const expected = sourceFingerprint(root);
const assets = resolve(root, 'demo/srv/assets');
mkdirSync(resolve(assets, 'houseplan-assets'), { recursive: true });
const files = [
['houseplan-card.js', 'entry'],
['houseplan-assets/editor-HASH.js', 'editor'],
];
for (const [name, contents] of files) writeFileSync(resolve(assets, name), contents, 'utf8');
writeFileSync(resolve(assets, 'houseplan-assets.json'), JSON.stringify({
schema: 1,
fingerprint: expected,
files: files.map(([path, contents]) => ({
path,
sha256: createHash('sha256').update(contents).digest('hex'),
})),
}), 'utf8');
const page = { evaluate: async () => expected };
assert.equal(await assertFreshDemoBundle(page, root), expected);
writeFileSync(resolve(assets, 'houseplan-assets/editor-HASH.js'), 'tampered', 'utf8');
await assert.rejects(assertFreshDemoBundle(page, root), /hash mismatch/);
} finally {
rmSync(root, { recursive: true, force: true });
}
});
test('bundle freshness uses the target tree fingerprint contract', async () => {
const root = fixtureRoot();
try {
+3 -2
View File
@@ -5,6 +5,7 @@ import {
hexToRgb, hsvToHex, hsvToRgb, normalizeHexColor, normalizeHue, rgbToHex, rgbToHsv,
} from '../test-build/color-picker.js';
import { readAllStylesSource } from './styles-source.mjs';
import { readHouseplanProductionSource } from './houseplan-source.mjs';
test('hex drafts accept only three or six digits and normalize on commit', () => {
assert.equal(normalizeHexColor('#AbC'), '#aabbcc');
@@ -40,7 +41,7 @@ test('HSV helpers wrap hue, clamp finite dimensions and keep grayscale achromati
test('the shared component keeps its API and contains no nested native color picker', () => {
const component = readFileSync(new URL('../src/hp-color-opacity.ts', import.meta.url), 'utf8');
const card = readFileSync(new URL('../src/houseplan-card.ts', import.meta.url), 'utf8');
const card = readHouseplanProductionSource();
const sourceFiles = (directory) => readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
const target = new URL(entry.name + (entry.isDirectory() ? '/' : ''), directory);
return entry.isDirectory() ? sourceFiles(target) : entry.name.endsWith('.ts') ? [target] : [];
@@ -80,7 +81,7 @@ test('the hue range exposes one cyclic spectrum without restyling other ranges',
});
test('activity color and ripple size keep independent readable rows', () => {
const card = readFileSync(new URL('../src/houseplan-card.ts', import.meta.url), 'utf8');
const card = readHouseplanProductionSource();
const styles = readAllStylesSource();
const start = card.indexOf("d.display === 'icon_ripple'");
const end = card.indexOf("marker.activity_alarm_note", start);
+7 -2
View File
@@ -1,6 +1,7 @@
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import test from 'node:test';
import { readHouseplanProductionSource } from './houseplan-source.mjs';
import {
COORDINATE_DECIMALS,
@@ -110,11 +111,15 @@ test('one position changes only x/y and preserves future metadata (#224)', () =>
});
test('frontend write paths adopt canonical candidates before persistence (#224)', () => {
const source = readFileSync(new URL('../src/houseplan-card.ts', import.meta.url), 'utf8');
const source = readHouseplanProductionSource();
assert.match(source, /enqueueSerializedWrite\(this\._writeChain, async \(\) =>/);
assert.match(
source,
/const candidate = canonicalizeConfigGeometry\(this\._serverCfg\);[\s\S]*config: candidate/,
/const candidate = canonicalizeConfigGeometry\(this\._serverCfg\);/,
);
assert.match(
source,
/const canonicalCandidate = canonicalizeConfigGeometry\(candidate\);[\s\S]*config: canonicalCandidate/,
);
assert.match(
source,
@@ -3,11 +3,14 @@ import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { readAllStylesSource } from './styles-source.mjs';
import { readHouseplanProductionSource } from './houseplan-source.mjs';
// #266: styles.ts is being split into surface files — style greps must
// see the whole sheet, other sources keep the direct read.
const source = (name) => name === 'styles.ts'
? readAllStylesSource()
: name === 'houseplan-card.ts'
? readHouseplanProductionSource()
: readFileSync(new URL(`../src/${name}`, import.meta.url), 'utf8');
test('issue 213 resolves the effective base before the face without a late visual factor', () => {
+105
View File
@@ -0,0 +1,105 @@
import assert from 'node:assert/strict';
import { readFileSync, readdirSync, statSync } from 'node:fs';
import test from 'node:test';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { EditorRuntimeLoader } from '../test-build/editor-runtime-loader.js';
const repoRoot = dirname(dirname(fileURLToPath(import.meta.url)));
const deferred = () => {
let resolve;
let reject;
const promise = new Promise((ok, fail) => { resolve = ok; reject = fail; });
return { promise, resolve, reject };
};
test('editor runtime loader deduplicates concurrent intent and installs atomically', async () => {
const pending = deferred();
const states = [];
const installed = [];
let loads = 0;
const loader = new EditorRuntimeLoader({
expectedFingerprint: 'same',
load: async () => { loads++; return pending.promise; },
install: (runtime) => installed.push(runtime),
stateChanged: (state) => states.push(state),
});
const first = loader.ensure();
const second = loader.ensure();
assert.equal(loads, 1);
assert.equal(loader.state, 'loading');
assert.equal(installed.length, 0);
pending.resolve({ fingerprint: 'same', create: () => ({ ready: true }) });
assert.deepEqual(await Promise.all([first, second]), [true, true]);
assert.equal(loads, 1);
assert.deepEqual(installed, [{ ready: true }]);
assert.deepEqual(states, ['loading', 'ready']);
});
test('editor runtime loader retries the first failure exactly once', async () => {
const attempts = [];
const loader = new EditorRuntimeLoader({
expectedFingerprint: 'same',
load: async (attempt) => {
attempts.push(attempt);
if (attempt === 0) throw new Error('404');
return { fingerprint: 'same', create: () => 'runtime' };
},
install: () => {},
});
assert.equal(await loader.ensure(), true);
assert.deepEqual(attempts, [0, 1]);
assert.equal(await loader.ensure(), true);
assert.deepEqual(attempts, [0, 1], 'a ready loader never imports again');
});
test('fingerprint mismatch fails closed after one retry and remains terminal', async () => {
const attempts = [];
const installed = [];
const failures = [];
const loader = new EditorRuntimeLoader({
expectedFingerprint: 'entry',
load: async (attempt) => {
attempts.push(attempt);
return { fingerprint: 'other', create: () => 'mixed-runtime' };
},
install: (runtime) => installed.push(runtime),
failed: (error) => failures.push(String(error)),
});
assert.equal(await loader.ensure(), false);
assert.deepEqual(attempts, [0, 1]);
assert.deepEqual(installed, []);
assert.equal(loader.state, 'failed');
assert.match(failures[0], /fingerprint mismatch/);
assert.equal(await loader.ensure(), false);
assert.deepEqual(attempts, [0, 1], 'terminal failure requires a page refresh');
});
test('lazy runtime boundary keeps the eager entry independent and does not grow any usage', () => {
const card = readFileSync(join(repoRoot, 'src', 'houseplan-card.ts'), 'utf8');
assert.doesNotMatch(
card,
/^import(?!\s+type\b)[^;]*houseplan-editor-runtime[^;]*;/m,
'the eager card must not statically import the editor runtime',
);
const sources = [];
const visit = (directory) => {
for (const name of readdirSync(directory)) {
const path = join(directory, name);
if (statSync(path).isDirectory()) visit(path);
else if (/\.ts$/.test(name)) sources.push(readFileSync(path, 'utf8'));
}
};
visit(join(repoRoot, 'src'));
const anyCount = sources.reduce(
(total, source) => total + (source.match(/\bany\b/g)?.length || 0), 0,
);
assert.ok(anyCount <= 1123, `src any count grew from 1123 to ${anyCount}`);
});
+2 -1
View File
@@ -1,8 +1,9 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { readHouseplanProductionSource } from './houseplan-source.mjs';
const card = readFileSync(new URL('../src/houseplan-card.ts', import.meta.url), 'utf8');
const card = readHouseplanProductionSource();
const editor = readFileSync(new URL('../src/editor.ts', import.meta.url), 'utf8');
test('active-space writes go through the fixed-floor mutation boundary', () => {
+3 -2
View File
@@ -6,6 +6,7 @@ import {
GOLDEN_MATRIX_VERSION, GOLDEN_SCENARIOS, OPENING_SYMBOL_EXISTING_GOLDEN_IMPACT,
} from '../demo/golden/matrix.mjs';
import { fixtureWallKey } from '../demo/fixtures/visual-matrix.mjs';
import { readHouseplanProductionSource } from './houseplan-source.mjs';
test('golden matrix has stable unique ids and bounded comparison thresholds', () => {
assert.equal(Number.isInteger(GOLDEN_MATRIX_VERSION) && GOLDEN_MATRIX_VERSION > 0, true);
@@ -739,7 +740,7 @@ test('golden overrides fail closed on misspelled fixture references', () => {
});
test('a light source paints exactly one region: the floor it can see', () => {
const source = readFileSync(new URL('../src/houseplan-card.ts', import.meta.url), 'utf8');
const source = readHouseplanProductionSource();
// One region per source, and it is the visibility polygon clipped to floor.
assert.match(source, /visibilityPolygon\(\[pos\.x, pos\.y\], R, occluders/);
assert.match(source,
@@ -772,7 +773,7 @@ test('a light source paints exactly one region: the floor it can see', () => {
});
test('all destructive editor dialogs use the medium shell and shared responsive footer groups', () => {
const source = readFileSync(new URL('../src/houseplan-card.ts', import.meta.url), 'utf8');
const source = readHouseplanProductionSource();
for (const method of ['_renderOpeningDialog', '_renderPhysicalDialog', '_renderSpaceDialog']) {
const start = source.indexOf(`private ${method}`);
assert.notEqual(start, -1, method);
+55
View File
@@ -0,0 +1,55 @@
import { readFileSync } from 'node:fs';
import ts from 'typescript';
/**
* Source-contract tests historically inspected one monolithic card file.
* Editor implementations are now lazy, so expose the same logical production
* surface with the implementation before its typed host stubs.
*/
export function readHouseplanProductionSource() {
const runtime = readFileSync(
new URL('../src/houseplan-editor-runtime.ts', import.meta.url),
'utf8',
);
const card = readFileSync(new URL('../src/houseplan-card.ts', import.meta.url), 'utf8');
const parse = (name, source) => ts.createSourceFile(
name, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS,
);
const runtimeFile = parse('houseplan-editor-runtime.ts', runtime);
const cardFile = parse('houseplan-card.ts', card);
const classOf = (file, name) => file.statements.find(
(statement) => ts.isClassDeclaration(statement) && statement.name?.text === name,
);
const runtimeClass = classOf(runtimeFile, 'HouseplanEditorRuntime');
const cardClass = classOf(cardFile, 'HouseplanCard');
if (!runtimeClass || !cardClass) throw new Error('Houseplan source classes are missing');
const nameOf = (member, file) => member.name?.getText(file);
const implementations = new Map(runtimeClass.members
.map((member) => [nameOf(member, runtimeFile), member])
.filter(([name]) => name));
const replacements = [];
for (const member of cardClass.members) {
const name = nameOf(member, cardFile);
const implementation = implementations.get(name);
if (!implementation) continue;
const baseText = card.slice(member.getStart(cardFile), member.end);
if (!baseText.includes('_editorRuntime')) continue;
const visibility = /^(private|protected|public)\b/.exec(baseText)?.[1] || 'private';
const implementationText = runtime
.slice(implementation.getStart(runtimeFile), implementation.end)
.replace(/^public\b/, visibility)
.replaceAll('this.host.', 'this.');
replacements.push({
from: member.getStart(cardFile),
to: member.end,
text: implementationText,
});
}
let reconstructed = card;
for (const replacement of replacements.sort((a, b) => b.from - a.from)) {
reconstructed = reconstructed.slice(0, replacement.from)
+ replacement.text
+ reconstructed.slice(replacement.to);
}
return reconstructed;
}
+2 -1
View File
@@ -12,13 +12,14 @@ import {
resolveLanguageCode,
} from '../test-build/i18n/registry.js';
import { langOf } from '../test-build/i18n.js';
import { readHouseplanProductionSource } from './houseplan-source.mjs';
const dictionaries = new Map(
LANGUAGE_REGISTRY.map(({ code, dictionary }) => [code, dictionary]),
);
const en = dictionaries.get('en');
const ru = dictionaries.get('ru');
const cardSource = readFileSync(new URL('../src/houseplan-card.ts', import.meta.url), 'utf8');
const cardSource = readHouseplanProductionSource();
test('i18n: registry codes and English fallback are valid', () => {
const codes = LANGUAGE_REGISTRY.map(({ code }) => code);
+2 -3
View File
@@ -9,6 +9,7 @@ import {
cmToUnits, newViolations,
} from '../test-build/junction-limits.js';
import { GRID_STEP_N } from '../test-build/space-geometry.js';
import { readHouseplanProductionSource } from './houseplan-source.mjs';
// #329, решения владельца 2026-08-27. Пороги абсолютные: 15°, 6 стен,
// max(20 см, толщина), 5 см, 25 см². cell_cm на них не влияет.
@@ -383,9 +384,7 @@ test('#330 AC4: кэш baseline инвалидируется по конфиг-
// приёмом, каким #293 пинит обвязку смока. Поведенческая половина AC4
// (N move → N+1 вычислений в реальном жесте) живёт в
// demo/smoke_junction_limits.mjs (resizeBaselineCachedPerGesture).
const source = readFileSync(
new URL('../src/houseplan-card.ts', import.meta.url), 'utf8',
);
const source = readHouseplanProductionSource();
const method = source.slice(
source.indexOf('private _junctionLimitsIntroduced('),
source.indexOf('private _commitPhysicalGeometry('),
+3 -2
View File
@@ -3,9 +3,10 @@ import { readdirSync, readFileSync } from 'node:fs';
import { join, relative } from 'node:path';
import { fileURLToPath } from 'node:url';
import test from 'node:test';
import { readHouseplanProductionSource } from './houseplan-source.mjs';
const srcRoot = fileURLToPath(new URL('../src/', import.meta.url));
const source = readFileSync(join(srcRoot, 'houseplan-card.ts'), 'utf8');
const source = readHouseplanProductionSource();
const walkTypescript = (directory) => readdirSync(directory, { withFileTypes: true })
.flatMap((entry) => {
const path = join(directory, entry.name);
@@ -27,7 +28,7 @@ test('native selects explicitly project selection unless they are command menus'
// This is a command menu, not persisted state: it intentionally resets to
// its empty first row after inserting an HA variable into decorative text.
assert.deepEqual(unsafe, ['houseplan-card.ts:decor-live-attribute']);
assert.deepEqual(unsafe, ['houseplan-editor-runtime.ts:decor-live-attribute']);
});
test('persisted dynamic marker selects project their selected option explicitly', () => {
+2 -1
View File
@@ -12,6 +12,7 @@ import {
import { optimizePlans } from '../test-build/plan-optimizer.js';
import { MULTI_WALL_NEAR_ORTHOGONAL_MAX_DEGREES } from '../test-build/wall-thickness.js';
import { nearAxisProfile } from '../scripts/model-invariants.mjs';
import { readHouseplanProductionSource } from './houseplan-source.mjs';
const fixture = JSON.parse(readFileSync(
new URL('./fixtures/279-near-orthogonal-junction.json', import.meta.url), 'utf8',
@@ -21,7 +22,7 @@ const sourceOf = (file) => readFileSync(new URL(`../src/${file}`, import.meta.ur
test('#290 authoring, Optimize and renderer use one near-axis threshold source', () => {
const renderer = sourceOf('wall-thickness.ts');
const authoring = sourceOf('houseplan-card.ts');
const authoring = readHouseplanProductionSource();
const optimizer = sourceOf('plan-optimizer.ts');
assert.match(renderer, /import \{ NEAR_AXIS_MAX_DEGREES \} from '\.\/near-axis';/);
+2 -1
View File
@@ -2,8 +2,9 @@ import test from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { readAllStylesSource } from './styles-source.mjs';
import { readHouseplanProductionSource } from './houseplan-source.mjs';
const card = readFileSync(new URL('../src/houseplan-card.ts', import.meta.url), 'utf8');
const card = readHouseplanProductionSource();
const styles = readAllStylesSource();
const staticRender = readFileSync(new URL('../src/space-render.ts', import.meta.url), 'utf8');
const backend = readFileSync(new URL('../custom_components/houseplan/websocket_api.py', import.meta.url), 'utf8');
+2 -1
View File
@@ -1,8 +1,9 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { readHouseplanProductionSource } from './houseplan-source.mjs';
const source = readFileSync(new URL('../src/houseplan-card.ts', import.meta.url), 'utf8');
const source = readHouseplanProductionSource();
const methodBody = (name) => {
const start = source.search(new RegExp(`(?:private|protected)\\s+(?:get\\s+|async\\s+)?${name}(?:\\s*=)?(?:\\(|:)`));
+6 -5
View File
@@ -6,6 +6,7 @@ import {
GLOW_CARD_CONTRACT,
LARGE_HOUSE_CARD_CONTRACT,
} from '../demo/performance/card-contract.mjs';
import { readHouseplanProductionSource } from './houseplan-source.mjs';
const directCardMembers = (relativePath) => {
const source = readFileSync(new URL(`../${relativePath}`, import.meta.url), 'utf8');
@@ -38,10 +39,10 @@ test('Glow benchmark declares every private card member it consumes', () => {
});
test('performance contracts reference real production members', () => {
const source = readFileSync(new URL('../src/houseplan-card.ts', import.meta.url), 'utf8');
const source = readHouseplanProductionSource();
for (const contract of [LARGE_HOUSE_CARD_CONTRACT, GLOW_CARD_CONTRACT]) {
for (const name of declaredMembers(contract)) {
assert.match(source, new RegExp(`\\b(?:private\\s+(?:get\\s+)?|get\\s+)${name}\\b`),
assert.match(source, new RegExp(`\\b(?:private\\s+(?:declare\\s+|get\\s+)?|get\\s+)${name}\\b`),
`${contract.label} declares missing production member ${name}`);
}
}
@@ -66,7 +67,7 @@ test('contract accepts recent optional fields only when their runtime type is va
});
test('view render resolves structural wall cuts once per frame, not once per room', () => {
const source = readFileSync(new URL('../src/houseplan-card.ts', import.meta.url), 'utf8');
const source = readHouseplanProductionSource();
const start = source.indexOf('const allZeroCuts = this._openCuts();');
const end = source.indexOf('${this._renderOpeningTunnelFills(space, roomFills)}', start);
assert.ok(start >= 0 && end > start, 'view room-render block is present');
@@ -87,7 +88,7 @@ test('view render resolves structural wall cuts once per frame, not once per roo
});
test('room inner faces are structurally cached and shared by both fill layers', () => {
const source = readFileSync(new URL('../src/houseplan-card.ts', import.meta.url), 'utf8');
const source = readHouseplanProductionSource();
const helperStart = source.indexOf('private _innerRoomContour(');
const helperEnd = source.indexOf('\n /**', helperStart);
assert.ok(helperStart >= 0 && helperEnd > helperStart, 'inner-contour cache helper is present');
@@ -113,7 +114,7 @@ test('room inner faces are structurally cached and shared by both fill layers',
});
test('wall and light geometry reuse bounded caches before structural work', () => {
const source = readFileSync(new URL('../src/houseplan-card.ts', import.meta.url), 'utf8');
const source = readHouseplanProductionSource();
const unionStart = source.indexOf('private _wallUnionGeometry()');
const unionEnd = source.indexOf('\n /** Thick-wall spans', unionStart);
+6 -5
View File
@@ -5,6 +5,7 @@ import { readFileSync } from 'node:fs';
import {
createRenderDeviceSnapshot, presentationSnapshotKey, renderDeviceSnapshotPositions,
} from '../test-build/render-device-snapshot.js';
import { readHouseplanProductionSource } from './houseplan-source.mjs';
test('snapshot positions skip resolution until a renderable plan exists', () => {
const devices = [{ id: 'one' }, { id: 'two' }];
@@ -71,7 +72,7 @@ const methodBody = (source, name) => {
};
test('atomic plan render paths do not bypass RenderDeviceSnapshot with this.hass', () => {
const source = readFileSync(new URL('../src/houseplan-card.ts', import.meta.url), 'utf8');
const source = readHouseplanProductionSource();
for (const name of [
'_roomLqi', '_resolvedRoomFills', '_sunNow', '_renderSunRays', '_renderGlowLayer',
'_renderVacuums', '_renderVacFit', '_renderDevice', '_roomTemp', '_roomHum', '_openingAmt',
@@ -82,7 +83,7 @@ test('atomic plan render paths do not bypass RenderDeviceSnapshot with this.hass
});
test('the card gates snapshot positions on the render model', () => {
const source = readFileSync(new URL('../src/houseplan-card.ts', import.meta.url), 'utf8');
const source = readHouseplanProductionSource();
const capture = methodBody(source, '_captureRenderDeviceSnapshot');
assert.match(
capture,
@@ -97,7 +98,7 @@ test('the card gates snapshot positions on the render model', () => {
});
test('opening references use their own availability policy without weakening plan tombstones', () => {
const source = readFileSync(new URL('../src/houseplan-card.ts', import.meta.url), 'utf8');
const source = readHouseplanProductionSource();
for (const name of [
'_contactCandidates', '_lockCandidates', '_openingAmt', '_renderOpenings',
'_renderOpeningLocks', '_renderOpeningInfoCard', '_lockAction',
@@ -125,7 +126,7 @@ test('opening references use their own availability policy without weakening pla
});
test('lock actuation remains guarded inside the one sanctioned opening-card method', () => {
const source = readFileSync(new URL('../src/houseplan-card.ts', import.meta.url), 'utf8');
const source = readHouseplanProductionSource();
const action = methodBody(source, '_lockAction');
const guardAt = action.indexOf('_openingEntityAvailable(entityId)');
const confirmAt = action.indexOf('confirm(');
@@ -136,7 +137,7 @@ test('lock actuation remains guarded inside the one sanctioned opening-card meth
});
test('marker delete/re-add and opening save remain separate config transactions', () => {
const source = readFileSync(new URL('../src/houseplan-card.ts', import.meta.url), 'utf8');
const source = readHouseplanProductionSource();
const saveOpening = methodBody(source, '_saveOpening');
const saveMarker = methodBody(source, '_saveMarker');
assert.match(saveOpening, /sp\.openings/);
+4 -2
View File
@@ -2,10 +2,12 @@ import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import { readAllStylesSource } from './styles-source.mjs';
import { readHouseplanProductionSource } from './houseplan-source.mjs';
const card = fs.readFileSync(new URL('../src/houseplan-card.ts', import.meta.url), 'utf8');
const card = readHouseplanProductionSource();
const resize = fs.readFileSync(new URL('../src/resize.ts', import.meta.url), 'utf8');
const controller = fs.readFileSync(new URL('../src/resize-controller.ts', import.meta.url), 'utf8');
const editorRuntime = fs.readFileSync(new URL('../src/houseplan-editor-runtime.ts', import.meta.url), 'utf8');
const wallInvariant = fs.readFileSync(new URL('../src/wall-record-preservation.ts', import.meta.url), 'utf8');
const invariantCli = fs.readFileSync(new URL('../scripts/model-invariants.mjs', import.meta.url), 'utf8');
const styles = readAllStylesSource();
@@ -99,7 +101,7 @@ test('#264 Resize controller is the sole mutable gesture owner', () => {
for (const mirror of ['_rszSel', '_rszDrag', '_rszPreview', '_rszLive', '_rszEligibilityCache']) {
assert.equal(card.includes(`private ${mirror}`), false, `${mirror} remains a root state mirror`);
}
assert.match(card, /new ResizeController</);
assert.match(editorRuntime, /new ResizeController</);
assert.match(controller, /class ResizeController/);
assert.doesNotMatch(controller, /LitElement|PointerEvent|requestUpdate|_serverCfg|_writeConfig/);
});
+1 -1
View File
@@ -43,7 +43,7 @@ const UNIT_FORMATTING = new RegExp(
*/
const FORMATTERS = [
{ file: 'src/logic.ts', anchor: 'export function formatLength(', lines: 12 },
{ file: 'src/resize.ts', anchor: 'export function formatArea(', lines: 8 },
{ file: 'src/area-format.ts', anchor: 'export function formatArea(', lines: 8 },
];
const sourceFiles = (root) => {
+2 -1
View File
@@ -16,6 +16,7 @@ import {
} from '../test-build/wall-segment-model.js';
import { wallKey } from '../test-build/wall-thickness.js';
import { GRID_STEP_N } from '../test-build/space-geometry.js';
import { readHouseplanProductionSource } from './houseplan-source.mjs';
const rectangle = (id, x1 = 0, y1 = 0, x2 = 1, y2 = 1) => ({
id,
@@ -377,7 +378,7 @@ test('initial migration resolves a reserved deterministic id with the documented
});
test('every frontend structural transaction crosses the wall identity barrier', () => {
const source = readFileSync(new URL('../src/houseplan-card.ts', import.meta.url), 'utf8');
const source = readHouseplanProductionSource();
const optimizer = readFileSync(new URL('../src/plan-optimizer.ts', import.meta.url), 'utf8');
const methodSource = (name) => {
const start = source.indexOf(`private ${name}(`);
+2 -1
View File
@@ -10,6 +10,7 @@ import {
import { spaceModels } from '../test-build/space-geometry.js';
import { wallBodiesGeometry, wallBodiesUnionPath } from '../test-build/wall-thickness.js';
import { checkPhysicalGeometry } from '../scripts/model-invariants.mjs';
import { readHouseplanProductionSource } from './houseplan-source.mjs';
const fixture = JSON.parse(readFileSync(
new URL('./fixtures/278-wall-union-isolation.json', import.meta.url), 'utf8',
@@ -98,7 +99,7 @@ test('#278 physical fingerprint ignores decor but covers every strict writer fie
});
test('#278 production source routes physical writers through one barrier and decor around it', () => {
const source = readFileSync(new URL('../src/houseplan-card.ts', import.meta.url), 'utf8');
const source = readHouseplanProductionSource();
for (const historyKey of [
'draft_segment', 'wall_chain_finish', 'column_add', 'physical_edit', 'physical_delete',
'physical_move', 'resize_room', 'wall_thickness',