1import * as NodeServices from "@effect/platform-node/NodeServices";
2import { it } from "@effect/vitest";
3import * as HostProcess from "@t3tools/shared/HostProcess";
4import * as Effect from "effect/Effect";
5import * as FileSystem from "effect/FileSystem";
6import * as Path from "effect/Path";
7import { expect, vi } from "vite-plus/test";
8
9import * as OpenCodeRuntime from "../OpenCodeRuntime.ts";
10import { replaceOutdatedService } from "./OpenCode2Service.ts";
11
12const service = vi.hoisted(() => ({ discover: vi.fn(), stop: vi.fn(), ensure: vi.fn() }));
13vi.mock("@opencode/client/effect/service", () => ({ Service: service }));
14
15const runtimeWithBinaryVersion = (stdout: string) =>
16 ({
17 runOpenCodeCommand: () => Effect.succeed({ stdout, stderr: "", code: 0 }),
18 }) as unknown as OpenCodeRuntime.OpenCodeRuntime["Service"];
19
20it.layer(NodeServices.layer)("replaceOutdatedService", (it) => {
21 it.effect.skipIf(HostProcess.Platform.defaultValue() === "win32")(
22 "replaces only this install's 2.x service, keeping its configured env",
23 () =>
24 Effect.gen(function* () {
25 const fileSystem = yield* FileSystem.FileSystem;
26 const path = yield* Path.Path;
27 const home = yield* fileSystem.makeTempDirectoryScoped();
28 const binDirectory = path.join(home, "bin");
29 yield* fileSystem.makeDirectory(binDirectory);
30 yield* fileSystem.writeFileString(path.join(binDirectory, "opencode"), "");
31 yield* fileSystem.chmod(path.join(binDirectory, "opencode"), 0o755);
32 const environment = {
33 HOME: home,
34 PATH: binDirectory,
35 OPENCODE_CONFIG_DIR: home,
36 ELECTRON_RUN_AS_NODE: "1",
37 };
38 yield* fileSystem.writeFileString(
39 path.join(home, "service.json"),
40 JSON.stringify({ env: { PATH: "/nowhere", XDG_STATE_HOME: "/elsewhere" } }),
41 );
42 const file = path.join(home, ".local", "state", "opencode", "service.json");
43 const run = (binary: string, serverUrl = "http://127.0.0.1:49374") =>
44 replaceOutdatedService({
45 binaryPath: "opencode",
46 serverUrl,
47 environment,
48 }).pipe(
49 Effect.provideService(
50 OpenCodeRuntime.OpenCodeRuntime,
51 runtimeWithBinaryVersion(binary),
52 ),
53 );
54 service.stop.mockReturnValue(Effect.void);
55 service.ensure.mockReturnValue(Effect.void);
56 const outdated = (options: { readonly version?: string }) =>
57 Effect.succeed(
58 options.version === undefined ? { url: "http://0.0.0.0:49374" } : undefined,
59 );
60
61 service.discover.mockImplementation(outdated);
62 yield* run("opencode v1.18.32");
63 expect(service.discover).not.toHaveBeenCalled();
64
65 for (const elsewhere of [
66 "http://192.168.1.5:49374",
67 "http://127.0.0.1:49999",
68 "http://[::1]:49374",
69 "http://localhost:49374",
70 ]) {
71 yield* run("opencode v2.2.5", elsewhere);
72 }
73 expect(service.discover).toHaveBeenCalledWith({ file });
74 service.discover.mockReturnValue(Effect.succeed({ url: "http://0.0.0.0" }));
75 yield* run("opencode v2.2.5", "https://127.0.0.1");
76 expect(service.ensure).not.toHaveBeenCalled();
77
78 service.discover.mockReturnValue(Effect.undefined);
79 yield* run("opencode v2.2.5");
80 expect(service.ensure).not.toHaveBeenCalled();
81
82 service.discover.mockReturnValue(Effect.succeed({ url: "http://0.0.0.0:49374" }));
83 yield* run("opencode v2.2.5");
84 expect(service.stop).not.toHaveBeenCalled();
85
86 service.discover.mockImplementation(outdated);
87 yield* run("opencode v2.2.5");
88 expect(service.stop).toHaveBeenCalledWith({ file, pty: "handoff" });
89 expect(service.ensure).toHaveBeenCalledWith({
90 file,
91 command: [path.join(binDirectory, "opencode"), "serve", "--service"],
92 env: { ...environment, PATH: "/nowhere", ELECTRON_RUN_AS_NODE: undefined },
93 });
94 }),
95 );
96});