161 lines
5.6 KiB
TypeScript
161 lines
5.6 KiB
TypeScript
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}`
|
|
})
|
|
})
|