authorgravatar for 73941998+UtkarshUsername@users.noreply.github.comUtkarsh Patil <73941998+UtkarshUsername@users.noreply.github.com> 2026-07-20 15:52:00+05:30
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-07-20 12:22:00+02:00
log0ca3240691bf1773802b3ed70330515d68b0a6b8
tree22d6f96d1c4819a3e33edff3b4311cd7c0b1d8da
parentf4da4f3b4037260bbb0d8914acbebafd2206607a
signature Signed by PGP key B5690EEEBB952194

fix(server): use CLI for OpenCode health check instead of spawning server (#4153)


6 files changed, 465 insertions(+), 20 deletions(-)

apps/server/src/provider/Layers/OpenCodeAdapter.test.ts+8
...@@ -221,6 +221,14 @@ const OpenCodeRuntimeTestDouble: OpenCodeRuntimeShape = {...@@ -221,6 +221,14 @@ const OpenCodeRuntimeTestDouble: OpenCodeRuntimeShape = {
221 cause: null,221 cause: null,
222 }),222 }),
223 ),223 ),
224 loadInventoryFromCli: () =>
225 Effect.fail(
226 new OpenCodeRuntimeError({
227 operation: "loadInventoryFromCli",
228 detail: "OpenCodeRuntimeTestDouble.loadInventoryFromCli not used in this test",
229 cause: null,
230 }),
231 ),
224};232};
225233
226const providerSessionDirectoryTestLayer = Layer.succeed(ProviderSessionDirectory, {234const providerSessionDirectoryTestLayer = Layer.succeed(ProviderSessionDirectory, {
apps/server/src/provider/Layers/OpenCodeProvider.test.ts+20-2
...@@ -95,6 +95,13 @@ const OpenCodeRuntimeTestDouble: OpenCodeRuntimeShape = {...@@ -95,6 +95,13 @@ const OpenCodeRuntimeTestDouble: OpenCodeRuntimeShape = {
95 }),95 }),
96 )96 )
97 : Effect.succeed(runtimeMock.state.inventory as OpenCodeInventory),97 : Effect.succeed(runtimeMock.state.inventory as OpenCodeInventory),
98 loadInventoryFromCli: () =>
99 runtimeMock.state.inventoryError
100 ? Effect.succeed({
101 providerList: { all: [], default: {}, connected: [] as string[] },
102 agents: [],
103 } as OpenCodeInventory)
104 : Effect.succeed(runtimeMock.state.inventory as OpenCodeInventory),
98};105};
99106
100beforeEach(() => {107beforeEach(() => {
...@@ -197,11 +204,22 @@ it.layer(testLayer)("checkOpenCodeProviderStatus", (it) => {...@@ -197,11 +204,22 @@ it.layer(testLayer)("checkOpenCodeProviderStatus", (it) => {
197 }),204 }),
198 );205 );
199206
200 it.effect("closes the local OpenCode server scope after provider refresh", () =>207 it.effect("does not spawn a local server for health check (uses CLI instead)", () =>
201 Effect.gen(function* () {208 Effect.gen(function* () {
202 yield* checkOpenCodeProviderStatus(makeOpenCodeSettings(), process.cwd());209 yield* checkOpenCodeProviderStatus(makeOpenCodeSettings(), process.cwd());
203210
204 NodeAssert.equal(runtimeMock.state.closeCalls, 1);211 NodeAssert.equal(runtimeMock.state.closeCalls, 0);
212 }),
213 );
214
215 it.effect("degrades gracefully on CLI failure for local installs", () =>
216 Effect.gen(function* () {
217 runtimeMock.state.inventoryError = new Error("opencode models failed");
218 const snapshot = yield* checkOpenCodeProviderStatus(makeOpenCodeSettings(), process.cwd());
219
220 NodeAssert.equal(snapshot.status, "warning");
221 NodeAssert.equal(snapshot.installed, true);
222 NodeAssert.equal(snapshot.models.length, 0);
205 }),223 }),
206 );224 );
207});225});
apps/server/src/provider/Layers/OpenCodeProvider.ts+24-18
...@@ -409,26 +409,32 @@ export const checkOpenCodeProviderStatus = Effect.fn("checkOpenCodeProviderStatu...@@ -409,26 +409,32 @@ export const checkOpenCodeProviderStatus = Effect.fn("checkOpenCodeProviderStatu
409 }409 }
410410
411 const inventoryExit = yield* Effect.exit(411 const inventoryExit = yield* Effect.exit(
412 Effect.scoped(412 (isExternalServer
413 Effect.gen(function* () {413 ? Effect.scoped(
414 const server = yield* openCodeRuntime.connectToOpenCodeServer({414 Effect.gen(function* () {
415 const server = yield* openCodeRuntime.connectToOpenCodeServer({
416 binaryPath: openCodeSettings.binaryPath,
417 serverUrl: openCodeSettings.serverUrl,
418 environment: resolvedEnvironment,
419 });
420 return yield* openCodeRuntime.loadOpenCodeInventory(
421 openCodeRuntime.createOpenCodeSdkClient({
422 baseUrl: server.url,
423 directory: cwd,
424 ...(openCodeSettings.serverPassword
425 ? { serverPassword: openCodeSettings.serverPassword }
426 : {}),
427 }),
428 );
429 }),
430 )
431 : openCodeRuntime.loadInventoryFromCli({
415 binaryPath: openCodeSettings.binaryPath,432 binaryPath: openCodeSettings.binaryPath,
416 serverUrl: openCodeSettings.serverUrl,
417 environment: resolvedEnvironment,433 environment: resolvedEnvironment,
418 });434 })
419 return yield* openCodeRuntime.loadOpenCodeInventory(435 ).pipe(
420 openCodeRuntime.createOpenCodeSdkClient({436 Effect.mapError(
421 baseUrl: server.url,437 (cause) => new OpenCodeProbeError({ cause, detail: openCodeRuntimeErrorDetail(cause) }),
422 directory: cwd,
423 ...(isExternalServer && openCodeSettings.serverPassword
424 ? { serverPassword: openCodeSettings.serverPassword }
425 : {}),
426 }),
427 );
428 }).pipe(
429 Effect.mapError(
430 (cause) => new OpenCodeProbeError({ cause, detail: openCodeRuntimeErrorDetail(cause) }),
431 ),
432 ),438 ),
433 ),439 ),
434 );440 );
apps/server/src/provider/opencodeRuntime.cliParsers.test.ts created+229
...@@ -0,0 +1,229 @@
1import * as NodeAssert from "node:assert/strict";
2
3import { describe, it } from "vite-plus/test";
4
5import { parseModelsCliOutput, parseAgentListCliOutput } from "./opencodeRuntime.ts";
6
7describe("parseModelsCliOutput", () => {
8 it("parses a single model from a single provider", () => {
9 const stdout = [
10 "anthropic/claude-sonnet-4-5",
11 JSON.stringify({
12 id: "claude-sonnet-4-5",
13 providerID: "anthropic",
14 name: "Claude Sonnet 4.5",
15 capabilities: { temperature: true, reasoning: true, toolcall: true },
16 cost: { input: 3, output: 15 },
17 limit: { context: 200000, output: 8192 },
18 status: "active",
19 options: {},
20 headers: {},
21 release_date: "2025-01-01",
22 }),
23 ].join("\n");
24
25 const result = parseModelsCliOutput(stdout);
26 NodeAssert.equal(result.providers.size, 1);
27 NodeAssert.equal(result.connected.length, 1);
28 NodeAssert.equal(result.connected[0], "anthropic");
29
30 const provider = result.providers.get("anthropic")!;
31 NodeAssert.ok(provider);
32 NodeAssert.equal(provider.id, "anthropic");
33 NodeAssert.equal(provider.name, "anthropic");
34 NodeAssert.equal(Object.keys(provider.models).length, 1);
35
36 const model = provider.models["claude-sonnet-4-5"]!;
37 NodeAssert.ok(model);
38 NodeAssert.equal(model.id, "claude-sonnet-4-5");
39 NodeAssert.equal(model.providerID, "anthropic");
40 NodeAssert.equal(model.name, "Claude Sonnet 4.5");
41 });
42
43 it("parses multiple models from multiple providers", () => {
44 const stdout = [
45 "anthropic/claude-sonnet-4-5",
46 JSON.stringify({ id: "claude-sonnet-4-5", providerID: "anthropic", name: "Sonnet 4.5" }),
47 "anthropic/claude-haiku-4-5",
48 JSON.stringify({ id: "claude-haiku-4-5", providerID: "anthropic", name: "Haiku 4.5" }),
49 "openai/gpt-4o",
50 JSON.stringify({ id: "gpt-4o", providerID: "openai", name: "GPT-4o" }),
51 ].join("\n");
52
53 const result = parseModelsCliOutput(stdout);
54 NodeAssert.equal(result.providers.size, 2);
55 NodeAssert.equal(result.connected.length, 2);
56 NodeAssert.equal([...result.connected].sort().join(","), "anthropic,openai");
57 NodeAssert.equal(Object.keys(result.providers.get("anthropic")!.models).length, 2);
58 NodeAssert.equal(Object.keys(result.providers.get("openai")!.models).length, 1);
59 });
60
61 it("handles empty input", () => {
62 const result = parseModelsCliOutput("");
63 NodeAssert.equal(result.providers.size, 0);
64 NodeAssert.equal(result.connected.length, 0);
65 });
66
67 it("skips unparseable JSON blocks", () => {
68 const stdout = [
69 "anthropic/claude-sonnet-4-5",
70 "this is not valid json {{{",
71 "anthropic/claude-haiku-4-5",
72 JSON.stringify({ id: "claude-haiku-4-5", providerID: "anthropic", name: "Haiku 4.5" }),
73 ].join("\n");
74
75 const result = parseModelsCliOutput(stdout);
76 NodeAssert.equal(result.providers.size, 1);
77 const provider = result.providers.get("anthropic")!;
78 NodeAssert.equal(Object.keys(provider.models).length, 1);
79 NodeAssert.ok(provider.models["claude-haiku-4-5"]);
80 });
81
82 it("handles Windows-style CRLF line endings", () => {
83 const stdout =
84 "anthropic/claude-sonnet-4-5\r\n" +
85 JSON.stringify({ id: "claude-sonnet-4-5", providerID: "anthropic", name: "Sonnet" }) +
86 "\r\n";
87
88 const result = parseModelsCliOutput(stdout);
89 NodeAssert.equal(result.providers.size, 1);
90 NodeAssert.ok(result.providers.get("anthropic")!.models["claude-sonnet-4-5"]);
91 });
92
93 it("handles model JSON with variants and nested fields", () => {
94 const stdout = [
95 "opencode/gpt-5.4",
96 JSON.stringify({
97 id: "gpt-5.4",
98 providerID: "opencode",
99 name: "GPT-5.4",
100 family: "gpt",
101 capabilities: {
102 temperature: true,
103 reasoning: true,
104 attachment: false,
105 toolcall: true,
106 input: { text: true, audio: false, image: false, video: false, pdf: false },
107 output: { text: true, audio: false, image: false, video: false, pdf: false },
108 interleaved: false,
109 },
110 cost: { input: 0, output: 0, cache: { read: 0, write: 0 } },
111 limit: { context: 200000, input: 160000, output: 32000 },
112 status: "active",
113 options: {},
114 headers: {},
115 release_date: "2025-01-01",
116 variants: { none: {}, low: {}, medium: {}, high: {} },
117 }),
118 ].join("\n");
119
120 const result = parseModelsCliOutput(stdout);
121 const model = result.providers.get("opencode")!.models["gpt-5.4"]!;
122 NodeAssert.ok(model);
123 NodeAssert.ok(model.capabilities);
124 NodeAssert.equal(model.capabilities!.reasoning, true);
125 NodeAssert.ok(model.variants);
126 NodeAssert.equal(model.variants!["medium"] !== undefined, true);
127 });
128});
129
130describe("parseAgentListCliOutput", () => {
131 it("parses a single agent", () => {
132 const stdout = [
133 "build (primary)",
134 " " + JSON.stringify([{ permission: "*", action: "allow", pattern: "*" }]),
135 ].join("\n");
136
137 const result = parseAgentListCliOutput(stdout);
138 NodeAssert.equal(result.length, 1);
139 NodeAssert.equal(result[0]!.name, "build");
140 NodeAssert.equal(result[0]!.mode, "primary");
141 NodeAssert.equal(result[0]!.permission.length, 1);
142 });
143
144 it("parses multiple agents", () => {
145 const stdout = [
146 "build (primary)",
147 " " + JSON.stringify([{ permission: "*", action: "allow", pattern: "*" }]),
148 "explore (subagent)",
149 " " + JSON.stringify([{ permission: "read", action: "allow", pattern: "*" }]),
150 "plan (primary)",
151 " " + JSON.stringify([{ permission: "edit", action: "ask", pattern: "*.md" }]),
152 ].join("\n");
153
154 const result = parseAgentListCliOutput(stdout);
155 NodeAssert.equal(result.length, 3);
156 NodeAssert.equal(result[0]!.name, "build");
157 NodeAssert.equal(result[0]!.mode, "primary");
158 NodeAssert.equal(result[1]!.name, "explore");
159 NodeAssert.equal(result[1]!.mode, "subagent");
160 NodeAssert.equal(result[2]!.name, "plan");
161 NodeAssert.equal(result[2]!.mode, "primary");
162 });
163
164 it("handles empty input", () => {
165 const result = parseAgentListCliOutput("");
166 NodeAssert.equal(result.length, 0);
167 });
168
169 it("skips agents with unparseable permission JSON", () => {
170 const stdout = [
171 "build (primary)",
172 " not valid json {",
173 "explore (subagent)",
174 " " + JSON.stringify([{ permission: "read", action: "allow", pattern: "*" }]),
175 ].join("\n");
176
177 const result = parseAgentListCliOutput(stdout);
178 NodeAssert.equal(result.length, 1);
179 NodeAssert.equal(result[0]!.name, "explore");
180 });
181
182 it("handles real-world permission blocks with nested paths", () => {
183 const permissions = [
184 { permission: "*", action: "allow", pattern: "*" },
185 {
186 permission: "external_directory",
187 pattern: "C:\\Users\\test\\.local\\*",
188 action: "allow",
189 },
190 { permission: "read", pattern: "*.env", action: "ask" },
191 ];
192 const stdout = ["build (primary)", " " + JSON.stringify(permissions)].join("\n");
193
194 const result = parseAgentListCliOutput(stdout);
195 NodeAssert.equal(result.length, 1);
196 NodeAssert.equal(result[0]!.permission.length, 3);
197 NodeAssert.equal(result[0]!.permission[0]!.action, "allow");
198 NodeAssert.equal(result[0]!.permission[2]!.action, "ask");
199 });
200
201 it("handles agent names with spaces", () => {
202 const stdout = [
203 "code reviewer (subagent)",
204 " " + JSON.stringify([{ permission: "read", action: "allow", pattern: "*" }]),
205 "my custom agent (primary)",
206 " " + JSON.stringify([{ permission: "edit", action: "ask", pattern: "*.ts" }]),
207 ].join("\n");
208
209 const result = parseAgentListCliOutput(stdout);
210 NodeAssert.equal(result.length, 2);
211 NodeAssert.equal(result[0]!.name, "code reviewer");
212 NodeAssert.equal(result[0]!.mode, "subagent");
213 NodeAssert.equal(result[1]!.name, "my custom agent");
214 NodeAssert.equal(result[1]!.mode, "primary");
215 });
216
217 it("marks known hidden agents", () => {
218 const stdout = [
219 "compaction (primary)",
220 " " + JSON.stringify([{ permission: "*", action: "allow", pattern: "*" }]),
221 "build (primary)",
222 " " + JSON.stringify([{ permission: "*", action: "allow", pattern: "*" }]),
223 ].join("\n");
224
225 const result = parseAgentListCliOutput(stdout);
226 NodeAssert.equal(result[0]!.hidden, true);
227 NodeAssert.equal(result[1]!.hidden, false);
228 });
229});
apps/server/src/provider/opencodeRuntime.ts+176
...@@ -5,6 +5,7 @@ import {...@@ -5,6 +5,7 @@ import {
5 createOpencodeClient,5 createOpencodeClient,
6 type Agent,6 type Agent,
7 type FilePartInput,7 type FilePartInput,
8 type Model,
8 type OpencodeClient,9 type OpencodeClient,
9 type PermissionRuleset,10 type PermissionRuleset,
10 type ProviderListResponse,11 type ProviderListResponse,
...@@ -147,6 +148,10 @@ export interface OpenCodeRuntimeShape {...@@ -147,6 +148,10 @@ export interface OpenCodeRuntimeShape {
147 readonly loadOpenCodeInventory: (148 readonly loadOpenCodeInventory: (
148 client: OpencodeClient,149 client: OpencodeClient,
149 ) => Effect.Effect<OpenCodeInventory, OpenCodeRuntimeError>;150 ) => Effect.Effect<OpenCodeInventory, OpenCodeRuntimeError>;
151 readonly loadInventoryFromCli: (input: {
152 readonly binaryPath: string;
153 readonly environment?: NodeJS.ProcessEnv;
154 }) => Effect.Effect<OpenCodeInventory, OpenCodeRuntimeError>;
150}155}
151156
152function parseServerUrlFromOutput(output: string): string | null {157function parseServerUrlFromOutput(output: string): string | null {
...@@ -160,6 +165,113 @@ function parseServerUrlFromOutput(output: string): string | null {...@@ -160,6 +165,113 @@ function parseServerUrlFromOutput(output: string): string | null {
160 return null;165 return null;
161}166}
162167
168const SLUG_LINE_RE = /^(\S+\/\S+)\s*$/;
169const AGENT_HEADER_RE = /^(.+)\s+\((\S+)\)\s*$/;
170
171// Agents that are always hidden in OpenCode but the CLI "agent list" command
172// does not expose the hidden flag. Keep in sync with OpenCode agent
173// definitions (in the OpenCode repo: packages/opencode/src/agent/agent.ts).
174const KNOWN_HIDDEN_AGENTS = new Set(["compaction", "summary", "title"]);
175
176/** @internal */
177export function parseModelsCliOutput(stdout: string): {
178 readonly providers: ReadonlyMap<
179 string,
180 { readonly id: string; readonly name: string; readonly models: { [key: string]: Model } }
181 >;
182 readonly connected: ReadonlyArray<string>;
183} {
184 const providers = new Map<
185 string,
186 { id: string; name: string; models: { [key: string]: Model } }
187 >();
188 const lines = stdout.split("\n");
189 let currentSlug: string | null = null;
190 const jsonLines: Array<string> = [];
191
192 const flushModel = () => {
193 if (currentSlug !== null && jsonLines.length > 0) {
194 const jsonStr = jsonLines.join("\n").trim();
195 if (jsonStr.length > 0) {
196 try {
197 const model = JSON.parse(jsonStr) as Model;
198 const separator = currentSlug.indexOf("/");
199 if (separator > 0) {
200 const providerID = currentSlug.slice(0, separator);
201 const modelID = currentSlug.slice(separator + 1);
202 let provider = providers.get(providerID);
203 if (!provider) {
204 provider = { id: providerID, name: providerID, models: {} };
205 providers.set(providerID, provider);
206 }
207 provider.models[modelID] = model;
208 }
209 } catch {
210 // Skip unparseable model JSON
211 }
212 }
213 }
214 currentSlug = null;
215 jsonLines.length = 0;
216 };
217
218 for (const line of lines) {
219 const slugMatch = SLUG_LINE_RE.exec(line);
220 if (slugMatch) {
221 flushModel();
222 currentSlug = slugMatch[1]!;
223 } else if (currentSlug !== null) {
224 jsonLines.push(line);
225 }
226 }
227 flushModel();
228
229 return { providers, connected: [...providers.keys()] };
230}
231
232/** @internal */
233export function parseAgentListCliOutput(stdout: string): ReadonlyArray<Agent> {
234 const agents: Array<Agent> = [];
235 const lines = stdout.split("\n");
236 let currentHeader: { name: string; mode: string } | null = null;
237 const blockLines: Array<string> = [];
238
239 const flushAgent = () => {
240 if (currentHeader !== null) {
241 const jsonStr = blockLines.join("\n").trim();
242 if (jsonStr.length > 0) {
243 try {
244 const permission = JSON.parse(jsonStr);
245 agents.push({
246 name: currentHeader.name,
247 mode: currentHeader.mode as Agent["mode"],
248 hidden: KNOWN_HIDDEN_AGENTS.has(currentHeader.name),
249 permission,
250 options: {},
251 });
252 } catch {
253 // Skip unparseable agent
254 }
255 }
256 }
257 currentHeader = null;
258 blockLines.length = 0;
259 };
260
261 for (const line of lines) {
262 const match = AGENT_HEADER_RE.exec(line);
263 if (match) {
264 flushAgent();
265 currentHeader = { name: match[1]!, mode: match[2]! };
266 } else if (currentHeader !== null) {
267 blockLines.push(line);
268 }
269 }
270 flushAgent();
271
272 return agents;
273}
274
163export function parseOpenCodeModelSlug(275export function parseOpenCodeModelSlug(
164 slug: string | null | undefined,276 slug: string | null | undefined,
165): ParsedOpenCodeModelSlug | null {277): ParsedOpenCodeModelSlug | null {
...@@ -542,12 +654,76 @@ const makeOpenCodeRuntime = Effect.gen(function* () {...@@ -542,12 +654,76 @@ const makeOpenCodeRuntime = Effect.gen(function* () {
542 Effect.map(([providerList, agents]) => ({ providerList, agents })),654 Effect.map(([providerList, agents]) => ({ providerList, agents })),
543 );655 );
544656
657 const loadInventoryFromCli: OpenCodeRuntimeShape["loadInventoryFromCli"] = (input) =>
658 Effect.gen(function* () {
659 const env = input.environment !== undefined ? { environment: input.environment } : ({} as {});
660
661 const runModelsCli = () =>
662 runOpenCodeCommand({
663 binaryPath: input.binaryPath,
664 args: ["models", "--verbose"],
665 ...env,
666 }).pipe(Effect.exit);
667 const runAgentsCli = () =>
668 runOpenCodeCommand({ binaryPath: input.binaryPath, args: ["agent", "list"], ...env }).pipe(
669 Effect.exit,
670 );
671
672 // First attempt — run both in parallel
673 let [modelsResult, agentsResult] = yield* Effect.all([runModelsCli(), runAgentsCli()], {
674 concurrency: "unbounded",
675 });
676
677 // Retry once after 1s on transient failures (e.g. SQLite "database is locked")
678 const needsModelsRetry = modelsResult._tag === "Failure" || modelsResult.value.code !== 0;
679 const needsAgentsRetry = agentsResult._tag === "Failure" || agentsResult.value.code !== 0;
680 if (needsModelsRetry || needsAgentsRetry) {
681 yield* Effect.sleep("1 second");
682 const [m2, a2] = yield* Effect.all(
683 [
684 needsModelsRetry ? runModelsCli() : Effect.succeed(modelsResult),
685 needsAgentsRetry ? runAgentsCli() : Effect.succeed(agentsResult),
686 ],
687 { concurrency: "unbounded" },
688 );
689 modelsResult = m2;
690 agentsResult = a2;
691 }
692
693 // Degrade gracefully on failure — return empty inventory (warning status, not error)
694 let connected: string[] = [];
695 let allProviders: ProviderListResponse["all"] = [];
696 if (modelsResult._tag === "Success" && modelsResult.value.code === 0) {
697 const parsed = parseModelsCliOutput(modelsResult.value.stdout);
698 connected = [...parsed.connected];
699 allProviders = [...parsed.providers.values()].map((p) => ({
700 id: p.id,
701 name: p.name,
702 source: "config" as const,
703 env: [],
704 options: {},
705 models: p.models,
706 }));
707 }
708
709 let agents: ReadonlyArray<Agent> = [];
710 if (agentsResult._tag === "Success" && agentsResult.value.code === 0) {
711 agents = parseAgentListCliOutput(agentsResult.value.stdout);
712 }
713
714 return {
715 providerList: { all: allProviders, default: {}, connected },
716 agents,
717 };
718 });
719
545 return {720 return {
546 startOpenCodeServerProcess,721 startOpenCodeServerProcess,
547 connectToOpenCodeServer,722 connectToOpenCodeServer,
548 runOpenCodeCommand,723 runOpenCodeCommand,
549 createOpenCodeSdkClient,724 createOpenCodeSdkClient,
550 loadOpenCodeInventory,725 loadOpenCodeInventory,
726 loadInventoryFromCli,
551 } satisfies OpenCodeRuntimeShape;727 } satisfies OpenCodeRuntimeShape;
552});728});
553729
apps/server/src/textGeneration/OpenCodeTextGeneration.test.ts+8
...@@ -107,6 +107,14 @@ const OpenCodeRuntimeTestDouble: OpenCodeRuntime.OpenCodeRuntimeShape = {...@@ -107,6 +107,14 @@ const OpenCodeRuntimeTestDouble: OpenCodeRuntime.OpenCodeRuntimeShape = {
107 cause: null,107 cause: null,
108 }),108 }),
109 ),109 ),
110 loadInventoryFromCli: () =>
111 Effect.fail(
112 new OpenCodeRuntime.OpenCodeRuntimeError({
113 operation: "loadInventoryFromCli",
114 detail: "OpenCodeRuntimeTestDouble.loadInventoryFromCli not used in this test",
115 cause: null,
116 }),
117 ),
110};118};
111119
112const DEFAULT_TEST_MODEL_SELECTION = {120const DEFAULT_TEST_MODEL_SELECTION = {