Merge origin/master into feat/tui-package
Master unifies every live agent with its exact SessionId and moves declarative startup failures to agent-loop/config-start-failed. Keeping the branch’s AgentId label binding would let the TUI target the wrong lifecycle after reload and would miss asynchronous resume failures. Resolve that contract migration by giving the selected terminal front door the same generated or resumed SessionId as agent-core, mounting the front door first, and entering fullscreen only after the matching root appears. Refresh the source-derived catalogs and keyless terminal goldens so Code Mode, workflow, Cordis-tool, and transient UI scenarios all exercise the merged identity model.
This commit is contained in:
@@ -4,15 +4,15 @@ import { join } from 'node:path'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { Context } from 'cordis'
|
||||
import Loader from '@cordisjs/plugin-loader'
|
||||
import { AgentId, agentEvents, type Agent } from '@deepseek-ai/dsh-agent'
|
||||
import { agentEvents, type Agent } from '@deepseek-ai/dsh-agent'
|
||||
|
||||
import type { Message } from '@deepseek-ai/dsh-llm'
|
||||
import { TOOL_ORDER_REST } from '@deepseek-ai/dsh-system-prompt'
|
||||
import * as stdioAgent from '../src/index.ts'
|
||||
|
||||
/**
|
||||
* Unit coverage for app composition and config forwarding: pre-created main agent,
|
||||
* agent-spine-demo spine, JSONL backend, and adaptive terminal UI with readline logging.
|
||||
* HMR is a Loader-only leaf concern covered by the
|
||||
* agent-spine-demo spine, JSONL backend, and adaptive terminal UI. HMR is a Loader-only leaf concern covered by the
|
||||
* keyless echo smoke; this tier pins the export shape because an inject-less app could otherwise
|
||||
* survive namespace collapse while silently losing its schema.
|
||||
*/
|
||||
@@ -74,7 +74,7 @@ describe('dsh-stdio-demo app', () => {
|
||||
expect(() => stdioAgent.resolveTerminalMode({ mode: 'tui' }, false)).toThrow('requires both stdin and stdout')
|
||||
})
|
||||
|
||||
it('composes only the selected terminal package and keeps TUI settings in dsh-tui', () => {
|
||||
it('binds only the selected terminal package to the app-owned exact session identity', () => {
|
||||
const calls: Array<{ name: string; config: unknown }> = []
|
||||
const ctx = {
|
||||
plugin(plugin: { name?: string }, config?: unknown) {
|
||||
@@ -92,18 +92,32 @@ describe('dsh-stdio-demo app', () => {
|
||||
expect(calls.map(call => call.name)).toContain('ui-tui')
|
||||
expect(calls.map(call => call.name)).not.toContain('ui-stdio')
|
||||
expect(calls.map(call => call.name)).not.toContain('ConsoleExporter')
|
||||
expect(calls.find(call => call.name === 'ui-tui')?.config).toMatchObject({
|
||||
agent: 'main', welcome: 'TUI ready', color: false, maxToolOutputLines: 3,
|
||||
})
|
||||
const tuiConfig = calls.find(call => call.name === 'ui-tui')?.config as { sessionId: string }
|
||||
expect(tuiConfig).toMatchObject({ welcome: 'TUI ready', color: false, maxToolOutputLines: 3 })
|
||||
expect(tuiConfig.sessionId).toMatch(/^main-session-/)
|
||||
const spineConfig = calls.find(call => call.name === 'agent-spine-demo')?.config as {
|
||||
agents: Array<{ id: string; sessionId?: string; resumeSessionId?: string }>
|
||||
}
|
||||
expect(spineConfig.agents[0]).toMatchObject({ id: 'main', sessionId: tuiConfig.sessionId })
|
||||
|
||||
calls.length = 0
|
||||
stdioAgent.composeTerminalApp(ctx, { provider: 'mock', model: 'mock', workspaceContext: false, ui: { mode: 'tui' } }, true)
|
||||
stdioAgent.composeTerminalApp(ctx, {
|
||||
provider: 'mock',
|
||||
model: 'mock',
|
||||
resumeSessionId: 'persisted-session',
|
||||
workspaceContext: false,
|
||||
ui: { mode: 'tui' },
|
||||
}, true)
|
||||
expect(calls.find(call => call.name === 'ui-tui')?.config).toMatchObject({
|
||||
agent: 'main', welcome: 'ready.',
|
||||
sessionId: 'persisted-session', welcome: 'ready.',
|
||||
})
|
||||
expect((calls.find(call => call.name === 'agent-spine-demo')?.config as typeof spineConfig).agents[0])
|
||||
.toMatchObject({ id: 'main', resumeSessionId: 'persisted-session' })
|
||||
|
||||
calls.length = 0
|
||||
stdioAgent.composeTerminalApp(ctx, { provider: 'mock', model: 'mock', workspaceContext: false, ui: { mode: 'readline' } }, false)
|
||||
stdioAgent.composeTerminalApp(ctx, {
|
||||
provider: 'mock', model: 'mock', workspaceContext: false, ui: { mode: 'readline' },
|
||||
}, false)
|
||||
expect(calls.map(call => call.name)).toContain('ui-stdio')
|
||||
expect(calls.map(call => call.name)).toContain('ConsoleExporter')
|
||||
expect(calls.map(call => call.name)).not.toContain('ui-tui')
|
||||
@@ -117,24 +131,44 @@ describe('dsh-stdio-demo app', () => {
|
||||
expect(ctx.get('sessionPersistence')).toBeDefined()
|
||||
expect(ctx.get('userInteraction')).toBeDefined()
|
||||
expect(ctx.get('tools')?.get('ask_user_question')).toBeDefined()
|
||||
// The pre-created `main` agent the UI drives.
|
||||
const agent = ctx.get('agents')?.get(AgentId('main'))
|
||||
// The sole pre-created agent the UI drives. `main` is its stable config
|
||||
// label; each fresh process mints a durable combined agent/session id.
|
||||
await expect.poll(() => ctx.get('agents')?.list()).toHaveLength(1)
|
||||
const agent = ctx.get('agents')?.list()[0]
|
||||
expect(agent).toBeDefined()
|
||||
expect(agent?.id).toBe(agent?.session.id)
|
||||
expect(agent?.id).toMatch(/^main-session-/)
|
||||
expect(agent?.session.header.cwd).toBe(process.cwd())
|
||||
await ctx.fiber.dispose()
|
||||
})
|
||||
|
||||
it('normalizes an empty resume id to a fresh exact app identity', async () => {
|
||||
const ctx = await mount({
|
||||
provider: 'mock',
|
||||
model: 'mock',
|
||||
resumeSessionId: '',
|
||||
persistenceRoot: '/tmp/dsh-stdio-agent-spec-empty-resume',
|
||||
skills: await isolatedSkillsConfig(),
|
||||
workspaceContext: false,
|
||||
})
|
||||
await expect.poll(() => ctx.get('agents')?.list()).toHaveLength(1)
|
||||
const agent = ctx.get('agents')?.list()[0]
|
||||
expect(agent?.id).toMatch(/^main-session-[0-9a-f-]{36}$/)
|
||||
expect(agent?.id).toBe(agent?.session.id)
|
||||
await ctx.fiber.dispose()
|
||||
})
|
||||
|
||||
it('defaults persistenceRoot and welcome when omitted', async () => {
|
||||
// Direct apply (NOT via ctx.plugin, which validates+defaults the config
|
||||
// first) so the runtime `?? './.sessions'` / `?? 'ready.'` fallbacks on
|
||||
// first) so the runtime `DEFAULT_PERSISTENCE_ROOT` / `DEFAULT_WELCOME` fallbacks on
|
||||
// apply()'s last two lines are the ones that fire — covering a
|
||||
// schema-bypassing direct-mount caller.
|
||||
const ctx = new Context()
|
||||
// No persona: covers the omitted-persona forwarding branch too.
|
||||
stdioAgent.apply(ctx, { provider: 'mock', model: 'mock', skills: await isolatedSkillsConfig(), workspaceContext: false })
|
||||
await new Promise(resolve => setTimeout(resolve, 80))
|
||||
await expect.poll(() => ctx.get('agents')?.list()).toHaveLength(1)
|
||||
expect(ctx.get('sessionPersistence')).toBeDefined()
|
||||
expect(ctx.get('agents')?.get(AgentId('main'))).toBeDefined()
|
||||
expect(ctx.get('agents')?.list()[0]?.id).toMatch(/^main-session-/)
|
||||
await ctx.fiber.dispose()
|
||||
})
|
||||
|
||||
@@ -146,7 +180,8 @@ describe('dsh-stdio-demo app', () => {
|
||||
persistenceRoot: '/tmp/dsh-stdio-demo-spec-workspace-context',
|
||||
workspaceContext: false,
|
||||
})
|
||||
expect(ctx.get('agents')?.get(AgentId('main'))).toBeDefined()
|
||||
await expect.poll(() => ctx.get('agents')?.list()).toHaveLength(1)
|
||||
expect(ctx.get('agents')?.list()[0]?.id).toMatch(/^main-session-/)
|
||||
await ctx.fiber.dispose()
|
||||
})
|
||||
|
||||
@@ -163,7 +198,7 @@ describe('dsh-stdio-demo app', () => {
|
||||
|
||||
it('forwards resumeSessionId onto the pre-created agent when set', async () => {
|
||||
// A resume id defers agent creation until persistence loads; with no backing
|
||||
// session the resume is contained + logged, so no `main` agent registers —
|
||||
// session the resume is contained + logged, so no agent registers —
|
||||
// the branch that maps resumeSessionId through is what this covers.
|
||||
const ctx = await mount({
|
||||
provider: 'mock',
|
||||
@@ -174,7 +209,7 @@ describe('dsh-stdio-demo app', () => {
|
||||
skills: await isolatedSkillsConfig(),
|
||||
workspaceContext: false,
|
||||
})
|
||||
expect(ctx.get('agents')?.get(AgentId('main'))).toBeUndefined()
|
||||
expect(ctx.get('agents')?.list()).toEqual([])
|
||||
await ctx.fiber.dispose()
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user