Preserve colon-containing Vault paths with Commonlib 0.1.28

This commit is contained in:
vorotamoroz
2026-09-25 07:48:28 +00:00
parent 2c2b9c90e4
commit ae9145daa5
8 changed files with 169 additions and 21 deletions
+64
View File
@@ -0,0 +1,64 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
const mocks = vi.hoisted(() => ({
normalizePath: vi.fn((path: string) => `normalised(${path})`),
path2idBase: vi.fn(async (path: string) => path),
id2pathBase: vi.fn((path: string) => path),
expandFilePathPrefix: vi.fn((path: string): [string, string] => {
if (path.startsWith("i:")) return ["i:", path.substring(2)];
return ["", path];
}),
}));
vi.mock("@/deps.ts", () => ({
normalizePath: mocks.normalizePath,
Platform: {},
requestUrl: vi.fn(),
}));
vi.mock("@vrtmrz/livesync-commonlib/compat/string_and_binary/path", () => ({
path2id_base: mocks.path2idBase,
id2path_base: mocks.id2pathBase,
expandFilePathPrefix: mocks.expandFilePathPrefix,
isValidFilenameInLinux: vi.fn(),
isValidFilenameInDarwin: vi.fn(),
isValidFilenameInWidows: vi.fn(),
isValidFilenameInAndroid: vi.fn(),
stripAllPrefixes: vi.fn(),
}));
describe("path ID normalisation", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it.each([
["Folder/Note.md", "", "Folder/Note.md"],
["Folder/Poem: Example.md", "", "Folder/Poem: Example.md"],
["Folder/Poem: Example: Final Draft.md", "", "Folder/Poem: Example: Final Draft.md"],
["i:Folder/Poem: Example.md", "i:", "Folder/Poem: Example.md"],
])("normalises the complete path body for %s", async (filename, prefix, body) => {
const { path2id } = await import("./utils.ts");
const result = await path2id(filename as never, false, false);
expect(mocks.normalizePath).toHaveBeenCalledWith(body);
expect(mocks.path2idBase).toHaveBeenCalledWith(`${prefix}normalised(${body})`, false, false);
expect(result).toBe(`${prefix}normalised(${body})`);
});
it.each([
["Folder/Note.md", "", "Folder/Note.md"],
["Folder/Poem: Example.md", "", "Folder/Poem: Example.md"],
["Folder/Poem: Example: Final Draft.md", "", "Folder/Poem: Example: Final Draft.md"],
["i:Folder/Poem: Example.md", "i:", "Folder/Poem: Example.md"],
])("preserves the path namespace while normalising %s", async (filename, prefix, body) => {
mocks.id2pathBase.mockReturnValue(filename);
const { id2path } = await import("./utils.ts");
const result = id2path(filename as never);
expect(mocks.normalizePath).toHaveBeenCalledWith(body);
expect(result).toBe(`${prefix}normalised(${body})`);
});
});
+5 -8
View File
@@ -7,6 +7,7 @@ import {
isValidFilenameInWidows,
isValidFilenameInAndroid,
stripAllPrefixes,
expandFilePathPrefix,
} from "@vrtmrz/livesync-commonlib/compat/string_and_binary/path";
import { Logger } from "@vrtmrz/livesync-commonlib/compat/common/logger";
@@ -41,22 +42,18 @@ export async function path2id(
obfuscatePassphrase: string | false,
caseInsensitive: boolean
): Promise<DocumentID> {
const temp = filename.split(":");
const path = temp.pop();
const [prefix, path] = expandFilePathPrefix(filename);
const normalizedPath = normalizePath(path as FilePath);
temp.push(normalizedPath);
const fixedPath = temp.join(":") as FilePathWithPrefix;
const fixedPath = `${prefix}${normalizedPath}` as FilePathWithPrefix;
const out = await path2id_base(fixedPath, obfuscatePassphrase, caseInsensitive);
return out;
}
export function id2path(id: DocumentID, entry?: EntryHasPath): FilePathWithPrefix {
const filename = id2path_base(id, entry);
const temp = filename.split(":");
const path = temp.pop();
const [prefix, path] = expandFilePathPrefix(filename);
const normalizedPath = normalizePath(path as FilePath);
temp.push(normalizedPath);
const fixedPath = temp.join(":") as FilePathWithPrefix;
const fixedPath = `${prefix}${normalizedPath}` as FilePathWithPrefix;
return fixedPath;
}
@@ -1,7 +1,11 @@
import { promiseWithResolvers } from "octagonal-wheels/promises";
import { reactiveSource } from "octagonal-wheels/dataobject/reactive";
import { describe, expect, it, vi } from "vitest";
import { VER, type EntryDoc } from "@vrtmrz/livesync-commonlib/compat/common/types";
import { VER, type EntryDoc, type FilePathWithPrefix } from "@vrtmrz/livesync-commonlib/compat/common/types";
import {
isValidFilenameInAndroid,
isValidFilenameInWidows,
} from "@vrtmrz/livesync-commonlib/compat/string_and_binary/path";
import {
defaultLogger,
LOG_LEVEL_DEBUG,
@@ -28,6 +32,7 @@ function note(id: string): PouchDB.Core.ExistingDocument<EntryDoc> {
type SetupOptions = {
applicationReady?: boolean;
isValidPath?: (path: string) => boolean;
processSynchroniseResult?: (entry: unknown) => Promise<boolean>;
setSnapshot?: (key: string, value: unknown) => Promise<unknown>;
};
@@ -38,6 +43,8 @@ function setup(options: SetupOptions = {}) {
const runBoundedLocalApplicationActivity = vi.fn(async (task: () => Promise<void>) => await task());
const onCloseActiveReplication = vi.fn(async () => true);
const isReady = vi.fn(() => options.applicationReady ?? true);
const isValidPath = vi.fn(options.isValidPath ?? (() => true));
const getDBEntryFromMeta = vi.fn(async (entry: object) => ({ ...entry, data: "x" }));
const core = {
services: {
appLifecycle: { isReady, isSuspended: () => false },
@@ -54,13 +61,13 @@ function setup(options: SetupOptions = {}) {
vault: {
isTargetFile: vi.fn(async () => true),
isFileSizeTooLarge: vi.fn(() => false),
isValidPath: vi.fn(() => true),
isValidPath,
},
},
kvDB: { set: setSnapshot },
localDatabase: {
getRaw: vi.fn(async (id: string) => ({ _id: id, _rev: "1-test" })),
getDBEntryFromMeta: vi.fn(async (entry: object) => ({ ...entry, data: "x" })),
getDBEntryFromMeta,
},
};
const processor = new ReplicateResultProcessor({
@@ -74,7 +81,9 @@ function setup(options: SetupOptions = {}) {
services: core.services,
} as never);
return {
getDBEntryFromMeta,
isReady,
isValidPath,
onCloseActiveReplication,
processor,
processSynchroniseResult,
@@ -83,6 +92,24 @@ function setup(options: SetupOptions = {}) {
}
describe("ReplicateResultProcessor", () => {
it.each([
["Windows", isValidFilenameInWidows],
["Android", isValidFilenameInAndroid],
])("does not reflect a replicated colon path into the %s Vault", async (_platform, validatePath) => {
const path = "Folder/Poem: Example.md" as FilePathWithPrefix;
const document = { ...note("colon-path"), path };
const { getDBEntryFromMeta, isValidPath, processor, processSynchroniseResult } = setup({
isValidPath: validatePath,
});
processor.enqueueAll([document]);
await vi.waitFor(() => expect(isValidPath).toHaveBeenCalledWith(path));
await vi.waitFor(() => expect(processor["_processingChanges"]).toHaveLength(0));
expect(getDBEntryFromMeta).toHaveBeenCalledWith(expect.objectContaining({ path }), false, true);
expect(processSynchroniseResult).not.toHaveBeenCalled();
});
it("resumes another document after in-flight updates to one document fill the application slots", async () => {
const hotGate = promiseWithResolvers<boolean>();
const { processor, processSynchroniseResult } = setup({