fix(graph): normalize Windows backslash paths to forward slashes

On Windows, path.relative() and path.join() return backslash separators.
Graph node keys were stored with native separators, but query inputs use
forward slashes, causing silent lookup failures on Windows.

Add toForwardSlash() utility and apply it at build time (file walker,
resolution functions) and query time (getFileDependencies,
getSymbolContext, listSymbols) for defense-in-depth.

No-op on macOS/Linux where path.relative() already returns forward
slashes. Existing Windows symbol graph caches require one rebuild.

Fixes #60
This commit is contained in:
Giancarlo Erra
2026-05-22 15:59:13 +01:00
parent c126c525fa
commit e9ee3ea116
7 changed files with 102 additions and 23 deletions
+32
View File
@@ -27,6 +27,7 @@ import {
SEARCH_MIN_SCORE,
SPECIAL_FILES,
SUPPORTED_EXTENSIONS,
toForwardSlash,
} from "../../src/constants.js";
describe("constants", () => {
@@ -373,6 +374,37 @@ describe("constants", () => {
});
});
describe("toForwardSlash", () => {
it("returns the same string when no backslashes are present", () => {
expect(toForwardSlash("src/index.ts")).toBe("src/index.ts");
});
it("replaces single backslash with forward slash", () => {
expect(toForwardSlash("src\\index.ts")).toBe("src/index.ts");
});
it("replaces multiple backslashes in a path", () => {
expect(toForwardSlash("src\\services\\graph-analysis.ts")).toBe("src/services/graph-analysis.ts");
});
it("handles deeply nested Windows paths", () => {
expect(toForwardSlash("src\\a\\b\\c\\d\\file.ts")).toBe("src/a/b/c/d/file.ts");
});
it("handles empty string", () => {
expect(toForwardSlash("")).toBe("");
});
it("handles path with mixed separators", () => {
expect(toForwardSlash("src/services\\graph-analysis.ts")).toBe("src/services/graph-analysis.ts");
});
it("is a no-op on POSIX-style paths", () => {
const posixPath = "src/services/code-graph.ts";
expect(toForwardSlash(posixPath)).toBe(posixPath);
});
});
describe("resolveQdrantPort", () => {
it("returns explicit port from URL", () => {
expect(resolveQdrantPort("https://qdrant.example.com:6333")).toBe(6333);
+32
View File
@@ -113,6 +113,38 @@ describe("graph-analysis", () => {
expect(deps.imports).toHaveLength(0);
expect(deps.importedBy).toHaveLength(0);
});
it("normalizes Windows backslash paths to match forward-slash graph keys", () => {
const graph = createSampleGraph();
// Graph keys use forward slashes; simulate a Windows-style query
const deps = getFileDependencies(graph, "src\\index.ts");
expect(deps.imports).toContain("src/utils.ts");
expect(deps.imports).toContain("src/types.ts");
});
it("normalizes deeply nested Windows paths", () => {
const nodes: CodeGraphNode[] = [
makeNode("src/services/graph/analysis.ts", ["src/types.ts"], []),
makeNode("src/types.ts", [], ["src/services/graph/analysis.ts"]),
];
const edges: CodeGraphEdge[] = [
makeEdge("src/services/graph/analysis.ts", "src/types.ts"),
];
const graph = makeGraph(nodes, edges);
const deps = getFileDependencies(graph, "src\\services\\graph\\analysis.ts");
expect(deps.imports).toContain("src/types.ts");
});
it("handles mixed separator paths", () => {
const graph = createSampleGraph();
const deps = getFileDependencies(graph, "src/utils.ts");
const depsMixed = getFileDependencies(graph, "src\\utils.ts");
expect(depsMixed.imports).toEqual(deps.imports);
expect(depsMixed.importedBy).toEqual(deps.importedBy);
});
});
describe("findCircularDependencies", () => {