diff --git a/packages/cli/src/cmd/lockfile/info.ts b/packages/cli/src/cmd/lockfile/info.ts index aa532db41..7edc68736 100644 --- a/packages/cli/src/cmd/lockfile/info.ts +++ b/packages/cli/src/cmd/lockfile/info.ts @@ -40,7 +40,6 @@ export async function runLockfileInfo({ flags }: CLILockfileInfoCmdOptions) { const { storeDir, json } = flags; const storePath = storeDir ? resolve(storeDir) : process.cwd(); - console.error("Reading lockfile info from store at:", storePath); try { const fs = NodeFileSystemBridge({ basePath: storePath }); const lockfilePath = getLockfilePath(); diff --git a/packages/shared/src/files.ts b/packages/shared/src/files.ts index 6facd4ed1..e73fb8238 100644 --- a/packages/shared/src/files.ts +++ b/packages/shared/src/files.ts @@ -1,5 +1,86 @@ import type { UnicodeFileTreeNodeWithoutLastModified } from "@ucdjs/schemas"; -import { prependLeadingSlash } from "@luxass/utils"; +import { prependLeadingSlash, trimLeadingSlash, trimTrailingSlash } from "@luxass/utils"; +import { hasUCDFolderPath } from "@unicode-utils/core"; + +/** + * Normalizes an API file-tree path to a version-relative path suitable for filtering. + * + * This strips: + * - Leading/trailing slashes + * - Version prefix (e.g., "16.0.0/") + * - "ucd/" prefix for versions that have it + * + * @param {string} version - The Unicode version string + * @param {string} rawPath - The raw path from the API file tree (e.g., "/16.0.0/ucd/Blocks.txt") + * @returns {string} The normalized path (e.g., "Blocks.txt") + * + * @example + * ```typescript + * normalizePathForFiltering("16.0.0", "/16.0.0/ucd/Blocks.txt"); + * // Returns: "Blocks.txt" + * + * normalizePathForFiltering("16.0.0", "/16.0.0/ucd/auxiliary/GraphemeBreakProperty.txt"); + * // Returns: "auxiliary/GraphemeBreakProperty.txt" + * ``` + */ +export function normalizePathForFiltering(version: string, rawPath: string): string { + // Strip leading and trailing slashes + let path = trimTrailingSlash(trimLeadingSlash(rawPath)); + + // Strip version prefix if present + const versionPrefix = `${version}/`; + if (path.startsWith(versionPrefix)) { + path = path.slice(versionPrefix.length); + } + + // Strip "ucd/" prefix for versions that have it + if (hasUCDFolderPath(version) && path.startsWith("ucd/")) { + path = path.slice(4); + } + + return path; +} + +/** + * Creates a normalized view of a file tree for filtering purposes. + * + * This recursively maps all `path` properties to version-relative paths, + * so that filter patterns like "Blocks.txt" or "auxiliary/**" will match + * against paths like "/16.0.0/ucd/Blocks.txt". + * + * @template {UnicodeFileTreeNodeWithoutLastModified} T - A tree node type that extends the base TreeNode interface + * @param {string} version - The Unicode version string + * @param {T[]} entries - Array of file tree nodes from the API + * @returns {T[]} A new tree with normalized paths suitable for filtering + * + * @example + * ```typescript + * const apiTree = [{ type: "file", name: "Blocks.txt", path: "/16.0.0/ucd/Blocks.txt" }]; + * const normalizedTree = normalizeTreeForFiltering("16.0.0", apiTree); + * // Returns: [{ type: "file", name: "Blocks.txt", path: "Blocks.txt" }] + * ``` + */ +export function normalizeTreeForFiltering( + version: string, + entries: T[], +): T[] { + return entries.map((entry) => { + const normalizedPath = normalizePathForFiltering(version, entry.path); + + if (entry.type === "directory" && entry.children) { + return { + ...entry, + path: normalizedPath, + children: normalizeTreeForFiltering(version, entry.children), + }; + } + + return { + ...entry, + path: normalizedPath, + }; + }); +} /** * Recursively find a node (file or directory) by its path in the tree. diff --git a/packages/shared/src/filter.ts b/packages/shared/src/filter.ts index 1ff402d17..d6e1d91fa 100644 --- a/packages/shared/src/filter.ts +++ b/packages/shared/src/filter.ts @@ -141,6 +141,32 @@ export function createPathFilter(options: PathFilterOptions = {}): PathFilter { return filterFn; } +function normalizeForMatching(value: string): string { + // 1) unify slashes + let normalized = value.replace(/\\/g, "/"); + + // 2) drop leading "./" segments + normalized = normalized.replace(/^\.\/+/, ""); + + // 3) drop leading slash to let relative globs match absolute-style inputs + normalized = normalized.replace(/^\//, ""); + + // 4) drop trailing slash so directory paths don't require it in patterns + normalized = normalized.replace(/\/$/, ""); + + return normalized; +} + +function normalizePatterns(patterns: string[]): string[] { + const normalized: string[] = []; + + for (let i = 0; i < patterns.length; i += 1) { + normalized.push(normalizeForMatching(patterns[i]!)); + } + + return normalized; +} + function internal__createFilterFunction(config: PathFilterOptions): PathFilterFn { // If include is empty or not set, include everything using "**" pattern const includePatterns = config.include && config.include.length > 0 ? config.include : ["**"]; @@ -153,14 +179,17 @@ function internal__createFilterFunction(config: PathFilterOptions): PathFilterFn ...(config.exclude || []), ]; + const normalizedIncludePatterns = normalizePatterns(includePatterns); + const normalizedExcludePatterns = normalizePatterns(rawExcludePatterns); + // Transform directory-only patterns to include their contents // e.g., "**/extracted" becomes both "**/extracted" and "**/extracted/**" - const excludePatterns = expandDirectoryPatterns(rawExcludePatterns); + const excludePatterns = expandDirectoryPatterns(normalizedExcludePatterns); return (path: string): boolean => { - const normalizedPath = path.replace(/\\/g, "/").replace(/^\.\//, ""); + const normalizedPath = normalizeForMatching(path); - return picomatch.isMatch(normalizedPath, includePatterns, { + return picomatch.isMatch(normalizedPath, normalizedIncludePatterns, { ...DEFAULT_PICOMATCH_OPTIONS, ignore: excludePatterns, } satisfies PicomatchOptions); diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index 9ddf52cc6..86e36a264 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -5,7 +5,7 @@ export * from "./debugger"; export { customFetch } from "./fetch/fetch"; export type { FetchOptions, FetchResponse, SafeFetchResponse } from "./fetch/types"; -export { findFileByPath, flattenFilePaths } from "./files"; +export { findFileByPath, flattenFilePaths, normalizePathForFiltering, normalizeTreeForFiltering } from "./files"; export { createPathFilter, diff --git a/packages/shared/test/files.test.ts b/packages/shared/test/files.test.ts index 580dd8257..a3b6021df 100644 --- a/packages/shared/test/files.test.ts +++ b/packages/shared/test/files.test.ts @@ -1,6 +1,11 @@ import type { UnicodeFileTreeNode } from "@ucdjs/schemas"; import { describe, expect, it } from "vitest"; -import { findFileByPath, flattenFilePaths } from "../src/files"; +import { + findFileByPath, + flattenFilePaths, + normalizePathForFiltering, + normalizeTreeForFiltering, +} from "../src/files"; describe("findFileByPath", () => { it("should return undefined for empty input", () => { @@ -435,3 +440,126 @@ describe("flattenFilePaths", () => { expect(result).toEqual(["/file1.txt", "/folder/nested.txt"]); }); }); + +describe("normalizePathForFiltering", () => { + it("strips leading and trailing slashes", () => { + expect(normalizePathForFiltering("16.0.0", "/16.0.0/ucd/auxiliary/")).toBe("auxiliary"); + expect(normalizePathForFiltering("16.0.0", "16.0.0/ucd/Blocks.txt")).toBe("Blocks.txt"); + }); + + it("strips version prefix regardless of leading slash", () => { + expect(normalizePathForFiltering("17.0.0", "/17.0.0/ucd/auxiliary/Grapheme.txt")).toBe("auxiliary/Grapheme.txt"); + expect(normalizePathForFiltering("17.0.0", "17.0.0/ucd/auxiliary/Grapheme.txt")).toBe("auxiliary/Grapheme.txt"); + }); + + it("strips ucd/ when version uses UCD folder", () => { + expect(normalizePathForFiltering("16.0.0", "/16.0.0/ucd/Blocks.txt")).toBe("Blocks.txt"); + expect(normalizePathForFiltering("16.0.0", "/16.0.0/ucd/auxiliary/Grapheme.txt")).toBe("auxiliary/Grapheme.txt"); + }); + + it("keeps ucd/ when version does not use UCD folder", () => { + expect(normalizePathForFiltering("1.1.0", "/1.1.0/ucd/Blocks.txt")).toBe("ucd/Blocks.txt"); + }); + + it("handles nested paths with all prefixes", () => { + expect(normalizePathForFiltering("16.0.0", "/16.0.0/ucd/auxiliary/LineBreak.txt")).toBe("auxiliary/LineBreak.txt"); + expect(normalizePathForFiltering("16.0.0", "/16.0.0/ucd/emoji/emoji-data.txt")).toBe("emoji/emoji-data.txt"); + }); + + it("no-ops when already normalized", () => { + expect(normalizePathForFiltering("16.0.0", "Blocks.txt")).toBe("Blocks.txt"); + expect(normalizePathForFiltering("16.0.0", "auxiliary/LineBreak.txt")).toBe("auxiliary/LineBreak.txt"); + }); +}); + +describe("normalizeTreeForFiltering", () => { + it("normalizes file paths and preserves structure", () => { + const tree: UnicodeFileTreeNode[] = [ + { type: "file", name: "Blocks.txt", path: "/16.0.0/ucd/Blocks.txt", lastModified: null }, + { + type: "directory", + name: "auxiliary", + path: "/16.0.0/ucd/auxiliary/", + lastModified: null, + children: [ + { type: "file", name: "Grapheme.txt", path: "/16.0.0/ucd/auxiliary/Grapheme.txt", lastModified: null }, + { + type: "directory", + name: "nested", + path: "/16.0.0/ucd/auxiliary/nested/", + lastModified: null, + children: [ + { type: "file", name: "Deep.txt", path: "/16.0.0/ucd/auxiliary/nested/Deep.txt", lastModified: null }, + ], + }, + ], + }, + ]; + + const normalized = normalizeTreeForFiltering("16.0.0", tree); + + expect(normalized).toEqual([ + { type: "file", name: "Blocks.txt", path: "Blocks.txt", lastModified: null }, + { + type: "directory", + name: "auxiliary", + path: "auxiliary", + lastModified: null, + children: [ + { type: "file", name: "Grapheme.txt", path: "auxiliary/Grapheme.txt", lastModified: null }, + { + type: "directory", + name: "nested", + path: "auxiliary/nested", + lastModified: null, + children: [ + { type: "file", name: "Deep.txt", path: "auxiliary/nested/Deep.txt", lastModified: null }, + ], + }, + ], + }, + ]); + }); + + it("retains ucd/ prefix when version lacks UCD folder", () => { + const tree: UnicodeFileTreeNode[] = [ + { type: "file", name: "Blocks.txt", path: "/1.1.0/ucd/Blocks.txt", lastModified: null }, + { type: "file", name: "ReadMe.txt", path: "/1.1.0/ReadMe.txt", lastModified: null }, + ]; + + const normalized = normalizeTreeForFiltering("1.1.0", tree); + + expect(normalized).toEqual([ + { type: "file", name: "Blocks.txt", path: "ucd/Blocks.txt", lastModified: null }, + { type: "file", name: "ReadMe.txt", path: "ReadMe.txt", lastModified: null }, + ]); + }); + + it("handles trailing slashes on directories", () => { + const tree: UnicodeFileTreeNode[] = [ + { + type: "directory", + name: "emoji", + path: "/17.0.0/ucd/emoji/", + lastModified: null, + children: [ + { type: "file", name: "emoji-data.txt", path: "/17.0.0/ucd/emoji/emoji-data.txt", lastModified: null }, + ], + }, + ]; + + const normalized = normalizeTreeForFiltering("17.0.0", tree); + + expect(normalized).toEqual([ + { + type: "directory", + name: "emoji", + path: "emoji", + lastModified: null, + children: [ + { type: "file", name: "emoji-data.txt", path: "emoji/emoji-data.txt", lastModified: null }, + ], + }, + ]); + }); +}); diff --git a/packages/shared/test/filter.test.ts b/packages/shared/test/filter.test.ts index a838e4972..8eac7da9e 100644 --- a/packages/shared/test/filter.test.ts +++ b/packages/shared/test/filter.test.ts @@ -35,6 +35,9 @@ describe("createPathFilter", () => { [["src/**", "lib/**"], "src/file.txt", true], [["src/**", "lib/**"], "lib/file.txt", true], [["src/**", "lib/**"], "test/file.txt", false], + // leading slash paths should still match relative patterns + [["src/**"], "/src/file.txt", true], + [["src/**"], "/src/", true], ])("with multiple include patterns %j, path \"%s\" should return %s", (include, path, expected) => { const filter = createPathFilter({ include }); expect(filter(path)).toBe(expected); @@ -79,6 +82,7 @@ describe("createPathFilter", () => { }); it.each([ + // typical project filter ["src/components/Button.tsx", true, "includes source tsx file"], ["src/utils/helper.js", true, "includes source js file"], ["src/components/Button.vue", true, "includes vue file"], @@ -89,6 +93,9 @@ describe("createPathFilter", () => { ["src/utils/helper.spec.js", false, "excludes spec files"], ["README.md", false, "excludes non-matching extensions"], ["package.json", false, "excludes json files"], + ["/src/utils/helper.js", true, "leading slash should still match"], + ["/src/", false, "leading slash directory should not match file-only pattern"], + ["/node_modules/react/index.js", false, "leading slash excluded node_modules"], ])("typical project filter: path \"%s\" should return %s (%s)", (path, expected) => { const filter = createPathFilter({ include: ["**/*.{js,ts,jsx,tsx,vue,svelte}"], @@ -100,7 +107,6 @@ describe("createPathFilter", () => { "**/*.spec.*", ], }); - expect(filter(path)).toBe(expected); }); @@ -675,42 +681,42 @@ describe("filterTreeStructure", () => { { type: "file" as const, name: "root-file.txt", - path: "root-file.txt", + path: "/root-file.txt", }, { type: "file" as const, name: "root-config.json", - path: "root-config.json", + path: "/root-config.json", }, { type: "directory" as const, name: "extracted", - path: "extracted", + path: "/extracted/", children: [ { type: "file" as const, name: "DerivedBidiClass.txt", - path: "extracted/DerivedBidiClass.txt", + path: "/extracted/DerivedBidiClass.txt", }, { type: "file" as const, name: "config.json", - path: "extracted/config.json", + path: "/extracted/config.json", }, { type: "directory" as const, name: "nested", - path: "extracted/nested", + path: "/extracted/nested/", children: [ { type: "file" as const, name: "DeepFile.txt", - path: "extracted/nested/DeepFile.txt", + path: "/extracted/nested/DeepFile.txt", }, { type: "file" as const, name: "debug.log", - path: "extracted/nested/debug.log", + path: "/extracted/nested/debug.log", }, ], }, @@ -727,17 +733,17 @@ describe("filterTreeStructure", () => { { type: "file", name: "root-config.json", - path: "root-config.json", + path: "/root-config.json", }, { type: "directory", name: "extracted", - path: "extracted", + path: "/extracted/", children: [ { type: "file", name: "config.json", - path: "extracted/config.json", + path: "/extracted/config.json", }, ], }, @@ -761,27 +767,27 @@ describe("filterTreeStructure", () => { { type: "file", name: "root-file.txt", - path: "root-file.txt", + path: "/root-file.txt", }, { type: "directory", name: "extracted", - path: "extracted", + path: "/extracted/", children: [ { type: "file", name: "DerivedBidiClass.txt", - path: "extracted/DerivedBidiClass.txt", + path: "/extracted/DerivedBidiClass.txt", }, { type: "directory", name: "nested", - path: "extracted/nested", + path: "/extracted/nested/", children: [ { type: "file", name: "DeepFile.txt", - path: "extracted/nested/DeepFile.txt", + path: "/extracted/nested/DeepFile.txt", }, ], }, @@ -800,17 +806,17 @@ describe("filterTreeStructure", () => { { type: "directory", name: "extracted", - path: "extracted", + path: "/extracted/", children: [ { type: "directory", name: "nested", - path: "extracted/nested", + path: "/extracted/nested/", children: [ { type: "file", name: "DeepFile.txt", - path: "extracted/nested/DeepFile.txt", + path: "/extracted/nested/DeepFile.txt", }, ], }, @@ -834,32 +840,32 @@ describe("filterTreeStructure", () => { { type: "directory", name: "extracted", - path: "extracted", + path: "/extracted/", children: [ { type: "file", name: "DerivedBidiClass.txt", - path: "extracted/DerivedBidiClass.txt", + path: "/extracted/DerivedBidiClass.txt", }, { type: "file", name: "config.json", - path: "extracted/config.json", + path: "/extracted/config.json", }, { type: "directory", name: "nested", - path: "extracted/nested", + path: "/extracted/nested/", children: [ { type: "file", name: "DeepFile.txt", - path: "extracted/nested/DeepFile.txt", + path: "/extracted/nested/DeepFile.txt", }, { type: "file", name: "debug.log", - path: "extracted/nested/debug.log", + path: "/extracted/nested/debug.log", }, ], }, @@ -964,17 +970,17 @@ describe("filterTreeStructure", () => { { type: "directory", name: "extracted", - path: "extracted", + path: "/extracted/", children: [ { type: "directory", name: "nested", - path: "extracted/nested", + path: "/extracted/nested/", children: [ { type: "file", name: "DeepFile.txt", - path: "extracted/nested/DeepFile.txt", + path: "/extracted/nested/DeepFile.txt", }, ], }, @@ -994,37 +1000,37 @@ describe("filterTreeStructure", () => { { type: "file", name: "root-file.txt", - path: "root-file.txt", + path: "/root-file.txt", }, { type: "file", name: "root-config.json", - path: "root-config.json", + path: "/root-config.json", }, { type: "directory", name: "extracted", - path: "extracted", + path: "/extracted/", children: [ { type: "file", name: "DerivedBidiClass.txt", - path: "extracted/DerivedBidiClass.txt", + path: "/extracted/DerivedBidiClass.txt", }, { type: "file", name: "config.json", - path: "extracted/config.json", + path: "/extracted/config.json", }, { type: "directory", name: "nested", - path: "extracted/nested", + path: "/extracted/nested/", children: [ { type: "file", name: "DeepFile.txt", - path: "extracted/nested/DeepFile.txt", + path: "/extracted/nested/DeepFile.txt", }, // debug.log should be excluded ], diff --git a/packages/test-utils/src/fs-bridges/memory-fs-bridge.ts b/packages/test-utils/src/fs-bridges/memory-fs-bridge.ts index 1e62c697e..a4b1e6bea 100644 --- a/packages/test-utils/src/fs-bridges/memory-fs-bridge.ts +++ b/packages/test-utils/src/fs-bridges/memory-fs-bridge.ts @@ -1,5 +1,6 @@ import type { FileSystemBridgeOperations, FSEntry } from "@ucdjs/fs-bridge"; import { Buffer } from "node:buffer"; +import { appendTrailingSlash, prependLeadingSlash } from "@luxass/utils/path"; import { defineFileSystemBridge } from "@ucdjs/fs-bridge"; import { FileEntrySchema } from "@ucdjs/schemas"; import { z } from "zod"; @@ -12,6 +13,16 @@ function normalizeRootPath(path: string | undefined): string { return (!path || path === "." || path === "/") ? "" : path; } +/** + * Formats a relative path to match FSEntry schema requirements (parity with node/http bridges): + * - Leading slash required for all paths + * - Trailing slash required for directories + */ +function formatEntryPath(relativePath: string, isDirectory: boolean): string { + const withLeadingSlash = prependLeadingSlash(relativePath); + return isDirectory ? appendTrailingSlash(withLeadingSlash) : withLeadingSlash; +} + /** * Marker value for explicit directories in the flat Map storage. * Directories are stored as "path/" -> DIR_MARKER @@ -195,7 +206,7 @@ export const createMemoryMockFS = defineFileSystemBridge({ entries.push({ type: "directory" as const, name: dirName, - path: dirName, + path: formatEntryPath(dirName, true), children: [], }); } @@ -216,7 +227,7 @@ export const createMemoryMockFS = defineFileSystemBridge({ dirEntry = { type: "directory" as const, name: part, - path: partPath, + path: formatEntryPath(partPath, true), children: [], }; currentLevel.push(dirEntry); @@ -237,7 +248,7 @@ export const createMemoryMockFS = defineFileSystemBridge({ entries.push({ type: "file" as const, name: parts[0], - path: relativePath, + path: formatEntryPath(relativePath, false), }); } else if (parts.length > 1 && parts[0]) { // Directory (implicit from file path) @@ -247,7 +258,7 @@ export const createMemoryMockFS = defineFileSystemBridge({ entries.push({ type: "directory" as const, name: dirName, - path: dirName, + path: formatEntryPath(dirName, true), children: [], }); } @@ -267,7 +278,7 @@ export const createMemoryMockFS = defineFileSystemBridge({ currentLevel.push({ type: "file" as const, name: part, - path: partPath, + path: formatEntryPath(partPath, false), }); } else { // It's a directory - find or create it @@ -279,7 +290,7 @@ export const createMemoryMockFS = defineFileSystemBridge({ dirEntry = { type: "directory" as const, name: part, - path: partPath, + path: formatEntryPath(partPath, true), children: [], }; currentLevel.push(dirEntry); diff --git a/packages/test-utils/src/mock-store/handlers/files.ts b/packages/test-utils/src/mock-store/handlers/files.ts index f999da526..251f20a99 100644 --- a/packages/test-utils/src/mock-store/handlers/files.ts +++ b/packages/test-utils/src/mock-store/handlers/files.ts @@ -142,19 +142,13 @@ export const filesRoute = defineMockRouteHandler({ // If it's a file with _content, return the content if (fileNode && "_content" in fileNode && typeof fileNode._content === "string") { - let content = fileNode._content; + const content = fileNode._content; const contentLength = new TextEncoder().encode(content).length; const headers: Record = { "Content-Type": "text/plain; charset=utf-8", [UCD_STAT_TYPE_HEADER]: "file", }; - // Check if the content ends with a newline; if not, add one for better terminal display - if (!content.endsWith("\n")) { - headers["X-Content-Warning"] = "Content did not end with a newline; added for display purposes."; - content += "\n"; - } - // Only include size headers for HEAD requests (buffered response) if (isHeadRequest) { headers["Content-Length"] = `${contentLength}`; diff --git a/packages/test-utils/test/fs-bridge/memory-fs-bridge.test.ts b/packages/test-utils/test/fs-bridge/memory-fs-bridge.test.ts index aabab5031..0ebca6c22 100644 --- a/packages/test-utils/test/fs-bridge/memory-fs-bridge.test.ts +++ b/packages/test-utils/test/fs-bridge/memory-fs-bridge.test.ts @@ -133,8 +133,8 @@ describe("memory fs bridge", () => { const entries = await fs.listdir("", false); expect(entries).toHaveLength(2); expect(entries).toEqual(expect.arrayContaining([ - { type: "file", name: "file1.txt", path: "file1.txt" }, - { type: "file", name: "file2.txt", path: "file2.txt" }, + { type: "file", name: "file1.txt", path: "/file1.txt" }, + { type: "file", name: "file2.txt", path: "/file2.txt" }, ])); }); @@ -149,8 +149,8 @@ describe("memory fs bridge", () => { const entries = await fs.listdir("", false); expect(entries).toHaveLength(2); expect(entries).toEqual(expect.arrayContaining([ - { type: "file", name: "file.txt", path: "file.txt" }, - { type: "directory", name: "dir", path: "dir", children: [] }, + { type: "file", name: "file.txt", path: "/file.txt" }, + { type: "directory", name: "dir", path: "/dir/", children: [] }, ])); }); @@ -166,9 +166,9 @@ describe("memory fs bridge", () => { const entries = await fs.listdir("dir", false); expect(entries).toHaveLength(3); expect(entries).toEqual(expect.arrayContaining([ - { type: "file", name: "file1.txt", path: "file1.txt" }, - { type: "file", name: "file2.txt", path: "file2.txt" }, - { type: "directory", name: "nested", path: "nested", children: [] }, + { type: "file", name: "file1.txt", path: "/file1.txt" }, + { type: "file", name: "file2.txt", path: "/file2.txt" }, + { type: "directory", name: "nested", path: "/nested/", children: [] }, ])); }); @@ -191,19 +191,19 @@ describe("memory fs bridge", () => { expect(entries).toHaveLength(2); const fileEntry = entries.find((e) => e.type === "file"); - expect(fileEntry).toEqual({ type: "file", name: "file.txt", path: "file.txt" }); + expect(fileEntry).toEqual({ type: "file", name: "file.txt", path: "/file.txt" }); const dirEntry = entries.find((e) => e.type === "directory" && e.name === "dir"); expect(dirEntry).toBeDefined(); if (dirEntry?.type === "directory") { expect(dirEntry.children).toHaveLength(2); expect(dirEntry.children).toEqual(expect.arrayContaining([ - { type: "file", name: "nested.txt", path: "dir/nested.txt" }, + { type: "file", name: "nested.txt", path: "/dir/nested.txt" }, { type: "directory", name: "deep", - path: "dir/deep", - children: [{ type: "file", name: "file.txt", path: "dir/deep/file.txt" }], + path: "/dir/deep/", + children: [{ type: "file", name: "file.txt", path: "/dir/deep/file.txt" }], }, ])); } @@ -238,7 +238,7 @@ describe("memory fs bridge", () => { expect(cDir.name).toBe("c"); expect(cDir.children).toHaveLength(1); - expect(cDir.children[0]).toEqual({ type: "file", name: "file.txt", path: "a/b/c/file.txt" }); + expect(cDir.children[0]).toEqual({ type: "file", name: "file.txt", path: "/a/b/c/file.txt" }); }); }); @@ -266,7 +266,7 @@ describe("memory fs bridge", () => { assertCapability(fs, "mkdir"); await fs.mkdir("emptydir"); const entries = await fs.listdir("", false); - expect(entries).toEqual([{ type: "directory", name: "emptydir", path: "emptydir", children: [] }]); + expect(entries).toEqual([{ type: "directory", name: "emptydir", path: "/emptydir/", children: [] }]); }); it("should throw EISDIR when reading a directory", async () => {