101 lines
3.2 KiB
TypeScript
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([])
|
|
})
|
|
})
|