mirror of
https://github.com/tiennm99/rplace.git
synced 2026-06-03 08:15:57 +00:00
5765055588
Pure-function unit tests for chunk-storage (BLOB-grow, orphan-row read, multi-chunk grouping) and cooldown-store (TTL math, INSERT cursor drain, GC sampling, release/refund) cover the bugs Phase 1 fixed. Integration suite boots a local Worker + real DO via wrangler unstable_dev and asserts cookie issuance, cooldown isolation across identities, Content-Length 411/413 guards, WS upgrade semantics, WS broadcast frame shape with monotonic seq, ping/pong, per-identity WS cap, and Origin allowlist. WS uses the ws package directly since unstable_dev's fetch strips CF's webSocket Response field. 156 tests pass in ~3.3s; verified stable across 3 consecutive runs.
182 lines
5.9 KiB
JavaScript
182 lines
5.9 KiB
JavaScript
import { describe, it, expect } from 'vitest';
|
|
import { readChunk, readAllChunks, writePixels } from '../../src/durable-objects/lib/chunk-storage.js';
|
|
import {
|
|
CANVAS_WIDTH,
|
|
CHUNK_BYTES,
|
|
CHUNK_COUNT,
|
|
TOTAL_PIXELS,
|
|
} from '../../src/lib/constants.js';
|
|
import { createFakeSql } from '../helpers/fake-sql.js';
|
|
|
|
describe('readChunk', () => {
|
|
it('returns a zero-filled buffer for missing rows (lazy init)', () => {
|
|
const sql = createFakeSql();
|
|
const buf = readChunk(sql, 0);
|
|
expect(buf).toBeInstanceOf(Uint8Array);
|
|
expect(buf.length).toBe(CHUNK_BYTES);
|
|
expect(buf.every((b) => b === 0)).toBe(true);
|
|
});
|
|
|
|
it('returns the persisted blob when row exists', () => {
|
|
const sql = createFakeSql();
|
|
const seed = new Uint8Array(CHUNK_BYTES);
|
|
seed[42] = 99;
|
|
sql._seedChunk(0, seed);
|
|
const buf = readChunk(sql, 0);
|
|
expect(buf[42]).toBe(99);
|
|
});
|
|
});
|
|
|
|
describe('writePixels — single chunk', () => {
|
|
it('persists pixels and reads them back identically', () => {
|
|
const sql = createFakeSql();
|
|
writePixels(sql, [
|
|
{ x: 0, y: 0, color: 5 },
|
|
{ x: 1, y: 0, color: 17 },
|
|
{ x: 5, y: 0, color: 250 },
|
|
]);
|
|
const chunk0 = readChunk(sql, 0);
|
|
expect(chunk0[0]).toBe(5);
|
|
expect(chunk0[1]).toBe(17);
|
|
expect(chunk0[5]).toBe(250);
|
|
});
|
|
|
|
it('overwrites previous color at same coordinate', () => {
|
|
const sql = createFakeSql();
|
|
writePixels(sql, [{ x: 10, y: 0, color: 1 }]);
|
|
writePixels(sql, [{ x: 10, y: 0, color: 42 }]);
|
|
const chunk0 = readChunk(sql, 0);
|
|
expect(chunk0[10]).toBe(42);
|
|
});
|
|
|
|
it('is a no-op on empty input', () => {
|
|
const sql = createFakeSql();
|
|
writePixels(sql, []);
|
|
expect(sql._chunks.size).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe('writePixels — multi-chunk', () => {
|
|
it('groups writes across chunks correctly', () => {
|
|
const sql = createFakeSql();
|
|
// (x=0,y=0) → offset 0 → chunk 0
|
|
// (x=CHUNK_BYTES,y=0) → offset CHUNK_BYTES → chunk 1, byteOffset 0
|
|
writePixels(sql, [
|
|
{ x: 0, y: 0, color: 11 },
|
|
{ x: CHUNK_BYTES, y: 0, color: 22 },
|
|
]);
|
|
expect(sql._chunks.size).toBe(2);
|
|
expect(readChunk(sql, 0)[0]).toBe(11);
|
|
expect(readChunk(sql, 1)[0]).toBe(22);
|
|
});
|
|
|
|
it('issues exactly one INSERT per touched chunk regardless of pixel count', () => {
|
|
const sql = createFakeSql();
|
|
let writes = 0;
|
|
const origExec = sql.exec;
|
|
sql.exec = (q, ...p) => {
|
|
if (q.startsWith('INSERT INTO canvas_chunks')) writes++;
|
|
return origExec(q, ...p);
|
|
};
|
|
writePixels(sql, [
|
|
{ x: 0, y: 0, color: 1 },
|
|
{ x: 1, y: 0, color: 2 },
|
|
{ x: 2, y: 0, color: 3 },
|
|
{ x: 3, y: 0, color: 4 },
|
|
]);
|
|
expect(writes).toBe(1);
|
|
});
|
|
});
|
|
|
|
describe('writePixels — BLOB-grow safety', () => {
|
|
it('writes against chunkSize, not the persisted blob length', () => {
|
|
const sql = createFakeSql();
|
|
// Pre-seed chunk 0 with a short blob (e.g. legacy 8 KB).
|
|
const shortBlob = new Uint8Array(8192);
|
|
shortBlob[100] = 7;
|
|
sql._seedChunk(0, shortBlob);
|
|
|
|
// Write a pixel at byteOffset 30000 — well beyond the seeded 8 KB.
|
|
// y=0, x=30000 → offset=30000 → chunk 0, byteOffset 30000.
|
|
writePixels(sql, [{ x: 30000, y: 0, color: 123 }]);
|
|
|
|
const buf = readChunk(sql, 0);
|
|
expect(buf.length).toBe(CHUNK_BYTES);
|
|
expect(buf[100]).toBe(7); // preserved
|
|
expect(buf[30000]).toBe(123); // newly written, would have been dropped
|
|
});
|
|
});
|
|
|
|
describe('readAllChunks', () => {
|
|
it('returns a TOTAL_PIXELS Uint8Array even when chunks are empty', () => {
|
|
const sql = createFakeSql();
|
|
const out = readAllChunks(sql);
|
|
expect(out).toBeInstanceOf(Uint8Array);
|
|
expect(out.length).toBe(TOTAL_PIXELS);
|
|
expect(out[0]).toBe(0);
|
|
expect(out[TOTAL_PIXELS - 1]).toBe(0);
|
|
});
|
|
|
|
it('concatenates seeded chunks at correct offsets', () => {
|
|
const sql = createFakeSql();
|
|
const c0 = new Uint8Array(CHUNK_BYTES);
|
|
c0[0] = 1;
|
|
c0[CHUNK_BYTES - 1] = 2;
|
|
const c1 = new Uint8Array(CHUNK_BYTES);
|
|
c1[0] = 3;
|
|
sql._seedChunk(0, c0);
|
|
sql._seedChunk(1, c1);
|
|
|
|
const out = readAllChunks(sql);
|
|
expect(out[0]).toBe(1);
|
|
expect(out[CHUNK_BYTES - 1]).toBe(2);
|
|
expect(out[CHUNK_BYTES]).toBe(3);
|
|
});
|
|
|
|
it('skips orphan rows with chunk_id >= CHUNK_COUNT (canvas-shrink residue)', () => {
|
|
const sql = createFakeSql();
|
|
// Seed a valid chunk and an orphan past the boundary.
|
|
const c0 = new Uint8Array(CHUNK_BYTES);
|
|
c0[0] = 9;
|
|
sql._seedChunk(0, c0);
|
|
const orphan = new Uint8Array(CHUNK_BYTES);
|
|
orphan[0] = 99;
|
|
sql._seedChunk(CHUNK_COUNT + 5, orphan);
|
|
|
|
const out = readAllChunks(sql);
|
|
expect(out.length).toBe(TOTAL_PIXELS);
|
|
expect(out[0]).toBe(9);
|
|
// Orphan would have caused a RangeError on out.set() if not bounded.
|
|
});
|
|
|
|
it('trims oversized blobs at the last chunk to expected size', () => {
|
|
const sql = createFakeSql();
|
|
// Last chunk's "expected" size equals CHUNK_BYTES when TOTAL_PIXELS is a
|
|
// multiple of CHUNK_BYTES (our current case). Seed an oversized blob and
|
|
// verify no overflow into adjacent memory.
|
|
const lastId = CHUNK_COUNT - 1;
|
|
const oversized = new Uint8Array(CHUNK_BYTES + 100);
|
|
oversized[0] = 4;
|
|
oversized[CHUNK_BYTES + 99] = 7; // beyond expected — must be trimmed
|
|
sql._seedChunk(lastId, oversized);
|
|
|
|
// Should not throw.
|
|
const out = readAllChunks(sql);
|
|
expect(out.length).toBe(TOTAL_PIXELS);
|
|
const lastChunkStart = lastId * CHUNK_BYTES;
|
|
expect(out[lastChunkStart]).toBe(4);
|
|
});
|
|
});
|
|
|
|
describe('pixel-to-chunk math (coverage via writePixels)', () => {
|
|
it('maps (x,y) coordinates to the expected chunk index', () => {
|
|
const sql = createFakeSql();
|
|
// Pick a y that crosses chunk boundaries: y=16 with CANVAS_WIDTH=4096
|
|
// gives offset = 65536 → chunk 1, byteOffset 0.
|
|
writePixels(sql, [{ x: 0, y: CHUNK_BYTES / CANVAS_WIDTH, color: 200 }]);
|
|
expect(sql._chunks.size).toBe(1);
|
|
expect(sql._chunks.has(1)).toBe(true);
|
|
expect(readChunk(sql, 1)[0]).toBe(200);
|
|
});
|
|
});
|