mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-11-05 07:48:18 +00:00
Test conflict shutdown and Legacy settings saves
This commit is contained in:
1 parent
9920da8f94
commit
f015e11957
2 files changed
+167
No files matched your search
+103
@@ -452,6 +452,109 @@ describe("interactive conflict resolution operations", () => {
|
||||
await expect(messages).resolves.toEqual([]);
|
||||
});
|
||||
|
||||
it("does not inspect or publish conflict state when called after disposal", async () => {
|
||||
const fixture = createOperations();
|
||||
const emit = vi.spyOn(fixture.context.events, "emitEvent");
|
||||
fixture.operations.dispose();
|
||||
emit.mockClear();
|
||||
|
||||
await expect(fixture.operations.getActiveConflictMessages()).resolves.toEqual([]);
|
||||
await fixture.operations.refreshConflictState(path);
|
||||
|
||||
expect(fixture.getConflictedRevs).not.toHaveBeenCalled();
|
||||
expect(fixture.getActiveFilePath).not.toHaveBeenCalled();
|
||||
expect(fixture.log).not.toHaveBeenCalled();
|
||||
expect(emit).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not requeue a changed comparison when its conflict refresh finishes after disposal", async () => {
|
||||
let finishInspection!: (revisions: string[]) => void;
|
||||
const pendingInspection = new Promise<string[]>((resolve) => {
|
||||
finishInspection = resolve;
|
||||
});
|
||||
const sink = vi.fn();
|
||||
const addLog = bindableFunction<(message: unknown) => void>("addLog");
|
||||
const unregisterLog = addLog.setHandler(sink);
|
||||
const fixture = createOperations(undefined, {
|
||||
log: createInstanceLogFunction("Conflict shutdown", { addLog } as never),
|
||||
});
|
||||
fixture.dialogueResult.value = "2-right";
|
||||
fixture.getDBEntry.mockResolvedValue({ _rev: "3-new-left", _conflicts: ["2-right"] });
|
||||
fixture.getConflictedRevs.mockReturnValue(pendingInspection);
|
||||
const emit = vi.spyOn(fixture.context.events, "emitEvent");
|
||||
|
||||
const resolving = fixture.operations.resolveByUserInteraction(path, conflict);
|
||||
await vi.waitFor(() => expect(fixture.getConflictedRevs).toHaveBeenCalledOnce());
|
||||
const logsBeforeDisposal = sink.mock.calls.length;
|
||||
fixture.operations.dispose();
|
||||
unregisterLog();
|
||||
emit.mockClear();
|
||||
finishInspection(["2-right"]);
|
||||
|
||||
await expect(resolving).resolves.toBe(false);
|
||||
expect(sink).toHaveBeenCalledTimes(logsBeforeDisposal);
|
||||
expect(emit).not.toHaveBeenCalled();
|
||||
expect(fixture.storeContent).not.toHaveBeenCalled();
|
||||
expect(fixture.resolveByDeletingRevision).not.toHaveBeenCalled();
|
||||
expect(fixture.queueCheckFor).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it.each([AUTO_MERGED, MISSING_OR_ERROR] as const)(
|
||||
"does not continue concatenated resolution after deletion settles following disposal (%s)",
|
||||
async (result) => {
|
||||
let finishDeletion!: (value: typeof AUTO_MERGED | typeof MISSING_OR_ERROR) => void;
|
||||
const pendingDeletion = new Promise<typeof AUTO_MERGED | typeof MISSING_OR_ERROR>((resolve) => {
|
||||
finishDeletion = resolve;
|
||||
});
|
||||
const sink = vi.fn();
|
||||
const addLog = bindableFunction<(message: unknown) => void>("addLog");
|
||||
const unregisterLog = addLog.setHandler(sink);
|
||||
const fixture = createOperations(undefined, {
|
||||
log: createInstanceLogFunction("Conflict shutdown", { addLog } as never),
|
||||
});
|
||||
fixture.dialogueResult.value = LEAVE_TO_SUBSEQUENT;
|
||||
fixture.getDBEntry.mockResolvedValue({ _rev: "2-left", _conflicts: ["2-right"] });
|
||||
fixture.resolveByDeletingRevision.mockReturnValue(pendingDeletion);
|
||||
fixture.currentSettings.mockReturnValue({ syncAfterMerge: true });
|
||||
|
||||
const resolving = fixture.operations.resolveByUserInteraction(path, conflict);
|
||||
await vi.waitFor(() => expect(fixture.resolveByDeletingRevision).toHaveBeenCalledOnce());
|
||||
expect(fixture.storeContent).toHaveBeenCalledOnce();
|
||||
const logsBeforeDisposal = sink.mock.calls.length;
|
||||
fixture.operations.dispose();
|
||||
unregisterLog();
|
||||
finishDeletion(result);
|
||||
|
||||
await expect(resolving).resolves.toBe(false);
|
||||
expect(sink).toHaveBeenCalledTimes(logsBeforeDisposal);
|
||||
expect(fixture.resolveByDeletingRevision).toHaveBeenCalledTimes(1);
|
||||
expect(fixture.replicateUnattendedByEvent).not.toHaveBeenCalled();
|
||||
expect(fixture.queueCheckFor).not.toHaveBeenCalled();
|
||||
}
|
||||
);
|
||||
|
||||
it("does not requeue after in-flight replication completes following disposal", async () => {
|
||||
let finishReplication!: (result: { status: "completed" }) => void;
|
||||
const pendingReplication = new Promise<{ status: "completed" }>((resolve) => {
|
||||
finishReplication = resolve;
|
||||
});
|
||||
const fixture = createOperations();
|
||||
fixture.dialogueResult.value = "2-right";
|
||||
fixture.getDBEntry.mockResolvedValue({ _rev: "2-left", _conflicts: ["2-right"] });
|
||||
fixture.currentSettings.mockReturnValue({ syncAfterMerge: true });
|
||||
fixture.replicateUnattendedByEvent.mockReturnValue(pendingReplication);
|
||||
|
||||
const resolving = fixture.operations.resolveByUserInteraction(path, conflict);
|
||||
await vi.waitFor(() => expect(fixture.replicateUnattendedByEvent).toHaveBeenCalledOnce());
|
||||
fixture.operations.dispose();
|
||||
finishReplication({ status: "completed" });
|
||||
|
||||
await expect(resolving).resolves.toBe(false);
|
||||
expect(fixture.resolveByDeletingRevision).toHaveBeenCalledOnce();
|
||||
expect(fixture.replicateUnattendedByEvent).toHaveBeenCalledTimes(1);
|
||||
expect(fixture.queueCheckFor).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("replaces an active same-file dialogue with only the newest waiting request", async () => {
|
||||
const fixture = createDialogueConcurrencyHarness();
|
||||
|
||||
|
||||
@@ -237,6 +237,70 @@ describe("WebAppRuntime lifecycle", () => {
|
||||
expect(getDirectoryHandle).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it.each([undefined, 0] as const)("saves supported Legacy settings without adopting NFC (%s)", async (version) => {
|
||||
const write = vi.fn(async (_data: string) => undefined);
|
||||
const close = vi.fn(async () => undefined);
|
||||
const getFileHandle = vi.fn(async () => ({ createWritable: async () => ({ write, close }) }));
|
||||
const getDirectoryHandle = vi.fn(async () => ({ getFileHandle }));
|
||||
const runtime = new WebAppRuntime(
|
||||
{ name: "legacy-settings", getDirectoryHandle } as unknown as FileSystemDirectoryHandle,
|
||||
{ deviceValues: new MemoryValueStore() }
|
||||
);
|
||||
const options = (
|
||||
runtime as unknown as { createServiceOptions(): LiveSyncBrowserServiceHubOptions<never> }
|
||||
).createServiceOptions();
|
||||
if (!options.settings) throw new Error("The runtime must provide settings callbacks");
|
||||
const settings = {
|
||||
...unconfiguredSettings,
|
||||
...(version === undefined ? {} : { pathNormalizationVersion: version }),
|
||||
} as ObsidianLiveSyncSettings;
|
||||
|
||||
await options.settings.save(settings);
|
||||
|
||||
expect(getDirectoryHandle).toHaveBeenCalledWith(".livesync", { create: true });
|
||||
expect(getFileHandle).toHaveBeenCalledWith("settings.json", { create: true });
|
||||
expect(write).toHaveBeenCalledOnce();
|
||||
expect(JSON.parse(write.mock.calls[0][0])).toEqual(settings);
|
||||
expect(JSON.parse(write.mock.calls[0][0]).pathNormalizationVersion).toBe(version);
|
||||
expect(close).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("reports a Legacy settings write failure without logging a successful save", async () => {
|
||||
const denied = new DOMException("Settings write denied", "NotAllowedError");
|
||||
const write = vi.fn(async () => {
|
||||
throw denied;
|
||||
});
|
||||
const close = vi.fn(async () => undefined);
|
||||
const runtime = new WebAppRuntime(
|
||||
{
|
||||
name: "denied-settings",
|
||||
getDirectoryHandle: async () => ({
|
||||
getFileHandle: async () => ({ createWritable: async () => ({ write, close }) }),
|
||||
}),
|
||||
} as unknown as FileSystemDirectoryHandle,
|
||||
{ deviceValues: new MemoryValueStore() }
|
||||
);
|
||||
const options = (
|
||||
runtime as unknown as { createServiceOptions(): LiveSyncBrowserServiceHubOptions<never> }
|
||||
).createServiceOptions();
|
||||
if (!options.settings) throw new Error("The runtime must provide settings callbacks");
|
||||
const settings = { ...unconfiguredSettings, pathNormalizationVersion: 0 } as ObsidianLiveSyncSettings;
|
||||
await runtime.start();
|
||||
runtimeMocks.addLog.mockClear();
|
||||
|
||||
await expect(options.settings.save(settings)).resolves.toBeUndefined();
|
||||
|
||||
expect(write).toHaveBeenCalledOnce();
|
||||
expect(close).not.toHaveBeenCalled();
|
||||
expect(runtimeMocks.addLog).toHaveBeenCalledWith(
|
||||
expect.stringContaining("Failed to save settings: NotAllowedError: Settings write denied"),
|
||||
expect.any(Number),
|
||||
"settings"
|
||||
);
|
||||
expect(runtimeMocks.addLog.mock.calls.some(([message]) => String(message).startsWith("Saved to "))).toBe(false);
|
||||
expect(settings.pathNormalizationVersion).toBe(0);
|
||||
});
|
||||
|
||||
it("owns core start, directory scan, status reporting, and shutdown", async () => {
|
||||
const reportStatus = vi.fn();
|
||||
const runtime = new WebAppRuntime(createRootHandle(), { reportStatus, deviceValues: new MemoryValueStore() });
|
||||
|
||||
Reference in new issue
Block a user