Skip to content

Commit 05b0ec8

Browse files
committed
fix(codex): relPath home containment — component boundary + platform case semantics
The warning-display helper matched home by lowercase string prefix, so C:\Users\bob2\... rendered as inside ~ for home C:\Users\bob, and POSIX case-sensitive paths were case-folded. Use path.relative() with a full containment predicate (exact home, parent, cross-drive, sibling-prefix all handled); case semantics now come from the platform path API. devlog: 260715_cross_platform_audit/030
1 parent 9eaff97 commit 05b0ec8

2 files changed

Lines changed: 71 additions & 7 deletions

File tree

src/codex/project-config-warnings.ts

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { existsSync, readFileSync } from "node:fs";
2-
import { dirname, join, resolve } from "node:path";
2+
import path, { dirname, join, resolve } from "node:path";
33
import { expandUserPath } from "../config";
44
import { defaultCodexHome } from "./home";
55
import { readRootTomlString } from "./paths";
@@ -223,12 +223,24 @@ export function dedupeRelatedProjectCodexWarnings(
223223
});
224224
}
225225

226-
function relPath(abs: string): string {
226+
/**
227+
* Render a path under the user's home as `~/...` for warning display.
228+
* Platform-correct containment (devlog 260715_cross_platform_audit/030): the old
229+
* lowercase prefix match had no component boundary (`C:\Users\bob2` rendered as
230+
* inside `~` for home `C:\Users\bob`) and case-folded on case-sensitive POSIX
231+
* filesystems. `relative()` carries the right case semantics per platform; reject
232+
* parent (`..`, `..\x`) and cross-drive (absolute) results.
233+
*/
234+
export function relPath(
235+
abs: string,
236+
pathApi: Pick<typeof path, "relative" | "sep" | "isAbsolute"> = path,
237+
): string {
227238
const home = process.env.USERPROFILE ?? process.env.HOME ?? "";
228-
if (home && abs.toLowerCase().startsWith(home.toLowerCase())) {
229-
return `~${abs.slice(home.length).replace(/\\/g, "/")}`;
230-
}
231-
return abs;
239+
if (!home) return abs;
240+
const rel = pathApi.relative(home, abs);
241+
if (rel === "") return "~";
242+
if (rel === ".." || rel.startsWith(`..${pathApi.sep}`) || pathApi.isAbsolute(rel)) return abs;
243+
return `~/${rel.replace(/\\/g, "/")}`;
232244
}
233245

234246
export function discoverProjectCodexConfigPaths(options: {

tests/project-config-warnings.test.ts

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,69 @@
11
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
22
import { mkdirSync, rmSync, writeFileSync } from "node:fs";
33
import { tmpdir } from "node:os";
4-
import { join } from "node:path";
4+
import { join, posix, win32 } from "node:path";
55
import {
66
analyzeProjectCodexConfig,
77
collectProjectCodexConfigWarnings,
88
getCachedProjectConfigDiagnostics,
99
isGlobalOpencodexRoutingActive,
1010
invalidateProjectConfigDiagnosticsCache,
1111
parseTrustedProjectPathsFromCodexConfig,
12+
relPath,
1213
resolveEffectiveProjectModelProvider,
1314
} from "../src/codex/project-config-warnings";
1415

16+
describe("relPath home containment (devlog 260715_cross_platform_audit/030)", () => {
17+
let savedUserProfile: string | undefined;
18+
let savedHome: string | undefined;
19+
20+
beforeEach(() => {
21+
savedUserProfile = process.env.USERPROFILE;
22+
savedHome = process.env.HOME;
23+
});
24+
25+
afterEach(() => {
26+
if (savedUserProfile === undefined) delete process.env.USERPROFILE; else process.env.USERPROFILE = savedUserProfile;
27+
if (savedHome === undefined) delete process.env.HOME; else process.env.HOME = savedHome;
28+
});
29+
30+
function setHome(value: string) {
31+
process.env.USERPROFILE = value;
32+
delete process.env.HOME;
33+
}
34+
35+
test("win32: contained descendants render as ~/, exact home as ~", () => {
36+
setHome("C:\\Users\\bob");
37+
expect(relPath("C:\\Users\\bob\\proj\\.codex\\config.toml", win32)).toBe("~/proj/.codex/config.toml");
38+
expect(relPath("C:\\Users\\bob", win32)).toBe("~");
39+
// relative() case-folds on win32 (drive letters and components).
40+
expect(relPath("c:\\users\\bob\\x", win32)).toBe("~/x");
41+
});
42+
43+
test("win32: sibling prefix (bob vs bob2) is NOT rendered as home", () => {
44+
setHome("C:\\Users\\bob");
45+
expect(relPath("C:\\Users\\bob2\\proj\\config.toml", win32)).toBe("C:\\Users\\bob2\\proj\\config.toml");
46+
});
47+
48+
test("win32: parent and cross-drive paths stay absolute", () => {
49+
setHome("C:\\Users\\bob");
50+
expect(relPath("C:\\Users", win32)).toBe("C:\\Users");
51+
expect(relPath("D:\\work\\config.toml", win32)).toBe("D:\\work\\config.toml");
52+
});
53+
54+
test("posix: comparison is case-sensitive (no false ~ for different-case home)", () => {
55+
setHome("/home/Bob");
56+
expect(relPath("/home/bob/x", posix)).toBe("/home/bob/x");
57+
expect(relPath("/home/Bob/x", posix)).toBe("~/x");
58+
});
59+
60+
test("no home env leaves paths untouched", () => {
61+
delete process.env.USERPROFILE;
62+
delete process.env.HOME;
63+
expect(relPath("/anywhere/x", posix)).toBe("/anywhere/x");
64+
});
65+
});
66+
1567
let testDir = "";
1668
let previousHome: string | undefined;
1769
let previousCodexHome: string | undefined;

0 commit comments

Comments
 (0)