postgresai 0.16.0-rc.4 → 0.16.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/README.md +154 -0
- package/dist/bin/postgres-ai.js +2911 -255
- package/package.json +12 -3
- package/schemas/A002.schema.json +63 -0
- package/schemas/A003.schema.json +73 -0
- package/schemas/A004.schema.json +81 -0
- package/schemas/A007.schema.json +71 -0
- package/schemas/A013.schema.json +61 -0
- package/schemas/D001.schema.json +71 -0
- package/schemas/D004.schema.json +136 -0
- package/schemas/F001.schema.json +73 -0
- package/schemas/F002.schema.json +108 -0
- package/schemas/F003.schema.json +138 -0
- package/schemas/F004.schema.json +125 -0
- package/schemas/F005.schema.json +131 -0
- package/schemas/F009.schema.json +155 -0
- package/schemas/G001.schema.json +135 -0
- package/schemas/G003.schema.json +90 -0
- package/schemas/H001.schema.json +141 -0
- package/schemas/H002.schema.json +129 -0
- package/schemas/H004.schema.json +128 -0
- package/schemas/I001.schema.json +149 -0
- package/schemas/K001.schema.json +161 -0
- package/schemas/K003.schema.json +163 -0
- package/schemas/K004.schema.json +110 -0
- package/schemas/K005.schema.json +110 -0
- package/schemas/K006.schema.json +110 -0
- package/schemas/K007.schema.json +110 -0
- package/schemas/K008.schema.json +110 -0
- package/schemas/M001.schema.json +119 -0
- package/schemas/M002.schema.json +110 -0
- package/schemas/M003.schema.json +128 -0
- package/schemas/N001.schema.json +161 -0
- package/schemas/query.schema.json +62 -0
- package/CHANGELOG.md +0 -11
- package/bin/postgres-ai.ts +0 -5578
- package/bun.lock +0 -258
- package/bunfig.toml +0 -20
- package/lib/aas-onboard.ts +0 -251
- package/lib/auth-server.ts +0 -285
- package/lib/checkup-api.ts +0 -526
- package/lib/checkup-dictionary.ts +0 -103
- package/lib/checkup-summary.ts +0 -338
- package/lib/checkup.ts +0 -2261
- package/lib/config.ts +0 -171
- package/lib/init.ts +0 -1152
- package/lib/instances.ts +0 -245
- package/lib/issues.ts +0 -1060
- package/lib/mcp-server.ts +0 -667
- package/lib/metrics-loader.ts +0 -134
- package/lib/pkce.ts +0 -79
- package/lib/reports.ts +0 -373
- package/lib/storage.ts +0 -367
- package/lib/supabase.ts +0 -826
- package/lib/util.ts +0 -134
- package/packages/postgres-ai/README.md +0 -26
- package/packages/postgres-ai/bin/postgres-ai.js +0 -27
- package/packages/postgres-ai/package.json +0 -27
- package/scripts/embed-checkup-dictionary.ts +0 -115
- package/scripts/embed-metrics.ts +0 -160
- package/scripts/generate-release-notes.ts +0 -668
- package/test/PERMISSION_CHECK_TEST_SUMMARY.md +0 -139
- package/test/aas-onboard.test.ts +0 -301
- package/test/auth.test.ts +0 -287
- package/test/checkup.integration.test.ts +0 -413
- package/test/checkup.test.ts +0 -3626
- package/test/compose-cmd.test.ts +0 -120
- package/test/config-consistency.test.ts +0 -352
- package/test/init.integration.test.ts +0 -438
- package/test/init.test.ts +0 -1816
- package/test/issues.cli.test.ts +0 -1162
- package/test/issues.test.ts +0 -456
- package/test/mcp-server.test.ts +0 -2530
- package/test/monitoring.test.ts +0 -746
- package/test/permission-check-sql.test.ts +0 -116
- package/test/reports.cli.test.ts +0 -793
- package/test/reports.test.ts +0 -977
- package/test/schema-validation.test.ts +0 -231
- package/test/storage.test.ts +0 -935
- package/test/supabase.test.ts +0 -709
- package/test/targets-add-config.test.ts +0 -28
- package/test/test-utils.ts +0 -190
- package/test/upgrade.test.ts +0 -1056
- package/test/util.test.ts +0 -44
- package/tsconfig.json +0 -20
package/test/compose-cmd.test.ts
DELETED
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
import { describe, test, expect } from "bun:test";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Test getComposeCmd() selection logic.
|
|
5
|
-
* WARNING: This replicates the logic from postgres-ai.ts. If the production
|
|
6
|
-
* function changes, this replica must be updated to match.
|
|
7
|
-
* Since the function is internal to postgres-ai.ts, we replicate its logic
|
|
8
|
-
* with an injectable command checker (same pattern as monitoring.test.ts).
|
|
9
|
-
*/
|
|
10
|
-
function getComposeCmd(
|
|
11
|
-
tryCmd: (cmd: string, args: string[]) => boolean,
|
|
12
|
-
): string[] | null {
|
|
13
|
-
if (tryCmd("docker", ["compose", "version"])) return ["docker", "compose"];
|
|
14
|
-
if (tryCmd("docker-compose", ["version"])) return ["docker-compose"];
|
|
15
|
-
return null;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
describe("getComposeCmd", () => {
|
|
19
|
-
test("prefers docker compose V2 when both are available", () => {
|
|
20
|
-
const result = getComposeCmd(() => true);
|
|
21
|
-
expect(result).toEqual(["docker", "compose"]);
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
test("falls back to docker-compose V1 when V2 is unavailable", () => {
|
|
25
|
-
const result = getComposeCmd((cmd, args) => {
|
|
26
|
-
// V2 plugin fails, V1 standalone succeeds
|
|
27
|
-
if (cmd === "docker" && args[0] === "compose") return false;
|
|
28
|
-
if (cmd === "docker-compose") return true;
|
|
29
|
-
return false;
|
|
30
|
-
});
|
|
31
|
-
expect(result).toEqual(["docker-compose"]);
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
test("returns null when neither is available", () => {
|
|
35
|
-
const result = getComposeCmd(() => false);
|
|
36
|
-
expect(result).toBeNull();
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
test("does not check V1 when V2 succeeds", () => {
|
|
40
|
-
const calls: Array<{ cmd: string; args: string[] }> = [];
|
|
41
|
-
getComposeCmd((cmd, args) => {
|
|
42
|
-
calls.push({ cmd, args });
|
|
43
|
-
return cmd === "docker" && args[0] === "compose";
|
|
44
|
-
});
|
|
45
|
-
expect(calls).toHaveLength(1);
|
|
46
|
-
expect(calls[0]).toEqual({ cmd: "docker", args: ["compose", "version"] });
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
test("checks V2 first, then V1", () => {
|
|
50
|
-
const calls: Array<{ cmd: string; args: string[] }> = [];
|
|
51
|
-
getComposeCmd((cmd, args) => {
|
|
52
|
-
calls.push({ cmd, args });
|
|
53
|
-
return false;
|
|
54
|
-
});
|
|
55
|
-
expect(calls).toHaveLength(2);
|
|
56
|
-
expect(calls[0]).toEqual({ cmd: "docker", args: ["compose", "version"] });
|
|
57
|
-
expect(calls[1]).toEqual({ cmd: "docker-compose", args: ["version"] });
|
|
58
|
-
});
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Test the monitoring startup sequence's container cleanup logic.
|
|
63
|
-
* Before "up --force-recreate", stopped containers from "run --rm" dependencies
|
|
64
|
-
* (e.g. config-init) must be removed to avoid docker-compose v1's
|
|
65
|
-
* KeyError: 'ContainerConfig' bug.
|
|
66
|
-
*
|
|
67
|
-
* We replicate the relevant sequence from the monitoring start command
|
|
68
|
-
* with an injectable runCompose to verify ordering and error tolerance.
|
|
69
|
-
*/
|
|
70
|
-
async function monitoringStartSequence(
|
|
71
|
-
runCompose: (args: string[]) => Promise<number>,
|
|
72
|
-
): Promise<number> {
|
|
73
|
-
// Best-effort: remove stopped containers left by "run --rm" dependencies
|
|
74
|
-
await runCompose(["rm", "-f", "-s", "config-init"]);
|
|
75
|
-
// Start services
|
|
76
|
-
const code = await runCompose(["up", "-d", "--force-recreate"]);
|
|
77
|
-
return code;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
describe("monitoring start: config-init cleanup", () => {
|
|
81
|
-
test("calls rm before up", async () => {
|
|
82
|
-
const calls: string[][] = [];
|
|
83
|
-
await monitoringStartSequence(async (args) => {
|
|
84
|
-
calls.push(args);
|
|
85
|
-
return 0;
|
|
86
|
-
});
|
|
87
|
-
expect(calls).toHaveLength(2);
|
|
88
|
-
expect(calls[0]).toEqual(["rm", "-f", "-s", "config-init"]);
|
|
89
|
-
expect(calls[1]).toEqual(["up", "-d", "--force-recreate"]);
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
test("continues to up even when rm fails", async () => {
|
|
93
|
-
const calls: string[][] = [];
|
|
94
|
-
await monitoringStartSequence(async (args) => {
|
|
95
|
-
calls.push(args);
|
|
96
|
-
// rm returns non-zero (container doesn't exist)
|
|
97
|
-
if (args[0] === "rm") return 1;
|
|
98
|
-
return 0;
|
|
99
|
-
});
|
|
100
|
-
expect(calls).toHaveLength(2);
|
|
101
|
-
expect(calls[0][0]).toBe("rm");
|
|
102
|
-
expect(calls[1][0]).toBe("up");
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
test("returns up exit code, not rm exit code", async () => {
|
|
106
|
-
// rm fails but up succeeds → overall success
|
|
107
|
-
const result1 = await monitoringStartSequence(async (args) => {
|
|
108
|
-
if (args[0] === "rm") return 1;
|
|
109
|
-
return 0;
|
|
110
|
-
});
|
|
111
|
-
expect(result1).toBe(0);
|
|
112
|
-
|
|
113
|
-
// rm succeeds but up fails → overall failure
|
|
114
|
-
const result2 = await monitoringStartSequence(async (args) => {
|
|
115
|
-
if (args[0] === "up") return 2;
|
|
116
|
-
return 0;
|
|
117
|
-
});
|
|
118
|
-
expect(result2).toBe(2);
|
|
119
|
-
});
|
|
120
|
-
});
|
|
@@ -1,352 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests that config files are consistent with what the CLI expects.
|
|
3
|
-
* Catches schema mismatches like pg_statistic in wrong schema.
|
|
4
|
-
*/
|
|
5
|
-
import { describe, test, expect } from "bun:test";
|
|
6
|
-
import { readdirSync, readFileSync } from "fs";
|
|
7
|
-
import { resolve } from "path";
|
|
8
|
-
|
|
9
|
-
type PgwatchConfig = {
|
|
10
|
-
metrics: Record<string, unknown>;
|
|
11
|
-
presets?: Record<string, { metrics?: Record<string, unknown> }>;
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
type GrafanaDatasource = {
|
|
15
|
-
name?: unknown;
|
|
16
|
-
orgId?: unknown;
|
|
17
|
-
uid?: unknown;
|
|
18
|
-
editable?: unknown;
|
|
19
|
-
basicAuth?: unknown;
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
type GrafanaDatasourceConfig = {
|
|
23
|
-
deleteDatasources?: Array<{ name?: unknown; orgId?: unknown }>;
|
|
24
|
-
datasources?: GrafanaDatasource[];
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
type DockerComposeConfig = {
|
|
28
|
-
services?: Record<string, { restart?: unknown }>;
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
// These UIDs are referenced by provisioned Grafana dashboards.
|
|
32
|
-
// Changing them without updating dashboard JSON silently breaks panels.
|
|
33
|
-
const expectedGrafanaDatasourceUids = new Map([
|
|
34
|
-
["PGWatch-PostgreSQL", "P031DD592934B2F1F"],
|
|
35
|
-
["PGWatch-Prometheus", "P7A0D6631BB10B34F"],
|
|
36
|
-
["Infinity", "aerffb0z8rjlsc"],
|
|
37
|
-
]);
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Normalizes raw Grafana datasource orgId values to a positive integer.
|
|
41
|
-
*
|
|
42
|
-
* Grafana treats omitted/null orgId as org 1 in the default single-org setup.
|
|
43
|
-
* Numeric strings are accepted because YAML or API callers may preserve them
|
|
44
|
-
* as strings. Booleans, objects, empty strings, non-positive values, and
|
|
45
|
-
* non-integers are rejected because Grafana cannot address those orgs.
|
|
46
|
-
*/
|
|
47
|
-
const normalizeGrafanaOrgId = (orgId: unknown) => {
|
|
48
|
-
if (orgId === undefined || orgId === null) {
|
|
49
|
-
return 1;
|
|
50
|
-
}
|
|
51
|
-
if (typeof orgId === "boolean" || typeof orgId === "object" || orgId === "") {
|
|
52
|
-
throw new Error(`Invalid Grafana datasource orgId: ${orgId}`);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const normalizedOrgId = Number(orgId);
|
|
56
|
-
if (!Number.isInteger(normalizedOrgId) || normalizedOrgId < 1) {
|
|
57
|
-
throw new Error(`Invalid Grafana datasource orgId: ${orgId}`);
|
|
58
|
-
}
|
|
59
|
-
return normalizedOrgId;
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
// Normalize orgId so omitted/null orgId and explicit orgId: 1 map to the same
|
|
63
|
-
// datasource key, matching Grafana's implicit single-org behavior.
|
|
64
|
-
const datasourceKey = (name: unknown, orgId: unknown) =>
|
|
65
|
-
`${name}:${normalizeGrafanaOrgId(orgId)}`;
|
|
66
|
-
|
|
67
|
-
const configDir = resolve(import.meta.dir, "../../config");
|
|
68
|
-
const metricsPath = resolve(configDir, "pgwatch-prometheus/metrics.yml");
|
|
69
|
-
const datasourcePath = resolve(
|
|
70
|
-
configDir,
|
|
71
|
-
"grafana/provisioning/datasources/datasources.yml"
|
|
72
|
-
);
|
|
73
|
-
const composePath = resolve(import.meta.dir, "../../docker-compose.yml");
|
|
74
|
-
const envExamplePath = resolve(import.meta.dir, "../../.env.example");
|
|
75
|
-
const dashboardDir = resolve(configDir, "grafana/dashboards");
|
|
76
|
-
const dashboardGeneratedMetricRefs = new Set([
|
|
77
|
-
// Exported by monitoring_flask_backend/app.py and joined into query dashboards.
|
|
78
|
-
"query_info",
|
|
79
|
-
]);
|
|
80
|
-
const pgwatchMetricRefPattern = /\bpgwatch_([a-zA-Z_:][a-zA-Z0-9_:]*)\b/g;
|
|
81
|
-
|
|
82
|
-
const loadPgwatchConfig = () =>
|
|
83
|
-
Bun.YAML.parse(readFileSync(metricsPath, "utf8")) as PgwatchConfig;
|
|
84
|
-
|
|
85
|
-
const parseGrafanaDatasourceConfig = (content: string) =>
|
|
86
|
-
Bun.YAML.parse(content) as GrafanaDatasourceConfig;
|
|
87
|
-
|
|
88
|
-
const grafanaQueryStringKeys = new Set(["expr", "definition", "query"]);
|
|
89
|
-
|
|
90
|
-
const collectStringValuesByKeys = (
|
|
91
|
-
value: unknown,
|
|
92
|
-
keys: Set<string>
|
|
93
|
-
): string[] => {
|
|
94
|
-
const matches: string[] = [];
|
|
95
|
-
|
|
96
|
-
const visit = (node: unknown) => {
|
|
97
|
-
if (Array.isArray(node)) {
|
|
98
|
-
for (const item of node) {
|
|
99
|
-
visit(item);
|
|
100
|
-
}
|
|
101
|
-
return;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
if (node && typeof node === "object") {
|
|
105
|
-
const record = node as Record<string, unknown>;
|
|
106
|
-
for (const [recordKey, item] of Object.entries(record)) {
|
|
107
|
-
if (keys.has(recordKey) && typeof item === "string") {
|
|
108
|
-
matches.push(item);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
visit(item);
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
};
|
|
115
|
-
|
|
116
|
-
visit(value);
|
|
117
|
-
return matches;
|
|
118
|
-
};
|
|
119
|
-
|
|
120
|
-
const resolveMetricReference = (
|
|
121
|
-
metricRef: string,
|
|
122
|
-
metricNamesByLength: string[]
|
|
123
|
-
) =>
|
|
124
|
-
metricNamesByLength.find((metricName) =>
|
|
125
|
-
metricRef === metricName || metricRef.startsWith(`${metricName}_`)
|
|
126
|
-
);
|
|
127
|
-
|
|
128
|
-
describe("Config consistency", () => {
|
|
129
|
-
test("target-db/init.sql creates pg_statistic in postgres_ai schema", () => {
|
|
130
|
-
const initSql = readFileSync(resolve(configDir, "target-db/init.sql"), "utf8");
|
|
131
|
-
|
|
132
|
-
// Must create postgres_ai schema
|
|
133
|
-
expect(initSql).toMatch(/create\s+schema\s+if\s+not\s+exists\s+postgres_ai/i);
|
|
134
|
-
|
|
135
|
-
// Must create view in postgres_ai schema, not public
|
|
136
|
-
expect(initSql).toMatch(/create\s+or\s+replace\s+view\s+postgres_ai\.pg_statistic/i);
|
|
137
|
-
expect(initSql).not.toMatch(/create\s+or\s+replace\s+view\s+public\.pg_statistic/i);
|
|
138
|
-
|
|
139
|
-
// Must grant on postgres_ai.pg_statistic
|
|
140
|
-
expect(initSql).toMatch(/grant\s+select\s+on\s+postgres_ai\.pg_statistic/i);
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
test("pgwatch metrics.yml uses postgres_ai.pg_statistic", () => {
|
|
144
|
-
const metricsYml = readFileSync(metricsPath, "utf8");
|
|
145
|
-
|
|
146
|
-
// Should reference postgres_ai.pg_statistic, not public.pg_statistic
|
|
147
|
-
expect(metricsYml).not.toMatch(/public\.pg_statistic/);
|
|
148
|
-
expect(metricsYml).toMatch(/postgres_ai\.pg_statistic/);
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
test("Grafana datasource YAML parser rejects malformed config", () => {
|
|
152
|
-
expect(() =>
|
|
153
|
-
parseGrafanaDatasourceConfig("apiVersion: 1\ndatasources:\n - name: [")
|
|
154
|
-
).toThrow();
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
test("Grafana orgId normalization rejects invalid values", () => {
|
|
158
|
-
for (const orgId of [undefined, null, 1, "1"]) {
|
|
159
|
-
expect(normalizeGrafanaOrgId(orgId)).toBe(1);
|
|
160
|
-
}
|
|
161
|
-
expect(normalizeGrafanaOrgId("2")).toBe(2);
|
|
162
|
-
|
|
163
|
-
for (const orgId of [
|
|
164
|
-
0,
|
|
165
|
-
-1,
|
|
166
|
-
true,
|
|
167
|
-
"",
|
|
168
|
-
1.5,
|
|
169
|
-
"0",
|
|
170
|
-
"-1",
|
|
171
|
-
"1.5",
|
|
172
|
-
"abc",
|
|
173
|
-
NaN,
|
|
174
|
-
Infinity,
|
|
175
|
-
]) {
|
|
176
|
-
expect(() => normalizeGrafanaOrgId(orgId)).toThrow();
|
|
177
|
-
}
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
test("datasources.yml delete entries match stable non-editable datasources", () => {
|
|
181
|
-
const datasourceConfig = parseGrafanaDatasourceConfig(
|
|
182
|
-
readFileSync(datasourcePath, "utf8")
|
|
183
|
-
);
|
|
184
|
-
const deleteDatasources = datasourceConfig.deleteDatasources ?? [];
|
|
185
|
-
const datasources = datasourceConfig.datasources ?? [];
|
|
186
|
-
const deletedKeys = new Set(
|
|
187
|
-
deleteDatasources.map((datasource) =>
|
|
188
|
-
datasourceKey(datasource.name, datasource.orgId)
|
|
189
|
-
)
|
|
190
|
-
);
|
|
191
|
-
const datasourceByNameAndOrg = new Map(
|
|
192
|
-
datasources.map((datasource) => [
|
|
193
|
-
datasourceKey(datasource.name, datasource.orgId),
|
|
194
|
-
datasource,
|
|
195
|
-
])
|
|
196
|
-
);
|
|
197
|
-
|
|
198
|
-
expect(deleteDatasources.length).toBeGreaterThan(0);
|
|
199
|
-
expect(deleteDatasources.length).toBe(datasources.length);
|
|
200
|
-
expect(datasourceByNameAndOrg.size).toBe(datasources.length);
|
|
201
|
-
|
|
202
|
-
for (const deletedDatasource of deleteDatasources) {
|
|
203
|
-
const datasource = datasourceByNameAndOrg.get(
|
|
204
|
-
datasourceKey(deletedDatasource.name, deletedDatasource.orgId)
|
|
205
|
-
);
|
|
206
|
-
const expectedUid = expectedGrafanaDatasourceUids.get(
|
|
207
|
-
String(deletedDatasource.name)
|
|
208
|
-
);
|
|
209
|
-
expect(datasource).toBeDefined();
|
|
210
|
-
expect(expectedUid).toBeDefined();
|
|
211
|
-
expect(datasource?.uid).toBe(expectedUid);
|
|
212
|
-
expect(datasource?.editable).toBe(false);
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
for (const datasource of datasources) {
|
|
216
|
-
const expectedUid = expectedGrafanaDatasourceUids.get(
|
|
217
|
-
String(datasource.name)
|
|
218
|
-
);
|
|
219
|
-
expect(deletedKeys.has(datasourceKey(datasource.name, datasource.orgId))).toBe(
|
|
220
|
-
true
|
|
221
|
-
);
|
|
222
|
-
expect(expectedUid).toBeDefined();
|
|
223
|
-
expect(datasource.uid).toBe(expectedUid);
|
|
224
|
-
expect(datasource.editable).toBe(false);
|
|
225
|
-
}
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
test("Grafana Prometheus datasource requires VM auth environment", () => {
|
|
229
|
-
const datasourceConfig = parseGrafanaDatasourceConfig(
|
|
230
|
-
readFileSync(datasourcePath, "utf8")
|
|
231
|
-
);
|
|
232
|
-
const composeYml = readFileSync(composePath, "utf8");
|
|
233
|
-
const prometheusDatasource = datasourceConfig.datasources?.find(
|
|
234
|
-
(datasource) => datasource.name === "PGWatch-Prometheus"
|
|
235
|
-
) as
|
|
236
|
-
| {
|
|
237
|
-
basicAuth?: unknown;
|
|
238
|
-
basicAuthUser?: unknown;
|
|
239
|
-
secureJsonData?: { basicAuthPassword?: unknown };
|
|
240
|
-
}
|
|
241
|
-
| undefined;
|
|
242
|
-
|
|
243
|
-
expect(prometheusDatasource?.basicAuth).toBe(true);
|
|
244
|
-
expect(prometheusDatasource?.basicAuthUser).toBe("${VM_AUTH_USERNAME}");
|
|
245
|
-
expect(prometheusDatasource?.secureJsonData?.basicAuthPassword).toBe(
|
|
246
|
-
"${VM_AUTH_PASSWORD}"
|
|
247
|
-
);
|
|
248
|
-
expect(composeYml).toContain(
|
|
249
|
-
"VM_AUTH_USERNAME: ${VM_AUTH_USERNAME:?VM_AUTH_USERNAME is required for Grafana datasource provisioning}"
|
|
250
|
-
);
|
|
251
|
-
expect(composeYml).toContain(
|
|
252
|
-
"VM_AUTH_PASSWORD: ${VM_AUTH_PASSWORD:?VM_AUTH_PASSWORD is required for Grafana datasource provisioning}"
|
|
253
|
-
);
|
|
254
|
-
});
|
|
255
|
-
|
|
256
|
-
test(".env.example documents direct Docker Compose credentials", () => {
|
|
257
|
-
const envExample = readFileSync(envExamplePath, "utf8");
|
|
258
|
-
|
|
259
|
-
expect(envExample).toContain("REPLICATOR_PASSWORD=");
|
|
260
|
-
expect(envExample).toContain("VM_AUTH_USERNAME=vmauth");
|
|
261
|
-
expect(envExample).toContain("VM_AUTH_PASSWORD=");
|
|
262
|
-
expect(envExample).toContain("openssl rand -hex 32");
|
|
263
|
-
expect(envExample).toContain("openssl rand -base64 18");
|
|
264
|
-
});
|
|
265
|
-
|
|
266
|
-
test("long-running Docker Compose services survive host reboot", () => {
|
|
267
|
-
const composeConfig = Bun.YAML.parse(
|
|
268
|
-
readFileSync(composePath, "utf8")
|
|
269
|
-
) as DockerComposeConfig;
|
|
270
|
-
const services = composeConfig.services ?? {};
|
|
271
|
-
|
|
272
|
-
// All services should survive host reboot unless they are one-shot setup jobs.
|
|
273
|
-
const oneShotServices = new Set(["config-init", "sources-generator"]);
|
|
274
|
-
const serviceEntries = Object.entries(services);
|
|
275
|
-
|
|
276
|
-
expect(serviceEntries.length).toBeGreaterThan(0);
|
|
277
|
-
|
|
278
|
-
for (const serviceName of oneShotServices) {
|
|
279
|
-
expect(services[serviceName]).toBeDefined();
|
|
280
|
-
expect(services[serviceName]?.restart).toBeUndefined();
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
for (const [serviceName, serviceConfig] of serviceEntries) {
|
|
284
|
-
if (oneShotServices.has(serviceName)) {
|
|
285
|
-
continue;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
expect(serviceConfig.restart).toBe("unless-stopped");
|
|
289
|
-
}
|
|
290
|
-
});
|
|
291
|
-
|
|
292
|
-
test("pgwatch presets only reference configured metrics", () => {
|
|
293
|
-
const pgwatchConfig = loadPgwatchConfig();
|
|
294
|
-
const metricNames = new Set(Object.keys(pgwatchConfig.metrics));
|
|
295
|
-
const presetNames = Object.keys(pgwatchConfig.presets ?? {});
|
|
296
|
-
const unknownPresetMetrics = new Set<string>();
|
|
297
|
-
|
|
298
|
-
expect(metricNames.size).toBeGreaterThan(0);
|
|
299
|
-
expect(presetNames.length).toBeGreaterThan(0);
|
|
300
|
-
|
|
301
|
-
for (const [presetName, preset] of Object.entries(pgwatchConfig.presets ?? {})) {
|
|
302
|
-
for (const metricName of Object.keys(preset.metrics ?? {})) {
|
|
303
|
-
if (!metricNames.has(metricName)) {
|
|
304
|
-
unknownPresetMetrics.add(`${presetName}.${metricName}`);
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
expect([...unknownPresetMetrics].sort()).toEqual([]);
|
|
310
|
-
});
|
|
311
|
-
|
|
312
|
-
test("Grafana dashboard pgwatch metric references exist in metrics.yml", () => {
|
|
313
|
-
const pgwatchConfig = loadPgwatchConfig();
|
|
314
|
-
const metricNamesByLength = Object.keys(pgwatchConfig.metrics).sort(
|
|
315
|
-
(a, b) => b.length - a.length
|
|
316
|
-
);
|
|
317
|
-
const dashboardFiles = readdirSync(dashboardDir).filter((file) =>
|
|
318
|
-
file.endsWith(".json")
|
|
319
|
-
);
|
|
320
|
-
const unknownDashboardMetrics = new Set<string>();
|
|
321
|
-
let observedDashboardMetricRefs = 0;
|
|
322
|
-
|
|
323
|
-
expect(metricNamesByLength.length).toBeGreaterThan(0);
|
|
324
|
-
expect(dashboardFiles.length).toBeGreaterThan(0);
|
|
325
|
-
|
|
326
|
-
for (const dashboardFile of dashboardFiles) {
|
|
327
|
-
const dashboard = JSON.parse(
|
|
328
|
-
readFileSync(resolve(dashboardDir, dashboardFile), "utf8")
|
|
329
|
-
);
|
|
330
|
-
|
|
331
|
-
for (const queryString of collectStringValuesByKeys(
|
|
332
|
-
dashboard,
|
|
333
|
-
grafanaQueryStringKeys
|
|
334
|
-
)) {
|
|
335
|
-
for (const match of queryString.matchAll(pgwatchMetricRefPattern)) {
|
|
336
|
-
observedDashboardMetricRefs += 1;
|
|
337
|
-
const metricRef = match[1];
|
|
338
|
-
if (dashboardGeneratedMetricRefs.has(metricRef)) {
|
|
339
|
-
continue;
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
if (!resolveMetricReference(metricRef, metricNamesByLength)) {
|
|
343
|
-
unknownDashboardMetrics.add(`${dashboardFile}: pgwatch_${metricRef}`);
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
expect(observedDashboardMetricRefs).toBeGreaterThan(0);
|
|
350
|
-
expect([...unknownDashboardMetrics].sort()).toEqual([]);
|
|
351
|
-
});
|
|
352
|
-
});
|