fix(gui): close attachment durability gaps
This commit is contained in:
@@ -3,11 +3,13 @@
|
||||
*/
|
||||
|
||||
import { z } from 'zod'
|
||||
import type { ModelModality } from '@deepseek-ai/dsh-llm'
|
||||
import type { RequestPayload, ResponseValue } from './rpc-map.ts'
|
||||
import type { Wire } from './rpc.schema.ts'
|
||||
import { imageMediaTypeSchema } from './sessions.schema.ts'
|
||||
|
||||
const modalitySchema = z.union([z.literal('text'), z.literal('image')])
|
||||
/** Merge-extensible modality passthrough: declaration merging cannot extend a runtime Zod union. */
|
||||
const modalitySchema = z.string() as unknown as z.ZodType<ModelModality>
|
||||
|
||||
/** host.describe request payload (empty object literal). */
|
||||
export const hostDescribeRequestSchema = z.object({}) satisfies z.ZodType<Wire<RequestPayload<'host.describe'>>>
|
||||
|
||||
@@ -1,12 +1,24 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import type { ApiProxy, HostFrame, MuxFrame } from '../src/api/index.ts'
|
||||
import type { ResponseValue } from '../src/api/rpc-map.ts'
|
||||
import type { ClientResponse, RpcMessage, RpcReceipt, RpcRequest } from '../src/api/rpc.ts'
|
||||
import { RpcId } from '../src/api/rpc.ts'
|
||||
import { toFetchHandler } from '../src/fetch/handler.ts'
|
||||
import { AbstractApiClient, InProcessApiClient } from '../src/fetch/client.ts'
|
||||
|
||||
declare module '@deepseek-ai/dsh-llm' {
|
||||
interface ModelModalityMap {
|
||||
audio: 'audio'
|
||||
}
|
||||
}
|
||||
|
||||
/** Minimal in-memory ApiProxy: echoes rpcIds, scripts one frame per stream. */
|
||||
function fakeApi(overrides: Partial<{ muxFrames: MuxFrame[]; hostFrames: HostFrame[]; crashOn: string }> = {}): ApiProxy {
|
||||
function fakeApi(overrides: Partial<{
|
||||
muxFrames: MuxFrame[]
|
||||
hostFrames: HostFrame[]
|
||||
crashOn: string
|
||||
hostDescription: ResponseValue<'host.describe'>
|
||||
}> = {}): ApiProxy {
|
||||
const muxFrames = overrides.muxFrames ?? [{ type: 'session/subscribed', sessionId: 's1' as never, lastSeq: -1 }]
|
||||
const hostFrames = overrides.hostFrames ?? [{ type: 'host/session-removed', sessionId: 's1' as never }]
|
||||
async function * stream<F>(frames: F[], signal: AbortSignal): AsyncGenerator<RpcRequest<F>> {
|
||||
@@ -45,7 +57,13 @@ function fakeApi(overrides: Partial<{ muxFrames: MuxFrame[]; hostFrames: HostFra
|
||||
},
|
||||
host: {
|
||||
async describe(request) {
|
||||
return { rpcId: request.rpcId, result: { ok: true, value: { version: 'v', cwd: '/w', attachedSessions: 0 } } }
|
||||
return {
|
||||
rpcId: request.rpcId,
|
||||
result: {
|
||||
ok: true,
|
||||
value: overrides.hostDescription ?? { version: 'v', cwd: '/w', attachedSessions: 0 },
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
workspace: {
|
||||
@@ -137,6 +155,40 @@ describe('unary round trip (handler ⇄ client, no network)', () => {
|
||||
expect((await c.host.describe({})).result.ok).toBe(true)
|
||||
})
|
||||
|
||||
it('round-trips a declaration-merged model modality through host.describe', async () => {
|
||||
const c = client(fakeApi({
|
||||
hostDescription: {
|
||||
version: 'v',
|
||||
cwd: '/w',
|
||||
activeModel: {
|
||||
provider: 'future',
|
||||
id: 'audio-model',
|
||||
name: 'Audio Model',
|
||||
inputModalities: ['text', 'audio'],
|
||||
outputModalities: ['audio'],
|
||||
},
|
||||
attachedSessions: 0,
|
||||
},
|
||||
}))
|
||||
|
||||
const response = await c.host.describe({})
|
||||
expect(response.result).toEqual({
|
||||
ok: true,
|
||||
value: {
|
||||
version: 'v',
|
||||
cwd: '/w',
|
||||
activeModel: {
|
||||
provider: 'future',
|
||||
id: 'audio-model',
|
||||
name: 'Audio Model',
|
||||
inputModalities: ['text', 'audio'],
|
||||
outputModalities: ['audio'],
|
||||
},
|
||||
attachedSessions: 0,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('round-trips command.list / command.execute / skill.list through the wire form', async () => {
|
||||
const c = client()
|
||||
const list = await c.commands.list({ sessionId: 's' as never })
|
||||
|
||||
@@ -155,11 +155,32 @@ describe('sessions domain schemas', () => {
|
||||
})
|
||||
|
||||
describe('host domain schemas', () => {
|
||||
it('validates describe request/value', () => {
|
||||
it('validates describe request/value and preserves merge-extensible modalities', () => {
|
||||
expect(hostDescribeRequestSchema.parse({})).toEqual({})
|
||||
const value = hostDescribeValueSchema.parse({ version: '1', cwd: '/x', provider: 'p', model: 'm', attachedSessions: 2 })
|
||||
const value = hostDescribeValueSchema.parse({
|
||||
version: '1',
|
||||
cwd: '/x',
|
||||
provider: 'p',
|
||||
model: 'm',
|
||||
activeModel: {
|
||||
provider: 'p',
|
||||
id: 'm',
|
||||
name: 'Model',
|
||||
inputModalities: ['text', 'audio'],
|
||||
outputModalities: ['text', 'audio'],
|
||||
},
|
||||
attachedSessions: 2,
|
||||
})
|
||||
expect(value.attachedSessions).toBe(2)
|
||||
expect(value.activeModel?.inputModalities).toEqual(['text', 'audio'])
|
||||
expect(value.activeModel?.outputModalities).toEqual(['text', 'audio'])
|
||||
expect(hostDescribeValueSchema.parse({ version: '1', cwd: '/x', attachedSessions: 0 }).provider).toBeUndefined()
|
||||
expect(() => hostDescribeValueSchema.parse({
|
||||
version: '1',
|
||||
cwd: '/x',
|
||||
activeModel: { provider: 'p', id: 'm', name: 'Model', inputModalities: [{ type: 'audio' }] },
|
||||
attachedSessions: 0,
|
||||
})).toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user