Files
NODEDC_MISSION_CORE/apps/control-station/test/planningProjectDeletion.test.mjs
DCCONSTRUCTIONS e515ab1b8c feat(planning): consolidate recorded-route localization and spatial scene
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.
2026-09-21 08:47:19 +03:00

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