godcrm/backend/scripts/smoke-chat-prefs-pgnotify.mjs
GOD CRM Release f89e074dd1
Some checks failed
CI / Lint / Typecheck / Test / Build (push) Has been cancelled
CI / PostgreSQL Integration Tests (push) Has been cancelled
GOD CRM — public scrubbed snapshot
Governed substrate for autonomous agents: scoped identity (passports),
audited actions, MCP workspace. Infra IPs and secrets redacted for public release.
2026-08-10 04:01:45 +03:00

93 lines
3 KiB
JavaScript

#!/usr/bin/env node
// smoke-chat-prefs-pgnotify.mjs — ADR-0064 WP-A.
//
// Verifies the cross-process invalidation path used by resolveChatPrefs:
//
// 1. Spawn TWO independent pg.Client LISTENers on `chat_prefs_invalidate`.
// 2. Fire `SELECT pg_notify(...)` from a THIRD pg.Client (simulates the PUT path).
// 3. Both listeners must observe the notification with the original payload
// within a short window. This is the cluster-wide eviction guarantee.
//
// Exits 0 on green, 1 on red. Run on DEV only:
// POSTGRES_DB=godcrm_prod node backend/scripts/smoke-chat-prefs-pgnotify.mjs
import pg from 'pg';
const CHANNEL = 'chat_prefs_invalidate';
const WAIT_MS = 1500;
function connectionConfig() {
return process.env.POSTGRES_URL
? { connectionString: process.env.POSTGRES_URL }
: {
host: process.env.POSTGRES_HOST || 'localhost',
port: parseInt(process.env.POSTGRES_PORT || '5432', 10),
database: process.env.POSTGRES_DB || 'godcrm_prod',
user: process.env.POSTGRES_USER || 'godcrm',
password: process.env.POSTGRES_PASSWORD,
ssl: process.env.POSTGRES_SSL === 'true' ? { rejectUnauthorized: false } : false,
};
}
async function makeListener(label) {
const client = new pg.Client(connectionConfig());
await client.connect();
const received = [];
client.on('notification', (msg) => {
if (msg.channel === CHANNEL) {
received.push({ at: Date.now(), payload: msg.payload, label });
}
});
client.on('error', (err) => console.error(`[${label}] LISTEN error:`, err.message));
await client.query(`LISTEN ${CHANNEL}`);
return { client, received };
}
async function main() {
const listenerA = await makeListener('A');
const listenerB = await makeListener('B');
const notifier = new pg.Client(connectionConfig());
await notifier.connect();
const scopes = [
{ user_id: 1 },
{ conversation_id: 999 },
{ space_id: 11 },
{},
];
for (const scope of scopes) {
await notifier.query('SELECT pg_notify($1, $2)', [CHANNEL, JSON.stringify(scope)]);
}
// Allow the LISTEN clients to drain.
await new Promise((r) => setTimeout(r, WAIT_MS));
let ok = true;
for (const listener of [listenerA, listenerB]) {
const got = listener.received.length;
if (got !== scopes.length) {
console.error(`[${listener.received[0]?.label || '?'}] expected ${scopes.length} notifications, got ${got}`);
ok = false;
} else {
const labels = listener.received.map((r) => r.label).join(',');
console.log(`listener ${labels.split(',')[0]} received ${got}/${scopes.length} notifications: ${listener.received.map(r => r.payload).join(' | ')}`);
}
}
await listenerA.client.end();
await listenerB.client.end();
await notifier.end();
if (!ok) {
console.error('SMOKE FAIL');
process.exit(1);
}
console.log('SMOKE OK — pg_notify reaches both listeners with the original payloads.');
}
main().catch((err) => {
console.error('SMOKE ERROR', err);
process.exit(1);
});