import test from 'node:test'; import assert from 'node:assert/strict'; import fs from 'node:fs'; import path from 'node:path'; import { waitForHealth, healthOptions } from '../platform/health.mjs'; import { fixture } from './helpers/runtime-fixture.mjs'; const healthy = () => ({ Id:'a'.repeat(64), RestartCount:0, State:{Status:'running',Running:true,StartedAt:'start',Health:{Status:'healthy'}} }); function check(inspect, probe=()=>({status:0,stdout:'Minecraft 1.21.1'}), options={}) { let time=0; return waitForHealth({inspect,probe,timeoutSeconds:6,stableSeconds:2,now:()=>time,pause:ms=>{time+=ms;},...options}); } test('readiness requires repeated successful Minecraft queries and stable container', () => { let queries=0; const result=check(healthy,()=>{queries++;return{status:0};}); assert.equal(result.ok,true); assert.equal(result.elapsed_ms,2000); assert.equal(queries,3); }); test('running TCP-ready/healthy container with failed Minecraft query times out', () => { const result=check(healthy,()=>({status:1,stderr:'protocol unavailable'})); assert.equal(result.reason,'timeout'); assert.equal(result.probe.status,1); }); test('starting or unhealthy cannot pass and transient failures reset stability window', () => { for(const status of ['starting','unhealthy'])assert.equal(check(()=>({...healthy(),State:{...healthy().State,Health:{Status:status}}})).reason,'timeout'); let calls=0; assert.equal(check(healthy,()=>({status:++calls===2?1:0})).elapsed_ms,4000); }); test('crash/OOM and paused fail immediately with diagnostic state', () => { const dead=healthy();Object.assign(dead.State,{Running:false,Status:'exited',ExitCode:137,OOMKilled:true}); const result=check(()=>dead); assert.equal(result.reason,'crash');assert.equal(result.state.oom_killed,true);assert.equal(result.state.exit_code,137); const paused=healthy();paused.State.Paused=true; assert.equal(check(()=>paused).reason,'paused'); }); test('restarting, increasing restart count, replacement and start time changes fail', () => { const restart=healthy();restart.State.Restarting=true; assert.equal(check(()=>restart).reason,'restart-loop'); for(const change of [c=>c.RestartCount++,c=>c.State.StartedAt='new',c=>c.Id='b'.repeat(64)]){ let calls=0;assert.match(check(()=>{const c=healthy();if(calls++)change(c);return c;}).reason,/restart-loop|container-replaced/); } }); test('missing container/healthcheck and inspection errors never report success', () => { assert.equal(check(()=>null).reason,'missing-container'); assert.equal(check(()=>{const c=healthy();delete c.State.Health;return c;}).reason,'missing-healthcheck'); assert.equal(check(()=>{throw new Error('Docker denied');}).reason,'inspection-error'); }); test('health flags require positive bounded options and detached startup', () => { assert.deepEqual(healthOptions(['up','--wait','--wait-timeout','30','--stable-seconds','2']).forwarded,['up','-d']); assert.equal(healthOptions(['health','--wait-timeout','60']).timeoutSeconds,60); for(const args of [['up','--wait-timeout','30'],['health','--wait-timeout','NaN'],['health','--wait-timeout','3'],['health','--stable-seconds','0'],['stop','--wait'],['health','--wait-timeout','20','--wait-timeout','30'],['up','--wait','--abort-on-container-exit']])assert.throws(()=>healthOptions(args)); }); test('failed waited startup writes report/logs, preserves binding, releases lifecycle lock', t => { const f=fixture(t); fs.writeFileSync(path.join(f.root,'.tools/fail-start'),''); assert.equal(f.run(['up','--wait','--wait-timeout','30']),1); const files=fs.readdirSync(path.join(f.host,'diagnostics')); const report=JSON.parse(fs.readFileSync(path.join(f.host,'diagnostics',files.find(name=>name.endsWith('.json'))))); assert.equal(report.reason,'compose-start-failed');assert.equal(report.compose_exit,19); assert.equal(report.recovery.automatic_rollback,false); assert(fs.existsSync(report.logs)); assert(fs.existsSync(path.join(f.host,'.mcpack-runtime.json'))); assert(!fs.existsSync(path.join(f.host,'.mcpack-runtime.lock'))); }); test('deadline includes launch time and a late successful query cannot pass', () => { assert.equal(check(healthy,()=>({status:0}),{startedAtMs:-5000}).reason,'timeout'); let time=0; const result=waitForHealth({inspect:healthy,probe:()=>{time+=6000;return{status:0};},timeoutSeconds:6,stableSeconds:2,now:()=>time,pause:ms=>{time+=ms;}}); assert.equal(result.reason,'timeout'); assert.equal(healthOptions(['up','--wait','--wait-timeout=30']).timeoutSeconds,30); assert.throws(()=>healthOptions(['up','--wait=true'])); });