diff --git a/packages/client/file-upload/README.i18n.yaml b/packages/client/file-upload/README.i18n.yaml index 123e508897..4dfc341b3f 100644 --- a/packages/client/file-upload/README.i18n.yaml +++ b/packages/client/file-upload/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/client/file-upload/README.md -README.md: d7cac384c1cef4f52f4f7c09838697d53bba7c4d -README.zh.md: 6ced413d1813c1808daef10e0cf283cd973eff68 +README.md: 9abd4106dbd7cd1cb9671f8f830952f6faff11c6 +README.zh.md: 4ad14ca5a91acd482f54cab74fb974208a264a5a diff --git a/packages/client/file-upload/README.md b/packages/client/file-upload/README.md index d7cac384c1..9abd4106db 100644 --- a/packages/client/file-upload/README.md +++ b/packages/client/file-upload/README.md @@ -52,6 +52,8 @@ The Client plugin provides one inherited `ctx.fileUpload` service. Its provider +**Runtime invariant:** No companion is published. Each request uses exactly one selected carrier, and fixture or unsupported-browser requests fail before the body is sent. + ----- diff --git a/packages/client/file-upload/README.zh.md b/packages/client/file-upload/README.zh.md index 6ced413d18..4ad14ca5a9 100644 --- a/packages/client/file-upload/README.zh.md +++ b/packages/client/file-upload/README.zh.md @@ -52,6 +52,8 @@ Client 插件提供可被各级上下文继承的 `ctx.fileUpload` 服务。其 +**运行时不变式:** 不发布伴生入口。每个请求只使用一个已选定载体;fixture 或不支持的浏览器请求会在发送请求体前失败。 + ----- diff --git a/packages/client/file-upload/tests/file-upload.client.spec.ts b/packages/client/file-upload/tests/file-upload.client.spec.ts index dc71993334..0545b6ce29 100644 --- a/packages/client/file-upload/tests/file-upload.client.spec.ts +++ b/packages/client/file-upload/tests/file-upload.client.spec.ts @@ -124,7 +124,7 @@ describe('file upload worker body', () => { fileUploadWorker( scope, () => { throw new Error('unused') }, - () => Promise.reject('offline'), + () => Promise.reject(new Error('offline')), ) scope.onmessage?.({ data: { url: '/upload', body, headers: {} } } as never) await vi.waitFor(() => { @@ -190,9 +190,10 @@ describe('file upload service', () => { const ctx = new Context() const fiber = ctx.plugin({ apply }) await fiber - await ctx.fileUpload.post({ path: '/fallback', body: new Blob() }) + const body = new Blob() + await ctx.fileUpload.post({ path: '/fallback', body }) expect(fetch).toHaveBeenCalledWith(new URL('http://dsh.internal/fallback'), { - method: 'POST', body: expect.any(Blob), + method: 'POST', body, }) await fiber.dispose() }) diff --git a/packages/client/ui-conversation/tests/service-orchestration.client.spec.ts b/packages/client/ui-conversation/tests/service-orchestration.client.spec.ts index 806fb4250d..3582e0af61 100644 --- a/packages/client/ui-conversation/tests/service-orchestration.client.spec.ts +++ b/packages/client/ui-conversation/tests/service-orchestration.client.spec.ts @@ -6,7 +6,9 @@ import { Context } from '@deepseek-ai/cordis' import { describe, expect, it, vi } from 'vitest' import { makeTranslate, RemoteError, SlotTestRuntime } from '@deepseek-ai/dsh-client-test-runtime' -import type { QueuedMessage } from '@deepseek-ai/dsh-api-session-controller/client' +import type { + BeginSubmissionInput, PendingSubmissionRetirement, QueuedMessage, +} from '@deepseek-ai/dsh-api-session-controller/client' import { ComposerBlockRegistry } from '../src/client/input/blocks.ts' import { InputHub } from '../src/client/input/hub.ts' import { ConversationController } from '../src/client/service.ts' @@ -453,9 +455,9 @@ describe('sendSession submission echo', () => { /** Bench with an observable beginSubmission on the session face. */ async function echoBench() { const b = await bench() - const retire: { onRetire?: ((retirement: unknown) => void) | undefined } = {} + const retire: { onRetire?: ((retirement: PendingSubmissionRetirement) => void) | undefined } = {} const abandon = vi.fn() - const beginSubmission = vi.fn((input: { onRetire?: (retirement: unknown) => void }) => { + const beginSubmission = vi.fn((input: BeginSubmissionInput) => { retire.onRetire = input.onRetire return { requestId: 'req-echo' as never, abandon } }) @@ -480,14 +482,12 @@ describe('sendSession submission echo', () => { const session = b.runtime.sessions.binding('s1')!.session const sending = b.root.sendSession(session, '带图', [attachment!.id], 'queue') // Synchronous: the echo is registered before any encoding starts. - expect(b.beginSubmission).toHaveBeenCalledWith(expect.objectContaining({ - mode: 'queue', - text: '带图', - attachments: [{ - type: 'image', - value: expect.objectContaining({ previewUrl: 'blob:echo-1', name: 'a.png' }), - }], - })) + const echo = b.beginSubmission.mock.calls[0]?.[0] + expect(echo?.mode).toBe('queue') + expect(echo?.text).toBe('带图') + expect(echo?.attachments).toHaveLength(1) + expect(echo?.attachments[0]?.type).toBe('image') + expect(echo?.attachments[0]?.value).toMatchObject({ previewUrl: 'blob:echo-1', name: 'a.png' }) expect(b.prompt).not.toHaveBeenCalled() await vi.waitFor(() => { expect(b.prompt).toHaveBeenCalledOnce() }) expect(b.prompt).toHaveBeenCalledWith( @@ -531,14 +531,15 @@ describe('sendSession submission echo', () => { expect(b.root.fileUploads.getSnapshot()[drafts[1]!.id]?.status).toBe('ready') }) const sending = b.root.sendSession(session, 'ordered', drafts.map(draft => draft.id), 'steer') - expect(b.beginSubmission).toHaveBeenCalledWith(expect.objectContaining({ - mode: 'steer', - attachments: [ - { type: 'image', value: expect.objectContaining({ name: 'first.png' }) }, - { type: 'file', value: { attachmentId: 'mixed-file', name: 'notes.txt', bytes: 1 } }, - { type: 'image', value: expect.objectContaining({ name: 'last.png' }) }, - ], - })) + const echo = b.beginSubmission.mock.calls[0]?.[0] + expect(echo?.mode).toBe('steer') + expect(echo?.attachments.map(attachment => attachment.type === 'image' + ? { type: attachment.type, name: attachment.value.name } + : { type: attachment.type, value: attachment.value })).toEqual([ + { type: 'image', name: 'first.png' }, + { type: 'file', value: { attachmentId: 'mixed-file', name: 'notes.txt', bytes: 1 } }, + { type: 'image', name: 'last.png' }, + ]) await vi.waitFor(() => { expect(b.prompt).toHaveBeenCalledOnce() }) expect(b.prompt.mock.calls[0]?.[0]).toEqual([ { type: 'image', mediaType: 'image/png', data: expect.any(String) as string, name: 'first.png' }, @@ -549,9 +550,21 @@ describe('sendSession submission echo', () => { b.retire.onRetire?.({ reason: 'observed', attachments: [ - { attachmentId: 'image-first', mediaType: 'image/png' }, - { attachmentId: 'mixed-file', name: 'notes.txt', bytes: 1 }, - { attachmentId: 'image-last', mediaType: 'image/png' }, + { + attachmentId: 'image-first' as never, + mediaType: 'image/png', + bytes: 1, + width: 1, + height: 1, + }, + { attachmentId: 'mixed-file' as never, name: 'notes.txt', bytes: 1 }, + { + attachmentId: 'image-last' as never, + mediaType: 'image/png', + bytes: 1, + width: 1, + height: 1, + }, ], }) await expect(sending).resolves.toEqual({ kind: 'success' }) @@ -598,7 +611,11 @@ describe('sendSession submission echo', () => { const sending = b.root.sendSession(session, '', [attachment!.id], 'queue') await vi.waitFor(() => { expect(b.prompt).toHaveBeenCalledOnce() }) const ref = { - attachmentId: 'att-1', mediaType: 'image/png', bytes: 1, width: 1, height: 1, + attachmentId: 'att-1' as never, + mediaType: 'image/png' as const, + bytes: 1, + width: 1, + height: 1, } b.retire.onRetire?.({ reason: 'observed', attachments: [ref] }) await expect(sending).resolves.toEqual({ kind: 'success' })