test(support): add shared AgentLoop testkit

This commit is contained in:
Yichen Jiang
2026-07-16 17:39:53 +08:00
parent 4139e093dd
commit d3b959389a
48 changed files with 361 additions and 292 deletions

View File

@@ -0,0 +1,27 @@
# `@deepseek-ai/dsh-agent-loop-testkit`
Shared prerequisite mounting for tests that exercise the concrete `AgentLoop`. `mountAgentLoopTestDependencies(ctx, options?)` installs the LLM, session, system-prompt, tool, and agent services in dependency order, then returns before the loop is mounted.
The caller registers adapters and optional plugins, mounts `AgentLoop` with the configuration under test, and disposes its own Context. System-prompt and tool-registry configuration can be forwarded through `options`; the helper does not provide test defaults beyond those owned by the services. A plugin-load failure rejects the helper call, while services activated earlier in the sequence remain owned by the caller's Context.
```ts
import { Context } from 'cordis'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit'
const ctx = new Context()
await mountAgentLoopTestDependencies(ctx)
// Register the test adapter and any optional plugins here.
await ctx.plugin(AgentLoop, { agents: [] })
```
Tests of injection failures, partial topology, service load order, or service teardown mount their dependencies directly instead of using this helper.
## Model Experience
None, as this test-only composition helper neither drives nor modifies model requests.
## Known Limitations and Deferred Work
- **Only the mandatory prerequisite spine is shared** — adapters, optional plugins, `AgentLoop`, agents, and Context teardown remain caller-owned so scenario-specific ordering stays visible.

View File

@@ -0,0 +1,41 @@
{
"name": "@deepseek-ai/dsh-agent-loop-testkit",
"description": "Shared prerequisite mounting for tests that exercise the concrete agent loop",
"version": "0.0.1",
"private": true,
"type": "module",
"main": "lib/index.js",
"types": "lib/types/index.d.ts",
"exports": {
".": {
"types": "./lib/types/index.d.ts",
"default": "./lib/index.js"
},
"./src/*": "./src/*",
"./package.json": "./package.json"
},
"files": [
"lib/index.js",
"lib/types/**/*.d.ts",
"lib/types/**/*.d.ts.map",
"src"
],
"license": "BSD-3-Clause",
"peerDependencies": {
"@deepseek-ai/dsh-agent": "^0.0.1",
"@deepseek-ai/dsh-llm": "^0.0.1",
"@deepseek-ai/dsh-session": "^0.0.1",
"@deepseek-ai/dsh-system-prompt": "^0.0.1",
"@deepseek-ai/dsh-tools": "^0.0.1",
"cordis": "^4.0.0-rc.7"
},
"devDependencies": {
"@deepseek-ai/dsh-agent": "workspace:^",
"@deepseek-ai/dsh-agent-loop": "workspace:^",
"@deepseek-ai/dsh-llm": "workspace:^",
"@deepseek-ai/dsh-session": "workspace:^",
"@deepseek-ai/dsh-system-prompt": "workspace:^",
"@deepseek-ai/dsh-tools": "workspace:^",
"cordis": "^4.0.0-rc.7"
}
}

View File

@@ -0,0 +1,46 @@
/**
* Shared mounting for the services required before tests load the concrete
* agent loop. The caller retains ownership of the context, loop, adapters,
* optional plugins, and teardown.
* @module @deepseek-ai/dsh-agent-loop-testkit
*/
import type { Context } from 'cordis'
import AgentRegistry from '@deepseek-ai/dsh-agent'
import LlmService from '@deepseek-ai/dsh-llm'
import SessionStore from '@deepseek-ai/dsh-session'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import type { Config as SystemPromptConfig } from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry from '@deepseek-ai/dsh-tools'
import type { Config as ToolRegistryConfig } from '@deepseek-ai/dsh-tools'
/** Configuration forwarded to the prerequisite service plugins. */
export interface AgentLoopTestDependenciesOptions {
/** Configuration for the system-prompt registry. */
readonly systemPrompt?: SystemPromptConfig
/** Configuration for the tool registry. */
readonly tools?: ToolRegistryConfig
}
/**
* Mount the standard prerequisite services for an AgentLoop test.
*
* The function deliberately does not mount AgentLoop or register an adapter,
* so tests retain control of load order and the topology under test. The
* context owns every mounted service and remains responsible for disposal. A
* plugin-load failure rejects the promise; services activated earlier in the
* sequence remain context-owned and unwind with that context.
* @param ctx - test context that owns the mounted services.
* @param options - optional service configuration forwarded without mutation.
* @returns after every prerequisite service has activated.
*/
export async function mountAgentLoopTestDependencies(
ctx: Context,
options: AgentLoopTestDependenciesOptions = {},
): Promise<void> {
await ctx.plugin(LlmService)
await ctx.plugin(SessionStore)
await ctx.plugin(SystemPrompt, options.systemPrompt ?? {})
await ctx.plugin(ToolRegistry, options.tools ?? {})
await ctx.plugin(AgentRegistry)
}

View File

@@ -0,0 +1,20 @@
import { describe, expect, it } from 'vitest'
import { Context } from 'cordis'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
import { renderPrompt } from '@deepseek-ai/dsh-system-prompt'
import { mountAgentLoopTestDependencies } from '../src/index.ts'
describe('dsh-agent-loop-testkit', () => {
it('mounts a configurable prerequisite spine that can activate AgentLoop', async () => {
const ctx = new Context()
await mountAgentLoopTestDependencies(ctx, {
systemPrompt: { persona: 'Test persona.' },
tools: { mode: 'native' },
})
expect(renderPrompt(await ctx.systemPrompt.assemble())).toContain('Test persona.')
await expect(ctx.plugin(AgentLoop, { agents: [] })).resolves.toBeDefined()
await ctx.fiber.dispose()
})
})

View File

@@ -0,0 +1,33 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "lib/types"
},
"include": [
"src"
],
"references": [
{
"path": "../../../vendor/cosmokit"
},
{
"path": "../../../vendor/cordis"
},
{
"path": "../../core/agent"
},
{
"path": "../../llm/llm"
},
{
"path": "../../core/session"
},
{
"path": "../../core/system-prompt"
},
{
"path": "../../core/tools"
}
]
}