// Handler-level tests for the queueing explore route (Task 5a): POST /api/explore with a crew // team queues an EXPLORE row instead of resolving instantly — same system snapshot (findings key // kept, empty), same pre-check codes, no turn spent at queue time. import { eq } from 'drizzle-orm' 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 { getSystemState, type SystemState } from '@/lib/game/save' 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-explore-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 }) }) const post = (body: unknown) => POST(new Request('http://localhost/api/explore', { method: 'POST', body: JSON.stringify(body) })) describe('POST /api/explore (5a)', () => { it('queues an EXPLORE: row with type/payload, turn unchanged, planet not yet scanned, no haul', async () => { const { db } = await freshFile() const f = await buildWorldFixture(db) const [c1, c2] = await addCrew(db, 2) const [before] = await db.db.select().from(schema.saves) const invBefore = (await db.db.select().from(schema.inventoryItems)).map((i) => [i.kind, i.name, i.qty] as const) const res = await post({ planetId: f.p0, leadCrewId: c1, helperCrewIds: [c2] }) expect(res.status).toBe(200) const body = await res.json() const sys = await getSystemState(db) for (const k of Object.keys(sys) as (keyof SystemState)[]) expect(body[k]).toEqual(sys[k]) // today's system snapshot part unchanged; the findings key stays, empty (JC-2) const [row] = await db.db.select().from(schema.tasks) expect(row).toMatchObject({ type: 'EXPLORE', leadCrewId: c1, status: 'ACTIVE', queuedAtTurn: before.turn }) expect(JSON.parse(row.helpers)).toEqual([c2]) // helpers is a JSON text column expect(JSON.parse(row.payload)).toEqual({ planetId: f.p0 }) expect((await db.db.select().from(schema.saves))[0].turn).toBe(before.turn) expect((await db.db.select().from(schema.planets)).find((p) => p.id === f.p0)!.explored).toBe(false) // the effect waits for the tick expect((await db.db.select().from(schema.inventoryItems)).map((i) => [i.kind, i.name, i.qty] as const)).toEqual(invBefore) // no haul yet }) it('unknown crew → 404 (queueTask)', async () => { const { db } = await freshFile() const f = await buildWorldFixture(db) expect((await post({ planetId: f.p0, leadCrewId: 999 })).status).toBe(404) expect(await db.db.select().from(schema.tasks)).toHaveLength(0) }) it('busy helper → 409, nothing queued', async () => { const { db } = await freshFile() const f = await buildWorldFixture(db) const [c1, c2] = await addCrew(db, 2) await insertTaskRow(db, { type: 'EXPLORE', payload: { planetId: f.p1 }, leadCrewId: c2, helpers: [], duration: 2, remaining: 2, status: 'ACTIVE', queuedAtTurn: 1 }) expect((await post({ planetId: f.p0, leadCrewId: c1, helperCrewIds: [c2] })).status).toBe(409) expect(await db.db.select().from(schema.tasks)).toHaveLength(1) }) it('already scanned → 409 (existing pre-check, existing code)', async () => { const { db } = await freshFile() const f = await buildWorldFixture(db) const [c1] = await addCrew(db, 1) await db.db.update(schema.planets).set({ explored: true }).where(eq(schema.planets.id, f.p0)) expect((await post({ planetId: f.p0, leadCrewId: c1 })).status).toBe(409) expect(await db.db.select().from(schema.tasks)).toHaveLength(0) }) })