session-target.test.ts 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596
  1. import { afterEach, describe, expect, mock, spyOn, test } from "bun:test"
  2. import { OpenCode, type LocationGetOutput, type ModelRef, type SessionInfo } from "@opencode-ai/client/promise"
  3. import { resolveSessionTarget, SessionTargetMutationError } from "../src/session-target"
  4. function location(directory: string, workspaceID?: string): LocationGetOutput {
  5. return { directory, workspaceID, project: { id: "project", directory, canonical: directory } }
  6. }
  7. function session(id: string, directory: string, workspaceID?: string, model?: ModelRef): SessionInfo {
  8. return {
  9. id,
  10. projectID: "project",
  11. title: id,
  12. location: { directory, workspaceID },
  13. model,
  14. cost: 0,
  15. tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
  16. time: { created: 1, updated: 1 },
  17. }
  18. }
  19. const prepare = async (input: { model: ModelRef | undefined; agent: string | undefined }) => ({
  20. model: input.model,
  21. agent: input.agent,
  22. })
  23. afterEach(() => mock.restore())
  24. describe("session target resolver", () => {
  25. test("adopts an explicit Session location and model", async () => {
  26. const client = OpenCode.make({ baseUrl: "https://opencode.test" })
  27. const selected = session("ses_resume", "/session", "work_1", { providerID: "openai", id: "gpt-5" })
  28. spyOn(client.session, "get").mockResolvedValue(selected)
  29. spyOn(client.location, "get").mockResolvedValue(location("/session", "work_1"))
  30. const target = await resolveSessionTarget({ client, session: selected.id, prepare })
  31. expect(target).toMatchObject({
  32. session: { id: "ses_resume" },
  33. location: { directory: "/session", workspaceID: "work_1" },
  34. model: { providerID: "openai", id: "gpt-5" },
  35. resume: true,
  36. })
  37. })
  38. test("paginates to continue the exact implicit workspace", async () => {
  39. const client = OpenCode.make({ baseUrl: "https://opencode.test" })
  40. spyOn(client.location, "get").mockResolvedValue(location("/project"))
  41. const explicit = Array.from({ length: 50 }, (_, index) => session(`ses_${index}`, "/project", `work_${index}`))
  42. const list = spyOn(client.session, "list")
  43. .mockResolvedValueOnce({ data: explicit, cursor: { next: "page_2" } })
  44. .mockResolvedValueOnce({ data: [session("ses_implicit", "/project")], cursor: {} })
  45. const target = await resolveSessionTarget({ client, location: { directory: "/project" }, continue: true, prepare })
  46. expect(list).toHaveBeenCalledTimes(2)
  47. expect(target.session.id).toBe("ses_implicit")
  48. })
  49. test("prepares a fresh Session at the server Location before creation", async () => {
  50. const client = OpenCode.make({ baseUrl: "https://opencode.test" })
  51. const order: string[] = []
  52. spyOn(client.location, "get").mockResolvedValue(location("/server", "work_1"))
  53. const create = spyOn(client.session, "create").mockImplementation(async (input) => {
  54. order.push("create")
  55. expect(input).toMatchObject({ agent: "prepared", location: { directory: "/server", workspaceID: "work_1" } })
  56. return session("ses_fresh", "/server", "work_1")
  57. })
  58. await resolveSessionTarget({
  59. client,
  60. agent: "requested",
  61. prepare: async (input) => {
  62. order.push("prepare")
  63. expect(input.location.workspaceID).toBe("work_1")
  64. return { model: input.model, agent: "prepared" }
  65. },
  66. })
  67. expect(create).toHaveBeenCalledTimes(1)
  68. expect(order).toEqual(["prepare", "create"])
  69. })
  70. test("uses the agent resolved by the server for a fresh Session", async () => {
  71. const client = OpenCode.make({ baseUrl: "https://opencode.test" })
  72. spyOn(client.location, "get").mockResolvedValue(location("/project"))
  73. spyOn(client.session, "create").mockResolvedValue({ ...session("ses_fresh", "/project"), agent: "review" })
  74. const target = await resolveSessionTarget({ client, prepare })
  75. expect(target.agent).toBe("review")
  76. })
  77. test("does not retry an ambiguous Session creation", async () => {
  78. const client = OpenCode.make({ baseUrl: "https://opencode.test" })
  79. spyOn(client.location, "get").mockResolvedValue(location("/project"))
  80. spyOn(client.session, "create").mockRejectedValue(new Error("connection closed after create"))
  81. await expect(resolveSessionTarget({ client, prepare })).rejects.toBeInstanceOf(SessionTargetMutationError)
  82. })
  83. })