postgresai 0.14.0-dev.44 → 0.14.0-dev.46
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/dist/bin/postgres-ai.js +210 -3
- package/lib/checkup.ts +282 -2
- package/package.json +1 -1
- package/test/checkup.test.ts +56 -7
- package/test/init.integration.test.ts +6 -6
- package/test/schema-validation.test.ts +69 -0
package/dist/bin/postgres-ai.js
CHANGED
|
@@ -13064,7 +13064,7 @@ var {
|
|
|
13064
13064
|
// package.json
|
|
13065
13065
|
var package_default = {
|
|
13066
13066
|
name: "postgresai",
|
|
13067
|
-
version: "0.14.0-dev.
|
|
13067
|
+
version: "0.14.0-dev.46",
|
|
13068
13068
|
description: "postgres_ai CLI",
|
|
13069
13069
|
license: "Apache-2.0",
|
|
13070
13070
|
private: false,
|
|
@@ -15881,7 +15881,7 @@ var Result = import_lib.default.Result;
|
|
|
15881
15881
|
var TypeOverrides = import_lib.default.TypeOverrides;
|
|
15882
15882
|
var defaults = import_lib.default.defaults;
|
|
15883
15883
|
// package.json
|
|
15884
|
-
var version = "0.14.0-dev.
|
|
15884
|
+
var version = "0.14.0-dev.46";
|
|
15885
15885
|
var package_default2 = {
|
|
15886
15886
|
name: "postgresai",
|
|
15887
15887
|
version,
|
|
@@ -26956,7 +26956,7 @@ function parseVersionNum(versionNum) {
|
|
|
26956
26956
|
function formatBytes(bytes) {
|
|
26957
26957
|
if (bytes === 0)
|
|
26958
26958
|
return "0 B";
|
|
26959
|
-
const units = ["B", "
|
|
26959
|
+
const units = ["B", "KiB", "MiB", "GiB", "TiB", "PiB"];
|
|
26960
26960
|
const i3 = Math.floor(Math.log(bytes) / Math.log(1024));
|
|
26961
26961
|
return `${(bytes / Math.pow(1024, i3)).toFixed(2)} ${units[i3]}`;
|
|
26962
26962
|
}
|
|
@@ -27375,12 +27375,216 @@ async function generateH004(client, nodeName = "node-01") {
|
|
|
27375
27375
|
};
|
|
27376
27376
|
return report;
|
|
27377
27377
|
}
|
|
27378
|
+
async function generateD004(client, nodeName) {
|
|
27379
|
+
const report = createBaseReport("D004", "pg_stat_statements and pg_stat_kcache settings", nodeName);
|
|
27380
|
+
const postgresVersion = await getPostgresVersion(client);
|
|
27381
|
+
const allSettings = await getSettings(client);
|
|
27382
|
+
const pgssSettings = {};
|
|
27383
|
+
for (const [name, setting] of Object.entries(allSettings)) {
|
|
27384
|
+
if (name.startsWith("pg_stat_statements") || name.startsWith("pg_stat_kcache")) {
|
|
27385
|
+
pgssSettings[name] = setting;
|
|
27386
|
+
}
|
|
27387
|
+
}
|
|
27388
|
+
let pgssAvailable = false;
|
|
27389
|
+
let pgssMetricsCount = 0;
|
|
27390
|
+
let pgssTotalCalls = 0;
|
|
27391
|
+
const pgssSampleQueries = [];
|
|
27392
|
+
try {
|
|
27393
|
+
const extCheck = await client.query("select 1 from pg_extension where extname = 'pg_stat_statements'");
|
|
27394
|
+
if (extCheck.rows.length > 0) {
|
|
27395
|
+
pgssAvailable = true;
|
|
27396
|
+
const statsResult = await client.query(`
|
|
27397
|
+
select count(*) as cnt, coalesce(sum(calls), 0) as total_calls
|
|
27398
|
+
from pg_stat_statements
|
|
27399
|
+
`);
|
|
27400
|
+
pgssMetricsCount = parseInt(statsResult.rows[0]?.cnt || "0", 10);
|
|
27401
|
+
pgssTotalCalls = parseInt(statsResult.rows[0]?.total_calls || "0", 10);
|
|
27402
|
+
const sampleResult = await client.query(`
|
|
27403
|
+
select
|
|
27404
|
+
queryid::text as queryid,
|
|
27405
|
+
coalesce(usename, 'unknown') as "user",
|
|
27406
|
+
coalesce(datname, 'unknown') as database,
|
|
27407
|
+
calls
|
|
27408
|
+
from pg_stat_statements s
|
|
27409
|
+
left join pg_database d on s.dbid = d.oid
|
|
27410
|
+
left join pg_user u on s.userid = u.usesysid
|
|
27411
|
+
order by calls desc
|
|
27412
|
+
limit 5
|
|
27413
|
+
`);
|
|
27414
|
+
for (const row of sampleResult.rows) {
|
|
27415
|
+
pgssSampleQueries.push({
|
|
27416
|
+
queryid: row.queryid,
|
|
27417
|
+
user: row.user,
|
|
27418
|
+
database: row.database,
|
|
27419
|
+
calls: parseInt(row.calls, 10)
|
|
27420
|
+
});
|
|
27421
|
+
}
|
|
27422
|
+
}
|
|
27423
|
+
} catch {}
|
|
27424
|
+
let kcacheAvailable = false;
|
|
27425
|
+
let kcacheMetricsCount = 0;
|
|
27426
|
+
let kcacheTotalExecTime = 0;
|
|
27427
|
+
let kcacheTotalUserTime = 0;
|
|
27428
|
+
let kcacheTotalSystemTime = 0;
|
|
27429
|
+
const kcacheSampleQueries = [];
|
|
27430
|
+
try {
|
|
27431
|
+
const extCheck = await client.query("select 1 from pg_extension where extname = 'pg_stat_kcache'");
|
|
27432
|
+
if (extCheck.rows.length > 0) {
|
|
27433
|
+
kcacheAvailable = true;
|
|
27434
|
+
const statsResult = await client.query(`
|
|
27435
|
+
select
|
|
27436
|
+
count(*) as cnt,
|
|
27437
|
+
coalesce(sum(exec_user_time + exec_system_time), 0) as total_exec_time,
|
|
27438
|
+
coalesce(sum(exec_user_time), 0) as total_user_time,
|
|
27439
|
+
coalesce(sum(exec_system_time), 0) as total_system_time
|
|
27440
|
+
from pg_stat_kcache
|
|
27441
|
+
`);
|
|
27442
|
+
kcacheMetricsCount = parseInt(statsResult.rows[0]?.cnt || "0", 10);
|
|
27443
|
+
kcacheTotalExecTime = parseFloat(statsResult.rows[0]?.total_exec_time || "0");
|
|
27444
|
+
kcacheTotalUserTime = parseFloat(statsResult.rows[0]?.total_user_time || "0");
|
|
27445
|
+
kcacheTotalSystemTime = parseFloat(statsResult.rows[0]?.total_system_time || "0");
|
|
27446
|
+
const sampleResult = await client.query(`
|
|
27447
|
+
select
|
|
27448
|
+
queryid::text as queryid,
|
|
27449
|
+
coalesce(usename, 'unknown') as "user",
|
|
27450
|
+
(exec_user_time + exec_system_time) as exec_total_time
|
|
27451
|
+
from pg_stat_kcache k
|
|
27452
|
+
left join pg_user u on k.userid = u.usesysid
|
|
27453
|
+
order by (exec_user_time + exec_system_time) desc
|
|
27454
|
+
limit 5
|
|
27455
|
+
`);
|
|
27456
|
+
for (const row of sampleResult.rows) {
|
|
27457
|
+
kcacheSampleQueries.push({
|
|
27458
|
+
queryid: row.queryid,
|
|
27459
|
+
user: row.user,
|
|
27460
|
+
exec_total_time: parseFloat(row.exec_total_time)
|
|
27461
|
+
});
|
|
27462
|
+
}
|
|
27463
|
+
}
|
|
27464
|
+
} catch {}
|
|
27465
|
+
report.results[nodeName] = {
|
|
27466
|
+
data: {
|
|
27467
|
+
settings: pgssSettings,
|
|
27468
|
+
pg_stat_statements_status: {
|
|
27469
|
+
extension_available: pgssAvailable,
|
|
27470
|
+
metrics_count: pgssMetricsCount,
|
|
27471
|
+
total_calls: pgssTotalCalls,
|
|
27472
|
+
sample_queries: pgssSampleQueries
|
|
27473
|
+
},
|
|
27474
|
+
pg_stat_kcache_status: {
|
|
27475
|
+
extension_available: kcacheAvailable,
|
|
27476
|
+
metrics_count: kcacheMetricsCount,
|
|
27477
|
+
total_exec_time: kcacheTotalExecTime,
|
|
27478
|
+
total_user_time: kcacheTotalUserTime,
|
|
27479
|
+
total_system_time: kcacheTotalSystemTime,
|
|
27480
|
+
sample_queries: kcacheSampleQueries
|
|
27481
|
+
}
|
|
27482
|
+
},
|
|
27483
|
+
postgres_version: postgresVersion
|
|
27484
|
+
};
|
|
27485
|
+
return report;
|
|
27486
|
+
}
|
|
27487
|
+
async function generateF001(client, nodeName) {
|
|
27488
|
+
const report = createBaseReport("F001", "Autovacuum: current settings", nodeName);
|
|
27489
|
+
const postgresVersion = await getPostgresVersion(client);
|
|
27490
|
+
const allSettings = await getSettings(client);
|
|
27491
|
+
const autovacuumSettings = {};
|
|
27492
|
+
for (const [name, setting] of Object.entries(allSettings)) {
|
|
27493
|
+
if (name.includes("autovacuum") || name.includes("vacuum")) {
|
|
27494
|
+
autovacuumSettings[name] = setting;
|
|
27495
|
+
}
|
|
27496
|
+
}
|
|
27497
|
+
report.results[nodeName] = {
|
|
27498
|
+
data: autovacuumSettings,
|
|
27499
|
+
postgres_version: postgresVersion
|
|
27500
|
+
};
|
|
27501
|
+
return report;
|
|
27502
|
+
}
|
|
27503
|
+
async function generateG001(client, nodeName) {
|
|
27504
|
+
const report = createBaseReport("G001", "Memory-related settings", nodeName);
|
|
27505
|
+
const postgresVersion = await getPostgresVersion(client);
|
|
27506
|
+
const allSettings = await getSettings(client);
|
|
27507
|
+
const memorySettingNames = [
|
|
27508
|
+
"shared_buffers",
|
|
27509
|
+
"work_mem",
|
|
27510
|
+
"maintenance_work_mem",
|
|
27511
|
+
"effective_cache_size",
|
|
27512
|
+
"wal_buffers",
|
|
27513
|
+
"temp_buffers",
|
|
27514
|
+
"max_connections",
|
|
27515
|
+
"autovacuum_work_mem",
|
|
27516
|
+
"hash_mem_multiplier",
|
|
27517
|
+
"logical_decoding_work_mem",
|
|
27518
|
+
"max_stack_depth",
|
|
27519
|
+
"max_prepared_transactions",
|
|
27520
|
+
"max_locks_per_transaction",
|
|
27521
|
+
"max_pred_locks_per_transaction"
|
|
27522
|
+
];
|
|
27523
|
+
const memorySettings = {};
|
|
27524
|
+
for (const name of memorySettingNames) {
|
|
27525
|
+
if (allSettings[name]) {
|
|
27526
|
+
memorySettings[name] = allSettings[name];
|
|
27527
|
+
}
|
|
27528
|
+
}
|
|
27529
|
+
let memoryUsage = {};
|
|
27530
|
+
try {
|
|
27531
|
+
const memQuery = await client.query(`
|
|
27532
|
+
select
|
|
27533
|
+
pg_size_bytes(current_setting('shared_buffers')) as shared_buffers_bytes,
|
|
27534
|
+
pg_size_bytes(current_setting('wal_buffers')) as wal_buffers_bytes,
|
|
27535
|
+
pg_size_bytes(current_setting('work_mem')) as work_mem_bytes,
|
|
27536
|
+
pg_size_bytes(current_setting('maintenance_work_mem')) as maintenance_work_mem_bytes,
|
|
27537
|
+
pg_size_bytes(current_setting('effective_cache_size')) as effective_cache_size_bytes,
|
|
27538
|
+
current_setting('max_connections')::int as max_connections
|
|
27539
|
+
`);
|
|
27540
|
+
if (memQuery.rows.length > 0) {
|
|
27541
|
+
const row = memQuery.rows[0];
|
|
27542
|
+
const sharedBuffersBytes = parseInt(row.shared_buffers_bytes, 10);
|
|
27543
|
+
const walBuffersBytes = parseInt(row.wal_buffers_bytes, 10);
|
|
27544
|
+
const workMemBytes = parseInt(row.work_mem_bytes, 10);
|
|
27545
|
+
const maintenanceWorkMemBytes = parseInt(row.maintenance_work_mem_bytes, 10);
|
|
27546
|
+
const effectiveCacheSizeBytes = parseInt(row.effective_cache_size_bytes, 10);
|
|
27547
|
+
const maxConnections = row.max_connections;
|
|
27548
|
+
const sharedMemoryTotal = sharedBuffersBytes + walBuffersBytes;
|
|
27549
|
+
const maxWorkMemUsage = workMemBytes * maxConnections;
|
|
27550
|
+
memoryUsage = {
|
|
27551
|
+
shared_buffers_bytes: sharedBuffersBytes,
|
|
27552
|
+
shared_buffers_pretty: formatBytes(sharedBuffersBytes),
|
|
27553
|
+
wal_buffers_bytes: walBuffersBytes,
|
|
27554
|
+
wal_buffers_pretty: formatBytes(walBuffersBytes),
|
|
27555
|
+
shared_memory_total_bytes: sharedMemoryTotal,
|
|
27556
|
+
shared_memory_total_pretty: formatBytes(sharedMemoryTotal),
|
|
27557
|
+
work_mem_per_connection_bytes: workMemBytes,
|
|
27558
|
+
work_mem_per_connection_pretty: formatBytes(workMemBytes),
|
|
27559
|
+
max_work_mem_usage_bytes: maxWorkMemUsage,
|
|
27560
|
+
max_work_mem_usage_pretty: formatBytes(maxWorkMemUsage),
|
|
27561
|
+
maintenance_work_mem_bytes: maintenanceWorkMemBytes,
|
|
27562
|
+
maintenance_work_mem_pretty: formatBytes(maintenanceWorkMemBytes),
|
|
27563
|
+
effective_cache_size_bytes: effectiveCacheSizeBytes,
|
|
27564
|
+
effective_cache_size_pretty: formatBytes(effectiveCacheSizeBytes)
|
|
27565
|
+
};
|
|
27566
|
+
}
|
|
27567
|
+
} catch {}
|
|
27568
|
+
report.results[nodeName] = {
|
|
27569
|
+
data: {
|
|
27570
|
+
settings: memorySettings,
|
|
27571
|
+
analysis: {
|
|
27572
|
+
estimated_total_memory_usage: memoryUsage
|
|
27573
|
+
}
|
|
27574
|
+
},
|
|
27575
|
+
postgres_version: postgresVersion
|
|
27576
|
+
};
|
|
27577
|
+
return report;
|
|
27578
|
+
}
|
|
27378
27579
|
var REPORT_GENERATORS = {
|
|
27379
27580
|
A002: generateA002,
|
|
27380
27581
|
A003: generateA003,
|
|
27381
27582
|
A004: generateA004,
|
|
27382
27583
|
A007: generateA007,
|
|
27383
27584
|
A013: generateA013,
|
|
27585
|
+
D004: generateD004,
|
|
27586
|
+
F001: generateF001,
|
|
27587
|
+
G001: generateG001,
|
|
27384
27588
|
H001: generateH001,
|
|
27385
27589
|
H002: generateH002,
|
|
27386
27590
|
H004: generateH004
|
|
@@ -27391,6 +27595,9 @@ var CHECK_INFO = {
|
|
|
27391
27595
|
A004: "Cluster information",
|
|
27392
27596
|
A007: "Altered settings",
|
|
27393
27597
|
A013: "Postgres minor version",
|
|
27598
|
+
D004: "pg_stat_statements and pg_stat_kcache settings",
|
|
27599
|
+
F001: "Autovacuum: current settings",
|
|
27600
|
+
G001: "Memory-related settings",
|
|
27394
27601
|
H001: "Invalid indexes",
|
|
27395
27602
|
H002: "Unused indexes",
|
|
27396
27603
|
H004: "Redundant indexes"
|
package/lib/checkup.ts
CHANGED
|
@@ -186,11 +186,15 @@ export function parseVersionNum(versionNum: string): { major: string; minor: str
|
|
|
186
186
|
}
|
|
187
187
|
|
|
188
188
|
/**
|
|
189
|
-
* Format bytes to human readable string
|
|
189
|
+
* Format bytes to human readable string using binary units (1024-based).
|
|
190
|
+
* Uses IEC standard: KiB, MiB, GiB, etc.
|
|
191
|
+
*
|
|
192
|
+
* Note: PostgreSQL's pg_size_pretty() uses kB/MB/GB with 1024 base (technically
|
|
193
|
+
* incorrect SI usage), but we follow IEC binary units per project style guide.
|
|
190
194
|
*/
|
|
191
195
|
export function formatBytes(bytes: number): string {
|
|
192
196
|
if (bytes === 0) return "0 B";
|
|
193
|
-
const units = ["B", "
|
|
197
|
+
const units = ["B", "KiB", "MiB", "GiB", "TiB", "PiB"];
|
|
194
198
|
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
|
195
199
|
return `${(bytes / Math.pow(1024, i)).toFixed(2)} ${units[i]}`;
|
|
196
200
|
}
|
|
@@ -776,6 +780,276 @@ export async function generateH004(client: Client, nodeName: string = "node-01")
|
|
|
776
780
|
return report;
|
|
777
781
|
}
|
|
778
782
|
|
|
783
|
+
/**
|
|
784
|
+
* Generate D004 report - pg_stat_statements and pg_stat_kcache settings
|
|
785
|
+
*/
|
|
786
|
+
async function generateD004(client: Client, nodeName: string): Promise<Report> {
|
|
787
|
+
const report = createBaseReport("D004", "pg_stat_statements and pg_stat_kcache settings", nodeName);
|
|
788
|
+
const postgresVersion = await getPostgresVersion(client);
|
|
789
|
+
const allSettings = await getSettings(client);
|
|
790
|
+
|
|
791
|
+
// Filter settings related to pg_stat_statements and pg_stat_kcache
|
|
792
|
+
const pgssSettings: Record<string, SettingInfo> = {};
|
|
793
|
+
for (const [name, setting] of Object.entries(allSettings)) {
|
|
794
|
+
if (name.startsWith("pg_stat_statements") || name.startsWith("pg_stat_kcache")) {
|
|
795
|
+
pgssSettings[name] = setting;
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
// Check pg_stat_statements extension
|
|
800
|
+
let pgssAvailable = false;
|
|
801
|
+
let pgssMetricsCount = 0;
|
|
802
|
+
let pgssTotalCalls = 0;
|
|
803
|
+
const pgssSampleQueries: Array<{ queryid: string; user: string; database: string; calls: number }> = [];
|
|
804
|
+
|
|
805
|
+
try {
|
|
806
|
+
const extCheck = await client.query(
|
|
807
|
+
"select 1 from pg_extension where extname = 'pg_stat_statements'"
|
|
808
|
+
);
|
|
809
|
+
if (extCheck.rows.length > 0) {
|
|
810
|
+
pgssAvailable = true;
|
|
811
|
+
const statsResult = await client.query(`
|
|
812
|
+
select count(*) as cnt, coalesce(sum(calls), 0) as total_calls
|
|
813
|
+
from pg_stat_statements
|
|
814
|
+
`);
|
|
815
|
+
pgssMetricsCount = parseInt(statsResult.rows[0]?.cnt || "0", 10);
|
|
816
|
+
pgssTotalCalls = parseInt(statsResult.rows[0]?.total_calls || "0", 10);
|
|
817
|
+
|
|
818
|
+
// Get sample queries (top 5 by calls)
|
|
819
|
+
const sampleResult = await client.query(`
|
|
820
|
+
select
|
|
821
|
+
queryid::text as queryid,
|
|
822
|
+
coalesce(usename, 'unknown') as "user",
|
|
823
|
+
coalesce(datname, 'unknown') as database,
|
|
824
|
+
calls
|
|
825
|
+
from pg_stat_statements s
|
|
826
|
+
left join pg_database d on s.dbid = d.oid
|
|
827
|
+
left join pg_user u on s.userid = u.usesysid
|
|
828
|
+
order by calls desc
|
|
829
|
+
limit 5
|
|
830
|
+
`);
|
|
831
|
+
for (const row of sampleResult.rows) {
|
|
832
|
+
pgssSampleQueries.push({
|
|
833
|
+
queryid: row.queryid,
|
|
834
|
+
user: row.user,
|
|
835
|
+
database: row.database,
|
|
836
|
+
calls: parseInt(row.calls, 10),
|
|
837
|
+
});
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
} catch {
|
|
841
|
+
// Extension not available or accessible
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
// Check pg_stat_kcache extension
|
|
845
|
+
let kcacheAvailable = false;
|
|
846
|
+
let kcacheMetricsCount = 0;
|
|
847
|
+
let kcacheTotalExecTime = 0;
|
|
848
|
+
let kcacheTotalUserTime = 0;
|
|
849
|
+
let kcacheTotalSystemTime = 0;
|
|
850
|
+
const kcacheSampleQueries: Array<{ queryid: string; user: string; exec_total_time: number }> = [];
|
|
851
|
+
|
|
852
|
+
try {
|
|
853
|
+
const extCheck = await client.query(
|
|
854
|
+
"select 1 from pg_extension where extname = 'pg_stat_kcache'"
|
|
855
|
+
);
|
|
856
|
+
if (extCheck.rows.length > 0) {
|
|
857
|
+
kcacheAvailable = true;
|
|
858
|
+
const statsResult = await client.query(`
|
|
859
|
+
select
|
|
860
|
+
count(*) as cnt,
|
|
861
|
+
coalesce(sum(exec_user_time + exec_system_time), 0) as total_exec_time,
|
|
862
|
+
coalesce(sum(exec_user_time), 0) as total_user_time,
|
|
863
|
+
coalesce(sum(exec_system_time), 0) as total_system_time
|
|
864
|
+
from pg_stat_kcache
|
|
865
|
+
`);
|
|
866
|
+
kcacheMetricsCount = parseInt(statsResult.rows[0]?.cnt || "0", 10);
|
|
867
|
+
kcacheTotalExecTime = parseFloat(statsResult.rows[0]?.total_exec_time || "0");
|
|
868
|
+
kcacheTotalUserTime = parseFloat(statsResult.rows[0]?.total_user_time || "0");
|
|
869
|
+
kcacheTotalSystemTime = parseFloat(statsResult.rows[0]?.total_system_time || "0");
|
|
870
|
+
|
|
871
|
+
// Get sample queries (top 5 by exec time)
|
|
872
|
+
const sampleResult = await client.query(`
|
|
873
|
+
select
|
|
874
|
+
queryid::text as queryid,
|
|
875
|
+
coalesce(usename, 'unknown') as "user",
|
|
876
|
+
(exec_user_time + exec_system_time) as exec_total_time
|
|
877
|
+
from pg_stat_kcache k
|
|
878
|
+
left join pg_user u on k.userid = u.usesysid
|
|
879
|
+
order by (exec_user_time + exec_system_time) desc
|
|
880
|
+
limit 5
|
|
881
|
+
`);
|
|
882
|
+
for (const row of sampleResult.rows) {
|
|
883
|
+
kcacheSampleQueries.push({
|
|
884
|
+
queryid: row.queryid,
|
|
885
|
+
user: row.user,
|
|
886
|
+
exec_total_time: parseFloat(row.exec_total_time),
|
|
887
|
+
});
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
} catch {
|
|
891
|
+
// Extension not available or accessible
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
report.results[nodeName] = {
|
|
895
|
+
data: {
|
|
896
|
+
settings: pgssSettings,
|
|
897
|
+
pg_stat_statements_status: {
|
|
898
|
+
extension_available: pgssAvailable,
|
|
899
|
+
metrics_count: pgssMetricsCount,
|
|
900
|
+
total_calls: pgssTotalCalls,
|
|
901
|
+
sample_queries: pgssSampleQueries,
|
|
902
|
+
},
|
|
903
|
+
pg_stat_kcache_status: {
|
|
904
|
+
extension_available: kcacheAvailable,
|
|
905
|
+
metrics_count: kcacheMetricsCount,
|
|
906
|
+
total_exec_time: kcacheTotalExecTime,
|
|
907
|
+
total_user_time: kcacheTotalUserTime,
|
|
908
|
+
total_system_time: kcacheTotalSystemTime,
|
|
909
|
+
sample_queries: kcacheSampleQueries,
|
|
910
|
+
},
|
|
911
|
+
},
|
|
912
|
+
postgres_version: postgresVersion,
|
|
913
|
+
};
|
|
914
|
+
|
|
915
|
+
return report;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
/**
|
|
919
|
+
* Generate F001 report - Autovacuum: current settings
|
|
920
|
+
*/
|
|
921
|
+
async function generateF001(client: Client, nodeName: string): Promise<Report> {
|
|
922
|
+
const report = createBaseReport("F001", "Autovacuum: current settings", nodeName);
|
|
923
|
+
const postgresVersion = await getPostgresVersion(client);
|
|
924
|
+
const allSettings = await getSettings(client);
|
|
925
|
+
|
|
926
|
+
// Filter autovacuum-related settings
|
|
927
|
+
const autovacuumSettings: Record<string, SettingInfo> = {};
|
|
928
|
+
for (const [name, setting] of Object.entries(allSettings)) {
|
|
929
|
+
if (name.includes("autovacuum") || name.includes("vacuum")) {
|
|
930
|
+
autovacuumSettings[name] = setting;
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
report.results[nodeName] = {
|
|
935
|
+
data: autovacuumSettings,
|
|
936
|
+
postgres_version: postgresVersion,
|
|
937
|
+
};
|
|
938
|
+
|
|
939
|
+
return report;
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
/**
|
|
943
|
+
* Generate G001 report - Memory-related settings
|
|
944
|
+
*/
|
|
945
|
+
async function generateG001(client: Client, nodeName: string): Promise<Report> {
|
|
946
|
+
const report = createBaseReport("G001", "Memory-related settings", nodeName);
|
|
947
|
+
const postgresVersion = await getPostgresVersion(client);
|
|
948
|
+
const allSettings = await getSettings(client);
|
|
949
|
+
|
|
950
|
+
// Memory-related setting names
|
|
951
|
+
const memorySettingNames = [
|
|
952
|
+
"shared_buffers",
|
|
953
|
+
"work_mem",
|
|
954
|
+
"maintenance_work_mem",
|
|
955
|
+
"effective_cache_size",
|
|
956
|
+
"wal_buffers",
|
|
957
|
+
"temp_buffers",
|
|
958
|
+
"max_connections",
|
|
959
|
+
"autovacuum_work_mem",
|
|
960
|
+
"hash_mem_multiplier",
|
|
961
|
+
"logical_decoding_work_mem",
|
|
962
|
+
"max_stack_depth",
|
|
963
|
+
"max_prepared_transactions",
|
|
964
|
+
"max_locks_per_transaction",
|
|
965
|
+
"max_pred_locks_per_transaction",
|
|
966
|
+
];
|
|
967
|
+
|
|
968
|
+
const memorySettings: Record<string, SettingInfo> = {};
|
|
969
|
+
for (const name of memorySettingNames) {
|
|
970
|
+
if (allSettings[name]) {
|
|
971
|
+
memorySettings[name] = allSettings[name];
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
// Calculate memory usage estimates
|
|
976
|
+
interface MemoryUsage {
|
|
977
|
+
shared_buffers_bytes: number;
|
|
978
|
+
shared_buffers_pretty: string;
|
|
979
|
+
wal_buffers_bytes: number;
|
|
980
|
+
wal_buffers_pretty: string;
|
|
981
|
+
shared_memory_total_bytes: number;
|
|
982
|
+
shared_memory_total_pretty: string;
|
|
983
|
+
work_mem_per_connection_bytes: number;
|
|
984
|
+
work_mem_per_connection_pretty: string;
|
|
985
|
+
max_work_mem_usage_bytes: number;
|
|
986
|
+
max_work_mem_usage_pretty: string;
|
|
987
|
+
maintenance_work_mem_bytes: number;
|
|
988
|
+
maintenance_work_mem_pretty: string;
|
|
989
|
+
effective_cache_size_bytes: number;
|
|
990
|
+
effective_cache_size_pretty: string;
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
let memoryUsage: MemoryUsage | Record<string, never> = {};
|
|
994
|
+
|
|
995
|
+
try {
|
|
996
|
+
// Get actual byte values from PostgreSQL
|
|
997
|
+
const memQuery = await client.query(`
|
|
998
|
+
select
|
|
999
|
+
pg_size_bytes(current_setting('shared_buffers')) as shared_buffers_bytes,
|
|
1000
|
+
pg_size_bytes(current_setting('wal_buffers')) as wal_buffers_bytes,
|
|
1001
|
+
pg_size_bytes(current_setting('work_mem')) as work_mem_bytes,
|
|
1002
|
+
pg_size_bytes(current_setting('maintenance_work_mem')) as maintenance_work_mem_bytes,
|
|
1003
|
+
pg_size_bytes(current_setting('effective_cache_size')) as effective_cache_size_bytes,
|
|
1004
|
+
current_setting('max_connections')::int as max_connections
|
|
1005
|
+
`);
|
|
1006
|
+
|
|
1007
|
+
if (memQuery.rows.length > 0) {
|
|
1008
|
+
const row = memQuery.rows[0];
|
|
1009
|
+
const sharedBuffersBytes = parseInt(row.shared_buffers_bytes, 10);
|
|
1010
|
+
const walBuffersBytes = parseInt(row.wal_buffers_bytes, 10);
|
|
1011
|
+
const workMemBytes = parseInt(row.work_mem_bytes, 10);
|
|
1012
|
+
const maintenanceWorkMemBytes = parseInt(row.maintenance_work_mem_bytes, 10);
|
|
1013
|
+
const effectiveCacheSizeBytes = parseInt(row.effective_cache_size_bytes, 10);
|
|
1014
|
+
const maxConnections = row.max_connections;
|
|
1015
|
+
|
|
1016
|
+
const sharedMemoryTotal = sharedBuffersBytes + walBuffersBytes;
|
|
1017
|
+
const maxWorkMemUsage = workMemBytes * maxConnections;
|
|
1018
|
+
|
|
1019
|
+
memoryUsage = {
|
|
1020
|
+
shared_buffers_bytes: sharedBuffersBytes,
|
|
1021
|
+
shared_buffers_pretty: formatBytes(sharedBuffersBytes),
|
|
1022
|
+
wal_buffers_bytes: walBuffersBytes,
|
|
1023
|
+
wal_buffers_pretty: formatBytes(walBuffersBytes),
|
|
1024
|
+
shared_memory_total_bytes: sharedMemoryTotal,
|
|
1025
|
+
shared_memory_total_pretty: formatBytes(sharedMemoryTotal),
|
|
1026
|
+
work_mem_per_connection_bytes: workMemBytes,
|
|
1027
|
+
work_mem_per_connection_pretty: formatBytes(workMemBytes),
|
|
1028
|
+
max_work_mem_usage_bytes: maxWorkMemUsage,
|
|
1029
|
+
max_work_mem_usage_pretty: formatBytes(maxWorkMemUsage),
|
|
1030
|
+
maintenance_work_mem_bytes: maintenanceWorkMemBytes,
|
|
1031
|
+
maintenance_work_mem_pretty: formatBytes(maintenanceWorkMemBytes),
|
|
1032
|
+
effective_cache_size_bytes: effectiveCacheSizeBytes,
|
|
1033
|
+
effective_cache_size_pretty: formatBytes(effectiveCacheSizeBytes),
|
|
1034
|
+
};
|
|
1035
|
+
}
|
|
1036
|
+
} catch {
|
|
1037
|
+
// If we can't calculate, leave empty object (schema allows this)
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
report.results[nodeName] = {
|
|
1041
|
+
data: {
|
|
1042
|
+
settings: memorySettings,
|
|
1043
|
+
analysis: {
|
|
1044
|
+
estimated_total_memory_usage: memoryUsage,
|
|
1045
|
+
},
|
|
1046
|
+
},
|
|
1047
|
+
postgres_version: postgresVersion,
|
|
1048
|
+
};
|
|
1049
|
+
|
|
1050
|
+
return report;
|
|
1051
|
+
}
|
|
1052
|
+
|
|
779
1053
|
/**
|
|
780
1054
|
* Available report generators
|
|
781
1055
|
*/
|
|
@@ -785,6 +1059,9 @@ export const REPORT_GENERATORS: Record<string, (client: Client, nodeName: string
|
|
|
785
1059
|
A004: generateA004,
|
|
786
1060
|
A007: generateA007,
|
|
787
1061
|
A013: generateA013,
|
|
1062
|
+
D004: generateD004,
|
|
1063
|
+
F001: generateF001,
|
|
1064
|
+
G001: generateG001,
|
|
788
1065
|
H001: generateH001,
|
|
789
1066
|
H002: generateH002,
|
|
790
1067
|
H004: generateH004,
|
|
@@ -799,6 +1076,9 @@ export const CHECK_INFO: Record<string, string> = {
|
|
|
799
1076
|
A004: "Cluster information",
|
|
800
1077
|
A007: "Altered settings",
|
|
801
1078
|
A013: "Postgres minor version",
|
|
1079
|
+
D004: "pg_stat_statements and pg_stat_kcache settings",
|
|
1080
|
+
F001: "Autovacuum: current settings",
|
|
1081
|
+
G001: "Memory-related settings",
|
|
802
1082
|
H001: "Invalid indexes",
|
|
803
1083
|
H002: "Unused indexes",
|
|
804
1084
|
H004: "Redundant indexes",
|
package/package.json
CHANGED
package/test/checkup.test.ts
CHANGED
|
@@ -99,6 +99,25 @@ function createMockClient(versionRows: any[], settingsRows: any[], options: Mock
|
|
|
99
99
|
if (sql.includes("redundant_indexes") && sql.includes("columns like")) {
|
|
100
100
|
return { rows: redundantIndexesRows };
|
|
101
101
|
}
|
|
102
|
+
// D004: pg_stat_statements extension check
|
|
103
|
+
if (sql.includes("pg_extension") && sql.includes("pg_stat_statements")) {
|
|
104
|
+
return { rows: [] }; // Extension not installed by default
|
|
105
|
+
}
|
|
106
|
+
// D004: pg_stat_kcache extension check
|
|
107
|
+
if (sql.includes("pg_extension") && sql.includes("pg_stat_kcache")) {
|
|
108
|
+
return { rows: [] }; // Extension not installed by default
|
|
109
|
+
}
|
|
110
|
+
// G001: Memory settings query
|
|
111
|
+
if (sql.includes("pg_size_bytes") && sql.includes("shared_buffers") && sql.includes("work_mem")) {
|
|
112
|
+
return { rows: [{
|
|
113
|
+
shared_buffers_bytes: "134217728",
|
|
114
|
+
wal_buffers_bytes: "4194304",
|
|
115
|
+
work_mem_bytes: "4194304",
|
|
116
|
+
maintenance_work_mem_bytes: "67108864",
|
|
117
|
+
effective_cache_size_bytes: "4294967296",
|
|
118
|
+
max_connections: 100,
|
|
119
|
+
}] };
|
|
120
|
+
}
|
|
102
121
|
throw new Error(`Unexpected query: ${sql}`);
|
|
103
122
|
},
|
|
104
123
|
};
|
|
@@ -208,6 +227,21 @@ describe("CHECK_INFO", () => {
|
|
|
208
227
|
expect("H004" in checkup.CHECK_INFO).toBe(true);
|
|
209
228
|
expect(checkup.CHECK_INFO.H004).toBe("Redundant indexes");
|
|
210
229
|
});
|
|
230
|
+
|
|
231
|
+
test("contains D004", () => {
|
|
232
|
+
expect("D004" in checkup.CHECK_INFO).toBe(true);
|
|
233
|
+
expect(checkup.CHECK_INFO.D004).toBe("pg_stat_statements and pg_stat_kcache settings");
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
test("contains F001", () => {
|
|
237
|
+
expect("F001" in checkup.CHECK_INFO).toBe(true);
|
|
238
|
+
expect(checkup.CHECK_INFO.F001).toBe("Autovacuum: current settings");
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
test("contains G001", () => {
|
|
242
|
+
expect("G001" in checkup.CHECK_INFO).toBe(true);
|
|
243
|
+
expect(checkup.CHECK_INFO.G001).toBe("Memory-related settings");
|
|
244
|
+
});
|
|
211
245
|
});
|
|
212
246
|
|
|
213
247
|
// Tests for REPORT_GENERATORS
|
|
@@ -252,6 +286,21 @@ describe("REPORT_GENERATORS", () => {
|
|
|
252
286
|
expect(typeof checkup.REPORT_GENERATORS.H004).toBe("function");
|
|
253
287
|
});
|
|
254
288
|
|
|
289
|
+
test("has generator for D004", () => {
|
|
290
|
+
expect("D004" in checkup.REPORT_GENERATORS).toBe(true);
|
|
291
|
+
expect(typeof checkup.REPORT_GENERATORS.D004).toBe("function");
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
test("has generator for F001", () => {
|
|
295
|
+
expect("F001" in checkup.REPORT_GENERATORS).toBe(true);
|
|
296
|
+
expect(typeof checkup.REPORT_GENERATORS.F001).toBe("function");
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
test("has generator for G001", () => {
|
|
300
|
+
expect("G001" in checkup.REPORT_GENERATORS).toBe(true);
|
|
301
|
+
expect(typeof checkup.REPORT_GENERATORS.G001).toBe("function");
|
|
302
|
+
});
|
|
303
|
+
|
|
255
304
|
test("REPORT_GENERATORS and CHECK_INFO have same keys", () => {
|
|
256
305
|
const generatorKeys = Object.keys(checkup.REPORT_GENERATORS).sort();
|
|
257
306
|
const infoKeys = Object.keys(checkup.CHECK_INFO).sort();
|
|
@@ -269,17 +318,17 @@ describe("formatBytes", () => {
|
|
|
269
318
|
expect(checkup.formatBytes(500)).toBe("500.00 B");
|
|
270
319
|
});
|
|
271
320
|
|
|
272
|
-
test("formats
|
|
273
|
-
expect(checkup.formatBytes(1024)).toBe("1.00
|
|
274
|
-
expect(checkup.formatBytes(1536)).toBe("1.50
|
|
321
|
+
test("formats kibibytes", () => {
|
|
322
|
+
expect(checkup.formatBytes(1024)).toBe("1.00 KiB");
|
|
323
|
+
expect(checkup.formatBytes(1536)).toBe("1.50 KiB");
|
|
275
324
|
});
|
|
276
325
|
|
|
277
|
-
test("formats
|
|
278
|
-
expect(checkup.formatBytes(1048576)).toBe("1.00
|
|
326
|
+
test("formats mebibytes", () => {
|
|
327
|
+
expect(checkup.formatBytes(1048576)).toBe("1.00 MiB");
|
|
279
328
|
});
|
|
280
329
|
|
|
281
|
-
test("formats
|
|
282
|
-
expect(checkup.formatBytes(1073741824)).toBe("1.00
|
|
330
|
+
test("formats gibibytes", () => {
|
|
331
|
+
expect(checkup.formatBytes(1073741824)).toBe("1.00 GiB");
|
|
283
332
|
});
|
|
284
333
|
});
|
|
285
334
|
|
|
@@ -213,7 +213,7 @@ describe.skipIf(skipTests)("integration: prepare-db", () => {
|
|
|
213
213
|
} finally {
|
|
214
214
|
await pg.cleanup();
|
|
215
215
|
}
|
|
216
|
-
});
|
|
216
|
+
}, { timeout: 30000 });
|
|
217
217
|
|
|
218
218
|
test("requires explicit monitoring password in non-interactive mode", async () => {
|
|
219
219
|
pg = await createTempPostgres();
|
|
@@ -237,7 +237,7 @@ describe.skipIf(skipTests)("integration: prepare-db", () => {
|
|
|
237
237
|
} finally {
|
|
238
238
|
await pg.cleanup();
|
|
239
239
|
}
|
|
240
|
-
});
|
|
240
|
+
}, { timeout: 30000 });
|
|
241
241
|
|
|
242
242
|
test("fixes slightly-off permissions idempotently", async () => {
|
|
243
243
|
pg = await createTempPostgres();
|
|
@@ -291,7 +291,7 @@ describe.skipIf(skipTests)("integration: prepare-db", () => {
|
|
|
291
291
|
} finally {
|
|
292
292
|
await pg.cleanup();
|
|
293
293
|
}
|
|
294
|
-
});
|
|
294
|
+
}, { timeout: 30000 });
|
|
295
295
|
|
|
296
296
|
test("reports nicely when lacking permissions", async () => {
|
|
297
297
|
pg = await createTempPostgres();
|
|
@@ -324,7 +324,7 @@ describe.skipIf(skipTests)("integration: prepare-db", () => {
|
|
|
324
324
|
} finally {
|
|
325
325
|
await pg.cleanup();
|
|
326
326
|
}
|
|
327
|
-
});
|
|
327
|
+
}, { timeout: 30000 });
|
|
328
328
|
|
|
329
329
|
test("--verify returns 0 when ok and non-zero when missing", async () => {
|
|
330
330
|
pg = await createTempPostgres();
|
|
@@ -360,7 +360,7 @@ describe.skipIf(skipTests)("integration: prepare-db", () => {
|
|
|
360
360
|
} finally {
|
|
361
361
|
await pg.cleanup();
|
|
362
362
|
}
|
|
363
|
-
});
|
|
363
|
+
}, { timeout: 30000 });
|
|
364
364
|
|
|
365
365
|
test("--reset-password updates the monitoring role login password", async () => {
|
|
366
366
|
pg = await createTempPostgres();
|
|
@@ -392,5 +392,5 @@ describe.skipIf(skipTests)("integration: prepare-db", () => {
|
|
|
392
392
|
} finally {
|
|
393
393
|
await pg.cleanup();
|
|
394
394
|
}
|
|
395
|
-
});
|
|
395
|
+
}, { timeout: 30000 });
|
|
396
396
|
});
|
|
@@ -28,6 +28,7 @@ function validateReport(report: any, checkId: string): { valid: boolean; errors:
|
|
|
28
28
|
// Mock client for testing
|
|
29
29
|
function createMockClient(options: {
|
|
30
30
|
versionRows?: any[];
|
|
31
|
+
settingsRows?: any[];
|
|
31
32
|
invalidIndexesRows?: any[];
|
|
32
33
|
unusedIndexesRows?: any[];
|
|
33
34
|
redundantIndexesRows?: any[];
|
|
@@ -37,6 +38,12 @@ function createMockClient(options: {
|
|
|
37
38
|
{ name: "server_version", setting: "16.3" },
|
|
38
39
|
{ name: "server_version_num", setting: "160003" },
|
|
39
40
|
],
|
|
41
|
+
settingsRows = [
|
|
42
|
+
{ name: "shared_buffers", setting: "128MB", unit: "", category: "Resource Usage / Memory", context: "postmaster", vartype: "string", pretty_value: "128 MB" },
|
|
43
|
+
{ name: "work_mem", setting: "4MB", unit: "", category: "Resource Usage / Memory", context: "user", vartype: "string", pretty_value: "4 MB" },
|
|
44
|
+
{ name: "autovacuum", setting: "on", unit: "", category: "Autovacuum", context: "sighup", vartype: "bool", pretty_value: "on" },
|
|
45
|
+
{ name: "pg_stat_statements.max", setting: "5000", unit: "", category: "Custom", context: "superuser", vartype: "integer", pretty_value: "5000" },
|
|
46
|
+
],
|
|
40
47
|
invalidIndexesRows = [],
|
|
41
48
|
unusedIndexesRows = [],
|
|
42
49
|
redundantIndexesRows = [],
|
|
@@ -47,6 +54,10 @@ function createMockClient(options: {
|
|
|
47
54
|
if (sql.includes("server_version") && sql.includes("server_version_num") && !sql.includes("order by")) {
|
|
48
55
|
return { rows: versionRows };
|
|
49
56
|
}
|
|
57
|
+
// Full settings query
|
|
58
|
+
if (sql.includes("pg_settings") && sql.includes("order by") && sql.includes("is_default")) {
|
|
59
|
+
return { rows: settingsRows };
|
|
60
|
+
}
|
|
50
61
|
if (sql.includes("current_database()") && sql.includes("pg_database_size")) {
|
|
51
62
|
return { rows: [{ datname: "testdb", size_bytes: "1073741824" }] };
|
|
52
63
|
}
|
|
@@ -68,6 +79,25 @@ function createMockClient(options: {
|
|
|
68
79
|
if (sql.includes("redundant_indexes") && sql.includes("columns like")) {
|
|
69
80
|
return { rows: redundantIndexesRows };
|
|
70
81
|
}
|
|
82
|
+
// D004: pg_stat_statements extension check
|
|
83
|
+
if (sql.includes("pg_extension") && sql.includes("pg_stat_statements")) {
|
|
84
|
+
return { rows: [] }; // Extension not installed
|
|
85
|
+
}
|
|
86
|
+
// D004: pg_stat_kcache extension check
|
|
87
|
+
if (sql.includes("pg_extension") && sql.includes("pg_stat_kcache")) {
|
|
88
|
+
return { rows: [] }; // Extension not installed
|
|
89
|
+
}
|
|
90
|
+
// G001: Memory settings query
|
|
91
|
+
if (sql.includes("pg_size_bytes") && sql.includes("shared_buffers") && sql.includes("work_mem")) {
|
|
92
|
+
return { rows: [{
|
|
93
|
+
shared_buffers_bytes: "134217728",
|
|
94
|
+
wal_buffers_bytes: "4194304",
|
|
95
|
+
work_mem_bytes: "4194304",
|
|
96
|
+
maintenance_work_mem_bytes: "67108864",
|
|
97
|
+
effective_cache_size_bytes: "4294967296",
|
|
98
|
+
max_connections: 100,
|
|
99
|
+
}] };
|
|
100
|
+
}
|
|
71
101
|
throw new Error(`Unexpected query: ${sql}`);
|
|
72
102
|
},
|
|
73
103
|
};
|
|
@@ -186,3 +216,42 @@ describe("H004 schema validation", () => {
|
|
|
186
216
|
});
|
|
187
217
|
});
|
|
188
218
|
|
|
219
|
+
describe("D004 schema validation", () => {
|
|
220
|
+
test("D004 report validates against schema (extensions not installed)", async () => {
|
|
221
|
+
const mockClient = createMockClient();
|
|
222
|
+
const report = await checkup.REPORT_GENERATORS.D004(mockClient as any, "node-01");
|
|
223
|
+
|
|
224
|
+
const result = validateReport(report, "D004");
|
|
225
|
+
if (!result.valid) {
|
|
226
|
+
console.error("D004 validation errors:", result.errors);
|
|
227
|
+
}
|
|
228
|
+
expect(result.valid).toBe(true);
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
describe("F001 schema validation", () => {
|
|
233
|
+
test("F001 report validates against schema", async () => {
|
|
234
|
+
const mockClient = createMockClient();
|
|
235
|
+
const report = await checkup.REPORT_GENERATORS.F001(mockClient as any, "node-01");
|
|
236
|
+
|
|
237
|
+
const result = validateReport(report, "F001");
|
|
238
|
+
if (!result.valid) {
|
|
239
|
+
console.error("F001 validation errors:", result.errors);
|
|
240
|
+
}
|
|
241
|
+
expect(result.valid).toBe(true);
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
describe("G001 schema validation", () => {
|
|
246
|
+
test("G001 report validates against schema", async () => {
|
|
247
|
+
const mockClient = createMockClient();
|
|
248
|
+
const report = await checkup.REPORT_GENERATORS.G001(mockClient as any, "node-01");
|
|
249
|
+
|
|
250
|
+
const result = validateReport(report, "G001");
|
|
251
|
+
if (!result.valid) {
|
|
252
|
+
console.error("G001 validation errors:", result.errors);
|
|
253
|
+
}
|
|
254
|
+
expect(result.valid).toBe(true);
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
|