diff --git a/docs/adr/2026_08_replicator_capabilities_01_core_contract.md b/docs/adr/2026_08_replicator_capabilities_01_core_contract.md index e8313ca7..cbc17742 100644 --- a/docs/adr/2026_08_replicator_capabilities_01_core_contract.md +++ b/docs/adr/2026_08_replicator_capabilities_01_core_contract.md @@ -369,8 +369,17 @@ type InteractionAuthority = const NO_INTERACTION = { kind: "forbidden" } as const; +const REPLICATION_PROGRESS_PRESENTATIONS = { + QUIET: "quiet", + NOTICE: "notice", +} as const; + +type ReplicationProgressPresentation = + (typeof REPLICATION_PROGRESS_PRESENTATIONS)[keyof typeof REPLICATION_PROGRESS_PRESENTATIONS]; + interface UserInitiatedOneShotRequest { readonly trigger: "manual"; + readonly progressPresentation: ReplicationProgressPresentation; readonly interaction: InteractionAuthority; } @@ -393,11 +402,33 @@ same upper bound and expose only relevant permissions. An unattended path may use persisted or automatic acceptance policy, but cannot obtain local interaction authority implicitly. -The same authority bounds presentation. An unattended P2P path may retain an -informational diagnostic, but no-target, authentication, tweak-mismatch, and -overlapping-transfer settlements must not promote themselves to a Notice. -User-initiated paths retain their existing Notice-level presentation. This is -a caller-authority rule, not a new process-wide presentation framework. +Progress presentation and interaction authority are independent parts of the +request. `quiet` suppresses routine Notice-level progress, but does not remove +authorised peer selection or failure-recovery dialogue. `notice` permits +routine progress, but cannot grant an interaction which the request authority +forbids. Unattended work always uses `quiet` with `NO_INTERACTION`; it may retain +an informational diagnostic, but no-target, authentication, tweak-mismatch, +and overlapping-transfer settlements must not promote themselves to a Notice. +This remains a request-local policy, not a new process-wide presentation +framework. + +The ordinary typed OneShot entry points deliberately retain these established +interaction shapes: + +| Entry point | Routine progress | Interaction authority | Request received while another OneShot attempt is active | +| --- | --- | --- | --- | +| Command-palette `Sync now` | Quiet | Peer selection and failure recovery remain permitted | Settles as `blocked: replication-in-progress` | +| Ribbon synchronisation action | Notice | Full user-initiated authority | Settles as `blocked: replication-in-progress` | +| Config Sync `Sync once` | Notice | Full user-initiated authority | Settles as `blocked: replication-in-progress` | +| CLI `sync` | Notice | Full user-initiated authority, subject to the host's available interaction mechanisms | Settles as `blocked: replication-in-progress` | +| Resume, periodic, database-event, editor-save, file-open, merge, and daemon triggers | Quiet | `NO_INTERACTION` | Settles as `blocked: replication-in-progress` | + +The OneShot owner is acquired before readiness evaluation and retained through +provider acquisition, finite activity, and failure handling. A later request +is not queued, joined to the running result, or treated as a failed transfer; +no work starts for that request. Directional Fetch and Rebuild, P2P-specific +commands, and other maintenance workflows remain outside this ordinary +OneShot gate and retain their own activity ownership. Outcomes do not collapse failure into `void` or `boolean`: diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 14d40344..6b1fbf62 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -88,6 +88,8 @@ Some settings must match across devices. LiveSync pauses synchronisation when th Current releases automatically align compatible settings which control how new chunks are created, by default and where possible. This applies to the chunk hash algorithm, chunk size, and splitter version. Existing content remains readable across these choices, although using different choices can reduce chunk reuse and increase storage or transfer work. An explicit opt-out retains the manual review. A mismatch involving encryption, path obfuscation, file-name case handling, or any combination which includes one of those settings always remains a manual decision. +The `Sync now` command keeps routine replication progress quiet so that it is convenient to assign to a keyboard shortcut; assign one in Obsidian if that suits your workflow. A quiet command may still open this dialogue when a mismatch or another decision requires your attention. + The available actions depend on when the mismatch is found: - While checking a remote profile, `Use configured settings` accepts the shared values already stored in that remote. `Dismiss` leaves this device's settings unchanged. diff --git a/src/apps/cli/commands/runCommand.ts b/src/apps/cli/commands/runCommand.ts index 85030a45..45524207 100644 --- a/src/apps/cli/commands/runCommand.ts +++ b/src/apps/cli/commands/runCommand.ts @@ -24,6 +24,7 @@ import { CENTRAL_COMPATIBILITY_REJECTION_REASONS, isReplicationCompleted, NO_INTERACTION, + REPLICATION_PROGRESS_PRESENTATIONS, REMOTE_RESOURCE_KINDS, USER_INITIATED_REPLICATION_AUTHORITY, } from "@vrtmrz/livesync-commonlib/replication"; @@ -173,6 +174,7 @@ export async function runCommand(options: CLIOptions, context: CLICommandContext writeStdoutLine(standardIo, "[Command] sync"); const result = await core.services.replication.replicateUserInitiated({ trigger: "manual", + progressPresentation: REPLICATION_PROGRESS_PRESENTATIONS.NOTICE, interaction: USER_INITIATED_REPLICATION_AUTHORITY, }); if (!isReplicationCompleted(result)) { diff --git a/src/apps/cli/commands/runCommand.unit.spec.ts b/src/apps/cli/commands/runCommand.unit.spec.ts index 1b26d6cb..651e47d1 100644 --- a/src/apps/cli/commands/runCommand.unit.spec.ts +++ b/src/apps/cli/commands/runCommand.unit.spec.ts @@ -19,6 +19,7 @@ import { REMOTE_RESOURCE_KINDS, CENTRAL_COMPATIBILITY_REJECTION_REASONS, REPLICATION_COMPLETED, + REPLICATION_PROGRESS_PRESENTATIONS, replicationFailed, } from "@vrtmrz/livesync-commonlib/replication"; @@ -265,6 +266,21 @@ describe("runCommand abnormal cases", () => { vi.restoreAllMocks(); }); + it("retains visible progress for the interactive sync command", async () => { + const core = createCoreMock(); + + await expect( + runCommand(makeOptions("sync", []), { + ...context, + core, + }) + ).resolves.toBe(true); + + expect(core.services.replication.replicateUserInitiated).toHaveBeenCalledWith( + expect.objectContaining({ progressPresentation: REPLICATION_PROGRESS_PRESENTATIONS.NOTICE }) + ); + }); + it("reports a lock from the exact sync outcome without inspecting a replacement Replicator", async () => { const core = createCoreMock(); core.services.replication.replicateUserInitiated.mockResolvedValue( diff --git a/src/common/replicatorProviders.ts b/src/common/replicatorProviders.ts index efd8d589..527215e6 100644 --- a/src/common/replicatorProviders.ts +++ b/src/common/replicatorProviders.ts @@ -3,6 +3,7 @@ import { CAPABILITY_NOT_APPLICABLE, CENTRAL_REMOTE_REPLICATION_READINESS, NO_INTERACTION, + REPLICATION_PROGRESS_PRESENTATIONS, REMOTE_RESOURCE_KINDS, defineReplicatorProviderDefinitions, supportedOpenReplicationContinuous, @@ -72,7 +73,7 @@ const couchDBUserInitiatedOneShot: UserInitiatedOneShotRunner = async (instance, return await runOneShotWithOutcome( instance, setting, - request.interaction.kind === "permitted" && request.interaction.permissions.failureRecovery + request.progressPresentation === REPLICATION_PROGRESS_PRESENTATIONS.NOTICE ); }; @@ -85,7 +86,7 @@ const objectStorageUserInitiatedOneShot: UserInitiatedOneShotRunner = async (ins return await runOneShotWithOutcome( instance, setting, - request.interaction.kind === "permitted" && request.interaction.permissions.failureRecovery + request.progressPresentation === REPLICATION_PROGRESS_PRESENTATIONS.NOTICE ); }; diff --git a/src/common/replicatorProviders.unit.spec.ts b/src/common/replicatorProviders.unit.spec.ts index 8b3bcd5b..fecfc4f3 100644 --- a/src/common/replicatorProviders.unit.spec.ts +++ b/src/common/replicatorProviders.unit.spec.ts @@ -5,6 +5,7 @@ import { CAPABILITY_SUPPORT_KINDS, NO_INTERACTION, REPLICATION_COMPLETED, + REPLICATION_PROGRESS_PRESENTATIONS, REMOTE_RESOURCE_KINDS, USER_INITIATED_REPLICATION_AUTHORITY, } from "@vrtmrz/livesync-commonlib/replication"; @@ -135,6 +136,7 @@ describe("central Replicator provider definitions", () => { await expect( couchDB.userInitiatedOneShot.run(couchInstance, setting, { trigger: "manual", + progressPresentation: REPLICATION_PROGRESS_PRESENTATIONS.NOTICE, interaction: USER_INITIATED_REPLICATION_AUTHORITY, }) ).resolves.toBe(REPLICATION_COMPLETED); @@ -149,6 +151,40 @@ describe("central Replicator provider definitions", () => { expect(constructorMocks.objectStorageOneShot).toHaveBeenCalledWith(setting, false); }); + it.each([ + ["CouchDB", REMOTE_COUCHDB], + ["Object Storage", REMOTE_MINIO], + ] as const)("maps %s progress presentation independently of recovery authority", async (label, remoteType) => { + const definitions = createCentralReplicatorProviderDefinitions({} as never); + const definition = definitions.get(remoteType)!; + if (definition.userInitiatedOneShot.kind !== CAPABILITY_SUPPORT_KINDS.SUPPORTED) { + throw new Error(`${label} OneShot is unavailable`); + } + const setting = Object.assign(createNewVaultSettings(), { remoteType }); + const openOneShotReplicationWithOutcome = vi.fn(async () => REPLICATION_COMPLETED); + const instance = { + initializeDatabaseForReplication: vi.fn(async () => true), + openReplication: vi.fn(async () => true), + terminateSync: vi.fn(), + closeReplication: vi.fn(), + openOneShotReplicationWithOutcome, + }; + + await definition.userInitiatedOneShot.run(instance, setting, { + trigger: "manual", + progressPresentation: REPLICATION_PROGRESS_PRESENTATIONS.QUIET, + interaction: USER_INITIATED_REPLICATION_AUTHORITY, + }); + await definition.userInitiatedOneShot.run(instance, setting, { + trigger: "manual", + progressPresentation: REPLICATION_PROGRESS_PRESENTATIONS.NOTICE, + interaction: USER_INITIATED_REPLICATION_AUTHORITY, + }); + + expect(openOneShotReplicationWithOutcome).toHaveBeenNthCalledWith(1, setting, false); + expect(openOneShotReplicationWithOutcome).toHaveBeenNthCalledWith(2, setting, true); + }); + it("dispatches central finite work through the declared operation rather than constructor identity", async () => { const definitions = createCentralReplicatorProviderDefinitions({} as never); const couchDB = definitions.get(REMOTE_COUCHDB)!; @@ -172,6 +208,7 @@ describe("central Replicator provider definitions", () => { const couchOutcome = await couchDB.userInitiatedOneShot.run(couchInstance, setting, { trigger: "manual", + progressPresentation: REPLICATION_PROGRESS_PRESENTATIONS.NOTICE, interaction: USER_INITIATED_REPLICATION_AUTHORITY, }); const objectStorageOutcome = await objectStorage.unattendedOneShot.run(objectStorageInstance, setting, { @@ -201,6 +238,7 @@ describe("central Replicator provider definitions", () => { const outcome = await couchDB.userInitiatedOneShot.run(incompleteInstance, setting, { trigger: "manual", + progressPresentation: REPLICATION_PROGRESS_PRESENTATIONS.NOTICE, interaction: USER_INITIATED_REPLICATION_AUTHORITY, }); diff --git a/src/features/ConfigSync/PluginPane.svelte b/src/features/ConfigSync/PluginPane.svelte index cd07fd36..a6be2a6e 100644 --- a/src/features/ConfigSync/PluginPane.svelte +++ b/src/features/ConfigSync/PluginPane.svelte @@ -24,7 +24,10 @@ import { LOG_LEVEL_NOTICE, Logger } from "octagonal-wheels/common/logger"; import type { LiveSyncBaseCore } from "@/LiveSyncBaseCore.ts"; import { $msg as translateMessage } from "@/common/translation"; - import { USER_INITIATED_REPLICATION_AUTHORITY } from "@vrtmrz/livesync-commonlib/replication"; + import { + REPLICATION_PROGRESS_PRESENTATIONS, + USER_INITIATED_REPLICATION_AUTHORITY, + } from "@vrtmrz/livesync-commonlib/replication"; export let plugin: ObsidianLiveSyncPlugin; export let core :LiveSyncBaseCore; // $: core = plugin.core; @@ -107,6 +110,7 @@ async function replicate() { await core.services.replication.replicateUserInitiated({ trigger: "manual", + progressPresentation: REPLICATION_PROGRESS_PRESENTATIONS.NOTICE, interaction: USER_INITIATED_REPLICATION_AUTHORITY, }); } diff --git a/src/modules/essential/ModuleBasicMenu.ts b/src/modules/essential/ModuleBasicMenu.ts index 7bab5ef9..11671eae 100644 --- a/src/modules/essential/ModuleBasicMenu.ts +++ b/src/modules/essential/ModuleBasicMenu.ts @@ -4,7 +4,10 @@ import { fireAndForget } from "octagonal-wheels/promises"; import { AbstractModule } from "@/modules/AbstractModule"; import { $msg } from "@/common/translation"; import { copyFileDatabaseInfo } from "@/serviceFeatures/fileDatabaseInfo"; -import { USER_INITIATED_REPLICATION_AUTHORITY } from "@vrtmrz/livesync-commonlib/replication"; +import { + REPLICATION_PROGRESS_PRESENTATIONS, + USER_INITIATED_REPLICATION_AUTHORITY, +} from "@vrtmrz/livesync-commonlib/replication"; // Separated Module for basic menu commands, which are not related to obsidian specific features. It is expected to be used in other platforms with minimal changes. // However, it is odd that it has here at all; it really ought to be in each respective feature. It will likely be moved eventually. Until now, addCommand pointed to Obsidian's version. export class ModuleBasicMenu extends AbstractModule { @@ -15,6 +18,7 @@ export class ModuleBasicMenu extends AbstractModule { callback: async () => { await this.services.replication.replicateUserInitiated({ trigger: "manual", + progressPresentation: REPLICATION_PROGRESS_PRESENTATIONS.QUIET, interaction: USER_INITIATED_REPLICATION_AUTHORITY, }); }, diff --git a/src/modules/essential/ModuleBasicMenu.unit.spec.ts b/src/modules/essential/ModuleBasicMenu.unit.spec.ts index e0c13bbf..1add302e 100644 --- a/src/modules/essential/ModuleBasicMenu.unit.spec.ts +++ b/src/modules/essential/ModuleBasicMenu.unit.spec.ts @@ -1,5 +1,9 @@ import { describe, expect, it, vi } from "vitest"; import type { Command } from "@/deps"; +import { + REPLICATION_PROGRESS_PRESENTATIONS, + USER_INITIATED_REPLICATION_AUTHORITY, +} from "@vrtmrz/livesync-commonlib/replication"; import { ModuleBasicMenu } from "./ModuleBasicMenu"; type RegisteredCommand = Command & { @@ -124,6 +128,19 @@ describe("ModuleBasicMenu command palette", () => { expect(fixture.getCommand("livesync-runbatch").name).toBe("Apply pending changes now"); }); + it("keeps Sync now progress quiet while retaining failure-recovery authority", async () => { + const fixture = createFixture(); + + await fixture.module._everyOnloadStart(); + await fixture.getCommand("livesync-replicate").callback?.(); + + expect(fixture.services.replication.replicateUserInitiated).toHaveBeenCalledWith({ + trigger: "manual", + progressPresentation: REPLICATION_PROGRESS_PRESENTATIONS.QUIET, + interaction: USER_INITIATED_REPLICATION_AUTHORITY, + }); + }); + it("keeps maintenance commands out of the normal palette", async () => { const fixture = createFixture(); diff --git a/src/modules/essentialObsidian/ModuleObsidianMenu.ts b/src/modules/essentialObsidian/ModuleObsidianMenu.ts index f11d8b9b..f162bde8 100644 --- a/src/modules/essentialObsidian/ModuleObsidianMenu.ts +++ b/src/modules/essentialObsidian/ModuleObsidianMenu.ts @@ -2,7 +2,10 @@ import { addIcon } from "@/deps.ts"; import { $msg } from "@/common/translation"; import type { LiveSyncCore } from "@/main.ts"; import { AbstractModule } from "@/modules/AbstractModule.ts"; -import { USER_INITIATED_REPLICATION_AUTHORITY } from "@vrtmrz/livesync-commonlib/replication"; +import { + REPLICATION_PROGRESS_PRESENTATIONS, + USER_INITIATED_REPLICATION_AUTHORITY, +} from "@vrtmrz/livesync-commonlib/replication"; // Obsidian specific menu commands. export class ModuleObsidianMenu extends AbstractModule { _everyOnloadStart(): Promise { @@ -20,6 +23,7 @@ export class ModuleObsidianMenu extends AbstractModule { this.addRibbonIcon("replicate", $msg("moduleObsidianMenu.replicate"), async () => { await this.services.replication.replicateUserInitiated({ trigger: "manual", + progressPresentation: REPLICATION_PROGRESS_PRESENTATIONS.NOTICE, interaction: USER_INITIATED_REPLICATION_AUTHORITY, }); }).addClass("livesync-ribbon-replicate"); diff --git a/src/modules/essentialObsidian/ModuleObsidianMenu.unit.spec.ts b/src/modules/essentialObsidian/ModuleObsidianMenu.unit.spec.ts new file mode 100644 index 00000000..23864d5f --- /dev/null +++ b/src/modules/essentialObsidian/ModuleObsidianMenu.unit.spec.ts @@ -0,0 +1,41 @@ +import { describe, expect, it, vi } from "vitest"; + +vi.mock("@/deps.ts", () => ({ addIcon: vi.fn() })); + +import { + REPLICATION_PROGRESS_PRESENTATIONS, + USER_INITIATED_REPLICATION_AUTHORITY, +} from "@vrtmrz/livesync-commonlib/replication"; +import { ModuleObsidianMenu } from "./ModuleObsidianMenu"; + +describe("ModuleObsidianMenu ribbon", () => { + it("retains visible progress and full interaction authority", async () => { + let runRibbonAction: (() => Promise) | undefined; + const addClass = vi.fn(); + const replicateUserInitiated = vi.fn(async () => ({ status: "completed" as const })); + const services = { + API: { + addLog: vi.fn(), + addCommand: vi.fn(), + registerWindow: vi.fn(), + registerProtocolHandler: vi.fn(), + addRibbonIcon: vi.fn((_icon: string, _title: string, callback: () => Promise) => { + runRibbonAction = callback; + return { addClass }; + }), + }, + replication: { replicateUserInitiated }, + }; + const module = new ModuleObsidianMenu({ _services: services, services } as never); + + await module._everyOnloadStart(); + await runRibbonAction?.(); + + expect(replicateUserInitiated).toHaveBeenCalledWith({ + trigger: "manual", + progressPresentation: REPLICATION_PROGRESS_PRESENTATIONS.NOTICE, + interaction: USER_INITIATED_REPLICATION_AUTHORITY, + }); + expect(addClass).toHaveBeenCalledWith("livesync-ribbon-replicate"); + }); +}); diff --git a/src/serviceFeatures/replication/centralCompatibilityRecovery.ts b/src/serviceFeatures/replication/centralCompatibilityRecovery.ts index 47257d5f..3c4b50e4 100644 --- a/src/serviceFeatures/replication/centralCompatibilityRecovery.ts +++ b/src/serviceFeatures/replication/centralCompatibilityRecovery.ts @@ -5,6 +5,7 @@ import { balanceChunkPurgedDBs, purgeUnreferencedChunks } from "@vrtmrz/livesync import { LiveSyncCouchDBReplicator } from "@vrtmrz/livesync-commonlib/compat/replication/couchdb/LiveSyncReplicator"; import { CENTRAL_COMPATIBILITY_REJECTION_REASONS, + REPLICATION_PROGRESS_PRESENTATIONS, type ReplicatorInstance, type ReplicationFailureRequest, } from "@vrtmrz/livesync-commonlib/replication"; @@ -52,11 +53,11 @@ function canMarkRemoteResolved(replicator: ReplicatorInstance): replicator is Re */ export function createCentralCompatibilityRecovery(context: CentralCompatibilityRecoveryContext) { async function reconcileCleanedRemote( - showMessage: boolean, + showProgress: boolean, setting: ObsidianLiveSyncSettings, expectedContext: ReplicationFailureRequest["context"] ) { - Logger("The remote database has been cleaned.", showMessage ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO); + Logger("The remote database has been cleaned.", showProgress ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO); await skipIfDuplicated("cleanup", async () => { const count = await purgeUnreferencedChunks(context.getLocalDatabase().localDatabase, true); const message = `The remote database has been cleaned up. @@ -100,7 +101,7 @@ Even if you choose to clean up, you will see this option again if you exit Obsid await purgeUnreferencedChunks(localDatabase.localDatabase, false); localDatabase.clearCaches(); const replicated = await context.services.replicator.runFiniteReplicationActivity( - () => replicator.openOneShotReplication(setting, showMessage, false, "sync", true), + () => replicator.openOneShotReplication(setting, showProgress, false, "sync", true), { label: "replication" } ); if (replicated) { @@ -110,12 +111,12 @@ Even if you choose to clean up, you will see this option again if you exit Obsid await replicator.markRemoteResolved(setting); Logger( "The local database has been cleaned up.", - showMessage ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO + showProgress ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO ); } else { Logger( "Replication has been cancelled. Please try it again.", - showMessage ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO + showProgress ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO ); } } finally { @@ -128,14 +129,15 @@ Even if you choose to clean up, you will see this option again if you exit Obsid } async function handleReplicationFailure(request: ReplicationFailureRequest): Promise { - const { context: failedContext, interaction, outcome, setting, showMessage } = request; - if (!showMessage) { + const { context: failedContext, interaction, outcome, progressPresentation, setting } = request; + const showProgress = progressPresentation === REPLICATION_PROGRESS_PRESENTATIONS.NOTICE; + if (interaction.kind === "forbidden") { // Automatic requests may report the failure, but must not enter // tweak, lock, fetch, unlock, or cleanup dialogues. Logger("Replication failed on an unattended path.", LOG_LEVEL_INFO); return false; } - if (interaction.kind !== "permitted" || !interaction.permissions.failureRecovery) return false; + if (!interaction.permissions.failureRecovery) return false; const recovery = outcome.recoveryHint; if (!recovery) return false; @@ -169,7 +171,7 @@ Even if you choose to clean up, you will see this option again if you exit Obsid recovery.reason === CENTRAL_COMPATIBILITY_REJECTION_REASONS.NODE_CLEANED && usesLegacyIndexedDBAdapter(setting) ) { - await reconcileCleanedRemote(showMessage, setting, failedContext); + await reconcileCleanedRemote(showProgress, setting, failedContext); return false; } diff --git a/src/serviceFeatures/replication/centralCompatibilityRecovery.unit.spec.ts b/src/serviceFeatures/replication/centralCompatibilityRecovery.unit.spec.ts index 50d2714f..bd169d01 100644 --- a/src/serviceFeatures/replication/centralCompatibilityRecovery.unit.spec.ts +++ b/src/serviceFeatures/replication/centralCompatibilityRecovery.unit.spec.ts @@ -4,6 +4,7 @@ import { defaultLogger, LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, setGlobalLogFunction } import { CENTRAL_COMPATIBILITY_REJECTION_REASONS, NO_INTERACTION, + REPLICATION_PROGRESS_PRESENTATIONS, USER_INITIATED_REPLICATION_AUTHORITY, replicationFailed, } from "@vrtmrz/livesync-commonlib/replication"; @@ -43,7 +44,7 @@ describe("central compatibility recovery", () => { context: { provider: {}, replicator: {} }, setting: {}, outcome: replicationFailed(new Error("provider failed")), - showMessage: false, + progressPresentation: REPLICATION_PROGRESS_PRESENTATIONS.QUIET, interaction: NO_INTERACTION, } as never) ).resolves.toBe(false); @@ -93,7 +94,7 @@ describe("central compatibility recovery", () => { context: failedContext, setting: {}, outcome, - showMessage: false, + progressPresentation: REPLICATION_PROGRESS_PRESENTATIONS.QUIET, interaction: NO_INTERACTION, } as never); expect(askResolvingMismatched).not.toHaveBeenCalled(); @@ -102,7 +103,7 @@ describe("central compatibility recovery", () => { context: failedContext, setting: {}, outcome, - showMessage: false, + progressPresentation: REPLICATION_PROGRESS_PRESENTATIONS.QUIET, interaction: { kind: "permitted", permissions: { ...USER_INITIATED_REPLICATION_AUTHORITY.permissions, failureRecovery: false }, @@ -114,7 +115,7 @@ describe("central compatibility recovery", () => { context: failedContext, setting: {}, outcome, - showMessage: true, + progressPresentation: REPLICATION_PROGRESS_PRESENTATIONS.QUIET, interaction: USER_INITIATED_REPLICATION_AUTHORITY, } as never); expect(askResolvingMismatched).toHaveBeenCalledWith(preferredTweakValue, expect.any(Function)); @@ -158,7 +159,7 @@ describe("central compatibility recovery", () => { reason: CENTRAL_COMPATIBILITY_REJECTION_REASONS.TWEAK_MISMATCH, preferredTweakValue: { customChunkSize: 60 }, }), - showMessage: true, + progressPresentation: REPLICATION_PROGRESS_PRESENTATIONS.NOTICE, interaction: USER_INITIATED_REPLICATION_AUTHORITY, } as never); @@ -196,7 +197,7 @@ describe("central compatibility recovery", () => { outcome: replicationFailed(new Error("locked"), { reason: CENTRAL_COMPATIBILITY_REJECTION_REASONS.NODE_LOCKED, }), - showMessage: true, + progressPresentation: REPLICATION_PROGRESS_PRESENTATIONS.NOTICE, interaction: USER_INITIATED_REPLICATION_AUTHORITY, } as never); diff --git a/updates.md b/updates.md index 8109f0d6..de37bf53 100644 --- a/updates.md +++ b/updates.md @@ -16,6 +16,7 @@ Earlier releases remain available in the 1.0 release history, the 1.0 preview hi #### Fixed +- **Sync now** once again keeps routine progress quiet, while still opening recovery dialogues when a decision is required. Repeated OneShot Sync requests received while an earlier attempt is running are now ignored instead of starting overlapping work. - **Sync on Startup** now runs an immediate Object Storage synchronisation after start-up or resume, including migrated profiles which retain a Continuous setting that Object Storage cannot use. - A temporarily unavailable Object Storage synchronisation-parameter read is no longer treated as a missing object and cannot regenerate the shared Security Seed. Flow-specific Security Seed checks also bypass an earlier process-cached result. - Local database reset and plug-in unload now retire active replication through its owner before closing the database, without reporting a missing active Replicator or describing unload as a database reset.