65 lines
2.9 KiB
TypeScript
65 lines
2.9 KiB
TypeScript
// @vitest-environment jsdom
|
|
import { Context } from 'cordis'
|
|
import { describe, expect, it, vi } from 'vitest'
|
|
import { scopeOf } from '@deepseek-ai/dsh-client-runtime/client'
|
|
import type { SessionId, SessionsService } from '@deepseek-ai/dsh-client-runtime/client'
|
|
import { ConversationService } from '@deepseek-ai/dsh-client-ui-conversation/client'
|
|
|
|
const sid = (id: string) => id as SessionId
|
|
const SCOPE_TAG: symbol = (() => {
|
|
const reads: (string | symbol)[] = []
|
|
const proxy = new Proxy(new Context(), {
|
|
get(target, property, receiver): unknown {
|
|
reads.push(property)
|
|
return Reflect.get(target, property, receiver)
|
|
},
|
|
})
|
|
void scopeOf(proxy)
|
|
return reads.find((value): value is symbol => typeof value === 'symbol')!
|
|
})()
|
|
|
|
async function bench(withSessions = true) {
|
|
const ctx = new Context()
|
|
const prompt = vi.fn(() => Promise.resolve({ ok: true as const, value: { accepted: true as const } }))
|
|
const cancel = vi.fn(() => Promise.resolve({ ok: true as const, value: { accepted: true as const } }))
|
|
const loadOlder = vi.fn(() => Promise.resolve())
|
|
const sessions = {
|
|
binding: (sessionId: SessionId) => ({
|
|
sessionId, session: { prompt, cancel, loadOlder },
|
|
}),
|
|
scopeOf,
|
|
} as unknown as SessionsService
|
|
if (withSessions) ctx.provide('sessions', sessions)
|
|
await ctx.plugin(ConversationService).await()
|
|
const root = ctx.get('conversation') as ConversationService
|
|
const scoped = ctx.plugin(() => {}).ctx.extend({ [SCOPE_TAG]: sid('s1') }).get('conversation') as ConversationService
|
|
return { root, scoped, prompt, cancel, loadOlder }
|
|
}
|
|
|
|
describe('ConversationService', () => {
|
|
it('routes operations through the public Session binding', async () => {
|
|
const b = await bench()
|
|
await b.scoped.send('hello', 'steer')
|
|
await b.scoped.cancel()
|
|
await b.scoped.loadOlder()
|
|
expect(b.prompt).toHaveBeenCalledWith([{ type: 'text', text: 'hello' }], 'steer')
|
|
expect(b.cancel).toHaveBeenCalledOnce()
|
|
expect(b.loadOlder).toHaveBeenCalledOnce()
|
|
})
|
|
|
|
it('folds Session business failures into callback rejections', async () => {
|
|
const b = await bench()
|
|
b.prompt.mockResolvedValueOnce({ ok: false, error: { code: 'agent-busy', message: 'busy', details: {} } } as never)
|
|
await expect(b.scoped.send('x', 'queue')).rejects.toThrow('conversation.send failed: agent-busy: busy')
|
|
b.cancel.mockResolvedValueOnce({ ok: false, error: { code: 'internal', message: 'nope', details: {} } } as never)
|
|
await expect(b.scoped.cancel()).rejects.toThrow('conversation.cancel failed: internal: nope')
|
|
})
|
|
|
|
it('fails loudly from the root scope or without SessionsService', async () => {
|
|
const b = await bench()
|
|
await expect(b.root.send('x', 'queue')).rejects.toThrow(/requires a session scope/)
|
|
const missing = await bench(false)
|
|
await expect(missing.root.send('x', 'queue')).rejects.toThrow(/sessions service unavailable/)
|
|
})
|
|
})
|