managarten/packages/spiral-db/src/image.test.ts
Till JS d4d08cc68b fix(spiral-db): add test suite and fix critical bugs
Add comprehensive test suite (174 tests) covering encoding, schema,
image, database CRUD, and PNG round-trip. Fix critical bugs:

- PNG compression: replace non-functional zlibCompress with pako.deflate
- PNG import: add CRC validation, support all filter types (Sub/Up/Avg/Paeth)
- Input validation: validate records against schema before insert
- Index overflow: dynamic dataStartRing prevents index/data ring overlap
- Image expansion: expand before writes instead of after to prevent OOB
- update() read bug: search index from end to find latest entry, not deleted one
- String encoding: enforce 511-byte max length
- Index ring count: use 6 bits (2 pixels) instead of 3 bits for >7 ring support

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 09:52:18 +01:00

251 lines
7.1 KiB
TypeScript

/**
* Image Tests
*/
import { describe, it, expect } from 'vitest';
import {
createImage,
createImageForRing,
getPixelByIndex,
setPixelByIndex,
getPixelByXY,
setPixelByXY,
readPixelRange,
writePixelRange,
expandImage,
getMaxRingForImage,
imageToRGBA,
rgbaToImage,
imageToColorGrid,
visualizeSpiralOrder,
visualizeImageEmoji,
} from './image.js';
import type { ColorIndex } from './types.js';
// =============================================================================
// CREATE IMAGE
// =============================================================================
describe('createImage', () => {
it('should create a square image with correct dimensions', () => {
const image = createImage(5);
expect(image.width).toBe(5);
expect(image.height).toBe(5);
expect(image.pixels.length).toBe(5 * 5 * 3);
});
it('should initialize all pixels to black (0)', () => {
const image = createImage(3);
for (let i = 0; i < image.pixels.length; i++) {
expect(image.pixels[i]).toBe(0);
}
});
it('should reject even sizes', () => {
expect(() => createImage(4)).toThrow('Image size must be odd');
});
it('should create 1x1 image', () => {
const image = createImage(1);
expect(image.width).toBe(1);
expect(image.pixels.length).toBe(3);
});
});
describe('createImageForRing', () => {
it('should create correct size for ring 0', () => {
const image = createImageForRing(0);
expect(image.width).toBe(1);
});
it('should create correct size for ring 2', () => {
const image = createImageForRing(2);
expect(image.width).toBe(5);
});
it('should create correct size for ring 5', () => {
const image = createImageForRing(5);
expect(image.width).toBe(11);
});
});
// =============================================================================
// PIXEL ACCESS
// =============================================================================
describe('Pixel Access by XY', () => {
it('should set and get pixel', () => {
const image = createImage(3);
setPixelByXY(image, 1, 1, 7); // white at center
expect(getPixelByXY(image, 1, 1)).toBe(7);
});
it('should set all 8 colors', () => {
const image = createImage(3);
for (let i = 0; i < 8; i++) {
setPixelByXY(image, i % 3, Math.floor(i / 3), i as ColorIndex);
}
for (let i = 0; i < 8; i++) {
expect(getPixelByXY(image, i % 3, Math.floor(i / 3))).toBe(i);
}
});
it('should throw on out-of-bounds access', () => {
const image = createImage(3);
expect(() => getPixelByXY(image, -1, 0)).toThrow('out of bounds');
expect(() => getPixelByXY(image, 3, 0)).toThrow('out of bounds');
expect(() => getPixelByXY(image, 0, 3)).toThrow('out of bounds');
expect(() => setPixelByXY(image, 0, -1, 0)).toThrow('out of bounds');
});
});
describe('Pixel Access by Index', () => {
it('should set and get center pixel (index 0)', () => {
const image = createImage(5);
setPixelByIndex(image, 0, 7);
expect(getPixelByIndex(image, 0)).toBe(7);
});
it('should set and get ring 1 pixels', () => {
const image = createImage(5);
for (let i = 1; i <= 8; i++) {
setPixelByIndex(image, i, (i % 8) as ColorIndex);
}
for (let i = 1; i <= 8; i++) {
expect(getPixelByIndex(image, i)).toBe(i % 8);
}
});
});
describe('Pixel Range Operations', () => {
it('should write and read a range', () => {
const image = createImage(5);
const colors: ColorIndex[] = [1, 2, 3, 4, 5];
writePixelRange(image, 0, colors);
const read = readPixelRange(image, 0, 5);
expect(read).toEqual(colors);
});
it('should handle range of 1', () => {
const image = createImage(3);
writePixelRange(image, 0, [7]);
expect(readPixelRange(image, 0, 1)).toEqual([7]);
});
});
// =============================================================================
// IMAGE EXPANSION
// =============================================================================
describe('expandImage', () => {
it('should grow image to accommodate new ring', () => {
const image = createImage(3); // ring 1
setPixelByIndex(image, 0, 7); // white center
const expanded = expandImage(image, 3);
expect(expanded.width).toBe(7); // ring 3 → 2*3+1
expect(expanded.height).toBe(7);
// Center pixel should be preserved
expect(getPixelByIndex(expanded, 0)).toBe(7);
});
it('should not expand if already large enough', () => {
const image = createImage(7);
const same = expandImage(image, 2); // ring 2 needs 5, we have 7
expect(same).toBe(image); // same reference
});
it('should preserve all existing pixels', () => {
const image = createImage(3);
// Set all 9 pixels
for (let i = 0; i < 9; i++) {
setPixelByIndex(image, i, (i % 8) as ColorIndex);
}
const expanded = expandImage(image, 3);
// Verify all original pixels preserved
for (let i = 0; i < 9; i++) {
expect(getPixelByIndex(expanded, i)).toBe(i % 8);
}
});
});
// =============================================================================
// FORMAT CONVERSIONS
// =============================================================================
describe('RGBA Conversion', () => {
it('should convert to RGBA and back', () => {
const image = createImage(3);
setPixelByIndex(image, 0, 7); // white center
setPixelByIndex(image, 1, 4); // red
const rgba = imageToRGBA(image);
expect(rgba.length).toBe(3 * 3 * 4); // 4 bytes per pixel
const back = rgbaToImage(rgba, 3, 3);
expect(getPixelByIndex(back, 0)).toBe(7);
expect(getPixelByIndex(back, 1)).toBe(4);
});
it('should set alpha to 255 in RGBA', () => {
const image = createImage(1);
const rgba = imageToRGBA(image);
expect(rgba[3]).toBe(255); // alpha
});
it('should reject non-square RGBA', () => {
const rgba = new Uint8Array(2 * 3 * 4);
expect(() => rgbaToImage(rgba, 2, 3)).toThrow('must be square');
});
it('should reject even-sized RGBA', () => {
const rgba = new Uint8Array(4 * 4 * 4);
expect(() => rgbaToImage(rgba, 4, 4)).toThrow('must be odd');
});
});
describe('Color Grid', () => {
it('should return 2D grid of color indices', () => {
const image = createImage(3);
setPixelByXY(image, 0, 0, 4);
setPixelByXY(image, 2, 2, 2);
const grid = imageToColorGrid(image);
expect(grid.length).toBe(3);
expect(grid[0].length).toBe(3);
expect(grid[0][0]).toBe(4);
expect(grid[2][2]).toBe(2);
});
});
describe('getMaxRingForImage', () => {
it('should return correct max ring', () => {
expect(getMaxRingForImage(createImage(1))).toBe(0);
expect(getMaxRingForImage(createImage(3))).toBe(1);
expect(getMaxRingForImage(createImage(5))).toBe(2);
expect(getMaxRingForImage(createImage(11))).toBe(5);
});
});
// =============================================================================
// VISUALIZATION
// =============================================================================
describe('Visualization', () => {
it('should produce emoji visualization with correct dimensions', () => {
const image = createImage(3);
setPixelByIndex(image, 0, 7);
const emoji = visualizeImageEmoji(image);
const lines = emoji.split('\n');
expect(lines.length).toBe(3);
});
it('should produce spiral order visualization', () => {
const viz = visualizeSpiralOrder(3);
expect(viz).toContain('0'); // center
expect(viz.split('\n')).toHaveLength(3);
});
});