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); });