import { nextTick } from "vue"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { matchQuickOpenText, useQuickOpen } from "@/composables/useQuickOpen"; import * as api from "@/lib/backend/api"; import { getSqlFileFilter, getSqlFileFolderPaths, sqlFileFoldersVersion } from "@/lib/sqlFile/sqlFileFolders"; import { useConnectionStore } from "@/stores/connectionStore"; import { useSavedSqlStore } from "@/stores/savedSqlStore"; vi.mock("@/stores/connectionStore", () => ({ useConnectionStore: vi.fn(), })); vi.mock("@/stores/savedSqlStore", () => ({ useSavedSqlStore: vi.fn(), })); vi.mock("@/lib/backend/api", () => ({ listSqlFilesInFolder: vi.fn(), readExternalSqlFile: vi.fn(), })); vi.mock("@/lib/sqlFile/sqlFileFolders", async () => { const { ref } = await import("vue"); return { getSqlFileFilter: vi.fn(() => "*.sql"), getSqlFileFolderPaths: vi.fn(), sqlFileFoldersVersion: ref(0), }; }); function emptySavedSqlStore() { return { allFiles: [] as any[], getFile: vi.fn().mockReturnValue(undefined), }; } function deferred() { let resolve!: (value: T) => void; let reject!: (reason?: unknown) => void; const promise = new Promise((promiseResolve, promiseReject) => { resolve = promiseResolve; reject = promiseReject; }); return { promise, resolve, reject }; } async function flushAsyncWork(): Promise { await Promise.resolve(); await Promise.resolve(); await nextTick(); } describe("useQuickOpen", () => { beforeEach(() => { vi.mocked(useSavedSqlStore).mockReturnValue(emptySavedSqlStore() as any); vi.mocked(getSqlFileFolderPaths).mockReturnValue([]); }); describe("external SQL files", () => { it("reloads when folders change during an in-flight scan", async () => { vi.mocked(useConnectionStore).mockReturnValue({ connections: [], treeNodes: [] } as any); vi.mocked(getSqlFileFolderPaths).mockReturnValueOnce(["/old"]).mockReturnValue(["/new"]); const oldScan = deferred>>(); vi.mocked(api.listSqlFilesInFolder).mockImplementation((path) => { if (path === "/old") return oldScan.promise; return Promise.resolve([{ name: "new.sql", path: "/new/new.sql", is_dir: false, children: [] }]); }); const { filteredItems, loadExternalSqlFiles, setQuery } = useQuickOpen(); const initialLoad = loadExternalSqlFiles(); expect(api.listSqlFilesInFolder).toHaveBeenCalledWith("/old", getSqlFileFilter()); sqlFileFoldersVersion.value++; await nextTick(); oldScan.resolve([{ name: "old.sql", path: "/old/old.sql", is_dir: false, children: [] }]); await initialLoad; expect(api.listSqlFilesInFolder).toHaveBeenCalledTimes(2); expect(api.listSqlFilesInFolder).toHaveBeenLastCalledWith("/new", getSqlFileFilter()); setQuery(".sql"); expect(filteredItems.value.map((item) => item.label)).toContain("new.sql"); expect(filteredItems.value.map((item) => item.label)).not.toContain("old.sql"); }); }); describe("fuzzyMatch function", () => { it("ranks exact names, initials, prefixes, substrings, and fuzzy matches in order", () => { const exact = matchQuickOpenText("shop", "shop"); const initials = matchQuickOpenText("gafi", "groupon_apply_finance_invoice"); const prefix = matchQuickOpenText("shop", "shop_attribute"); const substring = matchQuickOpenText("shop", "sale_payway_shop"); const fuzzy = matchQuickOpenText("gafi", "giftcard_define_item"); expect([exact?.kind, initials?.kind, prefix?.kind, substring?.kind, fuzzy?.kind]).toEqual(["exact", "initials", "prefix", "substring", "fuzzy"]); expect(exact!.score).toBeLessThan(initials!.score); expect(initials!.score).toBeLessThan(prefix!.score); expect(prefix!.score).toBeLessThan(substring!.score); expect(substring!.score).toBeLessThan(fuzzy!.score); expect(initials?.indices).toEqual([0, 8, 14, 22]); }); it("matches multi-word prefix combinations and highlights their source characters", () => { const label = "giftcard_define_shop_log"; expect(matchQuickOpenText("gdsl", label)?.kind).toBe("initials"); for (const query of ["giftdsl", "gdshopl", "gdesl"]) { const match = matchQuickOpenText(query, label); expect(match?.kind).toBe("word-prefix"); expect(match?.indices.map((index) => label[index]).join("")).toBe(query); } expect(matchQuickOpenText("giftcard", label)?.kind).toBe("prefix"); expect(matchQuickOpenText("define", label)?.kind).toBe("substring"); expect(matchQuickOpenText("shop", label)?.kind).toBe("substring"); expect(matchQuickOpenText("cardshoplog", label)?.kind).toBe("fuzzy"); }); it("matches Chinese table names by pinyin initials (issue #7912)", () => { // Reported: 全局搜索中文表名输入首字母模糊搜索搜不出来 expect(matchQuickOpenText("zzj", "总租金")?.kind).toBe("initials"); expect(matchQuickOpenText("yhmx", "用户明细")?.kind).toBe("initials"); }); it("matches Chinese table names by a non-contiguous pinyin-initials subsequence", () => { const match = matchQuickOpenText("zj", "总租金"); expect(match?.kind).toBe("fuzzy"); expect(match?.indices.map((index) => "总租金"[index]).join("")).toBe("总租金".slice(0, 1) + "总租金".slice(2, 3)); }); it("keeps pinyin-initials highlight indices aligned across unmapped supplementary-plane characters", () => { const match = matchQuickOpenText("bx", "𠮷表x"); expect(match?.kind).toBe("fuzzy"); expect(match?.indices).toEqual([2, 3]); }); it("prefers a literal prefix match over pinyin-initials for mixed Han+Latin names", () => { const literal = matchQuickOpenText("abc", "abc表"); expect(literal?.kind).toBe("prefix"); expect(literal?.score).toBeLessThan(250); }); it("puts the exact shop result before prefixed and containing names", () => { vi.mocked(useConnectionStore).mockReturnValue({ connections: [ { id: "contains", name: "sale_payway_shop", type: "mssql" }, { id: "prefix", name: "shop_attribute", type: "mssql" }, { id: "exact", name: "shop", type: "mssql" }, ], treeNodes: [], } as any); const { filteredItems, setQuery } = useQuickOpen(); setQuery("shop"); expect(filteredItems.value.map((item) => item.label)).toEqual(["shop", "shop_attribute", "sale_payway_shop"]); }); it("puts an exact identifier acronym before loose fuzzy matches", () => { vi.mocked(useConnectionStore).mockReturnValue({ connections: [ { id: "fuzzy", name: "giftcard_define_item", type: "mssql" }, { id: "initials", name: "groupon_apply_finance_invoice", type: "mssql" }, ], treeNodes: [], } as any); const { filteredItems, setQuery } = useQuickOpen(); setQuery("gafi"); expect(filteredItems.value.map((item) => item.label)).toEqual(["groupon_apply_finance_invoice", "giftcard_define_item"]); expect(filteredItems.value[0].matchIndices).toEqual([0, 8, 14, 22]); }); it("should return exact substring match with score 1", () => { // Mock store with test data const mockStore = { connections: [{ id: "conn1", name: "MyConnection", type: "mssql" }], treeNodes: [ { connectionId: "conn1", type: "database", database: "MyDatabase", label: "MyDatabase", }, ], }; vi.mocked(useConnectionStore).mockReturnValue(mockStore as any); const { filteredItems, setQuery } = useQuickOpen(); setQuery("MyDatabase"); // After search, we should find the exact match expect(filteredItems.value.length).toBeGreaterThan(0); const result = filteredItems.value.find((item) => item.label === "MyDatabase"); expect(result).toBeDefined(); if (result) { expect(result.matchScore).toBe(1); // Exact substring match score } }); it("should handle empty query by returning all items", () => { const mockStore = { connections: [ { id: "conn1", name: "Connection1", type: "mssql" }, { id: "conn2", name: "Connection2", type: "postgres" }, ], treeNodes: [], }; vi.mocked(useConnectionStore).mockReturnValue(mockStore as any); const { filteredItems, setQuery } = useQuickOpen(); setQuery(""); // Empty query should return all items (2 connections + 0 SQL library files) expect(filteredItems.value.length).toBe(2); filteredItems.value.forEach((item) => { expect(item.matchScore).toBe(Infinity); }); }); it("should perform fuzzy matching for non-consecutive characters", () => { const mockStore = { connections: [{ id: "conn1", name: "MyConnection", type: "mssql" }], treeNodes: [], }; vi.mocked(useConnectionStore).mockReturnValue(mockStore as any); const { filteredItems, setQuery } = useQuickOpen(); setQuery("MyCo"); // Fuzzy match should find "MyConnection" const result = filteredItems.value.find((item) => item.label === "MyConnection"); expect(result).toBeDefined(); }); it("should return null for non-matching query", () => { const mockStore = { connections: [{ id: "conn1", name: "MyConnection", type: "mssql" }], treeNodes: [], }; vi.mocked(useConnectionStore).mockReturnValue(mockStore as any); const { filteredItems, setQuery } = useQuickOpen(); setQuery("XYZ"); // No match should return empty results expect(filteredItems.value.length).toBe(0); }); it("should score consecutive characters higher than non-consecutive", () => { const mockStore = { connections: [ { id: "conn1", name: "user_login_table", type: "mssql" }, { id: "conn2", name: "user_data_login", type: "mssql" }, ], treeNodes: [], }; vi.mocked(useConnectionStore).mockReturnValue(mockStore as any); const { filteredItems, setQuery } = useQuickOpen(); setQuery("login"); // "user_login_table" has consecutive "login" match (better score) // "user_data_login" has consecutive "login" match too expect(filteredItems.value.length).toBe(2); // Both should have score 1.0 (consecutive match: login appears consecutively) }); }); describe("filtering and searching", () => { it("keeps database-object highlight indices relative to the visible label", () => { vi.mocked(useConnectionStore).mockReturnValue({ connections: [{ id: "conn1", name: "Test TiDB", db_type: "mysql" }], treeNodes: [ { id: "conn1", connectionId: "conn1", type: "connection", label: "Test TiDB", children: [ { id: "conn1:retail_mps", connectionId: "conn1", type: "database", database: "retail_mps", label: "retail_mps", children: [ { id: "table1", connectionId: "conn1", type: "table", database: "retail_mps", label: "groupon_apply_finance_invoice", }, ], }, ], }, ], } as any); const { filteredItems, setQuery } = useQuickOpen(); setQuery("gafi"); const table = filteredItems.value.find((item) => item.type === "table"); expect(table?.label).toBe("groupon_apply_finance_invoice"); expect(table?.matchIndices).toEqual([0, 8, 14, 22]); }); it("does not highlight the label when only connection metadata matches", () => { vi.mocked(useConnectionStore).mockReturnValue({ connections: [{ id: "conn1", name: "ProdConnection", db_type: "mysql" }], treeNodes: [ { connectionId: "conn1", type: "database", database: "UserDB", label: "UserDB", }, ], } as any); const { filteredItems, setQuery } = useQuickOpen(); setQuery("Prod"); const database = filteredItems.value.find((item) => item.type === "database"); expect(database?.matchIndices).toEqual([]); expect(database?.matchScore).toBeGreaterThan(1000); }); it("indexes database objects under connection groups", () => { const mockStore = { connections: [{ id: "conn1", name: "Grouped PG", db_type: "postgres" }], treeNodes: [ { id: "group1", type: "connection-group", label: "Production", children: [ { id: "conn1", connectionId: "conn1", type: "connection", label: "Grouped PG", children: [ { id: "conn1:postgres", connectionId: "conn1", type: "database", database: "postgres", label: "postgres", children: [ { id: "conn1:postgres:public", connectionId: "conn1", type: "schema", database: "postgres", schema: "public", label: "public", children: [ { id: "conn1:postgres:public:users", connectionId: "conn1", type: "table", database: "postgres", schema: "public", label: "users", }, ], }, ], }, ], }, ], }, ], }; vi.mocked(useConnectionStore).mockReturnValue(mockStore as any); const { filteredItems, setQuery } = useQuickOpen(); setQuery("users"); expect(filteredItems.value.map((item) => item.label)).toContain("users"); }); it("includes schemas as quick open results", () => { const mockStore = { connections: [{ id: "conn1", name: "PG", db_type: "postgres" }], treeNodes: [ { id: "conn1", connectionId: "conn1", type: "connection", label: "PG", children: [ { id: "conn1:postgres", connectionId: "conn1", type: "database", database: "postgres", label: "postgres", children: [ { id: "conn1:postgres:analytics", connectionId: "conn1", type: "schema", database: "postgres", schema: "analytics", label: "analytics", children: [], }, ], }, ], }, ], }; vi.mocked(useConnectionStore).mockReturnValue(mockStore as any); const { filteredItems, setQuery } = useQuickOpen(); setQuery("analytics"); expect(filteredItems.value).toEqual(expect.arrayContaining([expect.objectContaining({ type: "schema", label: "analytics" })])); }); it("should filter items based on search query", () => { const mockStore = { connections: [ { id: "conn1", name: "ProdDB", type: "mssql" }, { id: "conn2", name: "DevDB", type: "mssql" }, ], treeNodes: [], }; vi.mocked(useConnectionStore).mockReturnValue(mockStore as any); const { filteredItems, setQuery } = useQuickOpen(); setQuery("Prod"); expect(filteredItems.value.length).toBe(1); expect(filteredItems.value[0].label).toBe("ProdDB"); }); it("should be case-insensitive", () => { const mockStore = { connections: [{ id: "conn1", name: "MyConnection", type: "mssql" }], treeNodes: [], }; vi.mocked(useConnectionStore).mockReturnValue(mockStore as any); const { filteredItems, setQuery } = useQuickOpen(); setQuery("myconnection"); expect(filteredItems.value.length).toBe(1); expect(filteredItems.value[0].label).toBe("MyConnection"); }); it("should search across connection name and database name", () => { const mockStore = { connections: [{ id: "conn1", name: "ProdConnection", type: "mssql" }], treeNodes: [ { connectionId: "conn1", type: "database", database: "UserDB", label: "UserDB", }, ], }; vi.mocked(useConnectionStore).mockReturnValue(mockStore as any); const { filteredItems, setQuery } = useQuickOpen(); // Search by connection name setQuery("Prod"); expect(filteredItems.value.length).toBeGreaterThan(0); }); it("should sort by match score (lower scores first)", () => { const mockStore = { connections: [ { id: "conn1", name: "Database", type: "mssql" }, { id: "conn2", name: "MyDB", type: "mssql" }, ], treeNodes: [], }; vi.mocked(useConnectionStore).mockReturnValue(mockStore as any); const { filteredItems, setQuery } = useQuickOpen(); setQuery("db"); expect(filteredItems.value.length).toBe(2); // First result should have better (lower) score expect(filteredItems.value[0].matchScore).toBeLessThanOrEqual(filteredItems.value[1].matchScore); }); it("should sort by type for equal match scores", () => { const mockStore = { connections: [{ id: "conn1", name: "test", type: "mssql" }], treeNodes: [ { connectionId: "conn1", type: "database", database: "test_db", label: "test_db", }, ], }; vi.mocked(useConnectionStore).mockReturnValue(mockStore as any); const { filteredItems, setQuery } = useQuickOpen(); setQuery("test"); // Connections should come before databases for the same query if (filteredItems.value.length >= 2) { const connectionItem = filteredItems.value.find((item) => item.type === "connection"); const databaseItem = filteredItems.value.find((item) => item.type === "database"); if (connectionItem && databaseItem) { expect(filteredItems.value.indexOf(connectionItem)).toBeLessThan(filteredItems.value.indexOf(databaseItem)); } } }); }); describe("item selection navigation", () => { it("should initialize with selectedIndex at 0", () => { const mockStore = { connections: [], treeNodes: [], }; vi.mocked(useConnectionStore).mockReturnValue(mockStore as any); const { selectedIndex } = useQuickOpen(); expect(selectedIndex.value).toBe(0); }); it("should select next item", () => { const mockStore = { connections: [ { id: "conn1", name: "Conn1", type: "mssql" }, { id: "conn2", name: "Conn2", type: "mssql" }, ], treeNodes: [], }; vi.mocked(useConnectionStore).mockReturnValue(mockStore as any); const { selectNext, selectedIndex, setQuery } = useQuickOpen(); setQuery(""); expect(selectedIndex.value).toBe(0); selectNext(); expect(selectedIndex.value).toBe(1); }); it("should not exceed max index when selecting next", () => { const mockStore = { connections: [{ id: "conn1", name: "Conn1", type: "mssql" }], treeNodes: [], }; vi.mocked(useConnectionStore).mockReturnValue(mockStore as any); const { selectNext, selectedIndex, setQuery } = useQuickOpen(); setQuery(""); selectNext(); selectNext(); // Attempt to go beyond max expect(selectedIndex.value).toBe(0); // Should stay at 0 (only 1 item) }); it("should select previous item", () => { const mockStore = { connections: [ { id: "conn1", name: "Conn1", type: "mssql" }, { id: "conn2", name: "Conn2", type: "mssql" }, ], treeNodes: [], }; vi.mocked(useConnectionStore).mockReturnValue(mockStore as any); const { selectNext, selectPrevious, selectedIndex, setQuery } = useQuickOpen(); setQuery(""); selectNext(); expect(selectedIndex.value).toBe(1); selectPrevious(); expect(selectedIndex.value).toBe(0); }); it("should not go below 0 when selecting previous", () => { const mockStore = { connections: [], treeNodes: [], }; vi.mocked(useConnectionStore).mockReturnValue(mockStore as any); const { selectPrevious, selectedIndex } = useQuickOpen(); // Verify initial state expect(selectedIndex.value).toBe(0); selectPrevious(); expect(selectedIndex.value).toBe(0); }); it("should return correct selectedItem", () => { const mockStore = { connections: [ { id: "conn1", name: "Conn1", type: "mssql" }, { id: "conn2", name: "Conn2", type: "mssql" }, ], treeNodes: [], }; vi.mocked(useConnectionStore).mockReturnValue(mockStore as any); const { selectNext, selectedItem, setQuery } = useQuickOpen(); setQuery(""); expect(selectedItem.value?.label).toBe("Conn1"); selectNext(); expect(selectedItem.value?.label).toBe("Conn2"); }); it("should return null selectedItem when index is out of bounds", () => { const mockStore = { connections: [{ id: "conn1", name: "Conn1", type: "mssql" }], treeNodes: [], }; vi.mocked(useConnectionStore).mockReturnValue(mockStore as any); const { selectedItem, selectedIndex } = useQuickOpen(); // Manually set invalid index selectedIndex.value = 999; expect(selectedItem.value).toBeNull(); }); }); describe("reset and query setting", () => { it("resets selection when v-model changes the search query directly", () => { vi.mocked(useConnectionStore).mockReturnValue({ connections: [ { id: "conn1", name: "Connection1", type: "mssql" }, { id: "conn2", name: "Connection2", type: "mssql" }, ], treeNodes: [], } as any); const { searchQuery, selectNext, selectedIndex } = useQuickOpen(); selectNext(); expect(selectedIndex.value).toBe(1); searchQuery.value = "Connection"; expect(selectedIndex.value).toBe(0); }); it("should reset selection to 0 when setQuery is called", () => { const mockStore = { connections: [ { id: "conn1", name: "Conn1", type: "mssql" }, { id: "conn2", name: "Conn2", type: "mssql" }, ], treeNodes: [], }; vi.mocked(useConnectionStore).mockReturnValue(mockStore as any); const { selectNext, setQuery, selectedIndex } = useQuickOpen(); selectNext(); expect(selectedIndex.value).toBe(1); setQuery("test"); expect(selectedIndex.value).toBe(0); }); it("should update searchQuery when setQuery is called", () => { const mockStore = { connections: [], treeNodes: [], }; vi.mocked(useConnectionStore).mockReturnValue(mockStore as any); const { searchQuery, setQuery } = useQuickOpen(); setQuery("NewQuery"); expect(searchQuery.value).toBe("NewQuery"); }); it("should resetSelection to 0", () => { const mockStore = { connections: [ { id: "conn1", name: "Conn1", type: "mssql" }, { id: "conn2", name: "Conn2", type: "mssql" }, ], treeNodes: [], }; vi.mocked(useConnectionStore).mockReturnValue(mockStore as any); const { resetSelection, selectNext, selectedIndex, setQuery } = useQuickOpen(); setQuery(""); selectNext(); expect(selectedIndex.value).toBe(1); resetSelection(); expect(selectedIndex.value).toBe(0); }); }); describe("allItems with different database object types", () => { it("should include tables from tree nodes", () => { const mockStore = { connections: [{ id: "conn1", name: "MyConn", type: "mssql" }], treeNodes: [ { connectionId: "conn1", type: "table", database: "MyDB", schema: "dbo", label: "Users", }, ], }; vi.mocked(useConnectionStore).mockReturnValue(mockStore as any); const { filteredItems, setQuery } = useQuickOpen(); setQuery(""); const tableItem = filteredItems.value.find((item) => item.type === "table"); expect(tableItem).toBeDefined(); expect(tableItem?.label).toBe("Users"); }); it("should include views from tree nodes", () => { const mockStore = { connections: [{ id: "conn1", name: "MyConn", type: "mssql" }], treeNodes: [ { connectionId: "conn1", type: "view", database: "MyDB", schema: "dbo", label: "UserView", }, ], }; vi.mocked(useConnectionStore).mockReturnValue(mockStore as any); const { filteredItems, setQuery } = useQuickOpen(); setQuery(""); const viewItem = filteredItems.value.find((item) => item.type === "view"); expect(viewItem).toBeDefined(); expect(viewItem?.label).toBe("UserView"); }); it("should include procedures from tree nodes", () => { const mockStore = { connections: [{ id: "conn1", name: "MyConn", type: "mssql" }], treeNodes: [ { connectionId: "conn1", type: "procedure", database: "MyDB", schema: "dbo", label: "GetUsers", }, ], }; vi.mocked(useConnectionStore).mockReturnValue(mockStore as any); const { filteredItems, setQuery } = useQuickOpen(); setQuery(""); const procItem = filteredItems.value.find((item) => item.type === "procedure"); expect(procItem).toBeDefined(); expect(procItem?.label).toBe("GetUsers"); }); it("should include functions from tree nodes", () => { const mockStore = { connections: [{ id: "conn1", name: "MyConn", type: "mssql" }], treeNodes: [ { connectionId: "conn1", type: "function", database: "MyDB", schema: "dbo", label: "ComputeAge", }, ], }; vi.mocked(useConnectionStore).mockReturnValue(mockStore as any); const { filteredItems, setQuery } = useQuickOpen(); setQuery(""); const funcItem = filteredItems.value.find((item) => item.type === "function"); expect(funcItem).toBeDefined(); expect(funcItem?.label).toBe("ComputeAge"); }); }); describe("SQL library files", () => { function savedSqlStoreWithFiles(files: any[]) { const fileMap = new Map(files.map((f) => [f.id, f])); return { allFiles: files, getFile: vi.fn().mockImplementation((id: string) => fileMap.get(id)), }; } it("shows limited SQL library files when no search query", () => { const mockConnStore = { connections: [{ id: "conn1", name: "MyConn", type: "mssql" }], treeNodes: [], }; vi.mocked(useConnectionStore).mockReturnValue(mockConnStore as any); const files = Array.from({ length: 30 }, (_, i) => ({ id: `file${i}`, name: `query_${i}.sql`, connectionId: "conn1", updatedAt: `2024-01-${String(i + 1).padStart(2, "0")}T00:00:00.000Z`, })); vi.mocked(useSavedSqlStore).mockReturnValue(savedSqlStoreWithFiles(files) as any); const { filteredItems, setQuery } = useQuickOpen(); setQuery(""); // Should show 1 connection + 20 recent SQL library files = 21 expect(filteredItems.value.length).toBe(21); const sqlItems = filteredItems.value.filter((item) => item.type === "sql_library_file"); expect(sqlItems.length).toBe(20); }); it("includes all SQL library files when searching", () => { const mockConnStore = { connections: [{ id: "conn1", name: "MyConn", type: "mssql" }], treeNodes: [], }; vi.mocked(useConnectionStore).mockReturnValue(mockConnStore as any); const files = [ { id: "f1", name: "get_users.sql", connectionId: "conn1", updatedAt: "2024-01-01T00:00:00.000Z" }, { id: "f2", name: "create_orders.sql", connectionId: "conn1", updatedAt: "2024-01-02T00:00:00.000Z" }, { id: "f3", name: "update_inventory.sql", connectionId: "conn1", updatedAt: "2024-01-03T00:00:00.000Z" }, ]; vi.mocked(useSavedSqlStore).mockReturnValue(savedSqlStoreWithFiles(files) as any); const { filteredItems, setQuery } = useQuickOpen(); setQuery("users"); const sqlItems = filteredItems.value.filter((item) => item.type === "sql_library_file"); expect(sqlItems).toHaveLength(1); expect(sqlItems[0].label).toBe("get_users.sql"); expect(sqlItems[0].sqlFileId).toBe("f1"); }); it("includes SQL library files whose connection was deleted", () => { const mockConnStore = { connections: [{ id: "conn1", name: "Active", type: "mssql" }], treeNodes: [], }; vi.mocked(useConnectionStore).mockReturnValue(mockConnStore as any); const files = [ { id: "f1", name: "active_query.sql", connectionId: "conn1", updatedAt: "2024-01-01T00:00:00.000Z" }, { id: "f2", name: "orphaned_query.sql", connectionId: "deleted_conn", updatedAt: "2024-01-02T00:00:00.000Z" }, ]; vi.mocked(useSavedSqlStore).mockReturnValue(savedSqlStoreWithFiles(files) as any); const { filteredItems, setQuery } = useQuickOpen(); setQuery("query"); const sqlItems = filteredItems.value.filter((item) => item.type === "sql_library_file"); expect(sqlItems).toHaveLength(2); expect(sqlItems.find((item) => item.label === "orphaned_query.sql")?.description).toBe("Connection deleted"); }); it("labels SQL library files without a connection as unassociated", () => { vi.mocked(useConnectionStore).mockReturnValue({ connections: [], treeNodes: [] } as any); vi.mocked(useSavedSqlStore).mockReturnValue(savedSqlStoreWithFiles([{ id: "f1", name: "draft.sql", connectionId: "", updatedAt: "2024-01-01T00:00:00.000Z" }]) as any); const { filteredItems, setQuery } = useQuickOpen(); setQuery("draft"); expect(filteredItems.value.find((item) => item.type === "sql_library_file")?.description).toBe("Unassociated"); }); }); describe("remote metadata search", () => { beforeEach(() => { vi.useFakeTimers(); }); afterEach(() => { vi.clearAllTimers(); vi.useRealTimers(); }); function remoteSearchStore(overrides: Record = {}) { return { connections: [{ id: "conn1", name: "MySQL", db_type: "mysql" }], connectedIds: new Set(["conn1"]), treeNodes: [ { id: "conn1:app", connectionId: "conn1", type: "database", database: "app", label: "app", }, ], listCompletionTables: vi.fn().mockResolvedValue([]), ...overrides, }; } async function runDebouncedSearch(): Promise { await vi.advanceTimersByTimeAsync(200); await flushAsyncWork(); } it("finds unloaded tables through server metadata", async () => { const mockStore = remoteSearchStore({ listCompletionTables: vi.fn().mockResolvedValue([{ name: "orders", type: "table" }]), }); vi.mocked(useConnectionStore).mockReturnValue(mockStore as any); const { filteredItems, setQuery } = useQuickOpen(); setQuery("ord"); await runDebouncedSearch(); expect(mockStore.listCompletionTables).toHaveBeenCalledWith("conn1", "app", "ord", 25, undefined, true, undefined, undefined, { activateConnection: false }); expect(filteredItems.value).toEqual(expect.arrayContaining([expect.objectContaining({ label: "orders", type: "table", database: "app" })])); }); it("deduplicates loaded and remote table results", async () => { const mockStore = remoteSearchStore({ treeNodes: [ { id: "conn1:app", connectionId: "conn1", type: "database", database: "app", label: "app", children: [ { id: "conn1:app:users", connectionId: "conn1", type: "table", database: "app", label: "users", }, ], }, ], listCompletionTables: vi.fn().mockResolvedValue([{ name: "users", type: "table" }]), }); vi.mocked(useConnectionStore).mockReturnValue(mockStore as any); const { filteredItems, setQuery } = useQuickOpen(); setQuery("users"); await runDebouncedSearch(); expect(filteredItems.value.filter((item) => item.label === "users")).toHaveLength(1); }); it("ignores stale remote responses", async () => { const alpha = deferred>(); const beta = deferred>(); const mockStore = remoteSearchStore({ listCompletionTables: vi.fn((_connectionId, _database, query) => (query === "alpha" ? alpha.promise : beta.promise)), }); vi.mocked(useConnectionStore).mockReturnValue(mockStore as any); const { filteredItems, setQuery } = useQuickOpen(); setQuery("alpha"); await runDebouncedSearch(); setQuery("beta"); await runDebouncedSearch(); beta.resolve([{ name: "beta_table", type: "table" }]); await flushAsyncWork(); expect(filteredItems.value.map((item) => item.label)).toContain("beta_table"); alpha.resolve([{ name: "alpha_table", type: "table" }]); await flushAsyncWork(); expect(filteredItems.value.map((item) => item.label)).toContain("beta_table"); expect(filteredItems.value.map((item) => item.label)).not.toContain("alpha_table"); }); it("does not request metadata for empty or one-character queries", async () => { const mockStore = remoteSearchStore(); vi.mocked(useConnectionStore).mockReturnValue(mockStore as any); const { setQuery } = useQuickOpen(); setQuery(""); setQuery("a"); await runDebouncedSearch(); expect(mockStore.listCompletionTables).not.toHaveBeenCalled(); }); it("searches disconnected contexts through lazy connection", async () => { const mockStore = remoteSearchStore({ connectedIds: new Set() }); vi.mocked(useConnectionStore).mockReturnValue(mockStore as any); const { setQuery } = useQuickOpen(); setQuery("users"); await runDebouncedSearch(); expect(mockStore.listCompletionTables).toHaveBeenCalledWith("conn1", "app", "users", 25, undefined, true, undefined, undefined, { activateConnection: false }); }); it("prioritizes the active connection before applying the remote request cap", async () => { const connections = Array.from({ length: 9 }, (_, index) => ({ id: `conn${index}`, name: `Connection ${index}`, db_type: "mysql", database: `db${index}` })); const mockStore = remoteSearchStore({ connections, activeConnectionId: "conn8", connectedIds: new Set(["conn8"]), treeNodes: [], }); vi.mocked(useConnectionStore).mockReturnValue(mockStore as any); const { setQuery } = useQuickOpen(); setQuery("users"); await runDebouncedSearch(); const requestedConnections = mockStore.listCompletionTables.mock.calls.map(([connectionId]) => connectionId); expect(requestedConnections).toContain("conn8"); expect(requestedConnections).not.toContain("conn7"); expect(mockStore.listCompletionTables).toHaveBeenCalledTimes(8); }); it("publishes active connection results without waiting for slow cold connections", async () => { const slowSearch = deferred>(); const mockStore = remoteSearchStore({ connections: [ { id: "cold", name: "Cold", db_type: "mysql", database: "cold_db" }, { id: "active", name: "Active", db_type: "mysql", database: "active_db" }, ], activeConnectionId: "active", connectedIds: new Set(["active"]), treeNodes: [], listCompletionTables: vi.fn((connectionId) => (connectionId === "active" ? Promise.resolve([{ name: "active_users", type: "table" as const }]) : slowSearch.promise)), }); vi.mocked(useConnectionStore).mockReturnValue(mockStore as any); const { filteredItems, setQuery } = useQuickOpen(); setQuery("users"); await runDebouncedSearch(); expect(filteredItems.value.map((item) => item.label)).toContain("active_users"); slowSearch.resolve([]); await flushAsyncWork(); }); it("drops stale queued contexts before scheduling a newer query", async () => { const alphaRequests: Array>>> = []; const listCompletionTables = vi.fn((_connectionId, _database, query) => { if (query === "alpha") { const request = deferred>(); alphaRequests.push(request); return request.promise; } return Promise.resolve([{ name: "beta_table", type: "table" as const }]); }); const mockStore = remoteSearchStore({ treeNodes: Array.from({ length: 4 }, (_, index) => ({ id: `conn1:db${index}`, connectionId: "conn1", type: "database", database: `db${index}`, label: `db${index}`, })), listCompletionTables, }); vi.mocked(useConnectionStore).mockReturnValue(mockStore as any); const { filteredItems, setQuery } = useQuickOpen(); setQuery("alpha"); await runDebouncedSearch(); expect(listCompletionTables).toHaveBeenCalledTimes(2); setQuery("beta"); await runDebouncedSearch(); expect(listCompletionTables).toHaveBeenCalledTimes(2); alphaRequests[0]!.resolve([]); await flushAsyncWork(); expect(listCompletionTables.mock.calls[2]?.[2]).toBe("beta"); expect(listCompletionTables.mock.calls.filter(([, , query]) => query === "alpha")).toHaveLength(2); alphaRequests[1]!.resolve([]); await flushAsyncWork(); expect(filteredItems.value.map((item) => item.label)).toContain("beta_table"); }); it("derives the SQLite main database before its tree is expanded", async () => { const mockStore = remoteSearchStore({ connections: [{ id: "sqlite-1", name: "SQLite", db_type: "sqlite", host: "/tmp/app.sqlite" }], connectedIds: new Set(), treeNodes: [], listCompletionTables: vi.fn().mockResolvedValue([{ name: "scroll_test", type: "table" }]), }); vi.mocked(useConnectionStore).mockReturnValue(mockStore as any); const { filteredItems, setQuery } = useQuickOpen(); setQuery("scroll_test"); await runDebouncedSearch(); expect(mockStore.listCompletionTables).toHaveBeenCalledWith("sqlite-1", "main", "scroll_test", 25, undefined, true, undefined, undefined, { activateConnection: false }); expect(filteredItems.value).toEqual(expect.arrayContaining([expect.objectContaining({ label: "scroll_test", type: "table", database: "main" })])); }); it("searches the PostgreSQL backend default database before its tree is expanded", async () => { const mockStore = remoteSearchStore({ connections: [{ id: "pg-1", name: "PostgreSQL", db_type: "postgres", database: "" }], connectedIds: new Set(), treeNodes: [], listCompletionTables: vi.fn().mockResolvedValue([{ name: "cold_start_table", type: "table" }]), }); vi.mocked(useConnectionStore).mockReturnValue(mockStore as any); const { filteredItems, setQuery } = useQuickOpen(); setQuery("cold_start_table"); await runDebouncedSearch(); expect(mockStore.listCompletionTables).toHaveBeenCalledWith("pg-1", "postgres", "cold_start_table", 25, undefined, true, undefined, undefined, { activateConnection: false }); expect(filteredItems.value).toEqual(expect.arrayContaining([expect.objectContaining({ label: "cold_start_table", type: "table", database: "postgres" })])); }); it("keeps local results when remote metadata search fails", async () => { const mockStore = remoteSearchStore({ treeNodes: [ { id: "conn1:app", connectionId: "conn1", type: "database", database: "app", label: "app", children: [ { id: "conn1:app:users", connectionId: "conn1", type: "table", database: "app", label: "users", }, ], }, ], listCompletionTables: vi.fn().mockRejectedValue(new Error("metadata unavailable")), }); vi.mocked(useConnectionStore).mockReturnValue(mockStore as any); const { filteredItems, setQuery } = useQuickOpen(); setQuery("users"); await runDebouncedSearch(); expect(filteredItems.value.map((item) => item.label)).toContain("users"); }); it("caps requests, concurrency, and merged remote results", async () => { const pending: Array>>> = []; let callIndex = 0; const listCompletionTables = vi.fn(() => { const request = deferred>(); pending.push(request); return request.promise; }); const mockStore = remoteSearchStore({ treeNodes: Array.from({ length: 12 }, (_, index) => ({ id: `conn1:db${index}`, connectionId: "conn1", type: "database", database: `db${index}`, label: `db${index}`, })), listCompletionTables, }); vi.mocked(useConnectionStore).mockReturnValue(mockStore as any); const { filteredItems, setQuery } = useQuickOpen(); setQuery("table"); await vi.advanceTimersByTimeAsync(200); await flushAsyncWork(); expect(listCompletionTables).toHaveBeenCalledTimes(2); for (let wave = 0; wave < 4; wave++) { const active = pending.slice(wave * 2, wave * 2 + 2); for (const request of active) { const requestIndex = callIndex++; request.resolve(Array.from({ length: 30 }, (_, index) => ({ name: `table_${requestIndex}_${index}`, type: "table" }))); } await flushAsyncWork(); expect(listCompletionTables.mock.calls.length).toBeLessThanOrEqual(Math.min((wave + 2) * 2, 8)); } await flushAsyncWork(); expect(listCompletionTables).toHaveBeenCalledTimes(8); expect(filteredItems.value).toHaveLength(100); }); }); });