1import * as NodeAssert from "node:assert/strict";
2
3import * as NodeServices from "@effect/platform-node/NodeServices";
4import { createOpencodeClient, type OpencodeClient } from "@opencode-ai/sdk/v2";
5import { it } from "@effect/vitest";
6import * as Effect from "effect/Effect";
7import * as Fiber from "effect/Fiber";
8import * as FileSystem from "effect/FileSystem";
9import * as Layer from "effect/Layer";
10import * as Path from "effect/Path";
11import * as Queue from "effect/Queue";
12import * as HostProcess from "@t3tools/shared/HostProcess";
13
14import * as OpenCodeRuntime from "./OpenCodeRuntime.ts";
15import * as OpenCodeServerLedger from "./OpenCodeServerLedger.ts";
16
17const layerTest = OpenCodeRuntime.layer.pipe(
18 Layer.provide(OpenCodeServerLedger.layerTest),
19 Layer.provideMerge(NodeServices.layer),
20);
21
22it.layer(layerTest)("OpenCodeRuntime inventory", (it) => {
23 it.effect("aborts pending SDK requests when inventory loading is interrupted", () =>
24 Effect.gen(function* () {
25 const runtime = yield* OpenCodeRuntime.OpenCodeRuntime;
26 const started = yield* Queue.make<void>();
27 const aborted = yield* Queue.make<string>();
28 const client = createOpencodeClient({
29 baseUrl: "http://opencode.test",
30 fetch: Object.assign(
31 (input: string | Request | URL) => {
32 const request = input instanceof Request ? input : new Request(input.toString());
33 return new Promise<Response>((_resolve, reject) => {
34 request.signal.addEventListener(
35 "abort",
36 () => {
37 Queue.offerUnsafe(aborted, new URL(request.url).pathname);
38 reject(request.signal.reason);
39 },
40 { once: true },
41 );
42 Queue.offerUnsafe(started, undefined);
43 });
44 },
45 { preconnect: () => undefined },
46 ),
47 });
48
49 const inventoryFiber = yield* runtime.loadOpenCodeInventory(client).pipe(Effect.forkChild);
50 yield* Queue.takeN(started, 4);
51 yield* Fiber.interrupt(inventoryFiber);
52
53 NodeAssert.deepEqual((yield* Queue.takeAll(aborted)).toSorted(), [
54 "/agent",
55 "/command",
56 "/provider",
57 "/skill",
58 ]);
59 }),
60 );
61
62 it.effect("discovers directory-scoped commands without retaining prompt templates", () =>
63 Effect.gen(function* () {
64 const runtime = yield* OpenCodeRuntime.OpenCodeRuntime;
65 const requests: Request[] = [];
66 const client = createOpencodeClient({
67 baseUrl: "http://opencode.test",
68 directory: "/workspace/project",
69 fetch: Object.assign(
70 async (input: string | Request | URL) => {
71 const request = input instanceof Request ? input : new Request(input.toString());
72 requests.push(request);
73 const route = new URL(request.url).pathname;
74 return Response.json(
75 route === "/provider"
76 ? { connected: ["openai"], all: [], default: {} }
77 : route === "/command"
78 ? [
79 {
80 name: "review",
81 description: "Review changes",
82 source: "command",
83 hints: ["$ARGUMENTS"],
84 template: "private native template",
85 },
86 ]
87 : [],
88 );
89 },
90 { preconnect: () => undefined },
91 ),
92 });
93 const inventory = yield* runtime.loadOpenCodeInventory(client);
94 NodeAssert.deepEqual(inventory.commands, [
95 {
96 name: "review",
97 description: "Review changes",
98 source: "command",
99 hints: ["$ARGUMENTS"],
100 },
101 ]);
102 const commandRequest = requests.find(
103 (request) => new URL(request.url).pathname === "/command",
104 );
105 NodeAssert.ok(commandRequest);
106 NodeAssert.equal(
107 new URL(commandRequest.url).searchParams.get("directory") ??
108 decodeURIComponent(commandRequest.headers.get("x-opencode-directory") ?? ""),
109 "/workspace/project",
110 );
111 }),
112 );
113
114 it.effect("keeps provider inventory when agent discovery fails", () =>
115 Effect.gen(function* () {
116 const runtime = yield* OpenCodeRuntime.OpenCodeRuntime;
117 const client = {
118 provider: {
119 list: () =>
120 Promise.resolve({
121 data: {
122 connected: ["openai"],
123 all: [],
124 default: {},
125 },
126 }),
127 },
128 app: {
129 agents: () => Promise.reject(new Error("agents endpoint unavailable")),
130 skills: () => Promise.resolve({ data: [] }),
131 },
132 } as unknown as OpencodeClient;
133
134 const inventory = yield* runtime.loadOpenCodeInventory(client);
135
136 NodeAssert.deepEqual(inventory.providerList.connected, ["openai"]);
137 NodeAssert.deepEqual(inventory.agents, []);
138 NodeAssert.deepEqual(inventory.skills, []);
139 }),
140 );
141
142 it.effect("keeps provider inventory when skill discovery fails", () =>
143 Effect.gen(function* () {
144 const runtime = yield* OpenCodeRuntime.OpenCodeRuntime;
145 const client = {
146 provider: {
147 list: () =>
148 Promise.resolve({
149 data: {
150 connected: ["openai"],
151 all: [],
152 default: {},
153 },
154 }),
155 },
156 app: {
157 agents: () => Promise.resolve({ data: [] }),
158 skills: () => Promise.reject(new Error("skills endpoint unavailable")),
159 },
160 } as unknown as OpencodeClient;
161
162 const inventory = yield* runtime.loadOpenCodeInventory(client);
163
164 NodeAssert.deepEqual(inventory.providerList.connected, ["openai"]);
165 NodeAssert.deepEqual(inventory.agents, []);
166 NodeAssert.deepEqual(inventory.skills, []);
167 }),
168 );
169
170 it.effect("keeps only SDK skill metadata in inventory", () =>
171 Effect.gen(function* () {
172 const runtime = yield* OpenCodeRuntime.OpenCodeRuntime;
173 const client = {
174 provider: {
175 list: () =>
176 Promise.resolve({
177 data: {
178 connected: ["openai"],
179 all: [],
180 default: {},
181 },
182 }),
183 },
184 app: {
185 agents: () => Promise.resolve({ data: [] }),
186 skills: () =>
187 Promise.resolve({
188 data: [
189 {
190 name: "review",
191 description: "Review code changes",
192 location: "/skills/review/SKILL.md",
193 content: "unused skill content",
194 },
195 ],
196 }),
197 },
198 } as unknown as OpencodeClient;
199
200 const inventory = yield* runtime.loadOpenCodeInventory(client);
201
202 NodeAssert.deepEqual(inventory.skills, [
203 {
204 name: "review",
205 description: "Review code changes",
206 location: "/skills/review/SKILL.md",
207 },
208 ]);
209 }),
210 );
211
212 it.effect("drops oversized CLI skill output without losing the model inventory", () =>
213 Effect.gen(function* () {
214 const fs = yield* FileSystem.FileSystem;
215 const path = yield* Path.Path;
216 const hostEnvironment = yield* HostProcess.Environment;
217 const executablePath = yield* HostProcess.ExecutablePath;
218 const hostPlatform = yield* HostProcess.Platform;
219 const tempDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-opencode-inventory-" });
220 const isWindows = hostPlatform === "win32";
221 const binaryPath = path.join(tempDir, isWindows ? "opencode.cmd" : "opencode");
222 const scriptPath = path.join(tempDir, "opencode.mjs");
223 const oversizedContentBytes = 8 * 1024 * 1024 + 1;
224
225 yield* fs.writeFileString(
226 scriptPath,
227 [
228 'if (process.argv[2] === "models") {',
229 ' process.stdout.write(`openai/gpt-test\\n{"id":"gpt-test","providerID":"openai","name":"GPT Test"}\\n`);',
230 '} else if (process.argv[2] === "debug") {',
231 ` const content = "x".repeat(${oversizedContentBytes});`,
232 ' process.stdout.write(`[{"name":"oversized","content":"${content}"}]`);',
233 "}",
234 "",
235 ].join("\n"),
236 );
237 yield* fs.writeFileString(
238 binaryPath,
239 [
240 ...(isWindows ? ["@echo off"] : ["#!/bin/sh"]),
241 isWindows
242 ? '"%T3_TEST_NODE_BINARY%" "%T3_TEST_OPENCODE_SCRIPT%" %*'
243 : 'exec "$T3_TEST_NODE_BINARY" "$T3_TEST_OPENCODE_SCRIPT" "$@"',
244 "",
245 ].join("\n"),
246 );
247 if (!isWindows) {
248 yield* fs.chmod(binaryPath, 0o755);
249 }
250
251 const runtime = yield* OpenCodeRuntime.OpenCodeRuntime;
252 const inventory = yield* runtime.loadInventoryFromCli({
253 binaryPath,
254 cwd: tempDir,
255 environment: {
256 ...hostEnvironment,
257 T3_TEST_NODE_BINARY: executablePath,
258 T3_TEST_OPENCODE_SCRIPT: scriptPath,
259 },
260 });
261
262 NodeAssert.deepEqual(inventory.providerList.connected, ["openai"]);
263 NodeAssert.equal(inventory.skills.length, 0);
264 }),
265 );
266
267 it.effect("caps and drains command stdout and stderr when requested", () =>
268 Effect.gen(function* () {
269 const runtime = yield* OpenCodeRuntime.OpenCodeRuntime;
270 const executablePath = yield* HostProcess.ExecutablePath;
271 const outputBytes = 2 * 1024 * 1024;
272 const result = yield* runtime.runOpenCodeCommand({
273 binaryPath: executablePath,
274 args: [
275 "-e",
276 `process.stdout.write("o".repeat(${outputBytes})); process.stderr.write("e".repeat(${outputBytes}));`,
277 ],
278 maxOutputBytes: 64,
279 });
280
281 NodeAssert.equal(result.stdout, "o".repeat(64));
282 NodeAssert.equal(result.stderr, "e".repeat(64));
283 NodeAssert.equal(result.code, 0);
284 }),
285 );
286});