| 1 | import * as NodeAssert from "node:assert/strict"; |
| 2 | |
| 3 | import * as NodeServices from "@effect/platform-node/NodeServices"; |
| 4 | import { createOpencodeClient, type OpencodeClient } from "@opencode-ai/sdk/v2"; |
| 5 | import { it } from "@effect/vitest"; |
| 6 | import * as Effect from "effect/Effect"; |
| 7 | import * as Fiber from "effect/Fiber"; |
| 8 | import * as FileSystem from "effect/FileSystem"; |
| 9 | import * as Layer from "effect/Layer"; |
| 10 | import * as Path from "effect/Path"; |
| 11 | import * as Queue from "effect/Queue"; |
| 12 | import * as HostProcess from "@t3tools/shared/HostProcess"; |
| 13 | |
| 14 | import * as OpenCodeRuntime from "./OpenCodeRuntime.ts"; |
| 15 | import * as OpenCodeServerLedger from "./OpenCodeServerLedger.ts"; |
| 16 | |
| 17 | const layerTest = OpenCodeRuntime.layer.pipe( |
| 18 | Layer.provide(OpenCodeServerLedger.layerTest), |
| 19 | Layer.provideMerge(NodeServices.layer), |
| 20 | ); |
| 21 | |
| 22 | it.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 | }); |