@@ -8,6 +8,14 @@ import {
88 replyRunRegistry ,
99} from "../auto-reply/reply/reply-run-registry.js" ;
1010import { onAgentEvent , resetAgentEventsForTest } from "../infra/agent-events.js" ;
11+ import {
12+ onInternalDiagnosticEvent ,
13+ waitForDiagnosticEventsDrained ,
14+ } from "../infra/diagnostic-events.js" ;
15+ import {
16+ getDiagnosticSessionActivitySnapshot ,
17+ resetDiagnosticRunActivityForTest ,
18+ } from "../logging/diagnostic-run-activity.js" ;
1119import type { getProcessSupervisor } from "../process/supervisor/index.js" ;
1220import {
1321 makeBootstrapWarn as realMakeBootstrapWarn ,
@@ -45,6 +53,7 @@ type SupervisorSpawnFn = ProcessSupervisor["spawn"];
4553
4654beforeEach ( ( ) => {
4755 resetAgentEventsForTest ( ) ;
56+ resetDiagnosticRunActivityForTest ( ) ;
4857 resetClaudeLiveSessionsForTest ( ) ;
4958 replyRunTesting . resetReplyRunRegistry ( ) ;
5059 restoreCliRunnerPrepareTestDeps ( ) ;
@@ -53,6 +62,8 @@ beforeEach(() => {
5362
5463afterEach ( ( ) => {
5564 vi . restoreAllMocks ( ) ;
65+ vi . useRealTimers ( ) ;
66+ resetDiagnosticRunActivityForTest ( ) ;
5667 resetClaudeLiveSessionsForTest ( ) ;
5768 replyRunTesting . resetReplyRunRegistry ( ) ;
5869} ) ;
@@ -73,6 +84,7 @@ function buildPreparedCliRunContext(params: {
7384 skillsSnapshot ?: PreparedCliRunContext [ "params" ] [ "skillsSnapshot" ] ;
7485 thinkLevel ?: PreparedCliRunContext [ "params" ] [ "thinkLevel" ] ;
7586 workspaceDir ?: string ;
87+ timeoutMs ?: number ;
7688} ) : PreparedCliRunContext {
7789 const workspaceDir = params . workspaceDir ?? "/tmp" ;
7890 const baseBackend =
@@ -116,7 +128,7 @@ function buildPreparedCliRunContext(params: {
116128 provider : params . provider ,
117129 model : params . model ,
118130 thinkLevel : params . thinkLevel ,
119- timeoutMs : 1_000 ,
131+ timeoutMs : params . timeoutMs ?? 1_000 ,
120132 runId : params . runId ,
121133 skillsSnapshot : params . skillsSnapshot ,
122134 } ,
@@ -1604,6 +1616,164 @@ ${JSON.stringify({
16041616 expect ( parsed . response . response . toolUseID ) . toBe ( "tool-allow-1" ) ;
16051617 } ) ;
16061618
1619+ it ( "reports Claude live stream progress and keeps native tools fresh while they are running" , async ( ) => {
1620+ vi . useFakeTimers ( {
1621+ toFake : [ "Date" , "setTimeout" , "clearTimeout" , "setInterval" , "clearInterval" ] ,
1622+ } ) ;
1623+ vi . setSystemTime ( new Date ( "2026-05-28T00:00:00.000Z" ) ) ;
1624+ const diagnosticEvents : string [ ] = [ ] ;
1625+ const stopDiagnostics = onInternalDiagnosticEvent ( ( event ) => {
1626+ if ( event . type === "run.progress" || event . type . startsWith ( "tool.execution." ) ) {
1627+ diagnosticEvents . push ( event . type ) ;
1628+ }
1629+ } ) ;
1630+ let stdoutListener : ( ( chunk : string ) => void ) | undefined ;
1631+ const stdin = {
1632+ write : vi . fn ( ( data : string , cb ?: ( err ?: Error | null ) => void ) => {
1633+ stdoutListener ?.(
1634+ [
1635+ JSON . stringify ( {
1636+ type : "system" ,
1637+ subtype : "init" ,
1638+ session_id : "live-diagnostics" ,
1639+ } ) ,
1640+ JSON . stringify ( {
1641+ type : "assistant" ,
1642+ session_id : "live-diagnostics" ,
1643+ message : {
1644+ role : "assistant" ,
1645+ content : [
1646+ {
1647+ type : "tool_use" ,
1648+ id : "tool-live-1" ,
1649+ name : "Bash" ,
1650+ input : { command : "sleep 45" } ,
1651+ } ,
1652+ ] ,
1653+ } ,
1654+ } ) ,
1655+ ] . join ( "\n" ) + "\n" ,
1656+ ) ;
1657+ cb ?.( ) ;
1658+ } ) ,
1659+ end : vi . fn ( ) ,
1660+ } ;
1661+ supervisorSpawnMock . mockImplementation ( async ( ...args : unknown [ ] ) => {
1662+ const input = ( args [ 0 ] ?? { } ) as { onStdout ?: ( chunk : string ) => void } ;
1663+ stdoutListener = input . onStdout ;
1664+ return {
1665+ runId : "live-run-diagnostics" ,
1666+ pid : 3060 ,
1667+ startedAtMs : Date . now ( ) ,
1668+ stdin,
1669+ wait : vi . fn ( ( ) => new Promise ( ( ) => { } ) ) ,
1670+ cancel : vi . fn ( ) ,
1671+ } ;
1672+ } ) ;
1673+
1674+ try {
1675+ const context = buildPreparedCliRunContext ( {
1676+ provider : "claude-cli" ,
1677+ model : "sonnet" ,
1678+ runId : "run-live-diagnostics" ,
1679+ sessionId : "session-live-diagnostics" ,
1680+ sessionKey : "agent:main:diagnostics" ,
1681+ prompt : "hello" ,
1682+ backend : { liveSession : "claude-stdio" } ,
1683+ timeoutMs : 120_000 ,
1684+ } ) ;
1685+ const resultPromise = runClaudeLiveSessionTurn ( {
1686+ context,
1687+ args : context . preparedBackend . backend . args ?? [ ] ,
1688+ env : { } ,
1689+ prompt : "hello" ,
1690+ useResume : false ,
1691+ noOutputTimeoutMs : 120_000 ,
1692+ getProcessSupervisor : ( ) => ( {
1693+ spawn : ( params : Parameters < SupervisorSpawnFn > [ 0 ] ) =>
1694+ supervisorSpawnMock ( params ) as ReturnType < SupervisorSpawnFn > ,
1695+ cancel : vi . fn ( ) ,
1696+ cancelScope : vi . fn ( ) ,
1697+ reconcileOrphans : vi . fn ( ) ,
1698+ getRecord : vi . fn ( ) ,
1699+ } ) ,
1700+ onAssistantDelta : ( ) => { } ,
1701+ cleanup : async ( ) => { } ,
1702+ } ) ;
1703+
1704+ await waitForDiagnosticEventsDrained ( ) ;
1705+ await vi . waitFor ( ( ) =>
1706+ expect (
1707+ getDiagnosticSessionActivitySnapshot ( {
1708+ sessionKey : "agent:main:diagnostics" ,
1709+ } ) . activeToolName ,
1710+ ) . toBe ( "Bash" ) ,
1711+ ) ;
1712+ expect (
1713+ getDiagnosticSessionActivitySnapshot ( { sessionKey : "agent:main:diagnostics" } )
1714+ . lastProgressReason ,
1715+ ) . toBe ( "cli_live:tool_started" ) ;
1716+
1717+ await vi . advanceTimersByTimeAsync ( 10_000 ) ;
1718+ await waitForDiagnosticEventsDrained ( ) ;
1719+ expect (
1720+ getDiagnosticSessionActivitySnapshot ( { sessionKey : "agent:main:diagnostics" } )
1721+ . lastProgressReason ,
1722+ ) . toBe ( "cli_live:tool_running" ) ;
1723+ expect (
1724+ getDiagnosticSessionActivitySnapshot ( { sessionKey : "agent:main:diagnostics" } )
1725+ . lastProgressAgeMs ,
1726+ ) . toBeLessThan ( 100 ) ;
1727+
1728+ stdoutListener ?.(
1729+ [
1730+ JSON . stringify ( {
1731+ type : "user" ,
1732+ session_id : "live-diagnostics" ,
1733+ message : {
1734+ role : "user" ,
1735+ content : [
1736+ {
1737+ type : "tool_result" ,
1738+ tool_use_id : "tool-live-1" ,
1739+ content : "done" ,
1740+ } ,
1741+ ] ,
1742+ } ,
1743+ } ) ,
1744+ JSON . stringify ( {
1745+ type : "assistant" ,
1746+ session_id : "live-diagnostics" ,
1747+ message : {
1748+ role : "assistant" ,
1749+ content : [ { type : "text" , text : "ok" } ] ,
1750+ } ,
1751+ } ) ,
1752+ JSON . stringify ( {
1753+ type : "result" ,
1754+ session_id : "live-diagnostics" ,
1755+ result : "ok" ,
1756+ } ) ,
1757+ ] . join ( "\n" ) + "\n" ,
1758+ ) ;
1759+
1760+ await expect ( resultPromise ) . resolves . toMatchObject ( { output : { text : "ok" } } ) ;
1761+ await waitForDiagnosticEventsDrained ( ) ;
1762+ expect (
1763+ getDiagnosticSessionActivitySnapshot ( { sessionKey : "agent:main:diagnostics" } )
1764+ . activeToolName ,
1765+ ) . toBeUndefined ( ) ;
1766+ expect (
1767+ getDiagnosticSessionActivitySnapshot ( { sessionKey : "agent:main:diagnostics" } )
1768+ . lastProgressReason ,
1769+ ) . toBe ( "cli_live:result" ) ;
1770+ expect ( diagnosticEvents ) . toContain ( "tool.execution.started" ) ;
1771+ expect ( diagnosticEvents ) . toContain ( "tool.execution.completed" ) ;
1772+ } finally {
1773+ stopDiagnostics ( ) ;
1774+ }
1775+ } ) ;
1776+
16071777 it ( "answers Claude live control_request can_use_tool with deny when exec policy is restrictive" , async ( ) => {
16081778 let stdoutListener : ( ( chunk : string ) => void ) | undefined ;
16091779 const writes : string [ ] = [ ] ;
0 commit comments