Compare commits

...
Author SHA1 Message Date
vorotamoroz e018cab039 Warn when live Vault reflection fails 2026-09-05 02:10:52 +00:00
vorotamoroz 5d251d1f92 Merge pull request #1171 from vrtmrz/test/partial-startup-file-failure-e2e
test: cover partial start-up file failures in real Obsidian
2026-09-05 03:13:18 +09:00
3 changed files with 77 additions and 11 deletions
@@ -25,6 +25,7 @@ import type { LiveSyncBaseCore } from "@/LiveSyncBaseCore";
import { isNotFoundError } from "@vrtmrz/livesync-commonlib/compat/common/utils.doc";
import type PouchDB from "pouchdb-core";
import { promiseWithResolvers, type PromiseWithResolvers } from "octagonal-wheels/promises";
import { $msg } from "@/common/translation";
const KV_KEY_REPLICATION_RESULT_PROCESSOR_SNAPSHOT = "replicationResultProcessorSnapshot";
const REPROCESS_BATCH_SIZE = 100;
@@ -79,6 +80,14 @@ export class ReplicateResultProcessor {
private logError(e: unknown) {
Logger(e, LOG_LEVEL_VERBOSE);
}
private reportVaultReflectionFailure(entry: MetaEntry, cause?: unknown) {
this.log(
`Live replication could not reflect ${this.getPath(entry)} from the local database to the Vault; this path remains eligible for a later Vault scan.`,
LOG_LEVEL_VERBOSE
);
if (cause !== undefined) this.logError(cause);
Logger($msg("Ui.Common.SomeFilesCouldNotBeSynchronised"), LOG_LEVEL_NOTICE);
}
constructor(private readonly context: ReplicateResultProcessorContext) {}
private get localDatabase() {
@@ -510,8 +519,16 @@ export class ReplicateResultProcessor {
this.log(`Processed by other processor: ${docNote}`, LOG_LEVEL_DEBUG);
} else if (this.services.vault.isValidPath(this.getPath(doc))) {
// Apply to storage if the path is valid
await this.applyToStorage(doc as MetaEntry);
this.log(`Processed: ${docNote}`, LOG_LEVEL_DEBUG);
try {
const reflected = await this.applyToStorage(doc as MetaEntry);
if (!reflected) {
this.reportVaultReflectionFailure(doc as MetaEntry);
return;
}
this.log(`Processed: ${docNote}`, LOG_LEVEL_DEBUG);
} catch (error) {
this.reportVaultReflectionFailure(doc as MetaEntry, error);
}
} else {
// Should process, but have an invalid path
this.log(`Unprocessed (Invalid path): ${docNote}`, LOG_LEVEL_VERBOSE);
@@ -525,9 +542,10 @@ export class ReplicateResultProcessor {
* @returns
*/
protected applyToStorage(entry: MetaEntry) {
return this.withCounting(async () => {
await this.services.replication.processSynchroniseResult(entry);
}, this.services.replication.storageApplyingCount);
return this.withCounting(
() => this.services.replication.processSynchroniseResult(entry),
this.services.replication.storageApplyingCount
);
}
/**
@@ -2,6 +2,13 @@ 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 {
defaultLogger,
LOG_LEVEL_DEBUG,
LOG_LEVEL_NOTICE,
LOG_LEVEL_VERBOSE,
setGlobalLogFunction,
} from "octagonal-wheels/common/logger";
import { ReplicateResultProcessor } from "./ReplicateResultProcessor";
function note(id: string): PouchDB.Core.ExistingDocument<EntryDoc> {
@@ -21,12 +28,12 @@ function note(id: string): PouchDB.Core.ExistingDocument<EntryDoc> {
type SetupOptions = {
applicationReady?: boolean;
processSynchroniseResult?: (entry: unknown) => Promise<void>;
processSynchroniseResult?: (entry: unknown) => Promise<boolean>;
setSnapshot?: (key: string, value: unknown) => Promise<unknown>;
};
function setup(options: SetupOptions = {}) {
const processSynchroniseResult = vi.fn(options.processSynchroniseResult ?? (async () => undefined));
const processSynchroniseResult = vi.fn(options.processSynchroniseResult ?? (async () => true));
const setSnapshot = vi.fn(options.setSnapshot ?? (async () => undefined));
const runBoundedLocalApplicationActivity = vi.fn(async (task: () => Promise<void>) => await task());
const onCloseActiveReplication = vi.fn(async () => true);
@@ -120,7 +127,7 @@ describe("ReplicateResultProcessor", () => {
});
it("keeps one local application activity until every replicated document has been applied", async () => {
const applying = promiseWithResolvers<void>();
const applying = promiseWithResolvers<boolean>();
let activityFinished = false;
const { processor, processSynchroniseResult, runBoundedLocalApplicationActivity } = setup({
processSynchroniseResult: async () => applying.promise,
@@ -139,7 +146,7 @@ describe("ReplicateResultProcessor", () => {
});
expect(activityFinished).toBe(false);
applying.resolve();
applying.resolve(true);
await vi.waitFor(() => expect(activityFinished).toBe(true));
});
@@ -160,7 +167,7 @@ describe("ReplicateResultProcessor", () => {
});
it("releases and reacquires local application activity around processing suspension", async () => {
const applying = promiseWithResolvers<void>();
const applying = promiseWithResolvers<boolean>();
let completedActivities = 0;
const { processor, processSynchroniseResult, runBoundedLocalApplicationActivity } = setup({
processSynchroniseResult: async () => applying.promise,
@@ -178,7 +185,47 @@ describe("ReplicateResultProcessor", () => {
processor.resume();
await vi.waitFor(() => expect(runBoundedLocalApplicationActivity).toHaveBeenCalledTimes(2));
applying.resolve();
applying.resolve(true);
await vi.waitFor(() => expect(completedActivities).toBe(2));
});
it.each([
["returns false", async () => false, undefined],
["throws", async () => Promise.reject(new Error("File name too long")), "File name too long"],
])("reports when Vault reflection %s", async (_description, processSynchroniseResult, errorMessage) => {
const log = vi.fn((_message: unknown, _level?: number) => undefined);
setGlobalLogFunction(log);
try {
const { processor } = setup({ processSynchroniseResult });
processor.enqueueAll([note("unreflectable")]);
await vi.waitFor(() =>
expect(log).toHaveBeenCalledWith(
"Not all files could be synchronised. Check the affected files. Generate a report to review the detailed log.",
LOG_LEVEL_NOTICE,
undefined
)
);
expect(log).toHaveBeenCalledWith(
"[ReplicateResultProcessor] Live replication could not reflect unreflectable.md from the local database to the Vault; this path remains eligible for a later Vault scan.",
LOG_LEVEL_VERBOSE,
undefined
);
if (errorMessage !== undefined) {
expect(log).toHaveBeenCalledWith(
expect.objectContaining({ message: errorMessage }),
LOG_LEVEL_VERBOSE,
undefined
);
}
expect(log).not.toHaveBeenCalledWith(
expect.stringContaining("Processed: unreflectable.md"),
LOG_LEVEL_DEBUG,
undefined
);
} finally {
setGlobalLogFunction(defaultLogger);
}
});
});
+1
View File
@@ -18,6 +18,7 @@ Earlier releases remain available in the 1.0 release history, the 1.0 preview hi
- Conflict resolution dialogues now close when the same file is resolved elsewhere or the plug-in unloads. Requests for different files are shown one at a time, while a newer request for the same file replaces the stale dialogue.
- An individual file-processing failure during ordinary start-up no longer keeps the entire application unready. A start-up notice asks the user to check the affected files and generate a report for details; each path is recorded in verbose logs and remains eligible for retry, while explicit Fetch and Rebuild operations retain strict completion.
- When a replicated file cannot be written to the Vault, LiveSync now warns immediately instead of appearing to have processed it successfully. The affected path is recorded in the report and remains eligible for a later scan. (#1164)
- Replication readiness diagnostics now state that application initialisation is incomplete instead of reporting only 'Not ready'. Database-preparation failures show a short notice, with the failed stage available in verbose logs.
#### Improved