mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-11-04 07:18:18 +00:00
Confirm Fast Setup choices from the scheduled fetch flag
This commit is contained in:
1 parent
efa3e356b4
commit
9632f2ae5a
12 files changed
+1440
-150
No files matched your search
@@ -1,3 +1,6 @@
|
||||
import { MemoryValueStore } from "@vrtmrz/value-store";
|
||||
import { SettingService } from "@vrtmrz/livesync-commonlib/compat/services/base/SettingService";
|
||||
import { ServiceRebuilder } from "@vrtmrz/livesync-commonlib/compat/serviceModules/Rebuilder";
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { createServiceContext } from "@vrtmrz/livesync-commonlib/context";
|
||||
import type { LogFunction } from "@vrtmrz/livesync-commonlib/compat/services/lib/logUtils";
|
||||
@@ -74,12 +77,24 @@ const availableRemoteTweaks = (values: Record<string, unknown>) => ({ status: "a
|
||||
|
||||
const createStorageAccessMock = () => {
|
||||
const files: Set<string> = new Set();
|
||||
const contents = new Map<string, string>();
|
||||
return {
|
||||
files,
|
||||
contents,
|
||||
readFileText: vi.fn(async (path: string) => {
|
||||
if (!files.has(path)) throw new Error("Missing flag");
|
||||
return contents.get(path) ?? "";
|
||||
}),
|
||||
writeFileAuto: vi.fn(async (path: string, text: string) => {
|
||||
files.add(path);
|
||||
contents.set(path, text);
|
||||
return true;
|
||||
}),
|
||||
isExists: vi.fn((path: string) => Promise.resolve(files.has(path))),
|
||||
normalisePath: vi.fn((path: string) => path),
|
||||
delete: vi.fn((path: string, _recursive?: boolean) => {
|
||||
files.delete(path);
|
||||
contents.delete(path);
|
||||
return Promise.resolve();
|
||||
}),
|
||||
getFileNames: vi.fn(() => Array.from(files)),
|
||||
@@ -88,10 +103,13 @@ const createStorageAccessMock = () => {
|
||||
|
||||
const createSettingServiceMock = () => {
|
||||
const settings: any = {
|
||||
...DEFAULT_SETTINGS,
|
||||
couchDB_URI: "https://example.invalid",
|
||||
couchDB_DBNAME: "vault",
|
||||
batchSave: true,
|
||||
suspendFileWatching: false,
|
||||
writeLogToTheFile: false,
|
||||
remoteType: "CouchDB",
|
||||
remoteType: "",
|
||||
};
|
||||
const smallConfig = new Map<string, string>();
|
||||
return {
|
||||
@@ -149,7 +167,7 @@ const createRebuilderMock = () => {
|
||||
$fetchLocal: vi.fn(async () => {}),
|
||||
$fetchLocalDBFast: vi.fn(async () => {}),
|
||||
$rebuildEverything: vi.fn(async () => {}),
|
||||
finishRebuild: vi.fn(async () => {}),
|
||||
finishRebuild: vi.fn(async () => true),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -161,8 +179,9 @@ const createTweakValueMock = () => {
|
||||
};
|
||||
};
|
||||
|
||||
const createHostMock = () => {
|
||||
const createHostMock = (withFetchFlag = false) => {
|
||||
const storageAccessMock = createStorageAccessMock();
|
||||
if (withFetchFlag) storageAccessMock.files.add(FlagFilesHumanReadable.FETCH_ALL);
|
||||
const settingMock = createSettingServiceMock();
|
||||
const appLifecycleMock = createAppLifecycleMock();
|
||||
const uiMock = createUIServiceMock();
|
||||
@@ -677,21 +696,21 @@ describe("Red Flag Feature", () => {
|
||||
|
||||
describe("askSimpleFetchMode", () => {
|
||||
it("should return cancelled when stage1 is cancelled", async () => {
|
||||
const host = createHostMock();
|
||||
const host = createHostMock(true);
|
||||
host.mocks.ui.confirm.confirmWithMessage.mockResolvedValueOnce(false);
|
||||
|
||||
await expect(askSimpleFetchMode(host as any)).resolves.toBe("cancelled");
|
||||
});
|
||||
|
||||
it("selects the detailed Fetch flow", async () => {
|
||||
const host = createHostMock();
|
||||
const host = createHostMock(true);
|
||||
host.mocks.ui.confirm.confirmWithMessage.mockResolvedValueOnce(SIMPLE_FETCH_STAGE1_DETAILED);
|
||||
|
||||
await expect(askSimpleFetchMode(host as any)).resolves.toEqual({ mode: "detailed", options: {} });
|
||||
});
|
||||
|
||||
it("should return remote-only with keep-local option", async () => {
|
||||
const host = createHostMock();
|
||||
const host = createHostMock(true);
|
||||
host.mocks.ui.confirm.confirmWithMessage
|
||||
.mockResolvedValueOnce(SIMPLE_FETCH_STAGE1_REMOTE_WINS)
|
||||
.mockResolvedValueOnce(SIMPLE_FETCH_STAGE2_REMOTE_DELETE_NONE);
|
||||
@@ -706,7 +725,7 @@ describe("Red Flag Feature", () => {
|
||||
});
|
||||
|
||||
it("should return cancelled when remote-only stage2 is cancelled", async () => {
|
||||
const host = createHostMock();
|
||||
const host = createHostMock(true);
|
||||
host.mocks.ui.confirm.confirmWithMessage
|
||||
.mockResolvedValueOnce(SIMPLE_FETCH_STAGE1_REMOTE_WINS)
|
||||
.mockResolvedValueOnce(false);
|
||||
@@ -715,7 +734,7 @@ describe("Red Flag Feature", () => {
|
||||
});
|
||||
|
||||
it("should return aborted when remote-only stage2 aborts", async () => {
|
||||
const host = createHostMock();
|
||||
const host = createHostMock(true);
|
||||
host.mocks.ui.confirm.confirmWithMessage
|
||||
.mockResolvedValueOnce(SIMPLE_FETCH_STAGE1_REMOTE_WINS)
|
||||
.mockResolvedValueOnce(STAGE2_ABORT);
|
||||
@@ -724,7 +743,7 @@ describe("Red Flag Feature", () => {
|
||||
});
|
||||
|
||||
it("should return newer-wins cleanup option", async () => {
|
||||
const host = createHostMock();
|
||||
const host = createHostMock(true);
|
||||
host.mocks.ui.confirm.confirmWithMessage
|
||||
.mockResolvedValueOnce(SIMPLE_FETCH_STAGE1_NEWER_WINS)
|
||||
.mockResolvedValueOnce(SIMPLE_FETCH_STAGE2_NEWER_CLEANUP);
|
||||
@@ -739,7 +758,7 @@ describe("Red Flag Feature", () => {
|
||||
});
|
||||
|
||||
it("should return newer-wins keep-all option", async () => {
|
||||
const host = createHostMock();
|
||||
const host = createHostMock(true);
|
||||
host.mocks.ui.confirm.confirmWithMessage
|
||||
.mockResolvedValueOnce(SIMPLE_FETCH_STAGE1_NEWER_WINS)
|
||||
.mockResolvedValueOnce(SIMPLE_FETCH_STAGE2_NEWER_SYNC_ALL);
|
||||
@@ -754,7 +773,7 @@ describe("Red Flag Feature", () => {
|
||||
});
|
||||
|
||||
it("should return cancelled when newer-wins stage2 is cancelled", async () => {
|
||||
const host = createHostMock();
|
||||
const host = createHostMock(true);
|
||||
host.mocks.ui.confirm.confirmWithMessage
|
||||
.mockResolvedValueOnce(SIMPLE_FETCH_STAGE1_NEWER_WINS)
|
||||
.mockResolvedValueOnce(false);
|
||||
@@ -763,7 +782,7 @@ describe("Red Flag Feature", () => {
|
||||
});
|
||||
|
||||
it("should return aborted when newer-wins stage2 aborts", async () => {
|
||||
const host = createHostMock();
|
||||
const host = createHostMock(true);
|
||||
host.mocks.ui.confirm.confirmWithMessage
|
||||
.mockResolvedValueOnce(SIMPLE_FETCH_STAGE1_NEWER_WINS)
|
||||
.mockResolvedValueOnce(STAGE2_ABORT);
|
||||
@@ -774,9 +793,11 @@ describe("Red Flag Feature", () => {
|
||||
|
||||
describe("askAndPerformFastSetupOnScheduledFetchAll", () => {
|
||||
it("uses the detailed flow instead of Simple Fetch while remediation mode is active", async () => {
|
||||
const host = createHostMock();
|
||||
const host = createHostMock(true);
|
||||
const log = createLoggerMock();
|
||||
const cleanupFlag = vi.fn().mockResolvedValue(undefined);
|
||||
const cleanupFlag = vi.fn(async () => {
|
||||
await host.mocks.storageAccess.delete(FlagFilesHumanReadable.FETCH_ALL);
|
||||
});
|
||||
|
||||
Object.assign(host.mocks.setting.settings, {
|
||||
maxMTimeForReflectEvents: Date.parse("2026-09-01T00:00:00Z"),
|
||||
@@ -787,14 +808,16 @@ describe("Red Flag Feature", () => {
|
||||
);
|
||||
|
||||
expect(host.mocks.ui.confirm.confirmWithMessage).not.toHaveBeenCalled();
|
||||
expect(host.mocks.setting.config.delete).toHaveBeenCalledWith("simple-fetch-mode");
|
||||
expect(host.mocks.setting.config.getValue).not.toHaveBeenCalled();
|
||||
expect(cleanupFlag).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("releases both reflection suspensions after Fast Setup succeeds", async () => {
|
||||
const host = createHostMock();
|
||||
const host = createHostMock(true);
|
||||
const log = createLoggerMock();
|
||||
const cleanupFlag = vi.fn().mockResolvedValue(undefined);
|
||||
const cleanupFlag = vi.fn(async () => {
|
||||
await host.mocks.storageAccess.delete(FlagFilesHumanReadable.FETCH_ALL);
|
||||
});
|
||||
|
||||
Object.assign(host.mocks.setting.settings, {
|
||||
doNotSuspendOnFetching: true,
|
||||
@@ -814,9 +837,11 @@ describe("Red Flag Feature", () => {
|
||||
});
|
||||
|
||||
it("keeps Vault reflection suspended and preserves recovery state when Fast Fetch fails", async () => {
|
||||
const host = createHostMock();
|
||||
const host = createHostMock(true);
|
||||
const log = createLoggerMock();
|
||||
const cleanupFlag = vi.fn().mockResolvedValue(undefined);
|
||||
const cleanupFlag = vi.fn(async () => {
|
||||
await host.mocks.storageAccess.delete(FlagFilesHumanReadable.FETCH_ALL);
|
||||
});
|
||||
|
||||
host.mocks.ui.confirm.confirmWithMessage
|
||||
.mockResolvedValueOnce(SIMPLE_FETCH_STAGE1_NEWER_WINS)
|
||||
@@ -832,13 +857,17 @@ describe("Red Flag Feature", () => {
|
||||
});
|
||||
expect(host.mocks.rebuilder.finishRebuild).not.toHaveBeenCalled();
|
||||
expect(cleanupFlag).not.toHaveBeenCalled();
|
||||
expect(host.mocks.setting.config.delete).not.toHaveBeenCalledWith("simple-fetch-mode");
|
||||
expect(host.mocks.storageAccess.contents.get(FlagFilesHumanReadable.FETCH_ALL)).toContain(
|
||||
"livesync-fast-setup"
|
||||
);
|
||||
});
|
||||
|
||||
it("re-suspends both reflection directions when finalisation fails after releasing them", async () => {
|
||||
const host = createHostMock();
|
||||
const host = createHostMock(true);
|
||||
const log = createLoggerMock();
|
||||
const cleanupFlag = vi.fn().mockResolvedValue(undefined);
|
||||
const cleanupFlag = vi.fn(async () => {
|
||||
await host.mocks.storageAccess.delete(FlagFilesHumanReadable.FETCH_ALL);
|
||||
});
|
||||
|
||||
host.mocks.ui.confirm.confirmWithMessage
|
||||
.mockResolvedValueOnce(SIMPLE_FETCH_STAGE1_NEWER_WINS)
|
||||
@@ -864,28 +893,33 @@ describe("Red Flag Feature", () => {
|
||||
expect(cleanupFlag).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should remember quick flow choices while the scheduled fetch is pending", async () => {
|
||||
const host = createHostMock();
|
||||
it("should confirm remembered quick flow choices while the scheduled fetch is pending", async () => {
|
||||
const host = createHostMock(true);
|
||||
const log = createLoggerMock();
|
||||
const cleanupFlag = vi.fn().mockResolvedValue(undefined);
|
||||
const cleanupFlag = vi.fn(async () => {
|
||||
await host.mocks.storageAccess.delete(FlagFilesHumanReadable.FETCH_ALL);
|
||||
});
|
||||
|
||||
host.mocks.ui.confirm.confirmWithMessage
|
||||
.mockResolvedValueOnce(SIMPLE_FETCH_STAGE1_NEWER_WINS)
|
||||
.mockResolvedValueOnce(SIMPLE_FETCH_STAGE2_NEWER_CLEANUP);
|
||||
.mockResolvedValueOnce(SIMPLE_FETCH_STAGE2_NEWER_CLEANUP)
|
||||
.mockResolvedValueOnce("Continue with these choices");
|
||||
host.mocks.tweakValue.fetchRemotePreferred.mockResolvedValue(availableRemoteTweaks({ batchSave: false }));
|
||||
host.mocks.rebuilder.$fetchLocalDBFast.mockRejectedValueOnce(new Error("offline"));
|
||||
|
||||
await askAndPerformFastSetupOnScheduledFetchAll(host as any, log, cleanupFlag);
|
||||
await askAndPerformFastSetupOnScheduledFetchAll(host as any, log, cleanupFlag);
|
||||
|
||||
expect(host.mocks.ui.confirm.confirmWithMessage).toHaveBeenCalledTimes(2);
|
||||
expect(host.mocks.ui.confirm.confirmWithMessage).toHaveBeenCalledTimes(3);
|
||||
expect(host.mocks.rebuilder.$fetchLocalDBFast).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("should clear remembered quick flow choices after completion", async () => {
|
||||
const host = createHostMock();
|
||||
const host = createHostMock(true);
|
||||
const log = createLoggerMock();
|
||||
const cleanupFlag = vi.fn().mockResolvedValue(undefined);
|
||||
const cleanupFlag = vi.fn(async () => {
|
||||
await host.mocks.storageAccess.delete(FlagFilesHumanReadable.FETCH_ALL);
|
||||
});
|
||||
|
||||
host.mocks.ui.confirm.confirmWithMessage
|
||||
.mockResolvedValueOnce(SIMPLE_FETCH_STAGE1_REMOTE_WINS)
|
||||
@@ -894,13 +928,15 @@ describe("Red Flag Feature", () => {
|
||||
|
||||
await askAndPerformFastSetupOnScheduledFetchAll(host as any, log, cleanupFlag);
|
||||
|
||||
expect(host.mocks.setting.config.delete).toHaveBeenCalledWith("simple-fetch-mode");
|
||||
expect(host.mocks.storageAccess.files.has(FlagFilesHumanReadable.FETCH_ALL)).toBe(false);
|
||||
});
|
||||
|
||||
it("should return false and cleanup when quick flow is cancelled", async () => {
|
||||
const host = createHostMock();
|
||||
const host = createHostMock(true);
|
||||
const log = createLoggerMock();
|
||||
const cleanupFlag = vi.fn().mockResolvedValue(undefined);
|
||||
const cleanupFlag = vi.fn(async () => {
|
||||
await host.mocks.storageAccess.delete(FlagFilesHumanReadable.FETCH_ALL);
|
||||
});
|
||||
|
||||
host.mocks.ui.confirm.confirmWithMessage.mockResolvedValueOnce(false);
|
||||
|
||||
@@ -912,9 +948,11 @@ describe("Red Flag Feature", () => {
|
||||
});
|
||||
|
||||
it("should return false without cleanup when quick flow is aborted", async () => {
|
||||
const host = createHostMock();
|
||||
const host = createHostMock(true);
|
||||
const log = createLoggerMock();
|
||||
const cleanupFlag = vi.fn().mockResolvedValue(undefined);
|
||||
const cleanupFlag = vi.fn(async () => {
|
||||
await host.mocks.storageAccess.delete(FlagFilesHumanReadable.FETCH_ALL);
|
||||
});
|
||||
|
||||
host.mocks.ui.confirm.confirmWithMessage
|
||||
.mockResolvedValueOnce(SIMPLE_FETCH_STAGE1_REMOTE_WINS)
|
||||
@@ -928,9 +966,11 @@ describe("Red Flag Feature", () => {
|
||||
});
|
||||
|
||||
it("leaves the detailed Fetch flow to its existing handler", async () => {
|
||||
const host = createHostMock();
|
||||
const host = createHostMock(true);
|
||||
const log = createLoggerMock();
|
||||
const cleanupFlag = vi.fn().mockResolvedValue(undefined);
|
||||
const cleanupFlag = vi.fn(async () => {
|
||||
await host.mocks.storageAccess.delete(FlagFilesHumanReadable.FETCH_ALL);
|
||||
});
|
||||
|
||||
host.mocks.ui.confirm.confirmWithMessage.mockResolvedValueOnce(SIMPLE_FETCH_STAGE1_DETAILED);
|
||||
|
||||
@@ -941,8 +981,10 @@ describe("Red Flag Feature", () => {
|
||||
});
|
||||
|
||||
it("should preserve automatic synchronisation choices and enter Scram when quick Fetch is cancelled", async () => {
|
||||
const host = createHostMock();
|
||||
const cleanupFlag = vi.fn().mockResolvedValue(undefined);
|
||||
const host = createHostMock(true);
|
||||
const cleanupFlag = vi.fn(async () => {
|
||||
await host.mocks.storageAccess.delete(FlagFilesHumanReadable.FETCH_ALL);
|
||||
});
|
||||
Object.assign(host.mocks.setting.settings, {
|
||||
liveSync: true,
|
||||
periodicReplication: true,
|
||||
@@ -1000,9 +1042,11 @@ describe("Red Flag Feature", () => {
|
||||
});
|
||||
|
||||
it("should reboot and return false when sync has failures and user chooses rerun", async () => {
|
||||
const host = createHostMock();
|
||||
const host = createHostMock(true);
|
||||
const log = createLoggerMock();
|
||||
const cleanupFlag = vi.fn().mockResolvedValue(undefined);
|
||||
const cleanupFlag = vi.fn(async () => {
|
||||
await host.mocks.storageAccess.delete(FlagFilesHumanReadable.FETCH_ALL);
|
||||
});
|
||||
|
||||
host.mocks.ui.confirm.confirmWithMessage
|
||||
.mockResolvedValueOnce(SIMPLE_FETCH_STAGE1_REMOTE_WINS)
|
||||
@@ -1022,9 +1066,11 @@ describe("Red Flag Feature", () => {
|
||||
});
|
||||
|
||||
it("should continue and finalise when sync has failures but user releases flag", async () => {
|
||||
const host = createHostMock();
|
||||
const host = createHostMock(true);
|
||||
const log = createLoggerMock();
|
||||
const cleanupFlag = vi.fn().mockResolvedValue(undefined);
|
||||
const cleanupFlag = vi.fn(async () => {
|
||||
await host.mocks.storageAccess.delete(FlagFilesHumanReadable.FETCH_ALL);
|
||||
});
|
||||
|
||||
host.mocks.ui.confirm.confirmWithMessage
|
||||
.mockResolvedValueOnce(SIMPLE_FETCH_STAGE1_REMOTE_WINS)
|
||||
@@ -2000,3 +2046,239 @@ describe("Red Flag Feature", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Issue 1244: completed Fetch followed by another Join", () => {
|
||||
it.each(["receiver-app", "imported-source-app"])(
|
||||
"asks again after successful Fetch with initial database suffix %s",
|
||||
async (initialSuffix) => {
|
||||
const host = createHostMock();
|
||||
const context = createServiceContext();
|
||||
class FixtureSettings extends SettingService {
|
||||
protected async saveData() {}
|
||||
protected async loadData() {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
const setting = new FixtureSettings(context, {
|
||||
deviceValues: new MemoryValueStore(),
|
||||
APIService: {
|
||||
getSystemVaultName: () => "receiver-vault",
|
||||
getAppID: () => "receiver-app",
|
||||
addLog: vi.fn(),
|
||||
} as any,
|
||||
});
|
||||
Object.assign(host.mocks.setting.settings, DEFAULT_SETTINGS, {
|
||||
additionalSuffixOfDatabaseName: initialSuffix,
|
||||
couchDB_URI: "https://example.invalid",
|
||||
couchDB_DBNAME: "vault",
|
||||
});
|
||||
setting.settings = host.mocks.setting.settings;
|
||||
host.services.setting.config = setting.config as any;
|
||||
host.mocks.tweakValue.fetchRemotePreferred.mockResolvedValue(availableRemoteTweaks({}));
|
||||
host.mocks.ui.confirm.confirmWithMessage.mockImplementation(
|
||||
async (_title, _message, buttons) => buttons[0]
|
||||
);
|
||||
const reset = {
|
||||
API: { getAppID: () => "receiver-app" },
|
||||
setting: host.services.setting,
|
||||
database: { resetDatabaseForCurrentSettings: vi.fn(async () => true) },
|
||||
databaseEvents: {},
|
||||
replicator: {},
|
||||
events: context.events,
|
||||
};
|
||||
host.mocks.rebuilder.$fetchLocalDBFast.mockImplementation(async () => {
|
||||
await ServiceRebuilder.prototype.resetLocalDatabase.call(reset as any);
|
||||
});
|
||||
const cleanup = vi.fn(async () => {
|
||||
await host.mocks.storageAccess.delete(FlagFilesHumanReadable.FETCH_ALL);
|
||||
});
|
||||
host.mocks.storageAccess.files.add(FlagFilesHumanReadable.FETCH_ALL);
|
||||
await expect(askAndPerformFastSetupOnScheduledFetchAll(host as any, vi.fn(), cleanup)).resolves.toBe(true);
|
||||
expect(host.mocks.ui.confirm.confirmWithMessage).toHaveBeenCalledTimes(2);
|
||||
expect(host.mocks.setting.settings.additionalSuffixOfDatabaseName).toBe("receiver-app");
|
||||
expect(cleanup).toHaveBeenCalledOnce();
|
||||
|
||||
await host.services.setting.applyPartial({ additionalSuffixOfDatabaseName: initialSuffix });
|
||||
host.mocks.storageAccess.files.add(FlagFilesHumanReadable.FETCH_ALL);
|
||||
await expect(askAndPerformFastSetupOnScheduledFetchAll(host as any, vi.fn(), cleanup)).resolves.toBe(true);
|
||||
expect(host.mocks.ui.confirm.confirmWithMessage).toHaveBeenCalledTimes(4);
|
||||
expect(cleanup).toHaveBeenCalledTimes(2);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
describe("Fetch flag selection lifecycle", () => {
|
||||
function scheduledFetch() {
|
||||
const host = createHostMock(true);
|
||||
host.mocks.ui.confirm.confirmWithMessage.mockImplementation(async (_title, _message, buttons) => buttons[0]);
|
||||
host.mocks.tweakValue.fetchRemotePreferred.mockResolvedValue(availableRemoteTweaks({}));
|
||||
return { host, handler: createFetchAllFlagHandler(host as any, vi.fn()) };
|
||||
}
|
||||
|
||||
it("ignores a stale localStorage choice and records the new selection in Markdown", async () => {
|
||||
const { host, handler } = scheduledFetch();
|
||||
await host.services.setting.config.setValue(
|
||||
"simple-fetch-mode",
|
||||
JSON.stringify({
|
||||
stage1: SIMPLE_FETCH_STAGE1_REMOTE_WINS,
|
||||
stage2: SIMPLE_FETCH_STAGE2_REMOTE_DELETE_ALL,
|
||||
})
|
||||
);
|
||||
host.mocks.rebuilder.$fetchLocalDBFast.mockRejectedValueOnce(new Error("offline"));
|
||||
await expect(handler.handle()).resolves.toBe(false);
|
||||
expect(host.mocks.ui.confirm.confirmWithMessage).toHaveBeenCalledTimes(2);
|
||||
expect(host.mocks.storageAccess.contents.get(FlagFilesHumanReadable.FETCH_ALL)).toContain("```json");
|
||||
expect(host.mocks.storageAccess.contents.get(FlagFilesHumanReadable.FETCH_ALL)).toContain(
|
||||
SIMPLE_FETCH_STAGE1_NEWER_WINS
|
||||
);
|
||||
});
|
||||
|
||||
it("confirms the same selection after the database suffix changes and Fetch fails", async () => {
|
||||
const { host, handler } = scheduledFetch();
|
||||
host.mocks.rebuilder.$fetchLocalDBFast.mockImplementationOnce(async () => {
|
||||
host.mocks.setting.settings.additionalSuffixOfDatabaseName = "receiver-app";
|
||||
throw new Error("offline after resetting database");
|
||||
});
|
||||
await expect(handler.handle()).resolves.toBe(false);
|
||||
expect(host.mocks.storageAccess.contents.get(FlagFilesHumanReadable.FETCH_ALL)).toContain(
|
||||
"livesync-fast-setup"
|
||||
);
|
||||
await expect(createFetchAllFlagHandler(host as any, vi.fn()).handle()).resolves.toBe(true);
|
||||
expect(host.mocks.ui.confirm.confirmWithMessage).toHaveBeenCalledTimes(3);
|
||||
expect(host.mocks.ui.confirm.confirmWithMessage.mock.calls[2][0]).toBe("Resume scheduled fetch?");
|
||||
expect(host.mocks.storageAccess.files.has(FlagFilesHumanReadable.FETCH_ALL)).toBe(false);
|
||||
});
|
||||
|
||||
it("does not fetch or reflect files when saving the choice returns false", async () => {
|
||||
const { host, handler } = scheduledFetch();
|
||||
host.mocks.storageAccess.writeFileAuto.mockResolvedValueOnce(false);
|
||||
await expect(handler.handle()).resolves.toBe(false);
|
||||
expect(host.mocks.rebuilder.$fetchLocalDBFast).not.toHaveBeenCalled();
|
||||
expect(host.mocks.setting.settings).toMatchObject({
|
||||
suspendFileWatching: true,
|
||||
suspendParseReplicationResult: true,
|
||||
});
|
||||
expect(host.mocks.storageAccess.files.has(FlagFilesHumanReadable.FETCH_ALL)).toBe(true);
|
||||
});
|
||||
|
||||
it("retains the choices when rebuild finalisation returns false", async () => {
|
||||
const { host, handler } = scheduledFetch();
|
||||
host.mocks.rebuilder.finishRebuild.mockResolvedValueOnce(false);
|
||||
await expect(handler.handle()).resolves.toBe(false);
|
||||
expect(host.mocks.storageAccess.files.has(FlagFilesHumanReadable.FETCH_ALL)).toBe(true);
|
||||
expect(host.mocks.storageAccess.contents.get(FlagFilesHumanReadable.FETCH_ALL)).toContain(
|
||||
"livesync-fast-setup"
|
||||
);
|
||||
expect(host.mocks.setting.settings).toMatchObject({
|
||||
suspendFileWatching: true,
|
||||
suspendParseReplicationResult: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps reflection suspended if the flag cannot be removed after completion", async () => {
|
||||
const { host, handler } = scheduledFetch();
|
||||
host.mocks.storageAccess.delete.mockRejectedValueOnce(new Error("read-only storage"));
|
||||
await expect(handler.handle()).resolves.toBe(false);
|
||||
expect(host.mocks.storageAccess.files.has(FlagFilesHumanReadable.FETCH_ALL)).toBe(true);
|
||||
expect(host.mocks.setting.settings).toMatchObject({
|
||||
suspendFileWatching: true,
|
||||
suspendParseReplicationResult: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Detailed Fetch cancellation regression", () => {
|
||||
it("preserves an existing parse suspension when Commonlib cancels restricted fetch", async () => {
|
||||
const host = createHostMock(true);
|
||||
Object.assign(host.mocks.setting.settings, {
|
||||
maxMTimeForReflectEvents: 123456789,
|
||||
suspendParseReplicationResult: true,
|
||||
});
|
||||
host.mocks.tweakValue.fetchRemotePreferred.mockResolvedValue(availableRemoteTweaks({}));
|
||||
host.mocks.ui.dialogManager.openWithExplicitCancel.mockResolvedValueOnce({
|
||||
vault: "identical",
|
||||
extra: { preventFetchingConfig: true },
|
||||
});
|
||||
host.mocks.ui.confirm.askSelectStringDialogue.mockResolvedValueOnce("Cancel operation");
|
||||
const runBoundedRemoteActivity = vi.fn();
|
||||
const rebuilder = {
|
||||
checkPathIdentitiesBeforeReconstruction: vi.fn(),
|
||||
checkRemotePathIdentityBeforeFetch: vi.fn(),
|
||||
setting: host.services.setting,
|
||||
UI: host.services.UI,
|
||||
replicator: { runBoundedRemoteActivity },
|
||||
};
|
||||
host.mocks.rebuilder.$fetchLocal.mockImplementation(async () => {
|
||||
await ServiceRebuilder.prototype.fetchLocal.call(rebuilder as any, false, false);
|
||||
});
|
||||
await createFetchAllFlagHandler(host as any, vi.fn()).handle();
|
||||
expect(host.mocks.ui.confirm.askSelectStringDialogue).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
expect.any(Array),
|
||||
expect.objectContaining({ title: "Confirm restricted fetch" })
|
||||
);
|
||||
expect(runBoundedRemoteActivity).not.toHaveBeenCalled();
|
||||
expect(host.mocks.setting.settings.suspendParseReplicationResult).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Fetch confirmation lifecycle", () => {
|
||||
it("cancels a retained completed flag without fetching again", async () => {
|
||||
const host = createHostMock(true);
|
||||
host.mocks.tweakValue.fetchRemotePreferred.mockResolvedValue(availableRemoteTweaks({}));
|
||||
host.mocks.ui.confirm.confirmWithMessage
|
||||
.mockResolvedValueOnce(SIMPLE_FETCH_STAGE1_REMOTE_WINS)
|
||||
.mockResolvedValueOnce(SIMPLE_FETCH_STAGE2_REMOTE_DELETE_NONE)
|
||||
.mockResolvedValueOnce("Cancel");
|
||||
host.mocks.storageAccess.delete.mockRejectedValueOnce(new Error("deletion interrupted"));
|
||||
const handler = createFetchAllFlagHandler(host as any, vi.fn());
|
||||
await expect(handler.handle()).resolves.toBe(false);
|
||||
expect(host.mocks.storageAccess.files.has(FlagFilesHumanReadable.FETCH_ALL)).toBe(true);
|
||||
await expect(handler.handle()).resolves.toBe(false);
|
||||
expect(host.mocks.rebuilder.$fetchLocalDBFast).toHaveBeenCalledOnce();
|
||||
expect(host.mocks.ui.confirm.confirmWithMessage.mock.calls[2][0]).toBe("Resume scheduled fetch?");
|
||||
expect(host.mocks.storageAccess.files.has(FlagFilesHumanReadable.FETCH_ALL)).toBe(false);
|
||||
expect(host.mocks.setting.settings).toMatchObject({
|
||||
suspendFileWatching: true,
|
||||
suspendParseReplicationResult: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("retains unreadable Markdown after interruption and asks again on the next attempt", async () => {
|
||||
const host = createHostMock(true);
|
||||
const markdown = "# Recovery notes\n\n<!-- unfinished comment";
|
||||
host.mocks.storageAccess.contents.set(FlagFilesHumanReadable.FETCH_ALL, markdown);
|
||||
host.mocks.tweakValue.fetchRemotePreferred.mockResolvedValue(availableRemoteTweaks({}));
|
||||
for (let i = 0; i < 2; i++) {
|
||||
host.mocks.ui.confirm.confirmWithMessage
|
||||
.mockResolvedValueOnce("Choose new options and continue")
|
||||
.mockResolvedValueOnce(SIMPLE_FETCH_STAGE1_REMOTE_WINS)
|
||||
.mockResolvedValueOnce(SIMPLE_FETCH_STAGE2_REMOTE_DELETE_NONE);
|
||||
}
|
||||
host.mocks.rebuilder.$fetchLocalDBFast.mockRejectedValueOnce(new Error("offline"));
|
||||
const handler = createFetchAllFlagHandler(host as any, vi.fn());
|
||||
await expect(handler.handle()).resolves.toBe(false);
|
||||
expect(host.mocks.storageAccess.contents.get(FlagFilesHumanReadable.FETCH_ALL)).toBe(markdown);
|
||||
expect(host.mocks.storageAccess.writeFileAuto).not.toHaveBeenCalled();
|
||||
await expect(handler.handle()).resolves.toBe(true);
|
||||
expect(host.mocks.ui.confirm.confirmWithMessage).toHaveBeenCalledTimes(6);
|
||||
expect(host.mocks.storageAccess.files.has(FlagFilesHumanReadable.FETCH_ALL)).toBe(false);
|
||||
});
|
||||
|
||||
it("keeps both reflection directions suspended when detailed Fetch cannot delete its flag", async () => {
|
||||
const host = createHostMock(true);
|
||||
host.mocks.ui.confirm.confirmWithMessage.mockResolvedValueOnce(SIMPLE_FETCH_STAGE1_DETAILED);
|
||||
host.mocks.ui.dialogManager.openWithExplicitCancel.mockResolvedValueOnce({
|
||||
vault: "identical",
|
||||
extra: { preventFetchingConfig: true },
|
||||
});
|
||||
host.mocks.storageAccess.delete.mockRejectedValueOnce(new Error("read-only storage"));
|
||||
await expect(createFetchAllFlagHandler(host as any, vi.fn()).handle()).resolves.toBe(false);
|
||||
expect(host.mocks.rebuilder.$fetchLocal).toHaveBeenCalledOnce();
|
||||
expect(host.mocks.storageAccess.files.has(FlagFilesHumanReadable.FETCH_ALL)).toBe(true);
|
||||
expect(host.mocks.setting.settings).toMatchObject({
|
||||
suspendFileWatching: true,
|
||||
suspendParseReplicationResult: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user