2026-10-14 01:18:23 +02:00
import test from 'node:test' ; import assert from 'node:assert/strict' ; import { mkdtempSync , rmSync , readdirSync , readFileSync } from 'node:fs' ; import { tmpdir } from 'node:os' ; import { join , resolve } from 'node:path' ; import { spawn } from 'node:child_process' ; import { createServer } from 'node:net' ; import { request } from 'node:http' ;
2026-10-13 16:58:15 +02:00
test ( 'actual Windows protected collector rotates encrypted exports and rejects oversized storage without acknowledgement' ,{ skip : process . platform !== 'win32' }, async t =>{
const root = mkdtempSync ( join ( tmpdir (), 'safetynet-collector-http-' )); const probe = createServer (); await new Promise ( r => probe . listen ( 0 , '127.0.0.1' , r )); const port = probe . address (). port ; await new Promise ( r => probe . close ( r ));
2026-10-14 01:18:23 +02:00
let child = spawn ( process . execPath ,[ resolve ( 'scripts/debug-collector.mjs' )],{ windowsHide : true , stdio : 'pipe' , env : {... process . env , DEBUG_COLLECTOR_DIRECTORY : root , DEBUG_COLLECTOR_PORT : String ( port ), DEBUG_COLLECTOR_SEGMENT_BYTES : '1024' , DEBUG_COLLECTOR_RETIRED_SEGMENTS : '1' , DEBUG_COLLECTOR_MAX_REQUESTS : '2' , DEBUG_COLLECTOR_MAX_BUFFERED_BYTES : '4096' }});
2026-10-13 16:58:15 +02:00
let output = '' ; child . stdout . on ( 'data' , b => output += b ); child . stderr . on ( 'data' , b => output += b ); t . after ( async ()=>{ if ( child . exitCode === null ){ child . kill (); await new Promise ( r => child . once ( 'exit' , r ));} assert . ok ( root . startsWith ( join ( tmpdir (), 'safetynet-collector-http-' ))); rmSync ( root ,{ recursive : true , force : true });});
const base = `http://127.0.0.1: ${ port } ` ; let health ;
for ( let i = 0 ; i < 80 ; i ++ ){ if ( child . exitCode !== null ) throw Error ( 'Collector test process failed: ' + output ); try { health = await ( await fetch ( base + '/health' )). json (); break ;} catch { await new Promise ( r => setTimeout ( r , 100 ));}}
assert . ok ( health ); const marker = 'SYNTHETIC_COLLECTOR_ROTATION' ; const post = body => fetch ( base + '/v1/logs' ,{ method : 'POST' , headers : { 'content-type' : 'application/json' }, body : JSON . stringify ( body )});
for ( let i = 0 ; i < 12 ; i ++ ) assert . equal (( await post ({ resourceLogs : [{ scopeLogs : [{ logRecords : [{ body : { stringValue : marker }}]}]}]})). status , 200 );
health = await ( await fetch ( base + '/api/summary' )). json (); assert . equal ( health . requests , 12 ); assert . equal ( health . counts . logs , 12 ); assert . ok ( health . rotations > 1 ); assert . ok ( health . droppedSegments > 0 ); assert . equal ( health . retiredSegments , 1 ); assert . ok ( health . activeBytes + health . retiredBytes <= 2048 );
for ( const name of readdirSync ( root ). filter ( n => n . endsWith ( '.ndjson' ))){ const content = readFileSync ( join ( root , name ), 'utf8' ); assert . equal ( content . includes ( marker ), false ); for ( const line of content . trim (). split ( '\n' )){ const envelope = JSON . parse ( line ); assert . equal ( envelope . v , 1 ); assert . ok ( envelope . nonce && envelope . tag && envelope . ciphertext );}}
2026-10-14 01:18:23 +02:00
const waitAdmission = async check =>{ for ( let n = 0 ; n < 80 ; n ++ ){ const value = await ( await fetch ( base + '/health' )). json (); if ( check ( value . admission )) return value ; await new Promise ( resolve => setTimeout ( resolve , 10 ));} throw Error ( 'Collector admission state timed out' );};
const hold = ()=>{ let respond ; const response = new Promise ( resolve =>{ respond = resolve ;}), req = request ( base + '/v1/logs' ,{ method : 'POST' , headers : { 'content-type' : 'application/json' , 'transfer-encoding' : 'chunked' }}, res =>{ let body = '' ; res . on ( 'data' , chunk => body += chunk ); res . on ( 'end' ,()=> respond ({ status : res . statusCode , body }));}); req . on ( 'error' ,()=>{}); req . write ( ' ' ); return { req , response };};
const a = hold (), b = hold (); await waitAdmission ( value => value . activeRequests === 2 );
const busy = await post ({ resourceLogs : []}); assert . equal ( busy . status , 503 ); assert . deepEqual ( await busy . json (),{ error : 'Collector busy' }); a . req . destroy (); b . req . destroy (); await waitAdmission ( value => value . activeRequests === 0 && value . bufferedBytes === 0 );
const c = hold (); c . req . write ( ' ' . repeat ( 2999 )); await waitAdmission ( value => value . bufferedBytes === 3000 ); const d = hold (); d . req . write ( ' ' . repeat ( 1099 )); const overflow = await d . response ; assert . equal ( overflow . status , 503 ); assert . deepEqual ( JSON . parse ( overflow . body ),{ error : 'Collector busy' }); c . req . destroy (); d . req . destroy ();
health = await waitAdmission ( value => value . activeRequests === 0 && value . bufferedBytes === 0 ); assert . equal ( health . requests , 12 ); assert . equal ( health . admission . requestLimitRejected , 1 ); assert . equal ( health . admission . bufferLimitRejected , 1 ); assert . ok ( health . admission . peakBufferedBytes <= 4096 ); assert . ok ( health . admission . maxQueueWaitMs >= 0 ); assert . ok ( health . admission . maxCommitMs >= 0 ); assert . ok ( health . eventLoopLag . maxMs >= 0 );
2026-10-13 16:58:15 +02:00
assert . equal (( await post ({ large : 'x' . repeat ( 2048 )})). status , 507 ); health = await ( await fetch ( base + '/health' )). json (); assert . equal ( health . requests , 12 ); assert . equal ( health . rejectedWrites , 1 );
const before = structuredClone ( health ); const tracePost = body => fetch ( base + '/v1/traces' ,{ method : 'POST' , headers : { 'content-type' : 'application/json' }, body : JSON . stringify ( body )});
const span = { traceId : '1234567890abcdef1234567890abcdef' , spanId : '1234567890abcdef' , name : 'synthetic.stage' , startTimeUnixNano : '10' , endTimeUnixNano : '11' };
for ( const bad of [{... span , endTimeUnixNano : '9' },{... span , startTimeUnixNano : '18446744073709551616' },{... span , traceId : 123 },{... span , endTimeUnixNano : 'oops' },{... span , attributes : {}}]) assert . equal (( await tracePost ({ resourceSpans : [{ scopeSpans : [{ spans : [ bad ]}]}]})). status , 400 );
assert . equal (( await tracePost ({ resourceSpans : {}})). status , 400 ); assert . equal (( await post ({ resourceLogs : [{ scopeLogs : {}}]})). status , 400 );
health = await ( await fetch ( base + '/health' )). json (); for ( const field of [ 'requests' , 'storedBytes' , 'activeBytes' , 'legacyBytes' , 'retiredBytes' , 'rotations' , 'droppedSegments' , 'droppedBytes' , 'rejectedWrites' , 'retainedSpans' ]) assert . equal ( health [ field ], before [ field ]); assert . deepEqual ( health . counts , before . counts );
assert . equal (( await tracePost ({ resourceSpans : []})). status , 200 ); assert . equal (( await tracePost ({ resourceSpans : [{ scopeSpans : [{ spans : [ span ]}]}]})). status , 200 ); const trace = await ( await fetch ( base + '/api/trace/' + span . traceId )). json (); assert . equal ( trace . spans [ 0 ]. durationMs , 0.000001 );
2026-10-14 01:18:23 +02:00
const beforeRestart = await ( await fetch ( base + '/health' )). json (); child . kill (); await new Promise ( resolve => child . once ( 'exit' , resolve ));
child = spawn ( process . execPath ,[ resolve ( 'scripts/debug-collector.mjs' )],{ windowsHide : true , stdio : 'pipe' , env : {... process . env , DEBUG_COLLECTOR_DIRECTORY : root , DEBUG_COLLECTOR_PORT : String ( port ), DEBUG_COLLECTOR_SEGMENT_BYTES : '1024' , DEBUG_COLLECTOR_RETIRED_SEGMENTS : '1' , DEBUG_COLLECTOR_MAX_REQUESTS : '2' , DEBUG_COLLECTOR_MAX_BUFFERED_BYTES : '4096' }}); child . stdout . on ( 'data' , buffer => output += buffer ); child . stderr . on ( 'data' , buffer => output += buffer );
let restarted ; for ( let n = 0 ; n < 80 ; n ++ ){ if ( child . exitCode !== null ) throw Error ( 'Protected collector restart failed' ); try { restarted = await ( await fetch ( base + '/health' )). json (); break ;} catch { await new Promise ( resolve => setTimeout ( resolve , 100 ));}} assert . ok ( restarted ); assert . equal ( restarted . requests , 0 ); for ( const field of [ 'storedBytes' , 'activeBytes' , 'legacyBytes' , 'retiredBytes' , 'retiredSegments' ]) assert . equal ( restarted [ field ], beforeRestart [ field ]);
2026-10-13 16:58:15 +02:00
});