mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
`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
145 lines
5.1 KiB
JavaScript
145 lines
5.1 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
|
|
import {
|
|
applyCalibrationProposal,
|
|
saveAutomaticCalibration,
|
|
saveManualCalibration,
|
|
saveVacuumMatrix,
|
|
} from '../test-build/vacuum-calibration-write.js';
|
|
import { installAdoption } from './helpers/adoption-host.mjs';
|
|
|
|
function deferred() {
|
|
let resolve;
|
|
let reject;
|
|
const promise = new Promise((done, fail) => { resolve = done; reject = fail; });
|
|
return { promise, resolve, reject };
|
|
}
|
|
|
|
function fixture({ marker = true, save } = {}) {
|
|
const accepted = {
|
|
spaces: [{ id: 'ground', title: 'Ground', rooms: [] }],
|
|
markers: marker ? [{
|
|
id: 'vac', binding: 'entity:vacuum.demo', space: 'ground',
|
|
vacuum: {
|
|
calibration: { old: [1, 0, 10, 0, 1, 20] },
|
|
map_routes: [{
|
|
id: 'upstairs', source: 'camera.map', map_id: 'new', space: 'ground',
|
|
calibration: [1, 0, 10, 0, 1, 20],
|
|
}],
|
|
},
|
|
}] : [],
|
|
};
|
|
const toasts = [];
|
|
let rebuilds = 0;
|
|
const host = {
|
|
_devices: [{
|
|
id: 'vac', bindingKind: 'entity', bindingRef: 'vacuum.demo',
|
|
space: 'ground', area: null, hidden: false,
|
|
}],
|
|
_saveConfigDebounced: { pending: () => true, cancel: () => {} },
|
|
_regSignature: 'accepted',
|
|
_markerDialog: { busy: false, name: 'Vacuum draft' },
|
|
_vacCalConfirm: null,
|
|
_vacFit: null,
|
|
_space: 'ground',
|
|
_commitSpace: () => true,
|
|
_maybeRebuildDevices: () => { rebuilds += 1; },
|
|
_showToast: (message) => { toasts.push(message); },
|
|
_t: (key, vars) => `${key}${vars ? `:${JSON.stringify(vars)}` : ''}`,
|
|
_errText: (error) => String(error?.message || error),
|
|
requestUpdate: () => {},
|
|
};
|
|
installAdoption(host, { config: accepted, configRev: 7 });
|
|
const runtime = {
|
|
host,
|
|
_prepareConfigCandidate: (config) => config,
|
|
_saveConfigNow: save || (async () => {
|
|
host._adoption.refreshConfigFingerprint();
|
|
}),
|
|
};
|
|
return { accepted, host, runtime, toasts, rebuilds: () => rebuilds };
|
|
}
|
|
|
|
test('rejected matrix restores the accepted marker and a retry can persist (#442)', async () => {
|
|
let reject = true;
|
|
const f = fixture({ save: async () => {
|
|
if (reject) throw new Error('semantic reject');
|
|
f.host._adoption.refreshConfigFingerprint();
|
|
} });
|
|
|
|
assert.equal(await saveVacuumMatrix(
|
|
f.runtime, 'vac', 'camera.map', 'new', [2, 0, 30, 0, 2, 40], 'upstairs',
|
|
), false);
|
|
assert.deepEqual(f.host._serverCfg, f.accepted);
|
|
assert.notEqual(f.host._serverCfg, f.accepted, 'rollback uses an isolated accepted snapshot');
|
|
assert.match(f.toasts.at(-1), /^toast\.cfg_save_failed/);
|
|
|
|
reject = false;
|
|
assert.equal(await saveVacuumMatrix(
|
|
f.runtime, 'vac', 'camera.map', 'new', [2, 0, 30, 0, 2, 40], 'upstairs',
|
|
), true);
|
|
const route = f.host._serverCfg.markers[0].vacuum.map_routes
|
|
.find((item) => item.id === 'upstairs');
|
|
assert.deepEqual(route.calibration, [2, 0, 30, 0, 2, 40]);
|
|
});
|
|
|
|
test('rejected first-use calibration leaves no synthetic marker (#442)', async () => {
|
|
const f = fixture({ marker: false, save: async () => { throw new Error('offline'); } });
|
|
|
|
assert.equal(await saveVacuumMatrix(
|
|
f.runtime, 'vac', 'camera.map', 'default', [1, 0, 0, 0, 1, 0],
|
|
), false);
|
|
assert.deepEqual(f.host._serverCfg.markers, []);
|
|
});
|
|
|
|
test('automatic calibration stays busy and has no early success (#442)', async () => {
|
|
const gate = deferred();
|
|
const f = fixture({ save: () => gate.promise });
|
|
const pending = saveAutomaticCalibration(f.runtime, {
|
|
markerId: 'vac', source: 'camera.map', mapId: 'default', space: 'ground',
|
|
matrix: [1, 0, 0, 0, 1, 0], rooms: 3,
|
|
});
|
|
|
|
assert.equal(f.host._markerDialog.busy, true);
|
|
assert.equal(f.toasts.some((toast) => toast.startsWith('vac.autocal_done')), false);
|
|
const duplicate = saveAutomaticCalibration(f.runtime, {
|
|
markerId: 'vac', source: 'camera.map', mapId: 'default', space: 'ground',
|
|
matrix: [1, 0, 0, 0, 1, 0], rooms: 3,
|
|
});
|
|
await duplicate;
|
|
gate.resolve();
|
|
await pending;
|
|
assert.equal(f.host._markerDialog.busy, false);
|
|
assert.equal(f.toasts.filter((toast) => toast.startsWith('vac.autocal_done')).length, 1);
|
|
});
|
|
|
|
test('rejected proposal and manual fit preserve their exact retry drafts (#442)', async () => {
|
|
let reject = true;
|
|
const f = fixture({ save: async () => {
|
|
if (reject) throw new Error('invalid calibration');
|
|
} });
|
|
const matrix = [1, 0, 11, 0, 1, 22];
|
|
const proposal = {
|
|
markerId: 'vac', source: 'camera.map', mapId: 'default', routeId: 'ground',
|
|
space: 'ground', matrix, rooms: 4, error: '42 cm',
|
|
};
|
|
f.host._vacCalConfirm = proposal;
|
|
await applyCalibrationProposal(f.runtime, false);
|
|
assert.deepEqual(f.host._vacCalConfirm, { ...proposal, busy: false });
|
|
|
|
const fit = {
|
|
markerId: 'vac', source: 'camera.map', mapId: 'default', routeId: 'ground',
|
|
p: { s: 1.25, rot: 90, mir: true, ox: 12, oy: 34 }, drag: null,
|
|
};
|
|
f.host._vacCalConfirm = null;
|
|
f.host._vacFit = fit;
|
|
await saveManualCalibration(f.runtime, () => matrix);
|
|
assert.deepEqual(f.host._vacFit, { ...fit, busy: false, drag: null });
|
|
|
|
reject = false;
|
|
await saveManualCalibration(f.runtime, () => matrix);
|
|
assert.equal(f.host._vacFit, null);
|
|
assert.equal(f.toasts.at(-1), 'vac.cal_done');
|
|
});
|