import assert from 'node:assert/strict';import {readFileSync,writeFileSync,mkdirSync} from 'node:fs';import {randomUUID,createHash,createHmac} from 'node:crypto';import {createServer} from 'node:http';import {chromium,expect} from '@playwright/test';import pg from 'pg'; import {drainTick} from '../../lib/agent-engine/edge/crm/drain';import {claimJobs,completeJob,failJob} from '../../lib/agent-engine/queue/queue';import {claimOfJob} from '../../lib/agent-engine/queue/claim';import {withServiceJob} from '../../lib/atendimento/fronteira-server';import {createInboundTurnHandler} from '../../lib/agent-engine/agent/inbound-turn';import {requestTurnDeps} from '../../lib/agent-engine/agent/request-deps'; const m=JSON.parse(readFileSync('/tmp/codex3475-manifest.json','utf8'));const db=new pg.Pool({connectionString:process.env.SUPABASE_DB_URL,max:3});const org=m.organization.id;const dir='evidence/atendimento-followups-3475/real-engine';mkdirSync(dir,{recursive:true});const received:any[]=[];const receiver=createServer(async(req,res)=>{const chunks=[];for await(const c of req)chunks.push(c);if(req.method==='POST'&&req.url==='/api/sendText'){const body=JSON.parse(Buffer.concat(chunks).toString());const id='qa3475-engine-'+randomUUID();received.push({id,...body});res.writeHead(200,{'content-type':'application/json'});return res.end(JSON.stringify({id}));}res.writeHead(200,{'content-type':'application/json'});res.end('{}');});async function q(s:string,v:unknown[]=[]){return(await db.query(s,v)).rows;} (async()=>{assert(!process.env.INTERNAL_AGENT_RUN_STUB,'stub_not_allowed');const deps=requestTurnDeps();const handler=createInboundTurnHandler(deps);await new Promise((r,j)=>receiver.once('error',j).listen(39751,'127.0.0.1',r));const cases=[];for(const [name,marker,phone] of [['invisible','QA3475_INVISIBLE','+12025550195'],['valid','QA3475_REAL_ENGINE_VALID','+12025550196']]){const externalId='qa3475-inbound-'+randomUUID();const raw=JSON.stringify({event:'message',session:m.ack.session_name,payload:{id:externalId,from:phone.slice(1)+'@c.us',fromMe:false,body:marker+' Saudação sintética autorizada.',type:'chat',timestamp:Math.floor(Date.now()/1000),_data:{notifyName:'QA3475 Engine '+name}}});const wh=await fetch(process.env.QA_3475_BASE_URL+'/api/v1/webhooks/waha/'+process.env.QA_3475_WEBHOOK_PATH_TOKEN,{method:'POST',headers:{'content-type':'application/json','x-webhook-hmac':'sha512='+createHmac('sha512',process.env.QA_3475_WEBHOOK_HMAC_SECRET!).update(raw).digest('hex')},body:raw});assert.equal(wh.status,200,'real_inbound_hmac_webhook_failed');const stored=(await q('select id,contact_id,conversation_id from messages where organization_id=$1 and external_id=$2',[org,externalId]))[0];assert(stored,'actual_ingestion_did_not_persist');const contact={id:stored.contact_id};const cv={id:stored.conversation_id};const message={id:stored.id};await q("update contacts set ai_authorized_at=now(),ai_authorized_reason='retomada_manual' where organization_id=$1 and id=$2",[org,contact.id]);const cron=await fetch(process.env.QA_3475_BASE_URL+'/api/v1/cron/event-log-drain',{method:'POST',headers:{authorization:'Bearer '+process.env.QA_3475_CRON_SECRET}});assert.equal(cron.status,200,'canonical_event_drain_failed');for(let i=0;i<3;i++)await drainTick(db,{batchSize:100,intervalMs:1000,idleIntervalMs:1000,debounceMs:0,reapTimeoutMs:60000},deps.log);const jobs=await q("select id from job_queue where organization_id=$1 and contact_id=$2 and kind='inbound_turn' order by created_at desc limit 1",[org,contact.id]);assert.equal(jobs.length,1,'canonical_drain_did_not_enqueue');await q("update job_queue set priority=0,run_after=now(),max_attempts=1 where organization_id=$1 and id=$2 and status='pending'",[org,jobs[0].id]);const workerId='codex3475-bounded-'+name;const claimed=await claimJobs(db,{workerId,maxConcurrency:1,batchSize:1});assert.equal(claimed[0]?.id,jobs[0].id,'claim_did_not_select_own_target');const job=claimed[0];const before=received.length;let errorName:null|string=null;try{await withServiceJob(db,job,()=>handler(job,db,{workerId}));await completeJob(db,job.id,workerId,undefined,claimOfJob(job)?.acquired_at);}catch(e){errorName=e instanceof Error?e.name:'unknown';await failJob(db,job.id,workerId,e,claimOfJob(job)?.acquired_at);}const calls=JSON.parse(readFileSync('/tmp/codex3475-llm-receipts.json','utf8'));const sent=received.slice(before);const outbound=await q("select id,body,status,external_id from messages where organization_id=$1 and conversation_id=$2 and direction='outbound'",[org,cv.id]);const traces=await q('select id,status,is_dry_run,messages_sent,ferramentas from agent_turn_traces where organization_id=$1 and job_id=$2',[org,job.id]);if(name==='invisible'){assert(calls.some((r:any)=>r.invisible&&r.corpo_vazio_observado),'corpo_vazio_not_observed_by_http_provider');assert(calls.some((r:any)=>r.invisible&&r.tool_body_codepoints?.length===1&&r.tool_body_codepoints[0]===0x200e),'provider_did_not_send_exact_u200e');assert.equal(sent.length,0,'invisible_caused_send_http');assert.equal(outbound.length,0,'invisible_outbound_persisted');assert(errorName,'bounded_provider_stop_not_observed');}else{assert.equal(errorName,null,'valid_real_engine_failed');assert.equal(outbound.length,1,'valid_outbound_missing');assert.equal(sent.length,1,'valid_send_http_missing');assert.equal(sent[0].text,'*'+m.signatures.agent_name+'*\n'+outbound[0].body,'valid_signature_wrong');assert(['sent','delivered','read'].includes(outbound[0].status),'valid_not_accepted');}cases.push({name,contact_id:contact.id,conversation_id:cv.id,message_id:message.id,job_id:job.id,errorName,outbound,trace:traces,http_receipts:sent.map(r=>({id:r.id,text_sha256:createHash('sha256').update(r.text).digest('hex')}))});writeFileSync(dir+'/relatorio.json',JSON.stringify({synthetic:true,build:m.build,cases,provider_calls:calls,limits:['HTTP provider controlado; não é modelo externo nem WhatsApp real.','A tentativa invisível é encerrada intencionalmente por HTTP400 do provider APÓS receber corpo_vazio; mede a guarda real, não recuperação do modelo.','Handler, lease, fronteira e liquidação são os mesmos do worker, executados uma vez sem iniciar os outros loops.']},null,2));} const browser=await chromium.launch({headless:true});try{const page=await browser.newPage({baseURL:process.env.QA_3475_BASE_URL,viewport:{width:1440,height:1000}});await page.goto('/login');await page.locator('#email').fill(m.users.admin.email);await page.locator('#password').fill(process.env[m.users.admin.password_env]!);await page.getByRole('button',{name:'Entrar',exact:true}).click();await page.waitForURL(/\/app(?:\/|$|\?)/);for(const c of cases){await page.goto('/app/inbox?id='+c.conversation_id);await expect(page.getByTestId('message-bubble').filter({hasText:c.name==='invisible'?'QA3475_INVISIBLE':'A mensagem de controle foi recebida e registrada.'}).first()).toBeVisible();await page.screenshot({path:dir+'/'+c.name+'-inbox.png',fullPage:true});}}finally{await browser.close();}process.stdout.write('REAL_ENGINE_GUARD_AND_VALID_CONTROL_PASSED\n');})().catch(e=>{process.stderr.write('REAL_ENGINE_FAILED '+e.message+'\n');process.exitCode=1}).finally(async()=>{receiver.close();await db.end();});