Polish diagnostic reporting and local pseudonym follow-up

This commit is contained in:
vorotamoroz committed 2026-10-20 08:23:56 +00:00
1 parent c1ed71f686
commit 32874ea97f
21 files changed
+2425 -154

No files matched your search

@@ -1,12 +1,31 @@
import { describe, expect, it, vi } from "vitest";
import { DEFAULT_SETTINGS, REMOTE_P2P } from "@vrtmrz/livesync-commonlib/compat/common/types";
import {
DEFAULT_SETTINGS,
REMOTE_COUCHDB,
REMOTE_P2P,
type ObsidianLiveSyncSettings,
} from "@vrtmrz/livesync-commonlib/compat/common/types";
const diagnosticsMocks = vi.hoisted(() => ({ analyseOptions: vi.fn() }));
vi.mock("@vrtmrz/livesync-commonlib/diagnostics", async (importOriginal) => {
const actual = await importOriginal<typeof import("@vrtmrz/livesync-commonlib/diagnostics")>();
return {
...actual,
analyseDatabaseIntegrity: (options: Parameters<typeof actual.analyseDatabaseIntegrity>[0]) => {
diagnosticsMocks.analyseOptions(options);
return actual.analyseDatabaseIntegrity(options);
},
};
});
import { readDiagnosticStorage, runDatabaseDiagnostic, type DatabaseDiagnosticHost } from "./databaseDiagnostics";
function fixture() {
const path = "Private/日本語.md";
const body = "private file contents";
const bytes = new TextEncoder().encode(body);
const settings = {
const settings: ObsidianLiveSyncSettings = {
...DEFAULT_SETTINGS,
remoteType: REMOTE_P2P,
readChunksOnline: true,
@@ -81,6 +100,41 @@ function fixture() {
}
describe("database diagnostic host composition", () => {
it("passes growing Usage totals and the separate accounting phase to the dialogue", async () => {
const onProgress = vi.fn();
await runDatabaseDiagnostic(fixture().host, "usage", "pseudonym", { onProgress });
const progress = onProgress.mock.calls.map(([value]) => value);
expect(progress).toContainEqual(
expect.objectContaining({
phase: "usage",
processed: 0,
total: 0,
totalIsFinal: false,
})
);
expect(progress).toContainEqual(
expect.objectContaining({
phase: "usage",
processed: 2,
total: 2,
totalIsFinal: true,
})
);
const accounting = progress.filter((value) => value.phase === "accounting");
expect(accounting.length).toBeGreaterThan(1);
const final = accounting[accounting.length - 1];
expect(final).toMatchObject({ processed: final.total, totalIsFinal: true });
});
it("preserves phase, source, and totals for the inspection dialogue", async () => {
const onProgress = vi.fn();
await runDatabaseDiagnostic(fixture().host, "integrity", "pseudonym", { onProgress });
expect(onProgress).toHaveBeenCalledWith(
expect.objectContaining({ phase: "metadata", source: "local", processed: 0 })
);
expect(onProgress).toHaveBeenCalledWith(expect.objectContaining({ phase: "content", processed: 1, total: 1 }));
});
it("keeps on-demand fetching enabled while analysing only existing local revisions", async () => {
const data = fixture();
const before = JSON.stringify(data.settings);
@@ -113,6 +167,9 @@ describe("database diagnostic host composition", () => {
it("keeps local findings when the remote type is unsupported and projects both formats consistently", async () => {
const data = fixture();
const result = await runDatabaseDiagnostic(data.host, "integrity", "pseudonym");
expect(diagnosticsMocks.analyseOptions).toHaveBeenLastCalledWith(
expect.objectContaining({ includeRemote: true })
);
expect(result.status).toBe("partial");
expect(data.fetch).not.toHaveBeenCalled();
expect(data.put).not.toHaveBeenCalled();
@@ -135,6 +192,82 @@ describe("database diagnostic host composition", () => {
}
});
it("excludes the remote source for a local integrity run", async () => {
const data = fixture();
data.settings.remoteType = REMOTE_COUCHDB;
data.settings.couchDB_DBNAME = "private-database";
data.settings.useRequestAPI = true;
data.fetch
.mockImplementationOnce(async () => {
await new Promise((resolve) => setTimeout(resolve, 2));
return { ok: false, status: 503 } as Response;
})
.mockRejectedValue(new Error("simulated diagnostic request failure"));
diagnosticsMocks.analyseOptions.mockClear();
const result = await runDatabaseDiagnostic(data.host, "integrity", "pseudonym", {
includeRemote: false,
});
expect(data.fetch).not.toHaveBeenCalled();
const passedOptions = diagnosticsMocks.analyseOptions.mock.calls[0][0];
expect(passedOptions.includeRemote).toBe(false);
expect(passedOptions.remote).toBeUndefined();
expect(result.artifacts.map((artifact) => artifact.format)).toEqual(["markdown", "json"]);
const report = JSON.parse(result.artifacts.find((artifact) => artifact.format === "json")!.content);
expect(report.scope.remote).toBe("excluded");
expect(report.coverage.remote.status).toBe("not-checked");
expect(report.errors.some((error: { source: string }) => error.source === "remote")).toBe(false);
});
it("logs cumulative HTTP dispatch, failure, and duration counters for remote inspection", async () => {
const log = vi.spyOn(console, "log").mockImplementation(() => {});
try {
const nonOk = fixture();
nonOk.settings.remoteType = REMOTE_COUCHDB;
nonOk.settings.couchDB_DBNAME = "private-database";
nonOk.settings.useRequestAPI = true;
nonOk.fetch.mockImplementation(async () => {
await new Promise((resolve) => setTimeout(resolve, 2));
return { ok: false, status: 503 } as Response;
});
await runDatabaseDiagnostic(nonOk.host, "integrity", "pseudonym");
const thrown = fixture();
thrown.settings.remoteType = REMOTE_COUCHDB;
thrown.settings.couchDB_DBNAME = "private-database";
thrown.settings.useRequestAPI = true;
thrown.fetch.mockRejectedValue(new Error("simulated diagnostic request failure"));
await runDatabaseDiagnostic(thrown.host, "integrity", "pseudonym");
const messages = log.mock.calls.map(([message]) => String(message));
const inspectionLogs = messages.filter((message) => message.includes("Database integrity inspection:"));
expect(inspectionLogs.length).toBeGreaterThan(0);
const inspectionEvents = inspectionLogs.map((message) => {
const json = message.slice(message.indexOf("{"));
return JSON.parse(json);
});
expect(inspectionEvents.some((event) => event.httpRequests > 0)).toBe(true);
expect(inspectionEvents.some((event) => event.httpFailedRequests > 0)).toBe(true);
expect(inspectionEvents.every((event) => typeof event.httpRequestDurationMs === "number")).toBe(true);
const summaries = messages.filter((message) => message.includes("Database integrity report:"));
expect(summaries).toHaveLength(2);
for (const [index, summary] of summaries.entries()) {
const match = /httpRequests=(\d+), httpFailedRequests=(\d+), httpRequestDurationMs=(\d+)/.exec(summary);
expect(match).not.toBeNull();
const data = index === 0 ? nonOk : thrown;
expect(Number(match![1])).toBe(data.fetch.mock.calls.length);
expect(Number(match![2])).toBe(data.fetch.mock.calls.length);
expect(Number(match![3])).toBeGreaterThanOrEqual(0);
if (index === 0) expect(Number(match![3])).toBeGreaterThan(0);
}
} finally {
log.mockRestore();
}
});
it("includes names only when requested and never includes credentials or file contents", async () => {
const data = fixture();
const result = await runDatabaseDiagnostic(data.host, "usage", "original");
@@ -172,6 +305,17 @@ describe("database diagnostic host composition", () => {
});
describe("diagnostic Vault reads", () => {
it("reuses the validated snapshot timestamp without adding stat or content reads", async () => {
const data = fixture();
expect(await readDiagnosticStorage(data.storage, data.path)).toEqual({
status: "available",
bytes: data.bytes,
mtime: 1,
});
expect(data.storage.statHidden).toHaveBeenCalledTimes(2);
expect(data.storage.readHiddenFileBinary).toHaveBeenCalledTimes(1);
});
it("distinguishes an absent file, a read error, and a file changing during its read", async () => {
const data = fixture();
data.storage.statHidden.mockResolvedValueOnce(null as never);