Files
gridline/src/lib/commands.test.ts
T
adrianbonpin 04e8ed3300 v0.7.5: bundled PG tools, schema CRUD, object search, copy-as-DDL, object dependencies (#11)
* chore: bump version to 0.7.5 (Task 1.1)

* feat(db): pure object DDL/search/depend builders (Task 1.2)

* feat(models): ObjectSearchHit, DependencyInfo, PgToolPaths, tool source fields (Task 1.3)

* feat(backup): bundle-aware pg tool resolution, system-first fallback (Task 2.1)

* feat(objects): schema CRUD commands + integration test (Task 2.2)

* feat(objects): search_objects command (current-schema, all types) (Task 2.3)

* feat(objects): get_object_ddl for all browsable types (Task 2.4)

* feat(objects): pg_depend object dependencies + schema contents (Task 2.5)

* feat(ipc): register object management commands (Task 3.1)

* feat(ipc): frontend wrappers + types for object management (Task 3.2)

* feat(build): declare bundled pg_tools resources (Task 3.3)

* test(capabilities): lock objects capability PG-only for search/ddl/dependencies (Task 3.4)

* feat(ui): Cmd+K object search palette in DB viewer (Task 4.1)

* feat(ui): schema CRUD menu + dependency dialog (Task 4.2)

* feat(ui): copy-as-DDL + dependency view context menu in Objects (Task 4.3)

* feat(ui): dependency check before table drop + bundled-tool status (Task 4.4)

* docs: v0.7.5 release notes + status table + bundled-tools (Task 5.1)

* ci: build/verify bundled pg client tools per platform before tauri build (Task 5.2)

* fix(objects): report pg_rewrite dependencies as the dependent view (pg_class)

pg_depend records view dependencies via the view's internal rewrite rule
(classid = pg_rewrite). The user-facing dependent object is the VIEW itself,
so map that classid to pg_class (name still resolved through ev_class).
Fixes the live-PG integration test object_dependencies_for_table_includes_view
and makes the dependency dialog readable for view dependents.

* test: add idempotent PG integration-test seed script

Seeds the first PG test db (GRIDLINE_TEST_SRC) with the objects the
#[ignore] integration tests assert against: users (+users_id_seq),
products (3 rows), orders (3 rows), order_summary view, audit_log +
get_user functions, user_role enum. Idempotent: DROP + recreate.

* fix(build): regenerate multi-resolution icons + roadmap Keychain item

- icon.ico previously contained a single 16x16 frame (Windows scaled it
  up -> blurry taskbar/start-menu icon). Regenerated from the 512px source
  via 'tauri icon': ICO now has 16/24/32/48/64/256 frames, icns has full
  @2x coverage up to 1024px, PNGs re-rendered from the same source.
- Added icons/icon.png (512px) to bundle.icon so Linux hicolor installs
  a high-DPI entry.
- ROADMAP: log the 'Enable Keychain' toggle (currently a form-only
  placeholder) under Next up -> Connection & credentials.

* docs(readme): dynamic version-free download links + 3-col table

- release.yml: set releaseAssetNamePattern to '[name]_[platform]_[arch][setup][ext]'
  (version-free), so asset filenames are stable across releases:
  Gridline_darwin_aarch64.dmg, Gridline_windows_x64-setup.exe,
  Gridline_linux_amd64.deb, Gridline_linux_x86_64.rpm, etc.
- README: both download tables now 3-column (OS | Architecture | Download)
  and link via GitHub's releases/latest/download/<file> redirect — they
  always point at the newest published release, no per-release edits.
- AGENTS.md: releases checklist updated — download links stay version-free.

* docs(readme): platform-per-column download table (macOS | Windows | Linux)

Table now mirrors the release layout: one column per OS with a logo row
and a download-links row underneath. Links stay version-free via
releases/latest/download/<file> (releaseAssetNamePattern in release.yml).

* docs(readme): revert download table to clean image-less 3-col layout

Logo row was hard to read on GitHub dark mode; the plain OS | Architecture
| Download table is cleaner and still uses dynamic releases/latest links.
2026-08-05 20:46:36 +08:00

335 lines
11 KiB
TypeScript

import { describe, it, expect, vi, afterEach } from "vitest";
vi.mock("@tauri-apps/api/core", () => ({
invoke: vi.fn().mockResolvedValue({ tables: [], relationships: [] }),
}));
import { invoke } from "@tauri-apps/api/core";
import {
testConnection,
dbConnect,
dbDisconnect,
getDatabases,
getSchemas,
getTables,
getTableData,
executeChange,
refreshConnection,
getSchemaGraph,
executeQuery,
getQueryHistory,
clearQueryHistory,
setHistoryFavorite,
saveQuery,
getSavedQueries,
updateSavedQuery,
deleteSavedQuery,
setConnectionFavorite,
recordRecentConnection,
getRecentConnections,
clearRecentConnections,
getIndexes,
getConstraints,
createSchema,
renameSchema,
dropSchema,
searchObjects,
getObjectDdl,
getObjectDependencies,
} from "./commands";
import type { SchemaGraph } from "./types";
import type { QueryHistoryEntry } from "./commands";
describe("commands", () => {
it("testConnection has correct signature", () => {
expect(typeof testConnection).toBe("function");
});
it("dbConnect returns void promise", () => {
expect(typeof dbConnect).toBe("function");
});
it("dbDisconnect returns void promise", () => {
expect(typeof dbDisconnect).toBe("function");
});
it("getDatabases returns string array promise", () => {
expect(typeof getDatabases).toBe("function");
});
it("getSchemas returns string array promise", () => {
expect(typeof getSchemas).toBe("function");
});
it("getTables returns TableInfo array promise", () => {
expect(typeof getTables).toBe("function");
});
it("getTableData returns QueryResult-shaped promise", () => {
expect(typeof getTableData).toBe("function");
});
it("executeChange returns void promise", () => {
expect(typeof executeChange).toBe("function");
});
it("refreshConnection returns full tree promise", () => {
expect(typeof refreshConnection).toBe("function");
});
describe("getSchemaGraph", () => {
it("is a callable function with correct signature", () => {
expect(typeof getSchemaGraph).toBe("function");
const result: Promise<SchemaGraph> = getSchemaGraph("conn-1", "public");
expect(result).toBeInstanceOf(Promise);
});
it("accepts schema as optional", () => {
const result: Promise<SchemaGraph> = getSchemaGraph("conn-1");
expect(result).toBeInstanceOf(Promise);
});
});
});
describe("query commands", () => {
it("executeQuery calls invoke with correct args", async () => {
const mockResult = { columns: [], rows: [], total_rows: 0, page: 1, page_size: 50 };
vi.mocked(invoke).mockResolvedValueOnce(mockResult);
const result = await executeQuery("conn-1", "SELECT 1", 1, 50);
expect(invoke).toHaveBeenCalledWith("execute_query", {
connectionId: "conn-1",
query: "SELECT 1",
page: 1,
pageSize: 50,
});
expect(result).toEqual(mockResult);
});
it("getQueryHistory calls invoke", async () => {
const mockHistory = [{ id: "h1", connection_id: "c1", query_text: "SELECT 1", status: "success", executed_at: "2025-01-01" }];
vi.mocked(invoke).mockResolvedValueOnce(mockHistory);
const result = await getQueryHistory("conn-1", 10, 0);
expect(invoke).toHaveBeenCalledWith("get_query_history", { connectionId: "conn-1", limit: 10, offset: 0 });
expect(result).toEqual(mockHistory);
});
it("clearQueryHistory calls invoke", async () => {
vi.mocked(invoke).mockResolvedValueOnce(undefined);
await clearQueryHistory("conn-1");
expect(invoke).toHaveBeenCalledWith("clear_query_history", { connectionId: "conn-1" });
});
it("getQueryHistory with null calls invoke with null connectionId", async () => {
vi.mocked(invoke).mockResolvedValueOnce([]);
await getQueryHistory(null, 50, 0);
expect(invoke).toHaveBeenCalledWith("get_query_history", {
connectionId: null,
limit: 50,
offset: 0,
});
});
it("clearQueryHistory with null clears all", async () => {
vi.mocked(invoke).mockResolvedValueOnce(undefined);
await clearQueryHistory(null);
expect(invoke).toHaveBeenCalledWith("clear_query_history", {
connectionId: null,
});
});
});
describe("Query History — v6", () => {
afterEach(() => {
vi.restoreAllMocks();
});
it("QueryHistoryEntry includes favorite field", () => {
const entry: QueryHistoryEntry = {
id: "h1",
connection_id: "c1",
query_text: "SELECT 1",
execution_time_ms: 42,
row_count: 5,
status: "success",
error_message: null,
executed_at: "2026-01-01T00:00:00Z",
favorite: false, // NEW — must be accepted
};
expect(entry.favorite).toBe(false);
});
it("setHistoryFavorite calls invoke with correct params", async () => {
const mockInvoke = vi.fn().mockResolvedValue(undefined);
vi.mocked(invoke).mockImplementation(mockInvoke);
await setHistoryFavorite("entry-id-1", "conn-abc");
expect(mockInvoke).toHaveBeenCalledWith("set_history_favorite", {
id: "entry-id-1",
connectionId: "conn-abc",
});
});
it("saveQuery calls invoke with correct params", async () => {
const mockInvoke = vi.fn().mockResolvedValue(undefined);
vi.mocked(invoke).mockImplementation(mockInvoke);
await saveQuery({
connectionId: "conn-xyz",
name: "My Saved Query",
queryText: "SELECT * FROM users",
folder: "reports",
});
expect(mockInvoke).toHaveBeenCalledWith("save_query", {
connectionId: "conn-xyz",
name: "My Saved Query",
queryText: "SELECT * FROM users",
folder: "reports",
});
});
it("saveQuery accepts null connectionId for global queries", async () => {
const mockInvoke = vi.fn().mockResolvedValue(undefined);
vi.mocked(invoke).mockImplementation(mockInvoke);
await saveQuery({
connectionId: null,
name: "Global query",
queryText: "SELECT 1",
folder: "",
});
expect(mockInvoke).toHaveBeenCalledWith("save_query", {
connectionId: null,
name: "Global query",
queryText: "SELECT 1",
folder: "",
});
});
it("getSavedQueries calls invoke with correct params", async () => {
const mockInvoke = vi.fn().mockResolvedValue([]);
vi.mocked(invoke).mockImplementation(mockInvoke);
await getSavedQueries("conn-123");
expect(mockInvoke).toHaveBeenCalledWith("get_saved_queries", {
connectionId: "conn-123",
});
});
it("getSavedQueries accepts null for all-connections", async () => {
const mockInvoke = vi.fn().mockResolvedValue([]);
vi.mocked(invoke).mockImplementation(mockInvoke);
await getSavedQueries(null);
expect(mockInvoke).toHaveBeenCalledWith("get_saved_queries", {
connectionId: null,
});
});
it("updateSavedQuery calls invoke with correct params", async () => {
const mockInvoke = vi.fn().mockResolvedValue(undefined);
vi.mocked(invoke).mockImplementation(mockInvoke);
await updateSavedQuery("q-id", { name: "Renamed" });
expect(mockInvoke).toHaveBeenCalledWith("update_saved_query", {
id: "q-id",
patch: { name: "Renamed" },
});
});
it("deleteSavedQuery calls invoke with correct params", async () => {
const mockInvoke = vi.fn().mockResolvedValue(undefined);
vi.mocked(invoke).mockImplementation(mockInvoke);
await deleteSavedQuery("q-id");
expect(mockInvoke).toHaveBeenCalledWith("delete_saved_query", {
id: "q-id",
});
});
});
describe("v0.5.0 command wrappers", () => {
it("setConnectionFavorite invokes set_connection_favorite with camelCase", async () => {
const mockInvoke = vi.fn().mockResolvedValue(undefined);
vi.mocked(invoke).mockImplementation(mockInvoke);
await setConnectionFavorite("c1", true);
expect(mockInvoke).toHaveBeenCalledWith("set_connection_favorite", { connectionId: "c1", favorite: true });
});
it("recordRecentConnection invokes record_recent_connection", async () => {
const mockInvoke = vi.fn().mockResolvedValue(undefined);
vi.mocked(invoke).mockImplementation(mockInvoke);
await recordRecentConnection("c1");
expect(mockInvoke).toHaveBeenCalledWith("record_recent_connection", { connectionId: "c1" });
});
it("getRecentConnections invokes get_recent_connections with limit", async () => {
const mockInvoke = vi.fn().mockResolvedValue([]);
vi.mocked(invoke).mockImplementation(mockInvoke);
await getRecentConnections(8);
expect(mockInvoke).toHaveBeenCalledWith("get_recent_connections", { limit: 8 });
});
it("clearRecentConnections invokes clear_recent_connections", async () => {
const mockInvoke = vi.fn().mockResolvedValue(undefined);
vi.mocked(invoke).mockImplementation(mockInvoke);
await clearRecentConnections();
expect(mockInvoke).toHaveBeenCalledWith("clear_recent_connections", {});
});
it("getIndexes invokes get_indexes with connectionId + schema", async () => {
const mockInvoke = vi.fn().mockResolvedValue([]);
vi.mocked(invoke).mockImplementation(mockInvoke);
await getIndexes("c1", "public");
expect(mockInvoke).toHaveBeenCalledWith("get_indexes", { connectionId: "c1", schema: "public" });
});
it("getConstraints invokes get_constraints with connectionId + schema", async () => {
const mockInvoke = vi.fn().mockResolvedValue([]);
vi.mocked(invoke).mockImplementation(mockInvoke);
await getConstraints("c1", "public");
expect(mockInvoke).toHaveBeenCalledWith("get_constraints", { connectionId: "c1", schema: "public" });
});
});
describe("object management commands (v0.7.5)", () => {
afterEach(() => vi.restoreAllMocks());
it("createSchema calls invoke with name", async () => {
vi.mocked(invoke).mockResolvedValueOnce(undefined);
await createSchema("c1", "my_schema");
expect(invoke).toHaveBeenCalledWith("create_schema", { connectionId: "c1", name: "my_schema" });
});
it("renameSchema maps old/new names", async () => {
vi.mocked(invoke).mockResolvedValueOnce(undefined);
await renameSchema("c1", "old", "new");
expect(invoke).toHaveBeenCalledWith("rename_schema", { connectionId: "c1", oldName: "old", newName: "new" });
});
it("dropSchema passes cascade", async () => {
vi.mocked(invoke).mockResolvedValueOnce(undefined);
await dropSchema("c1", "s", true);
expect(invoke).toHaveBeenCalledWith("drop_schema", { connectionId: "c1", name: "s", cascade: true });
});
it("searchObjects maps query+schema", async () => {
vi.mocked(invoke).mockResolvedValueOnce([]);
await searchObjects("c1", "public", "user");
expect(invoke).toHaveBeenCalledWith("search_objects", { connectionId: "c1", schema: "public", query: "user" });
});
it("getObjectDdl maps objectType+name", async () => {
vi.mocked(invoke).mockResolvedValueOnce("CREATE SEQUENCE ...");
await getObjectDdl("c1", "public", "sequence", "users_id_seq");
expect(invoke).toHaveBeenCalledWith("get_object_ddl", { connectionId: "c1", schema: "public", objectType: "sequence", name: "users_id_seq" });
});
it("getObjectDependencies maps objectType+name", async () => {
vi.mocked(invoke).mockResolvedValueOnce([]);
await getObjectDependencies("c1", "public", "table", "orders");
expect(invoke).toHaveBeenCalledWith("get_object_dependencies", { connectionId: "c1", schema: "public", objectType: "table", name: "orders" });
});
});