fix(settings): keep persisted query timeout across upgrades
This commit is contained in:
3 files changed
+109
-2
No files matched your search
@@ -312,6 +312,88 @@ describe("connectionStore timeout recovery", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps a persisted global query timeout over a stale backup when upgrading", async () => {
|
||||
// Reproduces #10024: the user saved a 60s global query timeout, but the
|
||||
// timeout-inheritance migration re-sourced the global value from a stale
|
||||
// localStorage backup (30s) on the first launch of the new build, resetting
|
||||
// the saved setting. The persisted blob already carries the value, so it must
|
||||
// win over the backup.
|
||||
localStorage.setItem(
|
||||
"dbx-timeout-inheritance-backup-v1",
|
||||
JSON.stringify({
|
||||
version: 1,
|
||||
globalConnectTimeoutSecs: 10,
|
||||
globalQueryTimeoutSecs: 30,
|
||||
connectSnapshots: {},
|
||||
querySnapshots: {},
|
||||
}),
|
||||
);
|
||||
const saveEditorSettings = vi.fn().mockResolvedValue(undefined);
|
||||
vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false }));
|
||||
vi.doMock("@/lib/backend/api", () => ({
|
||||
deleteSchemaCachePrefix: vi.fn().mockResolvedValue(undefined),
|
||||
loadEditorSettings: vi.fn().mockResolvedValue({
|
||||
globalConnectTimeoutSecs: 20,
|
||||
globalQueryTimeoutSecs: 60,
|
||||
timeoutInheritanceMigrationVersion: 1,
|
||||
}),
|
||||
loadConnections: vi.fn().mockResolvedValue([postgresConnection({ id: "custom", connect_timeout_secs: 45, query_timeout_secs: 300 })]),
|
||||
loadPinnedTreeNodeIds: vi.fn().mockResolvedValue([]),
|
||||
loadSidebarLayout: vi.fn().mockResolvedValue(null),
|
||||
loadTableVGroups: vi.fn().mockResolvedValue({}),
|
||||
loadTunnelProfiles: vi.fn().mockResolvedValue([]),
|
||||
saveConnections: vi.fn().mockResolvedValue(undefined),
|
||||
saveEditorSettings,
|
||||
saveSidebarLayout: vi.fn().mockResolvedValue(undefined),
|
||||
}));
|
||||
|
||||
const { useSettingsStore } = await import("@/stores/settingsStore");
|
||||
const settingsStore = useSettingsStore();
|
||||
const { useConnectionStore } = await import("@/stores/connectionStore");
|
||||
await useConnectionStore().initFromDisk();
|
||||
|
||||
expect(settingsStore.editorSettings.globalQueryTimeoutSecs).toBe(60);
|
||||
expect(settingsStore.editorSettings.globalConnectTimeoutSecs).toBe(20);
|
||||
expect(saveEditorSettings).toHaveBeenCalledWith(expect.objectContaining({ globalQueryTimeoutSecs: 60, globalConnectTimeoutSecs: 20 }));
|
||||
});
|
||||
|
||||
it("still recovers an absent global query timeout from the backup after a downgrade", async () => {
|
||||
// The migration must keep recovering the global value from the backup when
|
||||
// the persisted settings predate the setting entirely (no global keys on
|
||||
// disk) — the case the backup precedence was originally written for.
|
||||
localStorage.setItem(
|
||||
"dbx-timeout-inheritance-backup-v1",
|
||||
JSON.stringify({
|
||||
version: 1,
|
||||
globalConnectTimeoutSecs: 7,
|
||||
globalQueryTimeoutSecs: 12,
|
||||
connectSnapshots: { inherited: 7 },
|
||||
querySnapshots: { inherited: 12 },
|
||||
}),
|
||||
);
|
||||
vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false }));
|
||||
vi.doMock("@/lib/backend/api", () => ({
|
||||
deleteSchemaCachePrefix: vi.fn().mockResolvedValue(undefined),
|
||||
loadEditorSettings: vi.fn().mockResolvedValue({ timeoutInheritanceMigrationVersion: 1 }),
|
||||
loadConnections: vi.fn().mockResolvedValue([postgresConnection({ id: "inherited", connect_timeout_secs: 7, query_timeout_secs: 12 })]),
|
||||
loadPinnedTreeNodeIds: vi.fn().mockResolvedValue([]),
|
||||
loadSidebarLayout: vi.fn().mockResolvedValue(null),
|
||||
loadTableVGroups: vi.fn().mockResolvedValue({}),
|
||||
loadTunnelProfiles: vi.fn().mockResolvedValue([]),
|
||||
saveConnections: vi.fn().mockResolvedValue(undefined),
|
||||
saveEditorSettings: vi.fn().mockResolvedValue(undefined),
|
||||
saveSidebarLayout: vi.fn().mockResolvedValue(undefined),
|
||||
}));
|
||||
|
||||
const { useSettingsStore } = await import("@/stores/settingsStore");
|
||||
const settingsStore = useSettingsStore();
|
||||
const { useConnectionStore } = await import("@/stores/connectionStore");
|
||||
await useConnectionStore().initFromDisk();
|
||||
|
||||
expect(settingsStore.editorSettings.globalQueryTimeoutSecs).toBe(12);
|
||||
expect(settingsStore.editorSettings.queryTimeoutInheritConnectionIds).toContain("inherited");
|
||||
});
|
||||
|
||||
it("does not reclassify local default-valued overrides after migration", async () => {
|
||||
vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false }));
|
||||
vi.doMock("@/lib/backend/api", () => ({
|
||||
|
||||
@@ -9002,8 +9002,15 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
const backup = loadTimeoutInheritanceBackup();
|
||||
const connectIdsBefore = new Set(settingsStore.editorSettings.connectTimeoutInheritConnectionIds);
|
||||
const queryIdsBefore = new Set(settingsStore.editorSettings.queryTimeoutInheritConnectionIds);
|
||||
const globalConnectTimeoutSecs = migrationVersion < 2 && backup ? backup.globalConnectTimeoutSecs : settingsStore.editorSettings.globalConnectTimeoutSecs;
|
||||
const globalQueryTimeoutSecs = migrationVersion < 2 && backup ? backup.globalQueryTimeoutSecs : settingsStore.editorSettings.globalQueryTimeoutSecs;
|
||||
// Recover the global timeout from the localStorage backup only when the
|
||||
// settings blob on disk never carried one — the downgrade case, where an
|
||||
// older build predated the setting. When the user's persisted value exists it
|
||||
// is authoritative and must win over a backup that can lag behind it (the
|
||||
// upgrade case, where a stale backup otherwise reset a saved timeout).
|
||||
const recoverGlobalConnectFromBackup = migrationVersion < 2 && !!backup && !settingsStore.hasPersistedGlobalTimeout("connect");
|
||||
const recoverGlobalQueryFromBackup = migrationVersion < 2 && !!backup && !settingsStore.hasPersistedGlobalTimeout("query");
|
||||
const globalConnectTimeoutSecs = recoverGlobalConnectFromBackup ? backup!.globalConnectTimeoutSecs : settingsStore.editorSettings.globalConnectTimeoutSecs;
|
||||
const globalQueryTimeoutSecs = recoverGlobalQueryFromBackup ? backup!.globalQueryTimeoutSecs : settingsStore.editorSettings.globalQueryTimeoutSecs;
|
||||
|
||||
const resolveInheritance = (connection: ConnectionConfig, scope: "connect" | "query") => {
|
||||
const explicit = scope === "connect" ? connection.connect_timeout_inherit : connection.query_timeout_inherit;
|
||||
|
||||
@@ -1918,6 +1918,19 @@ export const useSettingsStore = defineStore("settings", () => {
|
||||
persistedAlwaysOnTopToolbarVisibility = visible;
|
||||
}
|
||||
|
||||
// Whether the editor-settings blob loaded from disk actually carried a global
|
||||
// timeout value. A normalized number is not enough to tell a user's saved
|
||||
// choice apart from the built-in default filled in by normalizeEditorSettings,
|
||||
// and the timeout-inheritance migration relies on that distinction: a persisted
|
||||
// value must win over the localStorage backup, while a value that was never on
|
||||
// disk (a downgrade, where the older build predated the setting) is recovered
|
||||
// from the backup. Tracked here at load time and read by the migration.
|
||||
const persistedGlobalTimeoutScopes = ref({ connect: false, query: false });
|
||||
|
||||
function hasPersistedGlobalTimeout(scope: "connect" | "query"): boolean {
|
||||
return persistedGlobalTimeoutScopes.value[scope];
|
||||
}
|
||||
|
||||
function enqueueEditorSettingsOperation<T>(operation: () => Promise<T>): Promise<T> {
|
||||
const queuedOperation = editorSettingsOperationQueue ? editorSettingsOperationQueue.then(operation) : operation();
|
||||
const trackedOperation = queuedOperation.then(
|
||||
@@ -1996,6 +2009,10 @@ export const useSettingsStore = defineStore("settings", () => {
|
||||
}
|
||||
editorSettings.value = normalized;
|
||||
persistedAlwaysOnTopToolbarVisibility = normalized.toolbarItems.alwaysOnTop;
|
||||
persistedGlobalTimeoutScopes.value = {
|
||||
connect: typeof savedSettings.globalConnectTimeoutSecs === "number",
|
||||
query: typeof savedSettings.globalQueryTimeoutSecs === "number" || typeof (savedSettings as { queryTimeoutSecs?: unknown }).queryTimeoutSecs === "number",
|
||||
};
|
||||
const needsExecuteModeDefaultMigration = typeof savedSettings.executeModeDefaultVersion !== "number" || savedSettings.executeModeDefaultVersion < EXECUTE_MODE_CURRENT_DEFAULT_VERSION;
|
||||
const needsTabNavigationShortcutMigration = needsTabNavigationHistoryShortcutMigration(savedSettings.shortcuts);
|
||||
const savedNullText = (savedSettings.dataGridExtractorOptions as Partial<DataGridExtractorOptions> | undefined)?.dsv?.nullText;
|
||||
@@ -2805,6 +2822,7 @@ export const useSettingsStore = defineStore("settings", () => {
|
||||
isConfigured,
|
||||
isEditorSettingsLoaded,
|
||||
editorSettings,
|
||||
hasPersistedGlobalTimeout,
|
||||
desktopSettings,
|
||||
mcpGlobalPolicy,
|
||||
initEditorSettings,
|
||||
|
||||
Reference in new issue
Block a user