mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-10-09 02:42:30 +00:00
fix: initialise missing compatibility markers without pausing
This commit is contained in:
@@ -8,7 +8,7 @@ export const DATABASE_COMPATIBILITY_LEGACY_VERSION_KEY_PREFIX = "obsidian-live-s
|
||||
export const COMPATIBILITY_PAUSE_SETTING_MESSAGE =
|
||||
"Remote synchronisation is paused until this device's compatibility review has been completed.";
|
||||
|
||||
export type DatabaseCompatibilityVersionState = "missing" | "invalid" | "upgrade" | "downgrade";
|
||||
export type DatabaseCompatibilityVersionState = "invalid" | "upgrade" | "downgrade";
|
||||
|
||||
export interface DatabaseCompatibilityReason {
|
||||
source: "database-version";
|
||||
@@ -57,18 +57,9 @@ export interface CompatibilityEvaluationInput {
|
||||
|
||||
function databaseVersionReason(
|
||||
acknowledgedVersion: string | null,
|
||||
currentVersion: number,
|
||||
isNewVault: boolean
|
||||
currentVersion: number
|
||||
): DatabaseCompatibilityReason | undefined {
|
||||
if (acknowledgedVersion === null || acknowledgedVersion === "") {
|
||||
if (isNewVault) return undefined;
|
||||
return {
|
||||
source: "database-version",
|
||||
state: "missing",
|
||||
currentVersion,
|
||||
resumable: true,
|
||||
};
|
||||
}
|
||||
if (acknowledgedVersion === null || acknowledgedVersion === "") return undefined;
|
||||
|
||||
const parsed = Number(acknowledgedVersion);
|
||||
if (!Number.isSafeInteger(parsed)) {
|
||||
@@ -104,9 +95,8 @@ function databaseVersionReason(
|
||||
* The caller owns persistence, user interaction, and the actual replication gate.
|
||||
*/
|
||||
export function evaluateCompatibilityPause(input: CompatibilityEvaluationInput): CompatibilityEvaluation {
|
||||
const isNewVault = input.migrationState?.isNewVault === true;
|
||||
const reasons: CompatibilityPauseReason[] = [];
|
||||
const databaseReason = databaseVersionReason(input.acknowledgedVersion, input.currentVersion, isNewVault);
|
||||
const databaseReason = databaseVersionReason(input.acknowledgedVersion, input.currentVersion);
|
||||
if (databaseReason) reasons.push(databaseReason);
|
||||
|
||||
if (input.migrationState?.requiresSyncReview === true) {
|
||||
@@ -130,8 +120,7 @@ export function evaluateCompatibilityPause(input: CompatibilityEvaluationInput):
|
||||
|
||||
if (reasons.length === 0) {
|
||||
return {
|
||||
initialiseAcknowledgedVersion:
|
||||
isNewVault && (input.acknowledgedVersion === null || input.acknowledgedVersion === ""),
|
||||
initialiseAcknowledgedVersion: input.acknowledgedVersion === null || input.acknowledgedVersion === "",
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -68,8 +68,32 @@ describe("database compatibility evaluation", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("requires review when an existing Vault has no valid acknowledged version", () => {
|
||||
for (const acknowledgedVersion of [null, "invalid"]) {
|
||||
it.each([null, ""])(
|
||||
"initialises a missing marker (%s) without pausing an existing Vault",
|
||||
(acknowledgedVersion) => {
|
||||
const result = evaluateCompatibilityPause({
|
||||
acknowledgedVersion,
|
||||
currentVersion: 12,
|
||||
migrationState: migrationState(),
|
||||
legacyReviewMessage: "",
|
||||
});
|
||||
|
||||
expect(result).toEqual({ initialiseAcknowledgedVersion: true });
|
||||
}
|
||||
);
|
||||
|
||||
it("initialises a missing marker when no settings migration state is available", () => {
|
||||
expect(
|
||||
evaluateCompatibilityPause({
|
||||
acknowledgedVersion: null,
|
||||
currentVersion: 12,
|
||||
legacyReviewMessage: "",
|
||||
})
|
||||
).toEqual({ initialiseAcknowledgedVersion: true });
|
||||
});
|
||||
|
||||
it("requires review when an existing Vault has an invalid acknowledged version", () => {
|
||||
for (const acknowledgedVersion of ["invalid", "NaN", "12.5"]) {
|
||||
const result = evaluateCompatibilityPause({
|
||||
acknowledgedVersion,
|
||||
currentVersion: 12,
|
||||
@@ -77,39 +101,44 @@ describe("database compatibility evaluation", () => {
|
||||
legacyReviewMessage: "",
|
||||
});
|
||||
expect(result.pause?.resumable).toBe(true);
|
||||
expect(result.pause?.reasons[0]).toMatchObject({ source: "database-version" });
|
||||
expect(result.pause?.reasons[0]).toMatchObject({ source: "database-version", state: "invalid" });
|
||||
expect(result.initialiseAcknowledgedVersion).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
it("does not permit a future settings schema to be acknowledged by an older implementation", () => {
|
||||
const result = evaluateCompatibilityPause({
|
||||
acknowledgedVersion: "12",
|
||||
currentVersion: 12,
|
||||
migrationState: migrationState({
|
||||
sourceVersion: 3,
|
||||
targetVersion: 2,
|
||||
isFromFutureSchema: true,
|
||||
requiresSyncReview: true,
|
||||
}),
|
||||
legacyReviewMessage: "",
|
||||
});
|
||||
|
||||
expect(result.pause).toEqual({
|
||||
resumable: false,
|
||||
reasons: [
|
||||
{
|
||||
source: "settings-schema",
|
||||
it.each(["12", null])(
|
||||
"does not permit a future settings schema with marker %s to be acknowledged",
|
||||
(acknowledgedVersion) => {
|
||||
const result = evaluateCompatibilityPause({
|
||||
acknowledgedVersion,
|
||||
currentVersion: 12,
|
||||
migrationState: migrationState({
|
||||
sourceVersion: 3,
|
||||
currentVersion: 2,
|
||||
targetVersion: 2,
|
||||
isFromFutureSchema: true,
|
||||
resumable: false,
|
||||
reviewReasons: [],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
requiresSyncReview: true,
|
||||
}),
|
||||
legacyReviewMessage: "",
|
||||
});
|
||||
|
||||
it("retains a settings migration review in the host compatibility reason", () => {
|
||||
expect(result.pause).toEqual({
|
||||
resumable: false,
|
||||
reasons: [
|
||||
{
|
||||
source: "settings-schema",
|
||||
sourceVersion: 3,
|
||||
currentVersion: 2,
|
||||
isFromFutureSchema: true,
|
||||
resumable: false,
|
||||
reviewReasons: [],
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(result.initialiseAcknowledgedVersion).toBe(false);
|
||||
}
|
||||
);
|
||||
|
||||
it.each(["12", null])("retains a settings migration review with marker %s", (acknowledgedVersion) => {
|
||||
const reviewReasons = [
|
||||
{
|
||||
code: "legacy-update-review-pending",
|
||||
@@ -118,7 +147,7 @@ describe("database compatibility evaluation", () => {
|
||||
},
|
||||
];
|
||||
const result = evaluateCompatibilityPause({
|
||||
acknowledgedVersion: "12",
|
||||
acknowledgedVersion,
|
||||
currentVersion: 12,
|
||||
migrationState: migrationState({
|
||||
sourceVersion: 9,
|
||||
@@ -137,27 +166,32 @@ describe("database compatibility evaluation", () => {
|
||||
resumable: true,
|
||||
reviewReasons,
|
||||
});
|
||||
expect(result.initialiseAcknowledgedVersion).toBe(false);
|
||||
});
|
||||
|
||||
it("compatibility: retains an earlier unstructured review when no structured reason can be reconstructed", () => {
|
||||
const result = evaluateCompatibilityPause({
|
||||
acknowledgedVersion: "12",
|
||||
currentVersion: 12,
|
||||
migrationState: migrationState(),
|
||||
legacyReviewMessage: "Review an earlier compatibility change.",
|
||||
});
|
||||
it.each(["12", null])(
|
||||
"compatibility: retains an earlier unstructured review with marker %s",
|
||||
(acknowledgedVersion) => {
|
||||
const result = evaluateCompatibilityPause({
|
||||
acknowledgedVersion,
|
||||
currentVersion: 12,
|
||||
migrationState: migrationState(),
|
||||
legacyReviewMessage: "Review an earlier compatibility change.",
|
||||
});
|
||||
|
||||
expect(result.pause).toEqual({
|
||||
resumable: true,
|
||||
reasons: [
|
||||
{
|
||||
source: "legacy-review",
|
||||
message: "Review an earlier compatibility change.",
|
||||
resumable: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
expect(result.pause).toEqual({
|
||||
resumable: true,
|
||||
reasons: [
|
||||
{
|
||||
source: "legacy-review",
|
||||
message: "Review an earlier compatibility change.",
|
||||
resumable: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(result.initialiseAcknowledgedVersion).toBe(false);
|
||||
}
|
||||
);
|
||||
|
||||
it("compatibility: scopes the earlier review marker to the Vault", () => {
|
||||
expect(legacyDatabaseCompatibilityVersionKey("Example Vault")).toBe("obsidian-live-sync-verExample Vault");
|
||||
|
||||
@@ -66,9 +66,8 @@ export class CompatibilityReviewController {
|
||||
|
||||
// An existing unconfigured Vault cannot replicate, so a database
|
||||
// compatibility pause would only compete with onboarding and persist
|
||||
// a misleading sync warning. Do not acknowledge the missing marker:
|
||||
// activation on a later start must evaluate the same state again.
|
||||
// Genuinely new Vaults still initialise their marker below.
|
||||
// a misleading sync warning. Its next configured start evaluates any
|
||||
// known compatibility reasons before initialising an absent marker.
|
||||
if (settings.isConfigured !== true && migrationState?.isNewVault !== true) {
|
||||
this.pause = undefined;
|
||||
this.ui.clearReminder();
|
||||
|
||||
@@ -103,15 +103,43 @@ describe("compatibility review controller", () => {
|
||||
fixture.settings.isConfigured = true;
|
||||
await expect(fixture.controller.initialise()).resolves.toBe(true);
|
||||
|
||||
expect(fixture.controller.pendingPause?.reasons).toContainEqual({
|
||||
source: "database-version",
|
||||
state: "missing",
|
||||
currentVersion: 12,
|
||||
resumable: true,
|
||||
});
|
||||
expect(fixture.controller.pendingPause).toBeUndefined();
|
||||
expect(fixture.settings.versionUpFlash).toBe("");
|
||||
expect(fixture.local.get(DATABASE_COMPATIBILITY_VERSION_KEY)).toBe("12");
|
||||
expect(fixture.saveSettingData).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("starts a configured Vault with no device marker without a review or settings changes", async () => {
|
||||
const fixture = createFixture({ marker: null });
|
||||
const previousSettings = { ...fixture.settings };
|
||||
|
||||
await fixture.controller.initialise();
|
||||
await fixture.controller.openReview();
|
||||
|
||||
expect(fixture.local.get(DATABASE_COMPATIBILITY_VERSION_KEY)).toBe("12");
|
||||
expect(fixture.controller.pendingPause).toBeUndefined();
|
||||
expect(fixture.settings).toEqual(previousSettings);
|
||||
expect(fixture.saveSettingData).not.toHaveBeenCalled();
|
||||
expect(fixture.ui.showSummary).not.toHaveBeenCalled();
|
||||
expect(fixture.ui.showReminder).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("keeps an already persisted review when the device marker is absent", async () => {
|
||||
const fixture = createFixture({ marker: null, versionUpFlash: COMPATIBILITY_PAUSE_SETTING_MESSAGE });
|
||||
|
||||
await fixture.controller.initialise();
|
||||
await fixture.controller.openReview();
|
||||
|
||||
expect(fixture.settings.versionUpFlash).toBe(COMPATIBILITY_PAUSE_SETTING_MESSAGE);
|
||||
expect(fixture.controller.pendingPause?.reasons).toEqual([
|
||||
{
|
||||
source: "legacy-review",
|
||||
message: COMPATIBILITY_PAUSE_SETTING_MESSAGE,
|
||||
resumable: true,
|
||||
},
|
||||
]);
|
||||
expect(fixture.local.has(DATABASE_COMPATIBILITY_VERSION_KEY)).toBe(false);
|
||||
expect(fixture.saveSettingData).toHaveBeenCalledOnce();
|
||||
expect(fixture.ui.showReminder).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("preserves preferences and advances the marker only after an upgrade review is resumed", async () => {
|
||||
|
||||
@@ -19,9 +19,6 @@ function reasonMarkdown(reason: CompatibilityPauseReason): string {
|
||||
if (reason.state === "downgrade") {
|
||||
return `- This installation uses internal database version **${reason.currentVersion}**, but this device previously acknowledged newer version **${reason.acknowledgedVersion}**. An older installation must not resume synchronisation.`;
|
||||
}
|
||||
if (reason.state === "missing") {
|
||||
return `- No previously acknowledged internal database version was found for this existing Vault. This can happen when a Vault is copied or restored, or when it is opened with a new Obsidian profile. This installation uses version **${reason.currentVersion}**. An empty local database does not mean that it is safe to resume automatically.`;
|
||||
}
|
||||
return `- The saved internal database version marker is invalid. This installation uses version **${reason.currentVersion}**.`;
|
||||
}
|
||||
if (reason.source === "settings-schema") {
|
||||
|
||||
@@ -23,13 +23,13 @@ const resumablePause: CompatibilityPause = {
|
||||
};
|
||||
|
||||
describe("Obsidian compatibility review", () => {
|
||||
it("explains why a configured Vault can be missing its device-local acknowledgement", async () => {
|
||||
it("explains an invalid device-local acknowledgement", () => {
|
||||
const pause: CompatibilityPause = {
|
||||
resumable: true,
|
||||
reasons: [
|
||||
{
|
||||
source: "database-version",
|
||||
state: "missing",
|
||||
state: "invalid",
|
||||
currentVersion: 12,
|
||||
resumable: true,
|
||||
},
|
||||
@@ -37,9 +37,8 @@ describe("Obsidian compatibility review", () => {
|
||||
};
|
||||
|
||||
const details = compatibilityReviewDetailsMarkdown(pause);
|
||||
expect(details).toContain("copied or restored");
|
||||
expect(details).toContain("new Obsidian profile");
|
||||
expect(details).toContain("does not mean that it is safe to resume automatically");
|
||||
expect(details).toContain("saved internal database version marker is invalid");
|
||||
expect(details).toContain("version **12**");
|
||||
});
|
||||
|
||||
it("offers the generic resume action in a vertical action dialogue", async () => {
|
||||
|
||||
Reference in New Issue
Block a user