From 27d52a4edb2a2c5073e7b09f3a9425deb1e13bdf Mon Sep 17 00:00:00 2001 From: creatixchu Date: Mon, 14 Sep 2026 10:21:54 +0800 Subject: [PATCH] Update image tests for split adapters and durable offload failures --- .../tests/image-offload.spec.ts | 2 +- .../tests/messages/adapter.spec.ts | 6 ++--- .../llm-deepseek/tests/messages/files.spec.ts | 8 +++---- .../tests/messages/serialize.spec.ts | 23 ++++++++++++------- 4 files changed, 23 insertions(+), 16 deletions(-) diff --git a/packages/compaction/compaction-image-offload/tests/image-offload.spec.ts b/packages/compaction/compaction-image-offload/tests/image-offload.spec.ts index d0130034c9..59ecd36568 100644 --- a/packages/compaction/compaction-image-offload/tests/image-offload.spec.ts +++ b/packages/compaction/compaction-image-offload/tests/image-offload.spec.ts @@ -10,7 +10,7 @@ import type { Agent } from '@deepseek-ai/dsh-agent' import BasicCompactionEngine from '@deepseek-ai/dsh-compaction-basic' import TokenMeter from '@deepseek-ai/dsh-token-meter' import { ImageVariantId } from '@deepseek-ai/dsh-attachment' -import { serializeRequestWithImages } from '@deepseek-ai/dsh-llm-deepseek/src/serialize.ts' +import { serializeRequestWithImages } from '@deepseek-ai/dsh-llm-deepseek/src/protocols/chat-completions/serialize.ts' import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit' import { createAssistantMessage, createToolResultMessage, createUserMessage, IMAGE_OFFLOAD_REQUIRED_CODE, LlmAdapter, LlmError, ToolCallId } from '@deepseek-ai/dsh-llm' import type { ContentBlock, GenerateOptions, StreamChunk } from '@deepseek-ai/dsh-llm' diff --git a/packages/llm/llm-deepseek/tests/messages/adapter.spec.ts b/packages/llm/llm-deepseek/tests/messages/adapter.spec.ts index c88c9b2d18..5ecae3f9e8 100644 --- a/packages/llm/llm-deepseek/tests/messages/adapter.spec.ts +++ b/packages/llm/llm-deepseek/tests/messages/adapter.spec.ts @@ -173,10 +173,10 @@ describe('Cordis provider composition', () => { const model = 'deepseek-v4-flash-vision-exp' const price = () => ctx.llm.imageRequestPricing('deepseek-official', model)! const dummy = { attachmentId: AttachmentId(`sha256:${'a'.repeat(64)}`), width: 1, height: 1, bytes: 3, mediaType: 'image/png' as const } - expect(price().priceImages([dummy])[0]?.text).toBeDefined() + expect(price().priceImages([{ type: 'image', attachment: dummy }])[0]?.text).toBeDefined() await ctx.plugin(LocalAttachments, { dshHome: home }) const attachment = await ctx.attachments.saveImage({ data: await readFile(new URL('fixtures/red.png', import.meta.url)), mediaType: 'image/png' }) - expect(price().priceImages([attachment])[0]?.text).not.toContain('/mounted/image.png') + expect(price().priceImages([{ type: 'image', attachment }])[0]?.text).not.toContain('/mounted/image.png') class MappedFiles extends Service { constructor(context: Context) { super(context, 'fs') } processPathFromHostPath(_path: string) { return '/mounted/image.png' } @@ -185,7 +185,7 @@ describe('Cordis provider composition', () => { const message = user() await chunks(ctx.llm.stream(options({ model, messages: [{ ...message, content: [...message.content, { type: 'image', attachment }] }] }))) expect(JSON.stringify(http.requests[0]?.body)).toContain('/mounted/image.png') - expect(price().priceImages([attachment])[0]?.text).toContain('/mounted/image.png') + expect(price().priceImages([{ type: 'image', attachment }])[0]?.text).toContain('/mounted/image.png') }) async function boot(...args: Parameters) { diff --git a/packages/llm/llm-deepseek/tests/messages/files.spec.ts b/packages/llm/llm-deepseek/tests/messages/files.spec.ts index b07b16b20f..1b5ee31941 100644 --- a/packages/llm/llm-deepseek/tests/messages/files.spec.ts +++ b/packages/llm/llm-deepseek/tests/messages/files.spec.ts @@ -123,7 +123,7 @@ describe('Messages Files requests', () => { await chunks(h.adapter.stream(original)) expect(body(fetchImpl.mock.calls[0]?.[1]).match(/"file_id"/gu)).toHaveLength(2) h.ensureUploaded.mockRejectedValueOnce(new Error('offline')) - await expect(chunks(h.adapter.stream(original))).rejects.toMatchObject({ code: 'IMAGE_OFFLOAD_REQUIRED', offloadImages: 1 }) + await expect(chunks(h.adapter.stream(original))).rejects.toMatchObject({ failure: { code: 'IMAGE_OFFLOAD_REQUIRED', offloadImages: 1 } }) expect(fetchImpl).toHaveBeenCalledTimes(1) h.ensureUploaded.mockRejectedValueOnce(new Error('offline')) await chunks(h.adapter.stream(options({ model, messages: [{ ...user(), content: [ @@ -138,7 +138,7 @@ describe('Messages Files requests', () => { it('reads and prices only the retained occurrences selected by the logged offload', async () => { vi.stubGlobal('fetch', async () => success()) const h = harness({ maxRequestFilesBytes: 3, imageOffloadByteQuantum: 1, maxImagesPerRequest: 2, imageOffloadCountQuantum: 1 }) - const images = [{ type: 'image' as const, attachment: ref, offloaded: true }, { type: 'image' as const, attachment: second }] + const images = [{ type: 'image' as const, attachment: ref, offloaded: true as const }, { type: 'image' as const, attachment: second }] await chunks(h.adapter.stream(options({ model, messages: [{ ...user(), content: images }] }))) expect(h.readImageRequest).toHaveBeenCalledExactlyOnceWith(second, expect.anything(), expect.any(AbortSignal)) expect(h.adapter.imageRequestPricing('deepseek-official', model).priceImages(images).map(image => image.visualTokens)).toEqual([0, expect.any(Number)]) @@ -153,7 +153,7 @@ describe('Messages Files requests', () => { const h = harness(config) const original = request([ref, ref]) const saved = JSON.stringify(original.messages) - await expect(chunks(h.adapter.stream(original))).rejects.toMatchObject({ code: 'IMAGE_OFFLOAD_REQUIRED', offloadImages: 1 }) + await expect(chunks(h.adapter.stream(original))).rejects.toMatchObject({ failure: { code: 'IMAGE_OFFLOAD_REQUIRED', offloadImages: 1 } }) expect(h.readImageRequest).toHaveBeenCalledTimes(1) expect(h.ensureUploaded).not.toHaveBeenCalled() expect(fetchImpl).not.toHaveBeenCalled() @@ -163,7 +163,7 @@ describe('Messages Files requests', () => { it('uses exact prepared bytes when the durable reference fits the Files budget', async () => { const h = harness({ maxRequestFilesBytes: 3, imageOffloadByteQuantum: 1, imageOffloadCountQuantum: 1 }) h.readImageRequest.mockResolvedValue({ ...version(ref), bytes: 4, data: Uint8Array.of(1, 2, 3, 4) }) - await expect(chunks(h.adapter.stream(request()))).rejects.toMatchObject({ code: 'IMAGE_OFFLOAD_REQUIRED', offloadImages: 1 }) + await expect(chunks(h.adapter.stream(request()))).rejects.toMatchObject({ failure: { code: 'IMAGE_OFFLOAD_REQUIRED', offloadImages: 1 } }) expect(h.ensureUploaded).not.toHaveBeenCalled() }) diff --git a/packages/llm/llm-deepseek/tests/messages/serialize.spec.ts b/packages/llm/llm-deepseek/tests/messages/serialize.spec.ts index 347030f16d..ada2e7207b 100644 --- a/packages/llm/llm-deepseek/tests/messages/serialize.spec.ts +++ b/packages/llm/llm-deepseek/tests/messages/serialize.spec.ts @@ -1,7 +1,7 @@ /** Request conversion and durable replay validation. */ import { describe, expect, it, vi } from 'vitest' import { createAssistantMessage, createMessage, createSystemMessage, createToolResultMessage, ReasoningEffortId, ToolCallId } from '@deepseek-ai/dsh-llm' -import type { ContentBlock, GenerateOptions, Message } from '@deepseek-ai/dsh-llm' +import type { ContentBlock, GenerateOptions, ImageBlock, Message } from '@deepseek-ai/dsh-llm' import { AttachmentId, ImageVariantId } from '@deepseek-ai/dsh-attachment' import type { AttachmentStore, ImageAttachmentRef, RequestImageAttachment } from '@deepseek-ai/dsh-attachment' import { resolveAdapterOptions } from '../../src/config.ts' @@ -248,7 +248,7 @@ describe('validated configuration', () => { describe('Messages images', () => { const ref: ImageAttachmentRef = { attachmentId: AttachmentId(`sha256:${'a'.repeat(64)}`), mediaType: 'image/png', width: 1, height: 1, bytes: 3 } - const image: ContentBlock = { type: 'image', attachment: ref } + const image: ImageBlock = { type: 'image', attachment: ref } const version: RequestImageAttachment = { attachment: ref, variantId: ImageVariantId(`sha256:${'b'.repeat(64)}`), mediaType: 'image/png', bytes: 3, data: Uint8Array.of(1, 2, 3), width: 1, height: 1, depth: 'uchar', space: 'srgb', hasAlpha: false } const access = () => ({ readonlyPath: '/workspace/image.png' }) const model = 'deepseek-v4-flash-vision-exp' @@ -264,23 +264,30 @@ describe('Messages images', () => { { type: 'text', text: expect.stringContaining('/workspace/image.png') as string }, { type: 'image', source: { type: 'base64', media_type: 'image/png', data: 'AQID' } }, { type: 'text' }, { type: 'image' }, ] }) - expect(imagePricing(connection, model, access).priceImages([ref])[0]?.visualTokens).toBeGreaterThan(0) - expect(imagePricing(connection, MODEL, access).priceImages([ref])[0]?.visualTokens).toBe(0) + expect(imagePricing(connection, model, access).priceImages([image])[0]?.visualTokens).toBeGreaterThan(0) + expect(imagePricing(connection, MODEL, access).priceImages([image])[0]?.visualTokens).toBe(0) }) - it('offloads an oldest prefix using exact encoded bytes and preserves durable references', async () => { + it('requires logged offload at exact encoded bytes and preserves durable references', async () => { const config = resolveAdapterOptions({ maxInlineRequestImageBytes: 4, inlineImageOffloadByteQuantum: 1, maxImagesPerRequest: 2, imageOffloadCountQuantum: 1, }) const history = [result('a', [image, image])] const prepared = await prepareImages(history, config, model, attachments, access, signal) expect(prepared.messages[0]?.content[0]).toMatchObject({ content: [image, image] }) - expect(inlineImages(prepared.messages, prepared.versions, config, access)[0]?.content[0]).toMatchObject({ content: [{ type: 'text' }, { type: 'image' }] }) + expect(() => inlineImages(prepared.messages, prepared.versions, config)).toThrow(expect.objectContaining({ + failure: expect.objectContaining({ code: 'IMAGE_OFFLOAD_REQUIRED', offloadImages: 1 }) as unknown, + })) + const offloaded: ImageBlock = { ...image, offloaded: true } + const retry = await prepareImages([result('a', [offloaded, image])], config, model, attachments, access, signal) + expect(inlineImages(retry.messages, retry.versions, config)[0]?.content[0]).toMatchObject({ content: [{ type: 'text' }, { type: 'image' }] }) expect(history[0]?.content[0]).toMatchObject({ content: [image, image] }) - expect(imagePricing(config, model, access).priceImages([ref, ref]).map(entry => entry.visualTokens)) + expect(imagePricing(config, model, access).priceImages([image, image]).map(entry => entry.visualTokens)) .toEqual([expect.any(Number), expect.any(Number)]) const large = { readImageRequest: async () => ({ ...version, bytes: 30, data: new Uint8Array(30) }) } as unknown as AttachmentStore const exact = await prepareImages([result('a', [image])], config, model, large, access, signal) - expect(inlineImages(exact.messages, exact.versions, config, access)[0]?.content[0]).toMatchObject({ content: [{ type: 'text' }] }) + expect(() => inlineImages(exact.messages, exact.versions, config)).toThrow(expect.objectContaining({ + failure: expect.objectContaining({ code: 'IMAGE_OFFLOAD_REQUIRED', offloadImages: 1 }) as unknown, + })) }) it('rejects unsupported roles and unavailable image capabilities before HTTP', async () => { const history = [result('a', [image])]