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, );});
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,
);
});