import { describe, expect, it, vi } from 'vitest' import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises' import { join } from 'node:path' import { tmpdir } from 'node:os' import { Context } from 'cordis' import Loader from '@cordisjs/plugin-loader' import { renderPrompt, TOOL_ORDER_REST } from '@deepseek-ai/dsh-system-prompt' import * as agentCore from '../src/index.ts' import { agentEvents, type Agent } from '@deepseek-ai/dsh-agent' import { SessionId } from '@deepseek-ai/dsh-session' import LocalBashExecutor from '@deepseek-ai/dsh-bash-local' import LocalFileSystem from '@deepseek-ai/dsh-fs-local' import * as ToolFs from '@deepseek-ai/dsh-tool-fs' import { MockAdapter, textResponse, toolCallResponse } from '../../../core/agent-loop/tests/mock-adapter.ts' import { createUserMessage, CallId, LlmAdapter, LlmError, resolveRetryPolicy, type GenerateOptions, type Message, type ResolvedRetryPolicy, type StreamChunk, } from '@deepseek-ai/dsh-llm' import type { ToolExecution } from '@deepseek-ai/dsh-tools' import * as sessionInvariant from '@deepseek-ai/dsh-session/invariant' import * as agentInvariant from '@deepseek-ai/dsh-agent/invariant' import * as scopeInvariant from '@deepseek-ai/dsh-scope/invariant' import * as agentLoopInvariant from '@deepseek-ai/dsh-agent-loop/invariant' const testToolSignal = new AbortController().signal declare module '@deepseek-ai/dsh-tasks' { interface TaskKindMap { probe: 'probe' } } async function composePrefix(ctx: Context, cwd: string): Promise { const agent = ctx.agentLoop.create(SessionId('agent-spine-prefix'), {}, { cwd }) const signal = new AbortController().signal const decision = await agentEvents(ctx, agent).waterfall( 'agent/pre-step', { messages: [], turn: 1, step: 1, signal }, () => Promise.resolve({ kind: 'enter', messages: [] }), ) if (decision.kind === 'enter') { for (const message of decision.messages) { agent.session.append('user/message', message, { surfaceOp: 'append' }) } } return agent.session.deriveMessages() } /** * Unit coverage for the @deepseek-ai/dsh-agent-spine-demo bundle: mounting it brings * up the whole default spine in one `ctx.plugin`, and the forwarded * `agents` config reaches the loop (default `[]`, or a pre-created agent). * * The bundle is exercised through `ctx.plugin(agentCore, …)` — the NAMESPACE * import, the same shape the Loader builds from `unwrapExports`. The real * Loader-path guard (export shape, `unwrapExports`) is the app packages' keyless * bin smokes; here we assert the composition + config forwarding. */ async function mount(config: agentCore.Config, withBash = false): Promise { const oldDshHome = process.env.DSH_HOME const oldAgentsHome = process.env.DSH_AGENTS_HOME process.env.DSH_HOME = await mkdtemp(join(tmpdir(), 'dsh-agent-spine-demo-home-')) process.env.DSH_AGENTS_HOME = await mkdtemp(join(tmpdir(), 'dsh-agent-spine-demo-agents-')) const ctx = new Context() if (withBash) { ctx.provide('bash', { sandboxMode: undefined, resolve() { throw new Error('composition test does not execute bash') }, run() { throw new Error('composition test does not execute bash') }, start() { throw new Error('composition test does not execute bash') }, }) } try { await ctx.plugin(agentCore, config) // The bundle mounts its children inside apply() (not awaited there); let their // fibers settle so the spine services and any pre-created agent are ready. await new Promise(resolve => setTimeout(resolve, 50)) return ctx } finally { if (oldDshHome === undefined) { delete process.env.DSH_HOME } else { process.env.DSH_HOME = oldDshHome } if (oldAgentsHome === undefined) { delete process.env.DSH_AGENTS_HOME } else { process.env.DSH_AGENTS_HOME = oldAgentsHome } } } async function withIsolatedSkillHomes(run: () => Promise): Promise { const oldDshHome = process.env.DSH_HOME const oldAgentsHome = process.env.DSH_AGENTS_HOME process.env.DSH_HOME = await mkdtemp(join(tmpdir(), 'dsh-agent-spine-demo-home-')) process.env.DSH_AGENTS_HOME = await mkdtemp(join(tmpdir(), 'dsh-agent-spine-demo-agents-')) try { return await run() } finally { if (oldDshHome === undefined) { delete process.env.DSH_HOME } else { process.env.DSH_HOME = oldDshHome } if (oldAgentsHome === undefined) { delete process.env.DSH_AGENTS_HOME } else { process.env.DSH_AGENTS_HOME = oldAgentsHome } } } function waitForIdle(_ctx: Context, target: Agent): Promise { return target.whenIdle() } function messageText(message: Message | undefined): string { return message?.content.map(block => block.type === 'text' ? block.text : '').join('\n') ?? '' } class TransientOnceAdapter extends LlmAdapter { requests = 0 private readonly retryPolicy = resolveRetryPolicy({ mode: 'normal', maxRetries: 1, backoff: { initialDelayMs: 1, maxDelayMs: 1, jitterRatio: 0 }, }, 'agent-spine test provider retryPolicy') override providerRetryPolicy(_provider: string): ResolvedRetryPolicy { return this.retryPolicy } async * stream(_options: GenerateOptions): AsyncIterable { this.requests += 1 if (this.requests === 1) throw new LlmError('temporary outage', 'SERVER') yield* textResponse('recovered by bundled policy') } } describe('dsh-agent-spine-demo bundle', () => { it('brings up the full default spine', async () => { const ctx = await mount({ workspaceContext: false }) // One service from each layer of the spine proves the children loaded. expect(ctx.get('timer')).toBeDefined() expect(ctx.get('llm')).toBeDefined() expect(ctx.get('sessions')).toBeDefined() expect(ctx.get('sessionTitle')).toBeDefined() expect(ctx.get('systemPrompt')).toBeDefined() expect(ctx.get('tools')).toBeDefined() expect(ctx.get('skills')).toBeDefined() expect(ctx.get('agents')).toBeDefined() expect(ctx.get('tasks')).toBeDefined() expect(ctx.get('invariants')).toBeDefined() expect(ctx.get('agentLoop')).toBeDefined() expect(ctx.get('goals')).toBeUndefined() await ctx.fiber.dispose() }) it('forwards configurable fallback title limits to the bundled service', async () => { const ctx = await mount({ workspaceContext: false, sessionTitle: { fallbackMaxWords: 1, fallbackMaxBytes: 40, maxTitleBytes: 80, }, }) const session = ctx.sessions.create(SessionId('configured-title-limits')) session.append('turn/start', { turn: 1, }) session.append('user/message', createUserMessage({ content: [{ type: 'text', text: 'One two three four' }], source: { kind: 'user' }, }), { surfaceOp: 'append' }) await new Promise(resolve => setTimeout(resolve, 0)) expect(ctx.sessionTitle.get(session)?.title).toBe('One') await ctx.fiber.dispose() }) it('opts into the configured persisted-goal domain, tools, and same-session driver', async () => { const ctx = await mount({ workspaceContext: false, agents: [{ id: SessionId('configured-goal'), provider: 'mock', model: 'mock' }], goals: { domain: { defaultMaxGoalRounds: 17 }, tool: { blockedAfterConsecutiveRounds: 5 }, }, }) const agent = ctx.agents.list()[0] if (agent === undefined) throw new Error('configured goal test has no live agent') expect(ctx.goals.create(agent, { objective: 'configured' })).toMatchObject({ objective: 'configured', maxGoalRounds: 17, }) expect(['create_goal', 'get_goal', 'update_goal'].map(name => ctx.tools.get(name)?.name)) .toEqual(['create_goal', 'get_goal', 'update_goal']) expect((await ctx.systemPrompt.assemble()).sections.find(section => section.name === 'tool:goal')?.text) .toContain('at least 5 consecutive goal rounds') await ctx.fiber.dispose() }) it('accepts an explicit false goal composition without mounting it', async () => { const ctx = await mount({ workspaceContext: false, goals: false }) expect(ctx.get('goals')).toBeUndefined() expect(ctx.tools.get('get_goal')).toBeUndefined() await ctx.fiber.dispose() }) it('mounts package companions and forwards invariant selection config', async () => { const nestedTurn = (ctx: Context): void => { const session = ctx.sessions.create() session.append('turn/start', { turn: 1 }) session.append('turn/start', { turn: 2 }) } const enabled = await mount({ workspaceContext: false }) expect(() => { nestedTurn(enabled) }).toThrow(/turn 1 is still open/) await enabled.fiber.dispose() for (const invariants of [ { enabled: false }, { package_allowlist: ['^@deepseek-ai/dsh-agent$'] }, { package_blocklist: ['^@deepseek-ai/dsh-session$'] }, ]) { const filtered = await mount({ workspaceContext: false, invariants }) expect(() => { nestedTurn(filtered) }).not.toThrow() await filtered.fiber.dispose() } }) it('loads and configures bounded request recovery for every bundled front door', async () => { const adapter = new TransientOnceAdapter() const ctx = await mount({ workspaceContext: false }) ctx.llm.registerAdapter(['mock'], adapter) const handle = await ctx.agents.create({ sessionId: SessionId('bundled-retry-session'), meta: { cwd: process.cwd() }, agentOptions: { provider: 'mock', model: 'mock' }, }) handle.agent.followup(createUserMessage({ content: [{ type: 'text', text: 'recover' }], source: { kind: 'user' } })) await waitForIdle(ctx, handle.agent) expect(adapter.requests).toBe(2) const retryEvents = handle.agent.session.events.filter(event => event.type === 'llm/retry') expect(retryEvents).toHaveLength(1) expect(retryEvents[0]?.data.retry).toBe(1) expect(retryEvents[0]?.data).toMatchObject({ provider: 'mock', mode: 'normal', maxRetries: 1 }) expect(handle.agent.session.events.find(event => event.type === 'session/title')?.data.title).toBe('recover') expect(messageText(handle.agent.session.deriveMessages().at(-1))).toBe('recovered by bundled policy') await handle.dispose() await ctx.fiber.dispose() }) it('includes the skill registry, local provider, and skill tool without builtin skills', async () => { const ctx = await mount({ workspaceContext: false }) expect(ctx.skills).toBeDefined() expect(ctx.tools.schemas().map(tool => tool.name)).toContain('skill') expect(await ctx.skills.list()).toEqual([]) await ctx.fiber.dispose() }) it('defaults the agents list to empty (no pre-created agents)', async () => { const ctx = await mount({ workspaceContext: false }) expect(ctx.get('agents')?.get(SessionId('main'))).toBeUndefined() await ctx.fiber.dispose() }) it('forwards a pre-created agent to the loop and the persona to system-prompt', async () => { const ctx = await mount({ agents: [{ id: SessionId('main'), provider: 'mock', model: 'mock' }], persona: 'You are main.', workspaceContext: false, }) const agent = ctx.get('agents')?.list()[0] expect(agent?.id).toBe(agent?.session.id) expect(agent?.id).toMatch(/^main-session-/) const assembly = await ctx.get('systemPrompt')!.assemble() expect(assembly.sections.find(s => s.name === 'deployment:persona')?.text).toBe('You are main.') await ctx.fiber.dispose() }) it('forwards the global maxParallelToolCalls config to agent-loop', async () => { const ctx = await mount({ agents: [{ id: SessionId('main'), provider: 'mock', model: 'mock' }], maxParallelToolCalls: 3, workspaceContext: false, }) expect(ctx.get('agentLoop')?.config.maxParallelToolCalls).toBe(3) await ctx.fiber.dispose() }) it('tolerates a schema-bypassing direct apply (the ?? fallbacks fire)', async () => { // ctx.plugin validates + defaults the bundle config first; a direct apply // skips the schema, so the forwarding `?? []` / `?? ''` are what fire. const ctx = new Context() agentCore.apply(ctx, { workspaceContext: false }) await new Promise(resolve => setTimeout(resolve, 50)) expect(ctx.get('agentLoop')).toBeDefined() expect(ctx.get('agents')?.list()).toHaveLength(0) const assembly = await ctx.get('systemPrompt')!.assemble() expect(assembly.sections.find(s => s.name === 'deployment:persona')?.text).toBe('') await ctx.fiber.dispose() }) it('uses owner defaults for a schema-bypassing empty goal opt-in', async () => { const ctx = new Context() agentCore.apply(ctx, { workspaceContext: false, agents: [{ id: SessionId('defaulted-goal'), provider: 'mock', model: 'mock' }], goals: {}, }) await new Promise(resolve => setTimeout(resolve, 50)) const agent = ctx.agents.list()[0] if (agent === undefined) throw new Error('default goal test has no live agent') expect(ctx.goals.create(agent, { objective: 'defaulted' })).toMatchObject({ objective: 'defaulted', maxGoalRounds: 256, }) expect(ctx.tools.get('get_goal')).toBeDefined() await ctx.fiber.dispose() }) it('loads workspace instructions into requests through the bundled spine', async () => { const root = await mkdtemp(join(tmpdir(), 'dsh-agent-spine-demo-workspace-context-')) try { await mkdir(join(root, '.git'), { recursive: true }) await writeFile(join(root, 'AGENTS.md'), 'bundled project rule') const adapter = new MockAdapter([textResponse('first')]) const ctx = await mount({ workspaceContext: { maxBytes: 65536 } }) await ctx.plugin(LocalFileSystem, { cwd: '/' }) ctx.llm.registerAdapter(['mock'], adapter) const handle = await ctx.agents.create({ sessionId: SessionId('main-session'), meta: { cwd: root }, agentOptions: { provider: 'mock', model: 'mock' }, }) const agent = handle.agent agent.followup(createUserMessage({ content: [{ type: 'text', text: 'hi' }], source: { kind: 'user' } })) await waitForIdle(ctx, agent) expect(adapter.requests).toHaveLength(1) const firstRequestText = adapter.requests[0]?.messages.map(messageText).join('\n') expect(firstRequestText).toContain('hi') expect(firstRequestText).toContain('bundled project rule') expect(adapter.requests[0]?.system).toContain('You are an AI agent powered by the DeepSeek Harness SDK.') expect(adapter.requests[0]?.system).not.toContain('bundled project rule') await handle.dispose() await ctx.fiber.dispose() } finally { await rm(root, { recursive: true, force: true }) } }) it('forwards workspace-context config to the bundled loader', async () => { const root = await mkdtemp(join(tmpdir(), 'dsh-agent-spine-demo-workspace-context-disabled-')) try { await mkdir(join(root, '.git'), { recursive: true }) await writeFile(join(root, 'AGENTS.md'), 'must not be injected') const adapter = new MockAdapter([textResponse('ok')]) const ctx = await mount({ workspaceContext: { maxBytes: 0 } }) ctx.llm.registerAdapter(['mock'], adapter) const handle = await ctx.agents.create({ sessionId: SessionId('main-disabled-session'), meta: { cwd: root }, agentOptions: { provider: 'mock', model: 'mock' }, }) handle.agent.followup(createUserMessage({ content: [{ type: 'text', text: 'hi' }], source: { kind: 'user' } })) await waitForIdle(ctx, handle.agent) expect(adapter.requests[0]?.messages).toEqual([{ id: expect.any(String) as unknown, role: 'user', content: [{ type: 'text', text: 'hi' }], source: { kind: 'user' }, }]) await handle.dispose() await ctx.fiber.dispose() } finally { await rm(root, { recursive: true, force: true }) } }) it('forwards skill config to the registry, local provider, and model-facing consumer', async () => { const home = await mkdtemp(join(tmpdir(), 'dsh-agent-spine-demo-skill-home-')) const agentsHome = await mkdtemp(join(tmpdir(), 'dsh-agent-spine-demo-skill-agents-')) const custom = await mkdtemp(join(tmpdir(), 'dsh-agent-spine-demo-skill-custom-')) await mkdir(custom, { recursive: true }) await writeFile(join(custom, 'custom-skill.md'), '---\nname: custom-skill\ndescription: Custom skill\n---\n\nCustom body.\n') const ctx = await mount({ agents: [], workspaceContext: false, skills: { registry: { collectCacheMaxEntries: 4 }, local: { dshHome: join(home, '.dsh'), agentsHome: join(agentsHome, '.agents'), customSkillDirs: [custom], }, tool: { catalogDescriptionMaxLength: 6 }, }, }) expect((await ctx.skills.list()).map(skill => skill.name)).toEqual(['custom-skill']) expect(JSON.stringify(await composePrefix(ctx, '/tmp'))).toContain('- `custom-skill`: Cus...') await ctx.fiber.dispose() }) it('snapshots a created project skill through catalog refresh and progressive loading', async () => { const root = await mkdtemp(join(tmpdir(), 'dsh-agent-spine-demo-skill-refresh-')) const home = await mkdtemp(join(tmpdir(), 'dsh-agent-spine-demo-skill-refresh-home-')) try { await mkdir(join(root, '.git'), { recursive: true }) const skillPath = '.agents/skills/hot-skill/SKILL.md' const skillSource = '---\nname: hot-skill\ndescription: Hot-added skill\n---\n\nUse the freshly loaded body.\n' const adapter = new MockAdapter([ toolCallResponse('mkdir-skill', 'bash', { command: 'mkdir -p .agents/skills/hot-skill', description: 'Create the project skill directory', }), toolCallResponse('write-skill', 'write', { file_path: skillPath, content: skillSource, }), toolCallResponse('load-skill', 'skill', { name: 'hot-skill' }), textResponse('SKILL_REFRESH_OK'), ]) const ctx = await mount({ workspaceContext: false, skills: { local: { dshHome: join(home, '.dsh'), agentsHome: join(home, '.agents'), watchStabilityThresholdMs: 20, watchPollIntervalMs: 10, }, }, }) await ctx.plugin(LocalBashExecutor, {}) await ctx.plugin(LocalFileSystem, { cwd: root }) await ctx.plugin(ToolFs) ctx.llm.registerAdapter(['mock'], adapter) const handle = await ctx.agents.create({ sessionId: SessionId('skill-refresh-session'), meta: { cwd: root }, agentOptions: { provider: 'mock', model: 'mock' }, }) handle.agent.followup(createUserMessage({ content: [{ type: 'text', text: 'Create and load the project skill.' }], source: { kind: 'user' }, })) await waitForIdle(ctx, handle.agent) expect(adapter.requests).toHaveLength(4) expect(adapter.requests.slice(0, 2).map(request => request.messages.map(messageText).join('\n'))) .toEqual([ expect.not.stringContaining('hot-skill'), expect.not.stringContaining('hot-skill'), ]) const catalogRequest = adapter.requests[2]?.messages.map(messageText).join('\n') expect(catalogRequest).toContain('The following skills are available in this session:') expect(catalogRequest).toContain('- `hot-skill`: Hot-added skill') const loadedRequest = JSON.stringify(adapter.requests[3]?.messages) expect(loadedRequest).toContain('') expect(loadedRequest).toContain('Use the freshly loaded body.') const transcript = handle.agent.session.events.flatMap>((event) => { if (event.type === 'user/message' && event.data.source.kind === 'skill-catalog') { return [{ type: event.type, source: event.data.source, text: event.data.content.map(block => block.type === 'text' ? block.text : '').join('\n'), }] } if (event.type === 'tool/result' && ['write-skill', 'load-skill'].includes(event.data.message.source.callId)) { const result = event.data.message.content[0] return [{ type: event.type, callId: event.data.message.source.callId, isError: result.isError, text: result.content.map(block => block.type === 'text' ? block.text : '').join('\n') .replaceAll(root, '{{cwd}}'), }] } return [] }) expect(transcript).toMatchInlineSnapshot(` [ { "callId": "write-skill", "isError": false, "text": "{{cwd}}/.agents/skills/hot-skill/SKILL.md file Created file ", "type": "tool/result", }, { "source": { "entries": [ { "description": "Hot-added skill", "name": "hot-skill", }, ], "form": "catalog", "kind": "skill-catalog", }, "text": " A skill is a reusable set of task-specific instructions. The following skills are available in this session: - \`hot-skill\`: Hot-added skill If the user names a skill, or the task clearly matches a skill's description, call the \`skill\` tool with the exact skill name before taking task actions. Load all applicable skills, then follow their full instructions. This catalog contains summaries only; do not infer or follow a skill's instructions until it has been loaded. ", "type": "user/message", }, { "callId": "load-skill", "isError": false, "text": " Base directory for this skill: {{cwd}}/.agents/skills/hot-skill Resolve relative paths mentioned by this skill against the base directory before using them. Load referenced resources only as needed. Use the freshly loaded body. ", "type": "tool/result", }, ] `) await handle.dispose() await ctx.fiber.dispose() } finally { await rm(root, { recursive: true, force: true }) await rm(home, { recursive: true, force: true }) } }) it('shares top-level dshHome between local skills and the managed bash environment', async () => { const home = await mkdtemp(join(tmpdir(), 'dsh-agent-core-shared-home-')) const agentsHome = await mkdtemp(join(tmpdir(), 'dsh-agent-core-shared-agents-')) await mkdir(join(home, 'skills'), { recursive: true }) await writeFile(join(home, 'skills', 'shared-skill.md'), '---\nname: shared-skill\ndescription: Shared home skill\n---\n\nShared body.\n') const ctx = await mount({ dshHome: home, workspaceContext: false, skills: { local: { agentsHome } }, }, true) expect((await ctx.skills.list()).map(skill => skill.name)).toEqual(['shared-skill']) const execution: ToolExecution = { signal: testToolSignal, token: Symbol('agent-core-dsh-home-test') as ToolExecution['token'], callId: CallId('agent-core-dsh-home'), name: 'bash', arguments: { command: 'true' }, } expect(ctx.bashEnv.collect(execution)).toMatchObject({ DSH_HOME: home, DSH_SHELL: '1' }) await ctx.fiber.dispose() }) it('rejects conflicting global and nested DSH home directories', () => { expect(() => { agentCore.apply(new Context(), { dshHome: '/global-dsh-home', workspaceContext: false, skills: { local: { dshHome: '/nested-dsh-home' } }, }) }).toThrow('agent-spine-demo: dshHome and skills.local.dshHome must resolve to the same directory') }) it('delivers workspace instructions ahead of the first-step skill catalog', async () => { const root = await mkdtemp(join(tmpdir(), 'dsh-agent-spine-demo-prefix-order-')) try { await mkdir(join(root, '.git'), { recursive: true }) await writeFile(join(root, 'AGENTS.md'), 'workspace rule before skills') const adapter = new MockAdapter([textResponse('first')]) const ctx = await mount({ workspaceContext: { maxBytes: 65536 } }) await ctx.plugin(LocalFileSystem, { cwd: '/' }) ctx.llm.registerAdapter(['mock'], adapter) ctx.skills.register({ name: 'prefix-order-skill', description: 'Skill catalog after workspace rules', source: 'runtime', content: 'body', }) const handle = await ctx.agents.create({ sessionId: SessionId('prefix-order-session'), meta: { cwd: root }, agentOptions: { provider: 'mock', model: 'mock' }, }) handle.agent.followup(createUserMessage({ content: [{ type: 'text', text: 'hi' }], source: { kind: 'user' } })) await waitForIdle(ctx, handle.agent) expect(adapter.requests).toHaveLength(1) const workspaceIndex = adapter.requests[0]!.messages.findIndex( message => messageText(message).includes('workspace rule before skills'), ) const catalogIndex = adapter.requests[0]!.messages.findIndex( message => messageText(message).includes('prefix-order-skill'), ) expect(workspaceIndex).toBeGreaterThanOrEqual(0) expect(catalogIndex).toBeGreaterThanOrEqual(0) expect(workspaceIndex).toBeLessThan(catalogIndex) await handle.dispose() await ctx.fiber.dispose() } finally { await rm(root, { recursive: true, force: true }) } }) it('forwards its bundled tool configs to tool-bash and tool-tasks', async () => { const ctx = await mount({ workspaceContext: false, toolBash: { enableRunInBackground: false }, toolTasks: { waitTimeoutMs: 7, maxWaitTimeoutMs: 11 }, }, true) const bash = ctx.tools.schemas().find(tool => tool.name === 'bash') expect(bash).toBeDefined() expect(Object.keys((bash!.parameters as { properties: Record }).properties)) .not.toContain('run_in_background') const id = ctx.tasks.start({ kind: 'probe', label: 'config forwarding probe', run: () => ({ cancel: () => {}, done: Promise.resolve({ status: 'completed' }) }), }) const wait = vi.spyOn(ctx.tasks, 'wait') await ctx.tools.execute({ signal: testToolSignal, callId: CallId('task-config-forwarding'), name: 'task_output', arguments: { task_id: id, wait: true }, }) expect(wait).toHaveBeenCalledWith(id, 7, undefined, testToolSignal) await ctx.fiber.dispose() }) it('can omit skills and model-facing task controls for a foreground-only deployment', async () => { const ctx = await mount({ workspaceContext: false, skills: { enabled: false }, toolBash: { enableRunInBackground: false }, toolTasks: false, }, true) expect(ctx.tools.schemas().map(tool => tool.name)).toEqual(['bash']) expect(ctx.get('skills')).toBeUndefined() expect(ctx.get('tasks')).toBeDefined() await ctx.fiber.dispose() }) it('can omit the bundled bash tool and Harness identity for a compatibility deployment', async () => { const ctx = await mount({ includeHarnessIdentity: false, persona: 'You are a helpful software engineer assistant.', workspaceContext: false, skills: { enabled: false }, toolBash: false, toolTasks: false, }, true) expect(ctx.tools.schemas()).toEqual([]) expect(renderPrompt(await ctx.systemPrompt.assemble())) .toBe('You are a helpful software engineer assistant.') await ctx.fiber.dispose() }) it('picks shared spine config without leaking front-door fields', () => { const appConfig = { model: 'front-door-only', includeHarnessIdentity: false, persona: 'You are merged.', toolOrder: ['zulu'], tools: { mode: 'native' as const }, dshHome: '/tmp/dsh-home', sessionTitle: { fallbackMaxWords: 3, fallbackMaxBytes: 24, maxTitleBytes: 60 }, workspaceContext: false as const, skills: { enabled: false }, toolBash: { enableRunInBackground: false }, toolTasks: false as const, invariants: { enabled: false }, } expect(agentCore.pickSpineConfig(appConfig)).toEqual({ includeHarnessIdentity: appConfig.includeHarnessIdentity, persona: appConfig.persona, toolOrder: appConfig.toolOrder, tools: appConfig.tools, dshHome: appConfig.dshHome, sessionTitle: appConfig.sessionTitle, workspaceContext: false, skills: appConfig.skills, toolBash: appConfig.toolBash, toolTasks: appConfig.toolTasks, invariants: appConfig.invariants, }) expect(agentCore.pickSpineConfig({ workspaceContext: false })).toEqual({ workspaceContext: false }) }) it('uses the default skill config when apply is called directly without skills', async () => { await withIsolatedSkillHomes(async () => { const ctx = new Context() agentCore.apply(ctx, { agents: [], workspaceContext: false }) await new Promise(resolve => setTimeout(resolve, 50)) expect(ctx.skills).toBeDefined() expect(await ctx.skills.list()).toEqual([]) await ctx.fiber.dispose() }) }) it('forwards toolOrder to the system-prompt assembly', async () => { const ctx = await mount({ toolOrder: ['zulu', TOOL_ORDER_REST], workspaceContext: false }) // The bundle's own bash tools pend on the absent `ctx.bash` executor in // this providerless mount, so register two plain tools to order. for (const name of ['alpha', 'zulu']) { ctx.get('tools')!.register({ name, description: name, parameters: {}, output: { schema: { type: 'null' }, render: () => [] }, execute: async () => null, }) } const assembly = await ctx.get('systemPrompt')!.assemble() expect(assembly.tools.map(tool => tool.name)).toEqual(['zulu', 'alpha', 'skill', 'task_kill', 'task_list', 'task_output']) await ctx.fiber.dispose() }) it('supports direct apply with workspace instructions disabled and no forwarded agents', async () => { const ctx = new Context() agentCore.apply(ctx, { workspaceContext: false }) await new Promise(resolve => setTimeout(resolve, 50)) expect(ctx.get('agents')?.list()).toEqual([]) expect(ctx.get('systemPrompt')).toBeDefined() await ctx.fiber.dispose() }) it('re-exports the loop config schema as its own', () => { expect(agentCore.Config).toBeDefined() expect(agentCore.name).toBe('agent-spine-demo') }) it('has the namespace-plugin export shape (no stray default) so the Loader keeps name/Config/apply', () => { // A default export would make `unwrapExports` collapse this inject-less namespace and silently // drop `name`/`Config`. Apps import the bundle directly, so this is its Loader-shape guard. expect('default' in agentCore).toBe(false) expect(typeof agentCore.apply).toBe('function') const loader = Object.create(Loader.prototype) as Loader const unwrapped = loader.unwrapExports(agentCore) as Record expect(unwrapped).toBe(agentCore) expect(unwrapped.name).toBe('agent-spine-demo') expect(unwrapped.Config).toBeDefined() expect(typeof unwrapped.apply).toBe('function') }) it('keeps each standard-spine invariant companion loadable through the real Loader unwrap path', () => { const loader = Object.create(Loader.prototype) as Loader for (const companion of [sessionInvariant, agentInvariant, scopeInvariant, agentLoopInvariant]) { expect('default' in companion).toBe(false) const unwrapped = loader.unwrapExports(companion) as Record expect(unwrapped).toBe(companion) expect(typeof unwrapped.name).toBe('string') expect(unwrapped.inject).toContain('invariants') expect(typeof unwrapped.apply).toBe('function') } }) })