/** * tests/challenger-m4-determinism-parity.test.ts * * Empirical Adversarial Challenger Test Suite for Milestone 4 (Issue #396) * Author: teamwork_preview_challenger * * Scope: * 1. Multi-Worker Hash Determinism: * - Verify SHA-256 hash of `samples/d2-packs/entities/index.json` matches repeated multi-worker runs. * - Assert 100% byte-for-byte exact equality (0 diff lines). * 2. Full-Spectrum 365-Scene Asset Parity: * - Audit all 365 scenes in `samples/d2-packs/index.json`. * - Verify scene.json validity, collision dimensions (width=cellsX*5, height=cellsY*5), and non-empty floor draws. * - Verify all referenced PNG tile and object atlases exist on disk with valid RFC 2083 8-byte magic signatures. * 3. Index Structure & Deterministic Ordering: * - Assert 365 total levels (65 preset, 90 wilderness, 210 maze). * - Verify deterministic ordering by jobId/levelId. * - Verify scene links and world graph coherence. */ import { describe, it, expect } from 'vitest' import { existsSync, readFileSync, readdirSync, openSync, readSync, closeSync, statSync } from 'node:fs' import { createHash } from 'node:crypto' import { join } from 'node:path' const PNG_MAGIC = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]) const PACKS_DIR = 'samples/d2-packs' const ENTITIES_INDEX_PATH = join(PACKS_DIR, 'entities', 'index.json') const ROOT_INDEX_PATH = join(PACKS_DIR, 'index.json') function sha256File(filePath: string): string { const content = readFileSync(filePath) return createHash('sha256').update(content).digest('hex') } describe('Challenger M4_2: Asset Baking Pipeline Determinism & 365-Scene Parity', () => { it('verifies samples/d2-packs/entities/index.json hash determinism and 0 diff lines against repeated multi-worker runs', () => { expect(existsSync(ENTITIES_INDEX_PATH)).toBe(true) const baselineHash = sha256File(ENTITIES_INDEX_PATH) // Expected canonical SHA-256 for Schema 2 70-monster entity index (including vk-hth Valkyrie // and ec-hth Hell Bovine: MonStats.txt hellbovine Code=EC, level 39 Moo Moo Farm). Adding // ec-hth is the only change against the previous 69-monster index (51da3628…). expect(baselineHash).toBe('675757cffde277b24e2aec11b7440243654282cbb743424ef02ce1d253b41c66') const baselineContent = readFileSync(ENTITIES_INDEX_PATH, 'utf8') const parsed = JSON.parse(baselineContent) expect(parsed.schema).toBe(2) expect(parsed.character).toBe('char-so') expect(parsed.entities).toHaveLength(70) // Compare against /tmp/stress-entity-run1 and /tmp/stress-entity-run2 if present const testDirs = ['/tmp/stress-entity-run1', '/tmp/stress-entity-run2'] for (const dir of testDirs) { const runIndexPath = join(dir, 'entities', 'index.json') if (existsSync(runIndexPath)) { const runHash = sha256File(runIndexPath) expect(runHash).toBe(baselineHash) const runContent = readFileSync(runIndexPath, 'utf8') expect(runContent).toBe(baselineContent) } } }) it('verifies char-so.json hash and Schema 2 alias parity across repeated runs', () => { const charSoPath = join(PACKS_DIR, 'entities', 'char-so.json') expect(existsSync(charSoPath)).toBe(true) const baselineHash = sha256File(charSoPath) expect(baselineHash).toBe('3b0c78d275dd976ce447fbb7b19ff550c23db1ed1478f188907cb7a4538e7194') const meta = JSON.parse(readFileSync(charSoPath, 'utf8')) expect(meta.schema).toBe(2) expect(meta.walk).toBe(0) expect(meta.stand).toBe(16) expect(meta.walkOffset).toBe(0) expect(meta.standOffset).toBe(16) expect(meta.clips.wl.group).toBe(0) expect(meta.clips.nu.group).toBe(16) }) it('verifies full-spectrum asset parity across all 365 scenes in index.json', () => { expect(existsSync(ROOT_INDEX_PATH)).toBe(true) const index = JSON.parse(readFileSync(ROOT_INDEX_PATH, 'utf8')) expect(index.version).toBe(1) expect(Array.isArray(index.levels)).toBe(true) expect(index.levels).toHaveLength(365) const kindsCount = { preset: 0, wilderness: 0, maze: 0 } let totalFloors = 0 let totalWalls = 0 let totalPngs = 0 for (const level of index.levels) { const kind = level.kind || 'preset' kindsCount[kind as keyof typeof kindsCount] += 1 expect(level.act).toBeGreaterThanOrEqual(1) expect(level.act).toBeLessThanOrEqual(5) expect(level.levelId).toBeGreaterThanOrEqual(1) expect(level.path).toBeDefined() expect(level.ds1).toBeDefined() const sceneDir = join(PACKS_DIR, level.path) const sceneJsonPath = join(sceneDir, 'scene.json') expect(existsSync(sceneJsonPath), `Missing scene.json for ${level.path}`).toBe(true) const scene = JSON.parse(readFileSync(sceneJsonPath, 'utf8')) expect(scene.act).toBe(level.act) expect(scene.levelId).toBe(level.levelId) expect(scene.cellsX).toBeGreaterThan(0) expect(scene.cellsY).toBeGreaterThan(0) expect(scene.widthPx).toBeGreaterThan(0) expect(scene.heightPx).toBeGreaterThan(0) // Collision grid validation: exact 5x5 sub-tiles per cell expect(scene.collision).toBeDefined() expect(scene.collision.width).toBe(scene.cellsX * 5) expect(scene.collision.height).toBe(scene.cellsY * 5) expect(Array.isArray(scene.collision.runs)).toBe(true) expect(scene.collision.runs.length).toBeGreaterThan(0) // Floor and wall draws validation expect(Array.isArray(scene.floors)).toBe(true) expect(scene.floors.length).toBeGreaterThan(0) totalFloors += scene.floors.length if (scene.walls) totalWalls += scene.walls.length // Verify all PNG atlas files in scene directory const sceneFiles = readdirSync(sceneDir) for (const file of sceneFiles) { if (file.endsWith('.png')) { totalPngs += 1 const fullPngPath = join(sceneDir, file) const stat = statSync(fullPngPath) expect(stat.size).toBeGreaterThan(0) const headerBuf = Buffer.alloc(8) const fd = openSync(fullPngPath, 'r') readSync(fd, headerBuf, 0, 8, 0) closeSync(fd) expect( headerBuf.equals(PNG_MAGIC), `Invalid PNG magic bytes in ${fullPngPath}`, ).toBe(true) } } } expect(kindsCount.preset).toBe(65) expect(kindsCount.wilderness).toBe(90) expect(kindsCount.maze).toBe(210) // Measured on the full re-bake after the Act I outdoor levels (2-7, 17, 39) moved to // the D2MOO DRLG port. For reference, a fresh bake of the previous generator gave // 1020690 floors, 231738 walls and 1401 PNGs; the old pins (1020280 / 231464 / 1433) // no longer matched any bake of the current code. expect(totalFloors).toBe(1008898) expect(totalWalls).toBe(237542) expect(totalPngs).toBe(1390) }) it('verifies world graph integrity and edge consistency with index.json', () => { const graphPath = join(PACKS_DIR, 'world-graph.json') expect(existsSync(graphPath)).toBe(true) const graph = JSON.parse(readFileSync(graphPath, 'utf8')) expect(graph.version).toBe(1) expect(graph.levels.length).toBe(136) // 284: the Stony Field <-> Dark Wood seam (4 <-> 5, one edge each way) was invented and // is gone; in D2 the two levels are joined only through the Underground Passage. expect(graph.edges.length).toBe(284) expect(graph.waypoints.length).toBe(39) // All edge levels must exist in levels list const validLevelIds = new Set(graph.levels.map((l: { id: number }) => l.id)) for (const edge of graph.edges) { expect(validLevelIds.has(edge.from)).toBe(true) expect(validLevelIds.has(edge.to)).toBe(true) } }) it('verifies root index.json deterministic ordering and field invariants', () => { const index = JSON.parse(readFileSync(ROOT_INDEX_PATH, 'utf8')) expect(index.archiveDir).toBe('samples/d2') expect(index.pageSize).toBe(2048) expect(index.palettes).toBeDefined() expect(Object.keys(index.palettes)).toEqual(['act1', 'act2', 'act3', 'act4', 'act5']) // Verify palettes length (each act pal.pl2 has 256*3 rgb bytes) for (const act of [1, 2, 3, 4, 5]) { const pal = index.palettes[`act${act}`] expect(pal).toHaveLength(768) } // Verify ordering: acts must be grouped in ascending act order let prevAct = 1 for (const level of index.levels) { expect(level.act).toBeGreaterThanOrEqual(prevAct) prevAct = level.act } }) })