Files

281 lines
11 KiB
TypeScript

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()
})
})