@@ -2,7 +2,7 @@ import fs from "fs/promises"
22import { realpathSync } from "node:fs"
33import path from "path"
44import { describe , expect , test } from "bun:test"
5- import { DateTime , Duration , Effect , Fiber , Layer , Scope , Stream } from "effect"
5+ import { DateTime , Deferred , Duration , Effect , Fiber , Layer , Scope , Stream } from "effect"
66import { Money } from "@opencode-ai/schema/money"
77import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
88import { LayerNode } from "@opencode-ai/core/effect/layer-node"
@@ -166,10 +166,10 @@ const overflowCommand = (bytes: number) =>
166166 isWindows
167167 ? `[Console]::Out.Write(('x' * ${ bytes } )); Start-Sleep -Milliseconds 100`
168168 : `head -c ${ bytes } /dev/zero | tr '\\0' 'x'`
169- const progressOverflowCommand = ( bytes : number ) =>
169+ const progressOverflowCommand = ( bytes : number , release : string ) =>
170170 isWindows
171- ? `[Console]::Out.Write(('x' * ${ bytes } )); Start-Sleep -Milliseconds 1500 `
172- : `head -c ${ bytes } /dev/zero | tr '\\0' 'x'; sleep 1.5 `
171+ ? `[Console]::Out.Write(('x' * ${ bytes } )); while (!(Test-Path -LiteralPath ' ${ release } ')) { Start-Sleep -Milliseconds 50 } `
172+ : `head -c ${ bytes } /dev/zero | tr '\\0' 'x'; while [ ! -e ' ${ release } ' ]; do sleep 0.05; done `
173173
174174const withSession = < A , E , R > ( directory : string , body : ( registry : ToolRegistry . Interface ) => Effect . Effect < A , E , R > ) =>
175175 Effect . gen ( function * ( ) {
@@ -417,33 +417,49 @@ describe("ShellTool", () => {
417417 ) ,
418418 )
419419
420- it . live ( "reports bounded output progress for a running command" , ( ) =>
421- Effect . acquireUseRelease (
422- Effect . promise ( ( ) => tmpdir ( ) ) ,
423- ( tmp ) => {
424- reset ( )
425- const bytes = ShellTool . MAX_CAPTURE_BYTES + 1024
426- return withSession ( tmp . path , ( registry ) =>
427- Effect . gen ( function * ( ) {
428- const progress : ToolRegistry . Progress [ ] = [ ]
429- yield * settleTool ( registry , {
430- ...call ( { command : progressOverflowCommand ( bytes ) } , "call-progress" ) ,
431- progress : ( update ) => Effect . sync ( ( ) => progress . push ( update ) ) ,
432- } )
420+ it . live (
421+ "reports bounded output progress for a running command" ,
422+ ( ) =>
423+ Effect . acquireUseRelease (
424+ Effect . promise ( ( ) => tmpdir ( ) ) ,
425+ ( tmp ) => {
426+ reset ( )
427+ const release = "shell-progress-release"
428+ const releasePath = path . join ( tmp . path , release )
429+ return withSession ( tmp . path , ( registry ) =>
430+ Effect . gen ( function * ( ) {
431+ const observed = yield * Deferred . make < ToolRegistry . Progress > ( )
432+ yield * settleTool ( registry , {
433+ ...call (
434+ { command : progressOverflowCommand ( ShellTool . MAX_CAPTURE_BYTES + 1024 , release ) } ,
435+ "call-progress" ,
436+ ) ,
437+ progress : ( update ) =>
438+ Effect . gen ( function * ( ) {
439+ if ( update . structured . truncated !== true ) return
440+ const content = update . content [ 0 ]
441+ if ( content ?. type !== "text" ) return
442+ if ( content . text . indexOf ( "\n\n[output truncated; full output saved to:" ) !== ShellTool . MAX_CAPTURE_BYTES )
443+ return
444+ yield * Deferred . succeed ( observed , update )
445+ yield * Effect . promise ( ( ) => fs . writeFile ( releasePath , "" ) )
446+ } ) ,
447+ } )
433448
434- expect ( progress ) . toHaveLength ( 1 )
435- expect ( progress [ 0 ] ?. structured ) . toEqual ( { truncated : true } )
436- const content = progress [ 0 ] ?. content [ 0 ]
437- expect ( content ?. type ) . toBe ( "text" )
438- if ( content ?. type !== "text" ) return
439- expect ( content . text . indexOf ( "\n\n[output truncated; full output saved to:" ) ) . toBe (
440- ShellTool . MAX_CAPTURE_BYTES ,
441- )
442- } ) ,
443- )
444- } ,
445- ( tmp ) => Effect . promise ( ( ) => tmp [ Symbol . asyncDispose ] ( ) . then ( ( ) => undefined ) ) ,
446- ) ,
449+ const progress = yield * Deferred . await ( observed )
450+ expect ( progress . structured ) . toEqual ( { truncated : true } )
451+ const content = progress . content [ 0 ]
452+ expect ( content ?. type ) . toBe ( "text" )
453+ if ( content ?. type !== "text" ) return
454+ expect ( content . text . indexOf ( "\n\n[output truncated; full output saved to:" ) ) . toBe (
455+ ShellTool . MAX_CAPTURE_BYTES ,
456+ )
457+ } ) . pipe ( Effect . ensuring ( Effect . promise ( ( ) => fs . writeFile ( releasePath , "" ) ) . pipe ( Effect . ignore ) ) ) ,
458+ )
459+ } ,
460+ ( tmp ) => Effect . promise ( ( ) => tmp [ Symbol . asyncDispose ] ( ) . then ( ( ) => undefined ) ) ,
461+ ) ,
462+ { timeout : 15_000 } ,
447463 )
448464
449465 it . live ( "returns a useful timeout settlement" , ( ) =>
0 commit comments