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

65 lines
5.0 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import {execFileSync} from 'node:child_process';
import {release,validVersion} from '../platform/release.mjs';
import {parseArgs} from '../platform/commands.mjs';
import {fixture,commit} from './helpers/build-fixture.mjs';
import {build} from '../platform/build.mjs';
import {verifyRelease} from '../platform/runtime.mjs';
const git=(root,args)=>execFileSync('git',args,{cwd:root,encoding:'utf8'}).trim();
test('release CLI and supported SemVer reject ambiguous flags and versions',()=>{
assert.equal(parseArgs(['release','0.0.6']).version,'0.0.6');
for(const args of [['release'],['release','0.0.6','--release'],['release','0.0.6','--yes'],['release','0.0.6','--backup','x']])assert.throws(()=>parseArgs(args));
for(const version of ['0.0.6','1.0.0-rc.1'])assert(validVersion(version));
for(const version of ['01.0.0','1.0','1.0.0-01','v1.0.0','../1','1.0.0+metadata'])assert(!validVersion(version));
});
test('release builds twice, commits only version, verifies checksums and tags final commit',t=>{
const root=fixture(t),start=git(root,['rev-parse','HEAD']),original=fs.readFileSync(path.join(root,'pack/pack.toml'),'utf8');
const result=release({root,version:'0.0.6',log:()=>{}});
assert.notEqual(result.commit,start);assert.equal(git(root,['rev-parse','v0.0.6^{}']),result.commit);
assert.equal(git(root,['diff-tree','--no-commit-id','--name-only','-r',result.commit]),'pack/pack.toml');
assert.equal(git(root,['status','--porcelain']),'');assert.equal(verifyRelease(result.directory).manifest.git.dirty,false);
assert(fs.readFileSync(path.join(root,'pack/pack.toml'),'utf8').includes('version = "0.0.6"'));
assert(!fs.existsSync(path.join(root,'.tools/release.lock')));
assert(fs.readdirSync(path.join(root,'dist')).every(name=>!name.startsWith('.release-check-')));
assert.notEqual(original,fs.readFileSync(path.join(root,'pack/pack.toml'),'utf8'));
});
test('dirty tree, existing tag and existing artifact refuse before changing source',t=>{
const root=fixture(t),file=path.join(root,'pack/pack.toml'),original=fs.readFileSync(file,'utf8');
fs.writeFileSync(path.join(root,'untracked.txt'),'dirty');assert.throws(()=>release({root,version:'0.0.6'}),/clean/);fs.unlinkSync(path.join(root,'untracked.txt'));
git(root,['tag','v0.0.6']);assert.throws(()=>release({root,version:'0.0.6'}),/already exists/);
fs.mkdirSync(path.join(root,'dist/minecraft-lab-neoforge-spike-0.0.7'));assert.throws(()=>release({root,version:'0.0.7'}),/already exists/);
assert.equal(fs.readFileSync(file,'utf8'),original);
});
test('candidate export failure restores exact source and leaves HEAD/tag/output untouched',t=>{
const root=fixture(t),file=path.join(root,'pack/pack.toml'),original=fs.readFileSync(file,'utf8'),head=git(root,['rev-parse','HEAD']);
fs.writeFileSync(path.join(root,'.tools/state.json'),JSON.stringify({exportFailure:true}));
assert.throws(()=>release({root,version:'0.0.6',log:()=>{}}),/Version restored/);
assert.equal(fs.readFileSync(file,'utf8'),original);assert.equal(git(root,['rev-parse','HEAD']),head);assert.equal(git(root,['tag','--list']),'');assert.equal(git(root,['status','--porcelain']),'');
assert(!fs.existsSync(path.join(root,'dist/minecraft-lab-neoforge-spike-0.0.6')));
});
test('final build failure retains clean version commit but never creates tag',t=>{
const root=fixture(t),head=git(root,['rev-parse','HEAD']);let calls=0;
assert.throws(()=>release({root,version:'0.0.6',log:()=>{},buildRelease:options=>{if(++calls===2)throw new Error('final failure');return build(options);}}),/Version commit retained/);
assert.notEqual(git(root,['rev-parse','HEAD']),head);assert.equal(git(root,['tag','--list']),'');assert.equal(git(root,['status','--porcelain']),'');
});
test('release shared preparation lock refuses concurrent operation',t=>{
const root=fixture(t);fs.mkdirSync(path.join(root,'.tools/release.lock'));
assert.throws(()=>release({root,version:'0.0.6'}),/locked/);
});
test('commit hook rejection restores source and unstages only the release version',t=>{
const root=fixture(t),file=path.join(root,'pack/pack.toml'),original=fs.readFileSync(file,'utf8'),head=git(root,['rev-parse','HEAD']);
const hook=path.join(root,'.git/hooks/pre-commit');fs.writeFileSync(hook,'#!/bin/sh\nexit 1\n',{mode:0o755});
assert.throws(()=>release({root,version:'0.0.6',log:()=>{}}),/Version restored/);
assert.equal(fs.readFileSync(file,'utf8'),original);assert.equal(git(root,['status','--porcelain']),'');assert.equal(git(root,['rev-parse','HEAD']),head);
});
test('concurrent source edit during candidate validation is preserved on failure',t=>{
const root=fixture(t),file=path.join(root,'pack/pack.toml');
assert.throws(()=>release({root,version:'0.0.6',log:()=>{},buildRelease:()=>{fs.appendFileSync(file,'\n# concurrent operator edit\n');throw new Error('candidate failed');}}),/Concurrent changes retained/);
assert(fs.readFileSync(file,'utf8').endsWith('# concurrent operator edit\n'));assert.equal(git(root,['tag','--list']),'');
});