Files
vendoo/tools/verify-universal-action-registry-1.44.0.mjs
T

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