Manifestbasierter Production-Updater 3.3, Modulnavigation, optionales Produktbild Studio, Listing-Studio-Aktionsleiste sowie plattformübergreifende Windows-/Linux-Release-Gates.
88 lines
6.6 KiB
JavaScript
88 lines
6.6 KiB
JavaScript
import { mkdtempSync, readFileSync } from 'node:fs';
|
||
import { dirname, join, resolve } from 'node:path';
|
||
import { fileURLToPath } from 'node:url';
|
||
import { tmpdir } from 'node:os';
|
||
import { closeVendooDatabase, removeTempDirectory } from './test-runtime-cleanup.mjs';
|
||
import { ModuleRegistry } from '../app/kernel/module-registry.mjs';
|
||
import { PolicyEngine } from '../app/kernel/policy-engine.mjs';
|
||
import { RouteRegistry } from '../app/kernel/route-registry.mjs';
|
||
|
||
const tempPrefix = process.env.VENDOO_TEST_TEMP_PREFIX || 'vendoo-platform-architecture-';
|
||
const temp = mkdtempSync(join(tmpdir(), tempPrefix));
|
||
process.env.NODE_ENV = 'test';
|
||
process.env.VENDOO_DATA_DIR = temp;
|
||
process.env.VENDOO_DB_PATH = join(temp, 'db', 'vendoo.db');
|
||
process.env.VENDOO_CONFIG_PATH = join(temp, 'config', 'vendoo.env');
|
||
process.env.VENDOO_MODULE_STATE_PATH = join(temp, 'config', 'modules-state.json');
|
||
process.env.VENDOO_MODULES_DIR = join(temp, 'modules');
|
||
|
||
const root = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
||
const read = rel => readFileSync(join(root, rel), 'utf8');
|
||
const failures = [];
|
||
const assert = (condition, message) => { if (!condition) failures.push(message); };
|
||
const expectThrow = async (fn, code, message) => { try { await fn(); failures.push(message); } catch (error) { if (error.code !== code) failures.push(`${message} (${error.code || error.message})`); } };
|
||
|
||
try {
|
||
const { loadBuiltInModules } = await import('../app/modules/catalog.mjs');
|
||
const pkg = JSON.parse(read('package.json'));
|
||
assert(pkg.version === '1.43.0', `package.json ist ${pkg.version}`);
|
||
const modules = loadBuiltInModules();
|
||
assert(modules.length === 18, `Erwartet 18 Module, erhalten ${modules.length}`);
|
||
const nativeIds = modules.filter(item => item.manifest.status === 'native').map(item => item.manifest.id).sort();
|
||
for (const id of ['vendoo.auth','vendoo.users','vendoo.sessions','vendoo.settings','vendoo.security','vendoo.themes','vendoo.listings','vendoo.inventory','vendoo.media','vendoo.ai','vendoo.flux-studio','vendoo.quality','vendoo.publishing','vendoo.module-manager','vendoo.deployments']) {
|
||
assert(nativeIds.includes(id), `Natives Modul fehlt: ${id}`);
|
||
}
|
||
assert(nativeIds.length === 16, `Erwartet 16 native Module, erhalten ${nativeIds.length}`);
|
||
assert(modules.filter(item => item.manifest.status === 'legacy-bridge').length === 2, 'Es müssen 2 Legacy-Bridges verbleiben');
|
||
const productImages = modules.find(item => item.manifest.id === 'vendoo.product-image-tools')?.manifest;
|
||
assert(productImages?.status === 'native' && productImages?.defaultEnabled === false, 'Produktbild Studio muss nativ und initial deaktiviert sein');
|
||
|
||
const registry = new ModuleRegistry();
|
||
for (const module of modules) registry.register(module);
|
||
let realGraph = null;
|
||
try { realGraph = registry.validateGraph(); } catch (error) { failures.push(`Realer Modulgraph ungültig: ${error.code || error.message} – ${error.message}`); }
|
||
assert(registry.snapshot().length === 18, 'Modulregistrierung unvollständig');
|
||
assert(realGraph?.enabledModules.length === 18, 'Nicht alle 18 registrierten Module sind im realen Graph auflösbar');
|
||
assert(realGraph?.capabilities.includes('settings.configuration'), 'settings.configuration fehlt im realen Capability-Graph');
|
||
|
||
const cycle = new ModuleRegistry();
|
||
cycle.register({ manifest: { schemaVersion:1,id:'vendoo.a',name:'A',version:'1.0.0',type:'feature',status:'native',requires:['vendoo.b'],permissions:[],provides:['a.ready'],consumes:[],owns:['a.state'] } });
|
||
cycle.register({ manifest: { schemaVersion:1,id:'vendoo.b',name:'B',version:'1.0.0',type:'feature',status:'native',requires:['vendoo.a'],permissions:[],provides:['b.ready'],consumes:[],owns:['b.state'] } });
|
||
await expectThrow(() => cycle.startAll({}), 'MODULE_DEPENDENCY_CYCLE', 'Zyklus wurde nicht blockiert');
|
||
|
||
const policies = new PolicyEngine();
|
||
policies.register('test.read', ({ user }) => Boolean(user));
|
||
const routeModules = new ModuleRegistry();
|
||
routeModules.register({ manifest: { schemaVersion:1,id:'vendoo.test',name:'Test',version:'1.0.0',type:'feature',status:'native',requires:[],permissions:[],provides:[],consumes:[],owns:[] } }, { enabled: false });
|
||
const routes = new RouteRegistry({ policyEngine: policies, moduleRegistry: routeModules });
|
||
const fake = { get(_path, handler) { this.handler = handler; } };
|
||
routes.register(fake, { id:'test.route.read', method:'GET', path:'/x', owner:'vendoo.test', policy:'test.read', csrf:false, handler(_req,res){res.json({ok:true});} });
|
||
let status = 0; let payload = null;
|
||
await fake.handler({ user:{id:1}, requestId:'test' }, { headersSent:false, status(code){status=code;return this;}, json(value){payload=value;} }, () => {});
|
||
assert(status === 503 && payload?.code === 'MODULE_DISABLED', `Deaktiviertes Modul ergab ${status}`);
|
||
|
||
const moduleContract = read('app/kernel/module-contract.mjs');
|
||
assert(moduleContract.includes('defaultEnabled: manifest.defaultEnabled !== false'), 'Manifestvertrag für defaultEnabled fehlt');
|
||
const moduleSchema = JSON.parse(read('app/contracts/module.schema.json'));
|
||
assert(moduleSchema.properties.defaultEnabled?.type === 'boolean', 'JSON-Schema für defaultEnabled fehlt');
|
||
|
||
const kernel = read('app/kernel/platform-kernel.mjs');
|
||
for (const marker of ["policies.register('modules.manage'", "policies.register('updates.manage'", "features.define('deployments.coolify'", "features.define('modules.manager'", "features.define('publishing.native-module'", 'moduleStateStore', 'modulePackageStore']) assert(kernel.includes(marker), `Kernelvertrag fehlt: ${marker}`);
|
||
const catalog = read('app/modules/catalog.mjs');
|
||
for (const marker of ['createAiModule','createFluxStudioModule','createQualityModule','createPublishingModule','createModuleManagerModule','createDeploymentsModule','createProductImageToolsModule']) assert(catalog.includes(marker), `Modulkatalog fehlt: ${marker}`);
|
||
assert(catalog.includes("const legacyModuleNames = ['system', 'operations']"), 'Legacy-Liste ist nicht auf System und Operations begrenzt');
|
||
const boundaries = JSON.parse(read('app/architecture-boundaries.json'));
|
||
assert(boundaries.architecture === 'modular-monolith', 'Architekturtyp wurde verändert');
|
||
|
||
} finally {
|
||
await closeVendooDatabase({ failures, label: 'Architektur-Testdatenbank' });
|
||
await removeTempDirectory(temp, { failures, label: 'Architektur-Testordner' });
|
||
}
|
||
|
||
if (failures.length) {
|
||
console.error(`Platform-Architecture-Gate 1.43.0 fehlgeschlagen (${failures.length}):`);
|
||
failures.forEach(item => console.error(`- ${item}`));
|
||
process.exit(1);
|
||
}
|
||
console.log('Platform-Architecture-Gate 1.43.0 erfolgreich: 18 Module, 16 native Module, 2 Legacy-Bridges, davon 1 optionales Modul, dynamische Zustände und Modul-Routensperre geprüft.');
|