installation.test.ts 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156
  1. import { describe, expect, test } from "bun:test"
  2. import { Effect, Layer, Stream } from "effect"
  3. import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http"
  4. import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
  5. import { Installation } from "../../src/installation"
  6. const encoder = new TextEncoder()
  7. function mockHttpClient(handler: (request: HttpClientRequest.HttpClientRequest) => Response) {
  8. const client = HttpClient.make((request) => Effect.succeed(HttpClientResponse.fromWeb(request, handler(request))))
  9. return Layer.succeed(HttpClient.HttpClient, client)
  10. }
  11. function mockSpawner(handler: (cmd: string, args: readonly string[]) => string = () => "") {
  12. const spawner = ChildProcessSpawner.make((command) => {
  13. const std = ChildProcess.isStandardCommand(command) ? command : undefined
  14. const output = handler(std?.command ?? "", std?.args ?? [])
  15. return Effect.succeed(
  16. ChildProcessSpawner.makeHandle({
  17. pid: ChildProcessSpawner.ProcessId(0),
  18. exitCode: Effect.succeed(ChildProcessSpawner.ExitCode(0)),
  19. isRunning: Effect.succeed(false),
  20. kill: () => Effect.void,
  21. stdin: { [Symbol.for("effect/Sink/TypeId")]: Symbol.for("effect/Sink/TypeId") } as any,
  22. stdout: output ? Stream.make(encoder.encode(output)) : Stream.empty,
  23. stderr: Stream.empty,
  24. all: Stream.empty,
  25. getInputFd: () => ({ [Symbol.for("effect/Sink/TypeId")]: Symbol.for("effect/Sink/TypeId") }) as any,
  26. getOutputFd: () => Stream.empty,
  27. unref: Effect.succeed(Effect.void),
  28. }),
  29. )
  30. })
  31. return Layer.succeed(ChildProcessSpawner.ChildProcessSpawner, spawner)
  32. }
  33. function jsonResponse(body: unknown) {
  34. return new Response(JSON.stringify(body), {
  35. status: 200,
  36. headers: { "content-type": "application/json" },
  37. })
  38. }
  39. function testLayer(
  40. httpHandler: (request: HttpClientRequest.HttpClientRequest) => Response,
  41. spawnHandler?: (cmd: string, args: readonly string[]) => string,
  42. ) {
  43. return Installation.layer.pipe(Layer.provide(mockHttpClient(httpHandler)), Layer.provide(mockSpawner(spawnHandler)))
  44. }
  45. describe("installation", () => {
  46. describe("latest", () => {
  47. test("reads release version from GitHub releases", async () => {
  48. const layer = testLayer(() => jsonResponse({ tag_name: "v1.2.3" }))
  49. const result = await Effect.runPromise(
  50. Installation.Service.use((svc) => svc.latest("unknown")).pipe(Effect.provide(layer)),
  51. )
  52. expect(result).toBe("1.2.3")
  53. })
  54. test("strips v prefix from GitHub release tag", async () => {
  55. const layer = testLayer(() => jsonResponse({ tag_name: "v4.0.0-beta.1" }))
  56. const result = await Effect.runPromise(
  57. Installation.Service.use((svc) => svc.latest("curl")).pipe(Effect.provide(layer)),
  58. )
  59. expect(result).toBe("4.0.0-beta.1")
  60. })
  61. test("reads npm registry versions", async () => {
  62. const layer = testLayer(
  63. () => jsonResponse({ version: "1.5.0" }),
  64. (cmd, args) => {
  65. if (cmd === "npm" && args.includes("registry")) return "https://registry.npmjs.org\n"
  66. return ""
  67. },
  68. )
  69. const result = await Effect.runPromise(
  70. Installation.Service.use((svc) => svc.latest("npm")).pipe(Effect.provide(layer)),
  71. )
  72. expect(result).toBe("1.5.0")
  73. })
  74. test("reads npm registry versions for bun method", async () => {
  75. const layer = testLayer(
  76. () => jsonResponse({ version: "1.6.0" }),
  77. () => "",
  78. )
  79. const result = await Effect.runPromise(
  80. Installation.Service.use((svc) => svc.latest("bun")).pipe(Effect.provide(layer)),
  81. )
  82. expect(result).toBe("1.6.0")
  83. })
  84. test("reads scoop manifest versions", async () => {
  85. const layer = testLayer(() => jsonResponse({ version: "2.3.4" }))
  86. const result = await Effect.runPromise(
  87. Installation.Service.use((svc) => svc.latest("scoop")).pipe(Effect.provide(layer)),
  88. )
  89. expect(result).toBe("2.3.4")
  90. })
  91. test("reads chocolatey feed versions", async () => {
  92. const layer = testLayer(() => jsonResponse({ d: { results: [{ Version: "3.4.5" }] } }))
  93. const result = await Effect.runPromise(
  94. Installation.Service.use((svc) => svc.latest("choco")).pipe(Effect.provide(layer)),
  95. )
  96. expect(result).toBe("3.4.5")
  97. })
  98. test("reads brew formulae API versions", async () => {
  99. const layer = testLayer(
  100. () => jsonResponse({ versions: { stable: "2.0.0" } }),
  101. (cmd, args) => {
  102. // getBrewFormula: return core formula (no tap)
  103. // kilocode_change start
  104. if (cmd === "brew" && args.includes("--formula") && args.includes("Kilo-Org/tap/kilo")) return ""
  105. if (cmd === "brew" && args.includes("--formula") && args.includes("kilo")) return "kilo"
  106. // kilocode_change end
  107. return ""
  108. },
  109. )
  110. const result = await Effect.runPromise(
  111. Installation.Service.use((svc) => svc.latest("brew")).pipe(Effect.provide(layer)),
  112. )
  113. expect(result).toBe("2.0.0")
  114. })
  115. test("reads brew tap info JSON via CLI", async () => {
  116. const brewInfoJson = JSON.stringify({
  117. formulae: [{ versions: { stable: "2.1.0" } }],
  118. })
  119. const layer = testLayer(
  120. () => jsonResponse({}), // HTTP not used for tap formula
  121. (cmd, args) => {
  122. // kilocode_change start
  123. if (cmd === "brew" && args.includes("Kilo-Org/tap/kilo") && args.includes("--formula")) return "kilo"
  124. // kilocode_change end
  125. if (cmd === "brew" && args.includes("--json=v2")) return brewInfoJson
  126. return ""
  127. },
  128. )
  129. const result = await Effect.runPromise(
  130. Installation.Service.use((svc) => svc.latest("brew")).pipe(Effect.provide(layer)),
  131. )
  132. expect(result).toBe("2.1.0")
  133. })
  134. })
  135. })