174 lines
5.2 KiB
TypeScript
174 lines
5.2 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
|
|
import type {
|
|
GeneratedArtifactMetadata,
|
|
GeneratedArtifactMetadataStore,
|
|
ImmutableArtifactStorage,
|
|
StoreGeneratedArtifactResult,
|
|
WorkspaceAuthorizationRecord,
|
|
} from '..'
|
|
import {
|
|
createGeneratedArtifact,
|
|
downloadGeneratedArtifact,
|
|
} from './generated-artifact'
|
|
|
|
const workspaceId = '00000000-0000-4000-8000-000000000101'
|
|
const userId = '00000000-0000-4000-8000-000000000102'
|
|
const runId = '00000000-0000-4000-8000-000000000103'
|
|
const artifactId = '00000000-0000-4000-8000-000000000104'
|
|
|
|
class MemoryMetadata implements GeneratedArtifactMetadataStore {
|
|
artifact: GeneratedArtifactMetadata | null = null
|
|
|
|
async createIdempotently(
|
|
artifact: GeneratedArtifactMetadata,
|
|
): Promise<StoreGeneratedArtifactResult> {
|
|
if (!this.artifact) {
|
|
this.artifact = artifact
|
|
return { artifact, created: true }
|
|
}
|
|
return { artifact: this.artifact, created: false }
|
|
}
|
|
|
|
async findByIdInWorkspace(id: string, workspace: string) {
|
|
return this.artifact?.id === id && this.artifact.workspaceId === workspace
|
|
? this.artifact
|
|
: null
|
|
}
|
|
|
|
async listByRunInWorkspace(runId: string, workspace: string) {
|
|
return this.artifact?.runId === runId &&
|
|
this.artifact.workspaceId === workspace
|
|
? [this.artifact]
|
|
: []
|
|
}
|
|
}
|
|
|
|
class MemoryStorage implements ImmutableArtifactStorage {
|
|
readonly bytes = new Map<string, Uint8Array>()
|
|
|
|
async putImmutable(key: string, content: Uint8Array) {
|
|
const created = !this.bytes.has(key)
|
|
if (created) this.bytes.set(key, new Uint8Array(content))
|
|
return { created }
|
|
}
|
|
|
|
async read(key: string) {
|
|
const content = this.bytes.get(key)
|
|
if (!content) throw new Error('missing')
|
|
return new Uint8Array(content)
|
|
}
|
|
}
|
|
|
|
function authorization(role: 'viewer' | 'editor' | 'owner') {
|
|
return {
|
|
async findWorkspaceAuthorization(): Promise<WorkspaceAuthorizationRecord> {
|
|
return {
|
|
userId,
|
|
workspaceId,
|
|
instanceRole: 'user',
|
|
workspaceRole: role,
|
|
userStatus: 'active',
|
|
}
|
|
},
|
|
}
|
|
}
|
|
|
|
function request(content = new TextEncoder().encode('# Run\n')) {
|
|
return {
|
|
actor: { userId },
|
|
workspaceId,
|
|
artifactId,
|
|
runId,
|
|
artifactType: 'markdown' as const,
|
|
filename: 'run.md',
|
|
mediaType: 'text/markdown; charset=utf-8',
|
|
content,
|
|
}
|
|
}
|
|
|
|
describe('generated artifact use cases', () => {
|
|
it('creates immutable bytes and metadata idempotently', async () => {
|
|
const metadata = new MemoryMetadata()
|
|
const storage = new MemoryStorage()
|
|
const dependencies = {
|
|
authorization: authorization('editor'),
|
|
metadata,
|
|
storage,
|
|
now: () => new Date('2026-07-27T12:00:00.000Z'),
|
|
}
|
|
|
|
await expect(
|
|
createGeneratedArtifact(dependencies, request()),
|
|
).resolves.toMatchObject({ created: true })
|
|
await expect(
|
|
createGeneratedArtifact(dependencies, request()),
|
|
).resolves.toMatchObject({ created: false })
|
|
expect(metadata.artifact?.sha256).toHaveLength(64)
|
|
expect(metadata.artifact?.sizeBytes).toBe(6n)
|
|
expect(metadata.artifact?.storageKey).toMatch(/^[0-9a-f]{64}$/)
|
|
expect(storage.bytes).toHaveLength(1)
|
|
})
|
|
|
|
it('allows a viewer to download only from the authorized workspace', async () => {
|
|
const metadata = new MemoryMetadata()
|
|
const storage = new MemoryStorage()
|
|
await createGeneratedArtifact(
|
|
{
|
|
authorization: authorization('editor'),
|
|
metadata,
|
|
storage,
|
|
now: () => new Date('2026-07-27T12:00:00.000Z'),
|
|
},
|
|
request(),
|
|
)
|
|
|
|
const download = await downloadGeneratedArtifact(
|
|
{ authorization: authorization('viewer'), metadata, storage },
|
|
{ actor: { userId }, workspaceId, artifactId },
|
|
)
|
|
expect(new TextDecoder().decode(download.content)).toBe('# Run\n')
|
|
await expect(
|
|
downloadGeneratedArtifact(
|
|
{ authorization: authorization('viewer'), metadata, storage },
|
|
{
|
|
actor: { userId },
|
|
workspaceId: '00000000-0000-4000-8000-000000000999',
|
|
artifactId,
|
|
},
|
|
),
|
|
).rejects.toMatchObject({ code: 'workspace_access_denied' })
|
|
})
|
|
|
|
it('rejects unsafe filenames, viewer creation, and corrupted bytes', async () => {
|
|
const metadata = new MemoryMetadata()
|
|
const storage = new MemoryStorage()
|
|
const base = {
|
|
authorization: authorization('editor'),
|
|
metadata,
|
|
storage,
|
|
now: () => new Date('2026-07-27T12:00:00.000Z'),
|
|
}
|
|
await expect(
|
|
createGeneratedArtifact(base, { ...request(), filename: '../run.md' }),
|
|
).rejects.toMatchObject({ code: 'generated_artifact_filename_invalid' })
|
|
await expect(
|
|
createGeneratedArtifact(
|
|
{ ...base, authorization: authorization('viewer') },
|
|
request(),
|
|
),
|
|
).rejects.toMatchObject({ code: 'workspace_access_denied' })
|
|
|
|
await createGeneratedArtifact(base, request())
|
|
const key = metadata.artifact?.storageKey
|
|
if (!key) throw new Error('expected storage key')
|
|
storage.bytes.set(key, new TextEncoder().encode('tampered'))
|
|
await expect(
|
|
downloadGeneratedArtifact(
|
|
{ authorization: authorization('viewer'), metadata, storage },
|
|
{ actor: { userId }, workspaceId, artifactId },
|
|
),
|
|
).rejects.toMatchObject({ code: 'generated_artifact_integrity_failed' })
|
|
})
|
|
})
|