| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280 |
- import { Context } from '@deepseek-ai/cordis'
- import AgentRegistry from '@deepseek-ai/dsh-agent'
- import type { Agent, AgentHandle, CreateAgentOptions } from '@deepseek-ai/dsh-agent'
- import { PresetMountError } from '@deepseek-ai/dsh-agent-presets'
- import { createUserMessage } from '@deepseek-ai/dsh-llm'
- import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
- import type { Workspace, WorkspaceId } from '@deepseek-ai/dsh-workspace'
- import { describe, expect, it, vi } from 'vitest'
- import {
- ApiSessionAgentController,
- ApiSessionCwdConflict,
- } from '../src/agent.ts'
- import { SessionCommandController } from '../src/commands.ts'
- import { installSessionReadTestServices, testSessionPersistence } from './test-remote.ts'
- async function expectFailure(operation: Promise<unknown>, code: string): Promise<void> {
- await expect(operation).rejects.toMatchObject({ failure: { code } })
- }
- function controllerAgents(overrides: object = {}): ApiSessionAgentController {
- return {
- ensureSession: () => Promise.resolve(),
- composeAgent: () => Promise.resolve({ setup: () => {} }),
- presetForSession: () => undefined,
- presetForObservation: () => undefined,
- ...overrides,
- } as unknown as ApiSessionAgentController
- }
- async function baseContext(): Promise<Context> {
- const ctx = new Context()
- await ctx.plugin(SessionStore)
- await ctx.plugin(AgentRegistry)
- installSessionReadTestServices(ctx)
- ctx.provide('agentDefaultModel', {
- currentSelection: () => ({ provider: 'fixture', model: 'fixture-model' }),
- saveSelection: () => Promise.resolve(),
- } as never)
- return ctx
- }
- describe('Session creation failures', () => {
- it('mints an identity with the default cwd 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',
- )
- const created = await controller.create({})
- expect(created.sessionId).toMatch(/^session-/)
- expect(created).not.toHaveProperty('agentPreset')
- expect(ensureSession).toHaveBeenCalledWith(
- created.sessionId,
- '/default-workspace',
- false,
- undefined,
- )
- await ctx.fiber.dispose()
- })
- it('maps missing Workspaces and attachment failures', async () => {
- const missing = await baseContext()
- missing.provide('workspaceRegistry', { get: () => undefined, list: () => [] } as never)
- const missingController = new SessionCommandController(
- missing,
- controllerAgents(),
- '/default',
- )
- await expectFailure(missingController.create({
- workspaceId: 'missing' as WorkspaceId,
- }), 'workspace-not-found')
- await missing.fiber.dispose()
- const failed = await baseContext()
- const workspace = {
- id: 'workspace-1' as WorkspaceId,
- path: '/workspace',
- attachSession: () => Promise.reject(new Error('read-only workspace')),
- } as unknown as Workspace
- failed.provide('workspaceRegistry', {
- get: () => workspace,
- list: () => [workspace],
- } as never)
- const failedController = new SessionCommandController(
- failed,
- controllerAgents(),
- '/default',
- )
- await expectFailure(failedController.create({
- sessionId: SessionId('workspace-session'),
- workspaceId: workspace.id,
- }), 'workspace-attach-failed')
- await failed.fiber.dispose()
- })
- it.each([
- {
- error: new PresetMountError('broken', 'invalid composition'),
- code: 'agent-preset-invalid',
- },
- {
- error: new ApiSessionCwdConflict(SessionId('cwd-less'), '/requested', undefined),
- code: 'session-conflict',
- },
- {
- error: new ApiSessionCwdConflict(SessionId('wrong-cwd'), '/requested', '/stored'),
- code: 'session-conflict',
- },
- {
- error: new Error('factory unavailable'),
- code: 'internal',
- },
- ])('maps $code creation failures', async ({ error, code }) => {
- const ctx = await baseContext()
- ctx.provide('workspaceRegistry', { get: () => undefined, list: () => [] } as never)
- const controller = new SessionCommandController(
- ctx,
- controllerAgents({ ensureSession: () => Promise.reject(error) }),
- '/default',
- )
- await expectFailure(controller.create({
- sessionId: SessionId('failed-create'), cwd: '/requested',
- }), code)
- await ctx.fiber.dispose()
- })
- it('rejects contradictory create targets', async () => {
- const ctx = await baseContext()
- const controller = new SessionCommandController(ctx, controllerAgents(), '/default')
- await expectFailure(controller.create({
- workspaceId: 'workspace-1' as WorkspaceId,
- cwd: '/workspace',
- }), 'bad-request')
- await ctx.fiber.dispose()
- })
- })
- function completedSession(
- ctx: Context,
- id: string,
- cwd?: string,
- lineage: { parentSession?: SessionId; origin?: 'subagent' } = {},
- ) {
- const session = ctx.sessions.create(SessionId(id), {
- meta: { ...(cwd === undefined ? {} : { cwd }), ...lineage },
- })
- session.append('turn/start', { turn: 1 })
- session.append('user/message', createUserMessage({
- content: [{ type: 'text', text: 'work' }], source: { kind: 'user' },
- }), { surfaceOp: 'append' })
- session.append('turn/end', { turn: 1, reason: { kind: 'completed' } })
- return session
- }
- function resolvedHandle(ctx: Context, sessionId: SessionId): AgentHandle {
- return {
- agent: { id: sessionId, status: 'idle', ctx } as Agent,
- dispose: () => Promise.resolve(),
- }
- }
- describe('Session fork failures', () => {
- it('maps missing cold sources with and without persistence', async () => {
- const withoutPersistence = await baseContext()
- withoutPersistence.provide('workspaceRegistry', { list: () => [] } as never)
- const unavailableController = new SessionCommandController(
- withoutPersistence, controllerAgents(), '/default',
- )
- await expectFailure(unavailableController.fork({
- sessionId: SessionId('missing'),
- }), 'session-not-found')
- await withoutPersistence.fiber.dispose()
- const missing = await baseContext()
- missing.provide('workspaceRegistry', { list: () => [] } as never)
- missing.provide('sessionPersistence', testSessionPersistence(missing, {
- list: () => Promise.resolve([]),
- inspect: vi.fn(),
- }) as never)
- const missingController = new SessionCommandController(missing, controllerAgents(), '/default')
- await expectFailure(missingController.fork({
- sessionId: SessionId('missing'),
- }), 'session-not-found')
- await missing.fiber.dispose()
- })
- it('maps an observation failure to an internal fork error', async () => {
- 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')
- await expectFailure(controller.fork({ sessionId: SessionId('unreadable') }), 'internal')
- await ctx.fiber.dispose()
- })
- it('rejects a Session with no completed turn', async () => {
- 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')
- await expectFailure(controller.fork({ sessionId: source.id }), 'fork-unavailable')
- await ctx.fiber.dispose()
- })
- it('maps lineage lookup and Agent creation failures', async () => {
- const lineage = await baseContext()
- lineage.provide('workspaceRegistry', { list: () => [] } as never)
- vi.spyOn(lineage.sessionQuery, 'traceSession')
- .mockRejectedValue(new Error('lineage unavailable'))
- const child = completedSession(lineage, 'subagent-source', '/workspace', {
- parentSession: SessionId('parent'),
- origin: 'subagent',
- })
- const lineageController = new SessionCommandController(lineage, controllerAgents(), '/default')
- await expectFailure(lineageController.fork({ sessionId: child.id }), 'internal')
- await lineage.fiber.dispose()
- const creation = await baseContext()
- 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')
- await expectFailure(creationController.fork({ sessionId: source.id }), 'internal')
- await creation.fiber.dispose()
- })
- it('omits absent cwd and preset metadata before reporting Workspace attachment failure', async () => {
- const ctx = await baseContext()
- const source = completedSession(ctx, 'workspace-source')
- const workspace = {
- id: 'workspace-1' as WorkspaceId,
- sessionIds: [source.id],
- attachSession: () => Promise.reject(new Error('workspace write failed')),
- } as unknown as Workspace
- ctx.provide('workspaceRegistry', { list: () => [workspace] } as never)
- const create = vi.spyOn(ctx.agents, 'create').mockImplementation(
- (options: CreateAgentOptions) => Promise.resolve(resolvedHandle(ctx, options.sessionId)),
- )
- const controller = new SessionCommandController(ctx, controllerAgents(), '/default')
- await expectFailure(controller.fork({ sessionId: source.id }), 'workspace-attach-failed')
- const options = create.mock.calls[0]?.[0]
- if (options === undefined) throw new Error('Agent creation was not attempted')
- expect(options.meta).not.toHaveProperty('cwd')
- expect(options.meta).not.toHaveProperty('agentPreset')
- await ctx.fiber.dispose()
- })
- it('carries the composed Agent preset into the child metadata', async () => {
- const ctx = await baseContext()
- ctx.provide('workspaceRegistry', { list: () => [] } as never)
- const source = completedSession(ctx, 'preset-source', '/workspace')
- const create = vi.spyOn(ctx.agents, 'create').mockImplementation(
- (options: CreateAgentOptions) => Promise.resolve(resolvedHandle(ctx, options.sessionId)),
- )
- const controller = new SessionCommandController(ctx, controllerAgents({
- composeAgent: () => Promise.resolve({ agentPreset: 'minimal', setup: () => {} }),
- }), '/default')
- const forked = await controller.fork({ sessionId: source.id })
- expect(forked.sessionId).toMatch(/^session-/)
- const options = create.mock.calls[0]?.[0]
- if (options === undefined) throw new Error('Agent creation was not attempted')
- expect(options.meta?.agentPreset).toBe('minimal')
- await ctx.fiber.dispose()
- })
- })
|