godcrm/scripts/smoke-c3-regression.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

177 lines
7.1 KiB
JavaScript

#!/usr/bin/env node
// ADR-0003 §C-3 smoke — regression watcher.
//
// S1: bootstrap a throwaway verified criterion + bdd_link → ticket.
// S2: fire onTicketStateTransition with oldState=done, newState=in-progress.
// S3: assert criterion.status=='regressed', regressed_by_ticket_id is set,
// and a bdd_audit_log row exists with action='regress' + caused_by.
// S4: gate check — fire with newState=done (idempotent no-op) and with
// wrong table id (no-op).
// S5: cleanup everything inserted by this smoke.
// ADR-0009 Phase 5: boot guard — aborts (exit 2) if POSTGRES_HOST is PROD.
import '../backend/test/setup.js';
import pg from 'pg';
import { onTicketStateTransition } from '../backend/services/bdd/regressionWatcher.js';
const { Pool } = pg;
const pool = new Pool({
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 || 'godcrm_dev_2026',
});
const TICKETS_TABLE_ID = 1708;
const DONE_STATE_ID = 24278;
const INPROGRESS_STATE_ID = 24276;
const FAKE_TICKET_ID = 999999990; // never collides with a real ticket row
async function tableId(client, name) {
const { rows } = await client.query(`
SELECT ut.id FROM universal_tables ut
JOIN projects p ON p.id = ut.project_id
WHERE p.space_id = 11 AND ut.name = $1 LIMIT 1
`, [name]);
return rows[0]?.id;
}
async function insertRow(client, tid, data) {
const baseId = `SMK-${Date.now().toString(36).toUpperCase()}-${Math.floor(Math.random() * 1000)}`;
const { rows } = await client.query(
`INSERT INTO table_rows (table_id, base_id, data, created_at, updated_at)
VALUES ($1, $2, $3::jsonb, NOW(), NOW()) RETURNING id`,
[tid, baseId, JSON.stringify(data)]
);
return rows[0].id;
}
async function waitFor(predicate, { tries = 20, gapMs = 100 } = {}) {
for (let i = 0; i < tries; i++) {
if (await predicate()) return true;
await new Promise(r => setTimeout(r, gapMs));
}
return false;
}
async function main() {
const client = await pool.connect();
const created = { critRowId: null, linkRowId: null, auditRowIds: [] };
try {
const critTid = await tableId(client, 'bdd_criteria');
const linksTid = await tableId(client, 'bdd_links');
const auditTid = await tableId(client, 'bdd_audit_log');
if (!critTid || !linksTid || !auditTid) {
throw new Error(`missing BDD tables (crit=${critTid}, links=${linksTid}, audit=${auditTid})`);
}
console.log(`bootstrap ✓ tables: crit=${critTid} links=${linksTid} audit=${auditTid}`);
// S1: seed a verified criterion + link to FAKE_TICKET_ID
created.critRowId = await insertRow(client, critTid, {
code: 'SMK-C3-AC1',
title: 'Smoke C-3 regression criterion',
status: 'verified',
priority: 'must',
spec_id: null,
source_doc_id: null,
verified_at: new Date().toISOString(),
totp: { last_verified_at: new Date().toISOString() },
});
created.linkRowId = await insertRow(client, linksTid, {
from_kind: 'criterion',
from_id: created.critRowId,
to_kind: 'ticket',
to_id: FAKE_TICKET_ID,
relation: 'verifies',
});
console.log(`S1 ✓ criterion id=${created.critRowId} link id=${created.linkRowId} → ticket ${FAKE_TICKET_ID}`);
// S2: fire the watcher for done → in-progress
onTicketStateTransition(
TICKETS_TABLE_ID,
FAKE_TICKET_ID,
{ state: DONE_STATE_ID },
{ state: INPROGRESS_STATE_ID }
);
// S3: wait until criterion flips to regressed
const flipped = await waitFor(async () => {
const { rows } = await client.query(
`SELECT data FROM table_rows WHERE id = $1`, [created.critRowId]
);
const d = rows[0]?.data;
return d && d.status === 'regressed';
});
if (!flipped) throw new Error('criterion did not flip to regressed within 2s');
const { rows: critRows } = await client.query(
`SELECT data FROM table_rows WHERE id = $1`, [created.critRowId]
);
const critData = critRows[0].data;
if (critData.regressed_by_ticket_id !== FAKE_TICKET_ID) {
throw new Error(`regressed_by_ticket_id=${critData.regressed_by_ticket_id}, want ${FAKE_TICKET_ID}`);
}
console.log(`S3 ✓ status=regressed, regressed_by_ticket_id=${FAKE_TICKET_ID}, regressed_at=${critData.regressed_at}`);
// S4: audit-log row for this regression
const { rows: auditRows } = await client.query(
`SELECT id, data FROM table_rows
WHERE table_id = $1 AND data->>'caused_by' = $2
ORDER BY id DESC LIMIT 1`,
[auditTid, `ticket:${FAKE_TICKET_ID}:reopen`]
);
if (!auditRows.length) throw new Error('no bdd_audit_log row for regression');
const ad = auditRows[0].data;
created.auditRowIds.push(auditRows[0].id);
const auditChecks = [
['action=regress', ad.action === 'regress'],
['from_status=verified', ad.from_status === 'verified'],
['to_status=regressed', ad.to_status === 'regressed'],
[`criterion_id=${created.critRowId}`, ad.criterion_id === created.critRowId],
['actor_kind=system', ad.actor_kind === 'system'],
];
for (const [lbl, ok] of auditChecks) {
if (!ok) throw new Error(`S4 ✗ ${lbl}`);
console.log(`S4 ✓ ${lbl}`);
}
// S5: idempotent / guard semantics
// S5a: another done → done transition should be no-op
onTicketStateTransition(TICKETS_TABLE_ID, FAKE_TICKET_ID,
{ state: DONE_STATE_ID }, { state: DONE_STATE_ID });
// S5b: non-tickets table should be no-op
onTicketStateTransition(9999, FAKE_TICKET_ID,
{ state: DONE_STATE_ID }, { state: INPROGRESS_STATE_ID });
// S5c: old state wasn't done — no-op
onTicketStateTransition(TICKETS_TABLE_ID, FAKE_TICKET_ID,
{ state: INPROGRESS_STATE_ID }, { state: 43438 /* rejected */ });
// Give async handlers a window
await new Promise(r => setTimeout(r, 300));
const { rows: auditAfter } = await client.query(
`SELECT COUNT(*)::int AS n FROM table_rows
WHERE table_id = $1 AND data->>'caused_by' = $2`,
[auditTid, `ticket:${FAKE_TICKET_ID}:reopen`]
);
if (auditAfter[0].n !== 1) {
throw new Error(`expected 1 audit row after guard tests, got ${auditAfter[0].n}`);
}
console.log('S5 ✓ guards: same-state / non-tickets / old-state-not-done all no-op');
console.log('\nC-3 regression-watcher smoke: ALL GREEN');
} finally {
// S6: cleanup
for (const auditId of created.auditRowIds) {
await client.query(`DELETE FROM table_rows WHERE id = $1`, [auditId]);
}
if (created.linkRowId) await client.query(`DELETE FROM table_rows WHERE id = $1`, [created.linkRowId]);
if (created.critRowId) await client.query(`DELETE FROM table_rows WHERE id = $1`, [created.critRowId]);
console.log(`cleanup ✓ removed crit=${created.critRowId} link=${created.linkRowId} audit=${created.auditRowIds.join(',')}`);
client.release();
await pool.end();
}
}
main().catch((e) => {
console.error('C-3 smoke FAILED:', e.message);
process.exit(1);
});