import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection' import { mkdtempSync, rmSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' import { Context } from '@deepseek-ai/cordis' 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, SessionId } 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' import { afterEach, describe, expect, it, vi } from 'vitest' import { ApiSessionAgentController, ApiSessionCwdConflict, } from '../src/agent.ts' import { installSessionReadTestServices, testSessionPersistence } from './test-remote.ts' const roots: Context[] = [] /** Session cwd roots created per test, removed after their context settles. */ const tempDirs: string[] = [] afterEach(async () => { await Promise.all(roots.splice(0).map(ctx => ctx.fiber.dispose())) for (const dir of tempDirs.splice(0)) rmSync(dir, { recursive: true, force: true }) }) async function harness(): Promise<{ ctx: Context; agents: ApiSessionAgentController }> { const ctx = new Context() roots.push(ctx) await ctx.plugin(TypertRegistry) await ctx.plugin(SessionStore) await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) installSessionReadTestServices(ctx) await ctx.sessionProjections.register(agentPresetProjectionDefinition) ctx.provide('agentDefaultModel', { currentSelection: () => ({ provider: 'fixture', model: 'fixture-model' }), saveSelection: () => Promise.resolve(), } as never) const agents = new ApiSessionAgentController(ctx) agents.initialize() return { ctx, agents } } function header(id: string, cwd: string | null = '/workspace'): SessionHeader { return { version: SESSION_FORMAT_VERSION, id: SessionId(id), createdAt: 1, isSeeded: false, ...(cwd === null ? {} : { cwd }), } } function providePersistence(ctx: Context, persistence: Record): () => void { return ctx.provide('sessionPersistence', testSessionPersistence(ctx, persistence) as never) } function agent(ctx: Context, meta: SessionHeader): Agent { const session = ctx.sessions.create(meta.id, { meta }) return { id: meta.id, session, status: 'idle', ctx } as Agent } function unpublishedAgent(ctx: Context, meta: SessionHeader): Agent { return { id: meta.id, session: { id: meta.id, header: meta, events: [] }, status: 'idle', ctx, } as unknown as Agent } describe('ApiSession identity failures', () => { it('describes cwd conflicts with and without a recorded cwd', () => { expect(new ApiSessionCwdConflict(SessionId('missing-cwd'), '/wanted', undefined).message) .toContain('records no cwd') expect(new ApiSessionCwdConflict(SessionId('wrong-cwd'), '/wanted', '/existing').message) .toContain('belongs to "/existing"') }) }) describe('ApiSession Agent lookup and recovery', () => { it('resumes directly from a retained observation and rejects an invalid observed header', async () => { const { ctx, agents } = await harness() const meta = header('observed-resume') const resumed = unpublishedAgent(ctx, meta) const resume = vi.spyOn(ctx.agents, 'resume').mockResolvedValue({ agent: resumed, dispose: () => Promise.resolve(), }) const observed = { source: 'prepared', header: meta, events: [], cursor: -1, projections: { asOfSeq: -1, values: {} }, retain: vi.fn(), [Symbol.dispose]: vi.fn(), } as unknown as SessionObservation await expect(agents.resolveObservedAgent(observed)).resolves.toEqual({ agent: resumed }) expect(resume).toHaveBeenCalledWith(expect.objectContaining({ resumeSessionId: meta.id })) const invalid = { ...observed, header: header('observed-without-cwd', null), } as SessionObservation await expect(agents.resolveObservedAgent(invalid)).resolves.toMatchObject({ error: { code: 'session/not-found' }, }) }) it('projects live Agent contexts and maps missing cold identities through Typert lookup failures', async () => { const { ctx } = await harness() const live = agent(ctx, header('live')) await ctx.agents.register(live) providePersistence(ctx, { list: () => Promise.resolve([]), inspect: vi.fn(), }) const host = ctx.typert.contexts.getHost('agent') if (host === undefined) throw new Error('Agent Context resolver was not registered') await expect(host.resolve(live.id)).resolves.toBe(live.ctx) await expect(host.resolve(SessionId('missing'))).rejects.toMatchObject({ code: 'session/not-found' }) }) it('returns raced ordinary Agents and ownership failures after resume throws', async () => { const ordinary = await harness() const ordinaryMeta = header('ordinary-race') providePersistence(ordinary.ctx, { list: () => Promise.resolve([ordinaryMeta]), inspect: () => Promise.resolve({ meta: ordinaryMeta, events: [] }), }) const winner = agent(ordinary.ctx, ordinaryMeta) vi.spyOn(ordinary.ctx.agents, 'resume').mockImplementation(async () => { await ordinary.ctx.agents.register(winner) throw new Error('raced publication') }) await expect(ordinary.agents.resolveAgent(ordinaryMeta.id)).resolves.toEqual({ agent: winner }) const child = await harness() const childMeta = header('child-race') providePersistence(child.ctx, { list: () => Promise.resolve([childMeta]), inspect: () => Promise.resolve({ meta: childMeta, events: [] }), }) vi.spyOn(child.ctx.agents, 'resume').mockImplementation(async () => { child.ctx.sessions.create(childMeta.id, { meta: { ...childMeta, parentSession: SessionId('parent'), origin: 'subagent' }, }) throw new Error('raced child publication') }) await expect(child.agents.resolveAgent(childMeta.id)).resolves.toMatchObject({ error: { code: 'session/agent-busy' }, }) }) it('reports not-found and ordinary resume failures without fabricating an Agent', async () => { const missing = await harness() providePersistence(missing.ctx, { list: () => Promise.resolve([]), inspect: vi.fn(), }) await expect(missing.agents.resolveAgent(SessionId('missing'))).resolves.toMatchObject({ error: { code: 'session/not-found' }, }) const failed = await harness() const meta = header('failed') providePersistence(failed.ctx, { list: () => Promise.resolve([meta]), inspect: () => Promise.resolve({ meta, events: [] }), }) vi.spyOn(failed.ctx.agents, 'resume').mockRejectedValue(new Error('factory unavailable')) await expect(failed.agents.resolveAgent(meta.id)).resolves.toMatchObject({ error: { code: 'gateway/internal', message: expect.stringContaining('factory unavailable') as string }, }) }) it('identifies a held Session writer without classifying other resume failures as contention', async () => { const { ctx, agents } = await harness() const meta = header('owned-session') providePersistence(ctx, { list: () => Promise.resolve([meta]), inspect: () => Promise.resolve({ meta, events: [] }), }) const resume = vi.spyOn(ctx.agents, 'resume').mockRejectedValue(new SessionAlreadyOwnedError(meta.id)) await expect(agents.resolveAgent(meta.id)).resolves.toMatchObject({ error: { code: 'session/writer-held', details: { sessionId: meta.id } }, }) resume.mockRejectedValue(Object.assign(new Error('another module copy'), { name: 'SessionAlreadyOwnedError' })) await expect(agents.resolveAgent(meta.id)).resolves.toMatchObject({ error: { code: 'session/writer-held', details: { sessionId: meta.id } }, }) resume.mockRejectedValue(new Error('unrelated failure')) await expect(agents.resolveAgent(meta.id)).resolves.toMatchObject({ error: { code: 'gateway/internal' }, }) }) it('retains resume diagnostics without a persistence service', async () => { const { ctx, agents } = await harness() const meta = header('memory-only-resume') ctx.sessions.create(meta.id, { meta }) vi.spyOn(ctx.agents, 'resume').mockRejectedValue(new Error('factory unavailable')) await expect(agents.resolveAgent(meta.id)).resolves.toMatchObject({ error: { code: 'gateway/internal', message: expect.stringContaining('factory unavailable') as string }, }) }) it('requires projected observations before activation', async () => { const { agents } = await harness() const meta = header('unprojected-observation') const observed = { source: 'prepared', header: meta, events: [], cursor: -1, retain: vi.fn(), [Symbol.dispose]: vi.fn(), } as unknown as SessionObservation expect(() => agents.presetForObservation(observed)).toThrow( 'Agent activation requires a projected Session observation', ) }) }) 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, { list: () => Promise.resolve([childMeta]), inspect: () => Promise.resolve({ meta: childMeta, events: [] }), }) child.ctx.provide('agentPresets', { resolve: () => { child.ctx.sessions.create(childMeta.id, { meta: { ...childMeta, parentSession: SessionId('parent'), origin: 'subagent' }, }) return Promise.resolve({ id: 'standard' }) }, mount: () => Promise.resolve(), } as never) await expect(child.agents.resolveAgent(childMeta.id)).resolves.toMatchObject({ error: { code: 'session/agent-busy' }, }) }) it('surfaces directory creation failure', async () => { const { agents } = await harness() const parent = mkdtempSync(join(tmpdir(), 'dsh-session-controller-file-')) tempDirs.push(parent) const file = join(parent, 'file') writeFileSync(file, 'not a directory') await expect(agents.createSession({ cwd: join(file, 'child'), reusable: () => true })) .rejects.toThrow('failed to ensure project directory') }) })