@@ -27,7 +27,10 @@ interface RecordedCommand {
2727
2828const makeRecordingRunnerLayer = (
2929 commands : Array < RecordedCommand > ,
30- options ?: { readonly failCommand ?: string } ,
30+ options ?: {
31+ readonly failCommand ?: string ;
32+ readonly failWhen ?: ( command : string , args : ReadonlyArray < string > ) => boolean ;
33+ } ,
3134) =>
3235 Layer . succeed (
3336 ProcessRunner . ProcessRunner ,
@@ -36,7 +39,9 @@ const makeRecordingRunnerLayer = (
3639 Effect . sync ( ( ) => {
3740 assert . isUndefined ( input . env ) ;
3841 commands . push ( { command : input . command , args : input . args } ) ;
39- const failed = input . command === options ?. failCommand ;
42+ const failed =
43+ input . command === options ?. failCommand ||
44+ options ?. failWhen ?.( input . command , input . args ) === true ;
4045 return {
4146 stdout : "" ,
4247 stderr : failed ? `${ input . command } exploded` : "" ,
@@ -152,6 +157,11 @@ it("flags package-manager cache entry points as ephemeral", () => {
152157 BootService . isEphemeralCacheEntry ( "/home/theo/.bun/install/cache/[email protected] /dist/bin.mjs" ) , 153158 ) ;
154159 assert . isFalse ( BootService . isEphemeralCacheEntry ( "/usr/local/lib/node_modules/t3/dist/bin.mjs" ) ) ;
160+ assert . isFalse (
161+ BootService . isEphemeralCacheEntry (
162+ "/home/theo/dev/pnpm/dlx-tools/t3/node_modules/t3/dist/bin.mjs" ,
163+ ) ,
164+ ) ;
155165 assert . isFalse (
156166 BootService . isEphemeralCacheEntry (
157167 "/home/theo/.t3/runtime/versions/0.0.27/node_modules/t3/dist/bin.mjs" ,
@@ -234,6 +244,29 @@ it.layer(NodeServices.layer)("BootService", (it) => {
234244 } ) ,
235245 ) ;
236246
247+ it . effect ( "reinstalls a pinned runtime when its entry point is missing" , ( ) =>
248+ Effect . gen ( function * ( ) {
249+ const { dirs, fs, path } = yield * makeTestContext ( ) ;
250+ const commands : Array < RecordedCommand > = [ ] ;
251+ const service = yield * BootService . make ( {
252+ baseDir : dirs . baseDir ,
253+ logsDir : dirs . logsDir ,
254+ cliVersion : "0.0.27" ,
255+ host : makeHost ( "/home/theo/.npm/_npx/abc/node_modules/t3/dist/bin.mjs" ) ,
256+ } ) . pipe ( Effect . provide ( makeRecordingRunnerLayer ( commands ) ) , provideHostRefs ( dirs . home ) ) ;
257+
258+ const plan = yield * service . install ;
259+ yield * fs . makeDirectory ( path . dirname ( plan . t3EntryPath ) , { recursive : true } ) ;
260+ yield * fs . writeFileString ( plan . t3EntryPath , "#!/usr/bin/env node\n" ) ;
261+ yield * fs . remove ( plan . t3EntryPath ) ;
262+ commands . length = 0 ;
263+
264+ yield * service . install ;
265+
266+ assert . isTrue ( commands . some ( ( { command } ) => command === "npm" ) ) ;
267+ } ) ,
268+ ) ;
269+
237270 it . effect ( "reads executable metadata from host process references" , ( ) =>
238271 Effect . gen ( function * ( ) {
239272 const { dirs } = yield * makeTestContext ( ) ;
@@ -376,6 +409,95 @@ it.layer(NodeServices.layer)("BootService", (it) => {
376409 ) ;
377410 const status = yield * service . status ;
378411 assert . isFalse ( status . installed ) ;
412+ assert . isTrue (
413+ commands . some (
414+ ( { command, args } ) =>
415+ command === "systemctl" && args . join ( " " ) === "--user disable --now t3code.service" ,
416+ ) ,
417+ ) ;
418+ } ) ,
419+ ) ;
420+
421+ it . effect ( "restores the previous unit when a repair cannot activate" , ( ) =>
422+ Effect . gen ( function * ( ) {
423+ const { dirs, fs, path } = yield * makeTestContext ( ) ;
424+ const initialCommands : Array < RecordedCommand > = [ ] ;
425+ const initialService = yield * BootService . make ( {
426+ baseDir : dirs . baseDir ,
427+ logsDir : dirs . logsDir ,
428+ cliVersion : "0.0.27" ,
429+ host : makeHost ( dirs . stableEntry ) ,
430+ } ) . pipe (
431+ Effect . provide ( makeRecordingRunnerLayer ( initialCommands ) ) ,
432+ provideHostRefs ( dirs . home ) ,
433+ ) ;
434+ yield * initialService . install ;
435+
436+ const unitPath = path . join ( dirs . home , ".config" , "systemd" , "user" , "t3code.service" ) ;
437+ const previousUnit = yield * fs . readFileString ( unitPath ) ;
438+ const replacementEntry = path . join ( dirs . home , "replacement-bin.mjs" ) ;
439+ yield * fs . writeFileString ( replacementEntry , "#!/usr/bin/env node\n" ) ;
440+ const repairCommands : Array < RecordedCommand > = [ ] ;
441+ const repairService = yield * BootService . make ( {
442+ baseDir : dirs . baseDir ,
443+ logsDir : dirs . logsDir ,
444+ cliVersion : "0.0.28" ,
445+ host : makeHost ( replacementEntry ) ,
446+ } ) . pipe (
447+ Effect . provide ( makeRecordingRunnerLayer ( repairCommands , { failCommand : "loginctl" } ) ) ,
448+ provideHostRefs ( dirs . home ) ,
449+ ) ;
450+
451+ const error = yield * repairService . install . pipe ( Effect . flip ) ;
452+
453+ assert . isTrue ( isCommandError ( error ) ) ;
454+ assert . equal ( yield * fs . readFileString ( unitPath ) , previousUnit ) ;
455+ assert . isTrue (
456+ repairCommands . some (
457+ ( { command, args } ) =>
458+ command === "systemctl" && args . join ( " " ) === "--user restart t3code.service" ,
459+ ) ,
460+ ) ;
461+ } ) ,
462+ ) ;
463+
464+ it . effect ( "keeps the unit when stopping it during uninstall fails" , ( ) =>
465+ Effect . gen ( function * ( ) {
466+ const { dirs, fs, path } = yield * makeTestContext ( ) ;
467+ const installCommands : Array < RecordedCommand > = [ ] ;
468+ const installedService = yield * BootService . make ( {
469+ baseDir : dirs . baseDir ,
470+ logsDir : dirs . logsDir ,
471+ cliVersion : "0.0.27" ,
472+ host : makeHost ( dirs . stableEntry ) ,
473+ } ) . pipe (
474+ Effect . provide ( makeRecordingRunnerLayer ( installCommands ) ) ,
475+ provideHostRefs ( dirs . home ) ,
476+ ) ;
477+ yield * installedService . install ;
478+
479+ const uninstallCommands : Array < RecordedCommand > = [ ] ;
480+ const failingService = yield * BootService . make ( {
481+ baseDir : dirs . baseDir ,
482+ logsDir : dirs . logsDir ,
483+ cliVersion : "0.0.27" ,
484+ host : makeHost ( dirs . stableEntry ) ,
485+ } ) . pipe (
486+ Effect . provide (
487+ makeRecordingRunnerLayer ( uninstallCommands , {
488+ failWhen : ( command , args ) =>
489+ command === "systemctl" && args . includes ( "disable" ) && args . includes ( "--now" ) ,
490+ } ) ,
491+ ) ,
492+ provideHostRefs ( dirs . home ) ,
493+ ) ;
494+
495+ const error = yield * failingService . uninstall . pipe ( Effect . flip ) ;
496+
497+ assert . isTrue ( isCommandError ( error ) ) ;
498+ assert . isTrue (
499+ yield * fs . exists ( path . join ( dirs . home , ".config" , "systemd" , "user" , "t3code.service" ) ) ,
500+ ) ;
379501 } ) ,
380502 ) ;
381503
0 commit comments