diff --git a/src/features/HiddenFileCommon/JsonResolvePane.svelte b/src/features/HiddenFileCommon/JsonResolvePane.svelte
index 034b02f..bb1b22b 100644
--- a/src/features/HiddenFileCommon/JsonResolvePane.svelte
+++ b/src/features/HiddenFileCommon/JsonResolvePane.svelte
@@ -143,7 +143,7 @@
{#if selectedObj != false}
-
+
diff --git a/src/modules/features/InteractiveConflictResolving/ConflictResolveModal.ts b/src/modules/features/InteractiveConflictResolving/ConflictResolveModal.ts
index 826fbec..ad308e5 100644
--- a/src/modules/features/InteractiveConflictResolving/ConflictResolveModal.ts
+++ b/src/modules/features/InteractiveConflictResolving/ConflictResolveModal.ts
@@ -63,6 +63,7 @@ export class ConflictResolveModal extends Modal {
contentEl.createEl("span", { text: this.filename });
const div = contentEl.createDiv("");
div.addClass("op-scrollable");
+ div.addClass("ls-dialog");
let diff = "";
for (const v of this.result.diff) {
const x1 = v[0];
@@ -86,6 +87,7 @@ export class ConflictResolveModal extends Modal {
}
const div2 = contentEl.createDiv("");
+ div2.addClass("ls-dialog");
const date1 =
new Date(this.result.left.mtime).toLocaleString() + (this.result.left.deleted ? " (Deleted)" : "");
const date2 =
diff --git a/src/modules/services/ObsidianVaultService.ts b/src/modules/services/ObsidianVaultService.ts
index 4229e00..480c561 100644
--- a/src/modules/services/ObsidianVaultService.ts
+++ b/src/modules/services/ObsidianVaultService.ts
@@ -1,4 +1,4 @@
-import { getPathFromTFile } from "@/common/utils";
+import { getPathFromTFile, isValidPath } from "@/common/utils";
import { InjectableVaultService } from "@/lib/src/services/implements/injectable/InjectableVaultService";
import type { ObsidianServiceContext } from "@/lib/src/services/implements/obsidian/ObsidianServiceContext";
import type { FilePath } from "@/lib/src/common/types";
@@ -30,4 +30,7 @@ export class ObsidianVaultService extends InjectableVaultService Promise;
+ handle: () => Promise;
+}
+
+export async function isFlagFileExist(host: NecessaryServices, path: string) {
+ const redFlagExist = await host.serviceModules.storageAccess.isExists(
+ host.serviceModules.storageAccess.normalisePath(path)
+ );
+ if (redFlagExist) {
+ return true;
+ }
+ return false;
+}
+
+export async function deleteFlagFile(host: NecessaryServices, log: LogFunction, path: string) {
+ try {
+ const isFlagged = await host.serviceModules.storageAccess.isExists(
+ host.serviceModules.storageAccess.normalisePath(path)
+ );
+ if (isFlagged) {
+ await host.serviceModules.storageAccess.delete(path, true);
+ }
+ } catch (ex) {
+ log(`Could not delete ${path}`);
+ log(ex, LOG_LEVEL_VERBOSE);
+ }
+}
+/**
+ * Factory function to create a fetch all flag handler.
+ * All logic related to fetch all flag is encapsulated here.
+ */
+export function createFetchAllFlagHandler(
+ host: NecessaryServices<
+ "vault" | "fileProcessing" | "tweakValue" | "UI" | "setting" | "appLifecycle",
+ "storageAccess" | "rebuilder"
+ >,
+ log: LogFunction
+): FlagFileHandler {
+ // Check if fetch all flag is active
+ const isFlagActive = async () =>
+ (await isFlagFileExist(host, FlagFilesOriginal.FETCH_ALL)) ||
+ (await isFlagFileExist(host, FlagFilesHumanReadable.FETCH_ALL));
+
+ // Cleanup fetch all flag files
+ const cleanupFlag = async () => {
+ await deleteFlagFile(host, log, FlagFilesOriginal.FETCH_ALL);
+ await deleteFlagFile(host, log, FlagFilesHumanReadable.FETCH_ALL);
+ };
+
+ // Handle the fetch all scheduled operation
+ const onScheduled = async () => {
+ const method = await host.services.UI.dialogManager.openWithExplicitCancel(FetchEverything);
+ if (method === "cancelled") {
+ log("Fetch everything cancelled by user.", LOG_LEVEL_NOTICE);
+ await cleanupFlag();
+ host.services.appLifecycle.performRestart();
+ return false;
+ }
+ const { vault, extra } = method;
+ const settings = await host.services.setting.currentSettings();
+ // If remote is MinIO, makeLocalChunkBeforeSync is not available. (because no-deduplication on sending).
+ const makeLocalChunkBeforeSyncAvailable = settings.remoteType !== REMOTE_MINIO;
+ const mapVaultStateToAction = {
+ identical: {
+ makeLocalChunkBeforeSync: makeLocalChunkBeforeSyncAvailable,
+ makeLocalFilesBeforeSync: false,
+ },
+ independent: {
+ makeLocalChunkBeforeSync: false,
+ makeLocalFilesBeforeSync: false,
+ },
+ unbalanced: {
+ makeLocalChunkBeforeSync: false,
+ makeLocalFilesBeforeSync: true,
+ },
+ cancelled: {
+ makeLocalChunkBeforeSync: false,
+ makeLocalFilesBeforeSync: false,
+ },
+ } as const;
+
+ return await processVaultInitialisation(host, log, async () => {
+ const settings = host.services.setting.currentSettings();
+ await adjustSettingToRemoteIfNeeded(host, log, extra, settings);
+ const vaultStateToAction = mapVaultStateToAction[vault];
+ const { makeLocalChunkBeforeSync, makeLocalFilesBeforeSync } = vaultStateToAction;
+ log(
+ `Fetching everything with settings: makeLocalChunkBeforeSync=${makeLocalChunkBeforeSync}, makeLocalFilesBeforeSync=${makeLocalFilesBeforeSync}`,
+ LOG_LEVEL_INFO
+ );
+ await host.serviceModules.rebuilder.$fetchLocal(makeLocalChunkBeforeSync, !makeLocalFilesBeforeSync);
+ await cleanupFlag();
+ log("Fetch everything operation completed. Vault files will be gradually synced.", LOG_LEVEL_NOTICE);
+ return true;
+ });
+ };
+
+ return {
+ priority: 10,
+ check: () => isFlagActive(),
+ handle: async () => {
+ const res = await onScheduled();
+ if (res) {
+ return await verifyAndUnlockSuspension(host, log);
+ }
+ return false;
+ },
+ };
+}
+
+/**
+ * Adjust setting to remote configuration.
+ * @param config current configuration to retrieve remote preferred config
+ * @returns updated configuration if applied, otherwise null.
+ */
+export async function adjustSettingToRemote(
+ host: NecessaryServices<"tweakValue" | "UI" | "setting", any>,
+ log: LogFunction,
+ config: ObsidianLiveSyncSettings
+) {
+ // Fetch remote configuration unless prevented.
+ const SKIP_FETCH = "Skip and proceed";
+ const RETRY_FETCH = "Retry (recommended)";
+ let canProceed = false;
+ do {
+ const remoteTweaks = await host.services.tweakValue.fetchRemotePreferred(config);
+ if (!remoteTweaks) {
+ const choice = await host.services.UI.confirm.askSelectStringDialogue(
+ "Could not fetch configuration from remote. If you are new to the Self-hosted LiveSync, this might be expected. If not, you should check your network or server settings.",
+ [SKIP_FETCH, RETRY_FETCH] as const,
+ {
+ defaultAction: RETRY_FETCH,
+ timeout: 0,
+ title: "Fetch Remote Configuration Failed",
+ }
+ );
+ if (choice === SKIP_FETCH) {
+ canProceed = true;
+ }
+ } else {
+ const necessary = extractObject(TweakValuesShouldMatchedTemplate, remoteTweaks);
+ // Check if any necessary tweak value is different from current config.
+ const differentItems = Object.entries(necessary).filter(([key, value]) => {
+ return (config as any)[key] !== value;
+ });
+ if (differentItems.length === 0) {
+ log("Remote configuration matches local configuration. No changes applied.", LOG_LEVEL_NOTICE);
+ } else {
+ await host.services.UI.confirm.askSelectStringDialogue(
+ "Your settings differed slightly from the server's. The plug-in has supplemented the incompatible parts with the server settings!",
+ ["OK"] as const,
+ {
+ defaultAction: "OK",
+ timeout: 0,
+ }
+ );
+ }
+
+ config = {
+ ...config,
+ ...Object.fromEntries(differentItems),
+ } satisfies ObsidianLiveSyncSettings;
+ await host.services.setting.applyPartial(config, true);
+ log("Remote configuration applied.", LOG_LEVEL_NOTICE);
+ canProceed = true;
+ const updatedConfig = host.services.setting.currentSettings();
+ return updatedConfig;
+ }
+ } while (!canProceed);
+}
+
+/**
+ * Adjust setting to remote if needed.
+ * @param extra result of dialogues that may contain preventFetchingConfig flag (e.g, from FetchEverything or RebuildEverything)
+ * @param config current configuration to retrieve remote preferred config
+ */
+export async function adjustSettingToRemoteIfNeeded(
+ host: NecessaryServices<"tweakValue" | "UI" | "setting", any>,
+ log: LogFunction,
+ extra: { preventFetchingConfig: boolean },
+ config: ObsidianLiveSyncSettings
+) {
+ if (extra && extra.preventFetchingConfig) {
+ return;
+ }
+
+ // Remote configuration fetched and applied.
+ if (await adjustSettingToRemote(host, log, config)) {
+ config = host.services.setting.currentSettings();
+ } else {
+ log("Remote configuration not applied.", LOG_LEVEL_NOTICE);
+ }
+ log(JSON.stringify(config), LOG_LEVEL_VERBOSE);
+}
+
+/**
+ * Process vault initialisation with suspending file watching and sync.
+ * @param proc process to be executed during initialisation, should return true if can be continued, false if app is unable to continue the process.
+ * @param keepSuspending whether to keep suspending file watching after the process.
+ * @returns result of the process, or false if error occurs.
+ */
+export async function processVaultInitialisation(
+ host: NecessaryServices<"setting", any>,
+ log: LogFunction,
+ proc: () => Promise,
+ keepSuspending = false
+) {
+ try {
+ // Disable batch saving and file watching during initialisation.
+ await host.services.setting.applyPartial({ batchSave: false }, false);
+ await host.services.setting.suspendAllSync();
+ await host.services.setting.suspendExtraSync();
+ await host.services.setting.applyPartial({ suspendFileWatching: true }, true);
+ try {
+ const result = await proc();
+ return result;
+ } catch (ex) {
+ log("Error during vault initialisation process.", LOG_LEVEL_NOTICE);
+ log(ex, LOG_LEVEL_VERBOSE);
+ return false;
+ }
+ } catch (ex) {
+ log("Error during vault initialisation.", LOG_LEVEL_NOTICE);
+ log(ex, LOG_LEVEL_VERBOSE);
+ return false;
+ } finally {
+ if (!keepSuspending) {
+ // Re-enable file watching after initialisation.
+ await host.services.setting.applyPartial({ suspendFileWatching: false }, true);
+ }
+ }
+}
+
+export async function verifyAndUnlockSuspension(
+ host: NecessaryServices<"setting" | "appLifecycle" | "UI", any>,
+ log: LogFunction
+) {
+ if (!host.services.setting.currentSettings().suspendFileWatching) {
+ return true;
+ }
+ if (
+ (await host.services.UI.confirm.askYesNoDialog(
+ "Do you want to resume file and database processing, and restart obsidian now?",
+ { defaultOption: "Yes", timeout: 15 }
+ )) != "yes"
+ ) {
+ // TODO: Confirm actually proceed to next process.
+ return true;
+ }
+ await host.services.setting.applyPartial({ suspendFileWatching: false }, true);
+ host.services.appLifecycle.performRestart();
+ return false;
+}
+
+/**
+ * Factory function to create a rebuild flag handler.
+ * All logic related to rebuild flag is encapsulated here.
+ */
+export function createRebuildFlagHandler(
+ host: NecessaryServices<"setting" | "appLifecycle" | "UI" | "tweakValue", "storageAccess" | "rebuilder">,
+ log: LogFunction
+) {
+ // Check if rebuild flag is active
+ const isFlagActive = async () =>
+ (await isFlagFileExist(host, FlagFilesOriginal.REBUILD_ALL)) ||
+ (await isFlagFileExist(host, FlagFilesHumanReadable.REBUILD_ALL));
+
+ // Cleanup rebuild flag files
+ const cleanupFlag = async () => {
+ await deleteFlagFile(host, log, FlagFilesOriginal.REBUILD_ALL);
+ await deleteFlagFile(host, log, FlagFilesHumanReadable.REBUILD_ALL);
+ };
+
+ // Handle the rebuild everything scheduled operation
+ const onScheduled = async () => {
+ const method = await host.services.UI.dialogManager.openWithExplicitCancel(RebuildEverything);
+ if (method === "cancelled") {
+ log("Rebuild everything cancelled by user.", LOG_LEVEL_NOTICE);
+ await cleanupFlag();
+ host.services.appLifecycle.performRestart();
+ return false;
+ }
+ const { extra } = method;
+ const settings = host.services.setting.currentSettings();
+ await adjustSettingToRemoteIfNeeded(host, log, extra, settings);
+ return await processVaultInitialisation(host, log, async () => {
+ await host.serviceModules.rebuilder.$rebuildEverything();
+ await cleanupFlag();
+ log("Rebuild everything operation completed.", LOG_LEVEL_NOTICE);
+ return true;
+ });
+ };
+
+ return {
+ priority: 20,
+ check: () => isFlagActive(),
+ handle: async () => {
+ const res = await onScheduled();
+ if (res) {
+ return await verifyAndUnlockSuspension(host, log);
+ }
+ return false;
+ },
+ };
+}
+
+/**
+ * Factory function to create a suspend all flag handler.
+ * All logic related to suspend flag is encapsulated here.
+ */
+export function createSuspendFlagHandler(
+ host: NecessaryServices<"setting", "storageAccess">,
+ log: LogFunction
+): FlagFileHandler {
+ // Check if suspend flag is active
+ const isFlagActive = async () => await isFlagFileExist(host, FlagFilesOriginal.SUSPEND_ALL);
+
+ // Handle the suspend all scheduled operation
+ const onScheduled = async () => {
+ log("SCRAM is detected. All operations are suspended.", LOG_LEVEL_NOTICE);
+ return await processVaultInitialisation(
+ host,
+ log,
+ async () => {
+ log(
+ "All operations are suspended as per SCRAM.\nLogs will be written to the file. This might be a performance impact.",
+ LOG_LEVEL_NOTICE
+ );
+ await host.services.setting.applyPartial({ writeLogToTheFile: true }, true);
+ return Promise.resolve(false);
+ },
+ true
+ );
+ };
+
+ return {
+ priority: 5,
+ check: () => isFlagActive(),
+ handle: () => onScheduled(),
+ };
+}
+
+export function flagHandlerToEventHandler(flagHandler: FlagFileHandler) {
+ return async () => {
+ if (await flagHandler.check()) {
+ return await flagHandler.handle();
+ }
+ return true;
+ };
+}
+
+export function useRedFlagFeatures(
+ host: NecessaryServices<
+ "API" | "appLifecycle" | "UI" | "setting" | "tweakValue" | "fileProcessing" | "vault",
+ "storageAccess" | "rebuilder"
+ >
+) {
+ const log = createInstanceLogFunction("SF:RedFlag", host.services.API);
+ const handlerFetch = createFetchAllFlagHandler(host, log);
+ const handlerRebuild = createRebuildFlagHandler(host, log);
+ const handlerSuspend = createSuspendFlagHandler(host, log);
+ host.services.appLifecycle.onLayoutReady.addHandler(flagHandlerToEventHandler(handlerFetch), handlerFetch.priority);
+ host.services.appLifecycle.onLayoutReady.addHandler(
+ flagHandlerToEventHandler(handlerRebuild),
+ handlerRebuild.priority
+ );
+ host.services.appLifecycle.onLayoutReady.addHandler(
+ flagHandlerToEventHandler(handlerSuspend),
+ handlerSuspend.priority
+ );
+}
diff --git a/src/serviceFeatures/redFlag.unit.spec.ts b/src/serviceFeatures/redFlag.unit.spec.ts
new file mode 100644
index 0000000..65fcef4
--- /dev/null
+++ b/src/serviceFeatures/redFlag.unit.spec.ts
@@ -0,0 +1,1140 @@
+import { describe, it, expect, vi } from "vitest";
+import type { LogFunction } from "@lib/services/lib/logUtils";
+import { FlagFilesHumanReadable, FlagFilesOriginal } from "@lib/common/models/redflag.const";
+import { REMOTE_MINIO } from "@lib/common/models/setting.const";
+import {
+ createFetchAllFlagHandler,
+ createRebuildFlagHandler,
+ createSuspendFlagHandler,
+ isFlagFileExist,
+ deleteFlagFile,
+ adjustSettingToRemote,
+ adjustSettingToRemoteIfNeeded,
+ processVaultInitialisation,
+ verifyAndUnlockSuspension,
+ flagHandlerToEventHandler,
+} from "./redFlag";
+import {
+ TweakValuesRecommendedTemplate,
+ TweakValuesShouldMatchedTemplate,
+ TweakValuesTemplate,
+} from "@/lib/src/common/types";
+
+// Mock types and functions
+const createLoggerMock = (): LogFunction => {
+ return vi.fn();
+};
+
+const createStorageAccessMock = () => {
+ const files: Set = new Set();
+ return {
+ files,
+ 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);
+ return Promise.resolve();
+ }),
+ getFileNames: vi.fn(() => Array.from(files)),
+ };
+};
+
+const createSettingServiceMock = () => {
+ const settings: any = {
+ batchSave: true,
+ suspendFileWatching: false,
+ writeLogToTheFile: false,
+ remoteType: "CouchDB",
+ };
+ return {
+ settings,
+ currentSettings: vi.fn(() => settings),
+ applyPartial: vi.fn((partial: any, _feedback?: boolean) => {
+ Object.assign(settings, partial);
+ return Promise.resolve();
+ }),
+ suspendAllSync: vi.fn(() => Promise.resolve()),
+ suspendExtraSync: vi.fn(() => Promise.resolve()),
+ };
+};
+
+const createAppLifecycleMock = () => {
+ return {
+ performRestart: vi.fn(),
+ onLayoutReady: {
+ addHandler: vi.fn(),
+ },
+ };
+};
+
+const createUIServiceMock = () => {
+ return {
+ dialogManager: {
+ openWithExplicitCancel: vi.fn(),
+ },
+ confirm: {
+ askSelectStringDialogue: vi.fn(),
+ askYesNoDialog: vi.fn(),
+ },
+ };
+};
+
+const createRebuilderMock = () => {
+ return {
+ $fetchLocal: vi.fn(async () => {}),
+ $rebuildEverything: vi.fn(async () => {}),
+ };
+};
+
+const createTweakValueMock = () => {
+ return {
+ fetchRemotePreferred: vi.fn(() => Promise.resolve(null)),
+ };
+};
+
+const createHostMock = () => {
+ const storageAccessMock = createStorageAccessMock();
+ const settingMock = createSettingServiceMock();
+ const appLifecycleMock = createAppLifecycleMock();
+ const uiMock = createUIServiceMock();
+ const rebuilderMock = createRebuilderMock();
+ const tweakValueMock = createTweakValueMock();
+
+ return {
+ services: {
+ setting: settingMock,
+ appLifecycle: appLifecycleMock,
+ UI: uiMock,
+ tweakValue: tweakValueMock,
+ },
+ serviceModules: {
+ storageAccess: storageAccessMock,
+ rebuilder: rebuilderMock,
+ },
+ mocks: {
+ storageAccess: storageAccessMock,
+ setting: settingMock,
+ appLifecycle: appLifecycleMock,
+ ui: uiMock,
+ rebuilder: rebuilderMock,
+ tweakValue: tweakValueMock,
+ },
+ };
+};
+
+describe("Red Flag Feature", () => {
+ describe("isFlagFileExist", () => {
+ it("should return true if flag file exists", async () => {
+ const host = createHostMock();
+
+ host.mocks.storageAccess.files.add(FlagFilesOriginal.FETCH_ALL);
+
+ const result = await isFlagFileExist(host as any, FlagFilesOriginal.FETCH_ALL);
+ expect(result).toBe(true);
+ });
+
+ it("should return false if flag file does not exist", async () => {
+ const host = createHostMock();
+
+ const result = await isFlagFileExist(host as any, FlagFilesOriginal.FETCH_ALL);
+ expect(result).toBe(false);
+ });
+ });
+
+ describe("deleteFlagFile", () => {
+ it("should delete flag file if it exists", async () => {
+ const host = createHostMock();
+ const log = createLoggerMock();
+
+ host.mocks.storageAccess.files.add(FlagFilesOriginal.FETCH_ALL);
+
+ await deleteFlagFile(host as any, log, FlagFilesOriginal.FETCH_ALL);
+
+ const exists = await host.mocks.storageAccess.isExists(
+ host.mocks.storageAccess.normalisePath(FlagFilesOriginal.FETCH_ALL)
+ );
+ expect(exists).toBe(false);
+ });
+
+ it("should not throw error if file does not exist", async () => {
+ const host = createHostMock();
+ const log = createLoggerMock();
+
+ await expect(deleteFlagFile(host as any, log, FlagFilesOriginal.FETCH_ALL)).resolves.not.toThrow();
+ });
+
+ it("should log error if deletion fails", async () => {
+ const host = createHostMock();
+ const log = createLoggerMock();
+
+ host.mocks.storageAccess.delete.mockRejectedValueOnce(new Error("Delete failed"));
+ host.mocks.storageAccess.files.add(FlagFilesOriginal.FETCH_ALL);
+
+ await deleteFlagFile(host as any, log, FlagFilesOriginal.FETCH_ALL);
+
+ expect(log).toHaveBeenCalled();
+ });
+ });
+
+ describe("FlagFile Handler Priority", () => {
+ it("should handle suspend flag with priority 5", () => {
+ const host = createHostMock();
+ const log = createLoggerMock();
+
+ const handler = createSuspendFlagHandler(host as any, log);
+ expect(handler.priority).toBe(5);
+ expect(typeof handler.check).toBe("function");
+ expect(typeof handler.handle).toBe("function");
+ });
+
+ it("should handle fetch all flag with priority 10", () => {
+ const host = createHostMock();
+ const log = createLoggerMock();
+
+ const handler = createFetchAllFlagHandler(host as any, log);
+ expect(handler.priority).toBe(10);
+ expect(typeof handler.check).toBe("function");
+ expect(typeof handler.handle).toBe("function");
+ });
+
+ it("should handle rebuild all flag with priority 20", () => {
+ const host = createHostMock();
+ const log = createLoggerMock();
+
+ const handler = createRebuildFlagHandler(host as any, log);
+ expect(handler.priority).toBe(20);
+ expect(typeof handler.check).toBe("function");
+ expect(typeof handler.handle).toBe("function");
+ });
+ });
+
+ describe("Setting adjustment during vault initialisation", () => {
+ it("should suspend file watching during initialisation", async () => {
+ const host = createHostMock();
+ const log = createLoggerMock();
+
+ expect(host.mocks.setting.currentSettings().suspendFileWatching).toBe(false);
+
+ const result = await processVaultInitialisation(host as any, log, () => {
+ expect(host.mocks.setting.currentSettings().suspendFileWatching).toBe(true);
+ return Promise.resolve(true);
+ });
+
+ expect(result).toBe(true);
+ });
+
+ it("should disable batch save during initialisation", async () => {
+ const host = createHostMock();
+ const log = createLoggerMock();
+
+ expect(host.mocks.setting.currentSettings().batchSave).toBe(true);
+
+ const result = await processVaultInitialisation(host as any, log, () => {
+ expect(host.mocks.setting.currentSettings().batchSave).toBe(false);
+ return Promise.resolve(true);
+ });
+
+ expect(result).toBe(true);
+ });
+
+ it("should suspend all sync operations", async () => {
+ const host = createHostMock();
+ const log = createLoggerMock();
+
+ await processVaultInitialisation(host as any, log, () => {
+ return Promise.resolve(true);
+ });
+
+ expect(host.mocks.setting.suspendAllSync).toHaveBeenCalled();
+ expect(host.mocks.setting.suspendExtraSync).toHaveBeenCalled();
+ });
+
+ it("should resume file watching after initialisation completes", async () => {
+ const host = createHostMock();
+ const log = createLoggerMock();
+
+ await processVaultInitialisation(
+ host as any,
+ log,
+ () => {
+ return Promise.resolve(true);
+ },
+ false
+ );
+
+ expect(host.mocks.setting.currentSettings().suspendFileWatching).toBe(false);
+ });
+
+ it("should keep suspending when keepSuspending is true", async () => {
+ const host = createHostMock();
+ const log = createLoggerMock();
+
+ await processVaultInitialisation(
+ host as any,
+ log,
+ () => {
+ return Promise.resolve(true);
+ },
+ true
+ );
+
+ expect(host.mocks.setting.currentSettings().suspendFileWatching).toBe(true);
+ });
+
+ it("should return false when process fails", async () => {
+ const host = createHostMock();
+ const log = createLoggerMock();
+
+ const result = await processVaultInitialisation(
+ host as any,
+ log,
+ () => {
+ throw new Error("Process failed");
+ },
+ false
+ );
+
+ expect(result).toBe(false);
+ expect(log).toHaveBeenCalled();
+ });
+ });
+
+ describe("Suspend Flag Handler", () => {
+ it("should write logs to file when suspend flag is detected", () => {
+ const host = createHostMock();
+ const log = createLoggerMock();
+
+ const handler = createSuspendFlagHandler(host as any, log);
+
+ expect(handler.priority).toBe(5);
+ expect(typeof handler.check).toBe("function");
+ expect(typeof handler.handle).toBe("function");
+ });
+
+ it("should keep suspending after initialisation when suspend flag is active", async () => {
+ const host = createHostMock();
+
+ const handler = createSuspendFlagHandler(host as any, createLoggerMock());
+
+ const checkResult = await handler.check();
+ expect(typeof checkResult).toBe("boolean");
+ });
+
+ it("should apply writeLogToTheFile setting during suspension", async () => {
+ const host = createHostMock();
+
+ host.mocks.storageAccess.files.add(FlagFilesOriginal.SUSPEND_ALL);
+
+ const handler = createSuspendFlagHandler(host as any, createLoggerMock());
+ const checkResult = await handler.check();
+
+ expect(checkResult).toBe(true);
+ });
+ });
+
+ describe("Fetch All Flag Handler", () => {
+ it("should detect fetch all flag using original filename", async () => {
+ const host = createHostMock();
+ const log = createLoggerMock();
+
+ host.mocks.storageAccess.files.add(FlagFilesOriginal.FETCH_ALL);
+
+ const handler = createFetchAllFlagHandler(host as any, log);
+ const exists = await handler.check();
+
+ expect(exists).toBe(true);
+ });
+
+ it("should detect fetch all flag using human-readable filename", async () => {
+ const host = createHostMock();
+ const log = createLoggerMock();
+
+ host.mocks.storageAccess.files.add(FlagFilesHumanReadable.FETCH_ALL);
+
+ const handler = createFetchAllFlagHandler(host as any, log);
+ const exists = await handler.check();
+
+ expect(exists).toBe(true);
+ });
+
+ it("should clean up both original and human-readable fetch all flag files", async () => {
+ const host = createHostMock();
+ const log = createLoggerMock();
+
+ host.mocks.storageAccess.files.add(FlagFilesOriginal.FETCH_ALL);
+ host.mocks.storageAccess.files.add(FlagFilesHumanReadable.FETCH_ALL);
+
+ await deleteFlagFile(host as any, log, FlagFilesOriginal.FETCH_ALL);
+ await deleteFlagFile(host as any, log, FlagFilesHumanReadable.FETCH_ALL);
+
+ const originalExists = await host.mocks.storageAccess.isExists(
+ host.mocks.storageAccess.normalisePath(FlagFilesOriginal.FETCH_ALL)
+ );
+ const humanExists = await host.mocks.storageAccess.isExists(
+ host.mocks.storageAccess.normalisePath(FlagFilesHumanReadable.FETCH_ALL)
+ );
+
+ expect(originalExists).toBe(false);
+ expect(humanExists).toBe(false);
+ });
+
+ it("should have priority 10", () => {
+ const host = createHostMock();
+ const log = createLoggerMock();
+
+ const handler = createFetchAllFlagHandler(host as any, log);
+ expect(handler.priority).toBe(10);
+ });
+ });
+
+ describe("Rebuild All Flag Handler", () => {
+ it("should detect rebuild all flag using original filename", async () => {
+ const host = createHostMock();
+ const log = createLoggerMock();
+
+ host.mocks.storageAccess.files.add(FlagFilesOriginal.REBUILD_ALL);
+
+ const handler = createRebuildFlagHandler(host as any, log);
+ const exists = await handler.check();
+
+ expect(exists).toBe(true);
+ });
+
+ it("should detect rebuild all flag using human-readable filename", async () => {
+ const host = createHostMock();
+ const log = createLoggerMock();
+
+ host.mocks.storageAccess.files.add(FlagFilesHumanReadable.REBUILD_ALL);
+
+ const handler = createRebuildFlagHandler(host as any, log);
+ const exists = await handler.check();
+
+ expect(exists).toBe(true);
+ });
+
+ it("should clean up both original and human-readable rebuild all flag files", async () => {
+ const host = createHostMock();
+ const log = createLoggerMock();
+
+ host.mocks.storageAccess.files.add(FlagFilesOriginal.REBUILD_ALL);
+ host.mocks.storageAccess.files.add(FlagFilesHumanReadable.REBUILD_ALL);
+
+ await deleteFlagFile(host as any, log, FlagFilesOriginal.REBUILD_ALL);
+ await deleteFlagFile(host as any, log, FlagFilesHumanReadable.REBUILD_ALL);
+
+ const originalExists = await host.mocks.storageAccess.isExists(
+ host.mocks.storageAccess.normalisePath(FlagFilesOriginal.REBUILD_ALL)
+ );
+ const humanExists = await host.mocks.storageAccess.isExists(
+ host.mocks.storageAccess.normalisePath(FlagFilesHumanReadable.REBUILD_ALL)
+ );
+
+ expect(originalExists).toBe(false);
+ expect(humanExists).toBe(false);
+ });
+
+ it("should have priority 20", () => {
+ const host = createHostMock();
+ const log = createLoggerMock();
+
+ const handler = createRebuildFlagHandler(host as any, log);
+ expect(handler.priority).toBe(20);
+ });
+ });
+
+ describe("Flag file cleanup on error", () => {
+ it("should handle errors during flag file deletion gracefully", async () => {
+ const host = createHostMock();
+
+ // Simulate error in delete operation
+ host.mocks.storageAccess.delete.mockRejectedValueOnce(new Error("Delete failed"));
+
+ try {
+ await host.mocks.storageAccess.delete(FlagFilesOriginal.FETCH_ALL, true);
+ } catch {
+ // Error handled
+ }
+
+ expect(host.mocks.storageAccess.delete).toHaveBeenCalled();
+ });
+ });
+
+ describe("Integration: Handler registration on layout ready", () => {
+ it("should register handlers with correct priorities", () => {
+ const host = createHostMock();
+
+ expect(host.services.appLifecycle.onLayoutReady.addHandler).toBeDefined();
+ expect(typeof host.services.appLifecycle.onLayoutReady.addHandler).toBe("function");
+ });
+ });
+
+ describe("Dialog interaction scenarios", () => {
+ it("should handle fetch all dialog cancellation", async () => {
+ const host = createHostMock();
+
+ // Simulate user clicking cancel
+ host.mocks.ui.dialogManager.openWithExplicitCancel.mockResolvedValueOnce("cancelled");
+
+ // The dialog manager would return cancelled
+ const result = await host.mocks.ui.dialogManager.openWithExplicitCancel();
+ expect(result).toBe("cancelled");
+ });
+
+ it("should handle rebuild dialog cancellation", async () => {
+ const host = createHostMock();
+
+ // Simulate user clicking cancel
+ host.mocks.ui.dialogManager.openWithExplicitCancel.mockResolvedValueOnce("cancelled");
+
+ const result = await host.mocks.ui.dialogManager.openWithExplicitCancel();
+ expect(result).toBe("cancelled");
+ });
+
+ it("should handle confirm dialog for remote configuration mismatch", async () => {
+ const host = createHostMock();
+
+ await host.mocks.ui.confirm.askSelectStringDialogue("Your settings differed slightly.", ["OK"]);
+ expect(host.mocks.ui.confirm.askSelectStringDialogue).toHaveBeenCalled();
+ });
+ });
+
+ describe("Remote configuration adjustment", () => {
+ it("should skip remote configuration fetch when preventFetchingConfig is true", async () => {
+ const host = createHostMock();
+ const config = { preventFetchingConfig: true } as any;
+
+ await adjustSettingToRemoteIfNeeded(
+ host as any,
+ createLoggerMock(),
+ { preventFetchingConfig: true },
+ config
+ );
+
+ expect(host.mocks.tweakValue.fetchRemotePreferred).not.toHaveBeenCalled();
+ });
+
+ it("should fetch remote configuration when preventFetchingConfig is false", async () => {
+ const host = createHostMock();
+ const config = { batchSave: true } as any;
+
+ host.mocks.tweakValue.fetchRemotePreferred.mockResolvedValueOnce({
+ batchSave: false,
+ } as any);
+
+ await adjustSettingToRemoteIfNeeded(
+ host as any,
+ createLoggerMock(),
+ { preventFetchingConfig: false },
+ config
+ );
+
+ expect(host.mocks.tweakValue.fetchRemotePreferred).toHaveBeenCalled();
+ });
+
+ const mismatchDetectionKeys = Object.keys(TweakValuesShouldMatchedTemplate);
+ it.each(mismatchDetectionKeys)(
+ "should apply remote configuration when available and different:%s",
+ async (key) => {
+ const host = createHostMock();
+
+ const config = { [key]: TweakValuesTemplate[key as keyof typeof TweakValuesTemplate] } as any;
+ const differentValue =
+ typeof config[key as keyof typeof config] === "boolean"
+ ? !config[key as keyof typeof config]
+ : typeof config[key as keyof typeof config] === "number"
+ ? (config[key as keyof typeof config] as number) + 1
+ : "different";
+ const differentConfig = {
+ [key]: differentValue,
+ };
+ host.mocks.tweakValue.fetchRemotePreferred.mockResolvedValueOnce(differentConfig as any);
+ host.mocks.ui.confirm.askSelectStringDialogue.mockResolvedValueOnce("OK");
+
+ await adjustSettingToRemote(host as any, createLoggerMock(), config);
+ expect(host.mocks.ui.confirm.askSelectStringDialogue).toHaveBeenCalled();
+ expect(host.mocks.setting.applyPartial).toHaveBeenCalled();
+ }
+ );
+ const mismatchAcceptedKeys = Object.keys(TweakValuesRecommendedTemplate).filter(
+ (key) => !mismatchDetectionKeys.includes(key)
+ );
+
+ it.each(mismatchAcceptedKeys)(
+ "should apply remote configuration when available and different but acceptable: %s",
+ async (key) => {
+ const host = createHostMock();
+
+ const config = { [key]: TweakValuesTemplate[key as keyof typeof TweakValuesTemplate] } as any;
+ const differentValue =
+ typeof config[key as keyof typeof config] === "boolean"
+ ? !config[key as keyof typeof config]
+ : typeof config[key as keyof typeof config] === "number"
+ ? (config[key as keyof typeof config] as number) + 1
+ : "different";
+ const differentConfig = {
+ [key]: differentValue,
+ };
+ host.mocks.tweakValue.fetchRemotePreferred.mockResolvedValueOnce(differentConfig as any);
+ host.mocks.ui.confirm.askSelectStringDialogue.mockResolvedValueOnce("OK");
+
+ await adjustSettingToRemote(host as any, createLoggerMock(), config);
+
+ expect(host.mocks.setting.applyPartial).toHaveBeenCalled();
+ expect(host.mocks.ui.confirm.askSelectStringDialogue).not.toHaveBeenCalled();
+ }
+ );
+
+ it("should show dialog when remote fetch fails", async () => {
+ const host = createHostMock();
+ const log = createLoggerMock();
+ const config = { batchSave: true } as any;
+
+ host.mocks.tweakValue.fetchRemotePreferred.mockResolvedValueOnce(null);
+ host.mocks.ui.confirm.askSelectStringDialogue.mockResolvedValueOnce("Skip and proceed");
+
+ await adjustSettingToRemote(host as any, log, config);
+
+ expect(host.mocks.ui.confirm.askSelectStringDialogue).toHaveBeenCalled();
+ });
+
+ it("should retry when user selects retry option", async () => {
+ const host = createHostMock();
+ const log = createLoggerMock();
+ const config = { batchSave: true } as any;
+
+ host.mocks.tweakValue.fetchRemotePreferred
+ .mockResolvedValueOnce(null)
+ .mockResolvedValueOnce({ batchSave: false } as any);
+ host.mocks.ui.confirm.askSelectStringDialogue.mockResolvedValueOnce("Retry (recommended)");
+
+ await adjustSettingToRemote(host as any, log, config);
+
+ expect(host.mocks.tweakValue.fetchRemotePreferred).toHaveBeenCalledTimes(2);
+ });
+
+ it("should log when no changes needed", async () => {
+ const host = createHostMock();
+ const log = createLoggerMock();
+ const config = { batchSave: false } as any;
+
+ host.mocks.tweakValue.fetchRemotePreferred.mockResolvedValueOnce({
+ batchSave: false,
+ } as any);
+
+ await adjustSettingToRemote(host as any, log, config);
+
+ expect(log).toHaveBeenCalled();
+ });
+
+ it("should handle null extra parameter in adjustSettingToRemoteIfNeeded", async () => {
+ const host = createHostMock();
+ const log = createLoggerMock();
+ const config = { batchSave: true } as any;
+
+ host.mocks.tweakValue.fetchRemotePreferred.mockResolvedValueOnce(null);
+ host.mocks.ui.confirm.askSelectStringDialogue.mockResolvedValueOnce("Skip and proceed");
+
+ await adjustSettingToRemoteIfNeeded(host as any, log, null as any, config);
+
+ expect(host.mocks.tweakValue.fetchRemotePreferred).toHaveBeenCalled();
+ });
+ });
+
+ describe("MinIO configuration handling", () => {
+ it("should not enable makeLocalChunkBeforeSync when remote is MinIO", () => {
+ const host = createHostMock();
+ host.mocks.setting.settings.remoteType = REMOTE_MINIO;
+
+ const settings = host.mocks.setting.currentSettings();
+ const isMinIO = settings.remoteType === REMOTE_MINIO;
+
+ expect(isMinIO).toBe(true);
+ });
+
+ it("should enable makeLocalChunkBeforeSync for non-MinIO remotes", () => {
+ const host = createHostMock();
+ host.mocks.setting.settings.remoteType = "CouchDB";
+
+ const settings = host.mocks.setting.currentSettings();
+ const isMinIO = settings.remoteType === REMOTE_MINIO;
+
+ expect(isMinIO).toBe(false);
+ });
+ });
+
+ describe("Suspension unlock verification", () => {
+ it("should return true when suspension is not active", async () => {
+ const host = createHostMock();
+
+ const result = await verifyAndUnlockSuspension(host as any, createLoggerMock());
+ expect(result).toBe(true);
+ });
+
+ it("should ask for confirmation when suspension is active", async () => {
+ const host = createHostMock();
+
+ await host.mocks.setting.applyPartial({ suspendFileWatching: true });
+
+ host.mocks.ui.confirm.askYesNoDialog.mockResolvedValueOnce("yes");
+
+ await verifyAndUnlockSuspension(host as any, createLoggerMock());
+
+ expect(host.mocks.ui.confirm.askYesNoDialog).toHaveBeenCalled();
+ });
+
+ it("should return true when user declines suspension unlock", async () => {
+ const host = createHostMock();
+
+ await host.mocks.setting.applyPartial({ suspendFileWatching: true });
+ host.mocks.ui.confirm.askYesNoDialog.mockResolvedValueOnce("no");
+
+ const result = await verifyAndUnlockSuspension(host as any, createLoggerMock());
+
+ expect(result).toBe(true);
+ });
+
+ it("should resume file watching and restart when user accepts", async () => {
+ const host = createHostMock();
+
+ await host.mocks.setting.applyPartial({ suspendFileWatching: true }, true);
+ host.mocks.ui.confirm.askYesNoDialog.mockResolvedValueOnce("yes");
+
+ await verifyAndUnlockSuspension(host as any, createLoggerMock());
+
+ expect(host.mocks.appLifecycle.performRestart).toHaveBeenCalled();
+ });
+ });
+
+ describe("Error handling in vault initialization", () => {
+ it("should handle errors during initialisation gracefully", async () => {
+ const host = createHostMock();
+ const log = createLoggerMock();
+
+ const result = await processVaultInitialisation(host as any, log, () => {
+ throw new Error("Initialization failed");
+ });
+
+ expect(result).toBe(false);
+ expect(log).toHaveBeenCalled();
+ });
+
+ it("should track log calls during error conditions", async () => {
+ const host = createHostMock();
+ const log = createLoggerMock();
+
+ await processVaultInitialisation(host as any, log, () => {
+ throw new Error("Test error");
+ });
+
+ expect(log).toHaveBeenCalled();
+ });
+
+ it("should keep suspension state when error occurs during initialization", async () => {
+ const host = createHostMock();
+ const log = createLoggerMock();
+
+ await processVaultInitialisation(
+ host as any,
+ log,
+ () => {
+ return Promise.resolve(false);
+ },
+ true
+ );
+
+ expect(host.mocks.setting.currentSettings().suspendFileWatching).toBe(true);
+ });
+
+ it("should handle applySetting error in processVaultInitialisation", async () => {
+ const host = createHostMock();
+ const log = createLoggerMock();
+
+ host.mocks.setting.applyPartial.mockRejectedValueOnce(new Error("Apply partial failed"));
+
+ const result = await processVaultInitialisation(host as any, log, () => {
+ return Promise.resolve(true);
+ });
+
+ expect(result).toBe(false);
+ });
+ });
+
+ describe("Flag file detection with both formats", () => {
+ it("should detect either original or human-readable fetch all flag", async () => {
+ const host = createHostMock();
+
+ // Add only human-readable flag
+ host.mocks.storageAccess.files.add(FlagFilesHumanReadable.FETCH_ALL);
+
+ const humanExists = await host.mocks.storageAccess.isExists(
+ host.mocks.storageAccess.normalisePath(FlagFilesHumanReadable.FETCH_ALL)
+ );
+
+ expect(humanExists).toBe(true);
+ });
+
+ it("should detect either original or human-readable rebuild flag", async () => {
+ const host = createHostMock();
+
+ // Add only original flag
+ host.mocks.storageAccess.files.add(FlagFilesOriginal.REBUILD_ALL);
+
+ const originalExists = await host.mocks.storageAccess.isExists(
+ host.mocks.storageAccess.normalisePath(FlagFilesOriginal.REBUILD_ALL)
+ );
+
+ expect(originalExists).toBe(true);
+ });
+ });
+
+ describe("Handler execution", () => {
+ it("should execute fetch all handler check method", async () => {
+ const host = createHostMock();
+ const log = createLoggerMock();
+
+ host.mocks.storageAccess.files.add(FlagFilesOriginal.FETCH_ALL);
+
+ const handler = createFetchAllFlagHandler(host as any, log);
+ const result = await handler.check();
+
+ expect(result).toBe(true);
+ });
+
+ it("should execute rebuild handler check method", async () => {
+ const host = createHostMock();
+ const log = createLoggerMock();
+
+ host.mocks.storageAccess.files.add(FlagFilesOriginal.REBUILD_ALL);
+
+ const handler = createRebuildFlagHandler(host as any, log);
+ const result = await handler.check();
+
+ expect(result).toBe(true);
+ });
+
+ it("should execute suspend handler check method", async () => {
+ const host = createHostMock();
+ const log = createLoggerMock();
+
+ host.mocks.storageAccess.files.add(FlagFilesOriginal.SUSPEND_ALL);
+
+ const handler = createSuspendFlagHandler(host as any, log);
+ const result = await handler.check();
+
+ expect(result).toBe(true);
+ });
+
+ it("should return false when flag does not exist", async () => {
+ const host = createHostMock();
+ const log = createLoggerMock();
+
+ const handler = createFetchAllFlagHandler(host as any, log);
+ const result = await handler.check();
+
+ expect(result).toBe(false);
+ });
+
+ it("should return correct priority for each handler", () => {
+ const host = createHostMock();
+ const log = createLoggerMock();
+
+ const suspendHandler = createSuspendFlagHandler(host as any, log);
+ const fetchHandler = createFetchAllFlagHandler(host as any, log);
+ const rebuildHandler = createRebuildFlagHandler(host as any, log);
+
+ expect(suspendHandler.priority).toBe(5);
+ expect(fetchHandler.priority).toBe(10);
+ expect(rebuildHandler.priority).toBe(20);
+ });
+
+ it("should handle suspend flag and execute handler", async () => {
+ const host = createHostMock();
+ const log = createLoggerMock();
+
+ host.mocks.storageAccess.files.add(FlagFilesOriginal.SUSPEND_ALL);
+
+ const handler = createSuspendFlagHandler(host as any, log);
+ const checkResult = await handler.check();
+
+ expect(checkResult).toBe(true);
+ expect(typeof handler.handle).toBe("function");
+ });
+
+ it("should have handle method for all handlers", () => {
+ const host = createHostMock();
+ const log = createLoggerMock();
+
+ const suspendHandler = createSuspendFlagHandler(host as any, log);
+ const fetchHandler = createFetchAllFlagHandler(host as any, log);
+ const rebuildHandler = createRebuildFlagHandler(host as any, log);
+
+ expect(typeof suspendHandler.handle).toBe("function");
+ expect(typeof fetchHandler.handle).toBe("function");
+ expect(typeof rebuildHandler.handle).toBe("function");
+ });
+ });
+
+ describe("Multiple concurrent operations", () => {
+ it("should handle multiple flag files existing simultaneously", async () => {
+ const host = createHostMock();
+
+ host.mocks.storageAccess.files.add(FlagFilesOriginal.FETCH_ALL);
+ host.mocks.storageAccess.files.add(FlagFilesOriginal.REBUILD_ALL);
+ host.mocks.storageAccess.files.add(FlagFilesOriginal.SUSPEND_ALL);
+
+ const fetchExists = await host.mocks.storageAccess.isExists(
+ host.mocks.storageAccess.normalisePath(FlagFilesOriginal.FETCH_ALL)
+ );
+ const rebuildExists = await host.mocks.storageAccess.isExists(
+ host.mocks.storageAccess.normalisePath(FlagFilesOriginal.REBUILD_ALL)
+ );
+ const suspendExists = await host.mocks.storageAccess.isExists(
+ host.mocks.storageAccess.normalisePath(FlagFilesOriginal.SUSPEND_ALL)
+ );
+
+ expect(fetchExists).toBe(true);
+ expect(rebuildExists).toBe(true);
+ expect(suspendExists).toBe(true);
+ });
+
+ it("should cleanup all flags when processing completes", async () => {
+ const host = createHostMock();
+
+ host.mocks.storageAccess.files.add(FlagFilesHumanReadable.FETCH_ALL);
+ host.mocks.storageAccess.files.add(FlagFilesOriginal.FETCH_ALL);
+
+ await host.mocks.storageAccess.delete(FlagFilesHumanReadable.FETCH_ALL);
+ await host.mocks.storageAccess.delete(FlagFilesOriginal.FETCH_ALL);
+
+ expect(host.mocks.storageAccess.files.size).toBe(0);
+ });
+ });
+
+ describe("Setting state transitions", () => {
+ it("should apply complete state transition for initialization", async () => {
+ const host = createHostMock();
+
+ // Initial state
+ const initialState = {
+ batchSave: host.mocks.setting.currentSettings().batchSave,
+ suspendFileWatching: host.mocks.setting.currentSettings().suspendFileWatching,
+ };
+
+ // Initialization state
+ await host.mocks.setting.applyPartial(
+ {
+ batchSave: false,
+ suspendFileWatching: true,
+ },
+ true
+ );
+
+ const initState = {
+ batchSave: host.mocks.setting.currentSettings().batchSave,
+ suspendFileWatching: host.mocks.setting.currentSettings().suspendFileWatching,
+ };
+
+ // Post-initialization state
+ await host.mocks.setting.applyPartial(
+ {
+ batchSave: true,
+ suspendFileWatching: false,
+ },
+ true
+ );
+
+ const finalState = {
+ batchSave: host.mocks.setting.currentSettings().batchSave,
+ suspendFileWatching: host.mocks.setting.currentSettings().suspendFileWatching,
+ };
+
+ expect(initialState.batchSave).toBe(true);
+ expect(initialState.suspendFileWatching).toBe(false);
+
+ expect(initState.batchSave).toBe(false);
+ expect(initState.suspendFileWatching).toBe(true);
+
+ expect(finalState.batchSave).toBe(true);
+ expect(finalState.suspendFileWatching).toBe(false);
+ });
+ });
+
+ describe("flagHandlerToEventHandler integration", () => {
+ it("should return true when flag does not exist", async () => {
+ const host = createHostMock();
+ const log = createLoggerMock();
+
+ const handler = createFetchAllFlagHandler(host as any, log);
+ const eventHandler = flagHandlerToEventHandler(handler);
+
+ const result = await eventHandler();
+ expect(result).toBe(true);
+ });
+
+ it("should execute handle when flag exists and check returns true", async () => {
+ const host = createHostMock();
+ const log = createLoggerMock();
+
+ host.mocks.storageAccess.files.add(FlagFilesOriginal.FETCH_ALL);
+ host.mocks.ui.dialogManager.openWithExplicitCancel.mockResolvedValueOnce("cancelled");
+
+ const handler = createFetchAllFlagHandler(host as any, log);
+ const eventHandler = flagHandlerToEventHandler(handler);
+
+ await eventHandler();
+
+ // When dialog is cancelled, handle returns false
+ expect(host.mocks.ui.dialogManager.openWithExplicitCancel).toHaveBeenCalled();
+ });
+
+ it("should return handle result when flag exists", async () => {
+ const host = createHostMock();
+ const log = createLoggerMock();
+
+ host.mocks.storageAccess.files.add(FlagFilesOriginal.SUSPEND_ALL);
+
+ const handler = createSuspendFlagHandler(host as any, log);
+ const eventHandler = flagHandlerToEventHandler(handler);
+
+ const result = await eventHandler();
+
+ // Suspend handler execution results in false from processVaultInitialisation
+ expect(typeof result).toBe("boolean");
+ });
+
+ it("should not call handle when check returns false", async () => {
+ const host = createHostMock();
+ const log = createLoggerMock();
+
+ const handler = createRebuildFlagHandler(host as any, log);
+ const handleSpy = vi.spyOn(handler, "handle");
+ const eventHandler = flagHandlerToEventHandler(handler);
+
+ const result = await eventHandler();
+
+ // Check returns false because no rebuild flag exists
+ expect(handleSpy).not.toHaveBeenCalled();
+ expect(result).toBe(true);
+ });
+
+ it("should handle rebuild flag with flagHandlerToEventHandler", async () => {
+ const host = createHostMock();
+ const log = createLoggerMock();
+
+ host.mocks.storageAccess.files.add(FlagFilesOriginal.REBUILD_ALL);
+ host.mocks.ui.dialogManager.openWithExplicitCancel.mockResolvedValueOnce("cancelled");
+
+ const handler = createRebuildFlagHandler(host as any, log);
+ const eventHandler = flagHandlerToEventHandler(handler);
+
+ await eventHandler();
+
+ expect(host.mocks.ui.dialogManager.openWithExplicitCancel).toHaveBeenCalled();
+ });
+
+ it("should propagate errors from handle method", async () => {
+ const host = createHostMock();
+ const log = createLoggerMock();
+
+ host.mocks.storageAccess.files.add(FlagFilesOriginal.FETCH_ALL);
+ host.mocks.ui.dialogManager.openWithExplicitCancel.mockRejectedValueOnce(new Error("Dialog failed"));
+
+ const handler = createFetchAllFlagHandler(host as any, log);
+ const eventHandler = flagHandlerToEventHandler(handler);
+
+ try {
+ await eventHandler();
+ } catch (error) {
+ expect((error as Error).message).toBe("Dialog failed");
+ }
+ });
+
+ it("should handle fetchAll flag with flagHandlerToEventHandler identical", async () => {
+ const host = createHostMock();
+ const log = createLoggerMock();
+ host.mocks.tweakValue.fetchRemotePreferred.mockResolvedValueOnce({
+ customChunkSize: 1,
+ } as any);
+
+ host.mocks.storageAccess.files.add(FlagFilesOriginal.FETCH_ALL);
+ host.mocks.ui.dialogManager.openWithExplicitCancel.mockResolvedValueOnce({ vault: "identical", extra: {} });
+ host.mocks.rebuilder.$fetchLocal.mockResolvedValueOnce();
+ const handler = createFetchAllFlagHandler(host as any, log);
+ const eventHandler = flagHandlerToEventHandler(handler);
+
+ await Promise.resolve(eventHandler());
+ await new Promise((resolve) => setTimeout(resolve, 10));
+ expect(host.mocks.rebuilder.$fetchLocal).toHaveBeenCalled();
+
+ expect(host.mocks.ui.dialogManager.openWithExplicitCancel).toHaveBeenCalled();
+ });
+ it("should handle rebuildAll flag with flagHandlerToEventHandler", async () => {
+ const host = createHostMock();
+ const log = createLoggerMock();
+ host.mocks.tweakValue.fetchRemotePreferred.mockResolvedValueOnce({
+ customChunkSize: 1,
+ } as any);
+
+ host.mocks.storageAccess.files.add(FlagFilesOriginal.REBUILD_ALL);
+ host.mocks.ui.dialogManager.openWithExplicitCancel.mockResolvedValueOnce({ extra: {} });
+ host.mocks.rebuilder.$rebuildEverything.mockResolvedValueOnce();
+ const handler = createRebuildFlagHandler(host as any, log);
+ const eventHandler = flagHandlerToEventHandler(handler);
+
+ await Promise.resolve(eventHandler());
+ await new Promise((resolve) => setTimeout(resolve, 10));
+ expect(host.mocks.rebuilder.$rebuildEverything).toHaveBeenCalled();
+
+ expect(host.mocks.ui.dialogManager.openWithExplicitCancel).toHaveBeenCalled();
+ });
+
+ it("should execute all handlers in sequence", async () => {
+ const host = createHostMock();
+ const log = createLoggerMock();
+
+ const suspendHandler = createSuspendFlagHandler(host as any, log);
+ const fetchHandler = createFetchAllFlagHandler(host as any, log);
+ const rebuildHandler = createRebuildFlagHandler(host as any, log);
+
+ const suspendEvent = flagHandlerToEventHandler(suspendHandler);
+ const fetchEvent = flagHandlerToEventHandler(fetchHandler);
+ const rebuildEvent = flagHandlerToEventHandler(rebuildHandler);
+
+ // All should return true when flags don't exist
+ expect(await suspendEvent()).toBe(true);
+ expect(await fetchEvent()).toBe(true);
+ expect(await rebuildEvent()).toBe(true);
+ });
+
+ it("should return false from handle when suspending", async () => {
+ const host = createHostMock();
+ const log = createLoggerMock();
+
+ host.mocks.storageAccess.files.add(FlagFilesOriginal.SUSPEND_ALL);
+
+ const handler = createSuspendFlagHandler(host as any, log);
+ const eventHandler = flagHandlerToEventHandler(handler);
+
+ const result = await eventHandler();
+
+ // Suspend handler returns false from its handle method
+ expect(result).toBe(false);
+ });
+
+ it("should handle check error gracefully", async () => {
+ const host = createHostMock();
+ const log = createLoggerMock();
+
+ host.mocks.storageAccess.isExists.mockRejectedValueOnce(new Error("Check failed"));
+
+ const handler = createFetchAllFlagHandler(host as any, log);
+ const eventHandler = flagHandlerToEventHandler(handler);
+
+ try {
+ await eventHandler();
+ } catch (error) {
+ expect((error as Error).message).toBe("Check failed");
+ }
+ });
+ });
+});
diff --git a/styles.css b/styles.css
index 72e7ade..0125ecc 100644
--- a/styles.css
+++ b/styles.css
@@ -1,13 +1,13 @@
-.op-scrollable .added {
+.ls-dialog .added {
color: var(--text-on-accent);
background-color: var(--text-accent);
}
-.op-scrollable .normal {
+.ls-dialog .normal {
color: var(--text-normal);
}
-.op-scrollable .deleted {
+.ls-dialog .deleted {
color: var(--text-on-accent);
background-color: var(--text-muted);
}
|