| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156 |
- 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,
- unref: Effect.succeed(Effect.void),
- }),
- )
- })
- 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)
- // kilocode_change start
- if (cmd === "brew" && args.includes("--formula") && args.includes("Kilo-Org/tap/kilo")) return ""
- if (cmd === "brew" && args.includes("--formula") && args.includes("kilo")) return "kilo"
- // kilocode_change end
- 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) => {
- // kilocode_change start
- if (cmd === "brew" && args.includes("Kilo-Org/tap/kilo") && args.includes("--formula")) return "kilo"
- // kilocode_change end
- 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")
- })
- })
- })
|