mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-07-05 04:15:20 +00:00
Merge remote-tracking branch 'origin/main' into test/obsidian-real-e2e
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "self-hosted-livesync-cli",
|
||||
"private": true,
|
||||
"version": "0.25.78-cli",
|
||||
"version": "0.25.79-cli",
|
||||
"main": "dist/index.cjs",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
@@ -41,7 +41,7 @@
|
||||
"dependencies": {
|
||||
"chokidar": "^4.0.0",
|
||||
"minimatch": "^10.2.5",
|
||||
"octagonal-wheels": "^0.1.46",
|
||||
"octagonal-wheels": "^0.1.47",
|
||||
"pouchdb-adapter-http": "^9.0.0",
|
||||
"pouchdb-adapter-leveldb": "^9.0.0",
|
||||
"pouchdb-core": "^9.0.0",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "livesync-webapp",
|
||||
"private": true,
|
||||
"version": "0.25.78-webapp",
|
||||
"version": "0.25.79-webapp",
|
||||
"type": "module",
|
||||
"description": "Browser-based Self-hosted LiveSync using FileSystem API",
|
||||
"scripts": {
|
||||
@@ -12,7 +12,7 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"octagonal-wheels": "^0.1.46"
|
||||
"octagonal-wheels": "^0.1.47"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.58.2",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "webpeer",
|
||||
"private": true,
|
||||
"version": "0.25.78-webpeer",
|
||||
"version": "0.25.79-webpeer",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@@ -12,7 +12,7 @@
|
||||
"check": "svelte-check --tsconfig ./tsconfig.app.json && tsc -p tsconfig.node.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"octagonal-wheels": "^0.1.46"
|
||||
"octagonal-wheels": "^0.1.47"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint-plugin-svelte": "^3.19.0",
|
||||
|
||||
@@ -18,6 +18,7 @@ import { EVENT_ANALYSE_DB_USAGE, EVENT_REQUEST_PERFORM_GC_V3, eventHub } from "@
|
||||
import type { LiveSyncCouchDBReplicator } from "@lib/replication/couchdb/LiveSyncReplicator";
|
||||
import { delay } from "@lib/common/utils";
|
||||
import { isNotFoundError } from "@lib/common/utils.doc";
|
||||
import { ensureLocalDatabaseMaintenancePrerequisites } from "./maintenancePrerequisites";
|
||||
// import { _requestToCouchDB } from "@/common/utils";
|
||||
const DB_KEY_SEQ = "gc-seq";
|
||||
const DB_KEY_CHUNK_SET = "chunk-set";
|
||||
@@ -77,22 +78,22 @@ export class LocalDatabaseMaintenance extends LiveSyncCommands {
|
||||
})) === affirmative
|
||||
);
|
||||
}
|
||||
isAvailable() {
|
||||
if (!this.settings.doNotUseFixedRevisionForChunks) {
|
||||
this._notice("Please enable 'Compute revisions for chunks' in settings to use Garbage Collection.");
|
||||
return false;
|
||||
}
|
||||
if (this.settings.readChunksOnline) {
|
||||
this._notice("Please disable 'Read chunks online' in settings to use Garbage Collection.");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
async ensureAvailable(operationName: string) {
|
||||
return await ensureLocalDatabaseMaintenancePrerequisites({
|
||||
operationName,
|
||||
settings: this.settings,
|
||||
askSelectStringDialogue: this.core.confirm.askSelectStringDialogue.bind(this.core.confirm),
|
||||
applyPartial: async (settings, saveImmediately) => {
|
||||
await this.core.services.setting.applyPartial(settings, saveImmediately);
|
||||
Object.assign(this.core.settings, settings);
|
||||
},
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Resurrect deleted chunks that are still used in the database.
|
||||
*/
|
||||
async resurrectChunks() {
|
||||
if (!this.isAvailable()) return;
|
||||
if (!(await this.ensureAvailable("Resurrect Chunks"))) return;
|
||||
const { used, existing } = await this.allChunks(true);
|
||||
const excessiveDeletions = [...existing]
|
||||
.filter(([key, e]) => e._deleted)
|
||||
@@ -157,7 +158,7 @@ Do you want to resurrect these chunks?`;
|
||||
* After this, chunks that are used in the deleted files become ready for compaction.
|
||||
*/
|
||||
async commitFileDeletion() {
|
||||
if (!this.isAvailable()) return;
|
||||
if (!(await this.ensureAvailable("Delete Files"))) return;
|
||||
const p = this._progress("", LOG_LEVEL_NOTICE);
|
||||
p.log("Searching for deleted files..");
|
||||
const docs = await this.database.allDocs<MetaEntry>({ include_docs: true });
|
||||
@@ -199,7 +200,7 @@ Note: **Make sure to synchronise all devices before deletion.**
|
||||
* It is recommended to compact the database after this operation (History should be kept once before compaction).
|
||||
*/
|
||||
async commitChunkDeletion() {
|
||||
if (!this.isAvailable()) return;
|
||||
if (!(await this.ensureAvailable("Delete Chunks"))) return;
|
||||
const { existing } = await this.allChunks(true);
|
||||
const deletedChunks = [...existing].filter(([key, e]) => e._deleted && e.data !== "").map(([key, e]) => e);
|
||||
const deletedNotVacantChunks = deletedChunks.map((e) => ({ ...e, data: "", _deleted: true }));
|
||||
@@ -236,7 +237,7 @@ Note: **Make sure to synchronise all devices before deletion.**
|
||||
* Make sure all devices are synchronized before running this method.
|
||||
*/
|
||||
async markUnusedChunks() {
|
||||
if (!this.isAvailable()) return;
|
||||
if (!(await this.ensureAvailable("Mark unused chunks"))) return;
|
||||
const { used, existing } = await this.allChunks();
|
||||
const existChunks = [...existing];
|
||||
const unusedChunks = existChunks.filter(([key, e]) => !used.has(e._id)).map(([key, e]) => e);
|
||||
@@ -269,6 +270,7 @@ Note: **Make sure to synchronise all devices before deletion.**
|
||||
}
|
||||
|
||||
async removeUnusedChunks() {
|
||||
if (!(await this.ensureAvailable("Delete unused chunks"))) return;
|
||||
const { used, existing } = await this.allChunks();
|
||||
const existChunks = [...existing];
|
||||
const unusedChunks = existChunks.filter(([key, e]) => !used.has(e._id)).map(([key, e]) => e);
|
||||
@@ -326,7 +328,7 @@ Note: **Make sure to synchronise all devices before deletion.**
|
||||
* Note that this only able to perform without Fetch chunks on demand.
|
||||
*/
|
||||
async trackChanges(fromStart: boolean = false, showNotice: boolean = false) {
|
||||
if (!this.isAvailable()) return;
|
||||
if (!(await this.ensureAvailable("Track chunk usage"))) return;
|
||||
const logLevel = showNotice ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO;
|
||||
const kvDB = this.core.kvDB;
|
||||
|
||||
@@ -442,7 +444,7 @@ Note: **Make sure to synchronise all devices before deletion.**
|
||||
this._log(message, logLevel);
|
||||
}
|
||||
async performGC(showingNotice = false) {
|
||||
if (!this.isAvailable()) return;
|
||||
if (!(await this.ensureAvailable("Garbage Collection"))) return;
|
||||
await this.trackChanges(false, showingNotice);
|
||||
const title = "Are all devices synchronised?";
|
||||
const confirmMessage = `This function deletes unused chunks from the device. If there are differences between devices, some chunks may be missing when resolving conflicts.
|
||||
@@ -512,7 +514,7 @@ Success: ${successCount}, Errored: ${errored}`;
|
||||
|
||||
// Analyse the database and report chunk usage.
|
||||
async analyseDatabase() {
|
||||
if (!this.isAvailable()) return;
|
||||
if (!(await this.ensureAvailable("Analyse Database Usage"))) return;
|
||||
const db = this.localDatabase.localDatabase;
|
||||
// Map of chunk ID to its info
|
||||
type ChunkInfo = {
|
||||
@@ -822,7 +824,7 @@ Success: ${successCount}, Errored: ${errored}`;
|
||||
// }
|
||||
// }
|
||||
async gcv3() {
|
||||
if (!this.isAvailable()) return;
|
||||
if (!(await this.ensureAvailable("Garbage Collection"))) return;
|
||||
const replicator = this.core.replicator as LiveSyncCouchDBReplicator;
|
||||
// Start one-shot replication to ensure all changes are synced before GC.
|
||||
const r0 = await replicator.openOneShotReplication(this.settings, false, false, "sync");
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { DEFAULT_SETTINGS } from "@lib/common/types";
|
||||
import { ensureLocalDatabaseMaintenancePrerequisites } from "./maintenancePrerequisites";
|
||||
|
||||
function createPrerequisites(settingsOverride: Partial<typeof DEFAULT_SETTINGS> = {}) {
|
||||
const askSelectStringDialogue = vi.fn<() => Promise<"Apply and continue" | "Cancel" | false | undefined>>(
|
||||
async () => "Apply and continue"
|
||||
);
|
||||
const applyPartial = vi.fn(async () => undefined);
|
||||
const settings = {
|
||||
...DEFAULT_SETTINGS,
|
||||
doNotUseFixedRevisionForChunks: false,
|
||||
readChunksOnline: true,
|
||||
...settingsOverride,
|
||||
};
|
||||
|
||||
return { settings, askSelectStringDialogue, applyPartial };
|
||||
}
|
||||
|
||||
describe("LocalDatabaseMaintenance prerequisites", () => {
|
||||
it("asks to apply missing prerequisite settings before maintenance actions", async () => {
|
||||
const { settings, askSelectStringDialogue, applyPartial } = createPrerequisites();
|
||||
|
||||
const result = await ensureLocalDatabaseMaintenancePrerequisites({
|
||||
operationName: "Garbage Collection",
|
||||
settings: {
|
||||
doNotUseFixedRevisionForChunks: settings.doNotUseFixedRevisionForChunks,
|
||||
readChunksOnline: settings.readChunksOnline,
|
||||
},
|
||||
askSelectStringDialogue,
|
||||
applyPartial,
|
||||
});
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(askSelectStringDialogue).toHaveBeenCalledWith(
|
||||
expect.stringContaining("Garbage Collection requires the following settings"),
|
||||
["Apply and continue", "Cancel"],
|
||||
{
|
||||
title: "Garbage Collection prerequisites",
|
||||
defaultAction: "Cancel",
|
||||
}
|
||||
);
|
||||
expect(applyPartial).toHaveBeenCalledWith(
|
||||
{
|
||||
doNotUseFixedRevisionForChunks: true,
|
||||
readChunksOnline: false,
|
||||
},
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
it("cancels maintenance actions when prerequisite changes are rejected", async () => {
|
||||
const { settings, askSelectStringDialogue, applyPartial } = createPrerequisites();
|
||||
askSelectStringDialogue.mockResolvedValueOnce("Cancel");
|
||||
|
||||
const result = await ensureLocalDatabaseMaintenancePrerequisites({
|
||||
operationName: "Garbage Collection",
|
||||
settings: {
|
||||
doNotUseFixedRevisionForChunks: settings.doNotUseFixedRevisionForChunks,
|
||||
readChunksOnline: settings.readChunksOnline,
|
||||
},
|
||||
askSelectStringDialogue,
|
||||
applyPartial,
|
||||
});
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(applyPartial).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("continues without asking when prerequisite settings already match", async () => {
|
||||
const { settings, askSelectStringDialogue, applyPartial } = createPrerequisites({
|
||||
doNotUseFixedRevisionForChunks: true,
|
||||
readChunksOnline: false,
|
||||
});
|
||||
|
||||
const result = await ensureLocalDatabaseMaintenancePrerequisites({
|
||||
operationName: "Garbage Collection",
|
||||
settings: {
|
||||
doNotUseFixedRevisionForChunks: settings.doNotUseFixedRevisionForChunks,
|
||||
readChunksOnline: settings.readChunksOnline,
|
||||
},
|
||||
askSelectStringDialogue,
|
||||
applyPartial,
|
||||
});
|
||||
|
||||
expect(askSelectStringDialogue).not.toHaveBeenCalled();
|
||||
expect(applyPartial).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,54 @@
|
||||
import type { ObsidianLiveSyncSettings } from "@lib/common/types";
|
||||
|
||||
type MaintenancePrerequisiteSettings = Pick<
|
||||
ObsidianLiveSyncSettings,
|
||||
"doNotUseFixedRevisionForChunks" | "readChunksOnline"
|
||||
>;
|
||||
|
||||
type MaintenancePrerequisiteOptions = {
|
||||
operationName: string;
|
||||
settings: MaintenancePrerequisiteSettings;
|
||||
askSelectStringDialogue: (
|
||||
message: string,
|
||||
buttons: readonly ["Apply and continue", "Cancel"],
|
||||
options: { title: string; defaultAction: "Cancel" }
|
||||
) => Promise<"Apply and continue" | "Cancel" | false | undefined>;
|
||||
applyPartial: (settings: Partial<ObsidianLiveSyncSettings>, saveImmediately?: boolean) => Promise<void>;
|
||||
};
|
||||
|
||||
export async function ensureLocalDatabaseMaintenancePrerequisites({
|
||||
operationName,
|
||||
settings,
|
||||
askSelectStringDialogue,
|
||||
applyPartial,
|
||||
}: MaintenancePrerequisiteOptions): Promise<boolean> {
|
||||
const requiredSettings = {
|
||||
doNotUseFixedRevisionForChunks: true,
|
||||
readChunksOnline: false,
|
||||
} satisfies MaintenancePrerequisiteSettings;
|
||||
|
||||
const missing = [
|
||||
...(settings.doNotUseFixedRevisionForChunks ? [] : ["- Compute revisions for chunks: On (currently Off)"]),
|
||||
...(settings.readChunksOnline ? ["- Fetch chunks on demand: Off (currently On)"] : []),
|
||||
];
|
||||
|
||||
if (missing.length == 0) return true;
|
||||
|
||||
const APPLY = "Apply and continue";
|
||||
const CANCEL = "Cancel";
|
||||
const result = await askSelectStringDialogue(
|
||||
`${operationName} requires the following settings:\n\n${missing.join(
|
||||
"\n"
|
||||
)}\n\nApply these settings and continue?`,
|
||||
[APPLY, CANCEL],
|
||||
{
|
||||
title: `${operationName} prerequisites`,
|
||||
defaultAction: CANCEL,
|
||||
}
|
||||
);
|
||||
|
||||
if (result !== APPLY) return false;
|
||||
|
||||
await applyPartial(requiredSettings, true);
|
||||
return true;
|
||||
}
|
||||
+1
-1
Submodule src/lib updated: 0563f267ec...87dc724c9d
@@ -24,9 +24,73 @@ export const SIMPLE_FETCH_STAGE2_NEWER_CLEANUP = "Delete local files if deleted
|
||||
export const SIMPLE_FETCH_STAGE2_NEWER_SYNC_ALL = "Keep local files even if deleted on remote";
|
||||
export const STAGE2_ABORT = "Cancel all and reboot";
|
||||
|
||||
const SIMPLE_FETCH_MODE_KEY = "simple-fetch-mode";
|
||||
|
||||
function buildSimpleFetchResult(stage1: string, stage2?: string) {
|
||||
if (stage1 === SIMPLE_FETCH_STAGE1_LEGACY) {
|
||||
return { mode: "legacy", options: {} };
|
||||
}
|
||||
if (stage1 === SIMPLE_FETCH_STAGE1_REMOTE_WINS && stage2) {
|
||||
if (![SIMPLE_FETCH_STAGE2_REMOTE_DELETE_ALL, SIMPLE_FETCH_STAGE2_REMOTE_DELETE_NONE].includes(stage2)) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
mode: "remote-only",
|
||||
options: {
|
||||
mode: FullScanModes.DB_APPLY,
|
||||
extraOnRemote:
|
||||
stage2 === SIMPLE_FETCH_STAGE2_REMOTE_DELETE_ALL ? ExtraOnRemote.DELETE_LOCAL_MISSING : undefined,
|
||||
},
|
||||
};
|
||||
}
|
||||
if (stage1 === SIMPLE_FETCH_STAGE1_NEWER_WINS && stage2) {
|
||||
if (![SIMPLE_FETCH_STAGE2_NEWER_CLEANUP, SIMPLE_FETCH_STAGE2_NEWER_SYNC_ALL].includes(stage2)) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
mode: "newer-wins",
|
||||
options: {
|
||||
mode: FullScanModes.NEWER_WINS,
|
||||
extraOnLocal:
|
||||
stage2 === SIMPLE_FETCH_STAGE2_NEWER_CLEANUP
|
||||
? ExtraOnLocal.DELETE_DB_DELETED
|
||||
: ExtraOnLocal.APPEND_STORAGE_ONLY,
|
||||
},
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function rememberSimpleFetchMode(host: NecessaryServices<"setting", never>, stage1: string, stage2?: string) {
|
||||
host.services.setting.setSmallConfig(SIMPLE_FETCH_MODE_KEY, JSON.stringify({ stage1, stage2 }));
|
||||
}
|
||||
|
||||
function getRememberedSimpleFetchMode(host: NecessaryServices<"setting", never>) {
|
||||
const saved = host.services.setting.getSmallConfig(SIMPLE_FETCH_MODE_KEY);
|
||||
if (!saved) return undefined;
|
||||
try {
|
||||
const { stage1, stage2 } = JSON.parse(saved) as { stage1?: string; stage2?: string };
|
||||
if (stage1) {
|
||||
const remembered = buildSimpleFetchResult(stage1, stage2);
|
||||
if (remembered) return remembered;
|
||||
}
|
||||
} catch {
|
||||
// Clear below; the saved choice is optional and can be rebuilt by asking again.
|
||||
}
|
||||
host.services.setting.deleteSmallConfig(SIMPLE_FETCH_MODE_KEY);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function clearRememberedSimpleFetchMode(host: NecessaryServices<"setting", never>) {
|
||||
host.services.setting.deleteSmallConfig(SIMPLE_FETCH_MODE_KEY);
|
||||
}
|
||||
|
||||
export async function askSimpleFetchMode(
|
||||
host: NecessaryServices<"UI" | "vault", "storageAccess">
|
||||
host: NecessaryServices<"UI" | "setting", never>
|
||||
): Promise<{ mode: string; options: Partial<FullScanOptions> } | "cancelled" | "aborted"> {
|
||||
const remembered = getRememberedSimpleFetchMode(host);
|
||||
if (remembered) return remembered;
|
||||
|
||||
const msg = `We are about to retrieve the remote data.
|
||||
|
||||
Firstly, how shall we handle the data retrieved from this remote server?
|
||||
@@ -55,7 +119,7 @@ Firstly, how shall we handle the data retrieved from this remote server?
|
||||
if (!stage1 || stage1 === SIMPLE_FETCH_STAGE1_CANCEL) return "cancelled";
|
||||
|
||||
if (stage1 === SIMPLE_FETCH_STAGE1_LEGACY) {
|
||||
return { mode: "legacy", options: {} };
|
||||
return buildSimpleFetchResult(stage1)!;
|
||||
}
|
||||
|
||||
if (stage1 === SIMPLE_FETCH_STAGE1_REMOTE_WINS) {
|
||||
@@ -77,14 +141,8 @@ Firstly, how shall we handle the data retrieved from this remote server?
|
||||
if (stage2 === STAGE2_ABORT) {
|
||||
return "aborted";
|
||||
}
|
||||
return {
|
||||
mode: "remote-only",
|
||||
options: {
|
||||
mode: FullScanModes.DB_APPLY,
|
||||
extraOnRemote:
|
||||
stage2 === SIMPLE_FETCH_STAGE2_REMOTE_DELETE_ALL ? ExtraOnRemote.DELETE_LOCAL_MISSING : undefined,
|
||||
},
|
||||
};
|
||||
rememberSimpleFetchMode(host, stage1, stage2);
|
||||
return buildSimpleFetchResult(stage1, stage2)!;
|
||||
}
|
||||
|
||||
if (stage1 === SIMPLE_FETCH_STAGE1_NEWER_WINS) {
|
||||
@@ -107,16 +165,8 @@ Firstly, how shall we handle the data retrieved from this remote server?
|
||||
if (stage2 === STAGE2_ABORT) {
|
||||
return "aborted";
|
||||
}
|
||||
return {
|
||||
mode: "newer-wins",
|
||||
options: {
|
||||
mode: FullScanModes.NEWER_WINS,
|
||||
extraOnLocal:
|
||||
stage2 === SIMPLE_FETCH_STAGE2_NEWER_CLEANUP
|
||||
? ExtraOnLocal.DELETE_DB_DELETED
|
||||
: ExtraOnLocal.APPEND_STORAGE_ONLY,
|
||||
},
|
||||
};
|
||||
rememberSimpleFetchMode(host, stage1, stage2);
|
||||
return buildSimpleFetchResult(stage1, stage2)!;
|
||||
}
|
||||
|
||||
return "cancelled";
|
||||
@@ -143,12 +193,14 @@ export async function askAndPerformFastSetupOnScheduledFetchAll(
|
||||
const result = await askSimpleFetchMode(host);
|
||||
if (result === "cancelled") {
|
||||
log("Fetch cancelled by user.", LOG_LEVEL_NOTICE);
|
||||
clearRememberedSimpleFetchMode(host);
|
||||
await cleanupFlag();
|
||||
host.services.appLifecycle.performRestart();
|
||||
return false;
|
||||
}
|
||||
if (result === "aborted") {
|
||||
log("Fetch exited by user.", LOG_LEVEL_NOTICE);
|
||||
clearRememberedSimpleFetchMode(host);
|
||||
host.services.appLifecycle.performRestart();
|
||||
return false;
|
||||
}
|
||||
@@ -191,6 +243,7 @@ export async function askAndPerformFastSetupOnScheduledFetchAll(
|
||||
}
|
||||
await host.serviceModules.rebuilder.finishRebuild();
|
||||
await cleanupFlag();
|
||||
clearRememberedSimpleFetchMode(host);
|
||||
log("Simple fetch and scan operation completed.", LOG_LEVEL_NOTICE);
|
||||
return true;
|
||||
});
|
||||
|
||||
@@ -85,6 +85,7 @@ const createSettingServiceMock = () => {
|
||||
writeLogToTheFile: false,
|
||||
remoteType: "CouchDB",
|
||||
};
|
||||
const smallConfig = new Map<string, string>();
|
||||
return {
|
||||
settings,
|
||||
currentSettings: vi.fn(() => settings),
|
||||
@@ -98,6 +99,13 @@ const createSettingServiceMock = () => {
|
||||
}),
|
||||
suspendAllSync: vi.fn(() => Promise.resolve()),
|
||||
suspendExtraSync: vi.fn(() => Promise.resolve()),
|
||||
getSmallConfig: vi.fn((key: string) => smallConfig.get(key) ?? ""),
|
||||
setSmallConfig: vi.fn((key: string, value: string) => {
|
||||
smallConfig.set(key, value);
|
||||
}),
|
||||
deleteSmallConfig: vi.fn((key: string) => {
|
||||
smallConfig.delete(key);
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -739,6 +747,39 @@ describe("Red Flag Feature", () => {
|
||||
});
|
||||
|
||||
describe("askAndPerformFastSetupOnScheduledFetchAll", () => {
|
||||
it("should remember quick flow choices while the scheduled fetch is pending", async () => {
|
||||
const host = createHostMock();
|
||||
const log = createLoggerMock();
|
||||
const cleanupFlag = vi.fn().mockResolvedValue(undefined);
|
||||
|
||||
host.mocks.ui.confirm.confirmWithMessage
|
||||
.mockResolvedValueOnce(SIMPLE_FETCH_STAGE1_NEWER_WINS)
|
||||
.mockResolvedValueOnce(SIMPLE_FETCH_STAGE2_NEWER_CLEANUP);
|
||||
host.mocks.tweakValue.fetchRemotePreferred.mockResolvedValue({ batchSave: false } as any);
|
||||
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.rebuilder.$fetchLocalDBFast).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("should clear remembered quick flow choices after completion", async () => {
|
||||
const host = createHostMock();
|
||||
const log = createLoggerMock();
|
||||
const cleanupFlag = vi.fn().mockResolvedValue(undefined);
|
||||
|
||||
host.mocks.ui.confirm.confirmWithMessage
|
||||
.mockResolvedValueOnce(SIMPLE_FETCH_STAGE1_REMOTE_WINS)
|
||||
.mockResolvedValueOnce(SIMPLE_FETCH_STAGE2_REMOTE_DELETE_ALL);
|
||||
host.mocks.tweakValue.fetchRemotePreferred.mockResolvedValue({ batchSave: false } as any);
|
||||
|
||||
await askAndPerformFastSetupOnScheduledFetchAll(host as any, log, cleanupFlag);
|
||||
|
||||
expect(host.mocks.setting.deleteSmallConfig).toHaveBeenCalledWith("simple-fetch-mode");
|
||||
});
|
||||
|
||||
it("should return false and cleanup when quick flow is cancelled", async () => {
|
||||
const host = createHostMock();
|
||||
const log = createLoggerMock();
|
||||
|
||||
Reference in New Issue
Block a user