feat(updater): download and prepare desktop updates in background

This commit is contained in:
二丫讲梵 authored and GitHub committed 2026-09-10 11:44:18 +08:00
1 parent 7affad8ae0
commit e96af4aa30
77 files changed
+10536 -814

No files matched your search

@@ -71,15 +71,15 @@ describe("queryStore adoptDetachedTab", () => {
expect(queryStore.isTabDirty(tab)).toBe(false);
});
it("does not touch the backend when the handoff already carries unsaved edits", async () => {
it.each(["SELECT 2;", ""])("does not replace unsaved edits %j with file content", async (sql) => {
const { useQueryStore } = await import("@/stores/queryStore");
const queryStore = useQueryStore();
const tabId = await queryStore.adoptDetachedTab(handoffFor({ sql: "SELECT 2;", originalSql: "SELECT 1;" }));
const tabId = await queryStore.adoptDetachedTab(handoffFor({ sql, originalSql: "SELECT 1;" }));
expect(mocks.loadSavedSqlFile).not.toHaveBeenCalled();
const tab = queryStore.tabs.find((candidate) => candidate.id === tabId)!;
expect(tab.sql).toBe("SELECT 2;");
expect(tab.sql).toBe(sql);
expect(queryStore.isTabDirty(tab)).toBe(true);
});
@@ -0,0 +1,96 @@
import { createPinia, setActivePinia } from "pinia";
import { beforeEach, describe, expect, it, vi } from "vitest";
const savedFile = { id: "saved", connectionId: "conn", database: "db", name: "saved.sql", sql: "SELECT 1;", createdAt: "2026-09-10T00:00:00Z", updatedAt: "2026-09-10T00:00:00Z" };
const mocks = vi.hoisted(() => ({ saved: null as any }));
vi.mock("@/lib/backend/api", async (importOriginal) => ({
...(await importOriginal<typeof import("@/lib/backend/api")>()),
loadOpenTabsState: async () => mocks.saved,
saveOpenTabsState: async (payload: unknown) => {
mocks.saved = payload;
},
listDetachedTabHandoffs: async () => [],
}));
describe("update draft recovery", () => {
beforeEach(() => {
vi.resetModules();
mocks.saved = null;
const storage = new Map<string, string>();
vi.stubGlobal("localStorage", { getItem: (key: string) => storage.get(key) ?? null, setItem: (key: string, value: string) => storage.set(key, value), removeItem: (key: string) => storage.delete(key) });
setActivePinia(createPinia());
});
it.each(["", "SELECT 2;"])("preserves a saved SQL draft %j through recovery and file hydration", async (sql) => {
const { useQueryStore } = await import("@/stores/queryStore");
const { useSettingsStore } = await import("@/stores/settingsStore");
const { useSavedSqlStore } = await import("@/stores/savedSqlStore");
const { UPDATE_RESTORE_KEY } = await import("@/lib/app/updatePreparation");
const store = useQueryStore();
const id = store.createTab("conn", "db");
Object.assign(store.tabs.find((tab) => tab.id === id)!, { savedSqlId: "saved", sql, originalSql: "SELECT 1;" });
await store.flushPendingPersist();
localStorage.setItem(UPDATE_RESTORE_KEY, "1");
setActivePinia(createPinia());
useSettingsStore().updateEditorSettings({ openTabsRestoreMode: "none", confirmUnsavedSqlClose: false });
const ensureFileContent = vi.spyOn(useSavedSqlStore(), "ensureFileContent");
const restored = useQueryStore();
await restored.initOpenTabs({ validConnectionIds: ["conn"] });
await restored.hydrateSavedSqlTabs();
const tab = restored.tabs.find((tab) => tab.id === id)!;
expect(tab.sql).toBe(sql);
expect(tab.originalSql).toBe("SELECT 1;");
expect(restored.isTabDirty(tab)).toBe(true);
expect(ensureFileContent).not.toHaveBeenCalled();
expect(localStorage.getItem(UPDATE_RESTORE_KEY)).toBeNull();
expect(restored.openSavedSql(savedFile)).toBe(id);
expect(tab.sql).toBe(sql);
expect(tab.originalSql).toBe("SELECT 1;");
expect(restored.isTabDirty(tab)).toBe(true);
ensureFileContent.mockRestore();
});
it("loads file content when reopening a clean restored SQL library tab", async () => {
const { useQueryStore } = await import("@/stores/queryStore");
const { UPDATE_RESTORE_KEY } = await import("@/lib/app/updatePreparation");
const store = useQueryStore();
const id = store.openSavedSql(savedFile);
await store.flushPendingPersist();
localStorage.setItem(UPDATE_RESTORE_KEY, "1");
setActivePinia(createPinia());
const restored = useQueryStore();
await restored.initOpenTabs({ validConnectionIds: ["conn"] });
const tab = restored.tabs.find((tab) => tab.id === id)!;
expect(tab.sql).toBe("");
expect(tab.originalSql).toBeUndefined();
expect(restored.openSavedSql(savedFile)).toBe(id);
expect(tab.sql).toBe(savedFile.sql);
expect(tab.originalSql).toBe(savedFile.sql);
expect(restored.isTabDirty(tab)).toBe(false);
});
it("recovers SQL and structure drafts once even when normal restore and close protection are disabled", async () => {
const { useQueryStore } = await import("@/stores/queryStore");
const { useSettingsStore } = await import("@/stores/settingsStore");
const { UPDATE_RESTORE_KEY } = await import("@/lib/app/updatePreparation");
const settings = useSettingsStore();
settings.updateEditorSettings({ openTabsRestoreMode: "none", confirmUnsavedSqlClose: false });
const store = useQueryStore();
const sqlId = store.createTab("conn", "db");
store.updateSql(sqlId, "select unsaved;");
const structureId = store.openTableStructure("conn", "db", undefined, "users");
const draft = { dirty: true, initialized: true, ddlDraft: "alter table users add column name text" };
store.tabs.find((tab) => tab.id === structureId)!.structureDraft = draft as any;
await store.flushPendingPersist();
localStorage.setItem(UPDATE_RESTORE_KEY, "1");
setActivePinia(createPinia());
useSettingsStore().updateEditorSettings({ openTabsRestoreMode: "none", confirmUnsavedSqlClose: false });
const restored = useQueryStore();
await restored.initOpenTabs({ validConnectionIds: ["conn"] });
expect(restored.tabs.find((tab) => tab.id === sqlId)?.sql).toBe("select unsaved;");
expect(restored.tabs.find((tab) => tab.id === structureId)?.structureDraft).toEqual(draft);
expect(localStorage.getItem(UPDATE_RESTORE_KEY)).toBeNull();
setActivePinia(createPinia());
useSettingsStore().updateEditorSettings({ openTabsRestoreMode: "none" });
const subsequent = useQueryStore();
await subsequent.initOpenTabs({ validConnectionIds: ["conn"] });
expect(subsequent.tabs).toEqual([]);
});
});