Files
vendoo/tools/verify-safe-action-adapters-1.44.0.mjs

251 lines
13 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 adapters = read('public/safe-action-adapters.js');
const loader = read('public/module-extension-workspace.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}`));
const actionIds = [
'workspace:modules:refresh',
'workspace:modules:filter-all',
'workspace:modules:filter-active',
'workspace:modules:filter-disabled',
'workspace:modules:filter-external',
'workspace:modules:filter-problems',
'workspace:extensions:filter-all',
'workspace:extensions:filter-ready',
'workspace:extensions:filter-connected',
'workspace:extensions:filter-attention',
'workspace:articles:open-current',
'workspace:articles:view-cards',
'workspace:articles:view-list',
'workspace:articles:reset-filters',
'workspace:media:open-current-preview',
'workspace:media:density-compact',
'workspace:media:density-comfortable',
'workspace:media:density-large',
'workspace:media:view-grid',
'workspace:media:view-list',
'workspace:media:reset-filters',
];
includesAll(adapters, [
'uxSafeActionAdaptersReady',
'uxSafeActionAdaptersRegistered',
'window.VendooActions',
'window.VendooNavigation?.currentView',
'workspaceVisible',
"workspaceVisible('admin', 'admin-modules-panel')",
"workspaceVisible('extensions')",
"workspaceVisible('history')",
"workspaceVisible('media-library')",
'buttonAction',
'actions.registerMany(registered)',
"surfaces: ['command-palette']",
"danger: 'none'",
'vendoo:actions-ready',
'vendoo:safe-action-adapters-ready',
'baselineCount: 9',
'expansionCount: registered.length - 9',
...actionIds,
'#module-manager-refresh',
'#history-reset-filters',
'#media-view-grid',
'#media-view-list',
'[data-article-view-choice="cards"]',
'[data-article-view-choice="list"]',
'[data-media-density-option="compact"]',
'[data-media-density-option="comfortable"]',
'[data-media-density-option="large"]',
'.ux-media-filter-reset',
'focusin',
'pointerdown',
], 'Safe Action Adapter-Vertrag fehlt');
includesAll(loader, [
'loadCommandPlatformAssets',
'loadActionRegistryAssets',
'loadGlobalNavigationAssets',
'loadSafeActionAdapters',
'safe-action-adapters.js?v=1.44.0',
'data-ux-safe-action-adapters',
'loadSafeActionAdapters();',
], 'Safe-Action-Loader fehlt');
expect(loader.indexOf('loadActionRegistryAssets();') < loader.indexOf('loadSafeActionAdapters();'), 'Action Registry muss vor Safe Action Adapters geladen werden');
expect(loader.indexOf('loadGlobalNavigationAssets();') < loader.indexOf('loadSafeActionAdapters();'), 'Globale Navigation muss vor Safe Action Adapters geladen werden');
expect(pkg.scripts?.['verify:safe-action-adapters'] === 'node tools/verify-safe-action-adapters-1.44.0.mjs', 'verify:safe-action-adapters fehlt');
expect(pkg.scripts?.['verify:all']?.includes('npm run verify:safe-action-adapters'), 'verify:all enthält Safe-Action-Gate nicht');
expect(!/\.innerHTML\s*=/.test(adapters), 'Safe Action Adapters dürfen innerHTML nicht setzen');
expect(!/\bfetch\s*\(/.test(adapters), 'Safe Action Adapters dürfen keine Netzwerkaufrufe ausführen');
expect(!/\bconfirm\s*\(/.test(adapters), 'Safe Action Adapters dürfen window.confirm nicht verwenden');
expect(!/setInterval\s*\(/.test(adapters), 'Safe Action Adapters dürfen kein Polling starten');
expect(!/Vendoo(?:ModuleManager|App|Api)\.(?:publish|delete|remove|install|enable|disable|start|stop)/i.test(adapters), 'Safe Action Adapters dürfen keine produktive Fachlogik direkt aufrufen');
expect(!/danger:\s*['"](?:caution|destructive)['"]/.test(adapters), 'Safe Action Adapters dürfen keine vorsichtigen oder destruktiven Actions registrieren');
function element({ id = '', selector = '', parent = null } = {}) {
return {
id,
selector,
hidden: false,
disabled: false,
isConnected: true,
clicks: 0,
classList: { contains: () => false },
getAttribute: () => null,
click() { this.clicks += 1; },
closest(query) {
if (query === '#history-list .history-row' && parent?.kind === 'article-row') return parent;
if (query === '[data-media-preview]' && this.selector === '[data-media-preview]') return this;
if (query === '.history-row' && parent?.kind === 'article-row') return parent;
if (query === '.media-card' && parent?.kind === 'media-card') return parent;
return null;
},
querySelector(query) { return parent?.querySelector?.(query) || null; },
};
}
function verifyRuntime() {
const registered = [];
const documentListeners = new Map();
const events = [];
const rootElement = { dataset: {} };
const sectionMap = new Map([
['admin-modules-panel', element({ id: 'admin-modules-panel' })],
['extensions', element({ id: 'extensions' })],
['history', element({ id: 'history' })],
['media-library', element({ id: 'media-library' })],
]);
const selectors = [
'#module-manager-refresh',
'[data-ux-module-group="modules"][data-ux-module-filter="all"]',
'[data-ux-module-group="modules"][data-ux-module-filter="active"]',
'[data-ux-module-group="modules"][data-ux-module-filter="disabled"]',
'[data-ux-module-group="modules"][data-ux-module-filter="external"]',
'[data-ux-module-group="modules"][data-ux-module-filter="problems"]',
'[data-ux-module-group="extensions"][data-ux-module-filter="all"]',
'[data-ux-module-group="extensions"][data-ux-module-filter="ready"]',
'[data-ux-module-group="extensions"][data-ux-module-filter="connected"]',
'[data-ux-module-group="extensions"][data-ux-module-filter="attention"]',
'[data-article-view-choice="cards"]',
'[data-article-view-choice="list"]',
'#history-reset-filters',
'[data-media-density-option="compact"]',
'[data-media-density-option="comfortable"]',
'[data-media-density-option="large"]',
'#media-view-grid',
'#media-view-list',
'.ux-media-filter-reset',
];
const selectorMap = new Map(selectors.map(selector => [selector, element({ selector })]));
const articleTitle = element({ selector: '.history-article-title' });
const articleRow = {
kind: 'article-row',
hidden: false,
disabled: false,
isConnected: true,
getAttribute: () => null,
querySelector: query => query === '.history-article-title' ? articleTitle : null,
};
const articleTarget = element({ parent: articleRow });
const mediaPreview = element({ selector: '[data-media-preview]' });
const documentObject = {
documentElement: rootElement,
addEventListener(type, listener) { documentListeners.set(type, listener); },
getElementById(id) { return sectionMap.get(id) || null; },
querySelector(selector) { return selectorMap.get(selector) || null; },
};
const windowObject = {
VendooNavigation: { currentView: 'admin' },
VendooActions: {
registerMany(actions) { registered.push(...actions); },
refresh() {},
},
getComputedStyle: () => ({ display: 'block', visibility: 'visible' }),
addEventListener() {},
dispatchEvent(event) { events.push(event); return true; },
};
const context = vm.createContext({
window: windowObject,
document: documentObject,
CustomEvent: class CustomEvent { constructor(type, init = {}) { this.type = type; this.detail = init.detail; } },
console,
});
vm.runInContext(adapters, context, { filename: 'public/safe-action-adapters.js' });
expect(registered.length === 21, `Runtime: erwartet 21 Adapter-Actions, erhalten ${registered.length}`);
expect(new Set(registered.map(action => action.id)).size === registered.length, 'Runtime: Action-IDs sind nicht eindeutig');
expect(actionIds.every(id => registered.some(action => action.id === id)), 'Runtime: mindestens eine verbindliche Action-ID fehlt');
expect(registered.every(action => action.danger === 'none'), 'Runtime: nicht sichere Gefahrstufe registriert');
expect(registered.every(action => action.surfaces?.length === 1 && action.surfaces[0] === 'command-palette'), 'Runtime: unerlaubte Action-Surface registriert');
const byId = id => registered.find(action => action.id === id);
const clicked = selector => selectorMap.get(selector)?.clicks === 1;
const invoke = (id, selector, message) => {
expect(byId(id)?.enabled() === true, `${message}: Action ist deaktiviert`);
expect(byId(id)?.handler() === true && clicked(selector), `${message}: Delegation fehlt`);
};
expect(byId('workspace:modules:refresh')?.visible() === true, 'Runtime: Modulaktion ist in Verwaltung nicht sichtbar');
expect(byId('workspace:extensions:filter-all')?.visible() === false, 'Runtime: Extension-Aktion ist außerhalb des Extension-Bereichs sichtbar');
expect(byId('workspace:articles:view-list')?.visible() === false, 'Runtime: Artikelaktion ist außerhalb des Artikelbereichs sichtbar');
invoke('workspace:modules:refresh', '#module-manager-refresh', 'Modul-Refresh');
invoke('workspace:modules:filter-active', '[data-ux-module-group="modules"][data-ux-module-filter="active"]', 'Aktive Module');
invoke('workspace:modules:filter-disabled', '[data-ux-module-group="modules"][data-ux-module-filter="disabled"]', 'Deaktivierte Module');
invoke('workspace:modules:filter-external', '[data-ux-module-group="modules"][data-ux-module-filter="external"]', 'Installierte Module');
invoke('workspace:modules:filter-problems', '[data-ux-module-group="modules"][data-ux-module-filter="problems"]', 'Problematische Module');
windowObject.VendooNavigation.currentView = 'extensions';
expect(byId('workspace:modules:refresh')?.visible() === false, 'Runtime: Modulaktion bleibt außerhalb der Verwaltung sichtbar');
expect(byId('workspace:extensions:filter-connected')?.visible() === true, 'Runtime: Extension-Aktion ist im Extension-Bereich nicht sichtbar');
invoke('workspace:extensions:filter-ready', '[data-ux-module-group="extensions"][data-ux-module-filter="ready"]', 'Bereite Extensions');
invoke('workspace:extensions:filter-connected', '[data-ux-module-group="extensions"][data-ux-module-filter="connected"]', 'Verbundene Extensions');
invoke('workspace:extensions:filter-attention', '[data-ux-module-group="extensions"][data-ux-module-filter="attention"]', 'Extensions mit Handlungsbedarf');
windowObject.VendooNavigation.currentView = 'history';
expect(byId('workspace:articles:view-list')?.visible() === true, 'Runtime: Artikelaktion ist im Artikelbereich nicht sichtbar');
invoke('workspace:articles:view-list', '[data-article-view-choice="list"]', 'Artikel-Listenansicht');
invoke('workspace:articles:reset-filters', '#history-reset-filters', 'Artikelfilter-Reset');
documentListeners.get('focusin')?.({ target: articleTarget });
expect(byId('workspace:articles:open-current')?.enabled() === true, 'Runtime: fokussierter Artikel wird nicht als ausführbar erkannt');
expect(byId('workspace:articles:open-current')?.handler() === true && articleTitle.clicks === 1, 'Runtime: Artikel öffnen delegiert nicht an bestehenden Titel-Handler');
windowObject.VendooNavigation.currentView = 'media-library';
expect(byId('workspace:media:view-grid')?.visible() === true, 'Runtime: Medienaktion ist im Medienbereich nicht sichtbar');
invoke('workspace:media:density-compact', '[data-media-density-option="compact"]', 'Kompakte Galeriedichte');
invoke('workspace:media:density-large', '[data-media-density-option="large"]', 'Große Galeriedichte');
invoke('workspace:media:view-grid', '#media-view-grid', 'Medien-Rasteransicht');
invoke('workspace:media:view-list', '#media-view-list', 'Medien-Listenansicht');
invoke('workspace:media:reset-filters', '.ux-media-filter-reset', 'Medienfilter-Reset');
documentListeners.get('pointerdown')?.({ target: mediaPreview });
expect(byId('workspace:media:open-current-preview')?.enabled() === true, 'Runtime: fokussierte Medienvorschau wird nicht als ausführbar erkannt');
expect(byId('workspace:media:open-current-preview')?.handler() === true && mediaPreview.clicks === 1, 'Runtime: Medienvorschau delegiert nicht an bestehenden Preview-Handler');
expect(events.some(event => event.type === 'vendoo:safe-action-adapters-ready'
&& event.detail?.count === 21
&& event.detail?.baselineCount === 9
&& event.detail?.expansionCount === 12), 'Runtime: vollständiges Ready-Event fehlt');
}
verifyRuntime();
if (failures.length) {
console.error('Safe Action Adapter Gate fehlgeschlagen:');
failures.forEach(failure => console.error(`- ${failure}`));
process.exit(1);
}
console.log('Safe Action Adapter Gate bestanden: 21 sichere Actions.');