import test from 'node:test'; import assert from 'node:assert/strict'; import fs from 'node:fs'; import path from 'node:path'; import os from 'node:os'; import crypto from 'node:crypto'; import { spawnSync } from 'node:child_process'; import { fileURLToPath } from 'node:url'; import { parseArgs, packwizArguments } from '../platform/commands.mjs'; const repository = fileURLToPath(new URL('../', import.meta.url)); test('mod commands map to packwiz without automatic yes', () => { assert.deepEqual(packwizArguments(parseArgs(['add', 'mouse-tweaks'])), ['modrinth', 'add', 'mouse-tweaks']); assert.deepEqual(packwizArguments(parseArgs(['remove', 'mouse-tweaks'])), ['remove', 'mouse-tweaks']); assert.deepEqual(packwizArguments(parseArgs(['update', 'mouse-tweaks'])), ['update', 'mouse-tweaks']); assert.deepEqual(packwizArguments(parseArgs(['update', '--all'])), ['update', '--all']); }); test('add forwards literal search and yes while config selects project only', () => { const options = parseArgs(['add', 'some search $(literal)', '-y', '--config', 'platform/other.yaml']); assert.equal(options.configFile, 'platform/other.yaml'); assert.deepEqual(packwizArguments(options), ['--yes', 'modrinth', 'add', 'some search $(literal)']); const versionUrl = 'https://modrinth.com/mod/sophisticated-backpacks/version/pJxNzk4X'; assert.deepEqual(packwizArguments(parseArgs(['add', versionUrl])), ['modrinth', 'add', versionUrl]); }); test('invalid argument combinations fail before any tool invocation', () => { for (const args of [[], ['add'], ['remove'], ['update'], ['update', '--all', 'mod'], ['add', 'mod', '--all'], ['remove', 'mod', '--version-id', 'v'], ['build', '--yes'], ['add', 'mod', '--release'], ['add', 'mod', '--config'], ['update', '--all', '--all'], ['add', 'a', 'b'], ['build', '--config', '--release']]) { assert.throws(() => parseArgs(args)); } }); test('CLI preserves packwiz stdout/stderr/exit and configured cwd with separate argv', t => { const root = fs.mkdtempSync(path.join(os.tmpdir(), 'mcpack-command-')); t.after(() => fs.rmSync(root, { recursive: true, force: true })); for (const file of ['platform', 'scripts', 'package.json', 'toolchain.lock.env']) fs.cpSync(path.join(repository, file), path.join(root, file), { recursive: true }); fs.symlinkSync(path.join(repository, 'node_modules'), path.join(root, 'node_modules'), 'dir'); fs.mkdirSync(path.join(root, 'custom-pack')); fs.writeFileSync(path.join(root, 'custom-pack/pack.toml'), 'name = "fixture"\n'); const configuration = path.join(root, 'platform/config.yaml'); fs.writeFileSync(configuration, fs.readFileSync(configuration, 'utf8').replace('./pack', './custom-pack')); fs.mkdirSync(path.join(root, '.tools/packwiz'), { recursive: true }); const executable = path.join(root, '.tools/packwiz/packwiz'); const record = path.join(root, '.tools/record.json'); fs.writeFileSync(executable, `#!/usr/bin/env node import fs from 'node:fs'; fs.writeFileSync(${JSON.stringify(record)},JSON.stringify({cwd:process.cwd(),args:process.argv.slice(2)})); console.log('packwiz original stdout'); console.error('packwiz original stderr'); process.exit(17); `); fs.chmodSync(executable, 0o755); const sha256 = crypto.createHash('sha256').update(fs.readFileSync(executable)).digest('hex'); const lock = path.join(root, 'toolchain.lock.env'); fs.writeFileSync(lock, fs.readFileSync(lock, 'utf8').replace(/^PACKWIZ_BINARY_SHA256=.*$/m, `PACKWIZ_BINARY_SHA256=${sha256}`)); const result = spawnSync(process.execPath, [path.join(root, 'scripts/mcpack.mjs'), 'add', 'some search $(literal)', '--yes'], { cwd: os.tmpdir(), encoding: 'utf8' }); assert.equal(result.status, 17); assert.match(result.stdout, /packwiz original stdout/); assert.match(result.stderr, /packwiz original stderr/); assert(!result.stderr.includes('mcpack:')); const invocation = JSON.parse(fs.readFileSync(record)); assert.equal(invocation.cwd, path.join(root, 'custom-pack')); assert.deepEqual(invocation.args.slice(4), ['--yes', 'modrinth', 'add', 'some search $(literal)']); fs.appendFileSync(executable, '\n// tampered\n'); const rejected = spawnSync(process.execPath, [path.join(root, 'scripts/mcpack.mjs'), 'remove', 'fixture'], { encoding: 'utf8' }); assert.equal(rejected.status, 1); assert.match(rejected.stderr, /checksum mismatch/); }); test('backup and restore parse only their own complete arguments', () => { assert.equal(parseArgs(['backup']).waitTimeout, 120); assert.equal(parseArgs(['backup', '--target-release', '/release', '--wait-timeout', '30']).waitTimeout, 30); assert.equal(parseArgs(['restore', '--backup', '/snapshot', '--destination', '/new']).destination, '/new'); for (const args of [['backup', '--wait-timeout', '0'], ['backup', '--wait-timeout', 'NaN'], ['backup', '--release'], ['backup', '--destination', '/new'], ['restore'], ['restore', '--backup', '/snap'], ['restore', '--backup', '/snap', '--destination', '/new', '--wait-timeout', '3'], ['build', '--target-release', '/release']]) assert.throws(() => parseArgs(args)); });