184 lines
5.6 KiB
TypeScript
184 lines
5.6 KiB
TypeScript
import type {
|
|
GeneratedRun,
|
|
StoreGeneratedRunResult,
|
|
} from '@devrunbook/application'
|
|
import { describe, expect, it } from 'vitest'
|
|
|
|
import {
|
|
assertGeneratedRunPersistenceIntegrity,
|
|
decodeGeneratedRunCursor,
|
|
DrizzleGeneratedRunStore,
|
|
encodeGeneratedRunCursor,
|
|
type GeneratedRunTransaction,
|
|
type GeneratedRunTransactionRunner,
|
|
} from './generated-run-store'
|
|
|
|
function run(overrides: Partial<GeneratedRun> = {}): GeneratedRun {
|
|
return {
|
|
id: '00000000-0000-4000-8000-000000000101',
|
|
workspaceId: '00000000-0000-4000-8000-000000000102',
|
|
generatedBy: '00000000-0000-4000-8000-000000000103',
|
|
sourceDraftId: null,
|
|
playbookVersionId: '00000000-0000-4000-8000-000000000104',
|
|
snapshots: {
|
|
playbook: { slug: 'root-cause-bugfix' },
|
|
repositoryProfile: null,
|
|
normalizedInput: { problem: 'broken result' },
|
|
policy: { autonomy: 'verify' },
|
|
provenance: [],
|
|
},
|
|
lint: { exportReadiness: 'ready', findings: [] },
|
|
renderedPrompt: '# Task\n',
|
|
renderDigest: 'a'.repeat(64),
|
|
idempotencyKey: 'request-1',
|
|
generatedAt: '2026-07-27T12:00:00.000Z',
|
|
...overrides,
|
|
}
|
|
}
|
|
|
|
class MemoryGeneratedRunRunner implements GeneratedRunTransactionRunner {
|
|
stored: GeneratedRun | null = null
|
|
locks: string[] = []
|
|
audits: GeneratedRun[] = []
|
|
|
|
async run<T>(
|
|
work: (transaction: GeneratedRunTransaction) => Promise<T>,
|
|
): Promise<T> {
|
|
const transaction: GeneratedRunTransaction = {
|
|
acquireIdempotencyLock: async (workspaceId, key) => {
|
|
this.locks.push(`${workspaceId}:${key}`)
|
|
},
|
|
findByIdempotencyKey: async (workspaceId, key) =>
|
|
this.stored?.workspaceId === workspaceId &&
|
|
this.stored.idempotencyKey === key
|
|
? this.stored
|
|
: null,
|
|
insert: async (candidate) => {
|
|
this.stored = candidate
|
|
return candidate
|
|
},
|
|
appendCreationAudit: async (candidate) => {
|
|
this.audits.push(candidate)
|
|
},
|
|
}
|
|
return work(transaction)
|
|
}
|
|
}
|
|
|
|
describe('DrizzleGeneratedRunStore', () => {
|
|
it('serializes creation and returns an exact retry without inserting again', async () => {
|
|
const runner = new MemoryGeneratedRunRunner()
|
|
const store = new DrizzleGeneratedRunStore(runner)
|
|
const candidate = run()
|
|
|
|
await expect(store.createIdempotently(candidate)).resolves.toEqual({
|
|
run: candidate,
|
|
created: true,
|
|
} satisfies StoreGeneratedRunResult)
|
|
await expect(store.createIdempotently(candidate)).resolves.toEqual({
|
|
run: candidate,
|
|
created: false,
|
|
} satisfies StoreGeneratedRunResult)
|
|
expect(runner.locks).toHaveLength(2)
|
|
expect(runner.audits).toEqual([candidate])
|
|
})
|
|
|
|
it('rejects reuse of the key for different immutable input', async () => {
|
|
const runner = new MemoryGeneratedRunRunner()
|
|
const store = new DrizzleGeneratedRunStore(runner)
|
|
await store.createIdempotently(run())
|
|
|
|
await expect(
|
|
store.createIdempotently(
|
|
run({
|
|
snapshots: { ...run().snapshots, policy: { autonomy: 'repair' } },
|
|
}),
|
|
),
|
|
).rejects.toMatchObject({ code: 'generated_run_idempotency_conflict' })
|
|
})
|
|
|
|
it.each([
|
|
['digest', { renderDigest: '0'.repeat(64) }],
|
|
['idempotency key', { idempotencyKey: '' }],
|
|
[
|
|
'snapshot JSON',
|
|
{
|
|
snapshots: {
|
|
...run().snapshots,
|
|
normalizedInput: { invalid: undefined },
|
|
},
|
|
},
|
|
],
|
|
[
|
|
'lint JSON',
|
|
{
|
|
lint: { exportReadiness: 'ready', findings: [{ severity: 'error' }] },
|
|
},
|
|
],
|
|
[
|
|
'repository snapshot identity',
|
|
{
|
|
snapshots: {
|
|
...run().snapshots,
|
|
repositoryProfile: {
|
|
repositoryId: '00000000-0000-4000-8000-000000000105',
|
|
},
|
|
},
|
|
},
|
|
],
|
|
])('rejects corrupt persisted %s', (_, changes) => {
|
|
expect(() =>
|
|
assertGeneratedRunPersistenceIntegrity({
|
|
...run(),
|
|
...(changes as Partial<GeneratedRun>),
|
|
}),
|
|
).toThrowError(
|
|
expect.objectContaining({ code: 'generated_run_persistence_corrupt' }),
|
|
)
|
|
})
|
|
|
|
it('round-trips the stable generatedAt and ID cursor', () => {
|
|
const cursor = {
|
|
generatedAt: '2026-07-27T12:00:00.000Z',
|
|
id: '00000000-0000-4000-8000-000000000101',
|
|
}
|
|
expect(decodeGeneratedRunCursor(encodeGeneratedRunCursor(cursor))).toEqual(
|
|
cursor,
|
|
)
|
|
})
|
|
|
|
it.each([
|
|
'',
|
|
'not-base64-json',
|
|
Buffer.from('{}').toString('base64url'),
|
|
Buffer.from(
|
|
JSON.stringify({ generatedAt: 'invalid', id: run().id }),
|
|
).toString('base64url'),
|
|
Buffer.from(
|
|
JSON.stringify({ generatedAt: run().generatedAt, id: 'not-a-uuid' }),
|
|
).toString('base64url'),
|
|
])('rejects malformed history cursor %s', (cursor) => {
|
|
expect(() => decodeGeneratedRunCursor(cursor)).toThrowError(
|
|
expect.objectContaining({ code: 'generated_run_cursor_invalid' }),
|
|
)
|
|
})
|
|
|
|
it('rejects invalid history limits and filters before database access', async () => {
|
|
const store = new DrizzleGeneratedRunStore(new MemoryGeneratedRunRunner())
|
|
await expect(
|
|
store.listForWorkspace(run().workspaceId, { limit: 0 }),
|
|
).rejects.toMatchObject({ code: 'generated_run_list_limit_invalid' })
|
|
await expect(
|
|
store.listForWorkspace(run().workspaceId, { playbookSlug: '../other' }),
|
|
).rejects.toMatchObject({ code: 'generated_run_filter_invalid' })
|
|
await expect(
|
|
store.listForWorkspace(run().workspaceId, {
|
|
repositoryId: 'not-a-uuid',
|
|
}),
|
|
).rejects.toMatchObject({ code: 'generated_run_filter_invalid' })
|
|
await expect(
|
|
store.listForWorkspace(run().workspaceId, { cursor: 'invalid' }),
|
|
).rejects.toMatchObject({ code: 'generated_run_cursor_invalid' })
|
|
})
|
|
})
|