219 lines
10 KiB
JavaScript
219 lines
10 KiB
JavaScript
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import vm from 'node:vm';
|
|
|
|
const root = process.cwd();
|
|
const read = relative => fs.readFileSync(path.join(root, relative), 'utf8');
|
|
const js = read('public/action-registry.js');
|
|
const css = read('public/action-registry.css');
|
|
const loader = read('public/module-extension-workspace.js');
|
|
const commandJs = read('public/command-platform.js');
|
|
const navigationJs = read('public/global-navigation-ux.js');
|
|
const pkg = JSON.parse(read('package.json'));
|
|
const failures = [];
|
|
const expect = (condition, message) => { if (!condition) failures.push(message); };
|
|
const includesAll = (source, values, label) => values.forEach(value => expect(source.includes(value), `${label}: ${value}`));
|
|
|
|
includesAll(js, [
|
|
'uxActionRegistryReady',
|
|
'window.VendooActions',
|
|
'registerMany',
|
|
'resolveAction',
|
|
'evaluatePermission',
|
|
'selectionCount',
|
|
'surfaces',
|
|
'command-palette',
|
|
"kind: 'action'",
|
|
'single',
|
|
'parallel',
|
|
'requestConfirmation',
|
|
'setConfirmationProvider',
|
|
'setAuditSink',
|
|
'action-executing',
|
|
'action-executed',
|
|
'action-failed',
|
|
'action-blocked',
|
|
'action-cancelled',
|
|
'action-rolled-back',
|
|
'action-rollback-failed',
|
|
'action-shortcut-conflict',
|
|
'data-vendoo-action',
|
|
'renderSurface',
|
|
'mountSurface',
|
|
'normalizeShortcut',
|
|
'MutationObserver',
|
|
], 'Universal Action Registry JS-Vertrag fehlt');
|
|
|
|
includesAll(css, [
|
|
'.vd-action-surface',
|
|
'.vd-action-button',
|
|
'.vd-action-running',
|
|
'.vd-action-destructive',
|
|
'.vd-action-confirm-overlay',
|
|
'.vd-action-confirm-dialog',
|
|
'.vd-action-confirm-phrase',
|
|
'@media(max-width:620px)',
|
|
'@media(prefers-reduced-motion:reduce)',
|
|
':focus-visible',
|
|
'var(--',
|
|
], 'Universal Action Registry CSS-Vertrag fehlt');
|
|
|
|
includesAll(loader, [
|
|
'loadCommandPlatformAssets',
|
|
'loadActionRegistryAssets',
|
|
'action-registry.css?v=1.44.0',
|
|
'action-registry.js?v=1.44.0',
|
|
'data-ux-action-registry',
|
|
'loadGlobalNavigationAssets',
|
|
'loadCommandPlatformAssets();',
|
|
'loadActionRegistryAssets();',
|
|
'loadGlobalNavigationAssets();',
|
|
], 'Action-Registry-Loader fehlt');
|
|
expect(loader.indexOf('loadCommandPlatformAssets();') < loader.indexOf('loadActionRegistryAssets();'), 'Command Platform muss vor Action Registry geladen werden');
|
|
expect(loader.indexOf('loadActionRegistryAssets();') < loader.indexOf('loadGlobalNavigationAssets();'), 'Action Registry muss vor globaler Navigation geladen werden');
|
|
|
|
expect(commandJs.includes('window.VendooCommands'), 'Command Platform fehlt als Adapterziel');
|
|
expect(navigationJs.includes('registry.execute'), 'Command Palette delegiert nicht an die Command Registry');
|
|
expect(pkg.scripts?.['verify:universal-actions'] === 'node tools/verify-universal-action-registry-1.44.0.mjs', 'verify:universal-actions fehlt');
|
|
expect(pkg.scripts?.['verify:all']?.includes('npm run verify:universal-actions'), 'verify:all enthält Universal-Action-Gate nicht');
|
|
|
|
expect(!/\.innerHTML\s*=/.test(js), 'Action Registry darf innerHTML nicht setzen');
|
|
expect(!/\bfetch\s*\(/.test(js), 'Action Registry darf keine Netzwerkaufrufe ausführen');
|
|
expect(!/\b(?:POST|PUT|PATCH|DELETE)\b/.test(js), 'Action Registry darf keine HTTP-Schreiboperationen implementieren');
|
|
expect(!/\bconfirm\s*\(/.test(js), 'Destruktive Aktionen dürfen window.confirm nicht verwenden');
|
|
expect(!/setInterval\s*\(/.test(js), 'Action Registry darf kein Polling starten');
|
|
expect(!/Vendoo(?:ModuleManager|App|Api)\.(?:publish|delete|remove|install|enable|disable)/i.test(js), 'Action Registry darf produktive Fachaktionen nicht direkt implementieren');
|
|
|
|
async function verifyRuntime() {
|
|
const events = [];
|
|
const commandAdapters = new Map();
|
|
let commandUnregisters = 0;
|
|
const documentListeners = new Map();
|
|
const windowListeners = new Map();
|
|
const classSet = new Set();
|
|
const body = {
|
|
nodeType: 1,
|
|
classList: {
|
|
add: value => classSet.add(value),
|
|
remove: value => classSet.delete(value),
|
|
toggle: (value, force) => force ? classSet.add(value) : classSet.delete(value),
|
|
},
|
|
append() {},
|
|
querySelectorAll: () => [],
|
|
};
|
|
const documentObject = {
|
|
documentElement: { dataset: {} },
|
|
body,
|
|
activeElement: null,
|
|
addEventListener(type, listener) { documentListeners.set(type, listener); },
|
|
removeEventListener() {},
|
|
querySelectorAll: () => [],
|
|
createElement() { throw new Error('DOM-Erzeugung ist im Registry-Runtime-Test nicht erwartet.'); },
|
|
};
|
|
const windowObject = {
|
|
dispatchEvent(event) { events.push(event); return true; },
|
|
addEventListener(type, listener) { windowListeners.set(type, listener); },
|
|
VendooPermissions: undefined,
|
|
VendooNavigation: { currentView: 'dashboard' },
|
|
VendooCommands: {
|
|
register(command) {
|
|
commandAdapters.set(command.id, command);
|
|
return () => { commandUnregisters += 1; commandAdapters.delete(command.id); };
|
|
},
|
|
},
|
|
};
|
|
class Observer { observe() {} disconnect() {} }
|
|
windowObject.MutationObserver = Observer;
|
|
|
|
const context = vm.createContext({
|
|
window: windowObject,
|
|
document: documentObject,
|
|
CustomEvent: class CustomEvent { constructor(type, init = {}) { this.type = type; this.detail = init.detail; } },
|
|
MutationObserver: Observer,
|
|
requestAnimationFrame: callback => { callback(); return 1; },
|
|
cancelAnimationFrame() {},
|
|
console,
|
|
setTimeout,
|
|
clearTimeout,
|
|
});
|
|
|
|
vm.runInContext(js, context, { filename: 'public/action-registry.js' });
|
|
const actions = windowObject.VendooActions;
|
|
expect(actions && typeof actions.register === 'function', 'Runtime: VendooActions.register fehlt');
|
|
expect(typeof actions.renderSurface === 'function' && typeof actions.mountSurface === 'function', 'Runtime: Surface API fehlt');
|
|
expect(actions.normalizeShortcut('control + shift + p') === 'Ctrl+Shift+P', 'Runtime: Shortcut-Normalisierung fehlerhaft');
|
|
|
|
let executed = 0;
|
|
actions.register({
|
|
id: 'test:safe',
|
|
title: 'Sichere Aktion',
|
|
description: 'Runtime-Test',
|
|
group: 'Test',
|
|
surfaces: ['toolbar', 'command-palette'],
|
|
contexts: ['dashboard'],
|
|
selection: { min: 1, max: 2 },
|
|
handler: () => { executed += 1; return true; },
|
|
});
|
|
expect(commandAdapters.get('action:test:safe')?.kind === 'action', 'Runtime: Command-Palette-Adapter fehlt');
|
|
actions.refresh('test:safe');
|
|
expect(commandUnregisters === 0, 'Runtime: Status-Refresh darf Command-Adapter nicht abmelden und Favoriten verlieren');
|
|
expect(actions.list({ surface: 'toolbar', context: { scope: 'dashboard', selectionCount: 1 } })[0]?.id === 'test:safe', 'Runtime: Surface-/Kontextfilter liefert falsche Action');
|
|
expect(actions.resolve('test:safe', { scope: 'dashboard', selectionCount: 0 })?.enabled === false, 'Runtime: Mindestselektion wird nicht erzwungen');
|
|
expect(actions.resolve('test:safe', { scope: 'settings', selectionCount: 1 })?.visible === false, 'Runtime: Kontextgrenze wird nicht erzwungen');
|
|
expect(await actions.execute('test:safe', { scope: 'dashboard', selectionCount: 1, source: 'runtime' }) === true && executed === 1, 'Runtime: sichere Action wurde nicht ausgeführt');
|
|
|
|
let duplicateRejected = false;
|
|
try {
|
|
actions.register({ id: 'test:safe', title: 'Duplikat', handler: () => true });
|
|
} catch { duplicateRejected = true; }
|
|
expect(duplicateRejected, 'Runtime: doppelte Action-ID wurde nicht abgewiesen');
|
|
|
|
actions.register({ id: 'test:permission', title: 'Geschützte Aktion', permission: 'admin', handler: () => true });
|
|
expect(actions.resolve('test:permission')?.visible === false, 'Runtime: Permission ohne Resolver muss deny-by-default sein');
|
|
windowObject.VendooPermissions = { can: permission => permission === 'admin' };
|
|
expect(actions.resolve('test:permission')?.visible === true, 'Runtime: Permission-Resolver wird nicht ausgewertet');
|
|
|
|
let destructiveRuns = 0;
|
|
actions.register({ id: 'test:destructive', title: 'Löschen', danger: 'destructive', handler: () => { destructiveRuns += 1; } });
|
|
actions.setConfirmationProvider(() => false);
|
|
expect(await actions.execute('test:destructive', { source: 'runtime' }) === false && destructiveRuns === 0, 'Runtime: abgelehnte destruktive Action wurde ausgeführt');
|
|
actions.setConfirmationProvider(() => true);
|
|
expect(await actions.execute('test:destructive', { source: 'runtime' }) === true && destructiveRuns === 1, 'Runtime: bestätigte destruktive Action wurde nicht ausgeführt');
|
|
|
|
let releaseLongAction;
|
|
actions.register({
|
|
id: 'test:single-flight',
|
|
title: 'Langlauf',
|
|
concurrency: 'single',
|
|
handler: () => new Promise(resolve => { releaseLongAction = resolve; }),
|
|
});
|
|
const firstRun = actions.execute('test:single-flight', { source: 'runtime' });
|
|
await Promise.resolve();
|
|
expect(actions.resolve('test:single-flight')?.running === true, 'Runtime: Single-Flight-Status fehlt');
|
|
expect(await actions.execute('test:single-flight', { source: 'runtime' }) === false, 'Runtime: parallele Single-Flight-Ausführung wurde nicht blockiert');
|
|
releaseLongAction(true);
|
|
expect(await firstRun === true, 'Runtime: Single-Flight-Ausführung wurde nicht abgeschlossen');
|
|
|
|
let rollbackRuns = 0;
|
|
actions.register({
|
|
id: 'test:rollback',
|
|
title: 'Rollback-Test',
|
|
handler: () => { throw new Error('Fehler'); },
|
|
rollback: () => { rollbackRuns += 1; },
|
|
});
|
|
expect(await actions.execute('test:rollback', { source: 'runtime' }) === false && rollbackRuns === 1, 'Runtime: Rollback-Hook wurde nicht ausgeführt');
|
|
expect(events.some(event => event.type === 'vendoo:action-rolled-back' && event.detail?.actionId === 'test:rollback'), 'Runtime: Rollback-Event fehlt');
|
|
expect(events.some(event => event.type === 'vendoo:action-cancelled' && event.detail?.actionId === 'test:destructive'), 'Runtime: Cancel-Event fehlt');
|
|
expect(events.some(event => event.type === 'vendoo:action-executed' && event.detail?.actionId === 'test:safe'), 'Runtime: Executed-Event fehlt');
|
|
}
|
|
|
|
await verifyRuntime();
|
|
|
|
if (failures.length) {
|
|
console.error('Universal Action Registry Gate fehlgeschlagen:');
|
|
failures.forEach(failure => console.error(`- ${failure}`));
|
|
process.exit(1);
|
|
}
|
|
|
|
console.log('Universal Action Registry Gate bestanden.');
|