postgresai 0.15.0-dev.6 → 0.15.0-rc.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/bin/postgres-ai.ts +119 -71
- package/bun.lock +4 -4
- package/dist/bin/postgres-ai.js +867 -232
- package/instances.demo.yml +14 -0
- package/lib/checkup-api.ts +25 -6
- package/lib/checkup.ts +225 -0
- package/lib/init.ts +195 -3
- package/lib/metrics-loader.ts +3 -1
- package/lib/supabase.ts +8 -1
- package/package.json +4 -4
- package/scripts/embed-checkup-dictionary.ts +9 -0
- package/scripts/embed-metrics.ts +2 -0
- package/test/PERMISSION_CHECK_TEST_SUMMARY.md +139 -0
- package/test/checkup.test.ts +1288 -2
- package/test/config-consistency.test.ts +321 -5
- package/test/init.integration.test.ts +27 -28
- package/test/init.test.ts +528 -8
- package/test/monitoring.test.ts +2 -2
- package/test/permission-check-sql.test.ts +116 -0
- package/test/schema-validation.test.ts +81 -0
- package/test/test-utils.ts +51 -2
- package/test/upgrade.test.ts +422 -0
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
import { describe, test, expect, beforeAll, afterAll } from "bun:test";
|
|
2
|
+
import { resolve } from "path";
|
|
3
|
+
import * as fs from "fs";
|
|
4
|
+
import * as os from "os";
|
|
5
|
+
|
|
6
|
+
// 30 seconds timeout for tests that spawn CLI processes
|
|
7
|
+
// This accommodates file I/O operations and process startup overhead in CI environments
|
|
8
|
+
const TEST_TIMEOUT = 30000;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Run CLI command in a specific directory.
|
|
12
|
+
*
|
|
13
|
+
* @param args - CLI arguments to pass to postgres-ai (e.g., ["mon", "local-install", "--yes"])
|
|
14
|
+
* @param cwd - Working directory where the command should run
|
|
15
|
+
* @param env - Optional environment variables to override (merged with process.env)
|
|
16
|
+
* @returns Object containing:
|
|
17
|
+
* - status: Process exit code (0 = success)
|
|
18
|
+
* - stdout: Standard output as string
|
|
19
|
+
* - stderr: Standard error as string
|
|
20
|
+
*/
|
|
21
|
+
function runCliInDir(args: string[], cwd: string, env: Record<string, string | undefined> = {}) {
|
|
22
|
+
const cliPath = resolve(import.meta.dir, "..", "bin", "postgres-ai.ts");
|
|
23
|
+
const bunBin = typeof process.execPath === "string" && process.execPath.length > 0 ? process.execPath : "bun";
|
|
24
|
+
const result = Bun.spawnSync([bunBin, cliPath, ...args], {
|
|
25
|
+
env: { ...process.env, ...env },
|
|
26
|
+
cwd,
|
|
27
|
+
});
|
|
28
|
+
return {
|
|
29
|
+
status: result.exitCode,
|
|
30
|
+
stdout: new TextDecoder().decode(result.stdout),
|
|
31
|
+
stderr: new TextDecoder().decode(result.stderr),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe("upgrade workflow", () => {
|
|
36
|
+
/**
|
|
37
|
+
* These tests verify the upgrade process documented in README.md:
|
|
38
|
+
* 1. Update CLI (npm install -g postgresai@latest)
|
|
39
|
+
* 2. Stop services (postgresai mon stop)
|
|
40
|
+
* 3. Re-run local-install which updates .env with new version
|
|
41
|
+
* 4. Verify services (postgresai mon status/health)
|
|
42
|
+
*
|
|
43
|
+
* Since Docker can't run in unit tests, we focus on testing the
|
|
44
|
+
* configuration update behavior which is the core of the upgrade process.
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
let tempDir: string;
|
|
48
|
+
|
|
49
|
+
beforeAll(() => {
|
|
50
|
+
tempDir = fs.mkdtempSync(resolve(os.tmpdir(), "pgai-upgrade-test-"));
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
afterAll(() => {
|
|
54
|
+
if (tempDir && fs.existsSync(tempDir)) {
|
|
55
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("upgrade updates PGAI_TAG from old version to CLI version", () => {
|
|
60
|
+
// Simulate existing installation with old version
|
|
61
|
+
const testDir = resolve(tempDir, "upgrade-tag-test");
|
|
62
|
+
fs.mkdirSync(testDir, { recursive: true });
|
|
63
|
+
|
|
64
|
+
// Create .env with old version (simulating pre-upgrade state)
|
|
65
|
+
fs.writeFileSync(resolve(testDir, ".env"), "PGAI_TAG=0.13.0\n");
|
|
66
|
+
fs.writeFileSync(resolve(testDir, "docker-compose.yml"), "version: '3'\nservices: {}\n");
|
|
67
|
+
fs.writeFileSync(resolve(testDir, "instances.yml"), "# instances\n");
|
|
68
|
+
|
|
69
|
+
// Run local-install (simulating upgrade after CLI update)
|
|
70
|
+
// The --yes flag skips interactive prompts
|
|
71
|
+
// Note: Command will fail at Docker step (no Docker in CI), but .env is updated before that
|
|
72
|
+
runCliInDir(
|
|
73
|
+
["mon", "local-install", "--db-url", "postgresql://u:p@h:5432/d", "--yes"],
|
|
74
|
+
testDir,
|
|
75
|
+
{ PGAI_TAG: undefined }
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
// Read the updated .env (written before Docker operations)
|
|
79
|
+
const envContent = fs.readFileSync(resolve(testDir, ".env"), "utf8");
|
|
80
|
+
|
|
81
|
+
// The old version should be replaced with the CLI version
|
|
82
|
+
expect(envContent).not.toMatch(/PGAI_TAG=0\.13\.0/);
|
|
83
|
+
// Should have a valid version tag (either semver or dev version)
|
|
84
|
+
expect(envContent).toMatch(/PGAI_TAG=\d+\.\d+\.\d+|PGAI_TAG=0\.0\.0-dev/);
|
|
85
|
+
}, { timeout: TEST_TIMEOUT });
|
|
86
|
+
|
|
87
|
+
test("upgrade preserves Grafana password", () => {
|
|
88
|
+
const testDir = resolve(tempDir, "upgrade-password-test");
|
|
89
|
+
fs.mkdirSync(testDir, { recursive: true });
|
|
90
|
+
|
|
91
|
+
// Simulate existing installation with password
|
|
92
|
+
fs.writeFileSync(resolve(testDir, ".env"),
|
|
93
|
+
"PGAI_TAG=0.12.0\nGF_SECURITY_ADMIN_PASSWORD=my-secure-password-123\n");
|
|
94
|
+
fs.writeFileSync(resolve(testDir, "docker-compose.yml"), "version: '3'\nservices: {}\n");
|
|
95
|
+
fs.writeFileSync(resolve(testDir, "instances.yml"), "# instances\n");
|
|
96
|
+
|
|
97
|
+
// Run local-install (upgrade)
|
|
98
|
+
// Note: Command will fail at Docker step (no Docker in CI), but .env is updated before that
|
|
99
|
+
runCliInDir(
|
|
100
|
+
["mon", "local-install", "--db-url", "postgresql://u:p@h:5432/d", "--yes"],
|
|
101
|
+
testDir,
|
|
102
|
+
{ PGAI_TAG: undefined }
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
const envContent = fs.readFileSync(resolve(testDir, ".env"), "utf8");
|
|
106
|
+
|
|
107
|
+
// Password should be preserved
|
|
108
|
+
expect(envContent).toMatch(/GF_SECURITY_ADMIN_PASSWORD=my-secure-password-123/);
|
|
109
|
+
// Tag should be updated
|
|
110
|
+
expect(envContent).not.toMatch(/PGAI_TAG=0\.12\.0/);
|
|
111
|
+
}, { timeout: TEST_TIMEOUT });
|
|
112
|
+
|
|
113
|
+
test("upgrade preserves custom registry", () => {
|
|
114
|
+
const testDir = resolve(tempDir, "upgrade-registry-test");
|
|
115
|
+
fs.mkdirSync(testDir, { recursive: true });
|
|
116
|
+
|
|
117
|
+
// Simulate existing installation with custom registry
|
|
118
|
+
fs.writeFileSync(resolve(testDir, ".env"),
|
|
119
|
+
"PGAI_TAG=0.11.0\nPGAI_REGISTRY=registry.example.com/postgres-ai\n");
|
|
120
|
+
fs.writeFileSync(resolve(testDir, "docker-compose.yml"), "version: '3'\nservices: {}\n");
|
|
121
|
+
fs.writeFileSync(resolve(testDir, "instances.yml"), "# instances\n");
|
|
122
|
+
|
|
123
|
+
// Run local-install (upgrade)
|
|
124
|
+
// Note: Command will fail at Docker step (no Docker in CI), but .env is updated before that
|
|
125
|
+
runCliInDir(
|
|
126
|
+
["mon", "local-install", "--db-url", "postgresql://u:p@h:5432/d", "--yes"],
|
|
127
|
+
testDir,
|
|
128
|
+
{ PGAI_TAG: undefined }
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
const envContent = fs.readFileSync(resolve(testDir, ".env"), "utf8");
|
|
132
|
+
|
|
133
|
+
// Registry should be preserved
|
|
134
|
+
expect(envContent).toMatch(/PGAI_REGISTRY=registry\.example\.com\/postgres-ai/);
|
|
135
|
+
// Tag should be updated
|
|
136
|
+
expect(envContent).not.toMatch(/PGAI_TAG=0\.11\.0/);
|
|
137
|
+
}, { timeout: TEST_TIMEOUT });
|
|
138
|
+
|
|
139
|
+
test("upgrade preserves all settings together", () => {
|
|
140
|
+
const testDir = resolve(tempDir, "upgrade-all-settings-test");
|
|
141
|
+
fs.mkdirSync(testDir, { recursive: true });
|
|
142
|
+
|
|
143
|
+
// Simulate existing installation with all settings
|
|
144
|
+
fs.writeFileSync(resolve(testDir, ".env"),
|
|
145
|
+
"PGAI_TAG=0.10.0\nPGAI_REGISTRY=my.registry.io\nGF_SECURITY_ADMIN_PASSWORD=super-secret\n");
|
|
146
|
+
fs.writeFileSync(resolve(testDir, "docker-compose.yml"), "version: '3'\nservices: {}\n");
|
|
147
|
+
fs.writeFileSync(resolve(testDir, "instances.yml"), "# instances\n");
|
|
148
|
+
|
|
149
|
+
// Run local-install (upgrade)
|
|
150
|
+
// Note: Command will fail at Docker step (no Docker in CI), but .env is updated before that
|
|
151
|
+
runCliInDir(
|
|
152
|
+
["mon", "local-install", "--db-url", "postgresql://u:p@h:5432/d", "--yes"],
|
|
153
|
+
testDir,
|
|
154
|
+
{ PGAI_TAG: undefined }
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
const envContent = fs.readFileSync(resolve(testDir, ".env"), "utf8");
|
|
158
|
+
|
|
159
|
+
// All settings should be preserved
|
|
160
|
+
expect(envContent).toMatch(/PGAI_REGISTRY=my\.registry\.io/);
|
|
161
|
+
expect(envContent).toMatch(/GF_SECURITY_ADMIN_PASSWORD=super-secret/);
|
|
162
|
+
// Tag should be updated to new version
|
|
163
|
+
expect(envContent).not.toMatch(/PGAI_TAG=0\.10\.0/);
|
|
164
|
+
expect(envContent).toMatch(/PGAI_TAG=/);
|
|
165
|
+
}, { timeout: TEST_TIMEOUT });
|
|
166
|
+
|
|
167
|
+
test("upgrade with --tag flag uses specified version", () => {
|
|
168
|
+
const testDir = resolve(tempDir, "upgrade-custom-tag-test");
|
|
169
|
+
fs.mkdirSync(testDir, { recursive: true });
|
|
170
|
+
|
|
171
|
+
// Simulate existing installation
|
|
172
|
+
fs.writeFileSync(resolve(testDir, ".env"), "PGAI_TAG=0.9.0\n");
|
|
173
|
+
fs.writeFileSync(resolve(testDir, "docker-compose.yml"), "version: '3'\nservices: {}\n");
|
|
174
|
+
fs.writeFileSync(resolve(testDir, "instances.yml"), "# instances\n");
|
|
175
|
+
|
|
176
|
+
// Run local-install with specific tag (for rollback or specific version upgrade)
|
|
177
|
+
// Note: Command will fail at Docker step (no Docker in CI), but .env is updated before that
|
|
178
|
+
const result = runCliInDir(
|
|
179
|
+
["mon", "local-install", "--tag", "0.14.0-beta.5", "--db-url", "postgresql://u:p@h:5432/d", "--yes"],
|
|
180
|
+
testDir,
|
|
181
|
+
{ PGAI_TAG: undefined }
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
const envContent = fs.readFileSync(resolve(testDir, ".env"), "utf8");
|
|
185
|
+
|
|
186
|
+
// Should use the specified tag
|
|
187
|
+
expect(envContent).toMatch(/PGAI_TAG=0\.14\.0-beta\.5/);
|
|
188
|
+
// Stdout should confirm the tag being used (happens before Docker step)
|
|
189
|
+
expect(result.stdout).toMatch(/Using image tag: 0\.14\.0-beta\.5/);
|
|
190
|
+
}, { timeout: TEST_TIMEOUT });
|
|
191
|
+
|
|
192
|
+
test("upgrade preserves .pgwatch-config file", () => {
|
|
193
|
+
const testDir = resolve(tempDir, "upgrade-config-test");
|
|
194
|
+
fs.mkdirSync(testDir, { recursive: true });
|
|
195
|
+
|
|
196
|
+
// Simulate existing installation with config file
|
|
197
|
+
fs.writeFileSync(resolve(testDir, ".env"), "PGAI_TAG=0.8.0\n");
|
|
198
|
+
fs.writeFileSync(resolve(testDir, "docker-compose.yml"), "version: '3'\nservices: {}\n");
|
|
199
|
+
fs.writeFileSync(resolve(testDir, "instances.yml"), "# instances\n");
|
|
200
|
+
fs.writeFileSync(resolve(testDir, ".pgwatch-config"),
|
|
201
|
+
"api_key=test-api-key-12345\ngrafana_password=existing-password\n");
|
|
202
|
+
|
|
203
|
+
// Run local-install (upgrade)
|
|
204
|
+
// Note: Command will fail at Docker step (no Docker in CI), but config file is preserved
|
|
205
|
+
runCliInDir(
|
|
206
|
+
["mon", "local-install", "--db-url", "postgresql://u:p@h:5432/d", "--yes"],
|
|
207
|
+
testDir,
|
|
208
|
+
{ PGAI_TAG: undefined }
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
// Config file should still exist
|
|
212
|
+
expect(fs.existsSync(resolve(testDir, ".pgwatch-config"))).toBe(true);
|
|
213
|
+
|
|
214
|
+
const configContent = fs.readFileSync(resolve(testDir, ".pgwatch-config"), "utf8");
|
|
215
|
+
// Grafana password should be preserved (not overwritten since it exists)
|
|
216
|
+
expect(configContent).toMatch(/grafana_password=existing-password/);
|
|
217
|
+
}, { timeout: TEST_TIMEOUT });
|
|
218
|
+
|
|
219
|
+
test("instances.yml is preserved during upgrade", () => {
|
|
220
|
+
const testDir = resolve(tempDir, "upgrade-instances-test");
|
|
221
|
+
fs.mkdirSync(testDir, { recursive: true });
|
|
222
|
+
|
|
223
|
+
// Simulate existing installation with instances
|
|
224
|
+
fs.writeFileSync(resolve(testDir, ".env"), "PGAI_TAG=0.7.0\n");
|
|
225
|
+
fs.writeFileSync(resolve(testDir, "docker-compose.yml"), "version: '3'\nservices: {}\n");
|
|
226
|
+
|
|
227
|
+
const instancesContent = `# PostgreSQL instances to monitor
|
|
228
|
+
- name: production-db
|
|
229
|
+
conn_str: postgresql://monitor:pass@prod.example.com:5432/mydb
|
|
230
|
+
preset_metrics: full
|
|
231
|
+
is_enabled: true
|
|
232
|
+
`;
|
|
233
|
+
fs.writeFileSync(resolve(testDir, "instances.yml"), instancesContent);
|
|
234
|
+
|
|
235
|
+
// Note: local-install in production mode clears instances.yml
|
|
236
|
+
// This is intentional behavior - upgrade should use 'mon start' not 'local-install'
|
|
237
|
+
// for preserving instances. Testing that the file exists after operation.
|
|
238
|
+
// Note: Command will fail at Docker step (no Docker in CI), but file is created
|
|
239
|
+
runCliInDir(
|
|
240
|
+
["mon", "local-install", "--db-url", "postgresql://u:p@h:5432/d", "--yes"],
|
|
241
|
+
testDir,
|
|
242
|
+
{ PGAI_TAG: undefined }
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
// instances.yml should exist (content may be reset by local-install)
|
|
246
|
+
expect(fs.existsSync(resolve(testDir, "instances.yml"))).toBe(true);
|
|
247
|
+
}, { timeout: TEST_TIMEOUT });
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
describe("upgrade error handling", () => {
|
|
251
|
+
/**
|
|
252
|
+
* Tests for edge cases and error scenarios in the upgrade workflow.
|
|
253
|
+
* These ensure the CLI handles incomplete or malformed configurations gracefully.
|
|
254
|
+
*/
|
|
255
|
+
|
|
256
|
+
let tempDir: string;
|
|
257
|
+
|
|
258
|
+
beforeAll(() => {
|
|
259
|
+
tempDir = fs.mkdtempSync(resolve(os.tmpdir(), "pgai-upgrade-error-test-"));
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
afterAll(() => {
|
|
263
|
+
if (tempDir && fs.existsSync(tempDir)) {
|
|
264
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
test("local-install creates .env if missing", () => {
|
|
269
|
+
const testDir = resolve(tempDir, "missing-env-test");
|
|
270
|
+
fs.mkdirSync(testDir, { recursive: true });
|
|
271
|
+
|
|
272
|
+
// Only create docker-compose.yml (no .env)
|
|
273
|
+
fs.writeFileSync(resolve(testDir, "docker-compose.yml"), "version: '3'\nservices: {}\n");
|
|
274
|
+
|
|
275
|
+
// Run local-install without existing .env
|
|
276
|
+
// Note: Command will fail at Docker step (no Docker in CI), but .env is created before that
|
|
277
|
+
runCliInDir(
|
|
278
|
+
["mon", "local-install", "--db-url", "postgresql://u:p@h:5432/d", "--yes"],
|
|
279
|
+
testDir,
|
|
280
|
+
{ PGAI_TAG: undefined }
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
// .env should be created (before Docker operations fail)
|
|
284
|
+
expect(fs.existsSync(resolve(testDir, ".env"))).toBe(true);
|
|
285
|
+
|
|
286
|
+
const envContent = fs.readFileSync(resolve(testDir, ".env"), "utf8");
|
|
287
|
+
expect(envContent).toMatch(/PGAI_TAG=/);
|
|
288
|
+
expect(envContent).toMatch(/REPLICATOR_PASSWORD=[a-f0-9]{64}/);
|
|
289
|
+
expect(envContent).toMatch(/VM_AUTH_USERNAME=vmauth/);
|
|
290
|
+
expect(envContent).toMatch(/^VM_AUTH_PASSWORD=[A-Za-z0-9+/]+={0,2}\s*$/m);
|
|
291
|
+
}, { timeout: TEST_TIMEOUT });
|
|
292
|
+
|
|
293
|
+
test("local-install handles .env without PGAI_TAG line", () => {
|
|
294
|
+
const testDir = resolve(tempDir, "no-tag-line-test");
|
|
295
|
+
fs.mkdirSync(testDir, { recursive: true });
|
|
296
|
+
|
|
297
|
+
// Create .env without PGAI_TAG (only has other settings)
|
|
298
|
+
fs.writeFileSync(resolve(testDir, ".env"), "GF_SECURITY_ADMIN_PASSWORD=old-password\nREPLICATOR_PASSWORD=existing-repl\nVM_AUTH_USERNAME=existing-vm-user\nVM_AUTH_PASSWORD=existing-vm-pass\n");
|
|
299
|
+
fs.writeFileSync(resolve(testDir, "docker-compose.yml"), "version: '3'\nservices: {}\n");
|
|
300
|
+
fs.writeFileSync(resolve(testDir, "instances.yml"), "# instances\n");
|
|
301
|
+
|
|
302
|
+
// Run local-install
|
|
303
|
+
// Note: Command will fail at Docker step (no Docker in CI), but .env is updated before that
|
|
304
|
+
runCliInDir(
|
|
305
|
+
["mon", "local-install", "--db-url", "postgresql://u:p@h:5432/d", "--yes"],
|
|
306
|
+
testDir,
|
|
307
|
+
{ PGAI_TAG: undefined }
|
|
308
|
+
);
|
|
309
|
+
|
|
310
|
+
const envContent = fs.readFileSync(resolve(testDir, ".env"), "utf8");
|
|
311
|
+
// Should add PGAI_TAG to the file
|
|
312
|
+
expect(envContent).toMatch(/PGAI_TAG=/);
|
|
313
|
+
// Should preserve existing settings
|
|
314
|
+
expect(envContent).toMatch(/GF_SECURITY_ADMIN_PASSWORD=old-password/);
|
|
315
|
+
expect(envContent).toMatch(/REPLICATOR_PASSWORD=existing-repl/);
|
|
316
|
+
expect(envContent).toMatch(/VM_AUTH_USERNAME=existing-vm-user/);
|
|
317
|
+
expect(envContent).toMatch(/VM_AUTH_PASSWORD=existing-vm-pass/);
|
|
318
|
+
}, { timeout: TEST_TIMEOUT });
|
|
319
|
+
|
|
320
|
+
test("local-install strips only matching quotes from VM auth values", () => {
|
|
321
|
+
const testDir = resolve(tempDir, "quoted-vm-auth-test");
|
|
322
|
+
fs.mkdirSync(testDir, { recursive: true });
|
|
323
|
+
|
|
324
|
+
fs.writeFileSync(
|
|
325
|
+
resolve(testDir, ".env"),
|
|
326
|
+
"VM_AUTH_USERNAME=\"quoted-vm-user\"\nVM_AUTH_PASSWORD='quoted-vm-pass'\n"
|
|
327
|
+
);
|
|
328
|
+
fs.writeFileSync(resolve(testDir, "docker-compose.yml"), "version: '3'\nservices: {}\n");
|
|
329
|
+
fs.writeFileSync(resolve(testDir, "instances.yml"), "# instances\n");
|
|
330
|
+
|
|
331
|
+
runCliInDir(
|
|
332
|
+
["mon", "local-install", "--db-url", "postgresql://u:p@h:5432/d", "--yes"],
|
|
333
|
+
testDir,
|
|
334
|
+
{ PGAI_TAG: undefined }
|
|
335
|
+
);
|
|
336
|
+
|
|
337
|
+
const envContent = fs.readFileSync(resolve(testDir, ".env"), "utf8");
|
|
338
|
+
expect(envContent).toMatch(/VM_AUTH_USERNAME=quoted-vm-user/);
|
|
339
|
+
expect(envContent).toMatch(/VM_AUTH_PASSWORD=quoted-vm-pass/);
|
|
340
|
+
}, { timeout: TEST_TIMEOUT });
|
|
341
|
+
|
|
342
|
+
test("local-install handles same version (no-op scenario)", () => {
|
|
343
|
+
const testDir = resolve(tempDir, "same-version-test");
|
|
344
|
+
fs.mkdirSync(testDir, { recursive: true });
|
|
345
|
+
|
|
346
|
+
// First, run local-install to get the current CLI version
|
|
347
|
+
fs.writeFileSync(resolve(testDir, ".env"), "PGAI_TAG=0.0.0-placeholder\n");
|
|
348
|
+
fs.writeFileSync(resolve(testDir, "docker-compose.yml"), "version: '3'\nservices: {}\n");
|
|
349
|
+
fs.writeFileSync(resolve(testDir, "instances.yml"), "# instances\n");
|
|
350
|
+
|
|
351
|
+
// Note: Command will fail at Docker step (no Docker in CI), but .env is updated before that
|
|
352
|
+
runCliInDir(
|
|
353
|
+
["mon", "local-install", "--db-url", "postgresql://u:p@h:5432/d", "--yes"],
|
|
354
|
+
testDir,
|
|
355
|
+
{ PGAI_TAG: undefined }
|
|
356
|
+
);
|
|
357
|
+
|
|
358
|
+
const firstEnv = fs.readFileSync(resolve(testDir, ".env"), "utf8");
|
|
359
|
+
expect(firstEnv).toMatch(/PGAI_TAG=/);
|
|
360
|
+
|
|
361
|
+
// Run again with same version - should update .env identically
|
|
362
|
+
runCliInDir(
|
|
363
|
+
["mon", "local-install", "--db-url", "postgresql://u:p@h:5432/d", "--yes"],
|
|
364
|
+
testDir,
|
|
365
|
+
{ PGAI_TAG: undefined }
|
|
366
|
+
);
|
|
367
|
+
|
|
368
|
+
// .env should still have a valid tag
|
|
369
|
+
const finalEnv = fs.readFileSync(resolve(testDir, ".env"), "utf8");
|
|
370
|
+
expect(finalEnv).toMatch(/PGAI_TAG=/);
|
|
371
|
+
}, { timeout: TEST_TIMEOUT });
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
describe("upgrade CLI commands", () => {
|
|
375
|
+
test("mon stop command exists and shows help", () => {
|
|
376
|
+
const cliPath = resolve(import.meta.dir, "..", "bin", "postgres-ai.ts");
|
|
377
|
+
const bunBin = typeof process.execPath === "string" && process.execPath.length > 0 ? process.execPath : "bun";
|
|
378
|
+
const result = Bun.spawnSync([bunBin, cliPath, "mon", "stop", "--help"], {
|
|
379
|
+
env: process.env,
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
expect(result.exitCode).toBe(0);
|
|
383
|
+
const stdout = new TextDecoder().decode(result.stdout);
|
|
384
|
+
expect(stdout).toMatch(/stop monitoring services/i);
|
|
385
|
+
}, { timeout: TEST_TIMEOUT });
|
|
386
|
+
|
|
387
|
+
test("mon start command exists and shows help", () => {
|
|
388
|
+
const cliPath = resolve(import.meta.dir, "..", "bin", "postgres-ai.ts");
|
|
389
|
+
const bunBin = typeof process.execPath === "string" && process.execPath.length > 0 ? process.execPath : "bun";
|
|
390
|
+
const result = Bun.spawnSync([bunBin, cliPath, "mon", "start", "--help"], {
|
|
391
|
+
env: process.env,
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
expect(result.exitCode).toBe(0);
|
|
395
|
+
const stdout = new TextDecoder().decode(result.stdout);
|
|
396
|
+
expect(stdout).toMatch(/start monitoring services/i);
|
|
397
|
+
}, { timeout: TEST_TIMEOUT });
|
|
398
|
+
|
|
399
|
+
test("mon status command exists and shows help", () => {
|
|
400
|
+
const cliPath = resolve(import.meta.dir, "..", "bin", "postgres-ai.ts");
|
|
401
|
+
const bunBin = typeof process.execPath === "string" && process.execPath.length > 0 ? process.execPath : "bun";
|
|
402
|
+
const result = Bun.spawnSync([bunBin, cliPath, "mon", "status", "--help"], {
|
|
403
|
+
env: process.env,
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
expect(result.exitCode).toBe(0);
|
|
407
|
+
const stdout = new TextDecoder().decode(result.stdout);
|
|
408
|
+
expect(stdout).toMatch(/status/i);
|
|
409
|
+
}, { timeout: TEST_TIMEOUT });
|
|
410
|
+
|
|
411
|
+
test("mon health command exists and shows help", () => {
|
|
412
|
+
const cliPath = resolve(import.meta.dir, "..", "bin", "postgres-ai.ts");
|
|
413
|
+
const bunBin = typeof process.execPath === "string" && process.execPath.length > 0 ? process.execPath : "bun";
|
|
414
|
+
const result = Bun.spawnSync([bunBin, cliPath, "mon", "health", "--help"], {
|
|
415
|
+
env: process.env,
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
expect(result.exitCode).toBe(0);
|
|
419
|
+
const stdout = new TextDecoder().decode(result.stdout);
|
|
420
|
+
expect(stdout).toMatch(/health/i);
|
|
421
|
+
}, { timeout: TEST_TIMEOUT });
|
|
422
|
+
});
|