Files
houseplan-card/test/wall-union-isolation.test.mjs
T
2026-08-24 08:48:37 +03:00

119 lines
5.4 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import {
checkOptimizeGeometry,
prepareSpacePhysicalGeometryInputs,
spacePhysicalGeometryFingerprint,
} from '../test-build/plan-geometry-preflight.js';
import { spaceModels } from '../test-build/space-geometry.js';
import { wallBodiesGeometry, wallBodiesUnionPath } from '../test-build/wall-thickness.js';
import { checkPhysicalGeometry } from '../scripts/model-invariants.mjs';
const fixture = JSON.parse(readFileSync(
new URL('./fixtures/278-wall-union-isolation.json', import.meta.url), 'utf8',
));
const prepare = (config) => {
const raw = config.spaces[0];
const model = spaceModels(config)[0];
return prepareSpacePhysicalGeometryInputs(raw, model);
};
const build = (input) => wallBodiesGeometry(
input.space.rooms, input.walls, input.openCuts, input.roomOpenings,
input.wallKeyPitch, input.cellCm, input.gridPitch, input.coordScale,
input.physicalBodies,
);
test('#278 anonymized regression degrades one boolean merge instead of losing all masonry', () => {
assert.match(fixture.provenance, /Minimized and anonymized/);
assert.doesNotMatch(JSON.stringify(fixture), /Дет|Кабин|Холл|этаж/i);
const input = prepare(fixture.config);
const geometry = build(input);
assert.equal(geometry.status, 'degraded-extra');
assert.equal(geometry.degradedExtraCount, 1);
assert.equal(geometry.components.length, 2);
assert.ok(geometry.components.every((component) => component.geom.length > 0));
const projected = wallBodiesUnionPath(
input.space.rooms, input.walls, input.openCuts, input.roomOpenings,
input.wallKeyPitch, input.cellCm, input.gridPitch, input.coordScale,
input.physicalBodies,
);
assert.ok(projected, 'render-safe projection remains drawable');
assert.equal(projected.status, 'degraded-extra');
assert.equal(projected.paths.length, 2);
assert.ok(projected.paths.every((component) => component.d.length > 20));
});
test('#278 component set is deterministic under room and wall permutations', () => {
const variants = [
fixture.config,
{ ...fixture.config, spaces: [{
...fixture.config.spaces[0],
rooms: [...fixture.config.spaces[0].rooms].reverse(),
walls: [...fixture.config.spaces[0].walls].reverse(),
}] },
];
const projections = variants.map((config) => {
const input = prepare(config);
return wallBodiesUnionPath(
input.space.rooms, input.walls, input.openCuts, input.roomOpenings,
input.wallKeyPitch, input.cellCm, input.gridPitch, input.coordScale,
input.physicalBodies,
);
});
assert.ok(projections.every((projection) => projection?.status === 'degraded-extra'));
assert.deepEqual(
projections[0].paths.map((component) => component.d).sort(),
projections[1].paths.map((component) => component.d).sort(),
);
});
test('#278 Optimize and model-invariants use the same strict structural result', () => {
const preflight = checkOptimizeGeometry(fixture.config);
assert.equal(preflight.ok, false);
assert.equal(preflight.failures[0].reason, 'wall-degraded-extra');
const violations = checkPhysicalGeometry(fixture.config);
assert.deepEqual(violations, [{
invariant: 'physical_geometry', kind: 'physical_geometry', owner: 'space[1]',
reference: 'wall-degraded-extra',
detail: 'canonical wall geometry is not safe for a write',
}]);
assert.doesNotMatch(JSON.stringify(violations), /Wall union regression|r1|r2/);
});
test('#278 physical fingerprint ignores decor but covers every strict writer field', () => {
const raw = fixture.config.spaces[0];
const baseline = spacePhysicalGeometryFingerprint(raw);
assert.equal(spacePhysicalGeometryFingerprint({ ...raw, title: 'Else', decor: [{ id: 'd' }] }), baseline);
for (const field of [
'rooms', 'walls', 'open_spans', 'openings', 'partitions', 'room_drafts', 'wall_columns',
]) {
const changed = { ...raw, [field]: [...(raw[field] || []), { id: `changed-${field}` }] };
assert.notEqual(spacePhysicalGeometryFingerprint(changed), baseline, field);
}
});
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');
for (const historyKey of [
'draft_segment', 'wall_chain_finish', 'column_add', 'physical_edit', 'physical_delete',
'physical_move', 'resize_room', 'close_boundary', 'open_boundary', 'wall_thickness',
'move_opening', 'delete_opening', 'merge_rooms', 'contour_to_partitions',
]) {
assert.match(source, new RegExp(`_commitPhysicalGeometry\\(this\\._t\\('history\\.${historyKey}'`), historyKey);
}
assert.match(source, /_commitPhysicalGeometry\([\s\S]{0,160}history\.edit_opening/);
assert.match(source, /_commitPhysicalGeometry\([\s\S]{0,160}history\.split_room/);
assert.match(source, /_recordGeometry\(this\._t\('history\.decor_edit'/);
assert.doesNotMatch(source, /_commitPhysicalGeometry\(this\._t\('history\.decor_/);
assert.match(source, /this\._checkSpacePhysicalGeometry\(candidate, preview\.space\)/);
assert.match(source, /if \(physicalChanged\)[\s\S]{0,900}_pendingPhysicalWrites\.set\(state\.spaceId/,
'physical Undo/Redo must retain the deferred-write barrier');
assert.match(source, /if \(configChanged\)[\s\S]{0,300}_pendingPhysicalWrites\.clear\(\)/,
'an external baseline must invalidate pending local approvals');
});