thread.test.ts 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128
  1. import { afterEach, describe, expect, mock, spyOn, test } from "bun:test"
  2. import fs from "fs/promises"
  3. import path from "path"
  4. import { tmpdir } from "../../fixture/fixture"
  5. import * as App from "../../../src/cli/cmd/tui/app"
  6. import { Rpc } from "../../../src/util"
  7. import { UI } from "../../../src/cli/ui"
  8. import * as Timeout from "../../../src/util/timeout"
  9. import * as Network from "../../../src/cli/network"
  10. import * as Win32 from "../../../src/cli/cmd/tui/win32"
  11. import { TuiConfig } from "../../../src/config"
  12. import { Instance } from "../../../src/project/instance"
  13. const stop = new Error("stop")
  14. const seen = {
  15. tui: [] as string[],
  16. inst: [] as string[],
  17. }
  18. function setup() {
  19. // Intentionally avoid mock.module() here: Bun keeps module overrides in cache
  20. // and mock.restore() does not reset mock.module values. If this switches back
  21. // to module mocks, later suites can see mocked @/config/tui and fail (e.g.
  22. // plugin-loader tests expecting real TuiConfig.waitForDependencies). See:
  23. // https://github.com/oven-sh/bun/issues/7823 and #12823.
  24. spyOn(App, "tui").mockImplementation(async (input) => {
  25. if (input.directory) seen.tui.push(input.directory)
  26. throw stop
  27. })
  28. spyOn(Rpc, "client").mockImplementation(() => ({
  29. call: async () => ({ url: "http://127.0.0.1" }) as never,
  30. on: () => () => {},
  31. }))
  32. spyOn(UI, "error").mockImplementation(() => {})
  33. spyOn(Timeout, "withTimeout").mockImplementation((input) => input)
  34. spyOn(Network, "resolveNetworkOptions").mockResolvedValue({
  35. mdns: false,
  36. port: 0,
  37. hostname: "127.0.0.1",
  38. mdnsDomain: "opencode.local",
  39. cors: [],
  40. })
  41. spyOn(Win32, "win32DisableProcessedInput").mockImplementation(() => {})
  42. spyOn(Win32, "win32InstallCtrlCGuard").mockReturnValue(undefined)
  43. spyOn(TuiConfig, "get").mockResolvedValue({})
  44. spyOn(Instance, "provide").mockImplementation(async (input) => {
  45. seen.inst.push(input.directory)
  46. return input.fn()
  47. })
  48. }
  49. describe("tui thread", () => {
  50. afterEach(() => {
  51. mock.restore()
  52. })
  53. async function call(project?: string) {
  54. const { TuiThreadCommand } = await import("../../../src/cli/cmd/tui/thread")
  55. const args: Parameters<NonNullable<typeof TuiThreadCommand.handler>>[0] = {
  56. _: [],
  57. $0: "opencode",
  58. project,
  59. prompt: "hi",
  60. model: undefined,
  61. agent: undefined,
  62. session: undefined,
  63. continue: false,
  64. fork: false,
  65. port: 0,
  66. hostname: "127.0.0.1",
  67. mdns: false,
  68. "mdns-domain": "opencode.local",
  69. mdnsDomain: "opencode.local",
  70. cors: [],
  71. }
  72. return TuiThreadCommand.handler(args)
  73. }
  74. async function check(project?: string) {
  75. setup()
  76. await using tmp = await tmpdir({ git: true })
  77. const cwd = process.cwd()
  78. const pwd = process.env.PWD
  79. const worker = globalThis.Worker
  80. const tty = Object.getOwnPropertyDescriptor(process.stdin, "isTTY")
  81. const link = path.join(path.dirname(tmp.path), path.basename(tmp.path) + "-link")
  82. const type = process.platform === "win32" ? "junction" : "dir"
  83. seen.tui.length = 0
  84. seen.inst.length = 0
  85. await fs.symlink(tmp.path, link, type)
  86. Object.defineProperty(process.stdin, "isTTY", {
  87. configurable: true,
  88. value: true,
  89. })
  90. globalThis.Worker = class extends EventTarget {
  91. onerror = null
  92. onmessage = null
  93. onmessageerror = null
  94. postMessage() {}
  95. terminate() {}
  96. } as unknown as typeof Worker
  97. try {
  98. process.chdir(tmp.path)
  99. process.env.PWD = link
  100. await expect(call(project)).rejects.toBe(stop)
  101. expect(seen.inst[0]).toBe(tmp.path)
  102. expect(seen.tui[0]).toBe(tmp.path)
  103. } finally {
  104. process.chdir(cwd)
  105. if (pwd === undefined) delete process.env.PWD
  106. else process.env.PWD = pwd
  107. if (tty) Object.defineProperty(process.stdin, "isTTY", tty)
  108. else delete (process.stdin as { isTTY?: boolean }).isTTY
  109. globalThis.Worker = worker
  110. await fs.rm(link, { recursive: true, force: true }).catch(() => undefined)
  111. }
  112. }
  113. test("uses the real cwd when PWD points at a symlink", async () => {
  114. await check()
  115. })
  116. test("uses the real cwd after resolving a relative project from PWD", async () => {
  117. await check(".")
  118. })
  119. })