postgresai 0.14.0-dev.51 → 0.14.0-dev.53
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/bin/postgres-ai.ts +0 -324
- package/bun.lock +1 -3
- package/dist/bin/postgres-ai.js +171 -1712
- package/dist/sql/01.role.sql +16 -0
- package/dist/sql/02.permissions.sql +37 -0
- package/dist/sql/03.optional_rds.sql +6 -0
- package/dist/sql/04.optional_self_managed.sql +8 -0
- package/dist/sql/05.helpers.sql +415 -0
- package/lib/auth-server.ts +75 -65
- package/lib/config.ts +0 -3
- package/package.json +2 -4
- package/test/init.integration.test.ts +6 -6
- package/lib/checkup-api.ts +0 -175
- package/lib/checkup.ts +0 -1141
- package/lib/metrics-loader.ts +0 -514
- package/test/checkup.test.ts +0 -1016
- package/test/schema-validation.test.ts +0 -260
package/test/checkup.test.ts
DELETED
|
@@ -1,1016 +0,0 @@
|
|
|
1
|
-
import { describe, test, expect } from "bun:test";
|
|
2
|
-
import { resolve } from "path";
|
|
3
|
-
|
|
4
|
-
// Import from source directly since we're using Bun
|
|
5
|
-
import * as checkup from "../lib/checkup";
|
|
6
|
-
import * as api from "../lib/checkup-api";
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
function runCli(args: string[], env: Record<string, string> = {}) {
|
|
10
|
-
const cliPath = resolve(import.meta.dir, "..", "bin", "postgres-ai.ts");
|
|
11
|
-
const bunBin = typeof process.execPath === "string" && process.execPath.length > 0 ? process.execPath : "bun";
|
|
12
|
-
const result = Bun.spawnSync([bunBin, cliPath, ...args], {
|
|
13
|
-
env: { ...process.env, ...env },
|
|
14
|
-
});
|
|
15
|
-
return {
|
|
16
|
-
status: result.exitCode,
|
|
17
|
-
stdout: new TextDecoder().decode(result.stdout),
|
|
18
|
-
stderr: new TextDecoder().decode(result.stderr),
|
|
19
|
-
};
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
// Mock client for testing report generators
|
|
23
|
-
interface MockClientOptions {
|
|
24
|
-
alteredSettingsRows?: any[];
|
|
25
|
-
databaseSizesRows?: any[];
|
|
26
|
-
clusterStatsRows?: any[];
|
|
27
|
-
connectionStatesRows?: any[];
|
|
28
|
-
uptimeRows?: any[];
|
|
29
|
-
invalidIndexesRows?: any[];
|
|
30
|
-
unusedIndexesRows?: any[];
|
|
31
|
-
redundantIndexesRows?: any[];
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function createMockClient(versionRows: any[], settingsRows: any[], options: MockClientOptions = {}) {
|
|
35
|
-
const {
|
|
36
|
-
alteredSettingsRows = [],
|
|
37
|
-
databaseSizesRows = [],
|
|
38
|
-
clusterStatsRows = [],
|
|
39
|
-
connectionStatesRows = [],
|
|
40
|
-
uptimeRows = [],
|
|
41
|
-
invalidIndexesRows = [],
|
|
42
|
-
unusedIndexesRows = [],
|
|
43
|
-
redundantIndexesRows = [],
|
|
44
|
-
} = options;
|
|
45
|
-
|
|
46
|
-
return {
|
|
47
|
-
query: async (sql: string) => {
|
|
48
|
-
// Version query (used by many reports)
|
|
49
|
-
if (sql.includes("server_version") && sql.includes("server_version_num") && !sql.includes("order by")) {
|
|
50
|
-
return { rows: versionRows };
|
|
51
|
-
}
|
|
52
|
-
// Full settings query (A003)
|
|
53
|
-
if (sql.includes("pg_settings") && sql.includes("order by") && sql.includes("is_default")) {
|
|
54
|
-
return { rows: settingsRows };
|
|
55
|
-
}
|
|
56
|
-
// Altered settings query (A007)
|
|
57
|
-
if (sql.includes("pg_settings") && sql.includes("where source <> 'default'")) {
|
|
58
|
-
return { rows: alteredSettingsRows };
|
|
59
|
-
}
|
|
60
|
-
// Database sizes (A004)
|
|
61
|
-
if (sql.includes("pg_database") && sql.includes("pg_database_size") && !sql.includes("current_database")) {
|
|
62
|
-
return { rows: databaseSizesRows };
|
|
63
|
-
}
|
|
64
|
-
// Current database info (H001, H002, H004)
|
|
65
|
-
if (sql.includes("current_database()") && sql.includes("pg_database_size")) {
|
|
66
|
-
return { rows: [{ datname: "testdb", size_bytes: "1073741824" }] };
|
|
67
|
-
}
|
|
68
|
-
// Stats reset info (H002)
|
|
69
|
-
if (sql.includes("stats_reset") && sql.includes("pg_stat_database")) {
|
|
70
|
-
return { rows: [{
|
|
71
|
-
stats_reset_epoch: "1704067200",
|
|
72
|
-
stats_reset_time: "2024-01-01 00:00:00+00",
|
|
73
|
-
days_since_reset: "30",
|
|
74
|
-
postmaster_startup_epoch: "1704067200",
|
|
75
|
-
postmaster_startup_time: "2024-01-01 00:00:00+00"
|
|
76
|
-
}] };
|
|
77
|
-
}
|
|
78
|
-
// Cluster stats (A004)
|
|
79
|
-
if (sql.includes("pg_stat_database") && sql.includes("xact_commit")) {
|
|
80
|
-
return { rows: clusterStatsRows };
|
|
81
|
-
}
|
|
82
|
-
// Connection states (A004)
|
|
83
|
-
if (sql.includes("pg_stat_activity") && sql.includes("state")) {
|
|
84
|
-
return { rows: connectionStatesRows };
|
|
85
|
-
}
|
|
86
|
-
// Uptime info (A004)
|
|
87
|
-
if (sql.includes("pg_postmaster_start_time")) {
|
|
88
|
-
return { rows: uptimeRows };
|
|
89
|
-
}
|
|
90
|
-
// Invalid indexes (H001)
|
|
91
|
-
if (sql.includes("indisvalid = false")) {
|
|
92
|
-
return { rows: invalidIndexesRows };
|
|
93
|
-
}
|
|
94
|
-
// Unused indexes (H002)
|
|
95
|
-
if (sql.includes("Never Used Indexes") && sql.includes("idx_scan = 0")) {
|
|
96
|
-
return { rows: unusedIndexesRows };
|
|
97
|
-
}
|
|
98
|
-
// Redundant indexes (H004)
|
|
99
|
-
if (sql.includes("redundant_indexes") && sql.includes("columns like")) {
|
|
100
|
-
return { rows: redundantIndexesRows };
|
|
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
|
-
}
|
|
121
|
-
throw new Error(`Unexpected query: ${sql}`);
|
|
122
|
-
},
|
|
123
|
-
};
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// Unit tests for parseVersionNum
|
|
127
|
-
describe("parseVersionNum", () => {
|
|
128
|
-
test("parses PG 16.3 version number", () => {
|
|
129
|
-
const result = checkup.parseVersionNum("160003");
|
|
130
|
-
expect(result.major).toBe("16");
|
|
131
|
-
expect(result.minor).toBe("3");
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
test("parses PG 15.7 version number", () => {
|
|
135
|
-
const result = checkup.parseVersionNum("150007");
|
|
136
|
-
expect(result.major).toBe("15");
|
|
137
|
-
expect(result.minor).toBe("7");
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
test("parses PG 14.12 version number", () => {
|
|
141
|
-
const result = checkup.parseVersionNum("140012");
|
|
142
|
-
expect(result.major).toBe("14");
|
|
143
|
-
expect(result.minor).toBe("12");
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
test("handles empty string", () => {
|
|
147
|
-
const result = checkup.parseVersionNum("");
|
|
148
|
-
expect(result.major).toBe("");
|
|
149
|
-
expect(result.minor).toBe("");
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
test("handles null/undefined", () => {
|
|
153
|
-
const result = checkup.parseVersionNum(null as any);
|
|
154
|
-
expect(result.major).toBe("");
|
|
155
|
-
expect(result.minor).toBe("");
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
test("handles short string", () => {
|
|
159
|
-
const result = checkup.parseVersionNum("123");
|
|
160
|
-
expect(result.major).toBe("");
|
|
161
|
-
expect(result.minor).toBe("");
|
|
162
|
-
});
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
// Unit tests for createBaseReport
|
|
166
|
-
describe("createBaseReport", () => {
|
|
167
|
-
test("creates correct structure", () => {
|
|
168
|
-
const report = checkup.createBaseReport("A002", "Postgres major version", "test-node");
|
|
169
|
-
|
|
170
|
-
expect(report.checkId).toBe("A002");
|
|
171
|
-
expect(report.checkTitle).toBe("Postgres major version");
|
|
172
|
-
expect(typeof report.version).toBe("string");
|
|
173
|
-
expect(report.version!.length).toBeGreaterThan(0);
|
|
174
|
-
expect(typeof report.build_ts).toBe("string");
|
|
175
|
-
expect(report.nodes.primary).toBe("test-node");
|
|
176
|
-
expect(report.nodes.standbys).toEqual([]);
|
|
177
|
-
expect(report.results).toEqual({});
|
|
178
|
-
expect(typeof report.timestamptz).toBe("string");
|
|
179
|
-
// Verify timestamp is ISO format
|
|
180
|
-
expect(new Date(report.timestamptz).toISOString()).toBe(report.timestamptz);
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
test("uses provided node name", () => {
|
|
184
|
-
const report = checkup.createBaseReport("A003", "Postgres settings", "my-custom-node");
|
|
185
|
-
expect(report.nodes.primary).toBe("my-custom-node");
|
|
186
|
-
});
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
// Tests for CHECK_INFO
|
|
190
|
-
describe("CHECK_INFO", () => {
|
|
191
|
-
test("contains A002", () => {
|
|
192
|
-
expect("A002" in checkup.CHECK_INFO).toBe(true);
|
|
193
|
-
expect(checkup.CHECK_INFO.A002).toBe("Postgres major version");
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
test("contains A003", () => {
|
|
197
|
-
expect("A003" in checkup.CHECK_INFO).toBe(true);
|
|
198
|
-
expect(checkup.CHECK_INFO.A003).toBe("Postgres settings");
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
test("contains A013", () => {
|
|
202
|
-
expect("A013" in checkup.CHECK_INFO).toBe(true);
|
|
203
|
-
expect(checkup.CHECK_INFO.A013).toBe("Postgres minor version");
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
test("contains A004", () => {
|
|
207
|
-
expect("A004" in checkup.CHECK_INFO).toBe(true);
|
|
208
|
-
expect(checkup.CHECK_INFO.A004).toBe("Cluster information");
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
test("contains A007", () => {
|
|
212
|
-
expect("A007" in checkup.CHECK_INFO).toBe(true);
|
|
213
|
-
expect(checkup.CHECK_INFO.A007).toBe("Altered settings");
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
test("contains H001", () => {
|
|
217
|
-
expect("H001" in checkup.CHECK_INFO).toBe(true);
|
|
218
|
-
expect(checkup.CHECK_INFO.H001).toBe("Invalid indexes");
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
test("contains H002", () => {
|
|
222
|
-
expect("H002" in checkup.CHECK_INFO).toBe(true);
|
|
223
|
-
expect(checkup.CHECK_INFO.H002).toBe("Unused indexes");
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
test("contains H004", () => {
|
|
227
|
-
expect("H004" in checkup.CHECK_INFO).toBe(true);
|
|
228
|
-
expect(checkup.CHECK_INFO.H004).toBe("Redundant indexes");
|
|
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
|
-
});
|
|
245
|
-
});
|
|
246
|
-
|
|
247
|
-
// Tests for REPORT_GENERATORS
|
|
248
|
-
describe("REPORT_GENERATORS", () => {
|
|
249
|
-
test("has generator for A002", () => {
|
|
250
|
-
expect("A002" in checkup.REPORT_GENERATORS).toBe(true);
|
|
251
|
-
expect(typeof checkup.REPORT_GENERATORS.A002).toBe("function");
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
test("has generator for A003", () => {
|
|
255
|
-
expect("A003" in checkup.REPORT_GENERATORS).toBe(true);
|
|
256
|
-
expect(typeof checkup.REPORT_GENERATORS.A003).toBe("function");
|
|
257
|
-
});
|
|
258
|
-
|
|
259
|
-
test("has generator for A013", () => {
|
|
260
|
-
expect("A013" in checkup.REPORT_GENERATORS).toBe(true);
|
|
261
|
-
expect(typeof checkup.REPORT_GENERATORS.A013).toBe("function");
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
test("has generator for A004", () => {
|
|
265
|
-
expect("A004" in checkup.REPORT_GENERATORS).toBe(true);
|
|
266
|
-
expect(typeof checkup.REPORT_GENERATORS.A004).toBe("function");
|
|
267
|
-
});
|
|
268
|
-
|
|
269
|
-
test("has generator for A007", () => {
|
|
270
|
-
expect("A007" in checkup.REPORT_GENERATORS).toBe(true);
|
|
271
|
-
expect(typeof checkup.REPORT_GENERATORS.A007).toBe("function");
|
|
272
|
-
});
|
|
273
|
-
|
|
274
|
-
test("has generator for H001", () => {
|
|
275
|
-
expect("H001" in checkup.REPORT_GENERATORS).toBe(true);
|
|
276
|
-
expect(typeof checkup.REPORT_GENERATORS.H001).toBe("function");
|
|
277
|
-
});
|
|
278
|
-
|
|
279
|
-
test("has generator for H002", () => {
|
|
280
|
-
expect("H002" in checkup.REPORT_GENERATORS).toBe(true);
|
|
281
|
-
expect(typeof checkup.REPORT_GENERATORS.H002).toBe("function");
|
|
282
|
-
});
|
|
283
|
-
|
|
284
|
-
test("has generator for H004", () => {
|
|
285
|
-
expect("H004" in checkup.REPORT_GENERATORS).toBe(true);
|
|
286
|
-
expect(typeof checkup.REPORT_GENERATORS.H004).toBe("function");
|
|
287
|
-
});
|
|
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
|
-
|
|
304
|
-
test("REPORT_GENERATORS and CHECK_INFO have same keys", () => {
|
|
305
|
-
const generatorKeys = Object.keys(checkup.REPORT_GENERATORS).sort();
|
|
306
|
-
const infoKeys = Object.keys(checkup.CHECK_INFO).sort();
|
|
307
|
-
expect(generatorKeys).toEqual(infoKeys);
|
|
308
|
-
});
|
|
309
|
-
});
|
|
310
|
-
|
|
311
|
-
// Tests for formatBytes
|
|
312
|
-
describe("formatBytes", () => {
|
|
313
|
-
test("formats zero bytes", () => {
|
|
314
|
-
expect(checkup.formatBytes(0)).toBe("0 B");
|
|
315
|
-
});
|
|
316
|
-
|
|
317
|
-
test("formats bytes", () => {
|
|
318
|
-
expect(checkup.formatBytes(500)).toBe("500.00 B");
|
|
319
|
-
});
|
|
320
|
-
|
|
321
|
-
test("formats kibibytes", () => {
|
|
322
|
-
expect(checkup.formatBytes(1024)).toBe("1.00 KiB");
|
|
323
|
-
expect(checkup.formatBytes(1536)).toBe("1.50 KiB");
|
|
324
|
-
});
|
|
325
|
-
|
|
326
|
-
test("formats mebibytes", () => {
|
|
327
|
-
expect(checkup.formatBytes(1048576)).toBe("1.00 MiB");
|
|
328
|
-
});
|
|
329
|
-
|
|
330
|
-
test("formats gibibytes", () => {
|
|
331
|
-
expect(checkup.formatBytes(1073741824)).toBe("1.00 GiB");
|
|
332
|
-
});
|
|
333
|
-
});
|
|
334
|
-
|
|
335
|
-
// Mock client tests for report generators
|
|
336
|
-
describe("Report generators with mock client", () => {
|
|
337
|
-
test("getPostgresVersion extracts version info", async () => {
|
|
338
|
-
const mockClient = createMockClient([
|
|
339
|
-
{ name: "server_version", setting: "16.3" },
|
|
340
|
-
{ name: "server_version_num", setting: "160003" },
|
|
341
|
-
], []);
|
|
342
|
-
|
|
343
|
-
const version = await checkup.getPostgresVersion(mockClient as any);
|
|
344
|
-
expect(version.version).toBe("16.3");
|
|
345
|
-
expect(version.server_version_num).toBe("160003");
|
|
346
|
-
expect(version.server_major_ver).toBe("16");
|
|
347
|
-
expect(version.server_minor_ver).toBe("3");
|
|
348
|
-
});
|
|
349
|
-
|
|
350
|
-
test("getSettings transforms rows to keyed object", async () => {
|
|
351
|
-
const mockClient = createMockClient([], [
|
|
352
|
-
{
|
|
353
|
-
name: "shared_buffers",
|
|
354
|
-
setting: "16384",
|
|
355
|
-
unit: "8kB",
|
|
356
|
-
category: "Resource Usage / Memory",
|
|
357
|
-
context: "postmaster",
|
|
358
|
-
vartype: "integer",
|
|
359
|
-
pretty_value: "128 MB",
|
|
360
|
-
},
|
|
361
|
-
{
|
|
362
|
-
name: "work_mem",
|
|
363
|
-
setting: "4096",
|
|
364
|
-
unit: "kB",
|
|
365
|
-
category: "Resource Usage / Memory",
|
|
366
|
-
context: "user",
|
|
367
|
-
vartype: "integer",
|
|
368
|
-
pretty_value: "4 MB",
|
|
369
|
-
},
|
|
370
|
-
]);
|
|
371
|
-
|
|
372
|
-
const settings = await checkup.getSettings(mockClient as any);
|
|
373
|
-
expect("shared_buffers" in settings).toBe(true);
|
|
374
|
-
expect("work_mem" in settings).toBe(true);
|
|
375
|
-
expect(settings.shared_buffers.setting).toBe("16384");
|
|
376
|
-
expect(settings.shared_buffers.unit).toBe("8kB");
|
|
377
|
-
expect(settings.work_mem.pretty_value).toBe("4 MB");
|
|
378
|
-
});
|
|
379
|
-
|
|
380
|
-
test("generateA002 creates report with version data", async () => {
|
|
381
|
-
const mockClient = createMockClient([
|
|
382
|
-
{ name: "server_version", setting: "16.3" },
|
|
383
|
-
{ name: "server_version_num", setting: "160003" },
|
|
384
|
-
], []);
|
|
385
|
-
|
|
386
|
-
const report = await checkup.generateA002(mockClient as any, "test-node");
|
|
387
|
-
expect(report.checkId).toBe("A002");
|
|
388
|
-
expect(report.checkTitle).toBe("Postgres major version");
|
|
389
|
-
expect(report.nodes.primary).toBe("test-node");
|
|
390
|
-
expect("test-node" in report.results).toBe(true);
|
|
391
|
-
expect("version" in report.results["test-node"].data).toBe(true);
|
|
392
|
-
expect(report.results["test-node"].data.version.version).toBe("16.3");
|
|
393
|
-
});
|
|
394
|
-
|
|
395
|
-
test("generateA003 creates report with settings and version", async () => {
|
|
396
|
-
const mockClient = createMockClient(
|
|
397
|
-
[
|
|
398
|
-
{ name: "server_version", setting: "16.3" },
|
|
399
|
-
{ name: "server_version_num", setting: "160003" },
|
|
400
|
-
],
|
|
401
|
-
[
|
|
402
|
-
{
|
|
403
|
-
name: "shared_buffers",
|
|
404
|
-
setting: "16384",
|
|
405
|
-
unit: "8kB",
|
|
406
|
-
category: "Resource Usage / Memory",
|
|
407
|
-
context: "postmaster",
|
|
408
|
-
vartype: "integer",
|
|
409
|
-
pretty_value: "128 MB",
|
|
410
|
-
},
|
|
411
|
-
]
|
|
412
|
-
);
|
|
413
|
-
|
|
414
|
-
const report = await checkup.generateA003(mockClient as any, "test-node");
|
|
415
|
-
expect(report.checkId).toBe("A003");
|
|
416
|
-
expect(report.checkTitle).toBe("Postgres settings");
|
|
417
|
-
expect("test-node" in report.results).toBe(true);
|
|
418
|
-
expect("shared_buffers" in report.results["test-node"].data).toBe(true);
|
|
419
|
-
expect(report.results["test-node"].postgres_version).toBeTruthy();
|
|
420
|
-
expect(report.results["test-node"].postgres_version!.version).toBe("16.3");
|
|
421
|
-
});
|
|
422
|
-
|
|
423
|
-
test("generateA013 creates report with minor version data", async () => {
|
|
424
|
-
const mockClient = createMockClient([
|
|
425
|
-
{ name: "server_version", setting: "16.3" },
|
|
426
|
-
{ name: "server_version_num", setting: "160003" },
|
|
427
|
-
], []);
|
|
428
|
-
|
|
429
|
-
const report = await checkup.generateA013(mockClient as any, "test-node");
|
|
430
|
-
expect(report.checkId).toBe("A013");
|
|
431
|
-
expect(report.checkTitle).toBe("Postgres minor version");
|
|
432
|
-
expect(report.nodes.primary).toBe("test-node");
|
|
433
|
-
expect("test-node" in report.results).toBe(true);
|
|
434
|
-
expect("version" in report.results["test-node"].data).toBe(true);
|
|
435
|
-
expect(report.results["test-node"].data.version.server_minor_ver).toBe("3");
|
|
436
|
-
});
|
|
437
|
-
|
|
438
|
-
test("generateAllReports returns reports for all checks", async () => {
|
|
439
|
-
const mockClient = createMockClient(
|
|
440
|
-
[
|
|
441
|
-
{ name: "server_version", setting: "16.3" },
|
|
442
|
-
{ name: "server_version_num", setting: "160003" },
|
|
443
|
-
],
|
|
444
|
-
[
|
|
445
|
-
{
|
|
446
|
-
name: "shared_buffers",
|
|
447
|
-
setting: "16384",
|
|
448
|
-
unit: "8kB",
|
|
449
|
-
category: "Resource Usage / Memory",
|
|
450
|
-
context: "postmaster",
|
|
451
|
-
vartype: "integer",
|
|
452
|
-
pretty_value: "128 MB",
|
|
453
|
-
},
|
|
454
|
-
],
|
|
455
|
-
{
|
|
456
|
-
alteredSettingsRows: [
|
|
457
|
-
{ name: "shared_buffers", setting: "16384", unit: "8kB", category: "Resource Usage / Memory", pretty_value: "128 MB" },
|
|
458
|
-
],
|
|
459
|
-
databaseSizesRows: [{ datname: "postgres", size_bytes: "1073741824" }],
|
|
460
|
-
clusterStatsRows: [{ total_connections: 5, total_commits: 100, total_rollbacks: 1, blocks_read: 1000, blocks_hit: 9000, tuples_returned: 500, tuples_fetched: 400, tuples_inserted: 50, tuples_updated: 30, tuples_deleted: 10, total_deadlocks: 0, temp_files_created: 0, temp_bytes_written: 0 }],
|
|
461
|
-
connectionStatesRows: [{ state: "active", count: 2 }, { state: "idle", count: 3 }],
|
|
462
|
-
uptimeRows: [{ start_time: new Date("2024-01-01T00:00:00Z"), uptime: "10 days" }],
|
|
463
|
-
invalidIndexesRows: [],
|
|
464
|
-
unusedIndexesRows: [],
|
|
465
|
-
redundantIndexesRows: [],
|
|
466
|
-
}
|
|
467
|
-
);
|
|
468
|
-
|
|
469
|
-
const reports = await checkup.generateAllReports(mockClient as any, "test-node");
|
|
470
|
-
expect("A002" in reports).toBe(true);
|
|
471
|
-
expect("A003" in reports).toBe(true);
|
|
472
|
-
expect("A004" in reports).toBe(true);
|
|
473
|
-
expect("A007" in reports).toBe(true);
|
|
474
|
-
expect("A013" in reports).toBe(true);
|
|
475
|
-
expect("H001" in reports).toBe(true);
|
|
476
|
-
expect("H002" in reports).toBe(true);
|
|
477
|
-
expect("H004" in reports).toBe(true);
|
|
478
|
-
expect(reports.A002.checkId).toBe("A002");
|
|
479
|
-
expect(reports.A003.checkId).toBe("A003");
|
|
480
|
-
expect(reports.A004.checkId).toBe("A004");
|
|
481
|
-
expect(reports.A007.checkId).toBe("A007");
|
|
482
|
-
expect(reports.A013.checkId).toBe("A013");
|
|
483
|
-
expect(reports.H001.checkId).toBe("H001");
|
|
484
|
-
expect(reports.H002.checkId).toBe("H002");
|
|
485
|
-
expect(reports.H004.checkId).toBe("H004");
|
|
486
|
-
});
|
|
487
|
-
});
|
|
488
|
-
|
|
489
|
-
// Tests for A007 (Altered settings)
|
|
490
|
-
describe("A007 - Altered settings", () => {
|
|
491
|
-
test("getAlteredSettings returns non-default settings", async () => {
|
|
492
|
-
const mockClient = createMockClient([], [], {
|
|
493
|
-
alteredSettingsRows: [
|
|
494
|
-
{ name: "shared_buffers", setting: "256MB", unit: "", category: "Resource Usage / Memory", pretty_value: "256 MB" },
|
|
495
|
-
{ name: "work_mem", setting: "64MB", unit: "", category: "Resource Usage / Memory", pretty_value: "64 MB" },
|
|
496
|
-
],
|
|
497
|
-
});
|
|
498
|
-
|
|
499
|
-
const settings = await checkup.getAlteredSettings(mockClient as any);
|
|
500
|
-
expect("shared_buffers" in settings).toBe(true);
|
|
501
|
-
expect("work_mem" in settings).toBe(true);
|
|
502
|
-
expect(settings.shared_buffers.value).toBe("256MB");
|
|
503
|
-
expect(settings.work_mem.pretty_value).toBe("64 MB");
|
|
504
|
-
});
|
|
505
|
-
|
|
506
|
-
test("generateA007 creates report with altered settings", async () => {
|
|
507
|
-
const mockClient = createMockClient(
|
|
508
|
-
[
|
|
509
|
-
{ name: "server_version", setting: "16.3" },
|
|
510
|
-
{ name: "server_version_num", setting: "160003" },
|
|
511
|
-
],
|
|
512
|
-
[],
|
|
513
|
-
{
|
|
514
|
-
alteredSettingsRows: [
|
|
515
|
-
{ name: "max_connections", setting: "200", unit: "", category: "Connections and Authentication", pretty_value: "200" },
|
|
516
|
-
],
|
|
517
|
-
}
|
|
518
|
-
);
|
|
519
|
-
|
|
520
|
-
const report = await checkup.generateA007(mockClient as any, "test-node");
|
|
521
|
-
expect(report.checkId).toBe("A007");
|
|
522
|
-
expect(report.checkTitle).toBe("Altered settings");
|
|
523
|
-
expect(report.nodes.primary).toBe("test-node");
|
|
524
|
-
expect("test-node" in report.results).toBe(true);
|
|
525
|
-
expect("max_connections" in report.results["test-node"].data).toBe(true);
|
|
526
|
-
expect(report.results["test-node"].data.max_connections.value).toBe("200");
|
|
527
|
-
expect(report.results["test-node"].postgres_version).toBeTruthy();
|
|
528
|
-
});
|
|
529
|
-
});
|
|
530
|
-
|
|
531
|
-
// Tests for A004 (Cluster information)
|
|
532
|
-
describe("A004 - Cluster information", () => {
|
|
533
|
-
test("getDatabaseSizes returns database sizes", async () => {
|
|
534
|
-
const mockClient = createMockClient([], [], {
|
|
535
|
-
databaseSizesRows: [
|
|
536
|
-
{ datname: "postgres", size_bytes: "1073741824" },
|
|
537
|
-
{ datname: "mydb", size_bytes: "536870912" },
|
|
538
|
-
],
|
|
539
|
-
});
|
|
540
|
-
|
|
541
|
-
const sizes = await checkup.getDatabaseSizes(mockClient as any);
|
|
542
|
-
expect("postgres" in sizes).toBe(true);
|
|
543
|
-
expect("mydb" in sizes).toBe(true);
|
|
544
|
-
expect(sizes.postgres).toBe(1073741824);
|
|
545
|
-
expect(sizes.mydb).toBe(536870912);
|
|
546
|
-
});
|
|
547
|
-
|
|
548
|
-
test("getClusterInfo returns cluster metrics", async () => {
|
|
549
|
-
const mockClient = createMockClient([], [], {
|
|
550
|
-
clusterStatsRows: [{
|
|
551
|
-
total_connections: 10,
|
|
552
|
-
total_commits: 1000,
|
|
553
|
-
total_rollbacks: 5,
|
|
554
|
-
blocks_read: 500,
|
|
555
|
-
blocks_hit: 9500,
|
|
556
|
-
tuples_returned: 5000,
|
|
557
|
-
tuples_fetched: 4000,
|
|
558
|
-
tuples_inserted: 100,
|
|
559
|
-
tuples_updated: 50,
|
|
560
|
-
tuples_deleted: 25,
|
|
561
|
-
total_deadlocks: 0,
|
|
562
|
-
temp_files_created: 2,
|
|
563
|
-
temp_bytes_written: 1048576,
|
|
564
|
-
}],
|
|
565
|
-
connectionStatesRows: [
|
|
566
|
-
{ state: "active", count: 3 },
|
|
567
|
-
{ state: "idle", count: 7 },
|
|
568
|
-
],
|
|
569
|
-
uptimeRows: [{
|
|
570
|
-
start_time: new Date("2024-01-01T00:00:00Z"),
|
|
571
|
-
uptime: "30 days",
|
|
572
|
-
}],
|
|
573
|
-
});
|
|
574
|
-
|
|
575
|
-
const info = await checkup.getClusterInfo(mockClient as any);
|
|
576
|
-
expect("total_connections" in info).toBe(true);
|
|
577
|
-
expect("cache_hit_ratio" in info).toBe(true);
|
|
578
|
-
expect("connections_active" in info).toBe(true);
|
|
579
|
-
expect("connections_idle" in info).toBe(true);
|
|
580
|
-
expect("start_time" in info).toBe(true);
|
|
581
|
-
expect(info.total_connections.value).toBe("10");
|
|
582
|
-
expect(info.cache_hit_ratio.value).toBe("95.00");
|
|
583
|
-
expect(info.connections_active.value).toBe("3");
|
|
584
|
-
});
|
|
585
|
-
|
|
586
|
-
test("generateA004 creates report with cluster info and database sizes", async () => {
|
|
587
|
-
const mockClient = createMockClient(
|
|
588
|
-
[
|
|
589
|
-
{ name: "server_version", setting: "16.3" },
|
|
590
|
-
{ name: "server_version_num", setting: "160003" },
|
|
591
|
-
],
|
|
592
|
-
[],
|
|
593
|
-
{
|
|
594
|
-
databaseSizesRows: [
|
|
595
|
-
{ datname: "postgres", size_bytes: "1073741824" },
|
|
596
|
-
],
|
|
597
|
-
clusterStatsRows: [{
|
|
598
|
-
total_connections: 5,
|
|
599
|
-
total_commits: 100,
|
|
600
|
-
total_rollbacks: 1,
|
|
601
|
-
blocks_read: 100,
|
|
602
|
-
blocks_hit: 900,
|
|
603
|
-
tuples_returned: 500,
|
|
604
|
-
tuples_fetched: 400,
|
|
605
|
-
tuples_inserted: 50,
|
|
606
|
-
tuples_updated: 30,
|
|
607
|
-
tuples_deleted: 10,
|
|
608
|
-
total_deadlocks: 0,
|
|
609
|
-
temp_files_created: 0,
|
|
610
|
-
temp_bytes_written: 0,
|
|
611
|
-
}],
|
|
612
|
-
connectionStatesRows: [{ state: "active", count: 2 }],
|
|
613
|
-
uptimeRows: [{ start_time: new Date("2024-01-01T00:00:00Z"), uptime: "10 days" }],
|
|
614
|
-
}
|
|
615
|
-
);
|
|
616
|
-
|
|
617
|
-
const report = await checkup.generateA004(mockClient as any, "test-node");
|
|
618
|
-
expect(report.checkId).toBe("A004");
|
|
619
|
-
expect(report.checkTitle).toBe("Cluster information");
|
|
620
|
-
expect(report.nodes.primary).toBe("test-node");
|
|
621
|
-
expect("test-node" in report.results).toBe(true);
|
|
622
|
-
|
|
623
|
-
const data = report.results["test-node"].data;
|
|
624
|
-
expect("general_info" in data).toBe(true);
|
|
625
|
-
expect("database_sizes" in data).toBe(true);
|
|
626
|
-
expect("total_connections" in data.general_info).toBe(true);
|
|
627
|
-
expect("postgres" in data.database_sizes).toBe(true);
|
|
628
|
-
expect(data.database_sizes.postgres).toBe(1073741824);
|
|
629
|
-
expect(report.results["test-node"].postgres_version).toBeTruthy();
|
|
630
|
-
});
|
|
631
|
-
});
|
|
632
|
-
|
|
633
|
-
// Tests for H001 (Invalid indexes)
|
|
634
|
-
describe("H001 - Invalid indexes", () => {
|
|
635
|
-
test("getInvalidIndexes returns invalid indexes", async () => {
|
|
636
|
-
const mockClient = createMockClient([], [], {
|
|
637
|
-
invalidIndexesRows: [
|
|
638
|
-
{ schema_name: "public", table_name: "users", index_name: "users_email_idx", relation_name: "users", index_size_bytes: "1048576", supports_fk: false },
|
|
639
|
-
],
|
|
640
|
-
});
|
|
641
|
-
|
|
642
|
-
const indexes = await checkup.getInvalidIndexes(mockClient as any);
|
|
643
|
-
expect(indexes.length).toBe(1);
|
|
644
|
-
expect(indexes[0].schema_name).toBe("public");
|
|
645
|
-
expect(indexes[0].table_name).toBe("users");
|
|
646
|
-
expect(indexes[0].index_name).toBe("users_email_idx");
|
|
647
|
-
expect(indexes[0].index_size_bytes).toBe(1048576);
|
|
648
|
-
expect(indexes[0].index_size_pretty).toBeTruthy();
|
|
649
|
-
expect(indexes[0].relation_name).toBe("users");
|
|
650
|
-
expect(indexes[0].supports_fk).toBe(false);
|
|
651
|
-
});
|
|
652
|
-
|
|
653
|
-
test("generateH001 creates report with invalid indexes", async () => {
|
|
654
|
-
const mockClient = createMockClient(
|
|
655
|
-
[
|
|
656
|
-
{ name: "server_version", setting: "16.3" },
|
|
657
|
-
{ name: "server_version_num", setting: "160003" },
|
|
658
|
-
],
|
|
659
|
-
[],
|
|
660
|
-
{
|
|
661
|
-
invalidIndexesRows: [
|
|
662
|
-
{ schema_name: "public", table_name: "orders", index_name: "orders_status_idx", relation_name: "orders", index_size_bytes: "2097152", supports_fk: false },
|
|
663
|
-
],
|
|
664
|
-
}
|
|
665
|
-
);
|
|
666
|
-
|
|
667
|
-
const report = await checkup.generateH001(mockClient as any, "test-node");
|
|
668
|
-
expect(report.checkId).toBe("H001");
|
|
669
|
-
expect(report.checkTitle).toBe("Invalid indexes");
|
|
670
|
-
expect("test-node" in report.results).toBe(true);
|
|
671
|
-
|
|
672
|
-
// Data is now keyed by database name
|
|
673
|
-
const data = report.results["test-node"].data;
|
|
674
|
-
expect("testdb" in data).toBe(true);
|
|
675
|
-
const dbData = data["testdb"] as any;
|
|
676
|
-
expect(dbData.invalid_indexes).toBeTruthy();
|
|
677
|
-
expect(dbData.total_count).toBe(1);
|
|
678
|
-
expect(dbData.total_size_bytes).toBe(2097152);
|
|
679
|
-
expect(dbData.total_size_pretty).toBeTruthy();
|
|
680
|
-
expect(dbData.database_size_bytes).toBeTruthy();
|
|
681
|
-
expect(dbData.database_size_pretty).toBeTruthy();
|
|
682
|
-
expect(report.results["test-node"].postgres_version).toBeTruthy();
|
|
683
|
-
});
|
|
684
|
-
|
|
685
|
-
test("generateH001 has correct top-level structure", async () => {
|
|
686
|
-
const mockClient = createMockClient(
|
|
687
|
-
[
|
|
688
|
-
{ name: "server_version", setting: "16.3" },
|
|
689
|
-
{ name: "server_version_num", setting: "160003" },
|
|
690
|
-
],
|
|
691
|
-
[],
|
|
692
|
-
{
|
|
693
|
-
invalidIndexesRows: [
|
|
694
|
-
{ schema_name: "public", table_name: "orders", index_name: "orders_status_idx", relation_name: "orders", index_size_bytes: "2097152", supports_fk: false },
|
|
695
|
-
],
|
|
696
|
-
}
|
|
697
|
-
);
|
|
698
|
-
|
|
699
|
-
const report = await checkup.generateH001(mockClient as any, "test-node");
|
|
700
|
-
|
|
701
|
-
// Verify top-level structure matches schema expectations
|
|
702
|
-
expect(report.checkId).toBe("H001");
|
|
703
|
-
expect(report.checkTitle).toBe("Invalid indexes");
|
|
704
|
-
expect(typeof report.timestamptz).toBe("string");
|
|
705
|
-
expect(report.nodes.primary).toBe("test-node");
|
|
706
|
-
expect(Array.isArray(report.nodes.standbys)).toBe(true);
|
|
707
|
-
expect("test-node" in report.results).toBe(true);
|
|
708
|
-
expect(report.results["test-node"].postgres_version).toBeTruthy();
|
|
709
|
-
// Data is now keyed by database name
|
|
710
|
-
expect("testdb" in report.results["test-node"].data).toBe(true);
|
|
711
|
-
expect((report.results["test-node"].data as any)["testdb"].invalid_indexes).toBeTruthy();
|
|
712
|
-
});
|
|
713
|
-
});
|
|
714
|
-
|
|
715
|
-
// Tests for H002 (Unused indexes)
|
|
716
|
-
describe("H002 - Unused indexes", () => {
|
|
717
|
-
test("getUnusedIndexes returns unused indexes", async () => {
|
|
718
|
-
const mockClient = createMockClient([], [], {
|
|
719
|
-
unusedIndexesRows: [
|
|
720
|
-
{
|
|
721
|
-
schema_name: "public",
|
|
722
|
-
table_name: "products",
|
|
723
|
-
index_name: "products_old_idx",
|
|
724
|
-
index_definition: "CREATE INDEX products_old_idx ON public.products USING btree (old_column)",
|
|
725
|
-
reason: "Never Used Indexes",
|
|
726
|
-
index_size_bytes: "4194304",
|
|
727
|
-
idx_scan: "0",
|
|
728
|
-
idx_is_btree: true,
|
|
729
|
-
supports_fk: false,
|
|
730
|
-
},
|
|
731
|
-
],
|
|
732
|
-
});
|
|
733
|
-
|
|
734
|
-
const indexes = await checkup.getUnusedIndexes(mockClient as any);
|
|
735
|
-
expect(indexes.length).toBe(1);
|
|
736
|
-
expect(indexes[0].schema_name).toBe("public");
|
|
737
|
-
expect(indexes[0].index_name).toBe("products_old_idx");
|
|
738
|
-
expect(indexes[0].index_size_bytes).toBe(4194304);
|
|
739
|
-
expect(indexes[0].idx_scan).toBe(0);
|
|
740
|
-
expect(indexes[0].supports_fk).toBe(false);
|
|
741
|
-
expect(indexes[0].index_definition).toBeTruthy();
|
|
742
|
-
expect(indexes[0].idx_is_btree).toBe(true);
|
|
743
|
-
});
|
|
744
|
-
|
|
745
|
-
test("generateH002 creates report with unused indexes", async () => {
|
|
746
|
-
const mockClient = createMockClient(
|
|
747
|
-
[
|
|
748
|
-
{ name: "server_version", setting: "16.3" },
|
|
749
|
-
{ name: "server_version_num", setting: "160003" },
|
|
750
|
-
],
|
|
751
|
-
[],
|
|
752
|
-
{
|
|
753
|
-
unusedIndexesRows: [
|
|
754
|
-
{
|
|
755
|
-
schema_name: "public",
|
|
756
|
-
table_name: "logs",
|
|
757
|
-
index_name: "logs_created_idx",
|
|
758
|
-
index_definition: "CREATE INDEX logs_created_idx ON public.logs USING btree (created_at)",
|
|
759
|
-
reason: "Never Used Indexes",
|
|
760
|
-
index_size_bytes: "8388608",
|
|
761
|
-
idx_scan: "0",
|
|
762
|
-
idx_is_btree: true,
|
|
763
|
-
supports_fk: false,
|
|
764
|
-
},
|
|
765
|
-
],
|
|
766
|
-
}
|
|
767
|
-
);
|
|
768
|
-
|
|
769
|
-
const report = await checkup.generateH002(mockClient as any, "test-node");
|
|
770
|
-
expect(report.checkId).toBe("H002");
|
|
771
|
-
expect(report.checkTitle).toBe("Unused indexes");
|
|
772
|
-
expect("test-node" in report.results).toBe(true);
|
|
773
|
-
|
|
774
|
-
// Data is now keyed by database name
|
|
775
|
-
const data = report.results["test-node"].data;
|
|
776
|
-
expect("testdb" in data).toBe(true);
|
|
777
|
-
const dbData = data["testdb"] as any;
|
|
778
|
-
expect(dbData.unused_indexes).toBeTruthy();
|
|
779
|
-
expect(dbData.total_count).toBe(1);
|
|
780
|
-
expect(dbData.total_size_bytes).toBe(8388608);
|
|
781
|
-
expect(dbData.total_size_pretty).toBeTruthy();
|
|
782
|
-
expect(dbData.stats_reset).toBeTruthy();
|
|
783
|
-
expect(report.results["test-node"].postgres_version).toBeTruthy();
|
|
784
|
-
});
|
|
785
|
-
|
|
786
|
-
test("generateH002 has correct top-level structure", async () => {
|
|
787
|
-
const mockClient = createMockClient(
|
|
788
|
-
[
|
|
789
|
-
{ name: "server_version", setting: "16.3" },
|
|
790
|
-
{ name: "server_version_num", setting: "160003" },
|
|
791
|
-
],
|
|
792
|
-
[],
|
|
793
|
-
{
|
|
794
|
-
unusedIndexesRows: [
|
|
795
|
-
{
|
|
796
|
-
schema_name: "public",
|
|
797
|
-
table_name: "logs",
|
|
798
|
-
index_name: "logs_created_idx",
|
|
799
|
-
index_definition: "CREATE INDEX logs_created_idx ON public.logs USING btree (created_at)",
|
|
800
|
-
reason: "Never Used Indexes",
|
|
801
|
-
index_size_bytes: "8388608",
|
|
802
|
-
idx_scan: "0",
|
|
803
|
-
idx_is_btree: true,
|
|
804
|
-
supports_fk: false,
|
|
805
|
-
},
|
|
806
|
-
],
|
|
807
|
-
}
|
|
808
|
-
);
|
|
809
|
-
|
|
810
|
-
const report = await checkup.generateH002(mockClient as any, "test-node");
|
|
811
|
-
|
|
812
|
-
// Verify top-level structure matches schema expectations
|
|
813
|
-
expect(report.checkId).toBe("H002");
|
|
814
|
-
expect(report.checkTitle).toBe("Unused indexes");
|
|
815
|
-
expect(typeof report.timestamptz).toBe("string");
|
|
816
|
-
expect(report.nodes.primary).toBe("test-node");
|
|
817
|
-
expect(Array.isArray(report.nodes.standbys)).toBe(true);
|
|
818
|
-
expect("test-node" in report.results).toBe(true);
|
|
819
|
-
expect(report.results["test-node"].postgres_version).toBeTruthy();
|
|
820
|
-
// Data is now keyed by database name
|
|
821
|
-
expect("testdb" in report.results["test-node"].data).toBe(true);
|
|
822
|
-
expect((report.results["test-node"].data as any)["testdb"].unused_indexes).toBeTruthy();
|
|
823
|
-
});
|
|
824
|
-
});
|
|
825
|
-
|
|
826
|
-
// Tests for H004 (Redundant indexes)
|
|
827
|
-
describe("H004 - Redundant indexes", () => {
|
|
828
|
-
test("getRedundantIndexes returns redundant indexes", async () => {
|
|
829
|
-
const mockClient = createMockClient([], [], {
|
|
830
|
-
redundantIndexesRows: [
|
|
831
|
-
{
|
|
832
|
-
schema_name: "public",
|
|
833
|
-
table_name: "orders",
|
|
834
|
-
index_name: "orders_user_id_idx",
|
|
835
|
-
relation_name: "orders",
|
|
836
|
-
access_method: "btree",
|
|
837
|
-
reason: "public.orders_user_id_created_idx",
|
|
838
|
-
index_size_bytes: "2097152",
|
|
839
|
-
table_size_bytes: "16777216",
|
|
840
|
-
index_usage: "0",
|
|
841
|
-
supports_fk: false,
|
|
842
|
-
index_definition: "CREATE INDEX orders_user_id_idx ON public.orders USING btree (user_id)",
|
|
843
|
-
covering_indexes_json: JSON.stringify([
|
|
844
|
-
{ index_name: "public.orders_user_id_created_idx", index_definition: "CREATE INDEX orders_user_id_created_idx ON public.orders USING btree (user_id, created_at)" }
|
|
845
|
-
]),
|
|
846
|
-
},
|
|
847
|
-
],
|
|
848
|
-
});
|
|
849
|
-
|
|
850
|
-
const indexes = await checkup.getRedundantIndexes(mockClient as any);
|
|
851
|
-
expect(indexes.length).toBe(1);
|
|
852
|
-
expect(indexes[0].schema_name).toBe("public");
|
|
853
|
-
expect(indexes[0].index_name).toBe("orders_user_id_idx");
|
|
854
|
-
expect(indexes[0].reason).toBe("public.orders_user_id_created_idx");
|
|
855
|
-
expect(indexes[0].index_size_bytes).toBe(2097152);
|
|
856
|
-
expect(indexes[0].supports_fk).toBe(false);
|
|
857
|
-
expect(indexes[0].index_definition).toBeTruthy();
|
|
858
|
-
expect(indexes[0].relation_name).toBe("orders");
|
|
859
|
-
// Verify covering_indexes is populated with definitions
|
|
860
|
-
expect(indexes[0].covering_indexes).toBeInstanceOf(Array);
|
|
861
|
-
expect(indexes[0].covering_indexes.length).toBe(1);
|
|
862
|
-
expect(indexes[0].covering_indexes[0].index_name).toBe("public.orders_user_id_created_idx");
|
|
863
|
-
expect(indexes[0].covering_indexes[0].index_definition).toContain("CREATE INDEX");
|
|
864
|
-
});
|
|
865
|
-
|
|
866
|
-
test("generateH004 creates report with redundant indexes", async () => {
|
|
867
|
-
const mockClient = createMockClient(
|
|
868
|
-
[
|
|
869
|
-
{ name: "server_version", setting: "16.3" },
|
|
870
|
-
{ name: "server_version_num", setting: "160003" },
|
|
871
|
-
],
|
|
872
|
-
[],
|
|
873
|
-
{
|
|
874
|
-
redundantIndexesRows: [
|
|
875
|
-
{
|
|
876
|
-
schema_name: "public",
|
|
877
|
-
table_name: "products",
|
|
878
|
-
index_name: "products_category_idx",
|
|
879
|
-
relation_name: "products",
|
|
880
|
-
access_method: "btree",
|
|
881
|
-
reason: "public.products_category_name_idx",
|
|
882
|
-
index_size_bytes: "4194304",
|
|
883
|
-
table_size_bytes: "33554432",
|
|
884
|
-
index_usage: "5",
|
|
885
|
-
supports_fk: false,
|
|
886
|
-
index_definition: "CREATE INDEX products_category_idx ON public.products USING btree (category)",
|
|
887
|
-
covering_indexes_json: JSON.stringify([
|
|
888
|
-
{ index_name: "public.products_category_name_idx", index_definition: "CREATE INDEX products_category_name_idx ON public.products USING btree (category, name)" }
|
|
889
|
-
]),
|
|
890
|
-
},
|
|
891
|
-
],
|
|
892
|
-
}
|
|
893
|
-
);
|
|
894
|
-
|
|
895
|
-
const report = await checkup.generateH004(mockClient as any, "test-node");
|
|
896
|
-
expect(report.checkId).toBe("H004");
|
|
897
|
-
expect(report.checkTitle).toBe("Redundant indexes");
|
|
898
|
-
expect("test-node" in report.results).toBe(true);
|
|
899
|
-
|
|
900
|
-
// Data is now keyed by database name
|
|
901
|
-
const data = report.results["test-node"].data;
|
|
902
|
-
expect("testdb" in data).toBe(true);
|
|
903
|
-
const dbData = data["testdb"] as any;
|
|
904
|
-
expect(dbData.redundant_indexes).toBeTruthy();
|
|
905
|
-
expect(dbData.total_count).toBe(1);
|
|
906
|
-
expect(dbData.total_size_bytes).toBe(4194304);
|
|
907
|
-
expect(dbData.total_size_pretty).toBeTruthy();
|
|
908
|
-
expect(dbData.database_size_bytes).toBeTruthy();
|
|
909
|
-
expect(report.results["test-node"].postgres_version).toBeTruthy();
|
|
910
|
-
});
|
|
911
|
-
|
|
912
|
-
test("generateH004 has correct top-level structure", async () => {
|
|
913
|
-
const mockClient = createMockClient(
|
|
914
|
-
[
|
|
915
|
-
{ name: "server_version", setting: "16.3" },
|
|
916
|
-
{ name: "server_version_num", setting: "160003" },
|
|
917
|
-
],
|
|
918
|
-
[],
|
|
919
|
-
{
|
|
920
|
-
redundantIndexesRows: [
|
|
921
|
-
{
|
|
922
|
-
schema_name: "public",
|
|
923
|
-
table_name: "products",
|
|
924
|
-
index_name: "products_category_idx",
|
|
925
|
-
relation_name: "products",
|
|
926
|
-
access_method: "btree",
|
|
927
|
-
reason: "public.products_category_name_idx",
|
|
928
|
-
index_size_bytes: "4194304",
|
|
929
|
-
table_size_bytes: "33554432",
|
|
930
|
-
index_usage: "5",
|
|
931
|
-
supports_fk: false,
|
|
932
|
-
index_definition: "CREATE INDEX products_category_idx ON public.products USING btree (category)",
|
|
933
|
-
covering_indexes_json: JSON.stringify([
|
|
934
|
-
{ index_name: "public.products_category_name_idx", index_definition: "CREATE INDEX products_category_name_idx ON public.products USING btree (category, name)" }
|
|
935
|
-
]),
|
|
936
|
-
},
|
|
937
|
-
],
|
|
938
|
-
}
|
|
939
|
-
);
|
|
940
|
-
|
|
941
|
-
const report = await checkup.generateH004(mockClient as any, "test-node");
|
|
942
|
-
|
|
943
|
-
// Verify top-level structure matches schema expectations
|
|
944
|
-
expect(report.checkId).toBe("H004");
|
|
945
|
-
expect(report.checkTitle).toBe("Redundant indexes");
|
|
946
|
-
expect(typeof report.timestamptz).toBe("string");
|
|
947
|
-
expect(report.nodes.primary).toBe("test-node");
|
|
948
|
-
expect(Array.isArray(report.nodes.standbys)).toBe(true);
|
|
949
|
-
expect("test-node" in report.results).toBe(true);
|
|
950
|
-
expect(report.results["test-node"].postgres_version).toBeTruthy();
|
|
951
|
-
// Data is now keyed by database name
|
|
952
|
-
expect("testdb" in report.results["test-node"].data).toBe(true);
|
|
953
|
-
expect((report.results["test-node"].data as any)["testdb"].redundant_indexes).toBeTruthy();
|
|
954
|
-
});
|
|
955
|
-
});
|
|
956
|
-
|
|
957
|
-
// CLI tests
|
|
958
|
-
describe("CLI tests", () => {
|
|
959
|
-
test("checkup command exists and shows help", () => {
|
|
960
|
-
const r = runCli(["checkup", "--help"]);
|
|
961
|
-
expect(r.status).toBe(0);
|
|
962
|
-
expect(r.stdout).toMatch(/express mode/i);
|
|
963
|
-
expect(r.stdout).toMatch(/--check-id/);
|
|
964
|
-
expect(r.stdout).toMatch(/--node-name/);
|
|
965
|
-
expect(r.stdout).toMatch(/--output/);
|
|
966
|
-
expect(r.stdout).toMatch(/upload/);
|
|
967
|
-
expect(r.stdout).toMatch(/--json/);
|
|
968
|
-
});
|
|
969
|
-
|
|
970
|
-
test("checkup --help shows available check IDs", () => {
|
|
971
|
-
const r = runCli(["checkup", "--help"]);
|
|
972
|
-
expect(r.status).toBe(0);
|
|
973
|
-
expect(r.stdout).toMatch(/A002/);
|
|
974
|
-
expect(r.stdout).toMatch(/A003/);
|
|
975
|
-
expect(r.stdout).toMatch(/A004/);
|
|
976
|
-
expect(r.stdout).toMatch(/A007/);
|
|
977
|
-
expect(r.stdout).toMatch(/A013/);
|
|
978
|
-
expect(r.stdout).toMatch(/H001/);
|
|
979
|
-
expect(r.stdout).toMatch(/H002/);
|
|
980
|
-
expect(r.stdout).toMatch(/H004/);
|
|
981
|
-
});
|
|
982
|
-
|
|
983
|
-
test("checkup without connection shows help", () => {
|
|
984
|
-
const r = runCli(["checkup"]);
|
|
985
|
-
expect(r.status).not.toBe(0);
|
|
986
|
-
// Should show full help (options + examples), like `checkup --help`
|
|
987
|
-
expect(r.stdout).toMatch(/generate health check reports/i);
|
|
988
|
-
expect(r.stdout).toMatch(/--check-id/);
|
|
989
|
-
expect(r.stdout).toMatch(/available checks/i);
|
|
990
|
-
expect(r.stdout).toMatch(/A002/);
|
|
991
|
-
});
|
|
992
|
-
});
|
|
993
|
-
|
|
994
|
-
// Tests for checkup-api module
|
|
995
|
-
describe("checkup-api", () => {
|
|
996
|
-
test("formatRpcErrorForDisplay formats details/hint nicely", () => {
|
|
997
|
-
const err = new api.RpcError({
|
|
998
|
-
rpcName: "checkup_report_file_post",
|
|
999
|
-
statusCode: 402,
|
|
1000
|
-
payloadText: JSON.stringify({
|
|
1001
|
-
hint: "Start an express checkup subscription for the organization or contact support.",
|
|
1002
|
-
details: "Checkup report uploads require an active checkup subscription",
|
|
1003
|
-
}),
|
|
1004
|
-
payloadJson: {
|
|
1005
|
-
hint: "Start an express checkup subscription for the organization or contact support.",
|
|
1006
|
-
details: "Checkup report uploads require an active checkup subscription.",
|
|
1007
|
-
},
|
|
1008
|
-
});
|
|
1009
|
-
const lines = api.formatRpcErrorForDisplay(err);
|
|
1010
|
-
const text = lines.join("\n");
|
|
1011
|
-
expect(text).toMatch(/RPC checkup_report_file_post failed: HTTP 402/);
|
|
1012
|
-
expect(text).toMatch(/Details:/);
|
|
1013
|
-
expect(text).toMatch(/Hint:/);
|
|
1014
|
-
});
|
|
1015
|
-
});
|
|
1016
|
-
|