Files
houseplan-card/test/writer-fixed-point.test.mjs
2026-09-06 17:52:05 +00:00

269 lines
12 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { existsSync, readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import ts from 'typescript';
import { finalizeWallChainSpace } from '../test-build/writer-fixed-point.js';
import {
captureMarkerRoomReferences, restoreMarkerRoomReferences, rewriteMarkerRoomReferences,
} from '../test-build/room-reference-transaction.js';
import { optimizePlans, PLAN_MODEL_VERSION } from '../test-build/plan-optimizer.js';
import { geometryOpenCuts } from '../test-build/plan-geometry-preflight.js';
import { commitWallSegmentModel } from '../test-build/wall-segment-model.js';
import {
GRID_PITCH, GRID_STEP_N, spaceModels,
} from '../test-build/space-geometry.js';
const NORM_W = 1000;
const clone = (value) => structuredClone(value);
const configOf = (space, markers = []) => ({
model_version: PLAN_MODEL_VERSION,
spaces: [space], markers, settings: {},
});
const finish = (config, seedIds) => {
const model = spaceModels(config)[0];
const raw = config.spaces[0];
const result = finalizeWallChainSpace(
raw, model, geometryOpenCuts(raw, model), seedIds,
{
pitch: GRID_STEP_N,
cellCm: Number(raw.cell_cm) > 0 ? Number(raw.cell_cm) : 5,
gridPitch: GRID_PITCH,
coordScale: NORM_W,
},
);
const committed = commitWallSegmentModel({ ...config, spaces: [result.space] }).config;
return { config: committed, report: result.report };
};
test('#477 finishes a two/three-segment collinear chain at the Optimize fixed point', () => {
for (const count of [2, 3]) {
const partitions = Array.from({ length: count }, (_, index) => ({
id: `chain-${index}`,
a: [index / count, 0.5], b: [(index + 1) / count, 0.5], cm: 15,
}));
const input = configOf({
id: 'chain', cell_cm: 5, rooms: [], partitions,
});
const before = clone(input);
const result = finish(input, partitions.map((partition) => partition.id));
assert.deepEqual(input, before, 'the finalizer must not mutate the live candidate');
assert.equal(result.report.partitionsMerged, count - 1);
assert.equal(result.config.spaces[0].partitions.length, 1);
assert.deepEqual(result.config.spaces[0].partitions[0], {
id: 'chain-0', a: [0, 0.5], b: [1, 0.5], cm: 15,
});
const optimized = optimizePlans(result.config, {});
assert.equal(optimized.changed, false);
assert.deepEqual(optimized.config, result.config);
}
});
test('#477 seed scope merges the touched chain but leaves unrelated historical seams alone', () => {
const input = configOf({
id: 'bounded', cell_cm: 5, rooms: [], partitions: [
{ id: 'old-a', a: [0, 0.1], b: [0.25, 0.1], cm: 15 },
{ id: 'old-b', a: [0.25, 0.1], b: [0.5, 0.1], cm: 15 },
{ id: 'new-a', a: [0, 0.8], b: [0.25, 0.8], cm: 20 },
{ id: 'new-b', a: [0.25, 0.8], b: [0.5, 0.8], cm: 20 },
],
});
const result = finish(input, ['new-a', 'new-b']);
assert.equal(result.report.partitionsMerged, 1);
assert.deepEqual(result.config.spaces[0].partitions.map((partition) => partition.id),
['old-a', 'old-b', 'new-a']);
});
test('#477 a missing writer seed cannot reconcile unrelated coincident partitions', () => {
const legacy = JSON.parse(readFileSync(
new URL('./fixtures/276-coincident-partition.json', import.meta.url), 'utf8',
));
const baseline = optimizePlans({ ...legacy, spaces: [{
...legacy.spaces[0], partitions: [], openings: [],
}] }, {}).config;
const input = clone(baseline);
input.spaces[0].partitions = clone(legacy.spaces[0].partitions);
input.spaces[0].openings = clone(legacy.spaces[0].openings);
const before = clone(input.spaces[0]);
const result = finish(input, ['writer-seed-that-no-longer-exists']);
assert.equal(result.report.partitionsReconciled, 0);
assert.deepEqual(result.config.spaces[0], before);
});
test('#477 finishes a positive coincident wall with opening rehost at the Optimize fixed point', () => {
const legacy = JSON.parse(readFileSync(
new URL('./fixtures/276-coincident-partition.json', import.meta.url), 'utf8',
));
const baseline = optimizePlans({ ...legacy, spaces: [{
...legacy.spaces[0], partitions: [], openings: [],
}] }, {}).config;
const raw = baseline.spaces[0];
raw.partitions = clone(legacy.spaces[0].partitions);
raw.openings = clone(legacy.spaces[0].openings);
const beforeOpening = clone(raw.openings[0]);
const result = finish(baseline, ['redundant']);
assert.equal(result.report.partitionsReconciled, 1);
assert.equal(result.report.openingsRehosted, 1);
assert.equal(result.config.spaces[0].partitions, undefined);
const opening = result.config.spaces[0].openings[0];
assert.equal(opening.host.kind, 'wall');
assert.equal(opening.x, 121 / 240);
assert.equal(opening.y, 0.5);
assert.equal(opening.length, beforeOpening.length);
assert.equal(opening.contact, beforeOpening.contact);
assert.deepEqual(opening.future_field, beforeOpening.future_field);
const optimized = optimizePlans(result.config, {});
assert.equal(optimized.changed, false);
assert.deepEqual(optimized.config, result.config);
});
test('#477 unsafe coincidence is fail-closed and byte-equivalent', () => {
const legacy = JSON.parse(readFileSync(
new URL('./fixtures/276-coincident-partition.json', import.meta.url), 'utf8',
));
const current = optimizePlans({ ...legacy, spaces: [{
...legacy.spaces[0], partitions: [], openings: [],
}] }, {}).config;
const withCarrier = () => {
const candidate = clone(current);
candidate.spaces[0].partitions = clone(legacy.spaces[0].partitions);
candidate.spaces[0].openings = clone(legacy.spaces[0].openings);
return candidate;
};
const variants = [];
const unknown = withCarrier();
unknown.spaces[0].partitions[0].future_semantics = true;
variants.push(unknown);
const column = withCarrier();
column.spaces[0].wall_columns = [{
id: 'column', shape: 'circle', center: [121 / 240, 0.5], cm: 20,
}];
variants.push(column);
const orphan = withCarrier();
orphan.spaces[0].openings[0].host.t = 2;
variants.push(orphan);
for (const input of variants) {
const before = clone(input.spaces[0]);
const result = finish(input, ['redundant']);
assert.equal(result.report.partitionsReconciled, 0);
assert.deepEqual(result.config.spaces[0], before);
}
});
test('#477 room reference transaction rewrites direct and cross-space vacuum references only', () => {
const markers = [
{ id: 'direct', space: 's1', room_id: 'drop', hidden: true, future: { keep: 1 } },
{ id: 'vac', space: 's2', vacuum: {
entity: 'vacuum.bot', segment_map: { 1: 'drop', 2: 'keep', 3: 'elsewhere' },
calibration: { keep: true },
} },
{ id: 'unrelated', space: 's1', room_id: 'keep', custom: 7 },
];
const merged = rewriteMarkerRoomReferences(markers, {
kind: 'merge', dropId: 'drop', keepId: 'keep',
});
assert.equal(merged.changed, 2);
assert.equal(merged.markers[0].room_id, 'keep');
assert.deepEqual(merged.markers[1].vacuum.segment_map,
{ 1: 'keep', 2: 'keep', 3: 'elsewhere' });
assert.deepEqual(merged.markers[0].future, { keep: 1 });
assert.deepEqual(merged.markers[1].vacuum.calibration, { keep: true });
assert.deepEqual(merged.markers[2], markers[2]);
const later = clone(merged.markers);
later[0].future.keep = 2;
later[1].vacuum.calibration.keep = 'later';
restoreMarkerRoomReferences(later, merged.before);
assert.equal(later[0].room_id, 'drop');
assert.equal(later[0].future.keep, 2, 'unrelated later marker fields survive Undo');
assert.deepEqual(later[1].vacuum.segment_map,
{ 1: 'drop', 2: 'keep', 3: 'elsewhere' });
assert.equal(later[1].vacuum.calibration.keep, 'later');
const deleted = rewriteMarkerRoomReferences(markers, { kind: 'delete', roomId: 'drop' });
assert.equal('room_id' in deleted.markers[0], false);
assert.deepEqual(deleted.markers[1].vacuum.segment_map,
{ 2: 'keep', 3: 'elsewhere' });
assert.deepEqual(captureMarkerRoomReferences(markers, ['direct', 'vac']), deleted.before);
const onlyMap = rewriteMarkerRoomReferences([{
id: 'only-map', vacuum: { segment_map: { 9: 'drop' } },
}], { kind: 'delete', roomId: 'drop' });
assert.equal('vacuum' in onlyMap.markers[0], false);
});
test('#477 source contract routes every explicit chain finish through one finalizer', () => {
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 transaction = readFileSync(new URL('../src/draft-live-commit.ts', import.meta.url), 'utf8');
const finishTransaction = transaction.slice(
transaction.indexOf('export function commitWallChainFinishGeometry'),
transaction.indexOf('/** #461 bounded transaction'),
);
assert.match(runtime, /public _finishWallChain\(\): boolean[\s\S]*?_finalizeWallChainPartitions/);
assert.match(runtime, /_finishWallChain\(\)[\s\S]{0,300}btn\.reset/);
assert.match(runtime, /if \(!accepted\.length\)[\s\S]{0,300}_finalizeWallChainPartitions/);
assert.match(runtime, /_keepClosedAsPartitions[\s\S]*?_finalizeWallChainPartitions/);
assert.match(card, /_slideTo[\s\S]{0,500}_finishWallChain\(\)/);
assert.match(card, /_onHashChange[\s\S]{0,500}_finishWallChain\(\)/);
assert.match(card, /Escape releases the active wall chain[\s\S]{0,250}_finishWallChain\(\)/);
assert.doesNotMatch(runtime, /_markupClick[\s\S]{0,1200}_finalizeWallChainPartitions/,
'the latency-critical terminal click must not run the finalizer');
assert.match(finishTransaction,
/safe = wallModelOffGridValueCount[\s\S]{0,500}host\._checkSpacePhysicalGeometry/,
'the finish candidate must be judged by the bounded production physical proof');
assert.match(finishTransaction, /if \(!safe\) return rejectUnsafe\(runtime, before\);/);
assert.ok(
finishTransaction.indexOf('host._checkSpacePhysicalGeometry')
< finishTransaction.indexOf('if (!safe) return rejectUnsafe(runtime, before);')
&&
finishTransaction.indexOf('if (!safe) return rejectUnsafe(runtime, before);')
< finishTransaction.indexOf('adoptWallSegmentModelCandidateInPlace'),
'the finish candidate must pass the physical guard before live adoption',
);
});
test('#477 executable writer manifest rejects an unclassified geometry writer', () => {
const manifest = JSON.parse(readFileSync(
new URL('./fixtures/477-writer-fixed-point-writers.json', import.meta.url), 'utf8',
));
assert.equal(manifest.schema, 1);
const byOwner = new Map(manifest.cases.map((entry) => [entry.owner, entry]));
assert.equal(byOwner.size, manifest.cases.length, 'writer owners must be unique');
const repo = fileURLToPath(new URL('..', import.meta.url));
for (const entry of manifest.cases) {
assert.ok(entry.class && entry.witness, `incomplete manifest row: ${entry.owner}`);
assert.equal(existsSync(`${repo}/${entry.witness}`), true, `missing witness: ${entry.witness}`);
}
const source = readFileSync(new URL('../src/houseplan-editor-runtime.ts', import.meta.url), 'utf8');
const file = ts.createSourceFile(
'houseplan-editor-runtime.ts', source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS,
);
const discovered = new Set();
const visit = (node) => {
if (ts.isCallExpression(node)) {
const expression = node.expression.getText(file);
if (expression.endsWith('._commitPhysicalGeometry')
|| expression.endsWith('._recordGeometry')) {
let owner = node.parent;
while (owner && !ts.isMethodDeclaration(owner) && !ts.isPropertyDeclaration(owner)) {
owner = owner.parent;
}
const name = owner?.name?.getText(file);
if (name && name !== '_commitPhysicalGeometry') discovered.add(name);
}
}
ts.forEachChild(node, visit);
};
visit(file);
assert.deepEqual(
[...discovered].filter((owner) => !byOwner.has(owner)), [],
'every new geometry writer must declare its fixed-point witness or exception',
);
});