first cycle lab release
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
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']));
|
||||
});
|
||||
Reference in New Issue
Block a user