249 lines · 8.6 KB · TypeScript
RawDownload
import * as NodeServices from "@effect/platform-node/NodeServices";import { assert, expect, it } from "@effect/vitest";import { HostProcessEnvironment } from "@t3tools/shared/hostProcess";import * as NetService from "@t3tools/shared/Net";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 Terminal from "effect/Terminal";import { Command } from "effect/unstable/cli";import { afterEach, vi } from "vite-plus/test"; import packageJson from "../../package.json" with { type: "json" };import * as BootService from "../cloud/bootService.ts";import {  formatServiceStatus,  offerServiceDuringOnboarding,  reconcileService,  recoverServiceOnboardingOffer,  serviceCommand,} from "./service.ts"; afterEach(() => vi.restoreAllMocks()); const status = {  supported: true,  installed: true,  current: true,  unitPath: "/home/me/.config/systemd/user/t3code.service",  logPath: "/home/me/.t3/userdata/logs/boot-service.log",} as const; it("reports the installed service version and host paths", () => {  assert.equal(    formatServiceStatus(status, "0.0.29"),    [      "T3 Code service",      "  Status: installed · t3@0.0.29",      "  Unit: /home/me/.config/systemd/user/t3code.service",      "  Logs: /home/me/.t3/userdata/logs/boot-service.log",    ].join("\n"),  );}); it("gives a direct repair command for a stale service", () => {  assert.include(    formatServiceStatus({ ...status, current: false }, "0.0.29"),    "Next: Run `npx t3@0.0.29 service update`.",  );}); it("explains an incomplete nightly installation and keeps repair on its installed version", () => {  const output = formatServiceStatus(    {      ...status,      current: false,      installedVersion: "0.0.32-nightly.1",      problems: ["linger-disabled", "service-stopped"],    },    "0.0.32-nightly.1",  );   expect(output).toContain("[linger-disabled]");  expect(output).toContain("last login session ends");  expect(output).toContain('sudo loginctl enable-linger "$(id -un)"');  expect(output).toContain("[service-stopped]");  expect(output).toContain("npx t3@0.0.32-nightly.1 service update");  expect(output).not.toContain("t3@latest");}); it("suggests the newer CLI version when the installed service needs an update", () => {  const output = formatServiceStatus(    { ...status, current: false, installedVersion: "0.0.28" },    "0.0.29",  );  expect(output).toContain("npx t3@0.0.29 service update");  expect(output).not.toContain("npx t3@0.0.28 service update");}); it("explains where the service is supported", () => {  assert.include(    formatServiceStatus({ ...status, supported: false, installed: false }, "0.0.29"),    "Supported on: Linux with systemd, macOS with launchd",  );}); it("reports a newer installed service and gives an exact-version repair command", () => {  const output = formatServiceStatus(    { ...status, current: false, installedVersion: "0.0.32-nightly.1" },    "0.0.31",  );   assert.include(output, "t3@0.0.32-nightly.1 (newer than this t3@0.0.31 CLI)");  assert.include(output, "npx t3@0.0.32-nightly.1 service update");  assert.notInclude(output, "npx t3@latest service update");}); const newerServiceStatus = { ...status, current: false, installedVersion: "999.0.0" }; function makeTestService(serviceStatus: BootService.BootServiceStatus) {  const installOptions: Array<Parameters<BootService.BootService["Service"]["install"]>[0]> = [];  const service = BootService.BootService.of({    status: Effect.succeed(serviceStatus),    install: (options) =>      Effect.sync(() => {        installOptions.push(options);        return {          nodePath: "/test/node",          launcherPath: "/test/service-launcher.mjs",          baseDir: "/test/t3",          unitPath: serviceStatus.unitPath,          logPath: serviceStatus.logPath,        };      }),    uninstall: Effect.succeed(false),  });  return { service, installOptions };} it.layer(Layer.mergeAll(NodeServices.layer, NetService.layer))("service commands", (it) => {  it.effect.each(["install", "update"] as const)(    "%s refuses a downgrade before changing the service",    (command) =>      Effect.gen(function* () {        const fs = yield* FileSystem.FileSystem;        const baseDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-service-cli-test-" });        const { service, installOptions } = makeTestService(newerServiceStatus);        vi.spyOn(BootService, "layer").mockReturnValue(          Layer.succeed(BootService.BootService, service),        );         const error = yield* Command.runWith(serviceCommand, { version: packageJson.version })([          command,          "--base-dir",          baseDir,        ]).pipe(          Effect.provideService(HostProcessEnvironment, {}),          Effect.provide(ConfigProvider.layer(ConfigProvider.fromEnv({ env: {} }))),          Effect.flip,        );         expect(error).toMatchObject({          _tag: "BootServiceDowngradeRefusedError",          installedVersion: "999.0.0",          targetVersion: packageJson.version,        });        expect(installOptions).toEqual([]);      }),  );   it.effect.each(["install", "update"] as const)("%s allows an explicit downgrade", (command) =>    Effect.gen(function* () {      const fs = yield* FileSystem.FileSystem;      const baseDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-service-cli-test-" });      const { service, installOptions } = makeTestService(newerServiceStatus);      vi.spyOn(BootService, "layer").mockReturnValue(        Layer.succeed(BootService.BootService, service),      );       yield* Command.runWith(serviceCommand, { version: packageJson.version })([        command,        "--base-dir",        baseDir,        "--allow-downgrade",      ]).pipe(        Effect.provideService(HostProcessEnvironment, {}),        Effect.provide(ConfigProvider.layer(ConfigProvider.fromEnv({ env: {} }))),      );       expect(installOptions).toEqual([{ allowDowngrade: true }]);    }),  );}); it.effect.each([  { name: "a new service", state: { ...status, installed: false, current: false } },  { name: "an older service", state: { ...status, current: false, installedVersion: "0.0.0" } },  {    name: "the same version",    state: { ...status, current: false, installedVersion: packageJson.version },  },  {    name: "an incomplete install of the same version",    state: {      ...status,      current: false,      installedVersion: packageJson.version,      problems: ["linger-disabled"] as const,    },  },  { name: "an unknown version", state: { ...status, current: false } },])("installs or repairs $name without an override", ({ state }) =>  Effect.gen(function* () {    const { service, installOptions } = makeTestService(state);     const result = yield* reconcileService().pipe(      Effect.provideService(BootService.BootService, service),    );     expect(result.changed).toBe(true);    expect(installOptions).toEqual([undefined]);  }),); it.effect("leaves a newer service unchanged during onboarding without prompting", () =>  Effect.gen(function* () {    const { service, installOptions } = makeTestService(newerServiceStatus);    const terminal = Terminal.make({      columns: Effect.succeed(80),      rows: Effect.succeed(24),      readInput: Effect.die("Onboarding must not prompt to replace a newer service."),      readLine: Effect.die("Onboarding must not prompt to replace a newer service."),      display: () => Effect.die("Onboarding must not prompt to replace a newer service."),    });     const ready = yield* offerServiceDuringOnboarding.pipe(      Effect.provideService(BootService.BootService, service),      Effect.provideService(Terminal.Terminal, terminal),      Effect.provide(NodeServices.layer),    );     expect(ready).toBe(false);    expect(installOptions).toEqual([]);  }),); it.effect("keeps onboarding successful when a newer version appears before install", () =>  Effect.gen(function* () {    const ready = yield* recoverServiceOnboardingOffer(      Effect.fail(        new BootService.BootServiceDowngradeRefusedError({          installedVersion: "999.0.0",          targetVersion: packageJson.version,        }),      ),    );     expect(ready).toBe(false);  }),); it.effect("keeps the manual-server fallback when background prerequisites fail", () =>  Effect.gen(function* () {    const ready = yield* recoverServiceOnboardingOffer(      Effect.fail(new BootService.BootServicePrerequisiteError({ problem: "linger-disabled" })),    );    expect(ready).toBe(false);  }),);