// tests/build/icons-present.test.ts — Plan 01-12 Wave 0 RED unit test. // // Asserts that icons/icon{16,48,128}.png are rasterized from the Loom // brand mark via rsvg-convert (Wave 2 Task 1) — replacing the Bug A // placeholders (16-bit/color RGB) with 8-bit/color RGBA. // // Polarity at Wave 0 land: // - Dimension + size FLOOR cases: GREEN today (placeholders are at // correct dims + clear size floors). Stay GREEN after Wave 2. // - Color-type RGBA case: RED today (placeholders are color-type 2 = // RGB). Flips GREEN after Wave 2 (rsvg-convert default output is // 8-bit/color RGBA, color-type 6). // // PNG header layout per ISO/IEC 15948 §11.2.2: // - Bytes 0-7: signature 89 50 4E 47 0D 0A 1A 0A // - Bytes 8-11: chunk length (big-endian uint32) // - Bytes 12-15: chunk type 'IHDR' (49 48 44 52) // - Bytes 16-19: width (big-endian uint32) // - Bytes 20-23: height (big-endian uint32) // - Byte 24: bit depth (8 or 16 for our cases) // - Byte 25: color type — 2=RGB, 6=RGBA (load-bearing for RED→GREEN flip) // // References: // - RESEARCH §3 (verified locally: rsvg-convert defaults to 8-bit RGBA) // - assets-spec.md (Chrome imageUtil silent-rejection FLOORs) // - Plan 01-13 harness A9 (icon-size floor at UAT-time) import { describe, expect, it } from 'vitest'; import { existsSync, openSync, readSync, closeSync, statSync } from 'node:fs'; import { resolve as resolvePath } from 'node:path'; const PNG_SIGNATURE = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]); const COLOR_TYPE_RGBA = 6; interface IconSpec { readonly size: number; readonly sizeFloorBytes: number; } /** Per assets-spec.md Chrome imageUtil silent-rejection thresholds. */ const ICON_SPECS: ReadonlyArray = [ { size: 16, sizeFloorBytes: 200 }, { size: 48, sizeFloorBytes: 500 }, { size: 128, sizeFloorBytes: 1024 }, ]; /** * Read the first N bytes of a file synchronously. Used to parse the PNG * header without slurping the whole file. Returns a Buffer of length N * (zero-padded if the file is shorter, which only happens on a corrupt * non-PNG file we want to fail loudly on anyway). */ function readHead(filePath: string, n: number): Buffer { const buf = Buffer.alloc(n); const fd = openSync(filePath, 'r'); try { readSync(fd, buf, 0, n, 0); } finally { closeSync(fd); } return buf; } /** Read big-endian uint32 at offset; ISO/IEC 15948 §7 mandates network byte order. */ function readU32BE(buf: Buffer, offset: number): number { return buf.readUInt32BE(offset); } describe('Plan 01-12: icons rasterized from Loom mark (RGBA, 8-bit, clearing imageUtil FLOORs)', () => { for (const spec of ICON_SPECS) { const iconPath = resolvePath(process.cwd(), `icons/icon${spec.size}.png`); describe(`icons/icon${spec.size}.png (${spec.size}×${spec.size})`, () => { it('exists at icons/iconN.png', () => { expect(existsSync(iconPath), `Expected ${iconPath}`).toBe(true); }); it(`size >= ${spec.sizeFloorBytes} bytes (Chrome imageUtil floor)`, () => { const size = statSync(iconPath).size; expect( size, `icon${spec.size}.png is ${size} B; need >= ${spec.sizeFloorBytes} B to clear Chrome imageUtil floor`, ).toBeGreaterThanOrEqual(spec.sizeFloorBytes); }); it('PNG signature (first 8 bytes match \\x89PNG\\r\\n\\x1a\\n)', () => { const head = readHead(iconPath, 8); expect( head.equals(PNG_SIGNATURE), `Expected PNG signature; got ${head.toString('hex')}`, ).toBe(true); }); it(`dimensions === ${spec.size}×${spec.size} (IHDR offset 16-23, big-endian uint32)`, () => { const head = readHead(iconPath, 32); const width = readU32BE(head, 16); const height = readU32BE(head, 20); expect(width, `icon${spec.size}.png width`).toBe(spec.size); expect(height, `icon${spec.size}.png height`).toBe(spec.size); }); it('color-type byte (IHDR offset 25) === 6 (RGBA — RED until Wave 2 overwrites placeholder)', () => { const head = readHead(iconPath, 32); const colorType = head[25]; expect( colorType, colorType === COLOR_TYPE_RGBA ? 'unreachable' : `icon${spec.size}.png color-type=${colorType}; expected 6 (RGBA). ` + `Placeholder PNGs are color-type 2 (RGB). RED until Wave 2 Task 1 ` + `rasterizes via rsvg-convert (default output is 8-bit/color RGBA per RESEARCH §3).`, ).toBe(COLOR_TYPE_RGBA); }); }); } });