postgresai 0.15.0-dev.7 → 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
@@ -1288,12 +1288,12 @@ describe.skipIf(!dockerAvailable)("imageTag priority behavior", () => {
1288
1288
  expect(envContent).toMatch(/PGAI_TAG=\d+\.\d+\.\d+|PGAI_TAG=0\.0\.0-dev/);
1289
1289
  }, 60000);
1290
1290
 
1291
- test("existing registry and password are preserved while tag is updated", () => {
1291
+ test("existing registry and passwords are preserved while tag is updated", () => {
1292
1292
  const testDir = resolve(tempDir, "preserve-test");
1293
1293
  fs.mkdirSync(testDir, { recursive: true });
1294
- // Create .env with stale tag but valid registry and password
1294
+ // Create .env with stale tag but valid registry and passwords
1295
1295
  fs.writeFileSync(resolve(testDir, ".env"),
1296
- "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");
1297
1297
  fs.writeFileSync(resolve(testDir, "docker-compose.yml"), "version: '3'\nservices: {}\n");
1298
1298
 
1299
1299
  const cliPath = resolve(import.meta.dir, "..", "bin", "postgres-ai.ts");
@@ -1309,8 +1309,473 @@ describe.skipIf(!dockerAvailable)("imageTag priority behavior", () => {
1309
1309
  // Tag should be updated (not stale-tag)
1310
1310
  expect(envContent).not.toMatch(/PGAI_TAG=stale-tag/);
1311
1311
 
1312
- // But registry and password should be preserved
1312
+ // But registry and passwords should be preserved
1313
1313
  expect(envContent).toMatch(/PGAI_REGISTRY=my\.registry\.com/);
1314
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/);
1315
1318
  }, 60000);
1316
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
+ });
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Test the SQL logic for checking postgres_ai.pg_statistic view existence
3
+ * across different permission scenarios.
4
+ */
5
+ import { describe, test, expect } from "bun:test";
6
+
7
+ describe("postgres_ai.pg_statistic permission check SQL", () => {
8
+ test("to_regclass() returns NULL when schema doesn't exist", () => {
9
+ // Simulate the SQL check behavior
10
+ const viewExists = null; // to_regclass('postgres_ai.pg_statistic') when schema doesn't exist
11
+ const granted = viewExists !== null;
12
+
13
+ expect(granted).toBe(false);
14
+ });
15
+
16
+ test("to_regclass() returns NULL when user lacks USAGE on schema", () => {
17
+ // When user lacks USAGE on postgres_ai schema, to_regclass() returns NULL
18
+ // even if the schema and view exist
19
+ const viewExists = null; // to_regclass('postgres_ai.pg_statistic') when no USAGE
20
+ const granted = viewExists !== null;
21
+
22
+ expect(granted).toBe(false);
23
+ });
24
+
25
+ test("to_regclass() returns oid when view exists and user has access", () => {
26
+ // When user has USAGE on schema and view exists
27
+ const viewExists = 12345; // to_regclass('postgres_ai.pg_statistic') returns oid
28
+ const granted = viewExists !== null;
29
+
30
+ expect(granted).toBe(true);
31
+ });
32
+
33
+ test("has_table_privilege is skipped (returns null) when view doesn't exist", () => {
34
+ const viewExists = null;
35
+ const selectGranted = viewExists === null ? null : true; // skipped
36
+
37
+ expect(selectGranted).toBeNull();
38
+ });
39
+
40
+ test("has_table_privilege is checked when view exists", () => {
41
+ const viewExists = 12345;
42
+ const userHasSelect = true;
43
+ const selectGranted = viewExists === null ? null : userHasSelect;
44
+
45
+ expect(selectGranted).toBe(true);
46
+ });
47
+ });
48
+
49
+ describe("Expected behavior per scenario", () => {
50
+ test("Scenario 1: Superuser with postgres_ai.pg_statistic", () => {
51
+ // to_regclass returns oid, has_table_privilege returns true
52
+ const checkViewExists = true; // to_regclass('postgres_ai.pg_statistic') is not null
53
+ const checkSelectPrivilege = true; // has_table_privilege returns true
54
+
55
+ const missingOptional: string[] = [];
56
+ if (!checkViewExists) {
57
+ missingOptional.push("postgres_ai.pg_statistic view exists");
58
+ }
59
+ if (checkSelectPrivilege === false) {
60
+ missingOptional.push("select on postgres_ai.pg_statistic");
61
+ }
62
+
63
+ expect(missingOptional).toHaveLength(0);
64
+ });
65
+
66
+ test("Scenario 2: pg_monitor, no postgres_ai schema access (before prepare-db)", () => {
67
+ // to_regclass returns NULL because user lacks USAGE on postgres_ai schema
68
+ const checkViewExists = false; // to_regclass('postgres_ai.pg_statistic') is null
69
+ const checkSelectPrivilege = null; // skipped because view doesn't exist
70
+
71
+ const missingOptional: string[] = [];
72
+ if (!checkViewExists) {
73
+ missingOptional.push("postgres_ai.pg_statistic view exists");
74
+ }
75
+ if (checkSelectPrivilege === false) {
76
+ missingOptional.push("select on postgres_ai.pg_statistic");
77
+ }
78
+
79
+ // Should show warning about missing view but NOT crash
80
+ expect(missingOptional).toEqual(["postgres_ai.pg_statistic view exists"]);
81
+ });
82
+
83
+ test("Scenario 3: No pg_monitor (before prepare-db)", () => {
84
+ // to_regclass returns NULL because schema doesn't exist yet
85
+ const checkViewExists = false; // to_regclass('postgres_ai.pg_statistic') is null
86
+ const checkSelectPrivilege = null; // skipped
87
+
88
+ const missingOptional: string[] = [];
89
+ if (!checkViewExists) {
90
+ missingOptional.push("postgres_ai.pg_statistic view exists");
91
+ }
92
+ if (checkSelectPrivilege === false) {
93
+ missingOptional.push("select on postgres_ai.pg_statistic");
94
+ }
95
+
96
+ // Should show warning but NOT crash
97
+ expect(missingOptional).toEqual(["postgres_ai.pg_statistic view exists"]);
98
+ });
99
+
100
+ test("Scenario 8: After prepare-db with schema grants", () => {
101
+ // to_regclass returns oid, has_table_privilege returns true
102
+ const checkViewExists = true; // to_regclass('postgres_ai.pg_statistic') is not null
103
+ const checkSelectPrivilege = true; // has_table_privilege returns true
104
+
105
+ const missingOptional: string[] = [];
106
+ if (!checkViewExists) {
107
+ missingOptional.push("postgres_ai.pg_statistic view exists");
108
+ }
109
+ if (checkSelectPrivilege === false) {
110
+ missingOptional.push("select on postgres_ai.pg_statistic");
111
+ }
112
+
113
+ // Should be clean, no warnings
114
+ expect(missingOptional).toHaveLength(0);
115
+ });
116
+ });