postgresai 0.15.0-dev.6 → 0.15.0-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/test/init.test.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { describe, test, expect, beforeAll, afterAll } from "bun:test";
2
- import { resolve } from "path";
2
+ import path, { resolve } from "path";
3
3
  import * as fs from "fs";
4
4
  import * as os from "os";
5
5
 
@@ -1073,9 +1073,64 @@ describe("CLI commands", () => {
1073
1073
  test("cli: mon local-install --demo configures demo monitoring target", () => {
1074
1074
  // --demo should copy instances.demo.yml to instances.yml and print confirmation.
1075
1075
  // The command will fail later (no Docker), but we verify the demo target step succeeded.
1076
- const r = runCli(["mon", "local-install", "--demo"]);
1077
- expect(r.stdout).toMatch(/Demo mode enabled/);
1078
- expect(r.stdout).toMatch(/Demo monitoring target configured/);
1076
+ // resolvePaths() walks cwd() up to find docker-compose.yml, so instances.yml
1077
+ // is written next to docker-compose.yml in the repo root.
1078
+ const repoRoot = resolve(import.meta.dir, "..", "..");
1079
+ const instancesPath = path.join(repoRoot, "instances.yml");
1080
+ // Remove instances.yml if it exists — use rmSync to handle both files and
1081
+ // directories (the EISDIR test may have left a directory here if it failed).
1082
+ if (fs.existsSync(instancesPath)) fs.rmSync(instancesPath, { recursive: true, force: true });
1083
+ try {
1084
+ const r = runCli(["mon", "local-install", "--demo"]);
1085
+ expect(r.stdout).toMatch(/Demo mode enabled/);
1086
+ expect(r.stdout).toMatch(/Demo monitoring target configured/);
1087
+ // Verify instances.yml was actually written with the demo target
1088
+ expect(fs.existsSync(instancesPath)).toBe(true);
1089
+ const content = fs.readFileSync(instancesPath, "utf8");
1090
+ expect(content).toContain("name: target_database");
1091
+ expect(content).toContain("conn_str: postgresql://monitor:monitor_pass@target-db:5432/target_database");
1092
+ } finally {
1093
+ // Clean up — instances.yml is gitignored so safe to remove
1094
+ if (fs.existsSync(instancesPath)) fs.rmSync(instancesPath, { recursive: true, force: true });
1095
+ }
1096
+ });
1097
+
1098
+ test("cli: mon local-install --demo exits with code 1 when instances.demo.yml is missing", () => {
1099
+ // Regression: if instances.demo.yml cannot be found in any candidate path, the CLI
1100
+ // must exit with a non-zero code and a descriptive error (not silently create empty dashboards).
1101
+ const repoRoot = resolve(import.meta.dir, "..", "..");
1102
+ const demoFile = path.join(repoRoot, "instances.demo.yml");
1103
+ const tempBackup = path.join(os.tmpdir(), `instances.demo.yml.test-backup-${Date.now()}`);
1104
+ // Temporarily move instances.demo.yml so neither candidate path resolves
1105
+ fs.copyFileSync(demoFile, tempBackup);
1106
+ fs.unlinkSync(demoFile);
1107
+ try {
1108
+ const r = runCli(["mon", "local-install", "--demo"]);
1109
+ expect(r.status).not.toBe(0);
1110
+ expect(r.stderr).toContain("instances.demo.yml not found");
1111
+ } finally {
1112
+ // Restore the file — critical to do before any assertion can throw
1113
+ if (!fs.existsSync(demoFile)) fs.copyFileSync(tempBackup, demoFile);
1114
+ fs.rmSync(tempBackup, { force: true });
1115
+ }
1116
+ });
1117
+
1118
+ test("cli: mon local-install --demo with EISDIR recovers instances.yml", () => {
1119
+ // Docker bind-mounts create missing paths as directories; the CLI must handle this.
1120
+ const repoRoot = resolve(import.meta.dir, "..", "..");
1121
+ const instancesPath = path.join(repoRoot, "instances.yml");
1122
+ // Create instances.yml as a directory (simulating Docker artifact)
1123
+ if (fs.existsSync(instancesPath)) fs.rmSync(instancesPath, { recursive: true, force: true });
1124
+ fs.mkdirSync(instancesPath);
1125
+ try {
1126
+ const r = runCli(["mon", "local-install", "--demo"]);
1127
+ expect(r.stdout).toMatch(/Demo monitoring target configured/);
1128
+ expect(fs.statSync(instancesPath).isFile()).toBe(true);
1129
+ const content = fs.readFileSync(instancesPath, "utf8");
1130
+ expect(content).toContain("name: target_database");
1131
+ } finally {
1132
+ if (fs.existsSync(instancesPath)) fs.rmSync(instancesPath, { recursive: true, force: true });
1133
+ }
1079
1134
  });
1080
1135
 
1081
1136
  test("cli: mon local-install --demo with global --api-key shows error", () => {
@@ -1233,12 +1288,12 @@ describe.skipIf(!dockerAvailable)("imageTag priority behavior", () => {
1233
1288
  expect(envContent).toMatch(/PGAI_TAG=\d+\.\d+\.\d+|PGAI_TAG=0\.0\.0-dev/);
1234
1289
  }, 60000);
1235
1290
 
1236
- test("existing registry and password are preserved while tag is updated", () => {
1291
+ test("existing registry and passwords are preserved while tag is updated", () => {
1237
1292
  const testDir = resolve(tempDir, "preserve-test");
1238
1293
  fs.mkdirSync(testDir, { recursive: true });
1239
- // Create .env with stale tag but valid registry and password
1294
+ // Create .env with stale tag but valid registry and passwords
1240
1295
  fs.writeFileSync(resolve(testDir, ".env"),
1241
- "PGAI_TAG=stale-tag\nPGAI_REGISTRY=my.registry.com\nGF_SECURITY_ADMIN_PASSWORD=secret123\n");
1296
+ "PGAI_TAG=stale-tag\nPGAI_REGISTRY=my.registry.com\nGF_SECURITY_ADMIN_PASSWORD=secret123\nREPLICATOR_PASSWORD=repl-secret\nVM_AUTH_USERNAME=existing-vm-user\nVM_AUTH_PASSWORD=existing-vm-pass\n");
1242
1297
  fs.writeFileSync(resolve(testDir, "docker-compose.yml"), "version: '3'\nservices: {}\n");
1243
1298
 
1244
1299
  const cliPath = resolve(import.meta.dir, "..", "bin", "postgres-ai.ts");
@@ -1254,8 +1309,473 @@ describe.skipIf(!dockerAvailable)("imageTag priority behavior", () => {
1254
1309
  // Tag should be updated (not stale-tag)
1255
1310
  expect(envContent).not.toMatch(/PGAI_TAG=stale-tag/);
1256
1311
 
1257
- // But registry and password should be preserved
1312
+ // But registry and passwords should be preserved
1258
1313
  expect(envContent).toMatch(/PGAI_REGISTRY=my\.registry\.com/);
1259
1314
  expect(envContent).toMatch(/GF_SECURITY_ADMIN_PASSWORD=secret123/);
1315
+ expect(envContent).toMatch(/REPLICATOR_PASSWORD=repl-secret/);
1316
+ expect(envContent).toMatch(/VM_AUTH_USERNAME=existing-vm-user/);
1317
+ expect(envContent).toMatch(/VM_AUTH_PASSWORD=existing-vm-pass/);
1260
1318
  }, 60000);
1261
1319
  });
1320
+
1321
+ // ---------------------------------------------------------------------------
1322
+ // connectWithSslFallback — connectionTimeoutMillis and statement_timeout
1323
+ // Issues 9 & 10
1324
+ // ---------------------------------------------------------------------------
1325
+ describe("connectWithSslFallback", () => {
1326
+ // Issue 9: Verify that connectionTimeoutMillis is forwarded to the pg Client
1327
+ // constructor so slow-responding servers don't hang the CLI indefinitely.
1328
+ // Direct integration testing against a real TCP timeout would be flaky in CI,
1329
+ // so we use a mock ClientClass and assert the config passed to its constructor.
1330
+ test("passes connectionTimeoutMillis: 10_000 to the pg Client constructor", async () => {
1331
+ const receivedConfigs: any[] = [];
1332
+
1333
+ class MockClient {
1334
+ constructor(config: any) {
1335
+ receivedConfigs.push(config);
1336
+ }
1337
+ async connect() {}
1338
+ async query() { return {}; }
1339
+ }
1340
+
1341
+ const adminConn = init.resolveAdminConnection({ conn: "postgresql://u:p@localhost:5432/d" });
1342
+ // Disable SSL fallback so we exercise the simple (non-retry) path.
1343
+ (adminConn as any).sslFallbackEnabled = false;
1344
+
1345
+ await init.connectWithSslFallback(MockClient as any, adminConn);
1346
+
1347
+ expect(receivedConfigs.length).toBeGreaterThanOrEqual(1);
1348
+ expect(receivedConfigs[0].connectionTimeoutMillis).toBe(10_000);
1349
+ });
1350
+
1351
+ // Issue 10: Verify that SET statement_timeout is issued after every successful
1352
+ // connection to prevent runaway queries from blocking the CLI.
1353
+ test("issues SET statement_timeout = '30s' after connecting", async () => {
1354
+ const queriesSent: string[] = [];
1355
+
1356
+ class MockClient {
1357
+ constructor(_config: any) {}
1358
+ async connect() {}
1359
+ async query(sql: string) {
1360
+ queriesSent.push(sql);
1361
+ return {};
1362
+ }
1363
+ }
1364
+
1365
+ const adminConn = init.resolveAdminConnection({ conn: "postgresql://u:p@localhost:5432/d" });
1366
+ (adminConn as any).sslFallbackEnabled = false;
1367
+
1368
+ await init.connectWithSslFallback(MockClient as any, adminConn);
1369
+
1370
+ expect(queriesSent.some((q) => /SET\s+statement_timeout/i.test(q))).toBe(true);
1371
+ });
1372
+ });
1373
+
1374
+ describe("checkCurrentUserPermissions", () => {
1375
+ function makeMockClient(rows: init.PermissionCheckRow[]) {
1376
+ return {
1377
+ query: async () => ({ rows }),
1378
+ };
1379
+ }
1380
+
1381
+ function makeFailingClient(error: Error) {
1382
+ return {
1383
+ query: async () => { throw error; },
1384
+ };
1385
+ }
1386
+
1387
+ test("returns ok when all required permissions are granted", async () => {
1388
+ const rows: init.PermissionCheckRow[] = [
1389
+ { permission_name: "connect on database postgres", status: "required", granted: true, fix_command: null },
1390
+ { permission_name: "pg_monitor role membership", status: "required", granted: true, fix_command: null },
1391
+ { permission_name: "select on pg_catalog.pg_index", status: "required", granted: true, fix_command: null },
1392
+ { permission_name: "postgres_ai.pg_statistic view exists", status: "optional", granted: true, fix_command: null },
1393
+ { permission_name: "select on postgres_ai.pg_statistic", status: "optional", granted: true, fix_command: null },
1394
+ ];
1395
+
1396
+ const result = await init.checkCurrentUserPermissions(makeMockClient(rows) as any);
1397
+ expect(result.ok).toBe(true);
1398
+ expect(result.missingRequired).toHaveLength(0);
1399
+ expect(result.missingOptional).toHaveLength(0);
1400
+ });
1401
+
1402
+ test("reports missing required permissions with fix commands", async () => {
1403
+ const rows: init.PermissionCheckRow[] = [
1404
+ { permission_name: "connect on database postgres", status: "required", granted: true, fix_command: null },
1405
+ { permission_name: "pg_monitor role membership", status: "required", granted: false, fix_command: "grant pg_monitor to testuser;" },
1406
+ { permission_name: "select on pg_catalog.pg_index", status: "required", granted: true, fix_command: null },
1407
+ { permission_name: "postgres_ai.pg_statistic view exists", status: "optional", granted: true, fix_command: null },
1408
+ { permission_name: "select on postgres_ai.pg_statistic", status: "optional", granted: true, fix_command: null },
1409
+ ];
1410
+
1411
+ const result = await init.checkCurrentUserPermissions(makeMockClient(rows) as any);
1412
+ expect(result.ok).toBe(false);
1413
+ expect(result.missingRequired).toHaveLength(1);
1414
+ expect(result.missingRequired[0].permission_name).toBe("pg_monitor role membership");
1415
+ expect(result.missingRequired[0].fix_command).toBe("grant pg_monitor to testuser;");
1416
+ });
1417
+
1418
+ test("reports missing optional permissions without failing", async () => {
1419
+ const rows: init.PermissionCheckRow[] = [
1420
+ { permission_name: "connect on database postgres", status: "required", granted: true, fix_command: null },
1421
+ { permission_name: "pg_monitor role membership", status: "required", granted: true, fix_command: null },
1422
+ { permission_name: "select on pg_catalog.pg_index", status: "required", granted: true, fix_command: null },
1423
+ { permission_name: "postgres_ai.pg_statistic view exists", status: "optional", granted: false, fix_command: "-- create postgres_ai.pg_statistic view (see setup script)" },
1424
+ { permission_name: "select on postgres_ai.pg_statistic", status: "optional", granted: false, fix_command: "grant select on postgres_ai.pg_statistic to testuser;" },
1425
+ ];
1426
+
1427
+ const result = await init.checkCurrentUserPermissions(makeMockClient(rows) as any);
1428
+ expect(result.ok).toBe(true);
1429
+ expect(result.missingRequired).toHaveLength(0);
1430
+ expect(result.missingOptional).toHaveLength(2);
1431
+ expect(result.missingOptional[0].permission_name).toBe("postgres_ai.pg_statistic view exists");
1432
+ });
1433
+
1434
+ test("reports multiple missing required permissions", async () => {
1435
+ const rows: init.PermissionCheckRow[] = [
1436
+ { permission_name: "connect on database postgres", status: "required", granted: false, fix_command: "grant connect on database postgres to testuser;" },
1437
+ { permission_name: "pg_monitor role membership", status: "required", granted: false, fix_command: "grant pg_monitor to testuser;" },
1438
+ { permission_name: "select on pg_catalog.pg_index", status: "required", granted: false, fix_command: "grant select on pg_catalog.pg_index to testuser;" },
1439
+ { permission_name: "postgres_ai.pg_statistic view exists", status: "optional", granted: false, fix_command: "-- create postgres_ai.pg_statistic view (see setup script)" },
1440
+ { permission_name: "select on postgres_ai.pg_statistic", status: "optional", granted: null, fix_command: null },
1441
+ ];
1442
+
1443
+ const result = await init.checkCurrentUserPermissions(makeMockClient(rows) as any);
1444
+ expect(result.ok).toBe(false);
1445
+ expect(result.missingRequired).toHaveLength(3);
1446
+ expect(result.missingOptional).toHaveLength(1);
1447
+ // null granted for optional (view doesn't exist) should NOT count as missing optional
1448
+ expect(result.rows[4].granted).toBeNull();
1449
+ });
1450
+
1451
+ test("treats null granted as missing for required permissions (fail-safe)", async () => {
1452
+ const rows: init.PermissionCheckRow[] = [
1453
+ { permission_name: "connect on database postgres", status: "required", granted: null, fix_command: null },
1454
+ { permission_name: "pg_monitor role membership", status: "required", granted: true, fix_command: null },
1455
+ { permission_name: "select on pg_catalog.pg_index", status: "required", granted: true, fix_command: null },
1456
+ { permission_name: "postgres_ai.pg_statistic view exists", status: "optional", granted: true, fix_command: null },
1457
+ { permission_name: "select on postgres_ai.pg_statistic", status: "optional", granted: true, fix_command: null },
1458
+ ];
1459
+
1460
+ const result = await init.checkCurrentUserPermissions(makeMockClient(rows) as any);
1461
+ // null on a required check should be treated as not-granted
1462
+ expect(result.ok).toBe(false);
1463
+ expect(result.missingRequired).toHaveLength(1);
1464
+ expect(result.missingRequired[0].permission_name).toBe("connect on database postgres");
1465
+ });
1466
+
1467
+ test("null granted on optional permission is not treated as missing", async () => {
1468
+ const rows: init.PermissionCheckRow[] = [
1469
+ { permission_name: "connect on database postgres", status: "required", granted: true, fix_command: null },
1470
+ { permission_name: "pg_monitor role membership", status: "required", granted: true, fix_command: null },
1471
+ { permission_name: "select on pg_catalog.pg_index", status: "required", granted: true, fix_command: null },
1472
+ { permission_name: "postgres_ai.pg_statistic view exists", status: "optional", granted: false, fix_command: "-- create postgres_ai.pg_statistic view (see setup script)" },
1473
+ { permission_name: "select on postgres_ai.pg_statistic", status: "optional", granted: null, fix_command: null },
1474
+ ];
1475
+
1476
+ const result = await init.checkCurrentUserPermissions(makeMockClient(rows) as any);
1477
+ expect(result.ok).toBe(true);
1478
+ // null granted on optional should NOT be treated as missing — check was skipped
1479
+ expect(result.missingOptional).toHaveLength(1);
1480
+ expect(result.missingOptional[0].permission_name).toBe("postgres_ai.pg_statistic view exists");
1481
+ });
1482
+
1483
+ test("propagates query errors to caller", async () => {
1484
+ const dbError = new Error("permission denied for relation pg_roles");
1485
+ const client = makeFailingClient(dbError);
1486
+
1487
+ await expect(
1488
+ init.checkCurrentUserPermissions(client as any)
1489
+ ).rejects.toThrow("permission denied for relation pg_roles");
1490
+ });
1491
+
1492
+ test("returns all rows for inspection", async () => {
1493
+ const rows: init.PermissionCheckRow[] = [
1494
+ { permission_name: "connect on database postgres", status: "required", granted: true, fix_command: null },
1495
+ { permission_name: "pg_monitor role membership", status: "required", granted: true, fix_command: null },
1496
+ { permission_name: "select on pg_catalog.pg_index", status: "required", granted: true, fix_command: null },
1497
+ { permission_name: "postgres_ai.pg_statistic view exists", status: "optional", granted: true, fix_command: null },
1498
+ { permission_name: "select on postgres_ai.pg_statistic", status: "optional", granted: true, fix_command: null },
1499
+ ];
1500
+
1501
+ const result = await init.checkCurrentUserPermissions(makeMockClient(rows) as any);
1502
+ expect(result.rows).toHaveLength(5);
1503
+ expect(result.rows).toEqual(rows);
1504
+ });
1505
+
1506
+ test("handles empty rows (no permission checks returned)", async () => {
1507
+ const result = await init.checkCurrentUserPermissions(makeMockClient([]) as any);
1508
+ expect(result.ok).toBe(true);
1509
+ expect(result.rows).toHaveLength(0);
1510
+ expect(result.missingRequired).toHaveLength(0);
1511
+ expect(result.missingOptional).toHaveLength(0);
1512
+ });
1513
+ });
1514
+
1515
+ describe("formatPermissionCheckMessages", () => {
1516
+ test("returns no warnings or errors when all permissions granted", () => {
1517
+ const result: init.PreflightPermissionResult = {
1518
+ ok: true,
1519
+ rows: [],
1520
+ missingRequired: [],
1521
+ missingOptional: [],
1522
+ };
1523
+
1524
+ const messages = init.formatPermissionCheckMessages(result);
1525
+ expect(messages.failed).toBe(false);
1526
+ expect(messages.warnings).toHaveLength(0);
1527
+ expect(messages.errors).toHaveLength(0);
1528
+ });
1529
+
1530
+ test("returns warnings for missing optional permissions", () => {
1531
+ const result: init.PreflightPermissionResult = {
1532
+ ok: true,
1533
+ rows: [],
1534
+ missingRequired: [],
1535
+ missingOptional: [
1536
+ { permission_name: "postgres_ai.pg_statistic view exists", status: "optional", granted: false, fix_command: "-- create view" },
1537
+ ],
1538
+ };
1539
+
1540
+ const messages = init.formatPermissionCheckMessages(result);
1541
+ expect(messages.failed).toBe(false);
1542
+ expect(messages.warnings).toHaveLength(1);
1543
+ expect(messages.warnings[0]).toContain("postgres_ai.pg_statistic view exists");
1544
+ expect(messages.warnings[0]).toContain("Fix: -- create view");
1545
+ expect(messages.errors).toHaveLength(0);
1546
+ });
1547
+
1548
+ test("returns errors with fix commands for missing required permissions", () => {
1549
+ const result: init.PreflightPermissionResult = {
1550
+ ok: false,
1551
+ rows: [],
1552
+ missingRequired: [
1553
+ { permission_name: "pg_monitor role membership", status: "required", granted: false, fix_command: "grant pg_monitor to testuser;" },
1554
+ ],
1555
+ missingOptional: [],
1556
+ };
1557
+
1558
+ const messages = init.formatPermissionCheckMessages(result);
1559
+ expect(messages.failed).toBe(true);
1560
+ expect(messages.errors.some((e) => e.includes("pg_monitor role membership"))).toBe(true);
1561
+ expect(messages.errors.some((e) => e.includes("grant pg_monitor to testuser;"))).toBe(true);
1562
+ expect(messages.errors.some((e) => e.includes("postgresai prepare-db"))).toBe(true);
1563
+ });
1564
+
1565
+ test("omits fix section when all fix_commands are null", () => {
1566
+ const result: init.PreflightPermissionResult = {
1567
+ ok: false,
1568
+ rows: [],
1569
+ missingRequired: [
1570
+ { permission_name: "pg_monitor role membership", status: "required", granted: null, fix_command: null },
1571
+ ],
1572
+ missingOptional: [],
1573
+ };
1574
+
1575
+ const messages = init.formatPermissionCheckMessages(result);
1576
+ expect(messages.failed).toBe(true);
1577
+ expect(messages.errors.some((e) => e.includes("pg_monitor role membership"))).toBe(true);
1578
+ // Should NOT have "To fix" section when no fix commands
1579
+ expect(messages.errors.some((e) => e.includes("To fix"))).toBe(false);
1580
+ // Should still suggest prepare-db
1581
+ expect(messages.errors.some((e) => e.includes("postgresai prepare-db"))).toBe(true);
1582
+ });
1583
+
1584
+ test("includes both warnings and errors when both are present", () => {
1585
+ const result: init.PreflightPermissionResult = {
1586
+ ok: false,
1587
+ rows: [],
1588
+ missingRequired: [
1589
+ { permission_name: "pg_monitor role membership", status: "required", granted: false, fix_command: "grant pg_monitor to testuser;" },
1590
+ ],
1591
+ missingOptional: [
1592
+ { permission_name: "postgres_ai.pg_statistic view exists", status: "optional", granted: false, fix_command: null },
1593
+ ],
1594
+ };
1595
+
1596
+ const messages = init.formatPermissionCheckMessages(result);
1597
+ expect(messages.failed).toBe(true);
1598
+ expect(messages.warnings).toHaveLength(1);
1599
+ expect(messages.errors.length).toBeGreaterThan(0);
1600
+ });
1601
+
1602
+ test("warning without fix_command omits Fix: suffix", () => {
1603
+ const result: init.PreflightPermissionResult = {
1604
+ ok: true,
1605
+ rows: [],
1606
+ missingRequired: [],
1607
+ missingOptional: [
1608
+ { permission_name: "some optional check", status: "optional", granted: false, fix_command: null },
1609
+ ],
1610
+ };
1611
+
1612
+ const messages = init.formatPermissionCheckMessages(result);
1613
+ expect(messages.warnings[0]).not.toContain("Fix:");
1614
+ expect(messages.warnings[0]).toContain("some optional check");
1615
+ });
1616
+ });
1617
+
1618
+ describe("Permission check integration (checkup command)", () => {
1619
+ /**
1620
+ * Integration tests for the permission check flow in the checkup command.
1621
+ * These tests verify that the permission check integration in postgres-ai.ts
1622
+ * correctly handles different permission scenarios:
1623
+ * - Successful checks allow execution to proceed
1624
+ * - Missing required permissions halt execution with exitCode=1
1625
+ * - Missing optional permissions show warnings but allow execution to continue
1626
+ */
1627
+
1628
+ function makeMockClientForIntegration(permissionRows: init.PermissionCheckRow[], reportResult?: any) {
1629
+ const queriesExecuted: string[] = [];
1630
+ return {
1631
+ client: {
1632
+ query: async (sql: string) => {
1633
+ queriesExecuted.push(sql);
1634
+ // Return permission check results for the permission check query
1635
+ if (sql.includes("permission_checks")) {
1636
+ return { rows: permissionRows };
1637
+ }
1638
+ // Return empty result for other queries (like report generation)
1639
+ return reportResult || { rows: [] };
1640
+ },
1641
+ end: async () => {},
1642
+ },
1643
+ queriesExecuted,
1644
+ };
1645
+ }
1646
+
1647
+ test("successful permission check allows execution to proceed", async () => {
1648
+ const rows: init.PermissionCheckRow[] = [
1649
+ { permission_name: "connect on database postgres", status: "required", granted: true, fix_command: null },
1650
+ { permission_name: "pg_monitor role membership", status: "required", granted: true, fix_command: null },
1651
+ { permission_name: "select on pg_catalog.pg_index", status: "required", granted: true, fix_command: null },
1652
+ { permission_name: "postgres_ai.pg_statistic view exists", status: "optional", granted: true, fix_command: null },
1653
+ { permission_name: "select on postgres_ai.pg_statistic", status: "optional", granted: true, fix_command: null },
1654
+ ];
1655
+
1656
+ const mockClient = makeMockClientForIntegration(rows);
1657
+ const permCheck = await init.checkCurrentUserPermissions(mockClient.client as any);
1658
+ const permMessages = init.formatPermissionCheckMessages(permCheck);
1659
+
1660
+ // Verify permission check passed
1661
+ expect(permMessages.failed).toBe(false);
1662
+ expect(permMessages.warnings).toHaveLength(0);
1663
+ expect(permMessages.errors).toHaveLength(0);
1664
+
1665
+ // In the actual integration, process.exitCode would not be set and execution continues
1666
+ // This simulates the successful path where reports would be generated
1667
+ });
1668
+
1669
+ test("missing required permissions halt execution with clear error messages", async () => {
1670
+ const rows: init.PermissionCheckRow[] = [
1671
+ { permission_name: "connect on database postgres", status: "required", granted: true, fix_command: null },
1672
+ { permission_name: "pg_monitor role membership", status: "required", granted: false, fix_command: "grant pg_monitor to testuser;" },
1673
+ { permission_name: "select on pg_catalog.pg_index", status: "required", granted: false, fix_command: "grant select on pg_catalog.pg_index to testuser;" },
1674
+ { permission_name: "postgres_ai.pg_statistic view exists", status: "optional", granted: true, fix_command: null },
1675
+ { permission_name: "select on postgres_ai.pg_statistic", status: "optional", granted: true, fix_command: null },
1676
+ ];
1677
+
1678
+ const mockClient = makeMockClientForIntegration(rows);
1679
+ const permCheck = await init.checkCurrentUserPermissions(mockClient.client as any);
1680
+ const permMessages = init.formatPermissionCheckMessages(permCheck);
1681
+
1682
+ // Verify permission check failed
1683
+ expect(permMessages.failed).toBe(true);
1684
+ expect(permMessages.errors.length).toBeGreaterThan(0);
1685
+
1686
+ // Verify error messages include the missing permissions
1687
+ const errorText = permMessages.errors.join("\n");
1688
+ expect(errorText).toContain("pg_monitor role membership");
1689
+ expect(errorText).toContain("select on pg_catalog.pg_index");
1690
+
1691
+ // Verify fix commands are included
1692
+ expect(errorText).toContain("grant pg_monitor to testuser;");
1693
+ expect(errorText).toContain("grant select on pg_catalog.pg_index to testuser;");
1694
+
1695
+ // Verify alternative fix suggestion
1696
+ expect(errorText).toContain("postgresai prepare-db");
1697
+
1698
+ // In the actual integration, process.exitCode would be set to 1 and execution would halt
1699
+ });
1700
+
1701
+ test("missing optional permissions show warnings but allow execution to proceed", async () => {
1702
+ const rows: init.PermissionCheckRow[] = [
1703
+ { permission_name: "connect on database postgres", status: "required", granted: true, fix_command: null },
1704
+ { permission_name: "pg_monitor role membership", status: "required", granted: true, fix_command: null },
1705
+ { permission_name: "select on pg_catalog.pg_index", status: "required", granted: true, fix_command: null },
1706
+ { permission_name: "postgres_ai.pg_statistic view exists", status: "optional", granted: false, fix_command: "-- create postgres_ai.pg_statistic view (see setup script)" },
1707
+ { permission_name: "select on postgres_ai.pg_statistic", status: "optional", granted: null, fix_command: null },
1708
+ ];
1709
+
1710
+ const mockClient = makeMockClientForIntegration(rows);
1711
+ const permCheck = await init.checkCurrentUserPermissions(mockClient.client as any);
1712
+ const permMessages = init.formatPermissionCheckMessages(permCheck);
1713
+
1714
+ // Verify permission check passed (required permissions OK)
1715
+ expect(permMessages.failed).toBe(false);
1716
+
1717
+ // Verify warnings are present for optional permissions
1718
+ expect(permMessages.warnings).toHaveLength(1);
1719
+ expect(permMessages.warnings[0]).toContain("postgres_ai.pg_statistic view exists");
1720
+ expect(permMessages.warnings[0]).toContain("Fix: -- create postgres_ai.pg_statistic view");
1721
+
1722
+ // Verify no errors (only warnings)
1723
+ expect(permMessages.errors).toHaveLength(0);
1724
+
1725
+ // In the actual integration, warnings would be printed to stderr but execution continues
1726
+ });
1727
+
1728
+ test("permission check integration handles multiple missing required permissions", async () => {
1729
+ const rows: init.PermissionCheckRow[] = [
1730
+ { permission_name: "connect on database postgres", status: "required", granted: false, fix_command: "grant connect on database postgres to testuser;" },
1731
+ { permission_name: "pg_monitor role membership", status: "required", granted: false, fix_command: "grant pg_monitor to testuser;" },
1732
+ { permission_name: "select on pg_catalog.pg_index", status: "required", granted: false, fix_command: "grant select on pg_catalog.pg_index to testuser;" },
1733
+ { permission_name: "postgres_ai.pg_statistic view exists", status: "optional", granted: false, fix_command: "-- create postgres_ai.pg_statistic view (see setup script)" },
1734
+ { permission_name: "select on postgres_ai.pg_statistic", status: "optional", granted: null, fix_command: null },
1735
+ ];
1736
+
1737
+ const mockClient = makeMockClientForIntegration(rows);
1738
+ const permCheck = await init.checkCurrentUserPermissions(mockClient.client as any);
1739
+ const permMessages = init.formatPermissionCheckMessages(permCheck);
1740
+
1741
+ // Verify permission check failed
1742
+ expect(permMessages.failed).toBe(true);
1743
+
1744
+ // Verify all missing required permissions are reported
1745
+ const errorText = permMessages.errors.join("\n");
1746
+ expect(errorText).toContain("connect on database postgres");
1747
+ expect(errorText).toContain("pg_monitor role membership");
1748
+ expect(errorText).toContain("select on pg_catalog.pg_index");
1749
+
1750
+ // Verify all fix commands are included
1751
+ expect(errorText).toContain("grant connect on database postgres to testuser;");
1752
+ expect(errorText).toContain("grant pg_monitor to testuser;");
1753
+ expect(errorText).toContain("grant select on pg_catalog.pg_index to testuser;");
1754
+
1755
+ // Verify warning for optional permission
1756
+ expect(permMessages.warnings).toHaveLength(1);
1757
+ expect(permMessages.warnings[0]).toContain("postgres_ai.pg_statistic view exists");
1758
+ });
1759
+
1760
+ test("permission check integration handles null granted values correctly", async () => {
1761
+ const rows: init.PermissionCheckRow[] = [
1762
+ { permission_name: "connect on database postgres", status: "required", granted: null, fix_command: null },
1763
+ { permission_name: "pg_monitor role membership", status: "required", granted: true, fix_command: null },
1764
+ { permission_name: "select on pg_catalog.pg_index", status: "required", granted: true, fix_command: null },
1765
+ { permission_name: "postgres_ai.pg_statistic view exists", status: "optional", granted: null, fix_command: null },
1766
+ { permission_name: "select on postgres_ai.pg_statistic", status: "optional", granted: null, fix_command: null },
1767
+ ];
1768
+
1769
+ const mockClient = makeMockClientForIntegration(rows);
1770
+ const permCheck = await init.checkCurrentUserPermissions(mockClient.client as any);
1771
+ const permMessages = init.formatPermissionCheckMessages(permCheck);
1772
+
1773
+ // Verify null on required permission is treated as failure (fail-safe)
1774
+ expect(permMessages.failed).toBe(true);
1775
+ const errorText = permMessages.errors.join("\n");
1776
+ expect(errorText).toContain("connect on database postgres");
1777
+
1778
+ // Verify null on optional permissions does not generate warnings (skipped checks)
1779
+ expect(permMessages.warnings).toHaveLength(0);
1780
+ });
1781
+ });
@@ -286,13 +286,13 @@ describe("demo mode instances.demo.yml", () => {
286
286
  expect(content).toMatch(/^\s+conn_str:/m);
287
287
  expect(content).toMatch(/^\s+preset_metrics: full/m);
288
288
  expect(content).toMatch(/^\s+is_enabled: true/m);
289
- // ~sink_type~ is a placeholder replaced per-sink by generate-pgwatch-sources.sh
289
+ // ~sink_type~ is a sed token substituted by generate-pgwatch-sources.sh; values: postgres, prometheus
290
290
  expect(content).toMatch(/^\s+sink_type: ~sink_type~/m);
291
291
  });
292
292
 
293
293
  test("instances.yml is gitignored (not tracked)", () => {
294
294
  const gitignore = fs.readFileSync(path.join(repoRoot, ".gitignore"), "utf8");
295
- expect(gitignore).toContain("instances.yml");
295
+ expect(gitignore).toMatch(/^instances\.yml$/m);
296
296
  });
297
297
 
298
298
  test("demo config can be copied to instances.yml in temp dir", () => {