Big News: Socket raises $60M Series C at a $1B valuation to secure software supply chains for AI-driven development.Announcement
Sign In

postgresai

Package Overview
Dependencies
Maintainers
1
Versions
136
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

postgresai - npm Package Compare versions

Comparing version
0.15.0-dev.1
to
0.15.0-dev.2
+373
lib/reports.ts
import { formatHttpError, maskSecret, normalizeBaseUrl } from "./util";
// ============================================================================
// Types
// ============================================================================
export interface CheckupReport {
id: number;
org_id: number;
org_name: string;
project_id: number;
project_name: string;
created_at: string;
created_formatted: string;
epoch: number;
status: string;
}
export interface CheckupReportFile {
id: number;
checkup_report_id: number;
filename: string;
check_id: string;
type: "json" | "md";
created_at: string;
created_formatted: string;
project_id: number;
project_name: string;
}
export interface CheckupReportFileData extends CheckupReportFile {
data: string;
}
// ============================================================================
// Date parsing
// ============================================================================
/**
* Parse a date string in various formats into an ISO 8601 string.
* Supported formats:
* YYYY-MM-DD 2025-01-15
* YYYY-MM-DDTHH:mm:ss 2025-01-15T10:30:00
* YYYY-MM-DD HH:mm:ss 2025-01-15 10:30:00
* YYYY-MM-DD HH:mm 2025-01-15 10:30
* DD.MM.YYYY 15.01.2025
* DD.MM.YYYY HH:mm 15.01.2025 10:30
* DD.MM.YYYY HH:mm:ss 15.01.2025 10:30:00
*/
export function parseFlexibleDate(input: string): string {
const s = input.trim();
// DD.MM.YYYY [HH:mm[:ss]]
const dotMatch = s.match(/^(\d{1,2})\.(\d{1,2})\.(\d{4})(?:\s+(\d{1,2}):(\d{2})(?::(\d{2}))?)?$/);
if (dotMatch) {
const [, dd, mm, yyyy, hh, min, ss] = dotMatch;
const iso = `${yyyy}-${mm.padStart(2, "0")}-${dd.padStart(2, "0")}T${(hh ?? "00").padStart(2, "0")}:${(min ?? "00").padStart(2, "0")}:${(ss ?? "00").padStart(2, "0")}Z`;
const d = new Date(iso);
if (isNaN(d.getTime())) throw new Error(`Invalid date: ${input}`);
return d.toISOString();
}
// YYYY-MM-DD[T ]HH:mm[:ss] or YYYY-MM-DD
const isoMatch = s.match(/^(\d{4})-(\d{2})-(\d{2})(?:[T ](\d{2}):(\d{2})(?::(\d{2}))?)?$/);
if (isoMatch) {
const [, yyyy, mm, dd, hh, min, ss] = isoMatch;
const iso = `${yyyy}-${mm}-${dd}T${hh ?? "00"}:${min ?? "00"}:${ss ?? "00"}Z`;
const d = new Date(iso);
if (isNaN(d.getTime())) throw new Error(`Invalid date: ${input}`);
return d.toISOString();
}
throw new Error(`Unrecognized date format: ${input}. Use YYYY-MM-DD or DD.MM.YYYY`);
}
// ============================================================================
// Params
// ============================================================================
export interface FetchReportsParams {
apiKey: string;
apiBaseUrl: string;
projectId?: number;
status?: string;
limit?: number;
beforeDate?: string;
/** @internal Used by fetchAllReports for keyset pagination */
beforeId?: number;
debug?: boolean;
}
export interface FetchReportFilesParams {
apiKey: string;
apiBaseUrl: string;
reportId?: number;
type?: "json" | "md";
checkId?: string;
debug?: boolean;
}
export interface FetchReportFileDataParams {
apiKey: string;
apiBaseUrl: string;
reportId?: number;
type?: "json" | "md";
checkId?: string;
debug?: boolean;
}
// ============================================================================
// API functions
// ============================================================================
export async function fetchReports(params: FetchReportsParams): Promise<CheckupReport[]> {
const { apiKey, apiBaseUrl, projectId, status, limit = 20, beforeDate, beforeId, debug } = params;
if (!apiKey) {
throw new Error("API key is required");
}
const base = normalizeBaseUrl(apiBaseUrl);
const url = new URL(`${base}/checkup_reports`);
url.searchParams.set("order", "id.desc");
url.searchParams.set("limit", String(limit));
if (typeof projectId === "number") {
url.searchParams.set("project_id", `eq.${projectId}`);
}
if (status) {
url.searchParams.set("status", `eq.${status}`);
}
if (beforeDate) {
url.searchParams.set("created_at", `lt.${beforeDate}`);
}
if (typeof beforeId === "number") {
url.searchParams.set("id", `lt.${beforeId}`);
}
const headers: Record<string, string> = {
"access-token": apiKey,
"Prefer": "return=representation",
"Content-Type": "application/json",
"Connection": "close",
};
if (debug) {
const debugHeaders: Record<string, string> = { ...headers, "access-token": maskSecret(apiKey) };
console.log(`Debug: Resolved API base URL: ${base}`);
console.log(`Debug: GET URL: ${url.toString()}`);
console.log(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
}
const response = await fetch(url.toString(), { method: "GET", headers });
if (debug) {
console.log(`Debug: Response status: ${response.status}`);
}
const data = await response.text();
if (response.ok) {
try {
return JSON.parse(data) as CheckupReport[];
} catch {
throw new Error(`Failed to parse reports response: ${data}`);
}
} else {
throw new Error(formatHttpError("Failed to fetch reports", response.status, data));
}
}
const MAX_ALL_REPORTS = 10000;
export async function fetchAllReports(params: Omit<FetchReportsParams, "beforeId" | "beforeDate">): Promise<CheckupReport[]> {
const pageSize = params.limit ?? 100;
const all: CheckupReport[] = [];
let beforeId: number | undefined;
while (true) {
const page = await fetchReports({ ...params, limit: pageSize, beforeId });
if (page.length === 0) break;
all.push(...page);
if (all.length >= MAX_ALL_REPORTS) {
console.warn(`Warning: reached maximum of ${MAX_ALL_REPORTS} reports, stopping pagination`);
break;
}
beforeId = page[page.length - 1].id;
if (page.length < pageSize) break;
}
return all;
}
export async function fetchReportFiles(params: FetchReportFilesParams): Promise<CheckupReportFile[]> {
const { apiKey, apiBaseUrl, reportId, type, checkId, debug } = params;
if (!apiKey) {
throw new Error("API key is required");
}
if (reportId === undefined && !checkId) {
throw new Error("Either reportId or checkId is required");
}
const base = normalizeBaseUrl(apiBaseUrl);
const url = new URL(`${base}/checkup_report_files`);
if (typeof reportId === "number") {
url.searchParams.set("checkup_report_id", `eq.${reportId}`);
}
url.searchParams.set("order", "id.asc");
if (type) {
url.searchParams.set("type", `eq.${type}`);
}
if (checkId) {
url.searchParams.set("check_id", `eq.${checkId}`);
}
const headers: Record<string, string> = {
"access-token": apiKey,
"Prefer": "return=representation",
"Content-Type": "application/json",
"Connection": "close",
};
if (debug) {
const debugHeaders: Record<string, string> = { ...headers, "access-token": maskSecret(apiKey) };
console.log(`Debug: Resolved API base URL: ${base}`);
console.log(`Debug: GET URL: ${url.toString()}`);
console.log(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
}
const response = await fetch(url.toString(), { method: "GET", headers });
if (debug) {
console.log(`Debug: Response status: ${response.status}`);
}
const data = await response.text();
if (response.ok) {
try {
return JSON.parse(data) as CheckupReportFile[];
} catch {
throw new Error(`Failed to parse report files response: ${data}`);
}
} else {
throw new Error(formatHttpError("Failed to fetch report files", response.status, data));
}
}
export async function fetchReportFileData(params: FetchReportFileDataParams): Promise<CheckupReportFileData[]> {
const { apiKey, apiBaseUrl, reportId, type, checkId, debug } = params;
if (!apiKey) {
throw new Error("API key is required");
}
if (reportId === undefined && !checkId) {
throw new Error("Either reportId or checkId is required");
}
const base = normalizeBaseUrl(apiBaseUrl);
const url = new URL(`${base}/checkup_report_file_data`);
if (typeof reportId === "number") {
url.searchParams.set("checkup_report_id", `eq.${reportId}`);
}
url.searchParams.set("order", "id.asc");
if (type) {
url.searchParams.set("type", `eq.${type}`);
}
if (checkId) {
url.searchParams.set("check_id", `eq.${checkId}`);
}
const headers: Record<string, string> = {
"access-token": apiKey,
"Prefer": "return=representation",
"Content-Type": "application/json",
"Connection": "close",
};
if (debug) {
const debugHeaders: Record<string, string> = { ...headers, "access-token": maskSecret(apiKey) };
console.log(`Debug: Resolved API base URL: ${base}`);
console.log(`Debug: GET URL: ${url.toString()}`);
console.log(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
}
const response = await fetch(url.toString(), { method: "GET", headers });
if (debug) {
console.log(`Debug: Response status: ${response.status}`);
}
const data = await response.text();
if (response.ok) {
try {
return JSON.parse(data) as CheckupReportFileData[];
} catch {
throw new Error(`Failed to parse report file data response: ${data}`);
}
} else {
throw new Error(formatHttpError("Failed to fetch report file data", response.status, data));
}
}
// ============================================================================
// Lightweight markdown terminal renderer
// ============================================================================
export function renderMarkdownForTerminal(md: string): string {
if (!md) return "";
const RESET = "\x1b[0m";
const BOLD = "\x1b[1m";
const BOLD_UNDERLINE = "\x1b[1;4m";
const DIM = "\x1b[2m";
const ITALIC = "\x1b[3m";
const CYAN = "\x1b[36m";
const lines = md.split("\n");
const output: string[] = [];
let inCodeBlock = false;
for (const line of lines) {
// Code block toggle
if (line.trimStart().startsWith("```")) {
inCodeBlock = !inCodeBlock;
if (inCodeBlock) {
output.push(`${DIM}${"─".repeat(40)}${RESET}`);
} else {
output.push(`${DIM}${"─".repeat(40)}${RESET}`);
}
continue;
}
// Inside code block — dim output
if (inCodeBlock) {
output.push(`${DIM} ${line}${RESET}`);
continue;
}
// Horizontal rule
if (/^-{3,}$/.test(line.trim()) || /^\*{3,}$/.test(line.trim()) || /^_{3,}$/.test(line.trim())) {
output.push(`${DIM}${"─".repeat(60)}${RESET}`);
continue;
}
// Headings
const headingMatch = line.match(/^(#{1,6})\s+(.*)/);
if (headingMatch) {
const level = headingMatch[1].length;
const text = headingMatch[2];
if (level === 1) {
output.push(`${BOLD_UNDERLINE}${text}${RESET}`);
} else {
output.push(`${BOLD}${text}${RESET}`);
}
continue;
}
// Inline formatting
let formatted = line;
// Bold: **text** or __text__
formatted = formatted.replace(/\*\*(.+?)\*\*/g, `${BOLD}$1${RESET}`);
formatted = formatted.replace(/__(.+?)__/g, `${BOLD}$1${RESET}`);
// Italic: *text* (only single, not inside **)
formatted = formatted.replace(/(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)/g, `${ITALIC}$1${RESET}`);
// Italic: _text_ — only at word boundaries (not inside identifiers like foo_bar_baz)
formatted = formatted.replace(/(?<=^|[\s(])_([^\s_](?:.*?[^\s_])?)_(?=$|[\s),.:;!?])/g, `${ITALIC}$1${RESET}`);
// Inline code: `text`
formatted = formatted.replace(/`([^`]+)`/g, `${CYAN}$1${RESET}`);
output.push(formatted);
}
return output.join("\n");
}
import { describe, test, expect } from "bun:test";
import { resolve } from "path";
import { mkdtempSync, existsSync, readFileSync } from "fs";
import { tmpdir } from "os";
function runCli(args: string[], env: Record<string, string> = {}) {
const cliPath = resolve(import.meta.dir, "..", "bin", "postgres-ai.ts");
const bunBin =
typeof process.execPath === "string" && process.execPath.length > 0
? process.execPath
: "bun";
const result = Bun.spawnSync([bunBin, cliPath, ...args], {
env: { ...process.env, ...env },
});
return {
status: result.exitCode,
stdout: new TextDecoder().decode(result.stdout),
stderr: new TextDecoder().decode(result.stderr),
};
}
async function runCliAsync(
args: string[],
env: Record<string, string> = {}
) {
const cliPath = resolve(import.meta.dir, "..", "bin", "postgres-ai.ts");
const bunBin =
typeof process.execPath === "string" && process.execPath.length > 0
? process.execPath
: "bun";
const proc = Bun.spawn([bunBin, cliPath, ...args], {
env: { ...process.env, ...env },
stdout: "pipe",
stderr: "pipe",
});
const [status, stdout, stderr] = await Promise.all([
proc.exited,
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
]);
return { status, stdout, stderr };
}
function isolatedEnv(extra: Record<string, string> = {}) {
const cfgHome = mkdtempSync(resolve(tmpdir(), "postgresai-cli-test-"));
return {
XDG_CONFIG_HOME: cfgHome,
HOME: cfgHome,
...extra,
};
}
async function startFakeApi() {
const requests: Array<{
method: string;
pathname: string;
searchParams: Record<string, string>;
headers: Record<string, string>;
}> = [];
const server = Bun.serve({
hostname: "127.0.0.1",
port: 0,
async fetch(req) {
const url = new URL(req.url);
const headers: Record<string, string> = {};
for (const [k, v] of req.headers.entries()) headers[k.toLowerCase()] = v;
const searchParams: Record<string, string> = {};
for (const [k, v] of url.searchParams.entries()) searchParams[k] = v;
requests.push({
method: req.method,
pathname: url.pathname,
searchParams,
headers,
});
// GET /checkup_reports
if (
req.method === "GET" &&
url.pathname.endsWith("/checkup_reports")
) {
return new Response(
JSON.stringify([
{
id: 1,
org_id: 1,
org_name: "TestOrg",
project_id: 10,
project_name: "TestProj",
created_at: "2025-01-01T00:00:00Z",
created_formatted: "2025-01-01 00:00:00",
epoch: 1735689600,
status: "completed",
},
]),
{ status: 200, headers: { "Content-Type": "application/json" } }
);
}
// GET /checkup_report_files
if (
req.method === "GET" &&
url.pathname.endsWith("/checkup_report_files")
) {
return new Response(
JSON.stringify([
{
id: 100,
checkup_report_id: 1,
filename: "H002.json",
check_id: "H002",
type: "json",
created_at: "2025-01-01T00:00:00Z",
created_formatted: "2025-01-01 00:00:00",
project_id: 10,
project_name: "TestProj",
},
{
id: 101,
checkup_report_id: 1,
filename: "H002.md",
check_id: "H002",
type: "md",
created_at: "2025-01-01T00:00:00Z",
created_formatted: "2025-01-01 00:00:00",
project_id: 10,
project_name: "TestProj",
},
]),
{ status: 200, headers: { "Content-Type": "application/json" } }
);
}
// GET /checkup_report_file_data
if (
req.method === "GET" &&
url.pathname.endsWith("/checkup_report_file_data")
) {
return new Response(
JSON.stringify([
{
id: 100,
checkup_report_id: 1,
filename: "H002.md",
check_id: "H002",
type: "md",
created_at: "2025-01-01T00:00:00Z",
created_formatted: "2025-01-01 00:00:00",
project_id: 10,
project_name: "TestProj",
data: "# H002 Report\n\nUnused indexes found.\n",
},
]),
{ status: 200, headers: { "Content-Type": "application/json" } }
);
}
return new Response("not found", { status: 404 });
},
});
return {
baseUrl: `http://${server.hostname}:${server.port}/api/general`,
requests,
stop: () => server.stop(true),
};
}
// ---------------------------------------------------------------------------
// Help output
// ---------------------------------------------------------------------------
describe("CLI reports command group", () => {
test("reports help exposes list, files, data subcommands", () => {
const r = runCli(["reports", "--help"], isolatedEnv());
expect(r.status).toBe(0);
const out = `${r.stdout}\n${r.stderr}`;
expect(out).toContain("list");
expect(out).toContain("files");
expect(out).toContain("data");
});
// -----------------------------------------------------------------------
// Input validation
// -----------------------------------------------------------------------
test("reports list fails fast when API key is missing", () => {
const r = runCli(["reports", "list"], isolatedEnv());
expect(r.status).toBe(1);
expect(`${r.stdout}\n${r.stderr}`).toContain("API key is required");
});
test("reports files fails fast when API key is missing", () => {
const r = runCli(["reports", "files", "1"], isolatedEnv());
expect(r.status).toBe(1);
expect(`${r.stdout}\n${r.stderr}`).toContain("API key is required");
});
test("reports files fails when reportId is not a number", () => {
const r = runCli(
["reports", "files", "abc"],
isolatedEnv({ PGAI_API_KEY: "test-key" })
);
expect(r.status).toBe(1);
expect(`${r.stdout}\n${r.stderr}`).toContain("reportId must be a number");
});
test("reports files fails when neither reportId nor --check-id is provided", () => {
const r = runCli(
["reports", "files"],
isolatedEnv({ PGAI_API_KEY: "test-key" })
);
expect(r.status).toBe(1);
expect(`${r.stdout}\n${r.stderr}`).toContain("Either reportId or --check-id is required");
});
test("reports data fails fast when API key is missing", () => {
const r = runCli(["reports", "data", "1"], isolatedEnv());
expect(r.status).toBe(1);
expect(`${r.stdout}\n${r.stderr}`).toContain("API key is required");
});
test("reports data fails when reportId is not a number", () => {
const r = runCli(
["reports", "data", "abc"],
isolatedEnv({ PGAI_API_KEY: "test-key" })
);
expect(r.status).toBe(1);
expect(`${r.stdout}\n${r.stderr}`).toContain("reportId must be a number");
});
test("reports data fails when neither reportId nor --check-id is provided", () => {
const r = runCli(
["reports", "data"],
isolatedEnv({ PGAI_API_KEY: "test-key" })
);
expect(r.status).toBe(1);
expect(`${r.stdout}\n${r.stderr}`).toContain("Either reportId or --check-id is required");
});
// -----------------------------------------------------------------------
// Successful API calls
// -----------------------------------------------------------------------
test("reports list succeeds against a fake API", async () => {
const api = await startFakeApi();
try {
const r = await runCliAsync(
["reports", "list"],
isolatedEnv({
PGAI_API_KEY: "test-key",
PGAI_API_BASE_URL: api.baseUrl,
})
);
expect(r.status).toBe(0);
const out = JSON.parse(r.stdout.trim());
expect(Array.isArray(out)).toBe(true);
expect(out[0].id).toBe(1);
expect(out[0].status).toBe("completed");
const req = api.requests.find((x) =>
x.pathname.endsWith("/checkup_reports")
);
expect(req).toBeTruthy();
expect(req!.headers["access-token"]).toBe("test-key");
} finally {
api.stop();
}
});
test("reports list passes correct filters to API", async () => {
const api = await startFakeApi();
try {
await runCliAsync(
[
"reports",
"list",
"--project-id",
"10",
"--status",
"completed",
"--limit",
"5",
],
isolatedEnv({
PGAI_API_KEY: "test-key",
PGAI_API_BASE_URL: api.baseUrl,
})
);
const req = api.requests.find((x) =>
x.pathname.endsWith("/checkup_reports")
);
expect(req).toBeTruthy();
expect(req!.searchParams.project_id).toBe("eq.10");
expect(req!.searchParams.status).toBe("eq.completed");
expect(req!.searchParams.limit).toBe("5");
} finally {
api.stop();
}
});
test("reports files succeeds against a fake API", async () => {
const api = await startFakeApi();
try {
const r = await runCliAsync(
["reports", "files", "1"],
isolatedEnv({
PGAI_API_KEY: "test-key",
PGAI_API_BASE_URL: api.baseUrl,
})
);
expect(r.status).toBe(0);
const out = JSON.parse(r.stdout.trim());
expect(Array.isArray(out)).toBe(true);
expect(out[0].filename).toBe("H002.json");
const req = api.requests.find((x) =>
x.pathname.endsWith("/checkup_report_files")
);
expect(req).toBeTruthy();
expect(req!.searchParams.checkup_report_id).toBe("eq.1");
} finally {
api.stop();
}
});
test("reports files passes type and check-id filters", async () => {
const api = await startFakeApi();
try {
await runCliAsync(
["reports", "files", "1", "--type", "md", "--check-id", "H002"],
isolatedEnv({
PGAI_API_KEY: "test-key",
PGAI_API_BASE_URL: api.baseUrl,
})
);
const req = api.requests.find((x) =>
x.pathname.endsWith("/checkup_report_files")
);
expect(req).toBeTruthy();
expect(req!.searchParams.type).toBe("eq.md");
expect(req!.searchParams.check_id).toBe("eq.H002");
} finally {
api.stop();
}
});
test("reports data outputs raw markdown by default", async () => {
const api = await startFakeApi();
try {
const r = await runCliAsync(
["reports", "data", "1"],
isolatedEnv({
PGAI_API_KEY: "test-key",
PGAI_API_BASE_URL: api.baseUrl,
})
);
expect(r.status).toBe(0);
expect(r.stdout).toContain("# H002 Report");
expect(r.stdout).toContain("Unused indexes found.");
} finally {
api.stop();
}
});
test("reports data succeeds against a fake API with --json", async () => {
const api = await startFakeApi();
try {
const r = await runCliAsync(
["reports", "data", "1", "--json"],
isolatedEnv({
PGAI_API_KEY: "test-key",
PGAI_API_BASE_URL: api.baseUrl,
})
);
expect(r.status).toBe(0);
const out = JSON.parse(r.stdout.trim());
expect(Array.isArray(out)).toBe(true);
expect(out[0].data).toContain("# H002 Report");
} finally {
api.stop();
}
});
test("reports files succeeds with only --check-id (no reportId)", async () => {
const api = await startFakeApi();
try {
const r = await runCliAsync(
["reports", "files", "--check-id", "H002"],
isolatedEnv({
PGAI_API_KEY: "test-key",
PGAI_API_BASE_URL: api.baseUrl,
})
);
expect(r.status).toBe(0);
const req = api.requests.find((x) =>
x.pathname.endsWith("/checkup_report_files")
);
expect(req).toBeTruthy();
expect(req!.searchParams.check_id).toBe("eq.H002");
expect(req!.searchParams.checkup_report_id).toBeUndefined();
} finally {
api.stop();
}
});
test("reports data succeeds with only --check-id (no reportId)", async () => {
const api = await startFakeApi();
try {
const r = await runCliAsync(
["reports", "data", "--check-id", "H002", "--json"],
isolatedEnv({
PGAI_API_KEY: "test-key",
PGAI_API_BASE_URL: api.baseUrl,
})
);
expect(r.status).toBe(0);
const req = api.requests.find((x) =>
x.pathname.endsWith("/checkup_report_file_data")
);
expect(req).toBeTruthy();
expect(req!.searchParams.check_id).toBe("eq.H002");
expect(req!.searchParams.checkup_report_id).toBeUndefined();
} finally {
api.stop();
}
});
test("reports data --output saves files to directory", async () => {
const api = await startFakeApi();
try {
const outDir = mkdtempSync(resolve(tmpdir(), "pgai-output-test-"));
const r = await runCliAsync(
["reports", "data", "1", "--output", outDir],
isolatedEnv({
PGAI_API_KEY: "test-key",
PGAI_API_BASE_URL: api.baseUrl,
})
);
expect(r.status).toBe(0);
// Should print the file path to stdout
expect(r.stdout).toContain("H002.md");
// File should exist with correct content
const filePath = resolve(outDir, "H002.md");
expect(existsSync(filePath)).toBe(true);
const content = readFileSync(filePath, "utf-8");
expect(content).toContain("# H002 Report");
expect(content).toContain("Unused indexes found.");
} finally {
api.stop();
}
});
test("reports data -o creates directory if it does not exist", async () => {
const api = await startFakeApi();
try {
const base = mkdtempSync(resolve(tmpdir(), "pgai-output-test-"));
const outDir = resolve(base, "nested", "dir");
const r = await runCliAsync(
["reports", "data", "1", "-o", outDir],
isolatedEnv({
PGAI_API_KEY: "test-key",
PGAI_API_BASE_URL: api.baseUrl,
})
);
expect(r.status).toBe(0);
expect(existsSync(resolve(outDir, "H002.md"))).toBe(true);
} finally {
api.stop();
}
});
test("reports data --formatted is accepted", async () => {
const api = await startFakeApi();
try {
const r = await runCliAsync(
["reports", "data", "1", "--formatted"],
isolatedEnv({
PGAI_API_KEY: "test-key",
PGAI_API_BASE_URL: api.baseUrl,
})
);
expect(r.status).toBe(0);
// The command should succeed — ANSI formatting only active in TTY
// In non-TTY (our test pipe), it falls back to raw output
expect(r.stdout).toContain("H002 Report");
} finally {
api.stop();
}
});
test("reports data sends correct filters to API", async () => {
const api = await startFakeApi();
try {
await runCliAsync(
[
"reports",
"data",
"1",
"--type",
"md",
"--check-id",
"H001",
"--json",
],
isolatedEnv({
PGAI_API_KEY: "test-key",
PGAI_API_BASE_URL: api.baseUrl,
})
);
const req = api.requests.find((x) =>
x.pathname.endsWith("/checkup_report_file_data")
);
expect(req).toBeTruthy();
expect(req!.searchParams.checkup_report_id).toBe("eq.1");
expect(req!.searchParams.type).toBe("eq.md");
expect(req!.searchParams.check_id).toBe("eq.H001");
} finally {
api.stop();
}
});
test("reports list --before filters by created_at with ISO date", async () => {
const api = await startFakeApi();
try {
await runCliAsync(
["reports", "list", "--before", "2025-01-15"],
isolatedEnv({
PGAI_API_KEY: "test-key",
PGAI_API_BASE_URL: api.baseUrl,
})
);
const req = api.requests.find((x) =>
x.pathname.endsWith("/checkup_reports")
);
expect(req).toBeTruthy();
expect(req!.searchParams.created_at).toContain("lt.2025-01-15");
expect(req!.searchParams.order).toBe("id.desc");
} finally {
api.stop();
}
});
test("reports list --before accepts DD.MM.YYYY format", async () => {
const api = await startFakeApi();
try {
await runCliAsync(
["reports", "list", "--before", "15.01.2025"],
isolatedEnv({
PGAI_API_KEY: "test-key",
PGAI_API_BASE_URL: api.baseUrl,
})
);
const req = api.requests.find((x) =>
x.pathname.endsWith("/checkup_reports")
);
expect(req).toBeTruthy();
expect(req!.searchParams.created_at).toContain("lt.2025-01-15");
} finally {
api.stop();
}
});
test("reports list --before rejects invalid date", async () => {
const r = runCli(
["reports", "list", "--before", "not-a-date"],
isolatedEnv({ PGAI_API_KEY: "test-key" })
);
expect(r.status).toBe(1);
expect(`${r.stdout}\n${r.stderr}`).toContain("Unrecognized date format");
});
test("reports list --all --before rejects conflicting flags", () => {
const r = runCli(
["reports", "list", "--all", "--before", "2025-01-15"],
isolatedEnv({ PGAI_API_KEY: "test-key" })
);
expect(r.status).toBe(1);
expect(`${r.stdout}\n${r.stderr}`).toContain("--all and --before cannot be used together");
});
test("reports data without --type defaults to md for terminal output", async () => {
const api = await startFakeApi();
try {
await runCliAsync(
["reports", "data", "1"],
isolatedEnv({
PGAI_API_KEY: "test-key",
PGAI_API_BASE_URL: api.baseUrl,
})
);
const req = api.requests.find((x) =>
x.pathname.endsWith("/checkup_report_file_data")
);
expect(req).toBeTruthy();
expect(req!.searchParams.type).toBe("eq.md");
} finally {
api.stop();
}
});
test("reports data --json without --type fetches all types", async () => {
const api = await startFakeApi();
try {
await runCliAsync(
["reports", "data", "1", "--json"],
isolatedEnv({
PGAI_API_KEY: "test-key",
PGAI_API_BASE_URL: api.baseUrl,
})
);
const req = api.requests.find((x) =>
x.pathname.endsWith("/checkup_report_file_data")
);
expect(req).toBeTruthy();
expect(req!.searchParams.type).toBeUndefined();
} finally {
api.stop();
}
});
test("reports data --output strips path traversal from filenames", async () => {
// Start a fake API that returns a filename with path traversal
const traversalRequests: typeof Array.prototype = [];
const server = Bun.serve({
hostname: "127.0.0.1",
port: 0,
async fetch(req) {
const url = new URL(req.url);
if (url.pathname.endsWith("/checkup_report_file_data")) {
return new Response(
JSON.stringify([
{
id: 100,
checkup_report_id: 1,
filename: "../../etc/malicious.md",
check_id: "H002",
type: "md",
created_at: "2025-01-01T00:00:00Z",
created_formatted: "2025-01-01 00:00:00",
project_id: 10,
project_name: "TestProj",
data: "# Malicious content\n",
},
]),
{ status: 200, headers: { "Content-Type": "application/json" } }
);
}
return new Response("not found", { status: 404 });
},
});
try {
const outDir = mkdtempSync(resolve(tmpdir(), "pgai-traversal-test-"));
const r = await runCliAsync(
["reports", "data", "1", "--output", outDir],
isolatedEnv({
PGAI_API_KEY: "test-key",
PGAI_API_BASE_URL: `http://${server.hostname}:${server.port}/api/general`,
})
);
expect(r.status).toBe(0);
// File should be saved as basename only, not the traversal path
expect(existsSync(resolve(outDir, "malicious.md"))).toBe(true);
// Traversal path should NOT exist
expect(existsSync(resolve(outDir, "..", "..", "etc", "malicious.md"))).toBe(false);
// Stdout should show the safe name
expect(r.stdout).toContain("malicious.md");
expect(r.stdout).not.toContain("../../");
} finally {
server.stop(true);
}
});
test("reports list --all fetches all pages", async () => {
const api = await startFakeApi();
try {
const r = await runCliAsync(
["reports", "list", "--all"],
isolatedEnv({
PGAI_API_KEY: "test-key",
PGAI_API_BASE_URL: api.baseUrl,
})
);
expect(r.status).toBe(0);
const out = JSON.parse(r.stdout.trim());
expect(Array.isArray(out)).toBe(true);
// The fake API always returns the same 1-item array, so --all will get 1 item
// (the page size > result count triggers stop)
expect(out.length).toBeGreaterThanOrEqual(1);
} finally {
api.stop();
}
});
});
import { describe, test, expect, mock, afterEach, spyOn } from "bun:test";
import {
fetchReports,
fetchAllReports,
fetchReportFiles,
fetchReportFileData,
renderMarkdownForTerminal,
parseFlexibleDate,
} from "../lib/reports";
const originalFetch = globalThis.fetch;
// ---------------------------------------------------------------------------
// fetchReports
// ---------------------------------------------------------------------------
describe("fetchReports", () => {
afterEach(() => {
globalThis.fetch = originalFetch;
});
test("throws when apiKey is missing", async () => {
await expect(
fetchReports({ apiKey: "", apiBaseUrl: "https://api.example.com" })
).rejects.toThrow("API key is required");
});
test("constructs correct URL with no filters", async () => {
let capturedRequest: { url: string; options: RequestInit } | null = null;
globalThis.fetch = mock((url: string, options: RequestInit) => {
capturedRequest = { url, options };
return Promise.resolve(
new Response(JSON.stringify([]), {
status: 200,
headers: { "Content-Type": "application/json" },
})
);
}) as unknown as typeof fetch;
await fetchReports({
apiKey: "test-key",
apiBaseUrl: "https://api.example.com",
});
expect(capturedRequest).not.toBeNull();
const url = new URL(capturedRequest!.url);
expect(url.pathname).toBe("/checkup_reports");
expect(url.searchParams.get("order")).toBe("id.desc");
expect(url.searchParams.get("limit")).toBe("20");
expect(url.searchParams.has("project_id")).toBe(false);
expect(url.searchParams.has("status")).toBe(false);
});
test("constructs correct URL with all filters", async () => {
let capturedRequest: { url: string; options: RequestInit } | null = null;
globalThis.fetch = mock((url: string, options: RequestInit) => {
capturedRequest = { url, options };
return Promise.resolve(
new Response(JSON.stringify([]), {
status: 200,
headers: { "Content-Type": "application/json" },
})
);
}) as unknown as typeof fetch;
await fetchReports({
apiKey: "test-key",
apiBaseUrl: "https://api.example.com",
projectId: 5,
status: "completed",
limit: 10,
});
expect(capturedRequest).not.toBeNull();
const url = new URL(capturedRequest!.url);
expect(url.searchParams.get("project_id")).toBe("eq.5");
expect(url.searchParams.get("status")).toBe("eq.completed");
expect(url.searchParams.get("limit")).toBe("10");
});
test("sends correct headers", async () => {
let capturedRequest: { url: string; options: RequestInit } | null = null;
globalThis.fetch = mock((url: string, options: RequestInit) => {
capturedRequest = { url, options };
return Promise.resolve(
new Response(JSON.stringify([]), {
status: 200,
headers: { "Content-Type": "application/json" },
})
);
}) as unknown as typeof fetch;
await fetchReports({
apiKey: "test-key",
apiBaseUrl: "https://api.example.com",
});
const headers = capturedRequest!.options.headers as Record<string, string>;
expect(headers["access-token"]).toBe("test-key");
expect(headers["Prefer"]).toBe("return=representation");
expect(headers["Content-Type"]).toBe("application/json");
expect(headers["Connection"]).toBe("close");
});
test("returns parsed response array", async () => {
const mockData = [
{
id: 1,
org_id: 1,
org_name: "TestOrg",
project_id: 10,
project_name: "prod-db",
created_at: "2025-01-01T00:00:00Z",
created_formatted: "2025-01-01 00:00:00",
epoch: 1735689600,
status: "completed",
},
];
globalThis.fetch = mock(() =>
Promise.resolve(
new Response(JSON.stringify(mockData), {
status: 200,
headers: { "Content-Type": "application/json" },
})
)
) as unknown as typeof fetch;
const result = await fetchReports({
apiKey: "test-key",
apiBaseUrl: "https://api.example.com",
});
expect(result).toEqual(mockData);
expect(result[0].id).toBe(1);
expect(result[0].status).toBe("completed");
});
test("throws formatted error on non-200 response", async () => {
globalThis.fetch = mock(() =>
Promise.resolve(
new Response('{"message": "Unauthorized"}', {
status: 401,
headers: { "Content-Type": "application/json" },
})
)
) as unknown as typeof fetch;
await expect(
fetchReports({
apiKey: "invalid-key",
apiBaseUrl: "https://api.example.com",
})
).rejects.toThrow(/Failed to fetch reports/);
});
test("sets created_at filter when beforeDate is provided", async () => {
let capturedRequest: { url: string; options: RequestInit } | null = null;
globalThis.fetch = mock((url: string, options: RequestInit) => {
capturedRequest = { url, options };
return Promise.resolve(
new Response(JSON.stringify([]), {
status: 200,
headers: { "Content-Type": "application/json" },
})
);
}) as unknown as typeof fetch;
await fetchReports({
apiKey: "test-key",
apiBaseUrl: "https://api.example.com",
beforeDate: "2025-01-15T00:00:00.000Z",
});
expect(capturedRequest).not.toBeNull();
const url = new URL(capturedRequest!.url);
expect(url.searchParams.get("created_at")).toBe("lt.2025-01-15T00:00:00.000Z");
expect(url.searchParams.get("order")).toBe("id.desc");
});
test("sets id=lt.beforeId when beforeId is provided (internal pagination)", async () => {
let capturedRequest: { url: string; options: RequestInit } | null = null;
globalThis.fetch = mock((url: string, options: RequestInit) => {
capturedRequest = { url, options };
return Promise.resolve(
new Response(JSON.stringify([]), {
status: 200,
headers: { "Content-Type": "application/json" },
})
);
}) as unknown as typeof fetch;
await fetchReports({
apiKey: "test-key",
apiBaseUrl: "https://api.example.com",
beforeId: 50,
});
expect(capturedRequest).not.toBeNull();
const url = new URL(capturedRequest!.url);
expect(url.searchParams.get("id")).toBe("lt.50");
});
test("logs debug info when debug is true", async () => {
const consoleSpy = spyOn(console, "log").mockImplementation(() => {});
globalThis.fetch = mock(() =>
Promise.resolve(
new Response(JSON.stringify([]), {
status: 200,
headers: { "Content-Type": "application/json" },
})
)
) as unknown as typeof fetch;
await fetchReports({
apiKey: "test-key",
apiBaseUrl: "https://api.example.com",
debug: true,
});
const calls = consoleSpy.mock.calls.map((c) => c[0]);
expect(calls.some((c: string) => c.includes("Debug: Resolved API base URL"))).toBe(true);
expect(calls.some((c: string) => c.includes("Debug: GET URL"))).toBe(true);
expect(calls.some((c: string) => c.includes("Debug: Request headers"))).toBe(true);
expect(calls.some((c: string) => c.includes("Debug: Response status"))).toBe(true);
consoleSpy.mockRestore();
});
test("throws on invalid JSON response", async () => {
globalThis.fetch = mock(() =>
Promise.resolve(
new Response("not valid json", {
status: 200,
headers: { "Content-Type": "text/plain" },
})
)
) as unknown as typeof fetch;
await expect(
fetchReports({
apiKey: "test-key",
apiBaseUrl: "https://api.example.com",
})
).rejects.toThrow(/Failed to parse reports response/);
});
test("does not set id or created_at params when no before options provided", async () => {
let capturedRequest: { url: string; options: RequestInit } | null = null;
globalThis.fetch = mock((url: string, options: RequestInit) => {
capturedRequest = { url, options };
return Promise.resolve(
new Response(JSON.stringify([]), {
status: 200,
headers: { "Content-Type": "application/json" },
})
);
}) as unknown as typeof fetch;
await fetchReports({
apiKey: "test-key",
apiBaseUrl: "https://api.example.com",
});
const url = new URL(capturedRequest!.url);
expect(url.searchParams.has("id")).toBe(false);
expect(url.searchParams.has("created_at")).toBe(false);
});
});
// ---------------------------------------------------------------------------
// fetchAllReports
// ---------------------------------------------------------------------------
describe("fetchAllReports", () => {
afterEach(() => {
globalThis.fetch = originalFetch;
});
test("fetches all pages until empty response", async () => {
const page1 = [
{ id: 100, org_id: 1, org_name: "O", project_id: 1, project_name: "P", created_at: "", created_formatted: "", epoch: 0, status: "completed" },
{ id: 90, org_id: 1, org_name: "O", project_id: 1, project_name: "P", created_at: "", created_formatted: "", epoch: 0, status: "completed" },
];
const page2 = [
{ id: 80, org_id: 1, org_name: "O", project_id: 1, project_name: "P", created_at: "", created_formatted: "", epoch: 0, status: "completed" },
];
let callCount = 0;
globalThis.fetch = mock((url: string) => {
callCount++;
const u = new URL(url);
const idParam = u.searchParams.get("id");
let data;
if (!idParam) {
data = page1;
} else if (idParam === "lt.90") {
data = page2;
} else {
data = [];
}
return Promise.resolve(
new Response(JSON.stringify(data), {
status: 200,
headers: { "Content-Type": "application/json" },
})
);
}) as unknown as typeof fetch;
const result = await fetchAllReports({
apiKey: "test-key",
apiBaseUrl: "https://api.example.com",
limit: 2,
});
expect(result.length).toBe(3);
expect(result[0].id).toBe(100);
expect(result[1].id).toBe(90);
expect(result[2].id).toBe(80);
});
test("returns empty array when no reports exist", async () => {
globalThis.fetch = mock(() =>
Promise.resolve(
new Response(JSON.stringify([]), {
status: 200,
headers: { "Content-Type": "application/json" },
})
)
) as unknown as typeof fetch;
const result = await fetchAllReports({
apiKey: "test-key",
apiBaseUrl: "https://api.example.com",
});
expect(result).toEqual([]);
});
test("stops at MAX_ALL_REPORTS cap (10000)", async () => {
const warnSpy = spyOn(console, "warn").mockImplementation(() => {});
const pageSize = 500;
let callCount = 0;
globalThis.fetch = mock((url: string) => {
callCount++;
const u = new URL(url);
const idParam = u.searchParams.get("id");
// Generate a full page each time with descending IDs
const startId = idParam ? parseInt(idParam.replace("lt.", "")) - 1 : 100000;
const page = Array.from({ length: pageSize }, (_, i) => ({
id: startId - i,
org_id: 1,
org_name: "O",
project_id: 1,
project_name: "P",
created_at: "",
created_formatted: "",
epoch: 0,
status: "completed",
}));
return Promise.resolve(
new Response(JSON.stringify(page), {
status: 200,
headers: { "Content-Type": "application/json" },
})
);
}) as unknown as typeof fetch;
const result = await fetchAllReports({
apiKey: "test-key",
apiBaseUrl: "https://api.example.com",
limit: pageSize,
});
// Should stop at 10000 (MAX_ALL_REPORTS)
expect(result.length).toBe(10000);
// Should have logged a warning
const warnCalls = warnSpy.mock.calls.map((c) => c[0]);
expect(warnCalls.some((c: string) => c.includes("maximum of 10000"))).toBe(true);
// Should have made exactly 20 calls (10000 / 500)
expect(callCount).toBe(20);
warnSpy.mockRestore();
});
test("stops when page has fewer items than limit", async () => {
const page = [
{ id: 50, org_id: 1, org_name: "O", project_id: 1, project_name: "P", created_at: "", created_formatted: "", epoch: 0, status: "completed" },
];
let callCount = 0;
globalThis.fetch = mock(() => {
callCount++;
return Promise.resolve(
new Response(JSON.stringify(page), {
status: 200,
headers: { "Content-Type": "application/json" },
})
);
}) as unknown as typeof fetch;
const result = await fetchAllReports({
apiKey: "test-key",
apiBaseUrl: "https://api.example.com",
limit: 10,
});
expect(result.length).toBe(1);
expect(callCount).toBe(1);
});
});
// ---------------------------------------------------------------------------
// fetchReportFiles
// ---------------------------------------------------------------------------
describe("fetchReportFiles", () => {
afterEach(() => {
globalThis.fetch = originalFetch;
});
test("throws when apiKey is missing", async () => {
await expect(
fetchReportFiles({
apiKey: "",
apiBaseUrl: "https://api.example.com",
reportId: 1,
})
).rejects.toThrow("API key is required");
});
test("throws when neither reportId nor checkId is provided", async () => {
await expect(
fetchReportFiles({
apiKey: "test-key",
apiBaseUrl: "https://api.example.com",
})
).rejects.toThrow("Either reportId or checkId is required");
});
test("works with only checkId (no reportId)", async () => {
let capturedRequest: { url: string; options: RequestInit } | null = null;
globalThis.fetch = mock((url: string, options: RequestInit) => {
capturedRequest = { url, options };
return Promise.resolve(
new Response(JSON.stringify([]), {
status: 200,
headers: { "Content-Type": "application/json" },
})
);
}) as unknown as typeof fetch;
await fetchReportFiles({
apiKey: "test-key",
apiBaseUrl: "https://api.example.com",
checkId: "H002",
});
expect(capturedRequest).not.toBeNull();
const url = new URL(capturedRequest!.url);
expect(url.pathname).toBe("/checkup_report_files");
expect(url.searchParams.has("checkup_report_id")).toBe(false);
expect(url.searchParams.get("check_id")).toBe("eq.H002");
expect(url.searchParams.get("order")).toBe("id.asc");
});
test("constructs URL with required checkup_report_id", async () => {
let capturedRequest: { url: string; options: RequestInit } | null = null;
globalThis.fetch = mock((url: string, options: RequestInit) => {
capturedRequest = { url, options };
return Promise.resolve(
new Response(JSON.stringify([]), {
status: 200,
headers: { "Content-Type": "application/json" },
})
);
}) as unknown as typeof fetch;
await fetchReportFiles({
apiKey: "test-key",
apiBaseUrl: "https://api.example.com",
reportId: 42,
});
expect(capturedRequest).not.toBeNull();
const url = new URL(capturedRequest!.url);
expect(url.pathname).toBe("/checkup_report_files");
expect(url.searchParams.get("checkup_report_id")).toBe("eq.42");
expect(url.searchParams.get("order")).toBe("id.asc");
expect(url.searchParams.has("type")).toBe(false);
expect(url.searchParams.has("check_id")).toBe(false);
});
test("applies optional type and check_id filters", async () => {
let capturedRequest: { url: string; options: RequestInit } | null = null;
globalThis.fetch = mock((url: string, options: RequestInit) => {
capturedRequest = { url, options };
return Promise.resolve(
new Response(JSON.stringify([]), {
status: 200,
headers: { "Content-Type": "application/json" },
})
);
}) as unknown as typeof fetch;
await fetchReportFiles({
apiKey: "test-key",
apiBaseUrl: "https://api.example.com",
reportId: 42,
type: "md",
checkId: "H002",
});
const url = new URL(capturedRequest!.url);
expect(url.searchParams.get("type")).toBe("eq.md");
expect(url.searchParams.get("check_id")).toBe("eq.H002");
});
test("returns parsed response array", async () => {
const mockData = [
{
id: 100,
checkup_report_id: 42,
filename: "H002.json",
check_id: "H002",
type: "json",
created_at: "2025-01-01T00:00:00Z",
created_formatted: "2025-01-01 00:00:00",
project_id: 10,
project_name: "prod-db",
},
];
globalThis.fetch = mock(() =>
Promise.resolve(
new Response(JSON.stringify(mockData), {
status: 200,
headers: { "Content-Type": "application/json" },
})
)
) as unknown as typeof fetch;
const result = await fetchReportFiles({
apiKey: "test-key",
apiBaseUrl: "https://api.example.com",
reportId: 42,
});
expect(result).toEqual(mockData);
expect(result[0].filename).toBe("H002.json");
});
test("throws on error response", async () => {
globalThis.fetch = mock(() =>
Promise.resolve(
new Response('{"message": "Not found"}', {
status: 404,
headers: { "Content-Type": "application/json" },
})
)
) as unknown as typeof fetch;
await expect(
fetchReportFiles({
apiKey: "test-key",
apiBaseUrl: "https://api.example.com",
reportId: 999,
})
).rejects.toThrow(/Failed to fetch report files/);
});
test("logs debug info when debug is true", async () => {
const consoleSpy = spyOn(console, "log").mockImplementation(() => {});
globalThis.fetch = mock(() =>
Promise.resolve(
new Response(JSON.stringify([]), {
status: 200,
headers: { "Content-Type": "application/json" },
})
)
) as unknown as typeof fetch;
await fetchReportFiles({
apiKey: "test-key",
apiBaseUrl: "https://api.example.com",
reportId: 1,
debug: true,
});
const calls = consoleSpy.mock.calls.map((c) => c[0]);
expect(calls.some((c: string) => c.includes("Debug: Resolved API base URL"))).toBe(true);
expect(calls.some((c: string) => c.includes("Debug: Response status"))).toBe(true);
consoleSpy.mockRestore();
});
test("throws on invalid JSON response", async () => {
globalThis.fetch = mock(() =>
Promise.resolve(
new Response("not valid json", {
status: 200,
headers: { "Content-Type": "text/plain" },
})
)
) as unknown as typeof fetch;
await expect(
fetchReportFiles({
apiKey: "test-key",
apiBaseUrl: "https://api.example.com",
reportId: 1,
})
).rejects.toThrow(/Failed to parse report files response/);
});
});
// ---------------------------------------------------------------------------
// fetchReportFileData
// ---------------------------------------------------------------------------
describe("fetchReportFileData", () => {
afterEach(() => {
globalThis.fetch = originalFetch;
});
test("throws when apiKey is missing", async () => {
await expect(
fetchReportFileData({
apiKey: "",
apiBaseUrl: "https://api.example.com",
reportId: 1,
})
).rejects.toThrow("API key is required");
});
test("throws when neither reportId nor checkId is provided", async () => {
await expect(
fetchReportFileData({
apiKey: "test-key",
apiBaseUrl: "https://api.example.com",
})
).rejects.toThrow("Either reportId or checkId is required");
});
test("works with only checkId (no reportId)", async () => {
let capturedRequest: { url: string; options: RequestInit } | null = null;
globalThis.fetch = mock((url: string, options: RequestInit) => {
capturedRequest = { url, options };
return Promise.resolve(
new Response(JSON.stringify([]), {
status: 200,
headers: { "Content-Type": "application/json" },
})
);
}) as unknown as typeof fetch;
await fetchReportFileData({
apiKey: "test-key",
apiBaseUrl: "https://api.example.com",
checkId: "H002",
});
expect(capturedRequest).not.toBeNull();
const url = new URL(capturedRequest!.url);
expect(url.pathname).toBe("/checkup_report_file_data");
expect(url.searchParams.has("checkup_report_id")).toBe(false);
expect(url.searchParams.get("check_id")).toBe("eq.H002");
expect(url.searchParams.get("order")).toBe("id.asc");
});
test("constructs correct URL with reportId", async () => {
let capturedRequest: { url: string; options: RequestInit } | null = null;
globalThis.fetch = mock((url: string, options: RequestInit) => {
capturedRequest = { url, options };
return Promise.resolve(
new Response(JSON.stringify([]), {
status: 200,
headers: { "Content-Type": "application/json" },
})
);
}) as unknown as typeof fetch;
await fetchReportFileData({
apiKey: "test-key",
apiBaseUrl: "https://api.example.com",
reportId: 123,
});
const url = new URL(capturedRequest!.url);
expect(url.pathname).toBe("/checkup_report_file_data");
expect(url.searchParams.get("checkup_report_id")).toBe("eq.123");
expect(url.searchParams.get("order")).toBe("id.asc");
});
test("applies optional filters", async () => {
let capturedRequest: { url: string; options: RequestInit } | null = null;
globalThis.fetch = mock((url: string, options: RequestInit) => {
capturedRequest = { url, options };
return Promise.resolve(
new Response(JSON.stringify([]), {
status: 200,
headers: { "Content-Type": "application/json" },
})
);
}) as unknown as typeof fetch;
await fetchReportFileData({
apiKey: "test-key",
apiBaseUrl: "https://api.example.com",
reportId: 123,
type: "json",
checkId: "H001",
});
const url = new URL(capturedRequest!.url);
expect(url.searchParams.get("type")).toBe("eq.json");
expect(url.searchParams.get("check_id")).toBe("eq.H001");
});
test("returns parsed response with data field", async () => {
const mockData = [
{
id: 200,
checkup_report_id: 123,
filename: "H002.md",
check_id: "H002",
type: "md",
created_at: "2025-01-01T00:00:00Z",
created_formatted: "2025-01-01 00:00:00",
project_id: 10,
project_name: "prod-db",
data: "# H002 Report\n\nUnused indexes found.\n",
},
{
id: 201,
checkup_report_id: 123,
filename: "H002.json",
check_id: "H002",
type: "json",
created_at: "2025-01-01T00:00:00Z",
created_formatted: "2025-01-01 00:00:00",
project_id: 10,
project_name: "prod-db",
data: '{"unused_indexes": []}',
},
];
globalThis.fetch = mock(() =>
Promise.resolve(
new Response(JSON.stringify(mockData), {
status: 200,
headers: { "Content-Type": "application/json" },
})
)
) as unknown as typeof fetch;
const result = await fetchReportFileData({
apiKey: "test-key",
apiBaseUrl: "https://api.example.com",
reportId: 123,
});
expect(result.length).toBe(2);
expect(result[0].data).toContain("# H002 Report");
expect(result[1].data).toBe('{"unused_indexes": []}');
});
test("throws on error response", async () => {
globalThis.fetch = mock(() =>
Promise.resolve(
new Response('{"message": "Unauthorized"}', {
status: 401,
headers: { "Content-Type": "application/json" },
})
)
) as unknown as typeof fetch;
await expect(
fetchReportFileData({
apiKey: "invalid-key",
apiBaseUrl: "https://api.example.com",
reportId: 123,
})
).rejects.toThrow(/Failed to fetch report file data/);
});
test("logs debug info when debug is true", async () => {
const consoleSpy = spyOn(console, "log").mockImplementation(() => {});
globalThis.fetch = mock(() =>
Promise.resolve(
new Response(JSON.stringify([]), {
status: 200,
headers: { "Content-Type": "application/json" },
})
)
) as unknown as typeof fetch;
await fetchReportFileData({
apiKey: "test-key",
apiBaseUrl: "https://api.example.com",
reportId: 1,
debug: true,
});
const calls = consoleSpy.mock.calls.map((c) => c[0]);
expect(calls.some((c: string) => c.includes("Debug: Resolved API base URL"))).toBe(true);
expect(calls.some((c: string) => c.includes("Debug: Response status"))).toBe(true);
consoleSpy.mockRestore();
});
test("throws on invalid JSON response", async () => {
globalThis.fetch = mock(() =>
Promise.resolve(
new Response("not valid json", {
status: 200,
headers: { "Content-Type": "text/plain" },
})
)
) as unknown as typeof fetch;
await expect(
fetchReportFileData({
apiKey: "test-key",
apiBaseUrl: "https://api.example.com",
reportId: 1,
})
).rejects.toThrow(/Failed to parse report file data response/);
});
});
// ---------------------------------------------------------------------------
// renderMarkdownForTerminal
// ---------------------------------------------------------------------------
describe("renderMarkdownForTerminal", () => {
test("returns empty string for empty input", () => {
expect(renderMarkdownForTerminal("")).toBe("");
});
test("passes plain text through", () => {
const result = renderMarkdownForTerminal("Just plain text");
expect(result).toContain("Just plain text");
});
test("renders # heading with bold+underline", () => {
const result = renderMarkdownForTerminal("# Hello World");
expect(result).toContain("\x1b[");
expect(result).toContain("Hello World");
});
test("renders ## heading with bold", () => {
const result = renderMarkdownForTerminal("## Section Title");
expect(result).toContain("\x1b[1m");
expect(result).toContain("Section Title");
});
test("renders **bold** text", () => {
const result = renderMarkdownForTerminal("This is **bold** text");
expect(result).toContain("\x1b[1m");
expect(result).toContain("bold");
});
test("renders `inline code`", () => {
const result = renderMarkdownForTerminal("Run `SELECT 1`");
expect(result).toContain("SELECT 1");
});
test("renders code blocks", () => {
const input = "```\nSELECT 1;\nSELECT 2;\n```";
const result = renderMarkdownForTerminal(input);
expect(result).toContain("SELECT 1;");
expect(result).toContain("SELECT 2;");
});
test("renders horizontal rules", () => {
const result = renderMarkdownForTerminal("---");
expect(result.replace(/\x1b\[[0-9;]*m/g, "").trim().length).toBeGreaterThan(3);
});
test("renders bullet lists", () => {
const result = renderMarkdownForTerminal("- item one\n- item two");
expect(result).toContain("item one");
expect(result).toContain("item two");
});
test("does not italicize underscores inside identifiers", () => {
const result = renderMarkdownForTerminal("goodvibes_local_monitoring_dev");
// Strip ANSI codes and check the text is unchanged
const stripped = result.replace(/\x1b\[[0-9;]*m/g, "");
expect(stripped).toBe("goodvibes_local_monitoring_dev");
// Must NOT contain italic ANSI code
expect(result).not.toContain("\x1b[3m");
});
test("renders _word_ as italic at word boundaries", () => {
const result = renderMarkdownForTerminal("This is _unknown_ value");
expect(result).toContain("\x1b[3m");
expect(result).toContain("unknown");
});
test("renders *italic* with single asterisks", () => {
const result = renderMarkdownForTerminal("This is *italic* text");
expect(result).toContain("\x1b[3m");
expect(result).toContain("italic");
});
test("renders __bold__ with double underscores", () => {
const result = renderMarkdownForTerminal("This is __bold__ text");
expect(result).toContain("\x1b[1m");
expect(result).toContain("bold");
});
});
// ---------------------------------------------------------------------------
// parseFlexibleDate
// ---------------------------------------------------------------------------
describe("parseFlexibleDate", () => {
test("parses YYYY-MM-DD", () => {
expect(parseFlexibleDate("2025-01-15")).toBe("2025-01-15T00:00:00.000Z");
});
test("parses YYYY-MM-DDTHH:mm:ss", () => {
expect(parseFlexibleDate("2025-01-15T10:30:00")).toBe("2025-01-15T10:30:00.000Z");
});
test("parses YYYY-MM-DD HH:mm:ss", () => {
expect(parseFlexibleDate("2025-01-15 10:30:00")).toBe("2025-01-15T10:30:00.000Z");
});
test("parses YYYY-MM-DD HH:mm", () => {
expect(parseFlexibleDate("2025-01-15 10:30")).toBe("2025-01-15T10:30:00.000Z");
});
test("parses DD.MM.YYYY", () => {
expect(parseFlexibleDate("15.01.2025")).toBe("2025-01-15T00:00:00.000Z");
});
test("parses DD.MM.YYYY HH:mm", () => {
expect(parseFlexibleDate("15.01.2025 10:30")).toBe("2025-01-15T10:30:00.000Z");
});
test("parses DD.MM.YYYY HH:mm:ss", () => {
expect(parseFlexibleDate("15.01.2025 10:30:45")).toBe("2025-01-15T10:30:45.000Z");
});
test("parses D.M.YYYY (single-digit day/month)", () => {
expect(parseFlexibleDate("5.1.2025")).toBe("2025-01-05T00:00:00.000Z");
});
test("trims whitespace", () => {
expect(parseFlexibleDate(" 2025-01-15 ")).toBe("2025-01-15T00:00:00.000Z");
});
test("throws on unrecognized format", () => {
expect(() => parseFlexibleDate("Jan 15, 2025")).toThrow(/Unrecognized date format/);
});
test("throws on invalid date values", () => {
expect(() => parseFlexibleDate("2025-13-45")).toThrow(/Invalid date/);
});
test("throws on invalid DD.MM.YYYY values", () => {
expect(() => parseFlexibleDate("45.13.2025")).toThrow(/Invalid date/);
});
});
+90
-0

@@ -17,2 +17,3 @@ import pkg from "../package.json";

} from "./issues";
import { fetchReports, fetchAllReports, fetchReportFiles, fetchReportFileData, parseFlexibleDate } from "./reports";
import { resolveBaseUrls } from "./util";

@@ -254,2 +255,46 @@

// Reports Tools
if (toolName === "list_reports") {
const projectId = args.project_id !== undefined ? Number(args.project_id) : undefined;
const status = args.status ? String(args.status) : undefined;
const limit = args.limit !== undefined ? Number(args.limit) : undefined;
const beforeDate = args.before_date ? parseFlexibleDate(String(args.before_date)) : undefined;
const all = args.all === true;
let reports;
if (all) {
reports = await fetchAllReports({ apiKey, apiBaseUrl, projectId, status, limit, debug });
} else {
reports = await fetchReports({ apiKey, apiBaseUrl, projectId, status, limit, beforeDate, debug });
}
return { content: [{ type: "text", text: JSON.stringify(reports, null, 2) }] };
}
if (toolName === "list_report_files") {
const reportId = args.report_id !== undefined ? Number(args.report_id) : undefined;
if (reportId !== undefined && isNaN(reportId)) {
return { content: [{ type: "text", text: "report_id must be a number" }], isError: true };
}
const type = args.type ? String(args.type) as "json" | "md" : undefined;
const checkId = args.check_id ? String(args.check_id) : undefined;
if (reportId === undefined && !checkId) {
return { content: [{ type: "text", text: "Either report_id or check_id is required" }], isError: true };
}
const files = await fetchReportFiles({ apiKey, apiBaseUrl, reportId, type, checkId, debug });
return { content: [{ type: "text", text: JSON.stringify(files, null, 2) }] };
}
if (toolName === "get_report_data") {
const reportId = args.report_id !== undefined ? Number(args.report_id) : undefined;
if (reportId !== undefined && isNaN(reportId)) {
return { content: [{ type: "text", text: "report_id must be a number" }], isError: true };
}
const type = args.type ? String(args.type) as "json" | "md" : undefined;
const checkId = args.check_id ? String(args.check_id) : undefined;
if (reportId === undefined && !checkId) {
return { content: [{ type: "text", text: "Either report_id or check_id is required" }], isError: true };
}
const files = await fetchReportFileData({ apiKey, apiBaseUrl, reportId, type, checkId, debug });
return { content: [{ type: "text", text: JSON.stringify(files, null, 2) }] };
}
throw new Error(`Unknown tool: ${toolName}`);

@@ -447,2 +492,47 @@ } catch (err) {

},
// Reports Tools
{
name: "list_reports",
description: "List checkup reports. Returns report metadata: id, project, status, timestamps. Use get_report_data to fetch actual report content. Supports date-based filtering with before_date.",
inputSchema: {
type: "object",
properties: {
project_id: { type: "number", description: "Filter by project ID" },
status: { type: "string", description: "Filter by status (e.g., 'completed')" },
limit: { type: "number", description: "Max number of reports to return (default: 20)" },
before_date: { type: "string", description: "Show reports created before this date (YYYY-MM-DD, DD.MM.YYYY, YYYY-MM-DD HH:mm, etc.)" },
all: { type: "boolean", description: "Fetch all reports (paginated automatically)" },
debug: { type: "boolean", description: "Enable verbose debug logs" },
},
additionalProperties: false,
},
},
{
name: "list_report_files",
description: "List files in a checkup report (metadata only, no content). Each report contains json (raw data) and md (markdown analysis) files per check. Either report_id or check_id must be provided.",
inputSchema: {
type: "object",
properties: {
report_id: { type: "number", description: "Checkup report ID (optional if check_id is provided)" },
type: { type: "string", description: "Filter by file type: 'json' or 'md'" },
check_id: { type: "string", description: "Filter by check ID (e.g., 'H002', 'F004')" },
debug: { type: "boolean", description: "Enable verbose debug logs" },
},
additionalProperties: false,
},
},
{
name: "get_report_data",
description: "Get checkup report file content. Returns files with a 'data' field containing the actual content: markdown analysis or JSON raw data. Use type='md' for human-readable analysis with recommendations, type='json' for raw check data. Either report_id or check_id must be provided.",
inputSchema: {
type: "object",
properties: {
report_id: { type: "number", description: "Checkup report ID (optional if check_id is provided)" },
type: { type: "string", description: "Filter by file type: 'json' for raw data, 'md' for markdown analysis" },
check_id: { type: "string", description: "Filter by check ID (e.g., 'H002', 'F004')" },
debug: { type: "boolean", description: "Enable verbose debug logs" },
},
additionalProperties: false,
},
},
],

@@ -449,0 +539,0 @@ };

+1
-1
{
"name": "postgresai",
"version": "0.15.0-dev.1",
"version": "0.15.0-dev.2",
"description": "postgres_ai CLI",

@@ -5,0 +5,0 @@ "license": "Apache-2.0",

@@ -1534,2 +1534,392 @@ import { describe, test, expect, mock, beforeEach, afterEach, spyOn } from "bun:test";

describe("list_reports tool", () => {
test("successfully returns reports list as JSON", async () => {
const mockReports = [
{ id: 1, org_id: 1, org_name: "TestOrg", project_id: 10, project_name: "prod-db", status: "completed" },
];
const readConfigSpy = spyOn(config, "readConfig").mockReturnValue({
apiKey: "test-key",
baseUrl: null,
orgId: null,
defaultProject: null,
projectName: null,
});
globalThis.fetch = mock(() =>
Promise.resolve(
new Response(JSON.stringify(mockReports), {
status: 200,
headers: { "Content-Type": "application/json" },
})
)
) as unknown as typeof fetch;
const response = await handleToolCall(createRequest("list_reports"));
expect(response.isError).toBeUndefined();
const parsed = JSON.parse(getResponseText(response));
expect(parsed).toHaveLength(1);
expect(parsed[0].status).toBe("completed");
readConfigSpy.mockRestore();
});
test("passes filters to API", async () => {
const readConfigSpy = spyOn(config, "readConfig").mockReturnValue({
apiKey: "test-key",
baseUrl: null,
orgId: null,
defaultProject: null,
projectName: null,
});
let capturedUrl: string | undefined;
globalThis.fetch = mock((url: string) => {
capturedUrl = url;
return Promise.resolve(
new Response(JSON.stringify([]), {
status: 200,
headers: { "Content-Type": "application/json" },
})
);
}) as unknown as typeof fetch;
await handleToolCall(createRequest("list_reports", {
project_id: 5,
status: "completed",
limit: 10,
}));
expect(capturedUrl).toContain("project_id=eq.5");
expect(capturedUrl).toContain("status=eq.completed");
expect(capturedUrl).toContain("limit=10");
readConfigSpy.mockRestore();
});
test("handles API errors gracefully", async () => {
const readConfigSpy = spyOn(config, "readConfig").mockReturnValue({
apiKey: "test-key",
baseUrl: null,
orgId: null,
defaultProject: null,
projectName: null,
});
globalThis.fetch = mock(() =>
Promise.resolve(
new Response('{"message": "Unauthorized"}', {
status: 401,
headers: { "Content-Type": "application/json" },
})
)
) as unknown as typeof fetch;
const response = await handleToolCall(createRequest("list_reports"));
expect(response.isError).toBe(true);
expect(getResponseText(response)).toContain("401");
readConfigSpy.mockRestore();
});
test("passes before_date to API as created_at filter", async () => {
const readConfigSpy = spyOn(config, "readConfig").mockReturnValue({
apiKey: "test-key",
baseUrl: null,
orgId: null,
defaultProject: null,
projectName: null,
});
let capturedUrl: string | undefined;
globalThis.fetch = mock((url: string) => {
capturedUrl = url;
return Promise.resolve(
new Response(JSON.stringify([]), {
status: 200,
headers: { "Content-Type": "application/json" },
})
);
}) as unknown as typeof fetch;
await handleToolCall(createRequest("list_reports", {
before_date: "2025-01-15",
}));
expect(capturedUrl).toContain("created_at=lt.2025-01-15");
readConfigSpy.mockRestore();
});
test("fetches all reports when all=true", async () => {
const readConfigSpy = spyOn(config, "readConfig").mockReturnValue({
apiKey: "test-key",
baseUrl: null,
orgId: null,
defaultProject: null,
projectName: null,
});
const mockReports = [
{ id: 10, org_id: 1, org_name: "O", project_id: 1, project_name: "P", status: "completed" },
];
globalThis.fetch = mock(() =>
Promise.resolve(
new Response(JSON.stringify(mockReports), {
status: 200,
headers: { "Content-Type": "application/json" },
})
)
) as unknown as typeof fetch;
const response = await handleToolCall(createRequest("list_reports", {
all: true,
}));
expect(response.isError).toBeUndefined();
const parsed = JSON.parse(getResponseText(response));
expect(parsed).toHaveLength(1);
expect(parsed[0].id).toBe(10);
readConfigSpy.mockRestore();
});
});
describe("list_report_files tool", () => {
test("returns error when neither report_id nor check_id is provided", async () => {
const readConfigSpy = spyOn(config, "readConfig").mockReturnValue({
apiKey: "test-key",
baseUrl: null,
orgId: null,
defaultProject: null,
projectName: null,
});
const response = await handleToolCall(createRequest("list_report_files", {}));
expect(response.isError).toBe(true);
expect(getResponseText(response)).toContain("Either report_id or check_id is required");
readConfigSpy.mockRestore();
});
test("works with only check_id (no report_id)", async () => {
const mockFiles = [
{ id: 100, checkup_report_id: 1, filename: "H002.md", check_id: "H002", type: "md" },
];
const readConfigSpy = spyOn(config, "readConfig").mockReturnValue({
apiKey: "test-key",
baseUrl: null,
orgId: null,
defaultProject: null,
projectName: null,
});
let capturedUrl: string | undefined;
globalThis.fetch = mock((url: string) => {
capturedUrl = url;
return Promise.resolve(
new Response(JSON.stringify(mockFiles), {
status: 200,
headers: { "Content-Type": "application/json" },
})
);
}) as unknown as typeof fetch;
const response = await handleToolCall(createRequest("list_report_files", {
check_id: "H002",
}));
expect(response.isError).toBeUndefined();
const parsed = JSON.parse(getResponseText(response));
expect(parsed[0].filename).toBe("H002.md");
expect(capturedUrl).toContain("check_id=eq.H002");
expect(capturedUrl).not.toContain("checkup_report_id");
readConfigSpy.mockRestore();
});
test("successfully returns report files", async () => {
const mockFiles = [
{ id: 100, checkup_report_id: 1, filename: "H002.md", check_id: "H002", type: "md" },
];
const readConfigSpy = spyOn(config, "readConfig").mockReturnValue({
apiKey: "test-key",
baseUrl: null,
orgId: null,
defaultProject: null,
projectName: null,
});
let capturedUrl: string | undefined;
globalThis.fetch = mock((url: string) => {
capturedUrl = url;
return Promise.resolve(
new Response(JSON.stringify(mockFiles), {
status: 200,
headers: { "Content-Type": "application/json" },
})
);
}) as unknown as typeof fetch;
const response = await handleToolCall(createRequest("list_report_files", {
report_id: 1,
type: "md",
check_id: "H002",
}));
expect(response.isError).toBeUndefined();
const parsed = JSON.parse(getResponseText(response));
expect(parsed[0].filename).toBe("H002.md");
expect(capturedUrl).toContain("checkup_report_id=eq.1");
expect(capturedUrl).toContain("type=eq.md");
expect(capturedUrl).toContain("check_id=eq.H002");
readConfigSpy.mockRestore();
});
});
describe("get_report_data tool", () => {
test("returns error when neither report_id nor check_id is provided", async () => {
const readConfigSpy = spyOn(config, "readConfig").mockReturnValue({
apiKey: "test-key",
baseUrl: null,
orgId: null,
defaultProject: null,
projectName: null,
});
const response = await handleToolCall(createRequest("get_report_data", {}));
expect(response.isError).toBe(true);
expect(getResponseText(response)).toContain("Either report_id or check_id is required");
readConfigSpy.mockRestore();
});
test("works with only check_id (no report_id)", async () => {
const mockData = [
{
id: 100,
checkup_report_id: 1,
filename: "H002.md",
check_id: "H002",
type: "md",
data: "# H002\n\nUnused indexes found.",
},
];
const readConfigSpy = spyOn(config, "readConfig").mockReturnValue({
apiKey: "test-key",
baseUrl: null,
orgId: null,
defaultProject: null,
projectName: null,
});
let capturedUrl: string | undefined;
globalThis.fetch = mock((url: string) => {
capturedUrl = url;
return Promise.resolve(
new Response(JSON.stringify(mockData), {
status: 200,
headers: { "Content-Type": "application/json" },
})
);
}) as unknown as typeof fetch;
const response = await handleToolCall(createRequest("get_report_data", {
check_id: "H002",
}));
expect(response.isError).toBeUndefined();
const parsed = JSON.parse(getResponseText(response));
expect(parsed[0].data).toContain("# H002");
expect(capturedUrl).toContain("check_id=eq.H002");
expect(capturedUrl).not.toContain("checkup_report_id");
readConfigSpy.mockRestore();
});
test("successfully returns report data with content", async () => {
const mockData = [
{
id: 100,
checkup_report_id: 1,
filename: "H002.md",
check_id: "H002",
type: "md",
data: "# H002\n\nUnused indexes found.",
},
];
const readConfigSpy = spyOn(config, "readConfig").mockReturnValue({
apiKey: "test-key",
baseUrl: null,
orgId: null,
defaultProject: null,
projectName: null,
});
globalThis.fetch = mock(() =>
Promise.resolve(
new Response(JSON.stringify(mockData), {
status: 200,
headers: { "Content-Type": "application/json" },
})
)
) as unknown as typeof fetch;
const response = await handleToolCall(createRequest("get_report_data", {
report_id: 1,
type: "md",
check_id: "H002",
}));
expect(response.isError).toBeUndefined();
const parsed = JSON.parse(getResponseText(response));
expect(parsed[0].data).toContain("# H002");
readConfigSpy.mockRestore();
});
test("passes filters to API", async () => {
const readConfigSpy = spyOn(config, "readConfig").mockReturnValue({
apiKey: "test-key",
baseUrl: null,
orgId: null,
defaultProject: null,
projectName: null,
});
let capturedUrl: string | undefined;
globalThis.fetch = mock((url: string) => {
capturedUrl = url;
return Promise.resolve(
new Response(JSON.stringify([]), {
status: 200,
headers: { "Content-Type": "application/json" },
})
);
}) as unknown as typeof fetch;
await handleToolCall(createRequest("get_report_data", {
report_id: 42,
type: "json",
check_id: "F004",
}));
expect(capturedUrl).toContain("checkup_report_id=eq.42");
expect(capturedUrl).toContain("type=eq.json");
expect(capturedUrl).toContain("check_id=eq.F004");
readConfigSpy.mockRestore();
});
});
describe("error propagation", () => {

@@ -1536,0 +1926,0 @@ test("propagates API errors through MCP layer", async () => {

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display