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'; 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)); 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'}}); 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);}} 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); 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); 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]); });