first cycle lab release

This commit is contained in:
2026-10-04 00:30:09 +07:00
parent 85f6897365
commit d134daafd8
92 changed files with 5623 additions and 9 deletions
+110
View File
@@ -0,0 +1,110 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import path from 'node:path';
import {setup} from './helpers/deployment-fixture.mjs';
import {rollbackOnHost,inspectRollback} from '../platform/rollback.mjs';
import {verifySnapshot} from '../platform/backups.mjs';
import {verifyRelease} from '../platform/runtime.mjs';
import {parseArgs} from '../platform/commands.mjs';
async function seeded(t){
const f=setup(t);await f.deploy();
fs.mkdirSync(path.join(f.host,'runtime/world'));fs.writeFileSync(path.join(f.host,'runtime/world/progress'),'before');
fs.mkdirSync(path.join(f.host,'runtime/mod-state'));fs.writeFileSync(path.join(f.host,'runtime/mod-state/progress'),'before');
f.second=f.next();const update=await f.deploy(f.second);f.snapshot=update.backup_snapshot;f.backupId=path.basename(f.snapshot);
fs.writeFileSync(path.join(f.host,'runtime/world/progress'),'after');fs.writeFileSync(path.join(f.host,'runtime/mod-state/progress'),'after');
fs.writeFileSync(path.join(f.host,'runtime/new-file'),'new');
f.rollback=options=>rollbackOnHost({repository:f.root,root:f.host,env:f.env,execute:f.execute,timeout:30,...options});
f.rollbackState=()=>JSON.parse(fs.readFileSync(path.join(f.host,'rollback-state.json')));
return f;
}
test('no-mode plan is read-only and explicit modes require separate confirmations',async t=>{
const f=await seeded(t),files=fs.readdirSync(f.host),events=f.events().length;
const plan=inspectRollback(f.host);
assert.equal(plan.action,'plan-only');assert.equal(plan.active,'fixture-2.0.0');assert.equal(plan.previous,'fixture-1.0.0');assert.equal(plan.snapshots[0].id,f.backupId);
assert.deepEqual(fs.readdirSync(f.host),files);assert.equal(f.events().length,events);
assert.throws(()=>f.rollback({mode:'pack',version:'1.0.0'}),/confirm-compatible/);
assert.throws(()=>f.rollback({mode:'snapshot',backupId:f.backupId}),/accept-progress-loss/);
assert(!fs.existsSync(path.join(f.host,'rollback-state.json')));
});
test('snapshot rollback replaces full runtime, retains displaced progress/current backup and env',async t=>{
const f=await seeded(t),settings=fs.readFileSync(path.join(f.host,'.env'),'utf8');
const state=f.rollback({mode:'snapshot',backupId:f.backupId,acceptProgressLoss:true});
assert.equal(state.phase,'complete');assert.equal(state.runtime_replaced,true);
assert.equal(fs.readFileSync(path.join(f.host,'runtime/world/progress'),'utf8'),'before');assert.equal(fs.readFileSync(path.join(f.host,'runtime/mod-state/progress'),'utf8'),'before');
assert(!fs.existsSync(path.join(f.host,'runtime/new-file')));
assert.equal(fs.readFileSync(path.join(state.retired_runtime,'world/progress'),'utf8'),'after');assert.equal(fs.readFileSync(path.join(state.retired_runtime,'new-file'),'utf8'),'new');
assert.equal(verifySnapshot(state.backup_snapshot).source_release.name,'fixture-2.0.0');
assert.equal(fs.readFileSync(path.join(f.host,'.env'),'utf8'),settings);
assert.equal(fs.readFileSync(path.join(f.host,'active-release'),'utf8').trim(),'fixture-1.0.0');assert.equal(fs.readFileSync(path.join(f.host,'previous-release'),'utf8').trim(),'fixture-2.0.0');
assert.equal(JSON.parse(fs.readFileSync(path.join(f.host,'.mcpack-runtime.json'))).release_manifest_sha256,verifyRelease(f.release).identity);
assert(!fs.existsSync(path.join(f.host,'.mcpack-runtime.lock')));
});
test('pack-only uses retained release without restore/build and preserves current world',async t=>{
const f=await seeded(t);
const state=f.rollback({mode:'pack',version:'1.0.0',confirmCompatible:true});
assert.equal(state.mode,'pack');assert.equal(state.runtime_replaced,false);assert(state.backup_snapshot);
assert.equal(fs.readFileSync(path.join(f.host,'runtime/world/progress'),'utf8'),'after');assert.equal(fs.readFileSync(path.join(f.host,'runtime/mod-state/progress'),'utf8'),'after');
assert(fs.existsSync(path.join(f.host,'runtime/new-file')));assert.equal(state.target_release,'fixture-1.0.0');
});
test('corrupt snapshot or release is rejected before stopping/current backup',async t=>{
const f=await seeded(t),events=f.events().length;
fs.appendFileSync(path.join(f.snapshot,'runtime.tar.zst'),'corrupt');
assert.throws(()=>f.rollback({mode:'snapshot',backupId:f.backupId,acceptProgressLoss:true}),/checksum mismatch/);
assert.equal(f.events().length,events);assert(!fs.existsSync(path.join(f.host,'rollback-state.json')));
assert.equal(fs.readFileSync(path.join(f.host,'runtime/world/progress'),'utf8'),'after');
});
test('current-state backup failure blocks runtime swap and pointer/binding changes',async t=>{
const f=await seeded(t),binding=fs.readFileSync(path.join(f.host,'.mcpack-runtime.json'),'utf8');
fs.writeFileSync(path.join(f.root,'.tools/tar'),'#!/bin/sh\nexit 42\n',{mode:0o755});
// Pack-only avoids snapshot validation's tar and reaches backup failure.
assert.throws(()=>f.rollback({mode:'pack',version:'1.0.0',confirmCompatible:true}),/No snapshot published/);
assert.equal(f.rollbackState().applied,false);assert.equal(f.rollbackState().phase,'failed');
assert.equal(fs.readFileSync(path.join(f.host,'.mcpack-runtime.json'),'utf8'),binding);assert.equal(fs.readFileSync(path.join(f.host,'active-release'),'utf8').trim(),'fixture-2.0.0');
assert.equal(fs.readFileSync(path.join(f.host,'runtime/world/progress'),'utf8'),'after');
});
test('failed snapshot startup retains both worlds and source pointers, never auto starts previous pack',async t=>{
const f=await seeded(t);fs.writeFileSync(path.join(f.root,'.tools/fail-start'),'fail');
assert.throws(()=>f.rollback({mode:'snapshot',backupId:f.backupId,acceptProgressLoss:true}),/Rollback startup\/health failed/);
const state=f.rollbackState();assert.equal(state.phase,'failed');assert.equal(state.stopped_confirmed,true);assert(state.backup_snapshot);
assert.equal(fs.readFileSync(path.join(state.retired_runtime,'world/progress'),'utf8'),'after');assert.equal(fs.readFileSync(path.join(f.host,'runtime/world/progress'),'utf8'),'before');
assert.equal(fs.readFileSync(path.join(f.host,'active-release'),'utf8').trim(),'fixture-2.0.0');
assert.equal(JSON.parse(fs.readFileSync(path.join(f.host,'.mcpack-runtime.json'))).release_manifest_sha256,verifyRelease(f.release).identity);
const up=f.events().filter(e=>e.command==='up');assert.equal(up.length,3);
assert.throws(()=>f.rollback({mode:'pack',version:'2.0.0',confirmCompatible:true}),/Unresolved rollback state/);
await assert.rejects(f.deploy(f.second),/Unresolved rollback state/);
});
test('explicit snapshot recovery resolves a failed deployment using actual applied binding',async t=>{
const f=await seeded(t),third=f.next('3.0.0');fs.writeFileSync(path.join(f.root,'.tools/fail-start'),'fail');
await assert.rejects(f.deploy(third),/Target startup\/health failed/);
const failure=f.state();assert.equal(failure.phase,'failed');
assert.throws(()=>f.rollback({mode:'pack',version:'1.0.0',confirmCompatible:true}),/snapshot recovery/);
fs.unlinkSync(path.join(f.root,'.tools/fail-start'));
const state=f.rollback({mode:'snapshot',backupId:f.backupId,acceptProgressLoss:true});
assert.equal(state.source_release,'fixture-3.0.0');assert.equal(state.target_release,'fixture-1.0.0');assert.equal(state.phase,'complete');
assert.equal(JSON.parse(fs.readFileSync(state.prior_deployment_state)).phase,'failed');assert.equal(f.state().phase,'complete');
assert.equal(f.state().resolved_deployment,failure.id);
});
test('lock and snapshot ID/path validation precede mutation',async t=>{
const f=await seeded(t);fs.mkdirSync(path.join(f.host,'.mcpack-runtime.lock'));
assert.throws(()=>f.rollback({mode:'pack',version:'1.0.0',confirmCompatible:true}),/runtime operation/);
fs.rmdirSync(path.join(f.host,'.mcpack-runtime.lock'));
assert.throws(()=>f.rollback({mode:'snapshot',backupId:'../escape',acceptProgressLoss:true}),/Snapshot rollback requires/);
assert.throws(()=>inspectRollback(f.host,{backupId:'../escape'}),/snapshot ID/);
});
test('CLI confirmation flags cannot substitute for each other or leak to other commands',()=>{
assert.equal(parseArgs(['rollback']).acceptProgressLoss,false);
assert.equal(parseArgs(['rollback','--backup','id','--accept-progress-loss']).acceptProgressLoss,true);
assert.equal(parseArgs(['rollback-pack','0.0.4','--confirm-compatible']).confirmCompatible,true);
for(const args of [['rollback','--yes'],['rollback','--accept-progress-loss'],['rollback','--confirm-compatible'],['rollback-pack','0.0.4','--accept-progress-loss'],['rollback-pack'],['deploy','--confirm-compatible'],['restore','--backup','id','--destination','/new','--accept-progress-loss']])assert.throws(()=>parseArgs(args));
});