import { describe, expect, test } from "bun:test" import { Effect, Layer, Stream } from "effect" import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http" import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" import { Installation } from "../../src/installation" const encoder = new TextEncoder() function mockHttpClient(handler: (request: HttpClientRequest.HttpClientRequest) => Response) { const client = HttpClient.make((request) => Effect.succeed(HttpClientResponse.fromWeb(request, handler(request)))) return Layer.succeed(HttpClient.HttpClient, client) } function mockSpawner(handler: (cmd: string, args: readonly string[]) => string = () => "") { const spawner = ChildProcessSpawner.make((command) => { const std = ChildProcess.isStandardCommand(command) ? command : undefined const output = handler(std?.command ?? "", std?.args ?? []) return Effect.succeed( ChildProcessSpawner.makeHandle({ pid: ChildProcessSpawner.ProcessId(0), exitCode: Effect.succeed(ChildProcessSpawner.ExitCode(0)), isRunning: Effect.succeed(false), kill: () => Effect.void, stdin: { [Symbol.for("effect/Sink/TypeId")]: Symbol.for("effect/Sink/TypeId") } as any, stdout: output ? Stream.make(encoder.encode(output)) : Stream.empty, stderr: Stream.empty, all: Stream.empty, getInputFd: () => ({ [Symbol.for("effect/Sink/TypeId")]: Symbol.for("effect/Sink/TypeId") }) as any, getOutputFd: () => Stream.empty, }), ) }) return Layer.succeed(ChildProcessSpawner.ChildProcessSpawner, spawner) } function jsonResponse(body: unknown) { return new Response(JSON.stringify(body), { status: 200, headers: { "content-type": "application/json" }, }) } function testLayer( httpHandler: (request: HttpClientRequest.HttpClientRequest) => Response, spawnHandler?: (cmd: string, args: readonly string[]) => string, ) { return Installation.layer.pipe(Layer.provide(mockHttpClient(httpHandler)), Layer.provide(mockSpawner(spawnHandler))) } describe("installation", () => { describe("latest", () => { test("reads release version from GitHub releases", async () => { const layer = testLayer(() => jsonResponse({ tag_name: "v1.2.3" })) const result = await Effect.runPromise( Installation.Service.use((svc) => svc.latest("unknown")).pipe(Effect.provide(layer)), ) expect(result).toBe("1.2.3") }) test("strips v prefix from GitHub release tag", async () => { const layer = testLayer(() => jsonResponse({ tag_name: "v4.0.0-beta.1" })) const result = await Effect.runPromise( Installation.Service.use((svc) => svc.latest("curl")).pipe(Effect.provide(layer)), ) expect(result).toBe("4.0.0-beta.1") }) test("reads npm registry versions", async () => { const layer = testLayer( () => jsonResponse({ version: "1.5.0" }), (cmd, args) => { if (cmd === "npm" && args.includes("registry")) return "https://registry.npmjs.org\n" return "" }, ) const result = await Effect.runPromise( Installation.Service.use((svc) => svc.latest("npm")).pipe(Effect.provide(layer)), ) expect(result).toBe("1.5.0") }) test("reads npm registry versions for bun method", async () => { const layer = testLayer( () => jsonResponse({ version: "1.6.0" }), () => "", ) const result = await Effect.runPromise( Installation.Service.use((svc) => svc.latest("bun")).pipe(Effect.provide(layer)), ) expect(result).toBe("1.6.0") }) test("reads scoop manifest versions", async () => { const layer = testLayer(() => jsonResponse({ version: "2.3.4" })) const result = await Effect.runPromise( Installation.Service.use((svc) => svc.latest("scoop")).pipe(Effect.provide(layer)), ) expect(result).toBe("2.3.4") }) test("reads chocolatey feed versions", async () => { const layer = testLayer(() => jsonResponse({ d: { results: [{ Version: "3.4.5" }] } })) const result = await Effect.runPromise( Installation.Service.use((svc) => svc.latest("choco")).pipe(Effect.provide(layer)), ) expect(result).toBe("3.4.5") }) test("reads brew formulae API versions", async () => { const layer = testLayer( () => jsonResponse({ versions: { stable: "2.0.0" } }), (cmd, args) => { // getBrewFormula: return core formula (no tap) if (cmd === "brew" && args.includes("--formula") && args.includes("anomalyco/tap/opencode")) return "" if (cmd === "brew" && args.includes("--formula") && args.includes("opencode")) return "opencode" return "" }, ) const result = await Effect.runPromise( Installation.Service.use((svc) => svc.latest("brew")).pipe(Effect.provide(layer)), ) expect(result).toBe("2.0.0") }) test("reads brew tap info JSON via CLI", async () => { const brewInfoJson = JSON.stringify({ formulae: [{ versions: { stable: "2.1.0" } }], }) const layer = testLayer( () => jsonResponse({}), // HTTP not used for tap formula (cmd, args) => { if (cmd === "brew" && args.includes("anomalyco/tap/opencode") && args.includes("--formula")) return "opencode" if (cmd === "brew" && args.includes("--json=v2")) return brewInfoJson return "" }, ) const result = await Effect.runPromise( Installation.Service.use((svc) => svc.latest("brew")).pipe(Effect.provide(layer)), ) expect(result).toBe("2.1.0") }) }) })