import * as NodeServices from "@effect/platform-node/NodeServices"; import { expect, it } from "@effect/vitest"; import { HostProcessArguments, HostProcessExecutablePath, HostProcessPlatform, } from "@t3tools/shared/hostProcess"; import * as ConfigProvider from "effect/ConfigProvider"; 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 makeHarness = Effect.fn("test.make_boot_service_harness")(function* ( platform: NodeJS.Platform = "linux", usePinnedLauncher = false, ) { 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 control: { failCommand: string | undefined } = { failCommand: undefined }; const runner = ProcessRunner.ProcessRunner.of({ run: (input) => Effect.sync(() => { const command = `${input.command} ${input.args.join(" ")}`; commands.push(command); return { stdout: input.args[1] === "--version" ? "t3 v1.2.3\n" : "", stderr: "", code: ChildProcessSpawner.ExitCode(command === control.failCommand ? 1 : 0), timedOut: false, stdoutTruncated: false, stderrTruncated: false, }; }), }); const service = yield* 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(HostProcessExecutablePath, "/usr/bin/node"), Layer.succeed(HostProcessArguments, ["/usr/bin/node", path.join(home, "bin.mjs")]), ConfigProvider.layer(ConfigProvider.fromEnv({ env: { HOME: home } })), ), ), ); return { service, fs, statePath, commands, control }; }); it.layer(NodeServices.layer)("boot service install", (it) => { it.effect("installs, reports current state, and uninstalls", () => Effect.gen(function* () { const { service, fs, statePath, commands } = 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).current).toBe(true); // @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); }), ); 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 "))).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", }, }); yield* fs.writeFileString(statePath, pendingState); commands.length = 0; expect((yield* service.install.pipe(Effect.flip))._tag).toBe("BootServiceUpdatePendingError"); expect(serviceStateHasPendingUpdate(yield* fs.readFileString(statePath))).toBe(true); expect(commands.filter((command) => command.startsWith("systemctl "))).toEqual([ "systemctl --user stop t3code.service", "systemctl --user restart t3code.service", ]); }), ); it.effect("fails closed off Linux", () => Effect.gen(function* () { const { service } = yield* makeHarness("darwin"); expect((yield* service.status).supported).toBe(false); expect((yield* service.install.pipe(Effect.flip))._tag).toBe("BootServiceUnsupportedError"); }), ); });