11import { describe , expect } from "bun:test"
2+ import { realpathSync } from "node:fs"
3+ import { tmpdir } from "node:os"
24import { Effect , Exit , Stream } from "effect"
35import { ChildProcess } from "effect/unstable/process"
46import { AppProcess } from "@opencode-ai/core/process"
@@ -123,6 +125,82 @@ describe("AppProcess", () => {
123125 )
124126 } )
125127
128+ describe ( "run with stdin option" , ( ) => {
129+ const echoStdin = "process.stdin.on('data', c => process.stdout.write(c))"
130+
131+ it . effect (
132+ "feeds a string to stdin and returns it on stdout" ,
133+ Effect . gen ( function * ( ) {
134+ const svc = yield * AppProcess . Service
135+ const result = yield * svc . run ( cmd ( "-e" , echoStdin ) , { stdin : "hello" } )
136+ expect ( result . exitCode ) . toBe ( 0 )
137+ expect ( result . stdout . toString ( "utf8" ) ) . toBe ( "hello" )
138+ } ) ,
139+ )
140+
141+ it . effect (
142+ "feeds a Uint8Array to stdin" ,
143+ Effect . gen ( function * ( ) {
144+ const svc = yield * AppProcess . Service
145+ const bytes = new TextEncoder ( ) . encode ( "bytes" )
146+ const result = yield * svc . run ( cmd ( "-e" , echoStdin ) , { stdin : bytes } )
147+ expect ( result . exitCode ) . toBe ( 0 )
148+ expect ( result . stdout . toString ( "utf8" ) ) . toBe ( "bytes" )
149+ } ) ,
150+ )
151+
152+ it . effect (
153+ "feeds a Stream of Uint8Array chunks to stdin" ,
154+ Effect . gen ( function * ( ) {
155+ const svc = yield * AppProcess . Service
156+ const enc = new TextEncoder ( )
157+ const stream = Stream . fromIterable ( [ enc . encode ( "one" ) , enc . encode ( "-two" ) , enc . encode ( "-three" ) ] )
158+ const result = yield * svc . run ( cmd ( "-e" , echoStdin ) , { stdin : stream } )
159+ expect ( result . exitCode ) . toBe ( 0 )
160+ expect ( result . stdout . toString ( "utf8" ) ) . toBe ( "one-two-three" )
161+ } ) ,
162+ )
163+
164+ it . effect (
165+ "completes correctly with empty input" ,
166+ Effect . gen ( function * ( ) {
167+ const svc = yield * AppProcess . Service
168+ const result = yield * svc . run ( cmd ( "-e" , echoStdin ) , { stdin : "" } )
169+ expect ( result . exitCode ) . toBe ( 0 )
170+ expect ( result . stdout . toString ( "utf8" ) ) . toBe ( "" )
171+ } ) ,
172+ )
173+
174+ it . effect (
175+ "carries existing Command options like env" ,
176+ Effect . gen ( function * ( ) {
177+ const svc = yield * AppProcess . Service
178+ const script =
179+ "process.stdout.write(process.env.FEED + ':'); process.stdin.on('data', c => process.stdout.write(c))"
180+ const command = ChildProcess . make ( NODE , [ "-e" , script ] , { env : { FEED : "envset" } , extendEnv : true } )
181+ const result = yield * svc . run ( command , { stdin : "payload" } )
182+ expect ( result . exitCode ) . toBe ( 0 )
183+ expect ( result . stdout . toString ( "utf8" ) ) . toBe ( "envset:payload" )
184+ } ) ,
185+ )
186+
187+ it . effect (
188+ "carries existing Command options like cwd" ,
189+ Effect . gen ( function * ( ) {
190+ const svc = yield * AppProcess . Service
191+ const dir = realpathSync ( tmpdir ( ) )
192+ const script =
193+ "process.stdout.write(process.cwd() + '|'); process.stdin.on('data', c => process.stdout.write(c))"
194+ const command = ChildProcess . make ( NODE , [ "-e" , script ] , { cwd : dir } )
195+ const result = yield * svc . run ( command , { stdin : "ok" } )
196+ expect ( result . exitCode ) . toBe ( 0 )
197+ const [ cwd , stdin ] = result . stdout . toString ( "utf8" ) . split ( "|" )
198+ expect ( realpathSync ( cwd ) ) . toBe ( dir )
199+ expect ( stdin ) . toBe ( "ok" )
200+ } ) ,
201+ )
202+ } )
203+
126204 describe ( "runStream" , ( ) => {
127205 it . live (
128206 "emits lines incrementally and ends cleanly on exit 0" ,
@@ -136,11 +214,17 @@ describe("AppProcess", () => {
136214 )
137215
138216 it . live (
139- "fails with AppProcessError when exit not in okExitCodes " ,
217+ "okExitCodes determines whether a non-zero exit fails the stream " ,
140218 Effect . gen ( function * ( ) {
141219 const svc = yield * AppProcess . Service
220+ const allowed = yield * svc
221+ . runStream ( cmd ( "-e" , "console.log('only'); process.exit(1)" ) , { okExitCodes : [ 0 , 1 ] } )
222+ . pipe ( Stream . runCollect )
223+ expect ( Array . from ( allowed ) ) . toEqual ( [ "only" ] )
142224 const exit = yield * Effect . exit (
143- svc . runStream ( cmd ( "-e" , "console.log('a'); process.exit(2)" ) , { okExitCodes : [ 0 ] } ) . pipe ( Stream . runCollect ) ,
225+ svc
226+ . runStream ( cmd ( "-e" , "console.log('a'); process.exit(2)" ) , { okExitCodes : [ 0 , 1 ] } )
227+ . pipe ( Stream . runCollect ) ,
144228 )
145229 expect ( Exit . isFailure ( exit ) ) . toBe ( true )
146230 if ( Exit . isFailure ( exit ) ) {
@@ -152,17 +236,6 @@ describe("AppProcess", () => {
152236 } ) ,
153237 )
154238
155- it . live (
156- "okExitCodes allowlist treats non-zero as success" ,
157- Effect . gen ( function * ( ) {
158- const svc = yield * AppProcess . Service
159- const result = yield * svc
160- . runStream ( cmd ( "-e" , "console.log('only'); process.exit(1)" ) , { okExitCodes : [ 0 , 1 ] } )
161- . pipe ( Stream . runCollect )
162- expect ( Array . from ( result ) ) . toEqual ( [ "only" ] )
163- } ) ,
164- )
165-
166239 it . live (
167240 "without okExitCodes, never fails on exit code" ,
168241 Effect . gen ( function * ( ) {
@@ -177,12 +250,10 @@ describe("AppProcess", () => {
177250 Effect . gen ( function * ( ) {
178251 const svc = yield * AppProcess . Service
179252 const controller = new AbortController ( )
180- setTimeout ( ( ) => controller . abort ( ) , 50 )
253+ controller . abort ( )
181254 const exit = yield * Effect . exit (
182255 svc
183- . runStream ( cmd ( "-e" , "setInterval(() => console.log('tick'), 100); setTimeout(() => {}, 60_000)" ) , {
184- signal : controller . signal ,
185- } )
256+ . runStream ( cmd ( "-e" , "setInterval(() => {}, 60_000)" ) , { signal : controller . signal } )
186257 . pipe ( Stream . runCollect ) ,
187258 )
188259 expect ( Exit . isFailure ( exit ) ) . toBe ( true )
0 commit comments