feat(harness-f0): drain pula orgs em ai_dispatch_mode=external (spec 14)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QLrVxFCDjcRDCdWUSPftbZ
This commit is contained in:
Rafael Melgaço
2026-07-23 08:39:30 -03:00
co-authored by Claude Fable 5
parent 4efd340b51
commit 028ac190b0
2 changed files with 44 additions and 0 deletions
+33
View File
@@ -0,0 +1,33 @@
import { describe, expect, it, vi } from 'vitest';
import type pg from 'pg';
import { drainTick } from './drain';
const knobs = { batchSize: 10, intervalMs: 0, idleIntervalMs: 0, debounceMs: 0, reapTimeoutMs: 60000 };
const log = { info: vi.fn(), warn: vi.fn(), error: vi.fn() } as never;
const event = {
id: 'e1', organization_id: 'org1', attempts: 1,
payload: {
conversation_id: '11111111-1111-4111-8111-111111111111',
contact_id: '22222222-2222-4222-8222-222222222222',
channel_session_id: '33333333-3333-4333-8333-333333333333',
inbound_message_id: '44444444-4444-4444-8444-444444444444',
},
};
it('org em ai_dispatch_mode=external: evento vira done SEM enfileirar job', async () => {
const calls: string[] = [];
const query = vi.fn().mockImplementation((sql: string) => {
calls.push(sql);
if (sql.includes('returning e.id')) return { rows: [event] }; // claim
if (sql.includes("ai_dispatch_mode")) return { rows: [{ mode: 'external' }] }; // guard
if (sql.includes('is_group')) return { rows: [{ is_group: false }] };
return { rows: [] }; // reaper / done
});
await drainTick({ query } as unknown as pg.Pool, knobs, log);
// o guard TEM que consultar o modo (garante FAIL antes da implementação)...
expect(calls.some((s) => s.includes('ai_dispatch_mode'))).toBe(true);
// ...e nenhum job pode ser enfileirado (enqueueJob nunca roda).
expect(calls.some((s) => s.includes('job_queue'))).toBe(false);
expect(calls.some((s) => s.includes("status = 'done'"))).toBe(true);
});
+11
View File
@@ -120,6 +120,17 @@ async function processEvent(
}
const p = parsed.data;
// Spec 14: org em modo 'external' tem agente EXTERNO como dono da conversa —
// o engine não responde por cima. Evento é consumido (done) sem job.
const { rows: modeRows } = await pool.query<{ mode: string | null }>(
`select settings->>'ai_dispatch_mode' as mode from organizations where id = $1`,
[event.organization_id],
);
if (modeRows[0]?.mode === 'external') {
log.info('drain: org em modo external (spec 14) — evento pulado', { event_id: event.id });
return;
}
// Grupos: skip, sem exceção (regra dura nº 12).
const { rows: convRows } = await pool.query<{ is_group: boolean }>(
'select is_group from conversations where organization_id = $1 and id = $2',