Files
t8y2--dbx/packages/cli/tests/cli.test.ts
T
2026-07-13 13:09:14 +08:00

430 lines
14 KiB
TypeScript
Vendored

import assert from "node:assert/strict";
import { mkdtemp, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { test } from "vitest";
import type { Backend, ConnectionConfig, DbxDiagnostics } from "@dbx-app/node-core";
import { runCli } from "../src/cli.js";
const connection: ConnectionConfig = {
id: "1",
name: "local",
db_type: "postgres",
host: "127.0.0.1",
port: 5432,
username: "app",
password: "secret",
database: "demo",
ssh_enabled: false,
ssl: false,
};
function fakeBackend(overrides: Partial<Backend> = {}): Backend {
return {
loadConnections: async () => [connection],
findConnection: async (name) => (name === "local" ? connection : undefined),
addConnection: async () => connection,
removeConnection: async () => true,
listTables: async () => [{ name: "users", type: "BASE TABLE" }],
describeTable: async () => [
{
name: "id",
data_type: "integer",
is_nullable: false,
column_default: null,
is_primary_key: true,
comment: null,
},
],
executeQuery: async () => ({ columns: ["total"], rows: [{ total: 1 }], row_count: 1 }),
...overrides,
};
}
const diagnostics: DbxDiagnostics = {
appDataDir: "/tmp/dbx",
dbPath: "/tmp/dbx/dbx.db",
dbPathExists: true,
connectionsTableExists: true,
connectionRowCount: 2,
loadConnectionsOk: true,
loadedConnectionCount: 2,
bridgePortFile: "/tmp/dbx/mcp-bridge-port",
bridgePortFileExists: false,
directQueryTypes: ["postgres", "mysql", "sqlite", "rqlite"],
bridgeRequiredTypes: ["oracle", "mongodb"],
};
test("lists connections as json", async () => {
const result = await runCli(["connections", "list", "--json"], { backend: fakeBackend() });
assert.equal(result.exitCode, 0);
assert.deepEqual(JSON.parse(result.stdout), {
connections: [{ name: "local", type: "postgres", host: "127.0.0.1", port: 5432, database: "demo" }],
});
});
test("blocks write query by default", async () => {
const result = await runCli(["query", "local", "update users set name = 'x' where id = 1", "--json"], {
backend: fakeBackend(),
});
assert.equal(result.exitCode, 1);
assert.equal(JSON.parse(result.stderr).error.code, "SQL_BLOCKED");
});
test("runs read query as json", async () => {
const result = await runCli(["query", "local", "select count(*) as total from users", "--json"], {
backend: fakeBackend(),
});
assert.equal(result.exitCode, 0);
assert.deepEqual(JSON.parse(result.stdout), {
connection: "local",
columns: ["total"],
rows: [{ total: 1 }],
row_count: 1,
});
});
test("closes backend resources created by the CLI", async () => {
let closed = false;
const result = await runCli(["query", "local", "select count(*) as total from users", "--json"], {
backendFactory: async () =>
fakeBackend({
close: async () => {
closed = true;
},
}),
});
assert.equal(result.exitCode, 0);
assert.equal(closed, true);
});
test("closes backend resources created by the CLI after failures", async () => {
let closed = false;
const result = await runCli(["query", "local", "select count(*) as total from users", "--json"], {
backendFactory: async () =>
fakeBackend({
executeQuery: async () => {
throw new Error("boom");
},
close: async () => {
closed = true;
},
}),
});
assert.equal(result.exitCode, 1);
assert.equal(JSON.parse(result.stderr).error.message, "boom");
assert.equal(closed, true);
});
test("does not close caller-provided backend resources", async () => {
let closed = false;
const result = await runCli(["query", "local", "select count(*) as total from users", "--json"], {
backend: fakeBackend({
close: async () => {
closed = true;
},
}),
});
assert.equal(result.exitCode, 0);
assert.equal(closed, false);
});
test("runs query as csv", async () => {
const result = await runCli(["query", "local", "select count(*) as total from users", "--format", "csv"], {
backend: fakeBackend(),
});
assert.equal(result.exitCode, 0);
assert.equal(result.stdout, "total\n1\n");
});
test("uses json format alias", async () => {
const result = await runCli(["connections", "list", "--format", "json"], { backend: fakeBackend() });
assert.equal(result.exitCode, 0);
assert.deepEqual(JSON.parse(result.stdout), {
connections: [{ name: "local", type: "postgres", host: "127.0.0.1", port: 5432, database: "demo" }],
});
});
test("prints diagnostics as json", async () => {
const result = await runCli(["doctor", "--json"], {
backend: fakeBackend(),
diagnostics: async () => diagnostics,
});
assert.equal(result.exitCode, 0);
assert.deepEqual(JSON.parse(result.stdout), diagnostics);
});
test("prints connection-store remediation in doctor output", async () => {
const result = await runCli(["doctor"], {
backend: fakeBackend(),
diagnostics: async () => ({
...diagnostics,
loadConnectionsOk: false,
loadConnectionsError: "compiled against a different Node.js version",
loadConnectionsHint: "Rebuild DBX CLI native dependencies with your active Node.js.",
}),
});
assert.equal(result.exitCode, 0);
assert.match(result.stdout, /Connection fix/);
assert.match(result.stdout, /Rebuild DBX CLI native dependencies/);
});
test("surfaces connection store failures instead of returning an empty list", async () => {
const result = await runCli(["connections", "list", "--json"], {
backend: fakeBackend({
loadConnections: async () => {
throw Object.assign(new Error("Failed to load DBX connections from /tmp/dbx/dbx.db: NODE_MODULE_VERSION 127 mismatch"), {
code: "CONNECTION_STORE_ERROR",
});
},
}),
});
assert.equal(result.exitCode, 1);
assert.equal(JSON.parse(result.stderr).error.code, "CONNECTION_STORE_ERROR");
assert.match(JSON.parse(result.stderr).error.hint, /Rebuild DBX CLI native dependencies/);
});
test("prints capabilities as json", async () => {
const result = await runCli(["capabilities", "--json"], { backend: fakeBackend() });
assert.equal(result.exitCode, 0);
const payload = JSON.parse(result.stdout) as { directQueryTypes: string[]; bridgeRequiredTypes: string[] };
assert.ok(payload.directQueryTypes.includes("postgres"));
assert.ok(payload.directQueryTypes.includes("sqlite"));
assert.ok(payload.directQueryTypes.includes("rqlite"));
assert.ok(payload.bridgeRequiredTypes.includes("oracle"));
});
test("rejects invalid formats", async () => {
const result = await runCli(["query", "local", "select 1", "--json", "--format", "xml"], { backend: fakeBackend() });
assert.equal(result.exitCode, 1);
assert.equal(JSON.parse(result.stderr).error.code, "INVALID_OPTION");
});
test("uses DBX_CONNECTION when query connection is omitted", async () => {
const result = await runCli(["query", "select count(*) as total from users", "--json"], {
backend: fakeBackend(),
env: { DBX_CONNECTION: "local" },
});
assert.equal(result.exitCode, 0);
assert.equal(JSON.parse(result.stdout).connection, "local");
});
test("uses DBX_CONNECTION when context connection is omitted", async () => {
const result = await runCli(["context", "--tables", "users"], {
backend: fakeBackend(),
env: { DBX_CONNECTION: "local" },
});
assert.equal(result.exitCode, 0);
assert.match(result.stdout, /Connection: local/);
});
test("passes query limit and timeout to the backend", async () => {
let received: unknown;
const result = await runCli(["query", "local", "select * from users", "--limit", "5", "--timeout", "2s", "--json"], {
backend: fakeBackend({
executeQuery: async (_config, _sql, options) => {
received = options;
return { columns: ["id"], rows: [{ id: 1 }], row_count: 1 };
},
}),
});
assert.equal(result.exitCode, 0);
assert.deepEqual(received, { maxRows: 5, timeoutMs: 2000 });
});
test("rejects invalid query limits", async () => {
const result = await runCli(["query", "local", "select 1", "--limit", "0", "--json"], { backend: fakeBackend() });
assert.equal(result.exitCode, 1);
assert.equal(JSON.parse(result.stderr).error.code, "INVALID_OPTION");
});
test("rejects invalid query timeouts", async () => {
const result = await runCli(["query", "local", "select 1", "--timeout", "forever", "--json"], { backend: fakeBackend() });
assert.equal(result.exitCode, 1);
assert.equal(JSON.parse(result.stderr).error.code, "INVALID_OPTION");
});
test("requires a connection when DBX_CONNECTION is not set", async () => {
const result = await runCli(["query", "select 1", "--json"], { backend: fakeBackend(), env: {} });
assert.equal(result.exitCode, 1);
assert.equal(JSON.parse(result.stderr).error.code, "INVALID_ARGUMENT");
});
test("describes schema as json", async () => {
const result = await runCli(["schema", "describe", "local", "users", "--json"], { backend: fakeBackend() });
assert.equal(result.exitCode, 0);
assert.equal(JSON.parse(result.stdout).columns[0].name, "id");
});
test("lists schema tables as json", async () => {
const result = await runCli(["schema", "list", "local", "--schema", "public", "--json"], { backend: fakeBackend() });
assert.equal(result.exitCode, 0);
assert.deepEqual(JSON.parse(result.stdout), {
connection: "local",
schema: "public",
tables: [{ name: "users", type: "BASE TABLE" }],
});
});
test("builds schema context as prompt-ready text", async () => {
const result = await runCli(["context", "local", "--tables", "users"], { backend: fakeBackend() });
assert.equal(result.exitCode, 0);
assert.match(result.stdout, /Connection: local/);
assert.match(result.stdout, /## users/);
assert.match(result.stdout, /id integer NOT NULL PK/);
});
test("runs SQL from file", async () => {
const dir = await mkdtemp(join(tmpdir(), "dbx-cli-"));
const file = join(dir, "query.sql");
let executedSql = "";
try {
await writeFile(file, "select count(*) as total from users", "utf-8");
const result = await runCli(["query", "local", "--file", file, "--json"], {
backend: fakeBackend({
executeQuery: async (_config, sql) => {
executedSql = sql;
return { columns: ["total"], rows: [{ total: 1 }], row_count: 1 };
},
}),
});
assert.equal(result.exitCode, 0);
assert.equal(executedSql, "select count(*) as total from users");
} finally {
await rm(dir, { recursive: true, force: true });
}
});
test("allows non-dangerous writes when explicitly enabled", async () => {
let executed = false;
const result = await runCli(["query", "local", "update users set name = 'x' where id = 1", "--allow-writes", "--json"], {
backend: fakeBackend({
executeQuery: async () => {
executed = true;
return { columns: [], rows: [], row_count: 1 };
},
}),
});
assert.equal(result.exitCode, 0);
assert.equal(executed, true);
assert.equal(JSON.parse(result.stdout).row_count, 1);
});
test("keeps dangerous SQL blocked without allow-dangerous-sql", async () => {
const result = await runCli(["query", "local", "drop table users", "--allow-writes", "--json"], {
backend: fakeBackend(),
});
assert.equal(result.exitCode, 1);
assert.equal(JSON.parse(result.stderr).error.code, "SQL_BLOCKED");
});
test("rejects unknown options", async () => {
const result = await runCli(["connections", "list", "--wat", "--json"], { backend: fakeBackend() });
assert.equal(result.exitCode, 1);
assert.equal(JSON.parse(result.stderr).error.code, "UNKNOWN_OPTION");
});
test("rejects options with missing values", async () => {
const result = await runCli(["schema", "list", "local", "--schema", "--json"], { backend: fakeBackend() });
assert.equal(result.exitCode, 1);
assert.equal(JSON.parse(result.stderr).error.code, "INVALID_OPTION");
});
test("rejects query with both inline SQL and file SQL", async () => {
const dir = await mkdtemp(join(tmpdir(), "dbx-cli-"));
const file = join(dir, "query.sql");
try {
await writeFile(file, "select 1", "utf-8");
const result = await runCli(["query", "local", "select 2", "--file", file, "--json"], { backend: fakeBackend() });
assert.equal(result.exitCode, 1);
assert.equal(JSON.parse(result.stderr).error.code, "INVALID_ARGUMENT");
} finally {
await rm(dir, { recursive: true, force: true });
}
});
test("requires allow-writes before allow-dangerous-sql", async () => {
const result = await runCli(["query", "local", "drop table users", "--allow-dangerous-sql", "--json"], {
backend: fakeBackend(),
env: {},
});
assert.equal(result.exitCode, 1);
assert.equal(JSON.parse(result.stderr).error.code, "INVALID_OPTION");
});
test("rejects invalid max-tables values", async () => {
const result = await runCli(["context", "local", "--max-tables", "nope", "--json"], { backend: fakeBackend() });
assert.equal(result.exitCode, 1);
assert.equal(JSON.parse(result.stderr).error.code, "INVALID_OPTION");
});
test("prints the package version", async () => {
const result = await runCli(["--version"], { backend: fakeBackend() });
assert.equal(result.exitCode, 0);
assert.match(result.stdout, /^\d+\.\d+\.\d+\n$/);
});
test("rejects unexpected positional arguments", async () => {
const result = await runCli(["connections", "list", "extra", "--json"], { backend: fakeBackend() });
assert.equal(result.exitCode, 1);
assert.equal(JSON.parse(result.stderr).error.code, "INVALID_ARGUMENT");
});
test("uses a specific connection-not-found error code", async () => {
const result = await runCli(["schema", "list", "missing", "--json"], { backend: fakeBackend() });
assert.equal(result.exitCode, 1);
assert.equal(JSON.parse(result.stderr).error.code, "CONNECTION_NOT_FOUND");
});
test("supports -- to pass SQL that starts with a dash", async () => {
let executedSql = "";
const result = await runCli(["query", "local", "--json", "--", "-- comment\nselect 1"], {
backend: fakeBackend({
executeQuery: async (_config, sql) => {
executedSql = sql;
return { columns: ["total"], rows: [{ total: 1 }], row_count: 1 };
},
}),
});
assert.equal(result.exitCode, 0);
assert.equal(executedSql, "-- comment\nselect 1");
});