Files
2026-09-22 16:33:35 +08:00

86 lines
4.2 KiB
JavaScript

import { readFile } from "node:fs/promises";
import test from "node:test";
import assert from "node:assert/strict";
import { encodeSource, hasExactRoundTrip, parseJpwabc, serializeBytes, serializeText } from "../dist/domain/jpwabc.js";
const readRaw = (path) => readFile(path);
test("M1 decodes UTF-16LE/BOM and preserves the 康定情歌 source", async () => {
const result = parseJpwabc(await readRaw("research/jpw7/demo/JP-Word练习曲-歌曲-康定情歌.jpwabc"));
assert.equal(result.source.encoding, "utf-16le");
assert.equal(result.source.bom, "utf-16le");
assert.equal(result.header.formatVersion, "1.0");
assert.equal(result.header.productVersion, "7.30");
assert.deepEqual(result.sections.map((section) => section.name), ["<preamble>", ".Options", ".Fonts", ".Title", ".Voice", ".Words", ".Attachments", ".Page"]);
assert.ok(result.options.document.length >= 0);
assert.ok(result.fonts.length > 0);
assert.ok(result.titles.some((entry) => entry.field === "Title" && entry.value === "康定情歌"));
assert.ok(result.voices.some((line) => line.tokens.some((token) => token.kind === "note")));
assert.ok(result.voices.some((line) => line.tokens.some((token) => token.kind === "barline")));
assert.ok(result.words.length >= 4);
assert.ok(result.attachments.length > 0);
assert.equal(hasExactRoundTrip(result), true);
assert.equal(serializeText(result), result.source.text);
assert.deepEqual([...serializeBytes(result)], [...result.source.bytes]);
const reparsed = parseJpwabc(serializeBytes(result));
assert.equal(serializeText(reparsed), serializeText(result));
assert.deepEqual([...serializeBytes(reparsed)], [...result.source.bytes]);
});
test("M1 detects UTF-16BE and UTF-8 BOM variants", () => {
const source = ".Title\r\nTitle = 编码探针\r\n";
const bigEndian = parseJpwabc(encodeSource(source, "utf-16be", "utf-16be"));
const utf8 = parseJpwabc(encodeSource(source, "utf-8", "utf-8"));
assert.equal(bigEndian.source.encoding, "utf-16be");
assert.equal(bigEndian.source.bom, "utf-16be");
assert.equal(utf8.source.encoding, "utf-8");
assert.equal(utf8.source.bom, "utf-8");
assert.equal(hasExactRoundTrip(bigEndian), true);
assert.equal(hasExactRoundTrip(utf8), true);
});
test("M1 retains unknown sections, unknown options and source ranges", () => {
const source = [
"// ******** JPW-ABC File Ver 9.9 ********",
"",
".Options",
"KnownLater = {opaque:value}",
"",
".MysteryBlock",
"Payload {vendor:private} ",
"",
".Voice",
"1_ {vendor:private} | 0__",
""
].join("\r\n");
const result = parseJpwabc(encodeSource(source, "utf-16le", "utf-16le"));
assert.equal(result.source.encoding, "utf-16le");
assert.equal(result.sections.find((section) => section.name === ".MysteryBlock")?.known, false);
assert.ok(result.options.unknownDocument.some((entry) => entry.key === "KnownLater"));
assert.ok(result.unknownBlocks.some((block) => block.reason === "unknown-section"));
assert.ok(result.unknownTokens.some((token) => token.head === "vendor"));
assert.ok(result.diagnostics.some((item) => item.code === "UNKNOWN_SECTION"));
assert.equal(result.compatibility.originalExport, "blocked");
assert.equal(hasExactRoundTrip(result), true);
assert.ok(result.sections.every((section) => section.range.byteEnd >= section.range.byteStart));
});
test("M1 reports malformed structures without dropping their bytes", () => {
const source = ".Voice\n1_ {unclosed\n";
const result = parseJpwabc(encodeSource(source, "utf-8", "none"));
assert.ok(result.diagnostics.some((item) => item.code === "UNCLOSED_BRACE"));
assert.equal(result.compatibility.projectSave, "blocked");
assert.equal(result.compatibility.originalExport, "blocked");
assert.equal(hasExactRoundTrip(result), true);
});
test("M1 classification report covers all 33 fixtures", async () => {
const report = JSON.parse(await readFile("acceptance/m1/sample-classification.json", "utf8"));
assert.equal(report.sampleCountExpected, 33);
assert.equal(report.sampleCountActual, 33);
assert.equal(report.allSourceBytesPreserved, true);
assert.equal(report.failures.length, 0);
assert.equal(report.samples.length, 33);
assert.ok(report.samples.every((sample) => sample.status && sample.sourcePreserved === true));
});