import { it } from "@effect/vitest";import * as Deferred from "effect/Deferred";import * as Duration from "effect/Duration";import * as Effect from "effect/Effect";import * as Fiber from "effect/Fiber";import * as Ref from "effect/Ref";import * as TestClock from "effect/testing/TestClock";import { expect } from "vite-plus/test"; import { OpenCodeRuntime, OpenCodeRuntimeError, type OpenCodeRuntimeShape,} from "./opencodeRuntime.ts";import * as OpenCodeServerOwner from "./OpenCodeServerOwner.ts"; const unusedRuntimeMethod = () => Effect.fail( new OpenCodeRuntimeError({ operation: "unused", detail: "unused test method", }), ); const makeRuntime = Effect.gen(function* () { const starts = yield* Ref.make(0); const closes = yield* Ref.make(0); const failNextStart = yield* Ref.make(false); const started = yield* Deferred.make<void>(); const closed = yield* Deferred.make<void>(); const runtime: OpenCodeRuntimeShape = { startOpenCodeServerProcess: () => Effect.gen(function* () { if (yield* Ref.getAndSet(failNextStart, false)) { return yield* new OpenCodeRuntimeError({ operation: "startOpenCodeServerProcess", detail: "start failed", }); } const index = yield* Ref.updateAndGet(starts, (count) => count + 1); yield* Deferred.succeed(started, undefined).pipe(Effect.ignore); yield* Effect.addFinalizer(() => Ref.update(closes, (count) => count + 1).pipe( Effect.andThen(Deferred.succeed(closed, undefined)), Effect.ignore, ), ); return { url: `http://127.0.0.1:${index}`, version: "1.14.19", isRunning: Effect.succeed(true), exitCode: Effect.never, }; }), connectToOpenCodeServer: unusedRuntimeMethod, runOpenCodeCommand: unusedRuntimeMethod, createOpenCodeSdkClient: () => ({}) as never, loadOpenCodeInventory: unusedRuntimeMethod, loadOpenCodeSkills: unusedRuntimeMethod, loadInventoryFromCli: unusedRuntimeMethod, loadSkillsFromCli: unusedRuntimeMethod, }; return { runtime, starts, closes, failNextStart, started, closed };}); it.effect("shares concurrent borrowers and closes after the idle TTL", () => Effect.gen(function* () { const testRuntime = yield* makeRuntime; const release = yield* Deferred.make<void>(); yield* Effect.scoped( Effect.gen(function* () { const owner = yield* OpenCodeServerOwner.make({ binaryPath: "opencode", directory: "/project", }); const useServer = owner.withServer((server) => Deferred.await(release).pipe(Effect.as(server.url)), ); const fibers = yield* Effect.all([useServer, useServer], { concurrency: "unbounded", }).pipe(Effect.forkChild); yield* Deferred.await(testRuntime.started); expect(yield* Ref.get(testRuntime.starts)).toBe(1); yield* Deferred.succeed(release, undefined); expect(yield* Fiber.join(fibers)).toEqual(["http://127.0.0.1:1", "http://127.0.0.1:1"]); yield* TestClock.adjust(Duration.seconds(31)); yield* Deferred.await(testRuntime.closed); expect(yield* Ref.get(testRuntime.closes)).toBe(1); }), ).pipe(Effect.provideService(OpenCodeRuntime, testRuntime.runtime)); }).pipe(Effect.provide(TestClock.layer())),); it.effect("retries a failed start and closes on owner scope shutdown", () => Effect.gen(function* () { const testRuntime = yield* makeRuntime; yield* Ref.set(testRuntime.failNextStart, true); yield* Effect.scoped( Effect.gen(function* () { const owner = yield* OpenCodeServerOwner.make({ binaryPath: "opencode", directory: "/project", }); expect( (yield* Effect.exit(owner.withServer((server) => Effect.succeed(server.url))))._tag, ).toBe("Failure"); expect(yield* owner.withServer((server) => Effect.succeed(server.url))).toBe( "http://127.0.0.1:1", ); }), ).pipe(Effect.provideService(OpenCodeRuntime, testRuntime.runtime)); expect(yield* Ref.get(testRuntime.starts)).toBe(1); expect(yield* Ref.get(testRuntime.closes)).toBe(1); }),); it.effect("invalidates an exited process so the next borrower starts a new one", () => Effect.gen(function* () { const starts = yield* Ref.make(0); const processExits: Array<Deferred.Deferred<number>> = []; const processClosed = yield* Deferred.make<void>(); const runtime: OpenCodeRuntimeShape = { startOpenCodeServerProcess: () => Effect.gen(function* () { const index = yield* Ref.updateAndGet(starts, (count) => count + 1); const exitCode = yield* Deferred.make<number>(); processExits.push(exitCode); yield* Effect.addFinalizer(() => Deferred.succeed(processClosed, undefined).pipe(Effect.ignore), ); return { url: `http://127.0.0.1:${index}`, version: "1.14.19", isRunning: Effect.succeed(true), exitCode: Deferred.await(exitCode), }; }), connectToOpenCodeServer: unusedRuntimeMethod, runOpenCodeCommand: unusedRuntimeMethod, createOpenCodeSdkClient: () => ({}) as never, loadOpenCodeInventory: unusedRuntimeMethod, loadOpenCodeSkills: unusedRuntimeMethod, loadInventoryFromCli: unusedRuntimeMethod, loadSkillsFromCli: unusedRuntimeMethod, }; yield* Effect.scoped( Effect.gen(function* () { const owner = yield* OpenCodeServerOwner.make({ binaryPath: "opencode", directory: "/project", }); expect(yield* owner.withServer((server) => Effect.succeed(server.url))).toBe( "http://127.0.0.1:1", ); yield* Deferred.succeed(processExits[0]!, 1); yield* Deferred.await(processClosed); expect(yield* owner.withServer((server) => Effect.succeed(server.url))).toBe( "http://127.0.0.1:2", ); }), ).pipe(Effect.provideService(OpenCodeRuntime, runtime)); expect(yield* Ref.get(starts)).toBe(2); }),); it.effect("replaces a dead cached process before its exit watcher runs", () => Effect.gen(function* () { const starts = yield* Ref.make(0); const closes = yield* Ref.make(0); const processRunning: Array<Ref.Ref<boolean>> = []; const runtime: OpenCodeRuntimeShape = { startOpenCodeServerProcess: () => Effect.gen(function* () { const index = yield* Ref.updateAndGet(starts, (count) => count + 1); const isRunning = yield* Ref.make(true); processRunning.push(isRunning); yield* Effect.addFinalizer(() => Ref.update(closes, (count) => count + 1)); return { url: `http://127.0.0.1:${index}`, version: "1.14.19", isRunning: Ref.get(isRunning), exitCode: Effect.never, }; }), connectToOpenCodeServer: unusedRuntimeMethod, runOpenCodeCommand: unusedRuntimeMethod, createOpenCodeSdkClient: () => ({}) as never, loadOpenCodeInventory: unusedRuntimeMethod, loadOpenCodeSkills: unusedRuntimeMethod, loadInventoryFromCli: unusedRuntimeMethod, loadSkillsFromCli: unusedRuntimeMethod, }; yield* Effect.scoped( Effect.gen(function* () { const owner = yield* OpenCodeServerOwner.make({ binaryPath: "opencode", directory: "/project", }); expect(yield* owner.withServer((server) => Effect.succeed(server.url))).toBe( "http://127.0.0.1:1", ); yield* Ref.set(processRunning[0]!, false); expect(yield* owner.withServer((server) => Effect.succeed(server.url))).toBe( "http://127.0.0.1:2", ); expect(yield* Ref.get(starts)).toBe(2); expect(yield* Ref.get(closes)).toBe(1); }), ).pipe(Effect.provideService(OpenCodeRuntime, runtime)); }),); it.effect("cleans up an interrupted startup and allows a retry", () => Effect.gen(function* () { const starts = yield* Ref.make(0); const firstStartEntered = yield* Deferred.make<void>(); const firstStartClosed = yield* Deferred.make<void>(); const runtime: OpenCodeRuntimeShape = { startOpenCodeServerProcess: () => Effect.gen(function* () { const index = yield* Ref.updateAndGet(starts, (count) => count + 1); yield* Effect.addFinalizer(() => index === 1 ? Deferred.succeed(firstStartClosed, undefined).pipe(Effect.ignore) : Effect.void, ); if (index === 1) { yield* Deferred.succeed(firstStartEntered, undefined); return yield* Effect.never; } return { url: `http://127.0.0.1:${index}`, version: "1.14.19", isRunning: Effect.succeed(true), exitCode: Effect.never, }; }), connectToOpenCodeServer: unusedRuntimeMethod, runOpenCodeCommand: unusedRuntimeMethod, createOpenCodeSdkClient: () => ({}) as never, loadOpenCodeInventory: unusedRuntimeMethod, loadOpenCodeSkills: unusedRuntimeMethod, loadInventoryFromCli: unusedRuntimeMethod, loadSkillsFromCli: unusedRuntimeMethod, }; yield* Effect.scoped( Effect.gen(function* () { const owner = yield* OpenCodeServerOwner.make({ binaryPath: "opencode", directory: "/project", }); const firstBorrower = yield* owner .withServer((server) => Effect.succeed(server.url)) .pipe(Effect.forkChild); yield* Deferred.await(firstStartEntered); yield* Fiber.interrupt(firstBorrower); yield* Deferred.await(firstStartClosed); expect(yield* owner.withServer((server) => Effect.succeed(server.url))).toBe( "http://127.0.0.1:2", ); }), ).pipe(Effect.provideService(OpenCodeRuntime, runtime)); }),); it.effect("releases an interrupted borrower and closes after the idle TTL", () => Effect.gen(function* () { const testRuntime = yield* makeRuntime; const borrowerEntered = yield* Deferred.make<void>(); yield* Effect.scoped( Effect.gen(function* () { const owner = yield* OpenCodeServerOwner.make({ binaryPath: "opencode", directory: "/project", }); const borrower = yield* owner .withServer(() => Deferred.succeed(borrowerEntered, undefined).pipe(Effect.andThen(Effect.never)), ) .pipe(Effect.forkChild); yield* Deferred.await(borrowerEntered); yield* Fiber.interrupt(borrower); yield* TestClock.adjust(Duration.seconds(31)); yield* Deferred.await(testRuntime.closed); expect(yield* Ref.get(testRuntime.closes)).toBe(1); }), ).pipe(Effect.provideService(OpenCodeRuntime, testRuntime.runtime)); }).pipe(Effect.provide(TestClock.layer())),);
import { it } from "@effect/vitest";
import * as Deferred from "effect/Deferred";
import * as Duration from "effect/Duration";
import * as Effect from "effect/Effect";
import * as Fiber from "effect/Fiber";
import * as Ref from "effect/Ref";
import * as TestClock from "effect/testing/TestClock";
import { expect } from "vite-plus/test";
import {
OpenCodeRuntime,
OpenCodeRuntimeError,
type OpenCodeRuntimeShape,
} from "./opencodeRuntime.ts";
import * as OpenCodeServerOwner from "./OpenCodeServerOwner.ts";
const unusedRuntimeMethod = () =>
Effect.fail(
new OpenCodeRuntimeError({
operation: "unused",
detail: "unused test method",
}),
);
const makeRuntime = Effect.gen(function* () {
const starts = yield* Ref.make(0);
const closes = yield* Ref.make(0);
const failNextStart = yield* Ref.make(false);
const started = yield* Deferred.make<void>();
const closed = yield* Deferred.make<void>();
const runtime: OpenCodeRuntimeShape = {
startOpenCodeServerProcess: () =>
Effect.gen(function* () {
if (yield* Ref.getAndSet(failNextStart, false)) {
return yield* new OpenCodeRuntimeError({
operation: "startOpenCodeServerProcess",
detail: "start failed",
});
}
const index = yield* Ref.updateAndGet(starts, (count) => count + 1);
yield* Deferred.succeed(started, undefined).pipe(Effect.ignore);
yield* Effect.addFinalizer(() =>
Ref.update(closes, (count) => count + 1).pipe(
Effect.andThen(Deferred.succeed(closed, undefined)),
Effect.ignore,
),
);
return {
url: `http://127.0.0.1:${index}`,
version: "1.14.19",
isRunning: Effect.succeed(true),
exitCode: Effect.never,
};
}),
connectToOpenCodeServer: unusedRuntimeMethod,
runOpenCodeCommand: unusedRuntimeMethod,
createOpenCodeSdkClient: () => ({}) as never,
loadOpenCodeInventory: unusedRuntimeMethod,
loadOpenCodeSkills: unusedRuntimeMethod,
loadInventoryFromCli: unusedRuntimeMethod,
loadSkillsFromCli: unusedRuntimeMethod,
};
return { runtime, starts, closes, failNextStart, started, closed };
});
it.effect("shares concurrent borrowers and closes after the idle TTL", () =>
Effect.gen(function* () {
const testRuntime = yield* makeRuntime;
const release = yield* Deferred.make<void>();
yield* Effect.scoped(
Effect.gen(function* () {
const owner = yield* OpenCodeServerOwner.make({
binaryPath: "opencode",
directory: "/project",
});
const useServer = owner.withServer((server) =>
Deferred.await(release).pipe(Effect.as(server.url)),
);
const fibers = yield* Effect.all([useServer, useServer], {
concurrency: "unbounded",
}).pipe(Effect.forkChild);
yield* Deferred.await(testRuntime.started);
expect(yield* Ref.get(testRuntime.starts)).toBe(1);
yield* Deferred.succeed(release, undefined);
expect(yield* Fiber.join(fibers)).toEqual(["http://127.0.0.1:1", "http://127.0.0.1:1"]);
yield* TestClock.adjust(Duration.seconds(31));
yield* Deferred.await(testRuntime.closed);
expect(yield* Ref.get(testRuntime.closes)).toBe(1);
}),
).pipe(Effect.provideService(OpenCodeRuntime, testRuntime.runtime));
}).pipe(Effect.provide(TestClock.layer())),
);
it.effect("retries a failed start and closes on owner scope shutdown", () =>
Effect.gen(function* () {
const testRuntime = yield* makeRuntime;
yield* Ref.set(testRuntime.failNextStart, true);
yield* Effect.scoped(
Effect.gen(function* () {
const owner = yield* OpenCodeServerOwner.make({
binaryPath: "opencode",
directory: "/project",
});
expect(
(yield* Effect.exit(owner.withServer((server) => Effect.succeed(server.url))))._tag,
).toBe("Failure");
expect(yield* owner.withServer((server) => Effect.succeed(server.url))).toBe(
"http://127.0.0.1:1",
);
}),
).pipe(Effect.provideService(OpenCodeRuntime, testRuntime.runtime));
expect(yield* Ref.get(testRuntime.starts)).toBe(1);
expect(yield* Ref.get(testRuntime.closes)).toBe(1);
}),
);
it.effect("invalidates an exited process so the next borrower starts a new one", () =>
Effect.gen(function* () {
const starts = yield* Ref.make(0);
const processExits: Array<Deferred.Deferred<number>> = [];
const processClosed = yield* Deferred.make<void>();
const runtime: OpenCodeRuntimeShape = {
startOpenCodeServerProcess: () =>
Effect.gen(function* () {
const index = yield* Ref.updateAndGet(starts, (count) => count + 1);
const exitCode = yield* Deferred.make<number>();
processExits.push(exitCode);
yield* Effect.addFinalizer(() =>
Deferred.succeed(processClosed, undefined).pipe(Effect.ignore),
);
return {
url: `http://127.0.0.1:${index}`,
version: "1.14.19",
isRunning: Effect.succeed(true),
exitCode: Deferred.await(exitCode),
};
}),
connectToOpenCodeServer: unusedRuntimeMethod,
runOpenCodeCommand: unusedRuntimeMethod,
createOpenCodeSdkClient: () => ({}) as never,
loadOpenCodeInventory: unusedRuntimeMethod,
loadOpenCodeSkills: unusedRuntimeMethod,
loadInventoryFromCli: unusedRuntimeMethod,
loadSkillsFromCli: unusedRuntimeMethod,
};
yield* Effect.scoped(
Effect.gen(function* () {
const owner = yield* OpenCodeServerOwner.make({
binaryPath: "opencode",
directory: "/project",
});
expect(yield* owner.withServer((server) => Effect.succeed(server.url))).toBe(
"http://127.0.0.1:1",
);
yield* Deferred.succeed(processExits[0]!, 1);
yield* Deferred.await(processClosed);
expect(yield* owner.withServer((server) => Effect.succeed(server.url))).toBe(
"http://127.0.0.1:2",
);
}),
).pipe(Effect.provideService(OpenCodeRuntime, runtime));
expect(yield* Ref.get(starts)).toBe(2);
}),
);
it.effect("replaces a dead cached process before its exit watcher runs", () =>
Effect.gen(function* () {
const starts = yield* Ref.make(0);
const closes = yield* Ref.make(0);
const processRunning: Array<Ref.Ref<boolean>> = [];
const runtime: OpenCodeRuntimeShape = {
startOpenCodeServerProcess: () =>
Effect.gen(function* () {
const index = yield* Ref.updateAndGet(starts, (count) => count + 1);
const isRunning = yield* Ref.make(true);
processRunning.push(isRunning);
yield* Effect.addFinalizer(() => Ref.update(closes, (count) => count + 1));
return {
url: `http://127.0.0.1:${index}`,
version: "1.14.19",
isRunning: Ref.get(isRunning),
exitCode: Effect.never,
};
}),
connectToOpenCodeServer: unusedRuntimeMethod,
runOpenCodeCommand: unusedRuntimeMethod,
createOpenCodeSdkClient: () => ({}) as never,
loadOpenCodeInventory: unusedRuntimeMethod,
loadOpenCodeSkills: unusedRuntimeMethod,
loadInventoryFromCli: unusedRuntimeMethod,
loadSkillsFromCli: unusedRuntimeMethod,
};
yield* Effect.scoped(
Effect.gen(function* () {
const owner = yield* OpenCodeServerOwner.make({
binaryPath: "opencode",
directory: "/project",
});
expect(yield* owner.withServer((server) => Effect.succeed(server.url))).toBe(
"http://127.0.0.1:1",
);
yield* Ref.set(processRunning[0]!, false);
expect(yield* owner.withServer((server) => Effect.succeed(server.url))).toBe(
"http://127.0.0.1:2",
);
expect(yield* Ref.get(starts)).toBe(2);
expect(yield* Ref.get(closes)).toBe(1);
}),
).pipe(Effect.provideService(OpenCodeRuntime, runtime));
}),
);
it.effect("cleans up an interrupted startup and allows a retry", () =>
Effect.gen(function* () {
const starts = yield* Ref.make(0);
const firstStartEntered = yield* Deferred.make<void>();
const firstStartClosed = yield* Deferred.make<void>();
const runtime: OpenCodeRuntimeShape = {
startOpenCodeServerProcess: () =>
Effect.gen(function* () {
const index = yield* Ref.updateAndGet(starts, (count) => count + 1);
yield* Effect.addFinalizer(() =>
index === 1
? Deferred.succeed(firstStartClosed, undefined).pipe(Effect.ignore)
: Effect.void,
);
if (index === 1) {
yield* Deferred.succeed(firstStartEntered, undefined);
return yield* Effect.never;
}
return {
url: `http://127.0.0.1:${index}`,
version: "1.14.19",
isRunning: Effect.succeed(true),
exitCode: Effect.never,
};
}),
connectToOpenCodeServer: unusedRuntimeMethod,
runOpenCodeCommand: unusedRuntimeMethod,
createOpenCodeSdkClient: () => ({}) as never,
loadOpenCodeInventory: unusedRuntimeMethod,
loadOpenCodeSkills: unusedRuntimeMethod,
loadInventoryFromCli: unusedRuntimeMethod,
loadSkillsFromCli: unusedRuntimeMethod,
};
yield* Effect.scoped(
Effect.gen(function* () {
const owner = yield* OpenCodeServerOwner.make({
binaryPath: "opencode",
directory: "/project",
});
const firstBorrower = yield* owner
.withServer((server) => Effect.succeed(server.url))
.pipe(Effect.forkChild);
yield* Deferred.await(firstStartEntered);
yield* Fiber.interrupt(firstBorrower);
yield* Deferred.await(firstStartClosed);
expect(yield* owner.withServer((server) => Effect.succeed(server.url))).toBe(
"http://127.0.0.1:2",
);
}),
).pipe(Effect.provideService(OpenCodeRuntime, runtime));
}),
);
it.effect("releases an interrupted borrower and closes after the idle TTL", () =>
Effect.gen(function* () {
const testRuntime = yield* makeRuntime;
const borrowerEntered = yield* Deferred.make<void>();
yield* Effect.scoped(
Effect.gen(function* () {
const owner = yield* OpenCodeServerOwner.make({
binaryPath: "opencode",
directory: "/project",
});
const borrower = yield* owner
.withServer(() =>
Deferred.succeed(borrowerEntered, undefined).pipe(Effect.andThen(Effect.never)),
)
.pipe(Effect.forkChild);
yield* Deferred.await(borrowerEntered);
yield* Fiber.interrupt(borrower);
yield* TestClock.adjust(Duration.seconds(31));
yield* Deferred.await(testRuntime.closed);
expect(yield* Ref.get(testRuntime.closes)).toBe(1);
}),
).pipe(Effect.provideService(OpenCodeRuntime, testRuntime.runtime));
}).pipe(Effect.provide(TestClock.layer())),
);