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 crypto from 'node:crypto'; import { execFileSync } from 'node:child_process'; import { fileURLToPath } from 'node:url'; import { parse, stringify } from 'smol-toml'; import { parse as parseYaml, stringify as stringifyYaml } from 'yaml'; import { build } from '../platform/build.mjs'; import {fixture,commit} from './helpers/build-fixture.mjs'; const repository=fileURLToPath(new URL('../',import.meta.url)); const sha=file=>crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex'); const git=(root,args)=>execFileSync('git',args,{cwd:root,stdio:'pipe'}); function refresh(root) { const directory = path.join(root, 'pack'); const pack = parse(fs.readFileSync(path.join(directory, 'pack.toml'), 'utf8')); const index = parse(fs.readFileSync(path.join(directory, pack.index.file), 'utf8')); for (const entry of index.files) entry.hash = sha(path.join(directory, entry.file)); fs.writeFileSync(path.join(directory, pack.index.file), stringify(index)); pack.index.hash = sha(path.join(directory, pack.index.file)); fs.writeFileSync(path.join(directory, 'pack.toml'), stringify(pack)); } const perform = (root, options = {}) => build({ root, log: () => {}, ...options }); const leftovers = root => fs.existsSync(path.join(root, 'dist')) ? fs.readdirSync(path.join(root, 'dist')).filter(file => file.startsWith('.build-')) : []; test('full clean release has portable Compose, complete checksums, ownership and no secrets/target', t => { const root = fixture(t); const configFile = path.join(root, 'platform/config.yaml'); const config = parseYaml(fs.readFileSync(configFile, 'utf8')); config.server.memory = '3G'; config.server.port = 25570; config.server.host = 'private-deployment-host'; config.server.directory = '/private/remote/path'; config.build.output_directory = './releases'; fs.writeFileSync(configFile, stringifyYaml(config)); fs.writeFileSync(path.join(root, 'server/.env'), 'PRIVATE_TOKEN=secret-local-value\nMC_EULA=TRUE\n'); commit(root); const release = perform(root, { releaseOnly: true }); assert.equal(path.basename(path.dirname(release)), 'releases'); const manifest = JSON.parse(fs.readFileSync(path.join(release, 'manifest.json'))); assert.equal(manifest.git.dirty, false); assert.equal(manifest.git.commit, git(root, ['rev-parse', 'HEAD']).toString().trim()); assert.equal(manifest.server.memory, '3G'); assert.equal(manifest.server.port, 25570); assert(manifest.managed_files.client.some(file => /MouseTweaks/.test(file.path))); assert(!manifest.managed_files.server.some(file => /MouseTweaks/.test(file.path))); const serverMarker = manifest.managed_files.server.find(file => file.path === 'config/spike-marker.txt'); assert.equal(serverMarker.owner, 'server'); assert.equal(serverMarker.overrides, 'pack'); assert.equal(serverMarker.hashes.sha256, sha(path.join(root, 'server/config/spike-marker.txt'))); for (const file of manifest.release_files) assert.equal(file.sha256, sha(path.join(release, file.path))); const checksumLines = fs.readFileSync(path.join(release, 'SHA256SUMS'), 'utf8').trim().split('\n'); assert.equal(checksumLines.length, manifest.release_files.length + 1); assert(checksumLines.some(line => line.endsWith(' manifest.json'))); execFileSync('sha256sum', ['-c', 'SHA256SUMS'], { cwd: release }); const composeText = fs.readFileSync(path.join(release, 'server/compose.yaml'), 'utf8'); const compose = parseYaml(composeText).services.minecraft; assert.equal(compose.image, manifest.server.image); assert.equal(compose.environment.MEMORY, '${MC_MEMORY:-3G}'); assert.equal(compose.ports[0], '${MC_BIND_ADDRESS:-127.0.0.1}:${MC_PORT:-25570}:25565'); assert.equal(compose.environment.MODRINTH_MODPACK, `/modpacks/${manifest.artifact}`); assert(compose.volumes.every(volume => volume.source.startsWith('${MC_'))); for (const file of ['manifest.json', 'server/compose.yaml', 'server/.env.example']) { const text = fs.readFileSync(path.join(release, file), 'utf8'); for (const secret of ['secret-local-value', 'private-deployment-host', '/private/remote/path', root]) assert(!text.includes(secret)); } assert.match(fs.readFileSync(path.join(release, 'server/.env.example'), 'utf8'), /MC_EULA=FALSE/); assert.deepEqual(leftovers(root), []); }); test('development build records dirty state and leaves source unchanged', t => { const root = fixture(t); fs.writeFileSync(path.join(root, 'local-note.txt'), 'dirty\n'); const before = sha(path.join(root, 'pack/pack.toml')); const release = perform(root); assert.equal(JSON.parse(fs.readFileSync(path.join(release, 'manifest.json'))).git.dirty, true); assert.equal(sha(path.join(root, 'pack/pack.toml')), before); }); test('existing immutable release is never overwritten', t => { const root = fixture(t), release = perform(root); const before = sha(path.join(release, 'SHA256SUMS')); assert.throws(() => perform(root), /already exists/); assert.equal(sha(path.join(release, 'SHA256SUMS')), before); }); test('stale metadata is rejected before export', t => { const root = fixture(t); fs.appendFileSync(path.join(root, 'pack/config/spike-marker.txt'), 'stale\n'); assert.throws(() => perform(root), /stale indexed file/); }); test('refresh changes in temporary copy fail safely without changing source', t => { const root = fixture(t), index = path.join(root, 'pack/index.toml'), before = sha(index); fs.writeFileSync(path.join(root, '.tools/state.json'), '{"stale":true}'); assert.throws(() => perform(root), /Stale source/); assert.equal(sha(index), before); assert.deepEqual(fs.readdirSync(path.join(root, 'dist')), ['.gitkeep']); }); test('failed export leaves no partial release or build lock', t => { const root = fixture(t); fs.writeFileSync(path.join(root, '.tools/state.json'), '{"exportFailure":true}'); assert.throws(() => perform(root), /Simulated export failure/); assert.deepEqual(fs.readdirSync(path.join(root, 'dist')), ['.gitkeep']); }); test('unverified loader/image combination is rejected', t => { const root = fixture(t), file = path.join(root, 'pack/pack.toml'); const pack = parse(fs.readFileSync(file, 'utf8')); pack.versions.neoforge = '21.1.251'; fs.writeFileSync(file, stringify(pack)); assert.throws(() => perform(root), /not been verified/); }); test('duplicate mod definitions fail despite a consistent index', t => { const root = fixture(t), directory = path.join(root, 'pack'); const indexFile = path.join(directory, 'index.toml'), index = parse(fs.readFileSync(indexFile, 'utf8')); const entry = index.files.find(file => file.file.includes('mouse-tweaks')); fs.copyFileSync(path.join(directory, entry.file), path.join(directory, 'mods/duplicate.pw.toml')); index.files.push({ ...entry, file: 'mods/duplicate.pw.toml' }); fs.writeFileSync(indexFile, stringify(index)); refresh(root); assert.throws(() => perform(root), /Duplicate mod target/); }); test('invalid/duplicate config keys and protected output paths are rejected', t => { const root = fixture(t), file = path.join(root, 'platform/config.yaml'); fs.appendFileSync(file, '\nschema_version: 1\n'); assert.throws(() => perform(root), /unique/); fs.copyFileSync(path.join(repository, 'platform/config.yaml'), file); const config = parseYaml(fs.readFileSync(file, 'utf8')); config.build.output_directory = './pack/output'; fs.writeFileSync(file, stringifyYaml(config)); assert.throws(() => perform(root), /overlaps/); config.build.output_directory = '../outside'; fs.writeFileSync(file, stringifyYaml(config)); assert.throws(() => perform(root), /inside repository/); }); test('release mode rejects dirty worktree', t => { const root = fixture(t); fs.writeFileSync(path.join(root, 'untracked.txt'), 'dirty'); assert.throws(() => perform(root, { releaseOnly: true }), /clean Git worktree/); }); test('tracked known secrets are rejected', t => { const root = fixture(t); fs.writeFileSync(path.join(root, 'server/.env'), 'PRIVATE_TOKEN=x'); git(root, ['add', '-f', 'server/.env']); commit(root); assert.throws(() => perform(root), /secret path is tracked/); }); test('tampered tool and source symlinks fail validation', t => { const root = fixture(t); const marker = path.join(root, 'pack/config/spike-marker.txt'); fs.unlinkSync(marker); fs.symlinkSync('/tmp/nonexistent-mcpack-marker', marker); assert.throws(() => perform(root), /regular files/); fs.unlinkSync(marker); fs.copyFileSync(path.join(repository, 'pack/config/spike-marker.txt'), marker); fs.appendFileSync(path.join(root, '.tools/packwiz/packwiz'), '\n// tampered\n'); assert.throws(() => perform(root), /checksum mismatch/); });