Files
safetynet/tests/diagnostics-failure-facts.test.mjs
T

22 lines
5.2 KiB
JavaScript
Raw Normal View History

import test from 'node:test';
import assert from 'node:assert/strict';
import {createServer} from 'node:http';
import {createRequire} from 'node:module';
const {createDiagnostics}=createRequire(import.meta.url)('../apps/browser/diagnostics.cjs');
const delay=ms=>new Promise(resolve=>setTimeout(resolve,ms));
async function receiver(t,handler){const server=createServer(handler).listen(0,'127.0.0.1');await new Promise(resolve=>server.once('listening',resolve));t.after(()=>new Promise(resolve=>{server.closeAllConnections();server.close(resolve);}));return `http://127.0.0.1:${server.address().port}`;}
const config=endpoint=>({enabled:true,mode:'debug',endpoint,debugSampleRate:1,sampleRate:1,flushIntervalMs:10000,exportTimeoutMs:250});
async function produce(d){await d.span('browser.request',{},async()=>{});await d.flush();}
test('real timeout, refusal and HTTP503 keep fixed reasons without error text',{timeout:15000},async t=>{
const secret='SYNTHETIC_EXPORT_ERROR_MUST_NOT_RETAIN';
for(const kind of ['timeout','refused','503']){let endpoint;if(kind==='refused'){const server=createServer().listen(0,'127.0.0.1');await new Promise(resolve=>server.once('listening',resolve));endpoint=`http://127.0.0.1:${server.address().port}`;await new Promise(resolve=>server.close(resolve));}else endpoint=await receiver(t,(req,res)=>{req.resume();if(kind==='503'){res.writeHead(503,secret,{'retry-after':'100'});res.end(secret);}});const d=createDiagnostics();t.after(()=>d.shutdown());d.configure(config(endpoint));await produce(d);for(let i=0;i<100&&d.status().lifetimeFailedExports<2;i++)await delay(10);const status=d.status();assert.ok(status.lifetimeFailedExports>=1,kind);const reason=kind==='timeout'?'timeout':kind==='refused'?'connection-refused':'http-server';assert.ok(status.failureReasons[reason]>=1,kind);assert.ok(status.failureFacts.every(f=>f.reason===reason));assert.ok(!JSON.stringify(status).includes(secret));assert.ok(!JSON.stringify(status).includes(endpoint));assert.ok(status.failureFacts.every(f=>['traces','logs'].includes(f.signal)&&f.activeAtCompletion&&!f.retirementOverlapped));const before=status.lifetimeFailedExports;d.configure({enabled:false});assert.equal(d.status().lifetimeFailedExports,before);assert.equal(d.status().failedExports,0);}
});
test('inflight retirement cancellation remains a failure with bounded facts and generation context',{timeout:15000},async t=>{
let received=0;const endpoint=await receiver(t,(req,_res)=>{req.resume();received++;});const d=createDiagnostics();t.after(()=>d.shutdown());d.configure({...config(endpoint),exportTimeoutMs:5000},{policyVersion:1});await d.span('browser.request',{},async()=>{});const flush=d.flush();for(let i=0;i<100&&received<2;i++)await delay(10);assert.equal(received,2);d.configure(config(endpoint),{policyVersion:2});await flush;await delay(50);const status=d.status();assert.equal(status.failedExports,0);assert.equal(status.lifetimeFailedExports,2);assert.equal(status.failureReasons.cancelled,2);assert.ok(status.failureFacts.every(f=>f.generation===1&&!f.activeAtCompletion&&f.retirementOverlapped&&f.cancelSignalled));assert.deepEqual(new Set(status.failureFacts.map(f=>f.signal)),new Set(['traces','logs']));const copy=d.status();copy.failureFacts[0].reason='forged';copy.failureReasons.cancelled=0;assert.equal(d.status().failureReasons.cancelled,2);assert.equal(d.status().failureFacts[0].reason,'cancelled');d.configure({enabled:false});assert.equal(d.status().lifetimeFailedExports,2);assert.equal(received,2,'Retirement repeated a request');
});
test('failure history is bounded while lifetime reason totals retain every failed export',{timeout:15000},async t=>{
const endpoint=await receiver(t,(req,res)=>{req.resume();res.writeHead(400,'Synthetic error body');res.end('PRIVATE_BODY_NOT_RETAINED');});const d=createDiagnostics();t.after(()=>d.shutdown());d.configure(config(endpoint));for(let i=0;i<18;i++)await produce(d);for(let i=0;i<100&&d.status().lifetimeFailedExports<2;i++)await delay(10);const status=d.status();assert.equal(status.lifetimeFailedExports,36);assert.equal(status.failureReasons['http-client'],36);assert.equal(status.failureFacts.length,32);assert.ok(!JSON.stringify(status).includes('PRIVATE_BODY_NOT_RETAINED'));
});
test('concurrent rejected flush cannot erase held native export attribution',{timeout:15000},async t=>{
let received=0;const endpoint=await receiver(t,req=>{req.resume();received++;}),d=createDiagnostics();t.after(()=>d.shutdown());d.configure({...config(endpoint),exportTimeoutMs:5000},{policyVersion:1});await d.span('browser.request',{},async()=>{});const first=d.flush();for(let i=0;i<100&&received<2;i++)await delay(10);assert.equal(received,2);await d.span('browser.request',{},async()=>{});const second=d.flush();for(let i=0;i<100&&!d.status().failureReasons['concurrency-limit'];i++)await delay(10);assert.ok(d.status().failureReasons['concurrency-limit']>=1);d.configure({enabled:false});await Promise.all([first,second]);const status=d.status();assert.equal(received,2);assert.equal(status.failureReasons.cancelled,2);assert.equal(status.lifetimeFailedExports,status.failureReasons.cancelled+status.failureReasons['concurrency-limit']);assert.ok(status.failureFacts.filter(f=>f.reason==='cancelled').every(f=>f.cancelSignalled&&f.retirementOverlapped&&f.generation===1));
});