import * as NodeServices from "@effect/platform-node/NodeServices";import { expect, it } from "@effect/vitest";import { HostProcessArguments, HostProcessExecutablePath, HostProcessPlatform, HostProcessUserId,} from "@t3tools/shared/hostProcess";import * as ConfigProvider from "effect/ConfigProvider";import * as Duration from "effect/Duration";import * as Effect from "effect/Effect";import * as FileSystem from "effect/FileSystem";import * as Layer from "effect/Layer";import * as Path from "effect/Path";import * as ChildProcessSpawner from "effect/unstable/process/ChildProcessSpawner"; import * as ProcessRunner from "../processRunner.ts";import * as BootService from "./bootService.ts";import { pinnedRuntimePaths } from "./pinnedRuntime.ts";import { parseServiceState, SERVICE_LAUNCHER_PROTOCOL, serviceStateHasPendingUpdate,} from "./serviceProtocol.ts"; it("keeps systemd pinned to the stable launcher rather than a versioned server", () => { const unit = BootService.renderBootServiceUnit({ nodePath: "/usr/bin/node", launcherPath: "/home/theo/.t3/runtime/service-launcher.mjs", baseDir: "/home/theo/.t3", logPath: "/home/theo/.t3/userdata/logs/boot-service.log", unitPath: "/home/theo/.config/systemd/user/t3code.service", }); expect(unit).toContain("ExecStart=/usr/bin/node /home/theo/.t3/runtime/service-launcher.mjs"); expect(unit).toContain("KillMode=mixed"); expect(unit).not.toContain("versions/1.2.3");}); it("survives the kernel OOM-killing a greedy agent child", () => { const unit = BootService.renderBootServiceUnit({ nodePath: "/usr/bin/node", launcherPath: "/home/theo/.t3/runtime/service-launcher.mjs", baseDir: "/home/theo/.t3", logPath: "/home/theo/.t3/userdata/logs/boot-service.log", unitPath: "/home/theo/.config/systemd/user/t3code.service", }); expect(unit).toContain("OOMPolicy=continue");}); const macPlan = { nodePath: "/opt/homebrew/bin/node", launcherPath: "/Users/theo/.t3/runtime/service-launcher.mjs", baseDir: "/Users/theo/.t3", logPath: "/Users/theo/.t3/userdata/logs/boot-service.log", unitPath: "/Users/theo/Library/LaunchAgents/com.t3tools.t3code.service.plist",};const macInstallerPath = "/opt/homebrew/bin:/Users/theo/.npm-global/bin:/Users/theo/.nvm/versions/node/v22.16.0/bin:/usr/bin:/bin";const macRenderOptions = { homeDir: "/Users/theo", environmentPath: macInstallerPath }; it("keeps launchd pinned to the stable launcher rather than a versioned server", () => { const plist = BootService.renderBootServicePlist(macPlan, macRenderOptions); expect(plist).toContain("<string>/opt/homebrew/bin/node</string>"); expect(plist).toContain("<string>/Users/theo/.t3/runtime/service-launcher.mjs</string>"); expect(plist).not.toContain("versions/1.2.3");}); it("preserves the installer's provider search path in the launch agent", () => { const plist = BootService.renderBootServicePlist(macPlan, macRenderOptions); expect(plist).toContain(` <key>PATH</key>\n <string>${macInstallerPath}</string>`);}); it("restarts the launch agent on the systemd cadence", () => { const plist = BootService.renderBootServicePlist(macPlan, macRenderOptions); expect(plist).toContain("<key>RunAtLoad</key>\n <true/>"); expect(plist).toContain("<key>KeepAlive</key>\n <true/>"); expect(plist).toContain("<key>ThrottleInterval</key>\n <integer>5</integer>"); expect(plist).toContain("<key>ExitTimeOut</key>\n <integer>90</integer>");}); it("appends both stdio streams to the boot service log", () => { const plist = BootService.renderBootServicePlist(macPlan, macRenderOptions); expect(plist).toContain( "<key>StandardOutPath</key>\n <string>/Users/theo/.t3/userdata/logs/boot-service.log</string>", ); expect(plist).toContain( "<key>StandardErrorPath</key>\n <string>/Users/theo/.t3/userdata/logs/boot-service.log</string>", );}); it("escapes XML in host paths", () => { const plist = BootService.renderBootServicePlist( { ...macPlan, baseDir: "/Users/theo/T3 & <Co>" }, { homeDir: "/Users/theo", environmentPath: "/Users/theo/Tools & <Scripts>:/usr/bin" }, ); expect(plist).toContain("<string>/Users/theo/T3 & <Co></string>"); expect(plist).toContain("<string>/Users/theo/Tools & <Scripts>:/usr/bin</string>");}); const makeHarness = Effect.fn("test.make_boot_service_harness")(function* ( platform: NodeJS.Platform = "linux", usePinnedLauncher = false, installerPath = macInstallerPath,) { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; const home = yield* fs.makeTempDirectoryScoped({ prefix: "t3-boot-service-test-" }); const baseDir = path.join(home, ".t3"); const sourceLauncher = path.join(home, "service-launcher.mjs"); const statePath = path.join(baseDir, "runtime", "service-state.json"); yield* fs.writeFileString(sourceLauncher, "export {};\n"); const runtime = pinnedRuntimePaths(path, baseDir, "1.2.3"); yield* fs.makeDirectory(path.dirname(runtime.entryPath), { recursive: true }); yield* fs.writeFileString(runtime.entryPath, "export {};\n"); yield* fs.writeFileString( path.join(path.dirname(runtime.entryPath), "service-launcher.mjs"), "export const source = 'pinned runtime';\n", ); yield* fs.writeFileString(runtime.sentinelPath, "1.2.3\n"); const commands: string[] = []; const timeouts = new Map<string, unknown>(); const control: { failCommand: string | undefined; stateAfterStop?: string; linger: string; enabled: boolean; active: boolean; } = { failCommand: undefined, linger: "yes", enabled: true, active: true, }; const runner = ProcessRunner.ProcessRunner.of({ run: Effect.fn("test.run_boot_service_command")(function* ( input: ProcessRunner.ProcessRunInput, ) { const command = `${input.command} ${input.args.join(" ")}`; commands.push(command); timeouts.set(command, input.timeout); const failed = command === control.failCommand; if (!failed && command === "loginctl enable-linger --no-ask-password 501") control.linger = "yes"; if (!failed && command === "systemctl --user enable t3code.service") control.enabled = true; if (!failed && command === "systemctl --user restart t3code.service") control.active = true; if ( control.stateAfterStop !== undefined && (command === "systemctl --user stop t3code.service" || command.startsWith("launchctl bootout --wait ")) ) { yield* fs.writeFileString(statePath, control.stateAfterStop).pipe(Effect.orDie); } return { stdout: input.args[1] === "--version" ? "t3 v1.2.3\n" : input.command === "loginctl" && input.args[0] === "show-user" ? `${control.linger}\n` : input.args[1] === "is-enabled" ? control.enabled ? "enabled\n" : "disabled\n" : "", stderr: "", code: ChildProcessSpawner.ExitCode( failed || (input.args[1] === "is-active" && !control.active) ? 1 : 0, ), timedOut: false, stdoutTruncated: false, stderrTruncated: false, stdoutInvalidUtf8: false, stderrInvalidUtf8: false, }; }), }); const makeService = (environmentPath = installerPath) => BootService.make({ baseDir, logsDir: path.join(baseDir, "userdata", "logs"), cliVersion: "1.2.3", host: { execPath: "/usr/bin/node", ...(usePinnedLauncher ? {} : { launcherSourcePath: sourceLauncher }), }, }).pipe( Effect.provideService(ProcessRunner.ProcessRunner, runner), Effect.provide( Layer.mergeAll( Layer.succeed(HostProcessPlatform, platform), Layer.succeed(HostProcessUserId, 501), Layer.succeed(HostProcessExecutablePath, "/usr/bin/node"), Layer.succeed(HostProcessArguments, ["/usr/bin/node", path.join(home, "bin.mjs")]), ConfigProvider.layer( ConfigProvider.fromEnv({ env: { HOME: home, ...(environmentPath === "" ? {} : { PATH: environmentPath }) }, }), ), ), ), ); const service = yield* makeService(); return { service, makeService, fs, statePath, commands, timeouts, control, runtime };}); it.layer(NodeServices.layer)("boot service install", (it) => { it.effect( "fails before installing files or validating a runtime when lingering needs an administrator", () => Effect.gen(function* () { const { service, fs, statePath, commands, control, runtime } = yield* makeHarness(); const before = yield* service.status; control.linger = "no"; control.failCommand = "loginctl enable-linger --no-ask-password 501"; yield* fs.remove(runtime.sentinelPath); const error = yield* service.install().pipe(Effect.flip); expect(error).toMatchObject({ _tag: "BootServicePrerequisiteError", problem: "linger-disabled", }); expect(error.message).toContain('sudo loginctl enable-linger "$(id -un)"'); expect(error.message).toContain("last login session ends"); expect(yield* fs.exists(before.unitPath)).toBe(false); expect(yield* fs.exists(statePath)).toBe(false); expect( commands.some((command) => command.startsWith("npm ") || command.includes("--version")), ).toBe(false); expect( commands.some( (command) => command.includes("daemon-reload") || command.includes("restart"), ), ).toBe(false); expect(yield* fs.readFileString(before.logPath)).toContain("[linger-disabled]"); }), ); it.effect( "detects a partial install and preserves the running service when repair lacks permission", () => Effect.gen(function* () { const { service, fs, statePath, commands, control } = yield* makeHarness(); const plan = yield* service.install(); const before = yield* fs.readFileString(statePath); const unit = yield* fs.readFileString(plan.unitPath); control.linger = "no"; control.failCommand = "loginctl enable-linger --no-ask-password 501"; expect(yield* service.status).toMatchObject({ current: false, problems: ["linger-disabled"], }); commands.length = 0; expect((yield* service.install().pipe(Effect.flip))._tag).toBe( "BootServicePrerequisiteError", ); expect(yield* fs.readFileString(statePath)).toBe(before); expect(yield* fs.readFileString(plan.unitPath)).toBe(unit); expect(commands).not.toContain("systemctl --user stop t3code.service"); }), ); it.effect("enables lingering before installing and repairs stopped or disabled services", () => Effect.gen(function* () { const { service, commands, control } = yield* makeHarness(); control.linger = "no"; yield* service.install(); expect(control.linger).toBe("yes"); expect(commands.indexOf("loginctl enable-linger --no-ask-password 501")).toBeLessThan( commands.indexOf("systemctl --user daemon-reload"), ); control.enabled = false; control.active = false; expect(yield* service.status).toMatchObject({ current: false, problems: ["service-disabled", "service-stopped"], }); yield* service.install(); expect((yield* service.status).current).toBe(true); }), ); it.effect.each([ { command: "systemctl --user show-environment", problem: "user-manager-unavailable" }, { command: "loginctl show-user 501 --property=Linger --value", problem: "linger-unavailable" }, ])("reports failed prerequisite probes without installing: $command", ({ command, problem }) => Effect.gen(function* () { const { service, fs, statePath, control } = yield* makeHarness(); control.failCommand = command; expect(yield* service.install().pipe(Effect.flip)).toMatchObject({ _tag: "BootServicePrerequisiteError", problem, }); expect(yield* fs.exists(statePath)).toBe(false); }), ); it.effect("installs, reports current state, and uninstalls", () => Effect.gen(function* () { const { service, fs, statePath, commands, timeouts } = yield* makeHarness(); const plan = yield* service.install(); expect(parseServiceState(yield* fs.readFileString(statePath))).toEqual({ protocol: SERVICE_LAUNCHER_PROTOCOL, activeVersion: "1.2.3", }); expect(yield* fs.readFileString(plan.launcherPath)).toBe("export {};\n"); expect(yield* service.status).toMatchObject({ current: true, installedVersion: "1.2.3", }); // @effect-diagnostics-next-line preferSchemaOverJson:off - fixed launcher-owned test document. const pendingState = JSON.stringify({ protocol: SERVICE_LAUNCHER_PROTOCOL, activeVersion: "1.2.3", update: { id: "u", fromVersion: "1.2.3", targetVersion: "1.2.4", dbPath: "/tmp/state.sqlite", status: "pending", }, }); yield* fs.writeFileString(statePath, pendingState); expect((yield* service.status).current).toBe(false); expect(yield* service.uninstall).toBe(true); expect((yield* service.status).installed).toBe(false); expect(commands.some((command) => command.startsWith("npm "))).toBe(false); // The stop can block up to systemd's 90s TimeoutStopSec; the runner's // 60s default would cancel it mid-shutdown. expect(timeouts.get("systemctl --user disable --now t3code.service")).toEqual( Duration.seconds(120), ); }), ); it.effect.each(["linux", "darwin"] as const)( "reports the installed version across launcher protocols on %s", (platform) => Effect.gen(function* () { const { service, fs, statePath } = yield* makeHarness(platform); yield* service.install(); for (const protocol of [SERVICE_LAUNCHER_PROTOCOL - 1, SERVICE_LAUNCHER_PROTOCOL + 1]) { yield* fs.writeFileString( statePath, `{"protocol":${protocol},"activeVersion":"1.2.4-nightly.1","update":{"status":"unknown"}}`, ); expect(yield* service.status).toMatchObject({ current: false, installedVersion: "1.2.4-nightly.1", }); } }), ); it.effect("reports an unknown version for invalid service state", () => Effect.gen(function* () { const { service, fs, statePath } = yield* makeHarness(); yield* service.install(); for (const stateText of [ "{", '{"activeVersion":"latest"}', '{"activeVersion":"1.2"}', '{"activeVersion":123}', ]) { yield* fs.writeFileString(statePath, stateText); const status = yield* service.status; expect(status.current).toBe(false); expect(status.installedVersion).toBeUndefined(); } }), ); it.effect.each(["linux", "darwin"] as const)( "preserves a newer version that finishes updating during stop on %s", (platform) => Effect.gen(function* () { const { service, fs, statePath, commands, control } = yield* makeHarness(platform); const plan = yield* service.install(); const launcher = yield* fs.readFileString(plan.launcherPath); const unit = yield* fs.readFileString(plan.unitPath); control.stateAfterStop = `{"protocol":${SERVICE_LAUNCHER_PROTOCOL + 1},"activeVersion":"1.2.4"}`; commands.length = 0; const error = yield* service.install().pipe(Effect.flip); expect(error).toMatchObject({ _tag: "BootServiceDowngradeRefusedError", installedVersion: "1.2.4", targetVersion: "1.2.3", }); expect(yield* fs.readFileString(statePath)).toBe(control.stateAfterStop); expect(yield* fs.readFileString(plan.launcherPath)).toBe(launcher); expect(yield* fs.readFileString(plan.unitPath)).toBe(unit); expect( commands.filter( (command) => command.startsWith(platform === "linux" ? "systemctl " : "launchctl ") && !command.includes("show-environment"), ), ).toEqual( platform === "linux" ? ["systemctl --user stop t3code.service", "systemctl --user restart t3code.service"] : [ "launchctl bootout --wait gui/501/com.t3tools.t3code.service", `launchctl bootstrap gui/501 ${plan.unitPath}`, ], ); }), ); it.effect("allows an explicit downgrade", () => Effect.gen(function* () { const { service, fs, statePath } = yield* makeHarness(); yield* service.install(); yield* fs.writeFileString( statePath, `{"protocol":${SERVICE_LAUNCHER_PROTOCOL},"activeVersion":"1.2.4"}`, ); yield* service.install({ allowDowngrade: true }); expect(parseServiceState(yield* fs.readFileString(statePath))?.activeVersion).toBe("1.2.3"); expect((yield* service.status).current).toBe(true); }), ); it.effect("repairs versions with equal SemVer precedence without an override", () => Effect.gen(function* () { const { service, fs, statePath } = yield* makeHarness(); yield* service.install(); yield* fs.writeFileString( statePath, `{"protocol":${SERVICE_LAUNCHER_PROTOCOL},"activeVersion":"1.2.3+previous-build"}`, ); yield* service.install(); expect((yield* service.status).current).toBe(true); }), ); it.effect("copies the launcher from the prepared pinned runtime", () => Effect.gen(function* () { const { service, fs } = yield* makeHarness("linux", true); const plan = yield* service.install(); expect(yield* fs.readFileString(plan.launcherPath)).toBe( "export const source = 'pinned runtime';\n", ); }), ); it.effect("restarts an installed service when repair fails", () => Effect.gen(function* () { const { service, commands, control } = yield* makeHarness(); yield* service.install(); commands.length = 0; control.failCommand = "systemctl --user daemon-reload"; const error = yield* service.install().pipe(Effect.flip); expect(error._tag).toBe("BootServiceCommandError"); expect( commands.filter( (command) => command.startsWith("systemctl ") && !command.includes("show-environment"), ), ).toEqual([ "systemctl --user stop t3code.service", "systemctl --user daemon-reload", "systemctl --user restart t3code.service", ]); }), ); it.effect("restarts without overwriting a pending remote update", () => Effect.gen(function* () { const { service, fs, statePath, commands } = yield* makeHarness(); yield* service.install(); // @effect-diagnostics-next-line preferSchemaOverJson:off - fixed launcher-owned test document. const pendingState = JSON.stringify({ protocol: SERVICE_LAUNCHER_PROTOCOL - 1, activeVersion: "1.2.3", update: { id: "remote-update", fromVersion: "1.2.3", targetVersion: "1.2.4", status: "pending", }, });Lines 1–500 of 677. Load more
import * as NodeServices from "@effect/platform-node/NodeServices"; import { expect, it } from "@effect/vitest"; import { HostProcessArguments, HostProcessExecutablePath, HostProcessPlatform, HostProcessUserId, } from "@t3tools/shared/hostProcess"; import * as ConfigProvider from "effect/ConfigProvider"; import * as Duration from "effect/Duration"; import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; import * as Layer from "effect/Layer"; import * as Path from "effect/Path"; import * as ChildProcessSpawner from "effect/unstable/process/ChildProcessSpawner"; import * as ProcessRunner from "../processRunner.ts"; import * as BootService from "./bootService.ts"; import { pinnedRuntimePaths } from "./pinnedRuntime.ts"; import { parseServiceState, SERVICE_LAUNCHER_PROTOCOL, serviceStateHasPendingUpdate, } from "./serviceProtocol.ts"; it("keeps systemd pinned to the stable launcher rather than a versioned server", () => { const unit = BootService.renderBootServiceUnit({ nodePath: "/usr/bin/node", launcherPath: "/home/theo/.t3/runtime/service-launcher.mjs", baseDir: "/home/theo/.t3", logPath: "/home/theo/.t3/userdata/logs/boot-service.log", unitPath: "/home/theo/.config/systemd/user/t3code.service", }); expect(unit).toContain("ExecStart=/usr/bin/node /home/theo/.t3/runtime/service-launcher.mjs"); expect(unit).toContain("KillMode=mixed"); expect(unit).not.toContain("versions/1.2.3"); }); it("survives the kernel OOM-killing a greedy agent child", () => { const unit = BootService.renderBootServiceUnit({ nodePath: "/usr/bin/node", launcherPath: "/home/theo/.t3/runtime/service-launcher.mjs", baseDir: "/home/theo/.t3", logPath: "/home/theo/.t3/userdata/logs/boot-service.log", unitPath: "/home/theo/.config/systemd/user/t3code.service", }); expect(unit).toContain("OOMPolicy=continue"); }); const macPlan = { nodePath: "/opt/homebrew/bin/node", launcherPath: "/Users/theo/.t3/runtime/service-launcher.mjs", baseDir: "/Users/theo/.t3", logPath: "/Users/theo/.t3/userdata/logs/boot-service.log", unitPath: "/Users/theo/Library/LaunchAgents/com.t3tools.t3code.service.plist", }; const macInstallerPath = "/opt/homebrew/bin:/Users/theo/.npm-global/bin:/Users/theo/.nvm/versions/node/v22.16.0/bin:/usr/bin:/bin"; const macRenderOptions = { homeDir: "/Users/theo", environmentPath: macInstallerPath }; it("keeps launchd pinned to the stable launcher rather than a versioned server", () => { const plist = BootService.renderBootServicePlist(macPlan, macRenderOptions); expect(plist).toContain("<string>/opt/homebrew/bin/node</string>"); expect(plist).toContain("<string>/Users/theo/.t3/runtime/service-launcher.mjs</string>"); expect(plist).not.toContain("versions/1.2.3"); }); it("preserves the installer's provider search path in the launch agent", () => { const plist = BootService.renderBootServicePlist(macPlan, macRenderOptions); expect(plist).toContain(` <key>PATH</key>\n <string>${macInstallerPath}</string>`); }); it("restarts the launch agent on the systemd cadence", () => { const plist = BootService.renderBootServicePlist(macPlan, macRenderOptions); expect(plist).toContain("<key>RunAtLoad</key>\n <true/>"); expect(plist).toContain("<key>KeepAlive</key>\n <true/>"); expect(plist).toContain("<key>ThrottleInterval</key>\n <integer>5</integer>"); expect(plist).toContain("<key>ExitTimeOut</key>\n <integer>90</integer>"); }); it("appends both stdio streams to the boot service log", () => { const plist = BootService.renderBootServicePlist(macPlan, macRenderOptions); expect(plist).toContain( "<key>StandardOutPath</key>\n <string>/Users/theo/.t3/userdata/logs/boot-service.log</string>", ); expect(plist).toContain( "<key>StandardErrorPath</key>\n <string>/Users/theo/.t3/userdata/logs/boot-service.log</string>", ); }); it("escapes XML in host paths", () => { const plist = BootService.renderBootServicePlist( { ...macPlan, baseDir: "/Users/theo/T3 & <Co>" }, { homeDir: "/Users/theo", environmentPath: "/Users/theo/Tools & <Scripts>:/usr/bin" }, ); expect(plist).toContain("<string>/Users/theo/T3 & <Co></string>"); expect(plist).toContain("<string>/Users/theo/Tools & <Scripts>:/usr/bin</string>"); }); const makeHarness = Effect.fn("test.make_boot_service_harness")(function* ( platform: NodeJS.Platform = "linux", usePinnedLauncher = false, installerPath = macInstallerPath, ) { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; const home = yield* fs.makeTempDirectoryScoped({ prefix: "t3-boot-service-test-" }); const baseDir = path.join(home, ".t3"); const sourceLauncher = path.join(home, "service-launcher.mjs"); const statePath = path.join(baseDir, "runtime", "service-state.json"); yield* fs.writeFileString(sourceLauncher, "export {};\n"); const runtime = pinnedRuntimePaths(path, baseDir, "1.2.3"); yield* fs.makeDirectory(path.dirname(runtime.entryPath), { recursive: true }); yield* fs.writeFileString(runtime.entryPath, "export {};\n"); yield* fs.writeFileString( path.join(path.dirname(runtime.entryPath), "service-launcher.mjs"), "export const source = 'pinned runtime';\n", ); yield* fs.writeFileString(runtime.sentinelPath, "1.2.3\n"); const commands: string[] = []; const timeouts = new Map<string, unknown>(); const control: { failCommand: string | undefined; stateAfterStop?: string; linger: string; enabled: boolean; active: boolean; } = { failCommand: undefined, linger: "yes", enabled: true, active: true, }; const runner = ProcessRunner.ProcessRunner.of({ run: Effect.fn("test.run_boot_service_command")(function* ( input: ProcessRunner.ProcessRunInput, ) { const command = `${input.command} ${input.args.join(" ")}`; commands.push(command); timeouts.set(command, input.timeout); const failed = command === control.failCommand; if (!failed && command === "loginctl enable-linger --no-ask-password 501") control.linger = "yes"; if (!failed && command === "systemctl --user enable t3code.service") control.enabled = true; if (!failed && command === "systemctl --user restart t3code.service") control.active = true; if ( control.stateAfterStop !== undefined && (command === "systemctl --user stop t3code.service" || command.startsWith("launchctl bootout --wait ")) ) { yield* fs.writeFileString(statePath, control.stateAfterStop).pipe(Effect.orDie); } return { stdout: input.args[1] === "--version" ? "t3 v1.2.3\n" : input.command === "loginctl" && input.args[0] === "show-user" ? `${control.linger}\n` : input.args[1] === "is-enabled" ? control.enabled ? "enabled\n" : "disabled\n" : "", stderr: "", code: ChildProcessSpawner.ExitCode( failed || (input.args[1] === "is-active" && !control.active) ? 1 : 0, ), timedOut: false, stdoutTruncated: false, stderrTruncated: false, stdoutInvalidUtf8: false, stderrInvalidUtf8: false, }; }), }); const makeService = (environmentPath = installerPath) => BootService.make({ baseDir, logsDir: path.join(baseDir, "userdata", "logs"), cliVersion: "1.2.3", host: { execPath: "/usr/bin/node", ...(usePinnedLauncher ? {} : { launcherSourcePath: sourceLauncher }), }, }).pipe( Effect.provideService(ProcessRunner.ProcessRunner, runner), Effect.provide( Layer.mergeAll( Layer.succeed(HostProcessPlatform, platform), Layer.succeed(HostProcessUserId, 501), Layer.succeed(HostProcessExecutablePath, "/usr/bin/node"), Layer.succeed(HostProcessArguments, ["/usr/bin/node", path.join(home, "bin.mjs")]), ConfigProvider.layer( ConfigProvider.fromEnv({ env: { HOME: home, ...(environmentPath === "" ? {} : { PATH: environmentPath }) }, }), ), ), ), ); const service = yield* makeService(); return { service, makeService, fs, statePath, commands, timeouts, control, runtime }; }); it.layer(NodeServices.layer)("boot service install", (it) => { it.effect( "fails before installing files or validating a runtime when lingering needs an administrator", () => Effect.gen(function* () { const { service, fs, statePath, commands, control, runtime } = yield* makeHarness(); const before = yield* service.status; control.linger = "no"; control.failCommand = "loginctl enable-linger --no-ask-password 501"; yield* fs.remove(runtime.sentinelPath); const error = yield* service.install().pipe(Effect.flip); expect(error).toMatchObject({ _tag: "BootServicePrerequisiteError", problem: "linger-disabled", }); expect(error.message).toContain('sudo loginctl enable-linger "$(id -un)"'); expect(error.message).toContain("last login session ends"); expect(yield* fs.exists(before.unitPath)).toBe(false); expect(yield* fs.exists(statePath)).toBe(false); expect( commands.some((command) => command.startsWith("npm ") || command.includes("--version")), ).toBe(false); expect( commands.some( (command) => command.includes("daemon-reload") || command.includes("restart"), ), ).toBe(false); expect(yield* fs.readFileString(before.logPath)).toContain("[linger-disabled]"); }), ); it.effect( "detects a partial install and preserves the running service when repair lacks permission", () => Effect.gen(function* () { const { service, fs, statePath, commands, control } = yield* makeHarness(); const plan = yield* service.install(); const before = yield* fs.readFileString(statePath); const unit = yield* fs.readFileString(plan.unitPath); control.linger = "no"; control.failCommand = "loginctl enable-linger --no-ask-password 501"; expect(yield* service.status).toMatchObject({ current: false, problems: ["linger-disabled"], }); commands.length = 0; expect((yield* service.install().pipe(Effect.flip))._tag).toBe( "BootServicePrerequisiteError", ); expect(yield* fs.readFileString(statePath)).toBe(before); expect(yield* fs.readFileString(plan.unitPath)).toBe(unit); expect(commands).not.toContain("systemctl --user stop t3code.service"); }), ); it.effect("enables lingering before installing and repairs stopped or disabled services", () => Effect.gen(function* () { const { service, commands, control } = yield* makeHarness(); control.linger = "no"; yield* service.install(); expect(control.linger).toBe("yes"); expect(commands.indexOf("loginctl enable-linger --no-ask-password 501")).toBeLessThan( commands.indexOf("systemctl --user daemon-reload"), ); control.enabled = false; control.active = false; expect(yield* service.status).toMatchObject({ current: false, problems: ["service-disabled", "service-stopped"], }); yield* service.install(); expect((yield* service.status).current).toBe(true); }), ); it.effect.each([ { command: "systemctl --user show-environment", problem: "user-manager-unavailable" }, { command: "loginctl show-user 501 --property=Linger --value", problem: "linger-unavailable" }, ])("reports failed prerequisite probes without installing: $command", ({ command, problem }) => Effect.gen(function* () { const { service, fs, statePath, control } = yield* makeHarness(); control.failCommand = command; expect(yield* service.install().pipe(Effect.flip)).toMatchObject({ _tag: "BootServicePrerequisiteError", problem, }); expect(yield* fs.exists(statePath)).toBe(false); }), ); it.effect("installs, reports current state, and uninstalls", () => Effect.gen(function* () { const { service, fs, statePath, commands, timeouts } = yield* makeHarness(); const plan = yield* service.install(); expect(parseServiceState(yield* fs.readFileString(statePath))).toEqual({ protocol: SERVICE_LAUNCHER_PROTOCOL, activeVersion: "1.2.3", }); expect(yield* fs.readFileString(plan.launcherPath)).toBe("export {};\n"); expect(yield* service.status).toMatchObject({ current: true, installedVersion: "1.2.3", }); // @effect-diagnostics-next-line preferSchemaOverJson:off - fixed launcher-owned test document. const pendingState = JSON.stringify({ protocol: SERVICE_LAUNCHER_PROTOCOL, activeVersion: "1.2.3", update: { id: "u", fromVersion: "1.2.3", targetVersion: "1.2.4", dbPath: "/tmp/state.sqlite", status: "pending", }, }); yield* fs.writeFileString(statePath, pendingState); expect((yield* service.status).current).toBe(false); expect(yield* service.uninstall).toBe(true); expect((yield* service.status).installed).toBe(false); expect(commands.some((command) => command.startsWith("npm "))).toBe(false); // The stop can block up to systemd's 90s TimeoutStopSec; the runner's // 60s default would cancel it mid-shutdown. expect(timeouts.get("systemctl --user disable --now t3code.service")).toEqual( Duration.seconds(120), ); }), ); it.effect.each(["linux", "darwin"] as const)( "reports the installed version across launcher protocols on %s", (platform) => Effect.gen(function* () { const { service, fs, statePath } = yield* makeHarness(platform); yield* service.install(); for (const protocol of [SERVICE_LAUNCHER_PROTOCOL - 1, SERVICE_LAUNCHER_PROTOCOL + 1]) { yield* fs.writeFileString( statePath, `{"protocol":${protocol},"activeVersion":"1.2.4-nightly.1","update":{"status":"unknown"}}`, ); expect(yield* service.status).toMatchObject({ current: false, installedVersion: "1.2.4-nightly.1", }); } }), ); it.effect("reports an unknown version for invalid service state", () => Effect.gen(function* () { const { service, fs, statePath } = yield* makeHarness(); yield* service.install(); for (const stateText of [ "{", '{"activeVersion":"latest"}', '{"activeVersion":"1.2"}', '{"activeVersion":123}', ]) { yield* fs.writeFileString(statePath, stateText); const status = yield* service.status; expect(status.current).toBe(false); expect(status.installedVersion).toBeUndefined(); } }), ); it.effect.each(["linux", "darwin"] as const)( "preserves a newer version that finishes updating during stop on %s", (platform) => Effect.gen(function* () { const { service, fs, statePath, commands, control } = yield* makeHarness(platform); const plan = yield* service.install(); const launcher = yield* fs.readFileString(plan.launcherPath); const unit = yield* fs.readFileString(plan.unitPath); control.stateAfterStop = `{"protocol":${SERVICE_LAUNCHER_PROTOCOL + 1},"activeVersion":"1.2.4"}`; commands.length = 0; const error = yield* service.install().pipe(Effect.flip); expect(error).toMatchObject({ _tag: "BootServiceDowngradeRefusedError", installedVersion: "1.2.4", targetVersion: "1.2.3", }); expect(yield* fs.readFileString(statePath)).toBe(control.stateAfterStop); expect(yield* fs.readFileString(plan.launcherPath)).toBe(launcher); expect(yield* fs.readFileString(plan.unitPath)).toBe(unit); expect( commands.filter( (command) => command.startsWith(platform === "linux" ? "systemctl " : "launchctl ") && !command.includes("show-environment"), ), ).toEqual( platform === "linux" ? ["systemctl --user stop t3code.service", "systemctl --user restart t3code.service"] : [ "launchctl bootout --wait gui/501/com.t3tools.t3code.service", `launchctl bootstrap gui/501 ${plan.unitPath}`, ], ); }), ); it.effect("allows an explicit downgrade", () => Effect.gen(function* () { const { service, fs, statePath } = yield* makeHarness(); yield* service.install(); yield* fs.writeFileString( statePath, `{"protocol":${SERVICE_LAUNCHER_PROTOCOL},"activeVersion":"1.2.4"}`, ); yield* service.install({ allowDowngrade: true }); expect(parseServiceState(yield* fs.readFileString(statePath))?.activeVersion).toBe("1.2.3"); expect((yield* service.status).current).toBe(true); }), ); it.effect("repairs versions with equal SemVer precedence without an override", () => Effect.gen(function* () { const { service, fs, statePath } = yield* makeHarness(); yield* service.install(); yield* fs.writeFileString( statePath, `{"protocol":${SERVICE_LAUNCHER_PROTOCOL},"activeVersion":"1.2.3+previous-build"}`, ); yield* service.install(); expect((yield* service.status).current).toBe(true); }), ); it.effect("copies the launcher from the prepared pinned runtime", () => Effect.gen(function* () { const { service, fs } = yield* makeHarness("linux", true); const plan = yield* service.install(); expect(yield* fs.readFileString(plan.launcherPath)).toBe( "export const source = 'pinned runtime';\n", ); }), ); it.effect("restarts an installed service when repair fails", () => Effect.gen(function* () { const { service, commands, control } = yield* makeHarness(); yield* service.install(); commands.length = 0; control.failCommand = "systemctl --user daemon-reload"; const error = yield* service.install().pipe(Effect.flip); expect(error._tag).toBe("BootServiceCommandError"); expect( commands.filter( (command) => command.startsWith("systemctl ") && !command.includes("show-environment"), ), ).toEqual([ "systemctl --user stop t3code.service", "systemctl --user daemon-reload", "systemctl --user restart t3code.service", ]); }), ); it.effect("restarts without overwriting a pending remote update", () => Effect.gen(function* () { const { service, fs, statePath, commands } = yield* makeHarness(); yield* service.install(); // @effect-diagnostics-next-line preferSchemaOverJson:off - fixed launcher-owned test document. const pendingState = JSON.stringify({ protocol: SERVICE_LAUNCHER_PROTOCOL - 1, activeVersion: "1.2.3", update: { id: "remote-update", fromVersion: "1.2.3", targetVersion: "1.2.4", status: "pending", }, });