godcrm/backend/routes/v3/__tests__/agent-run-dispatcher.test.js
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

522 lines
20 KiB
JavaScript

/**
* Agent Run Dispatcher API Routes Tests — ADR-0030 Phase 5.
*
* HTTP-layer tests for `backend/routes/v3/agentRunDispatcher.js`. The
* underlying pure module (`approval-gate.js`) is covered by
* `scripts/smoke-adr0030-p5.mjs`; this file locks down the routes.
*
* Test isolation (ADR-0009): boot guard `backend/test/setup.js` (wired into
* vitest.config.ts) refuses to run unless POSTGRES_DB!=godcrm_prod and
* BUSINESS_CRM_IS_PROD!=1. Run via:
* BUSINESS_CRM_IS_PROD= POSTGRES_DB=godcrm_test POSTGRES_HOST=localhost \
* npx vitest run backend/routes/v3/__tests__/agent-run-dispatcher.test.js
*/
import { describe, test, expect, beforeAll, beforeEach, afterEach, afterAll } from 'vitest';
import request from 'supertest';
import express from 'express';
import cookieParser from 'cookie-parser';
import jwt from 'jsonwebtoken';
import { authenticate } from '../../../middleware/auth.js';
import dispatcherRouter from '../agentRunDispatcher.js';
import { dbRun, dbGet } from '../../../database/connection.js';
import {
generateApprovalCode,
persistApprovalRequest,
resolveApproval,
APPROVAL_CONSTANTS,
} from '../../../services/agent-run-dispatcher/approval-gate.js';
const JWT_SECRET = process.env.JWT_SECRET || 'test-secret-for-vitest';
const TICKETS_TABLE_ID = 1708;
const SMOKE_TAG_PREFIX = 'route_test_';
// ─── App setup ─────────────────────────────────────────────
const app = express();
app.use(express.json());
app.use(cookieParser());
app.use('/api/v3', authenticate, dispatcherRouter);
// ─── Helpers ───────────────────────────────────────────────
function genBaseId() {
const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
let s = '';
for (let i = 0; i < 8; i++) s += alphabet[Math.floor(Math.random() * alphabet.length)];
return s;
}
function makeJwt({ id, email = 'route-test@hltrn.cc', role = 'user' } = {}) {
return jwt.sign({ id, email, role }, JWT_SECRET, { expiresIn: '1h' });
}
let userIdCounter = 9_000_001;
function nextUserId() {
return userIdCounter++;
}
async function insertTicket({ smokeTag, assignedTo = null, runState = null, extra = {} } = {}) {
const data = {
title: `Route test ticket ${smokeTag}`,
smoke_tag: smokeTag,
...extra,
};
if (assignedTo != null) data.assigned_to = String(assignedTo);
if (runState != null) data.run_state = runState;
const row = await dbGet(
`INSERT INTO table_rows (table_id, base_id, data, created_at, updated_at)
VALUES ($1, $2, $3::jsonb, NOW(), NOW())
RETURNING id`,
[TICKETS_TABLE_ID, genBaseId(), JSON.stringify(data)]
);
return row.id;
}
async function getTicketData(ticketId) {
const row = await dbGet(
`SELECT data FROM table_rows WHERE table_id = $1 AND id = $2`,
[TICKETS_TABLE_ID, ticketId]
);
if (!row?.data) return null;
return typeof row.data === 'string' ? JSON.parse(row.data) : row.data;
}
/**
* Create a ticket already in `awaiting_approval` with a known plaintext code.
* Returns { ticketId, code }.
*/
async function makeTicketWithApproval({ smokeTag, assignedTo = null, expiresAtOverride = null }) {
const ticketId = await insertTicket({
smokeTag,
assignedTo,
runState: 'preparing',
});
const { code, code_hash, expires_at, generated_at } = generateApprovalCode();
const finalExpires = expiresAtOverride || expires_at;
await persistApprovalRequest(ticketId, {
code_hash,
expires_at: finalExpires,
generated_at,
});
return { ticketId, code };
}
async function cleanupAllSmokeRows() {
await dbRun(
`DELETE FROM table_rows WHERE data->>'smoke_tag' LIKE $1`,
[`${SMOKE_TAG_PREFIX}%`]
).catch(() => {});
}
// ─── Test suite ────────────────────────────────────────────
describe('Agent Run Dispatcher API (v3) — ADR-0030 Phase 5', () => {
beforeAll(async () => {
// Pre-flight scrub of any leftovers from previous runs.
await cleanupAllSmokeRows();
});
afterAll(async () => {
await cleanupAllSmokeRows();
});
afterEach(async () => {
await cleanupAllSmokeRows();
});
// ============================================================
// POST /api/v3/admin/agent-run-dispatcher/tick
// ============================================================
describe('POST /admin/agent-run-dispatcher/tick', () => {
test('non-admin → 403 forbidden', async () => {
const token = makeJwt({ id: nextUserId(), role: 'user' });
const res = await request(app)
.post('/api/v3/admin/agent-run-dispatcher/tick')
.set('Authorization', `Bearer ${token}`)
.expect(403);
expect(res.body.success).toBe(false);
expect(res.body.error.code).toBe('FORBIDDEN');
});
test('admin → 200 with stats object', async () => {
const token = makeJwt({ id: nextUserId(), role: 'admin' });
const res = await request(app)
.post('/api/v3/admin/agent-run-dispatcher/tick')
.set('Authorization', `Bearer ${token}`)
.expect(200);
expect(res.body.success).toBe(true);
expect(res.body.data).toBeDefined();
// Stats keys defined in runTick().
expect(typeof res.body.data.picked).toBe('number');
expect(typeof res.body.data.errors).toBe('number');
expect(typeof res.body.data.duration_ms).toBe('number');
});
test('does not crash when no claimable tickets exist', async () => {
const token = makeJwt({ id: nextUserId(), role: 'admin' });
const res = await request(app)
.post('/api/v3/admin/agent-run-dispatcher/tick')
.set('Authorization', `Bearer ${token}`)
.expect(200);
expect(res.body.success).toBe(true);
// Even with zero claimable, errors should be 0.
expect(res.body.data.errors).toBe(0);
});
});
// ============================================================
// GET /api/v3/admin/agent-run-dispatcher/health
// ============================================================
describe('GET /admin/agent-run-dispatcher/health', () => {
test('non-admin → 403', async () => {
const token = makeJwt({ id: nextUserId(), role: 'user' });
const res = await request(app)
.get('/api/v3/admin/agent-run-dispatcher/health')
.set('Authorization', `Bearer ${token}`)
.expect(403);
expect(res.body.success).toBe(false);
expect(res.body.error.code).toBe('FORBIDDEN');
});
test('admin → 200 with ok/running/phase/awaitingApprovalCount', async () => {
const token = makeJwt({ id: nextUserId(), role: 'admin' });
const res = await request(app)
.get('/api/v3/admin/agent-run-dispatcher/health')
.set('Authorization', `Bearer ${token}`)
.expect(200);
expect(res.body.success).toBe(true);
const h = res.body.data;
expect(h).toHaveProperty('ok');
expect(h).toHaveProperty('running');
expect(h).toHaveProperty('phase');
expect(h).toHaveProperty('awaitingApprovalCount');
});
test('healthAsync awaitingApprovalCount counts awaiting tickets', async () => {
const tag = `${SMOKE_TAG_PREFIX}health_count_${Date.now()}`;
// Insert two awaiting_approval tickets.
await makeTicketWithApproval({ smokeTag: tag });
await makeTicketWithApproval({ smokeTag: tag });
const token = makeJwt({ id: nextUserId(), role: 'admin' });
const res = await request(app)
.get('/api/v3/admin/agent-run-dispatcher/health')
.set('Authorization', `Bearer ${token}`)
.expect(200);
expect(res.body.data.awaitingApprovalCount).toBeGreaterThanOrEqual(2);
});
});
// ============================================================
// GET /api/v3/admin/agent-run-dispatcher/pending
// ============================================================
describe('GET /admin/agent-run-dispatcher/pending', () => {
test('non-admin → 403', async () => {
const token = makeJwt({ id: nextUserId(), role: 'user' });
const res = await request(app)
.get('/api/v3/admin/agent-run-dispatcher/pending')
.set('Authorization', `Bearer ${token}`)
.expect(403);
expect(res.body.success).toBe(false);
expect(res.body.error.code).toBe('FORBIDDEN');
});
test('admin with no awaiting_approval tickets → empty pending + counts', async () => {
const token = makeJwt({ id: nextUserId(), role: 'admin' });
const res = await request(app)
.get('/api/v3/admin/agent-run-dispatcher/pending')
.set('Authorization', `Bearer ${token}`)
.expect(200);
expect(res.body.success).toBe(true);
const d = res.body.data;
expect(Array.isArray(d.pending)).toBe(true);
expect(typeof d.count).toBe('number');
expect(d.max_attempts).toBe(APPROVAL_CONSTANTS.MAX_ATTEMPTS);
expect(d.ttl_ms).toBe(APPROVAL_CONSTANTS.TTL_MS);
});
test('admin with awaiting tickets → projection excludes code_hash and code', async () => {
const tag = `${SMOKE_TAG_PREFIX}pending_proj_${Date.now()}`;
const userId = nextUserId();
const { ticketId } = await makeTicketWithApproval({
smokeTag: tag,
assignedTo: userId,
});
const token = makeJwt({ id: nextUserId(), role: 'admin' });
const res = await request(app)
.get('/api/v3/admin/agent-run-dispatcher/pending')
.set('Authorization', `Bearer ${token}`)
.expect(200);
// Find our ticket in the list.
const mine = res.body.data.pending.find((p) => p.id === ticketId);
expect(mine).toBeDefined();
// Only safe projection fields.
expect(mine).toHaveProperty('id');
expect(mine).toHaveProperty('title');
expect(mine).toHaveProperty('expires_at');
expect(mine).toHaveProperty('attempts');
expect(mine).toHaveProperty('assigned_to');
// Code material MUST NOT leak.
expect(mine).not.toHaveProperty('code');
expect(mine).not.toHaveProperty('code_hash');
// Pull the actual stored hash and ensure no field carries it.
const data = await getTicketData(ticketId);
const storedHash = data.run_approval.code_hash;
const serialized = JSON.stringify(mine);
expect(serialized.includes(storedHash)).toBe(false);
});
});
// ============================================================
// POST /api/v3/admin/agent-run-dispatcher/approve/:ticketId
// ============================================================
describe('POST /admin/agent-run-dispatcher/approve/:ticketId', () => {
test('non-numeric ticketId → 400 BAD_TICKET_ID', async () => {
const token = makeJwt({ id: nextUserId(), role: 'admin' });
const res = await request(app)
.post('/api/v3/admin/agent-run-dispatcher/approve/abc')
.set('Authorization', `Bearer ${token}`)
.send({ code: '123456' })
.expect(400);
expect(res.body.error.code).toBe('BAD_TICKET_ID');
});
test('bad code (not 6 digits) → 400 BAD_CODE', async () => {
const token = makeJwt({ id: nextUserId(), role: 'admin' });
// Use a positive ticketId; the code-shape check happens before lookup.
const res = await request(app)
.post('/api/v3/admin/agent-run-dispatcher/approve/1')
.set('Authorization', `Bearer ${token}`)
.send({ code: '12345' })
.expect(400);
expect(res.body.error.code).toBe('BAD_CODE');
const resMissing = await request(app)
.post('/api/v3/admin/agent-run-dispatcher/approve/1')
.set('Authorization', `Bearer ${token}`)
.send({})
.expect(400);
expect(resMissing.body.error.code).toBe('BAD_CODE');
const resAlpha = await request(app)
.post('/api/v3/admin/agent-run-dispatcher/approve/1')
.set('Authorization', `Bearer ${token}`)
.send({ code: 'abcdef' })
.expect(400);
expect(resAlpha.body.error.code).toBe('BAD_CODE');
});
test('ticket not found → 404 TICKET_NOT_FOUND', async () => {
const token = makeJwt({ id: nextUserId(), role: 'admin' });
// Use a ticket id that almost certainly does not exist.
const res = await request(app)
.post('/api/v3/admin/agent-run-dispatcher/approve/2147483640')
.set('Authorization', `Bearer ${token}`)
.send({ code: '123456' })
.expect(404);
expect(res.body.error.code).toBe('TICKET_NOT_FOUND');
});
test('user neither admin nor assignee → 403', async () => {
const tag = `${SMOKE_TAG_PREFIX}approve_forbid_${Date.now()}`;
const ownerId = nextUserId();
const { ticketId, code } = await makeTicketWithApproval({
smokeTag: tag,
assignedTo: ownerId,
});
const otherUserId = nextUserId();
const token = makeJwt({ id: otherUserId, role: 'user' });
const res = await request(app)
.post(`/api/v3/admin/agent-run-dispatcher/approve/${ticketId}`)
.set('Authorization', `Bearer ${token}`)
.send({ code })
.expect(403);
expect(res.body.error.code).toBe('FORBIDDEN');
});
test('assignee submits wrong code → 401 APPROVAL_CODE_MISMATCH with attempts_remaining decremented', async () => {
const tag = `${SMOKE_TAG_PREFIX}approve_wrong_${Date.now()}`;
const ownerId = nextUserId();
const { ticketId } = await makeTicketWithApproval({
smokeTag: tag,
assignedTo: ownerId,
});
const token = makeJwt({ id: ownerId, role: 'user' });
const res = await request(app)
.post(`/api/v3/admin/agent-run-dispatcher/approve/${ticketId}`)
.set('Authorization', `Bearer ${token}`)
.send({ code: '000000' })
.expect(401);
expect(res.body.error.code).toBe('APPROVAL_CODE_MISMATCH');
expect(res.body.error.details.attempts_remaining).toBe(APPROVAL_CONSTANTS.MAX_ATTEMPTS - 1);
});
test('5 wrong codes → final returns 401 APPROVAL_DENIED + ticket run_state=failed', async () => {
const tag = `${SMOKE_TAG_PREFIX}approve_lockout_${Date.now()}`;
const ownerId = nextUserId();
const { ticketId } = await makeTicketWithApproval({
smokeTag: tag,
assignedTo: ownerId,
});
const token = makeJwt({ id: ownerId, role: 'user' });
let lastRes;
for (let i = 0; i < APPROVAL_CONSTANTS.MAX_ATTEMPTS; i++) {
lastRes = await request(app)
.post(`/api/v3/admin/agent-run-dispatcher/approve/${ticketId}`)
.set('Authorization', `Bearer ${token}`)
.send({ code: '000000' });
}
expect(lastRes.status).toBe(401);
expect(lastRes.body.error.code).toBe('APPROVAL_DENIED');
expect(lastRes.body.error.details.state).toBe('denied');
const data = await getTicketData(ticketId);
expect(data.run_state).toBe('failed');
expect(data.run_terminal_reason).toBe('approval_denied');
});
test('correct code → 200 approved + ticket run_state=preparing', async () => {
const tag = `${SMOKE_TAG_PREFIX}approve_ok_${Date.now()}`;
const ownerId = nextUserId();
const { ticketId, code } = await makeTicketWithApproval({
smokeTag: tag,
assignedTo: ownerId,
});
const token = makeJwt({ id: nextUserId(), role: 'admin' });
const res = await request(app)
.post(`/api/v3/admin/agent-run-dispatcher/approve/${ticketId}`)
.set('Authorization', `Bearer ${token}`)
.send({ code })
.expect(200);
expect(res.body.success).toBe(true);
expect(res.body.data.state).toBe('approved');
expect(res.body.data.ticketId).toBe(ticketId);
const data = await getTicketData(ticketId);
expect(data.run_state).toBe('preparing');
expect(data.run_approval.state).toBe('approved');
});
test('approve twice → second call 410 APPROVAL_ALREADY_RESOLVED', async () => {
const tag = `${SMOKE_TAG_PREFIX}approve_twice_${Date.now()}`;
const { ticketId, code } = await makeTicketWithApproval({ smokeTag: tag });
const token = makeJwt({ id: nextUserId(), role: 'admin' });
await request(app)
.post(`/api/v3/admin/agent-run-dispatcher/approve/${ticketId}`)
.set('Authorization', `Bearer ${token}`)
.send({ code })
.expect(200);
const res2 = await request(app)
.post(`/api/v3/admin/agent-run-dispatcher/approve/${ticketId}`)
.set('Authorization', `Bearer ${token}`)
.send({ code })
.expect(410);
expect(res2.body.error.code).toBe('APPROVAL_ALREADY_RESOLVED');
expect(res2.body.error.details.state).toBe('approved');
});
test('expired (expires_at in the past) → 410 APPROVAL_EXPIRED + run_approval.state=expired', async () => {
const tag = `${SMOKE_TAG_PREFIX}approve_expired_${Date.now()}`;
const pastIso = new Date(Date.now() - 60_000).toISOString();
const { ticketId, code } = await makeTicketWithApproval({
smokeTag: tag,
expiresAtOverride: pastIso,
});
const token = makeJwt({ id: nextUserId(), role: 'admin' });
const res = await request(app)
.post(`/api/v3/admin/agent-run-dispatcher/approve/${ticketId}`)
.set('Authorization', `Bearer ${token}`)
.send({ code })
.expect(410);
expect(res.body.error.code).toBe('APPROVAL_EXPIRED');
const data = await getTicketData(ticketId);
expect(data.run_approval.state).toBe('expired');
});
test('no run_approval at all → 410 NO_PENDING_APPROVAL', async () => {
const tag = `${SMOKE_TAG_PREFIX}approve_nopending_${Date.now()}`;
const ticketId = await insertTicket({ smokeTag: tag });
const token = makeJwt({ id: nextUserId(), role: 'admin' });
const res = await request(app)
.post(`/api/v3/admin/agent-run-dispatcher/approve/${ticketId}`)
.set('Authorization', `Bearer ${token}`)
.send({ code: '123456' })
.expect(410);
expect(res.body.error.code).toBe('NO_PENDING_APPROVAL');
});
});
// ============================================================
// POST /api/v3/admin/agent-run-dispatcher/deny/:ticketId
// ============================================================
describe('POST /admin/agent-run-dispatcher/deny/:ticketId', () => {
test('non-admin/non-assignee → 403', async () => {
const tag = `${SMOKE_TAG_PREFIX}deny_forbid_${Date.now()}`;
const ownerId = nextUserId();
const { ticketId } = await makeTicketWithApproval({
smokeTag: tag,
assignedTo: ownerId,
});
const token = makeJwt({ id: nextUserId(), role: 'user' });
const res = await request(app)
.post(`/api/v3/admin/agent-run-dispatcher/deny/${ticketId}`)
.set('Authorization', `Bearer ${token}`)
.send({})
.expect(403);
expect(res.body.error.code).toBe('FORBIDDEN');
});
test('ticket not found → 404', async () => {
const token = makeJwt({ id: nextUserId(), role: 'admin' });
const res = await request(app)
.post('/api/v3/admin/agent-run-dispatcher/deny/2147483641')
.set('Authorization', `Bearer ${token}`)
.send({})
.expect(404);
expect(res.body.error.code).toBe('TICKET_NOT_FOUND');
});
test('admin denies → 200 denied + ticket run_state=failed', async () => {
const tag = `${SMOKE_TAG_PREFIX}deny_ok_${Date.now()}`;
const { ticketId } = await makeTicketWithApproval({ smokeTag: tag });
const token = makeJwt({ id: nextUserId(), role: 'admin' });
const res = await request(app)
.post(`/api/v3/admin/agent-run-dispatcher/deny/${ticketId}`)
.set('Authorization', `Bearer ${token}`)
.send({})
.expect(200);
expect(res.body.success).toBe(true);
expect(res.body.data.state).toBe('denied');
const data = await getTicketData(ticketId);
expect(data.run_state).toBe('failed');
expect(data.run_terminal_reason).toBe('approval_denied');
expect(data.run_approval.state).toBe('denied');
});
test('deny when nothing pending → 410 NO_PENDING_APPROVAL', async () => {
const tag = `${SMOKE_TAG_PREFIX}deny_nopending_${Date.now()}`;
const ticketId = await insertTicket({ smokeTag: tag });
const token = makeJwt({ id: nextUserId(), role: 'admin' });
const res = await request(app)
.post(`/api/v3/admin/agent-run-dispatcher/deny/${ticketId}`)
.set('Authorization', `Bearer ${token}`)
.send({})
.expect(410);
expect(res.body.error.code).toBe('NO_PENDING_APPROVAL');
});
});
});