be8ee3cc2e
Verifies sense query returns data after signal persistence: - Default query returns rows (not 0 rows) - Output contains payload columns - --json flag returns valid JSON array with payload field - --sql flag runs custom read-only SQL Also adds --sql flag support to sense query CLI. Fixes #156
182 lines
5.4 KiB
TypeScript
182 lines
5.4 KiB
TypeScript
/**
|
|
* Tests for sense SQLite helpers used by `nerve sense schema` / `nerve sense query`.
|
|
*/
|
|
|
|
import { mkdirSync, rmSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { DatabaseSync } from "node:sqlite";
|
|
|
|
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
|
|
import {
|
|
assertSenseDbExists,
|
|
collectColumnKeys,
|
|
defaultPreviewSql,
|
|
formatRowsAsAlignedTable,
|
|
listTableSqlStatements,
|
|
parseSenseQueryArgs,
|
|
pickDefaultPreviewTable,
|
|
senseDbPath,
|
|
} from "../sense-sqlite.js";
|
|
|
|
let tmpDir: string;
|
|
|
|
beforeEach(() => {
|
|
tmpDir = join(
|
|
tmpdir(),
|
|
`nerve-sense-sqlite-${Date.now()}-${Math.random().toString(16).slice(2)}`,
|
|
);
|
|
mkdirSync(join(tmpDir, "data", "senses"), { recursive: true });
|
|
});
|
|
|
|
afterEach(() => {
|
|
rmSync(tmpDir, { recursive: true, force: true });
|
|
});
|
|
|
|
describe("senseDbPath", () => {
|
|
it("points at data/senses/<name>.db under the given root", () => {
|
|
expect(senseDbPath("/root", "cpu-usage")).toBe(join("/root", "data", "senses", "cpu-usage.db"));
|
|
});
|
|
});
|
|
|
|
describe("assertSenseDbExists", () => {
|
|
it("throws when the file is missing", () => {
|
|
expect(() => assertSenseDbExists(tmpDir, "nope")).toThrow(/No database at/);
|
|
});
|
|
|
|
it("returns the path when the file exists", () => {
|
|
const p = join(tmpDir, "data", "senses", "x.db");
|
|
new DatabaseSync(p).close();
|
|
expect(assertSenseDbExists(tmpDir, "x")).toBe(p);
|
|
});
|
|
});
|
|
|
|
describe("listTableSqlStatements", () => {
|
|
it("returns CREATE statements ordered by tbl_name", () => {
|
|
const p = join(tmpDir, "data", "senses", "t.db");
|
|
const db = new DatabaseSync(p);
|
|
db.exec("CREATE TABLE zebra (id INTEGER)");
|
|
db.exec("CREATE TABLE alpha (id INTEGER)");
|
|
const stmts = listTableSqlStatements(db);
|
|
db.close();
|
|
expect(stmts).toHaveLength(2);
|
|
expect(stmts[0]).toMatch(/^CREATE TABLE alpha/i);
|
|
expect(stmts[1]).toMatch(/^CREATE TABLE zebra/i);
|
|
});
|
|
});
|
|
|
|
describe("pickDefaultPreviewTable", () => {
|
|
it("prefers non-_migrations tables when both exist", () => {
|
|
const p = join(tmpDir, "data", "senses", "t.db");
|
|
const db = new DatabaseSync(p);
|
|
db.exec("CREATE TABLE _migrations (name TEXT PRIMARY KEY)");
|
|
db.exec("CREATE TABLE readings (id INTEGER)");
|
|
expect(pickDefaultPreviewTable(db)).toBe("readings");
|
|
db.close();
|
|
});
|
|
|
|
it("uses _migrations when it is the only table", () => {
|
|
const p = join(tmpDir, "data", "senses", "t.db");
|
|
const db = new DatabaseSync(p);
|
|
db.exec("CREATE TABLE _migrations (name TEXT PRIMARY KEY)");
|
|
expect(pickDefaultPreviewTable(db)).toBe("_migrations");
|
|
db.close();
|
|
});
|
|
});
|
|
|
|
describe("defaultPreviewSql", () => {
|
|
it("quotes identifiers for SQL safety", () => {
|
|
expect(defaultPreviewSql(`weird"name`)).toContain(`weird""name`);
|
|
});
|
|
});
|
|
|
|
describe("parseSenseQueryArgs", () => {
|
|
it("parses sense name only", () => {
|
|
expect(parseSenseQueryArgs(["cpu"])).toEqual({ name: "cpu", sql: undefined });
|
|
});
|
|
|
|
it("strips --json", () => {
|
|
expect(parseSenseQueryArgs(["cpu", "--json"])).toEqual({ name: "cpu", sql: undefined });
|
|
expect(parseSenseQueryArgs(["--json", "cpu"])).toEqual({ name: "cpu", sql: undefined });
|
|
});
|
|
|
|
it("joins remaining tokens into SQL", () => {
|
|
expect(parseSenseQueryArgs(["cpu", "SELECT", "1"])).toEqual({ name: "cpu", sql: "SELECT 1" });
|
|
});
|
|
|
|
it("uses --sql value instead of positional SQL", () => {
|
|
expect(parseSenseQueryArgs(["cpu", "--sql", "SELECT 2"])).toEqual({
|
|
name: "cpu",
|
|
sql: "SELECT 2",
|
|
});
|
|
expect(parseSenseQueryArgs(["cpu", "--sql=SELECT 3"])).toEqual({
|
|
name: "cpu",
|
|
sql: "SELECT 3",
|
|
});
|
|
});
|
|
|
|
it("prefers --sql over trailing positional SQL", () => {
|
|
expect(parseSenseQueryArgs(["cpu", "--sql", "SELECT a", "SELECT b"])).toEqual({
|
|
name: "cpu",
|
|
sql: "SELECT a",
|
|
});
|
|
});
|
|
|
|
it("throws when --sql has no value", () => {
|
|
expect(() => parseSenseQueryArgs(["cpu", "--sql"])).toThrow(/Missing value for --sql/);
|
|
});
|
|
|
|
it("throws when name is missing", () => {
|
|
expect(() => parseSenseQueryArgs(["--json"])).toThrow(/Missing sense name/);
|
|
});
|
|
});
|
|
|
|
describe("formatRowsAsAlignedTable", () => {
|
|
it("shows empty marker for no rows", () => {
|
|
expect(formatRowsAsAlignedTable([])).toContain("(0 rows)");
|
|
});
|
|
|
|
it("aligns columns from row data", () => {
|
|
const out = formatRowsAsAlignedTable([
|
|
{ a: 1, b: "x" },
|
|
{ a: 22, b: "yy" },
|
|
]);
|
|
expect(out).toContain("a");
|
|
expect(out).toContain("b");
|
|
expect(out).toContain("22");
|
|
});
|
|
});
|
|
|
|
describe("collectColumnKeys", () => {
|
|
it("preserves key order from first row then appends new keys", () => {
|
|
expect(
|
|
collectColumnKeys([
|
|
{ z: 1, a: 2 },
|
|
{ a: 3, b: 4 },
|
|
]),
|
|
).toEqual(["z", "a", "b"]);
|
|
});
|
|
});
|
|
|
|
describe("readonly query integration", () => {
|
|
it("runs default preview SQL on a real db", () => {
|
|
const p = join(tmpDir, "data", "senses", "demo.db");
|
|
const rw = new DatabaseSync(p);
|
|
rw.exec("CREATE TABLE items (id INTEGER PRIMARY KEY, v TEXT)");
|
|
rw.exec("INSERT INTO items (v) VALUES ('a'), ('b')");
|
|
rw.close();
|
|
|
|
const db = new DatabaseSync(p, { readOnly: true });
|
|
const table = pickDefaultPreviewTable(db);
|
|
expect(table).toBe("items");
|
|
if (table === null) {
|
|
throw new Error("expected items table");
|
|
}
|
|
const sql = defaultPreviewSql(table);
|
|
const rows = db.prepare(sql).all() as Record<string, unknown>[];
|
|
db.close();
|
|
expect(rows.length).toBeGreaterThanOrEqual(1);
|
|
});
|
|
});
|