From b152361184df6e74b4b8fce7aba41a319ed33394 Mon Sep 17 00:00:00 2001 From: zipg Date: Tue, 29 Sep 2026 01:01:00 +0800 Subject: [PATCH] =?UTF-8?q?fix(connection):=20=E5=90=8E=E5=8F=B0=20querySt?= =?UTF-8?q?ore=20=E5=8A=A8=E6=80=81=20import=20=E5=A4=B1=E8=B4=A5=E4=B8=8D?= =?UTF-8?q?=E5=86=8D=E6=8A=9B=E5=87=BA=E6=9C=AA=E5=A4=84=E7=90=86=E6=8B=92?= =?UTF-8?q?=E7=BB=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 被动断链(markConnectionLost)与断开连接会以 fire-and-forget 的方式 `void import("@/stores/queryStore")` 清理数据标签页的 tableMeta freshness。 该 import 依赖之后才解析的 chunk,失败时会产生未处理的拒绝: EnvironmentTeardownError: Cannot load '.../lib/sql/sqlDiagnostics.ts' imported from '.../lib/sql/errorPosition.ts' after the environment was torn down. Vitest 把这类未处理拒绝记为整个分片失败:CI 上 #10513 的 frontend-test 773 个文件、9310 条用例全部通过,却因为 4 个未处理拒绝让作业 exit 1。 清理 freshness 本身是尽力而为、晚一点清也安全,所以失败必须被吞掉。 新增回归测试 connectionStore.metadataInvalidationImport.spec.ts:让 queryStore 的 deferred import 必然失败,验证不再泄漏未处理拒绝(修复前该文件 exit 1, 修复后 exit 0)。 --- ...onStore.metadataInvalidationImport.spec.ts | 53 +++++++++++++++++++ apps/desktop/src/stores/connectionStore.ts | 11 ++-- 2 files changed, 61 insertions(+), 3 deletions(-) create mode 100644 apps/desktop/src/stores/__tests__/connectionStore.metadataInvalidationImport.spec.ts diff --git a/apps/desktop/src/stores/__tests__/connectionStore.metadataInvalidationImport.spec.ts b/apps/desktop/src/stores/__tests__/connectionStore.metadataInvalidationImport.spec.ts new file mode 100644 index 00000000000..51057937222 --- /dev/null +++ b/apps/desktop/src/stores/__tests__/connectionStore.metadataInvalidationImport.spec.ts @@ -0,0 +1,53 @@ +import { createPinia, setActivePinia } from "pinia"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import i18n from "@/i18n"; + +function installLocalStorage() { + const data = new Map(); + vi.stubGlobal("localStorage", { + getItem: vi.fn((key: string) => data.get(key) ?? null), + setItem: vi.fn((key: string, value: string) => data.set(key, value)), + removeItem: vi.fn((key: string) => data.delete(key)), + }); +} + +describe("connectionStore metadata lifetime invalidation", () => { + beforeEach(() => { + vi.resetModules(); + vi.unstubAllGlobals(); + installLocalStorage(); + setActivePinia(createPinia()); + i18n.global.locale.value = "en"; + vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false })); + vi.doMock("@/lib/backend/api", () => ({ + checkConnectionHealth: vi.fn().mockRejectedValue(new Error("back end pool is gone")), + listInstalledAgents: vi.fn().mockResolvedValue([]), + loadSchemaCache: vi.fn().mockResolvedValue(null), + saveConnections: vi.fn().mockResolvedValue(undefined), + saveSchemaCache: vi.fn().mockResolvedValue(undefined), + saveSidebarLayout: vi.fn().mockResolvedValue(undefined), + })); + }); + + afterEach(() => { + vi.doUnmock("@/stores/queryStore"); + }); + + it("swallows a failing deferred queryStore import instead of leaking an unhandled rejection", async () => { + vi.doMock("@/stores/queryStore", () => { + throw new Error("deferred queryStore module failed to load"); + }); + + const { useConnectionStore } = await import("@/stores/connectionStore"); + const store = useConnectionStore(); + + // 被动断链会以 fire-and-forget 方式(void import)清 data-tab freshness。该 + // deferred import 失败时——CI 上表现为「环境已销毁后 chunk 才请求失败」——必须 + // 被吞掉,否则未处理的拒绝会让整个 frontend-test 分片失败。 + store.markConnectionLost("mssql-1", new Error("back end pool is gone")); + + await vi.dynamicImportSettled(); + + expect(store.connectedIds.has("mssql-1")).toBe(false); + }); +}); diff --git a/apps/desktop/src/stores/connectionStore.ts b/apps/desktop/src/stores/connectionStore.ts index feb006eafbf..8ed15594ff3 100644 --- a/apps/desktop/src/stores/connectionStore.ts +++ b/apps/desktop/src/stores/connectionStore.ts @@ -2496,9 +2496,14 @@ export const useConnectionStore = defineStore("connection", () => { function invalidateConnectionMetadataLifetime(connectionId: string, database?: string) { invalidateMetadataCaches({ connectionId, database }); bumpMetadataGeneration(connectionId, database); - void import("@/stores/queryStore").then(({ useQueryStore }) => { - useQueryStore().staleConnectionDataTabMetadata(connectionId, database); - }); + // 该 import 是「稍后清理 freshness」的尽力而为路径:断开连接后数据标签页的 + // tableMeta 不再算 warm cache,晚一点清也安全。因此这里必须吞掉失败,避免 + // chunk 加载失败(或测试环境已销毁)变成未处理的拒绝。 + void import("@/stores/queryStore") + .then(({ useQueryStore }) => { + useQueryStore().staleConnectionDataTabMetadata(connectionId, database); + }) + .catch(() => undefined); } function invalidateMetadataCaches(match: MetadataCacheInvalidation): number {