httpapi-v2-location.test.ts 3.3 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495
  1. import { afterEach, describe, expect, test } from "bun:test"
  2. import { EventV2 } from "@opencode-ai/core/event"
  3. import { Location } from "@opencode-ai/core/location"
  4. import { Context, Schema } from "effect"
  5. import { HttpApiApp } from "../../src/server/routes/instance/httpapi/server"
  6. import { resetDatabase } from "../fixture/db"
  7. import { disposeAllInstances, tmpdir } from "../fixture/fixture"
  8. const context = Context.empty() as Context.Context<unknown>
  9. function request(route: string, directory: string, init: RequestInit = {}) {
  10. const headers = new Headers(init.headers)
  11. headers.set("x-opencode-directory", directory)
  12. return HttpApiApp.webHandler().handler(
  13. new Request(`http://localhost${route}`, {
  14. ...init,
  15. headers,
  16. }),
  17. context,
  18. )
  19. }
  20. const Event = Schema.Struct({
  21. id: EventV2.ID,
  22. type: Schema.String,
  23. location: Schema.optional(Location.Ref),
  24. data: Schema.Unknown,
  25. })
  26. async function readEvent(reader: ReadableStreamDefaultReader<Uint8Array>) {
  27. const value = await reader.read()
  28. if (value.done) throw new Error("event stream closed")
  29. return Schema.decodeUnknownSync(Event)(JSON.parse(new TextDecoder().decode(value.value).replace(/^data: /, "")))
  30. }
  31. async function readEventType(reader: ReadableStreamDefaultReader<Uint8Array>, type: string) {
  32. for (let index = 0; index < 20; index++) {
  33. const event = await readEvent(reader)
  34. if (event.type === type) return event
  35. }
  36. throw new Error(`timed out waiting for ${type}`)
  37. }
  38. afterEach(async () => {
  39. await disposeAllInstances()
  40. await resetDatabase()
  41. })
  42. describe("v2 location HttpApi", () => {
  43. test("decodes EventV2 location refs without resolved project metadata", () => {
  44. expect(
  45. Schema.decodeUnknownSync(Event)({
  46. id: "evt_test",
  47. type: "file.watcher.updated",
  48. location: { directory: "/tmp/project" },
  49. data: {},
  50. }),
  51. ).toMatchObject({ location: { directory: "/tmp/project" } })
  52. })
  53. test("returns command and skill snapshots with resolved locations", async () => {
  54. await using tmp = await tmpdir({ git: true })
  55. for (const route of ["/api/command", "/api/skill"]) {
  56. const response = await request(route, tmp.path)
  57. expect(response.status).toBe(200)
  58. const body = (await response.json()) as {
  59. location: { directory: string; project: { id: string } }
  60. data: unknown
  61. }
  62. expect(body.data).toBeArray()
  63. expect(body.location.directory).toBe(tmp.path)
  64. expect(body.location.project.id).toBeTruthy()
  65. }
  66. })
  67. test("streams native EventV2 payloads across locations", async () => {
  68. await using subscriber = await tmpdir({ git: true })
  69. await using publisher = await tmpdir({ git: true })
  70. const response = await request("/api/event", subscriber.path)
  71. const reader = response.body!.getReader()
  72. const connected = await readEvent(reader)
  73. expect(connected.type).toBe("server.connected")
  74. expect(connected.location).toBeUndefined()
  75. const created = await request("/session", publisher.path, { method: "POST" })
  76. expect(created.status).toBe(200)
  77. expect(await readEventType(reader, "session.created")).toMatchObject({
  78. type: "session.created",
  79. location: { directory: publisher.path },
  80. data: { sessionID: expect.any(String) },
  81. })
  82. await reader.cancel()
  83. })
  84. })