import assert from "node:assert/strict"; import { createHash } from "node:crypto"; import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import test from "node:test"; import { gzipSync } from "node:zlib"; import { ARCHIVE_SPELL_SOURCE, createArchiveSpellCacheApi } from "./archive-spells.mjs"; const fixtureSourceSha = "b".repeat(64); function sha256(bytes) { return createHash("sha256").update(bytes).digest("hex"); } function gitBlobSha(bytes) { return createHash("sha1") .update(Buffer.from("blob " + bytes.byteLength + "\0", "utf8")) .update(bytes) .digest("hex"); } function spellRow(id, options = {}) { const row = {}; for (let field = 0; field < 234; field += 1) row["f" + field] = 0; row.f0 = id; row.f41 = 1; row.f47 = 1.5; row.f71 = options.effectType ?? 0; row.f110 = options.miscValue ?? 0; row.f116 = options.triggerSpellId ?? 0; row.f136 = options.name ?? "Test spell " + id; row.f136i = id * 10; row.f153 = "Passive"; row.f153i = id * 10 + 1; row.f170 = "Description for " + id; row.f170i = id * 10 + 2; row.f187 = "Alternate " + id; row.f187i = id * 10 + 3; row.f229 = 0.25; return row; } function makeFixture({ sourceDbcSha256 = fixtureSourceSha } = {}) { const shardRows = [ { lo: 1, hi: 99, rows: [spellRow(1, { triggerSpellId: 101 })] }, { lo: 100, hi: 199, rows: [ spellRow(101, { effectType: 168, miscValue: 202 }), spellRow(155), ] }, { lo: 200, hi: 299, rows: [spellRow(202)] }, { lo: 300, hi: 399, rows: [spellRow(350)] }, ]; const shards = []; const archiveFiles = new Map(); for (const group of shardRows) { const name = String(group.lo).padStart(8, "0") + "-" + String(group.hi).padStart(8, "0") + ".jsonl.gz"; const bytes = gzipSync(Buffer.from(group.rows.map((row) => JSON.stringify(row)).join("\n") + "\n", "utf8"), { level: 9, mtime: 0 }); archiveFiles.set(name, bytes); shards.push({ file: name, format: "gzip", lo: group.lo, hi: group.hi, plainBytes: 1, rows: group.rows.length, sha256: sha256(bytes), storedBytes: bytes.byteLength, }); } const index = { file: "Spell.dbc", format: "gzip", rows: 5, columns: 234, table: "Spell", sourceSha256: sourceDbcSha256, shardKey: { column: "f0", maxRows: 5000, mode: "column", unsigned: true }, shards, }; const columnInfo = { table: "Spell", columnCount: 234, header: { records: 5 }, source: { winner: "Data/patch-T.MPQ", sha256: sourceDbcSha256 }, columns: Array.from({ length: 234 }, (_, index) => ({ index, inferred: index === 47 || index === 229 ? "float" : [136, 153, 170, 187].includes(index) ? "string" : "int", })), }; const indexBytes = Buffer.from(JSON.stringify(index), "utf8"); const colinfoBytes = Buffer.from(JSON.stringify(columnInfo), "utf8"); const variantPath = "raw/tables/Spell/variants/data-patch-t-mpq"; const source = { id: "fixture-global-spell-base", repository: "example/coa-datamine", revision: "a".repeat(40), shortRevision: "aaaaaaaaaaaa", capturedAt: "2026-08-12T21:02:51+00:00", variantPath, tableIndexPath: variantPath + "/index.json", tableIndexSha256: sha256(indexBytes), tableIndexGitBlobSha: gitBlobSha(indexBytes), columnInfoPath: variantPath + "/Spell.colinfo.json", columnInfoSha256: sha256(colinfoBytes), columnInfoGitBlobSha: gitBlobSha(colinfoBytes), sourceDbcSha256, rows: 5, columns: 234, }; const prefix = "https://raw.githubusercontent.com/" + source.repository + "/" + source.revision + "/"; const metadataFiles = new Map([ [prefix + source.tableIndexPath, indexBytes], [prefix + source.columnInfoPath, colinfoBytes], ]); for (const [name, bytes] of archiveFiles) metadataFiles.set(prefix + variantPath + "/" + name, bytes); return { source, archiveFiles, files: metadataFiles, prefix, variantPath, indexBytes, colinfoBytes }; } function fakeFetch(fileMap, calls) { return async (url) => { calls.push(url); const bytes = fileMap.get(url); if (!bytes) return { ok: false, status: 404 }; return { ok: true, arrayBuffer: async () => bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength), }; }; } async function withTempCache(run) { const cacheRoot = await fs.mkdtemp(path.join(os.tmpdir(), "coa-archive-spells-")); try { await run(cacheRoot); } finally { await fs.rm(cacheRoot, { recursive: true, force: true }); } } test("pins and reads the global patch-T source and closes trigger and effect-168 references", async () => { assert.equal(ARCHIVE_SPELL_SOURCE.variantPath, "raw/tables/Spell/variants/data-patch-t-mpq"); assert.equal(ARCHIVE_SPELL_SOURCE.sourceDbcSha256, "a1e4285cdbc8a269a1b8a3f4b304ee43eb4d1bfa5a98511639868c4ff6097623"); await withTempCache(async (cacheRoot) => { const fixture = makeFixture(); const api = createArchiveSpellCacheApi(fixture.source); const calls = []; const fetchImpl = fakeFetch(fixture.files, calls); const downloaded = await api.downloadArchiveSpells({ cacheRoot, spellIds: [1], fetchImpl }); assert.deepEqual(downloaded.records.map((row) => row.f0), [1, 101, 202]); assert.deepEqual(downloaded.missingIds, []); assert.deepEqual(downloaded.fieldTypes.slice(0, 3), ["int", "int", "int"]); assert.equal(downloaded.fieldTypes[47], "float"); assert.equal(downloaded.fieldTypes[136], "string"); assert.equal(downloaded.source.path, fixture.variantPath); assert.equal(downloaded.source.sourceDbcSha256, fixture.source.sourceDbcSha256); assert.equal(downloaded.source.originalRecordCount, 5); assert.equal(downloaded.source.bytes, downloaded.files.reduce((sum, file) => sum + file.size, 0)); assert.equal(downloaded.files.length, 3); assert.ok(downloaded.files.every((file) => file.archivePath.startsWith(fixture.variantPath + "/"))); assert.equal(calls.filter((url) => url.includes("/variants/") && url.endsWith(".jsonl.gz")).length, 3); assert.ok(!calls.some((url) => url.includes("/variants/") && url.includes("00000300-"))); const offline = await api.readArchiveSpells({ cacheRoot }); assert.deepEqual(offline.records, downloaded.records); assert.deepEqual(offline.fieldTypes, downloaded.fieldTypes); assert.deepEqual(offline.missingIds, []); }); }); test("resumes selected shards, repairs tampered cached bytes, and reports absent ids", async () => { await withTempCache(async (cacheRoot) => { const fixture = makeFixture(); const api = createArchiveSpellCacheApi(fixture.source); const calls = []; const fetchImpl = fakeFetch(fixture.files, calls); await api.downloadArchiveSpells({ cacheRoot, spellIds: [1], fetchImpl }); const firstCallCount = calls.length; const resumed = await api.downloadArchiveSpells({ cacheRoot, spellIds: [350, 999], fetchImpl }); assert.deepEqual(resumed.records.map((row) => row.f0), [1, 101, 202, 350]); assert.deepEqual(resumed.missingIds, [999]); assert.equal(calls.length, firstCallCount + 1); const cachedShard = path.join(cacheRoot, "archive-" + fixture.source.shortRevision, "spell-base", "shards", "00000001-00000099.jsonl.gz"); await fs.writeFile(cachedShard, Buffer.from("tampered")); await assert.rejects(api.readArchiveSpells({ cacheRoot }), /SHA-256 mismatch/); const beforeRepair = calls.length; const repaired = await api.downloadArchiveSpells({ cacheRoot, spellIds: [350], fetchImpl }); assert.deepEqual(repaired.records.map((row) => row.f0), [1, 101, 202, 350]); assert.equal(calls.length, beforeRepair + 1); const raw = await fs.readFile(cachedShard); assert.equal(sha256(raw), fixture.archiveFiles.get("00000001-00000099.jsonl.gz") && sha256(fixture.archiveFiles.get("00000001-00000099.jsonl.gz"))); }); }); test("reuses pin-matching remote-audit BASE metadata and shards without a network fetch", async () => { await withTempCache(async (cacheRoot) => { const fixture = makeFixture(); const archiveRoot = path.join(cacheRoot, "archive-" + fixture.source.shortRevision); const remoteAudit = path.join(archiveRoot, "remote-audit"); const shardDirectory = path.join(remoteAudit, "base-spell-shards"); await fs.mkdir(shardDirectory, { recursive: true }); await fs.writeFile(path.join(remoteAudit, "raw_tables_Spell_variants_data-patch-t-mpq_index.json"), fixture.indexBytes); await fs.writeFile(path.join(remoteAudit, "base_Spell.colinfo.json"), fixture.colinfoBytes); for (const name of [ "00000001-00000099.jsonl.gz", "00000100-00000199.jsonl.gz", "00000200-00000299.jsonl.gz", ]) { await fs.writeFile(path.join(shardDirectory, name), fixture.archiveFiles.get(name)); } const calls = []; const result = await createArchiveSpellCacheApi(fixture.source).downloadArchiveSpells({ cacheRoot, spellIds: [1], fetchImpl: async (url) => { calls.push(url); throw new Error("unexpected network request"); }, }); assert.equal(calls.length, 0); assert.deepEqual(result.records.map((row) => row.f0), [1, 101, 202]); assert.deepEqual(result.files.map((file) => file.path), [ "00000001-00000099.jsonl.gz", "00000100-00000199.jsonl.gz", "00000200-00000299.jsonl.gz", ]); }); }); test("rejects an overlay index even when its metadata hashes match the supplied bytes", async () => { await withTempCache(async (cacheRoot) => { const fixture = makeFixture(); const overlayFixture = makeFixture({ sourceDbcSha256: "c".repeat(64) }); const wrongIndexUrl = fixture.prefix + fixture.source.tableIndexPath; fixture.files.set(wrongIndexUrl, overlayFixture.files.get(wrongIndexUrl)); const overlayBytes = fixture.files.get(wrongIndexUrl); fixture.source.tableIndexSha256 = sha256(overlayBytes); fixture.source.tableIndexGitBlobSha = gitBlobSha(overlayBytes); const api = createArchiveSpellCacheApi(fixture.source); await assert.rejects( api.downloadArchiveSpells({ cacheRoot, spellIds: [1], fetchImpl: fakeFetch(fixture.files, []) }), /pinned BASE patch-T Spell\.dbc source/, ); }); });