Merge current master into status bar projection
This commit is contained in:
@@ -7,6 +7,7 @@ import { Context } from 'cordis'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import type { ConnectionHandle } from '@deepseek-ai/dsh-client-connection/client'
|
||||
import type { ConnectionSinks } from '@deepseek-ai/dsh-client-connection/client'
|
||||
import { SESSION_SEARCH_RESULT_LIMIT } from '@deepseek-ai/dsh-host-apiproxy/api'
|
||||
import * as RuntimeClient from '../src/client/index.ts'
|
||||
import type { SessionsService } from '../src/client/sessions/service.ts'
|
||||
import type { WorkspacesService } from '../src/client/workspaces/service.ts'
|
||||
@@ -50,6 +51,8 @@ describe('runtime client apply', () => {
|
||||
const workspaces = bench.ctx.get('workspaces')
|
||||
expect(sessions !== undefined).toBe(true)
|
||||
expect(workspaces !== undefined).toBe(true)
|
||||
// The bound the wire schema enforces, not a per-connection negotiation.
|
||||
expect((sessions as SessionsService).searchResultLimit).toBe(SESSION_SEARCH_RESULT_LIMIT)
|
||||
if (workspaces === undefined) throw new Error('WorkspacesService missing after runtime apply')
|
||||
expect(bench.sinks).toBeDefined()
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import type { CommandId } from '@deepseek-ai/dsh-commands/brand'
|
||||
import type {
|
||||
ClientResponse, CommandDescriptor, HostFrame, IApiClient, ModelTarget, MuxFrame,
|
||||
RpcError, RpcReceipt, RpcRequest, RpcResponse, SessionId, SessionModels, SkillEntry,
|
||||
RpcError, RpcReceipt, RpcRequest, RpcResponse, SessionId, SessionModels, SessionSearchItem, SkillEntry,
|
||||
WorkspaceId, WorkspaceView,
|
||||
} from '@deepseek-ai/dsh-client-connection/client'
|
||||
import { RpcId } from '@deepseek-ai/dsh-client-connection/client'
|
||||
@@ -61,6 +61,8 @@ export class FakeApiClient implements IApiClient {
|
||||
|
||||
// Programmable slots (defaults answer OK-empty); reassign per case.
|
||||
onList: (payload: unknown) => Promise<RpcResponse<{ items: never[] }>> = () => Promise.resolve(ok({ items: [] }))
|
||||
onSearch: (payload: unknown) => Promise<RpcResponse<{ items: SessionSearchItem[]; hasMore: boolean }>> =
|
||||
() => Promise.resolve(ok({ items: [], hasMore: false }))
|
||||
onCreate: (payload: unknown) => Promise<RpcResponse<{ sessionId: SessionId }>> = () => Promise.resolve(ok({ sessionId: 'fk-new' as SessionId }))
|
||||
readonly defaultModel: ModelTarget = { provider: 'deepseek-official', model: 'deepseek-v4-flash' }
|
||||
onRename: (payload: unknown) => Promise<RpcResponse<{ title: string; seq: number }>> = () => Promise.resolve(ok({ title: 'fk-renamed', seq: 0 }))
|
||||
@@ -106,12 +108,17 @@ export class FakeApiClient implements IApiClient {
|
||||
|
||||
private readonly muxConns: StreamConn<MuxFrame>[] = []
|
||||
private readonly hostConns: StreamConn<HostFrame>[] = []
|
||||
lastSearchSignal: AbortSignal | undefined
|
||||
|
||||
// Parameters carry local structural annotations: the CI lint lane runs
|
||||
// without built lib/, so IApiClient's indexed-access types collapse to any
|
||||
// and inferred parameters would trip no-unsafe-argument.
|
||||
readonly sessions: IApiClient['sessions'] = {
|
||||
list: (payload: unknown) => this.record('session.list', payload, this.onList(payload)),
|
||||
search: (payload: unknown, signal?: AbortSignal) => {
|
||||
this.lastSearchSignal = signal
|
||||
return this.record('session.search', payload, this.onSearch(payload))
|
||||
},
|
||||
create: (payload: unknown) => this.record('session.create', payload, this.onCreate(payload)),
|
||||
history: (payload: { sessionId: SessionId; beforeSeq?: number; maxMessages?: number }) =>
|
||||
this.record('session.history', payload, this.onHistory(payload)),
|
||||
|
||||
@@ -206,6 +206,49 @@ describe('list lifecycle', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('search', () => {
|
||||
it('returns bounded Host results and forwards the caller signal', async () => {
|
||||
const api = new FakeApiClient()
|
||||
api.onSearch = () => Promise.resolve(ok({
|
||||
items: [{ sessionId: S1, snippet: 'matching excerpt' }],
|
||||
hasMore: true,
|
||||
}))
|
||||
const manager = new SessionManager(api)
|
||||
const signal = new AbortController().signal
|
||||
|
||||
await expect(manager.search('exact phrase', signal)).resolves.toEqual({
|
||||
ok: true,
|
||||
value: {
|
||||
items: [{ sessionId: S1, snippet: 'matching excerpt' }],
|
||||
hasMore: true,
|
||||
},
|
||||
})
|
||||
expect(api.callsOf('session.search')).toEqual([{ query: 'exact phrase' }])
|
||||
expect(api.lastSearchSignal).toBe(signal)
|
||||
})
|
||||
|
||||
it('preserves business errors and folds transport failures', async () => {
|
||||
const api = new FakeApiClient()
|
||||
const manager = new SessionManager(api)
|
||||
api.onSearch = () => Promise.resolve(err({
|
||||
code: 'internal',
|
||||
message: 'index unavailable',
|
||||
details: {},
|
||||
}))
|
||||
const signal = new AbortController().signal
|
||||
await expect(manager.search('first', signal)).resolves.toMatchObject({
|
||||
ok: false,
|
||||
error: { code: 'internal', message: 'index unavailable' },
|
||||
})
|
||||
|
||||
api.onSearch = () => Promise.reject(new Error('wire down'))
|
||||
await expect(manager.search('second', signal)).resolves.toMatchObject({
|
||||
ok: false,
|
||||
error: { code: 'internal', message: 'wire down' },
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('host frame routing', () => {
|
||||
it('adds/removes/flips sessions from host frames and keeps removed instances resident', async () => {
|
||||
const api = new FakeApiClient()
|
||||
|
||||
@@ -69,6 +69,29 @@ describe('list store projection', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('search', () => {
|
||||
it('delegates transient content search without changing the list snapshot', async () => {
|
||||
const b = bench()
|
||||
await feedList(b, [{ id: 's1' }])
|
||||
const before = b.svc.list.getSnapshot()
|
||||
b.api.onSearch = () => Promise.resolve(ok({
|
||||
items: [{ sessionId: sid('s1'), snippet: 'matching excerpt' }],
|
||||
hasMore: false,
|
||||
}))
|
||||
const signal = new AbortController().signal
|
||||
|
||||
await expect(b.svc.search('needle', signal)).resolves.toEqual({
|
||||
ok: true,
|
||||
value: {
|
||||
items: [{ sessionId: 's1', snippet: 'matching excerpt' }],
|
||||
hasMore: false,
|
||||
},
|
||||
})
|
||||
expect(b.api.lastSearchSignal).toBe(signal)
|
||||
expect(b.svc.list.getSnapshot()).toBe(before)
|
||||
})
|
||||
})
|
||||
|
||||
describe('scope tree', () => {
|
||||
it('mints lazily on first resolution, tags the ctx, and keeps binding identity stable', async () => {
|
||||
const b = bench()
|
||||
|
||||
Reference in New Issue
Block a user