Files
DevRunbook-Public/tests/security/dependency-boundaries.test.ts
T
DevRunbook release export cfd2804e27
Managed validation / full (push) Successful in 3m18s
Publish DevRunbook source
2026-09-03 04:09:17 +02:00

101 lines
3.2 KiB
TypeScript

import { readFileSync, readdirSync } from 'node:fs'
import path from 'node:path'
import { describe, expect, it } from 'vitest'
const repositoryRoot = process.cwd()
function sourceFiles(relativeRoot: string): string[] {
const absoluteRoot = path.join(repositoryRoot, relativeRoot)
return readdirSync(absoluteRoot, { recursive: true, withFileTypes: true })
.filter(
(entry) =>
entry.isFile() &&
/\.(?:ts|tsx)$/u.test(entry.name) &&
!entry.name.endsWith('.test.ts'),
)
.map((entry) => path.join(entry.parentPath, entry.name))
}
function violations(files: readonly string[], forbidden: RegExp) {
return files
.filter((file) => forbidden.test(readFileSync(file, 'utf8')))
.map((file) => path.relative(repositoryRoot, file).replaceAll('\\', '/'))
}
describe('architectural dependency boundaries', () => {
it('keeps persistence adapters out of Next.js route handlers and UI', () => {
const routes = sourceFiles('apps/web/src/app').filter((file) =>
file.endsWith(`${path.sep}route.ts`),
)
const ui = sourceFiles('packages/ui/src')
const forbidden = /(?:@devrunbook\/db|drizzle-orm)/u
expect(violations(routes, forbidden)).toEqual([])
expect(violations(ui, forbidden)).toEqual([])
})
it('keeps domain and application packages independent of framework and persistence code', () => {
expect(
violations(
sourceFiles('packages/domain/src'),
/(?:@devrunbook\/(?:application|db)|drizzle-orm|from ['"]next(?:\/|['"]))/u,
),
).toEqual([])
expect(
violations(
sourceFiles('packages/application/src'),
/(?:@devrunbook\/db|drizzle-orm|from ['"]next(?:\/|['"]))/u,
),
).toEqual([])
})
it('prevents content and composer code from executing imported instructions', () => {
const files = [
...sourceFiles('packages/content/src'),
...sourceFiles('packages/composer/src'),
]
expect(
violations(
files,
/(?:node:child_process|child_process|Bun\.spawn|Deno\.Command)/u,
),
).toEqual([])
})
it('keeps operator commands attached to application use cases', () => {
const commands = sourceFiles('apps/worker/src/operator')
for (const command of commands) {
const source = readFileSync(command, 'utf8')
expect(source, path.relative(repositoryRoot, command)).toContain(
'@devrunbook/application',
)
expect(source, path.relative(repositoryRoot, command)).not.toMatch(
/(?:getSqlClient|\.execute\(|sql`)/u,
)
}
})
it('keeps worker orchestration downstream and job handlers adapter-free', () => {
const upstream = [
...sourceFiles('packages/domain/src'),
...sourceFiles('packages/application/src'),
...sourceFiles('packages/db/src'),
...sourceFiles('apps/web/src'),
]
expect(
violations(upstream, /(?:@devrunbook\/worker|apps\/worker)/u),
).toEqual([])
const handlers = sourceFiles('apps/worker/src/jobs').filter((file) =>
file.endsWith(`${path.sep}handlers.ts`),
)
expect(
violations(
handlers,
/(?:@devrunbook\/db|drizzle-orm|getSqlClient|node:child_process|sql`)/u,
),
).toEqual([])
})
})