| ... | ... | @@ -56,17 +56,26 @@ const macPlan = { |
| 56 | 56 | logPath: "/Users/theo/.t3/userdata/logs/boot-service.log", |
| 57 | 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 | 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 | 66 | expect(plist).toContain("<string>/opt/homebrew/bin/node</string>"); |
| 64 | 67 | expect(plist).toContain("<string>/Users/theo/.t3/runtime/service-launcher.mjs</string>"); |
| 65 | 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 | 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 | 80 | expect(plist).toContain("<key>RunAtLoad</key>\n <true/>"); |
| 72 | 81 | expect(plist).toContain("<key>KeepAlive</key>\n <true/>"); |
| ... | ... | @@ -75,7 +84,7 @@ it("restarts the launch agent on the systemd cadence", () => { |
| 75 | 84 | }); |
| 76 | 85 | |
| 77 | 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 | 89 | expect(plist).toContain( |
| 81 | 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 | 97 | it("escapes XML in host paths", () => { |
| 89 | 98 | const plist = BootService.renderBootServicePlist( |
| 90 | 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 | 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 | 107 | const makeHarness = Effect.fn("test.make_boot_service_harness")(function* ( |
| 98 | 108 | platform: NodeJS.Platform = "linux", |
| 99 | 109 | usePinnedLauncher = false, |
| 110 | installerPath = macInstallerPath, |
| 100 | 111 | ) { |
| 101 | 112 | const fs = yield* FileSystem.FileSystem; |
| 102 | 113 | const path = yield* Path.Path; |
| ... | ... | @@ -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({ |
| 139 | | baseDir, |
| 140 | | logsDir: path.join(baseDir, "userdata", "logs"), |
| 141 | | cliVersion: "1.2.3", |
| 142 | | host: { |
| 143 | | execPath: "/usr/bin/node", |
| 144 | | ...(usePinnedLauncher ? {} : { launcherSourcePath: sourceLauncher }), |
| 145 | | }, |
| 146 | | }).pipe( |
| 147 | | Effect.provideService(ProcessRunner.ProcessRunner, runner), |
| 148 | | Effect.provide( |
| 149 | | Layer.mergeAll( |
| 150 | | Layer.succeed(HostProcessPlatform, platform), |
| 151 | | Layer.succeed(HostProcessUserId, 501), |
| 152 | | Layer.succeed(HostProcessExecutablePath, "/usr/bin/node"), |
| 153 | | Layer.succeed(HostProcessArguments, ["/usr/bin/node", path.join(home, "bin.mjs")]), |
| 154 | | ConfigProvider.layer(ConfigProvider.fromEnv({ env: { HOME: home } })), |
| 149 | const makeService = (environmentPath = installerPath) => |
| 150 | BootService.make({ |
| 151 | baseDir, |
| 152 | logsDir: path.join(baseDir, "userdata", "logs"), |
| 153 | cliVersion: "1.2.3", |
| 154 | host: { |
| 155 | execPath: "/usr/bin/node", |
| 156 | ...(usePinnedLauncher ? {} : { launcherSourcePath: sourceLauncher }), |
| 157 | }, |
| 158 | }).pipe( |
| 159 | Effect.provideService(ProcessRunner.ProcessRunner, runner), |
| 160 | Effect.provide( |
| 161 | Layer.mergeAll( |
| 162 | Layer.succeed(HostProcessPlatform, platform), |
| 163 | Layer.succeed(HostProcessUserId, 501), |
| 164 | Layer.succeed(HostProcessExecutablePath, "/usr/bin/node"), |
| 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 | | ), |
| 157 | | ); |
| 158 | | return { service, fs, statePath, commands, timeouts, control }; |
| 173 | ); |
| 174 | const service = yield* makeService(); |
| 175 | return { service, makeService, fs, statePath, commands, timeouts, control }; |
| 159 | 176 | }); |
| 160 | 177 | |
| 161 | 178 | it.layer(NodeServices.layer)("boot service install", (it) => { |
| ... | ... | @@ -266,6 +283,9 @@ it.layer(NodeServices.layer)("boot service install", (it) => { |
| 266 | 283 | expect(plan.unitPath.endsWith("Library/LaunchAgents/com.t3tools.t3code.service.plist")).toBe( |
| 267 | 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 | 289 | expect(parseServiceState(yield* fs.readFileString(statePath))).toEqual({ |
| 270 | 290 | protocol: SERVICE_LAUNCHER_PROTOCOL, |
| 271 | 291 | activeVersion: "1.2.3", |
| ... | ... | @@ -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 | 378 | it.effect("ignores a bootout for an agent that is not loaded", () => |
| 307 | 379 | Effect.gen(function* () { |
| 308 | 380 | const { service, control } = yield* makeHarness("darwin"); |