test: isolate version recovery reload guards

Issue: #462
User-Visible: no
This commit is contained in:
Matysh
2026-09-05 16:10:19 +03:00
parent fcf0ce80eb
commit 152a6ca5cc
4 changed files with 149 additions and 11 deletions
+8 -4
View File
@@ -203,7 +203,8 @@ const MUTANT_DEFINITIONS = [
},
{
id: 'version-recovery-ignores-editor-state',
guard: 'node demo/smoke_version_recovery.mjs',
guard: 'node --test --test-name-pattern="card adapter maps editor state" '
+ 'test/version-recovery-card.test.mjs',
because: 'a kiosk card in an editor must preserve unsaved work instead of silently reloading '
+ 'when its versions differ (#462 AC8)',
patches: [{
@@ -214,7 +215,8 @@ const MUTANT_DEFINITIONS = [
},
{
id: 'version-recovery-ignores-dialog-state',
guard: 'node demo/smoke_version_recovery.mjs',
guard: 'node --test --test-name-pattern="card adapter maps blocking surfaces" '
+ 'test/version-recovery-card.test.mjs',
because: 'a first-class dialog or contextual surface must block the kiosk auto-reload '
+ 'rather than disappear underneath it (#462 AC8)',
patches: [{
@@ -225,7 +227,8 @@ const MUTANT_DEFINITIONS = [
},
{
id: 'version-recovery-ignores-pending-config-write',
guard: 'node demo/smoke_version_recovery.mjs',
guard: 'node --test --test-name-pattern="card adapter maps config writes" '
+ 'test/version-recovery-card.test.mjs',
because: 'a pending configuration write must block kiosk reload so an accepted edit cannot '
+ 'be lost during version recovery (#462 AC8)',
patches: [{
@@ -236,7 +239,8 @@ const MUTANT_DEFINITIONS = [
},
{
id: 'version-recovery-ignores-interaction-pause',
guard: 'node demo/smoke_version_recovery.mjs',
guard: 'node --test --test-name-pattern="card adapter maps interaction pause" '
+ 'test/version-recovery-card.test.mjs',
because: 'recent pointer, keyboard, touch and native more-info interaction must defer the '
+ 'silent kiosk reload by the same shared pause contract (#462 AC8)',
patches: [{
+8 -6
View File
@@ -11,15 +11,17 @@ const controller = read('src/version-recovery.ts');
const styles = read('src/styles/base.styles.ts');
const occurrences = (source, fragment) => source.split(fragment).length - 1;
const matches = (source, pattern) => source.match(pattern)?.length || 0;
test('one host seam owns every full-card config/get request', () => {
const direct = "callWS({ type: 'houseplan/config/get'";
assert.equal(occurrences(adapter, direct), 1, 'the adapter is the only WS request owner');
assert.equal(occurrences(card, direct), 0, 'card flows must use the authoritative seam');
assert.equal(occurrences(editor, direct), 0, 'lazy editor flows must use the host seam');
assert.equal(occurrences(onboarding, direct), 0, 'onboarding flows must use the host seam');
const direct = /callWS(?:<[^>]+>)?\(\{ type: 'houseplan\/config\/get'/g;
assert.equal(matches(adapter, direct), 1, 'the adapter is the only WS request owner');
assert.equal(matches(card, direct), 0, 'card flows must use the authoritative seam');
assert.equal(matches(editor, direct), 0, 'lazy editor flows must use the host seam');
assert.equal(matches(onboarding, direct), 0, 'onboarding flows must use the host seam');
assert.match(card, /private _getAuthoritativeConfig\(\): Promise<any>/);
assert.match(card,
/private _getAuthoritativeConfig\(\): Promise<AuthoritativeConfigResponse>/);
assert.equal(occurrences(card, 'this._getAuthoritativeConfig()'), 2,
'initial load and config-only reload both use the seam');
assert.equal(occurrences(editor, 'this.host._getAuthoritativeConfig()'), 3,
+132
View File
@@ -0,0 +1,132 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { cardVersionReloadSafetySnapshot } from '../test-build/version-recovery-card.js';
const pending = (size = 0) => ({ size });
const debounce = (value = false) => ({ pending: () => value });
function safeHost(overrides = {}) {
return {
ownerDocument: { defaultView: null },
isConnected: true,
_config: { kiosk: true },
_mode: 'view',
_editing: false,
_loadOk: true,
_loading: false,
_continuityDataReady: true,
_resumeSettling: false,
_connectionWasLost: false,
_booting: false,
_bootFading: false,
_bootSettling: false,
_bootSoft: false,
_continuity: {
hasCompleteFrame: true,
state: 'steady',
overlayBlocksInteraction: false,
},
_stageEl: { clientWidth: 800, clientHeight: 600 },
_pointers: pending(),
_touchContacts: pending(),
_touchSequenceMultitouch: false,
_roomPointer: null,
_panStart: null,
_panLock: null,
_pinchStart: null,
_swipeStart: null,
_tabDrag: null,
_tabDragRelease: null,
_drag: null,
_deviceDrag: null,
_rlResize: null,
_resize: null,
_physicalDrag: null,
_physicalRotate: null,
_opDrag: null,
_decorDraft: null,
_decorMove: null,
_dtDrag: null,
_bdDrag: null,
_furnTouchPending: null,
_compassDrag: null,
_viewportGestureDirty: false,
_devicePositionBusy: false,
_modeTransitionBusy: false,
_cameraTransition: { active: false },
_slide: '',
_warmModeRequest: 0,
_writesPending: 0,
_saveConfigDebounced: debounce(),
_pendingPhysicalWrites: pending(),
_persistLayout: debounce(),
_dirtyPos: pending(),
_sentPos: pending(),
_cyclePausedUntil: 0,
_zoom: 1,
_editorSecondaryDialogBlocked: false,
_partitionDeleteDialog: null,
_roomDeleteDialog: null,
_backdropGuard: null,
_vacFit: null,
_editorSecondary: null,
_furnPalette: null,
_decorImagePalette: null,
...overrides,
};
}
test('card adapter baseline is safe only after the full rendered card settles', () => {
assert.deepEqual(cardVersionReloadSafetySnapshot(safeHost()), {
connected: true,
initialFrameSettled: true,
viewOnly: true,
surfacesIdle: true,
configWritesIdle: true,
physicalWritesIdle: true,
layoutWritesIdle: true,
gesturesIdle: true,
interactionPauseElapsed: true,
baseZoom: true,
});
});
test('card adapter maps editor state into the kiosk reload guard', () => {
for (const overrides of [
{ _config: { kiosk: false } },
{ _mode: 'plan' },
{ _editing: true },
]) {
assert.equal(cardVersionReloadSafetySnapshot(safeHost(overrides)).viewOnly, false);
}
});
test('card adapter maps blocking surfaces into the kiosk reload guard', () => {
for (const overrides of [
{ _editorSecondaryDialogBlocked: true },
{ _partitionDeleteDialog: {} },
{ _roomDeleteDialog: {} },
{ _backdropGuard: {} },
{ _vacFit: {} },
{ _editorSecondary: { hasOpenGroup: true } },
{ _furnPalette: {} },
{ _decorImagePalette: {} },
]) {
assert.equal(cardVersionReloadSafetySnapshot(safeHost(overrides)).surfacesIdle, false);
}
});
test('card adapter maps config writes into the kiosk reload guard', () => {
assert.equal(cardVersionReloadSafetySnapshot(safeHost({ _writesPending: 1 })).configWritesIdle,
false);
assert.equal(cardVersionReloadSafetySnapshot(safeHost({
_saveConfigDebounced: debounce(true),
})).configWritesIdle, false);
});
test('card adapter maps interaction pause into the kiosk reload guard', () => {
assert.equal(cardVersionReloadSafetySnapshot(safeHost({
_cyclePausedUntil: Date.now() + 60_000,
})).interactionPauseElapsed, false);
});
+1 -1
View File
@@ -26,7 +26,7 @@
"src/space-geometry.ts", "src/junction-limits.ts",
"src/space-order.ts", "src/card-editor-validation.ts",
"src/signing.ts", "src/initial-load.ts", "src/space-model-selection.ts", "src/space-dialog.ts",
"src/visual-continuity.ts", "src/version-recovery.ts", "src/mode-transition.ts", "src/viewport-transition.ts", "src/room-fit.ts", "src/editor-runtime-loader.ts", "src/editor-secondary.ts", "src/pointer-modality.ts",
"src/visual-continuity.ts", "src/version-recovery.ts", "src/version-recovery-card.ts", "src/mode-transition.ts", "src/viewport-transition.ts", "src/room-fit.ts", "src/editor-runtime-loader.ts", "src/editor-secondary.ts", "src/pointer-modality.ts",
"src/render-device-snapshot.ts", "src/render-invalidation.ts", "src/houseplan-render-lifecycle.ts", "src/live-interaction-runtime.ts", "src/live-viewport.ts", "src/live-hover.ts", "src/live-editor.ts", "src/pointer-move-queue.ts",
"src/command-stack.ts", "src/device-position-history.ts",
"src/coordinate-canonicalization.ts",