diff --git a/src/common/replicatorResources.unit.spec.ts b/src/common/replicatorResources.unit.spec.ts index 6d62ac1b..d58861fa 100644 --- a/src/common/replicatorResources.unit.spec.ts +++ b/src/common/replicatorResources.unit.spec.ts @@ -224,8 +224,8 @@ describe("replicator probe factories", () => { objectSettings.endpoint = "https://changed.example.test"; await expect(couchResource.read()).resolves.toBe(couchSeed); await expect(objectResource.read()).resolves.toBe(objectSeed); - expect(couchReplicator.getReplicationPBKDF2Salt).toHaveBeenCalledWith(couchSnapshot); - expect(objectReplicator.getReplicationPBKDF2Salt).toHaveBeenCalledWith(objectSnapshot); + expect(couchReplicator.getReplicationPBKDF2Salt).toHaveBeenCalledWith(couchSnapshot, true); + expect(objectReplicator.getReplicationPBKDF2Salt).toHaveBeenCalledWith(objectSnapshot, true); await Promise.all([couchResource.dispose(), objectResource.dispose()]); expect(couchReplicator.closeReplication).toHaveBeenCalledOnce(); diff --git a/src/common/replicatorResources/securitySeed.ts b/src/common/replicatorResources/securitySeed.ts index 07bc6a6a..6abf91b9 100644 --- a/src/common/replicatorResources/securitySeed.ts +++ b/src/common/replicatorResources/securitySeed.ts @@ -22,7 +22,7 @@ function createSecuritySeedResourceFactory( const snapshot = snapshotRemoteSettings(setting); const replicator = createReplicator(); return Promise.resolve({ - read: () => replicator.getReplicationPBKDF2Salt(snapshot), + read: () => replicator.getReplicationPBKDF2Salt(snapshot, true), dispose: createReplicatorDisposer(replicator), }); }; diff --git a/src/serviceFeatures/replication/centralCompatibilityRecovery.unit.spec.ts b/src/serviceFeatures/replication/centralCompatibilityRecovery.unit.spec.ts index 83d8fbe5..0ef47748 100644 --- a/src/serviceFeatures/replication/centralCompatibilityRecovery.unit.spec.ts +++ b/src/serviceFeatures/replication/centralCompatibilityRecovery.unit.spec.ts @@ -1,5 +1,6 @@ import { describe, expect, it, vi } from "vitest"; import type { ObsidianLiveSyncSettings } from "@vrtmrz/livesync-commonlib/compat/common/types"; +import { defaultLogger, LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, setGlobalLogFunction } from "octagonal-wheels/common/logger"; import { CENTRAL_COMPATIBILITY_REJECTION_REASONS, NO_INTERACTION, @@ -21,6 +22,40 @@ import { LiveSyncCouchDBReplicator } from "@vrtmrz/livesync-commonlib/compat/rep import { createCentralCompatibilityRecovery } from "./centralCompatibilityRecovery"; describe("central compatibility recovery", () => { + it("characterises unattended central failure handling as one INFO log without a NOTICE", async () => { + const log = vi.fn((_message: unknown, _level?: number, _key?: string) => undefined); + setGlobalLogFunction(log); + try { + const recovery = createCentralCompatibilityRecovery({ + confirm: {}, + localDatabase: {}, + rebuilder: {}, + services: { + appLifecycle: {}, + API: {}, + replicator: {}, + tweakValue: {}, + }, + } as never); + + await expect( + recovery.handleReplicationFailure({ + context: { provider: {}, replicator: {} }, + setting: {}, + outcome: replicationFailed(new Error("provider failed")), + showMessage: false, + interaction: NO_INTERACTION, + } as never) + ).resolves.toBe(false); + + expect(log).toHaveBeenCalledOnce(); + expect(log).toHaveBeenCalledWith("Replication failed on an unattended path.", LOG_LEVEL_INFO, undefined); + expect(log.mock.calls.map(([, level]) => level)).not.toContain(LOG_LEVEL_NOTICE); + } finally { + setGlobalLogFunction(defaultLogger); + } + }); + it("uses the exact failed outcome and permits dialogue only with recovery authority", async () => { const askResolvingMismatched = vi.fn(async (..._arguments: unknown[]) => undefined); const failedSetPreferred = vi.fn(async (_setting: unknown) => undefined); diff --git a/src/serviceFeatures/replicationScheduling.unit.spec.ts b/src/serviceFeatures/replicationScheduling.unit.spec.ts index 154617c3..487370d2 100644 --- a/src/serviceFeatures/replicationScheduling.unit.spec.ts +++ b/src/serviceFeatures/replicationScheduling.unit.spec.ts @@ -1,5 +1,5 @@ import { describe, expect, it, vi } from "vitest"; -import { DEFAULT_SETTINGS } from "@vrtmrz/livesync-commonlib/compat/common/types"; +import { DEFAULT_SETTINGS, REMOTE_MINIO } from "@vrtmrz/livesync-commonlib/compat/common/types"; import { NO_INTERACTION, type ReplicationOutcome } from "@vrtmrz/livesync-commonlib/replication"; import { createReplicationSchedulingContext, @@ -22,6 +22,7 @@ function createDeferred() { function createControllerHarness( overrides: Partial<{ + remoteType: typeof DEFAULT_SETTINGS.remoteType; liveSync: boolean; syncOnStart: boolean; periodicReplication: boolean; @@ -111,6 +112,26 @@ describe("replication scheduling context", () => { expect(timer.enable).toHaveBeenCalledWith(45_000); }); + it("schedules migrated Object Storage syncOnStart through unattended OneShot", async () => { + const { context, replicateUnattended, startContinuous } = createControllerHarness({ + remoteType: REMOTE_MINIO, + liveSync: true, + syncOnStart: true, + }); + startContinuous.mockResolvedValue({ + status: "blocked", + reason: "capability-not-applicable", + }); + + resumeReplicationScheduling(context); + + await vi.waitFor(() => expect(replicateUnattended).toHaveBeenCalledOnce()); + expect(replicateUnattended).toHaveBeenCalledWith({ + trigger: "resume", + interaction: NO_INTERACTION, + }); + }); + it("does not run a finite fallback after Continuous starts successfully", async () => { const { context, replicateUnattended, startContinuous } = createControllerHarness({ liveSync: true });