mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-10-08 10:22:31 +00:00
fix(sync): honour readiness and suspension in Customisation Sync
(src/features/ConfigSync/CmdConfigSync.ts) - Call the readiness predicate before accepting configuration file events. - Stop the periodic sweep while settings are applied and let the resume lifecycle scan and restart it once. (src/features/ConfigSync/CmdConfigSync.applySettings.unit.spec.ts) - Exercise the ControlService settings path while active, suspended, and resumed. - Verify that suspended settings application performs no scan and leaves the periodic sweep stopped. (src/features/ConfigSync/CmdConfigSync.readiness.unit.spec.ts) - Cover configuration file events before and after application readiness. (updates.md) - Document the readiness and suspension behaviour.
This commit is contained in:
@@ -0,0 +1,190 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
vi.mock("@/deps.ts", () => ({
|
||||
addIcon: vi.fn(),
|
||||
diff_match_patch: class DiffMatchPatch {},
|
||||
normalizePath: vi.fn((path: string) => path),
|
||||
parseYaml: vi.fn(),
|
||||
Platform: {},
|
||||
}));
|
||||
vi.mock("./PluginDialogModal.ts", () => ({
|
||||
PluginDialogModal: class PluginDialogModal {},
|
||||
}));
|
||||
vi.mock("@/features/HiddenFileCommon/JsonResolveModal.ts", () => ({
|
||||
JsonResolveModal: class JsonResolveModal {},
|
||||
}));
|
||||
vi.mock("@/modules/features/InteractiveConflictResolving/ConflictResolveModal.ts", () => ({
|
||||
ConflictResolveModal: class ConflictResolveModal {},
|
||||
}));
|
||||
// The readiness and suspension helpers come from the real LiveSyncCommands, so only its log view dependency is replaced.
|
||||
vi.mock("@/modules/features/ModuleLog.ts", () => ({
|
||||
MARK_DONE: "",
|
||||
}));
|
||||
vi.mock("@/common/types.ts", () => ({
|
||||
ICXHeader: "ix:",
|
||||
PERIODIC_PLUGIN_SWEEP: 60,
|
||||
}));
|
||||
vi.mock("@/common/utils.ts", () => ({
|
||||
cancelTask: vi.fn(),
|
||||
EVEN: Symbol("even"),
|
||||
isCustomisationSyncMetadata: vi.fn(),
|
||||
isPluginMetadata: vi.fn(),
|
||||
scheduleTask: vi.fn(),
|
||||
}));
|
||||
vi.mock("@/common/PeriodicProcessor.ts", () => ({
|
||||
PeriodicProcessor: class PeriodicProcessor {},
|
||||
}));
|
||||
vi.mock("@/common/events.ts", () => ({
|
||||
EVENT_REQUEST_OPEN_PLUGIN_SYNC_DIALOG: "open-plugin-sync",
|
||||
eventHub: {
|
||||
onEvent: vi.fn(),
|
||||
},
|
||||
}));
|
||||
vi.mock("@/common/translation", () => ({
|
||||
$msg: vi.fn((message: string) => message),
|
||||
}));
|
||||
vi.mock("@/common/obsidianCommunityPlugins.ts", () => ({
|
||||
getObsidianCommunityPluginManager: vi.fn(),
|
||||
}));
|
||||
|
||||
import { createServiceContext, type ServiceContext } from "@vrtmrz/livesync-commonlib/context";
|
||||
import { ControlService } from "@vrtmrz/livesync-commonlib/compat/services/base/ControlService";
|
||||
import { InjectableAppLifecycleService } from "@vrtmrz/livesync-commonlib/compat/services/implements/injectable/InjectableAppLifecycleService";
|
||||
import { InjectableSettingService } from "@vrtmrz/livesync-commonlib/compat/services/implements/injectable/InjectableSettingService";
|
||||
import { ConfigSync } from "./CmdConfigSync";
|
||||
|
||||
type HarnessOptions = {
|
||||
ready?: boolean;
|
||||
suspended?: boolean;
|
||||
};
|
||||
|
||||
const PERIODIC_SWEEP_INTERVAL = 60 * 1000;
|
||||
|
||||
class TestAppLifecycleService extends InjectableAppLifecycleService {}
|
||||
class TestSettingService extends InjectableSettingService<ServiceContext> {}
|
||||
|
||||
/**
|
||||
* Binds ConfigSync to Commonlib's real ControlService, so settings are applied in the order the plug-in uses.
|
||||
*/
|
||||
function setup(options: HarnessOptions = {}) {
|
||||
const context = createServiceContext();
|
||||
const settings = {
|
||||
isConfigured: true,
|
||||
usePluginSync: true,
|
||||
autoSweepPlugins: true,
|
||||
autoSweepPluginsPeriodic: true,
|
||||
watchInternalFileChanges: false,
|
||||
pluginSyncExtendedSetting: {},
|
||||
};
|
||||
const API = {
|
||||
addLog: vi.fn(),
|
||||
getSystemConfigDir: () => ".obsidian",
|
||||
};
|
||||
const appLifecycle = new TestAppLifecycleService(context, {
|
||||
settingService: { currentSettings: () => settings },
|
||||
} as never);
|
||||
const setting = new TestSettingService(context, { APIService: API } as never);
|
||||
const control = new ControlService(context, {
|
||||
APIService: API,
|
||||
appLifecycleService: appLifecycle,
|
||||
databaseService: { localDatabase: { refreshSettings: vi.fn() } },
|
||||
fileProcessingService: { commitPendingFileEvents: vi.fn(async () => true) },
|
||||
settingService: setting,
|
||||
} as never);
|
||||
const handler = () => ({ addHandler: vi.fn() });
|
||||
const services = {
|
||||
API,
|
||||
appLifecycle,
|
||||
conflict: { getOptionalConflictCheckMethod: handler() },
|
||||
control,
|
||||
databaseEvents: { onDatabaseInitialised: handler() },
|
||||
fileProcessing: { processOptionalFileEvent: handler() },
|
||||
replication: { onBeforeReplicate: handler(), processVirtualDocument: handler() },
|
||||
setting,
|
||||
};
|
||||
const periodicPluginSweepProcessor = { disable: vi.fn(), enable: vi.fn() };
|
||||
const scanAllConfigFiles = vi.fn(async () => undefined);
|
||||
const configSync = Object.create(ConfigSync.prototype) as ConfigSync;
|
||||
Object.assign(configSync, {
|
||||
core: { settings, services },
|
||||
periodicPluginSweepProcessor,
|
||||
scanAllConfigFiles,
|
||||
});
|
||||
configSync.onBindFunction(configSync.core, services as never);
|
||||
|
||||
if (options.ready ?? true) appLifecycle.markIsReady();
|
||||
appLifecycle.setSuspended(options.suspended ?? false);
|
||||
|
||||
// The periodic sweep is running when the last call left it enabled with a non-zero interval.
|
||||
const isPeriodicSweepRunning = () => {
|
||||
const calls = [
|
||||
...periodicPluginSweepProcessor.disable.mock.invocationCallOrder.map((order) => ({ order, interval: 0 })),
|
||||
...periodicPluginSweepProcessor.enable.mock.calls.map(([interval], index) => ({
|
||||
order: periodicPluginSweepProcessor.enable.mock.invocationCallOrder[index],
|
||||
interval: interval as number,
|
||||
})),
|
||||
].sort((a, b) => a.order - b.order);
|
||||
return (calls[calls.length - 1]?.interval ?? 0) > 0;
|
||||
};
|
||||
|
||||
return { appLifecycle, control, isPeriodicSweepRunning, periodicPluginSweepProcessor, scanAllConfigFiles };
|
||||
}
|
||||
|
||||
describe("ConfigSync when settings are applied", () => {
|
||||
beforeEach(() => {
|
||||
vi.stubGlobal("activeDocument", { querySelector: vi.fn(() => null) });
|
||||
});
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it.each([true, false])(
|
||||
"does not sweep configuration files while the plug-in is suspended (ready: %s)",
|
||||
async (ready) => {
|
||||
const { control, isPeriodicSweepRunning, periodicPluginSweepProcessor, scanAllConfigFiles } = setup({
|
||||
ready,
|
||||
suspended: true,
|
||||
});
|
||||
|
||||
await control.applySettings();
|
||||
|
||||
expect(periodicPluginSweepProcessor.disable).toHaveBeenCalled();
|
||||
expect(scanAllConfigFiles).not.toHaveBeenCalled();
|
||||
expect(periodicPluginSweepProcessor.enable).not.toHaveBeenCalled();
|
||||
expect(isPeriodicSweepRunning()).toBe(false);
|
||||
}
|
||||
);
|
||||
|
||||
it("sweeps configuration files once and starts the periodic sweep while the plug-in is running", async () => {
|
||||
const { control, isPeriodicSweepRunning, periodicPluginSweepProcessor, scanAllConfigFiles } = setup();
|
||||
|
||||
await control.applySettings();
|
||||
|
||||
expect(scanAllConfigFiles).toHaveBeenCalledOnce();
|
||||
expect(periodicPluginSweepProcessor.enable).toHaveBeenCalledOnce();
|
||||
expect(periodicPluginSweepProcessor.enable).toHaveBeenCalledWith(PERIODIC_SWEEP_INTERVAL);
|
||||
expect(isPeriodicSweepRunning()).toBe(true);
|
||||
});
|
||||
|
||||
it("stops the periodic sweep when all synchronisation is suspended and restarts it once on resumption", async () => {
|
||||
const { appLifecycle, control, isPeriodicSweepRunning, periodicPluginSweepProcessor, scanAllConfigFiles } =
|
||||
setup();
|
||||
await control.applySettings();
|
||||
scanAllConfigFiles.mockClear();
|
||||
periodicPluginSweepProcessor.enable.mockClear();
|
||||
|
||||
appLifecycle.setSuspended(true);
|
||||
await control.applySettings();
|
||||
|
||||
expect(scanAllConfigFiles).not.toHaveBeenCalled();
|
||||
expect(periodicPluginSweepProcessor.enable).not.toHaveBeenCalled();
|
||||
expect(isPeriodicSweepRunning()).toBe(false);
|
||||
|
||||
appLifecycle.setSuspended(false);
|
||||
await control.applySettings();
|
||||
|
||||
expect(scanAllConfigFiles).toHaveBeenCalledOnce();
|
||||
expect(periodicPluginSweepProcessor.enable).toHaveBeenCalledOnce();
|
||||
expect(isPeriodicSweepRunning()).toBe(true);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user