'use strict'; const test = require('node:test'); const assert = require('node:assert/strict'); const fs = require('node:fs'); const os = require('node:os'); const path = require('node:path'); const { run } = require('../src/cli'); class Sink { constructor() { this.chunks = []; } write(s) { this.chunks.push(s); } get text() { return this.chunks.join(''); } } const FIX_ATLAS = path.join(__dirname, 'fixtures', 'atlases'); const FIX_DSL = path.join(__dirname, 'fixtures', 'dsl'); function tmpOut(name) { return path.join(os.tmpdir(), `${name}.json`); } test('build: minimal DSL → valid v2 JSON file', () => { const out = new Sink(); const err = new Sink(); const outPath = tmpOut('minimal'); try { const code = run(['build', path.join(FIX_DSL, 'minimal.txt'), outPath, '--atlas-dir', FIX_ATLAS], { out, err }); assert.equal(code, 0, err.text); const json = JSON.parse(fs.readFileSync(outPath, 'utf8')); assert.equal(json.schema_version, 2); assert.equal(json.id, 'minimal'); // derived from out filename assert.deepEqual(json.size, { w: 4, h: 4 }); assert.deepEqual(json.atlases, ['atlas_a']); assert.equal(json.layers.surface.tiles.length, 16); } finally { if (fs.existsSync(outPath)) fs.unlinkSync(outPath); } }); test('build: full DSL with id directive overrides filename-derived id', () => { const out = new Sink(); const err = new Sink(); const outPath = tmpOut('full'); try { const code = run(['build', path.join(FIX_DSL, 'full.txt'), outPath, '--atlas-dir', FIX_ATLAS], { out, err }); assert.equal(code, 0, err.text); const json = JSON.parse(fs.readFileSync(outPath, 'utf8')); assert.equal(json.id, 'full_demo'); assert.deepEqual(Object.keys(json.layers).sort(), ['surface', 'wall']); assert.equal(json.roof.length, 16); } finally { if (fs.existsSync(outPath)) fs.unlinkSync(outPath); } }); test('build: out-of-bounds DSL reports file:line on stderr, exit 1', () => { const out = new Sink(); const err = new Sink(); const outPath = tmpOut('err'); const inPath = path.join(FIX_DSL, 'error_oob.txt'); const code = run(['build', inPath, outPath, '--atlas-dir', FIX_ATLAS], { out, err }); assert.equal(code, 1); assert.match(err.text, /error_oob\.txt:4:/); assert.match(err.text, /out of bounds/); // Output file must NOT exist assert.equal(fs.existsSync(outPath), false); }); test('build: missing --atlas-dir for DSL that needs atlases → error', () => { const out = new Sink(); const err = new Sink(); const outPath = tmpOut('noatlas'); const code = run(['build', path.join(FIX_DSL, 'minimal.txt'), outPath, '--atlas-dir', '/no/such/dir'], { out, err }); assert.equal(code, 1); assert.match(err.text, /error/i); if (fs.existsSync(outPath)) fs.unlinkSync(outPath); }); test('build: insufficient positional args prints usage, exit 1', () => { const out = new Sink(); const err = new Sink(); const code = run(['build', 'only_one_arg.txt'], { out, err }); assert.equal(code, 1); assert.match(err.text, /usage: sporel-mapper build/); }); test('build: duplicate atlas across --atlas-dir paths warns on stderr but exits 0', () => { // Use both fixture dirs (atlases + atlases-dup) const FIX_DUP = path.join(__dirname, 'fixtures', 'atlases-dup'); const out = new Sink(); const err = new Sink(); const outPath = tmpOut('dup'); try { const code = run( ['build', path.join(FIX_DSL, 'minimal.txt'), outPath, '--atlas-dir', FIX_ATLAS, '--atlas-dir', FIX_DUP], { out, err } ); assert.equal(code, 0, err.text); assert.match(err.text, /warning.*atlas_a.*duplicate/i); } finally { if (fs.existsSync(outPath)) fs.unlinkSync(outPath); } });