mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-10-02 07:22:31 +00:00
414 lines
15 KiB
TypeScript
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"')
|
|
);
|
|
});
|
|
});
|