Files
obsidian-livesync/src/serviceFeatures/fileDatabaseInfo.unit.spec.ts
T

414 lines
15 KiB
TypeScript

import { describe, expect, it, vi } from "vitest";
import {
buildFileDatabaseInfoReport,
chooseAndCopyFileDatabaseInfo,
collectFileDatabaseInfoPaths,
inspectFileDatabaseInfo,
readFileDatabaseRevisionLocally,
retryReadFileDatabaseRevision,
} from "./fileDatabaseInfo";
async function* documents(paths: string[]) {
for (const path of paths) {
yield {
_id: `f:${path}`,
path,
};
}
}
function createCore() {
const current = {
_id: "f:note",
_rev: "3-current",
_conflicts: ["2-conflict"],
_revs_info: [
{ rev: "3-current", status: "available" },
{ rev: "2-parent", status: "missing" },
],
path: "note.md",
ctime: 100,
mtime: 300,
size: 42,
type: "plain",
datatype: "plain",
data: "secret current body",
children: ["h:private-current", "h:private-current", "h:private-embedded", "h:private-deleted"],
eden: {
"h:private-embedded": {
data: "secret embedded body",
epoch: 1,
},
},
};
const conflict = {
...current,
_rev: "2-conflict",
_conflicts: undefined,
_revs_info: [{ rev: "2-conflict", status: "available" }],
mtime: 200,
data: "secret conflict body",
children: ["h:private-missing"],
eden: {},
};
const promptCopyToClipboard = vi.fn(async (_title: string, _value: string) => true);
const askSelectString = vi.fn(async () => "db-only.md");
const core = {
settings: {
syncInternalFiles: false,
syncInternalFilesIgnorePatterns: "",
syncInternalFilesTargetPatterns: "",
},
storageAccess: {
isExistsIncludeHidden: vi.fn(async () => true),
statHidden: vi.fn(async () => ({
ctime: 90,
mtime: 310,
size: 45,
type: "file",
})),
getFileNames: vi.fn(async () => ["z.md", "a.md"]),
getFilesIncludeHidden: vi.fn(async () => [".obsidian/app.json", "a.md"]),
},
localDatabase: {
getDBEntryFromMeta: vi.fn(async (meta: typeof current) => ({
...meta,
data: ["loaded body"],
})),
getDBEntry: vi.fn(async () => current),
localDatabase: {
get: vi.fn(async (_id: string, options?: { rev?: string }) =>
options?.rev === "2-conflict" ? conflict : current
),
},
allDocsRaw: vi.fn(async ({ keys }: { keys: string[] }) => ({
rows: [
...(keys.includes("h:private-current")
? [
{
id: "h:private-current",
key: "h:private-current",
value: { rev: "1-chunk" },
},
]
: []),
...(keys.includes("h:private-deleted")
? [
{
id: "h:private-deleted",
key: "h:private-deleted",
value: { rev: "4-deleted-chunk", deleted: true },
},
]
: []),
],
})),
findAllDocs: vi.fn(() => documents(["db-only.md", "i:.obsidian/app.json", "ix:ignore", "ps:setting"])),
},
services: {
path: {
path2id: vi.fn(async () => "f:note"),
},
UI: {
promptCopyToClipboard,
confirm: {
askSelectString,
},
},
},
};
return {
askSelectString,
conflict,
core,
current,
promptCopyToClipboard,
};
}
describe("file database information", () => {
it("reports document and chunk revisions without exposing file contents", async () => {
const { core } = createCore();
const report = await buildFileDatabaseInfoReport(core as never, "note.md");
expect(report).toContain('"path": "note.md"');
expect(report).toContain('"documentId": "f:note"');
expect(report).toContain('"revision": "3-current"');
expect(report).toContain('"revision": "2-conflict"');
expect(report).toContain('"storageType": "plain"');
expect(report).toContain('"storageLayout": "chunked"');
expect(report).toContain('"contentAvailableLocally": false');
expect(report).toContain('"id": "h:private-current"');
expect(report).toContain('"localDatabaseRevision": "1-chunk"');
expect(report).toContain('"referenceCount": 2');
expect(report).toContain('"id": "h:private-embedded"');
expect(report).toContain('"embedded": true');
expect(report).toContain('"id": "h:private-deleted"');
expect(report).toContain('"localDatabaseState": "deleted"');
expect(report).toContain('"localDatabaseRevision": "4-deleted-chunk"');
expect(report).toContain('"id": "h:private-missing"');
expect(report).toContain('"localDatabaseState": "missing"');
expect(report).toContain('"localDatabaseRevision": null');
expect(report).not.toContain("secret current body");
expect(report).not.toContain("secret conflict body");
expect(report).not.toContain("secret embedded body");
});
it.each([
{
name: "notes",
document: {
type: "notes",
data: "secret legacy body",
},
storageType: "notes",
},
{
name: "an absent type",
document: {
type: undefined,
data: ["secret", " legacy body"],
},
storageType: "absent",
},
])("reports $name as legacy inline storage without exposing its body", async ({ document, storageType }) => {
const { core, current } = createCore();
core.localDatabase.localDatabase.get.mockResolvedValue({
...current,
...document,
_conflicts: [],
children: ["h:must-not-be-treated-as-a-chunk"],
} as never);
const info = await inspectFileDatabaseInfo(core as never, "note.md");
const report = await buildFileDatabaseInfoReport(core as never, "note.md");
expect(info.database.revisions).toEqual([
expect.objectContaining({
storageType,
storageLayout: "legacy-inline",
chunkReferences: 0,
contentAvailableLocally: true,
}),
]);
expect(report).not.toContain("secret legacy body");
expect(report).not.toContain("h:must-not-be-treated-as-a-chunk");
});
it("reports the exact shared ancestor and its missing chunks for each conflict", async () => {
const { conflict, core, current } = createCore();
const parent = {
...current,
_rev: "2-parent",
_conflicts: undefined,
_revs_info: [
{ rev: "2-parent", status: "available" },
{ rev: "1-root", status: "missing" },
],
children: ["h:missing-parent"],
eden: {},
};
core.localDatabase.localDatabase.get.mockImplementation(async (_id: string, options?: { rev?: string }) => {
if (options?.rev === "2-conflict") {
return {
...conflict,
_revs_info: [
{ rev: "2-conflict", status: "available" },
{ rev: "2-parent", status: "available" },
{ rev: "1-root", status: "missing" },
],
};
}
if (options?.rev === "2-parent") {
return parent;
}
return {
...current,
_revs_info: [
{ rev: "3-current", status: "available" },
{ rev: "2-parent", status: "available" },
{ rev: "1-root", status: "missing" },
],
};
});
const info = await inspectFileDatabaseInfo(core as never, "note.md");
expect(info.database.mergeBases).toEqual([
{
winnerRevision: "3-current",
conflictRevision: "2-conflict",
revision: "2-parent",
metadataAvailableLocally: true,
contentAvailableLocally: false,
missingChunkIds: ["h:missing-parent"],
unavailableSharedRevisions: ["1-root"],
},
]);
});
it("does not decode a revision whose chunks are not all available locally", async () => {
const { core } = createCore();
await expect(readFileDatabaseRevisionLocally(core as never, "note.md", "3-current")).resolves.toBe(false);
expect(core.localDatabase.getDBEntryFromMeta).not.toHaveBeenCalled();
});
it("decodes an exact revision after confirming that every chunk is available locally", async () => {
const { core, current } = createCore();
core.localDatabase.localDatabase.get.mockResolvedValue({
...current,
children: ["h:available"],
eden: {},
} as never);
core.localDatabase.allDocsRaw.mockResolvedValue({
rows: [
{
id: "h:available",
key: "h:available",
value: { rev: "1-available" },
},
],
});
await expect(readFileDatabaseRevisionLocally(core as never, "note.md", "3-current")).resolves.toEqual(
expect.objectContaining({
data: ["loaded body"],
})
);
expect(core.localDatabase.getDBEntryFromMeta).toHaveBeenCalledWith(
expect.objectContaining({
_rev: "3-current",
}),
false,
false
);
});
it("retries an exact revision through the configured chunk retrieval path", async () => {
const { core } = createCore();
await retryReadFileDatabaseRevision(core as never, "note.md", "2-conflict");
expect(core.localDatabase.getDBEntry).toHaveBeenCalledWith(
"note.md",
{ rev: "2-conflict" },
false,
true,
true
);
});
it("reports the exact revision as locally available after retry recovers its missing chunk", async () => {
const { conflict, core } = createCore();
let recovered = false;
core.localDatabase.getDBEntry.mockImplementation(async () => {
recovered = true;
return conflict as never;
});
core.localDatabase.allDocsRaw.mockImplementation(async ({ keys }: { keys: string[] }) => ({
rows:
recovered && keys.includes("h:private-missing")
? [
{
id: "h:private-missing",
key: "h:private-missing",
value: { rev: "1-recovered" },
},
]
: [],
}));
await expect(
retryReadFileDatabaseRevision(core as never, "note.md", "2-conflict")
).resolves.not.toBe(false);
const information = await inspectFileDatabaseInfo(core as never, "note.md");
expect(
information.database.revisions.find(({ revision }) => revision === "2-conflict")
).toEqual(
expect.objectContaining({
contentAvailableLocally: true,
chunks: [
expect.objectContaining({
id: "h:private-missing",
localDatabaseState: "available",
localDatabaseRevision: "1-recovered",
}),
],
})
);
});
it("keeps the exact revision identifiers when conflict metadata is unavailable", async () => {
const { conflict, core, current } = createCore();
core.localDatabase.localDatabase.get.mockImplementation(async (_id: string, options?: { rev?: string }) => {
if (options?.rev === "2-unavailable") {
throw Object.assign(new Error("missing"), { status: 404 });
}
if (options?.rev === "2-conflict") {
return conflict;
}
return { ...current, _conflicts: ["2-conflict", "2-unavailable"] };
});
const report = await buildFileDatabaseInfoReport(core as never, "note.md");
expect(report).toContain('"conflictRevisions"');
expect(report).toContain('"2-conflict"');
expect(report).toContain('"2-unavailable"');
expect(report).toContain('"unavailableConflictRevisions"');
});
it("reads an existing local document even when current synchronisation filters exclude its path", async () => {
const { core, current } = createCore();
core.services.path.path2id.mockResolvedValue("f:ignored");
core.localDatabase.localDatabase.get.mockResolvedValue({
...current,
_id: "f:ignored",
_rev: "5-ignored",
_conflicts: [],
_revs_info: [],
path: "ignored.md",
ctime: 10,
mtime: 20,
size: 30,
children: [],
});
const report = await buildFileDatabaseInfoReport(core as never, "ignored.md");
expect(report).toContain('"exists": true');
expect(report).toContain('"documentId": "f:ignored"');
expect(report).toContain('"revision": "5-ignored"');
});
it("offers the union of storage and database paths and excludes inactive internal namespaces", async () => {
const { core } = createCore();
await expect(collectFileDatabaseInfoPaths(core as never)).resolves.toEqual(["a.md", "db-only.md", "z.md"]);
core.settings.syncInternalFiles = true;
await expect(collectFileDatabaseInfoPaths(core as never)).resolves.toEqual([
".obsidian/app.json",
"a.md",
"db-only.md",
]);
});
it("copies the selected file report through the existing copy dialogue", async () => {
const { askSelectString, core, promptCopyToClipboard } = createCore();
await expect(chooseAndCopyFileDatabaseInfo(core as never)).resolves.toBe(true);
expect(askSelectString).toHaveBeenCalledWith("Choose a file to inspect", ["a.md", "db-only.md", "z.md"]);
expect(promptCopyToClipboard).toHaveBeenCalledWith(
"Database information for db-only.md",
expect.stringContaining('"path": "db-only.md"')
);
});
});