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), ["", ".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)); });