Files
obsidian-livesync/src/serviceFeatures/diagnosticFileLookup.unit.spec.ts

363 lines
16 KiB
TypeScript

import { createDiagnosticNameMapper } from "@vrtmrz/livesync-commonlib/diagnostics";
import { describe, expect, it, vi } from "vitest";
import { buildPseudonymousFileReport, findDiagnosticFiles, showDiagnosticFileLookup } from "./diagnosticFileLookup";
import type { DiagnosticFileLookupDialogOptions } from "./diagnosticFileLookupTypes";
type TestDocument = {
_id: string;
_rev?: string;
_conflicts?: string[];
_revs_info?: Array<{ rev: string; status: string }>;
_deleted?: boolean;
children?: string[];
ctime?: number;
data?: string;
deleted?: boolean;
eden?: Record<string, unknown>;
mtime?: number;
path?: string;
size?: number;
type?: string;
};
async function* entries(documents: TestDocument[]) {
for (const document of documents) yield document;
}
function createCore(vaultPaths: string[] = [], documents: TestDocument[] = []) {
const existingPaths = new Set(vaultPaths);
const chunkRevisions = new Map([["h:secret-chunk", "1-secret-chunk-revision"]]);
const core = {
settings: {},
storageAccess: {
getFileNames: vi.fn(async () => [...vaultPaths]),
getFilesIncludeHidden: vi.fn(async () => [...vaultPaths]),
isExistsIncludeHidden: vi.fn(async (path: string) => existingPaths.has(path)),
statHidden: vi.fn(async (path: string) =>
existingPaths.has(path) ? { ctime: 10, mtime: 20, size: 30, type: "file" } : undefined
),
},
localDatabase: {
allDocsRaw: vi.fn(async ({ keys }: { keys: string[] }) => ({
rows: keys.flatMap((key) => {
const rev = chunkRevisions.get(key);
return rev ? [{ id: key, key, value: { rev } }] : [];
}),
})),
findAllDocs: vi.fn(() => entries(documents)),
getDBEntryFromMeta: vi.fn(),
getDBEntry: vi.fn(),
localDatabase: {
get: vi.fn(async (id: string) => {
const document = documents.find(({ _id }) => _id === id);
if (document) return document;
throw Object.assign(new Error("missing"), { status: 404 });
}),
},
},
services: {
path: { path2id: vi.fn(async () => "f:path-derived") },
UI: {
promptCopyToClipboard: vi.fn(async () => true),
dialogManager: { open: vi.fn() },
},
},
};
return core;
}
function diagnosticPath(path: string, salt = "salt1234"): string {
return createDiagnosticNameMapper("pseudonym", salt).path(path);
}
describe("diagnostic file lookup", () => {
it.each(["metadata", "chunks"] as const)(
"stops follow-up reads when the dialogue closes during a %s read",
async (pendingRead) => {
const path = "cancelled.md";
const selected = {
_id: "f:cancelled",
_rev: "3-current",
_conflicts: ["3-conflict"],
path,
type: "plain",
children: ["h:secret-chunk"],
};
const core = createCore([path], [selected]);
let markStarted!: () => void;
const started = new Promise<void>((resolve) => {
markStarted = resolve;
});
let releaseRead!: () => void;
const pending = new Promise<void>((resolve) => {
releaseRead = resolve;
});
if (pendingRead === "metadata") {
core.localDatabase.localDatabase.get.mockImplementationOnce(async () => {
markStarted();
await pending;
return selected;
});
} else {
core.localDatabase.allDocsRaw.mockImplementationOnce(async () => {
markStarted();
await pending;
return { rows: [] };
});
}
core.services.UI.dialogManager.open.mockImplementation(async (...args: unknown[]) => {
const options = args[1] as DiagnosticFileLookupDialogOptions;
const preparation = options.prepareReport(
{ path, documentId: selected._id, storageExists: true, source: "metadata" },
diagnosticPath(path)
);
await started;
const cancelled = expect(preparation).rejects.toMatchObject({ name: "AbortError" });
options.cancel();
releaseRead();
await cancelled;
});
await showDiagnosticFileLookup(core as never);
expect(core.localDatabase.localDatabase.get).toHaveBeenCalledTimes(1);
expect(core.localDatabase.allDocsRaw).toHaveBeenCalledTimes(pendingRead === "metadata" ? 0 : 1);
}
);
it("rejects an invalid path token before reading storage or the database", async () => {
const core = createCore(["note.md"], [{ _id: "f:note", path: "note.md", type: "plain" }]);
await expect(findDiagnosticFiles(core as never, "not-a-path-token")).resolves.toEqual({
valid: false,
status: "invalid",
processed: 0,
candidates: [],
});
expect(core.storageAccess.getFileNames).not.toHaveBeenCalled();
expect(core.localDatabase.findAllDocs).not.toHaveBeenCalled();
expect(core.localDatabase.localDatabase.get).not.toHaveBeenCalled();
});
it("matches path spelling exactly without case folding or Unicode normalisation", async () => {
const decomposed = "Cafe\u0301.md";
const core = createCore(
[decomposed, "Café.md", "CAFÉ.md"],
[
{ _id: "f:decomposed", path: decomposed, type: "plain" },
{ _id: "f:composed", path: "Café.md", type: "plain" },
{ _id: "f:case", path: "CAFÉ.md", type: "plain" },
]
);
const progressCounts: number[] = [];
const result = await findDiagnosticFiles(core as never, diagnosticPath(decomposed), {
onProgress: (processed) => progressCounts.push(processed),
});
expect(result).toMatchObject({ valid: true, status: "complete" });
expect(result.candidates).toEqual([
expect.objectContaining({
path: decomposed,
documentId: "f:decomposed",
source: "metadata",
storageExists: true,
}),
]);
expect(core.localDatabase.localDatabase.get).not.toHaveBeenCalled();
expect(progressCounts).toEqual([0, 6]);
});
it("keeps distinct Metadata documents with the same exact path as separate candidates", async () => {
const path = "duplicate.md";
const core = createCore(
[path],
[
{ _id: "f:duplicate-one", path, type: "plain" },
{ _id: "f:duplicate-two", path, type: "plain" },
]
);
const result = await findDiagnosticFiles(core as never, diagnosticPath(path));
expect(result.candidates).toEqual([
expect.objectContaining({ documentId: "f:duplicate-one", path, storageExists: true, source: "metadata" }),
expect.objectContaining({ documentId: "f:duplicate-two", path, storageExists: true, source: "metadata" }),
]);
expect(result.candidates).toHaveLength(2);
});
it("keeps logically deleted current file Metadata as an exact candidate", async () => {
const path = "vault-only.md";
const core = createCore(
[path],
[
{ _id: "f:deleted", path, type: "plain", deleted: true },
{ _id: "f:chunk", path, type: "chunk" },
{ _id: "i:internal", path: "i:.obsidian/config.json", type: "plain" },
]
);
const result = await findDiagnosticFiles(core as never, diagnosticPath(path));
expect(result.candidates).toEqual([
expect.objectContaining({ path, documentId: "f:deleted", storageExists: true, source: "metadata" }),
]);
});
it("returns a Vault-only candidate when there is no current normal-file Metadata", async () => {
const path = "vault-only.md";
const core = createCore(
[path],
[
{ _id: "f:chunk", path, type: "chunk" },
{ _id: "f:legacy", path, type: "notes" },
{ _id: "i:internal", path: "i:.obsidian/config.json", type: "plain" },
]
);
const result = await findDiagnosticFiles(core as never, diagnosticPath(path));
expect(result.candidates).toEqual([expect.objectContaining({ path, storageExists: true, source: "vault" })]);
expect(result.candidates[0]).not.toHaveProperty("documentId");
const report = await buildPseudonymousFileReport(core as never, result.candidates[0], "salt1234");
const jsonText = report.artifacts.find(({ format }) => format === "json")!.content;
const json = JSON.parse(jsonText);
expect(json.file.path).toBe(diagnosticPath(path));
expect(jsonText).not.toContain(path);
expect(json.file.database).toEqual({
source: "local database on this device",
remoteQueried: false,
inspected: false,
reason: "no-local-target",
});
expect(json.scope).toEqual({
source: "local database on this device",
remoteQueried: false,
vaultContentRead: false,
fileContents: "omitted",
revisions: "current revision, current conflict revisions, and selected merge-base revisions",
revisionHistory: "status metadata is listed for current and conflict revisions where available",
historicalFileBodiesScanned: false,
});
expect(core.localDatabase.localDatabase.get).not.toHaveBeenCalled();
});
it("returns partial exact matches when cancellation arrives during enumeration", async () => {
const path = "match.md";
const documents = [
{ _id: "f:match", path, type: "plain" },
...Array.from({ length: 299 }, (_, index) => ({
_id: `f:other-${index}`,
path: `other-${index}.md`,
type: "plain",
})),
];
const core = createCore([], documents);
const controller = new AbortController();
const progressCounts: number[] = [];
const result = await findDiagnosticFiles(core as never, diagnosticPath(path), {
signal: controller.signal,
onProgress: (processed) => {
progressCounts.push(processed);
if (processed >= 128) controller.abort();
},
});
expect(result).toMatchObject({ valid: true, status: "cancelled", processed: 128 });
expect(result.candidates).toEqual([
expect.objectContaining({ documentId: "f:match", path, storageExists: false, source: "metadata" }),
]);
expect(progressCounts).toEqual([0, 128, 128]);
});
it("projects an exact target with the token salt, preserves revision generations, and omits raw values", async () => {
const path = "private-name.md";
const salt = "salt1234";
const token = diagnosticPath(path, salt);
const selected = {
_id: "f:selected-private-id",
_rev: "3-private-revision",
_conflicts: [],
_revs_info: [{ rev: "3-private-revision", status: "private-status-value" }],
path,
ctime: 11,
mtime: 22,
size: 33,
type: "plain",
data: "private file contents",
children: ["h:private-chunk-id"],
eden: {},
unreported: "private-extra-value",
};
const decoy = { ...selected, _id: "f:path-derived", path: "different.md", _rev: "9-decoy-revision" };
const core = createCore([path], [selected, decoy]);
core.localDatabase.localDatabase.get.mockImplementation(async (id: string) => {
if (id === selected._id) return selected;
if (id === decoy._id) return decoy;
throw Object.assign(new Error("missing"), { status: 404 });
});
const candidate = {
path,
databasePath: path as never,
documentId: selected._id,
storageExists: true,
source: "metadata" as const,
};
const output = await buildPseudonymousFileReport(core as never, candidate, salt);
const jsonText = output.artifacts.find(({ format }) => format === "json")!.content;
const json = JSON.parse(jsonText);
const revision = json.file.database.revisions[0];
expect(output.nameMode).toBe("pseudonym");
expect(json.kind).toBe("file");
expect(json.nameMode).toBe("pseudonym");
expect(json.salt).toBe(salt);
expect(json.file.path).toBe(token);
expect(json.scope).toMatchObject({ vaultContentRead: false, fileContents: "omitted" });
expect(revision.documentId).toMatch(/^document-[0-9a-f]{32}$/);
expect(revision.revision).toMatch(/^3-psu[0-9a-f]{32}$/);
expect(revision.chunks[0].id).toMatch(/^chunk-[0-9a-f]{32}$/);
expect(revision.revisionHistory[0].status).toBe("unknown");
expect(json.file.database.inspected).toBe(true);
expect(output.artifacts.map(({ format }) => format)).toEqual(["json", "markdown"]);
expect(output.artifacts.find(({ format }) => format === "markdown")?.content).toContain("```json");
expect(jsonText).not.toContain(path);
expect(jsonText).not.toContain(selected._id);
expect(jsonText).not.toContain("3-private-revision");
expect(jsonText).not.toContain("h:private-chunk-id");
expect(jsonText).not.toContain("private-status-value");
expect(jsonText).not.toContain("private file contents");
expect(jsonText).not.toContain("private-extra-value");
expect(core.localDatabase.localDatabase.get.mock.calls.map(([id]) => id)).toEqual([selected._id]);
expect(core.services.path.path2id).not.toHaveBeenCalled();
});
it("keeps an exact Metadata target visibly missing without substituting a path-derived document", async () => {
const path = "vanished.md";
const salt = "salt1234";
const selected = { _id: "f:vanished", path, type: "plain" };
const decoy = { _id: "f:path-derived", path: "wrong.md", type: "plain", _rev: "1-decoy" };
const core = createCore([path], [selected, decoy]);
const lookup = await findDiagnosticFiles(core as never, diagnosticPath(path, salt));
core.localDatabase.localDatabase.get.mockImplementation(async (id: string) => {
if (id === decoy._id) return decoy;
throw Object.assign(new Error("missing"), { status: 404 });
});
const output = await buildPseudonymousFileReport(core as never, lookup.candidates[0], salt);
const jsonText = output.artifacts.find(({ format }) => format === "json")!.content;
const json = JSON.parse(jsonText);
expect(json.file.database).toMatchObject({ inspected: true, exists: false });
expect(json.file.database.documentId).toMatch(/^document-[0-9a-f]{32}$/);
expect(jsonText).not.toContain("f:vanished");
expect(jsonText).not.toContain("f:path-derived");
expect(core.localDatabase.localDatabase.get.mock.calls.map(([id]) => id)).toEqual([selected._id]);
expect(core.services.path.path2id).not.toHaveBeenCalled();
});
});