serve-process.test.ts 2.8 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061
  1. // Subprocess integration tests for `opencode serve`. Spawns the real CLI in
  2. // headless mode and exercises it over HTTP — this is the only test tier that
  3. // catches bugs spanning argv → server boot → routing → instance loading.
  4. //
  5. // `serve` is long-lived: the harness returns a handle (url/port/kill/exited)
  6. // and kills the process when the test scope closes. The OS-assigned port is
  7. // parsed off the "listening on http://..." line.
  8. import { describe, expect } from "bun:test"
  9. import { Effect } from "effect"
  10. import { HttpClient } from "effect/unstable/http"
  11. import { cliIt } from "../../lib/cli-process"
  12. describe("kirincode serve (subprocess)", () => {
  13. // Smoke test: server starts, binds a port, and /global/health responds.
  14. // If this fails, all other serve tests likely will too — debug here first.
  15. cliIt.live(
  16. "starts, binds a port, and serves /global/health",
  17. ({ kirincode }) =>
  18. Effect.gen(function* () {
  19. const server = yield* kirincode.serve()
  20. expect(server.port).toBeGreaterThan(0)
  21. expect(server.url).toMatch(/^http:\/\//)
  22. const client = yield* HttpClient.HttpClient
  23. const res = yield* client.get(`${server.url}/global/health`)
  24. expect(res.status).toBe(200)
  25. // GlobalHealth schema is { success: true, ... } | { success: false, error }.
  26. // We don't lock in further shape here — any 200 with parseable JSON is
  27. // enough proof the routing + auth-bypass + instance loading is alive.
  28. const body = yield* res.json
  29. expect(body).toBeDefined()
  30. }),
  31. 60_000,
  32. )
  33. // The scope-close finalizer must actually terminate the child. Without this
  34. // test a regression in the kill path (e.g. a future refactor that forgets
  35. // to wire the finalizer) would leak processes on every test run.
  36. cliIt.live(
  37. "kills the subprocess on scope close",
  38. ({ kirincode }) =>
  39. Effect.gen(function* () {
  40. // Inner scope so we can observe `.exited` resolving after it closes.
  41. const exitedPromise = yield* Effect.scoped(
  42. Effect.gen(function* () {
  43. const server = yield* kirincode.serve()
  44. // Capture the Promise, not the resolved value — scope closes after
  45. // this gen returns, at which point the finalizer kills the child.
  46. return server.exited
  47. }),
  48. )
  49. // After scope close: finalizer fired, process must have exited.
  50. const code = yield* Effect.promise(() => exitedPromise)
  51. // Bun reports the exit code; SIGTERM-killed processes return non-null
  52. // (typically 143 on POSIX). We just require resolution within a sane
  53. // window — anything else means the kill didn't take.
  54. expect(typeof code === "number" || code === null).toBe(true)
  55. }),
  56. 60_000,
  57. )
  58. })