protocol.test.ts 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123
  1. import { describe, expect, test } from "bun:test"
  2. import { Effect } from "effect"
  3. import { Backend, Frontend, Handshake } from "../src/protocol"
  4. test("decodes ui.matches text params", () => {
  5. expect(
  6. Frontend.decodeRequest({
  7. jsonrpc: "2.0",
  8. id: 1,
  9. method: "ui.matches",
  10. params: { text: "OpenCode [ready].*" },
  11. }),
  12. ).toMatchObject({ method: "ui.matches", params: { text: "OpenCode [ready].*" } })
  13. expect(() =>
  14. Frontend.decodeRequest({
  15. jsonrpc: "2.0",
  16. id: 1,
  17. method: "ui.matches",
  18. params: { pattern: "OpenCode.*" },
  19. }),
  20. ).toThrow()
  21. })
  22. const params: Handshake.Params = {
  23. client: { name: "opencode-drive", version: "test" },
  24. expectedRole: "ui",
  25. offeredVersions: [1],
  26. requiredCapabilities: ["ui.state"],
  27. optionalCapabilities: ["ui.capture", "future.capability"],
  28. }
  29. const ui: Handshake.DispatchAction = {
  30. role: "ui",
  31. server: { name: "opencode", version: "test" },
  32. capabilities: Frontend.Capabilities,
  33. }
  34. describe("simulation.handshake", () => {
  35. test("decodes through both endpoint request protocols", () => {
  36. const request = {
  37. jsonrpc: "2.0" as const,
  38. id: 1,
  39. method: "simulation.handshake" as const,
  40. params,
  41. }
  42. expect(Frontend.decodeRequest(request)).toEqual(request)
  43. expect(Backend.decodeRequest({ ...request, params: { ...params, expectedRole: "backend" } })).toMatchObject({
  44. method: "simulation.handshake",
  45. params: { expectedRole: "backend" },
  46. })
  47. })
  48. test("rejects invalid version and capability declarations", () => {
  49. const request = {
  50. jsonrpc: "2.0" as const,
  51. id: 1,
  52. method: "simulation.handshake" as const,
  53. params,
  54. }
  55. expect(() =>
  56. Frontend.decodeRequest({
  57. ...request,
  58. params: { ...params, offeredVersions: [] },
  59. }),
  60. ).toThrow()
  61. expect(() =>
  62. Frontend.decodeRequest({
  63. ...request,
  64. params: { ...params, requiredCapabilities: ["ui.state", "ui.state"] },
  65. }),
  66. ).toThrow()
  67. expect(() =>
  68. Frontend.decodeRequest({
  69. ...request,
  70. params: { ...params, optionalCapabilities: [""] },
  71. }),
  72. ).toThrow()
  73. })
  74. test("selects the protocol and advertises only installed capabilities", async () => {
  75. await expect(Effect.runPromise(Handshake.dispatch(ui, params))).resolves.toEqual({
  76. protocolVersion: 1,
  77. role: "ui",
  78. server: { name: "opencode", version: "test" },
  79. capabilities: [...Frontend.Capabilities],
  80. })
  81. })
  82. test("rejects a role mismatch", async () => {
  83. await expect(
  84. Effect.runPromise(Handshake.dispatch(ui, { ...params, expectedRole: "backend" })),
  85. ).rejects.toMatchObject({ _tag: "SimulationHandshake.RoleMismatchError", expected: "backend", actual: "ui" })
  86. })
  87. test("rejects unsupported protocol versions", async () => {
  88. await expect(Effect.runPromise(Handshake.dispatch(ui, { ...params, offeredVersions: [2] }))).rejects.toMatchObject({
  89. _tag: "SimulationHandshake.UnsupportedProtocolError",
  90. offered: [2],
  91. supported: [1],
  92. })
  93. })
  94. test("rejects a missing required capability but ignores missing optional capabilities", async () => {
  95. await expect(
  96. Effect.runPromise(
  97. Handshake.dispatch(ui, {
  98. ...params,
  99. requiredCapabilities: ["ui.state", "ui.future"],
  100. }),
  101. ),
  102. ).rejects.toMatchObject({ _tag: "SimulationHandshake.MissingCapabilityError", missing: ["ui.future"] })
  103. await expect(
  104. Effect.runPromise(
  105. Handshake.dispatch(ui, {
  106. ...params,
  107. requiredCapabilities: [],
  108. optionalCapabilities: ["ui.future"],
  109. }),
  110. ),
  111. ).resolves.toMatchObject({ capabilities: Frontend.Capabilities })
  112. })
  113. })