fix(tui): canonicalize cwd after chdir (#16641)
This commit is contained in:
@@ -110,18 +110,20 @@ export const TuiThreadCommand = cmd({
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resolve relative paths against PWD to preserve behavior when using --cwd flag
|
// 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 root = Filesystem.resolve(process.env.PWD ?? process.cwd())
|
||||||
const cwd = args.project
|
const next = args.project
|
||||||
? Filesystem.resolve(path.isAbsolute(args.project) ? args.project : path.join(root, args.project))
|
? Filesystem.resolve(path.isAbsolute(args.project) ? args.project : path.join(root, args.project))
|
||||||
: root
|
: Filesystem.resolve(process.cwd())
|
||||||
const file = await target()
|
const file = await target()
|
||||||
try {
|
try {
|
||||||
process.chdir(cwd)
|
process.chdir(next)
|
||||||
} catch {
|
} catch {
|
||||||
UI.error("Failed to change directory to " + cwd)
|
UI.error("Failed to change directory to " + next)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
const cwd = Filesystem.resolve(process.cwd())
|
||||||
|
|
||||||
const worker = new Worker(file, {
|
const worker = new Worker(file, {
|
||||||
env: Object.fromEntries(
|
env: Object.fromEntries(
|
||||||
|
|||||||
@@ -0,0 +1,157 @@
|
|||||||
|
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: <T>(input: Promise<T>) => input,
|
||||||
|
}))
|
||||||
|
|
||||||
|
mock.module("@/cli/network", () => ({
|
||||||
|
withNetworkOptions: <T>(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> | 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<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) {
|
||||||
|
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(".")
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user