11import * as NodeServices from "@effect/platform-node/NodeServices" ;
2+ import * as Clock from "effect/Clock" ;
23import { assert , describe , it } from "@effect/vitest" ;
4+ import * as Deferred from "effect/Deferred" ;
35import * as Effect from "effect/Effect" ;
6+ import * as Fiber from "effect/Fiber" ;
47import * as Layer from "effect/Layer" ;
58import * as Logger from "effect/Logger" ;
69import * as PlatformError from "effect/PlatformError" ;
@@ -9,6 +12,7 @@ import * as Sink from "effect/Sink";
912import * as Stream from "effect/Stream" ;
1013import * as ChildProcess from "effect/process/ChildProcess" ;
1114import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner" ;
15+ import * as TestClock from "effect/testing/TestClock" ;
1216import * as NodeOS from "node:os" ;
1317import { vi } from "vite-plus/test" ;
1418
@@ -74,7 +78,10 @@ function runShellEnvironment(input: {
7478 readonly userShell ?: string ;
7579 readonly handler : (
7680 command : ChildProcess . Command ,
77- ) => string | { output : string ; exitCode : number } ;
81+ ) =>
82+ | string
83+ | { output : string ; exitCode : number }
84+ | Effect . Effect < ChildProcessSpawner . ChildProcessHandle , PlatformError . PlatformError > ;
7885 readonly failure ?: PlatformError . PlatformError ;
7986} ) {
8087 const layerEnvironment = Layer . succeed (
@@ -88,6 +95,7 @@ function runShellEnvironment(input: {
8895 ChildProcessSpawner . make ( ( command ) => {
8996 if ( input . failure !== undefined ) return Effect . fail ( input . failure ) ;
9097 const result = input . handler ( command ) ;
98+ if ( Effect . isEffect ( result ) ) return result ;
9199 return Effect . succeed (
92100 typeof result === "string"
93101 ? makeProcess ( result )
@@ -489,6 +497,85 @@ describe("DesktopShellEnvironment", () => {
489497 } ) ,
490498 ) ;
491499
500+ it . effect ( "waits at most 5 seconds in total when the PowerShell profile hangs" , ( ) =>
501+ Effect . gen ( function * ( ) {
502+ const env : NodeJS . ProcessEnv = { PATH : "C:\\Windows\\System32" } ;
503+ const profileShells : string [ ] = [ ] ;
504+ const started = yield * Deferred . make < void > ( ) ;
505+ const completed = yield * Deferred . make < void > ( ) ;
506+ const baseClock = yield * Clock . Clock ;
507+ let wallTime = 0 ;
508+ const clockWithBackwardWallJump : Clock . Clock = {
509+ ...baseClock ,
510+ currentTimeMillisUnsafe : ( ) => wallTime ,
511+ currentTimeMillis : Effect . sync ( ( ) => wallTime ) ,
512+ } ;
513+
514+ const install = yield * Effect . forkChild (
515+ runShellEnvironment ( {
516+ env,
517+ platform : "win32" ,
518+ handler : ( command ) => {
519+ if ( command . _tag !== "StandardCommand" ) return "" ;
520+ if ( command . args . includes ( "-NoProfile" ) ) {
521+ return envOutput ( { PATH : "C:\\NoProfile\\Bin;C:\\Windows\\System32" } ) ;
522+ }
523+ profileShells . push ( command . command ) ;
524+ return Deferred . succeed ( started , undefined ) . pipe (
525+ Effect . as (
526+ ChildProcessSpawner . makeHandle ( {
527+ ...makeProcess ( "" ) ,
528+ stdout : Stream . never ,
529+ all : Stream . never ,
530+ exitCode : Effect . never ,
531+ isRunning : Effect . succeed ( true ) ,
532+ } ) ,
533+ ) ,
534+ ) ;
535+ } ,
536+ } ) . pipe (
537+ Effect . tap ( ( ) => Deferred . succeed ( completed , undefined ) ) ,
538+ Effect . provide ( Logger . layer ( [ ] ) ) ,
539+ Effect . provideService ( Clock . Clock , clockWithBackwardWallJump ) ,
540+ ) ,
541+ ) ;
542+ yield * Deferred . await ( started ) ;
543+ wallTime = - 60_000 ;
544+ yield * TestClock . adjust ( "5 seconds" ) ;
545+ assert . isTrue ( yield * Deferred . isDone ( completed ) , "finishes within the shared budget" ) ;
546+ assert . isFalse ( yield * Fiber . join ( install ) ) ;
547+ assert . deepEqual ( profileShells , [ "pwsh.exe" ] ) ;
548+ assert . equal ( env . PATH , "C:\\NoProfile\\Bin;C:\\Windows\\System32" ) ;
549+ } ) ,
550+ ) ;
551+
552+ it . effect ( "falls back to Windows PowerShell when pwsh.exe is not installed" , ( ) =>
553+ Effect . gen ( function * ( ) {
554+ const env : NodeJS . ProcessEnv = { PATH : "C:\\Windows\\System32" } ;
555+ const missing = PlatformError . systemError ( {
556+ _tag : "NotFound" ,
557+ module : "ChildProcess" ,
558+ method : "spawn" ,
559+ pathOrDescriptor : "pwsh.exe" ,
560+ } ) ;
561+
562+ const prepared = yield * runShellEnvironment ( {
563+ env,
564+ platform : "win32" ,
565+ handler : ( command ) => {
566+ if ( command . _tag !== "StandardCommand" ) return "" ;
567+ if ( command . command === "pwsh.exe" ) return Effect . fail ( missing ) ;
568+ return command . args . includes ( "-NoProfile" )
569+ ? envOutput ( { PATH : "C:\\Windows\\System32" } )
570+ : envOutput ( { PATH : "C:\\Profile\\Node;C:\\Windows\\System32" } ) ;
571+ } ,
572+ } ) . pipe ( Effect . provide ( Logger . layer ( [ ] ) ) ) ;
573+
574+ assert . isTrue ( prepared ) ;
575+ assert . equal ( env . PATH , "C:\\Profile\\Node;C:\\Windows\\System32" ) ;
576+ } ) ,
577+ ) ;
578+
492579 it . effect . each ( [ false , true ] ) (
493580 "leaves Windows preparation incomplete when loadProfile=%s fails" ,
494581 ( loadProfile ) =>
0 commit comments