Merge remote-tracking branch 'origin/master' into worktree/provider-routed-llm-adapters
# Conflicts: # docs/cordis-catalog/events.md # docs/cordis-catalog/services.md # docs/event-producer-consumer.md # docs/module-graph.md # packages/context/time-context/tests/time-context.spec.ts # packages/cordis/tool-cordis/src/api-catalog.ts # packages/core/session/README.md # packages/core/session/src/types.ts # packages/core/session/tests/surface.spec.ts # packages/examples/acp-demo/tests/acp-agent.spec.ts # packages/examples/stdio-demo/tests/stdio-agent.spec.ts # packages/hooks/hooks-claude/tests/coverage.spec.ts # packages/hooks/hooks-codex/tests/coverage.spec.ts # packages/session-query/session-query/tests/session-query.spec.ts # packages/support/invariants/tests/invariants.spec.ts # packages/ui/acp/tests/harness.ts
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { mkdir, mkdtemp, writeFile } from 'node:fs/promises'
|
||||
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'
|
||||
import { join } from 'node:path'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { Context } from 'cordis'
|
||||
@@ -7,7 +7,11 @@ import Loader from '@cordisjs/plugin-loader'
|
||||
import { TOOL_ORDER_REST } from '@deepseek-ai/dsh-system-prompt'
|
||||
import * as agentCore from '../src/index.ts'
|
||||
import { AgentId, agentEvents, type Agent } from '@deepseek-ai/dsh-agent'
|
||||
import { SessionId } from '@deepseek-ai/dsh-session'
|
||||
import LocalFileSystem from '@deepseek-ai/dsh-fs-local'
|
||||
import { MockAdapter, textResponse } from '../../../core/agent-loop/tests/mock-adapter.ts'
|
||||
import { CallId, type Message } from '@deepseek-ai/dsh-llm'
|
||||
import type { ToolExecution } from '@deepseek-ai/dsh-tools'
|
||||
|
||||
declare module '@deepseek-ai/dsh-tasks' {
|
||||
interface TaskKindMap {
|
||||
@@ -34,7 +38,7 @@ async function composePrefix(ctx: Context, cwd: string): Promise<Message[]> {
|
||||
* 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<Context> {
|
||||
async function mount(config: agentCore.Config, withBash = false): Promise<Context> {
|
||||
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-'))
|
||||
@@ -82,9 +86,24 @@ async function withIsolatedSkillHomes<T>(run: () => Promise<T>): Promise<T> {
|
||||
}
|
||||
}
|
||||
|
||||
function waitForMainIdle(ctx: Context): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
const dispose = ctx.on('agent/status', (agent, status) => {
|
||||
if (agent.id === 'main' && status === 'idle') {
|
||||
dispose()
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function messageText(message: Message | undefined): string {
|
||||
return message?.content.map(block => block.type === 'text' ? block.text : '').join('\n') ?? ''
|
||||
}
|
||||
|
||||
describe('dsh-agent-spine-demo bundle', () => {
|
||||
it('brings up the full default spine', async () => {
|
||||
const ctx = await mount()
|
||||
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()
|
||||
@@ -99,7 +118,7 @@ describe('dsh-agent-spine-demo bundle', () => {
|
||||
})
|
||||
|
||||
it('includes the skill registry, local provider, and skill tool without builtin skills', async () => {
|
||||
const ctx = await mount()
|
||||
const ctx = await mount({ workspaceContext: false })
|
||||
|
||||
expect(ctx.skills).toBeDefined()
|
||||
expect(ctx.tools.schemas().map(tool => tool.name)).toContain('skill')
|
||||
@@ -109,7 +128,7 @@ describe('dsh-agent-spine-demo bundle', () => {
|
||||
})
|
||||
|
||||
it('defaults the agents list to empty (no pre-created agents)', async () => {
|
||||
const ctx = await mount()
|
||||
const ctx = await mount({ workspaceContext: false })
|
||||
expect(ctx.get('agents')?.get(AgentId('main'))).toBeUndefined()
|
||||
await ctx.fiber.dispose()
|
||||
})
|
||||
@@ -118,6 +137,7 @@ describe('dsh-agent-spine-demo bundle', () => {
|
||||
const ctx = await mount({
|
||||
agents: [{ id: AgentId('main'), provider: 'mock', model: 'mock' }],
|
||||
persona: 'You are main.',
|
||||
workspaceContext: false,
|
||||
})
|
||||
expect(ctx.get('agents')?.get(AgentId('main'))).toBeDefined()
|
||||
const assembly = await ctx.get('systemPrompt')!.assemble()
|
||||
@@ -129,7 +149,7 @@ describe('dsh-agent-spine-demo bundle', () => {
|
||||
// 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, {})
|
||||
agentCore.apply(ctx, { workspaceContext: false })
|
||||
await new Promise(resolve => setTimeout(resolve, 50))
|
||||
expect(ctx.get('agentLoop')).toBeDefined()
|
||||
expect(ctx.get('agents')?.list()).toHaveLength(0)
|
||||
@@ -138,6 +158,64 @@ describe('dsh-agent-spine-demo bundle', () => {
|
||||
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('ok')])
|
||||
const ctx = await mount({ workspaceContext: { maxBytes: 65536 } })
|
||||
await ctx.plugin(LocalFileSystem, { cwd: '/' })
|
||||
ctx.llm.registerAdapter(['mock'], adapter)
|
||||
const handle = await ctx.agents.create({
|
||||
agentId: AgentId('main'),
|
||||
sessionId: SessionId('main-session'),
|
||||
meta: { cwd: root },
|
||||
agentOptions: { provider: 'mock', model: 'mock' },
|
||||
})
|
||||
const agent = handle.agent
|
||||
|
||||
agent.send([{ type: 'text', text: 'hi' }])
|
||||
await waitForMainIdle(ctx)
|
||||
|
||||
const sentText = adapter.requests[0]?.messages.map(messageText).join('\n')
|
||||
expect(sentText).toContain('hi')
|
||||
expect(sentText).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({
|
||||
agentId: AgentId('main'),
|
||||
sessionId: SessionId('main-disabled-session'),
|
||||
meta: { cwd: root },
|
||||
agentOptions: { provider: 'mock', model: 'mock' },
|
||||
})
|
||||
|
||||
handle.agent.send([{ type: 'text', text: 'hi' }])
|
||||
await waitForMainIdle(ctx)
|
||||
|
||||
expect(adapter.requests[0]?.messages).toEqual([{ role: 'user', content: [{ type: 'text', text: 'hi' }] }])
|
||||
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-'))
|
||||
@@ -146,6 +224,7 @@ describe('dsh-agent-spine-demo bundle', () => {
|
||||
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: {
|
||||
@@ -161,8 +240,76 @@ describe('dsh-agent-spine-demo bundle', () => {
|
||||
await ctx.fiber.dispose()
|
||||
})
|
||||
|
||||
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 = {
|
||||
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(/must resolve to the same directory/)
|
||||
})
|
||||
|
||||
it('places workspace instructions before the skill catalog in the session prefix', 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('ok')])
|
||||
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({
|
||||
agentId: AgentId('main'),
|
||||
sessionId: SessionId('prefix-order-session'),
|
||||
meta: { cwd: root },
|
||||
agentOptions: { provider: 'mock', model: 'mock' },
|
||||
})
|
||||
|
||||
handle.agent.send([{ type: 'text', text: 'hi' }])
|
||||
await waitForMainIdle(ctx)
|
||||
|
||||
expect(messageText(adapter.requests[0]?.messages[0])).toContain('workspace rule before skills')
|
||||
expect(messageText(adapter.requests[0]?.messages[1])).toContain('prefix-order-skill')
|
||||
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)
|
||||
@@ -188,10 +335,36 @@ describe('dsh-agent-spine-demo bundle', () => {
|
||||
await ctx.fiber.dispose()
|
||||
})
|
||||
|
||||
it('picks shared spine config without leaking front-door fields', () => {
|
||||
const appConfig = {
|
||||
model: 'front-door-only',
|
||||
persona: 'You are merged.',
|
||||
toolOrder: ['zulu'],
|
||||
tools: { mode: 'native' as const },
|
||||
dshHome: '/tmp/dsh-home',
|
||||
workspaceContext: false as const,
|
||||
skills: {},
|
||||
toolBash: { enableRunInBackground: false },
|
||||
toolTasks: { waitTimeoutMs: 7, maxWaitTimeoutMs: 11 },
|
||||
}
|
||||
|
||||
expect(agentCore.pickSpineConfig(appConfig)).toEqual({
|
||||
persona: appConfig.persona,
|
||||
toolOrder: appConfig.toolOrder,
|
||||
tools: appConfig.tools,
|
||||
dshHome: appConfig.dshHome,
|
||||
workspaceContext: false,
|
||||
skills: {},
|
||||
toolBash: appConfig.toolBash,
|
||||
toolTasks: appConfig.toolTasks,
|
||||
})
|
||||
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: [] })
|
||||
agentCore.apply(ctx, { agents: [], workspaceContext: false })
|
||||
await new Promise(resolve => setTimeout(resolve, 50))
|
||||
expect(ctx.skills).toBeDefined()
|
||||
expect(await ctx.skills.list()).toEqual([])
|
||||
@@ -200,7 +373,7 @@ describe('dsh-agent-spine-demo bundle', () => {
|
||||
})
|
||||
|
||||
it('forwards toolOrder to the system-prompt assembly', async () => {
|
||||
const ctx = await mount({ toolOrder: ['zulu', TOOL_ORDER_REST] })
|
||||
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']) {
|
||||
@@ -216,6 +389,16 @@ describe('dsh-agent-spine-demo bundle', () => {
|
||||
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')
|
||||
|
||||
Reference in New Issue
Block a user