Publish DevRunbook source
Managed validation / full (push) Successful in 3m18s

This commit is contained in:
DevRunbook release export
2026-09-03 04:09:17 +02:00
commit cfd2804e27
928 changed files with 161642 additions and 0 deletions
+28
View File
@@ -0,0 +1,28 @@
import { request, type FullConfig } from '@playwright/test'
import { mkdir } from 'node:fs/promises'
export default async function globalSetup(config: FullConfig) {
const email = process.env.DEVRUNBOOK_E2E_EMAIL
const password = process.env.DEVRUNBOOK_E2E_PASSWORD
if (!email || !password) return
const baseURL = config.projects[0]?.use.baseURL
if (typeof baseURL !== 'string') {
throw new Error('PLAYWRIGHT_BASE_URL is required for authenticated setup')
}
const context = await request.newContext({
baseURL,
extraHTTPHeaders: { origin: new URL(baseURL).origin },
})
try {
const response = await context.post('/api/auth/sign-in/email', {
data: { email, password },
})
if (!response.ok()) {
throw new Error(`Browser authentication failed with ${response.status()}`)
}
await mkdir('test-results/.auth', { recursive: true })
await context.storageState({ path: 'test-results/.auth/m2.json' })
} finally {
await context.dispose()
}
}
+305
View File
@@ -0,0 +1,305 @@
import { expect, test, type APIRequestContext } from '@playwright/test'
const email = process.env.DEVRUNBOOK_E2E_EMAIL
const password = process.env.DEVRUNBOOK_E2E_PASSWORD
function profile(name: string) {
return {
apiVersion: 'devrunbook.io/v1alpha1',
kind: 'RepositoryProfile',
metadata: { name, revision: 1, source: 'manual' },
spec: {
repositoryType: 'single-app',
defaultBranch: 'main',
stack: {
languages: ['TypeScript'],
frameworks: ['Next.js'],
packageManagers: ['pnpm'],
databases: ['PostgreSQL'],
deploymentTypes: ['Docker'],
testFrameworks: ['Vitest', 'Playwright'],
},
commands: [
{
id: 'verify',
role: 'unit-test',
command: 'pnpm test',
workingDirectory: '.',
platform: 'any',
shell: 'auto',
source: 'manual',
confirmed: true,
safeForAgentSuggestion: false,
},
],
paths: {
applicationRoots: ['apps/web'],
testRoots: ['tests'],
documentationRoots: ['docs'],
generated: ['dist'],
protected: ['runtime/secrets'],
excluded: ['node_modules'],
},
policies: {
preserveBackwardCompatibility: true,
newDependencies: 'justify',
gitWrite: 'none',
migrations: 'plan-only',
documentationRequired: true,
networkAccess: 'forbidden',
productionDataAccess: 'forbidden',
requiredValidationRoles: ['unit-test'],
},
},
}
}
function origin(testInfo: { project: { use: { baseURL?: string } } }) {
return new URL(String(testInfo.project.use.baseURL)).origin
}
async function createRepository(
request: APIRequestContext,
requestOrigin: string,
name: string,
) {
const response = await request.post('/api/v1/repositories', {
headers: { origin: requestOrigin },
data: { displayName: name, initialProfile: profile(name) },
})
expect(response.status()).toBe(201)
return {
body: (await response.json()) as {
repository: { id: string; currentProfileRevision: number | null }
currentProfile: {
revision: number
profile: ReturnType<typeof profile> & {
metadata: { contentDigest: string }
}
}
},
etag: response.headers().etag,
}
}
test.describe('authenticated Milestone 3 repository profiles', () => {
test.skip(!email || !password, 'live local-account credentials are required')
test.use({ storageState: 'test-results/.auth/m2.json' })
test('immutable HTTP lifecycle, conflicts, exports and re-imports are governed', async ({
request,
}, testInfo) => {
test.skip(testInfo.project.name !== 'chromium', 'mutation is covered once')
const requestOrigin = origin(testInfo)
const name = `API evidence ${Date.now()}`
const created = await createRepository(request, requestOrigin, name)
const repositoryId = created.body.repository.id
expect(created.etag).toMatch(/^"profile:1:[0-9a-f]{64}"$/u)
expect(created.body.currentProfile.revision).toBe(1)
const listed = await request.get('/api/v1/repositories?source=manual')
expect(listed.status()).toBe(200)
expect(
(
(await listed.json()) as {
items: { id: string; currentProfileRevision: number | null }[]
}
).items,
).toContainEqual(
expect.objectContaining({ id: repositoryId, currentProfileRevision: 1 }),
)
const current = await request.get(
`/api/v1/repositories/${repositoryId}/profile`,
)
expect(current.status()).toBe(200)
expect(current.headers().etag).toBe(created.etag)
const currentBody = (await current.json()) as {
revision: number
profile: ReturnType<typeof profile>
}
const noOp = await request.put(
`/api/v1/repositories/${repositoryId}/profile`,
{
headers: { origin: requestOrigin, 'if-match': created.etag },
data: currentBody.profile,
},
)
expect(noOp.status()).toBe(200)
expect((await noOp.json()).revision).toBe(1)
const changedProfile = {
...currentBody.profile,
spec: {
...currentBody.profile.spec,
paths: {
...currentBody.profile.spec.paths,
protected: ['runtime/secrets', '.github/workflows'],
},
},
}
const revised = await request.put(
`/api/v1/repositories/${repositoryId}/profile`,
{
headers: { origin: requestOrigin, 'if-match': created.etag },
data: changedProfile,
},
)
expect(revised.status()).toBe(201)
expect((await revised.json()).revision).toBe(2)
const revisedEtag = revised.headers().etag
expect(revisedEtag).toMatch(/^"profile:2:[0-9a-f]{64}"$/u)
const stale = await request.put(
`/api/v1/repositories/${repositoryId}/profile`,
{
headers: { origin: requestOrigin, 'if-match': created.etag },
data: changedProfile,
},
)
expect(stale.status()).toBe(409)
await expect(stale.json()).resolves.toMatchObject({
error: {
code: 'repository_profile_conflict',
details: [
expect.objectContaining({
currentRevision: 2,
currentEtag: revisedEtag,
recovery: 'reload-and-review',
}),
],
},
})
const missingPrecondition = await request.put(
`/api/v1/repositories/${repositoryId}/profile`,
{ headers: { origin: requestOrigin }, data: changedProfile },
)
expect(missingPrecondition.status()).toBe(428)
for (const format of ['json', 'yaml'] as const) {
const exported = await request.get(
`/api/v1/repositories/${repositoryId}/profile/export?format=${format}`,
)
expect(exported.status()).toBe(200)
expect(exported.headers().etag).toBe(revisedEtag)
const reimported = await request.post('/api/v1/repositories', {
headers: {
origin: requestOrigin,
'content-type':
format === 'json' ? 'application/json' : 'application/yaml',
},
data: await exported.body(),
})
expect(reimported.status()).toBe(201)
}
const invalid = {
...changedProfile,
spec: {
...changedProfile.spec,
paths: { ...changedProfile.spec.paths, protected: ['../outside'] },
},
}
const rejected = await request.put(
`/api/v1/repositories/${repositoryId}/profile`,
{
headers: { origin: requestOrigin, 'if-match': revisedEtag },
data: invalid,
},
)
expect(rejected.status()).toBe(422)
expect(await rejected.text()).toContain('/spec/paths/protected/0')
const crossOrigin = await request.post('/api/v1/repositories', {
headers: { origin: 'https://attacker.example' },
data: { displayName: 'Rejected', initialProfile: profile('Rejected') },
})
expect(crossOrigin.status()).toBe(403)
})
test('manual UI creation, immutable editing and composer context remain usable', async ({
page,
}, testInfo) => {
test.skip(testInfo.project.name !== 'chromium', 'mutation is covered once')
const errors: string[] = []
page.on('console', (message) => {
if (message.type() === 'error') errors.push(message.text())
})
page.on('pageerror', (error) => errors.push(error.message))
const name = `Browser evidence ${Date.now()}`
await page.goto('/repositories/new')
await expect(
page.getByRole('heading', {
level: 1,
name: 'Create a repository profile',
}),
).toBeVisible()
await expect(
page.getByText(/Nothing entered here is executed/u),
).toBeVisible()
await page.getByLabel('Display name').fill(name)
await page.getByRole('button', { name: 'Create repository' }).click()
await expect(page).toHaveURL(/\/repositories\/[0-9a-f-]+$/u)
await expect(page.getByRole('heading', { level: 1, name })).toBeVisible()
await expect(page.getByText('Immutable profile revision 1')).toBeVisible()
await expect(
page.getByText(/does not run repository commands/u),
).toBeVisible()
await page.getByRole('link', { name: 'Edit profile' }).click()
await page
.getByLabel('Protected paths')
.fill('runtime/secrets, .github/workflows')
await page.getByRole('button', { name: 'Save immutable revision' }).click()
await expect(page.getByRole('status')).toContainText('Revision 2 saved')
await page.getByRole('link', { name: 'Back to repository' }).click()
await expect(page.getByText('Immutable profile revision 2')).toBeVisible()
await expect(page.getByText('.github/workflows')).toBeVisible()
await page.getByRole('link', { name: 'Create task' }).click()
await expect(page).toHaveURL(/\/composer\/new\?repository=/u)
await expect(page.getByText('Repository context retained')).toBeVisible()
await expect(page.getByText('.github/workflows')).toBeVisible()
expect(errors).toEqual([])
})
test('narrow keyboard, theme and reduced-motion behavior stay operable', async ({
page,
}) => {
const errors: string[] = []
page.on('console', (message) => {
if (message.type() === 'error') errors.push(message.text())
})
page.on('pageerror', (error) => errors.push(error.message))
await page.emulateMedia({ reducedMotion: 'reduce' })
await page.setViewportSize({ width: 390, height: 844 })
await page.goto('/repositories')
await expect(
page.getByRole('heading', { level: 1, name: 'Repositories' }),
).toBeVisible()
expect(
await page.evaluate(
() =>
document.documentElement.scrollWidth <=
document.documentElement.clientWidth,
),
).toBe(true)
expect(
await page.evaluate(
() => matchMedia('(prefers-reduced-motion: reduce)').matches,
),
).toBe(true)
await page.keyboard.press('Control+K')
const palette = page.getByRole('dialog', { name: 'Command palette' })
await expect(palette).toBeVisible()
await palette.getByRole('combobox').fill('repositories')
await page.keyboard.press('Enter')
await expect(page).toHaveURL(/\/repositories$/u)
await page.getByRole('button', { name: 'Use light theme' }).click()
await expect(page.locator('html')).toHaveAttribute('data-theme', 'light')
expect(errors).toEqual([])
})
})
+119
View File
@@ -0,0 +1,119 @@
import { expect, test } from '@playwright/test'
const email = process.env.DEVRUNBOOK_E2E_EMAIL
const password = process.env.DEVRUNBOOK_E2E_PASSWORD
test.describe('authenticated Milestone 2 library', () => {
test.skip(!email || !password, 'live local-account credentials are required')
test.use({ storageState: 'test-results/.auth/m2.json' })
test.beforeEach(async ({ page }) => {
await page.goto('/library')
await expect(page).toHaveURL(/\/library(?:\?.*)?$/u)
})
test('search, filter, view and refresh state remain URL-backed', async ({
page,
}) => {
const errors: string[] = []
page.on('console', (message) => {
if (message.type() === 'error') errors.push(message.text())
})
page.on('pageerror', (error) => errors.push(error.message))
await expect(
page.getByRole('heading', { level: 1, name: 'Library' }),
).toBeVisible()
await expect(page.locator('[data-playbook-slug]')).toHaveCount(28)
await page.getByLabel('Search playbooks').fill('root cause')
await page.getByRole('button', { name: 'Search', exact: true }).click()
await expect(page).toHaveURL(/q=root(?:\+|%20)cause/u)
await expect(
page.locator('[data-playbook-slug="root-cause-bugfix"]'),
).toBeVisible()
await page.getByRole('link', { name: 'Dense' }).click()
await expect(page).toHaveURL(/view=dense/u)
await page.reload()
await expect(page.locator('[data-view="dense"]')).toBeVisible()
await expect(page.getByLabel('Search playbooks')).toHaveValue('root cause')
expect(errors).toEqual([])
})
test('favorites persist and can be recovered through the saved filter', async ({
page,
}, testInfo) => {
test.skip(testInfo.project.name !== 'chromium', 'mutation is covered once')
const card = page.locator('[data-playbook-slug="root-cause-bugfix"]')
const favorite = card.getByRole('button', { name: /favorites/i })
const initiallySaved =
(await favorite.getAttribute('aria-pressed')) === 'true'
if (!initiallySaved) await favorite.click()
await expect(favorite).toHaveAttribute('aria-pressed', 'true')
await page.getByLabel('Saved playbooks only').check()
await page.getByRole('button', { name: 'Apply filters' }).click()
await expect(page).toHaveURL(/favorites=true/u)
await expect(card).toBeVisible()
await card.getByRole('button', { name: /favorites/i }).click()
await expect(
card.getByRole('button', { name: /favorites/i }),
).toHaveAttribute('aria-pressed', 'false')
})
test('detail exposes governance panels and version-bound composer handoff', async ({
page,
}) => {
await page.goto('/library/root-cause-bugfix')
await expect(
page.getByRole('heading', { level: 1, name: /root.?cause/i }),
).toBeVisible()
for (const panel of [
'Intent and outcome',
'Inputs and defaults',
'Repository compatibility',
'Guardrails',
'Workflow',
'Validation requirements',
'Completion criteria',
'Quality and limitations',
'Package files',
'Version history',
]) {
await expect(
page.getByRole('heading', { level: 2, name: panel }),
).toBeVisible()
}
await page.getByRole('link', { name: /Compose with this version/i }).click()
await expect(page).toHaveURL(
/\/composer\/new\?playbook=root-cause-bugfix&version=1\.0\.0/u,
)
await expect(page.getByText('root-cause-bugfix@1.0.0')).toBeVisible()
})
test('command palette, theme and narrow keyboard layout are operable', async ({
page,
}) => {
await page.setViewportSize({ width: 390, height: 844 })
await page.goto('/library')
await page.keyboard.press('Control+K')
await expect(
page.getByRole('dialog', { name: 'Command palette' }),
).toBeVisible()
await page
.getByRole('dialog', { name: 'Command palette' })
.getByRole('combobox')
.fill('root cause')
await page.keyboard.press('Enter')
await expect(page).toHaveURL(/\/library\/root-cause-bugfix$/u)
expect(
await page.evaluate(
() =>
document.documentElement.scrollWidth <=
document.documentElement.clientWidth,
),
).toBe(true)
await page.getByRole('button', { name: 'Use light theme' }).click()
await expect(page.locator('html')).toHaveAttribute('data-theme', 'light')
await page.reload()
await expect(page.locator('html')).toHaveAttribute('data-theme', 'light')
})
})
+172
View File
@@ -0,0 +1,172 @@
import { expect, test } from '@playwright/test'
test('catalog shell renders the verified built-in slice', async ({ page }) => {
await page.goto('/')
await expect(page).toHaveTitle(/DevRunbook/)
await expect(
page.getByRole('heading', {
level: 1,
name: /Turn intent into a verifiable change contract/i,
}),
).toBeVisible()
await expect(page.getByText('28 built-ins · no Gitea required')).toBeVisible()
await expect(page.getByRole('heading', { level: 3 })).toHaveCount(6)
await expect(page.locator('[data-nextjs-dialog]')).toHaveCount(0)
})
test('catalog API requires a local authenticated workspace', async ({
request,
}) => {
const response = await request.get('/api/v1/playbooks')
expect(response.status()).toBe(401)
await expect(response.json()).resolves.toMatchObject({
error: { code: 'authentication_required' },
})
})
test('legacy playbook links preserve the authentication boundary', async ({
page,
}) => {
await page.goto('/playbooks/root-cause-bugfix')
await expect(
page.getByRole('heading', {
level: 1,
name: /continue with your local account/i,
}),
).toBeVisible()
expect(new URL(page.url()).pathname).toBe('/login')
})
test('narrow layout has no horizontal overflow and keyboard focus is visible', async ({
page,
}) => {
await page.setViewportSize({ width: 390, height: 844 })
await page.goto('/')
expect(
await page.evaluate(
() =>
document.documentElement.scrollWidth <=
document.documentElement.clientWidth,
),
).toBe(true)
await page.keyboard.press('Tab')
await expect(page.getByRole('link', { name: /DevRunbook/i })).toBeFocused()
const outline = await page.evaluate(
() => getComputedStyle(document.activeElement as Element).outlineStyle,
)
expect(outline).not.toBe('none')
})
test('security headers are present on application pages', async ({
request,
}) => {
const response = await request.get('/')
expect(response.headers()['x-content-type-options']).toBe('nosniff')
expect(response.headers()['x-frame-options']).toBe('DENY')
expect(response.headers()['content-security-policy']).toContain(
"frame-ancestors 'none'",
)
})
test('tokenless bootstrap is denied through an untrusted proxy boundary', async ({
request,
}) => {
const response = await request.post('/api/v1/instance/setup', {
headers: { 'x-forwarded-for': '127.0.0.1' },
data: {
bootstrapToken: '',
instanceName: 'Test instance',
publicBaseUrl: 'http://127.0.0.1:3000',
owner: {
email: 'owner@example.test',
displayName: 'Owner',
password: 'not-a-real-password',
},
},
})
expect(response.status()).toBe(403)
expect(await response.text()).not.toContain('not-a-real-password')
})
test('setup UI exposes the database recovery state without enabling credentials', async ({
page,
}) => {
test.skip(
Boolean(process.env.DEVRUNBOOK_E2E_EMAIL),
'recovery state requires an uninitialized validation database',
)
const consoleErrors: string[] = []
const pageErrors: string[] = []
page.on('console', (message) => {
if (message.type() === 'error') consoleErrors.push(message.text())
})
page.on('pageerror', (error) => pageErrors.push(error.message))
await page.goto('/setup')
await expect(
page.getByRole('heading', { level: 1, name: 'Make this instance yours.' }),
).toBeVisible()
await expect(page.getByText('recovery required')).toBeVisible({
timeout: 20_000,
})
await expect(page.getByLabel('Owner email')).toBeDisabled()
await expect(page.getByText(/Gitea is optional/i)).toBeVisible()
expect(consoleErrors).toEqual([])
expect(pageErrors).toEqual([])
})
test('local sign-in fails generically and clears credentials when auth is unavailable', async ({
page,
}) => {
const pageErrors: string[] = []
page.on('pageerror', (error) => pageErrors.push(error.message))
await page.goto('/login?returnTo=https%3A%2F%2Fattacker.example%2Fsteal')
await expect(
page.getByRole('heading', {
level: 1,
name: /continue with your local account/i,
}),
).toBeVisible()
await page.getByLabel('Email address').fill('owner@example.test')
const password = page.getByLabel('Password', { exact: true })
await password.fill('not-a-real-password')
await page.getByRole('button', { name: 'Sign in' }).click()
await expect(page.locator('form').getByRole('alert')).toHaveText(
'Sign-in could not be completed. Check your credentials or try again later.',
)
await expect(password).toHaveValue('')
expect(new URL(page.url()).pathname).toBe('/login')
expect(pageErrors).toEqual([])
})
test('password recovery keeps its token out of HTTP and clears both credentials', async ({
page,
}) => {
const token = 'a'.repeat(43)
const requestedUrls: string[] = []
const pageErrors: string[] = []
page.on('request', (request) => requestedUrls.push(request.url()))
page.on('pageerror', (error) => pageErrors.push(error.message))
await page.goto(`/reset-password#token=${token}`)
await expect(
page.getByRole('heading', { level: 1, name: /restore access/i }),
).toBeVisible()
await expect(page).toHaveURL(/\/reset-password$/u)
expect(requestedUrls.every((url) => !url.includes(token))).toBe(true)
const password = page.getByLabel('New password', { exact: true })
const confirmation = page.getByLabel('Confirm new password')
await password.fill('a secure replacement password')
await confirmation.fill('a secure replacement password')
await page.getByRole('button', { name: 'Update password' }).click()
await expect(page.locator('form').getByRole('alert')).toContainText(
'invalid, expired, or already used',
)
await expect(password).toHaveValue('')
await expect(confirmation).toHaveValue('')
expect(pageErrors).toEqual([])
})
@@ -0,0 +1,132 @@
import AxeBuilder from '@axe-core/playwright'
import { expect, test } from '@playwright/test'
const email = process.env.DEVRUNBOOK_E2E_EMAIL
const password = process.env.DEVRUNBOOK_E2E_PASSWORD
const criticalRoutes = [
'/start',
'/composer/new',
'/repositories',
'/runs',
'/account',
'/account/security',
'/repositories/new',
'/management',
] as const
test.describe('Phase 14 accessibility regression', () => {
test.skip(!email || !password, 'live local-account credentials are required')
test.use({
storageState: 'test-results/.auth/m2.json',
reducedMotion: 'reduce',
})
for (const route of criticalRoutes) {
test(`${route} has one main and no serious accessibility violations`, async ({
page,
}) => {
await page.goto(route)
await expect(page.locator('main')).toHaveCount(1)
await expect(page.locator('[id]')).toHaveCount(
await page
.locator('[id]')
.evaluateAll(
(elements) => new Set(elements.map((element) => element.id)).size,
),
)
const results = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'])
.analyze()
expect(
results.violations.filter(
({ impact }) => impact === 'critical' || impact === 'serious',
),
).toEqual([])
})
}
test('Start remains keyboard complete, reflows and exposes touch targets', async ({
page,
}) => {
await page.setViewportSize({ width: 390, height: 844 })
await page.goto('/start')
await page.keyboard.press('Tab')
await expect(page.locator(':focus-visible')).toHaveCount(1)
expect(
await page.evaluate(
() =>
document.documentElement.scrollWidth <=
document.documentElement.clientWidth,
),
).toBe(true)
const undersizedPrimaryTargets = await page
.locator('main button, main a[href="/settings/integrations/gitea/new"]')
.evaluateAll((elements) =>
elements
.filter((element) => {
const box = element.getBoundingClientRect()
const style = getComputedStyle(element)
return (
style.display !== 'none' &&
style.visibility !== 'hidden' &&
box.width > 0 &&
box.height > 0 &&
(box.width < 44 || box.height < 44)
)
})
.map(
(element) =>
element.getAttribute('aria-label') ?? element.textContent,
),
)
expect(undersizedPrimaryTargets).toEqual([])
})
test('critical pages reflow at a 200 percent equivalent CSS viewport', async ({
page,
}) => {
await page.setViewportSize({ width: 640, height: 720 })
for (const route of criticalRoutes) {
await page.goto(route)
expect(
await page.evaluate(
() =>
document.documentElement.scrollWidth <=
document.documentElement.clientWidth,
),
).toBe(true)
}
})
for (const locale of ['en', 'nl'] as const) {
for (const mode of ['simple', 'expert'] as const) {
test(`${locale}/${mode} presentation stays accessible`, async ({
page,
context,
baseURL,
}) => {
const origin = new URL(baseURL ?? 'http://127.0.0.1:3000')
await context.addCookies([
{ name: 'devrunbook_locale', value: locale, url: origin.origin },
{ name: 'devrunbook_mode', value: mode, url: origin.origin },
])
await page.goto('/start')
await expect(
page.getByRole('heading', {
name:
locale === 'nl' ? 'Wat wil je doen?' : 'What do you want to do?',
}),
).toBeVisible()
const results = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'])
.analyze()
expect(
results.violations.filter(
({ impact }) => impact === 'critical' || impact === 'serious',
),
).toEqual([])
})
}
}
})
+102
View File
@@ -0,0 +1,102 @@
import { expect, test } from '@playwright/test'
const hasCredentials = Boolean(
process.env.DEVRUNBOOK_E2E_EMAIL && process.env.DEVRUNBOOK_E2E_PASSWORD,
)
test.describe('Usability recovery', () => {
test.skip(!hasCredentials, 'live local-account credentials are required')
test.use({ storageState: 'test-results/.auth/m2.json' })
test.beforeEach(async ({ context, baseURL }) => {
if (!baseURL) throw new Error('A browser base URL is required')
await context.addCookies([
{
name: 'devrunbook_locale',
value: 'nl',
url: baseURL,
sameSite: 'Lax',
},
])
})
test('Task library is compact and understandable on mobile', async ({
page,
}) => {
await page.setViewportSize({ width: 390, height: 844 })
await page.goto('/library')
await expect(page).toHaveTitle('Taakbibliotheek · DevRunbook')
await expect(
page.getByRole('heading', { level: 1, name: 'Kies een bewezen taak' }),
).toBeVisible()
await expect(
page.locator('.drb-library-filters details'),
).not.toHaveAttribute('open', '')
await expect(page.locator('.drb-playbook-card')).toHaveCount(8)
await expect(page.getByRole('button', { name: /Meer tonen/ })).toBeVisible()
expect(
await page.evaluate(
() =>
document.documentElement.scrollWidth <=
document.documentElement.clientWidth,
),
).toBe(true)
const undersizedNavigationTargets = await page
.locator(
'.drb-app-shell__header button, .drb-app-shell__header a, .drb-app-shell__mobile-tabs a',
)
.evaluateAll((elements) =>
elements
.filter((element) => {
const box = element.getBoundingClientRect()
const style = getComputedStyle(element)
return (
style.display !== 'none' &&
style.visibility !== 'hidden' &&
box.width > 0 &&
box.height > 0 &&
(box.width < 44 || box.height < 44)
)
})
.map((element) => element.textContent?.trim()),
)
expect(undersizedNavigationTargets).toEqual([])
})
test('Dutch shell, account and role context use ordinary language', async ({
page,
}) => {
await page.goto('/account')
await expect(
page.getByRole('link', { name: 'DevRunbook startpagina' }),
).toBeVisible()
await expect(page.getByLabel('Actieve werkruimte')).toBeVisible()
await expect(
page.getByRole('heading', { level: 1, name: 'Jouw account' }),
).toBeVisible()
await expect(page.getByText('Rol in deze werkruimte')).toBeVisible()
await page.goto('/account/security')
await expect(
page.getByRole('heading', { level: 1, name: 'Wachtwoord en sessies' }),
).toBeVisible()
await expect(
page.getByRole('link', { name: 'Wachtwoord veilig herstellen' }),
).toBeVisible()
})
test('Start action does not cover mobile content', async ({ page }) => {
await page.setViewportSize({ width: 390, height: 844 })
await page.goto('/start')
await expect(page).toHaveTitle('Nieuwe taak · DevRunbook')
expect(
await page.locator('main').evaluate((main) => {
const stickyElements = [...main.querySelectorAll('*')].filter(
(element) => getComputedStyle(element).position === 'sticky',
)
return stickyElements.length
}),
).toBe(0)
})
})
@@ -0,0 +1,160 @@
import { randomUUID } from 'node:crypto'
import { mkdtemp, rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import path from 'node:path'
import {
createGeneratedArtifact,
downloadGeneratedArtifact,
} from '../../packages/application/src/index'
import { LocalArtifactStorage } from '../../packages/artifacts/src/index'
import {
closeDatabase,
DrizzleGeneratedArtifactStore,
DrizzleWorkspaceAuthorizationLookup,
getSqlClient,
} from '../../packages/db/src/index'
import { afterAll, describe, expect, it } from 'vitest'
const databaseUrl = process.env.DATABASE_URL
const integration = databaseUrl ? describe : describe.skip
const cleanupRoots: string[] = []
afterAll(async () => {
await closeDatabase()
await Promise.all(
cleanupRoots.splice(0).map((root) => rm(root, { recursive: true })),
)
})
integration('generated artifact persistence', () => {
it('persists authorized metadata and restart-readable verified bytes', async () => {
const sql = getSqlClient(databaseUrl)
const userId = randomUUID()
const workspaceId = randomUUID()
const otherWorkspaceId = randomUUID()
const playbookId = randomUUID()
const versionId = randomUUID()
const runId = randomUUID()
const artifactId = randomUUID()
const namespace = `artifact-integration-${randomUUID()}`
const artifactRoot = await mkdtemp(
path.join(tmpdir(), 'devrunbook-artifact-integration-'),
)
cleanupRoots.push(artifactRoot)
await sql.begin(async (transaction) => {
await transaction`
insert into users (id, email, display_name, password_hash, instance_role)
values (${userId}, ${`${userId}@example.invalid`}, 'Artifact Integration', 'not-a-login-credential', 'user')
`
await transaction`
insert into workspaces (id, name)
values (${workspaceId}, 'Artifact Workspace'), (${otherWorkspaceId}, 'Other Workspace')
`
await transaction`
insert into workspace_memberships (user_id, workspace_id, role)
values (${userId}, ${workspaceId}, 'editor')
`
await transaction`
insert into playbooks (id, workspace_id, logical_id, slug, namespace, source_type)
values (${playbookId}, ${workspaceId}, 'artifact-integration', 'artifact-integration', ${namespace}, 'private')
`
await transaction`
insert into playbook_versions (
id, playbook_id, semantic_version, lifecycle, package_api_version,
title, summary, category, risk_tier, package_json, template_text,
content_digest, created_by
) values (
${versionId}, ${playbookId}, '1.0.0', 'validated',
'devrunbook.playbook/v1.2', 'Artifact integration',
'Integration fixture', 'test', 'low', '{}'::jsonb, '# Test',
${'a'.repeat(64)}, ${userId}
)
`
await transaction`
insert into generated_runs (
id, workspace_id, playbook_version_id, playbook_snapshot_json,
repository_profile_snapshot_json, normalized_input_json,
policy_snapshot_json, provenance_json, lint_result_json,
rendered_prompt, render_digest, idempotency_key, generated_by
) values (
${runId}, ${workspaceId}, ${versionId}, '{}'::jsonb, null,
'{}'::jsonb, '{}'::jsonb, '[]'::jsonb,
'{"exportReadiness":"ready","findings":[]}'::jsonb,
'# Test', ${'b'.repeat(64)}, ${`artifact-${artifactId}`}, ${userId}
)
`
})
const metadata = new DrizzleGeneratedArtifactStore()
const authorization = new DrizzleWorkspaceAuthorizationLookup()
const content = new TextEncoder().encode('# Test\n')
const created = await createGeneratedArtifact(
{
authorization,
metadata,
storage: new LocalArtifactStorage(artifactRoot),
now: () => new Date('2026-07-27T12:00:00.000Z'),
},
{
actor: { userId },
workspaceId,
artifactId,
runId,
artifactType: 'markdown',
filename: 'test.md',
mediaType: 'text/markdown; charset=utf-8',
content,
},
)
expect(created.created).toBe(true)
const [persisted] = await sql<
{ id: string; runId: string; sha256: string; sizeBytes: string }[]
>`
select id, run_id as "runId", sha256, size_bytes as "sizeBytes"
from generated_artifacts
where id = ${artifactId}
`
expect(persisted).toMatchObject({
id: artifactId,
runId,
sha256: created.artifact.sha256,
sizeBytes: String(content.byteLength),
})
const downloaded = await downloadGeneratedArtifact(
{
authorization,
metadata: new DrizzleGeneratedArtifactStore(),
storage: new LocalArtifactStorage(artifactRoot),
},
{ actor: { userId }, workspaceId, artifactId },
)
expect(downloaded.content).toEqual(content)
expect(downloaded.artifact.sha256).toHaveLength(64)
await expect(
metadata.listByRunInWorkspace(runId, workspaceId),
).resolves.toMatchObject([
{ id: artifactId, runId, workspaceId, sha256: created.artifact.sha256 },
])
await expect(
metadata.listByRunInWorkspace(runId, otherWorkspaceId),
).resolves.toEqual([])
await expect(
downloadGeneratedArtifact(
{
authorization,
metadata,
storage: new LocalArtifactStorage(artifactRoot),
},
{ actor: { userId }, workspaceId: otherWorkspaceId, artifactId },
),
).rejects.toMatchObject({ code: 'workspace_access_denied' })
await sql`delete from workspaces where id in (${workspaceId}, ${otherWorkspaceId})`
await sql`delete from users where id = ${userId}`
})
})
@@ -0,0 +1,574 @@
import { randomUUID } from 'node:crypto'
import { readFile } from 'node:fs/promises'
import path from 'node:path'
import {
authorizeWorkspaceAction,
completeFirstRun,
composeAndCreateGeneratedRun,
createGeneratedArtifact,
downloadGeneratedArtifact,
getGeneratedRun,
type ImmutableJsonObject,
} from '../../packages/application/src/index'
import { LocalArtifactStorage } from '../../packages/artifacts/src/index'
import type {
CanonicalPromptRequest,
PlaybookMetadata,
PlaybookSpecification,
RepositoryProfile,
} from '../../packages/composer/src/index'
import {
canonicalJson,
loadBuiltInPlaybookRecords,
sha256,
} from '../../packages/content/src/index'
import {
closeDatabase,
DrizzleFirstRunStore,
DrizzleFirstRunTransactionRunner,
DrizzleGeneratedArtifactStore,
DrizzleGeneratedRunStore,
DrizzlePlaybookCatalog,
DrizzleWorkspaceAuthorizationLookup,
getPersistedInstanceStatus,
getSqlClient,
} from '../../packages/db/src/index'
import { afterAll, describe, expect, it } from 'vitest'
import { parse } from 'yaml'
const exampleProfile: RepositoryProfile = {
metadata: { name: 'Example TypeScript Service', revision: 1 },
spec: {
repositoryType: 'single-app',
stack: {
languages: ['TypeScript'],
frameworks: ['Next.js'],
packageManagers: ['pnpm'],
databases: ['PostgreSQL'],
deploymentTypes: ['Docker Compose'],
},
commands: [
['lint', 'pnpm lint'],
['typecheck', 'pnpm typecheck'],
['unit-test', 'pnpm test'],
['build', 'pnpm build'],
].map(([role, command]) => ({
role: role!,
command: command!,
workingDirectory: '.',
})),
paths: {
applicationRoots: ['apps/web', 'packages'],
testRoots: ['tests', 'apps/web/tests'],
documentationRoots: ['docs'],
protected: ['data', 'backups', '.env'],
excluded: ['node_modules', '.git'],
},
policies: {
preserveBackwardCompatibility: true,
newDependencies: 'justify',
gitWrite: 'none',
migrations: 'reversible-only',
productionDataAccess: 'forbidden',
},
},
}
async function loadGovernedExampleProfile(): Promise<ImmutableJsonObject> {
return parse(
await readFile(
path.resolve('examples/repository-profiles/example-profile.yaml'),
'utf8',
),
) as ImmutableJsonObject
}
let ownerId = ''
let primaryWorkspaceId = ''
let generatedRunId = ''
let generatedArtifactId = ''
afterAll(async () => closeDatabase())
const databaseIntegration = process.env.DATABASE_URL ? describe : describe.skip
databaseIntegration('Milestone 0 service-backed vertical slice', () => {
it('atomically initializes, imports, composes, persists and protects the golden task', async () => {
await expect(getPersistedInstanceStatus()).resolves.toMatchObject({
state: 'uninitialized',
setupRequired: true,
})
const records = await loadBuiltInPlaybookRecords()
const setup = await completeFirstRun(new DrizzleFirstRunStore(records), {
instanceName: 'Integration instance',
publicBaseUrl: 'http://127.0.0.1:3000',
owner: {
email: 'owner@integration.test',
displayName: 'Integration owner',
passwordHash: 'better-auth-prehashed-integration-fixture',
},
configuration: { instanceName: 'Integration instance' },
configurationDigest: sha256(
canonicalJson({ instanceName: 'Integration instance' }),
),
})
ownerId = setup.ownerId
primaryWorkspaceId = setup.workspaceId
const sql = getSqlClient()
const [catalogCount] = await sql<{ count: number }[]>`
select count(*)::int as count from playbook_versions
`
expect(catalogCount?.count).toBe(28)
await expect(getPersistedInstanceStatus()).resolves.toMatchObject({
state: 'ready',
setupRequired: false,
})
const rootCause = records.find(
(record) => record.slug === 'root-cause-bugfix',
)
expect(rootCause).toBeDefined()
const [version] = await sql<{ id: string }[]>`
select pv.id
from playbook_versions pv
join playbooks p on p.id = pv.playbook_id
where p.slug = 'root-cause-bugfix' and pv.semantic_version = '1.0.0'
`
expect(version?.id).toBeTruthy()
const manifest = rootCause!.packageJson as unknown as {
metadata: PlaybookMetadata
spec: PlaybookSpecification
}
const inputs = {
problemStatement: 'Example value for Problem statement',
reproductionClues: '',
preserveCompatibility: true,
affectedScope: [],
}
const prompt: CanonicalPromptRequest = {
metadata: manifest.metadata,
specification: manifest.spec,
template: rootCause!.templateText,
inputs,
workMode: 'guided',
autonomyLevel: 'verify',
repositoryProfile: exampleProfile,
}
const request = {
prompt,
snapshots: {
playbook: rootCause!.packageJson as unknown as ImmutableJsonObject,
repositoryProfile: await loadGovernedExampleProfile(),
normalizedInput: inputs,
policy: { autonomyLevel: 'verify', conditionsResolved: true },
provenance: [],
},
lint: { exportReadiness: 'ready' as const, findings: [] },
generatedBy: setup.ownerId,
workspaceId: setup.workspaceId,
playbookVersionId: version!.id,
idempotencyKey: 'milestone-zero-root-cause-golden',
}
const dependencies = {
store: new DrizzleGeneratedRunStore(),
nextId: randomUUID,
now: () => new Date(),
workspaceAuthorization: new DrizzleWorkspaceAuthorizationLookup(),
}
const created = await composeAndCreateGeneratedRun(dependencies, request)
const golden = await readFile(
path.resolve('examples/rendered-prompts/root-cause-bugfix.md'),
'utf8',
)
expect(created.created).toBe(true)
expect(created.run.renderedPrompt).toBe(golden)
expect(created.run.renderDigest).toBe(sha256(golden))
generatedRunId = created.run.id
generatedArtifactId = randomUUID()
const artifactRoot = process.env.ARTIFACT_ROOT
if (!artifactRoot)
throw new Error('ARTIFACT_ROOT is required for integration tests')
const artifactBytes = new TextEncoder().encode(golden)
const artifact = await createGeneratedArtifact(
{
authorization: new DrizzleWorkspaceAuthorizationLookup(),
metadata: new DrizzleGeneratedArtifactStore(),
storage: new LocalArtifactStorage(artifactRoot),
now: () => new Date(),
},
{
actor: { userId: ownerId },
workspaceId: primaryWorkspaceId,
artifactId: generatedArtifactId,
runId: created.run.id,
artifactType: 'prompt_text',
filename: 'root-cause-bugfix.md',
mediaType: 'text/markdown; charset=utf-8',
content: artifactBytes,
},
)
expect(artifact.created).toBe(true)
expect(artifact.artifact.sha256).toBe(sha256(golden))
const retried = await composeAndCreateGeneratedRun(dependencies, request)
expect(retried.created).toBe(false)
expect(retried.run.id).toBe(created.run.id)
await expect(
sql`update generated_runs set rendered_prompt = 'mutated' where id = ${created.run.id}`,
).rejects.toThrow()
await expect(
sql`update playbook_versions set title = 'mutated' where id = ${version!.id}`,
).rejects.toThrow(/playbook_versions is immutable/u)
const duplicateRunner = new DrizzleFirstRunTransactionRunner()
await expect(
duplicateRunner.run(records, (transaction) =>
transaction.importBuiltInPlaybooks(),
),
).resolves.toEqual({ imported: 28 })
const catalogBeforeReconnect =
await new DrizzlePlaybookCatalog().listBuiltIns()
expect(catalogBeforeReconnect).toHaveLength(28)
expect(
catalogBeforeReconnect.map(({ slug, version, digest }) => ({
slug,
version,
digest,
})),
).toEqual(
records.map((record) => ({
slug: record.slug,
version: record.semanticVersion,
digest: record.contentDigest,
})),
)
const searchableCatalog = new DrizzlePlaybookCatalog()
const searchMatches = await searchableCatalog.list({
q: 'root cause defect',
category: ['bugfixing'],
riskTier: ['moderate'],
lifecycle: ['reviewed'],
source: ['built_in'],
})
expect(searchMatches.map(({ slug }) => slug)).toEqual(['root-cause-bugfix'])
const rootCauseDetail = await searchableCatalog.findBySlug(
'root-cause-bugfix',
'built_in',
)
expect(rootCauseDetail?.current).toMatchObject({
version: '1.0.0',
digest: rootCause!.contentDigest,
manifest: { metadata: { slug: 'root-cause-bugfix' } },
})
expect(rootCauseDetail?.versions).toHaveLength(1)
await expect(
searchableCatalog.findVersionBySlug(
'root-cause-bugfix',
'1.0.0',
'built_in',
),
).resolves.toMatchObject({
template: rootCause!.templateText,
quality: rootCause!.packageJson.quality,
})
const conflictingRecords = records.map((record) =>
record.slug === 'root-cause-bugfix'
? { ...record, contentDigest: '0'.repeat(64) }
: record,
)
await expect(
duplicateRunner.run(conflictingRecords, (transaction) =>
transaction.importBuiltInPlaybooks(),
),
).rejects.toMatchObject({ code: 'playbook_version_conflict' })
await closeDatabase()
await expect(new DrizzlePlaybookCatalog().listBuiltIns()).resolves.toEqual(
catalogBeforeReconnect,
)
const restoredArtifact = await downloadGeneratedArtifact(
{
authorization: new DrizzleWorkspaceAuthorizationLookup(),
metadata: new DrizzleGeneratedArtifactStore(),
storage: new LocalArtifactStorage(artifactRoot),
},
{
actor: { userId: ownerId },
workspaceId: primaryWorkspaceId,
artifactId: generatedArtifactId,
},
)
expect(Buffer.from(restoredArtifact.content).toString('utf8')).toBe(golden)
expect(restoredArtifact.artifact.sha256).toBe(sha256(golden))
}, 30_000)
it('persists role boundaries without cross-workspace or instance-admin bypass', async () => {
expect(ownerId).toBeTruthy()
expect(primaryWorkspaceId).toBeTruthy()
const sql = getSqlClient()
const viewerId = randomUUID()
const editorId = randomUUID()
const administratorId = randomUUID()
const isolatedWorkspaceId = randomUUID()
await sql`
insert into users (
id, email, display_name, password_hash, email_verified,
instance_role, status
) values
(${viewerId}, 'viewer@integration.test', 'Integration viewer', 'not-a-login-credential', true, 'user', 'active'),
(${editorId}, 'editor@integration.test', 'Integration editor', 'not-a-login-credential', true, 'user', 'active'),
(${administratorId}, 'admin@integration.test', 'Integration administrator', 'not-a-login-credential', true, 'instance_admin', 'active')
`
await sql`
insert into workspaces (id, name, type)
values (${isolatedWorkspaceId}, 'Isolated integration workspace', 'team')
`
await sql`
insert into workspace_memberships (workspace_id, user_id, role)
values
(${primaryWorkspaceId}, ${viewerId}, 'viewer'),
(${primaryWorkspaceId}, ${editorId}, 'editor')
`
const lookup = new DrizzleWorkspaceAuthorizationLookup()
await expect(
authorizeWorkspaceAction(lookup, {
actor: { userId: viewerId },
workspaceId: primaryWorkspaceId,
action: 'read',
}),
).resolves.toMatchObject({ workspaceRole: 'viewer' })
await expect(
authorizeWorkspaceAction(lookup, {
actor: { userId: viewerId },
workspaceId: primaryWorkspaceId,
action: 'write',
}),
).rejects.toMatchObject({ code: 'workspace_access_denied' })
const reader = new DrizzleGeneratedRunStore()
await expect(
getGeneratedRun(
{ reader, workspaceAuthorization: lookup },
{
actor: { userId: viewerId },
workspaceId: primaryWorkspaceId,
runId: generatedRunId,
},
),
).resolves.toMatchObject({ id: generatedRunId })
await expect(
getGeneratedRun(
{ reader, workspaceAuthorization: lookup },
{
actor: { userId: viewerId },
workspaceId: isolatedWorkspaceId,
runId: generatedRunId,
},
),
).rejects.toMatchObject({ code: 'workspace_access_denied' })
const artifactRoot = process.env.ARTIFACT_ROOT
if (!artifactRoot)
throw new Error('ARTIFACT_ROOT is required for integration tests')
await expect(
downloadGeneratedArtifact(
{
authorization: lookup,
metadata: new DrizzleGeneratedArtifactStore(),
storage: new LocalArtifactStorage(artifactRoot),
},
{
actor: { userId: viewerId },
workspaceId: isolatedWorkspaceId,
artifactId: generatedArtifactId,
},
),
).rejects.toMatchObject({ code: 'workspace_access_denied' })
await expect(
authorizeWorkspaceAction(lookup, {
actor: { userId: editorId },
workspaceId: primaryWorkspaceId,
action: 'write',
}),
).resolves.toMatchObject({ workspaceRole: 'editor' })
await expect(
authorizeWorkspaceAction(lookup, {
actor: { userId: editorId },
workspaceId: primaryWorkspaceId,
action: 'destructive',
}),
).rejects.toMatchObject({ code: 'workspace_access_denied' })
await expect(
authorizeWorkspaceAction(lookup, {
actor: { userId: ownerId },
workspaceId: primaryWorkspaceId,
action: 'destructive',
}),
).resolves.toMatchObject({ workspaceRole: 'owner' })
await expect(
authorizeWorkspaceAction(lookup, {
actor: { userId: viewerId },
workspaceId: isolatedWorkspaceId,
action: 'read',
}),
).rejects.toMatchObject({ code: 'workspace_access_denied' })
await expect(
authorizeWorkspaceAction(lookup, {
actor: { userId: administratorId },
workspaceId: primaryWorkspaceId,
action: 'read',
}),
).rejects.toMatchObject({ code: 'workspace_access_denied' })
})
it('persists the four representative golden compositions and their digests', async () => {
const slugs = [
'repository-health-audit',
'root-cause-bugfix',
'feature-from-spec',
'production-readiness-audit',
] as const
const records = await loadBuiltInPlaybookRecords()
const sql = getSqlClient()
const store = new DrizzleGeneratedRunStore()
const governedExampleProfile = await loadGovernedExampleProfile()
for (const slug of slugs) {
const record = records.find((candidate) => candidate.slug === slug)
expect(record, slug).toBeDefined()
const manifest = record!.packageJson as unknown as {
metadata: PlaybookMetadata
spec: PlaybookSpecification & {
compatibility?: { repositoryRequired?: boolean }
}
}
const example = parse(
await readFile(
path.resolve('content/playbooks', slug, 'examples/minimal.yaml'),
'utf8',
),
) as {
workMode: string
autonomyLevel: CanonicalPromptRequest['autonomyLevel']
inputs?: CanonicalPromptRequest['inputs'] & ImmutableJsonObject
repositoryProfile?: string
}
const [version] = await sql<{ id: string }[]>`
select pv.id
from playbook_versions pv
join playbooks p on p.id = pv.playbook_id
where p.slug = ${slug} and pv.semantic_version = ${record!.semanticVersion}
`
expect(version?.id, slug).toBeTruthy()
const inputs = example.inputs ?? {}
const selectedProfile =
example.repositoryProfile ||
manifest.spec.compatibility?.repositoryRequired
? exampleProfile
: null
const requiredInput = manifest.spec.inputs?.find(
(definition) => definition.required,
)
expect(requiredInput, `${slug} required input`).toBeDefined()
const invalidInputs = { ...inputs }
delete invalidInputs[requiredInput!.key]
await expect(
composeAndCreateGeneratedRun(
{
store,
nextId: randomUUID,
now: () => new Date(),
workspaceAuthorization: new DrizzleWorkspaceAuthorizationLookup(),
},
{
prompt: {
metadata: manifest.metadata,
specification: manifest.spec,
template: record!.templateText,
inputs: invalidInputs,
workMode: example.workMode,
autonomyLevel: example.autonomyLevel,
repositoryProfile: selectedProfile,
},
snapshots: {
playbook: record!.packageJson as unknown as ImmutableJsonObject,
repositoryProfile: selectedProfile
? governedExampleProfile
: null,
normalizedInput: invalidInputs,
policy: {
autonomyLevel: example.autonomyLevel,
conditionsResolved: true,
},
provenance: [],
},
lint: { exportReadiness: 'ready', findings: [] },
generatedBy: ownerId,
workspaceId: primaryWorkspaceId,
playbookVersionId: version!.id,
idempotencyKey: `milestone-zero-invalid-${slug}`,
},
),
).rejects.toMatchObject({ code: 'composition_input_invalid' })
const golden = await readFile(
path.resolve('examples/rendered-prompts', `${slug}.md`),
'utf8',
)
const result = await composeAndCreateGeneratedRun(
{
store,
nextId: randomUUID,
now: () => new Date(),
workspaceAuthorization: new DrizzleWorkspaceAuthorizationLookup(),
},
{
prompt: {
metadata: manifest.metadata,
specification: manifest.spec,
template: record!.templateText,
inputs,
workMode: example.workMode,
autonomyLevel: example.autonomyLevel,
repositoryProfile: selectedProfile,
},
snapshots: {
playbook: record!.packageJson as unknown as ImmutableJsonObject,
repositoryProfile: selectedProfile ? governedExampleProfile : null,
normalizedInput: inputs,
policy: {
autonomyLevel: example.autonomyLevel,
conditionsResolved: true,
},
provenance: [],
},
lint: { exportReadiness: 'ready', findings: [] },
generatedBy: ownerId,
workspaceId: primaryWorkspaceId,
playbookVersionId: version!.id,
idempotencyKey: `milestone-zero-representative-${slug}`,
},
)
expect(result.run.renderedPrompt, slug).toBe(golden)
expect(result.run.renderDigest, slug).toBe(sha256(golden))
const [persisted] = await sql<
{ renderedPrompt: string; renderDigest: string }[]
>`
select rendered_prompt as "renderedPrompt", render_digest as "renderDigest"
from generated_runs
where id = ${result.run.id} and workspace_id = ${primaryWorkspaceId}
`
expect(persisted, slug).toEqual({
renderedPrompt: golden,
renderDigest: sha256(golden),
})
}
}, 30_000)
})
@@ -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([])
})
})
@@ -0,0 +1,253 @@
import { cp, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import path from 'node:path'
import type {
AuthPersistence,
AuthSessionRecord,
AuthUserRecord,
CreateAuthSessionRecord,
} from '../../packages/application/src/auth/auth-service'
import { AuthService } from '../../packages/application/src/auth/auth-service'
import { TokenDigester } from '../../packages/application/src/auth/token-digest'
import { loadPlaybookPackage } from '../../packages/content/src/loader'
import { createLogger } from '../../packages/observability/src/index'
import { handleAuthRequest } from '../../apps/web/src/auth/csrf'
import {
assertNonSecretConfiguration,
authorizeBootstrap,
} from '../../apps/web/src/setup/setup-policy'
import { afterEach, describe, expect, it, vi } from 'vitest'
const temporaryDirectories: string[] = []
afterEach(async () => {
await Promise.all(
temporaryDirectories.splice(0).map((directory) =>
rm(directory, {
recursive: true,
force: true,
}),
),
)
vi.restoreAllMocks()
})
async function maliciousPackage(): Promise<string> {
const directory = await mkdtemp(
path.join(tmpdir(), 'devrunbook-security-content-'),
)
temporaryDirectories.push(directory)
await cp(path.resolve('content/playbooks/root-cause-bugfix'), directory, {
recursive: true,
})
const manifestPath = path.join(directory, 'playbook.yaml')
const manifest = (await readFile(manifestPath, 'utf8')).replace(
' sensitive: false\n includeInOutput: true',
' sensitive: true\n includeInOutput: true',
)
await writeFile(manifestPath, manifest)
return directory
}
class CapturingAuthPersistence implements AuthPersistence {
readonly user: AuthUserRecord = {
id: 'user-1',
email: 'owner@example.test',
displayName: 'Owner',
passwordHash: '[password-hash]',
emailVerified: true,
image: null,
status: 'active',
createdAt: new Date('2026-07-27T00:00:00.000Z'),
updatedAt: new Date('2026-07-27T00:00:00.000Z'),
}
sessionInput: CreateAuthSessionRecord | undefined
async findUserById(id: string) {
return id === this.user.id ? this.user : null
}
async findUserByEmail(email: string) {
return email === this.user.email ? this.user : null
}
async updateUser() {
return this.user
}
async createSession(input: CreateAuthSessionRecord) {
this.sessionInput = input
return {
id: 'session-1',
revokedAt: null,
...input,
}
}
async findSessionByTokenHash(): Promise<AuthSessionRecord | null> {
return null
}
async touchSession(): Promise<AuthSessionRecord | null> {
return null
}
async revokeSessionByTokenHash() {
return false
}
async revokeSessionsForUser() {
return 0
}
}
describe('production security boundaries', () => {
it('uses the database workspace migration runner copied into the image', async () => {
const dockerfile = await readFile(path.resolve('Dockerfile'), 'utf8')
expect(dockerfile).toContain(
'CMD ["./packages/db/node_modules/.bin/tsx", "packages/db/src/migrate.ts"]',
)
expect(dockerfile).not.toContain(
'CMD ["./node_modules/.bin/tsx", "packages/db/src/migrate.ts"]',
)
})
it('ships the worker as a standalone bundle without production node_modules', async () => {
const dockerfile = await readFile(path.resolve('Dockerfile'), 'utf8')
const workerPackage = JSON.parse(
await readFile(path.resolve('apps/worker/package.json'), 'utf8'),
) as { scripts: { build: string } }
expect(workerPackage.scripts.build).toContain(
'esbuild src/index.ts src/operator/password-reset.ts',
)
expect(workerPackage.scripts.build).toContain('--bundle --platform=node')
expect(workerPackage.scripts.build).toContain(
'const require = createRequire(import.meta.url)',
)
expect(dockerfile).toContain(
'cp -a /app/apps/worker/dist/. /out/worker/dist/',
)
expect(dockerfile).not.toContain(
'pnpm --filter @devrunbook/worker --prod deploy',
)
})
it('rejects secret-bearing package output and nested setup configuration', async () => {
await expect(loadPlaybookPackage(await maliciousPackage())).rejects.toThrow(
'sensitive input problemStatement cannot be included in output',
)
expect(() =>
assertNonSecretConfiguration({
integration: { apiToken: 'must-never-be-persisted' },
}),
).toThrow(
'configuration.integration.apiToken must not contain secret material',
)
expect(() =>
assertNonSecretConfiguration({
integrationEncryptionKeyVersion: 'v2',
}),
).not.toThrow()
})
it('persists only a peppered HMAC session digest', async () => {
const rawToken = 'raw-session-token-with-high-entropy-0123456789'
const persistence = new CapturingAuthPersistence()
const digester = new TokenDigester(Buffer.alloc(32, 7))
const service = new AuthService(
persistence,
digester,
() => new Date('2026-07-27T12:00:00.000Z'),
)
const active = await service.createSession({
userId: persistence.user.id,
rawToken,
})
expect(active?.rawToken).toBe(rawToken)
expect(JSON.stringify(persistence.sessionInput)).not.toContain(rawToken)
expect(persistence.sessionInput?.tokenHash).toMatch(
/^hmac-sha256:v1:[a-f0-9]{64}$/u,
)
expect(
digester.matches(rawToken, persistence.sessionInput!.tokenHash),
).toBe(true)
expect(new TokenDigester(Buffer.alloc(32, 8)).digest(rawToken)).not.toBe(
persistence.sessionInput?.tokenHash,
)
})
it('denies proxy bootstrap bypass and cross-origin credential requests without reflection', async () => {
const proxied = new Request('http://127.0.0.1/api/v1/instance/setup', {
method: 'POST',
headers: { 'x-forwarded-for': '127.0.0.1' },
})
expect(authorizeBootstrap(proxied, '', undefined)).toBe(false)
expect(
authorizeBootstrap(proxied, 'attacker-token', 'operator-token'),
).toBe(false)
const handler = vi.fn(async () => new Response('should not run'))
const rawCookie = 'devrunbook.session=raw-session-secret'
const request = new Request(
'https://runbook.example.test/api/auth/session',
{
method: 'POST',
headers: {
origin: 'https://attacker.example.test',
'sec-fetch-site': 'cross-site',
cookie: rawCookie,
},
},
)
const response = await handleAuthRequest(
request,
handler,
'https://runbook.example.test',
)
const body = await response.text()
expect(response.status).toBe(403)
expect(handler).not.toHaveBeenCalled()
expect(body).not.toContain('attacker.example.test')
expect(body).not.toContain(rawCookie)
expect(body).toContain('INVALID_ORIGIN')
})
it('redacts root and nested secret fields from structured logs', async () => {
let output = ''
vi.spyOn(process.stdout, 'write').mockImplementation((chunk) => {
output += String(chunk)
return true
})
const logger = createLogger('info')
logger.info(
{
password: 'root-password-secret',
token: 'root-token-secret',
req: {
headers: {
authorization: 'Bearer raw-authorization-secret',
cookie: 'session=raw-cookie-secret',
},
},
account: { secret: 'nested-account-secret' },
},
'security redaction probe',
)
await new Promise((resolve) => setTimeout(resolve, 20))
expect(output).not.toContain('root-password-secret')
expect(output).not.toContain('root-token-secret')
expect(output).not.toContain('raw-authorization-secret')
expect(output).not.toContain('raw-cookie-secret')
expect(output).not.toContain('nested-account-secret')
expect(output).toContain('[REDACTED]')
})
})