pgserve 2.1.2 → 2.2.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.
Files changed (227) hide show
  1. package/CHANGELOG.md +86 -0
  2. package/README.md +105 -1
  3. package/bin/autopg-wrapper.cjs +16 -0
  4. package/bin/pgserve-wrapper.cjs +31 -6
  5. package/bin/postgres-server.js +80 -7
  6. package/console/README.md +131 -0
  7. package/console/api.js +173 -0
  8. package/console/app.jsx +483 -0
  9. package/console/colors_and_type.css +227 -0
  10. package/console/components.jsx +167 -0
  11. package/console/console.css +1666 -0
  12. package/console/data.jsx +350 -0
  13. package/console/index.html +31 -0
  14. package/console/screens/databases.jsx +5 -0
  15. package/console/screens/health.jsx +5 -0
  16. package/console/screens/ingress.jsx +5 -0
  17. package/console/screens/optimizer.jsx +5 -0
  18. package/console/screens/rlm-sim.jsx +5 -0
  19. package/console/screens/rlm-trace.jsx +5 -0
  20. package/console/screens/security.jsx +5 -0
  21. package/console/screens/settings.jsx +611 -0
  22. package/console/screens/sql.jsx +5 -0
  23. package/console/screens/sync.jsx +5 -0
  24. package/console/screens/tables.jsx +5 -0
  25. package/console/tweaks-panel.jsx +425 -0
  26. package/package.json +11 -1
  27. package/src/cli-config.cjs +310 -0
  28. package/src/cli-install.cjs +98 -11
  29. package/src/cli-restart.cjs +228 -0
  30. package/src/cli-ui.cjs +580 -0
  31. package/src/cluster.js +43 -38
  32. package/src/postgres.js +141 -19
  33. package/src/settings-loader.cjs +235 -0
  34. package/src/settings-migrate.cjs +212 -0
  35. package/src/settings-pg-args.cjs +146 -0
  36. package/src/settings-schema.cjs +422 -0
  37. package/src/settings-validator.cjs +416 -0
  38. package/src/settings-writer.cjs +288 -0
  39. package/.claude/context/windows-debug.md +0 -119
  40. package/.genie/AGENTS.md +0 -15
  41. package/.genie/agents/README.md +0 -110
  42. package/.genie/agents/analyze.md +0 -176
  43. package/.genie/agents/forge.md +0 -290
  44. package/.genie/agents/garbage-cleaner.md +0 -324
  45. package/.genie/agents/garbage-collector.md +0 -596
  46. package/.genie/agents/github-issue-gc.md +0 -618
  47. package/.genie/agents/review.md +0 -380
  48. package/.genie/agents/semantic-analyzer/find-duplicates.md +0 -90
  49. package/.genie/agents/semantic-analyzer/find-orphans.md +0 -99
  50. package/.genie/agents/semantic-analyzer.md +0 -101
  51. package/.genie/agents/update.md +0 -182
  52. package/.genie/agents/wish.md +0 -357
  53. package/.genie/brainstorms/pgserve-v2/DESIGN.md +0 -174
  54. package/.genie/code/AGENTS.md +0 -694
  55. package/.genie/code/agents/audit/risk.md +0 -173
  56. package/.genie/code/agents/audit/security.md +0 -189
  57. package/.genie/code/agents/audit.md +0 -145
  58. package/.genie/code/agents/challenge.md +0 -230
  59. package/.genie/code/agents/change-reviewer.md +0 -295
  60. package/.genie/code/agents/code-garbage-collector.md +0 -425
  61. package/.genie/code/agents/code-quality.md +0 -410
  62. package/.genie/code/agents/commit-suggester.md +0 -255
  63. package/.genie/code/agents/commit.md +0 -124
  64. package/.genie/code/agents/consensus.md +0 -204
  65. package/.genie/code/agents/daily-standup.md +0 -722
  66. package/.genie/code/agents/docgen.md +0 -48
  67. package/.genie/code/agents/explore.md +0 -79
  68. package/.genie/code/agents/fix.md +0 -100
  69. package/.genie/code/agents/git/commit-advisory.md +0 -219
  70. package/.genie/code/agents/git/workflows/issue.md +0 -244
  71. package/.genie/code/agents/git/workflows/pr.md +0 -179
  72. package/.genie/code/agents/git/workflows/release.md +0 -460
  73. package/.genie/code/agents/git/workflows/report.md +0 -342
  74. package/.genie/code/agents/git.md +0 -432
  75. package/.genie/code/agents/implementor.md +0 -161
  76. package/.genie/code/agents/install.md +0 -515
  77. package/.genie/code/agents/issue-creator.md +0 -344
  78. package/.genie/code/agents/polish.md +0 -116
  79. package/.genie/code/agents/qa.md +0 -653
  80. package/.genie/code/agents/refactor.md +0 -294
  81. package/.genie/code/agents/release.md +0 -1129
  82. package/.genie/code/agents/roadmap.md +0 -885
  83. package/.genie/code/agents/tests.md +0 -557
  84. package/.genie/code/agents/tracer.md +0 -50
  85. package/.genie/code/agents/update/upstream-update.md +0 -85
  86. package/.genie/code/agents/update/versions/generic-update.md +0 -305
  87. package/.genie/code/agents/vibe.md +0 -1317
  88. package/.genie/code/spells/agent-configuration.md +0 -58
  89. package/.genie/code/spells/automated-rc-publishing.md +0 -106
  90. package/.genie/code/spells/branch-tracker-guidance.md +0 -28
  91. package/.genie/code/spells/debug.md +0 -320
  92. package/.genie/code/spells/emoji-naming-convention.md +0 -303
  93. package/.genie/code/spells/evidence-storage.md +0 -26
  94. package/.genie/code/spells/file-naming-rules.md +0 -35
  95. package/.genie/code/spells/forge-code-blueprints.md +0 -195
  96. package/.genie/code/spells/genie-integration.md +0 -153
  97. package/.genie/code/spells/publishing-protocol.md +0 -61
  98. package/.genie/code/spells/team-consultation-protocol.md +0 -284
  99. package/.genie/code/spells/tool-requirements.md +0 -20
  100. package/.genie/code/spells/triad-maintenance-protocol.md +0 -154
  101. package/.genie/code/teams/tech-council/council.md +0 -328
  102. package/.genie/code/teams/tech-council/jt.md +0 -352
  103. package/.genie/code/teams/tech-council/nayr.md +0 -305
  104. package/.genie/code/teams/tech-council/oettam.md +0 -375
  105. package/.genie/neurons/README.md +0 -193
  106. package/.genie/neurons/forge.md +0 -106
  107. package/.genie/neurons/genie.md +0 -63
  108. package/.genie/neurons/review.md +0 -106
  109. package/.genie/neurons/wish.md +0 -104
  110. package/.genie/product/README.md +0 -20
  111. package/.genie/product/cli-automation.md +0 -359
  112. package/.genie/product/environment.md +0 -60
  113. package/.genie/product/mission.md +0 -60
  114. package/.genie/product/roadmap.md +0 -44
  115. package/.genie/product/tech-stack.md +0 -34
  116. package/.genie/product/templates/context-template.md +0 -218
  117. package/.genie/product/templates/qa-done-report-template.md +0 -68
  118. package/.genie/product/templates/review-report-template.md +0 -89
  119. package/.genie/product/templates/wish-template.md +0 -120
  120. package/.genie/scripts/helpers/analyze-commit.js +0 -195
  121. package/.genie/scripts/helpers/bullet-counter.js +0 -194
  122. package/.genie/scripts/helpers/bullet-find.js +0 -289
  123. package/.genie/scripts/helpers/bullet-id.js +0 -244
  124. package/.genie/scripts/helpers/check-secrets.js +0 -237
  125. package/.genie/scripts/helpers/count-tokens.js +0 -200
  126. package/.genie/scripts/helpers/create-frontmatter.js +0 -456
  127. package/.genie/scripts/helpers/detect-markers.js +0 -293
  128. package/.genie/scripts/helpers/detect-todos.js +0 -267
  129. package/.genie/scripts/helpers/detect-unlabeled-blocks.js +0 -135
  130. package/.genie/scripts/helpers/embeddings.js +0 -344
  131. package/.genie/scripts/helpers/find-empty-sections.js +0 -158
  132. package/.genie/scripts/helpers/index.js +0 -319
  133. package/.genie/scripts/helpers/validate-frontmatter.js +0 -578
  134. package/.genie/scripts/helpers/validate-links.js +0 -207
  135. package/.genie/scripts/helpers/validate-paths.js +0 -373
  136. package/.genie/spells/README.md +0 -9
  137. package/.genie/spells/ace-protocol.md +0 -118
  138. package/.genie/spells/ask-one-at-a-time.md +0 -175
  139. package/.genie/spells/backup-analyzer.md +0 -542
  140. package/.genie/spells/blocker.md +0 -12
  141. package/.genie/spells/break-things-move-fast.md +0 -56
  142. package/.genie/spells/context-candidates.md +0 -72
  143. package/.genie/spells/context-critic.md +0 -51
  144. package/.genie/spells/defer-to-expertise.md +0 -278
  145. package/.genie/spells/delegate-dont-do.md +0 -292
  146. package/.genie/spells/error-investigation-protocol.md +0 -328
  147. package/.genie/spells/evidence-based-completion.md +0 -273
  148. package/.genie/spells/experiment.md +0 -65
  149. package/.genie/spells/file-creation-protocol.md +0 -229
  150. package/.genie/spells/forge-integration.md +0 -281
  151. package/.genie/spells/forge-orchestration.md +0 -514
  152. package/.genie/spells/gather-context.md +0 -18
  153. package/.genie/spells/global-health-check.md +0 -34
  154. package/.genie/spells/global-noop-roundtrip.md +0 -25
  155. package/.genie/spells/install-genie.md +0 -1232
  156. package/.genie/spells/install.md +0 -82
  157. package/.genie/spells/investigate-before-commit.md +0 -112
  158. package/.genie/spells/know-yourself.md +0 -288
  159. package/.genie/spells/learn.md +0 -828
  160. package/.genie/spells/mcp-diagnostic-protocol.md +0 -246
  161. package/.genie/spells/mcp-first.md +0 -124
  162. package/.genie/spells/multi-step-execution.md +0 -67
  163. package/.genie/spells/orchestration-boundary-protocol.md +0 -256
  164. package/.genie/spells/orchestrator-not-implementor.md +0 -189
  165. package/.genie/spells/prompt.md +0 -746
  166. package/.genie/spells/reflect.md +0 -404
  167. package/.genie/spells/routing-decision-matrix.md +0 -368
  168. package/.genie/spells/run-in-parallel.md +0 -12
  169. package/.genie/spells/session-state-updater-example.md +0 -196
  170. package/.genie/spells/session-state-updater.md +0 -220
  171. package/.genie/spells/track-long-running-tasks.md +0 -133
  172. package/.genie/spells/troubleshoot-infrastructure.md +0 -176
  173. package/.genie/spells/upgrade-genie.md +0 -415
  174. package/.genie/spells/url-presentation-protocol.md +0 -301
  175. package/.genie/spells/wish-initiation.md +0 -158
  176. package/.genie/spells/wish-issue-linkage.md +0 -410
  177. package/.genie/spells/wish-lifecycle.md +0 -100
  178. package/.genie/state/provider-status.json +0 -3
  179. package/.genie/state/version.json +0 -16
  180. package/.genie/wishes/canonical-pgserve-pm2-supervision/WISH.md +0 -290
  181. package/.genie/wishes/pgserve-v2/BRIEF-from-genie-pgserve.md +0 -99
  182. package/.genie/wishes/pgserve-v2/WISH.md +0 -442
  183. package/.genie/wishes/release-system-genie-pattern/WISH.md +0 -268
  184. package/.genie/wishes/release-system-genie-pattern/validation.md +0 -205
  185. package/.gitguardian.yaml +0 -29
  186. package/.gitguardianignore +0 -16
  187. package/.github/workflows/ci.yml +0 -122
  188. package/.github/workflows/release.yml +0 -289
  189. package/.github/workflows/version.yml +0 -228
  190. package/.husky/pre-commit +0 -2
  191. package/AGENTS.md +0 -433
  192. package/CLAUDE.md +0 -1
  193. package/Makefile +0 -285
  194. package/assets/icon.ico +0 -0
  195. package/bun.lock +0 -435
  196. package/bunfig.toml +0 -28
  197. package/ecosystem.config.cjs +0 -23
  198. package/eslint.config.js +0 -63
  199. package/examples/multi-tenant-demo.js +0 -104
  200. package/install.sh +0 -123
  201. package/knip.json +0 -9
  202. package/scripts/test-bun-self-heal.sh +0 -163
  203. package/scripts/test-npx.sh +0 -60
  204. package/tests/audit.test.js +0 -189
  205. package/tests/backpressure.test.js +0 -167
  206. package/tests/benchmarks/runner.js +0 -1197
  207. package/tests/benchmarks/vector-generator.js +0 -368
  208. package/tests/cli-install.test.js +0 -322
  209. package/tests/control-db.test.js +0 -285
  210. package/tests/daemon-control.test.js +0 -171
  211. package/tests/daemon-fingerprint-integration.test.js +0 -111
  212. package/tests/daemon-pr24-regression.test.js +0 -198
  213. package/tests/fingerprint.test.js +0 -263
  214. package/tests/fixtures/240-orphan-seed.sql +0 -30
  215. package/tests/multi-tenant.test.js +0 -374
  216. package/tests/orphan-cleanup.test.js +0 -390
  217. package/tests/pg-version-regex.test.js +0 -129
  218. package/tests/quick-bench.js +0 -135
  219. package/tests/router-handshake-retry.test.js +0 -119
  220. package/tests/router-handshake-watchdog.test.js +0 -110
  221. package/tests/sdk.test.js +0 -71
  222. package/tests/stale-postmaster-pid.test.js +0 -85
  223. package/tests/stress-test.js +0 -439
  224. package/tests/sync-perf-test.js +0 -150
  225. package/tests/tcp-listen.test.js +0 -368
  226. package/tests/tenancy.test.js +0 -403
  227. package/tests/wrapper-supervision.test.js +0 -107
@@ -1,403 +0,0 @@
1
- /**
2
- * Group 4 — database-per-fingerprint + enforcement + kill switch.
3
- *
4
- * Boots a real pgserve daemon with isolated control socket + audit log,
5
- * stubs SO_PEERCRED to return synthetic creds, and overrides the
6
- * fingerprint-derivation cwd per-accept so a single test process can
7
- * masquerade as several different "projects" connecting to the daemon.
8
- *
9
- * Coverage (mirrors WISH §Group 4 acceptance bullets):
10
- * 1. Two peers with different fingerprints get different DBs
11
- * 2. Same peer reconnecting reaches its existing DB
12
- * 3. Cross-fingerprint connection denied with SQLSTATE 28P01
13
- * 4. Kill-switch env: cross-fingerprint succeeds + audit event emitted
14
- * 5. Sanitizer: name "@scope/foo bar" → "_scope_foo_bar"
15
- *
16
- * Plus unit tests on `sanitizeName` and `resolveTenantDatabaseName` and a
17
- * boot-time deprecation warning check.
18
- */
19
-
20
- import {
21
- describe,
22
- test,
23
- expect,
24
- beforeAll,
25
- afterAll,
26
- beforeEach,
27
- afterEach,
28
- } from 'bun:test';
29
- import fs from 'fs';
30
- import os from 'os';
31
- import path from 'path';
32
- import pg from 'pg';
33
-
34
- import {
35
- PgserveDaemon,
36
- resolveControlSocketPath,
37
- resolvePidLockPath,
38
- resolveLibpqCompatPath,
39
- } from '../src/daemon.js';
40
- import { _setPeerCredImpl, initFingerprintFfi } from '../src/fingerprint.js';
41
- import { configureAudit, AUDIT_EVENTS } from '../src/audit.js';
42
- import {
43
- sanitizeName,
44
- resolveTenantDatabaseName,
45
- KILL_SWITCH_ENV,
46
- } from '../src/tenancy.js';
47
- import { createLogger } from '../src/logger.js';
48
-
49
- const { Client } = pg;
50
-
51
- // ---------------------------------------------------------------------------
52
- // Pure-function unit tests
53
- // ---------------------------------------------------------------------------
54
-
55
- describe('sanitizeName', () => {
56
- test('collapses non-[a-z0-9] runs to a single underscore', () => {
57
- expect(sanitizeName('hello-world')).toBe('hello_world');
58
- expect(sanitizeName('hello---world')).toBe('hello_world');
59
- expect(sanitizeName('a..b..c')).toBe('a_b_c');
60
- });
61
-
62
- test('lowercases', () => {
63
- expect(sanitizeName('UPPER-CASE')).toBe('upper_case');
64
- expect(sanitizeName('MixedCase')).toBe('mixedcase');
65
- });
66
-
67
- test('preserves alphanumerics', () => {
68
- expect(sanitizeName('foo123')).toBe('foo123');
69
- expect(sanitizeName('1to1')).toBe('1to1');
70
- });
71
-
72
- test('truncates to 30 chars', () => {
73
- const long = 'a'.repeat(50);
74
- expect(sanitizeName(long).length).toBe(30);
75
- });
76
-
77
- test('handles the wish-spec example', () => {
78
- expect(sanitizeName('@scope/foo bar')).toBe('_scope_foo_bar');
79
- });
80
-
81
- test('falls back to "anon" for empty or pure-non-alphanumeric input', () => {
82
- expect(sanitizeName('')).toBe('anon');
83
- expect(sanitizeName(null)).toBe('anon');
84
- expect(sanitizeName(undefined)).toBe('anon');
85
- expect(sanitizeName('@@@')).toBe('anon');
86
- });
87
- });
88
-
89
- describe('resolveTenantDatabaseName', () => {
90
- test('builds canonical app_<sanitized>_<fingerprint>', () => {
91
- expect(resolveTenantDatabaseName({ name: 'demo', fingerprint: 'abcdef012345' }))
92
- .toBe('app_demo_abcdef012345');
93
- });
94
-
95
- test('applies sanitization', () => {
96
- expect(resolveTenantDatabaseName({ name: '@scope/foo bar', fingerprint: 'abcdef012345' }))
97
- .toBe('app__scope_foo_bar_abcdef012345');
98
- });
99
-
100
- test('rejects malformed fingerprints', () => {
101
- expect(() => resolveTenantDatabaseName({ name: 'x', fingerprint: 'TOO-SHORT' }))
102
- .toThrow(/12 hex chars/);
103
- expect(() => resolveTenantDatabaseName({ name: 'x', fingerprint: 'GHIJKL012345' }))
104
- .toThrow(/12 hex chars/);
105
- });
106
-
107
- test('result fits in PG identifier limit (≤63 chars)', () => {
108
- const longName = 'a'.repeat(80);
109
- const ident = resolveTenantDatabaseName({ name: longName, fingerprint: 'abcdef012345' });
110
- expect(ident.length).toBeLessThanOrEqual(63);
111
- });
112
- });
113
-
114
- // ---------------------------------------------------------------------------
115
- // Daemon integration tests
116
- //
117
- // One daemon shared across the integration suite — PG startup is slow and
118
- // the tests are independent at the pgserve_meta level (each clears its
119
- // state). Per-accept fingerprint behaviour is driven by an override queue
120
- // the test pushes into before each connect.
121
- // ---------------------------------------------------------------------------
122
-
123
- describe('daemon tenancy enforcement', () => {
124
- let daemon;
125
- let scratch;
126
- let controlSocketDir;
127
- let auditFile;
128
- let overridesQueue;
129
- let savedAuditDefaults;
130
-
131
- beforeAll(async () => {
132
- await initFingerprintFfi();
133
- // Stub peer creds: every accept on the test daemon's control socket
134
- // appears to come from this process. The real creds matter only for
135
- // uid (used in fingerprint hashing); pid is ignored once we override
136
- // cwd via `_fingerprintAcceptOpts`.
137
- _setPeerCredImpl(() => ({
138
- pid: process.pid,
139
- uid: process.getuid(),
140
- gid: process.getgid(),
141
- }));
142
-
143
- scratch = fs.mkdtempSync(path.join(os.tmpdir(), 'pgserve-tenancy-test-'));
144
- controlSocketDir = path.join(scratch, 'sock');
145
- fs.mkdirSync(controlSocketDir, { recursive: true });
146
- auditFile = path.join(scratch, 'audit.log');
147
-
148
- // Save the audit module's mutable globals so we can restore them after
149
- // the suite (other tests rely on the defaults).
150
- savedAuditDefaults = {
151
- logFile: path.join(os.homedir(), '.pgserve', 'audit.log'),
152
- target: process.env.PGSERVE_AUDIT_TARGET || 'file',
153
- };
154
-
155
- overridesQueue = [];
156
-
157
- daemon = new PgserveDaemon({
158
- controlSocketDir,
159
- controlSocketPath: resolveControlSocketPath(controlSocketDir),
160
- pidLockPath: resolvePidLockPath(controlSocketDir),
161
- libpqCompatPath: resolveLibpqCompatPath(controlSocketDir, 5432),
162
- auditLogFile: auditFile,
163
- auditTarget: 'file',
164
- pgPort: 16700,
165
- logger: createLogger({ level: process.env.LOG_LEVEL || 'warn' }),
166
- _fingerprintAcceptOpts: () => overridesQueue.shift() || {},
167
- });
168
- await daemon.start();
169
- });
170
-
171
- afterAll(async () => {
172
- try { await daemon.stop(); } catch { /* swallow */ }
173
- _setPeerCredImpl(null);
174
- if (savedAuditDefaults) configureAudit(savedAuditDefaults);
175
- try { fs.rmSync(scratch, { recursive: true, force: true }); } catch { /* swallow */ }
176
- });
177
-
178
- beforeEach(async () => {
179
- overridesQueue.length = 0;
180
- // Clear pgserve_meta and drop any user DBs from prior tests so each
181
- // test starts from a clean slate.
182
- if (daemon._adminClient) {
183
- try { await daemon._adminClient.query('TRUNCATE pgserve_meta'); } catch { /* schema not yet created in odd cases */ }
184
- const r = await daemon._adminClient.query(`
185
- SELECT datname FROM pg_database
186
- WHERE datname LIKE 'app_%' AND datistemplate = false
187
- `);
188
- for (const row of r.rows) {
189
- try { await daemon._adminClient.query(`DROP DATABASE "${row.datname}"`); } catch { /* swallow */ }
190
- }
191
- }
192
- // Reset audit log so each test reads only its own events.
193
- try { fs.writeFileSync(auditFile, '', { mode: 0o600 }); } catch { /* swallow */ }
194
- daemon.enforcementDisabled = false;
195
- });
196
-
197
- function makeProject(name, dirName = name) {
198
- const dir = path.join(scratch, dirName);
199
- fs.mkdirSync(dir, { recursive: true });
200
- fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify({ name }));
201
- return dir;
202
- }
203
-
204
- function pushOverride(projDir, scriptArgv1 = 'index.js') {
205
- overridesQueue.push({
206
- cwdOverride: projDir,
207
- cmdlineOverride: ['bun', scriptArgv1],
208
- });
209
- }
210
-
211
- async function makeClient({ database, expectError = false } = {}) {
212
- const client = new Client({
213
- host: controlSocketDir,
214
- port: 5432,
215
- database: database || 'postgres',
216
- user: 'postgres',
217
- password: 'postgres',
218
- });
219
- // pg.Client's end() can hang after a FATAL connect failure (it tries
220
- // to send a Terminate message on a closed socket), so on the deny path
221
- // we swallow connect's rejection and return immediately. The TCP
222
- // socket is already FIN'd by the daemon.
223
- if (expectError) {
224
- // Suppress unhandled-error events on the underlying socket.
225
- client.on('error', () => { /* swallow */ });
226
- let err;
227
- try { await client.connect(); } catch (e) { err = e; }
228
- return { error: err };
229
- }
230
- await client.connect();
231
- return { client };
232
- }
233
-
234
- function readAudit() {
235
- if (!fs.existsSync(auditFile)) return [];
236
- return fs.readFileSync(auditFile, 'utf8')
237
- .split('\n')
238
- .filter(Boolean)
239
- .map((l) => JSON.parse(l));
240
- }
241
-
242
- test('two peers with different fingerprints get different DBs', async () => {
243
- const projA = makeProject('proj-a');
244
- const projB = makeProject('proj-b');
245
-
246
- pushOverride(projA);
247
- const { client: ca } = await makeClient();
248
- const ra = await ca.query('SELECT current_database() AS db');
249
- await ca.end();
250
-
251
- pushOverride(projB);
252
- const { client: cb } = await makeClient();
253
- const rb = await cb.query('SELECT current_database() AS db');
254
- await cb.end();
255
-
256
- expect(ra.rows[0].db).toMatch(/^app_proj_a_[0-9a-f]{12}$/);
257
- expect(rb.rows[0].db).toMatch(/^app_proj_b_[0-9a-f]{12}$/);
258
- expect(ra.rows[0].db).not.toBe(rb.rows[0].db);
259
-
260
- const events = readAudit();
261
- const created = events.filter((e) => e.event === AUDIT_EVENTS.DB_CREATED);
262
- expect(created.length).toBe(2);
263
- expect(created.map((e) => e.database).sort()).toEqual(
264
- [ra.rows[0].db, rb.rows[0].db].sort(),
265
- );
266
- });
267
-
268
- test('same peer reconnecting reaches its existing DB (no second db_created)', async () => {
269
- const projA = makeProject('reconnect-app');
270
-
271
- pushOverride(projA);
272
- const { client: c1 } = await makeClient();
273
- const r1 = await c1.query('SELECT current_database() AS db');
274
- await c1.end();
275
-
276
- pushOverride(projA);
277
- const { client: c2 } = await makeClient();
278
- const r2 = await c2.query('SELECT current_database() AS db');
279
- await c2.end();
280
-
281
- expect(r2.rows[0].db).toBe(r1.rows[0].db);
282
-
283
- const created = readAudit().filter((e) => e.event === AUDIT_EVENTS.DB_CREATED);
284
- expect(created.length).toBe(1);
285
- });
286
-
287
- test('cross-fingerprint connection denied with SQLSTATE 28P01', async () => {
288
- const projA = makeProject('tenant-a');
289
- const projB = makeProject('tenant-b');
290
-
291
- // Provision tenant A's DB first.
292
- pushOverride(projA);
293
- const { client: ca } = await makeClient();
294
- const ra = await ca.query('SELECT current_database() AS db');
295
- await ca.end();
296
- const tenantADb = ra.rows[0].db;
297
-
298
- // Now have tenant B try to connect explicitly into tenant A's DB.
299
- pushOverride(projB);
300
- const { error } = await makeClient({ database: tenantADb, expectError: true });
301
-
302
- expect(error).toBeDefined();
303
- expect(error.code).toBe('28P01');
304
-
305
- const denied = readAudit().filter(
306
- (e) => e.event === AUDIT_EVENTS.CONNECTION_DENIED_FINGERPRINT_MISMATCH,
307
- );
308
- expect(denied.length).toBe(1);
309
- expect(denied[0].requested_database).toBe(tenantADb);
310
- expect(denied[0].owned_database).toMatch(/^app_tenant_b_[0-9a-f]{12}$/);
311
- });
312
-
313
- test('kill-switch env: cross-fingerprint succeeds and emits audit event', async () => {
314
- const projA = makeProject('killswitch-a');
315
- const projB = makeProject('killswitch-b');
316
-
317
- // Provision tenant A.
318
- pushOverride(projA);
319
- const { client: ca } = await makeClient();
320
- const ra = await ca.query('SELECT current_database() AS db');
321
- await ca.end();
322
- const tenantADb = ra.rows[0].db;
323
-
324
- // Flip the live kill-switch flag on the daemon (the env var is read
325
- // once at construction; this is the test seam for the same effect).
326
- daemon.enforcementDisabled = true;
327
-
328
- pushOverride(projB);
329
- const { client: cb } = await makeClient({ database: tenantADb });
330
- const rb = await cb.query('SELECT current_database() AS db');
331
- await cb.end();
332
-
333
- // Bypass succeeded: tenant B's session reached tenant A's DB.
334
- expect(rb.rows[0].db).toBe(tenantADb);
335
-
336
- const events = readAudit();
337
- const bypass = events.filter(
338
- (e) => e.event === AUDIT_EVENTS.ENFORCEMENT_KILL_SWITCH_USED,
339
- );
340
- expect(bypass.length).toBe(1);
341
- expect(bypass[0].owned_database).toMatch(/^app_killswitch_b_[0-9a-f]{12}$/);
342
- expect(bypass[0].requested_database).toBe(tenantADb);
343
-
344
- // No deny event should fire while the kill switch is active.
345
- const denied = events.filter(
346
- (e) => e.event === AUDIT_EVENTS.CONNECTION_DENIED_FINGERPRINT_MISMATCH,
347
- );
348
- expect(denied.length).toBe(0);
349
- });
350
-
351
- test('sanitizer: name "@scope/foo bar" produces app__scope_foo_bar_<hex>', async () => {
352
- const projScoped = makeProject('@scope/foo bar', 'scoped-pkg');
353
-
354
- pushOverride(projScoped);
355
- const { client } = await makeClient();
356
- const r = await client.query('SELECT current_database() AS db');
357
- await client.end();
358
-
359
- expect(r.rows[0].db).toMatch(/^app__scope_foo_bar_[0-9a-f]{12}$/);
360
- });
361
- });
362
-
363
- // ---------------------------------------------------------------------------
364
- // Boot-time deprecation warning
365
- // ---------------------------------------------------------------------------
366
-
367
- describe('boot deprecation warning when kill switch is set', () => {
368
- test('writes a deprecation message to stderr at start()', async () => {
369
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'pgserve-killswitch-boot-'));
370
- const controlDir = path.join(dir, 'sock');
371
- fs.mkdirSync(controlDir, { recursive: true });
372
-
373
- const captured = [];
374
- const origWrite = process.stderr.write.bind(process.stderr);
375
- process.stderr.write = (chunk, ...rest) => {
376
- captured.push(typeof chunk === 'string' ? chunk : Buffer.from(chunk).toString('utf8'));
377
- return origWrite(chunk, ...rest);
378
- };
379
-
380
- let d;
381
- try {
382
- d = new PgserveDaemon({
383
- controlSocketDir: controlDir,
384
- controlSocketPath: resolveControlSocketPath(controlDir),
385
- pidLockPath: resolvePidLockPath(controlDir),
386
- libpqCompatPath: resolveLibpqCompatPath(controlDir, 5432),
387
- pgPort: 16780,
388
- enforcementDisabled: true,
389
- logger: createLogger({ level: 'warn' }),
390
- });
391
- await d.start();
392
-
393
- const merged = captured.join('');
394
- expect(merged).toContain(KILL_SWITCH_ENV);
395
- expect(merged).toContain('DISABLED');
396
- expect(merged).toContain('deprecated');
397
- } finally {
398
- process.stderr.write = origWrite;
399
- try { if (d) await d.stop(); } catch { /* swallow */ }
400
- try { fs.rmSync(dir, { recursive: true, force: true }); } catch { /* swallow */ }
401
- }
402
- });
403
- });
@@ -1,107 +0,0 @@
1
- /**
2
- * Wrapper supervision: postgres backend death surfaces to the wrapper
3
- *
4
- * Verifies that:
5
- * 1. PostgresManager extends EventEmitter and emits `backendExited` when
6
- * the postgres child exits.
7
- * 2. `expected: true` is reported when the exit was initiated by stop().
8
- * 3. `expected: false` is reported when the child was killed externally
9
- * (the case the wrapper needs to react to per pgserve#45).
10
- * 4. PgserveDaemon re-emits `backendDiedUnexpectedly` only for unexpected
11
- * exits, not for clean stop().
12
- *
13
- * Tests use the real Bun.spawn'd postgres binary via PostgresManager because
14
- * the supervision contract is end-to-end — a unit test with a mocked process
15
- * would prove only that the JS plumbing fires.
16
- */
17
-
18
- import { PostgresManager } from '../src/postgres.js';
19
- import { PgserveDaemon } from '../src/daemon.js';
20
- import { EventEmitter } from 'events';
21
- import { test, expect } from 'bun:test';
22
- import fs from 'fs';
23
- import path from 'path';
24
- import os from 'os';
25
-
26
- function quietLogger() {
27
- return {
28
- info: () => {}, warn: () => {}, error: () => {}, debug: () => {},
29
- child: () => quietLogger(),
30
- };
31
- }
32
-
33
- test('PostgresManager extends EventEmitter', () => {
34
- const mgr = new PostgresManager({});
35
- expect(mgr).toBeInstanceOf(EventEmitter);
36
- });
37
-
38
- test('PostgresManager emits backendExited with expected=true after stop()', async () => {
39
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'pgserve-supv-stop-'));
40
- const mgr = new PostgresManager({ dataDir: dir, logger: quietLogger() });
41
- let event = null;
42
- mgr.on('backendExited', (info) => { event = info; });
43
- await mgr.start();
44
- await mgr.stop();
45
- // Give event loop a tick to flush exited.then handler if not already drained
46
- await new Promise((r) => setTimeout(r, 50));
47
- expect(event).not.toBeNull();
48
- expect(event.expected).toBe(true);
49
- fs.rmSync(dir, { recursive: true, force: true });
50
- }, 60000);
51
-
52
- // External-SIGKILL integration coverage runs on Linux only. On macOS,
53
- // Bun.spawn'd postgres reliably refuses to surface its `exited` promise
54
- // within the test deadline when killed by SIGKILL — Bun's posix_spawn
55
- // path on darwin holds parent reaping until grandchildren reap, which
56
- // postgres never does fast enough for a deterministic test. The
57
- // `expected=false` branch is still covered cross-platform by the
58
- // daemon-level re-emit test below, which feeds a synthetic
59
- // `backendExited` payload through a fake EventEmitter and bypasses the
60
- // OS signal-handling variability entirely.
61
- const linuxOnly = process.platform === 'linux' ? test : test.skip;
62
- linuxOnly('PostgresManager emits backendExited with expected=false on external SIGKILL (linux)', async () => {
63
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'pgserve-supv-kill-'));
64
- const mgr = new PostgresManager({ dataDir: dir, logger: quietLogger() });
65
- let event = null;
66
- mgr.on('backendExited', (info) => { event = info; });
67
- await mgr.start();
68
- const childPid = mgr.process?.pid;
69
- expect(childPid).toBeGreaterThan(0);
70
- // External kill — _stopping stays false, so the handler must mark unexpected
71
- process.kill(childPid, 'SIGKILL');
72
- // Wait for the exit handler to fire (max 3s)
73
- for (let i = 0; i < 60 && event === null; i++) {
74
- await new Promise((r) => setTimeout(r, 50));
75
- }
76
- expect(event).not.toBeNull();
77
- expect(event.expected).toBe(false);
78
- // Cleanup: paths were nulled by the unexpected-exit branch, so stop() is a no-op
79
- await mgr.stop().catch(() => {});
80
- fs.rmSync(dir, { recursive: true, force: true });
81
- }, 60000);
82
-
83
- test('PgserveDaemon re-emits backendDiedUnexpectedly only on unexpected exit', () => {
84
- // Pure plumbing test — synthesize PgserveDaemon and a fake pgManager that
85
- // is just an EventEmitter; verify the wiring.
86
- const fakePgManager = new EventEmitter();
87
- // PgserveDaemon constructor needs a baseDir; passing a tmp dir avoids
88
- // touching real config.
89
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'pgserve-supv-daemon-'));
90
- const daemon = new PgserveDaemon({
91
- baseDir: dir,
92
- logger: quietLogger(),
93
- pgManager: fakePgManager,
94
- enforcementDisabled: true,
95
- });
96
- const events = [];
97
- daemon.on('backendDiedUnexpectedly', (info) => events.push(info));
98
-
99
- fakePgManager.emit('backendExited', { code: 0, expected: true });
100
- expect(events).toHaveLength(0); // clean stop — no re-emit
101
-
102
- fakePgManager.emit('backendExited', { code: 137, expected: false });
103
- expect(events).toHaveLength(1);
104
- expect(events[0]).toEqual({ code: 137, expected: false });
105
-
106
- fs.rmSync(dir, { recursive: true, force: true });
107
- });