test: Add canonicalizePath() utility using fs.realpathSync() with symli…
SF-Task: S01/T02
This commit is contained in:
parent
8418e88730
commit
51202225ec
4 changed files with 76 additions and 13 deletions
|
|
@ -3,6 +3,7 @@ import { homedir } from "os";
|
|||
import { basename, isAbsolute, join, resolve, sep } from "path";
|
||||
import { CONFIG_DIR_NAME, getPromptsDir } from "../config.js";
|
||||
import { parseFrontmatter } from "../utils/frontmatter.js";
|
||||
import { canonicalizePath } from "./tools/path-utils.js";
|
||||
|
||||
/**
|
||||
* Represents a prompt template loaded from a markdown file
|
||||
|
|
@ -253,6 +254,7 @@ export function loadPromptTemplates(options: LoadPromptTemplatesOptions = {}): P
|
|||
};
|
||||
|
||||
// 3. Load explicit prompt paths
|
||||
const seenPaths = new Set<string>();
|
||||
for (const rawPath of promptPaths) {
|
||||
const resolvedPath = resolvePromptPath(rawPath, resolvedCwd);
|
||||
if (!existsSync(resolvedPath)) {
|
||||
|
|
@ -263,11 +265,22 @@ export function loadPromptTemplates(options: LoadPromptTemplatesOptions = {}): P
|
|||
const stats = statSync(resolvedPath);
|
||||
const { source, label } = getSourceInfo(resolvedPath);
|
||||
if (stats.isDirectory()) {
|
||||
templates.push(...loadTemplatesFromDir(resolvedPath, source, label));
|
||||
const dirTemplates = loadTemplatesFromDir(resolvedPath, source, label);
|
||||
for (const template of dirTemplates) {
|
||||
const realPath = canonicalizePath(template.filePath);
|
||||
if (!seenPaths.has(realPath)) {
|
||||
seenPaths.add(realPath);
|
||||
templates.push(template);
|
||||
}
|
||||
}
|
||||
} else if (stats.isFile() && resolvedPath.endsWith(".md")) {
|
||||
const template = loadTemplateFromFile(resolvedPath, source, label);
|
||||
if (template) {
|
||||
templates.push(template);
|
||||
const realPath = canonicalizePath(resolvedPath);
|
||||
if (!seenPaths.has(realPath)) {
|
||||
seenPaths.add(realPath);
|
||||
const template = loadTemplateFromFile(resolvedPath, source, label);
|
||||
if (template) {
|
||||
templates.push(template);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
import { existsSync, readdirSync, readFileSync, realpathSync, statSync } from "fs";
|
||||
import { existsSync, readdirSync, readFileSync, statSync } from "fs";
|
||||
import ignore from "ignore";
|
||||
import { homedir } from "os";
|
||||
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "path";
|
||||
import { parseFrontmatter } from "../utils/frontmatter.js";
|
||||
import { toPosixPath } from "../utils/path-display.js";
|
||||
import { canonicalizePath } from "./tools/path-utils.js";
|
||||
import type { ResourceDiagnostic } from "./diagnostics.js";
|
||||
import { CONFIG_DIR_NAME } from "../config.js";
|
||||
|
||||
|
|
@ -386,12 +387,7 @@ export function loadSkills(options: LoadSkillsOptions = {}): LoadSkillsResult {
|
|||
allDiagnostics.push(...result.diagnostics);
|
||||
for (const skill of result.skills) {
|
||||
// Resolve symlinks to detect duplicate files
|
||||
let realPath: string;
|
||||
try {
|
||||
realPath = realpathSync(skill.filePath);
|
||||
} catch {
|
||||
realPath = skill.filePath;
|
||||
}
|
||||
const realPath = canonicalizePath(skill.filePath);
|
||||
|
||||
// Skip silently if we've already loaded this exact file (via symlink)
|
||||
if (realPathSet.has(realPath)) {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,42 @@
|
|||
import { describe, it, mock, afterEach } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { resolveToCwd, expandPath } from "./path-utils.js";
|
||||
import { mkdtempSync, writeFileSync, symlinkSync, unlinkSync, rmdirSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { resolveToCwd, expandPath, canonicalizePath } from "./path-utils.js";
|
||||
|
||||
describe("canonicalizePath", () => {
|
||||
it("returns realpath for existing files", () => {
|
||||
const result = canonicalizePath("./package.json");
|
||||
// Should resolve to absolute path
|
||||
assert.ok(result.startsWith("/"), "should be absolute path");
|
||||
assert.ok(result.endsWith("package.json"), "should end with package.json");
|
||||
});
|
||||
|
||||
it("falls back to input path when file does not exist", () => {
|
||||
const nonExistent = "/tmp/non-existent-file-12345.txt";
|
||||
const result = canonicalizePath(nonExistent);
|
||||
assert.equal(result, nonExistent);
|
||||
});
|
||||
|
||||
it("resolves symlinks to their target", () => {
|
||||
const tmpDir = mkdtempSync(join(tmpdir(), "canonicalize-test-"));
|
||||
const realFile = join(tmpDir, "real.txt");
|
||||
const symlink = join(tmpDir, "link.txt");
|
||||
|
||||
writeFileSync(realFile, "hello");
|
||||
symlinkSync(realFile, symlink);
|
||||
|
||||
try {
|
||||
const result = canonicalizePath(symlink);
|
||||
assert.equal(result, realFile, "symlink should resolve to real file path");
|
||||
} finally {
|
||||
unlinkSync(symlink);
|
||||
unlinkSync(realFile);
|
||||
rmdirSync(tmpDir);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveToCwd", () => {
|
||||
it("resolves relative paths against cwd", () => {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { accessSync, constants } from "node:fs";
|
||||
import { accessSync, constants, realpathSync } from "node:fs";
|
||||
import * as os from "node:os";
|
||||
import { isAbsolute, resolve as resolvePath } from "node:path";
|
||||
|
||||
|
|
@ -71,6 +71,24 @@ export function resolveToCwd(filePath: string, cwd: string): string {
|
|||
return resolvePath(cwd, expanded);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve symlinks to their canonical absolute path.
|
||||
* Returns the original path if realpathSync fails (e.g., path does not exist).
|
||||
*
|
||||
* Purpose: deduplicate resources that are reachable via multiple symlinked
|
||||
* paths — two different paths may resolve to the same underlying file.
|
||||
*
|
||||
* Consumer: resource loaders (skills, prompts, themes) that merge paths
|
||||
* from multiple sources and must avoid loading the same file twice.
|
||||
*/
|
||||
export function canonicalizePath(filePath: string): string {
|
||||
try {
|
||||
return realpathSync(filePath);
|
||||
} catch {
|
||||
return filePath;
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveReadPath(filePath: string, cwd: string): string {
|
||||
const resolved = resolveToCwd(filePath, cwd);
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue