import { mkdtempSync, 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 SessionStore, { SessionId } from '@deepseek-ai/dsh-session' import type { SessionEvent, SessionHeader } from '@deepseek-ai/dsh-session' import { TypertLookupFailure } from '@deepseek-ai/dsh-typert-protocol' import TypertRegistry from '@deepseek-ai/dsh-typert-registry' import { afterEach, describe, expect, it, vi } from 'vitest' import { ApiSessionAgentController, ApiSessionCwdConflict, ApiSessionNotFound, ApiSessionSubagentOwnership, inspectApiSession, } from '../src/agent.ts' const roots: Context[] = [] afterEach(async () => { await Promise.all(roots.splice(0).map(ctx => ctx.fiber.dispose())) }) 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(AgentRegistry) ctx.provide('agentDefaultModel', { currentSelection: () => ({ provider: 'fixture', model: 'fixture-model' }), saveSelection: () => Promise.resolve(), } as never) return { ctx, agents: new ApiSessionAgentController(ctx) } } function header(id: string, cwd: string | null = '/workspace'): SessionHeader { return { version: 0, id: SessionId(id), createdAt: 1, ...(cwd === null ? {} : { cwd }), } } 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"') }) it('rejects absent persistence, catalog misses, and cwd-less inspected artifacts', async () => { const ctx = new Context() roots.push(ctx) await expect(inspectApiSession(ctx, SessionId('missing'))) .rejects.toThrow('session persistence is not configured') const inspect = vi.fn(() => Promise.resolve({ meta: header('missing'), events: [] as SessionEvent[] })) const disposeMissing = ctx.provide('sessionPersistence', { list: () => Promise.resolve([]), inspect, } as never) await expect(inspectApiSession(ctx, SessionId('missing'))).rejects.toBeInstanceOf(ApiSessionNotFound) expect(inspect).not.toHaveBeenCalled() disposeMissing() const listed = header('cwd-less-catalog', null) const disposeListed = ctx.provide('sessionPersistence', { list: () => Promise.resolve([listed]), inspect, } as never) await expect(inspectApiSession(ctx, listed.id)).rejects.toBeInstanceOf(ApiSessionNotFound) disposeListed() const catalog = header('cwd-less-inspect') const inspected = header('cwd-less-inspect', null) ctx.provide('sessionPersistence', { list: () => Promise.resolve([catalog]), inspect: () => Promise.resolve({ meta: inspected, events: [] }), } as never) await expect(inspectApiSession(ctx, catalog.id)).rejects.toBeInstanceOf(ApiSessionNotFound) }) }) describe('ApiSession Agent lookup and recovery', () => { 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')) ctx.agents.register(live) ctx.provide('sessionPersistence', { list: () => Promise.resolve([]), inspect: vi.fn(), } as never) 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.toBeInstanceOf(TypertLookupFailure) }) it('returns raced ordinary Agents and ownership failures after resume throws', async () => { const ordinary = await harness() const ordinaryMeta = header('ordinary-race') ordinary.ctx.provide('sessionPersistence', { list: () => Promise.resolve([ordinaryMeta]), inspect: () => Promise.resolve({ meta: ordinaryMeta, events: [] }), } as never) const winner = agent(ordinary.ctx, ordinaryMeta) vi.spyOn(ordinary.ctx.agents, 'resume').mockImplementation(async () => { 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') child.ctx.provide('sessionPersistence', { list: () => Promise.resolve([childMeta]), inspect: () => Promise.resolve({ meta: childMeta, events: [] }), } as never) 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: 'agent-busy' }, }) }) it('reports not-found and ordinary resume failures without fabricating an Agent', async () => { const missing = await harness() missing.ctx.provide('sessionPersistence', { list: () => Promise.resolve([]), inspect: vi.fn(), } as never) await expect(missing.agents.resolveAgent(SessionId('missing'))).resolves.toMatchObject({ error: { code: 'session-not-found' }, }) const failed = await harness() const meta = header('failed') failed.ctx.provide('sessionPersistence', { list: () => Promise.resolve([meta]), inspect: () => Promise.resolve({ meta, events: [] }), } as never) vi.spyOn(failed.ctx.agents, 'resume').mockRejectedValue(new Error('factory unavailable')) await expect(failed.agents.resolveAgent(meta.id)).resolves.toMatchObject({ error: { code: 'internal', message: expect.stringContaining('factory unavailable') as string }, }) }) }) 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-')) const meta = header('concurrent-create', cwd) const created = unpublishedAgent(ctx, meta) let release!: () => void const gate = new Promise((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-')) const ordinaryMeta = header('create-race', cwd) const winner = agent(ordinary.ctx, ordinaryMeta) vi.spyOn(ordinary.ctx.agents, 'create').mockImplementation(async () => { 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-')) 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-')) 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-')) 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[] ctx.provide('sessionPersistence', { list: () => Promise.resolve([meta]), inspect: () => Promise.resolve({ meta, events }), } as never) 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, events }, 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 () => { const child = await harness() const childMeta = header('resume-child-race') child.ctx.provide('sessionPersistence', { list: () => Promise.resolve([childMeta]), inspect: () => Promise.resolve({ meta: childMeta, events: [] }), } as never) 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: 'agent-busy' }, }) const conflict = await harness() const stored = header('stored-cwd-conflict', '/stored') conflict.ctx.provide('sessionPersistence', { list: () => Promise.resolve([stored]), inspect: () => Promise.resolve({ meta: stored, events: [] }), } as never) await expect(conflict.agents.ensureSession(stored.id, '/requested', true)) .rejects.toBeInstanceOf(ApiSessionCwdConflict) }) it('surfaces directory creation failure and rejects setup without a scoped Agent', async () => { const { agents } = await harness() const parent = mkdtempSync(join(tmpdir(), 'dsh-session-controller-file-')) const file = join(parent, 'file') writeFileSync(file, 'not a directory') await expect(agents.ensureSession(SessionId('mkdir-failure'), join(file, 'child'), false)) .rejects.toThrow('failed to ensure project directory') const composition = await agents.composeAgent(undefined) expect(() => composition.setup(new Context())).toThrow('Agent setup has no scoped Agent') }) })