Merge remote-tracking branch 'origin/master' into turtle/image-cache-stale-0a243f674e12

# Conflicts:
#	packages/client/ui-primitives/README.i18n.yaml
#	packages/client/ui-primitives/README.md
#	packages/client/ui-primitives/README.zh.md
This commit is contained in:
Turtle committed 2026-10-18 14:49:15 +08:00
commit ef0a2b2e3d
4305 files changed
+174021 -23401

No files matched your search

@@ -6,7 +6,7 @@ import AgentRegistry from '@deepseek-ai/dsh-agent'
import type { Agent } from '@deepseek-ai/dsh-agent'
import { agentPresetProjectionDefinition } from '@deepseek-ai/dsh-agent-preset-registry'
import SessionStore, { SESSION_FORMAT_VERSION, SessionLogOffset, SessionId } from '@deepseek-ai/dsh-session'
import type { SessionEvent, SessionHeader } from '@deepseek-ai/dsh-session'
import type { SessionHeader } from '@deepseek-ai/dsh-session'
import { SessionAlreadyOwnedError } from '@deepseek-ai/dsh-session-persistence'
import type { SessionObservation } from '@deepseek-ai/dsh-session-query'
import TypertRegistry from '@deepseek-ai/dsh-typert-registry'
@@ -15,7 +15,6 @@ import {
ApiSessionAgentController,
ApiSessionCwdConflict,
ApiSessionNotFound,
ApiSessionSubagentOwnership,
inspectApiSession,
} from '../src/agent.ts'
import { installModelSelectionProjection } from '../src/model-selection-projection.ts'
@@ -329,123 +328,8 @@ describe('ApiSession model selection', () => {
})
})
describe('ApiSession create or adoption', () => {
it('shares one in-flight creation between concurrent callers', async () => {
const { ctx, agents } = await harness()
const cwd = mkdtempSync(join(tmpdir(), 'dsh-session-controller-concurrent-'))
tempDirs.push(cwd)
const meta = header('concurrent-create', cwd)
const created = unpublishedAgent(ctx, meta)
let release!: () => void
const gate = new Promise<void>((resolve) => { release = resolve })
const create = vi.spyOn(ctx.agents, 'create').mockImplementation(async () => {
await gate
return { agent: created, dispose: () => Promise.resolve() }
})
const first = agents.ensureSession(meta.id, cwd, false)
const second = agents.ensureSession(meta.id, cwd, false)
release()
await expect(Promise.all([first, second])).resolves.toEqual([created, created])
expect(create).toHaveBeenCalledOnce()
})
it('accepts a raced ordinary creation and rejects a raced attached child', async () => {
const ordinary = await harness()
const cwd = mkdtempSync(join(tmpdir(), 'dsh-session-controller-create-'))
tempDirs.push(cwd)
const ordinaryMeta = header('create-race', cwd)
const winner = agent(ordinary.ctx, ordinaryMeta)
vi.spyOn(ordinary.ctx.agents, 'create').mockImplementation(async () => {
await ordinary.ctx.agents.register(winner)
throw new Error('raced creation')
})
await expect(ordinary.agents.ensureSession(ordinaryMeta.id, cwd, false))
.resolves.toBe(winner)
const child = await harness()
const childCwd = mkdtempSync(join(tmpdir(), 'dsh-session-controller-child-'))
tempDirs.push(childCwd)
const childId = SessionId('create-child-race')
vi.spyOn(child.ctx.agents, 'create').mockImplementation(async () => {
child.ctx.sessions.create(childId, {
meta: { cwd: childCwd, parentSession: SessionId('parent'), origin: 'subagent' },
})
throw new Error('raced child creation')
})
await expect(child.agents.ensureSession(childId, childCwd, false))
.rejects.toBeInstanceOf(ApiSessionSubagentOwnership)
})
it('validates ownership and cwd on the Agent returned by creation', async () => {
const child = await harness()
const childCwd = mkdtempSync(join(tmpdir(), 'dsh-session-controller-returned-child-'))
tempDirs.push(childCwd)
const childMeta = {
...header('returned-child', childCwd),
parentSession: SessionId('parent'),
origin: 'subagent' as const,
}
const childAgent = unpublishedAgent(child.ctx, childMeta)
vi.spyOn(child.ctx.agents, 'create').mockResolvedValue({
agent: childAgent,
dispose: () => Promise.resolve(),
})
await expect(child.agents.ensureSession(childMeta.id, childCwd, false))
.rejects.toBeInstanceOf(ApiSessionSubagentOwnership)
const wrong = await harness()
const requestedCwd = mkdtempSync(join(tmpdir(), 'dsh-session-controller-wrong-cwd-'))
tempDirs.push(requestedCwd)
const wrongAgent = unpublishedAgent(wrong.ctx, header('wrong-returned-cwd', '/other'))
vi.spyOn(wrong.ctx.agents, 'create').mockResolvedValue({
agent: wrongAgent,
dispose: () => Promise.resolve(),
})
await expect(wrong.agents.ensureSession(wrongAgent.id, requestedCwd, false))
.rejects.toBeInstanceOf(ApiSessionCwdConflict)
})
it('resumes a matching persisted identity and preserves its selected preset', async () => {
const { ctx, agents } = await harness()
const meta = { ...header('stored'), agentPreset: 'minimal' }
const events = [{
type: 'agent-preset/selected',
seq: 0,
time: 1,
data: { agentPreset: 'minimal' },
}] as SessionEvent[]
providePersistence(ctx, {
list: () => Promise.resolve([meta]),
inspect: () => Promise.resolve({ meta, events }),
})
ctx.provide('agentPresets', {
resolve: (id?: string) => Promise.resolve({ id: id ?? 'minimal' }),
mount: () => Promise.resolve(),
} as never)
const resumed = {
id: meta.id,
session: {
id: meta.id,
header: meta,
snapshotEvents: () => events,
eventAt: (seq: number) => events[seq],
seq: events.length,
},
status: 'idle',
ctx,
} as unknown as Agent
const resume = vi.spyOn(ctx.agents, 'resume').mockResolvedValue({
agent: resumed,
dispose: () => Promise.resolve(),
})
await expect(agents.ensureSession(meta.id, '/workspace', true, 'minimal')).resolves.toBe(resumed)
expect(resume).toHaveBeenCalledWith(expect.objectContaining({ resumeSessionId: meta.id }))
})
it('rejects an ownership race before resume and a persisted cwd conflict', async () => {
describe('ApiSession creation', () => {
it('rejects a resume that subagent routing adopts before publication', async () => {
const child = await harness()
const childMeta = header('resume-child-race')
providePersistence(child.ctx, {
@@ -464,15 +348,6 @@ describe('ApiSession create or adoption', () => {
await expect(child.agents.resolveAgent(childMeta.id)).resolves.toMatchObject({
error: { code: 'session/agent-busy' },
})
const conflict = await harness()
const stored = header('stored-cwd-conflict', '/stored')
providePersistence(conflict.ctx, {
list: () => Promise.resolve([stored]),
inspect: () => Promise.resolve({ meta: stored, events: [] }),
})
await expect(conflict.agents.ensureSession(stored.id, '/requested', true))
.rejects.toBeInstanceOf(ApiSessionCwdConflict)
})
it('surfaces directory creation failure', async () => {
@@ -481,7 +356,7 @@ describe('ApiSession create or adoption', () => {
tempDirs.push(parent)
const file = join(parent, 'file')
writeFileSync(file, 'not a directory')
await expect(agents.ensureSession(SessionId('mkdir-failure'), join(file, 'child'), false))
await expect(agents.createSession({ cwd: join(file, 'child'), reusable: () => true }))
.rejects.toThrow('failed to ensure project directory')
})
})
@@ -5,11 +5,14 @@ import type {} from '@deepseek-ai/dsh-agent-preset-registry'
import { createUserMessage } from '@deepseek-ai/dsh-llm'
import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
import { RemoteError } from '@deepseek-ai/dsh-typert-protocol'
import TypertRegistry from '@deepseek-ai/dsh-typert-registry'
import type { Workspace, WorkspaceId } from '@deepseek-ai/dsh-workspace'
import { describe, expect, it, vi } from 'vitest'
import {
ApiSessionAgentController,
ApiSessionCwdConflict,
type ApiSessionCreated,
type ApiSessionCreateRequest,
} from '../src/agent.ts'
import { SessionCommandController } from '../src/commands.ts'
import { installSessionReadTestServices, testSessionPersistence } from './test-remote.ts'
@@ -18,18 +21,31 @@ async function expectFailure(operation: Promise<unknown>, code: string): Promise
await expect(operation).rejects.toMatchObject({ code })
}
function controllerAgents(overrides: object = {}): ApiSessionAgentController {
return {
ensureSession: () => Promise.resolve(),
composeAgent: () => Promise.resolve({ setup: () => {} }),
presetForSession: () => undefined,
presetForObservation: () => undefined,
...overrides,
} as unknown as ApiSessionAgentController
/** The Agent controller with a fixed Agent composition, and optionally a fixed creation outcome. */
function controllerAgents(
ctx: Context,
fixed: {
readonly composeAgent?: ApiSessionAgentController['composeAgent']
readonly createSession?: ApiSessionAgentController['createSession']
} = {},
): ApiSessionAgentController {
const agents = new ApiSessionAgentController(ctx)
vi.spyOn(agents, 'composeAgent').mockImplementation(fixed.composeAgent ?? (() => Promise.resolve({ setup: () => {} })))
if (fixed.createSession !== undefined) vi.spyOn(agents, 'createSession').mockImplementation(fixed.createSession)
return agents
}
/** A creation outcome that publishes `sessionId` in `cwd` as a stand-in Agent. */
function publishedSession(ctx: Context, sessionId: SessionId) {
return (request: ApiSessionCreateRequest): Promise<ApiSessionCreated> => {
const session = ctx.sessions.create(sessionId, { meta: { cwd: request.cwd } })
return Promise.resolve({ agent: { id: session.id, session, status: 'idle', ctx } as Agent, agentPreset: undefined })
}
}
async function baseContext(): Promise<Context> {
const ctx = new Context()
await ctx.plugin(TypertRegistry)
await ctx.plugin(SessionStore)
await ctx.plugin(AgentRegistry)
installSessionReadTestServices(ctx)
@@ -41,29 +57,19 @@ async function baseContext(): Promise<Context> {
}
describe('Session creation failures', () => {
it('mints an identity with the default cwd when no explicit target is supplied', async () => {
it('creates in the default cwd without a named identity when no explicit target is supplied', async () => {
const ctx = await baseContext()
ctx.provide('workspaceRegistry', { get: () => undefined, list: () => [] } as never)
const ensureSession = vi.fn((sessionId: SessionId, cwd: string) => {
const session = ctx.sessions.create(sessionId, { meta: { cwd } })
return Promise.resolve({ id: sessionId, session } as Agent)
})
const controller = new SessionCommandController(
ctx,
controllerAgents({ ensureSession }),
'/default-workspace',
)
ctx.provide('workspaceRegistry', { get: () => undefined, list: () => [], archivedSessionIds: [] } as never)
const createSession = vi.fn(publishedSession(ctx, SessionId('session-minted')))
const controller = new SessionCommandController(ctx, controllerAgents(ctx, { createSession }), '/default-workspace')
const created = await controller.create({})
expect(created.sessionId).toMatch(/^session-/)
expect(created).not.toHaveProperty('agentPreset')
expect(ensureSession).toHaveBeenCalledWith(
created.sessionId,
'/default-workspace',
false,
undefined,
)
expect(created).toEqual({ sessionId: 'session-minted' })
expect(createSession).toHaveBeenCalledOnce()
const request = createSession.mock.calls[0]?.[0]
expect(request?.cwd).toBe('/default-workspace')
expect(Object.keys(request ?? {}).sort()).toEqual(['cwd', 'reusable'])
await ctx.fiber.dispose()
})
@@ -72,7 +78,7 @@ describe('Session creation failures', () => {
missing.provide('workspaceRegistry', { get: () => undefined, list: () => [] } as never)
const missingController = new SessionCommandController(
missing,
controllerAgents(),
controllerAgents(missing),
'/default',
)
await expectFailure(missingController.create({
@@ -92,7 +98,7 @@ describe('Session creation failures', () => {
} as never)
const failedController = new SessionCommandController(
failed,
controllerAgents(),
controllerAgents(failed, { createSession: publishedSession(failed, SessionId('workspace-session')) }),
'/default',
)
await expectFailure(failedController.create({
@@ -119,10 +125,6 @@ describe('Session creation failures', () => {
error: new ApiSessionCwdConflict(SessionId('wrong-cwd'), '/requested', '/stored'),
code: 'session/conflict',
},
{
error: Object.assign(new Error('writer already held'), { name: 'SessionAlreadyOwnedError' }),
code: 'session/writer-held',
},
{
error: new Error('factory unavailable'),
code: 'gateway/internal',
@@ -132,7 +134,7 @@ describe('Session creation failures', () => {
ctx.provide('workspaceRegistry', { get: () => undefined, list: () => [] } as never)
const controller = new SessionCommandController(
ctx,
controllerAgents({ ensureSession: () => Promise.reject(error) }),
controllerAgents(ctx, { createSession: () => Promise.reject(error) }),
'/default',
)
@@ -144,7 +146,7 @@ describe('Session creation failures', () => {
it('rejects contradictory create targets', async () => {
const ctx = await baseContext()
const controller = new SessionCommandController(ctx, controllerAgents(), '/default')
const controller = new SessionCommandController(ctx, controllerAgents(ctx), '/default')
await expectFailure(controller.create({
workspaceId: 'workspace-1' as WorkspaceId,
@@ -184,7 +186,7 @@ describe('Session fork failures', () => {
const withoutPersistence = await baseContext()
withoutPersistence.provide('workspaceRegistry', { list: () => [] } as never)
const unavailableController = new SessionCommandController(
withoutPersistence, controllerAgents(), '/default',
withoutPersistence, controllerAgents(withoutPersistence), '/default',
)
await expectFailure(unavailableController.fork({
sessionId: SessionId('missing'),
@@ -197,7 +199,7 @@ describe('Session fork failures', () => {
list: () => Promise.resolve([]),
inspect: vi.fn(),
}) as never)
const missingController = new SessionCommandController(missing, controllerAgents(), '/default')
const missingController = new SessionCommandController(missing, controllerAgents(missing), '/default')
await expectFailure(missingController.fork({
sessionId: SessionId('missing'),
}), 'session/not-found')
@@ -208,7 +210,7 @@ describe('Session fork failures', () => {
const ctx = await baseContext()
ctx.provide('workspaceRegistry', { list: () => [] } as never)
vi.spyOn(ctx.sessionQuery, 'observeSession').mockRejectedValue(new Error('storage offline'))
const controller = new SessionCommandController(ctx, controllerAgents(), '/default')
const controller = new SessionCommandController(ctx, controllerAgents(ctx), '/default')
await expectFailure(controller.fork({ sessionId: SessionId('unreadable') }), 'gateway/internal')
await ctx.fiber.dispose()
@@ -218,7 +220,7 @@ describe('Session fork failures', () => {
const ctx = await baseContext()
ctx.provide('workspaceRegistry', { list: () => [] } as never)
const source = ctx.sessions.create(SessionId('empty-source'))
const controller = new SessionCommandController(ctx, controllerAgents(), '/default')
const controller = new SessionCommandController(ctx, controllerAgents(ctx), '/default')
await expectFailure(controller.fork({ sessionId: source.id }), 'session/fork-unavailable')
await expect(controller.fork({ sessionId: source.id, atSeq: 0 })).rejects.toMatchObject({
@@ -237,7 +239,7 @@ describe('Session fork failures', () => {
parentSession: SessionId('parent'),
origin: 'subagent',
})
const lineageController = new SessionCommandController(lineage, controllerAgents(), '/default')
const lineageController = new SessionCommandController(lineage, controllerAgents(lineage), '/default')
await expectFailure(lineageController.fork({ sessionId: child.id }), 'gateway/internal')
await lineage.fiber.dispose()
@@ -245,7 +247,7 @@ describe('Session fork failures', () => {
creation.provide('workspaceRegistry', { list: () => [] } as never)
const source = completedSession(creation, 'creation-source', '/workspace')
vi.spyOn(creation.agents, 'create').mockRejectedValue(new Error('factory failed'))
const creationController = new SessionCommandController(creation, controllerAgents(), '/default')
const creationController = new SessionCommandController(creation, controllerAgents(creation), '/default')
await expectFailure(creationController.fork({ sessionId: source.id }), 'gateway/internal')
await creation.fiber.dispose()
})
@@ -262,7 +264,7 @@ describe('Session fork failures', () => {
const create = vi.spyOn(ctx.agents, 'create').mockImplementation(
(options: CreateAgentOptions) => Promise.resolve(resolvedHandle(ctx, options.sessionId)),
)
const controller = new SessionCommandController(ctx, controllerAgents(), '/default')
const controller = new SessionCommandController(ctx, controllerAgents(ctx), '/default')
await expectFailure(controller.fork({ sessionId: source.id }), 'session/workspace-attach-failed')
const options = create.mock.calls[0]?.[0]
@@ -279,7 +281,7 @@ describe('Session fork failures', () => {
const create = vi.spyOn(ctx.agents, 'create').mockImplementation(
(options: CreateAgentOptions) => Promise.resolve(resolvedHandle(ctx, options.sessionId)),
)
const controller = new SessionCommandController(ctx, controllerAgents({
const controller = new SessionCommandController(ctx, controllerAgents(ctx, {
composeAgent: () => Promise.resolve({ agentPreset: 'minimal', setup: () => {} }),
}), '/default')
@@ -7,7 +7,7 @@ import type {
FileAttachmentRef, ImageAttachmentRef, SaveFileAttachment, SaveFileStreamAttachment,
} from '@deepseek-ai/dsh-attachment'
import { createUserMessage } from '@deepseek-ai/dsh-llm'
import type { UserMessage } from '@deepseek-ai/dsh-llm'
import type { MessageSource, UserMessage } from '@deepseek-ai/dsh-llm'
import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
import CommandRuntime from '@deepseek-ai/dsh-commands'
import { createScope } from '@deepseek-ai/dsh-scope'
@@ -113,6 +113,9 @@ async function uploadHarness(origin?: 'subagent'): Promise<{
}
}
/** The owner source `SessionController.prompt()` builds for {@link promptRequest}. */
const USER_SOURCE: MessageSource = { kind: 'user', rpcId: 'req-1' as SessionRequestId }
function promptRequest(content: Parameters<SessionCommandController['prompt']>[0]['content']) {
return {
requestId: 'req-1' as SessionRequestId,
@@ -163,7 +166,7 @@ describe('Session file uploads', () => {
{ type: 'file', receiptId: receipt.receiptId },
{ type: 'image', mediaType: 'image/png', data: 'AAAA' },
{ type: 'text', text: 'read it' },
]))
]), USER_SOURCE)
expect(followup).toHaveBeenCalledTimes(1)
const message = followup.mock.calls[0]?.[0] as UserMessage
expect(message.content).toEqual([
@@ -190,7 +193,7 @@ describe('Session file uploads', () => {
name: 'huge.bin',
}))
expect(receipt.file).toMatchObject({ name: 'huge.bin', bytes: 5 })
await controller.prompt(promptRequest([{ type: 'file', receiptId: receipt.receiptId }]))
await controller.prompt(promptRequest([{ type: 'file', receiptId: receipt.receiptId }]), USER_SOURCE)
expect((followup.mock.calls[0]?.[0] as UserMessage).content).toEqual([
{ type: 'file', attachment: receipt.file },
])
@@ -217,7 +220,7 @@ describe('Session file uploads', () => {
await expect(controller.prompt(promptRequest([
{ type: 'image', mediaType: 'image/png', data: 'AAAA' },
{ type: 'file', receiptId: 'missing-receipt' as FileUploadReceiptId },
]))).rejects.toMatchObject({ code: 'session/attachment-invalid', details: { reason: 'FILE_NOT_STAGED' } })
]), USER_SOURCE)).rejects.toMatchObject({ code: 'session/attachment-invalid', details: { reason: 'FILE_NOT_STAGED' } })
expect(followup).not.toHaveBeenCalled()
expect(saveImages).not.toHaveBeenCalled()
})
@@ -302,7 +305,7 @@ describe('Session file uploads', () => {
const { controller, uploads, agent } = await uploadHarness()
uploads.retirePrompt(agent, 'not-staged')
const receipt = await uploads.upload(agent, { data: 'AAAA' }, new AbortController().signal)
await controller.prompt(promptRequest([{ type: 'file', receiptId: receipt.receiptId }]))
await controller.prompt(promptRequest([{ type: 'file', receiptId: receipt.receiptId }]), USER_SOURCE)
expect(uploads.resolve(agent, receipt.receiptId)).toEqual(receipt.file)
uploads.retirePrompt(agent, 'other-request')
expect(uploads.resolve(agent, receipt.receiptId)).toEqual(receipt.file)
@@ -317,7 +320,7 @@ describe('Session file uploads', () => {
{ data: 'AAAA' },
new AbortController().signal,
)
await first.controller.prompt(promptRequest([{ type: 'file', receiptId: observed.receiptId }]))
await first.controller.prompt(promptRequest([{ type: 'file', receiptId: observed.receiptId }]), USER_SOURCE)
first.ctx.emit('session/event', first.agent.session, {
type: 'user/message',
data: createUserMessage({
@@ -345,9 +348,9 @@ describe('Session file uploads', () => {
it('deduplicates a retried rpcId already present in the Agent inbox', async () => {
const { controller, agent, followup } = await uploadHarness()
const request = promptRequest([{ type: 'text', text: 'once' }])
await controller.prompt(request)
await controller.prompt(request, USER_SOURCE)
agent.inbox.append('next-turn', followup.mock.calls[0]?.[0] as UserMessage)
await expect(controller.prompt(request)).resolves.toEqual({ accepted: true })
await expect(controller.prompt(request, USER_SOURCE)).resolves.toEqual({ accepted: true })
expect(followup).toHaveBeenCalledOnce()
})
@@ -364,7 +367,7 @@ describe('Session file uploads', () => {
source: { kind: 'user', rpcId: request.requestId },
}), { surfaceOp: 'append' })
await expect(controller.prompt(request)).resolves.toEqual({ accepted: true })
await expect(controller.prompt(request, USER_SOURCE)).resolves.toEqual({ accepted: true })
expect(followup).not.toHaveBeenCalled()
})
@@ -374,7 +377,7 @@ describe('Session file uploads', () => {
saveImages.mockReturnValueOnce(admitted.promise)
const prompting = controller.prompt(promptRequest([
{ type: 'image', mediaType: 'image/png', data: 'AAAA' },
]))
]), USER_SOURCE)
await vi.waitFor(() => { expect(saveImages).toHaveBeenCalledOnce() })
await disposeAgent()
admitted.resolve([{
@@ -386,7 +389,7 @@ describe('Session file uploads', () => {
it('rejects when a previously bound receipt retires during image admission', async () => {
const { controller, uploads, agent, saveImages } = await uploadHarness()
const receipt = await uploads.upload(agent, { data: 'AAAA' }, new AbortController().signal)
await controller.prompt(promptRequest([{ type: 'file', receiptId: receipt.receiptId }]))
await controller.prompt(promptRequest([{ type: 'file', receiptId: receipt.receiptId }]), USER_SOURCE)
const admitted = Promise.withResolvers<readonly ImageAttachmentRef[]>()
saveImages.mockReturnValueOnce(admitted.promise)
const prompting = controller.prompt({
@@ -395,7 +398,7 @@ describe('Session file uploads', () => {
{ type: 'image', mediaType: 'image/png', data: 'AAAA' },
]),
requestId: 'req-2' as SessionRequestId,
})
}, USER_SOURCE)
await vi.waitFor(() => { expect(saveImages).toHaveBeenCalledOnce() })
uploads.retirePrompt(agent, 'req-1')
admitted.resolve([{
@@ -409,12 +412,12 @@ describe('Session file uploads', () => {
it('keeps the prior receipt binding when a later prompt attempt fails', async () => {
const { controller, uploads, agent, followup } = await uploadHarness()
const receipt = await uploads.upload(agent, { data: 'AAAA' }, new AbortController().signal)
await controller.prompt(promptRequest([{ type: 'file', receiptId: receipt.receiptId }]))
await controller.prompt(promptRequest([{ type: 'file', receiptId: receipt.receiptId }]), USER_SOURCE)
followup.mockImplementationOnce(() => { throw new Error('busy') })
await expect(controller.prompt({
...promptRequest([{ type: 'file', receiptId: receipt.receiptId }]),
requestId: 'req-2' as SessionRequestId,
})).rejects.toMatchObject({ code: 'session/agent-busy' })
}, USER_SOURCE)).rejects.toMatchObject({ code: 'session/agent-busy' })
uploads.retirePrompt(agent, 'req-1')
expect(uploads.resolve(agent, receipt.receiptId)).toBeUndefined()
})
@@ -425,7 +428,7 @@ describe('Session file uploads', () => {
followup.mockImplementationOnce(() => { throw new Error('busy') })
await expect(controller.prompt(promptRequest([
{ type: 'file', receiptId: receipt.receiptId },
]))).rejects.toMatchObject({ code: 'session/agent-busy' })
]), USER_SOURCE)).rejects.toMatchObject({ code: 'session/agent-busy' })
uploads.retirePrompt(agent, 'req-1')
expect(uploads.resolve(agent, receipt.receiptId)).toEqual(receipt.file)
})
@@ -433,7 +436,7 @@ describe('Session file uploads', () => {
it('retires a prompt-bound receipt when its queued occurrence is removed', async () => {
const { controller, uploads, agent, followup } = await uploadHarness()
const receipt = await uploads.upload(agent, { data: 'AAAA' }, new AbortController().signal)
await controller.prompt(promptRequest([{ type: 'file', receiptId: receipt.receiptId }]))
await controller.prompt(promptRequest([{ type: 'file', receiptId: receipt.receiptId }]), USER_SOURCE)
const queued = followup.mock.calls[0]?.[0] as UserMessage
agent.inbox.append('next-turn', queued)
expect(await controller.updateQueue({
@@ -454,7 +457,7 @@ describe('Session file uploads', () => {
await controller.prompt(promptRequest([
{ type: 'file', receiptId: first.receiptId },
{ type: 'file', receiptId: second.receiptId },
]))
]), USER_SOURCE)
const message = followup.mock.calls[0]?.[0] as UserMessage
expect(message.content).toEqual([
@@ -8,6 +8,7 @@ import type { SessionRecord } from '@deepseek-ai/dsh-session-query'
import { resolve } from 'node:path'
import { performance } from 'node:perf_hooks'
import { scheduler } from 'node:timers/promises'
import RemoteAccessService from '@deepseek-ai/dsh-api-remote-access'
import SessionController from '../src/index.ts'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { ApiSessionList } from '../src/list.ts'
@@ -181,10 +182,15 @@ describe('Session-list work slices', () => {
it('routes an overridden budget through the public Session Controller', async () => {
const h = await harness([{ id: 'first', phase: 'cold' }, { id: 'middle', phase: 'cold' }, { id: 'last', phase: 'cold' }], 4)
const dispose = (): void => {}
h.ctx.provide('typert', { lookups: { configure: () => dispose }, contexts: { configureHost: () => dispose } } as never)
h.ctx.provide('typert', {
lookups: { configure: () => dispose },
contexts: { configureHost: () => dispose },
resources: { register: () => dispose },
} as never)
h.ctx.provide('fileUploads', { registerAgentResolver: () => dispose } as never)
h.ctx.provide('agentDefaultModel', { currentSelection: () => ({ provider: 'test', model: 'test' }) } as never)
h.ctx.provide('workspaceRegistry', { list: () => [], archivedSessionIds: [] } as never)
new RemoteAccessService(h.ctx)
const controller = new SessionController(h.ctx, { nativeOpen: false, listWorkSliceMs: 4 }, { canOpenPath: () => false })
const yieldWork = vi.spyOn(scheduler, 'yield')
const result = await controller.list({}, new AbortController().signal)
@@ -1194,6 +1194,17 @@ describe('remaining branches', () => {
expect(await manager.create()).toMatchObject({ ok: false })
})
it('create forwards the requested Agent preset and catalogs the identity the Host returns', async ({ mock, remote }) => {
remote.session.create.mockResolvedValue(ok({ sessionId: S2, agentPreset: 'minimal' }))
const manager = makeManager(mock, remote)
const result = await manager.create({ workspaceId: 'w1' as never, sessionId: S1, agentPreset: 'minimal' })
expect(remote.session.create).toHaveBeenCalledExactlyOnceWith({ workspaceId: 'w1', sessionId: S1, agentPreset: 'minimal' })
expect(result).toMatchObject({ ok: true, value: { sessionId: S2 } })
expect(manager.getListSnapshot().items.map(item => item.sessionId)).toEqual([S2])
})
it.for(['create', 'fork'] as const)('preserves pushed running state when a %s response arrives later', async (operation, { mock, remote }) => {
const response = Promise.withResolvers<Awaited<ReturnType<typeof remote.session.create>>>()
remote.session[operation].mockImplementation(() => response.promise)
@@ -9,7 +9,7 @@ import AgentRegistry from '@deepseek-ai/dsh-agent'
import SessionStore, { SESSION_FORMAT_VERSION, SessionId, SessionLogOffset, SessionSeq } from '@deepseek-ai/dsh-session'
import type { SessionHeader } from '@deepseek-ai/dsh-session'
import type { SessionPersistenceSnapshot } from '@deepseek-ai/dsh-session-persistence'
import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import * as JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import SessionProjectionCache, {
checkpointRecord,
@@ -0,0 +1,222 @@
/**
* Session Controller under non-owner callers: the Session resource kind it
* registers, the Session readers of `session/page` and `session/follow`,
* `session/list`, `session/search`, and `session/control` called through a
* `global` role and filtered per Session through `remoteAccess.allows()`, the caller recorded
* on prompt sources, and `session/projections` admitted per Session through
* the Gateway.
*/
import { Context } from '@deepseek-ai/cordis'
import type { Agent } from '@deepseek-ai/dsh-agent'
import { mountAgentLoopTestDependencies, mountAgentLoopTestHarness } from '@deepseek-ai/dsh-agent-loop-testkit'
import TypertGatewayService from '@deepseek-ai/dsh-api-gateway'
import type { RemoteAccessBinding, RemoteAccessRole } from '@deepseek-ai/dsh-api-remote-access'
import { createUserMessage } from '@deepseek-ai/dsh-llm'
import { SessionId } from '@deepseek-ai/dsh-session'
import { remoteMethods, type PeerId, type PeerScope, type Principal, type RemoteInvocation } from '@deepseek-ai/dsh-typert-protocol'
import TypertRegistry from '@deepseek-ai/dsh-typert-registry'
import { afterEach, describe, expect, it, vi } from 'vitest'
import type SessionController from '../src/index.ts'
import { ApiSessionList } from '../src/list.ts'
import type { SessionControlFrame, SessionRequestId, SessionSearchValue } from '../src/types.ts'
import { createSessionTestController } from './test-remote.ts'
declare module '@deepseek-ai/dsh-typert-protocol' {
interface PrincipalMap {
/** Test-only non-owner caller shared by the access specs. */
'test-guest': { readonly kind: 'test-guest'; readonly guest: string }
}
}
const OWNER: Principal = { kind: 'owner' }
const GUEST: Principal = { kind: 'test-guest', guest: 'session-controller' }
const SHARED = SessionId('shared-session')
const PRIVATE = SessionId('private-session')
/** Calls the Session list and control methods; each item still needs a Session role. */
const LISTER: RemoteAccessRole<'global'> = { name: 'test:session-lister', resource: 'global', actions: ['session/list', 'session/control'] }
/** Reads one Session: its list row, its control projections, and its stored projections. */
const READER: RemoteAccessRole<'session'> = {
name: 'test:session-reader', resource: 'session', actions: ['session/list', 'session/control', 'session/projections'],
}
const LIST_CALLS: RemoteAccessBinding = { role: LISTER, resource: { kind: 'global' } }
const READS_SHARED: RemoteAccessBinding = { role: READER, resource: { kind: 'session', id: SHARED } }
const SEARCH_CALLS: RemoteAccessBinding = {
role: { name: 'test:session-searcher', resource: 'global', actions: ['session/search'] },
resource: { kind: 'global' },
}
const SEARCHES_SHARED: RemoteAccessBinding = {
role: { name: 'test:session-search-reader', resource: 'session', actions: ['session/search'] },
resource: { kind: 'session', id: SHARED },
}
const owned = new Set<Context>()
afterEach(async () => {
await Promise.all([...owned].map(ctx => ctx.fiber.dispose()))
owned.clear()
})
/**
* Register the guest policy.
* @returns the bindings the policy answers with; edits apply from the next Remote call.
*/
function guestPolicy(ctx: Context, ...bindings: RemoteAccessBinding[]): RemoteAccessBinding[] {
const held = [...bindings]
ctx.remoteAccess.registerPolicy('test-guest', { bindings: () => held })
return held
}
/** Invoke `session/<method>` as the guest through the Gateway. */
function guestCall(ctx: Context, method: string, args: Readonly<Record<string, unknown>>): Promise<unknown> {
const peer: PeerScope = { id: 'session-access-peer' as PeerId, ctx, principal: GUEST, interactions: 'answer', signal: new AbortController().signal, dispose: async () => {} }
return ctx.typertGateway.invoke({ peer, namespace: 'session', method, args })
}
async function harness(): Promise<{
readonly ctx: Context
readonly controller: SessionController
readonly shared: Agent
readonly hidden: Agent
/** The Context a Gateway dispatch of `session/<method>` binds for `principal`. */
readonly as: (principal: Principal, method: string) => Context
}> {
const ctx = new Context()
owned.add(ctx)
await ctx.plugin(TypertRegistry)
await mountAgentLoopTestDependencies(ctx)
const loop = await mountAgentLoopTestHarness(ctx)
const shared = await loop.create(SHARED)
const hidden = await loop.create(PRIVATE)
const controller = createSessionTestController(ctx, { defaultModelSelection: () => ({ provider: 'p', model: 'm' }), cwd: '/tmp' })
const as = (principal: Principal, method: string): Context => {
const invocation: RemoteInvocation = {
request: { namespace: 'session', method, args: {} },
service: 'sessionController',
peer: { id: 'session-access-peer' as PeerId, ctx, principal, interactions: 'answer', signal: new AbortController().signal, dispose: async () => {} },
signal: new AbortController().signal,
uplink: <In>(): AsyncIterable<In> => ({ [Symbol.asyncIterator]: () => ({ next: async () => ({ done: true, value: undefined }) }) }),
}
return ctx.extend({ invocation })
}
return { ctx, controller, shared, hidden, as }
}
function message(text: string) {
return createUserMessage({ content: [{ type: 'text', text }], source: { kind: 'user' } })
}
describe('Session Controller access', () => {
it('owns the Session resource kind and its default sessionId field', async () => {
const { ctx } = await harness()
expect(() => ctx.typert.resources.register('session', { field: 'sessionId' })).toThrow(/already registered/)
})
it('reads the Session resource of page and follow requests from the addressed Session', async () => {
const { controller } = await harness()
for (const method of ['page', 'follow']) {
const reader = remoteMethods(controller).find(marker => marker.method === method)?.resources?.['session']
expect(reader?.({ address: { kind: 'session', sessionId: SHARED } })).toBe(SHARED)
// A subagent address reads the child transcript; whether the parent's access covers it is the policy's rule.
expect(reader?.({ address: { kind: 'subagent', parentSessionId: SHARED, childSessionId: PRIVATE } })).toBe(PRIVATE)
}
})
it('lets a global role call session/list and lists only the Sessions a Session role admits', async () => {
const { ctx, as } = await harness()
await ctx.plugin(TypertGatewayService)
const held = guestPolicy(ctx, LIST_CALLS, READS_SHARED)
const rows = async (): Promise<SessionId[]> => {
const value = await guestCall(ctx, 'list', { _request: {} }) as { readonly items: readonly { readonly sessionId: SessionId }[] }
return value.items.map(item => item.sessionId)
}
await expect(rows()).resolves.toEqual([SHARED])
// The global role admits the call, not any row.
held.splice(0, held.length, LIST_CALLS)
await expect(rows()).resolves.toEqual([])
// A Session role alone does not admit the call: the global resource does not inherit from it.
held.splice(0, held.length, READS_SHARED)
await expect(rows()).rejects.toMatchObject({
code: 'remote-access/denied', details: { action: 'session/list', reason: 'no-allow' },
})
const owner = await as(OWNER, 'list').sessionController.list({}, new AbortController().signal)
expect(owner.items.map(item => item.sessionId).sort()).toEqual([PRIVATE, SHARED].sort())
})
it('returns a non-owner only the session/search hits whose Session a Session role admits', async () => {
const { ctx, as } = await harness()
await ctx.plugin(TypertGatewayService)
guestPolicy(ctx, SEARCH_CALLS, SEARCHES_SHARED)
const hits: SessionSearchValue = {
items: [{ sessionId: PRIVATE, snippet: 'private hit' }, { sessionId: SHARED, snippet: 'shared hit' }],
hasMore: true,
}
vi.spyOn(ApiSessionList.prototype, 'search').mockResolvedValue(hits)
await expect(guestCall(ctx, 'search', { request: { query: 'hit' } })).resolves.toEqual({
items: [{ sessionId: SHARED, snippet: 'shared hit' }],
hasMore: true,
})
await expect(as(OWNER, 'search').sessionController.search({ query: 'hit' }, new AbortController().signal)).resolves.toEqual(hits)
})
it('filters session/control baselines and projection frames for a non-owner', async () => {
const { ctx, shared, hidden, as } = await harness()
guestPolicy(ctx, LIST_CALLS, READS_SHARED)
const abort = new AbortController()
const guest = as(GUEST, 'control').sessionController.control(abort.signal)[Symbol.asyncIterator]()
const owner = as(OWNER, 'control').sessionController.control(abort.signal)[Symbol.asyncIterator]()
const baselineSessions = async (iterator: AsyncIterator<SessionControlFrame>): Promise<string[]> => {
const next = await iterator.next()
if (next.done === true || next.value.type !== 'baseline') throw new Error('control stream opened without a baseline')
return Object.keys(next.value.value.projections).sort()
}
expect(await baselineSessions(guest)).toEqual([SHARED])
expect(await baselineSessions(owner)).toEqual([PRIVATE, SHARED].sort())
hidden.inbox.append('next-turn', message('private work'))
shared.inbox.append('next-turn', message('shared work'))
const projected = async (iterator: AsyncIterator<SessionControlFrame>): Promise<SessionId> => {
for (;;) {
const next = await iterator.next()
if (next.done === true) throw new Error('control stream ended')
if (next.value.type === 'projection' && next.value.key === 'inbox') return next.value.sessionId
}
}
// The owner sees the private Session's frame first; the guest's first Inbox frame is the shared one.
expect(await projected(owner)).toBe(PRIVATE)
expect(await projected(guest)).toBe(SHARED)
abort.abort()
await Promise.all([guest.return?.(), owner.return?.()])
})
it('records a non-owner caller on the prompt source and leaves owner sources unchanged', async () => {
const { shared, as } = await harness()
const signal = new AbortController().signal
const prompt = (principal: Principal, requestId: string) => as(principal, 'prompt').sessionController.prompt({
sessionId: SHARED,
requestId: requestId as SessionRequestId,
mode: 'queue',
content: [{ type: 'text', text: `from ${principal.kind}` }],
}, signal)
await expect(prompt(GUEST, 'guest-1')).resolves.toEqual({ accepted: true })
await expect(prompt(OWNER, 'owner-1')).resolves.toEqual({ accepted: true })
// The durable Inbox splice records each accepted message with its source.
const logged = shared.session.snapshotEvents()
.flatMap(event => event.type === 'agent/inbox/spliced' ? event.data.inserted.map(item => item.source) : [])
expect(logged).toEqual([
{ kind: 'user', rpcId: 'guest-1', principal: GUEST },
{ kind: 'user', rpcId: 'owner-1' },
])
})
it('lets a non-owner read projections only in a Session its Session role names', async () => {
const { ctx } = await harness()
await ctx.plugin(TypertGatewayService)
guestPolicy(ctx, READS_SHARED)
const projections = (sessionId: SessionId): Promise<unknown> => guestCall(ctx, 'projections', { request: { sessionId } })
await expect(projections(SHARED)).resolves.toMatchObject({ kind: 'sequenced' })
await expect(projections(PRIVATE)).rejects.toMatchObject({
code: 'remote-access/denied', details: { action: 'session/projections', reason: 'no-allow' },
})
})
})
@@ -1,4 +1,4 @@
/** Session creation and adoption rules for Agent preset identity. */
/** Session creation, blank-Session reuse, and Agent preset identity through `session.create`. */
import { mkdtempSync, realpathSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
@@ -6,12 +6,14 @@ import { join } from 'node:path'
import { Context } from '@deepseek-ai/cordis'
import AgentRegistry from '@deepseek-ai/dsh-agent'
import type { Agent, AgentFactory } from '@deepseek-ai/dsh-agent'
import { turnBoundaryProjectionDefinition } from '@deepseek-ai/dsh-agent-loop'
import { agentPresetProjectionDefinition } from '@deepseek-ai/dsh-agent-preset-registry'
import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
import type { Session } from '@deepseek-ai/dsh-session'
import SessionStore, { SESSION_FORMAT_VERSION, SessionId, SessionSeq } from '@deepseek-ai/dsh-session'
import type { Session, SessionEvent, SessionHeader } from '@deepseek-ai/dsh-session'
import { RemoteError } from '@deepseek-ai/dsh-typert-protocol'
import { afterEach, describe, expect, it } from 'vitest'
import { createSessionTestRemote } from './test-remote.ts'
import type { WorkspaceId } from '@deepseek-ai/dsh-workspace'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { createSessionTestRemote, testSessionPersistence } from './test-remote.ts'
/** Booted contexts and their temp roots, torn down after each test. */
const contexts: Context[] = []
@@ -25,53 +27,137 @@ function stubAgent(session: Session): Agent {
return { id: session.id, session, status: 'idle' } as unknown as Agent
}
function roster(ids: readonly string[]): unknown {
const presetOf = (id: string): object => ({
id,
trust: 'system',
})
return {
defaultId: ids[0],
resolve: (id?: string) => {
const wanted = id ?? ids[0] ?? ''
if (!ids.includes(wanted)) {
return Promise.reject(new RemoteError(
'agent-preset/not-found',
`agent-presets: preset "${wanted}" not found (available: ${ids.join(', ') || 'none'})`,
{ agentPreset: wanted, available: ids },
))
}
return Promise.resolve(presetOf(wanted))
},
mount: (_ctx: Context, id?: string) => Promise.resolve(presetOf(id ?? ids[0] ?? '')),
}
/** A stored Session the persistence double serves until an Agent resumes it. */
interface StoredSession {
readonly header: SessionHeader
readonly events: readonly SessionEvent[]
}
async function harness(presets?: readonly string[]) {
interface HarnessOptions {
/** Declared presets; the first is the default. Omitted, no roster is composed. */
readonly presets?: readonly string[]
/** Stored Sessions keyed by id. */
readonly stored?: ReadonlyMap<SessionId, StoredSession>
/** Stored Sessions whose writer another owner holds. */
readonly held?: ReadonlySet<SessionId>
/** Stored Sessions another resume in this process publishes while this one waits for the writer. */
readonly publishedByOtherResume?: ReadonlySet<SessionId>
/** Stored Sessions whose storage fails to open for writing. */
readonly unreadable?: ReadonlySet<SessionId>
/** Events a resume finds beyond the stored log, as when another Host wrote after the observation. */
readonly writtenSinceObservation?: ReadonlyMap<SessionId, readonly SessionEvent[]>
/** Live Sessions whose preset switch the roster refuses as already started. */
readonly locked?: ReadonlySet<SessionId>
/** Declared presets whose activation failed. */
readonly broken?: ReadonlySet<string>
/** Requested presets whose resolution waits for the paired promise. */
readonly resolving?: ReadonlyMap<string, Promise<void>>
/** Workspace placement served to `workspaceId` requests. */
readonly workspace?: { readonly members: readonly SessionId[]; readonly archived: readonly SessionId[] }
}
const WORKSPACE = 'workspace-1' as WorkspaceId
async function harness(options: HarnessOptions = {}) {
const cwd = realpathSync(mkdtempSync(join(tmpdir(), 'dsh-session-preset-')))
tempDirs.push(cwd)
const ctx = new Context()
contexts.push(ctx)
await ctx.plugin(SessionStore)
await ctx.plugin(AgentRegistry)
const presets = options.presets
const selections: { sessionId: SessionId; agentPreset: string }[] = []
const resumed: SessionId[] = []
if (presets !== undefined) {
ctx.provide('agentPresets', roster(presets) as never)
const known = (id?: string): string => {
const wanted = id ?? presets[0] ?? ''
if (!presets.includes(wanted)) {
throw new RemoteError('agent-preset/not-found', `Unknown agent preset: ${wanted}`, {
agentPreset: wanted, available: [...presets],
})
}
return wanted
}
const usable = (id?: string): string => {
const wanted = known(id)
if (options.broken?.has(wanted) === true) {
throw new RemoteError('agent-preset/invalid', 'activation failed', { agentPreset: wanted, reason: 'activation failed' })
}
return wanted
}
ctx.provide('agentPresets', {
defaultId: presets[0],
resolve: (id?: string) => Promise.resolve(id === undefined ? undefined : options.resolving?.get(id))
.then(() => ({ id: known(id) })),
mount: (_ctx: Context, id?: string) => Promise.resolve().then(() => ({ id: usable(id) })),
select: async (agent: Agent, agentPreset: string) => {
usable(agentPreset)
if (options.locked?.has(agent.id) === true) {
throw new RemoteError('agent-preset/locked', 'This session has already started', {
sessionId: agent.id, agentPreset,
})
}
selections.push({ sessionId: agent.id, agentPreset })
agent.session.append('agent-preset/selected', { agentPreset })
return agentPreset
},
} as never)
}
const factory: AgentFactory = {
async createAgent(_ownerCtx, options) {
const session = ctx.sessions.create(
options.sessionId,
options.meta === undefined ? {} : { meta: options.meta },
)
const agent = stubAgent(session)
;(agent as { ctx?: Context }).ctx = ctx
await options.setup?.(ctx, agent)
const unregister = await ctx.agents.register(agent)
return { agent, dispose: async () => { await unregister() } }
const stored = options.stored ?? new Map<SessionId, StoredSession>()
ctx.provide('sessionPersistence', testSessionPersistence(ctx, {
list: () => Promise.resolve([...stored.values()].map(record => record.header)),
inspect: (id: SessionId) => {
const record = stored.get(id)
return Promise.resolve(record === undefined ? undefined : { meta: record.header, events: [...record.events] })
},
async resume() {
throw new Error('test harness has no persisted sessions')
}) as never)
const { members, archived } = options.workspace ?? { members: [], archived: [] }
const workspace = { id: WORKSPACE, path: cwd, sessionIds: members, attachSession: () => Promise.resolve() }
ctx.provide('workspaceRegistry', {
get: (id: WorkspaceId) => id === WORKSPACE ? workspace : undefined,
list: () => [workspace],
archivedSessionIds: archived,
} as never)
/** Run setup on the unpublished Session, then publish it and its Agent, as the Agent factory does. */
const publish = async (
session: Session,
setup: Parameters<AgentFactory['createAgent']>[1]['setup'],
): Promise<{ agent: Agent; dispose: () => Promise<void> }> => {
const agent = stubAgent(session)
;(agent as { ctx?: Context }).ctx = ctx
await setup?.(ctx, agent)
ctx.effect(function* () {
yield ctx.sessions.enter(session)
ctx.sessions.announce(session)
}, 'test: published Session')
const unregister = await ctx.agents.register(agent)
return { agent, dispose: async () => { await unregister() } }
}
const factory: AgentFactory = {
async createAgent(_ownerCtx, create) {
const session = ctx.sessions.prepare(create.sessionId, create.meta === undefined ? {} : { meta: create.meta })
return publish(session, create.setup)
},
async resume(_ownerCtx, resume) {
const id = resume.resumeSessionId
if (options.held?.has(id) === true) {
throw Object.assign(new Error(`session "${id}" is written by another owner`), { name: 'SessionAlreadyOwnedError' })
}
const record = stored.get(id)
if (record === undefined || options.unreadable?.has(id) === true) {
throw new Error(`test persistence cannot open Session "${id}" for writing`)
}
if (options.publishedByOtherResume?.has(id) === true) {
await publish(ctx.sessions.prepare(id, { meta: record.header, seed: [...record.events] }), undefined)
throw Object.assign(new Error(`session "${id}" is written by this process`), { name: 'SessionAlreadyOwnedError' })
}
resumed.push(id)
const session = ctx.sessions.prepare(id, {
meta: record.header,
seed: [...record.events, ...options.writtenSinceObservation?.get(id) ?? []],
})
return publish(session, resume.setup)
},
}
ctx.agents.setFactory(factory)
@@ -79,13 +165,43 @@ async function harness(presets?: readonly string[]) {
defaultModelSelection: () => ({ provider: 'test', model: 'test-model' }),
cwd,
})
ctx.sessionProjections.register(turnBoundaryProjectionDefinition)
if (presets !== undefined) ctx.sessionProjections.register(agentPresetProjectionDefinition)
return { ctx, remote }
const presetOf = (id: SessionId): string | null | undefined => {
const session = ctx.sessions.get(id)
return session === undefined ? undefined : ctx.sessionProjections.stateOf(session, 'agentPreset')
}
const selectedEvents = (id: SessionId): unknown[] => {
const session = ctx.sessions.get(id)
return session === undefined ? [] : session.snapshotEvents()
.filter(event => event.type === 'agent-preset/selected')
.map(event => event.data)
}
return { ctx, remote, cwd, selections, resumed, presetOf, selectedEvents }
}
function storedHeader(id: SessionId, cwd: string, extra: Partial<SessionHeader> = {}): SessionHeader {
return { version: SESSION_FORMAT_VERSION, id, createdAt: 1, isSeeded: false, cwd, ...extra }
}
function modelSelection(seq: number): SessionEvent {
return { type: 'model/selection', seq: SessionSeq(seq), time: 1, data: { provider: 'test', model: 'test-model' } }
}
function presetSelection(seq: number, agentPreset: string): SessionEvent {
return { type: 'agent-preset/selected', seq: SessionSeq(seq), time: 1, data: { agentPreset } }
}
function completedTurn(seq: number): SessionEvent[] {
return [
{ type: 'turn/start', seq: SessionSeq(seq), time: 1, data: { turn: 1 } },
{ type: 'turn/end', seq: SessionSeq(seq + 1), time: 2, data: { turn: 1, reason: { kind: 'completed' } } },
]
}
describe('session.create Agent preset identity', () => {
it('records the requested preset on the Session header', async () => {
const { ctx, remote } = await harness(['standard', 'minimal'])
const { ctx, remote } = await harness({ presets: ['standard', 'minimal'] })
const created = await remote.create({ sessionId: SessionId('s1'), agentPreset: 'minimal' })
@@ -94,7 +210,7 @@ describe('session.create Agent preset identity', () => {
})
it('records the roster default when the caller names no preset', async () => {
const { ctx, remote } = await harness(['standard', 'minimal'])
const { ctx, remote } = await harness({ presets: ['standard', 'minimal'] })
await remote.create({ sessionId: SessionId('s2') })
@@ -102,55 +218,13 @@ describe('session.create Agent preset identity', () => {
})
it('rejects an unknown preset', async () => {
const { remote } = await harness(['standard'])
const { remote } = await harness({ presets: ['standard'] })
const response = await remote.create({ sessionId: SessionId('s3'), agentPreset: 'nope' })
expect(response).toMatchObject({ ok: false, error: { code: 'agent-preset/not-found' } })
})
it('refuses to adopt a live Session under a different preset', async () => {
const { remote } = await harness(['standard', 'minimal'])
await remote.create({ sessionId: SessionId('s4'), agentPreset: 'minimal' })
const response = await remote.create({ sessionId: SessionId('s4'), agentPreset: 'standard' })
expect(response).toMatchObject({
ok: false,
error: {
code: 'agent-preset/conflict',
details: {
sessionId: 's4',
requestedPreset: 'standard',
existingPreset: 'minimal',
},
},
})
})
it('adopts a live Session under the preset selected in its log', async () => {
const { ctx, remote } = await harness(['standard', 'minimal'])
await remote.create({ sessionId: SessionId('s4b'), agentPreset: 'standard' })
ctx.sessions.get(SessionId('s4b'))?.append('agent-preset/selected', { agentPreset: 'minimal' })
const adopted = await remote.create({ sessionId: SessionId('s4b'), agentPreset: 'minimal' })
const stale = await remote.create({ sessionId: SessionId('s4b'), agentPreset: 'standard' })
expect(adopted).toMatchObject({ ok: true, value: { agentPreset: 'minimal' } })
expect(stale).toMatchObject({
ok: false,
error: { details: { existingPreset: 'minimal' } },
})
})
it('adopts a live Session unchanged when the caller names no preset', async () => {
const { remote } = await harness(['standard', 'minimal'])
await remote.create({ sessionId: SessionId('s5'), agentPreset: 'minimal' })
await expect(remote.create({ sessionId: SessionId('s5') }))
.resolves.toMatchObject({ ok: true })
})
it('leaves the header preset-less when no roster is composed', async () => {
const { ctx, remote } = await harness()
@@ -158,6 +232,195 @@ describe('session.create Agent preset identity', () => {
expect(ctx.sessions.get(SessionId('s6'))?.header.agentPreset).toBeUndefined()
})
})
describe('session.create reuse of a live blank Session', () => {
it('recomposes a blank Session under the default when the caller names no preset', async () => {
const { remote, selections, presetOf } = await harness({ presets: ['standard', 'minimal'] })
await remote.create({ sessionId: SessionId('draft'), agentPreset: 'minimal' })
const reused = await remote.create({ sessionId: SessionId('draft') })
expect(reused).toMatchObject({ ok: true, value: { sessionId: 'draft', agentPreset: 'standard' } })
expect(selections).toEqual([{ sessionId: 'draft', agentPreset: 'standard' }])
expect(presetOf(SessionId('draft'))).toBe('standard')
})
it('recomposes a blank Session under the named preset', async () => {
const { remote, selections } = await harness({ presets: ['standard', 'minimal'] })
await remote.create({ sessionId: SessionId('draft') })
const reused = await remote.create({ sessionId: SessionId('draft'), agentPreset: 'minimal' })
expect(reused).toMatchObject({ ok: true, value: { sessionId: 'draft', agentPreset: 'minimal' } })
expect(selections).toEqual([{ sessionId: 'draft', agentPreset: 'minimal' }])
})
it('keeps a blank Session that already runs the requested preset', async () => {
const { ctx, remote, selections } = await harness({ presets: ['standard', 'minimal'] })
await remote.create({ sessionId: SessionId('draft') })
ctx.sessions.get(SessionId('draft'))?.append('agent-preset/selected', { agentPreset: 'minimal' })
const reused = await remote.create({ sessionId: SessionId('draft'), agentPreset: 'minimal' })
expect(reused).toMatchObject({ ok: true, value: { sessionId: 'draft', agentPreset: 'minimal' } })
expect(selections).toEqual([])
})
it('validates the requested preset before touching the blank Session', async () => {
const { remote, selections, presetOf } = await harness({ presets: ['standard', 'minimal'] })
await remote.create({ sessionId: SessionId('draft'), agentPreset: 'minimal' })
const response = await remote.create({ sessionId: SessionId('draft'), agentPreset: 'deleted' })
expect(response).toMatchObject({ ok: false, error: { code: 'agent-preset/not-found' } })
expect(selections).toEqual([])
expect(presetOf(SessionId('draft'))).toBe('minimal')
})
it('keeps the blank Session unchanged when the requested preset fails to activate', async () => {
const { remote, selections, presetOf } = await harness({
presets: ['standard', 'broken'],
broken: new Set(['broken']),
})
await remote.create({ sessionId: SessionId('draft') })
const response = await remote.create({ sessionId: SessionId('draft'), agentPreset: 'broken' })
expect(response).toMatchObject({ ok: false, error: { code: 'agent-preset/invalid' } })
expect(selections).toEqual([])
expect(presetOf(SessionId('draft'))).toBe('standard')
})
it('creates a new Session instead of reusing one that has started', async () => {
const { ctx, remote, selections, presetOf } = await harness({ presets: ['standard', 'minimal'] })
await remote.create({ sessionId: SessionId('talk'), agentPreset: 'minimal' })
const talk = ctx.sessions.get(SessionId('talk'))
talk?.append('turn/start', { turn: 1 })
talk?.append('turn/end', { turn: 1, reason: { kind: 'completed' } })
const created = await remote.create({ sessionId: SessionId('talk') })
if (!created.ok) throw new Error(`create failed: ${created.error.message}`)
expect(created.value.sessionId).not.toBe('talk')
expect(created.value.agentPreset).toBe('standard')
expect(selections).toEqual([])
expect(presetOf(SessionId('talk'))).toBe('minimal')
})
it('creates a new Session when a turn starts before the preset switch', async () => {
const { remote } = await harness({
presets: ['standard', 'minimal'],
locked: new Set([SessionId('racing')]),
})
await remote.create({ sessionId: SessionId('racing'), agentPreset: 'minimal' })
const created = await remote.create({ sessionId: SessionId('racing') })
if (!created.ok) throw new Error(`create failed: ${created.error.message}`)
expect(created.value.sessionId).not.toBe('racing')
})
it('serializes concurrent reuse of one Session in request order', async () => {
const { remote, selections, presetOf } = await harness({ presets: ['standard', 'minimal'] })
await remote.create({ sessionId: SessionId('draft') })
const [first, second] = await Promise.all([
remote.create({ sessionId: SessionId('draft'), agentPreset: 'minimal' }),
remote.create({ sessionId: SessionId('draft') }),
])
expect(first).toMatchObject({ ok: true, value: { sessionId: 'draft' } })
expect(second).toMatchObject({ ok: true, value: { sessionId: 'draft', agentPreset: 'standard' } })
expect(selections).toEqual([
{ sessionId: 'draft', agentPreset: 'minimal' },
{ sessionId: 'draft', agentPreset: 'standard' },
])
expect(presetOf(SessionId('draft'))).toBe('standard')
})
it('claims one Session in arrival order when an earlier preset resolves later', async () => {
const minimal = Promise.withResolvers<undefined>()
const { remote, selections, presetOf } = await harness({
presets: ['standard', 'minimal'], resolving: new Map([['minimal', minimal.promise]]),
})
await remote.create({ sessionId: SessionId('draft') })
const first = remote.create({ sessionId: SessionId('draft'), agentPreset: 'minimal' })
const second = remote.create({ sessionId: SessionId('draft') })
await new Promise(resolve => setTimeout(resolve, 0))
minimal.resolve(undefined)
expect(await first).toMatchObject({ ok: true, value: { sessionId: 'draft', agentPreset: 'minimal' } })
expect(await second).toMatchObject({ ok: true, value: { sessionId: 'draft', agentPreset: 'standard' } })
expect(selections).toEqual([
{ sessionId: 'draft', agentPreset: 'minimal' },
{ sessionId: 'draft', agentPreset: 'standard' },
])
expect(presetOf(SessionId('draft'))).toBe('standard')
})
it('shares one creation between concurrent callers of a new identity', async () => {
const { ctx, remote } = await harness({ presets: ['standard'] })
const results = await Promise.all([
remote.create({ sessionId: SessionId('fresh') }),
remote.create({ sessionId: SessionId('fresh') }),
])
expect(results).toMatchObject([
{ ok: true, value: { sessionId: 'fresh' } },
{ ok: true, value: { sessionId: 'fresh' } },
])
expect(ctx.agents.list().map(agent => agent.id)).toEqual(['fresh'])
})
it('refuses a Session in another directory', async () => {
const { ctx, remote } = await harness({ presets: ['standard'] })
const elsewhere = realpathSync(mkdtempSync(join(tmpdir(), 'dsh-session-preset-elsewhere-')))
tempDirs.push(elsewhere)
await remote.create({ sessionId: SessionId('other'), cwd: elsewhere })
const response = await remote.create({ sessionId: SessionId('other') })
expect(response).toMatchObject({
ok: false,
error: { code: 'session/conflict', details: { sessionId: 'other', existingCwd: elsewhere } },
})
expect(ctx.agents.list().map(agent => agent.id)).toEqual(['other'])
})
it('refuses a Session in another directory before resolving the requested preset', async () => {
const { remote } = await harness({ presets: ['standard'] })
const elsewhere = realpathSync(mkdtempSync(join(tmpdir(), 'dsh-session-preset-elsewhere-')))
tempDirs.push(elsewhere)
await remote.create({ sessionId: SessionId('other'), cwd: elsewhere })
const response = await remote.create({ sessionId: SessionId('other'), agentPreset: 'nope' })
expect(response).toMatchObject({ ok: false, error: { code: 'session/conflict', details: { sessionId: 'other' } } })
})
it('leaves a subagent identity to subagent routing', async () => {
const { ctx, remote, cwd } = await harness({ presets: ['standard'] })
ctx.sessions.create(SessionId('child'), {
meta: { cwd, parentSession: SessionId('parent'), origin: 'subagent' },
})
const response = await remote.create({ sessionId: SessionId('child') })
expect(response).toMatchObject({ ok: false, error: { code: 'session/agent-busy' } })
})
it('reuses a preset-less blank Session unchanged when no roster is composed', async () => {
const { ctx, remote } = await harness()
await remote.create({ sessionId: SessionId('s5') })
const reused = await remote.create({ sessionId: SessionId('s5') })
expect(reused).toMatchObject({ ok: true, value: { sessionId: 's5' } })
expect(ctx.sessions.get(SessionId('s5'))?.header.agentPreset).toBeUndefined()
})
it('explains why a preset-less Session cannot be adopted under one', async () => {
const { remote } = await harness()
@@ -177,6 +440,235 @@ describe('session.create Agent preset identity', () => {
})
if (response.ok) throw new Error('unreachable')
expect('existingPreset' in response.error.details).toBe(false)
expect(response.error.message).toContain('records no agent preset')
expect(response.error.message).toBe('the deployment composes no agent preset roster, so session "s7" cannot run preset "standard"')
})
})
describe('session.create reuse of a stored blank Session', () => {
it('resumes a blank Session whose recorded preset is no longer declared under the default', async () => {
const draft = SessionId('stale-draft')
const cwd = realpathSync(mkdtempSync(join(tmpdir(), 'dsh-session-preset-stored-')))
tempDirs.push(cwd)
const { remote, resumed, presetOf, selectedEvents } = await harness({
presets: ['standard', 'minimal'],
stored: new Map([[draft, { header: storedHeader(draft, cwd, { agentPreset: 'liangshen' }), events: [modelSelection(0)] }]]),
})
const reused = await remote.create({ sessionId: draft, cwd })
expect(reused).toMatchObject({ ok: true, value: { sessionId: 'stale-draft', agentPreset: 'standard' } })
expect(resumed).toEqual(['stale-draft'])
expect(presetOf(draft)).toBe('standard')
expect(selectedEvents(draft)).toEqual([{ agentPreset: 'standard' }])
})
it('resumes a blank Session under the default even when its previous choice is still declared', async () => {
const draft = SessionId('picked-draft')
const cwd = realpathSync(mkdtempSync(join(tmpdir(), 'dsh-session-preset-stored-')))
tempDirs.push(cwd)
const { remote, presetOf, selectedEvents } = await harness({
presets: ['standard', 'minimal'],
stored: new Map([[draft, { header: storedHeader(draft, cwd, { agentPreset: 'standard' }), events: [presetSelection(0, 'minimal')] }]]),
})
const reused = await remote.create({ sessionId: draft, cwd })
expect(reused).toMatchObject({ ok: true, value: { sessionId: 'picked-draft', agentPreset: 'standard' } })
expect(presetOf(draft)).toBe('standard')
expect(selectedEvents(draft)).toEqual([{ agentPreset: 'minimal' }, { agentPreset: 'standard' }])
})
it('resumes a blank Session under its recorded preset when the caller names it', async () => {
const draft = SessionId('restored-draft')
const cwd = realpathSync(mkdtempSync(join(tmpdir(), 'dsh-session-preset-stored-')))
tempDirs.push(cwd)
const { remote, presetOf, selectedEvents } = await harness({
presets: ['standard', 'minimal'],
stored: new Map([[draft, { header: storedHeader(draft, cwd, { agentPreset: 'standard' }), events: [presetSelection(0, 'minimal')] }]]),
})
const reused = await remote.create({ sessionId: draft, cwd, agentPreset: 'minimal' })
expect(reused).toMatchObject({ ok: true, value: { sessionId: 'restored-draft', agentPreset: 'minimal' } })
expect(presetOf(draft)).toBe('minimal')
expect(selectedEvents(draft)).toEqual([{ agentPreset: 'minimal' }])
})
it('rejects a named preset that is no longer declared without resuming the Session', async () => {
const draft = SessionId('stale-draft')
const cwd = realpathSync(mkdtempSync(join(tmpdir(), 'dsh-session-preset-stored-')))
tempDirs.push(cwd)
const { ctx, remote, resumed } = await harness({
presets: ['standard'],
stored: new Map([[draft, { header: storedHeader(draft, cwd, { agentPreset: 'liangshen' }), events: [modelSelection(0)] }]]),
})
const response = await remote.create({ sessionId: draft, cwd, agentPreset: 'liangshen' })
expect(response).toMatchObject({
ok: false,
error: { code: 'agent-preset/not-found', details: { agentPreset: 'liangshen', available: ['standard'] } },
})
expect(resumed).toEqual([])
expect(ctx.agents.list()).toEqual([])
})
it('creates a new Session instead of resuming one that has started', async () => {
const talk = SessionId('stored-talk')
const cwd = realpathSync(mkdtempSync(join(tmpdir(), 'dsh-session-preset-stored-')))
tempDirs.push(cwd)
const { remote, resumed } = await harness({
presets: ['standard'],
stored: new Map([[talk, { header: storedHeader(talk, cwd, { agentPreset: 'liangshen' }), events: completedTurn(0) }]]),
})
const created = await remote.create({ sessionId: talk, cwd })
if (!created.ok) throw new Error(`create failed: ${created.error.message}`)
expect(created.value.sessionId).not.toBe('stored-talk')
expect(created.value.agentPreset).toBe('standard')
expect(resumed).toEqual([])
})
it('creates a new Session when another owner holds the blank Session', async () => {
const draft = SessionId('held-draft')
const cwd = realpathSync(mkdtempSync(join(tmpdir(), 'dsh-session-preset-stored-')))
tempDirs.push(cwd)
const { ctx, remote } = await harness({
presets: ['standard'],
stored: new Map([[draft, { header: storedHeader(draft, cwd, { agentPreset: 'standard' }), events: [modelSelection(0)] }]]),
held: new Set([draft]),
})
const created = await remote.create({ sessionId: draft, cwd })
if (!created.ok) throw new Error(`create failed: ${created.error.message}`)
expect(created.value.sessionId).not.toBe('held-draft')
expect(ctx.sessions.get(draft)).toBeUndefined()
})
it('reuses the Agent another resume published while holding the writer', async () => {
const draft = SessionId('contended-draft')
const cwd = realpathSync(mkdtempSync(join(tmpdir(), 'dsh-session-preset-stored-')))
tempDirs.push(cwd)
const { remote, selections, presetOf } = await harness({
presets: ['standard', 'minimal'],
stored: new Map([[draft, { header: storedHeader(draft, cwd, { agentPreset: 'minimal' }), events: [modelSelection(0)] }]]),
publishedByOtherResume: new Set([draft]),
})
const reused = await remote.create({ sessionId: draft, cwd })
expect(reused).toMatchObject({ ok: true, value: { sessionId: 'contended-draft', agentPreset: 'standard' } })
expect(selections).toEqual([{ sessionId: 'contended-draft', agentPreset: 'standard' }])
expect(presetOf(draft)).toBe('standard')
})
it('reports an unreadable Session instead of creating another one', async () => {
const { ctx, remote } = await harness({ presets: ['standard'] })
vi.spyOn(ctx.sessionQuery, 'observeSession').mockRejectedValue(new Error('storage offline'))
const response = await remote.create({ sessionId: SessionId('unreadable') })
expect(response).toMatchObject({ ok: false, error: { code: 'gateway/internal' } })
expect(ctx.agents.list()).toEqual([])
})
it('reports a storage failure instead of creating another Session', async () => {
const draft = SessionId('unreadable-draft')
const cwd = realpathSync(mkdtempSync(join(tmpdir(), 'dsh-session-preset-stored-')))
tempDirs.push(cwd)
const { ctx, remote } = await harness({
presets: ['standard'],
stored: new Map([[draft, { header: storedHeader(draft, cwd, { agentPreset: 'standard' }), events: [modelSelection(0)] }]]),
unreadable: new Set([draft]),
})
const response = await remote.create({ sessionId: draft, cwd })
expect(response).toMatchObject({ ok: false, error: { code: 'gateway/internal' } })
expect(ctx.agents.list()).toEqual([])
})
it('creates a new Session when the blank Session started after it was observed', async () => {
const draft = SessionId('overtaken-draft')
const cwd = realpathSync(mkdtempSync(join(tmpdir(), 'dsh-session-preset-stored-')))
tempDirs.push(cwd)
const { ctx, remote, resumed } = await harness({
presets: ['standard', 'minimal'],
stored: new Map([[draft, { header: storedHeader(draft, cwd, { agentPreset: 'minimal' }), events: [modelSelection(0)] }]]),
writtenSinceObservation: new Map([[draft, completedTurn(1)]]),
})
const created = await remote.create({ sessionId: draft, cwd })
if (!created.ok) throw new Error(`create failed: ${created.error.message}`)
expect(created.value.sessionId).not.toBe('overtaken-draft')
expect(resumed).toEqual(['overtaken-draft'])
expect(ctx.sessions.get(draft)).toBeUndefined()
})
it('refuses a stored Session in another directory', async () => {
const other = SessionId('stored-other')
const cwd = realpathSync(mkdtempSync(join(tmpdir(), 'dsh-session-preset-stored-')))
tempDirs.push(cwd)
const { remote, resumed } = await harness({
presets: ['standard'],
stored: new Map([[other, { header: storedHeader(other, '/elsewhere'), events: [modelSelection(0)] }]]),
})
const response = await remote.create({ sessionId: other, cwd })
expect(response).toMatchObject({
ok: false,
error: { code: 'session/conflict', details: { sessionId: 'stored-other', existingCwd: '/elsewhere' } },
})
expect(resumed).toEqual([])
})
it('leaves a stored subagent identity to subagent routing', async () => {
const child = SessionId('stored-child')
const cwd = realpathSync(mkdtempSync(join(tmpdir(), 'dsh-session-preset-stored-')))
tempDirs.push(cwd)
const { remote, resumed } = await harness({
presets: ['standard'],
stored: new Map([[child, {
header: storedHeader(child, cwd, { parentSession: SessionId('parent'), origin: 'subagent' }),
events: [modelSelection(0)],
}]]),
})
const response = await remote.create({ sessionId: child, cwd })
expect(response).toMatchObject({ ok: false, error: { code: 'session/agent-busy' } })
expect(resumed).toEqual([])
})
})
describe('session.create Workspace placement', () => {
it('reuses a blank member of the requested Workspace', async () => {
const draft = SessionId('member-draft')
const { remote } = await harness({ presets: ['standard'], workspace: { members: [draft], archived: [] } })
await remote.create({ sessionId: draft })
const reused = await remote.create({ sessionId: draft, workspaceId: WORKSPACE })
expect(reused).toMatchObject({ ok: true, value: { sessionId: 'member-draft' } })
})
it.each([
{ name: 'outside the Workspace', members: [], archived: [] },
{ name: 'archived', members: [SessionId('placed-draft')], archived: [SessionId('placed-draft')] },
])('creates a new Session instead of reusing a blank Session $name', async ({ members, archived }) => {
const draft = SessionId('placed-draft')
const { remote, selections, presetOf } = await harness({ presets: ['standard', 'minimal'], workspace: { members, archived } })
await remote.create({ sessionId: draft, agentPreset: 'minimal' })
const created = await remote.create({ sessionId: draft, workspaceId: WORKSPACE })
if (!created.ok) throw new Error(`create failed: ${created.error.message}`)
expect(created.value.sessionId).not.toBe('placed-draft')
expect(selections).toEqual([])
expect(presetOf(draft)).toBe('minimal')
})
})
@@ -513,6 +513,19 @@ describe('prompt and cancel errors', () => {
expect(session.getSnapshot().subagent).toEqual({ address: CHILD, parentAvailable: true })
})
it('sends an addressed child continuation under its echo request id and retires a refused echo', async ({ mock, start }) => {
const session = await sessionBench(mock, start, SID, { address: CHILD, parentAvailable: true })
await session.open()
const accepted = session.beginSubmission({ mode: 'queue', text: '继续', attachments: [] })
await session.prompt([{ type: 'text', text: '继续' }], 'queue', undefined, accepted.requestId)
expect(mock.log.requests('subagents/prompt')).toMatchObject([{ requestId: accepted.requestId, ...CHILD }])
expect(session.getSnapshot().pendingSubmissions.map(echo => echo.requestId)).toEqual([accepted.requestId])
const refused = session.beginSubmission({ mode: 'queue', text: '附件', attachments: [] })
await session.prompt([{ type: 'file', receiptId: 'receipt' as never }], 'queue', undefined, refused.requestId)
expect(session.getSnapshot().pendingSubmissions.map(echo => echo.requestId)).toEqual([accepted.requestId])
})
it('forwards continuation image parts to the subagent prompt Remote unstripped', async ({ mock, start }) => {
const session = await sessionBench(mock, start, SID, { address: CHILD, parentAvailable: true })
await session.open()
@@ -31,6 +31,7 @@ import {
remoteErrorOf,
type RemoteResult,
} from '@deepseek-ai/dsh-typert-protocol'
import RemoteAccessService from '@deepseek-ai/dsh-api-remote-access'
import SessionController from '../src/index.ts'
import type {
ModelCatalog,
@@ -243,6 +244,7 @@ function installControllers(
ctx.provide('typert', {
lookups: { configure: () => dispose },
contexts: { configureHost: () => dispose },
resources: { register: () => dispose },
} as never)
}
if (ctx.get('agentDefaultModel') === undefined) {
@@ -289,6 +291,7 @@ function installControllers(
} as never)
}
installSessionReadTestServices(ctx)
if (ctx.get('remoteAccess') === undefined) new RemoteAccessService(ctx)
const cwd = vi.spyOn(process, 'cwd').mockReturnValue(defaults.cwd)
let controller: SessionController
try {