2026-07-19 01:31:55 +08:00
import { cp , mkdir , mkdtemp , readFile , readdir , rm , writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
2026-07-19 13:20:59 +08:00
import { basename , dirname , isAbsolute , join , relative , sep } from 'node:path'
2026-07-19 01:31:55 +08:00
import { fileURLToPath } from 'node:url'
2026-07-27 17:53:40 +08:00
import { afterAll , describe , expect , it , vi } from 'vitest'
2026-07-19 01:31:55 +08:00
import { Context } from 'cordis'
2026-07-28 23:46:01 +08:00
import { scrubRequestHeaders , tokenizeSessionFixtureCwd } from '@deepseek-ai/dsh-acp-snapshot'
2026-07-19 11:37:14 +08:00
import type { Agent } from '@deepseek-ai/dsh-agent'
2026-07-19 01:31:55 +08:00
import * as AgentCore from '@deepseek-ai/dsh-agent-spine-demo'
2026-07-30 14:57:27 +08:00
import { addHarnessSourceSection } from '@deepseek-ai/dsh-app-boot'
2026-07-19 01:31:55 +08:00
import { LocalBashExecutor } from '@deepseek-ai/dsh-bash-local'
2026-07-26 12:43:14 +08:00
import LocalSubprocessService from '@deepseek-ai/dsh-subprocess-local'
2026-07-19 01:31:55 +08:00
import WorkerCodeRuntime from '@deepseek-ai/dsh-code-runtime-worker'
2026-07-19 22:11:59 +08:00
import CommandService from '@deepseek-ai/dsh-commands'
2026-07-30 17:40:25 +08:00
import * as CommandCompact from '@deepseek-ai/dsh-command-compact'
import { BasicCompactService } from '@deepseek-ai/dsh-compact-basic'
import type { SummarizationInput } from '@deepseek-ai/dsh-compact-basic/src/summarizer.ts'
2026-07-19 01:31:55 +08:00
import LocalFileSystem from '@deepseek-ai/dsh-fs-local'
import * as FsPolicy from '@deepseek-ai/dsh-fs-policy'
2026-07-30 17:40:25 +08:00
import { createUserMessage } from '@deepseek-ai/dsh-llm'
2026-07-19 01:31:55 +08:00
import * as ToolFs from '@deepseek-ai/dsh-tool-fs'
import * as LlmDeepSeek from '@deepseek-ai/dsh-llm-deepseek'
import { installLlmReplay , parseSessionLog } from '@deepseek-ai/dsh-llm-replay'
2026-07-22 16:57:23 +08:00
import PlanModeService from '@deepseek-ai/dsh-plan-mode'
2026-07-20 22:44:01 +08:00
import TokenMeterService from '@deepseek-ai/dsh-token-meter'
2026-07-26 23:44:52 +08:00
import { packChunkRuns , SessionId , type Session , type SessionEvent } from '@deepseek-ai/dsh-session'
2026-07-19 01:31:55 +08:00
import SubagentService from '@deepseek-ai/dsh-subagent'
import * as SubagentSpawn from '@deepseek-ai/dsh-subagent-spawn'
import * as ToolSubagent from '@deepseek-ai/dsh-tool-subagent'
import * as ToolCordis from '@deepseek-ai/dsh-tool-cordis'
import * as ToolTodo from '@deepseek-ai/dsh-tool-todo'
2026-07-20 00:51:19 +08:00
import * as ToolRalph from '@deepseek-ai/dsh-tool-ralph'
2026-07-19 01:31:55 +08:00
import * as ToolWorkflow from '@deepseek-ai/dsh-tool-workflow'
2026-07-27 17:53:40 +08:00
import { createTuiChat , FILE_REFERENCE_PROMPT , TuiPromptService } from '@deepseek-ai/dsh-tui'
2026-07-26 18:29:09 +08:00
import LocalSpillStore from '@deepseek-ai/dsh-spill-local'
import * as SpillPolicy from '@deepseek-ai/dsh-spill-policy'
2026-07-19 01:31:55 +08:00
import UserInteractionService from '@deepseek-ai/dsh-user-interaction'
import WorkerWorkflowEngine from '@deepseek-ai/dsh-workflow-workerthread'
import { HeadlessTerminal } from '../../../packages/ui/tui/tests/headless-terminal.ts'
const SNAPSHOTS_DIR = join ( dirname ( fileURLToPath ( import . meta . url ) ) , 'snapshots' )
2026-07-19 01:42:16 +08:00
// Keep pre-normalization layout widths identical across macOS and Linux.
const SNAPSHOT_TMP_ROOT = process . platform === 'win32' ? tmpdir ( ) : '/tmp'
2026-07-29 16:36:07 +08:00
const PROVIDERS = [ { id : 'deepseek-official' , models : [ { id : 'deepseek-v4-flash' , contextWindow : 128_000 } ] } ]
2026-07-19 01:31:55 +08:00
const UUID_RE = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi
type SnapshotMode = 'replay' | 'record' | 'refresh'
type Composition = 'native' | 'code' | 'advanced'
2026-07-29 23:17:07 +08:00
type ScenarioInteraction = 'skill-invocation-policy'
2026-07-19 01:31:55 +08:00
interface Scenario {
name : string
2026-07-30 17:40:25 +08:00
/** Replay fixture owned by an earlier scenario, for a derived presentation case. */
fixture? : string
2026-07-19 01:31:55 +08:00
composition : Composition
expectedTools : string [ ]
expectedEventCounts? : Record < string , number >
childSessions? : number
2026-07-22 14:32:35 +08:00
enterPlanMode? : boolean
2026-07-23 21:06:03 +08:00
leavePlanModeAfterFirstTurn? : boolean
2026-07-19 01:31:55 +08:00
recorded : boolean
seedWorkspace? : boolean
2026-07-30 14:57:27 +08:00
/** Add the launcher's model-visible DSH source checkout at this fixed path. */
harnessSourceRoot? : string
2026-07-30 15:35:21 +08:00
/** Replace the real `pwd` result with a portable fixed-length workspace path. */
normalizePwdResult? : boolean
2026-07-22 10:55:20 +08:00
/**
2026-07-29 21:59:44 +08:00
* Load the opt-in `todo_write` tool for this scenario. The shipped TUI
2026-07-22 10:55:20 +08:00
* config omits it, so only the todo-plan scenario (the enabled-path proof)
* mounts it; the rest cover the default, todo-free composition.
*/
enableTodo? : boolean
2026-07-26 18:29:09 +08:00
/**
* Mount the spill stack (local backend + policy) with this inline cap, as the
* shipped configs do. The dispatch-spill scenario proves the durable
* `tool/code-dispatch` copy of an oversized sub-result is bounded to a
* preview + locator while the program value stays whole.
*/
spillMaxInlineBytes? : number
2026-07-29 23:17:07 +08:00
/** Run scenario-specific terminal input instead of replaying recorded user prompts. */
interaction? : ScenarioInteraction
2026-07-30 17:40:25 +08:00
/**
* Mount a deterministic compaction backend plus `/compact`, then run the
* human command with a held summary while a prompt and injected context
* arrive. Proves queued input waits for the standalone bracket's durability
* checkpoint instead of racing the replacement.
*/
manualCompact? : boolean
2026-07-19 01:31:55 +08:00
}
const SCENARIOS : Scenario [ ] = [
{
name : 'multi-turn-conversation' ,
composition : 'native' ,
expectedTools : [ ] ,
2026-07-23 21:06:03 +08:00
expectedEventCounts : { 'plan/mode' : 2 } ,
2026-07-22 14:32:35 +08:00
enterPlanMode : true ,
2026-07-23 21:06:03 +08:00
leavePlanModeAfterFirstTurn : true ,
2026-07-19 01:31:55 +08:00
recorded : true ,
} ,
2026-07-30 17:40:25 +08:00
{
name : 'queued-manual-compact' ,
fixture : 'multi-turn-conversation' ,
composition : 'native' ,
expectedTools : [ ] ,
recorded : false ,
manualCompact : true ,
} ,
2026-07-19 01:31:55 +08:00
{
name : 'todo-plan' ,
composition : 'native' ,
expectedTools : [ 'todo_write' ] ,
expectedEventCounts : { 'todo/write' : 1 } ,
recorded : true ,
2026-07-22 10:55:20 +08:00
enableTodo : true ,
2026-07-19 01:31:55 +08:00
} ,
{
name : 'bash-terminal-card' ,
composition : 'native' ,
expectedTools : [ 'bash' ] ,
recorded : true ,
} ,
2026-07-30 14:57:27 +08:00
{
name : 'source-checkout-workdir' ,
composition : 'native' ,
expectedTools : [ 'bash' ] ,
recorded : true ,
harnessSourceRoot : '/opt/dsh-source' ,
2026-07-30 15:35:21 +08:00
normalizePwdResult : true ,
2026-07-30 14:57:27 +08:00
} ,
2026-07-19 01:31:55 +08:00
{
name : 'parallel-file-reads' ,
composition : 'native' ,
expectedTools : [ 'read' , 'read' ] ,
recorded : true ,
seedWorkspace : true ,
} ,
2026-07-29 21:58:21 +08:00
{
name : 'skill-invocation-policy' ,
composition : 'native' ,
expectedTools : [ ] ,
recorded : false ,
seedWorkspace : true ,
2026-07-29 23:17:07 +08:00
interaction : 'skill-invocation-policy' ,
2026-07-29 21:58:21 +08:00
} ,
2026-07-19 01:31:55 +08:00
{
name : 'code-mode' ,
composition : 'code' ,
expectedTools : [ 'run_code' ] ,
expectedEventCounts : { 'tool/code-dispatch' : 2 } ,
recorded : true ,
} ,
2026-07-26 18:29:09 +08:00
{
name : 'code-mode-dispatch-spill' ,
composition : 'code' ,
expectedTools : [ 'run_code' ] ,
expectedEventCounts : { 'tool/code-dispatch-start' : 1 , 'tool/code-dispatch' : 1 } ,
recorded : true ,
spillMaxInlineBytes : 600 ,
} ,
2026-07-19 01:31:55 +08:00
{
name : 'dynamic-workflow' ,
composition : 'native' ,
expectedTools : [ 'workflow' ] ,
childSessions : 1 ,
recorded : true ,
} ,
{
name : 'cordis-dynamic-toolchain' ,
composition : 'advanced' ,
expectedTools : [ 'cordis_mount' , 'run_code' , 'subagent' , 'workflow' , 'cordis_unmount' ] ,
expectedEventCounts : { 'tool/code-dispatch' : 1 } ,
childSessions : 2 ,
recorded : false ,
} ,
]
function snapshotModeFromEnv ( value : string | undefined ) : SnapshotMode {
if ( value === undefined || value === '' || value === 'replay' ) return 'replay'
if ( value === 'record' || value === 'refresh' ) return value
throw new Error ( ` DSH_SNAPSHOT must be replay, record, or refresh; got ${ JSON . stringify ( value ) } ` )
}
const MODE = snapshotModeFromEnv ( process . env . DSH_SNAPSHOT )
const observedScenarios = new Set < string > ( )
2026-07-30 17:40:25 +08:00
const workerState = Reflect . get ( globalThis , '__vitest_worker__' ) as
| { readonly config ? : { readonly testNamePattern? : RegExp } }
| undefined
// Worker argv omits the parent CLI's `-t`; the serialized runner config is the
// authoritative distinction between a focused replay and the full suite.
const TEST_NAME_FILTERED = workerState ? . config ? . testNamePattern !== undefined
/**
* Deterministic keyless summary that pauses so the scenario can submit a real
* prompt and inject context while manual compaction holds turn admission.
*/
class DeferredSnapshotCompactService extends BasicCompactService {
readonly summaryStarted = Promise . withResolvers < undefined > ( )
readonly releaseSummary = Promise . withResolvers < undefined > ( )
override async summarize (
_input : SummarizationInput ,
_agent : Agent ,
signal? : AbortSignal ,
) : Promise < { summary : [ { type : 'text' ; text : string } ] ; provider : string ; model : string } > {
this . summaryStarted . resolve ( undefined )
await this . releaseSummary . promise
signal ? . throwIfAborted ( )
return {
summary : [ { type : 'text' , text : 'Keyless manual compaction checkpoint.' } ] ,
provider : 'snapshot' ,
model : 'snapshot-compactor' ,
}
}
}
/** Seed between-turn model-visible history without inventing a loop execution. */
function seedCompactableHistory ( agent : Agent ) : void {
agent . inject ( createUserMessage ( {
content : [ { type : 'text' , text : 'Older snapshot context. ' . repeat ( 60 ) } ] ,
source : { kind : 'plugin' , plugin : 'snapshot-seed' } ,
} ) )
}
2026-07-19 01:31:55 +08:00
2026-07-19 13:20:59 +08:00
function snapshotDisplayPath ( displayPath : string , cwd : string , displayCwd : string ) : string {
const rel = relative ( cwd , displayPath )
if ( rel === '' ) return displayCwd
if ( isAbsolute ( rel ) || rel === '..' || rel . startsWith ( ` .. ${ sep } ` ) ) return displayPath
return ` ${ displayCwd } / ${ rel . split ( sep ) . join ( '/' ) } `
}
2026-07-19 01:31:55 +08:00
function scenarioDir ( scenario : Scenario ) : string {
return join ( SNAPSHOTS_DIR , scenario . name )
}
2026-07-30 17:40:25 +08:00
/** Directory owning the replay fixture: the scenario's own, or the one it derives from. */
function fixtureDir ( scenario : Scenario ) : string {
return join ( SNAPSHOTS_DIR , scenario . fixture ? ? scenario . name )
}
2026-07-19 01:31:55 +08:00
function childFixturePaths ( scenario : Scenario ) : string [ ] {
return Array . from (
{ length : scenario.childSessions ? ? 0 } ,
2026-07-30 17:40:25 +08:00
( _ , index ) = > join ( fixtureDir ( scenario ) , ` session. ${ index + 1 } .jsonl ` ) ,
2026-07-19 01:31:55 +08:00
)
}
function userPrompts ( rawLog : string ) : string [ ] {
return parseSessionLog ( rawLog ) . flatMap ( ( event ) = > {
if ( event . type !== 'user/message' || event . data . source . kind !== 'user' ) return [ ]
const text = event . data . content
. filter ( block = > block . type === 'text' )
. map ( block = > block . text )
. join ( '' )
return text . length > 0 ? [ text ] : [ ]
} )
}
function rawSessionLog ( session : Session ) : string {
return [
JSON . stringify ( { type : 'session' , . . . session . header } ) ,
2026-07-26 23:44:52 +08:00
. . . packChunkRuns ( session . events ) . map ( record = > JSON . stringify ( record ) ) ,
2026-07-19 01:31:55 +08:00
'' ,
] . join ( '\n' )
}
2026-07-30 14:57:27 +08:00
async function materializeFixtureCwd ( fixtureFile : string , cwd : string , replayRoot : string ) : Promise < string > {
const realized = join ( replayRoot , basename ( fixtureFile ) )
await writeFile ( realized , ( await readFile ( fixtureFile , 'utf8' ) ) . split ( '{{cwd}}' ) . join ( cwd ) )
return realized
}
2026-07-19 13:20:59 +08:00
function normalizeTerminalSnapshot ( snapshot : string , cwd : string , displayCwd : string ) : string {
2026-07-19 01:31:55 +08:00
return snapshot
. split ( ` /private ${ cwd } ` ) . join ( '/workspace/project' )
2026-07-19 13:20:59 +08:00
. split ( displayCwd ) . join ( '/workspace/project' )
2026-07-19 01:31:55 +08:00
. split ( cwd ) . join ( '/workspace/project' )
. replace ( UUID_RE , '{{uuid}}' )
}
async function settleTerminal ( terminal : HeadlessTerminal ) : Promise < void > {
let stable = 0
for ( let attempt = 0 ; attempt < 20 && stable < 3 ; attempt ++ ) {
const before = terminal . frames
await new Promise ( resolve = > setTimeout ( resolve , 10 ) )
await terminal . flush ( )
stable = terminal . frames === before ? stable + 1 : 0
}
if ( stable < 3 ) throw new Error ( 'TUI frames did not quiesce within 200ms' )
}
2026-07-30 17:40:25 +08:00
/** Bound deterministic in-process coordination waits with actionable state. */
async function snapshotDeadline < T > (
operation : Promise < T > ,
detail : ( ) = > string ,
) : Promise < T > {
let timer : ReturnType < typeof setTimeout > | undefined
try {
return await Promise . race ( [
operation ,
new Promise < never > ( ( _resolve , reject ) = > {
timer = setTimeout ( ( ) = > { reject ( new Error ( detail ( ) ) ) } , 5 _000 )
} ) ,
] )
} finally {
if ( timer !== undefined ) clearTimeout ( timer )
}
}
2026-07-19 01:31:55 +08:00
async function mountScenarioContext (
scenario : Scenario ,
cwd : string ,
2026-07-19 13:20:59 +08:00
displayCwd : string ,
2026-07-19 01:31:55 +08:00
fixtureFile : string ,
childFiles : string [ ] ,
2026-07-30 14:57:27 +08:00
replayRoot : string | undefined ,
2026-07-19 01:31:55 +08:00
) : Promise < Context > {
2026-07-19 13:20:59 +08:00
class SnapshotLocalFileSystem extends LocalFileSystem {
override async resolve (
path : string ,
opts ? : { cwd? : string ; signal? : AbortSignal } ,
) : Promise < Awaited < ReturnType < LocalFileSystem [ 'resolve' ] > > > {
const target = await super . resolve ( path , opts )
return { . . . target , displayPath : snapshotDisplayPath ( target . displayPath , cwd , displayCwd ) }
}
}
2026-07-19 01:31:55 +08:00
const ctx = new Context ( )
await ctx . plugin ( AgentCore , {
agents : [ ] ,
dshHome : join ( cwd , '.dsh' ) ,
workspaceContext : false ,
tools : { mode : scenario.composition === 'code' ? 'code' : scenario . composition === 'advanced' ? 'both' : 'native' } ,
skills : { local : { agentsHome : join ( cwd , '.agents' ) } } ,
} )
2026-07-30 14:57:27 +08:00
if ( scenario . harnessSourceRoot !== undefined ) addHarnessSourceSection ( ctx , scenario . harnessSourceRoot )
2026-07-20 22:44:01 +08:00
await ctx . plugin ( TokenMeterService )
2026-07-30 17:40:25 +08:00
if ( scenario . manualCompact === true ) {
await ctx . plugin ( DeferredSnapshotCompactService , { auto : false } )
}
2026-07-26 12:43:14 +08:00
await ctx . plugin ( LocalSubprocessService )
2026-07-19 01:31:55 +08:00
await ctx . plugin ( LocalBashExecutor , { cwd , timeoutMs : 30_000 } )
2026-07-19 13:20:59 +08:00
await ctx . plugin ( SnapshotLocalFileSystem , { cwd : '/' } )
2026-07-19 01:31:55 +08:00
await ctx . plugin ( FsPolicy )
await ctx . plugin ( ToolFs )
await ctx . plugin ( UserInteractionService )
2026-07-27 17:53:40 +08:00
await ctx . plugin ( TuiPromptService )
2026-07-22 10:55:20 +08:00
// todo_write is opt-in: only the todo-plan scenario mounts it, matching the shipped
// config that omits it. The other scenarios prove the default todo-free composition.
if ( scenario . enableTodo === true ) await ctx . plugin ( ToolTodo )
2026-07-19 01:31:55 +08:00
await ctx . plugin ( SubagentService )
await ctx . plugin ( SubagentSpawn , { providerName : 'spawn' } )
await ctx . plugin ( ToolSubagent , { provider : 'spawn' , toolName : 'subagent' , enableRunInBackground : false } )
await ctx . plugin ( WorkerWorkflowEngine , { provider : 'spawn' } )
await ctx . plugin ( ToolWorkflow )
2026-07-20 00:51:19 +08:00
await ctx . plugin ( ToolRalph )
2026-07-19 22:11:59 +08:00
await ctx . plugin ( CommandService )
2026-07-30 17:40:25 +08:00
if ( scenario . manualCompact === true ) await ctx . plugin ( CommandCompact )
2026-07-22 14:32:35 +08:00
if ( scenario . enterPlanMode === true ) {
2026-07-22 16:57:23 +08:00
await ctx . plugin ( PlanModeService , { section : 'Snapshot plan mode instructions.' } )
2026-07-22 14:32:35 +08:00
}
2026-07-19 01:31:55 +08:00
if ( scenario . composition === 'code' || scenario . composition === 'advanced' ) {
await ctx . plugin ( WorkerCodeRuntime , { } )
}
2026-07-26 18:29:09 +08:00
if ( scenario . spillMaxInlineBytes !== undefined ) {
await ctx . plugin ( LocalSpillStore , { root : join ( cwd , '.spill' ) } )
await ctx . plugin ( SpillPolicy , { maxInlineBytes : scenario.spillMaxInlineBytes } )
}
2026-07-19 01:31:55 +08:00
if ( scenario . composition === 'advanced' ) await ctx . plugin ( ToolCordis , { vmTimeoutMs : 5_000 } )
if ( MODE === 'record' && scenario . recorded ) {
await ctx . plugin ( LlmDeepSeek )
} else {
2026-07-30 14:57:27 +08:00
if ( replayRoot === undefined ) throw new Error ( 'replay mode requires an isolated fixture directory' )
// Recorded model text may name the generated cwd. Realize the portable token
// outside that cwd so tools see only the scenario workspace during replay.
const replayFile = await materializeFixtureCwd ( fixtureFile , cwd , replayRoot )
const replayChildFiles = await Promise . all ( childFiles . map ( file = > materializeFixtureCwd ( file , cwd , replayRoot ) ) )
installLlmReplay ( ctx , { file : replayFile , childFiles : replayChildFiles , providers : PROVIDERS } )
2026-07-19 01:31:55 +08:00
}
return ctx
}
interface ScenarioResult {
terminal : string
parent : Session
children : Session [ ]
workflowEvents : string [ ]
}
async function runScenario ( scenario : Scenario ) : Promise < ScenarioResult > {
2026-07-30 21:59:33 +08:00
const snapshotTime = new Date ( 2026 , 6 , 21 , 12 , 0 , 0 ) . getTime ( )
const clock = vi . spyOn ( Date , 'now' ) . mockReturnValue ( snapshotTime )
2026-07-30 17:40:25 +08:00
const fixtureFile = join ( fixtureDir ( scenario ) , 'session.jsonl' )
2026-07-19 01:31:55 +08:00
const childFiles = childFixturePaths ( scenario )
2026-07-29 21:58:21 +08:00
const prompts = userPrompts ( await readFile ( fixtureFile , 'utf8' ) )
2026-07-29 23:17:07 +08:00
if ( scenario . interaction === undefined ) {
2026-07-29 21:58:21 +08:00
expect ( prompts . length , ` ${ scenario . name } must carry at least one recorded user prompt ` ) . toBeGreaterThan ( 0 )
}
2026-07-19 01:31:55 +08:00
2026-07-19 01:42:16 +08:00
const cwd = await mkdtemp ( join ( SNAPSHOT_TMP_ROOT , ` dsh-tui-snapshot- ${ scenario . name } - ` ) )
2026-07-19 13:20:59 +08:00
const displayCwd = ` /tmp/ ${ basename ( cwd ) } `
2026-07-30 14:57:27 +08:00
let replayRoot : string | undefined
2026-07-19 01:31:55 +08:00
let ctx : Context | undefined
let controller : ReturnType < typeof createTuiChat > | undefined
const terminal = new HeadlessTerminal ( 100 , 36 )
try {
2026-07-30 14:57:27 +08:00
if ( ! ( MODE === 'record' && scenario . recorded ) ) {
replayRoot = await mkdtemp ( join ( SNAPSHOT_TMP_ROOT , ` dsh-tui-replay- ${ scenario . name } - ` ) )
}
2026-07-19 01:31:55 +08:00
if ( scenario . seedWorkspace === true ) {
2026-07-30 17:40:25 +08:00
const source = join ( fixtureDir ( scenario ) , 'workspace' )
2026-07-19 01:31:55 +08:00
await cp ( source , cwd , { recursive : true } )
}
2026-07-30 14:57:27 +08:00
ctx = await mountScenarioContext ( scenario , cwd , displayCwd , fixtureFile , childFiles , replayRoot )
2026-07-30 15:35:21 +08:00
if ( scenario . normalizePwdResult === true ) {
ctx . on ( 'tools/post-execute' , async ( exec , result , next ) = > {
const args = exec . arguments as { command? : unknown }
return exec . name === 'bash' && args . command === 'pwd' && ! result . isError
? { kind : 'accept' , content : [ { type : 'text' , text : '/workspace/project\n' } ] }
: next ( )
} )
}
2026-07-19 01:31:55 +08:00
const disposedSessions : Session [ ] = [ ]
ctx . on ( 'session/disposed' , ( session ) = > { disposedSessions . push ( session ) } )
const workflowEvents : string [ ] = [ ]
for ( const name of [ 'workflow/start' , 'workflow/phase' , 'workflow/agent-start' , 'workflow/agent-end' , 'workflow/end' ] as const ) {
ctx . on ( name , ( ) = > { workflowEvents . push ( name ) } )
}
const handle = await ctx . agents . create ( {
sessionId : SessionId ( 'main-session' ) ,
meta : { cwd } ,
2026-07-29 16:36:07 +08:00
agentOptions : { provider : 'deepseek-official' , model : 'deepseek-v4-flash' } ,
2026-07-19 01:31:55 +08:00
} )
const agent : Agent = handle . agent
2026-07-30 17:40:25 +08:00
if ( scenario . manualCompact === true ) seedCompactableHistory ( agent )
2026-07-19 01:31:55 +08:00
controller = createTuiChat ( ctx , {
2026-07-19 11:37:14 +08:00
sessionId : 'main-session' ,
2026-07-27 17:53:40 +08:00
theme : { color : true } ,
2026-07-19 01:31:55 +08:00
showReasoning : true ,
title : 'DSH TUI snapshot' ,
welcome : ` Recorded replay: ${ scenario . name } ` ,
maxToolOutputLines : 8 ,
2026-07-19 13:20:59 +08:00
} , {
terminal ,
exit : ( ) = > { } ,
formatCwd : ( ) = > displayCwd ,
} )
2026-07-19 01:31:55 +08:00
await settleTerminal ( terminal )
2026-07-29 23:17:07 +08:00
let interactionSnapshot : string | undefined
if ( scenario . interaction === 'skill-invocation-policy' ) {
2026-07-29 21:58:21 +08:00
terminal . send ( '/skill' )
await settleTerminal ( terminal )
const discovery = normalizeTerminalSnapshot (
await terminal . snapshot ( { includeScrollback : true } ) ,
cwd ,
displayCwd ,
)
expect ( discovery ) . toContain ( 'user-only-skill' )
expect ( discovery ) . not . toContain ( 'model-only-skill' )
terminal . send ( '\x03' )
await settleTerminal ( terminal )
2026-07-29 23:36:49 +08:00
const skillContext = ctx
2026-07-29 23:17:07 +08:00
const skillTurnEnded = new Promise < void > ( ( resolve ) = > {
2026-07-29 23:36:49 +08:00
const detach = skillContext . on ( 'session/event' , ( session , event ) = > {
2026-07-29 23:17:07 +08:00
if ( session !== agent . session || event . type !== 'turn/end' ) return
detach ( )
resolve ( )
} )
} )
terminal . send ( '/skill:user-only-skill' )
terminal . send ( '\r' )
await skillTurnEnded
await agent . whenIdle ( )
await settleTerminal ( terminal )
const loaded = normalizeTerminalSnapshot (
await terminal . snapshot ( { includeScrollback : true } ) ,
cwd ,
displayCwd ,
)
expect ( loaded ) . toContain ( 'USER-ONLY SKILL LOADED' )
2026-07-29 21:58:21 +08:00
terminal . send ( '/skill:model-only-skill' )
terminal . send ( '\r' )
await settleTerminal ( terminal )
const denied = normalizeTerminalSnapshot (
await terminal . snapshot ( { includeScrollback : true } ) ,
cwd ,
displayCwd ,
)
expect ( denied ) . toContain ( 'model-only-skill' )
expect ( denied ) . toContain ( 'not available for user invocation.' )
expect ( denied ) . not . toContain ( 'MODEL-ONLY BODY MUST NOT LOAD' )
2026-07-29 23:17:07 +08:00
interactionSnapshot = [
2026-07-29 21:58:21 +08:00
'=== skill autocomplete ===' ,
discovery ,
'' ,
2026-07-29 23:17:07 +08:00
'=== loaded exact invocation ===' ,
loaded ,
'' ,
2026-07-29 21:58:21 +08:00
'=== denied exact invocation ===' ,
denied ,
] . join ( '\n' )
}
2026-07-22 15:51:02 +08:00
let remainingPrompts = prompts
2026-07-30 17:40:25 +08:00
let queuedPrompt : string | undefined
let manualOrder : string [ ] | undefined
let manualCommandId : string | undefined
if ( scenario . manualCompact === true ) {
expect ( prompts . length , 'queued manual compaction needs a second replayed prompt' ) . toBeGreaterThanOrEqual ( 2 )
queuedPrompt = prompts . at ( - 1 )
remainingPrompts = prompts . slice ( 0 , - 1 )
}
2026-07-22 14:32:35 +08:00
if ( scenario . enterPlanMode === true ) {
2026-07-22 15:51:02 +08:00
const firstPrompt = prompts [ 0 ] !
terminal . send ( ` /plan ${ firstPrompt } ` )
2026-07-22 14:32:35 +08:00
terminal . send ( '\r' )
2026-07-22 15:51:02 +08:00
await agent . whenIdle ( )
2026-07-22 14:32:35 +08:00
await settleTerminal ( terminal )
2026-07-22 15:51:02 +08:00
remainingPrompts = prompts . slice ( 1 )
2026-07-22 14:32:35 +08:00
}
2026-07-23 21:06:03 +08:00
if ( scenario . leavePlanModeAfterFirstTurn === true ) {
terminal . send ( '/plan off' )
terminal . send ( '\r' )
await settleTerminal ( terminal )
}
2026-07-22 15:51:02 +08:00
for ( const prompt of remainingPrompts ) {
2026-07-30 17:40:25 +08:00
const admitted = agent . session . events . filter ( event = >
event . type === 'user/message' && event . data . source . kind === 'user' ) . length
2026-07-19 01:31:55 +08:00
terminal . send ( prompt )
terminal . send ( '\r' )
2026-07-30 17:40:25 +08:00
await terminal . flush ( )
await expect . poll ( ( ) = > agent . session . events . filter ( event = >
event . type === 'user/message' && event . data . source . kind === 'user' ) . length ) . toBe ( admitted + 1 )
2026-07-19 01:31:55 +08:00
await agent . whenIdle ( )
await settleTerminal ( terminal )
}
2026-07-30 17:40:25 +08:00
if ( scenario . manualCompact === true && queuedPrompt !== undefined ) {
terminal . send ( '/help' )
terminal . send ( '\r' )
await settleTerminal ( terminal )
expect ( await terminal . snapshot ( { includeScrollback : true } ) )
. toContain ( '/compact — Compact older conversation history' )
const compact = ctx . compact as DeferredSnapshotCompactService
const inbox : string [ ] = [ ]
manualOrder = [ ]
ctx . on ( 'agent/inbox/enqueue' , ( subject , item ) = > {
if ( subject === agent ) inbox . push ( ` enqueue: ${ item . placement } : ${ item . id } ` )
} )
ctx . on ( 'agent/inbox/dequeue' , ( subject , message ) = > {
if ( subject === agent ) inbox . push ( ` dequeue: ${ message . id } ` )
} )
ctx . on ( 'session/event' , ( session , event ) = > {
if ( session !== agent . session ) return
if ( event . type === 'command/run' && event . data . name === 'compact' ) {
manualCommandId = event . data . commandId
manualOrder ? . push ( 'command/run' )
}
if ( event . type === 'command/done' && event . data . commandId === manualCommandId ) {
manualOrder ? . push ( 'command/done' )
}
if ( event . type . startsWith ( 'compact/' ) ) manualOrder ? . push ( event . type )
if ( event . type === 'user/message'
&& event . data . source . kind === 'plugin'
&& event . data . source . plugin === 'compact' ) manualOrder ? . push ( 'checkpoint' )
if ( event . type === 'turn/start' ) manualOrder ? . push ( ` turn/start: ${ event . data . trigger . kind } ` )
} )
ctx . on ( 'session/flush' , ( session ) = > {
if ( session === agent . session ) manualOrder ? . push ( 'flush' )
} )
terminal . send ( '/compact' )
terminal . send ( '\r' )
await terminal . flush ( )
await snapshotDeadline ( compact . summaryStarted . promise , ( ) = >
` manual summary did not start; status= ${ agent . status } ; tail= ${
agent . session . events . slice ( - 8 ) . map ( event = > event . type ) . join ( ',' )
} ` )
2026-07-30 21:59:33 +08:00
clock . mockReturnValue ( snapshotTime + 1 _000 )
await settleTerminal ( terminal )
await expect . poll ( ( ) = > terminal . snapshot ( ) ) . toContain ( 'dsh ⊙' )
2026-07-31 15:27:10 +08:00
await expect . poll ( ( ) = > terminal . snapshot ( ) ) . toContain ( 'Context being compacted 1.0s' )
const liveCompaction = await terminal . snapshot ( )
expect ( liveCompaction . indexOf ( 'Context being compacted 1.0s' ) ) . toBeLessThan ( liveCompaction . indexOf ( 'dsh ⊙' ) )
2026-07-30 21:59:33 +08:00
clock . mockReturnValue ( snapshotTime )
2026-07-30 17:40:25 +08:00
// Real keystrokes: the prompt keeps its ordinary queue identity while
// admission is reserved, and an injection appends immediately.
terminal . send ( queuedPrompt )
terminal . send ( '\r' )
await terminal . flush ( )
await expect . poll ( ( ) = > inbox . length ) . toBe ( 1 )
agent . inject ( createUserMessage ( {
content : [ { type : 'text' , text : 'Injected while compaction was running.' } ] ,
source : { kind : 'plugin' , plugin : 'snapshot-injector' } ,
} ) )
expect ( inbox [ 0 ] ) . toMatch ( / ^ e n q u e u e : q u e u e d : / u )
expect ( agent . status ) . toBe ( 'idle' )
expect ( agent . session . events . some ( event = > event . type === 'user/message'
&& event . data . source . kind === 'user'
&& event . data . content . some ( block = > block . type === 'text' && block . text === queuedPrompt ) ) ) . toBe ( false )
const idle = agent . whenIdle ( )
compact . releaseSummary . resolve ( undefined )
await snapshotDeadline ( idle , ( ) = >
` manual compaction did not reach idle; status= ${ agent . status } ; order= ${ manualOrder ? . join ( ',' ) ? ? '' } ; tail= ${
agent . session . events . slice ( - 12 ) . map ( event = > event . type ) . join ( ',' )
} ` )
await settleTerminal ( terminal )
expect ( inbox ) . toEqual ( [ inbox [ 0 ] , ` dequeue: ${ inbox [ 0 ] ? . slice ( 'enqueue:queued:' . length ) ? ? '' } ` ] )
}
2026-07-19 01:31:55 +08:00
const events : SessionEvent [ ] = [ . . . agent . session . events ]
2026-07-23 17:39:30 +08:00
const firstHeader = events . find ( event = > event . type === 'request/header' )
2026-07-29 23:17:07 +08:00
expect ( firstHeader ? . type === 'request/header' && firstHeader . data . header . system )
. toContain ( FILE_REFERENCE_PROMPT )
2026-07-30 14:57:27 +08:00
if ( scenario . harnessSourceRoot !== undefined ) {
expect ( firstHeader ? . type === 'request/header' && firstHeader . data . header . system )
. toContain ( ` The DeepSeek Harness implementation checkout is at ${ scenario . harnessSourceRoot } . The checkout location and current working directory are separate values and may differ; never infer the working directory from this path. Use pwd to determine the current working directory. Use this checkout only to inspect or extend DSH itself. ` )
}
2026-07-19 01:31:55 +08:00
expect ( events . filter ( event = > event . type === 'tool/call' ) . map ( event = > event . data . name ) ) . toEqual ( scenario . expectedTools )
for ( const [ type , count ] of Object . entries ( scenario . expectedEventCounts ? ? { } ) ) {
expect ( events . filter ( event = > event . type === type ) , ` ${ scenario . name } must emit ${ type } ` ) . toHaveLength ( count )
}
2026-07-22 14:32:35 +08:00
if ( scenario . enterPlanMode === true ) {
2026-07-23 21:06:03 +08:00
expect ( ctx . planMode . get ( agent ) ) . toEqual ( {
active : scenario.leavePlanModeAfterFirstTurn !== true ,
} )
2026-07-22 16:57:23 +08:00
const planMode = events . find ( event = > event . type === 'plan/mode' )
if ( planMode === undefined || firstHeader === undefined ) {
throw new Error ( 'plan-mode command snapshot needs plan/mode before its first request/header' )
2026-07-22 14:32:35 +08:00
}
2026-07-22 16:57:23 +08:00
expect ( planMode . seq ) . toBeLessThan ( firstHeader . seq )
2026-07-22 14:32:35 +08:00
expect ( firstHeader . data . header . system ) . toContain ( 'Snapshot plan mode instructions.' )
2026-07-22 15:51:02 +08:00
const firstMessage = events . find ( event = > event . type === 'user/message' )
expect ( firstMessage ? . data . content ) . toEqual ( [ { type : 'text' , text : prompts [ 0 ] } ] )
2026-07-22 14:32:35 +08:00
}
2026-07-23 21:06:03 +08:00
if ( scenario . leavePlanModeAfterFirstTurn === true ) {
const planModes = events . filter ( event = > event . type === 'plan/mode' )
expect ( planModes . map ( event = > event . data . active ) ) . toEqual ( [ true , false ] )
const headers = events . filter ( event = > event . type === 'request/header' )
const exit = planModes [ 1 ]
const afterExit = headers [ 1 ]
if ( exit === undefined || afterExit === undefined ) {
throw new Error ( 'active plan exit snapshot needs a committed exit and changed request header' )
}
expect ( exit . seq ) . toBeLessThan ( afterExit . seq )
expect ( afterExit . data . header . system ) . not . toContain ( 'Snapshot plan mode instructions.' )
2026-07-23 22:41:45 +08:00
expect ( events . filter ( event = > event . type === 'user/message' && event . data . source . kind === 'plugin' ) . map ( event = > ( event . data as { content : unknown } ) . content ) )
2026-07-23 21:06:03 +08:00
. toContainEqual ( [ { type : 'text' , text : 'The user switched this session back to the default mode.' } ] )
}
2026-07-30 17:40:25 +08:00
if ( scenario . manualCompact === true ) {
const compactStart = events . find ( event = > event . type === 'compact/start' )
const compactSummary = events . find ( event = > event . type === 'compact/summary' )
const compactCheckpoint = events . find ( event = > event . type === 'user/message'
&& event . data . source . kind === 'plugin' && event . data . source . plugin === 'compact' )
const injectedEvent = events . find ( event = > event . type === 'user/message'
&& event . data . source . kind === 'plugin' && event . data . source . plugin === 'snapshot-injector' )
const compactEnd = events . find ( event = > event . type === 'compact/end' )
expect ( compactStart ? . data . turn ) . toBeNull ( )
expect ( compactEnd ? . data . turn ) . toBeNull ( )
expect ( events . filter ( event = > event . type === 'compact/summary' ) ) . toHaveLength ( 1 )
if ( compactStart === undefined || compactSummary === undefined
|| compactCheckpoint === undefined || injectedEvent === undefined
|| compactEnd === undefined ) {
throw new Error ( 'manual compaction snapshot is missing its durable marker, summary, checkpoint, or injection' )
}
// The markers are time points, not an exclusive container: unrelated
// idle injection is allowed between them while the selected span stays stable.
expect ( compactStart . seq ) . toBeLessThan ( injectedEvent . seq )
expect ( injectedEvent . seq ) . toBeLessThan ( compactSummary . seq )
expect ( compactSummary . seq ) . toBeLessThan ( compactCheckpoint . seq )
expect ( compactCheckpoint . seq ) . toBeLessThan ( compactEnd . seq )
const manualTimeline = manualOrder ? ? [ ]
const commandRunIndex = manualTimeline . indexOf ( 'command/run' )
const compactStartIndex = manualTimeline . indexOf ( 'compact/start' )
const compactEndIndex = manualTimeline . indexOf ( 'compact/end' )
const firstFlushIndex = manualTimeline . indexOf ( 'flush' )
const queuedTurnIndex = manualTimeline . indexOf ( 'turn/start:message' )
const commandDoneIndex = manualTimeline . indexOf ( 'command/done' )
expect ( manualTimeline . filter ( item = > item === 'command/run' ) ) . toHaveLength ( 1 )
expect ( manualTimeline . filter ( item = > item === 'command/done' ) ) . toHaveLength ( 1 )
expect ( compactStartIndex ) . toBeGreaterThan ( commandRunIndex )
expect ( compactEndIndex ) . toBeGreaterThan ( compactStartIndex )
expect ( firstFlushIndex ) . toBeGreaterThan ( compactEndIndex )
expect ( queuedTurnIndex ) . toBeGreaterThan ( firstFlushIndex )
expect ( commandDoneIndex ) . toBeGreaterThan ( firstFlushIndex )
const commandRun = events . find ( event = > event . type === 'command/run'
&& event . data . name === 'compact' )
const commandRunId = commandRun ? . type === 'command/run'
? commandRun.data.commandId
: undefined
const commandDone = events . find ( event = > event . type === 'command/done'
&& event . data . commandId === commandRunId )
expect ( commandRun ? . type === 'command/run' && commandRun . data ) . toEqual ( {
commandId : commandRunId ,
name : 'compact' ,
args : '' ,
source : { kind : 'user' } ,
} )
expect ( commandDone ? . type === 'command/done' && commandDone . data ) . toEqual ( {
commandId : commandRunId ,
kind : 'success' ,
text : 'Compacted 2 history items (~387 tokens).' ,
} )
expect ( commandRun !== undefined && commandRun . seq < compactStart . seq ) . toBe ( true )
expect ( commandDone !== undefined && commandDone . seq > compactEnd . seq ) . toBe ( true )
expect ( agent . session . surface . nodes ) . not . toContain ( commandRun ? . seq )
expect ( agent . session . surface . nodes ) . not . toContain ( commandDone ? . seq )
// The command line itself never becomes a prompt.
expect ( events . some ( event = > event . type === 'user/message'
&& event . data . source . kind === 'user'
&& event . data . content . some ( block = > block . type === 'text' && block . text . trim ( ) === '/compact' ) ) ) . toBe ( false )
const derived = agent . session . deriveMessages ( ) . map ( message = > message . content
. map ( block = > block . type === 'text' ? block . text : '' )
. join ( '' ) )
const checkpoint = derived . findIndex ( text = > text . includes ( 'Keyless manual compaction checkpoint.' ) )
const injected = derived . findIndex ( text = > text . includes ( 'Injected while compaction was running.' ) )
const queued = derived . findIndex ( text = > text === queuedPrompt )
expect ( checkpoint ) . toBe ( 0 )
expect ( injected ) . toBeGreaterThan ( checkpoint )
expect ( queued ) . toBeGreaterThan ( injected )
expect ( derived ) . not . toContain ( '/compact' )
expect ( derived ) . not . toContain ( 'Compacted 2 history items (~387 tokens).' )
expect ( derived . filter ( text = > text . includes ( 'Injected while compaction was running.' ) ) ) . toHaveLength ( 1 )
expect ( compactSummary . data . shadowedSeqs ) . not . toContain ( injectedEvent . seq )
const queuedTurn = events . findLast ( event = > event . type === 'turn/start' )
expect ( queuedTurn !== undefined && compactEnd . seq < queuedTurn . seq ) . toBe ( true )
}
2026-07-26 18:29:09 +08:00
if ( scenario . spillMaxInlineBytes !== undefined ) {
// The REAL pipeline ran (tools execute on replay too): the durable
// dispatch copy is bounded to a preview + locator under the run cwd,
// while the outer result still carries the program's whole value.
const dispatch = events . find ( event = > ( event . type as string ) === 'tool/code-dispatch' )
const content = ( dispatch ? . data as { content : { type : string ; text? : string } [ ] } ) . content
const text = content . filter ( block = > block . type === 'text' ) . map ( block = > block . text ? ? '' ) . join ( '' )
expect ( Buffer . byteLength ( text , 'utf8' ) ) . toBeLessThanOrEqual ( scenario . spillMaxInlineBytes )
expect ( text ) . toContain ( 'Full formatted result stored at:' )
expect ( text ) . toContain ( '.spill' )
}
2026-07-28 13:55:59 +08:00
expect ( events . filter ( event = > event . type === 'tool/result' ) . every ( event = > ! event . data . message . content [ 0 ] . isError ) ) . toBe ( true )
2026-07-19 01:31:55 +08:00
expect ( events . filter ( event = > event . type === 'turn/end' ) . every ( event = > event . data . reason . kind !== 'error' ) ) . toBe ( true )
if ( scenario . name === 'dynamic-workflow' || scenario . name === 'cordis-dynamic-toolchain' ) {
expect ( workflowEvents ) . toEqual ( [
'workflow/start' ,
'workflow/phase' ,
'workflow/agent-start' ,
'workflow/agent-end' ,
'workflow/end' ,
] )
}
expect ( terminal . themeViolations ( ) , ` ${ scenario . name } must remain theme-agnostic ` ) . toEqual ( [ ] )
2026-07-29 23:17:07 +08:00
const snapshot = interactionSnapshot ? ? normalizeTerminalSnapshot (
2026-07-19 01:31:55 +08:00
await terminal . snapshot ( { includeScrollback : true } ) ,
cwd ,
2026-07-19 13:20:59 +08:00
displayCwd ,
2026-07-19 01:31:55 +08:00
)
await handle . dispose ( )
const children = disposedSessions
. filter ( session = > session !== agent . session )
. sort ( ( a , b ) = > a . header . createdAt - b . header . createdAt )
expect ( children ) . toHaveLength ( scenario . childSessions ? ? 0 )
return { terminal : snapshot , parent : agent.session , children , workflowEvents }
} finally {
await controller ? . dispose ( )
await ctx ? . fiber . dispose ( )
await terminal . dispose ( )
await rm ( cwd , { recursive : true , force : true } )
2026-07-30 14:57:27 +08:00
if ( replayRoot !== undefined ) await rm ( replayRoot , { recursive : true , force : true } )
2026-07-27 17:53:40 +08:00
clock . mockRestore ( )
2026-07-19 01:31:55 +08:00
}
}
async function writeRecording ( scenario : Scenario , result : ScenarioResult ) : Promise < void > {
const dir = scenarioDir ( scenario )
await mkdir ( dir , { recursive : true } )
2026-07-28 21:28:38 +08:00
await writeFile (
join ( dir , 'session.jsonl' ) ,
2026-07-28 23:46:01 +08:00
scrubRequestHeaders ( tokenizeSessionFixtureCwd ( rawSessionLog ( result . parent ) ) ) ,
2026-07-28 21:28:38 +08:00
)
2026-07-19 01:31:55 +08:00
expect ( result . children ) . toHaveLength ( scenario . childSessions ? ? 0 )
for ( const [ index , child ] of result . children . entries ( ) ) {
2026-07-28 21:28:38 +08:00
await writeFile (
join ( dir , ` session. ${ index + 1 } .jsonl ` ) ,
2026-07-28 23:46:01 +08:00
scrubRequestHeaders ( tokenizeSessionFixtureCwd ( rawSessionLog ( child ) ) ) ,
2026-07-28 21:28:38 +08:00
)
2026-07-19 01:31:55 +08:00
}
}
describe ( 'TUI recorded-session terminal snapshots' , ( ) = > {
for ( const scenario of SCENARIOS ) {
it ( scenario . name , async ( ) = > {
observedScenarios . add ( scenario . name )
const result = await runScenario ( scenario )
2026-07-19 17:45:49 +08:00
const terminalFile = join ( scenarioDir ( scenario ) , 'terminal.expected.txt' )
2026-07-19 01:31:55 +08:00
if ( MODE === 'record' || MODE === 'refresh' ) {
await mkdir ( scenarioDir ( scenario ) , { recursive : true } )
await writeFile ( terminalFile , result . terminal )
}
if ( MODE === 'record' && scenario . recorded ) await writeRecording ( scenario , result )
await expect ( result . terminal ) . toMatchFileSnapshot ( terminalFile )
} , 120 _000 )
}
} )
afterAll ( async ( ) = > {
2026-07-30 17:40:25 +08:00
const scenarioNames = SCENARIOS . map ( scenario = > scenario . name ) . sort ( )
const observedNames = [ . . . observedScenarios ] . sort ( )
if ( TEST_NAME_FILTERED ) {
expect ( observedNames ) . not . toHaveLength ( 0 )
expect ( scenarioNames ) . toEqual ( expect . arrayContaining ( observedNames ) )
} else {
expect ( observedNames ) . toEqual ( scenarioNames )
}
for ( const [ index , scenario ] of SCENARIOS . entries ( ) ) {
if ( scenario . fixture === undefined ) continue
const sourceIndex = SCENARIOS . findIndex ( candidate = > candidate . name === scenario . fixture )
expect ( sourceIndex , ` ${ scenario . name } fixture source ${ scenario . fixture } must exist ` ) . toBeGreaterThanOrEqual ( 0 )
expect ( sourceIndex , ` ${ scenario . name } fixture source must precede it ` ) . toBeLessThan ( index )
const source = SCENARIOS [ sourceIndex ]
expect ( source ? . fixture , ` ${ scenario . name } fixture source must own its replay files ` ) . toBeUndefined ( )
expect ( source ? . recorded , ` ${ scenario . name } fixture source must be recordable ` ) . toBe ( true )
}
2026-07-19 01:31:55 +08:00
const directories = ( await readdir ( SNAPSHOTS_DIR , { withFileTypes : true } ) )
. filter ( entry = > entry . isDirectory ( ) )
. map ( entry = > entry . name )
. sort ( )
expect ( directories ) . toEqual ( SCENARIOS . map ( scenario = > scenario . name ) . sort ( ) )
for ( const scenario of SCENARIOS ) {
const expected = [
2026-07-30 17:40:25 +08:00
. . . scenario . fixture === undefined ? [ 'session.jsonl' ] : [ ] ,
2026-07-19 17:45:49 +08:00
'terminal.expected.txt' ,
2026-07-30 17:40:25 +08:00
. . . scenario . seedWorkspace === true && scenario . fixture === undefined ? [ 'workspace' ] : [ ] ,
2026-07-19 01:31:55 +08:00
. . . Array . from ( { length : scenario.childSessions ? ? 0 } , ( _ , index ) = > ` session. ${ index + 1 } .jsonl ` ) ,
] . sort ( )
expect ( ( await readdir ( scenarioDir ( scenario ) ) ) . sort ( ) ) . toEqual ( expected )
for ( const fixture of [ 'session.jsonl' , . . . childFixturePaths ( scenario ) . map ( path = > basename ( path ) ) ] ) {
2026-07-30 17:40:25 +08:00
const content = await readFile ( join ( fixtureDir ( scenario ) , fixture ) , 'utf8' )
2026-07-19 01:31:55 +08:00
expect ( scrubRequestHeaders ( content ) , ` ${ scenario . name } / ${ fixture } carries request-header bulk ` ) . toBe ( content )
}
}
} )