Files
deepseek-harness/packages/ui/acp/tests/codec.spec.ts
T
Tianyi Cui 2745879132 refactor(llm): drop the image content block until a path can honor it
ImageBlock had no production producer and every consumer dropped it:
the deepseek serializer skipped it, the pi-ai converter skipped it as
unrepresentable, the ACP bridge neither advertises image prompt
capability nor forwards image blocks, and compact-basic charged a flat
85-token estimate and rendered an [image] placeholder. A block
constructed today would silently vanish from the wire — the vocabulary
advertised a capability no path honors, the silent-data-loss shape the
defensive patterns warn against. The only constructors were tests
pinning the skip/estimate branches.

Remove ImageBlock and its ContentBlockMap entry (its cache?: CacheHint
field leaves with it; CacheHint itself and the other two cache? fields
are out of scope). compact-basic loses its explicit image estimate and
placeholder arms (the merge-extensible default arms absorb the case);
the deepseek serializer, pi-ai converter, and ACP codec already handled
image in their default arms, so only their image-naming comments
change. The codec's inbound rejection of ACP-protocol image prompt
content stays — that guards wire content a client can send regardless
of our vocabulary.

Tests that constructed harness image blocks to pin the removed branches
are dropped (the 85-token estimate pin) or retargeted onto plugin-added
block types / other non-text blocks, which the surviving default arms
own. Docs, the type-equiv pastes, and the content-block vocabulary
RFC's block list and multimodal-home consequence are updated in the
same change; the RFC moves to implemented/ and the index is
regenerated. A real multimodal feature reintroduces image via
declaration merging together with the adapter mapping, ACP
advertisement, and compaction pricing that honor it.
2026-07-04 17:21:13 +08:00

70 lines
3.2 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import type { ContentBlock } from '@deepseek-ai/dsh-llm'
import type { TurnEndReason } from '@deepseek-ai/dsh-session'
import type { ContentBlock as AcpContentBlock } from '@agentclientprotocol/sdk'
import {
acpPromptToText,
harnessBlockToAcpContent,
promptHasUnsupportedContent,
turnEndToStopReason,
} from '../src/codec.ts'
describe('turnEndToStopReason', () => {
// The SDK rejects an unknown stopReason, so this must be total over every
// TurnEndReason kind and always produce a legal wire value.
it('maps every known TurnEndReason kind to a legal StopReason', () => {
expect(turnEndToStopReason({ kind: 'completed' })).toBe('end_turn')
expect(turnEndToStopReason({ kind: 'max-tokens' })).toBe('max_tokens')
expect(turnEndToStopReason({ kind: 'aborted', reason: 'x' })).toBe('cancelled')
expect(turnEndToStopReason({ kind: 'disposed' })).toBe('cancelled')
expect(turnEndToStopReason({ kind: 'rejected', reason: 'blocked by hook' })).toBe('cancelled')
expect(turnEndToStopReason({ kind: 'error', step: 1, message: 'boom' })).toBe('end_turn')
})
it('falls back to end_turn for an unknown (merge-extensible) future kind', () => {
// A plugin-added TurnEndReason variant the bridge does not yet know about
// must still produce a legal wire value, not throw into the SDK.
const future = { kind: 'refusal' } as unknown as TurnEndReason
expect(turnEndToStopReason(future)).toBe('end_turn')
})
})
describe('harnessBlockToAcpContent', () => {
it('maps a text block to ACP text content', () => {
expect(harnessBlockToAcpContent({ type: 'text', text: 'hi' })).toEqual({ type: 'text', text: 'hi' })
})
it('returns undefined for non-text blocks (reasoning / plugin-added)', () => {
expect(harnessBlockToAcpContent({ type: 'reasoning', text: 'think' })).toBeUndefined()
expect(harnessBlockToAcpContent({ type: 'chart', data: 'x' } as unknown as ContentBlock)).toBeUndefined()
})
})
describe('acpPromptToText', () => {
it('concatenates text blocks and renders resource links explicitly', () => {
const prompt: AcpContentBlock[] = [
{ type: 'text', text: 'hello ' },
{ type: 'resource_link', uri: 'file:///x', name: 'x' },
{ type: 'text', text: 'world' },
]
expect(acpPromptToText(prompt)).toBe('hello \n[resource_link name="x" uri="file:///x"]\nworld')
})
it('returns empty string for a prompt with no text blocks', () => {
expect(acpPromptToText([{ type: 'image', mimeType: 'image/png', data: 'AA==' }])).toBe('')
})
})
describe('promptHasUnsupportedContent', () => {
it('detects image, audio, and embedded resource blocks', () => {
expect(promptHasUnsupportedContent([{ type: 'image', mimeType: 'image/png', data: 'AA==' }])).toBe(true)
expect(promptHasUnsupportedContent([{ type: 'audio', mimeType: 'audio/wav', data: 'AA==' }])).toBe(true)
expect(promptHasUnsupportedContent([{ type: 'resource', resource: { uri: 'file:///x', text: 'x' } }])).toBe(true)
})
it('passes baseline text and resource_link prompt blocks', () => {
expect(promptHasUnsupportedContent([{ type: 'text', text: 'hi' }])).toBe(false)
expect(promptHasUnsupportedContent([{ type: 'resource_link', uri: 'file:///x', name: 'x' }])).toBe(false)
})
})