Files
dbx/apps/desktop/src/composables/__tests__/useSidebarDataOpenRuntime.spec.ts
T

672 lines
26 KiB
TypeScript
Vendored
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { beforeEach, describe, expect, it, vi } from "vitest";
import { useSidebarDataOpenRuntime } from "@/composables/useSidebarDataOpenRuntime";
import type { DataTabReuseMode } from "@/lib/tabs/dataTabReuseMode";
import type { QueryTab, TreeNode } from "@/types/database";
const mocks = vi.hoisted(() => ({
databaseType: "oceanbase" as string,
callOrder: [] as string[],
tabs: [] as QueryTab[],
activeTabId: null as string | null,
cachedMetadata: undefined as unknown,
dataTabReuseMode: "same-table" as DataTabReuseMode,
openDataTabsNextToActive: false,
metadataGeneration: 0,
ensureConnected: vi.fn(),
executeTabSql: vi.fn(),
loadTableMetadata: vi.fn(),
buildTableSelectSql: vi.fn(),
setErrorResult: vi.fn(),
cancelTabExecution: vi.fn(),
}));
vi.mock("@/stores/connectionStore", () => ({
useConnectionStore: () => ({
getConfig: () => ({ id: "connection-1", db_type: mocks.databaseType }),
ensureConnected: mocks.ensureConnected,
connectionIdentifierQuote: () => undefined,
metadataGenerationFor: () => mocks.metadataGeneration,
}),
}));
vi.mock("@/stores/queryStore", () => ({
useQueryStore: () => ({
tabs: mocks.tabs,
get activeTabId() {
return mocks.activeTabId;
},
createTab: (connectionId: string, database: string, title: string, mode: QueryTab["mode"], schema?: string, _initialSql?: string, catalog?: string, options: { forceNew?: boolean; insertAfterActive?: boolean } = {}) => {
if (!options.forceNew) {
const existing = mocks.tabs.find((tab) => tab.connectionId === connectionId && tab.database === database && tab.title === title && tab.mode === mode && (tab.schema || "") === (schema || "") && (tab.catalog || "") === (catalog || ""));
if (existing) {
mocks.activeTabId = existing.id;
return existing.id;
}
}
const tab = {
id: `tab-${mocks.tabs.length + 1}`,
connectionId,
database,
title,
mode,
schema,
catalog,
sql: "",
isDirty: false,
isExecuting: false,
isCancelling: false,
isExplaining: false,
} as QueryTab;
const activeIndex = options.insertAfterActive ? mocks.tabs.findIndex((item) => item.id === mocks.activeTabId) : -1;
if (activeIndex >= 0) mocks.tabs.splice(activeIndex + 1, 0, tab);
else mocks.tabs.push(tab);
mocks.activeTabId = tab.id;
return tab.id;
},
switchTab: vi.fn((id: string) => {
mocks.activeTabId = id;
}),
cancelTabExecution: mocks.cancelTabExecution,
setExecutingWithId: (id: string, executionId: string) => {
const tab = mocks.tabs.find((item) => item.id === id);
if (tab) {
tab.isExecuting = true;
tab.executionId = executionId;
}
},
setTableMeta: (id: string, tableMeta: NonNullable<QueryTab["tableMeta"]>) => {
const tab = mocks.tabs.find((item) => item.id === id);
if (tab) {
tab.tableMeta = tableMeta;
// 与真实 store 一致:记录写入时的连接元数据代次
tab.tableMetaGeneration = mocks.metadataGeneration;
tab.tableMetaUpdatedAt = Date.now();
// 与真实 store 一致:仅真实元数据(columns 非空)落地才结束行标识等待
if (tableMeta.columns.length > 0) tab.tableMetaPending = false;
}
},
updateSql: (id: string, sql: string) => {
const tab = mocks.tabs.find((item) => item.id === id);
if (tab) tab.sql = sql;
},
executeTabSql: mocks.executeTabSql,
setErrorResult: mocks.setErrorResult,
}),
}));
vi.mock("@/stores/settingsStore", () => ({
useSettingsStore: () => ({ editorSettings: { dataTabReuseMode: mocks.dataTabReuseMode, openDataTabsNextToActive: mocks.openDataTabsNextToActive, pageSize: 100 } }),
}));
vi.mock("@/lib/database/jdbcDialect", () => ({
effectiveDatabaseTypeForConnection: () => mocks.databaseType,
connectionObjectTreeNodeSchema: (_config: unknown, _database: string, schema?: string) => schema,
connectionObjectTreeQuerySchema: (_config: unknown, database: string, schema?: string) => schema ?? database,
}));
vi.mock("@/lib/metadata/tableMetadataCache", async (importOriginal) => {
const actual = await importOriginal<typeof import("@/lib/metadata/tableMetadataCache")>();
return {
...actual,
getCachedTableMetadata: () => mocks.cachedMetadata,
loadTableMetadata: mocks.loadTableMetadata,
};
});
vi.mock("@/lib/common/utils", () => ({ uuid: () => "open-data-id" }));
vi.mock("@/lib/backend/debugLog", () => ({ appendDebugLog: vi.fn(), isDebugLoggingEnabled: () => false }));
// dataTabOpenPolicy 使用真实实现,覆盖设置开关对应的复用范围
vi.mock("@/lib/sidebar/treeNodeContext", () => ({ hasTreeNodeDatabaseContext: () => true }));
vi.mock("@/lib/table/tableSelectSql", () => ({ buildTableSelectSql: mocks.buildTableSelectSql }));
vi.mock("@/lib/table/tableEditing", () => ({ usesSyntheticRowIdKey: () => false }));
vi.mock("@/lib/table/tableOpenPageLimit", () => ({ tableOpenPageLimit: () => 100 }));
vi.mock("@/lib/tabs/dataTabActivation", () => ({ canActivateExistingDataTableTab: () => false }));
const tableNode: TreeNode = {
id: "table-users",
label: "users",
type: "table",
connectionId: "connection-1",
database: "app",
schema: "public",
tableType: "TABLE",
};
const mysqlTableNode: TreeNode = {
...tableNode,
id: "table-zcyy-write-off-record",
label: "zcyy_write_off_record",
database: "yf_db",
schema: undefined,
};
describe("useSidebarDataOpenRuntime", () => {
beforeEach(() => {
vi.clearAllMocks();
mocks.databaseType = "oceanbase";
mocks.callOrder.length = 0;
mocks.tabs.length = 0;
mocks.activeTabId = null;
mocks.cachedMetadata = undefined;
mocks.dataTabReuseMode = "same-table";
mocks.openDataTabsNextToActive = false;
mocks.metadataGeneration = 0;
mocks.ensureConnected.mockResolvedValue(undefined);
mocks.buildTableSelectSql.mockResolvedValue("SELECT * FROM users");
mocks.executeTabSql.mockImplementation(async () => {
mocks.callOrder.push("query");
});
mocks.loadTableMetadata.mockImplementation(async () => {
mocks.callOrder.push("metadata");
return {
metadata: {
schema: "public",
tableName: "users",
tableType: "TABLE",
database: "app",
columns: [{ name: "id", data_type: "bigint", is_nullable: false, column_default: null, is_primary_key: true, extra: null }],
indexes: [],
primaryKeys: ["id"],
cachedAt: Date.now(),
},
cacheStatus: "miss",
ageMs: 0,
};
});
});
it("creates a new sidebar tab for the same table in always-new mode", async () => {
mocks.dataTabReuseMode = "always-new";
await useSidebarDataOpenRuntime().openData(tableNode);
await useSidebarDataOpenRuntime().openData(tableNode);
expect(mocks.tabs).toHaveLength(2);
});
it("keeps adjacent placement for every tab created in always-new mode", async () => {
mocks.dataTabReuseMode = "always-new";
mocks.openDataTabsNextToActive = true;
mocks.tabs.push(
{ id: "query-1", connectionId: "connection-1", database: "app", title: "Query 1", mode: "query", sql: "", isExecuting: false, isCancelling: false, isExplaining: false } as QueryTab,
{ id: "query-2", connectionId: "connection-1", database: "app", title: "Query 2", mode: "query", sql: "", isExecuting: false, isCancelling: false, isExplaining: false } as QueryTab,
);
mocks.activeTabId = "query-1";
await useSidebarDataOpenRuntime().openData(tableNode);
await useSidebarDataOpenRuntime().openData(tableNode);
expect(mocks.tabs.map((tab) => tab.title)).toEqual(["Query 1", "users", "users", "Query 2"]);
});
it("reuses a sidebar tab for the same table in same-table mode", async () => {
mocks.openDataTabsNextToActive = true;
await useSidebarDataOpenRuntime().openData(tableNode);
await useSidebarDataOpenRuntime().openData(tableNode);
expect(mocks.tabs).toHaveLength(1);
});
it("keeps different sidebar tables independent in same-table mode", async () => {
const ordersNode = { ...tableNode, id: "table-orders", label: "orders" };
await useSidebarDataOpenRuntime().openData(tableNode);
await useSidebarDataOpenRuntime().openData(ordersNode);
expect(mocks.tabs).toHaveLength(2);
expect(mocks.tabs.map((tab) => tab.title)).toEqual(["users", "orders"]);
});
it("opens a new table data tab next to the active tab when enabled", async () => {
mocks.openDataTabsNextToActive = true;
mocks.tabs.push(
{ id: "query-1", connectionId: "connection-1", database: "app", title: "Query 1", mode: "query", sql: "", isExecuting: false, isCancelling: false, isExplaining: false } as QueryTab,
{ id: "query-2", connectionId: "connection-1", database: "app", title: "Query 2", mode: "query", sql: "", isExecuting: false, isCancelling: false, isExplaining: false } as QueryTab,
);
mocks.activeTabId = "query-1";
await useSidebarDataOpenRuntime().openData(tableNode);
expect(mocks.tabs.map((tab) => tab.title)).toEqual(["Query 1", "users", "Query 2"]);
});
it("keeps appending new table data tabs when adjacent opening is disabled", async () => {
mocks.tabs.push(
{ id: "query-1", connectionId: "connection-1", database: "app", title: "Query 1", mode: "query", sql: "", isExecuting: false, isCancelling: false, isExplaining: false } as QueryTab,
{ id: "query-2", connectionId: "connection-1", database: "app", title: "Query 2", mode: "query", sql: "", isExecuting: false, isCancelling: false, isExplaining: false } as QueryTab,
);
mocks.activeTabId = "query-1";
await useSidebarDataOpenRuntime().openData(tableNode);
expect(mocks.tabs.map((tab) => tab.title)).toEqual(["Query 1", "Query 2", "users"]);
});
it("reuses the active safe data tab for a different table in active-tab mode", async () => {
mocks.dataTabReuseMode = "active-tab";
mocks.openDataTabsNextToActive = true;
const ordersNode = { ...tableNode, id: "table-orders", label: "orders" };
mocks.loadTableMetadata.mockImplementation(async (request: { database: string; schema?: string; tableName: string; tableType?: string }) => ({
metadata: {
schema: request.schema,
tableName: request.tableName,
tableType: request.tableType ?? "TABLE",
database: request.database,
columns: [{ name: "id", data_type: "bigint", is_nullable: false, column_default: null, is_primary_key: true, extra: null }],
indexes: [],
primaryKeys: ["id"],
cachedAt: Date.now(),
},
cacheStatus: "miss",
ageMs: 0,
}));
await useSidebarDataOpenRuntime().openData(tableNode);
mocks.tabs[0]!.isExecuting = false;
mocks.tabs[0]!.executionId = undefined;
mocks.tabs[0]!.resultLocalSortOriginalLargeValueCells = [{ row_index: 0, column_index: 1, original_bytes: 1_000_000 }];
await useSidebarDataOpenRuntime().openData(ordersNode);
expect(mocks.tabs).toHaveLength(1);
expect(mocks.tabs[0]?.title).toBe("orders");
expect(mocks.tabs[0]?.tableMeta?.tableName).toBe("orders");
expect(mocks.tabs[0]?.resultLocalSortOriginalLargeValueCells).toBeUndefined();
});
it.each([
["pinned", { pinned: true }],
["executing", { isExecuting: true, executionId: "running" }],
["pending edits", { pendingDataChangeCount: 1 }],
["pending editor draft", { hasPendingDataEditorDraft: true }],
])("opens a new tab instead of replacing an active %s data tab", async (_label, patch) => {
mocks.dataTabReuseMode = "active-tab";
const activeTab = {
id: "active-users",
connectionId: "connection-1",
database: "app",
title: "users",
mode: "data",
schema: "public",
sql: "SELECT * FROM users",
isExecuting: false,
isCancelling: false,
isExplaining: false,
tableMeta: { schema: "public", tableName: "users", tableType: "TABLE", columns: [], primaryKeys: [] },
...patch,
} as QueryTab;
mocks.tabs.push(activeTab);
mocks.activeTabId = activeTab.id;
await useSidebarDataOpenRuntime().openData({ ...tableNode, id: "table-orders", label: "orders" });
expect(mocks.tabs).toHaveLength(2);
expect(mocks.tabs[0]?.title).toBe("users");
expect(mocks.tabs[1]?.title).toBe("orders");
expect(mocks.cancelTabExecution).not.toHaveBeenCalledWith(activeTab.id);
});
it("creates a new HBase tab for the same table in always-new mode", async () => {
mocks.databaseType = "hbase";
mocks.dataTabReuseMode = "always-new";
await useSidebarDataOpenRuntime().openData(tableNode);
await useSidebarDataOpenRuntime().openData(tableNode);
expect(mocks.tabs).toHaveLength(2);
});
it("keeps different HBase tables independent in same-table mode", async () => {
mocks.databaseType = "hbase";
await useSidebarDataOpenRuntime().openData(tableNode);
await useSidebarDataOpenRuntime().openData({ ...tableNode, id: "table-orders", label: "orders" });
expect(mocks.tabs).toHaveLength(2);
});
it("opens a new HBase data tab next to the active tab when enabled", async () => {
mocks.databaseType = "hbase";
mocks.openDataTabsNextToActive = true;
mocks.tabs.push(
{ id: "query-1", connectionId: "connection-1", database: "app", title: "Query 1", mode: "query", sql: "", isExecuting: false, isCancelling: false, isExplaining: false } as QueryTab,
{ id: "query-2", connectionId: "connection-1", database: "app", title: "Query 2", mode: "query", sql: "", isExecuting: false, isCancelling: false, isExplaining: false } as QueryTab,
);
mocks.activeTabId = "query-1";
await useSidebarDataOpenRuntime().openData(tableNode);
expect(mocks.tabs.map((tab) => tab.title)).toEqual(["Query 1", "users", "Query 2"]);
});
it("starts cold-cache OceanBase metadata before the table query", async () => {
await useSidebarDataOpenRuntime().openData(tableNode);
await vi.waitFor(() => {
expect(mocks.callOrder).toEqual(["metadata", "query"]);
expect(mocks.tabs[0]?.tableMeta?.primaryKeys).toEqual(["id"]);
});
});
it("keeps Dameng metadata deferred until after the table query", async () => {
mocks.databaseType = "dameng";
await useSidebarDataOpenRuntime().openData(tableNode);
await vi.waitFor(() => {
expect(mocks.callOrder).toEqual(["query", "metadata"]);
expect(mocks.tabs[0]?.tableMeta?.primaryKeys).toEqual(["id"]);
});
});
it("keeps MySQL data-tab identity unqualified after metadata loads", async () => {
mocks.databaseType = "mysql";
mocks.loadTableMetadata.mockImplementation(async (request: { database: string; schema?: string; tableName: string; tableType?: string }) => ({
metadata: {
schema: request.schema,
tableName: request.tableName,
tableType: request.tableType,
database: request.database,
columns: [{ name: "id", data_type: "bigint", is_nullable: false, column_default: null, is_primary_key: true, extra: null }],
indexes: [],
primaryKeys: ["id"],
cachedAt: Date.now(),
},
cacheStatus: "miss",
ageMs: 0,
}));
await useSidebarDataOpenRuntime().openData(mysqlTableNode);
await vi.waitFor(() => expect(mocks.tabs[0]?.tableMeta?.primaryKeys).toEqual(["id"]));
expect(mocks.loadTableMetadata).toHaveBeenCalledWith(expect.objectContaining({ database: "yf_db", schema: "yf_db" }));
expect(mocks.buildTableSelectSql).toHaveBeenCalledWith(expect.objectContaining({ database: "yf_db", schema: undefined, tableName: "zcyy_write_off_record" }));
expect(mocks.tabs[0]?.tableMeta).toMatchObject({ database: "yf_db", schema: undefined, tableName: "zcyy_write_off_record" });
});
it("keeps cached MySQL table metadata unqualified", async () => {
mocks.databaseType = "mysql";
mocks.cachedMetadata = {
metadata: {
schema: "yf_db",
tableName: "zcyy_write_off_record",
tableType: "TABLE",
database: "yf_db",
columns: [{ name: "id", data_type: "bigint", is_nullable: false, column_default: null, is_primary_key: true, extra: null }],
indexes: [],
primaryKeys: ["id"],
cachedAt: Date.now(),
},
cacheStatus: "hit",
ageMs: 0,
};
await useSidebarDataOpenRuntime().openData(mysqlTableNode);
expect(mocks.loadTableMetadata).not.toHaveBeenCalled();
expect(mocks.buildTableSelectSql).toHaveBeenCalledWith(expect.objectContaining({ database: "yf_db", schema: undefined, tableName: "zcyy_write_off_record" }));
expect(mocks.tabs[0]?.tableMeta).toMatchObject({ database: "yf_db", schema: undefined, tableName: "zcyy_write_off_record" });
});
it("keeps row identity pending while delayed metadata is in flight and the query finishes first", async () => {
// 元数据延迟:查询先返回,元数据仍挂起
let releaseMetadata: () => void = () => {};
const gate = new Promise<void>((resolve) => {
releaseMetadata = resolve;
});
mocks.loadTableMetadata.mockImplementation(async () => {
mocks.callOrder.push("metadata-start");
await gate;
mocks.callOrder.push("metadata-done");
return {
metadata: {
schema: "public",
tableName: "users",
tableType: "TABLE",
database: "app",
columns: [{ name: "id", data_type: "bigint", is_nullable: false, column_default: null, is_primary_key: true, extra: null }],
indexes: [],
primaryKeys: ["id"],
cachedAt: Date.now(),
},
cacheStatus: "miss",
ageMs: 0,
};
});
await useSidebarDataOpenRuntime().openData(tableNode);
// 查询已完成、元数据尚未返回:行标识必须仍处于等待状态,
// primaryKeys 为空不得被当作"确认无主键"(#3727 整行 WHERE 保存路径)
expect(mocks.callOrder).toEqual(["metadata-start", "query"]);
expect(mocks.tabs[0]?.tableMeta?.primaryKeys).toEqual([]);
expect(mocks.tabs[0]?.tableMetaPending).toBe(true);
releaseMetadata();
await vi.waitFor(() => {
expect(mocks.tabs[0]?.tableMeta?.primaryKeys).toEqual(["id"]);
expect(mocks.tabs[0]?.tableMetaPending).toBe(false);
});
});
it("keeps the tab read-only when the metadata load fails", async () => {
mocks.loadTableMetadata.mockRejectedValue(new Error("metadata failed"));
await useSidebarDataOpenRuntime().openData(tableNode);
await vi.waitFor(() => {
expect(mocks.loadTableMetadata).toHaveBeenCalled();
});
// 行标识仍然未知:保持只读兜底,不回退到整行 WHERE 写入;
// 刷新或重开表会重试元数据加载并恢复可编辑
expect(mocks.tabs[0]?.tableMetaPending).toBe(true);
expect(mocks.tabs[0]?.tableMeta?.primaryKeys).toEqual([]);
});
it("keeps the superseded tab read-only when metadata never starts", async () => {
// openData 在 ensureConnected 阶段被新请求取代:元数据请求不会启动
let releaseConnect: () => void = () => {};
mocks.ensureConnected.mockImplementation(
() =>
new Promise<void>((resolve) => {
releaseConnect = resolve;
}),
);
let current = true;
const request = {
isCurrent: () => current,
signal: new AbortController().signal,
registerCancel: () => {},
};
const open = useSidebarDataOpenRuntime().openData(tableNode, request);
await vi.waitFor(() => {
expect(mocks.ensureConnected).toHaveBeenCalled();
});
current = false;
releaseConnect();
await open;
// 行标识从未确认:保持只读兜底;重开该表或刷新会重新加载元数据恢复
expect(mocks.loadTableMetadata).not.toHaveBeenCalled();
expect(mocks.executeTabSql).not.toHaveBeenCalled();
expect(mocks.tabs[0]?.tableMetaPending).toBe(true);
});
it("activates an already-running same-table tab without cancelling it", async () => {
mocks.tabs.push({
id: "existing-tab",
connectionId: "connection-1",
database: "app",
title: "users",
mode: "data",
schema: "public",
sql: "",
isDirty: false,
isExecuting: true,
executionId: "stale-execution",
isCancelling: false,
isExplaining: false,
tableMeta: {
schema: "public",
tableName: "users",
tableType: "TABLE",
columns: [{ name: "id", data_type: "bigint", is_nullable: false, column_default: null, is_primary_key: true, extra: null }],
primaryKeys: ["id"],
},
tableMetaUpdatedAt: Date.now(),
// 真实 tab 的 tableMeta 必经 setTableMeta 写入并记录当前代次(0)
tableMetaGeneration: 0,
} as QueryTab);
mocks.activeTabId = null;
await useSidebarDataOpenRuntime().openData(tableNode);
expect(mocks.tabs).toHaveLength(1);
expect(mocks.activeTabId).toBe("existing-tab");
expect(mocks.cancelTabExecution).not.toHaveBeenCalled();
expect(mocks.executeTabSql).not.toHaveBeenCalled();
expect(mocks.loadTableMetadata).not.toHaveBeenCalled();
});
it("does not mark row identity pending on a warm metadata cache", async () => {
mocks.cachedMetadata = {
metadata: {
schema: "public",
tableName: "users",
tableType: "TABLE",
database: "app",
columns: [{ name: "id", data_type: "bigint", is_nullable: false, column_default: null, is_primary_key: true, extra: null }],
indexes: [],
primaryKeys: ["id"],
cachedAt: Date.now(),
},
cacheStatus: "hit",
ageMs: 0,
};
await useSidebarDataOpenRuntime().openData(tableNode);
expect(mocks.tabs[0]?.tableMeta?.primaryKeys).toEqual(["id"]);
expect(mocks.tabs[0]?.tableMetaPending).toBeFalsy();
expect(mocks.loadTableMetadata).not.toHaveBeenCalled();
});
it("keeps tab-local metadata warm within the TTL window (no reload on reopen)", async () => {
const reload = vi.fn();
mocks.loadTableMetadata.mockImplementation(async () => {
reload();
return {
metadata: {
schema: "public",
tableName: "users",
tableType: "TABLE",
database: "app",
columns: [{ name: "id", data_type: "bigint", is_nullable: false, column_default: null, is_primary_key: true, extra: null }],
indexes: [],
primaryKeys: ["id"],
cachedAt: Date.now(),
},
cacheStatus: "miss",
ageMs: 0,
};
});
await useSidebarDataOpenRuntime().openData(tableNode);
// 第一次打开已把 tab-local tableMetaUpdatedAt 置为新鲜
expect(mocks.tabs[0]?.tableMetaUpdatedAt).toBeDefined();
// 复位执行态,让第二次打开走重开(reuse)路径而非 activate 早退
mocks.tabs[0]!.isExecuting = false;
mocks.tabs[0]!.executionId = undefined;
// 未发生连接生命周期变化:TTL 窗口内重开同表直接复用 tab-local 元数据
await useSidebarDataOpenRuntime().openData(tableNode);
expect(reload).toHaveBeenCalledTimes(1);
expect(mocks.tabs[0]?.tableMeta?.tableName).toBe("users");
});
it("reloads tab metadata after a reconnect boundary (issue #6623 reconnect regression)", async () => {
// 第一次打开的元数据:[id]
await useSidebarDataOpenRuntime().openData(tableNode);
expect(mocks.loadTableMetadata).toHaveBeenCalledTimes(1);
expect(mocks.tabs[0]?.tableMeta?.columns.map((column) => column.name)).toEqual(["id"]);
expect(mocks.tabs[0]?.tableMetaUpdatedAt).toBeDefined();
// 外部 ALTER TABLE ... ADD age:DBX 断开→重连。断开时 connectionStore 会
// 清掉该连接下数据标签页的 freshness 戳(staleConnectionDataTabMetadata),
// 此处等价模拟该生命周期边界。
mocks.tabs[0]!.tableMetaUpdatedAt = undefined;
mocks.tabs[0]!.isExecuting = false;
mocks.tabs[0]!.executionId = undefined;
// 第二次打开(重连后):即使处于原 30s TTL 窗口内,也必须重新拉取结构
mocks.loadTableMetadata.mockImplementationOnce(async () => ({
metadata: {
schema: "public",
tableName: "users",
tableType: "TABLE",
database: "app",
columns: [
{ name: "id", data_type: "bigint", is_nullable: false, column_default: null, is_primary_key: true, extra: null },
{ name: "age", data_type: "integer", is_nullable: true, column_default: null, is_primary_key: false, extra: null },
],
indexes: [],
primaryKeys: ["id"],
cachedAt: Date.now(),
},
cacheStatus: "miss",
ageMs: 0,
}));
await useSidebarDataOpenRuntime().openData(tableNode);
expect(mocks.loadTableMetadata).toHaveBeenCalledTimes(2);
await vi.waitFor(() => {
expect(mocks.tabs[0]?.tableMeta?.columns.map((column) => column.name)).toContain("age");
});
});
it("does not write stale in-flight metadata back to the tab after a disconnect boundary", async () => {
// 手动挂起的元数据加载:openData 启动后台加载后停留在 in-flight
let resolveMetadata!: (value: Awaited<ReturnType<typeof mocks.loadTableMetadata>>) => void;
const pendingMetadata = new Promise<Awaited<ReturnType<typeof mocks.loadTableMetadata>>>((resolve) => {
resolveMetadata = resolve;
});
mocks.loadTableMetadata.mockReturnValueOnce(pendingMetadata);
const openPromise = useSidebarDataOpenRuntime().openData(tableNode);
await vi.waitFor(() => {
expect(mocks.loadTableMetadata).toHaveBeenCalledTimes(1);
});
// 模拟 disconnect 生命周期边界:连接代次递增 + freshness 戳被清
// (staleConnectionDataTabMetadata)
mocks.metadataGeneration = 1;
mocks.tabs[0]!.tableMetaUpdatedAt = undefined;
// 旧连接在途的结果此时才返回:shared 缓存写回已被 per-scope 失效代数
// 挡住,tab-local 写回必须被连接代次校验拦下(PR #6640 review blocker 1)
resolveMetadata({
metadata: {
schema: "public",
tableName: "users",
tableType: "TABLE",
database: "app",
columns: [{ name: "id", data_type: "bigint", is_nullable: false, column_default: null, is_primary_key: true, extra: null }],
indexes: [],
primaryKeys: ["id"],
cachedAt: Date.now(),
},
cacheStatus: "miss",
ageMs: 0,
});
await openPromise;
await vi.waitFor(() => {
expect(mocks.tabs[0]?.tableMetaUpdatedAt).toBeUndefined();
});
// 占位元数据未被旧列覆盖:freshness 保持在失效后的"冷"状态
expect(mocks.tabs[0]?.tableMeta?.columns).toEqual([]);
expect(mocks.tabs[0]?.tableMetaGeneration).toBe(0);
});
});