mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
B3: _persistDevicePlacement sent canonicalizePosition(...) to the server and left the raw value in _layout, then recorded the fingerprint over that raw snapshot. Canonicalization is not identity — it snaps to the lattice — so 39 of 115 pixel-derived coordinates differ, and the next _reloadLayoutOnly or _adoptStructuralResponses saw its own write as a remote edit: history cleared, _layout replaced. The old _persistLayout wrote the canonical value back; the per-device path introduced by #74 lost that line. M1: the smoke that was supposed to prove AC10 assigned serverLayout = structuredClone(c._layout) right before the reload — erasing by hand the very divergence it existed to catch, so it could not fail. The fake WS already stores what went over the wire; the assignment is gone and the check now reddens on the unfixed code (verified: three checks red without the fix, including this one). Also proven, because the fix touches their neighbourhood: the echo of a DELETE keeps the history (the branch removes a key rather than replacing a value), and an in-flight write still wins the merge against a server answer holding the old position. One existing assertion was loosened deliberately: undo now restores a position that may differ from the raw one by the lattice snap (<1e-9 of the plan). That is the point of the fix — local and server agree — so the equality is stated to that precision, with the snap size pinned separately so a real drift would still fail. User-Visible: yes Issue: #397
49 lines
2.6 KiB
JavaScript
49 lines
2.6 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { readFileSync } from 'node:fs';
|
|
import { canonicalizePosition } from '../test-build/coordinate-canonicalization.js';
|
|
import { contentFingerprint } from '../test-build/visual-continuity.js';
|
|
|
|
// #397: the card sends the canonical position to the server and must keep the
|
|
// same value locally. While it kept the raw one, its own echo came back
|
|
// looking foreign — the fingerprints differed — and the next reload wiped the
|
|
// undo history the user had just filled (AC10 of #74).
|
|
|
|
const CARD = readFileSync(new URL('../src/houseplan-card.ts', import.meta.url), 'utf8');
|
|
|
|
test('#397: the premise holds — canonicalization is not identity', () => {
|
|
// If it were, the whole issue would be moot and the guards below decorative.
|
|
const raw = { s: 'ground', x: 0.024999999999999942, y: 0.63 / 3 };
|
|
const canonical = canonicalizePosition(raw);
|
|
assert.notEqual(contentFingerprint(canonical), contentFingerprint(raw),
|
|
'the lattice snap must actually change the value');
|
|
assert.ok(Math.abs(canonical.x - raw.x) < 1e-9,
|
|
'and it must be a snap, not a move — the difference is invisible on screen');
|
|
});
|
|
|
|
test('#397 AC1: the update branch stores what it sends, before sending it', () => {
|
|
const branch = CARD.slice(
|
|
CARD.indexOf('private async _persistDevicePlacement'),
|
|
CARD.indexOf('this._persistLocalLayout();', CARD.indexOf('private async _persistDevicePlacement')),
|
|
);
|
|
assert.ok(branch, 'the persist method must be found');
|
|
const write = branch.indexOf('this._layout = { ...this._layout, [deviceId]: pos }');
|
|
const send = branch.indexOf("type: 'houseplan/layout/update'");
|
|
const fingerprint = branch.indexOf('this._layoutContentFingerprint = contentFingerprint(this._layout)');
|
|
assert.ok(write > 0, 'the canonical position must be written back into _layout');
|
|
assert.ok(write < send,
|
|
'the local copy is updated BEFORE the wire, so a reload racing the answer '
|
|
+ 'sees the value that was sent');
|
|
assert.ok(send < fingerprint,
|
|
'the fingerprint is taken after the write, over the canonical layout');
|
|
});
|
|
|
|
test('#397 AC5a: the delete branch removes the key before the fingerprint', () => {
|
|
const method = CARD.slice(CARD.indexOf('private async _persistDevicePlacement'));
|
|
const apply = method.indexOf('applyDevicePlacement(this._layout, deviceId, placement)');
|
|
const fingerprint = method.indexOf('this._layoutContentFingerprint = contentFingerprint(this._layout)');
|
|
assert.ok(apply > 0 && apply < fingerprint,
|
|
'both branches mutate _layout through applyDevicePlacement before the '
|
|
+ 'fingerprint is recorded — deletion removes the key, not replaces a value');
|
|
});
|