| ... | @@ -56,17 +56,26 @@ const macPlan = { | ... | @@ -56,17 +56,26 @@ const macPlan = { |
| 56 | logPath: "/Users/theo/.t3/userdata/logs/boot-service.log", | 56 | logPath: "/Users/theo/.t3/userdata/logs/boot-service.log", |
| 57 | unitPath: "/Users/theo/Library/LaunchAgents/com.t3tools.t3code.service.plist", | 57 | unitPath: "/Users/theo/Library/LaunchAgents/com.t3tools.t3code.service.plist", |
| 58 | }; | 58 | }; |
| | 59 | const macInstallerPath = |
| | 60 | "/opt/homebrew/bin:/Users/theo/.npm-global/bin:/Users/theo/.nvm/versions/node/v22.16.0/bin:/usr/bin:/bin"; |
| | 61 | const macRenderOptions = { homeDir: "/Users/theo", environmentPath: macInstallerPath }; |
| 59 | | 62 | |
| 60 | it("keeps launchd pinned to the stable launcher rather than a versioned server", () => { | 63 | it("keeps launchd pinned to the stable launcher rather than a versioned server", () => { |
| 61 | const plist = BootService.renderBootServicePlist(macPlan, { homeDir: "/Users/theo" }); | 64 | const plist = BootService.renderBootServicePlist(macPlan, macRenderOptions); |
| 62 | | 65 | |
| 63 | expect(plist).toContain("<string>/opt/homebrew/bin/node</string>"); | 66 | expect(plist).toContain("<string>/opt/homebrew/bin/node</string>"); |
| 64 | expect(plist).toContain("<string>/Users/theo/.t3/runtime/service-launcher.mjs</string>"); | 67 | expect(plist).toContain("<string>/Users/theo/.t3/runtime/service-launcher.mjs</string>"); |
| 65 | expect(plist).not.toContain("versions/1.2.3"); | 68 | expect(plist).not.toContain("versions/1.2.3"); |
| 66 | }); | 69 | }); |
| 67 | | 70 | |
| | 71 | it("preserves the installer's provider search path in the launch agent", () => { |
| | 72 | const plist = BootService.renderBootServicePlist(macPlan, macRenderOptions); |
| | 73 | |
| | 74 | expect(plist).toContain(` <key>PATH</key>\n <string>${macInstallerPath}</string>`); |
| | 75 | }); |
| | 76 | |
| 68 | it("restarts the launch agent on the systemd cadence", () => { | 77 | it("restarts the launch agent on the systemd cadence", () => { |
| 69 | const plist = BootService.renderBootServicePlist(macPlan, { homeDir: "/Users/theo" }); | 78 | const plist = BootService.renderBootServicePlist(macPlan, macRenderOptions); |
| 70 | | 79 | |
| 71 | expect(plist).toContain("<key>RunAtLoad</key>\n <true/>"); | 80 | expect(plist).toContain("<key>RunAtLoad</key>\n <true/>"); |
| 72 | expect(plist).toContain("<key>KeepAlive</key>\n <true/>"); | 81 | expect(plist).toContain("<key>KeepAlive</key>\n <true/>"); |
| ... | @@ -75,7 +84,7 @@ it("restarts the launch agent on the systemd cadence", () => { | ... | @@ -75,7 +84,7 @@ it("restarts the launch agent on the systemd cadence", () => { |
| 75 | }); | 84 | }); |
| 76 | | 85 | |
| 77 | it("appends both stdio streams to the boot service log", () => { | 86 | it("appends both stdio streams to the boot service log", () => { |
| 78 | const plist = BootService.renderBootServicePlist(macPlan, { homeDir: "/Users/theo" }); | 87 | const plist = BootService.renderBootServicePlist(macPlan, macRenderOptions); |
| 79 | | 88 | |
| 80 | expect(plist).toContain( | 89 | expect(plist).toContain( |
| 81 | "<key>StandardOutPath</key>\n <string>/Users/theo/.t3/userdata/logs/boot-service.log</string>", | 90 | "<key>StandardOutPath</key>\n <string>/Users/theo/.t3/userdata/logs/boot-service.log</string>", |
| ... | @@ -88,15 +97,17 @@ it("appends both stdio streams to the boot service log", () => { | ... | @@ -88,15 +97,17 @@ it("appends both stdio streams to the boot service log", () => { |
| 88 | it("escapes XML in host paths", () => { | 97 | it("escapes XML in host paths", () => { |
| 89 | const plist = BootService.renderBootServicePlist( | 98 | const plist = BootService.renderBootServicePlist( |
| 90 | { ...macPlan, baseDir: "/Users/theo/T3 & <Co>" }, | 99 | { ...macPlan, baseDir: "/Users/theo/T3 & <Co>" }, |
| 91 | { homeDir: "/Users/theo" }, | 100 | { homeDir: "/Users/theo", environmentPath: "/Users/theo/Tools & <Scripts>:/usr/bin" }, |
| 92 | ); | 101 | ); |
| 93 | | 102 | |
| 94 | expect(plist).toContain("<string>/Users/theo/T3 &amp; &lt;Co&gt;</string>"); | 103 | expect(plist).toContain("<string>/Users/theo/T3 &amp; &lt;Co&gt;</string>"); |
| | 104 | expect(plist).toContain("<string>/Users/theo/Tools &amp; &lt;Scripts&gt;:/usr/bin</string>"); |
| 95 | }); | 105 | }); |
| 96 | | 106 | |
| 97 | const makeHarness = Effect.fn("test.make_boot_service_harness")(function* ( | 107 | const makeHarness = Effect.fn("test.make_boot_service_harness")(function* ( |
| 98 | platform: NodeJS.Platform = "linux", | 108 | platform: NodeJS.Platform = "linux", |
| 99 | usePinnedLauncher = false, | 109 | usePinnedLauncher = false, |
| | 110 | installerPath = macInstallerPath, |
| 100 | ) { | 111 | ) { |
| 101 | const fs = yield* FileSystem.FileSystem; | 112 | const fs = yield* FileSystem.FileSystem; |
| 102 | const path = yield* Path.Path; | 113 | const path = yield* Path.Path; |
| ... | @@ -135,27 +146,33 @@ const makeHarness = Effect.fn("test.make_boot_service_harness")(function* ( | ... | @@ -135,27 +146,33 @@ const makeHarness = Effect.fn("test.make_boot_service_harness")(function* ( |
| 135 | }; | 146 | }; |
| 136 | }), | 147 | }), |
| 137 | }); | 148 | }); |
| 138 | const service = yield* BootService.make({ | 149 | const makeService = (environmentPath = installerPath) => |
| 139 | baseDir, | 150 | BootService.make({ |
| 140 | logsDir: path.join(baseDir, "userdata", "logs"), | 151 | baseDir, |
| 141 | cliVersion: "1.2.3", | 152 | logsDir: path.join(baseDir, "userdata", "logs"), |
| 142 | host: { | 153 | cliVersion: "1.2.3", |
| 143 | execPath: "/usr/bin/node", | 154 | host: { |
| 144 | ...(usePinnedLauncher ? {} : { launcherSourcePath: sourceLauncher }), | 155 | execPath: "/usr/bin/node", |
| 145 | }, | 156 | ...(usePinnedLauncher ? {} : { launcherSourcePath: sourceLauncher }), |
| 146 | }).pipe( | 157 | }, |
| 147 | Effect.provideService(ProcessRunner.ProcessRunner, runner), | 158 | }).pipe( |
| 148 | Effect.provide( | 159 | Effect.provideService(ProcessRunner.ProcessRunner, runner), |
| 149 | Layer.mergeAll( | 160 | Effect.provide( |
| 150 | Layer.succeed(HostProcessPlatform, platform), | 161 | Layer.mergeAll( |
| 151 | Layer.succeed(HostProcessUserId, 501), | 162 | Layer.succeed(HostProcessPlatform, platform), |
| 152 | Layer.succeed(HostProcessExecutablePath, "/usr/bin/node"), | 163 | Layer.succeed(HostProcessUserId, 501), |
| 153 | Layer.succeed(HostProcessArguments, ["/usr/bin/node", path.join(home, "bin.mjs")]), | 164 | Layer.succeed(HostProcessExecutablePath, "/usr/bin/node"), |
| 154 | ConfigProvider.layer(ConfigProvider.fromEnv({ env: { HOME: home } })), | 165 | Layer.succeed(HostProcessArguments, ["/usr/bin/node", path.join(home, "bin.mjs")]), |
| | 166 | ConfigProvider.layer( |
| | 167 | ConfigProvider.fromEnv({ |
| | 168 | env: { HOME: home, ...(environmentPath === "" ? {} : { PATH: environmentPath }) }, |
| | 169 | }), |
| | 170 | ), |
| | 171 | ), |
| 155 | ), | 172 | ), |
| 156 | ), | 173 | ); |
| 157 | ); | 174 | const service = yield* makeService(); |
| 158 | return { service, fs, statePath, commands, timeouts, control }; | 175 | return { service, makeService, fs, statePath, commands, timeouts, control }; |
| 159 | }); | 176 | }); |
| 160 | | 177 | |
| 161 | it.layer(NodeServices.layer)("boot service install", (it) => { | 178 | it.layer(NodeServices.layer)("boot service install", (it) => { |
| ... | @@ -266,6 +283,9 @@ it.layer(NodeServices.layer)("boot service install", (it) => { | ... | @@ -266,6 +283,9 @@ it.layer(NodeServices.layer)("boot service install", (it) => { |
| 266 | expect(plan.unitPath.endsWith("Library/LaunchAgents/com.t3tools.t3code.service.plist")).toBe( | 283 | expect(plan.unitPath.endsWith("Library/LaunchAgents/com.t3tools.t3code.service.plist")).toBe( |
| 267 | true, | 284 | true, |
| 268 | ); | 285 | ); |
| | 286 | expect(yield* fs.readFileString(plan.unitPath)).toContain( |
| | 287 | ` <key>PATH</key>\n <string>${macInstallerPath}:/usr/local/bin:/usr/sbin:/sbin</string>`, |
| | 288 | ); |
| 269 | expect(parseServiceState(yield* fs.readFileString(statePath))).toEqual({ | 289 | expect(parseServiceState(yield* fs.readFileString(statePath))).toEqual({ |
| 270 | protocol: SERVICE_LAUNCHER_PROTOCOL, | 290 | protocol: SERVICE_LAUNCHER_PROTOCOL, |
| 271 | activeVersion: "1.2.3", | 291 | activeVersion: "1.2.3", |
| ... | @@ -303,6 +323,58 @@ it.layer(NodeServices.layer)("boot service install", (it) => { | ... | @@ -303,6 +323,58 @@ it.layer(NodeServices.layer)("boot service install", (it) => { |
| 303 | }), | 323 | }), |
| 304 | ); | 324 | ); |
| 305 | | 325 | |
| | 326 | it.effect("reconstructs a launch agent search path when the installer has no PATH", () => |
| | 327 | Effect.gen(function* () { |
| | 328 | const { service, fs } = yield* makeHarness("darwin", false, ""); |
| | 329 | const plan = yield* service.install; |
| | 330 | |
| | 331 | expect(yield* fs.readFileString(plan.unitPath)).toContain( |
| | 332 | " <key>PATH</key>\n <string>/usr/bin:/opt/homebrew/bin:/usr/local/bin:/bin:/usr/sbin:/sbin</string>", |
| | 333 | ); |
| | 334 | expect((yield* service.status).current).toBe(true); |
| | 335 | }), |
| | 336 | ); |
| | 337 | |
| | 338 | it.effect("adds missing provider directories to a minimal installer PATH", () => |
| | 339 | Effect.gen(function* () { |
| | 340 | const { service, fs } = yield* makeHarness("darwin", false, "/usr/bin:/bin"); |
| | 341 | const plan = yield* service.install; |
| | 342 | |
| | 343 | expect(yield* fs.readFileString(plan.unitPath)).toContain( |
| | 344 | " <key>PATH</key>\n <string>/usr/bin:/bin:/opt/homebrew/bin:/usr/local/bin:/usr/sbin:/sbin</string>", |
| | 345 | ); |
| | 346 | expect((yield* service.status).current).toBe(true); |
| | 347 | }), |
| | 348 | ); |
| | 349 | |
| | 350 | it.effect("keeps an installed launch agent current when the process PATH changes", () => |
| | 351 | Effect.gen(function* () { |
| | 352 | const { service, makeService } = yield* makeHarness("darwin"); |
| | 353 | yield* service.install; |
| | 354 | |
| | 355 | const restartedService = yield* makeService("/usr/local/bin:/usr/bin:/bin"); |
| | 356 | expect((yield* restartedService.status).current).toBe(true); |
| | 357 | }), |
| | 358 | ); |
| | 359 | |
| | 360 | it.effect("drops PATH directories that cannot be represented in a launch agent plist", () => |
| | 361 | Effect.gen(function* () { |
| | 362 | const { service, fs } = yield* makeHarness( |
| | 363 | "darwin", |
| | 364 | false, |
| | 365 | "/opt/homebrew/bin:/Users/theo/\u0001invalid:/usr/bin", |
| | 366 | ); |
| | 367 | const plan = yield* service.install; |
| | 368 | const plist = yield* fs.readFileString(plan.unitPath); |
| | 369 | |
| | 370 | expect(plist).toContain( |
| | 371 | " <key>PATH</key>\n <string>/opt/homebrew/bin:/usr/bin:/usr/local/bin:/bin:/usr/sbin:/sbin</string>", |
| | 372 | ); |
| | 373 | expect(plist).not.toContain("\u0001"); |
| | 374 | expect((yield* service.status).current).toBe(true); |
| | 375 | }), |
| | 376 | ); |
| | 377 | |
| 306 | it.effect("ignores a bootout for an agent that is not loaded", () => | 378 | it.effect("ignores a bootout for an agent that is not loaded", () => |
| 307 | Effect.gen(function* () { | 379 | Effect.gen(function* () { |
| 308 | const { service, control } = yield* makeHarness("darwin"); | 380 | const { service, control } = yield* makeHarness("darwin"); |