Files
houseplan-card/test/vacuum-calibration-write.test.mjs
T
claude[bot] 7194fb2df0 feat(adoption): one owner for config/layout identity and the adoption sequence
`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
2026-09-10 11:41:16 +00:00

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');
});