Add web multimodal image attachments
This commit is contained in:
96
packages/attachment/attachment-local/tests/store.spec.ts
Normal file
96
packages/attachment/attachment-local/tests/store.spec.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import { createHash } from 'node:crypto'
|
||||
import { chmod, mkdir, readFile, stat, writeFile } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { mkdtemp, rm } from 'node:fs/promises'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import type { ImageAttachmentLimits } from '@deepseek-ai/dsh-attachment'
|
||||
import { readImageFile, saveImageFile } from '../src/store.ts'
|
||||
|
||||
const PNG = Uint8Array.from(Buffer.from(
|
||||
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=',
|
||||
'base64',
|
||||
))
|
||||
|
||||
const LIMITS: ImageAttachmentLimits = {
|
||||
maxImageBytes: 1024,
|
||||
maxImagesPerMessage: 2,
|
||||
maxMessageImageBytes: 2048,
|
||||
maxImagePixels: 16,
|
||||
mediaTypes: ['image/png', 'image/jpeg', 'image/webp', 'image/gif'],
|
||||
}
|
||||
|
||||
const roots: string[] = []
|
||||
|
||||
async function root(): Promise<string> {
|
||||
const value = await mkdtemp(join(tmpdir(), 'dsh-attachment-'))
|
||||
roots.push(value)
|
||||
return join(value, 'attachments', 'v1')
|
||||
}
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(roots.splice(0).map(path => rm(path, { recursive: true, force: true })))
|
||||
})
|
||||
|
||||
describe('local attachment store', () => {
|
||||
it('publishes one private content-addressed object and deduplicates equal bytes', async () => {
|
||||
const storageRoot = await root()
|
||||
const first = await saveImageFile(storageRoot, {
|
||||
data: PNG, mediaType: 'image/png', name: '/private/tmp/pixel.png',
|
||||
}, LIMITS)
|
||||
const second = await saveImageFile(storageRoot, { data: PNG, mediaType: 'image/png' }, LIMITS)
|
||||
const sha256 = createHash('sha256').update(PNG).digest('hex')
|
||||
const object = join(storageRoot, 'objects', sha256.slice(0, 2), sha256)
|
||||
|
||||
expect(first).toEqual({
|
||||
attachmentId: `sha256:${sha256}`,
|
||||
mediaType: 'image/png',
|
||||
bytes: PNG.byteLength,
|
||||
width: 1,
|
||||
height: 1,
|
||||
name: 'pixel.png',
|
||||
})
|
||||
expect(second.attachmentId).toBe(first.attachmentId)
|
||||
expect(new Uint8Array(await readFile(object))).toEqual(PNG)
|
||||
expect((await stat(object)).mode & 0o777).toBe(0o600)
|
||||
expect((await stat(join(storageRoot, 'objects', sha256.slice(0, 2)))).mode & 0o777).toBe(0o700)
|
||||
await expect(readImageFile(storageRoot, first, LIMITS)).resolves.toEqual({ ref: first, data: PNG })
|
||||
})
|
||||
|
||||
it('rejects malformed bytes, mismatched declarations, byte limits, and decoded-pixel limits', async () => {
|
||||
const storageRoot = await root()
|
||||
await expect(saveImageFile(storageRoot, {
|
||||
data: Uint8Array.of(1, 2, 3), mediaType: 'image/png',
|
||||
}, LIMITS)).rejects.toMatchObject({ code: 'INVALID_IMAGE' })
|
||||
await expect(saveImageFile(storageRoot, {
|
||||
data: PNG, mediaType: 'image/jpeg',
|
||||
}, LIMITS)).rejects.toMatchObject({ code: 'IMAGE_TYPE_MISMATCH' })
|
||||
await expect(saveImageFile(storageRoot, {
|
||||
data: PNG, mediaType: 'image/png',
|
||||
}, { ...LIMITS, maxImageBytes: 1 })).rejects.toMatchObject({ code: 'IMAGE_TOO_LARGE' })
|
||||
|
||||
const wide = PNG.slice()
|
||||
wide.set([0, 0, 0, 5, 0, 0, 0, 5], 16)
|
||||
await expect(saveImageFile(storageRoot, {
|
||||
data: wide, mediaType: 'image/png',
|
||||
}, LIMITS)).rejects.toMatchObject({ code: 'IMAGE_TOO_MANY_PIXELS' })
|
||||
})
|
||||
|
||||
it('fails closed when an object is missing, corrupted, or addressed by an invalid reference', async () => {
|
||||
const storageRoot = await root()
|
||||
const ref = await saveImageFile(storageRoot, { data: PNG, mediaType: 'image/png' }, LIMITS)
|
||||
const sha256 = String(ref.attachmentId).slice('sha256:'.length)
|
||||
const object = join(storageRoot, 'objects', sha256.slice(0, 2), sha256)
|
||||
await chmod(object, 0o600)
|
||||
await writeFile(object, Uint8Array.of(1, 2, 3))
|
||||
await expect(readImageFile(storageRoot, ref, LIMITS))
|
||||
.rejects.toMatchObject({ code: 'ATTACHMENT_CORRUPT' })
|
||||
await expect(readImageFile(storageRoot, { ...ref, attachmentId: 'bad' as never }, LIMITS))
|
||||
.rejects.toMatchObject({ code: 'INVALID_ATTACHMENT_REF' })
|
||||
|
||||
const missingRoot = await root()
|
||||
await mkdir(missingRoot, { recursive: true })
|
||||
await expect(readImageFile(missingRoot, ref, LIMITS))
|
||||
.rejects.toMatchObject({ code: 'ATTACHMENT_NOT_FOUND' })
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user