Improve troubleshooting and compatible setting handling

This commit is contained in:
vorotamoroz
2026-07-24 08:51:30 +00:00
parent 058da1f34c
commit d489452583
15 changed files with 868 additions and 50 deletions
@@ -29,8 +29,6 @@ function valueToString(value: string | number | boolean | object | undefined): s
}
export class ModuleResolvingMismatchedTweaks extends AbstractModule {
private _hasNotifiedAutoAcceptCompatibleUndefined = false;
private _collectMismatchedTweakKeys(current: TweakValues, preferred: Partial<TweakValues>) {
const items = Object.keys(
TweakValuesShouldMatchedTemplate
@@ -64,33 +62,17 @@ export class ModuleResolvingMismatchedTweaks extends AbstractModule {
);
if (!hasOnlyCompatibleLossyMismatches) return undefined;
let autoAcceptCompatibleTweak = this.settings.autoAcceptCompatibleTweak;
if (this.settings.autoAcceptCompatibleTweak === undefined) {
if (this._hasNotifiedAutoAcceptCompatibleUndefined) {
return undefined;
}
this._hasNotifiedAutoAcceptCompatibleUndefined = true;
const CHOICE_ENABLE = $msg("TweakMismatchResolve.Action.EnableAutoAcceptCompatible");
const CHOICE_DISABLE = $msg("TweakMismatchResolve.Action.DisableAutoAcceptCompatible");
const CHOICES = [CHOICE_ENABLE, CHOICE_DISABLE] as const;
const message = $msg("TweakMismatchResolve.Message.AutoAcceptCompatibleUndefined");
const ret = await this.core.confirm.askSelectStringDialogue(message, CHOICES, {
title: $msg("TweakMismatchResolve.Title.AutoAcceptCompatible"),
timeout: 0,
defaultAction: CHOICE_ENABLE,
});
if (ret !== CHOICE_ENABLE) {
return undefined;
}
await this.services.setting.applyPartial(
{
autoAcceptCompatibleTweak: true,
},
true
);
Logger("Auto-accept for compatible tweak mismatch has been enabled.");
// Keep the settings object stable: settings panes and an in-flight replication retry can
// retain this reference while the default is persisted.
this.settings.autoAcceptCompatibleTweak = true;
await this.services.setting.saveSettingData();
autoAcceptCompatibleTweak = true;
Logger("Automatic alignment of compatible chunk settings has been enabled.");
}
if (this.settings.autoAcceptCompatibleTweak !== true) return undefined;
if (autoAcceptCompatibleTweak !== true) return undefined;
return this._selectNewerTweakSide(current, preferred);
}
@@ -215,7 +197,7 @@ export class ModuleResolvingMismatchedTweaks extends AbstractModule {
} else if (rebuildRecommended) {
CHOICE_AND_VALUES.push([CHOICE_USE_REMOTE, [preferred, false]]);
CHOICE_AND_VALUES.push([CHOICE_USE_MINE, [true, false]]);
CHOICE_AND_VALUES.push([CHOICE_USE_REMOTE_WITH_REBUILD, [true, true]]);
CHOICE_AND_VALUES.push([CHOICE_USE_REMOTE_WITH_REBUILD, [preferred, true]]);
CHOICE_AND_VALUES.push([CHOICE_USE_MINE_WITH_REBUILD, [true, true]]);
} else {
CHOICE_AND_VALUES.push([CHOICE_USE_REMOTE, [preferred, false]]);
@@ -255,9 +237,16 @@ export class ModuleResolvingMismatchedTweaks extends AbstractModule {
return "CHECKAGAIN";
}
if (conf) {
this.settings = { ...this.settings, ...conf };
await this.core.replicator.setPreferredRemoteTweakSettings(this.settings);
// ReplicationService retains the current settings object while it performs the immediate
// CHECKAGAIN retry. Update that object in place so the retry observes the accepted values.
Object.assign(this.settings, extractObject(TweakValuesTemplate, conf));
await this.services.setting.saveSettingData();
if (!rebuildRequired) {
// The failed replication has settled before mismatch resolution runs. Reinitialise the
// chunk-generation managers now so hash and splitter changes take effect before retrying.
await this.localDatabase.managers.reinitialise();
}
await this.core.replicator.setPreferredRemoteTweakSettings(this.settings);
if (rebuildRequired) {
await this.core.rebuilder.$fetchLocal();
}
@@ -1,9 +1,16 @@
import { describe, expect, it, vi } from "vitest";
import { DEFAULT_SETTINGS, REMOTE_COUCHDB, type RemoteDBSettings, type TweakValues } from "@vrtmrz/livesync-commonlib/compat/common/types";
import {
DEFAULT_SETTINGS,
REMOTE_COUCHDB,
type RemoteDBSettings,
type TweakValues,
} from "@vrtmrz/livesync-commonlib/compat/common/types";
import { ModuleResolvingMismatchedTweaks } from "./ModuleResolveMismatchedTweaks";
function createModule(settingsOverride: Partial<typeof DEFAULT_SETTINGS> = {}) {
const askSelectStringDialogue = vi.fn(async () => undefined);
const askSelectStringDialogue = vi.fn(async (..._args: unknown[]): Promise<string | undefined> => undefined);
const applyPartial = vi.fn(async (_partial: Record<string, unknown>): Promise<void> => undefined);
const reinitialise = vi.fn(async () => undefined);
const core = {
_services: {
API: {
@@ -15,6 +22,12 @@ function createModule(settingsOverride: Partial<typeof DEFAULT_SETTINGS> = {}) {
},
setting: {
saveSettingData: vi.fn(async () => undefined),
applyPartial,
},
},
localDatabase: {
managers: {
reinitialise,
},
},
settings: {
@@ -26,6 +39,9 @@ function createModule(settingsOverride: Partial<typeof DEFAULT_SETTINGS> = {}) {
askSelectStringDialogue,
},
} as any;
applyPartial.mockImplementation(async (partial: Record<string, unknown>) => {
core.settings = { ...core.settings, ...partial };
});
Object.defineProperty(core, "services", {
get() {
@@ -34,10 +50,35 @@ function createModule(settingsOverride: Partial<typeof DEFAULT_SETTINGS> = {}) {
});
const module = new ModuleResolvingMismatchedTweaks(core);
return { module, core, askSelectStringDialogue };
return { module, core, askSelectStringDialogue, applyPartial, reinitialise };
}
describe("ModuleResolvingMismatchedTweaks", () => {
it("should enable and auto-accept compatible mismatches when the preference is undefined", async () => {
const { module, core, askSelectStringDialogue, applyPartial } = createModule({
autoAcceptCompatibleTweak: undefined,
hashAlg: "xxhash64",
tweakModified: 100,
});
const initialSettings = core.settings;
const preferred = {
...(DEFAULT_SETTINGS as unknown as TweakValues),
hashAlg: "xxhash32",
tweakModified: 200,
} as Partial<TweakValues>;
const [conf, rebuild] = await module._checkAndAskResolvingMismatchedTweaks(preferred);
expect(conf).toEqual(preferred);
expect(rebuild).toBe(false);
expect(core.settings).toBe(initialSettings);
expect(core.settings.autoAcceptCompatibleTweak).toBe(true);
expect(core._services.setting.saveSettingData).toHaveBeenCalledTimes(1);
expect(applyPartial).not.toHaveBeenCalled();
expect(askSelectStringDialogue).not.toHaveBeenCalled();
});
it("should auto-accept compatible mismatches on connect check using newer remote tweakModified", async () => {
const { module, askSelectStringDialogue } = createModule({
autoAcceptCompatibleTweak: true,
@@ -58,6 +99,28 @@ describe("ModuleResolvingMismatchedTweaks", () => {
expect(askSelectStringDialogue).not.toHaveBeenCalled();
});
it.each([
{ label: "neither side has a recorded time", currentModified: 0, preferredModified: 0 },
{ label: "the recorded times are equal", currentModified: 200, preferredModified: 200 },
])("should use the remote compatible value when $label", async ({ currentModified, preferredModified }) => {
const { module, askSelectStringDialogue } = createModule({
autoAcceptCompatibleTweak: true,
hashAlg: "xxhash64",
tweakModified: currentModified,
});
const preferred = {
...(DEFAULT_SETTINGS as unknown as TweakValues),
hashAlg: "xxhash32",
tweakModified: preferredModified,
} as Partial<TweakValues>;
const [conf, rebuild] = await module._checkAndAskResolvingMismatchedTweaks(preferred);
expect(conf).toEqual(preferred);
expect(rebuild).toBe(false);
expect(askSelectStringDialogue).not.toHaveBeenCalled();
});
it("should fallback to manual confirmation when mismatches are mixed on connect check", async () => {
const { module, askSelectStringDialogue } = createModule({
autoAcceptCompatibleTweak: true,
@@ -80,6 +143,24 @@ describe("ModuleResolvingMismatchedTweaks", () => {
expect(askSelectStringDialogue).toHaveBeenCalledTimes(1);
});
it("should fetch after applying a compatible remote setting when the user selects the rebuild option", async () => {
const { module, askSelectStringDialogue } = createModule({
autoAcceptCompatibleTweak: false,
hashAlg: "xxhash64",
});
askSelectStringDialogue.mockResolvedValueOnce("Apply settings to this device, and fetch again");
const preferred = {
...(DEFAULT_SETTINGS as unknown as TweakValues),
hashAlg: "xxhash32",
} as TweakValues;
const [conf, rebuild] = await module._checkAndAskResolvingMismatchedTweaks(preferred);
expect(conf).toEqual(preferred);
expect(rebuild).toBe(true);
});
it("should auto-accept compatible mismatches on remote-config check using newer local tweakModified", async () => {
const { module, askSelectStringDialogue } = createModule({
autoAcceptCompatibleTweak: true,
@@ -105,4 +186,42 @@ describe("ModuleResolvingMismatchedTweaks", () => {
expect(result).toEqual({ result: false, requireFetch: false });
expect(askSelectStringDialogue).not.toHaveBeenCalled();
});
it("should apply remote compatible settings in place and reinitialise managers before retrying", async () => {
const { module, core, reinitialise } = createModule({
autoAcceptCompatibleTweak: true,
hashAlg: "xxhash64",
tweakModified: 100,
});
const initialSettings = core.settings;
const preferred = {
...(DEFAULT_SETTINGS as unknown as TweakValues),
hashAlg: "xxhash32",
tweakModified: 200,
} as TweakValues;
const calls: string[] = [];
core._services.tweakValue = {
checkAndAskResolvingMismatched: vi.fn(async () => [preferred, false]),
};
core._services.setting.saveSettingData = vi.fn(async () => {
calls.push("save");
});
core.replicator = {
tweakSettingsMismatched: true,
preferredTweakValue: preferred,
setPreferredRemoteTweakSettings: vi.fn(async () => {
calls.push("set-preferred");
}),
};
reinitialise.mockImplementation(async () => {
calls.push("reinitialise");
});
const result = await module._askResolvingMismatchedTweaks();
expect(result).toBe("CHECKAGAIN");
expect(core.settings).toBe(initialSettings);
expect(core.settings.hashAlg).toBe("xxhash32");
expect(calls).toEqual(["save", "reinitialise", "set-preferred"]);
});
});
@@ -206,7 +206,8 @@ export class LiveSyncSetting extends Setting {
const setValue = wrapMemo((value: boolean) => {
toggle.setValue(opt?.invert ? !value : value);
});
this.invalidateValue = () => setValue(LiveSyncSetting.env.editingSettings[key] ?? false);
this.invalidateValue = () =>
setValue(LiveSyncSetting.env.editingSettings[key] ?? opt?.defaultToggleValue ?? false);
this.invalidateValue();
toggle.onChange(async (value) => {
@@ -35,7 +35,9 @@ export function paneAdvanced(this: ObsidianLiveSyncSettingTab, paneEl: HTMLEleme
clampMin: 10,
onUpdate: this.onlyOnCouchDB,
});
new Setting(paneEl).setClass("wizardHidden").autoWireToggle("autoAcceptCompatibleTweak");
new Setting(paneEl)
.setClass("wizardHidden")
.autoWireToggle("autoAcceptCompatibleTweak", { defaultToggleValue: true });
// new Setting(paneEl)
// .setClass("wizardHidden")
// .autoWireToggle("sendChunksBulk", { onUpdate: onlyOnCouchDB })
@@ -75,6 +75,7 @@ export type AutoWireOption = {
holdValue?: boolean;
isPassword?: boolean;
invert?: boolean;
defaultToggleValue?: boolean;
onUpdate?: OnUpdateFunc;
obsolete?: boolean;
};