111 lines
8.6 KiB
JavaScript
111 lines
8.6 KiB
JavaScript
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));
|
|
});
|