Merge remote-tracking branch 'origin/master' into dshw/pr-2250
# Conflicts: # packages/client/connection/tests/fake-api.client.ts # packages/client/runtime/tests/manager.client.spec.ts # packages/client/runtime/tests/workspaces-service.client.spec.ts # packages/host/apiproxy/tests/rpc-schemas.spec.ts
This commit is contained in:
@@ -5,8 +5,8 @@
|
||||
*/
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import type { ConnectionHandle } from '@deepseek-ai/dsh-client-connection/client'
|
||||
import type { ConnectionSinks } from '@deepseek-ai/dsh-client-connection/client'
|
||||
import type { ConnectionHandle } from '@deepseek-ai/dsh-api-remotes/client'
|
||||
import type { ConnectionSinks } from '@deepseek-ai/dsh-api-remotes/client'
|
||||
import { SESSION_SEARCH_RESULT_LIMIT } from '@deepseek-ai/dsh-host-apiproxy/api'
|
||||
import TypertRegistry from '@deepseek-ai/dsh-typert-registry'
|
||||
import * as RuntimeClient from '../src/client/index.ts'
|
||||
@@ -14,7 +14,7 @@ import type { ConversationNodeDefinition } from '../src/client/contract/conversa
|
||||
import { Session } from '../src/client/sessions/session.ts'
|
||||
import type { SessionsService } from '../src/client/sessions/service.ts'
|
||||
import type { WorkspacesService } from '../src/client/workspaces/service.ts'
|
||||
import { FakeApiClient, ok } from './fake-api.ts'
|
||||
import { FakeApiClient, fakeRemote, ok } from './fake-api.client.ts'
|
||||
|
||||
interface Bench {
|
||||
ctx: Context
|
||||
@@ -45,6 +45,7 @@ async function mount(): Promise<Bench> {
|
||||
}
|
||||
ctx.reflect.provide('connection', handle)
|
||||
ctx.reflect.provide('remote', {})
|
||||
ctx.reflect.provide('remote.commands', fakeRemote().commands)
|
||||
await ctx.plugin(RuntimeClient).await()
|
||||
return bench
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import type { SessionId } from '@deepseek-ai/dsh-client-connection/client'
|
||||
import type { SessionId } from '@deepseek-ai/dsh-api-remotes/client'
|
||||
import { ConversationEventRegistry } from '../src/client/conversation/event-registry.ts'
|
||||
import { ConversationViewRegistry } from '../src/client/conversation/view-registry.ts'
|
||||
import type {
|
||||
@@ -8,7 +8,7 @@ import type {
|
||||
} from '../src/client/contract/conversation.ts'
|
||||
import { Session } from '../src/client/sessions/session.ts'
|
||||
import { SessionsService } from '../src/client/sessions/service.ts'
|
||||
import { FakeApiClient, ok } from './fake-api.ts'
|
||||
import { FakeApiClient, fakeRemote, ok } from './fake-api.client.ts'
|
||||
|
||||
function eventDefinition(kind: string): ConversationNodeDefinition<null> {
|
||||
return {
|
||||
@@ -145,7 +145,7 @@ describe('Conversation registries', () => {
|
||||
api.onList = () => Promise.resolve(ok({
|
||||
items: [{ sessionId, updatedAt: 1, running: false, blank: true }],
|
||||
}) as never)
|
||||
const sessions = new SessionsService(ctx, api)
|
||||
const sessions = new SessionsService(ctx, api, fakeRemote())
|
||||
await sessions.refresh()
|
||||
await Promise.resolve()
|
||||
sessions.scope(sessionId)
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { AttachmentId } from '@deepseek-ai/dsh-attachment'
|
||||
import type { ContentBlock } from '@deepseek-ai/dsh-client-connection/client'
|
||||
import type { ContentBlock } from '@deepseek-ai/dsh-api-remotes/client'
|
||||
import { toAssistantBlock, toAssistantBlocks } from '../src/client/sessions/conversation.ts'
|
||||
|
||||
describe('toAssistantBlock', () => {
|
||||
@@ -1,13 +1,13 @@
|
||||
// Test-local programmable IApiClient fake (NOT the fixture: fixture is a demo
|
||||
// data source on a real clock; behavior tests need per-case responses and
|
||||
// deferred-controlled timing). Streams are hand pumps: pushMux/pushHost.
|
||||
import type { CommandId } from '@deepseek-ai/dsh-commands/brand'
|
||||
import type {
|
||||
ClientResponse, CommandDescriptor, HostFrame, IApiClient, ModelSelection, MuxFrame,
|
||||
ClientResponse, HostFrame, IApiClient, ModelSelection, MuxFrame,
|
||||
RpcError, RpcReceipt, RpcRequest, RpcResponse, SessionId, SessionModels, SessionSearchItem, SkillEntry,
|
||||
WorkspaceId, WorkspaceView,
|
||||
} from '@deepseek-ai/dsh-client-connection/client'
|
||||
} from '@deepseek-ai/dsh-api-remotes/client'
|
||||
import { RpcId } from '@deepseek-ai/dsh-client-connection/client'
|
||||
import type { SessionRemotes } from '../src/client/sessions/remotes.ts'
|
||||
|
||||
/** Programmable-default workspace row (branded id, ISO-ish times). */
|
||||
function fakeWorkspace(id: string, over: Partial<WorkspaceView> = {}): WorkspaceView {
|
||||
@@ -55,6 +55,20 @@ interface StreamConn<F> {
|
||||
feed(item: StreamItem<F>): void
|
||||
}
|
||||
|
||||
/**
|
||||
* Commands Remote double: the generated face delivers the carrier's outcome, so
|
||||
* a test that programs nothing sees an empty catalog and an unmatched line.
|
||||
* @returns the Remote namespaces the session cluster calls.
|
||||
*/
|
||||
export function fakeRemote(): SessionRemotes {
|
||||
return {
|
||||
commands: {
|
||||
list: () => Promise.resolve({ ok: true, value: [] }),
|
||||
execute: () => Promise.resolve({ ok: true, value: undefined }),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export class FakeApiClient implements IApiClient {
|
||||
/** Chronological call record: [method, payload]. */
|
||||
readonly calls: { method: string; payload: unknown }[] = []
|
||||
@@ -210,19 +224,10 @@ export class FakeApiClient implements IApiClient {
|
||||
// Payloads stay `unknown` (lint-lane note above); response rows are the real
|
||||
// wire shapes so cases can program requires-bearing catalogs and dual-address
|
||||
// skill lists without casts.
|
||||
onCommandList: (payload: unknown) => Promise<RpcResponse<{ commands: CommandDescriptor[] }>>
|
||||
= () => Promise.resolve(ok({ commands: [] }))
|
||||
onCommandExecute: (payload: unknown) => Promise<RpcResponse<{ matched: boolean; commandId?: CommandId }>>
|
||||
= () => Promise.resolve(ok({ matched: false }))
|
||||
onSkillList: (payload: unknown) => Promise<RpcResponse<{ skills: SkillEntry[] }>>
|
||||
= () => Promise.resolve(ok({ skills: [] }))
|
||||
|
||||
|
||||
readonly commands: IApiClient['commands'] = {
|
||||
list: (payload: unknown) => this.record('command.list', payload, this.onCommandList(payload)),
|
||||
execute: (payload: unknown) => this.record('command.execute', payload, this.onCommandExecute(payload)),
|
||||
}
|
||||
|
||||
readonly agentPresets: IApiClient['agentPresets'] = {
|
||||
list: (payload: unknown) => this.record('agentPreset.list', payload, Promise.resolve(ok({ presets: [], authorable: false, hasDocument: false }))),
|
||||
select: (payload: { agentPreset: string }) =>
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import type { SessionId, SessionSummary } from '@deepseek-ai/dsh-client-connection/client'
|
||||
import type { SessionId, SessionSummary } from '@deepseek-ai/dsh-api-remotes/client'
|
||||
import { flattenLineage } from '../src/client/sessions/lineage.ts'
|
||||
|
||||
const s = (id: string, updatedAt: number, parent?: string): SessionSummary => ({
|
||||
@@ -4,10 +4,10 @@
|
||||
*/
|
||||
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import type { SessionId } from '@deepseek-ai/dsh-client-connection/client'
|
||||
import type { SessionId } from '@deepseek-ai/dsh-api-remotes/client'
|
||||
import { SessionManager } from '../src/client/sessions/manager.ts'
|
||||
import { FakeApiClient, deferred, err, ok } from './fake-api.ts'
|
||||
import { entries, ev, plainTurn } from './event-script.ts'
|
||||
import { FakeApiClient, deferred, err, fakeRemote, ok } from './fake-api.client.ts'
|
||||
import { entries, ev, plainTurn } from './event-script.client.ts'
|
||||
|
||||
const S1 = 'fk-m1' as SessionId
|
||||
const S2 = 'fk-m2' as SessionId
|
||||
@@ -28,7 +28,7 @@ describe('instances', () => {
|
||||
it('lazily builds one resident instance per id and syncs the running bit from the list', async () => {
|
||||
const api = new FakeApiClient()
|
||||
api.onList = () => Promise.resolve(ok({ items: [summary(S1, { running: true })] as never[] }))
|
||||
const manager = new SessionManager(api)
|
||||
const manager = new SessionManager(api, fakeRemote())
|
||||
await manager.refreshList()
|
||||
const session = manager.get(S1)
|
||||
expect(manager.get(S1)).toBe(session) // resident: same instance forever
|
||||
@@ -37,7 +37,7 @@ describe('instances', () => {
|
||||
|
||||
it('replays buffered approval frames on instantiation and drops ordinary frames for uninstantiated sessions', () => {
|
||||
const api = new FakeApiClient()
|
||||
const manager = new SessionManager(api)
|
||||
const manager = new SessionManager(api, fakeRemote())
|
||||
// Uninstantiated: approval buffers, plain session/event drops.
|
||||
manager.handleMuxEnvelope({ rpcId: 'ra' as never, payload: { type: 'approval/requested', sessionId: S1, approvalId: 'ap1' as never, toolName: 'rm' } })
|
||||
manager.handleMuxEnvelope({ rpcId: 'ra' as never, payload: { type: 'approval/requested', sessionId: S1, approvalId: 'ap1' as never, toolName: 'rm' } })
|
||||
@@ -50,7 +50,7 @@ describe('instances', () => {
|
||||
|
||||
it('retains every live answerable request and compacts resolutions before instantiation', () => {
|
||||
const api = new FakeApiClient()
|
||||
const manager = new SessionManager(api)
|
||||
const manager = new SessionManager(api, fakeRemote())
|
||||
manager.handleHostEnvelope({ rpcId: 'h1' as never, payload: { type: 'host/session-added', sessionId: S1, blank: false } })
|
||||
for (let i = 0; i < 40; i++) {
|
||||
manager.handleMuxEnvelope({ rpcId: `q${i}` as never, payload: { type: 'question/requested', sessionId: S1, questions: [] } })
|
||||
@@ -67,7 +67,7 @@ describe('instances', () => {
|
||||
})
|
||||
|
||||
it('drops buffered answerable requests on session removal', () => {
|
||||
const manager = new SessionManager(new FakeApiClient())
|
||||
const manager = new SessionManager(new FakeApiClient(), fakeRemote())
|
||||
// Removed session: buffered frames must not replay on a future instantiation.
|
||||
manager.handleMuxEnvelope({ rpcId: 'qz' as never, payload: { type: 'question/requested', sessionId: S2, questions: [] } })
|
||||
manager.handleHostEnvelope({ rpcId: 'hz' as never, payload: { type: 'host/session-removed', sessionId: S2 } })
|
||||
@@ -80,7 +80,7 @@ describe('list lifecycle', () => {
|
||||
const api = new FakeApiClient()
|
||||
const gate = deferred<Awaited<ReturnType<FakeApiClient['onList']>>>()
|
||||
api.onList = () => gate.promise
|
||||
const manager = new SessionManager(api)
|
||||
const manager = new SessionManager(api, fakeRemote())
|
||||
const first = manager.refreshList()
|
||||
const second = manager.refreshList()
|
||||
expect(manager.getListSnapshot().state).toBe('loading')
|
||||
@@ -96,7 +96,7 @@ describe('list lifecycle', () => {
|
||||
const api = new FakeApiClient()
|
||||
const first = deferred<Awaited<ReturnType<FakeApiClient['onList']>>>()
|
||||
api.onList = () => first.promise
|
||||
const manager = new SessionManager(api)
|
||||
const manager = new SessionManager(api, fakeRemote())
|
||||
const hydration = manager.refreshList()
|
||||
manager.handleHostEnvelope({
|
||||
rpcId: 'during-first' as never,
|
||||
@@ -116,7 +116,7 @@ describe('list lifecycle', () => {
|
||||
it('advances list activity only for direct user messages', async () => {
|
||||
const api = new FakeApiClient()
|
||||
api.onList = () => Promise.resolve(ok({ items: [summary(S1)] as never[] }))
|
||||
const manager = new SessionManager(api)
|
||||
const manager = new SessionManager(api, fakeRemote())
|
||||
await manager.refreshList()
|
||||
|
||||
// Both a new prompt and an admitted steer land as a user-sourced message.
|
||||
@@ -156,7 +156,7 @@ describe('list lifecycle', () => {
|
||||
it('keeps the error in the list snapshot on failure', async () => {
|
||||
const api = new FakeApiClient()
|
||||
api.onList = () => Promise.resolve(err({ code: 'internal', message: 'boom', details: {} }))
|
||||
const manager = new SessionManager(api)
|
||||
const manager = new SessionManager(api, fakeRemote())
|
||||
await manager.refreshList()
|
||||
expect(manager.getListSnapshot()).toMatchObject({ state: 'error', error: { code: 'internal' } })
|
||||
// A failed pull does not step the arrival phase: still pending.
|
||||
@@ -165,7 +165,7 @@ describe('list lifecycle', () => {
|
||||
|
||||
it('phase steps pending → ready on the first successful pull and never returns', async () => {
|
||||
const api = new FakeApiClient()
|
||||
const manager = new SessionManager(api)
|
||||
const manager = new SessionManager(api, fakeRemote())
|
||||
expect(manager.getListSnapshot().phase).toBe('pending')
|
||||
await manager.refreshList()
|
||||
expect(manager.getListSnapshot().phase).toBe('ready')
|
||||
@@ -184,7 +184,7 @@ describe('list lifecycle', () => {
|
||||
it('merges create into the list immediately without waiting for a refresh', async () => {
|
||||
const api = new FakeApiClient()
|
||||
api.onCreate = () => Promise.resolve(ok({ sessionId: S2 }))
|
||||
const manager = new SessionManager(api)
|
||||
const manager = new SessionManager(api, fakeRemote())
|
||||
const result = await manager.create()
|
||||
expect(result).toMatchObject({ ok: true, value: { sessionId: S2 } })
|
||||
expect(manager.getListSnapshot().items.map(i => i.sessionId)).toEqual([S2])
|
||||
@@ -192,7 +192,7 @@ describe('list lifecycle', () => {
|
||||
|
||||
it('retains title projections before list arrival, keeps last-wins by seq, and clears them on removal', async () => {
|
||||
const api = new FakeApiClient()
|
||||
const manager = new SessionManager(api)
|
||||
const manager = new SessionManager(api, fakeRemote())
|
||||
const titleFrame = (rpcId: string, title: string, seq: number) => {
|
||||
manager.handleMuxEnvelope({
|
||||
rpcId: rpcId as never,
|
||||
@@ -219,7 +219,7 @@ describe('list lifecycle', () => {
|
||||
|
||||
it('seeds cold titles from the list rows\' projections block under higher-seq-wins', async () => {
|
||||
const api = new FakeApiClient()
|
||||
const manager = new SessionManager(api)
|
||||
const manager = new SessionManager(api, fakeRemote())
|
||||
// A push frame landed before the list (S2's title is newer than the block's cut).
|
||||
manager.handleMuxEnvelope({
|
||||
rpcId: 'push-newer' as never,
|
||||
@@ -242,7 +242,7 @@ describe('list lifecycle', () => {
|
||||
it('drops a projection row beyond the subscription baseline before accepting its durable replay', async () => {
|
||||
const api = new FakeApiClient()
|
||||
api.onList = () => Promise.resolve(ok({ items: [summary(S1)] as never[] }))
|
||||
const manager = new SessionManager(api)
|
||||
const manager = new SessionManager(api, fakeRemote())
|
||||
await manager.refreshList()
|
||||
const frame = (rpcId: string, payload: object) => {
|
||||
manager.handleMuxEnvelope({ rpcId: rpcId as never, payload: payload as never })
|
||||
@@ -270,7 +270,7 @@ describe('search', () => {
|
||||
items: [{ sessionId: S1, snippet: 'matching excerpt' }],
|
||||
hasMore: true,
|
||||
}))
|
||||
const manager = new SessionManager(api)
|
||||
const manager = new SessionManager(api, fakeRemote())
|
||||
const signal = new AbortController().signal
|
||||
|
||||
await expect(manager.search('exact phrase', signal)).resolves.toEqual({
|
||||
@@ -286,7 +286,7 @@ describe('search', () => {
|
||||
|
||||
it('preserves business errors and folds transport failures', async () => {
|
||||
const api = new FakeApiClient()
|
||||
const manager = new SessionManager(api)
|
||||
const manager = new SessionManager(api, fakeRemote())
|
||||
api.onSearch = () => Promise.resolve(err({
|
||||
code: 'internal',
|
||||
message: 'index unavailable',
|
||||
@@ -309,7 +309,7 @@ describe('search', () => {
|
||||
describe('host frame routing', () => {
|
||||
it('adds/removes/flips sessions from host frames and keeps removed instances resident', async () => {
|
||||
const api = new FakeApiClient()
|
||||
const manager = new SessionManager(api)
|
||||
const manager = new SessionManager(api, fakeRemote())
|
||||
manager.handleHostEnvelope({ rpcId: 'h1' as never, payload: { type: 'host/session-added', blank: true, sessionId: S1 } })
|
||||
manager.handleHostEnvelope({ rpcId: 'h2' as never, payload: { type: 'host/session-added', blank: true, sessionId: S1 } }) // dup: ignored
|
||||
expect(manager.getListSnapshot().items).toHaveLength(1)
|
||||
@@ -343,7 +343,7 @@ describe('subagent catalogs', () => {
|
||||
}] as never[],
|
||||
parentAvailable: true,
|
||||
}))
|
||||
const manager = new SessionManager(api)
|
||||
const manager = new SessionManager(api, fakeRemote())
|
||||
await manager.refreshList()
|
||||
await manager.refreshSubagents(S1)
|
||||
manager.selectSubagent({ parentSessionId: S1, childSessionId: S2, mode: 'continuable' })
|
||||
@@ -408,7 +408,7 @@ describe('subagent catalogs', () => {
|
||||
vi.useFakeTimers()
|
||||
try {
|
||||
const api = new FakeApiClient()
|
||||
const manager = new SessionManager(api)
|
||||
const manager = new SessionManager(api, fakeRemote())
|
||||
await manager.refreshSubagents(S1)
|
||||
manager.setSubagentCatalogOpen(S1, true)
|
||||
await Promise.resolve()
|
||||
@@ -458,7 +458,7 @@ describe('subagent catalogs', () => {
|
||||
] as never[],
|
||||
parentAvailable: true,
|
||||
}))
|
||||
const manager = new SessionManager(api)
|
||||
const manager = new SessionManager(api, fakeRemote())
|
||||
await manager.refreshSubagents(root)
|
||||
|
||||
manager.handleHostEnvelope({
|
||||
@@ -487,7 +487,7 @@ describe('subagent catalogs', () => {
|
||||
const root = 'fk-root' as SessionId
|
||||
const response = deferred<Awaited<ReturnType<FakeApiClient['onSubagentList']>>>()
|
||||
api.onSubagentList = () => response.promise
|
||||
const manager = new SessionManager(api)
|
||||
const manager = new SessionManager(api, fakeRemote())
|
||||
const refresh = manager.refreshSubagents(root)
|
||||
|
||||
manager.handleHostEnvelope({
|
||||
@@ -528,7 +528,7 @@ describe('subagent catalogs', () => {
|
||||
const root = 'fk-root' as SessionId
|
||||
const response = deferred<Awaited<ReturnType<FakeApiClient['onSubagentList']>>>()
|
||||
api.onSubagentList = () => response.promise
|
||||
const manager = new SessionManager(api)
|
||||
const manager = new SessionManager(api, fakeRemote())
|
||||
const refresh = manager.refreshSubagents(root)
|
||||
|
||||
manager.handleHostEnvelope({
|
||||
@@ -569,7 +569,7 @@ describe('subagent catalogs', () => {
|
||||
}] as never[],
|
||||
parentAvailable: true,
|
||||
}))
|
||||
const manager = new SessionManager(api)
|
||||
const manager = new SessionManager(api, fakeRemote())
|
||||
await manager.refreshSubagents(S1)
|
||||
|
||||
manager.handleHostEnvelope({
|
||||
@@ -587,7 +587,7 @@ describe('subagent catalogs', () => {
|
||||
const root = 'fk-root' as SessionId
|
||||
const first = deferred<Awaited<ReturnType<FakeApiClient['onSubagentList']>>>()
|
||||
api.onSubagentList = () => first.promise
|
||||
const manager = new SessionManager(api)
|
||||
const manager = new SessionManager(api, fakeRemote())
|
||||
|
||||
const refresh = manager.refreshSubagents(root)
|
||||
expect(manager.refreshSubagents(root)).toBe(refresh)
|
||||
@@ -606,7 +606,7 @@ describe('subagent catalogs', () => {
|
||||
const first = deferred<Awaited<ReturnType<FakeApiClient['onSubagentList']>>>()
|
||||
const second = deferred<Awaited<ReturnType<FakeApiClient['onSubagentList']>>>()
|
||||
api.onSubagentList = () => first.promise
|
||||
const manager = new SessionManager(api, root)
|
||||
const manager = new SessionManager(api, fakeRemote(), root)
|
||||
const refresh = manager.refreshSubagents(root)
|
||||
|
||||
// A membership frame arrives while the pull is in flight; the debounced
|
||||
@@ -664,7 +664,7 @@ describe('subagent catalogs', () => {
|
||||
})
|
||||
const first = deferred<Awaited<ReturnType<FakeApiClient['onSubagentList']>>>()
|
||||
api.onSubagentList = () => first.promise
|
||||
const manager = new SessionManager(api)
|
||||
const manager = new SessionManager(api, fakeRemote())
|
||||
const refresh = manager.refreshSubagents(root)
|
||||
first.resolve(ok({ entries: [child()] as never[], parentAvailable: true }))
|
||||
await refresh
|
||||
@@ -711,7 +711,7 @@ describe('subagent catalogs', () => {
|
||||
}] as never[],
|
||||
parentAvailable: true,
|
||||
}))
|
||||
const manager = new SessionManager(api)
|
||||
const manager = new SessionManager(api, fakeRemote())
|
||||
await manager.refreshSubagents(root)
|
||||
manager.selectSubagent({ parentSessionId: root, childSessionId: S2, mode: 'continuable' })
|
||||
expect(manager.get(S2).getSnapshot().subagent).toMatchObject({ parentAvailable: true })
|
||||
@@ -730,14 +730,14 @@ describe('remaining branches', () => {
|
||||
it('refreshList folds a transport throw into the error state', async () => {
|
||||
const api = new FakeApiClient()
|
||||
api.onList = () => Promise.reject(new Error('list wire down'))
|
||||
const manager = new SessionManager(api)
|
||||
const manager = new SessionManager(api, fakeRemote())
|
||||
await manager.refreshList()
|
||||
expect(manager.getListSnapshot()).toMatchObject({ state: 'error', error: { code: 'internal', message: 'list wire down' } })
|
||||
})
|
||||
|
||||
it('refreshList pushes running bits down to already-instantiated sessions', async () => {
|
||||
const api = new FakeApiClient()
|
||||
const manager = new SessionManager(api)
|
||||
const manager = new SessionManager(api, fakeRemote())
|
||||
const session = manager.get(S1)
|
||||
api.onList = () => Promise.resolve(ok({ items: [summary(S1, { running: true })] as never[] }))
|
||||
await manager.refreshList()
|
||||
@@ -747,7 +747,7 @@ describe('remaining branches', () => {
|
||||
it('create passes cwd and a preallocated id, folds transport throws, and deduplicates the echo', async () => {
|
||||
const api = new FakeApiClient()
|
||||
api.onCreate = () => Promise.resolve(ok({ sessionId: S1 }))
|
||||
const manager = new SessionManager(api)
|
||||
const manager = new SessionManager(api, fakeRemote())
|
||||
await manager.create({ cwd: '/tmp/w', sessionId: S1 })
|
||||
expect(api.callsOf('session.create')).toEqual([{ cwd: '/tmp/w', sessionId: S1 }])
|
||||
expect(manager.getListSnapshot().items[0]).toMatchObject({ sessionId: S1, cwd: '/tmp/w' })
|
||||
@@ -767,7 +767,7 @@ describe('remaining branches', () => {
|
||||
message: 'published but unattached',
|
||||
details: { sessionId: S1, workspaceId: 'w1' },
|
||||
} as never))
|
||||
const manager = new SessionManager(api)
|
||||
const manager = new SessionManager(api, fakeRemote())
|
||||
const result = await manager.create({ workspaceId: 'w1' as never, sessionId: S1 })
|
||||
expect(result).toMatchObject({ ok: false, error: { code: 'workspace-attach-failed' } })
|
||||
expect(manager.getListSnapshot().items).toEqual([expect.objectContaining({ sessionId: S1 })])
|
||||
@@ -781,7 +781,7 @@ describe('remaining branches', () => {
|
||||
message: 'forked but unattached',
|
||||
details: { sessionId: S2, workspaceId: 'w1' },
|
||||
} as never))
|
||||
const manager = new SessionManager(api)
|
||||
const manager = new SessionManager(api, fakeRemote())
|
||||
const result = await manager.fork({ sessionId: S1 })
|
||||
expect(result).toMatchObject({ ok: false, error: { code: 'workspace-attach-failed' } })
|
||||
expect(manager.getListSnapshot().items).toEqual([expect.objectContaining({
|
||||
@@ -794,7 +794,7 @@ describe('remaining branches', () => {
|
||||
it('reconciles a preallocated id after an ordinary transport failure', async () => {
|
||||
const api = new FakeApiClient()
|
||||
api.onCreate = () => Promise.reject(new Error('response lost'))
|
||||
const manager = new SessionManager(api)
|
||||
const manager = new SessionManager(api, fakeRemote())
|
||||
const failed = await manager.create({ workspaceId: 'w1' as never, sessionId: S1 })
|
||||
expect(failed).toMatchObject({ ok: false, error: { message: 'response lost' } })
|
||||
expect(manager.getListSnapshot().items).toEqual([])
|
||||
@@ -815,7 +815,7 @@ describe('remaining branches', () => {
|
||||
|
||||
it('subscribe notifies on list changes and stops after unsubscribe', async () => {
|
||||
const api = new FakeApiClient()
|
||||
const manager = new SessionManager(api)
|
||||
const manager = new SessionManager(api, fakeRemote())
|
||||
let notified = 0
|
||||
const unsubscribe = manager.subscribe(() => { notified++ })
|
||||
await manager.refreshList()
|
||||
@@ -830,7 +830,7 @@ describe('remaining branches', () => {
|
||||
|
||||
it('routes stream/error and unknown frames to the documented drops, and dispatches to instantiated sessions', () => {
|
||||
const api = new FakeApiClient()
|
||||
const manager = new SessionManager(api)
|
||||
const manager = new SessionManager(api, fakeRemote())
|
||||
manager.handleMuxEnvelope({ rpcId: 'e' as never, payload: { type: 'stream/error', error: { code: 'internal', message: 'x', details: {} } } })
|
||||
manager.handleHostEnvelope({ rpcId: 'e2' as never, payload: { type: 'stream/error', error: { code: 'internal', message: 'x', details: {} } } })
|
||||
manager.handleHostEnvelope({ rpcId: 'e3' as never, payload: { type: 'future/host-frame' } as never })
|
||||
@@ -845,7 +845,7 @@ describe('remaining branches', () => {
|
||||
it('keeps list-entry identity for unchanged rows across an unrelated list change', async () => {
|
||||
const api = new FakeApiClient()
|
||||
api.onList = () => Promise.resolve(ok({ items: [summary(S1), summary(S2, { updatedAt: 200 })] as never[] }))
|
||||
const manager = new SessionManager(api)
|
||||
const manager = new SessionManager(api, fakeRemote())
|
||||
await manager.refreshList()
|
||||
const before = manager.getListSnapshot()
|
||||
manager.handleHostEnvelope({ rpcId: 'h' as never, payload: { type: 'host/session-status', sessionId: S2, running: true } })
|
||||
@@ -861,7 +861,7 @@ describe('remaining branches', () => {
|
||||
|
||||
it('carries parentSessionId from host/session-added into the lineage row', () => {
|
||||
const api = new FakeApiClient()
|
||||
const manager = new SessionManager(api)
|
||||
const manager = new SessionManager(api, fakeRemote())
|
||||
manager.handleHostEnvelope({ rpcId: 'h1' as never, payload: { type: 'host/session-added', blank: true, sessionId: S1 } })
|
||||
manager.handleHostEnvelope({
|
||||
rpcId: 'h2' as never,
|
||||
@@ -885,7 +885,7 @@ describe('connected generation', () => {
|
||||
hasMore: false,
|
||||
modelSelection: { provider: 'deepseek-official', model: 'deepseek-chat' },
|
||||
}))
|
||||
const manager = new SessionManager(api)
|
||||
const manager = new SessionManager(api, fakeRemote())
|
||||
const openedSession = manager.get(S1)
|
||||
await openedSession.open()
|
||||
manager.get(S2) // instantiated but never opened
|
||||
@@ -903,7 +903,7 @@ describe('connected generation', () => {
|
||||
const address = {
|
||||
parentSessionId: S1, childSessionId: S2, mode: 'continuable' as const,
|
||||
}
|
||||
const manager = new SessionManager(api, S2, address)
|
||||
const manager = new SessionManager(api, fakeRemote(), S2, address)
|
||||
|
||||
manager.handleConnected()
|
||||
|
||||
@@ -916,7 +916,7 @@ describe('connected generation', () => {
|
||||
|
||||
describe('pending-interaction list status', () => {
|
||||
it('tracks approval requests through replay and resolution without instantiation', () => {
|
||||
const manager = new SessionManager(new FakeApiClient())
|
||||
const manager = new SessionManager(new FakeApiClient(), fakeRemote())
|
||||
manager.handleHostEnvelope({ rpcId: 'h1' as never, payload: { type: 'host/session-added', sessionId: S1, blank: false } })
|
||||
expect(manager.getListSnapshot().items[0]?.pendingInteraction).toBeUndefined()
|
||||
manager.handleMuxEnvelope({ rpcId: 'ra' as never, payload: { type: 'approval/requested', sessionId: S1, approvalId: 'ap1' as never, toolName: 'rm' } })
|
||||
@@ -929,7 +929,7 @@ describe('pending-interaction list status', () => {
|
||||
})
|
||||
|
||||
it('classifies ordinary questions and renderable plan reviews, then clears by question rpcId', () => {
|
||||
const manager = new SessionManager(new FakeApiClient())
|
||||
const manager = new SessionManager(new FakeApiClient(), fakeRemote())
|
||||
manager.handleHostEnvelope({ rpcId: 'h1' as never, payload: { type: 'host/session-added', sessionId: S1, blank: false } })
|
||||
manager.handleMuxEnvelope({
|
||||
rpcId: 'q1' as never,
|
||||
@@ -962,7 +962,7 @@ describe('pending-interaction list status', () => {
|
||||
['more than two options', { detail: '# Plan', options: [{ label: 'Approve' }, { label: 'Refuse' }, { label: 'Revise' }] }],
|
||||
['missing approve option', { detail: '# Plan', options: [{ label: 'Refuse' }] }],
|
||||
])('keeps an unrenderable %s plan intent on the ordinary question flow', (_name, over) => {
|
||||
const manager = new SessionManager(new FakeApiClient())
|
||||
const manager = new SessionManager(new FakeApiClient(), fakeRemote())
|
||||
manager.handleHostEnvelope({ rpcId: 'h1' as never, payload: { type: 'host/session-added', sessionId: S1, blank: false } })
|
||||
manager.handleMuxEnvelope({
|
||||
rpcId: 'q-plan' as never,
|
||||
@@ -979,7 +979,7 @@ describe('pending-interaction list status', () => {
|
||||
})
|
||||
|
||||
it('the first question outranks sibling approvals and resolving it reveals the remaining wait', () => {
|
||||
const manager = new SessionManager(new FakeApiClient())
|
||||
const manager = new SessionManager(new FakeApiClient(), fakeRemote())
|
||||
manager.handleHostEnvelope({ rpcId: 'h1' as never, payload: { type: 'host/session-added', sessionId: S1, blank: false } })
|
||||
manager.handleMuxEnvelope({ rpcId: 'r1' as never, payload: { type: 'approval/requested', sessionId: S1, approvalId: 'a1' as never, toolName: 'rm' } })
|
||||
manager.handleMuxEnvelope({
|
||||
@@ -998,7 +998,7 @@ describe('pending-interaction list status', () => {
|
||||
})
|
||||
|
||||
it('drops stale status at generation death before replay re-adds live interactions', () => {
|
||||
const manager = new SessionManager(new FakeApiClient())
|
||||
const manager = new SessionManager(new FakeApiClient(), fakeRemote())
|
||||
manager.handleHostEnvelope({ rpcId: 'h1' as never, payload: { type: 'host/session-added', sessionId: S1, blank: false } })
|
||||
manager.handleMuxEnvelope({ rpcId: 'ra' as never, payload: { type: 'approval/requested', sessionId: S1, approvalId: 'ap1' as never, toolName: 'rm' } })
|
||||
expect(manager.getListSnapshot().items[0]?.pendingInteraction).toBe('approval')
|
||||
@@ -1013,7 +1013,7 @@ describe('pending-interaction list status', () => {
|
||||
})
|
||||
|
||||
it('generation death drops buffered answerable frames (a dead generation cannot be answered)', () => {
|
||||
const manager = new SessionManager(new FakeApiClient())
|
||||
const manager = new SessionManager(new FakeApiClient(), fakeRemote())
|
||||
manager.handleHostEnvelope({ rpcId: 'h1' as never, payload: { type: 'host/session-added', sessionId: S1, blank: false } })
|
||||
// Buffered pre-instantiation: an approval pair and a queued row.
|
||||
manager.handleMuxEnvelope({ rpcId: 'ra' as never, payload: { type: 'approval/requested', sessionId: S1, approvalId: 'ap1' as never, toolName: 'rm' } })
|
||||
@@ -1040,7 +1040,7 @@ describe('completed reminder', () => {
|
||||
manager.getListSnapshot().items.find(item => item.sessionId === sessionId)
|
||||
|
||||
it('arms on a running→idle flip of a non-selected session and clears on select', () => {
|
||||
const manager = new SessionManager(new FakeApiClient())
|
||||
const manager = new SessionManager(new FakeApiClient(), fakeRemote())
|
||||
manager.handleHostEnvelope(added('h1', S1))
|
||||
manager.handleHostEnvelope(added('h2', S2))
|
||||
manager.select(S1)
|
||||
@@ -1054,7 +1054,7 @@ describe('completed reminder', () => {
|
||||
})
|
||||
|
||||
it('never arms for the session being watched and re-arms after a switch-away re-run', () => {
|
||||
const manager = new SessionManager(new FakeApiClient())
|
||||
const manager = new SessionManager(new FakeApiClient(), fakeRemote())
|
||||
manager.handleHostEnvelope(added('h1', S1))
|
||||
manager.handleHostEnvelope(added('h2', S2))
|
||||
manager.select(S2)
|
||||
@@ -1069,7 +1069,7 @@ describe('completed reminder', () => {
|
||||
})
|
||||
|
||||
it('a re-run disarms the reminder while running and re-arms on its completion', () => {
|
||||
const manager = new SessionManager(new FakeApiClient())
|
||||
const manager = new SessionManager(new FakeApiClient(), fakeRemote())
|
||||
manager.handleHostEnvelope(added('h1', S1))
|
||||
manager.handleHostEnvelope(added('h2', S2))
|
||||
manager.select(S1)
|
||||
@@ -1084,7 +1084,7 @@ describe('completed reminder', () => {
|
||||
})
|
||||
|
||||
it('session-removed drops the reminder and a re-add starts clean', () => {
|
||||
const manager = new SessionManager(new FakeApiClient())
|
||||
const manager = new SessionManager(new FakeApiClient(), fakeRemote())
|
||||
manager.handleHostEnvelope(added('h1', S1))
|
||||
manager.handleHostEnvelope(added('h2', S2))
|
||||
manager.select(S1)
|
||||
@@ -1100,7 +1100,7 @@ describe('completed reminder', () => {
|
||||
it('a list refresh carrying the running→idle transition arms the reminder', async () => {
|
||||
const api = new FakeApiClient()
|
||||
api.onList = () => Promise.resolve(ok({ items: [summary(S1), summary(S2, { updatedAt: 200, running: true })] as never[] }))
|
||||
const manager = new SessionManager(api)
|
||||
const manager = new SessionManager(api, fakeRemote())
|
||||
await manager.refreshList()
|
||||
manager.select(S1)
|
||||
expect(entry(manager, S2)?.completed).toBe(false)
|
||||
@@ -1112,7 +1112,7 @@ describe('completed reminder', () => {
|
||||
it('never arms for sessions already idle at first observation', async () => {
|
||||
const api = new FakeApiClient()
|
||||
api.onList = () => Promise.resolve(ok({ items: [summary(S1), summary(S2, { updatedAt: 200 })] as never[] }))
|
||||
const manager = new SessionManager(api)
|
||||
const manager = new SessionManager(api, fakeRemote())
|
||||
await manager.refreshList()
|
||||
manager.select(S1)
|
||||
expect(entry(manager, S2)?.completed).toBe(false)
|
||||
@@ -1125,7 +1125,7 @@ describe('completed reminder', () => {
|
||||
const api = new FakeApiClient()
|
||||
const gate = deferred<Awaited<ReturnType<FakeApiClient['onList']>>>()
|
||||
api.onList = () => gate.promise
|
||||
const manager = new SessionManager(api)
|
||||
const manager = new SessionManager(api, fakeRemote())
|
||||
const refresh = manager.refreshList()
|
||||
// The session finishes while the first pull is still in flight; the pull
|
||||
// response recorded it as running at pull time.
|
||||
@@ -1139,7 +1139,7 @@ describe('completed reminder', () => {
|
||||
const api = new FakeApiClient()
|
||||
const gate = deferred<Awaited<ReturnType<FakeApiClient['onList']>>>()
|
||||
api.onList = () => gate.promise
|
||||
const manager = new SessionManager(api)
|
||||
const manager = new SessionManager(api, fakeRemote())
|
||||
const refresh = manager.refreshList()
|
||||
// The unknown session starts and finishes while the first pull is in
|
||||
// flight; the pull-time baseline recorded it idle, so the running→idle
|
||||
@@ -1160,7 +1160,7 @@ describe('background-task mirror', () => {
|
||||
({ rpcId: 't' as never, payload: { type: 'session/tasks', sessionId, tasks } as never })
|
||||
|
||||
it('mirrors the whole set last-wins, keyed per session, with no Session instance needed', () => {
|
||||
const manager = new SessionManager(new FakeApiClient())
|
||||
const manager = new SessionManager(new FakeApiClient(), fakeRemote())
|
||||
manager.handleMuxEnvelope(tasksFrame(S1, [view()]))
|
||||
manager.handleMuxEnvelope(tasksFrame(S2, [view({ id: 'pwsh-1', label: 'other' })]))
|
||||
const first = manager.getListSnapshot().tasksBySession
|
||||
@@ -1173,7 +1173,7 @@ describe('background-task mirror', () => {
|
||||
})
|
||||
|
||||
it('stores an emptied set as an absent key so absence and [] read alike', () => {
|
||||
const manager = new SessionManager(new FakeApiClient())
|
||||
const manager = new SessionManager(new FakeApiClient(), fakeRemote())
|
||||
manager.handleMuxEnvelope(tasksFrame(S1, [view()]))
|
||||
expect(S1 in manager.getListSnapshot().tasksBySession).toBe(true)
|
||||
manager.handleMuxEnvelope(tasksFrame(S1, []))
|
||||
@@ -1181,7 +1181,7 @@ describe('background-task mirror', () => {
|
||||
})
|
||||
|
||||
it('clears the mirror on re-subscribe, because a task-free generation sends no baseline', () => {
|
||||
const manager = new SessionManager(new FakeApiClient())
|
||||
const manager = new SessionManager(new FakeApiClient(), fakeRemote())
|
||||
manager.handleMuxEnvelope(tasksFrame(S1, [view()]))
|
||||
manager.handleMuxEnvelope({
|
||||
rpcId: 's' as never,
|
||||
@@ -1191,7 +1191,7 @@ describe('background-task mirror', () => {
|
||||
})
|
||||
|
||||
it('drops the rows when the session is removed, whichever stream lands first', () => {
|
||||
const manager = new SessionManager(new FakeApiClient())
|
||||
const manager = new SessionManager(new FakeApiClient(), fakeRemote())
|
||||
manager.handleHostEnvelope({ rpcId: 'a' as never, payload: { type: 'host/session-added', blank: true, sessionId: S1 } })
|
||||
manager.handleMuxEnvelope(tasksFrame(S1, [view()]))
|
||||
manager.handleHostEnvelope({ rpcId: 'r' as never, payload: { type: 'host/session-removed', sessionId: S1 } })
|
||||
@@ -1199,7 +1199,7 @@ describe('background-task mirror', () => {
|
||||
})
|
||||
|
||||
it('notifies list subscribers so an open header re-renders without a poll', async () => {
|
||||
const manager = new SessionManager(new FakeApiClient())
|
||||
const manager = new SessionManager(new FakeApiClient(), fakeRemote())
|
||||
const seen = vi.fn()
|
||||
manager.subscribe(seen)
|
||||
manager.handleMuxEnvelope(tasksFrame(S1, [view()]))
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import type { StreamChunk } from '@deepseek-ai/dsh-client-connection/client'
|
||||
import type { StreamChunk } from '@deepseek-ai/dsh-api-remotes/client'
|
||||
import { PartialAccumulator } from '../src/client/sessions/partial.ts'
|
||||
|
||||
const chunk = (c: Record<string, unknown>): StreamChunk => c as unknown as StreamChunk
|
||||
@@ -8,12 +8,12 @@
|
||||
* list rows' title projection).
|
||||
*/
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import type { SessionId } from '@deepseek-ai/dsh-client-connection/client'
|
||||
import type { SessionId } from '@deepseek-ai/dsh-api-remotes/client'
|
||||
import { ProjectionValueStore } from '../src/client/sessions/projection-store.ts'
|
||||
import { Session } from '../src/client/sessions/session.ts'
|
||||
import { SessionManager } from '../src/client/sessions/manager.ts'
|
||||
import { FakeApiClient, ok } from './fake-api.ts'
|
||||
import { entries, plainTurn } from './event-script.ts'
|
||||
import { FakeApiClient, fakeRemote, ok } from './fake-api.client.ts'
|
||||
import { entries, plainTurn } from './event-script.client.ts'
|
||||
|
||||
// Test-domain keys merged into the projection map (the Service Definition package's
|
||||
// pure-type outlet), the same way domain host plugins merge theirs.
|
||||
@@ -103,7 +103,7 @@ describe('ProjectionValueStore semantics', () => {
|
||||
describe('Session tail-page seeding', () => {
|
||||
it('seeds the store from a history response carrying a projections block', async () => {
|
||||
const api = new FakeApiClient()
|
||||
const session = new Session(SID, api)
|
||||
const session = new Session(SID, api, fakeRemote())
|
||||
api.onHistory = () => Promise.resolve(ok({
|
||||
events: entries(plainTurn(0, 0, '问', '答')) as never[], hasMore: false,
|
||||
projections: { asOfSeq: 5, values: { 'test/marks': { marks: ['from-baseline'] } } },
|
||||
@@ -114,7 +114,7 @@ describe('Session tail-page seeding', () => {
|
||||
|
||||
it('a resync serving a stale block keeps the newer pushed value (seq rule end to end)', async () => {
|
||||
const api = new FakeApiClient()
|
||||
const session = new Session(SID, api)
|
||||
const session = new Session(SID, api, fakeRemote())
|
||||
api.onHistory = () => Promise.resolve(ok({
|
||||
events: entries(plainTurn(0, 0, 'a', 'b')) as never[], hasMore: false,
|
||||
projections: { asOfSeq: 5, values: { 'test/marks': { marks: ['baseline'] } } },
|
||||
@@ -127,7 +127,7 @@ describe('Session tail-page seeding', () => {
|
||||
|
||||
it('treats a blockless response as no reset: pushed values survive', async () => {
|
||||
const api = new FakeApiClient()
|
||||
const session = new Session(SID, api)
|
||||
const session = new Session(SID, api, fakeRemote())
|
||||
api.onHistory = () => Promise.resolve(ok({ events: entries(plainTurn(0, 0, 'a', 'b')) as never[], hasMore: false }))
|
||||
await session.open()
|
||||
session.projections.apply('test/marks', { marks: ['pushed'] }, 9)
|
||||
@@ -141,7 +141,7 @@ describe('manager frame routing', () => {
|
||||
|
||||
it('lands session/projection frames before instantiation and the Session adopts the same store', async () => {
|
||||
const api = new FakeApiClient()
|
||||
const manager = new SessionManager(api)
|
||||
const manager = new SessionManager(api, fakeRemote())
|
||||
manager.handleMuxEnvelope({
|
||||
rpcId: 'p1' as never,
|
||||
payload: { type: 'session/projection', sessionId: sid('s1'), key: 'test/marks', value: { marks: ['early'] }, seq: 7 } as never,
|
||||
@@ -158,7 +158,7 @@ describe('manager frame routing', () => {
|
||||
|
||||
it('projects the title key into list rows and truncates phantom rows on the subscribed baseline', async () => {
|
||||
const api = new FakeApiClient()
|
||||
const manager = new SessionManager(api)
|
||||
const manager = new SessionManager(api, fakeRemote())
|
||||
api.onList = () => Promise.resolve(ok({
|
||||
items: [{ sessionId: sid('s1'), updatedAt: 1, running: false, blank: false }],
|
||||
}) as never)
|
||||
@@ -181,7 +181,7 @@ describe('manager frame routing', () => {
|
||||
|
||||
it('projects every retained value into list rows with stable snapshot identity', async () => {
|
||||
const api = new FakeApiClient()
|
||||
const manager = new SessionManager(api)
|
||||
const manager = new SessionManager(api, fakeRemote())
|
||||
api.onList = () => Promise.resolve(ok({
|
||||
items: [{
|
||||
sessionId: sid('s1'), updatedAt: 1, running: false, blank: false,
|
||||
@@ -211,7 +211,7 @@ describe('manager frame routing', () => {
|
||||
|
||||
it('drops the projection store with the removed session', async () => {
|
||||
const api = new FakeApiClient()
|
||||
const manager = new SessionManager(api)
|
||||
const manager = new SessionManager(api, fakeRemote())
|
||||
api.onList = () => Promise.resolve(ok({
|
||||
items: [{ sessionId: sid('s1'), updatedAt: 1, running: false, blank: false }],
|
||||
}) as never)
|
||||
@@ -7,10 +7,10 @@ import { describe, expect, it } from 'vitest'
|
||||
import { createUserMessage } from '@deepseek-ai/dsh-llm'
|
||||
import type { ContentBlock, UserMessage } from '@deepseek-ai/dsh-llm/types'
|
||||
import type { SessionEvent } from '@deepseek-ai/dsh-session/types'
|
||||
import type { MessageId, MuxFrame, RpcId, SessionId } from '@deepseek-ai/dsh-client-connection/client'
|
||||
import type { MessageId, MuxFrame, RpcId, SessionId } from '@deepseek-ai/dsh-api-remotes/client'
|
||||
import { Session } from '../src/client/sessions/session.ts'
|
||||
import { SessionManager } from '../src/client/sessions/manager.ts'
|
||||
import { FakeApiClient } from './fake-api.ts'
|
||||
import { FakeApiClient, fakeRemote } from './fake-api.client.ts'
|
||||
|
||||
const SID = 'fk-q1' as SessionId
|
||||
const text = (value: string): ContentBlock[] => [{ type: 'text', text: value }]
|
||||
@@ -42,7 +42,7 @@ function queueFrame(items: QueueFixture[]): MuxFrame {
|
||||
}
|
||||
|
||||
function makeSession(): Session {
|
||||
return new Session(SID, new FakeApiClient())
|
||||
return new Session(SID, new FakeApiClient(), fakeRemote())
|
||||
}
|
||||
|
||||
describe('queue snapshot intake', () => {
|
||||
@@ -198,7 +198,7 @@ describe('queue snapshot intake', () => {
|
||||
describe('queue operation transport', () => {
|
||||
it('addresses the session.updateQueue RPC without optimistic local mutation', async () => {
|
||||
const api = new FakeApiClient()
|
||||
const session = new Session(SID, api)
|
||||
const session = new Session(SID, api, fakeRemote())
|
||||
session.handleMuxEnvelope(rid('env-op'), queueFrame([{ id: 'q-op', body: 'pending' }]))
|
||||
const before = session.getSnapshot().queue
|
||||
|
||||
@@ -251,14 +251,14 @@ describe('queue reconnect semantics', () => {
|
||||
|
||||
describe('manager buffering of queue snapshots', () => {
|
||||
it('replays only the latest snapshot for an uninstantiated session', () => {
|
||||
const manager = new SessionManager(new FakeApiClient())
|
||||
const manager = new SessionManager(new FakeApiClient(), fakeRemote())
|
||||
manager.handleMuxEnvelope({ rpcId: rid('b1'), payload: queueFrame([{ id: 'q-old', body: '旧' }]) })
|
||||
manager.handleMuxEnvelope({ rpcId: rid('b2'), payload: queueFrame([{ id: 'q-new', body: '新' }]) })
|
||||
expect(manager.get(SID).getSnapshot().queue.map(row => row.id)).toEqual(['q-new'])
|
||||
})
|
||||
|
||||
it('subscribed drops the prior-generation snapshot while preserving answerable frames', () => {
|
||||
const manager = new SessionManager(new FakeApiClient())
|
||||
const manager = new SessionManager(new FakeApiClient(), fakeRemote())
|
||||
manager.handleMuxEnvelope({ rpcId: rid('g1a'), payload: queueFrame([{ id: 'q-g1', body: '第一代' }]) })
|
||||
manager.handleMuxEnvelope({
|
||||
rpcId: rid('g1b'),
|
||||
@@ -8,7 +8,7 @@
|
||||
*/
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import type { SessionId } from '@deepseek-ai/dsh-client-connection/client'
|
||||
import type { SessionId } from '@deepseek-ai/dsh-api-remotes/client'
|
||||
import { createScope, scopeOf } from '../src/client/agents/scope.ts'
|
||||
|
||||
const sid = (k: string): SessionId => k as SessionId
|
||||
@@ -9,7 +9,7 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { SessionEvent } from '@deepseek-ai/dsh-session/types'
|
||||
import type {} from '@deepseek-ai/dsh-commands/types'
|
||||
import type { SessionId } from '@deepseek-ai/dsh-client-connection/client'
|
||||
import type { SessionId } from '@deepseek-ai/dsh-api-remotes/client'
|
||||
import { Session } from '../src/client/sessions/session.ts'
|
||||
import type {
|
||||
ChatConversationViewNode, ChatLocationNodeIndex, ChatNodeStore, ChatSnapshot,
|
||||
@@ -17,8 +17,8 @@ import type {
|
||||
ConversationRuntime, ConversationSnapshot, ConversationTimelineSnapshot,
|
||||
ConversationViewDefinition,
|
||||
} from '../src/client/index.ts'
|
||||
import { FakeApiClient, deferred, err, ok } from './fake-api.ts'
|
||||
import { entries, ev, plainTurn } from './event-script.ts'
|
||||
import { FakeApiClient, deferred, err, fakeRemote, ok } from './fake-api.client.ts'
|
||||
import { entries, ev, plainTurn } from './event-script.client.ts'
|
||||
|
||||
const SID = 'fk-s1' as SessionId
|
||||
const PARENT = 'fk-parent' as SessionId
|
||||
@@ -159,7 +159,7 @@ const TEST_CONVERSATION: ConversationRuntime = {
|
||||
}
|
||||
|
||||
function makeSession(api = new FakeApiClient()): { api: FakeApiClient; session: Session } {
|
||||
return { api, session: new Session(SID, api, { conversation: TEST_CONVERSATION }) }
|
||||
return { api, session: new Session(SID, api, fakeRemote(), { conversation: TEST_CONVERSATION }) }
|
||||
}
|
||||
|
||||
function chatEvents(snapshot: ConversationSnapshot): readonly TestEventState[] {
|
||||
@@ -343,7 +343,7 @@ describe('live event path', () => {
|
||||
entries: () => [testViewDefinition()],
|
||||
} as unknown as ConversationRuntime['views'],
|
||||
}
|
||||
const session = new Session(SID, api, { conversation })
|
||||
const session = new Session(SID, api, fakeRemote(), { conversation })
|
||||
await session.open()
|
||||
const snapshots: ConversationSnapshot[] = []
|
||||
session.subscribe(() => { snapshots.push(session.getSnapshot()) })
|
||||
@@ -448,7 +448,7 @@ describe('paging', () => {
|
||||
describe('prompt and cancel errors', () => {
|
||||
it('routes an addressed child through non-activating history, continuation prompt, and interrupt only', async () => {
|
||||
const api = new FakeApiClient()
|
||||
const session = new Session(SID, api, {
|
||||
const session = new Session(SID, api, fakeRemote(), {
|
||||
address: { parentSessionId: PARENT, childSessionId: SID, mode: 'continuable' },
|
||||
parentAvailable: true,
|
||||
})
|
||||
@@ -487,7 +487,7 @@ describe('prompt and cancel errors', () => {
|
||||
api.onSubagentInterrupt = () => Promise.resolve(err({
|
||||
code: 'subagent-unauthorized', message: 'nope', details: { childSessionId: SID },
|
||||
}) as never)
|
||||
const session = new Session(SID, api, {
|
||||
const session = new Session(SID, api, fakeRemote(), {
|
||||
address: { parentSessionId: PARENT, childSessionId: SID, mode: 'continuable' },
|
||||
parentAvailable: true,
|
||||
})
|
||||
@@ -501,7 +501,7 @@ describe('prompt and cancel errors', () => {
|
||||
|
||||
it('keeps one-shot history readable without exposing prompt or cancel transport', async () => {
|
||||
const api = new FakeApiClient()
|
||||
const session = new Session(SID, api, {
|
||||
const session = new Session(SID, api, fakeRemote(), {
|
||||
address: { parentSessionId: PARENT, childSessionId: SID, mode: 'one-shot' },
|
||||
})
|
||||
await session.open()
|
||||
@@ -8,9 +8,9 @@
|
||||
*/
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { SessionId } from '@deepseek-ai/dsh-client-connection/client'
|
||||
import type { SessionId } from '@deepseek-ai/dsh-api-remotes/client'
|
||||
import { SessionCreateError, SessionsService, scopeOf } from '../src/client/sessions/service.ts'
|
||||
import { FakeApiClient, deferred, err, ok } from './fake-api.ts'
|
||||
import { FakeApiClient, deferred, err, fakeRemote, ok } from './fake-api.client.ts'
|
||||
|
||||
const sid = (s: string): SessionId => s as SessionId
|
||||
|
||||
@@ -23,7 +23,7 @@ interface Bench {
|
||||
function bench(): Bench {
|
||||
const ctx = new Context()
|
||||
const api = new FakeApiClient()
|
||||
const svc = new SessionsService(ctx, api)
|
||||
const svc = new SessionsService(ctx, api, fakeRemote())
|
||||
return { ctx, api, svc }
|
||||
}
|
||||
|
||||
@@ -6,14 +6,14 @@
|
||||
*/
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import type { ConnectionHandle, ConnectionSinks } from '@deepseek-ai/dsh-client-connection/client'
|
||||
import type { ConnectionHandle, ConnectionSinks } from '@deepseek-ai/dsh-api-remotes/client'
|
||||
import TypertRegistry from '@deepseek-ai/dsh-typert-registry'
|
||||
// Type-only: the api-remotes facade carries both the allowlist's selection seat
|
||||
// and the owner packages' `./types` declarations, which together give `$on` its
|
||||
// key face and per-event listener signatures.
|
||||
import type {} from '@deepseek-ai/dsh-api-remotes/client'
|
||||
import * as RuntimeClient from '../src/client/index.ts'
|
||||
import { FakeApiClient } from './fake-api.ts'
|
||||
import { FakeApiClient, fakeRemote } from './fake-api.client.ts'
|
||||
|
||||
/**
|
||||
* Compile-time face of `ctx.remote.$on`, asserted by type-checking this file
|
||||
@@ -74,6 +74,7 @@ async function mount(): Promise<Bench> {
|
||||
},
|
||||
}
|
||||
ctx.reflect.provide('connection', handle)
|
||||
ctx.reflect.provide('remote.commands', fakeRemote().commands)
|
||||
await ctx.plugin(RuntimeClient).await()
|
||||
return bench
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import type { SessionId, WorkspaceId, WorkspaceView } from '@deepseek-ai/dsh-client-connection/client'
|
||||
import type { SessionId, WorkspaceId, WorkspaceView } from '@deepseek-ai/dsh-api-remotes/client'
|
||||
import { SessionsService } from '../src/client/sessions/service.ts'
|
||||
import { WorkspaceManager } from '../src/client/workspaces/manager.ts'
|
||||
import { DirectoryBrowseError, WorkspaceCreateError, WorkspacesService } from '../src/client/workspaces/service.ts'
|
||||
import { FakeApiClient, deferred, err, ok } from './fake-api.ts'
|
||||
import { FakeApiClient, deferred, err, fakeRemote, ok } from './fake-api.client.ts'
|
||||
|
||||
const sid = (id: string): SessionId => id as SessionId
|
||||
const wid = (id: string): WorkspaceId => id as WorkspaceId
|
||||
@@ -191,7 +191,7 @@ describe('WorkspacesService', () => {
|
||||
it('feeds readiness and recent-Workspace targeting without changing Host order', async () => {
|
||||
const ctx = new Context()
|
||||
const api = new FakeApiClient()
|
||||
const sessions = new SessionsService(ctx, api)
|
||||
const sessions = new SessionsService(ctx, api, fakeRemote())
|
||||
const workspaces = new WorkspacesService(ctx, api, sessions)
|
||||
api.onWorkspaceList = () => Promise.resolve(ok({
|
||||
items: [
|
||||
@@ -219,7 +219,7 @@ describe('WorkspacesService', () => {
|
||||
it('connectWorkspace reuses the workspace-member blank session and creates otherwise', async () => {
|
||||
const ctx = new Context()
|
||||
const api = new FakeApiClient()
|
||||
const sessions = new SessionsService(ctx, api)
|
||||
const sessions = new SessionsService(ctx, api, fakeRemote())
|
||||
const workspaces = new WorkspacesService(ctx, api, sessions)
|
||||
api.onWorkspaceList = () => Promise.resolve(ok({
|
||||
items: [workspace('alpha', [sid('s-blank')]), workspace('beta'), workspace('gamma')] as never[],
|
||||
@@ -278,7 +278,7 @@ describe('WorkspacesService', () => {
|
||||
it('a rejected first prompt keeps the blank session eligible for connectWorkspace reuse', async () => {
|
||||
const ctx = new Context()
|
||||
const api = new FakeApiClient()
|
||||
const sessions = new SessionsService(ctx, api)
|
||||
const sessions = new SessionsService(ctx, api, fakeRemote())
|
||||
const workspaces = new WorkspacesService(ctx, api, sessions)
|
||||
api.onWorkspaceList = () => Promise.resolve(ok({ items: [workspace('alpha', [sid('s-blank')])] as never[] }))
|
||||
api.onList = () => Promise.resolve(ok({
|
||||
@@ -298,7 +298,7 @@ describe('WorkspacesService', () => {
|
||||
it('returns created Workspaces and preserves Host business errors', async () => {
|
||||
const ctx = new Context()
|
||||
const api = new FakeApiClient()
|
||||
const sessions = new SessionsService(ctx, api)
|
||||
const sessions = new SessionsService(ctx, api, fakeRemote())
|
||||
const workspaces = new WorkspacesService(ctx, api, sessions)
|
||||
api.onWorkspaceCreate = () => Promise.resolve(ok({
|
||||
workspace: { ...workspace('picked'), path: '/w/alpha', title: 'alpha' }, created: true,
|
||||
@@ -317,7 +317,7 @@ describe('WorkspacesService', () => {
|
||||
it('passes native directory selection and cancellation through without local state', async () => {
|
||||
const ctx = new Context()
|
||||
const api = new FakeApiClient()
|
||||
const sessions = new SessionsService(ctx, api)
|
||||
const sessions = new SessionsService(ctx, api, fakeRemote())
|
||||
const workspaces = new WorkspacesService(ctx, api, sessions)
|
||||
api.onPickDirectory = () => Promise.resolve(ok({ path: '/w/alpha' }))
|
||||
await expect(workspaces.pickDirectory()).resolves.toBe('/w/alpha')
|
||||
@@ -331,7 +331,7 @@ describe('WorkspacesService', () => {
|
||||
it('passes listings and creation through the browse wire, wrapping business failures', async () => {
|
||||
const ctx = new Context()
|
||||
const api = new FakeApiClient()
|
||||
const workspaces = new WorkspacesService(ctx, api, new SessionsService(ctx, api))
|
||||
const workspaces = new WorkspacesService(ctx, api, new SessionsService(ctx, api, fakeRemote()))
|
||||
const listing = { path: '/home/u', home: '/home/u', crumbs: [{ name: '/', path: '/', hidden: false }], entries: [{ name: 'p', path: '/home/u/p', hidden: false }], truncated: false }
|
||||
api.onListDirectory = () => Promise.resolve(ok(listing))
|
||||
await expect(workspaces.listDirectory()).resolves.toEqual(listing)
|
||||
@@ -352,7 +352,7 @@ describe('WorkspacesService', () => {
|
||||
it('opens a filesystem path through the host without local state', async () => {
|
||||
const ctx = new Context()
|
||||
const api = new FakeApiClient()
|
||||
const sessions = new SessionsService(ctx, api)
|
||||
const sessions = new SessionsService(ctx, api, fakeRemote())
|
||||
const workspaces = new WorkspacesService(ctx, api, sessions)
|
||||
await expect(workspaces.openPath('/w/alpha/a.ts')).resolves.toBeUndefined()
|
||||
expect(api.callsOf('host.openPath')).toEqual([{ path: '/w/alpha/a.ts' }])
|
||||
@@ -363,7 +363,7 @@ describe('WorkspacesService', () => {
|
||||
it('deletes a Workspace or preserves it when the Host rejects deletion', async () => {
|
||||
const ctx = new Context()
|
||||
const api = new FakeApiClient()
|
||||
const sessions = new SessionsService(ctx, api)
|
||||
const sessions = new SessionsService(ctx, api, fakeRemote())
|
||||
const workspaces = new WorkspacesService(ctx, api, sessions)
|
||||
api.onWorkspaceList = () => Promise.resolve(ok({ items: [workspace('alpha')] as never[] }))
|
||||
await workspaces.refresh()
|
||||
@@ -379,7 +379,7 @@ describe('WorkspacesService', () => {
|
||||
it('moves a Workspace through the durable order RPC and surfaces Host rejection', async () => {
|
||||
const ctx = new Context()
|
||||
const api = new FakeApiClient()
|
||||
const workspaces = new WorkspacesService(ctx, api, new SessionsService(ctx, api))
|
||||
const workspaces = new WorkspacesService(ctx, api, new SessionsService(ctx, api, fakeRemote()))
|
||||
api.onWorkspaceList = () => Promise.resolve(ok({
|
||||
items: [workspace('one'), workspace('two')] as never[],
|
||||
}))
|
||||
@@ -402,7 +402,7 @@ describe('WorkspacesService', () => {
|
||||
it('targets New Session at explicit, current-session, then recent Workspaces and clears with none', async () => {
|
||||
const ctx = new Context()
|
||||
const api = new FakeApiClient()
|
||||
const sessions = new SessionsService(ctx, api)
|
||||
const sessions = new SessionsService(ctx, api, fakeRemote())
|
||||
const workspaces = new WorkspacesService(ctx, api, sessions)
|
||||
api.onWorkspaceList = () => Promise.resolve(ok({
|
||||
items: [
|
||||
@@ -435,7 +435,7 @@ describe('WorkspacesService', () => {
|
||||
|
||||
const emptyCtx = new Context()
|
||||
const emptyApi = new FakeApiClient()
|
||||
const emptySessions = new SessionsService(emptyCtx, emptyApi)
|
||||
const emptySessions = new SessionsService(emptyCtx, emptyApi, fakeRemote())
|
||||
const emptyWorkspaces = new WorkspacesService(emptyCtx, emptyApi, emptySessions)
|
||||
const clear = vi.spyOn(emptySessions, 'clear')
|
||||
emptyWorkspaces.startSession()
|
||||
@@ -445,7 +445,7 @@ describe('WorkspacesService', () => {
|
||||
it('archives a session, projects the set from the response, list, and frame, and clears only the current one', async () => {
|
||||
const ctx = new Context()
|
||||
const api = new FakeApiClient()
|
||||
const sessions = new SessionsService(ctx, api)
|
||||
const sessions = new SessionsService(ctx, api, fakeRemote())
|
||||
const workspaces = new WorkspacesService(ctx, api, sessions)
|
||||
api.onList = () => Promise.resolve(ok({
|
||||
items: [
|
||||
@@ -491,7 +491,7 @@ describe('WorkspacesService', () => {
|
||||
it('clears a current archived by a remote frame and shields the set from a stale in-flight baseline', async () => {
|
||||
const ctx = new Context()
|
||||
const api = new FakeApiClient()
|
||||
const sessions = new SessionsService(ctx, api)
|
||||
const sessions = new SessionsService(ctx, api, fakeRemote())
|
||||
const workspaces = new WorkspacesService(ctx, api, sessions)
|
||||
api.onList = () => Promise.resolve(ok({
|
||||
items: [{ sessionId: sid('s-open'), updatedAt: 1, running: false, blank: false }],
|
||||
@@ -525,7 +525,7 @@ describe('startInitialSelection', () => {
|
||||
function bench() {
|
||||
const ctx = new Context()
|
||||
const api = new FakeApiClient()
|
||||
const sessions = new SessionsService(ctx, api)
|
||||
const sessions = new SessionsService(ctx, api, fakeRemote())
|
||||
const workspaces = new WorkspacesService(ctx, api, sessions)
|
||||
return { api, sessions, workspaces }
|
||||
}
|
||||
Reference in New Issue
Block a user