Files
minecraft-lab/tests/health.test.mjs
T
2026-10-04 00:30:09 +07:00

83 lines
4.5 KiB
JavaScript

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