This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
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([])
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user