Files
deepseek-harness/packages/api/session-controller/tests/media-references.host.spec.ts
T

225 lines
9.8 KiB
TypeScript

import { appendFile, mkdir, mkdtemp, open, realpath, rm, symlink, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
import { FsError, FsTargetKey, FsVersion } from '@deepseek-ai/dsh-fs'
import { LocalFileSystem } from '@deepseek-ai/dsh-fs-local'
import { SessionMediaReferences } from '../src/media-references.ts'
const PNG_BYTES = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 1, 2, 3, 4])
const DEFAULT_LIMIT = 20 * 1024 * 1024
async function responseBytes(response: Response): Promise<Uint8Array> {
return new Uint8Array(await response.arrayBuffer())
}
describe('SessionMediaReferences /api/file', () => {
let root: string
const contexts: Context[] = []
beforeEach(async () => {
root = await realpath(await mkdtemp(join(tmpdir(), 'dsh-media-references-')))
})
afterEach(async () => {
await Promise.all(contexts.splice(0).map(ctx => ctx.fiber.dispose()))
await rm(root, { recursive: true, force: true })
})
async function mount(maxBytes = DEFAULT_LIMIT) {
const ctx = new Context()
contexts.push(ctx)
let handler: ((request: Request) => Promise<Response>) | undefined
const unregister = vi.fn(() => {})
ctx.provide('connection', {
fetch: {
register: (registered: { fetch: (request: Request) => Promise<Response> }) => {
handler = registered.fetch
return unregister
},
},
} as never)
ctx.provide('attachments', { imageLimits: { maxImageBytes: maxBytes } } as never)
await ctx.plugin(LocalFileSystem, { cwd: root }).await()
await ctx.plugin(SessionMediaReferences).await()
const raw = (url: string, init?: RequestInit) => {
if (handler === undefined) throw new Error('route not registered')
return handler(new Request(url, init))
}
return {
call: (path: string, init?: RequestInit) => raw(`http://127.0.0.1/api/file?path=${encodeURIComponent(path)}`, init),
raw,
fs: ctx.fs as LocalFileSystem,
unregister,
dispose: () => ctx.fiber.dispose(),
}
}
it('serves the inclusive image cap and refuses larger images for GET, HEAD and Range', async () => {
const route = await mount(PNG_BYTES.length)
const path = join(root, 'bounded.png')
await writeFile(path, PNG_BYTES)
expect(await responseBytes(await route.call(path))).toEqual(PNG_BYTES)
await appendFile(path, new Uint8Array(1))
expect((await route.call(path)).status).toBe(413)
expect((await route.call(path, { headers: { range: 'bytes=0-0' } })).status).toBe(413)
const head = await route.call(path, { method: 'HEAD' })
expect(head.status).toBe(413)
expect(head.body).toBeNull()
})
it('rejects a sparse 1 GiB image before content I/O', async () => {
const route = await mount()
const inspect = vi.fn()
route.fs.internals.inspectReadBytesAfterStat = inspect
const path = join(root, 'huge.png')
const handle = await open(path, 'w')
try {
await handle.truncate(1024 * 1024 * 1024)
} finally {
await handle.close()
}
expect((await route.call(path)).status).toBe(413)
expect(inspect).not.toHaveBeenCalled()
})
it('uses the filesystem byte reader to reject post-stat image growth', async () => {
const route = await mount(PNG_BYTES.length)
const path = join(root, 'growing.png')
await writeFile(path, PNG_BYTES)
route.fs.internals.inspectReadBytesAfterStat = async () => {
await appendFile(path, new Uint8Array(1))
}
expect((await route.call(path)).status).toBe(413)
})
it.each([
['png', 'image/png'], ['svg', 'image/svg+xml'], ['mp4', 'video/mp4'], ['mp3', 'audio/mpeg'],
['txt', 'text/plain'], ['html', 'text/html'], ['bin', 'application/octet-stream'], ['', 'application/octet-stream'],
])('serves .%s files with their MIME type and response protections', async (extension, mediaType) => {
const route = await mount()
const path = join(root, `file${extension === '' ? '' : `.${extension}`}`)
await writeFile(path, PNG_BYTES)
const response = await route.call(path)
expect(response.status).toBe(200)
expect(response.headers.get('content-type')).toBe(mediaType)
expect(response.headers.get('content-length')).toBe(String(PNG_BYTES.length))
expect(response.headers.get('cache-control')).toBe('private, no-store')
expect(response.headers.get('x-content-type-options')).toBe('nosniff')
expect(response.headers.get('content-security-policy')).toBe("sandbox; default-src 'none'")
expect(await responseBytes(response)).toEqual(PNG_BYTES)
})
it.each(['mp4', 'mp3', 'bin'])('applies the attachment byte cap to .%s files', async (extension) => {
const route = await mount(PNG_BYTES.length)
const path = join(root, `file.${extension}`)
await writeFile(path, PNG_BYTES)
expect(await responseBytes(await route.call(path))).toEqual(PNG_BYTES)
await appendFile(path, new Uint8Array(1))
expect((await route.call(path)).status).toBe(413)
expect((await route.call(path, { method: 'HEAD' })).status).toBe(413)
})
it('ignores Range headers and returns complete bodies without advertising ranges', async () => {
const route = await mount()
const path = join(root, 'clip.mp4')
await writeFile(path, PNG_BYTES)
for (const range of ['bytes=0-3', 'bytes=-4', 'bytes=999-', 'bytes=abc', 'items=0-0', 'bytes=0-1,3-4']) {
const response = await route.call(path, { headers: { range } })
expect(response.status).toBe(200)
expect(response.headers.get('accept-ranges')).toBeNull()
expect(response.headers.get('content-range')).toBeNull()
expect(await responseBytes(response)).toEqual(PNG_BYTES)
}
})
it('answers HEAD without reading content and reports missing and non-regular files', async () => {
const route = await mount()
const path = join(root, 'image.png')
await writeFile(path, PNG_BYTES)
const read = vi.spyOn(route.fs, 'readBytes')
const response = await route.call(path, { method: 'HEAD', headers: { range: 'bytes=0-3' } })
expect(response.status).toBe(200)
expect(response.headers.get('content-length')).toBe(String(PNG_BYTES.length))
expect(response.body).toBeNull()
expect(read).not.toHaveBeenCalled()
expect((await route.call(join(root, 'missing'), { method: 'HEAD' })).status).toBe(404)
expect((await route.call(root, { method: 'HEAD' })).status).toBe(403)
vi.spyOn(route.fs, 'stat').mockResolvedValue({ type: 'file', version: FsVersion('v1') })
expect((await route.call(path, { method: 'HEAD' })).headers.get('content-length')).toBeNull()
})
it('rejects malformed paths, absent files, and directories', async () => {
const route = await mount()
expect((await route.raw('http://127.0.0.1/api/file')).status).toBe(400)
for (const path of ['', 'relative.png', '/a\0b.png']) {
expect((await route.call(path)).status).toBe(400)
}
const head = await route.call('', { method: 'HEAD' })
expect(head.status).toBe(400)
expect(head.body).toBeNull()
expect((await route.call(join(root, 'missing.png'))).status).toBe(404)
await mkdir(join(root, 'frames.png'))
expect((await route.call(join(root, 'frames.png'))).status).toBe(403)
})
it('reads files and symlink targets outside the default cwd without a workspace registry', async () => {
const route = await mount()
const outside = await mkdtemp(join(tmpdir(), 'dsh-media-outside-'))
try {
const path = join(outside, 'image.png')
await writeFile(path, PNG_BYTES)
expect(await responseBytes(await route.call(path))).toEqual(PNG_BYTES)
const link = join(root, 'linked.png')
await symlink(path, link)
expect(await responseBytes(await route.call(link))).toEqual(PNG_BYTES)
} finally {
await rm(outside, { recursive: true, force: true })
}
})
it.skipIf(process.platform === 'win32')('rejects a FIFO before opening it', async () => {
const route = await mount()
const path = join(root, 'stream.png')
const { execFile } = await import('node:child_process')
const { promisify } = await import('node:util')
await promisify(execFile)('mkfifo', [path])
expect((await route.call(path)).status).toBe(403)
})
it('reads opaque remote targets through ctx.fs and preserves provider failures', async () => {
const route = await mount()
const target = { targetKey: FsTargetKey('opaque-remote-id'), displayPath: '/remote/photo.png' }
vi.spyOn(route.fs, 'resolve').mockResolvedValue(target)
const read = vi.spyOn(route.fs, 'readBytes').mockResolvedValue(PNG_BYTES)
expect(await responseBytes(await route.call('/remote/photo.png'))).toEqual(PNG_BYTES)
expect(read).toHaveBeenCalledWith(target, expect.any(AbortSignal), DEFAULT_LIMIT)
for (const [code, status] of [
['FS_PERMISSION_DENIED', 403], ['FS_SANDBOX_DENIED', 403], ['FS_NOT_FOUND', 404],
['FS_NOT_REGULAR_FILE', 403], ['FS_TOO_LARGE', 413], ['FS_IO_ERROR', 500],
] as const) {
read.mockRejectedValueOnce(new FsError('provider rejected read', code))
expect((await route.call('/remote/photo.png')).status).toBe(status)
}
read.mockRejectedValueOnce(new Error('provider bug'))
await expect(route.call('/remote/photo.png')).rejects.toThrow('provider bug')
})
it('serves an empty file and respects an aborted request', async () => {
const route = await mount()
const path = join(root, 'empty.png')
await writeFile(path, '')
const response = await route.call(path)
expect(response.headers.get('content-length')).toBe('0')
expect(await response.text()).toBe('')
expect((await route.call(path, { signal: AbortSignal.abort() })).status).toBe(499)
})
it('unregisters the route on disposal', async () => {
const route = await mount()
await route.dispose()
expect(route.unregister).toHaveBeenCalledTimes(1)
})
})