test: add test infrastructure for frontend and server

- Frontend: vitest.config.ts with vue plugin + jsdom, dummy component test
- Server: in-memory SQLite test DB factory + Hono testClient helper + smoke test
- CI: add pnpm audit and server coverage threshold steps
- Root: vitest workspace config for multi-project test discovery

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-12 22:30:43 +00:00
co-authored by Claude Opus 4.6
parent 6f0eb92ebb
commit d9e32123fe
11 changed files with 997 additions and 2 deletions
+35
View File
@@ -0,0 +1,35 @@
import { describe, it, expect } from 'vitest'
import { eq } from 'drizzle-orm'
import { createTestDb, insertTestBot } from './db.js'
describe('test helpers', () => {
it('creates in-memory DB, inserts a bot, and queries it back', () => {
const { db, schema } = createTestDb()
const bot = insertTestBot(db, { name: 'SatoshiBot' })
const found = db
.select()
.from(schema.bots)
.where(eq(schema.bots.id, bot.id))
.get()
expect(found).toBeDefined()
expect(found!.name).toBe('SatoshiBot')
expect(found!.eloRating).toBe(1200)
expect(found!.webhookUrl).toBe('http://test.local/webhook')
})
it('each createTestDb is isolated', () => {
const { db: db1, schema: s1 } = createTestDb()
const { db: db2, schema: s2 } = createTestDb()
insertTestBot(db1, { name: 'BotInDb1' })
const inDb1 = db1.select().from(s1.bots).all()
const inDb2 = db2.select().from(s2.bots).all()
expect(inDb1).toHaveLength(1)
expect(inDb2).toHaveLength(0)
})
})
+189
View File
@@ -0,0 +1,189 @@
import Database from 'better-sqlite3'
import { drizzle, type BetterSQLite3Database } from 'drizzle-orm/better-sqlite3'
import * as schema from '../db/schema.js'
/**
* Creates an isolated in-memory SQLite database with full schema.
* Each call returns a fresh DB — no test pollution.
*/
export function createTestDb() {
const sqlite = new Database(':memory:')
sqlite.pragma('journal_mode = WAL')
sqlite.pragma('foreign_keys = ON')
// Create all tables (mirrors startup.ts)
sqlite.exec(`
CREATE TABLE IF NOT EXISTS bots (
id TEXT PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
webhook_url TEXT NOT NULL,
avatar_seed TEXT NOT NULL,
archetype TEXT NOT NULL DEFAULT 'standard',
secret_hash TEXT NOT NULL,
public_key TEXT,
profile_pic_url TEXT,
elo_rating REAL NOT NULL DEFAULT 1200,
wins INTEGER NOT NULL DEFAULT 0,
losses INTEGER NOT NULL DEFAULT 0,
win_streak INTEGER NOT NULL DEFAULT 0,
best_streak INTEGER NOT NULL DEFAULT 0,
tier INTEGER NOT NULL DEFAULT 0,
is_active INTEGER NOT NULL DEFAULT 1,
last_fight_at TEXT,
consecutive_errors INTEGER NOT NULL DEFAULT 0,
last_error_at TEXT,
customization TEXT,
sats_won INTEGER NOT NULL DEFAULT 0,
sats_wagered INTEGER NOT NULL DEFAULT 0,
has_wallet INTEGER NOT NULL DEFAULT 0,
zaps_received INTEGER NOT NULL DEFAULT 0,
bot_type TEXT NOT NULL DEFAULT 'regular',
created_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS fights (
id TEXT PRIMARY KEY,
bot_a_id TEXT NOT NULL REFERENCES bots(id),
bot_b_id TEXT NOT NULL REFERENCES bots(id),
arena TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'scheduled',
winner_id TEXT REFERENCES bots(id),
bot_a_hp INTEGER NOT NULL DEFAULT 200,
bot_b_hp INTEGER NOT NULL DEFAULT 200,
total_rounds INTEGER NOT NULL DEFAULT 0,
scheduled_at TEXT,
started_at TEXT,
ended_at TEXT,
mode TEXT NOT NULL DEFAULT 'free',
pot_sats INTEGER NOT NULL DEFAULT 0,
payout_status TEXT,
current_season TEXT,
created_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS rounds (
id TEXT PRIMARY KEY,
fight_id TEXT NOT NULL REFERENCES fights(id),
round_number INTEGER NOT NULL,
challenge_type TEXT NOT NULL,
challenge_data TEXT NOT NULL,
bot_a_response TEXT,
bot_a_time_ms INTEGER,
bot_a_score REAL,
bot_b_response TEXT,
bot_b_time_ms INTEGER,
bot_b_score REAL,
winner_id TEXT REFERENCES bots(id),
narration TEXT,
created_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS payments (
id TEXT PRIMARY KEY,
fight_id TEXT REFERENCES fights(id),
bot_id TEXT NOT NULL REFERENCES bots(id),
direction TEXT NOT NULL,
amount_sats INTEGER NOT NULL,
method TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'pending',
invoice TEXT,
preimage TEXT,
cashu_token TEXT,
error_reason TEXT,
created_at TEXT NOT NULL,
confirmed_at TEXT,
refunded_at TEXT
);
CREATE TABLE IF NOT EXISTS wallet_connections (
id TEXT PRIMARY KEY,
bot_id TEXT NOT NULL UNIQUE REFERENCES bots(id),
method TEXT NOT NULL,
connection_data TEXT NOT NULL,
created_at TEXT NOT NULL,
last_used_at TEXT
);
CREATE TABLE IF NOT EXISTS bets (
id TEXT PRIMARY KEY,
fight_id TEXT NOT NULL REFERENCES fights(id),
bettor_pubkey TEXT NOT NULL,
bot_id TEXT NOT NULL REFERENCES bots(id),
amount_sats INTEGER NOT NULL,
odds_at_placement REAL NOT NULL,
cashu_token TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'pending',
payout_sats INTEGER,
payout_token TEXT,
created_at TEXT NOT NULL,
settled_at TEXT
);
CREATE TABLE IF NOT EXISTS tournaments (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
format TEXT NOT NULL DEFAULT 'single_elim',
size INTEGER NOT NULL,
entry_sats INTEGER NOT NULL DEFAULT 0,
prize_sats INTEGER NOT NULL DEFAULT 0,
status TEXT NOT NULL DEFAULT 'open',
current_round INTEGER NOT NULL DEFAULT 0,
season_id TEXT,
created_at TEXT NOT NULL,
started_at TEXT,
finished_at TEXT
);
CREATE TABLE IF NOT EXISTS tournament_entries (
id TEXT PRIMARY KEY,
tournament_id TEXT NOT NULL REFERENCES tournaments(id),
bot_id TEXT NOT NULL REFERENCES bots(id),
seed INTEGER NOT NULL DEFAULT 0,
eliminated INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS tournament_matches (
id TEXT PRIMARY KEY,
tournament_id TEXT NOT NULL REFERENCES tournaments(id),
round INTEGER NOT NULL,
match_index INTEGER NOT NULL,
bot_a_id TEXT REFERENCES bots(id),
bot_b_id TEXT REFERENCES bots(id),
fight_id TEXT REFERENCES fights(id),
winner_id TEXT REFERENCES bots(id),
status TEXT NOT NULL DEFAULT 'pending'
);
CREATE TABLE IF NOT EXISTS analytics (
date TEXT NOT NULL,
metric TEXT NOT NULL,
value INTEGER NOT NULL DEFAULT 0
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_analytics_date_metric ON analytics(date, metric);
`)
const db = drizzle(sqlite, { schema })
return { db, sqlite, schema }
}
/** Insert a minimal test bot and return it */
export function insertTestBot(
db: BetterSQLite3Database<typeof schema>,
overrides: Partial<typeof schema.bots.$inferInsert> = {},
) {
const id = overrides.id || `bot-${Math.random().toString(36).slice(2, 8)}`
const bot = {
id,
name: overrides.name || `TestBot-${id.slice(4)}`,
webhookUrl: overrides.webhookUrl || 'http://test.local/webhook',
avatarSeed: overrides.avatarSeed || 'test-seed',
archetype: overrides.archetype || 'standard',
secretHash: overrides.secretHash || 'testhash',
createdAt: overrides.createdAt || new Date().toISOString(),
...overrides,
}
db.insert(schema.bots).values(bot).run()
return bot
}
+31
View File
@@ -0,0 +1,31 @@
import { Hono } from 'hono'
/**
* Test helper for Hono route testing without HTTP server.
* Uses Hono's built-in `app.request()` for direct request simulation.
*/
export function testClient(app: Hono) {
return {
get: (path: string, init?: RequestInit) =>
app.request(path, { method: 'GET', ...init }),
post: (path: string, body?: unknown, init?: RequestInit) =>
app.request(path, {
method: 'POST',
headers: { 'Content-Type': 'application/json', ...init?.headers },
body: body ? JSON.stringify(body) : undefined,
...init,
}),
put: (path: string, body?: unknown, init?: RequestInit) =>
app.request(path, {
method: 'PUT',
headers: { 'Content-Type': 'application/json', ...init?.headers },
body: body ? JSON.stringify(body) : undefined,
...init,
}),
delete: (path: string, init?: RequestInit) =>
app.request(path, { method: 'DELETE', ...init }),
}
}