mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
File diff suppressed because one or more lines are too long
@@ -0,0 +1,86 @@
|
||||
/**
|
||||
* #314 anonymised dense-plan fixture.
|
||||
*
|
||||
* It preserves the structural shape of the owner's report without names,
|
||||
* markers, layout, or original coordinates: 13 rooms, 44 catalogue walls,
|
||||
* 24 intentional partitions, and one pre-existing two-point draft. The last
|
||||
* object is known debt and must remain the only invariant finding before and
|
||||
* after drawing another room.
|
||||
*/
|
||||
import { WALL_KEY_PITCH } from './wall-key.mjs';
|
||||
|
||||
export function makeV8DraftRegressionFixture() {
|
||||
const step = WALL_KEY_PITCH;
|
||||
const point = (x, y) => [x * step, y * step];
|
||||
const segmentIds = new Map();
|
||||
const wallSegments = [];
|
||||
const keyOf = (a, b) => {
|
||||
const left = `${a[0]},${a[1]}`;
|
||||
const right = `${b[0]},${b[1]}`;
|
||||
return left < right ? `${left}|${right}` : `${right}|${left}`;
|
||||
};
|
||||
const wallId = (a, b) => {
|
||||
const key = keyOf(a, b);
|
||||
let id = segmentIds.get(key);
|
||||
if (!id) {
|
||||
id = `wall-314-${String(wallSegments.length + 1).padStart(3, '0')}`;
|
||||
segmentIds.set(key, id);
|
||||
wallSegments.push({ id, a: [...a], b: [...b], cm: 15 });
|
||||
}
|
||||
return id;
|
||||
};
|
||||
|
||||
const rooms = Array.from({ length: 13 }, (_, index) => {
|
||||
const x0 = 4 + index * 12;
|
||||
const x1 = x0 + 12;
|
||||
const y0 = 4;
|
||||
const y1 = 16;
|
||||
const poly = [point(x0, y0), point(x1, y0), point(x1, y1)];
|
||||
// Four split top edges make the catalogue population exactly 44 while
|
||||
// keeping all rooms valid and non-overlapping.
|
||||
if (index < 4) poly.push(point(x0 + 6, y1));
|
||||
poly.push(point(x0, y1));
|
||||
return {
|
||||
id: `room-314-${String(index + 1).padStart(2, '0')}`,
|
||||
name: `Room ${index + 1}`,
|
||||
poly,
|
||||
wall_ids: poly.map((a, edge) => wallId(a, poly[(edge + 1) % poly.length])),
|
||||
};
|
||||
});
|
||||
|
||||
const partitions = Array.from({ length: 24 }, (_, index) => {
|
||||
const row = Math.floor(index / 8);
|
||||
const x = 4 + (index % 8) * 18;
|
||||
const y = 28 + row * 8;
|
||||
return {
|
||||
id: `partition-314-${String(index + 1).padStart(2, '0')}`,
|
||||
a: point(x, y),
|
||||
b: point(x + 8, y + 2),
|
||||
cm: 15 + (index % 3) * 5,
|
||||
};
|
||||
});
|
||||
|
||||
if (wallSegments.length !== 44) {
|
||||
throw new Error(`fixture catalogue drifted: expected 44, got ${wallSegments.length}`);
|
||||
}
|
||||
|
||||
return {
|
||||
model_version: 8,
|
||||
spaces: [{
|
||||
id: 'space-314-dense',
|
||||
title: 'Dense regression fixture',
|
||||
cell_cm: 5,
|
||||
view_box: [0, 0, 400, 400],
|
||||
rooms,
|
||||
wall_segments: wallSegments,
|
||||
partitions,
|
||||
room_drafts: [{
|
||||
id: 'draft-314-known-debt',
|
||||
points: [point(4, 56), point(14, 64)],
|
||||
segments: [{ id: 'wall-314-known-draft', cm: 15 }],
|
||||
}],
|
||||
}],
|
||||
markers: [],
|
||||
settings: {},
|
||||
};
|
||||
}
|
||||
@@ -1,9 +1,18 @@
|
||||
/** #314: model-v8 draft identities survive writes/Undo; rejected writes fail closed. */
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import { makeV8DraftRegressionFixture } from './fixtures/v8-draft-regression.mjs';
|
||||
import { launch, checkAll, finish } from './serve.mjs';
|
||||
|
||||
const { page, browser } = await launch({ width: 900, height: 760 });
|
||||
const repoRoot = fileURLToPath(new URL('..', import.meta.url));
|
||||
const denseFixture = makeV8DraftRegressionFixture();
|
||||
|
||||
const result = await page.evaluate(async () => {
|
||||
const result = await page.evaluate(async (denseFixture) => {
|
||||
const out = {};
|
||||
const card = window.__card;
|
||||
const clone = (value) => JSON.parse(JSON.stringify(value));
|
||||
@@ -48,10 +57,10 @@ const result = await page.evaluate(async () => {
|
||||
} };
|
||||
};
|
||||
|
||||
const reset = async () => {
|
||||
const reset = async (initialConfig = emptyConfig()) => {
|
||||
card._saveConfigDebounced.cancel();
|
||||
await card._writeChain.catch(() => undefined);
|
||||
server = emptyConfig();
|
||||
server = clone(initialConfig);
|
||||
rev = 1;
|
||||
writes = [];
|
||||
writeBehavior = null;
|
||||
@@ -62,7 +71,7 @@ const result = await page.evaluate(async () => {
|
||||
card._geometryHistory.clear();
|
||||
card._clearGeometryGesture();
|
||||
card._toast = '';
|
||||
card._space = 'v8-draft';
|
||||
card._space = server.spaces[0].id;
|
||||
card._layout = {};
|
||||
card._modelCache = null;
|
||||
card._wallUnionCache = null;
|
||||
@@ -221,7 +230,70 @@ const result = await page.evaluate(async () => {
|
||||
&& !server.spaces[0].room_drafts?.length
|
||||
&& !server.spaces[0].partitions?.length;
|
||||
|
||||
return out;
|
||||
});
|
||||
// AC8: repeat the real click/close/reload path on an anonymised fixture with
|
||||
// the same population shape as the owner's report. Existing independent
|
||||
// geometry and the one known unusable draft must survive byte-for-byte.
|
||||
await reset(denseFixture);
|
||||
const invariantBefore = clone(server);
|
||||
const originalPartitionIds = server.spaces[0].partitions.map((item) => item.id);
|
||||
const originalDraft = JSON.stringify(server.spaces[0].room_drafts[0]);
|
||||
for (const point of [[800, 100], [900, 100], [900, 200], [800, 200], [800, 100]]) {
|
||||
await clickPoint(point[0], point[1]);
|
||||
card._saveConfigDebounced.flush();
|
||||
await settleWrites();
|
||||
}
|
||||
card._nameSel = 'Invariant room';
|
||||
card._saveRoom();
|
||||
card._saveConfigDebounced.flush();
|
||||
await settleWrites();
|
||||
await card._reloadConfigOnly(true);
|
||||
const denseAfter = card._serverCfg.spaces[0];
|
||||
out.denseFixtureHasOwnerReportShape = invariantBefore.spaces[0].rooms.length === 13
|
||||
&& invariantBefore.spaces[0].wall_segments.length === 44
|
||||
&& originalPartitionIds.length === 24
|
||||
&& invariantBefore.spaces[0].room_drafts.length === 1;
|
||||
out.denseFixtureRoomSurvivesReload = denseAfter.rooms.length === 14
|
||||
&& denseAfter.rooms.some((room) => room.name === 'Invariant room');
|
||||
out.denseFixturePreservesIndependentGeometry = JSON.stringify(
|
||||
denseAfter.partitions.map((item) => item.id),
|
||||
) === JSON.stringify(originalPartitionIds)
|
||||
&& denseAfter.room_drafts.length === 1
|
||||
&& JSON.stringify(denseAfter.room_drafts[0]) === originalDraft;
|
||||
|
||||
await finish(browser, checkAll(result));
|
||||
return { checks: out, invariantBefore, invariantAfter: clone(card._serverCfg) };
|
||||
}, denseFixture);
|
||||
|
||||
const runInvariants = (config, label) => {
|
||||
const dir = mkdtempSync(join(tmpdir(), `houseplan-314-${label}-`));
|
||||
const path = join(dir, 'config.json');
|
||||
try {
|
||||
writeFileSync(path, JSON.stringify(config));
|
||||
const run = spawnSync('npm', [
|
||||
'run', 'invariants', '--', '--config', path, '--json',
|
||||
], { cwd: repoRoot, encoding: 'utf8', shell: process.platform === 'win32' });
|
||||
const stdout = run.stdout || '';
|
||||
const jsonStart = stdout.indexOf('{');
|
||||
if (jsonStart < 0) {
|
||||
throw new Error(`invariant CLI produced no JSON (status ${run.status}):\n`
|
||||
+ `${stdout}\n${run.stderr || run.error || ''}`);
|
||||
}
|
||||
const parsed = JSON.parse(stdout.slice(jsonStart));
|
||||
return { status: run.status, violations: parsed.violations, notes: parsed.notes };
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
};
|
||||
|
||||
const before = runInvariants(result.invariantBefore, 'before');
|
||||
const after = runInvariants(result.invariantAfter, 'after');
|
||||
const signature = (run) => run.violations.map((item) => [
|
||||
item.invariant, item.kind, item.owner,
|
||||
]);
|
||||
result.checks.denseFixtureKnownDebtIsExplicit = before.status === 1
|
||||
&& before.violations.length === 1
|
||||
&& before.violations[0].kind === 'unusable_draft'
|
||||
&& before.violations[0].owner.endsWith(':draft-314-known-debt');
|
||||
result.checks.denseFixtureAddsNoInvariantViolations = after.status === 1
|
||||
&& JSON.stringify(signature(after)) === JSON.stringify(signature(before));
|
||||
|
||||
await finish(browser, checkAll(result.checks));
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
@@ -66,6 +66,13 @@
|
||||
observes synchronous rollback of its whole pending batch: active path,
|
||||
pending map and command history are empty, and finishing the tool cannot
|
||||
create a ghost partition.
|
||||
- [ ] The smoke also loads the anonymised
|
||||
`demo/fixtures/v8-draft-regression.mjs` population (13 rooms, 44 wall
|
||||
catalogue segments, 24 intentional partitions, one known unusable
|
||||
draft), closes and reloads a new room through the real editor path, then
|
||||
runs the `model-invariants.mjs` CLI before and after. The exact existing
|
||||
`unusable_draft` finding must remain the only violation; no independent
|
||||
object may disappear and no new hidden obstacle may appear.
|
||||
- [ ] `tests_backend/test_wall_segment_model.py` accepts valid independent v8
|
||||
draft/partition/column/hosted-opening changes with an unchanged contour
|
||||
catalog, while the existing current/downgraded stale-contour negatives
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"version": 1,
|
||||
"fixture": "synthetic-only",
|
||||
"chromium": "151.0.7922.34",
|
||||
"sourceFingerprint": "cd14953b3d076cfa936f2d3d9f614bc245e14f13374594bf93868cc86bb14a95",
|
||||
"sourceFingerprint": "daf805126d889c54ca7ae63af86c016f442b148fedf12a570f650efb93245ceb",
|
||||
"captureScriptSha256": "ce2e9542fed9dade3085be87d16f69adb2ac8262893ad78ad966b1b9673f2983",
|
||||
"command": "npm run build && node demo/docs/capture.mjs",
|
||||
"scenarios": {
|
||||
@@ -14,7 +14,7 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "cd14953b3d076cfa936f2d3d9f614bc245e14f13374594bf93868cc86bb14a95",
|
||||
"sourceSha256": "daf805126d889c54ca7ae63af86c016f442b148fedf12a570f650efb93245ceb",
|
||||
"imageSha256": "36223106c073f07d8cc3ecf8eaab37192ebb2687daba65c5c21047d0b7890de0"
|
||||
},
|
||||
"view-touch": {
|
||||
@@ -25,7 +25,7 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "cd14953b3d076cfa936f2d3d9f614bc245e14f13374594bf93868cc86bb14a95",
|
||||
"sourceSha256": "daf805126d889c54ca7ae63af86c016f442b148fedf12a570f650efb93245ceb",
|
||||
"imageSha256": "41e3ba67f8db0e98f26f484293af83ef937c369ca5ca6a59a3350d8954c906f4"
|
||||
},
|
||||
"space-create": {
|
||||
@@ -36,7 +36,7 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "cd14953b3d076cfa936f2d3d9f614bc245e14f13374594bf93868cc86bb14a95",
|
||||
"sourceSha256": "daf805126d889c54ca7ae63af86c016f442b148fedf12a570f650efb93245ceb",
|
||||
"imageSha256": "c33a7279165a4cec6fa6fadb6fd08cd967e082a17fe101ef442d27d36ae59b6b"
|
||||
},
|
||||
"room-contour-close": {
|
||||
@@ -47,7 +47,7 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "cd14953b3d076cfa936f2d3d9f614bc245e14f13374594bf93868cc86bb14a95",
|
||||
"sourceSha256": "daf805126d889c54ca7ae63af86c016f442b148fedf12a570f650efb93245ceb",
|
||||
"imageSha256": "b4777162eae89e0d95801721330bcd74ba761624b82b3362069d7e2d38317e08"
|
||||
},
|
||||
"plan-context-tray": {
|
||||
@@ -58,7 +58,7 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "cd14953b3d076cfa936f2d3d9f614bc245e14f13374594bf93868cc86bb14a95",
|
||||
"sourceSha256": "daf805126d889c54ca7ae63af86c016f442b148fedf12a570f650efb93245ceb",
|
||||
"imageSha256": "c0e28edf82f45ccc6df568d3023b681e9e4394262ad34c1605fe9eacdb57a390"
|
||||
},
|
||||
"device-editor": {
|
||||
@@ -69,7 +69,7 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "cd14953b3d076cfa936f2d3d9f614bc245e14f13374594bf93868cc86bb14a95",
|
||||
"sourceSha256": "daf805126d889c54ca7ae63af86c016f442b148fedf12a570f650efb93245ceb",
|
||||
"imageSha256": "d0ffd31ce80bfde21ab75da356a5fc1af38246f2b301030880320620c228d89d"
|
||||
},
|
||||
"device-display-preview": {
|
||||
@@ -80,7 +80,7 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "cd14953b3d076cfa936f2d3d9f614bc245e14f13374594bf93868cc86bb14a95",
|
||||
"sourceSha256": "daf805126d889c54ca7ae63af86c016f442b148fedf12a570f650efb93245ceb",
|
||||
"imageSha256": "2cdabae1f89c3286e4fac0ce30f757ee1690b707ab8a5488748b7cd420626160"
|
||||
},
|
||||
"background-editor": {
|
||||
@@ -91,7 +91,7 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "cd14953b3d076cfa936f2d3d9f614bc245e14f13374594bf93868cc86bb14a95",
|
||||
"sourceSha256": "daf805126d889c54ca7ae63af86c016f442b148fedf12a570f650efb93245ceb",
|
||||
"imageSha256": "30147bb00a90eea7136b4cee30995f6e6a9217b5132f3e8d3ad7471413b1af8a"
|
||||
},
|
||||
"room-card": {
|
||||
@@ -102,7 +102,7 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "cd14953b3d076cfa936f2d3d9f614bc245e14f13374594bf93868cc86bb14a95",
|
||||
"sourceSha256": "daf805126d889c54ca7ae63af86c016f442b148fedf12a570f650efb93245ceb",
|
||||
"imageSha256": "029a3e69ec647a8a370d99e6bb7f9225833c526739076022f6b52ba54bff30ea"
|
||||
},
|
||||
"device-info": {
|
||||
@@ -113,7 +113,7 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "cd14953b3d076cfa936f2d3d9f614bc245e14f13374594bf93868cc86bb14a95",
|
||||
"sourceSha256": "daf805126d889c54ca7ae63af86c016f442b148fedf12a570f650efb93245ceb",
|
||||
"imageSha256": "dd492f53150b7149085daada5cce9eeae9bde9e7ea1d86679a54b3041f72f517"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -281,6 +281,13 @@ F2 остаётся pending и следующим write сохраняется.
|
||||
|
||||
**Доказательство:** model-invariant unit + review кода.
|
||||
|
||||
Исполняемое доказательство находится в `demo/smoke_v8_draft_write.mjs`: оно
|
||||
загружает обезличенную `demo/fixtures/v8-draft-regression.mjs` с формой отчёта
|
||||
13 rooms / 44 wall segments / 24 partitions / 1 unusable draft, закрывает новую
|
||||
комнату реальным editor path и запускает CLI `model-invariants.mjs` на снимках
|
||||
до и после. Единственная заранее известная находка обязана сохраниться без
|
||||
новых violations.
|
||||
|
||||
### AC9. Совместимость, View и touch
|
||||
|
||||
Persisted schema остаётся v8; v7 safe round-trip и v8 read path не меняют
|
||||
|
||||
Reference in New Issue
Block a user