Files
deepseek-harness/packages/attachment/attachment-local/tests/store.spec.ts
creatixchu f73bf425ec fix: address ds-review-bot v8 findings
- llm-deepseek: the uncatalogued resolveModel fallback declares text-only
  modalities — the wire route is text-only regardless of catalog
  membership, so "unknown" must not let the host persist-then-fail images.
- session.selectModel also consults the pending-inbox mirror: a queued
  image prompt enters the log only when claimed, after a switch would land.
- attachment store: ensureDurableDirectory syncs every ancestor entry up to
  a caller-vouched boundary regardless of what mkdir reports — a raced
  "already existed" is not "already durable".
- One image walker (imageBlockIn/imageInEvent) now serves both attachment
  authorization and the selection gate; referencedImage therefore also
  authorizes references inside wrapped message content.
- InputHub: the scope disposer resolves the conversation service optionally
  (teardown/HMR must reach quiescence), and a send failing after its scope
  died releases the in-flight drafts instead of restoring them onto a
  disposed shell.
- http-bridge destroys declared-oversize requests with connection: close
  instead of draining a body the client can trickle indefinitely.
- LlmService validates AND detaches modality arrays identically on the
  advisory and exact routes; READMEs record the fourth INVALID_MODEL_INFO
  rejection reason.
- CLI provider docs (JSDoc, README pair, Agent Note pair) describe the
  reuse behavior; llm-route.spec now parses the SHIPPED cordis.yml through
  the production extraction, pinning the row coupling.
- image-display lane pins gallery/rail shape in inline snapshots and the
  object-URL scheme this environment must take; stale host.schema comment
  dropped.
2026-07-29 19:40:17 +08:00

185 lines
7.9 KiB
TypeScript

import { createHash } from 'node:crypto'
import { constants } from 'node:fs'
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, vi } from 'vitest'
import type { ImageAttachmentLimits } from '@deepseek-ai/dsh-attachment'
import { readImageFile, saveImageFile } from '../src/store.ts'
const fsControl = vi.hoisted(() => ({ syncedDirectories: [] as string[] }))
vi.mock('node:fs/promises', async (importOriginal) => {
const actual = await importOriginal<typeof import('node:fs/promises')>()
return {
...actual,
async open(...args: Parameters<typeof actual.open>): ReturnType<typeof actual.open> {
if (args[1] === constants.O_RDONLY) fsControl.syncedDirectories.push(String(args[0]))
return actual.open(...args)
},
}
})
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.skipIf(process.platform === 'win32')('syncs every object ancestor up to the durable boundary before returning', async () => {
const storageRoot = await root()
const base = join(storageRoot, '..', '..')
const sha256 = createHash('sha256').update(PNG).digest('hex')
const objects = join(storageRoot, 'objects')
const bucket = join(objects, sha256.slice(0, 2))
fsControl.syncedDirectories.length = 0
await saveImageFile(storageRoot, { data: PNG, mediaType: 'image/png' }, LIMITS)
// Every level between each created directory and the vouched boundary
// syncs unconditionally — "already existed" is not "already durable"
// when a concurrent first save may have created but not yet synced it.
expect(fsControl.syncedDirectories).toEqual([
// bucket chain: every parent entry between the bucket and the boundary.
objects,
storageRoot,
join(storageRoot, '..'),
base,
// staging chain re-walks the shared ancestors after creating tmp.
storageRoot,
join(storageRoot, '..'),
base,
// publication: the settled object's bucket and its parent for the rename.
bucket,
objects,
])
})
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)).resolves.toEqual({ ref: first, data: PNG })
})
it('keeps admitted history readable after deployment limits become stricter', async () => {
const storageRoot = await root()
const ref = await saveImageFile(storageRoot, { data: PNG, mediaType: 'image/png' }, LIMITS)
await expect(readImageFile(storageRoot, ref)).resolves.toEqual({ ref, data: PNG })
})
it('rejects malformed bytes, mismatched declarations, byte limits, and decoded-pixel limits', async () => {
const storageRoot = await root()
await expect(saveImageFile(storageRoot, {
data: new Uint8Array(0), mediaType: 'image/png',
}, LIMITS)).rejects.toMatchObject({ code: 'INVALID_IMAGE' })
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' })
const unnamed = await saveImageFile(storageRoot, {
data: PNG, mediaType: 'image/png', name: '\u0000',
}, LIMITS)
expect(unnamed).not.toHaveProperty('name')
})
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))
.rejects.toMatchObject({ code: 'ATTACHMENT_CORRUPT' })
await expect(readImageFile(storageRoot, { ...ref, attachmentId: 'bad' as never }))
.rejects.toMatchObject({ code: 'INVALID_ATTACHMENT_REF' })
const missingRoot = await root()
await mkdir(missingRoot, { recursive: true })
await expect(readImageFile(missingRoot, ref))
.rejects.toMatchObject({ code: 'ATTACHMENT_NOT_FOUND' })
const unreadableRoot = await root()
const target = join(unreadableRoot, 'objects', sha256.slice(0, 2), sha256)
await mkdir(target, { recursive: true })
await expect(readImageFile(unreadableRoot, ref))
.rejects.toMatchObject({ code: 'ATTACHMENT_READ_FAILED' })
})
it('rejects conflicting existing objects and reference metadata mismatches', async () => {
const storageRoot = await root()
const sha256 = createHash('sha256').update(PNG).digest('hex')
const target = join(storageRoot, 'objects', sha256.slice(0, 2), sha256)
await mkdir(join(storageRoot, 'objects', sha256.slice(0, 2)), { recursive: true })
await writeFile(target, Uint8Array.of(1, 2, 3))
await expect(saveImageFile(storageRoot, { data: PNG, mediaType: 'image/png' }, LIMITS))
.rejects.toMatchObject({ code: 'ATTACHMENT_CORRUPT' })
await writeFile(target, PNG)
const ref = await saveImageFile(storageRoot, { data: PNG, mediaType: 'image/png' }, LIMITS)
await expect(readImageFile(storageRoot, { ...ref, width: ref.width + 1 }))
.rejects.toMatchObject({ code: 'ATTACHMENT_CORRUPT' })
})
it('maps unexpected publication failures to a stable storage error', async () => {
const storageRoot = await root()
const sha256 = createHash('sha256').update(PNG).digest('hex')
const target = join(storageRoot, 'objects', sha256.slice(0, 2), sha256)
await mkdir(target, { recursive: true })
await expect(saveImageFile(storageRoot, { data: PNG, mediaType: 'image/png' }, LIMITS))
.rejects.toMatchObject({ code: 'ATTACHMENT_WRITE_FAILED' })
})
})