239 lines · 8.2 KB · TypeScript
RawDownload
import type { OpencodeClient } from "@opencode-ai/sdk/v2";import * as NodeServices from "@effect/platform-node/NodeServices";import { it as effectIt } from "@effect/vitest";import {  HostProcessEnvironment,  HostProcessExecutablePath,  HostProcessPlatform,} from "@t3tools/shared/hostProcess";import * as Effect from "effect/Effect";import * as Fiber from "effect/Fiber";import * as FileSystem from "effect/FileSystem";import * as Layer from "effect/Layer";import * as Path from "effect/Path";import * as TestClock from "effect/testing/TestClock";import { FetchHttpClient, HttpClient } from "effect/unstable/http";import { describe, expect, it } from "vite-plus/test"; import {  OpenCodeRuntime,  OpenCodeRuntimeError,  OpenCodeRuntimeLive,  resolveOpenCodeConfigContent,  resolveOpenCodeServerPassword,  verifyOpenCodeServerVersion,} from "./opencodeRuntime.ts"; describe("resolveOpenCodeConfigContent", () => {  it("prefers the caller environment over the inherited environment", () => {    expect(      resolveOpenCodeConfigContent(        { OPENCODE_CONFIG_CONTENT: '{"source":"caller"}' },        { OPENCODE_CONFIG_CONTENT: '{"source":"process"}' },      ),    ).toBe('{"source":"caller"}');  });   it("falls back to the inherited environment and then an empty config", () => {    expect(      resolveOpenCodeConfigContent(undefined, {        OPENCODE_CONFIG_CONTENT: '{"source":"process"}',      }),    ).toBe('{"source":"process"}');    expect(resolveOpenCodeConfigContent(undefined, {})).toBe("{}");  });}); describe("resolveOpenCodeServerPassword", () => {  it("uses the local environment password when settings do not provide one", () => {    expect(      resolveOpenCodeServerPassword(        { external: false, environment: { OPENCODE_SERVER_PASSWORD: " env password " } },        {},      ),    ).toBe(" env password ");  });   it("uses the settings password for a local server", () => {    expect(      resolveOpenCodeServerPassword({ external: false, serverPassword: " settings password " }, {}),    ).toBe(" settings password ");  });   it("uses the settings password when local settings and environment differ", () => {    expect(      resolveOpenCodeServerPassword(        {          external: false,          serverPassword: "settings-password",          environment: { OPENCODE_SERVER_PASSWORD: "environment-password" },        },        {},      ),    ).toBe("settings-password");  });   it("does not send an inherited local password to an external server", () => {    expect(      resolveOpenCodeServerPassword(        { external: true, environment: { OPENCODE_SERVER_PASSWORD: "local-secret" } },        { OPENCODE_SERVER_PASSWORD: "inherited-secret" },      ),    ).toBeUndefined();  });}); function makeHealthClient(  result: (options?: { readonly signal?: AbortSignal }) => Promise<unknown>,): OpencodeClient {  return {    global: {      health: result,    },  } as unknown as OpencodeClient;} describe("verifyOpenCodeServerVersion", () => {  effectIt.effect("accepts a supported server version", () =>    Effect.gen(function* () {      const version = yield* verifyOpenCodeServerVersion(        makeHealthClient(() => Promise.resolve({ data: { healthy: true, version: "1.14.19" } })),      );      expect(version).toBe("1.14.19");    }),  );   effectIt.effect("rejects a server below the supported version", () =>    Effect.gen(function* () {      const error = yield* verifyOpenCodeServerVersion(        makeHealthClient(() => Promise.resolve({ data: { healthy: true, version: "1.14.18" } })),      ).pipe(Effect.flip);      expect(error).toBeInstanceOf(OpenCodeRuntimeError);      expect(error.detail).toContain("v1.14.18 is too old");    }),  );   for (const data of [    { healthy: true },    { healthy: true, version: "not-a-version" },    { healthy: false, version: "1.14.19" },  ]) {    effectIt.effect(`rejects an invalid health response: ${JSON.stringify(data)}`, () =>      Effect.gen(function* () {        const error = yield* verifyOpenCodeServerVersion(          makeHealthClient(() => Promise.resolve({ data })),        ).pipe(Effect.flip);        expect(error).toBeInstanceOf(OpenCodeRuntimeError);        expect(error.detail).toContain("requires OpenCode v1.14.19 or newer");      }),    );  }   effectIt.effect("preserves an unauthorized health error", () =>    Effect.gen(function* () {      const error = yield* verifyOpenCodeServerVersion(        makeHealthClient(() =>          Promise.reject({ response: { status: 401 }, error: { message: "Unauthorized" } }),        ),      ).pipe(Effect.flip);      expect(error).toBeInstanceOf(OpenCodeRuntimeError);      expect(error.detail).toContain("status=401");      expect(error.detail).toContain("Unauthorized");    }),  );   effectIt.effect("aborts a health request when the version check times out", () =>    Effect.gen(function* () {      let requestSignal: AbortSignal | undefined;      const checkFiber = yield* verifyOpenCodeServerVersion(        makeHealthClient((options) => {          requestSignal = options?.signal;          return new Promise(() => undefined);        }),      ).pipe(Effect.flip, Effect.forkChild);       yield* Effect.yieldNow;      expect(requestSignal).toBeDefined();      yield* TestClock.adjust("6 seconds");       const error = yield* Fiber.join(checkFiber);      expect(error.detail).toBe("Timed out while checking the OpenCode server version.");      expect(requestSignal?.aborted).toBe(true);    }).pipe(Effect.provide(TestClock.layer())),  );}); describe("OpenCode server output", () => {  effectIt.live(    "drains stdout and stderr after startup so server requests can finish",    () =>      Effect.gen(function* () {        const fs = yield* FileSystem.FileSystem;        const path = yield* Path.Path;        const environment = yield* HostProcessEnvironment;        const executablePath = yield* HostProcessExecutablePath;        const platform = yield* HostProcessPlatform;        const tempDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-opencode-output-" });        const isWindows = platform === "win32";        const binaryPath = path.join(tempDir, isWindows ? "opencode.cmd" : "opencode");        const scriptPath = path.join(tempDir, "opencode.mjs");         yield* fs.writeFileString(          scriptPath,          `import { createServer } from "node:http";const writeOutput = (stream) => new Promise((resolve, reject) => {  stream.write("x".repeat(2 * 1024 * 1024), (error) => error ? reject(error) : resolve());});const server = createServer(async (request, response) => {  if (request.url.startsWith("/global/health")) {    response.setHeader("Content-Type", "application/json");    response.end(JSON.stringify({ healthy: true, version: "1.14.19" }));    return;  }  await Promise.all([writeOutput(process.stdout), writeOutput(process.stderr)]);  response.end("drained");});server.listen(0, "127.0.0.1", () => {  process.stdout.write("opencode server listening on http://127.0.0.1:" + server.address().port + "\\n");});`,        );        yield* fs.writeFileString(          binaryPath,          [            ...(isWindows ? ["@echo off"] : ["#!/bin/sh"]),            isWindows              ? '"%T3_TEST_NODE_BINARY%" "%T3_TEST_OPENCODE_SCRIPT%" %*'              : 'exec "$T3_TEST_NODE_BINARY" "$T3_TEST_OPENCODE_SCRIPT" "$@"',            "",          ].join("\n"),        );        if (!isWindows) {          yield* fs.chmod(binaryPath, 0o755);        }         const runtime = yield* OpenCodeRuntime;        const server = yield* runtime.startOpenCodeServerProcess({          binaryPath,          directory: tempDir,          port: 0,          environment: {            ...environment,            T3_TEST_NODE_BINARY: executablePath,            T3_TEST_OPENCODE_SCRIPT: scriptPath,          },        });        const response = yield* HttpClient.get(`${server.url}/output`);         expect(yield* response.text).toBe("drained");        expect(yield* server.isRunning).toBe(true);      }).pipe(        Effect.scoped,        Effect.provide([          OpenCodeRuntimeLive.pipe(Layer.provideMerge(NodeServices.layer)),          FetchHttpClient.layer,        ]),      ),    10_000,  );});