293 lines
9.9 KiB
TypeScript
293 lines
9.9 KiB
TypeScript
import { createHash, randomUUID } from 'node:crypto'
|
|
import {
|
|
applyRepositoryProfileServerMetadata,
|
|
type RepositoryProfile,
|
|
} from '@devrunbook/repository-intel'
|
|
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
|
|
|
|
import { closeDatabase, getSqlClient } from '../index'
|
|
import { DrizzleGeneratedRunStore } from './generated-run-store'
|
|
|
|
const databaseIntegration =
|
|
process.env.DEVRUNBOOK_DATABASE_INTEGRATION === 'true' &&
|
|
Boolean(process.env.DATABASE_URL)
|
|
|
|
function profile(name: string): RepositoryProfile {
|
|
return applyRepositoryProfileServerMetadata(
|
|
{
|
|
apiVersion: 'devrunbook.io/v1alpha1',
|
|
kind: 'RepositoryProfile',
|
|
metadata: { name, revision: 1, source: 'manual' },
|
|
spec: {
|
|
repositoryType: 'single-app',
|
|
stack: {
|
|
languages: ['TypeScript'],
|
|
frameworks: [],
|
|
packageManagers: ['pnpm'],
|
|
databases: ['PostgreSQL'],
|
|
deploymentTypes: ['Docker'],
|
|
testFrameworks: ['Vitest'],
|
|
},
|
|
commands: [],
|
|
paths: {
|
|
applicationRoots: ['apps/web'],
|
|
testRoots: ['tests'],
|
|
documentationRoots: ['docs'],
|
|
generated: ['dist'],
|
|
protected: ['runtime'],
|
|
excluded: ['node_modules'],
|
|
},
|
|
policies: {
|
|
preserveBackwardCompatibility: true,
|
|
newDependencies: 'justify',
|
|
gitWrite: 'none',
|
|
migrations: 'reversible-only',
|
|
documentationRequired: true,
|
|
networkAccess: 'forbidden',
|
|
productionDataAccess: 'forbidden',
|
|
},
|
|
},
|
|
},
|
|
1,
|
|
)
|
|
}
|
|
|
|
describe.skipIf(!databaseIntegration)(
|
|
'generated-run history integration',
|
|
() => {
|
|
const workspaceA = randomUUID()
|
|
const workspaceB = randomUUID()
|
|
const userId = randomUUID()
|
|
const repositoryA = randomUUID()
|
|
const repositoryB = randomUUID()
|
|
const alphaSlug = `history-alpha-${randomUUID()}`
|
|
const betaSlug = `history-beta-${randomUUID()}`
|
|
let alphaVersionId: string
|
|
let betaVersionId: string
|
|
let profileRevisionA: string
|
|
let profileRevisionB: string
|
|
let store: DrizzleGeneratedRunStore
|
|
const runIds = [randomUUID(), randomUUID(), randomUUID()]
|
|
|
|
async function insertVersion(slug: string) {
|
|
const sql = getSqlClient()
|
|
const [playbook] = await sql<{ id: string }[]>`
|
|
insert into playbooks (
|
|
workspace_id, logical_id, slug, namespace, source_type
|
|
) values (
|
|
${workspaceA}, ${slug}, ${slug}, ${`private-${workspaceA}`}, 'private'
|
|
) returning id
|
|
`
|
|
const manifest = {
|
|
metadata: { slug, version: '1.0.0', title: 'History fixture' },
|
|
spec: { intent: { outcome: 'Verify history.' } },
|
|
}
|
|
const [version] = await sql<{ id: string }[]>`
|
|
insert into playbook_versions (
|
|
playbook_id, semantic_version, lifecycle, package_api_version,
|
|
title, summary, category, risk_tier, package_json, template_text,
|
|
content_digest, published_at, created_by
|
|
) values (
|
|
${playbook!.id}, '1.0.0', 'validated', 'devrunbook.io/v1alpha1',
|
|
'History fixture', 'History fixture', 'testing', 'low',
|
|
${JSON.stringify(manifest)}::jsonb, '# Task', ${'a'.repeat(64)},
|
|
'2026-07-27T12:00:00.000Z', ${userId}
|
|
) returning id
|
|
`
|
|
return version!.id
|
|
}
|
|
|
|
async function insertRun(input: {
|
|
id: string
|
|
workspaceId: string
|
|
playbookVersionId: string
|
|
slug: string
|
|
generatedAt: string
|
|
repository?: {
|
|
id: string
|
|
revisionId: string
|
|
document: RepositoryProfile
|
|
}
|
|
corruptRepository?: boolean
|
|
}) {
|
|
const sql = getSqlClient()
|
|
const prompt = `# ${input.id}\n`
|
|
const digest = createHash('sha256').update(prompt, 'utf8').digest('hex')
|
|
const repositorySnapshot = input.corruptRepository
|
|
? {
|
|
revisionId: input.repository!.revisionId,
|
|
repositoryId: input.repository!.id,
|
|
revisionNumber: 1,
|
|
contentDigest: input.repository!.document.metadata.contentDigest,
|
|
profile: {},
|
|
}
|
|
: input.repository
|
|
? {
|
|
revisionId: input.repository.revisionId,
|
|
repositoryId: input.repository.id,
|
|
revisionNumber: 1,
|
|
contentDigest: input.repository.document.metadata.contentDigest,
|
|
profile: input.repository.document,
|
|
}
|
|
: null
|
|
await sql`
|
|
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,
|
|
generated_at
|
|
) values (
|
|
${input.id}, ${input.workspaceId}, ${input.playbookVersionId},
|
|
${JSON.stringify({ slug: input.slug })}::jsonb,
|
|
${repositorySnapshot === null ? null : JSON.stringify(repositorySnapshot)}::jsonb,
|
|
'{}'::jsonb, '{}'::jsonb, '[]'::jsonb,
|
|
${JSON.stringify({ exportReadiness: 'ready', findings: [] })}::jsonb,
|
|
${prompt}, ${digest}, ${randomUUID()}, ${userId}, ${input.generatedAt}
|
|
)
|
|
`
|
|
}
|
|
|
|
beforeAll(async () => {
|
|
const sql = getSqlClient()
|
|
await sql`
|
|
insert into users (
|
|
id, email, display_name, password_hash, instance_role, status
|
|
) values (
|
|
${userId}, ${`run-history-${userId}@example.invalid`}, 'Run history',
|
|
'not-a-real-password-hash', 'user', 'active'
|
|
)
|
|
`
|
|
await sql`
|
|
insert into workspaces (id, name, type)
|
|
values
|
|
(${workspaceA}, 'Run history A', 'team'),
|
|
(${workspaceB}, 'Run history B', 'team')
|
|
`
|
|
await sql`
|
|
insert into repositories (id, workspace_id, display_name, source_type)
|
|
values
|
|
(${repositoryA}, ${workspaceA}, 'Repository A', 'manual'),
|
|
(${repositoryB}, ${workspaceA}, 'Repository B', 'manual')
|
|
`
|
|
alphaVersionId = await insertVersion(alphaSlug)
|
|
betaVersionId = await insertVersion(betaSlug)
|
|
const profileA = profile('Repository A')
|
|
const profileB = profile('Repository B')
|
|
const [revisionA] = await sql<{ id: string }[]>`
|
|
insert into repository_profile_revisions (
|
|
repository_id, revision_number, profile_json, content_digest, created_by
|
|
) values (
|
|
${repositoryA}, 1, ${JSON.stringify(profileA)}::jsonb,
|
|
${profileA.metadata.contentDigest!}, ${userId}
|
|
) returning id
|
|
`
|
|
const [revisionB] = await sql<{ id: string }[]>`
|
|
insert into repository_profile_revisions (
|
|
repository_id, revision_number, profile_json, content_digest, created_by
|
|
) values (
|
|
${repositoryB}, 1, ${JSON.stringify(profileB)}::jsonb,
|
|
${profileB.metadata.contentDigest!}, ${userId}
|
|
) returning id
|
|
`
|
|
profileRevisionA = revisionA!.id
|
|
profileRevisionB = revisionB!.id
|
|
await insertRun({
|
|
id: runIds[0]!,
|
|
workspaceId: workspaceA,
|
|
playbookVersionId: alphaVersionId,
|
|
slug: alphaSlug,
|
|
generatedAt: '2026-07-27T12:00:00.000Z',
|
|
repository: {
|
|
id: repositoryA,
|
|
revisionId: profileRevisionA,
|
|
document: profileA,
|
|
},
|
|
})
|
|
await insertRun({
|
|
id: runIds[1]!,
|
|
workspaceId: workspaceA,
|
|
playbookVersionId: alphaVersionId,
|
|
slug: alphaSlug,
|
|
generatedAt: '2026-07-27T12:00:00.000Z',
|
|
})
|
|
await insertRun({
|
|
id: runIds[2]!,
|
|
workspaceId: workspaceA,
|
|
playbookVersionId: betaVersionId,
|
|
slug: betaSlug,
|
|
generatedAt: '2026-07-26T12:00:00.000Z',
|
|
repository: {
|
|
id: repositoryB,
|
|
revisionId: profileRevisionB,
|
|
document: profileB,
|
|
},
|
|
})
|
|
store = new DrizzleGeneratedRunStore()
|
|
})
|
|
|
|
afterAll(async () => {
|
|
const sql = getSqlClient()
|
|
await sql`delete from workspaces where id in (${workspaceA}, ${workspaceB})`
|
|
await sql`delete from users where id = ${userId}`
|
|
await closeDatabase()
|
|
})
|
|
|
|
it('paginates generatedAt plus ID without duplicates or omissions', async () => {
|
|
const seen: string[] = []
|
|
let cursor: string | null = null
|
|
do {
|
|
const page = await store.listForWorkspace(workspaceA, {
|
|
limit: 1,
|
|
cursor,
|
|
})
|
|
seen.push(...page.items.map((item) => item.id))
|
|
cursor = page.nextCursor
|
|
} while (cursor)
|
|
|
|
const tied = runIds
|
|
.slice(0, 2)
|
|
.sort((left, right) => left.localeCompare(right, 'en'))
|
|
expect(seen).toEqual([...tied, runIds[2]])
|
|
expect(new Set(seen).size).toBe(3)
|
|
await expect(store.listForWorkspace(workspaceB, {})).resolves.toEqual({
|
|
items: [],
|
|
nextCursor: null,
|
|
})
|
|
})
|
|
|
|
it('filters playbooks relationally and repositories through validated snapshots', async () => {
|
|
const byPlaybook = await store.listForWorkspace(workspaceA, {
|
|
playbookSlug: alphaSlug,
|
|
})
|
|
expect(byPlaybook.items.map((item) => item.id).sort()).toEqual(
|
|
runIds.slice(0, 2).sort(),
|
|
)
|
|
const byRepository = await store.listForWorkspace(workspaceA, {
|
|
repositoryId: repositoryA,
|
|
})
|
|
expect(byRepository.items.map((item) => item.id)).toEqual([runIds[0]])
|
|
})
|
|
|
|
it('fails closed when a repository filter encounters a corrupt frozen snapshot', async () => {
|
|
const corruptId = randomUUID()
|
|
await insertRun({
|
|
id: corruptId,
|
|
workspaceId: workspaceA,
|
|
playbookVersionId: alphaVersionId,
|
|
slug: alphaSlug,
|
|
generatedAt: '2026-07-28T12:00:00.000Z',
|
|
repository: {
|
|
id: repositoryA,
|
|
revisionId: profileRevisionA,
|
|
document: profile('Repository A'),
|
|
},
|
|
corruptRepository: true,
|
|
})
|
|
await expect(
|
|
store.listForWorkspace(workspaceA, { repositoryId: repositoryA }),
|
|
).rejects.toMatchObject({ code: 'generated_run_persistence_corrupt' })
|
|
})
|
|
},
|
|
)
|