mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-10-14 19:02:22 +02:00
143 lines
8.1 KiB
JavaScript
143 lines
8.1 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import fs from 'node:fs';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
import test from 'node:test';
|
|
import { allowsDependency, imports, checkArchitecture, inventory } from '../scripts/check_frontend.mjs';
|
|
import * as source from '../frontend/src/shared/actionMenuOrder.js';
|
|
import * as deployed from '../static/js/actionMenuOrder.js';
|
|
|
|
test('stateless helper preserves its public API, ordering, references and inputs', () => {
|
|
for (const module of [source, deployed]) {
|
|
assert.deepEqual(Object.keys(module).sort(), ['SELECT_MENU_ICON', 'actionMenuRank', 'orderActionMenuItems']);
|
|
const callback = () => {};
|
|
const items = ['Delete', 'Select', 'Copy', 'Rename', 'Archive', 'Open', 'Cancel'].map(label => Object.freeze({ label, action: callback }));
|
|
Object.freeze(items);
|
|
const sorted = module.orderActionMenuItems(items);
|
|
assert.deepEqual(sorted.map(i => i.label), ['Open', 'Rename', 'Copy', 'Select', 'Archive', 'Delete', 'Cancel']);
|
|
assert.ok(sorted.every(i => items.includes(i)));
|
|
assert.deepEqual(module.orderActionMenuItems([]), []);
|
|
assert.equal(module.actionMenuRank({ menuOrder: 12, label: 'Delete' }), 12);
|
|
assert.equal(module.actionMenuRank({ label: 'Unknown' }), 100);
|
|
const equalRank = [{ label: 'Open first' }, { label: 'Open second' }];
|
|
assert.deepEqual(module.orderActionMenuItems(equalRank), equalRank);
|
|
}
|
|
assert.equal(source.SELECT_MENU_ICON, deployed.SELECT_MENU_ICON);
|
|
});
|
|
|
|
test('dependency direction separates composition, workflow, adapters and views', () => {
|
|
const n = name => `features/notes/${name}.js`;
|
|
const allowed = [
|
|
['app/bootstrap.js', n('index')],
|
|
[n('index'), n('infrastructure/api')],
|
|
[n('application/crud'), n('domain/recurrence')],
|
|
[n('infrastructure/api'), 'shared/platform/http.js'],
|
|
[n('ui/panel'), n('application/crud')],
|
|
[n('application/reminders'), 'features/calendar/index.js'],
|
|
];
|
|
for (const [from, to] of allowed) assert.ok(allowsDependency(from, to), `${from} -> ${to}`);
|
|
const forbidden = [
|
|
[n('domain/recurrence'), n('infrastructure/api')],
|
|
[n('application/crud'), n('ui/panel')],
|
|
[n('application/crud'), n('infrastructure/api')],
|
|
[n('ui/panel'), 'features/calendar/index.js'],
|
|
[n('application/crud'), 'features/calendar/application/store.js'],
|
|
['app/bootstrap.js', n('domain/recurrence')],
|
|
['shared/platform/http.js', n('index')],
|
|
[n('domain/recurrence'), 'app/bootstrap.js'],
|
|
];
|
|
for (const [from, to] of forbidden) assert.equal(allowsDependency(from, to), false, `${from} -> ${to}`);
|
|
});
|
|
|
|
test('parser sees re-exports, multiline and dynamic imports without comment false positives', () => {
|
|
const code = `// import './false.js';
|
|
const text = "import './false.js'";
|
|
import { a }\n from './a.js';
|
|
export { b } from './b.js';
|
|
export * from './c.js';
|
|
const load = () => import('./d.js');`;
|
|
assert.deepEqual(imports(code).map(i => i.specifier), ['./a.js', './b.js', './c.js', './d.js']);
|
|
assert.throws(() => imports('const load = p => import(p);'), /computed import/);
|
|
assert.throws(() => imports('export const now = () => Date.now();', { domain: true }), /ambient clock/);
|
|
assert.throws(() => imports('export const load = () => fetch("/api/notes");', { domain: true }), /platform identifier/);
|
|
assert.doesNotThrow(() => imports('export const day = value => new Date(value);', { domain: true }));
|
|
assert.doesNotThrow(() => imports('export const copy = value => ({ document: value.document });', { domain: true }));
|
|
});
|
|
|
|
function fixture(t, modules, { cache = true } = {}) {
|
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ody-frontend-gates-'));
|
|
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
|
|
const entries = Object.entries(modules).map(([name, code]) => {
|
|
const file = path.join(root, 'frontend/src', name);
|
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
fs.writeFileSync(file, code);
|
|
return { source: name, deployment: `js/${name}` };
|
|
});
|
|
fs.mkdirSync(path.join(root, 'static'), { recursive: true });
|
|
fs.writeFileSync(path.join(root, 'frontend/modules.json'), JSON.stringify({ version: 1, modules: entries }));
|
|
const urls = cache ? entries.map(m => `/static/${m.deployment}`) : [];
|
|
fs.writeFileSync(path.join(root, 'static/sw.js'), `const PRECACHE = ${JSON.stringify(urls)}; const PANEL_PRECACHE = [];`);
|
|
return root;
|
|
}
|
|
|
|
test('complete mapped graph and exact precache pass', t => {
|
|
const root = fixture(t, { 'features/notes/index.js': "export { value } from './domain/value.js';", 'features/notes/domain/value.js': 'export const value = 1;' });
|
|
assert.equal(checkArchitecture(root), 2);
|
|
});
|
|
|
|
test('feature-private import fails through the full checker', t => {
|
|
const root = fixture(t, { 'features/notes/application/a.js': "import '../../calendar/domain/b.js';", 'features/calendar/domain/b.js': 'export const value = 1;' });
|
|
assert.throws(() => checkArchitecture(root), /forbidden dependency/);
|
|
});
|
|
|
|
test('undeclared legacy coupling and alternate identities fail', t => {
|
|
for (const specifier of ['/static/js/notes.js?v=legacy', './b.js?v=other', '/frontend/src/shared/b.js', 'some-package']) {
|
|
const root = fixture(t, { 'shared/a.js': `import '${specifier}';`, 'shared/b.js': 'export const value = 1;' });
|
|
assert.throws(() => checkArchitecture(root), /legacy import|one unversioned|unsupported|canonical static/);
|
|
}
|
|
});
|
|
|
|
test('a copy mapping cannot silently change relative import resolution', t => {
|
|
const root = fixture(t, { 'shared/a.js': "import './b.js';", 'shared/b.js': 'export const value = 1;' });
|
|
const manifest = JSON.parse(fs.readFileSync(path.join(root, 'frontend/modules.json')));
|
|
manifest.modules[1].deployment = 'js/elsewhere/b.js';
|
|
fs.writeFileSync(path.join(root, 'frontend/modules.json'), JSON.stringify(manifest));
|
|
fs.writeFileSync(path.join(root, 'static/sw.js'), "const PRECACHE = ['/static/js/shared/a.js', '/static/js/elsewhere/b.js'];");
|
|
assert.throws(() => checkArchitecture(root), /resolve differently/);
|
|
});
|
|
|
|
test('new modules must join exact offline manifests and cannot introduce cycles', t => {
|
|
const missing = fixture(t, { 'shared/a.js': 'export const a = 1;' }, { cache: false });
|
|
assert.throws(() => checkArchitecture(missing), /absent from explicit precache/);
|
|
const cyclic = fixture(t, { 'shared/a.js': "import './b.js';", 'shared/b.js': "import './a.js';" });
|
|
assert.throws(() => checkArchitecture(cyclic), /dependency cycle/);
|
|
});
|
|
|
|
test('legacy adapters require exact, used exceptions and reserved names cannot bypass ownership', t => {
|
|
const root = fixture(t, { 'features/notes/infrastructure/api.js': "import '/static/js/legacy.js?v=existing';" });
|
|
fs.mkdirSync(path.join(root, 'static/js'), { recursive: true });
|
|
fs.writeFileSync(path.join(root, 'static/js/legacy.js'), 'export const legacy = true;');
|
|
const file = path.join(root, 'frontend/modules.json');
|
|
const manifest = JSON.parse(fs.readFileSync(file));
|
|
manifest.modules[0].legacyImports = [{ specifier: '/static/js/legacy.js?v=existing', reason: 'Temporary existing transport adapter; remove in N2.' }];
|
|
fs.writeFileSync(file, JSON.stringify(manifest));
|
|
assert.equal(checkArchitecture(root), 1);
|
|
manifest.modules[0].legacyImports.push({ specifier: '/static/js/unused.js', reason: 'stale' });
|
|
fs.writeFileSync(file, JSON.stringify(manifest));
|
|
assert.throws(() => checkArchitecture(root), /unused legacy import exception/);
|
|
assert.throws(() => allowsDependency('features/shared/index.js', 'app/bootstrap.js'), /reserved feature name/);
|
|
});
|
|
|
|
test('inventory reports executable static cycles and separates dynamic edges', t => {
|
|
const root = fixture(t, { 'shared/a.js': 'export const a = 1;' });
|
|
fs.mkdirSync(path.join(root, 'static/js'), { recursive: true });
|
|
fs.writeFileSync(path.join(root, 'static/js/a.js'), "import './b.js'; const load = () => import('./c.js');");
|
|
fs.writeFileSync(path.join(root, 'static/js/b.js'), "import './a.js';");
|
|
fs.writeFileSync(path.join(root, 'static/js/c.js'), 'export const c = 1;');
|
|
const result = inventory(root);
|
|
assert.equal(result.staticImports, 2);
|
|
assert.equal(result.dynamicLiteralImports, 1);
|
|
assert.deepEqual(result.staticCycles, [['js/a.js', 'js/b.js']]);
|
|
assert.deepEqual(result.missing, []);
|
|
});
|