2026-07-12 16:00:56 +08:00
import { describe , expect , it , vi } from 'vitest'
2026-07-11 23:02:24 +08:00
import { mkdir , mkdtemp , rm , writeFile } from 'node:fs/promises'
2026-06-25 16:04:42 +08:00
import { join } from 'node:path'
import { tmpdir } from 'node:os'
2026-06-21 12:03:44 +08:00
import { Context } from 'cordis'
2026-06-21 12:49:59 +08:00
import Loader from '@cordisjs/plugin-loader'
2026-07-29 14:12:27 +08:00
import { renderPrompt , TOOL_ORDER_REST } from '@deepseek-ai/dsh-system-prompt'
2026-06-21 12:03:44 +08:00
import * as agentCore from '../src/index.ts'
2026-07-14 01:59:21 +08:00
import { agentEvents , type Agent } from '@deepseek-ai/dsh-agent'
2026-06-25 16:04:42 +08:00
import { SessionId } from '@deepseek-ai/dsh-session'
2026-07-27 16:50:56 +08:00
import LocalBashExecutor from '@deepseek-ai/dsh-bash-local'
2026-07-03 11:56:58 +08:00
import LocalFileSystem from '@deepseek-ai/dsh-fs-local'
2026-07-27 16:50:56 +08:00
import * as ToolFs from '@deepseek-ai/dsh-tool-fs'
import { MockAdapter , textResponse , toolCallResponse } from '../../../core/agent-loop/tests/mock-adapter.ts'
2026-07-25 10:18:16 +08:00
import {
2026-07-29 16:40:40 +08:00
createUserMessage ,
2026-07-25 10:18:16 +08:00
CallId ,
LlmAdapter ,
LlmError ,
resolveRetryPolicy ,
type GenerateOptions ,
type Message ,
type ResolvedRetryPolicy ,
type StreamChunk ,
} from '@deepseek-ai/dsh-llm'
2026-07-12 15:41:42 +08:00
import type { ToolExecution } from '@deepseek-ai/dsh-tools'
2026-07-19 19:19:57 +08:00
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'
2026-07-10 14:19:06 +08:00
2026-07-19 23:38:54 +08:00
const testToolSignal = new AbortController ( ) . signal
2026-07-15 21:45:30 +08:00
declare module '@deepseek-ai/dsh-tasks' {
interface TaskKindMap {
probe : 'probe'
}
}
2026-06-21 12:03:44 +08:00
2026-07-10 14:19:06 +08:00
async function composePrefix ( ctx : Context , cwd : string ) : Promise < Message [ ] > {
2026-07-24 21:18:48 +08:00
const agent = ctx . agentLoop . create ( SessionId ( 'agent-spine-prefix' ) , { } , { cwd } )
2026-07-31 19:21:16 +08:00
const signal = new AbortController ( ) . signal
const decision = await agentEvents ( ctx , agent ) . waterfall (
2026-08-06 12:13:14 +08:00
'agent/pre-step' , { messages : [ ] , turn : 1 , step : 1 , signal } ,
2026-07-31 19:21:16 +08:00
( ) = > Promise . resolve ( { kind : 'enter' , messages : [ ] } ) ,
)
if ( decision . kind === 'enter' ) {
for ( const message of decision . messages ) {
agent . session . append ( 'user/message' , message , { surfaceOp : 'append' } )
}
}
2026-07-24 21:18:48 +08:00
return agent . session . deriveMessages ( )
2026-07-10 14:19:06 +08:00
}
2026-06-21 12:03:44 +08:00
/**
2026-07-15 15:57:57 +08:00
* Unit coverage for the @deepseek-ai/dsh-agent-spine-demo bundle: mounting it brings
2026-07-08 15:50:38 +08:00
* up the whole default spine in one `ctx.plugin`, and the forwarded
2026-06-21 12:03:44 +08:00
* `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.
*/
2026-07-15 22:27:02 +08:00
async function mount ( config : agentCore.Config , withBash = false ) : Promise < Context > {
2026-06-25 23:35:13 +08:00
const oldDshHome = process . env . DSH_HOME
2026-07-05 18:33:27 +08:00
const oldAgentsHome = process . env . DSH_AGENTS_HOME
2026-07-15 15:57:57 +08:00
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-' ) )
2026-06-21 12:03:44 +08:00
const ctx = new Context ( )
2026-07-20 00:38:37 +08:00
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' ) } ,
} )
}
2026-06-25 23:35:13 +08:00
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
}
2026-07-05 18:33:27 +08:00
if ( oldAgentsHome === undefined ) {
delete process . env . DSH_AGENTS_HOME
} else {
process . env . DSH_AGENTS_HOME = oldAgentsHome
}
2026-06-25 23:35:13 +08:00
}
2026-06-21 12:03:44 +08:00
}
2026-07-05 21:46:38 +08:00
async function withIsolatedSkillHomes < T > ( run : ( ) = > Promise < T > ) : Promise < T > {
const oldDshHome = process . env . DSH_HOME
const oldAgentsHome = process . env . DSH_AGENTS_HOME
2026-07-15 15:57:57 +08:00
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-' ) )
2026-07-05 21:46:38 +08:00
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
}
}
2026-06-21 12:03:44 +08:00
}
2026-07-24 21:18:48 +08:00
function waitForIdle ( _ctx : Context , target : Agent ) : Promise < void > {
return target . whenIdle ( )
2026-06-25 16:04:42 +08:00
}
2026-07-06 15:44:42 +08:00
function messageText ( message : Message | undefined ) : string {
return message ? . content . map ( block = > block . type === 'text' ? block . text : '' ) . join ( '\n' ) ? ? ''
2026-06-25 16:04:42 +08:00
}
2026-07-20 03:34:19 +08:00
class TransientOnceAdapter extends LlmAdapter {
requests = 0
2026-07-25 10:18:16 +08:00
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
}
2026-07-20 03:34:19 +08:00
async * stream ( _options : GenerateOptions ) : AsyncIterable < StreamChunk > {
this . requests += 1
if ( this . requests === 1 ) throw new LlmError ( 'temporary outage' , 'SERVER' )
yield * textResponse ( 'recovered by bundled policy' )
}
}
2026-07-15 15:57:57 +08:00
describe ( 'dsh-agent-spine-demo bundle' , ( ) = > {
2026-07-08 15:50:38 +08:00
it ( 'brings up the full default spine' , async ( ) = > {
2026-07-12 12:09:35 +08:00
const ctx = await mount ( { workspaceContext : false } )
2026-06-21 12:03:44 +08:00
// 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 ( )
2026-07-21 01:53:37 +08:00
expect ( ctx . get ( 'sessionTitle' ) ) . toBeDefined ( )
2026-06-21 12:03:44 +08:00
expect ( ctx . get ( 'systemPrompt' ) ) . toBeDefined ( )
expect ( ctx . get ( 'tools' ) ) . toBeDefined ( )
2026-06-25 23:35:13 +08:00
expect ( ctx . get ( 'skills' ) ) . toBeDefined ( )
2026-06-21 12:03:44 +08:00
expect ( ctx . get ( 'agents' ) ) . toBeDefined ( )
2026-07-11 23:04:27 +08:00
expect ( ctx . get ( 'tasks' ) ) . toBeDefined ( )
2026-07-19 19:19:57 +08:00
expect ( ctx . get ( 'invariants' ) ) . toBeDefined ( )
2026-06-21 12:03:44 +08:00
expect ( ctx . get ( 'agentLoop' ) ) . toBeDefined ( )
2026-07-19 23:55:33 +08:00
expect ( ctx . get ( 'goals' ) ) . toBeUndefined ( )
await ctx . fiber . dispose ( )
} )
2026-07-21 14:09:17 +08:00
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 ,
} )
2026-07-28 13:55:59 +08:00
session . append ( 'user/message' , createUserMessage ( {
2026-07-21 14:09:17 +08:00
content : [ { type : 'text' , text : 'One two three four' } ] ,
source : { kind : 'user' } ,
2026-07-28 13:55:59 +08:00
} ) , { surfaceOp : 'append' } )
2026-07-21 14:09:17 +08:00
await new Promise ( resolve = > setTimeout ( resolve , 0 ) )
expect ( ctx . sessionTitle . get ( session ) ? . title ) . toBe ( 'One' )
await ctx . fiber . dispose ( )
} )
2026-07-19 23:55:33 +08:00
it ( 'opts into the configured persisted-goal domain, tools, and same-session driver' , async ( ) = > {
const ctx = await mount ( {
workspaceContext : false ,
2026-07-20 18:06:22 +08:00
agents : [ { id : SessionId ( 'configured-goal' ) , provider : 'mock' , model : 'mock' } ] ,
2026-07-19 23:55:33 +08:00
goals : {
domain : { defaultMaxGoalRounds : 17 } ,
tool : { blockedAfterConsecutiveRounds : 5 } ,
} ,
} )
2026-07-20 18:06:22 +08:00
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 ,
2026-07-19 23:55:33 +08:00
} )
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 ( )
2026-06-21 12:03:44 +08:00
await ctx . fiber . dispose ( )
} )
2026-07-19 19:19:57 +08:00
it ( 'mounts package companions and forwards invariant selection config' , async ( ) = > {
const nestedTurn = ( ctx : Context ) : void = > {
const session = ctx . sessions . create ( )
2026-07-30 13:49:57 +08:00
session . append ( 'turn/start' , { turn : 1 } )
session . append ( 'turn/start' , { turn : 2 } )
2026-07-19 19:19:57 +08:00
}
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 ( )
}
} )
2026-07-20 03:34:19 +08:00
it ( 'loads and configures bounded request recovery for every bundled front door' , async ( ) = > {
const adapter = new TransientOnceAdapter ( )
2026-07-25 10:18:16 +08:00
const ctx = await mount ( { workspaceContext : false } )
2026-07-20 03:34:19 +08:00
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' } ,
} )
2026-07-28 13:55:59 +08:00
handle . agent . followup ( createUserMessage ( { content : [ { type : 'text' , text : 'recover' } ] , source : { kind : 'user' } } ) )
2026-07-20 03:34:19 +08:00
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 )
2026-07-25 10:18:16 +08:00
expect ( retryEvents [ 0 ] ? . data ) . toMatchObject ( { provider : 'mock' , mode : 'normal' , maxRetries : 1 } )
2026-07-21 01:53:37 +08:00
expect ( handle . agent . session . events . find ( event = > event . type === 'session/title' ) ? . data . title ) . toBe ( 'recover' )
2026-07-20 03:34:19 +08:00
expect ( messageText ( handle . agent . session . deriveMessages ( ) . at ( - 1 ) ) ) . toBe ( 'recovered by bundled policy' )
await handle . dispose ( )
await ctx . fiber . dispose ( )
} )
2026-07-08 15:50:38 +08:00
it ( 'includes the skill registry, local provider, and skill tool without builtin skills' , async ( ) = > {
2026-07-12 12:09:35 +08:00
const ctx = await mount ( { workspaceContext : false } )
2026-06-25 23:35:13 +08:00
expect ( ctx . skills ) . toBeDefined ( )
expect ( ctx . tools . schemas ( ) . map ( tool = > tool . name ) ) . toContain ( 'skill' )
2026-07-08 15:50:38 +08:00
expect ( await ctx . skills . list ( ) ) . toEqual ( [ ] )
2026-06-25 23:35:13 +08:00
await ctx . fiber . dispose ( )
} )
2026-06-21 12:03:44 +08:00
it ( 'defaults the agents list to empty (no pre-created agents)' , async ( ) = > {
2026-07-12 12:09:35 +08:00
const ctx = await mount ( { workspaceContext : false } )
2026-07-14 01:59:21 +08:00
expect ( ctx . get ( 'agents' ) ? . get ( SessionId ( 'main' ) ) ) . toBeUndefined ( )
2026-06-21 12:03:44 +08:00
await ctx . fiber . dispose ( )
} )
2026-07-05 23:23:46 +08:00
it ( 'forwards a pre-created agent to the loop and the persona to system-prompt' , async ( ) = > {
2026-06-21 12:03:44 +08:00
const ctx = await mount ( {
2026-07-18 12:21:15 +08:00
agents : [ { id : SessionId ( 'main' ) , provider : 'mock' , model : 'mock' } ] ,
2026-07-05 23:23:46 +08:00
persona : 'You are main.' ,
2026-07-12 12:09:35 +08:00
workspaceContext : false ,
2026-06-21 12:03:44 +08:00
} )
2026-07-14 01:59:21 +08:00
const agent = ctx . get ( 'agents' ) ? . list ( ) [ 0 ]
expect ( agent ? . id ) . toBe ( agent ? . session . id )
expect ( agent ? . id ) . toMatch ( /^main-session-/ )
2026-07-05 23:23:46 +08:00
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 ( )
} )
2026-07-16 14:36:16 +08:00
it ( 'forwards the global maxParallelToolCalls config to agent-loop' , async ( ) = > {
2026-07-16 11:36:16 +08:00
const ctx = await mount ( {
2026-07-19 01:54:03 +08:00
agents : [ { id : SessionId ( 'main' ) , provider : 'mock' , model : 'mock' } ] ,
2026-07-16 11:36:16 +08:00
maxParallelToolCalls : 3 ,
2026-07-17 22:08:40 +08:00
workspaceContext : false ,
2026-07-16 11:36:16 +08:00
} )
2026-07-16 14:36:16 +08:00
expect ( ctx . get ( 'agentLoop' ) ? . config . maxParallelToolCalls ) . toBe ( 3 )
2026-07-16 11:36:16 +08:00
await ctx . fiber . dispose ( )
} )
2026-07-05 23:23:46 +08:00
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 ( )
2026-07-12 12:09:35 +08:00
agentCore . apply ( ctx , { workspaceContext : false } )
2026-07-05 23:23:46 +08:00
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 ( '' )
2026-06-21 12:03:44 +08:00
await ctx . fiber . dispose ( )
} )
2026-07-19 23:55:33 +08:00
it ( 'uses owner defaults for a schema-bypassing empty goal opt-in' , async ( ) = > {
const ctx = new Context ( )
2026-07-20 18:06:22 +08:00
agentCore . apply ( ctx , {
workspaceContext : false ,
agents : [ { id : SessionId ( 'defaulted-goal' ) , provider : 'mock' , model : 'mock' } ] ,
goals : { } ,
} )
2026-07-19 23:55:33 +08:00
await new Promise ( resolve = > setTimeout ( resolve , 50 ) )
2026-07-20 18:06:22 +08:00
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 ,
2026-07-19 23:55:33 +08:00
} )
expect ( ctx . tools . get ( 'get_goal' ) ) . toBeDefined ( )
await ctx . fiber . dispose ( )
} )
2026-07-10 14:32:44 +08:00
it ( 'loads workspace instructions into requests through the bundled spine' , async ( ) = > {
2026-07-15 17:04:16 +08:00
const root = await mkdtemp ( join ( tmpdir ( ) , 'dsh-agent-spine-demo-workspace-context-' ) )
2026-06-25 16:04:42 +08:00
try {
await mkdir ( join ( root , '.git' ) , { recursive : true } )
await writeFile ( join ( root , 'AGENTS.md' ) , 'bundled project rule' )
2026-08-05 16:02:58 +08:00
const adapter = new MockAdapter ( [ textResponse ( 'first' ) ] )
2026-07-12 12:09:35 +08:00
const ctx = await mount ( { workspaceContext : { maxBytes : 65536 } } )
2026-07-03 11:56:58 +08:00
await ctx . plugin ( LocalFileSystem , { cwd : '/' } )
2026-06-25 16:04:42 +08:00
ctx . llm . registerAdapter ( [ 'mock' ] , adapter )
2026-07-13 14:37:32 +08:00
const handle = await ctx . agents . create ( {
2026-06-25 16:04:42 +08:00
sessionId : SessionId ( 'main-session' ) ,
meta : { cwd : root } ,
2026-07-17 21:56:10 +08:00
agentOptions : { provider : 'mock' , model : 'mock' } ,
2026-06-25 16:04:42 +08:00
} )
const agent = handle . agent
2026-07-28 13:55:59 +08:00
agent . followup ( createUserMessage ( { content : [ { type : 'text' , text : 'hi' } ] , source : { kind : 'user' } } ) )
2026-07-18 12:54:45 +08:00
await waitForIdle ( ctx , agent )
2026-06-25 16:04:42 +08:00
2026-08-05 16:02:58 +08:00
expect ( adapter . requests ) . toHaveLength ( 1 )
2026-07-31 19:40:59 +08:00
const firstRequestText = adapter . requests [ 0 ] ? . messages . map ( messageText ) . join ( '\n' )
expect ( firstRequestText ) . toContain ( 'hi' )
2026-08-05 16:02:58 +08:00
expect ( firstRequestText ) . toContain ( 'bundled project rule' )
2026-07-06 10:14:47 +08:00
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' )
2026-06-25 16:04:42 +08:00
await handle . dispose ( )
await ctx . fiber . dispose ( )
} finally {
await rm ( root , { recursive : true , force : true } )
}
} )
2026-07-10 14:32:44 +08:00
it ( 'forwards workspace-context config to the bundled loader' , async ( ) = > {
2026-07-15 17:04:16 +08:00
const root = await mkdtemp ( join ( tmpdir ( ) , 'dsh-agent-spine-demo-workspace-context-disabled-' ) )
2026-06-25 16:04:42 +08:00
try {
await mkdir ( join ( root , '.git' ) , { recursive : true } )
await writeFile ( join ( root , 'AGENTS.md' ) , 'must not be injected' )
const adapter = new MockAdapter ( [ textResponse ( 'ok' ) ] )
2026-07-10 14:32:44 +08:00
const ctx = await mount ( { workspaceContext : { maxBytes : 0 } } )
2026-06-25 16:04:42 +08:00
ctx . llm . registerAdapter ( [ 'mock' ] , adapter )
2026-07-13 14:37:32 +08:00
const handle = await ctx . agents . create ( {
2026-06-25 16:04:42 +08:00
sessionId : SessionId ( 'main-disabled-session' ) ,
meta : { cwd : root } ,
2026-07-17 21:56:10 +08:00
agentOptions : { provider : 'mock' , model : 'mock' } ,
2026-06-25 16:04:42 +08:00
} )
2026-07-28 13:55:59 +08:00
handle . agent . followup ( createUserMessage ( { content : [ { type : 'text' , text : 'hi' } ] , source : { kind : 'user' } } ) )
2026-07-18 12:54:45 +08:00
await waitForIdle ( ctx , handle . agent )
2026-06-25 16:04:42 +08:00
2026-07-28 14:15:23 +08:00
expect ( adapter . requests [ 0 ] ? . messages ) . toEqual ( [ {
id : expect.any ( String ) as unknown ,
role : 'user' ,
content : [ { type : 'text' , text : 'hi' } ] ,
source : { kind : 'user' } ,
} ] )
2026-06-25 16:04:42 +08:00
await handle . dispose ( )
await ctx . fiber . dispose ( )
} finally {
await rm ( root , { recursive : true , force : true } )
}
} )
2026-07-10 14:19:06 +08:00
it ( 'forwards skill config to the registry, local provider, and model-facing consumer' , async ( ) = > {
2026-07-15 15:57:57 +08:00
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-' ) )
2026-07-08 15:50:38 +08:00
await mkdir ( custom , { recursive : true } )
await writeFile ( join ( custom , 'custom-skill.md' ) , '---\nname: custom-skill\ndescription: Custom skill\n---\n\nCustom body.\n' )
2026-07-05 18:33:27 +08:00
const ctx = await mount ( {
agents : [ ] ,
2026-07-12 12:09:35 +08:00
workspaceContext : false ,
2026-07-05 18:33:27 +08:00
skills : {
2026-07-10 14:19:06 +08:00
registry : { collectCacheMaxEntries : 4 } ,
2026-07-08 15:50:38 +08:00
local : {
dshHome : join ( home , '.dsh' ) ,
agentsHome : join ( agentsHome , '.agents' ) ,
customSkillDirs : [ custom ] ,
} ,
2026-07-10 14:19:06 +08:00
tool : { catalogDescriptionMaxLength : 6 } ,
2026-07-05 18:33:27 +08:00
} ,
} )
2026-07-08 15:50:38 +08:00
expect ( ( await ctx . skills . list ( ) ) . map ( skill = > skill . name ) ) . toEqual ( [ 'custom-skill' ] )
2026-07-10 14:19:06 +08:00
expect ( JSON . stringify ( await composePrefix ( ctx , '/tmp' ) ) ) . toContain ( '- `custom-skill`: Cus...' )
2026-07-05 18:33:27 +08:00
await ctx . fiber . dispose ( )
} )
2026-07-27 16:50:56 +08:00
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' } ,
} )
2026-07-29 16:40:40 +08:00
handle . agent . followup ( createUserMessage ( {
2026-07-28 01:10:33 +08:00
content : [ { type : 'text' , text : 'Create and load the project skill.' } ] ,
source : { kind : 'user' } ,
2026-07-29 16:40:40 +08:00
} ) )
2026-07-27 16:50:56 +08:00
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' )
2026-07-28 01:10:33 +08:00
expect ( catalogRequest ) . toContain ( 'The following skills are available in this session:' )
2026-07-27 16:50:56 +08:00
expect ( catalogRequest ) . toContain ( '- `hot-skill`: Hot-added skill' )
const loadedRequest = JSON . stringify ( adapter . requests [ 3 ] ? . messages )
expect ( loadedRequest ) . toContain ( '<skill_instructions>' )
expect ( loadedRequest ) . toContain ( 'Use the freshly loaded body.' )
const transcript = handle . agent . session . events . flatMap < Record < string , unknown > > ( ( event ) = > {
if ( event . type === 'user/message'
&& event . data . source . kind === 'plugin'
2026-07-29 16:40:40 +08:00
&& event . data . source . plugin === 'dsh-tool-skill' ) {
2026-07-27 16:50:56 +08:00
return [ {
type : event . type ,
source : event.data.source ,
text : event.data.content.map ( block = > block . type === 'text' ? block . text : '' ) . join ( '\n' ) ,
} ]
}
2026-07-29 16:40:40 +08:00
if ( event . type === 'tool/result'
&& [ 'write-skill' , 'load-skill' ] . includes ( event . data . message . source . callId ) ) {
const result = event . data . message . content [ 0 ]
2026-07-27 16:50:56 +08:00
return [ {
type : event . type ,
2026-07-29 16:40:40 +08:00
callId : event.data.message.source.callId ,
isError : result.isError ,
text : result.content.map ( block = > block . type === 'text' ? block . text : '' ) . join ( '\n' )
2026-07-27 16:50:56 +08:00
. replaceAll ( root , '{{cwd}}' ) ,
} ]
}
return [ ]
} )
expect ( transcript ) . toMatchInlineSnapshot ( `
[
{
"callId": "write-skill",
"isError": false,
"text": "<path>{{cwd}}/.agents/skills/hot-skill/SKILL.md</path>
<type>file</type>
<content>
Created file
</content>",
"type": "tool/result",
},
{
"source": {
"kind": "plugin",
2026-07-29 16:40:40 +08:00
"plugin": "dsh-tool-skill",
2026-07-27 16:50:56 +08:00
},
"text": "<system-reminder>
2026-07-28 01:10:33 +08:00
A skill is a reusable set of task-specific instructions. The following skills are available in this session:
2026-07-27 16:50:56 +08:00
<available_skills>
- \` hot-skill \` : Hot-added skill
</available_skills>
2026-07-28 01:10:33 +08:00
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.
2026-07-27 16:50:56 +08:00
</system-reminder>",
"type": "user/message",
},
{
"callId": "load-skill",
"isError": false,
"text": "<skill_content name="hot-skill">
<skill_resources>
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.
</skill_resources>
<skill_instructions>
Use the freshly loaded body.
</skill_instructions>
</skill_content>",
"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 } )
}
} )
2026-07-12 15:41:42 +08:00
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' )
2026-07-16 16:45:52 +08:00
const ctx = await mount ( {
2026-07-12 15:41:42 +08:00
dshHome : home ,
2026-07-17 18:35:48 +08:00
workspaceContext : false ,
2026-07-12 15:41:42 +08:00
skills : { local : { agentsHome } } ,
2026-07-16 16:45:52 +08:00
} , true )
2026-07-12 15:41:42 +08:00
expect ( ( await ctx . skills . list ( ) ) . map ( skill = > skill . name ) ) . toEqual ( [ 'shared-skill' ] )
const execution : ToolExecution = {
2026-07-19 23:38:54 +08:00
signal : testToolSignal ,
2026-07-13 16:18:44 +08:00
token : Symbol ( 'agent-core-dsh-home-test' ) as ToolExecution [ 'token' ] ,
2026-07-12 15:41:42 +08:00
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' ,
2026-07-17 18:35:48 +08:00
workspaceContext : false ,
2026-07-12 15:41:42 +08:00
skills : { local : { dshHome : '/nested-dsh-home' } } ,
} )
2026-07-19 18:38:35 +08:00
} ) . toThrow ( 'agent-spine-demo: dshHome and skills.local.dshHome must resolve to the same directory' )
2026-07-12 15:41:42 +08:00
} )
2026-08-05 16:02:58 +08:00
it ( 'delivers workspace instructions ahead of the first-step skill catalog' , async ( ) = > {
2026-07-15 17:04:16 +08:00
const root = await mkdtemp ( join ( tmpdir ( ) , 'dsh-agent-spine-demo-prefix-order-' ) )
2026-07-11 23:02:24 +08:00
try {
await mkdir ( join ( root , '.git' ) , { recursive : true } )
await writeFile ( join ( root , 'AGENTS.md' ) , 'workspace rule before skills' )
2026-08-05 16:02:58 +08:00
const adapter = new MockAdapter ( [ textResponse ( 'first' ) ] )
2026-07-12 12:09:35 +08:00
const ctx = await mount ( { workspaceContext : { maxBytes : 65536 } } )
2026-07-11 23:02:24 +08:00
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' ,
} )
2026-07-13 14:37:32 +08:00
const handle = await ctx . agents . create ( {
2026-07-11 23:02:24 +08:00
sessionId : SessionId ( 'prefix-order-session' ) ,
meta : { cwd : root } ,
2026-07-17 21:56:10 +08:00
agentOptions : { provider : 'mock' , model : 'mock' } ,
2026-07-11 23:02:24 +08:00
} )
2026-07-28 13:55:59 +08:00
handle . agent . followup ( createUserMessage ( { content : [ { type : 'text' , text : 'hi' } ] , source : { kind : 'user' } } ) )
2026-07-18 12:54:45 +08:00
await waitForIdle ( ctx , handle . agent )
2026-07-11 23:02:24 +08:00
2026-08-05 16:02:58 +08:00
expect ( adapter . requests ) . toHaveLength ( 1 )
const workspaceIndex = adapter . requests [ 0 ] ! . messages . findIndex (
2026-07-31 19:40:59 +08:00
message = > messageText ( message ) . includes ( 'workspace rule before skills' ) ,
)
2026-08-05 16:02:58 +08:00
const catalogIndex = adapter . requests [ 0 ] ! . messages . findIndex (
message = > messageText ( message ) . includes ( 'prefix-order-skill' ) ,
)
2026-07-31 19:40:59 +08:00
expect ( workspaceIndex ) . toBeGreaterThanOrEqual ( 0 )
2026-08-05 16:02:58 +08:00
expect ( catalogIndex ) . toBeGreaterThanOrEqual ( 0 )
expect ( workspaceIndex ) . toBeLessThan ( catalogIndex )
2026-07-11 23:02:24 +08:00
await handle . dispose ( )
await ctx . fiber . dispose ( )
} finally {
await rm ( root , { recursive : true , force : true } )
}
} )
2026-07-12 16:00:56 +08:00
it ( 'forwards its bundled tool configs to tool-bash and tool-tasks' , async ( ) = > {
const ctx = await mount ( {
2026-07-15 22:27:02 +08:00
workspaceContext : false ,
2026-07-12 16:00:56 +08:00
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 < string , unknown > } ) . 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 ( {
2026-07-19 23:38:54 +08:00
signal : testToolSignal ,
2026-07-12 16:00:56 +08:00
callId : CallId ( 'task-config-forwarding' ) ,
name : 'task_output' ,
arguments : { task_id : id , wait : true } ,
} )
2026-07-19 23:38:54 +08:00
expect ( wait ) . toHaveBeenCalledWith ( id , 7 , undefined , testToolSignal )
2026-07-12 16:00:56 +08:00
await ctx . fiber . dispose ( )
} )
2026-07-17 17:29:38 +08:00
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 ( )
} )
2026-07-29 14:12:27 +08:00
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 ( )
} )
2026-07-15 22:27:02 +08:00
it ( 'picks shared spine config without leaking front-door fields' , ( ) = > {
const appConfig = {
model : 'front-door-only' ,
2026-07-29 14:12:27 +08:00
includeHarnessIdentity : false ,
2026-07-15 22:27:02 +08:00
persona : 'You are merged.' ,
toolOrder : [ 'zulu' ] ,
tools : { mode : 'native' as const } ,
2026-07-17 18:35:48 +08:00
dshHome : '/tmp/dsh-home' ,
2026-07-21 14:09:17 +08:00
sessionTitle : { fallbackMaxWords : 3 , fallbackMaxBytes : 24 , maxTitleBytes : 60 } ,
2026-07-15 22:27:02 +08:00
workspaceContext : false as const ,
2026-07-17 17:29:38 +08:00
skills : { enabled : false } ,
2026-07-15 22:27:02 +08:00
toolBash : { enableRunInBackground : false } ,
2026-07-17 17:29:38 +08:00
toolTasks : false as const ,
2026-07-19 19:19:57 +08:00
invariants : { enabled : false } ,
2026-07-15 22:27:02 +08:00
}
expect ( agentCore . pickSpineConfig ( appConfig ) ) . toEqual ( {
2026-07-29 14:12:27 +08:00
includeHarnessIdentity : appConfig.includeHarnessIdentity ,
2026-07-15 22:27:02 +08:00
persona : appConfig.persona ,
toolOrder : appConfig.toolOrder ,
tools : appConfig.tools ,
2026-07-17 18:35:48 +08:00
dshHome : appConfig.dshHome ,
2026-07-21 14:09:17 +08:00
sessionTitle : appConfig.sessionTitle ,
2026-07-15 22:27:02 +08:00
workspaceContext : false ,
2026-07-17 17:29:38 +08:00
skills : appConfig.skills ,
2026-07-15 22:27:02 +08:00
toolBash : appConfig.toolBash ,
toolTasks : appConfig.toolTasks ,
2026-07-19 19:19:57 +08:00
invariants : appConfig.invariants ,
2026-07-15 22:27:02 +08:00
} )
expect ( agentCore . pickSpineConfig ( { workspaceContext : false } ) ) . toEqual ( { workspaceContext : false } )
} )
2026-07-05 21:46:38 +08:00
it ( 'uses the default skill config when apply is called directly without skills' , async ( ) = > {
await withIsolatedSkillHomes ( async ( ) = > {
const ctx = new Context ( )
2026-07-12 12:09:35 +08:00
agentCore . apply ( ctx , { agents : [ ] , workspaceContext : false } )
2026-07-05 21:46:38 +08:00
await new Promise ( resolve = > setTimeout ( resolve , 50 ) )
expect ( ctx . skills ) . toBeDefined ( )
2026-07-08 15:50:38 +08:00
expect ( await ctx . skills . list ( ) ) . toEqual ( [ ] )
2026-07-05 21:46:38 +08:00
await ctx . fiber . dispose ( )
} )
} )
2026-07-07 11:03:11 +08:00
it ( 'forwards toolOrder to the system-prompt assembly' , async ( ) = > {
2026-07-12 12:09:35 +08:00
const ctx = await mount ( { toolOrder : [ 'zulu' , TOOL_ORDER_REST ] , workspaceContext : false } )
2026-07-07 11:03:11 +08:00
// 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 : { } ,
2026-07-21 03:08:35 +08:00
output : { schema : { type : 'null' } , render : ( ) = > [ ] } ,
execute : async ( ) = > null ,
2026-07-07 11:03:11 +08:00
} )
}
const assembly = await ctx . get ( 'systemPrompt' ) ! . assemble ( )
2026-07-11 23:04:27 +08:00
expect ( assembly . tools . map ( tool = > tool . name ) ) . toEqual ( [ 'zulu' , 'alpha' , 'skill' , 'task_kill' , 'task_list' , 'task_output' ] )
2026-07-07 11:03:11 +08:00
await ctx . fiber . dispose ( )
} )
2026-07-10 14:32:44 +08:00
it ( 'supports direct apply with workspace instructions disabled and no forwarded agents' , async ( ) = > {
2026-06-29 10:58:43 +08:00
const ctx = new Context ( )
2026-07-10 14:32:44 +08:00
agentCore . apply ( ctx , { workspaceContext : false } )
2026-06-29 10:58:43 +08:00
await new Promise ( resolve = > setTimeout ( resolve , 50 ) )
expect ( ctx . get ( 'agents' ) ? . list ( ) ) . toEqual ( [ ] )
expect ( ctx . get ( 'systemPrompt' ) ) . toBeDefined ( )
await ctx . fiber . dispose ( )
} )
2026-06-21 12:03:44 +08:00
it ( 're-exports the loop config schema as its own' , ( ) = > {
expect ( agentCore . Config ) . toBeDefined ( )
2026-07-15 16:05:58 +08:00
expect ( agentCore . name ) . toBe ( 'agent-spine-demo' )
2026-06-21 12:03:44 +08:00
} )
2026-06-21 12:49:59 +08:00
it ( 'has the namespace-plugin export shape (no stray default) so the Loader keeps name/Config/apply' , ( ) = > {
2026-07-13 23:27:00 +08:00
// 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.
2026-06-21 12:49:59 +08:00
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 < string , unknown >
expect ( unwrapped ) . toBe ( agentCore )
2026-07-15 16:05:58 +08:00
expect ( unwrapped . name ) . toBe ( 'agent-spine-demo' )
2026-06-21 12:49:59 +08:00
expect ( unwrapped . Config ) . toBeDefined ( )
expect ( typeof unwrapped . apply ) . toBe ( 'function' )
} )
2026-07-19 19:19:57 +08:00
2026-07-21 00:25:38 +08:00
it ( 'keeps each standard-spine invariant companion loadable through the real Loader unwrap path' , ( ) = > {
2026-07-19 19:19:57 +08:00
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 < string , unknown >
expect ( unwrapped ) . toBe ( companion )
expect ( typeof unwrapped . name ) . toBe ( 'string' )
expect ( unwrapped . inject ) . toContain ( 'invariants' )
expect ( typeof unwrapped . apply ) . toBe ( 'function' )
}
} )
2026-06-21 12:03:44 +08:00
} )