'use strict';
// These cover issues independently reproduced by the real-tool audit.
const test = require('node:test');
const H = require('./public-cli-helper.cjs');
function rawAction(nodes, exports = '', extra = '') {
return `
${exports}
${nodes}
${extra}
`;
}
test('public build identifies a sound character payload not exported by cc3tools', () => {
const work = H.testCase('unsupported-sound');
const source = H.expanded(work, rawAction('', '', ''));
const rebuilt = H.build(work, source.xml);
H.assert.notEqual(rebuilt.result.status, 0);
H.assert.match(rebuilt.result.stderr, /cc3tools did not export the sound character payload \(code 6\)/);
});
test('public roundtrip retains an unlabelled button-action flag bit', () => {
const work = H.testCase('opaque-button-flag');
const button = '';
const source = H.expanded(work, rawAction('', '', button));
H.assert.equal(H.element(source.xml, 'buttonaction', 'keycode', 0).flags, '0x40');
const rebuilt = H.build(work, source.xml);
H.succeeded(rebuilt.result);
H.assert.equal(Number(H.element(rebuilt.raw, 'buttonaction', 'keycode', 0).flags), 64);
});
test('public action-string roundtrip retains quotes, comma, backslash and &Outline literally', () => {
const work = H.testCase('quoted-action');
const literal = 'say "Q", &Outline \\literal';
const source = H.expanded(work, rawAction(''));
H.assert.match(source.xml, / {
const work = H.testCase('entity-literal');
const source = H.expanded(work, rawAction(''));
const rebuilt = H.build(work, source.xml);
H.succeeded(rebuilt.result);
H.assert.equal(H.element(rebuilt.raw, 'string', 'action', 161).str, '" & &Outline');
});
test('public action-string roundtrip preserves a literal CDATA closing delimiter', () => {
const work = H.testCase('cdata-delimiter');
const source = H.expanded(work, rawAction(''));
const rebuilt = H.build(work, source.xml);
H.succeeded(rebuilt.result);
H.assert.equal(H.element(rebuilt.raw, 'string', 'action', 161).str, ']]>');
});
test('public action-string roundtrip retains actual LF and CR rather than replacing them with slash letters', () => {
const work = H.testCase('newlines');
const source = H.expanded(work, rawAction(''));
const rebuilt = H.build(work, source.xml);
H.succeeded(rebuilt.result);
H.assert.equal(H.element(rebuilt.raw, 'string', 'action', 161).str, 'before\nafter\rlast');
});
test('public geturl roundtrip does not split a comma inside an escaped quoted argument', () => {
const work = H.testCase('geturl-comma');
const source = H.expanded(work, rawAction(''));
const rebuilt = H.build(work, source.xml);
H.succeeded(rebuilt.result);
const url = H.element(rebuilt.raw, 'geturl', 'action', 131);
H.assert.equal(url.str1, 'x"y,z?a&b');
H.assert.equal(url.str2, 'target"room');
});
test('public constant pooling accepts constructor/__proto__/toString as ordinary string values', () => {
const work = H.testCase('constant-object-keys');
const source = H.expanded(work, rawAction(`
`));
const rebuilt = H.build(work, source.xml);
H.succeeded(rebuilt.result);
H.assert.deepEqual(H.constRecords(rebuilt.output).map(record => record.value).sort(), ['__proto__', 'constructor', 'toString']);
for (const value of ['constructor', '__proto__', 'toString']) {
H.assert.equal(H.element(rebuilt.raw, 'constant', 'str', value).str, value);
}
});
test('public build writes Chinese string constants with UTF-8 byte lengths and distinct aligned pointers', () => {
const work = H.testCase('utf8-pool');
const source = H.expanded(work, rawAction(`
`));
const rebuilt = H.build(work, source.xml);
H.succeeded(rebuilt.result);
const records = H.constRecords(rebuilt.output);
H.assert.deepEqual(records.map(record => record.value).sort(), ['ASCII&Outline', '网络标题']);
for (const record of records) H.assert.equal(record.offset % 4, 0);
const byOffset = records.slice().sort((a, b) => a.offset - b.offset);
H.assert.ok(byOffset[1].offset >= byOffset[0].offset + Buffer.byteLength(byOffset[0].value, 'utf8') + 1);
});
test('public roundtrip writes the actual type-7 -1 sentinel as 0xffffffff without RangeError', () => {
const work = H.testCase('signed-integer-pool');
const source = H.expanded(work, rawAction(`
`));
H.assert.match(source.xml, /pushconst -1/);
const rebuilt = H.build(work, source.xml);
H.succeeded(rebuilt.result);
H.assert.deepEqual(H.constRecords(rebuilt.output), [{ type: 7, bits: 0xffffffff, value: -1 }]);
H.assert.equal(H.element(rebuilt.raw, 'constant', 'num', '-1').num, '-1');
});
test('public build explicitly rejects fractional constant-pool values instead of silently truncating', () => {
const work = H.testCase('fractional-pool');
const human = `
`;
const rebuilt = H.build(work, human);
H.assert.notEqual(rebuilt.result.status, 0);
H.assert.match(rebuilt.result.stderr, /constant|non.?integer|fraction|unsupported|numeric/i);
});
test('public build rejects an integer that would wrap into a different signed type-7 value', () => {
const work = H.testCase('integer-pool-overflow');
const human = `
`;
const rebuilt = H.build(work, human);
H.assert.notEqual(rebuilt.result.status, 0);
H.assert.match(rebuilt.result.stderr, /signed 32-bit integers/);
H.assert.equal(H.fs.existsSync(rebuilt.output), false);
});
test('public texture-manifest roundtrip decodes and encodes an ampersand path once', () => {
const work = H.testCase('texture-entity');
const input = H.writeRaw(work, rawAction('', '', ''));
H.fs.writeFileSync(input.replace(/\.apt$/, '.xml'), '');
H.fs.writeFileSync(input.replace(/\.apt$/, '.dat'), '1->1\n');
const before = H.path.join(work.directory, 'image_edit.xml');
H.succeeded(H.cli(work, 'expand', input, before));
const human = H.fs.readFileSync(before, 'utf8');
H.assert.equal(H.element(human, 'image', 'id', 1).image, 'art/A&B.tga');
const rebuilt = H.build(work, human);
H.succeeded(rebuilt.result);
const after = H.path.join(work.directory, 'after_edit.xml');
H.succeeded(H.cli(work, 'expand', rebuilt.output, after));
H.assert.equal(H.element(H.fs.readFileSync(after, 'utf8'), 'image', 'id', 1).image, 'art/A&B.tga');
});
test('public expand/build preserves all original export names and repeated declarations in order', () => {
const work = H.testCase('export-object-keys');
const exports = '' +
'';
const source = H.expanded(work, rawAction('', exports, ''));
const rebuilt = H.build(work, source.xml);
H.succeeded(rebuilt.result);
for (const xml of [source.xml, rebuilt.raw]) {
H.assert.deepEqual(Array.from(xml.matchAll(/]*>/g), match => H.attributes(match[0]).name),
['constructor', 'toString', 'repeated', 'repeated']);
}
});