Просмотр исходного кода

Isolate TUI thread cwd resolution test (#25147)

Kit Langton 4 месяцев назад
Родитель
Сommit
cedff6fb89

+ 7 - 4
packages/opencode/src/cli/cmd/tui/thread.ts

@@ -71,6 +71,12 @@ async function input(value?: string) {
   return piped + "\n" + value
 }
 
+export function resolveThreadDirectory(project?: string, envPWD = process.env.PWD, cwd = process.cwd()) {
+  const root = Filesystem.resolve(envPWD ?? cwd)
+  if (project) return Filesystem.resolve(path.isAbsolute(project) ? project : path.join(root, project))
+  return Filesystem.resolve(cwd)
+}
+
 export const TuiThreadCommand = cmd({
   command: "$0 [project]",
   describe: "start opencode tui",
@@ -124,10 +130,7 @@ export const TuiThreadCommand = cmd({
 
       // Resolve relative --project paths from PWD, then use the real cwd after
       // chdir so the thread and worker share the same directory key.
-      const root = Filesystem.resolve(process.env.PWD ?? process.cwd())
-      const next = args.project
-        ? Filesystem.resolve(path.isAbsolute(args.project) ? args.project : path.join(root, args.project))
-        : Filesystem.resolve(process.cwd())
+      const next = resolveThreadDirectory(args.project)
       const file = await target()
       try {
         process.chdir(next)

+ 6 - 119
packages/opencode/test/cli/tui/thread.test.ts

@@ -1,141 +1,28 @@
-import { afterEach, describe, expect, mock, spyOn, test } from "bun:test"
+import { describe, expect, test } from "bun:test"
 import fs from "fs/promises"
 import path from "path"
 import { tmpdir } from "../../fixture/fixture"
-import * as App from "../../../src/cli/cmd/tui/app"
-import { UI } from "../../../src/cli/ui"
-import * as Timeout from "../../../src/util/timeout"
-import * as Network from "../../../src/cli/network"
-import * as Win32 from "../../../src/cli/cmd/tui/win32"
-
-const stop = new Error("stop")
-const packageRoot = path.resolve(import.meta.dir, "../../..")
-const seen = {
-  tui: [] as string[],
-}
-
-class TestWorker extends EventTarget {
-  onerror: Worker["onerror"] = null
-  onmessage: Worker["onmessage"] = null
-  onmessageerror: Worker["onmessageerror"] = null
-
-  postMessage(data: string) {
-    const parsed = JSON.parse(data)
-    if (!parsed || typeof parsed !== "object" || !("method" in parsed) || !("id" in parsed)) return
-    if (typeof parsed.method !== "string" || typeof parsed.id !== "number") return
-    const result =
-      parsed.method === "fetch"
-        ? { status: 200, headers: {}, body: "" }
-        : parsed.method === "server"
-          ? { url: "http://127.0.0.1" }
-          : parsed.method === "snapshot"
-            ? ""
-            : undefined
-    queueMicrotask(() => {
-      this.onmessage?.(
-        new MessageEvent("message", { data: JSON.stringify({ type: "rpc.result", result, id: parsed.id }) }),
-      )
-    })
-  }
-
-  terminate() {}
-}
-
-function setup() {
-  // Intentionally avoid mock.module() here: Bun keeps module overrides in cache
-  // and mock.restore() does not reset mock.module values. If this switches back
-  // to module mocks, later suites can see mocked @/config/tui and fail (e.g.
-  // plugin-loader tests expecting real TuiConfig.waitForDependencies). See:
-  // https://github.com/oven-sh/bun/issues/7823 and #12823.
-  spyOn(App, "tui").mockImplementation(async (input) => {
-    if (input.directory) seen.tui.push(input.directory)
-    throw stop
-  })
-  spyOn(UI, "error").mockImplementation(() => {})
-  spyOn(Timeout, "withTimeout").mockImplementation((input) => input)
-  spyOn(Network, "resolveNetworkOptions").mockResolvedValue({
-    mdns: false,
-    port: 0,
-    hostname: "127.0.0.1",
-    mdnsDomain: "opencode.local",
-    cors: [],
-  })
-  spyOn(Win32, "win32DisableProcessedInput").mockImplementation(() => {})
-  spyOn(Win32, "win32InstallCtrlCGuard").mockReturnValue(undefined)
-}
+import { resolveThreadDirectory } from "../../../src/cli/cmd/tui/thread"
 
 describe("tui thread", () => {
-  afterEach(() => {
-    mock.restore()
-  })
-
-  async function call(project?: string) {
-    const { TuiThreadCommand } = await import("../../../src/cli/cmd/tui/thread")
-    const args: Parameters<NonNullable<typeof TuiThreadCommand.handler>>[0] = {
-      _: [],
-      $0: "opencode",
-      project,
-      prompt: "hi",
-      model: undefined,
-      agent: undefined,
-      session: undefined,
-      continue: false,
-      fork: false,
-      port: 0,
-      hostname: "127.0.0.1",
-      mdns: false,
-      "mdns-domain": "opencode.local",
-      mdnsDomain: "opencode.local",
-      cors: [],
-    }
-    return TuiThreadCommand.handler(args)
-  }
-
   async function check(project?: string) {
-    setup()
-    const pwd = process.env.PWD
-    const worker = globalThis.Worker
-    const tty = Object.getOwnPropertyDescriptor(process.stdin, "isTTY")
     await using tmp = await tmpdir({ git: true })
     const link = path.join(path.dirname(tmp.path), path.basename(tmp.path) + "-link")
     const type = process.platform === "win32" ? "junction" : "dir"
-    seen.tui.length = 0
-    await fs.symlink(tmp.path, link, type)
-
-    Object.defineProperty(process.stdin, "isTTY", {
-      configurable: true,
-      value: true,
-    })
-    Object.defineProperty(globalThis, "Worker", { configurable: true, value: TestWorker })
 
     try {
-      process.chdir(tmp.path)
-      process.env.PWD = link
-      let error: unknown
-      try {
-        await call(project)
-      } catch (caught) {
-        error = caught
-      }
-      expect(error).toBe(stop)
-      expect(seen.tui[0]).toBe(tmp.path)
+      await fs.symlink(tmp.path, link, type)
+      expect(resolveThreadDirectory(project, link, tmp.path)).toBe(tmp.path)
     } finally {
-      process.chdir(packageRoot)
-      if (pwd === undefined) delete process.env.PWD
-      else process.env.PWD = pwd
-      if (tty) Object.defineProperty(process.stdin, "isTTY", tty)
-      else delete (process.stdin as { isTTY?: boolean }).isTTY
-      Object.defineProperty(globalThis, "Worker", { configurable: true, value: worker })
       await fs.rm(link, { recursive: true, force: true }).catch(() => undefined)
     }
   }
 
-  // serial because both modify real env vars
-  test.serial("uses the real cwd when PWD points at a symlink", async () => {
+  test("uses the real cwd when PWD points at a symlink", async () => {
     await check()
   })
 
-  test.serial("uses the real cwd after resolving a relative project from PWD", async () => {
+  test("uses the real cwd after resolving a relative project from PWD", async () => {
     await check(".")
   })
 })