import { describe, expect, mock, test } from "bun:test" import fs from "fs/promises" import path from "path" import { tmpdir } from "../../fixture/fixture" const stop = new Error("stop") const seen = { tui: [] as string[], inst: [] as string[], } mock.module("../../../src/cli/cmd/tui/app", () => ({ tui: async (input: { directory: string }) => { seen.tui.push(input.directory) throw stop }, })) mock.module("@/util/rpc", () => ({ Rpc: { client: () => ({ call: async () => ({ url: "http://127.0.0.1" }), on: () => {}, }), }, })) mock.module("@/cli/ui", () => ({ UI: { error: () => {}, }, })) mock.module("@/util/log", () => ({ Log: { init: async () => {}, create: () => ({ error: () => {}, info: () => {}, warn: () => {}, debug: () => {}, time: () => ({ stop: () => {} }), }), Default: { error: () => {}, info: () => {}, warn: () => {}, debug: () => {}, }, }, })) mock.module("@/util/timeout", () => ({ withTimeout: (input: Promise) => input, })) mock.module("@/cli/network", () => ({ withNetworkOptions: (input: T) => input, resolveNetworkOptions: async () => ({ mdns: false, port: 0, hostname: "127.0.0.1", }), })) mock.module("../../../src/cli/cmd/tui/win32", () => ({ win32DisableProcessedInput: () => {}, win32InstallCtrlCGuard: () => undefined, })) mock.module("@/config/tui", () => ({ TuiConfig: { get: () => ({}), }, })) mock.module("@/project/instance", () => ({ Instance: { provide: async (input: { directory: string; fn: () => Promise | unknown }) => { seen.inst.push(input.directory) return input.fn() }, }, })) describe("tui thread", () => { async function call(project?: string) { const { TuiThreadCommand } = await import("../../../src/cli/cmd/tui/thread") const args: Parameters>[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) { await using tmp = await tmpdir({ git: true }) const cwd = process.cwd() const pwd = process.env.PWD const worker = globalThis.Worker const tty = Object.getOwnPropertyDescriptor(process.stdin, "isTTY") const link = path.join(path.dirname(tmp.path), path.basename(tmp.path) + "-link") const type = process.platform === "win32" ? "junction" : "dir" seen.tui.length = 0 seen.inst.length = 0 await fs.symlink(tmp.path, link, type) Object.defineProperty(process.stdin, "isTTY", { configurable: true, value: true, }) globalThis.Worker = class extends EventTarget { onerror = null onmessage = null onmessageerror = null postMessage() {} terminate() {} } as unknown as typeof Worker try { process.chdir(tmp.path) process.env.PWD = link await expect(call(project)).rejects.toBe(stop) expect(seen.inst[0]).toBe(tmp.path) expect(seen.tui[0]).toBe(tmp.path) } finally { process.chdir(cwd) 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 globalThis.Worker = worker await fs.rm(link, { recursive: true, force: true }).catch(() => undefined) } } test("uses the real cwd when PWD points at a symlink", async () => { await check() }) test("uses the real cwd after resolving a relative project from PWD", async () => { await check(".") }) })