Files
deepseek-harness/packages/ui/tui/tests/harness.ts
T
Tianyi Cui 0f8d0082e4 fix(tui): stabilize Windows terminal snapshots
Treat an absolute path.relative() result as a cross-volume path instead of incorrectly abbreviating it beneath the user's home directory.

Allow embeddings to project a logical footer cwd without changing the operational session cwd. The recorded-session harness now uses a POSIX-shaped display alias for both the footer and filesystem result paths, preserving the existing pre-normalization layout width on every host.

Keep runtime-provided labels behind terminal-control escaping, cover that boundary, and document the embedding contract.
2026-07-19 13:20:59 +08:00

137 lines
4.2 KiB
TypeScript

import { Context } from 'cordis'
import type { Terminal } from '@earendil-works/pi-tui'
import AgentRegistry, { type Agent, type AgentStatus } from '@deepseek-ai/dsh-agent'
import type { ContentBlock } from '@deepseek-ai/dsh-llm'
import SessionStore, { SessionId, type Session } from '@deepseek-ai/dsh-session'
import type { ToolDefinition } from '@deepseek-ai/dsh-tools'
import UserInteractionService from '@deepseek-ai/dsh-user-interaction'
import { createTuiChat, type Config, type TuiRuntime } from '../src/index.ts'
interface FakeAgent extends Agent {
status: AgentStatus
sent: ContentBlock[][]
steered: ContentBlock[][]
cancelled: string[]
}
export interface TuiHarnessOptions {
status?: AgentStatus
config?: Config
tools?: Record<string, ToolDefinition>
configureContext?: (ctx: Context) => Promise<void>
beforeMount?: (session: Session) => void
cwd?: string | null
formatCwd?: TuiRuntime['formatCwd']
}
export interface TuiHarness<TerminalType extends Terminal, Exit extends (code: number) => void> {
ctx: Context
session: Session
agent: FakeAgent
terminal: TerminalType
exit: Exit
controller: ReturnType<typeof createTuiChat>
}
/**
* Compose the production TUI around an in-memory session and controllable agent.
* @param terminal - Terminal boundary driven by the test.
* @param exit - Process-exit observer.
* @param options - Initial session, agent, tool, and TUI configuration.
* @returns The mounted TUI and every boundary the test may drive or inspect.
*/
export async function createTuiTestHarness<TerminalType extends Terminal, Exit extends (code: number) => void>(
terminal: TerminalType,
exit: Exit,
options: TuiHarnessOptions = {},
): Promise<TuiHarness<TerminalType, Exit>> {
const ctx = new Context()
await ctx.plugin(SessionStore)
await ctx.plugin(AgentRegistry)
await ctx.plugin(UserInteractionService)
if (options.configureContext === undefined) {
const tools = options.tools ?? {}
ctx.provide('tools', {
get(name: string) {
return tools[name]
},
} as never)
} else {
await options.configureContext(ctx)
}
const sessionId = SessionId('main-session')
const session = ctx.sessions.create(
sessionId,
options.cwd === null ? undefined : { meta: { cwd: options.cwd ?? '/workspace' } },
)
options.beforeMount?.(session)
const sent: ContentBlock[][] = []
const steered: ContentBlock[][] = []
const cancelled: string[] = []
const agent: FakeAgent = {
id: sessionId,
options: { model: 'deepseek-v4-flash' },
session,
status: options.status ?? 'idle',
ctx,
sent,
steered,
cancelled,
send(content) {
sent.push(content)
},
steer(content) {
steered.push(content)
},
inject() {},
cancel(reason) {
cancelled.push(reason ?? '')
},
whenIdle() {
return Promise.resolve()
},
}
ctx.agents.register(agent)
const controller = createTuiChat(ctx, Object.assign({
welcome: 'Coding agent ready.',
sessionId,
color: false,
}, options.config), {
terminal,
exit,
...(options.formatCwd === undefined ? {} : { formatCwd: options.formatCwd }),
})
return { ctx, session, agent, terminal, exit, controller }
}
/** Dispose the mounted TUI before its owning Cordis context. */
export async function disposeTuiTestHarness(
setup: Pick<TuiHarness<Terminal, (code: number) => void>, 'controller' | 'ctx'>,
): Promise<void> {
await setup.controller.dispose()
await setup.ctx.fiber.dispose()
}
/** Append a production-shaped user message to the active session surface. */
export function appendUser(session: Session, text: string): void {
session.append('user/message', {
content: [{ type: 'text', text }],
source: { kind: 'user' },
}, { surfaceOp: 'append' })
}
/** Append a production-shaped assistant message to the active session surface. */
export function appendAssistant(
session: Session,
content: ContentBlock[],
usage?: { inputTokens: number; outputTokens: number },
): void {
session.append('assistant/message', {
turn: 1,
step: 0,
provenance: { provider: 'mock', model: 'deepseek-v4-flash' },
content,
...usage === undefined ? {} : { usage },
}, { surfaceOp: 'append' })
}