116 lines
5.5 KiB
JavaScript
116 lines
5.5 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 {
|
|
checkImportBoundaries,
|
|
importBoundaryViolation,
|
|
} from '../../scripts/check-import-boundaries.mjs';
|
|
|
|
test('architecture rejects reverse ownership imports', () => {
|
|
assert.equal(
|
|
importBoundaryViolation('src/shared/contracts/state.ts', '../../server/main.ts'),
|
|
'shared cannot import server or web',
|
|
);
|
|
assert.equal(
|
|
importBoundaryViolation('src/server/http/routes/state.ts', '../../infrastructure/filesystem/store.ts'),
|
|
'server/http cannot import infrastructure directly',
|
|
);
|
|
assert.equal(
|
|
importBoundaryViolation('src/web/ui/Button.tsx', '../api/harborClient.ts'),
|
|
'web/ui cannot import api or features',
|
|
);
|
|
assert.equal(
|
|
importBoundaryViolation('src/web/ui/Button.tsx', '../api.js'),
|
|
'web/ui cannot import api or features',
|
|
);
|
|
});
|
|
|
|
test('architecture requires cross-feature imports to use the public index', () => {
|
|
assert.equal(
|
|
importBoundaryViolation(
|
|
'src/web/features/connection/controller.ts',
|
|
'../servers/private/selectors.ts',
|
|
),
|
|
'cross-feature imports must use the feature index',
|
|
);
|
|
assert.equal(
|
|
importBoundaryViolation('src/web/features/connection/controller.ts', '../servers/index.ts'),
|
|
null,
|
|
);
|
|
});
|
|
|
|
test('strict TypeScript source tree passes the active boundaries', () => {
|
|
const result = checkImportBoundaries();
|
|
assert.equal(result.violations.length, 0, JSON.stringify(result.violations, null, 2));
|
|
assert.ok(result.filesChecked > 0);
|
|
});
|
|
|
|
test('state views cannot read flat v0 wire aliases or a parallel state transport', () => {
|
|
const root = path.resolve(import.meta.dirname, '../..');
|
|
const page = fs.readFileSync(path.join(root, 'src/web/components/ClientOverviewPage.tsx'), 'utf8');
|
|
const app = fs.readFileSync(path.join(root, 'src/web/App.tsx'), 'utf8');
|
|
const client = fs.readFileSync(path.join(root, 'src/web/api/harborClient.ts'), 'utf8');
|
|
const server = fs.readFileSync(path.join(root, 'src/server/index.ts'), 'utf8');
|
|
const consumers = [
|
|
'src/web/App.tsx',
|
|
'src/web/components/ClientOverviewPage.tsx',
|
|
'src/web/features/diagnostics/ConnectivityDiagnosticsPanel.tsx',
|
|
'src/web/features/devices/DevicesPanel.tsx',
|
|
'src/web/features/servers/ServerPicker.tsx',
|
|
'src/web/features/connection/ConnectionPanel.tsx',
|
|
'src/web/features/subscription/SubscriptionFeature.tsx',
|
|
'src/web/features/routing/RoutingFeature.tsx',
|
|
'src/web/features/instructions/InstructionsFeature.tsx',
|
|
].map((file) => fs.readFileSync(path.join(root, file), 'utf8')).join('\n');
|
|
const componentSources = fs.readdirSync(path.join(root, 'src/web/components'))
|
|
.filter((file) => /\.[jt]sx?$/.test(file))
|
|
.map((file) => fs.readFileSync(path.join(root, 'src/web/components', file), 'utf8'))
|
|
.concat([
|
|
fs.readFileSync(path.join(root, 'src/web/features/connection/ConnectionPanel.tsx'), 'utf8'),
|
|
fs.readFileSync(path.join(root, 'src/web/features/servers/ServerPicker.tsx'), 'utf8'),
|
|
fs.readFileSync(path.join(root, 'src/web/features/subscription/SubscriptionFeature.tsx'), 'utf8'),
|
|
fs.readFileSync(path.join(root, 'src/web/features/routing/RoutingFeature.tsx'), 'utf8'),
|
|
fs.readFileSync(path.join(root, 'src/web/features/devices/DevicesFeature.tsx'), 'utf8'),
|
|
fs.readFileSync(path.join(root, 'src/web/features/devices/DevicesPanel.tsx'), 'utf8'),
|
|
fs.readFileSync(path.join(root, 'src/web/features/diagnostics/DiagnosticsFeature.tsx'), 'utf8'),
|
|
fs.readFileSync(path.join(root, 'src/web/features/diagnostics/ConnectivityDiagnosticsPanel.tsx'), 'utf8'),
|
|
fs.readFileSync(path.join(root, 'src/web/features/instructions/InstructionsFeature.tsx'), 'utf8'),
|
|
])
|
|
.join('\n');
|
|
const flatRead = /state\??\.(?:hasSubscription|subscriptionHost|singboxRunning|singboxStartedAt|configExists|proxyPort|userInfo|gatewayAuto)\b/;
|
|
|
|
assert.doesNotMatch(page, flatRead);
|
|
assert.doesNotMatch(app, flatRead);
|
|
assert.doesNotMatch(client, /\bstate:\s*\(\)\s*=>\s*request\(['"]\/api\/state/);
|
|
assert.equal(fs.existsSync(path.join(root, 'src/web/api.js')), false);
|
|
assert.doesNotMatch(consumers, /from ['"][^'"]*\/api\.js['"]/);
|
|
assert.doesNotMatch(componentSources, /from ['"][^'"]*\/api(?:\/|\.js)/);
|
|
assert.match(client, /export class HarborApiError/);
|
|
assert.match(client, /export async function request/);
|
|
assert.match(client, /export const api =/);
|
|
assert.match(client, /export function parseHarborState/);
|
|
assert.doesNotMatch(server, /function\s+publicState\b/);
|
|
assert.doesNotMatch(server, /req\.url\s*===\s*['"]\/api\/state['"]/);
|
|
assert.match(app, /const displayState = previewReady \? \{[\s\S]*mode: 'client' as const/);
|
|
});
|
|
|
|
test('comments, strings, and property names are not treated as imports', (t) => {
|
|
const repository = fs.mkdtempSync(path.join(os.tmpdir(), 'harbor-boundaries-'));
|
|
t.after(() => fs.rmSync(repository, { recursive: true, force: true }));
|
|
const file = path.join(repository, 'src/web/ui/example.ts');
|
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
fs.writeFileSync(file, [
|
|
'// import value from "../../server/private.ts";',
|
|
'const example = \'import value from "../../server/private.ts";\';',
|
|
'const metadata = { import: "../../server/private.ts" };',
|
|
'const loader = { import() {} };',
|
|
'loader.import("../../server/private.ts");',
|
|
'export { example };',
|
|
'',
|
|
].join('\n'));
|
|
assert.equal(checkImportBoundaries(repository).violations.length, 0);
|
|
});
|