Files
houseplan-card/test/wall-union-isolation.test.mjs
claude[bot] 7194fb2df0 feat(adoption): one owner for config/layout identity and the adoption sequence
`src/config-adoption.ts` owns config/layout with revision and fingerprint;
the host keeps `_serverCfg`/`_cfgRev`/`_layout`/`_layoutRev` as delegates.
All seven authoritative adoptions go through `adoptAuthoritativeGated`
(backdrop readiness → continuity → adopt → profile tail); the post-write
paths (space/delete ×2, optimize_undo, import/apply) gain the gate and take
revisions from the re-read bodies. `rollbackOptimistic` moves to the owner;
plan-optimize, space copy and the vacuum writers stop assigning identity.

Issue: #500
User-Visible: no
2026-09-10 11:41:16 +00:00

140 lines
6.5 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';
import { readHouseplanProductionSource } from './houseplan-source.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', '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 = readHouseplanProductionSource();
for (const historyKey of [
'column_add', 'physical_edit', 'physical_delete',
'physical_move', 'resize_room', 'wall_thickness',
'move_opening', 'delete_opening', 'merge_rooms',
]) {
// The pattern tolerates a line break after the opening parenthesis: a
// wrapped call must not slip past the barrier check (CODE-REVIEW-313-r1).
assert.match(source,
new RegExp(`_commitPhysicalGeometry\\(\\s*this\\._t\\('history\\.${historyKey}'`), historyKey);
}
const runtime = readFileSync(
new URL('../src/houseplan-editor-runtime.ts', import.meta.url), 'utf8',
);
assert.match(runtime,
/commitWallChainSegmentGeometry\(this, this\.host\._t\('history\.wall_segment'/,
'an intermediate wall append uses its dedicated bounded physical barrier');
// #313 introduced a second thickness commit point (independent masonry).
// BOTH must go through the barrier: replacing either with _recordGeometry
// reduces the count and reddens this line.
assert.equal(
(source.match(/_commitPhysicalGeometry\(\s*this\._t\('history\.wall_thickness'/g) || []).length,
2, 'both thickness writers route through the common barrier');
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\._rszSpaceCandidateRenderable\(preview\.space, preview\.sp\)/);
assert.match(source, /this\._checkSpacePhysicalGeometry\(candidate, spaceId\)\.ok/);
assert.match(source,
/if \(physicalChanged\)[\s\S]{0,2500}_pendingPhysicalWrites\.set\((?:state|target)\.spaceId/,
'physical Undo/Redo must retain the deferred-write barrier');
// #500: the baseline replacement hook lives with the identity owner; the
// clear runs inside the config-replace hook passed to adoptResponses.
const adoption = readFileSync(new URL('../src/config-adoption.ts', import.meta.url), 'utf8');
assert.match(adoption, /adoptResponses\(cfgResp \?\? \{\}, layResp,\s*\(\) => \{[\s\S]{0,400}_pendingPhysicalWrites\.clear\(\)/,
'an external baseline must invalidate pending local approvals');
});