sync.test.tsx 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149
  1. /** @jsxImportSource @opentui/solid */
  2. import { describe, expect, test } from "bun:test"
  3. import { testRender } from "@opentui/solid"
  4. import { onMount } from "solid-js"
  5. import { Global } from "@opencode-ai/core/global"
  6. import { ArgsProvider } from "../../../../src/cli/cmd/tui/context/args"
  7. import { ExitProvider } from "../../../../src/cli/cmd/tui/context/exit"
  8. import { KVProvider, useKV } from "../../../../src/cli/cmd/tui/context/kv"
  9. import { ProjectProvider } from "../../../../src/cli/cmd/tui/context/project"
  10. import { SDKProvider, type EventSource } from "../../../../src/cli/cmd/tui/context/sdk"
  11. import { SyncProvider, useSync } from "../../../../src/cli/cmd/tui/context/sync"
  12. import { tmpdir } from "../../../fixture/fixture"
  13. const worktree = "/tmp/opencode"
  14. const directory = `${worktree}/packages/opencode`
  15. async function wait(fn: () => boolean, timeout = 2000) {
  16. const start = Date.now()
  17. while (!fn()) {
  18. if (Date.now() - start > timeout) throw new Error("timed out waiting for condition")
  19. await Bun.sleep(10)
  20. }
  21. }
  22. function json(data: unknown) {
  23. return new Response(JSON.stringify(data), {
  24. headers: { "content-type": "application/json" },
  25. })
  26. }
  27. function eventSource(): EventSource {
  28. return {
  29. subscribe: async () => () => {},
  30. }
  31. }
  32. function createFetch() {
  33. const session = [] as URL[]
  34. const fetch = (async (input: RequestInfo | URL) => {
  35. const url = new URL(input instanceof Request ? input.url : String(input))
  36. if (url.pathname === "/session") session.push(url)
  37. switch (url.pathname) {
  38. case "/agent":
  39. case "/command":
  40. case "/experimental/workspace":
  41. case "/experimental/workspace/status":
  42. case "/formatter":
  43. case "/lsp":
  44. return json([])
  45. case "/config":
  46. case "/experimental/resource":
  47. case "/mcp":
  48. case "/provider/auth":
  49. case "/session/status":
  50. return json({})
  51. case "/config/providers":
  52. return json({ providers: {}, default: {} })
  53. case "/experimental/console":
  54. return json({ consoleManagedProviders: [], switchableOrgCount: 0 })
  55. case "/path":
  56. return json({ home: "", state: "", config: "", worktree, directory })
  57. case "/project/current":
  58. return json({ id: "proj_test" })
  59. case "/provider":
  60. return json({ all: [], default: {}, connected: [] })
  61. case "/session":
  62. return json([])
  63. case "/vcs":
  64. return json({ branch: "main" })
  65. }
  66. throw new Error(`unexpected request: ${url.pathname}`)
  67. }) as typeof globalThis.fetch
  68. return { fetch, session }
  69. }
  70. async function mount() {
  71. const calls = createFetch()
  72. let sync!: ReturnType<typeof useSync>
  73. let kv!: ReturnType<typeof useKV>
  74. let done!: () => void
  75. const ready = new Promise<void>((resolve) => {
  76. done = resolve
  77. })
  78. const app = await testRender(() => (
  79. <ArgsProvider>
  80. <ExitProvider>
  81. <KVProvider>
  82. <SDKProvider url="http://test" directory={directory} fetch={calls.fetch} events={eventSource()}>
  83. <ProjectProvider>
  84. <SyncProvider>
  85. <Probe
  86. onReady={(ctx) => {
  87. sync = ctx.sync
  88. kv = ctx.kv
  89. done()
  90. }}
  91. />
  92. </SyncProvider>
  93. </ProjectProvider>
  94. </SDKProvider>
  95. </KVProvider>
  96. </ExitProvider>
  97. </ArgsProvider>
  98. ))
  99. await ready
  100. await wait(() => sync.status === "complete")
  101. return { app, kv, sync, session: calls.session }
  102. }
  103. function Probe(props: { onReady: (ctx: { kv: ReturnType<typeof useKV>; sync: ReturnType<typeof useSync> }) => void }) {
  104. const kv = useKV()
  105. const sync = useSync()
  106. onMount(() => {
  107. props.onReady({ kv, sync })
  108. })
  109. return <box />
  110. }
  111. describe("tui sync", () => {
  112. test("refresh scopes sessions by default and lists project sessions when disabled", async () => {
  113. const previous = Global.Path.state
  114. await using tmp = await tmpdir()
  115. Global.Path.state = tmp.path
  116. await Bun.write(`${tmp.path}/kv.json`, "{}")
  117. const { app, kv, sync, session } = await mount()
  118. try {
  119. expect(kv.get("session_directory_filter_enabled", true)).toBe(true)
  120. expect(session.at(-1)?.searchParams.get("scope")).toBeNull()
  121. expect(session.at(-1)?.searchParams.get("path")).toBe("packages/opencode")
  122. kv.set("session_directory_filter_enabled", false)
  123. await sync.session.refresh()
  124. expect(session.at(-1)?.searchParams.get("scope")).toBe("project")
  125. expect(session.at(-1)?.searchParams.get("path")).toBeNull()
  126. } finally {
  127. app.renderer.destroy()
  128. Global.Path.state = previous
  129. }
  130. })
  131. })