fix(coding-agent): resolved symlinked paths before passing to brush
- Resolved symlinked paths before passing to brush shell to keep `pwd` output aligned with canonical Git worktree paths. - Added `resolveShellCwd` helper that safely resolves symlinks and falls back to original path on error. - Added test case verifying symlinked directories are canonicalized before execution.
This commit is contained in:
@@ -1,6 +1,9 @@
|
||||
# Changelog
|
||||
|
||||
## [Unreleased]
|
||||
### Fixed
|
||||
|
||||
- Canonicalized bash executor working directories before handing them to brush so `pwd` stays aligned with canonical Git worktree paths in symlinked workspaces
|
||||
|
||||
### Added
|
||||
|
||||
@@ -5789,4 +5792,4 @@ Initial public release.
|
||||
- Git branch display in footer
|
||||
- Message queueing during streaming responses
|
||||
- OAuth integration for Gmail and Google Calendar access
|
||||
- HTML export with syntax highlighting and collapsible sections
|
||||
- HTML export with syntax highlighting and collapsible sections
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
*
|
||||
* Uses brush-core via native bindings for shell execution.
|
||||
*/
|
||||
import * as fs from "node:fs/promises";
|
||||
import { Shell } from "@oh-my-pi/pi-natives";
|
||||
import { Settings } from "../config/settings";
|
||||
import { OutputSink } from "../session/streaming-output";
|
||||
@@ -39,10 +40,23 @@ const HARD_TIMEOUT_GRACE_MS = 5_000;
|
||||
|
||||
const shellSessions = new Map<string, Shell>();
|
||||
|
||||
async function resolveShellCwd(cwd: string | undefined): Promise<string | undefined> {
|
||||
if (!cwd) return undefined;
|
||||
|
||||
try {
|
||||
// Brush preserves the working directory string verbatim, so resolve symlinks
|
||||
// up front to keep `pwd` aligned with tools like `git worktree list`.
|
||||
return await fs.realpath(cwd);
|
||||
} catch {
|
||||
return cwd;
|
||||
}
|
||||
}
|
||||
|
||||
export async function executeBash(command: string, options?: BashExecutorOptions): Promise<BashResult> {
|
||||
const settings = await Settings.init();
|
||||
const { shell, env: shellEnv, prefix } = settings.getShellConfig();
|
||||
const snapshotPath = shell.includes("bash") ? await getOrCreateSnapshot(shell, shellEnv) : null;
|
||||
const commandCwd = await resolveShellCwd(options?.cwd);
|
||||
|
||||
// Apply command prefix if configured
|
||||
const prefixedCommand = prefix ? `${prefix} ${command}` : command;
|
||||
@@ -97,7 +111,7 @@ export async function executeBash(command: string, options?: BashExecutorOptions
|
||||
const runPromise = shellSession.run(
|
||||
{
|
||||
command: finalCommand,
|
||||
cwd: options?.cwd,
|
||||
cwd: commandCwd,
|
||||
env: options?.env ? { ...NON_INTERACTIVE_ENV, ...options.env } : NON_INTERACTIVE_ENV,
|
||||
timeoutMs: options?.timeout,
|
||||
signal,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import {
|
||||
CombinedAutocompleteProvider,
|
||||
getEditorKeybindings,
|
||||
type AutocompleteItem,
|
||||
type AutocompleteProvider,
|
||||
CombinedAutocompleteProvider,
|
||||
getEditorKeybindings,
|
||||
type SlashCommand,
|
||||
} from "@oh-my-pi/pi-tui";
|
||||
import { formatKeyHints, type KeybindingsManager } from "../config/keybindings";
|
||||
|
||||
@@ -39,6 +39,20 @@ describe("executeBash", () => {
|
||||
expect(result.output.trim()).toBe(tempDir);
|
||||
});
|
||||
|
||||
it("canonicalizes symlinked cwd before execution", async () => {
|
||||
if (process.platform === "win32") {
|
||||
return;
|
||||
}
|
||||
|
||||
const realDir = path.join(tempDir, "real");
|
||||
const linkDir = path.join(tempDir, "link");
|
||||
fs.mkdirSync(realDir);
|
||||
fs.symlinkSync(realDir, linkDir, "dir");
|
||||
|
||||
const result = await executeBash("pwd", { cwd: linkDir, timeout: 5000 });
|
||||
expect(result.output.trim()).toBe(fs.realpathSync(linkDir));
|
||||
});
|
||||
|
||||
it("passes env vars", async () => {
|
||||
const result = await executeBash("echo $PI_TEST_ENV", {
|
||||
cwd: tempDir,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import { SessionManager } from "@oh-my-pi/pi-coding-agent/session/session-manager";
|
||||
import type { AssistantMessage } from "@oh-my-pi/pi-ai";
|
||||
import { SessionManager } from "@oh-my-pi/pi-coding-agent/session/session-manager";
|
||||
import { TempDir } from "@oh-my-pi/pi-utils";
|
||||
|
||||
describe("SessionManager signature persistence", () => {
|
||||
@@ -33,8 +33,11 @@ describe("SessionManager signature persistence", () => {
|
||||
await session.flush();
|
||||
|
||||
const reloaded = await SessionManager.open(session.getSessionFile()!);
|
||||
const assistant = reloaded.getEntries().find(entry => entry.type === "message" && entry.message.role === "assistant")
|
||||
?.message as AssistantMessage | undefined;
|
||||
const assistant = reloaded
|
||||
.getEntries()
|
||||
.find(entry => entry.type === "message" && entry.message.role === "assistant")?.message as
|
||||
| AssistantMessage
|
||||
| undefined;
|
||||
|
||||
expect(assistant).toBeDefined();
|
||||
expect(assistant?.content[0]).toMatchObject({ type: "thinking", thinking: "reasoning", thinkingSignature: "" });
|
||||
|
||||
Reference in New Issue
Block a user