singularity-forge/packages/pi-coding-agent/src/tests/path-display.test.ts
Tom Boucher 3542b17c97 fix: normalize Windows paths in LLM-visible text to prevent bash failures (#874) (#884)
On Windows, process.cwd() returns backslash paths (C:\Users\name\...).
When these paths are injected into system prompts, worktree context
blocks, or tool results, the model copies them into bash commands.
Bash interprets backslashes as escape characters, silently stripping
them — producing invalid paths like 'C:Usersnamedevelopmentapp-name'.

This is not a regex hack — it's a proper cross-platform boundary:
- Filesystem operations (fs, path.join, spawn cwd) use native paths
  unchanged. Node handles both separators correctly for I/O.
- LLM-visible text (prompts, tool results, extension messages) uses
  toPosixPath() to normalize to forward slashes. C:/Users/name/...
  is valid in Git Bash, WSL bash, PowerShell, and Node.js.

Changes:

- utils/path-display.ts: New toPosixPath() utility in pi-coding-agent
  package (for system prompt) and shared extension module (for
  extensions that can't import from the compiled package at dev time)

- system-prompt.ts: Normalize resolvedCwd before injecting into the
  'Current working directory' line

- gsd/index.ts: Normalize all process.cwd() and originalBase paths in
  worktree context blocks injected into the system prompt

- bg-shell/index.ts: Normalize cwd in tool result text (start, env
  actions) that the model reads and may reference in commands

- path-display.test.ts: 9 regression tests covering toPosixPath
  behavior and system prompt output verification. Includes a scanner
  that fails if any Windows absolute paths with backslashes appear in
  buildSystemPrompt() output.

Audit scope: Checked all process.cwd() usage across pi-coding-agent
and all bundled extensions. Filesystem-only paths (join, readFile,
spawn cwd, existsSync) are correct and left unchanged. Only paths
entering LLM text are normalized.
2026-03-17 09:02:23 -06:00

85 lines
3 KiB
TypeScript

/**
* Cross-platform path display tests.
*
* Verifies that toPosixPath correctly normalizes Windows paths and that
* the system prompt builder produces forward-slash paths for LLM consumption.
*/
import test from "node:test";
import assert from "node:assert/strict";
import { toPosixPath } from "../utils/path-display.js";
import { buildSystemPrompt } from "../core/system-prompt.js";
// ─── toPosixPath ────────────────────────────────────────────────────────────
test("toPosixPath: converts Windows backslash paths to forward slashes", () => {
assert.equal(toPosixPath("C:\\Users\\name\\project"), "C:/Users/name/project");
});
test("toPosixPath: handles mixed separators", () => {
assert.equal(toPosixPath("C:\\Users/name\\project/src"), "C:/Users/name/project/src");
});
test("toPosixPath: no-op for Unix paths", () => {
assert.equal(toPosixPath("/home/user/project"), "/home/user/project");
});
test("toPosixPath: handles empty string", () => {
assert.equal(toPosixPath(""), "");
});
test("toPosixPath: handles Windows UNC paths", () => {
assert.equal(toPosixPath("\\\\server\\share\\dir"), "//server/share/dir");
});
test("toPosixPath: handles .gsd/worktrees path on Windows", () => {
assert.equal(
toPosixPath("C:\\Users\\name\\project\\.gsd\\worktrees\\M001"),
"C:/Users/name/project/.gsd/worktrees/M001",
);
});
// ─── System prompt path normalization ───────────────────────────────────────
test("buildSystemPrompt: cwd uses forward slashes even with Windows input", () => {
const prompt = buildSystemPrompt({
cwd: "C:\\Users\\name\\development\\app-name",
});
assert.ok(
prompt.includes("C:/Users/name/development/app-name"),
"System prompt should contain forward-slash path",
);
assert.ok(
!prompt.includes("C:\\Users\\name\\development\\app-name"),
"System prompt must NOT contain backslash path",
);
});
test("buildSystemPrompt: Unix paths pass through unchanged", () => {
const prompt = buildSystemPrompt({
cwd: "/home/user/project",
});
assert.ok(prompt.includes("/home/user/project"));
});
// ─── Regression: no backslash paths in LLM-visible text ────────────────────
/**
* Pattern that matches Windows-style absolute paths with backslashes.
* Catches: C:\Users\..., D:\Projects\..., \\server\share\...
* Does not match: escaped chars in regex, JSON strings, etc.
*/
const WINDOWS_ABS_PATH_RE = /[A-Z]:\\[A-Za-z]/;
test("buildSystemPrompt: no Windows absolute paths with backslashes in output", () => {
// Simulate a Windows-like cwd
const prompt = buildSystemPrompt({
cwd: "D:\\Projects\\my-app\\.gsd\\worktrees\\M002",
});
const lines = prompt.split("\n");
const violations = lines.filter(line => WINDOWS_ABS_PATH_RE.test(line));
assert.equal(
violations.length, 0,
`System prompt contains Windows backslash paths:\n${violations.join("\n")}`,
);
});