installation.test.ts 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151
  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. }),
  28. )
  29. })
  30. return Layer.succeed(ChildProcessSpawner.ChildProcessSpawner, spawner)
  31. }
  32. function jsonResponse(body: unknown) {
  33. return new Response(JSON.stringify(body), {
  34. status: 200,
  35. headers: { "content-type": "application/json" },
  36. })
  37. }
  38. function testLayer(
  39. httpHandler: (request: HttpClientRequest.HttpClientRequest) => Response,
  40. spawnHandler?: (cmd: string, args: readonly string[]) => string,
  41. ) {
  42. return Installation.layer.pipe(Layer.provide(mockHttpClient(httpHandler)), Layer.provide(mockSpawner(spawnHandler)))
  43. }
  44. describe("installation", () => {
  45. describe("latest", () => {
  46. test("reads release version from GitHub releases", async () => {
  47. const layer = testLayer(() => jsonResponse({ tag_name: "v1.2.3" }))
  48. const result = await Effect.runPromise(
  49. Installation.Service.use((svc) => svc.latest("unknown")).pipe(Effect.provide(layer)),
  50. )
  51. expect(result).toBe("1.2.3")
  52. })
  53. test("strips v prefix from GitHub release tag", async () => {
  54. const layer = testLayer(() => jsonResponse({ tag_name: "v4.0.0-beta.1" }))
  55. const result = await Effect.runPromise(
  56. Installation.Service.use((svc) => svc.latest("curl")).pipe(Effect.provide(layer)),
  57. )
  58. expect(result).toBe("4.0.0-beta.1")
  59. })
  60. test("reads npm registry versions", async () => {
  61. const layer = testLayer(
  62. () => jsonResponse({ version: "1.5.0" }),
  63. (cmd, args) => {
  64. if (cmd === "npm" && args.includes("registry")) return "https://registry.npmjs.org\n"
  65. return ""
  66. },
  67. )
  68. const result = await Effect.runPromise(
  69. Installation.Service.use((svc) => svc.latest("npm")).pipe(Effect.provide(layer)),
  70. )
  71. expect(result).toBe("1.5.0")
  72. })
  73. test("reads npm registry versions for bun method", async () => {
  74. const layer = testLayer(
  75. () => jsonResponse({ version: "1.6.0" }),
  76. () => "",
  77. )
  78. const result = await Effect.runPromise(
  79. Installation.Service.use((svc) => svc.latest("bun")).pipe(Effect.provide(layer)),
  80. )
  81. expect(result).toBe("1.6.0")
  82. })
  83. test("reads scoop manifest versions", async () => {
  84. const layer = testLayer(() => jsonResponse({ version: "2.3.4" }))
  85. const result = await Effect.runPromise(
  86. Installation.Service.use((svc) => svc.latest("scoop")).pipe(Effect.provide(layer)),
  87. )
  88. expect(result).toBe("2.3.4")
  89. })
  90. test("reads chocolatey feed versions", async () => {
  91. const layer = testLayer(() => jsonResponse({ d: { results: [{ Version: "3.4.5" }] } }))
  92. const result = await Effect.runPromise(
  93. Installation.Service.use((svc) => svc.latest("choco")).pipe(Effect.provide(layer)),
  94. )
  95. expect(result).toBe("3.4.5")
  96. })
  97. test("reads brew formulae API versions", async () => {
  98. const layer = testLayer(
  99. () => jsonResponse({ versions: { stable: "2.0.0" } }),
  100. (cmd, args) => {
  101. // getBrewFormula: return core formula (no tap)
  102. if (cmd === "brew" && args.includes("--formula") && args.includes("anomalyco/tap/opencode")) return ""
  103. if (cmd === "brew" && args.includes("--formula") && args.includes("opencode")) return "opencode"
  104. return ""
  105. },
  106. )
  107. const result = await Effect.runPromise(
  108. Installation.Service.use((svc) => svc.latest("brew")).pipe(Effect.provide(layer)),
  109. )
  110. expect(result).toBe("2.0.0")
  111. })
  112. test("reads brew tap info JSON via CLI", async () => {
  113. const brewInfoJson = JSON.stringify({
  114. formulae: [{ versions: { stable: "2.1.0" } }],
  115. })
  116. const layer = testLayer(
  117. () => jsonResponse({}), // HTTP not used for tap formula
  118. (cmd, args) => {
  119. if (cmd === "brew" && args.includes("anomalyco/tap/opencode") && args.includes("--formula")) return "opencode"
  120. if (cmd === "brew" && args.includes("--json=v2")) return brewInfoJson
  121. return ""
  122. },
  123. )
  124. const result = await Effect.runPromise(
  125. Installation.Service.use((svc) => svc.latest("brew")).pipe(Effect.provide(layer)),
  126. )
  127. expect(result).toBe("2.1.0")
  128. })
  129. })
  130. })