clone

an rts arena for scripts: submit tribe strategies over the web or mcp, run deterministic matches, watch them

main
tribes / worker / test / fake-d1.ts
fake-d1.ts 4.5K
1/**2 * Minimal D1Database adapter over node:sqlite (Node >= 22) for integration3 * tests: real SQL, real migrations, no wrangler. Implements exactly the4 * surface the worker uses — prepare/bind/first/all/run/batch — with D1's5 * conventions (numbered ?N params bound positionally, meta.changes, batch as6 * one transaction that rolls back atomically on failure).7 */8import { DatabaseSync } from 'node:sqlite';9import { readFileSync } from 'node:fs';10import { fileURLToPath } from 'node:url';1112const MIGRATIONS = ['0001_init.sql', '0002_sim_metrics.sql', '0003_social.sql', '0004_r2_metrics.sql', '0005_keypair_auth.sql', '0006_link_codes.sql', '0007_name_cooldown.sql'];1314type Param = string | number | null | Uint8Array;1516function normalizeParam(v: unknown): Param {17 if (v === undefined || v === null) return null;18 if (typeof v === 'boolean') return v ? 1 : 0;19 if (v instanceof ArrayBuffer) return new Uint8Array(v);20 return v as Param;21}2223class FakeStatement {24 constructor(25 private readonly db: DatabaseSync,26 private readonly sql: string,27 private readonly params: Param[] = [],28 ) {}2930 bind(...params: unknown[]): FakeStatement {31 return new FakeStatement(this.db, this.sql, params.map(normalizeParam));32 }3334 async first<T>(): Promise<T | null> {35 const row = this.db.prepare(this.sql).get(...this.params);36 return (row as T | undefined) ?? null;37 }3839 async all<T>(): Promise<{ results: T[]; success: true; meta: Record<string, number> }> {40 const results = this.db.prepare(this.sql).all(...this.params) as T[];41 return { results, success: true, meta: {} };42 }4344 async run(): Promise<{ success: true; results: unknown[]; meta: { changes: number; last_row_id: number } }> {45 const info = this.db.prepare(this.sql).run(...this.params);46 return {47 success: true,48 results: [],49 meta: { changes: Number(info.changes), last_row_id: Number(info.lastInsertRowid) },50 };51 }5253 /** Executes inside an open transaction (batch). */54 runSync(): { success: true; results: unknown[]; meta: { changes: number; last_row_id: number } } {55 const info = this.db.prepare(this.sql).run(...this.params);56 return {57 success: true,58 results: [],59 meta: { changes: Number(info.changes), last_row_id: Number(info.lastInsertRowid) },60 };61 }62}6364class FakeD1 {65 constructor(private readonly db: DatabaseSync) {}6667 prepare(sql: string): FakeStatement {68 return new FakeStatement(this.db, sql);69 }7071 async batch(stmts: FakeStatement[]): Promise<Array<ReturnType<FakeStatement['runSync']>>> {72 this.db.exec('BEGIN');73 try {74 const results = stmts.map((s) => s.runSync());75 this.db.exec('COMMIT');76 return results;77 } catch (err) {78 this.db.exec('ROLLBACK');79 throw err;80 }81 }82}8384/** Fresh in-memory DB with all migrations applied. Cast to D1Database. */85export function createFakeD1(): D1Database {86 const raw = new DatabaseSync(':memory:');87 raw.exec('PRAGMA foreign_keys = ON;');88 const dir = fileURLToPath(new URL('../../migrations/', import.meta.url));89 for (const file of MIGRATIONS) raw.exec(readFileSync(dir + file, 'utf8'));90 return new FakeD1(raw) as unknown as D1Database;91}9293/**94 * Map-backed fake R2 bucket — the subset the worker uses (put/get/head/95 * delete with string-array support). Cast to R2Bucket in tests.96 */97export function createFakeR2(): {98 bucket: unknown;99 store: Map<string, Uint8Array>;100} {101 const store = new Map<string, Uint8Array>();102 const toBytes = (v: unknown): Uint8Array => {103 if (typeof v === 'string') return new TextEncoder().encode(v);104 if (v instanceof Uint8Array) return new Uint8Array(v);105 if (v instanceof ArrayBuffer) return new Uint8Array(v);106 throw new Error('fake R2: unsupported value type');107 };108 const obj = (bytes: Uint8Array): unknown => ({109 text: async () => new TextDecoder().decode(bytes),110 arrayBuffer: async () =>111 bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength),112 });113 const bucket = {114 async put(key: string, value: unknown): Promise<unknown> {115 store.set(key, toBytes(value));116 return {};117 },118 async get(key: string): Promise<unknown> {119 const bytes = store.get(key);120 return bytes === undefined ? null : obj(bytes);121 },122 async head(key: string): Promise<unknown> {123 return store.has(key) ? {} : null;124 },125 async delete(keys: string | string[]): Promise<void> {126 for (const k of Array.isArray(keys) ? keys : [keys]) store.delete(k);127 },128 };129 return { bucket, store };130}