Files
deepseek-harness/packages/llm/llm-pi-ai/tests/context.spec.ts
Yichen Jiang 6601830907 Merge remote-tracking branch 'origin/master' into worktree/web-multimodal-image-input
# Conflicts:
#	apps/cli/README.i18n.yaml
#	docs/core-data-structures/core.i18n.yaml
#	docs/core-data-structures/llm-streaming.i18n.yaml
#	examples/acp-agent/tests/snapshots/cordis-inspect-jsdoc/session.jsonl
#	packages/client/connection/src/client/fixture.ts
#	packages/client/connection/src/client/index.ts
#	packages/client/runtime/README.i18n.yaml
#	packages/client/runtime/README.md
#	packages/client/runtime/README.zh.md
#	packages/client/ui-conversation/README.i18n.yaml
#	packages/client/ui-conversation/src/client/input/hub.ts
#	packages/client/ui-conversation/src/client/service.ts
#	packages/client/ui-conversation/src/client/skeleton/InputBar.tsx
#	packages/compact/compact-basic/src/summarizer.ts
#	packages/compact/compact-basic/tests/compact-basic.spec.ts
#	packages/host/apiproxy/src/api-proxy.ts
#	packages/host/apiproxy/src/api/rpc.schema.ts
#	packages/host/apiproxy/src/api/rpc.ts
#	packages/host/apiproxy/src/api/sessions.ts
#	packages/host/apiproxy/tests/rpc-schemas.spec.ts
#	packages/llm/llm-deepseek/tests/serialize.spec.ts
#	packages/llm/llm-pi-ai/tests/adapter.spec.ts
#	packages/llm/llm-pi-ai/tests/convert.spec.ts
#	packages/llm/llm-pi-ai/tests/provider-apis.e2e.ts
#	packages/llm/token-meter/tests/token-meter.spec.ts
#	packages/ui/tui/README.i18n.yaml
2026-07-29 10:18:53 +08:00

187 lines
6.0 KiB
TypeScript

import { describe, expect, it, vi } from 'vitest'
import { AttachmentId } from '@deepseek-ai/dsh-attachment'
import type { AttachmentStore, ImageAttachmentRef } from '@deepseek-ai/dsh-attachment'
import { CallId, createMessage, createUserMessage } from '@deepseek-ai/dsh-llm'
import type { ContentBlock, GenerateOptions, Message } from '@deepseek-ai/dsh-llm'
import { toPiContext } from '../src/context.ts'
import { toPiAssistant } from '../src/replay.ts'
const ref: ImageAttachmentRef = {
attachmentId: AttachmentId(`sha256:${'a'.repeat(64)}`),
mediaType: 'image/png',
bytes: 1,
width: 1,
height: 1,
}
const attachments = {
readImage: vi.fn(() => Promise.resolve({ ref, data: Uint8Array.of(1) })),
} as unknown as AttachmentStore
function request(messages: GenerateOptions['messages']): GenerateOptions {
return {
provider: 'openai',
model: 'gpt-4.1',
system: 'system prompt',
tools: [{ name: 'lookup', description: 'look up', parameters: { type: 'object' } }],
messages,
}
}
function user(content: ContentBlock[]): Message {
return createUserMessage({ content, source: { kind: 'plugin', plugin: 'test' } })
}
function history(role: 'system' | 'assistant', content: ContentBlock[]): Message {
return createMessage({ role, content, source: { kind: 'plugin', plugin: 'test' } })
}
describe('pi-ai request context conversion', () => {
it('omits absent and empty request-level optional fields', () => {
const base = { provider: 'openai', model: 'gpt-4.1', messages: [] }
expect(toPiContext(base)).toEqual({ messages: [] })
expect(toPiContext({ ...base, tools: [] })).toEqual({ messages: [] })
})
it('converts complete text-only history and rejects nested images without storage', () => {
const callId = CallId('call-1')
expect(toPiContext(request([
history('system', [{ type: 'text', text: 'history system' }]),
history('assistant', [{ type: 'tool-call', id: callId, name: 'lookup', arguments: '{}' }]),
user([
{ type: 'text', text: 'after tool' },
{
type: 'tool-result',
toolCallId: callId,
content: [{ type: 'text', text: '' }],
},
]),
]))).toMatchObject({
systemPrompt: 'system prompt',
tools: [{ name: 'lookup' }],
messages: [
{ role: 'user', content: 'history system' },
{ role: 'assistant' },
{ role: 'user', content: 'after tool' },
{
role: 'toolResult',
toolCallId: 'call-1',
toolName: 'lookup',
content: [{ type: 'text', text: '(no output)' }],
isError: false,
},
],
})
expect(() => toPiContext(request([user([{
type: 'tool-result',
toolCallId: callId,
content: [{ type: 'image', attachment: ref }],
}])]))).toThrow(/durable attachment service/)
})
it('resolves user and tool-result images while preserving explicit fallbacks', async () => {
const callId = CallId('missing-call')
const knownCallId = CallId('known-call')
const context = await toPiContext(request([
user([{ type: 'text', text: '' }]),
history('assistant', [
{ type: 'text', text: 'calling' },
{ type: 'tool-call', id: knownCallId, name: 'lookup', arguments: '{}' },
]),
user([
{ type: 'image', attachment: ref },
{ type: 'text', text: 'caption' },
{ type: 'reasoning', text: 'ignored' },
]),
user([{
type: 'tool-result',
toolCallId: knownCallId,
content: [{ type: 'text', text: '' }],
}]),
user([{
type: 'tool-result',
toolCallId: callId,
isError: true,
content: [
{ type: 'tool-result', toolCallId: callId, content: [] },
{ type: 'image', attachment: ref },
],
}]),
]), attachments)
expect(context.messages).toEqual([
{ role: 'user', content: '', timestamp: 0 },
expect.objectContaining({ role: 'assistant' }),
{
role: 'user',
content: [
{ type: 'image', data: 'AQ==', mimeType: 'image/png' },
{ type: 'text', text: 'caption' },
],
timestamp: 0,
},
{
role: 'toolResult',
toolCallId: 'known-call',
toolName: 'lookup',
content: [{ type: 'text', text: '(no output)' }],
isError: false,
timestamp: 0,
},
{
role: 'toolResult',
toolCallId: 'missing-call',
toolName: 'unknown',
content: [{ type: 'image', data: 'AQ==', mimeType: 'image/png' }],
isError: true,
timestamp: 0,
},
])
})
it('keeps empty text-only users while separating result-only messages', () => {
const callId = CallId('unknown-call')
expect(toPiContext(request([
user([]),
history('assistant', [
{ type: 'text', text: 'answer' },
{ type: 'tool-call', id: CallId('other-call'), name: 'lookup', arguments: '{}' },
]),
user([{
type: 'tool-result',
toolCallId: callId,
content: [{ type: 'text', text: 'result' }],
}]),
]))).toMatchObject({
messages: [
{ role: 'user', content: '' },
{ role: 'assistant' },
{ role: 'toolResult', toolName: 'unknown' },
],
})
})
it('handles in-history system and assistant messages explicitly on the image path', async () => {
await expect(toPiContext(request([
history('system', [{ type: 'image', attachment: ref }]),
]), attachments)).rejects.toMatchObject({ code: 'UNSUPPORTED_CONTENT' })
await expect(toPiContext(request([
history('system', [{ type: 'text', text: 'history system' }]),
history('assistant', [{ type: 'text', text: 'answer' }]),
user([{ type: 'text', text: 'plain' }]),
]), attachments)).resolves.toMatchObject({
messages: [
{ role: 'user', content: 'history system' },
{ role: 'assistant' },
{ role: 'user', content: 'plain' },
],
})
expect(() => toPiAssistant(
history('assistant', [{ type: 'image', attachment: ref }]),
)).toThrow(/assistant image output/)
})
})