Open History to see the last commit
1import { it } from "@effect/vitest";
2import * as Deferred from "effect/Deferred";
3import * as Duration from "effect/Duration";
4import * as Effect from "effect/Effect";
5import * as Fiber from "effect/Fiber";
6import * as Ref from "effect/Ref";
7import * as TestClock from "effect/testing/TestClock";
8import { expect } from "vite-plus/test";
9
10import * as OpenCodeRuntime from "./OpenCodeRuntime.ts";
11import * as OpenCodeServerOwner from "./OpenCodeServerOwner.ts";
12
13const unusedRuntimeMethod = () =>
14 Effect.fail(
15 new OpenCodeRuntime.OpenCodeRuntimeError({
16 operation: "unused",
17 detail: "unused test method",
18 }),
19 );
20
21const makeRuntime = Effect.gen(function* () {
22 const starts = yield* Ref.make(0);
23 const closes = yield* Ref.make(0);
24 const failNextStart = yield* Ref.make(false);
25 const started = yield* Deferred.make<void>();
26 const closed = yield* Deferred.make<void>();
27 const runtime: OpenCodeRuntime.OpenCodeRuntime["Service"] = {
28 startOpenCodeServerProcess: () =>
29 Effect.gen(function* () {
30 if (yield* Ref.getAndSet(failNextStart, false)) {
31 return yield* new OpenCodeRuntime.OpenCodeRuntimeError({
32 operation: "startOpenCodeServerProcess",
33 detail: "start failed",
34 });
35 }
36 const index = yield* Ref.updateAndGet(starts, (count) => count + 1);
37 yield* Deferred.succeed(started, undefined).pipe(Effect.ignore);
38 yield* Effect.addFinalizer(() =>
39 Ref.update(closes, (count) => count + 1).pipe(
40 Effect.andThen(Deferred.succeed(closed, undefined)),
41 Effect.ignore,
42 ),
43 );
44 return {
45 url: `http://127.0.0.1:${index}`,
46 version: "1.14.19",
47 isRunning: Effect.succeed(true),
48 exitCode: Effect.never,
49 };
50 }),
51 connectToOpenCodeServer: unusedRuntimeMethod,
52 runOpenCodeCommand: unusedRuntimeMethod,
53 createOpenCodeSdkClient: () => ({}) as never,
54 loadOpenCodeInventory: unusedRuntimeMethod,
55 loadOpenCodeSkills: unusedRuntimeMethod,
56 };
57 return { runtime, starts, closes, failNextStart, started, closed };
58});
59
60it.effect("shares concurrent borrowers and closes after the idle TTL", () =>
61 Effect.gen(function* () {
62 const testRuntime = yield* makeRuntime;
63 const release = yield* Deferred.make<void>();
64 yield* Effect.scoped(
65 Effect.gen(function* () {
66 const owner = yield* OpenCodeServerOwner.make({
67 binaryPath: "opencode",
68 directory: "/project",
69 });
70 const useServer = owner.withServer((server) =>
71 Deferred.await(release).pipe(Effect.as(server.url)),
72 );
73 const fibers = yield* Effect.all([useServer, useServer], {
74 concurrency: "unbounded",
75 }).pipe(Effect.forkChild);
76 yield* Deferred.await(testRuntime.started);
77 expect(yield* Ref.get(testRuntime.starts)).toBe(1);
78 yield* Deferred.succeed(release, undefined);
79 expect(yield* Fiber.join(fibers)).toEqual(["http://127.0.0.1:1", "http://127.0.0.1:1"]);
80 yield* TestClock.adjust(Duration.seconds(31));
81 yield* Deferred.await(testRuntime.closed);
82 expect(yield* Ref.get(testRuntime.closes)).toBe(1);
83 }),
84 ).pipe(Effect.provideService(OpenCodeRuntime.OpenCodeRuntime, testRuntime.runtime));
85 }).pipe(Effect.provide(TestClock.layer())),
86);
87
88it.effect("retries a failed start and closes on owner scope shutdown", () =>
89 Effect.gen(function* () {
90 const testRuntime = yield* makeRuntime;
91 yield* Ref.set(testRuntime.failNextStart, true);
92 yield* Effect.scoped(
93 Effect.gen(function* () {
94 const owner = yield* OpenCodeServerOwner.make({
95 binaryPath: "opencode",
96 directory: "/project",
97 });
98 expect(
99 (yield* Effect.exit(owner.withServer((server) => Effect.succeed(server.url))))._tag,
100 ).toBe("Failure");
101 expect(yield* owner.withServer((server) => Effect.succeed(server.url))).toBe(
102 "http://127.0.0.1:1",
103 );
104 }),
105 ).pipe(Effect.provideService(OpenCodeRuntime.OpenCodeRuntime, testRuntime.runtime));
106 expect(yield* Ref.get(testRuntime.starts)).toBe(1);
107 expect(yield* Ref.get(testRuntime.closes)).toBe(1);
108 }),
109);
110
111it.effect("replaces a dead cached process before its exit watcher runs", () =>
112 Effect.gen(function* () {
113 const starts = yield* Ref.make(0);
114 const closes = yield* Ref.make(0);
115 const processRunning: Array<Ref.Ref<boolean>> = [];
116 const runtime: OpenCodeRuntime.OpenCodeRuntime["Service"] = {
117 startOpenCodeServerProcess: () =>
118 Effect.gen(function* () {
119 const index = yield* Ref.updateAndGet(starts, (count) => count + 1);
120 const isRunning = yield* Ref.make(true);
121 processRunning.push(isRunning);
122 yield* Effect.addFinalizer(() => Ref.update(closes, (count) => count + 1));
123 return {
124 url: `http://127.0.0.1:${index}`,
125 version: "1.14.19",
126 isRunning: Ref.get(isRunning),
127 exitCode: Effect.never,
128 };
129 }),
130 connectToOpenCodeServer: unusedRuntimeMethod,
131 runOpenCodeCommand: unusedRuntimeMethod,
132 createOpenCodeSdkClient: () => ({}) as never,
133 loadOpenCodeInventory: unusedRuntimeMethod,
134 loadOpenCodeSkills: unusedRuntimeMethod,
135 };
136
137 yield* Effect.scoped(
138 Effect.gen(function* () {
139 const owner = yield* OpenCodeServerOwner.make({
140 binaryPath: "opencode",
141 directory: "/project",
142 });
143 expect(yield* owner.withServer((server) => Effect.succeed(server.url))).toBe(
144 "http://127.0.0.1:1",
145 );
146 yield* Ref.set(processRunning[0]!, false);
147
148 expect(yield* owner.withServer((server) => Effect.succeed(server.url))).toBe(
149 "http://127.0.0.1:2",
150 );
151 expect(yield* Ref.get(starts)).toBe(2);
152 expect(yield* Ref.get(closes)).toBe(1);
153 }),
154 ).pipe(Effect.provideService(OpenCodeRuntime.OpenCodeRuntime, runtime));
155 }),
156);
157
158it.effect("cleans up an interrupted startup and allows a retry", () =>
159 Effect.gen(function* () {
160 const starts = yield* Ref.make(0);
161 const firstStartEntered = yield* Deferred.make<void>();
162 const firstStartClosed = yield* Deferred.make<void>();
163 const runtime: OpenCodeRuntime.OpenCodeRuntime["Service"] = {
164 startOpenCodeServerProcess: () =>
165 Effect.gen(function* () {
166 const index = yield* Ref.updateAndGet(starts, (count) => count + 1);
167 yield* Effect.addFinalizer(() =>
168 index === 1
169 ? Deferred.succeed(firstStartClosed, undefined).pipe(Effect.ignore)
170 : Effect.void,
171 );
172 if (index === 1) {
173 yield* Deferred.succeed(firstStartEntered, undefined);
174 return yield* Effect.never;
175 }
176 return {
177 url: `http://127.0.0.1:${index}`,
178 version: "1.14.19",
179 isRunning: Effect.succeed(true),
180 exitCode: Effect.never,
181 };
182 }),
183 connectToOpenCodeServer: unusedRuntimeMethod,
184 runOpenCodeCommand: unusedRuntimeMethod,
185 createOpenCodeSdkClient: () => ({}) as never,
186 loadOpenCodeInventory: unusedRuntimeMethod,
187 loadOpenCodeSkills: unusedRuntimeMethod,
188 };
189
190 yield* Effect.scoped(
191 Effect.gen(function* () {
192 const owner = yield* OpenCodeServerOwner.make({
193 binaryPath: "opencode",
194 directory: "/project",
195 });
196 const firstBorrower = yield* owner
197 .withServer((server) => Effect.succeed(server.url))
198 .pipe(Effect.forkChild);
199 yield* Deferred.await(firstStartEntered);
200 yield* Fiber.interrupt(firstBorrower);
201 yield* Deferred.await(firstStartClosed);
202 expect(yield* owner.withServer((server) => Effect.succeed(server.url))).toBe(
203 "http://127.0.0.1:2",
204 );
205 }),
206 ).pipe(Effect.provideService(OpenCodeRuntime.OpenCodeRuntime, runtime));
207 }),
208);