import test from 'node:test'; import assert from 'node:assert/strict'; import * as fs from 'node:fs/promises'; import { execFile } from 'node:child_process'; import { promisify } from 'node:util'; import { realpath } from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import { createHash } from 'node:crypto'; import { packageGame, readRelease, SOURCE, verifyPackage } from '../scripts/package.mjs'; import { backupWorld, restoreWorld, verifyBackup } from '../scripts/backup.mjs'; import { absolute, atomicDirectory, disjoint, exists, inventory, MANIFEST, relativeName, withLauncherLock } from '../scripts/files.mjs'; import { assertEngineVersion, coordinates, engineExecutable, environmentSettings, fixtureSettings, graphicsSettings, parseEngineVersion, prepareCourse, runtimeHome, snapshotBeforePlay, validateEngine } from '../scripts/launcher.mjs'; import { writeWorld } from '../scripts/run.mjs'; import { isTerrainSetting, terrainSettings } from '../scripts/terrain.mjs'; const exec = promisify(execFile); const cli = path.join(SOURCE, 'scripts/run.mjs'); const binary = Buffer.from([0, 255, 254, 128, 10, 13, 0, 42]); async function temporary(t) { // macOS /var is normally a symlink; resolve the OS-owned temp root before // testing the deliberately symlink-rejecting filesystem API. const root = await fs.mkdtemp(path.join(await promisify(realpath)(os.tmpdir()), 'hearth-tooling-')); t.after(() => fs.rm(root, { recursive: true, force: true })); return root; } async function sourceTree(root) { const source = path.join(root, 'source'); await fs.mkdir(path.join(source, 'mods/hearth'), { recursive: true }); await fs.mkdir(path.join(source, 'tests/fixtures'), { recursive: true }); for (const filename of ['game.conf', 'minetest.conf', 'settingtypes.txt', 'LICENSE.txt', 'release.json', 'tests/fixtures/scenarios.json']) { await fs.copyFile(path.join(SOURCE, filename), path.join(source, filename)); } for (const name of ['init', 'model', 'runtime', 'content', 'encounters']) { await fs.writeFile(path.join(source, 'mods/hearth', name + '.lua'), '-- fixture #abcdef\n'); } await fs.writeFile(path.join(source, 'mods/hearth/mod.conf'), 'name = hearth\n'); await fs.writeFile(path.join(source, 'secret.env'), 'not part of payload\n'); await fs.writeFile(path.join(source, 'mods/hearth/data.png'), binary); await fs.mkdir(path.join(source, 'mods/hearth/textures')); await fs.writeFile(path.join(source, 'mods/hearth/textures/stale.png'), binary); return source; } async function worldTree(root) { const world = path.join(root, 'old-world'); await fs.mkdir(path.join(world, 'players/empty'), { recursive: true }); await fs.mkdir(path.join(world, 'copy')); await fs.writeFile(path.join(world, 'world.mt'), 'gameid = last_hearth\nbackend = sqlite3\n'); await fs.writeFile(path.join(world, 'map.sqlite'), binary); await fs.writeFile(path.join(world, 'players/player.bin'), binary); return world; } function options(root) { return { offline: true, runtime: path.join(root, 'runtime') }; } test('package twice: identical full inventory and manifest hashes, original assets, no engine', async t => { const root = await temporary(t), source = await sourceTree(root); const a = path.join(root, 'a'), b = path.join(root, 'b'); await packageGame(a, { source }); await packageGame(b, { source }); assert.deepEqual(await inventory(a), await inventory(b)); const hash = data => createHash('sha256').update(data).digest('hex'); assert.equal(hash(await fs.readFile(path.join(a, MANIFEST))), hash(await fs.readFile(path.join(b, MANIFEST)))); const manifest = await verifyPackage(a,{requireBundled:false}); assert.ok(manifest.entries.some(entry => entry.path === 'release.json')); for (const absent of ['scripts', 'tests', '.git', 'README.md', 'secret.env', 'mods/hearth/textures/stale.png']) assert.equal(await exists(path.join(a, absent)), false); assert.deepEqual(await fs.readFile(path.join(a, 'mods/hearth/data.png')), binary); assert.deepEqual((await fs.readFile(path.join(a, 'mods/hearth/textures/hearth_abcdef.png'))).subarray(0, 8), Buffer.from([137,80,78,71,13,10,26,10])); assert.match(await fs.readFile(path.join(a, 'mods/hearth/models/hearth_person.obj'), 'utf8'), /^# Original Last Hearth/); assert.equal((await readRelease(a)).engine.version, '5.18.0'); assert.equal((await readRelease(a)).version, '0.5.0-m4'); }); test('optional runtime fixture contract is copied verbatim', async t => { const root = await temporary(t), source = await sourceTree(root); const bytes = Buffer.from('{"owned_by":"runtime"}\n'); await fs.writeFile(path.join(source, 'tests/fixtures/config.json'), bytes); await fs.writeFile(path.join(source, 'tests/fixtures/first-session.json'), bytes); const destination = path.join(root, 'game'); await packageGame(destination, { source }); assert.deepEqual(await fs.readFile(path.join(destination, 'fixtures/config.json')), bytes); assert.deepEqual(await fs.readFile(path.join(destination, 'fixtures/first-session.json')), bytes); }); test('package refuses existing/unmanaged output without changing it', async t => { const root = await temporary(t), source = await sourceTree(root); const destination = path.join(root, 'shared'); await fs.mkdir(destination); await fs.writeFile(path.join(destination, 'keep'), binary); const before = await inventory(destination); await assert.rejects(packageGame(destination, { source }), /overwrite/); assert.deepEqual(await inventory(destination), before); await assert.rejects(packageGame(path.join(source, 'mods/output'), { source }), /overlap/); }); test('atomic build failure leaves no destination, staging directory or guard', async t => { const root = await temporary(t), destination = path.join(root, 'new'); await assert.rejects(atomicDirectory(destination, async stage => { await fs.writeFile(path.join(stage, 'partial'), binary); throw new Error('injected failure'); }), /injected/); assert.deepEqual(await fs.readdir(root), []); }); test('package manifest rejects missing, extra, corrupted files and extra empty dirs', async t => { const root = await temporary(t), source = await sourceTree(root); for (const kind of ['missing', 'extra', 'corrupt', 'directory']) { const destination = path.join(root, kind); await packageGame(destination, { source }); if (kind === 'missing') await fs.unlink(path.join(destination, 'game.conf')); if (kind === 'extra') await fs.writeFile(path.join(destination, 'extra'), binary); if (kind === 'corrupt') await fs.appendFile(path.join(destination, 'game.conf'), 'bad'); if (kind === 'directory') await fs.mkdir(path.join(destination, 'empty-extra')); await assert.rejects(verifyPackage(destination,{requireBundled:false}), /manifest mismatch/); } }); test('offline full backup/restore preserves binary bytes, directories and original world', async t => { const root = await temporary(t), world = await worldTree(root); const before = await inventory(world), backup = path.join(root, 'snapshot'), restored = path.join(root, 'restored'); await backupWorld(world, backup, options(root)); await verifyBackup(backup); await restoreWorld(backup, restored, options(root)); assert.deepEqual(await inventory(world), before); assert.deepEqual(await inventory(restored), before); assert.deepEqual(await fs.readFile(path.join(restored, 'map.sqlite')), binary); assert.equal(await exists(path.join(restored, MANIFEST)), false); await assert.rejects(restoreWorld(backup, world, options(root)), /new world/); await assert.rejects(backupWorld(world, backup, options(root)), /overwrite/); assert.deepEqual(await inventory(world), before); }); test('restore rejects corruption, missing/extra files, empty dirs and unsafe manifest paths atomically', async t => { const root = await temporary(t), world = await worldTree(root), before = await inventory(world); for (const kind of ['corrupt', 'missing', 'extra', 'directory', 'traversal']) { const backup = path.join(root, 'backup-' + kind), restored = path.join(root, 'restore-' + kind); await backupWorld(world, backup, options(root)); if (kind === 'corrupt') await fs.writeFile(path.join(backup, 'world/map.sqlite'), Buffer.from('broken')); if (kind === 'missing') await fs.unlink(path.join(backup, 'world/map.sqlite')); if (kind === 'extra') await fs.writeFile(path.join(backup, 'extra'), binary); if (kind === 'directory') await fs.mkdir(path.join(backup, 'unexpected')); if (kind === 'traversal') { const filename = path.join(backup, MANIFEST), manifest = JSON.parse(await fs.readFile(filename, 'utf8')); manifest.entries[0].path = '../escape'; await fs.writeFile(filename, JSON.stringify(manifest)); } await assert.rejects(restoreWorld(backup, restored, options(root))); assert.equal(await exists(restored), false); assert.equal(await exists(restored + '.hearth-write-lock'), false); } assert.deepEqual(await inventory(world), before); }); test('offline confirmation and own launcher lock required; active lease permits internal snapshot', async t => { const root = await temporary(t), world = await worldTree(root), opt = options(root); await assert.rejects(backupWorld(world, path.join(root, 'no-confirmation'), { runtime: opt.runtime }), /offline/); await withLauncherLock(opt.runtime, async lease => { await assert.rejects(backupWorld(world, path.join(root, 'blocked'), opt), /Launcher lock/); await assert.rejects(backupWorld(world, path.join(root, 'forged'), { ...opt, lease: { runtime: opt.runtime } }), /Launcher lock/); await backupWorld(world, path.join(root, 'internal'), { ...opt, lease }); }); assert.equal(await exists(path.join(opt.runtime, 'launcher.lock')), false); }); test('world lock files and SQLite sidecars fail closed, env_meta remains normal data', async t => { const root = await temporary(t), world = await worldTree(root); await fs.writeFile(path.join(world, 'env_meta.txt'), 'normal engine metadata'); for (const name of ['world.lock', '.lock', 'world.mt.lock', 'map.sqlite-wal', 'players.sqlite-shm', 'auth.sqlite-journal']) { await fs.writeFile(path.join(world, name), ''); await assert.rejects(backupWorld(world, path.join(root, 'blocked'), options(root)), /lock or sidecar/); await fs.unlink(path.join(world, name)); } await backupWorld(world, path.join(root, 'allowed'), options(root)); }); test('every managed play snapshots existing world without deleting prior snapshots or world data', async t => { const root = await temporary(t), world = await worldTree(root), runtime = path.join(root, 'runtime'); await fs.mkdir(path.join(runtime, 'worlds'), { recursive: true }); const managed = path.join(runtime, 'worlds/last_hearth'); await fs.rename(world, managed); const original = await inventory(managed); await withLauncherLock(runtime, async lease => { const first = await snapshotBeforePlay(runtime, lease); await fs.appendFile(path.join(managed, 'world.mt'), 'some_new_setting = true\n'); const second = await snapshotBeforePlay(runtime, lease); assert.notEqual(first, second); assert.deepEqual(await inventory(path.join(first, 'world')), original); assert.deepEqual(await inventory(path.join(second, 'world')), await inventory(managed)); assert.equal((await fs.readdir(path.join(runtime, 'backups'))).length, 2); await verifyBackup(first); await verifyBackup(second); }); await restoreWorld((await fs.readdir(path.join(runtime, 'backups'))).map(name => path.join(runtime, 'backups', name)).sort()[0], path.join(root, 'restored-before-migration'), options(root)); assert.deepEqual(await inventory(path.join(root, 'restored-before-migration')), original); }); test('new play world is not snapshotted; nonempty unmanaged world fails closed', async t => { const root = await temporary(t), runtime = path.join(root, 'runtime'); await withLauncherLock(runtime, async lease => { assert.equal(await snapshotBeforePlay(runtime, lease), undefined); assert.equal(await exists(path.join(runtime, 'backups')), false); await fs.mkdir(path.join(runtime, 'worlds/last_hearth'), { recursive: true }); await fs.writeFile(path.join(runtime, 'worlds/last_hearth/keep'), binary); await assert.rejects(snapshotBeforePlay(runtime, lease), /unmanaged/); assert.deepEqual(await fs.readFile(path.join(runtime, 'worlds/last_hearth/keep')), binary); }); }); test('symlink/junction input, parent and destination rejected', async t => { const root = await temporary(t), source = await sourceTree(root), world = await worldTree(root); const outside = path.join(root, 'outside'); await fs.mkdir(outside); const link = path.join(root, 'linked'); // Windows junctions do not require developer mode or administrator privileges. await fs.symlink(outside, link, process.platform === 'win32' ? 'junction' : 'dir'); await assert.rejects(packageGame(path.join(link, 'game'), { source }), /link|junction/); await assert.rejects(packageGame(link, { source }), /link|junction/); await fs.symlink(outside, path.join(source, 'mods/hearth/escape'), process.platform === 'win32' ? 'junction' : 'dir'); await assert.rejects(packageGame(path.join(root, 'game'), { source }), /link|junction/); await fs.symlink(outside, path.join(world, 'escape'), process.platform === 'win32' ? 'junction' : 'dir'); await assert.rejects(backupWorld(world, path.join(root, 'snapshot'), options(root)), /link|junction/); assert.deepEqual(await fs.readdir(outside), []); }); test('absolute paths, portable names and overlap validation reject traversal', () => { for (const name of ['../escape', '/absolute', 'a\\b', 'a/../b', 'a//b', 'C:ads', 'CON.txt', 'x.']) assert.throws(() => relativeName(name)); assert.throws(() => absolute('relative')); assert.throws(() => absolute(path.join(os.tmpdir(), 'a') + path.sep + '..' + path.sep + 'b')); assert.throws(() => disjoint(os.tmpdir(), path.join(os.tmpdir(), 'world'))); }); test('runtime defaults preserve legacy Windows world path and support HEARTH_HOME on all platforms', () => { const home = path.resolve(os.tmpdir(), 'person'), local = path.join(home, 'Local'); const legacy = runtimeHome({ LOCALAPPDATA: local }, 'win32', home); assert.equal(legacy, path.join(local, 'last-hearth-runtime')); assert.equal(engineExecutable({ LOCALAPPDATA: local }, 'win32'), path.join(legacy, 'luanti-5.18.0-win64/bin/luanti.exe')); for (const platform of ['win32', 'linux', 'darwin']) { assert.equal(runtimeHome({ HEARTH_HOME: home }, platform), home); assert.equal(engineExecutable({ HEARTH_ENGINE: process.execPath }, platform, home), process.execPath); } assert.match(runtimeHome({}, 'darwin', home), /Application Support/); assert.equal(runtimeHome({ XDG_DATA_HOME: local }, 'linux', home), path.join(local, 'last-hearth-runtime')); assert.throws(() => engineExecutable({}, 'linux', home), /HEARTH_ENGINE/); assert.throws(() => engineExecutable({ HEARTH_ENGINE: 'relative' }, 'linux', home), /absolute/); }); test('engine version parsing is exact and actual non-engine executable fails closed', async () => { assert.equal(parseEngineVersion('Luanti 5.18.0 (Windows)\nUsing LuaJIT\n'), '5.18.0'); assert.equal(parseEngineVersion('Luanti 5.19.0\n'), '5.19.0'); assert.equal(assertEngineVersion('Luanti 5.18.0 (Windows)\n'), '5.18.0'); for (const version of ['5.17.0', '5.19.0', '5.18.1']) assert.throws(() => assertEngineVersion(`Luanti ${version}\n`), /version mismatch/); for (const text of ['Luanti 5.18.0-dev', 'Minetest 5.18.0', 'prefix Luanti 5.18.0', 'Luanti 5.18.00x', 'v24.0.0']) assert.throws(() => parseEngineVersion(text)); await assert.rejects(validateEngine(process.execPath), /Unrecognized Luanti/); }); test('fixture contract: first-session real clock, summer/winter fixed UTC/location/weather, production rejects', async () => { assert.deepEqual(await fixtureSettings('first-session', 'local'), ['fixed_map_seed = 731104']); for (const [name, utc, weather] of [['summer', 1782043200, 'clear'], ['winter', 1797854400, 'rain']]) { const settings = await fixtureSettings(name, 'staging'); for (const expected of [`hearth_preview_date = ${utc}`, `hearth_preview_weather = ${weather}`, 'hearth_latitude = 45', 'hearth_longitude = 0']) assert.ok(settings.includes(expected)); } await assert.rejects(fixtureSettings('summer', 'production'), /Production/); await assert.rejects(fixtureSettings('__proto__', 'local'), /Unknown/); assert.deepEqual(environmentSettings(), ['hearth_environment = production']); assert.deepEqual(environmentSettings('staging', ['hearth_launcher_mode = smoke']), ['hearth_environment = staging', 'hearth_launcher_mode = smoke']); for (const line of ['hearth_preview_date = 1', 'hearth_preview_weather = clear', 'hearth_launcher_mode = smoke', 'hearth_smoke_token = token']) assert.throws(() => environmentSettings('production', [line]), /Production/); assert.throws(() => environmentSettings('local', ['a = 1\nb = 2']), /Invalid/); assert.throws(() => coordinates({ HEARTH_LATITUDE: '45\nfoo=bar', HEARTH_LONGITUDE: '0' })); }); test('release and npm commands retain M0-M3 tooling entry points', async () => { const pkg = JSON.parse(await fs.readFile(path.join(SOURCE, 'package.json'), 'utf8')); assert.equal(pkg.version, '0.5.0-m4'); assert.equal((await readRelease()).version, pkg.version); assert.equal(pkg.scripts.course, 'node scripts/run.mjs course'); assert.equal(pkg.scripts['test:course'], 'node scripts/run.mjs test:course'); assert.equal(pkg.scripts['test:seeds'], 'node scripts/run.mjs test:seeds'); assert.equal(pkg.scripts.test, 'node --test tests/*.test.mjs'); assert.equal(pkg.scripts.play, 'node scripts/run.mjs play'); assert.equal(pkg.scripts['test:recovery'], 'node scripts/recovery.mjs'); }); test('course creates only fresh stage/course with matching exclusive UUID marker and summer fixture', async t => { const root = await temporary(t), world = await worldTree(root); const original = await inventory(world); const stage = path.join(root, 'session'), secondStage = path.join(root, 'second-session'); await fs.mkdir(stage); await fs.mkdir(secondStage); const first = await prepareCourse(stage); const second = await prepareCourse(secondStage, { environment: 'staging' }); assert.equal(first.world, path.join(stage, 'course')); assert.equal(path.basename(second.world), 'course'); assert.match(first.nonce, /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/); assert.notEqual(first.nonce, second.nonce); assert.deepEqual(first.settings, [...await fixtureSettings('summer', 'local'), 'hearth_m1_course = true', `hearth_m1_course_nonce = ${first.nonce}`]); assert.deepEqual(environmentSettings('local', first.settings).filter(line => /^hearth_environment/.test(line)), ['hearth_environment = local']); for (const entry of [first, second]) { assert.deepEqual(await fs.readdir(entry.world), ['.hearth-m1-course-nonce']); assert.equal(await fs.readFile(path.join(entry.world, '.hearth-m1-course-nonce'), 'utf8'), entry.nonce + '\n'); assert.ok(entry.settings.includes('hearth_preview_date = 1782043200')); assert.ok(entry.settings.includes('hearth_preview_weather = clear')); assert.ok(entry.settings.includes('hearth_latitude = 45')); assert.ok(entry.settings.includes('hearth_longitude = 0')); } const before = await inventory(stage); await assert.rejects(prepareCourse(stage), /overwrite/); assert.deepEqual(await inventory(stage), before); assert.deepEqual(await inventory(world), original); assert.equal(await exists(path.join(root, 'backups')), false); }); test('course refuses existing empty/unmanaged worlds, linked stages, and production without mutation', async t => { const root = await temporary(t), stage = path.join(root, 'session'); await fs.mkdir(path.join(stage, 'course'), { recursive: true }); await assert.rejects(prepareCourse(stage), /overwrite/); await fs.writeFile(path.join(stage, 'course/keep'), binary); const before = await inventory(stage); await assert.rejects(prepareCourse(stage), /overwrite/); for (const environment of ['production', 'invalid']) await assert.rejects(prepareCourse(stage, { environment }), /local\/staging/); assert.deepEqual(await inventory(stage), before); const link = path.join(root, 'linked-stage'); await fs.symlink(stage, link, process.platform === 'win32' ? 'junction' : 'dir'); await assert.rejects(prepareCourse(link), /link|junction/); assert.deepEqual(await inventory(stage), before); }); test('production rejects presence of all course, recovery, debug and launcher settings, including false/empty', () => { for (const key of ['hearth_m1_course', 'hearth_m1_course_nonce', 'hearth_m1_course_snag', 'hearth_m1_course_future', 'hearth_recovery_phase', 'hearth_recovery_token', 'hearth_recovery_gameid', 'hearth_debug', 'hearth_debug_future', 'hearth_course_probe_token', 'hearth_launcher_mode', 'hearth_smoke_token']) { for (const value of ['', 'false', '0', 'test']) { const line = ` ${key} = ${value}`; assert.throws(() => environmentSettings('production', [line]), /Production/, key); assert.deepEqual(environmentSettings('staging', [line]), ['hearth_environment = staging', line]); } } assert.deepEqual(environmentSettings('production', ['hearth_latitude = 45', 'hearth_longitude = 0']), ['hearth_environment = production', 'hearth_latitude = 45', 'hearth_longitude = 0']); }); test('CLI package/verify work with absent engine and invalid runtime env; backup/restore offline CLI', async t => { const root = await temporary(t), destination = path.join(root, 'game'); const env = { ...process.env, HEARTH_ENGINE: 'not-an-absolute-engine', HEARTH_HOME: 'not-a-runtime' }; const packaged = await exec(process.execPath, [cli, 'package', destination], { env, timeout: 90_000 }); assert.match(packaged.stdout, /PACKAGE_PASS/); const verified = await exec(process.execPath, [cli, 'verify', destination], { env, timeout: 30_000 }); assert.match(verified.stdout, /PACKAGE_VERIFY_PASS/); const world = await worldTree(root), backup = path.join(root, 'backup'), restored = path.join(root, 'restored'); env.HEARTH_HOME = path.join(root, 'runtime'); await exec(process.execPath, [cli, 'backup', world, backup, '--offline'], { env, timeout: 30_000 }); await exec(process.execPath, [cli, 'restore', backup, restored, '--offline'], { env, timeout: 30_000 }); assert.deepEqual(await inventory(world), await inventory(restored)); await assert.rejects(exec(process.execPath, [cli, 'backup', world, path.join(root, 'unconfirmed')], { env, timeout: 10_000 }), /OFFLINE/); // Version check happens before creating any runtime data or touching the world. env.HEARTH_ENGINE = process.execPath; const before = await inventory(world); await assert.rejects(exec(process.execPath, [cli, 'play'], { env, timeout: 10_000 }), /Unrecognized Luanti/); for (const command of ['course', 'test:course']) { await assert.rejects(exec(process.execPath, [cli, command], { env: { ...env, HEARTH_FIXTURE: 'winter', HEARTH_LATITUDE: 'invalid' }, timeout: 10_000, }), /Unrecognized Luanti/); // Course ignores inherited fixture/coordinates, but never engine pinning. } assert.deepEqual(await inventory(world), before); assert.deepEqual(await fs.readdir(env.HEARTH_HOME), []); }); test('new-world terrain preset is pure, explicit, bounded and matches packaged game defaults', async () => { const first = terrainSettings(true); const expected = [ 'mg_name = v7', 'fixed_map_seed = 731104', 'water_level = 32', 'mapgen_limit = 4096', 'mg_flags = caves,nodungeons,light,biomes,ores', 'mgv7_spflags = mountains,ridges,nofloatlands,caverns', 'mgv7_mount_zero_level = 0', 'mgv7_np_terrain_base = 12, 45, (900, 384, 900), 82341, 5, 0.6, 2.0', 'mgv7_np_terrain_alt = -8, 24, (900, 384, 900), 5934, 5, 0.6, 2.0', 'mgv7_np_terrain_persist = 0.6, 0.1, (1200, 1200, 1200), 539, 3, 0.6, 2.0', 'mgv7_np_height_select = -4, 14, (1400, 320, 1400), 4213, 6, 0.7, 2.0', 'mgv7_np_filler_depth = 0, 1.2, (150, 150, 150), 261, 3, 0.7, 2.0', 'mgv7_np_mount_height = 250, 100, (700, 700, 700), 72449, 3, 0.6, 2.0', 'mgv7_np_mountain = -0.6, 1, (320, 280, 320), 5333, 5, 0.63, 2.0', 'mgv7_np_ridge_uwater = 0, 1, (700, 700, 700), 85039, 5, 0.6, 2.0', 'mgv7_np_ridge = 0, 1, (100, 100, 100), 6467, 4, 0.75, 2.0', 'mg_biome_np_heat = 50, 50, (256, 256, 256), 5349, 3, 0.5, 2.0', 'mg_biome_np_humidity = 50, 50, (256, 256, 256), 842, 3, 0.5, 2.0', 'epic_terrain_enable = true', ]; assert.deepEqual(first, expected); assert.deepEqual(terrainSettings(false), []); assert.throws(() => terrainSettings('true'), /boolean/); assert.ok(first.includes('mapgen_limit = 4096')); assert.ok(first.includes('mgv7_spflags = mountains,ridges,nofloatlands,caverns')); assert.ok(first.includes('mgv7_np_mount_height = 250, 100, (700, 700, 700), 72449, 3, 0.6, 2.0')); const defaults = (await fs.readFile(path.join(SOURCE, 'minetest.conf'), 'utf8')) .split(/\r?\n/).filter(isTerrainSetting).filter(line => !line.startsWith('epic_terrain_enable')).sort(); assert.deepEqual(first.filter(line => !line.startsWith('fixed_map_seed') && !line.startsWith('epic_terrain_enable')).sort(), defaults); assert.equal(new Set(first.map(line => line.split('=')[0].trim())).size, first.length); first[0] = 'mg_name = flat'; first.push('untrusted = true'); assert.equal(terrainSettings(true)[0], 'mg_name = v7'); for (const line of ['mg_name = v7', ' mgv7_np_terrain_base = 1', 'mg_biome_np_heat = 1', 'epic_terrain_enable = true', 'fixed_map_seed = 42', 'mapgen_limit = 512', 'water_level = 1']) assert.ok(isTerrainSetting(line), line); assert.equal(isTerrainSetting('hearth_latitude = 45'), false); }); test('world creation writes terrain once; subsequent ephemeral configs never retune or reseed saved worlds', async t => { const root = await temporary(t), world = path.join(root, 'world'), config = path.join(root, 'first.conf'); await writeWorld(world, config, ['hearth_latitude = 45', 'hearth_longitude = 0'], ['fixed_map_seed = 42'], 'local'); const first = await fs.readFile(config, 'utf8'); for (const line of terrainSettings(true)) assert.ok(first.includes(line + '\n'), line); assert.ok(first.indexOf('fixed_map_seed = 42') > first.indexOf('fixed_map_seed = 731104')); const worldFile = await fs.readFile(path.join(world, 'world.mt')); // An old world may have an entirely different mapgen, seed and boundary. const saved = Buffer.from('mg_name = valleys\nseed = 99\nmapgen_limit = 512\n[end_of_params]\n'); await fs.writeFile(path.join(world, 'map_meta.txt'), saved); const before = await inventory(world); const secondConfig = path.join(root, 'second.conf'); await writeWorld(world, secondConfig, ['hearth_latitude = 45'], [ ...await fixtureSettings('first-session', 'local'), 'mg_name = flat', 'mgv7_np_mountain = 1', 'water_level = 30', 'mapgen_limit = 4096', ], 'local'); const second = await fs.readFile(secondConfig, 'utf8'); assert.deepEqual(second.split(/\r?\n/).filter(isTerrainSetting), ['epic_terrain_enable = false']); assert.match(second, /hearth_latitude = 45/); assert.match(second, /secure.enable_security = true/); assert.deepEqual(await fs.readFile(path.join(world, 'world.mt')), worldFile); assert.deepEqual(await fs.readFile(path.join(world, 'map_meta.txt')), saved); assert.deepEqual(await inventory(world), before); await assert.rejects(writeWorld(world, secondConfig, [], [], 'local'), /EEXIST/); assert.deepEqual(await inventory(world), before); }); test('world.mt without map metadata and orphan map metadata each suppress creation overrides', async t => { const root = await temporary(t); for (const name of ['world.mt', 'map_meta.txt']) { const world = path.join(root, name === 'world.mt' ? 'legacy' : 'orphan'); await fs.mkdir(world); const bytes = Buffer.from(name === 'world.mt' ? 'gameid = last_hearth\n' : 'seed = 123\n[end_of_params]\n'); await fs.writeFile(path.join(world, name), bytes); const config = path.join(root, path.basename(world) + '.conf'); await writeWorld(world, config, [], ['fixed_map_seed = 731104'], 'local'); assert.deepEqual((await fs.readFile(config, 'utf8')).split(/\r?\n/).filter(isTerrainSetting), ['epic_terrain_enable = false']); assert.deepEqual(await fs.readFile(path.join(world, name)), bytes); } }); test('graphics preset defaults to 512 nodes with bounded full-detail send/generate distances', () => { const expected = distance => [ `viewing_range = ${distance}`, `max_block_send_distance = ${Math.ceil(distance / 16) + 2}`, `max_block_generate_distance = ${Math.ceil(distance / 16) + 2}`, 'client_mesh_chunk = 2', 'fog_start = 0.65', ]; assert.deepEqual(graphicsSettings({}), expected(512)); for (let distance = 128; distance <= 1024; distance++) { const settings = graphicsSettings({ HEARTH_VIEW_DISTANCE: String(distance) }); assert.deepEqual(settings, expected(distance)); assert.ok(Number(settings[1].split('=')[1]) <= 66); assert.equal(new Set(settings.map(line => line.split('=')[0].trim())).size, settings.length); assert.deepEqual(settings.filter(isTerrainSetting), []); assert.ok(settings.every(line => !/active|activation|culling|occlusion|simulation/.test(line))); } const changed = graphicsSettings({}); changed[0] = 'viewing_range = 1'; changed.push('untrusted = true'); assert.deepEqual(graphicsSettings({}), expected(512)); }); const invalidViewDistances = ['', '127', '1025', '4000', '0', '-512', '+512', '0512', '512.0', '5e2', '0x200', 'NaN', 'Infinity', ' 512', '512 ', '512\n', '512\r', '512\nactive_block_range = 100', '512\r\nsecure.enable_security = false', '512; enable_occlusion_culling = false', '512', '9'.repeat(1000), null, 512, true, {}]; test('graphics rejects malformed, injected and out-of-range values before any world/config write', async t => { const root = await temporary(t), existing = await worldTree(root); const before = await inventory(existing); for (const value of invalidViewDistances) { const env = { HEARTH_VIEW_DISTANCE: value }; assert.throws(() => graphicsSettings(env), /HEARTH_VIEW_DISTANCE.*128.*1024/); for (const world of [path.join(root, 'absent'), existing]) { await assert.rejects(writeWorld(world, path.join(root, 'invalid.conf'), [], [], 'local', 'last_hearth', env), /HEARTH_VIEW_DISTANCE.*128.*1024/); } assert.equal(await exists(path.join(root, 'absent')), false); assert.equal(await exists(path.join(root, 'invalid.conf')), false); assert.deepEqual(await inventory(existing), before); } }); test('play/course/test configs include unique graphics keys for new and existing worlds without mapgen rewrite', async t => { const root = await temporary(t); for (const mode of ['play', 'course', 'test']) { for (const distance of [512, 768, 1024]) { for (const existing of [false, true]) { const directory = path.join(root, `${mode}-${distance}-${existing}`); await fs.mkdir(directory); const world = path.join(directory, 'world'), config = path.join(directory, `${mode}.conf`); if (existing) { await fs.mkdir(world); await fs.writeFile(path.join(world, 'world.mt'), 'gameid = last_hearth\nbackend = sqlite3\n'); await fs.writeFile(path.join(world, 'map_meta.txt'), 'mg_name = valleys\nseed = 99\nmapgen_limit = 512\n[end_of_params]\n'); await fs.writeFile(path.join(world, 'map.sqlite'), binary); } const before = existing ? await inventory(world) : undefined; const env = distance === 512 ? {} : { HEARTH_VIEW_DISTANCE: String(distance) }; const extra = mode === 'course' ? [...await fixtureSettings('summer', 'local'), 'hearth_m1_course = true'] : mode === 'test' ? ['hearth_launcher_mode = check', 'max_users = 0'] : []; await writeWorld(world, config, [], extra, mode === 'test' ? 'staging' : 'local', 'last_hearth', env); const lines = (await fs.readFile(config, 'utf8')).split(/\r?\n/); for (const setting of graphicsSettings(env)) { assert.ok(lines.includes(setting), setting); const key = setting.split('=')[0].trim(); assert.equal(lines.filter(line => line.split('=')[0].trim() === key).length, 1, key); } assert.ok(lines.includes('secure.enable_security = true')); if (existing) { assert.deepEqual(lines.filter(isTerrainSetting), ['epic_terrain_enable = false']); assert.deepEqual(await inventory(world), before); } else { for (const setting of terrainSettings(true)) assert.ok(lines.includes(setting), setting); } } } } }); test('managed graphics overrides cannot duplicate preset keys or bypass the environment bounds', async t => { const root = await temporary(t), world = path.join(root, 'world'), config = path.join(root, 'play.conf'); for (const line of graphicsSettings({})) { for (const location of [false, true]) { await assert.rejects(writeWorld(world, config, location ? [` ${line}`] : [], location ? [] : [` ${line}`], 'local', 'last_hearth', {}), /launcher-managed/); assert.equal(await exists(world), false); assert.equal(await exists(config), false); } } }); test('CLI validates graphics before any engine check or runtime/world writes in every launch mode', async t => { const root = await temporary(t), runtime = path.join(root, 'runtime'); const env = { ...process.env, HEARTH_HOME: runtime, HEARTH_ENGINE: process.execPath, HEARTH_VIEW_DISTANCE: '512\nsecure.enable_security = false' }; for (const mode of ['play', 'course', 'test:course', 'check', 'smoke', 'test', 'test:seeds']) { await assert.rejects(exec(process.execPath, [cli, mode], { env, timeout: 10_000 }), error => { assert.equal(error.code, 1); assert.match(error.stderr, /HEARTH_VIEW_DISTANCE.*128.*1024/); assert.doesNotMatch(error.stderr, /Unrecognized Luanti/); return true; }); assert.equal(await exists(runtime), false); } }); // Node-only tooling regression suite: never opens a GUI or the original world.