fix: harden Web image admission
This commit is contained in:
@@ -13,6 +13,7 @@ import { SessionInputShell } from '../src/client/input/facade.ts'
|
||||
import { InputBar } from '../src/client/skeleton/InputBar.tsx'
|
||||
import type { InputBarProps } from '../src/client/skeleton/InputBar.tsx'
|
||||
import type { ComposerAttachment } from '../src/client/contract/slots.ts'
|
||||
import type { DraftAttachmentId } from '../src/client/input/contract.ts'
|
||||
|
||||
afterEach(cleanup)
|
||||
|
||||
@@ -78,7 +79,7 @@ function bench(over?: BenchOptions) {
|
||||
promptError: over?.promptError ?? null,
|
||||
}))
|
||||
const stop = vi.fn()
|
||||
const removeImage = vi.fn((id: string) => { shell.removeImage(id) })
|
||||
const removeImage = vi.fn((id: DraftAttachmentId) => { shell.removeImage(id) })
|
||||
const slotCalls: { key: string; owner: unknown }[] = []
|
||||
const renderSlot = ((key: string, owner: object) => {
|
||||
slotCalls.push({ key, owner })
|
||||
@@ -523,7 +524,7 @@ describe('image draft rail', () => {
|
||||
|
||||
it('allows image-only send, removes a thumbnail, and opens original preview on double-click', () => {
|
||||
const file = new File([Uint8Array.of(1)], 'pixel.png', { type: 'image/png' })
|
||||
const attachment = { kind: 'image' as const, id: 'draft-1', file, previewUrl: 'blob:draft-1' }
|
||||
const attachment = { kind: 'image' as const, id: 'draft-1' as DraftAttachmentId, file, previewUrl: 'blob:draft-1' }
|
||||
const { view, textarea, sink, removeImage } = bench({ attachments: [attachment] })
|
||||
const send = view.getByRole('button', { name: 'Send message' }) as HTMLButtonElement
|
||||
expect(send.disabled).toBe(false)
|
||||
@@ -536,7 +537,7 @@ describe('image draft rail', () => {
|
||||
|
||||
it('opens the original preview on double-click and closes it with Escape', () => {
|
||||
const file = new File([Uint8Array.of(1)], 'pixel.png', { type: 'image/png' })
|
||||
const attachment = { kind: 'image' as const, id: 'draft-1', file, previewUrl: 'blob:draft-1' }
|
||||
const attachment = { kind: 'image' as const, id: 'draft-1' as DraftAttachmentId, file, previewUrl: 'blob:draft-1' }
|
||||
const { view } = bench({ attachments: [attachment] })
|
||||
fireEvent.doubleClick(view.getByTitle('双击查看原图'))
|
||||
expect(view.getByRole('dialog', { name: '原图预览' })).toBeTruthy()
|
||||
|
||||
@@ -6,17 +6,19 @@
|
||||
import { Context } from 'cordis'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { SlotTestRuntime } from '@deepseek-ai/dsh-client-test-runtime'
|
||||
import { ConversationService } from '@deepseek-ai/dsh-client-ui-conversation/client'
|
||||
import { AttachmentId } from '@deepseek-ai/dsh-attachment'
|
||||
import type { SessionFace } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import { InputHub } from '../src/client/input/hub.ts'
|
||||
import { ConversationService } from '../src/client/service.ts'
|
||||
|
||||
async function bench() {
|
||||
async function bench(readAttachment?: SessionFace['readAttachment']) {
|
||||
const runtime = await SlotTestRuntime.create()
|
||||
const prompt = vi.fn(() => Promise.resolve({ ok: true as const, value: { accepted: true as const } }))
|
||||
const cancel = vi.fn(() => Promise.resolve({ ok: true as const, value: { accepted: true as const } }))
|
||||
const loadOlder = vi.fn(() => Promise.resolve())
|
||||
await runtime.sessions.add({
|
||||
id: 's1',
|
||||
session: { prompt, cancel, loadOlder },
|
||||
session: { prompt, cancel, loadOlder, ...(readAttachment === undefined ? {} : { readAttachment }) },
|
||||
})
|
||||
// config.input is required (the apply shares its hub with the inject
|
||||
// factories); the bench passes its own instance explicitly.
|
||||
@@ -25,7 +27,7 @@ async function bench() {
|
||||
await fiber.await()
|
||||
const root = runtime.ctx.get('conversation') as ConversationService
|
||||
const scoped = runtime.sessions.scope('s1')!.get('conversation') as ConversationService
|
||||
return { runtime, hub, root, scoped, prompt, cancel, loadOlder }
|
||||
return { runtime, fiber, hub, root, scoped, prompt, cancel, loadOlder }
|
||||
}
|
||||
|
||||
describe('ConversationService', () => {
|
||||
@@ -122,6 +124,37 @@ describe('ConversationService', () => {
|
||||
await b.runtime.dispose()
|
||||
})
|
||||
|
||||
it('does not publish a historical image URL after disposal', async () => {
|
||||
let resolveRead!: (result: Awaited<ReturnType<SessionFace['readAttachment']>>) => void
|
||||
const readAttachment: SessionFace['readAttachment'] = vi.fn(() => new Promise<Awaited<ReturnType<SessionFace['readAttachment']>>>(
|
||||
(resolve) => { resolveRead = resolve },
|
||||
))
|
||||
const b = await bench(readAttachment)
|
||||
const created = vi.spyOn(URL, 'createObjectURL')
|
||||
const sessionId = b.runtime.sessions.behavior('s1').sessionId
|
||||
const attachment = {
|
||||
attachmentId: AttachmentId('image-1'),
|
||||
mediaType: 'image/png',
|
||||
bytes: 1,
|
||||
width: 1,
|
||||
height: 1,
|
||||
} as const
|
||||
const pending = b.root.resolveImage(sessionId, attachment)
|
||||
await b.fiber.dispose()
|
||||
await expect(b.root.resolveImage(sessionId, attachment)).rejects.toThrow('service is disposed')
|
||||
resolveRead({
|
||||
ok: true,
|
||||
value: {
|
||||
attachment,
|
||||
data: Uint8Array.of(1),
|
||||
},
|
||||
})
|
||||
await expect(pending).rejects.toThrow('service was disposed before loading completed')
|
||||
expect(created).not.toHaveBeenCalled()
|
||||
created.mockRestore()
|
||||
await b.runtime.dispose()
|
||||
})
|
||||
|
||||
it('fails loudly from the root scope, on an unbound session, or without SessionsService', async () => {
|
||||
const b = await bench()
|
||||
await expect(b.root.send('x', 'queue')).rejects.toThrow(/requires a session scope/)
|
||||
|
||||
Reference in New Issue
Block a user