// Handler-level tests for the manual tick route (Task 5b): POST /api/turn (no body) advances // exactly one turn — an empty queue is a FREE SKIP (200, turn +1, nothing else changes, the // route never rejects), and a mid-countdown ACTIVE task decrements by exactly one in both the // returned snapshot and the persisted row (no resolution, no mission hook — it never hits 0). import { mkdtemp, rm } from 'node:fs/promises' import { tmpdir } from 'node:os' import path from 'node:path' import { afterAll, describe, expect, it } from 'vitest' import { getGameDb, type GameDb } from '@/lib/db/client' import * as schema from '@/lib/db/schema' import { addCrew, buildWorldFixture, insertTaskRow } from '@/lib/game/test-fixtures' import { POST } from './route' const dirs: string[] = [] // The route talks to the app-process singleton (getGameDb), so each test points it at a // fresh temp file and uses that very instance for fixtures and assertions — two in-memory // sql.js copies of one file would never see each other's writes. const g = globalThis as unknown as { __spaceWandererGameDb?: Promise } async function freshFile(): Promise<{ db: GameDb; file: string }> { const dir = await mkdtemp(path.join(tmpdir(), 'sw-turn-route-test-')) dirs.push(dir) const file = path.join(dir, 'test.db') process.env.SPACE_WANDERER_DB_PATH = file delete g.__spaceWandererGameDb return { db: await getGameDb(), file } } afterAll(async () => { delete process.env.SPACE_WANDERER_DB_PATH delete g.__spaceWandererGameDb for (const d of dirs) await rm(d, { recursive: true, force: true }) }) // No request body — the handler never reads one. const post = () => POST() describe('POST /api/turn', () => { it('empty queue → free skip: 200, turn +1, nothing else changes', async () => { const { db } = await freshFile() await buildWorldFixture(db) const [before] = await db.db.select().from(schema.saves) const res = await post() expect(res.status).toBe(200) const snap = await res.json() expect(snap.turn).toBe(before.turn + 1) expect(await db.db.select().from(schema.tasks)).toHaveLength(0) // the tick creates nothing expect(snap.tasks).toEqual([]) // the composed queue array is empty }) it('one ACTIVE task → 200, remaining decrements by exactly 1 in the returned snapshot', async () => { const { db } = await freshFile() const f = await buildWorldFixture(db) const [c1] = await addCrew(db, 1) const [before] = await db.db.select().from(schema.saves) // Fixture task: one ACTIVE row, mid-countdown — a direct insert on the queue table // (the 4b save.test pattern: it works with the queue rows directly); it never reaches 0 // this tick, so no resolver and no mission hook runs await insertTaskRow(db, { type: 'JUMP', payload: { systemId: f.sys1, hops: 1 }, leadCrewId: c1, helpers: [], duration: 5, remaining: 3, status: 'ACTIVE', queuedAtTurn: before.turn }) const res = await post() expect(res.status).toBe(200) const snap = await res.json() expect(snap.turn).toBe(before.turn + 1) expect(snap.tasks).toHaveLength(1) expect(snap.tasks[0].remaining).toBe(2) // 3 − 1: exactly one decrement expect((await db.db.select().from(schema.tasks))[0].remaining).toBe(2) // the persisted row matches the snapshot }) })