Preserve the completed teach-and-repeat laboratory stage: reference preparation, cascaded acquisition, local tracking and recovery, recording lifecycle, replay qualification, and persistent Rerun scene controls. Document the open grid-picking regression and Rerun upgrade contract. No autonomous driving or loop-closure optimization is claimed.
51 lines
3.0 KiB
JavaScript
51 lines
3.0 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import { before, after, test } from 'node:test';
|
|
import { readFile } from 'node:fs/promises';
|
|
import { createServer } from 'vite';
|
|
let server, api;
|
|
before(async () => {
|
|
server = await createServer({appType: 'custom', logLevel: 'silent', server: {middlewareMode: true}});
|
|
api = await server.ssrLoadModule('/src/core/missions/planningProjects.ts');
|
|
});
|
|
after(async () => { await server?.close(); });
|
|
const item = {key: 'live:a', id: 'a', name: 'same name', kind: 'live', state: 'completed', revision: 2};
|
|
|
|
test('only terminal runs or drafts admit catalog deletion', () => {
|
|
for (const state of ['completed', 'cancelled', 'error', 'interrupted']) assert.equal(api.planningProjectDeletable({...item, state}), true);
|
|
for (const state of ['running', 'waiting', 'preparing', 'new-unknown']) assert.equal(api.planningProjectDeletable({...item, state}), false);
|
|
assert.equal(api.planningProjectDeletable({...item, kind: 'draft', state: 'draft'}), true);
|
|
assert.equal(api.planningProjectDeletable({...item, kind: 'recorded', state: 'ready'}), true);
|
|
});
|
|
|
|
test('delete sends exact kind/id/revision and requires a matching receipt', async t => {
|
|
const calls = [];
|
|
let receipt = {key: item.key, deleted: true};
|
|
t.mock.method(globalThis, 'fetch', async (...args) => { calls.push(args); return {ok: true, json: async () => receipt}; });
|
|
await api.deletePlanningProject(item);
|
|
assert.equal(calls[0][0], '/api/v1/mission-planner/projects/live/a');
|
|
assert.equal(calls[0][1].method, 'DELETE');
|
|
assert.deepEqual(JSON.parse(calls[0][1].body), {revision: 2});
|
|
receipt = {key: 'live:another-same-name', deleted: true};
|
|
await assert.rejects(api.deletePlanningProject(item), /не подтверждено/);
|
|
const count = calls.length;
|
|
await assert.rejects(api.deletePlanningProject({...item, state: 'running'}), /завершите/);
|
|
assert.equal(calls.length, count);
|
|
});
|
|
|
|
test('server refusal stays an error, never a successful deletion', async t => {
|
|
t.mock.method(globalThis, 'fetch', async () => ({ok: false, json: async () => ({detail: 'Проект изменён.'})}));
|
|
await assert.rejects(api.deletePlanningProject(item), /Проект изменён/);
|
|
});
|
|
|
|
test('UI uses canonical row actions and modal, and stale loads cannot resurrect a deleted item', async () => {
|
|
const component = await readFile(new URL('../src/components/missions/PlanningProjectSelect.tsx', import.meta.url), 'utf8');
|
|
const hook = await readFile(new URL('../src/core/missions/usePlanningProjects.ts', import.meta.url), 'utf8');
|
|
assert.match(component, /<Select/); assert.match(component, /<ConfirmationModal/);
|
|
assert.match(component, /disabled: !planningProjectDeletable\(item\)/);
|
|
assert.match(component, /await onRemove\(target\); setTarget\(null\)/);
|
|
assert.match(component, /<ToastStack/);
|
|
assert.match(hook, /removedKeys.current.has\(item.key\)/);
|
|
assert.match(hook, /removedKeys.current.has\(key\)/);
|
|
assert.match(hook, /if \(selectedKey.current === project.key\) select\(''\)/);
|
|
});
|