// Handler-level tests for mission accept (Task 5a(ii)): instant and free per the approved // spec (JC-19) — the AVAILABLE → ACTIVE flip and the event land at the current turn, no // turn is spent, no crew params; the board response shape is unchanged. 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 { getMissions } from '@/lib/game/missions' import { buildWorldFixture } 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-mission-accept-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 = (id: string) => POST(new Request(`http://localhost/api/missions/${id}/accept`, { method: 'POST' }), { params: Promise.resolve({ id }) }) // The fixture suite mints no mission rows (JC-6) — insert exactly what the test needs (same // shape as the insertMission helper in lib/game/missions.test.ts, plus a state override). async function insertMission( db: GameDb, fields: { type: string; description: string; targetSystemId?: number | null; conditionKind?: string | null; progressTarget?: number; rewardKind?: string; rewardQty?: number; state?: string }, ): Promise { const [save] = await db.db.select().from(schema.saves).limit(1) const [row] = await db.db .insert(schema.missions) .values({ saveId: save.id, type: fields.type, description: fields.description, targetSystemId: fields.targetSystemId ?? null, conditionKind: fields.conditionKind ?? null, progressTarget: fields.progressTarget ?? 1, rewardKind: fields.rewardKind ?? 'resource', rewardQty: fields.rewardQty ?? 1, state: fields.state ?? 'AVAILABLE' }) .returning() return row.id } describe('POST /api/missions/[id]/accept (5a(ii))', () => { it('happy: mission goes active, board returned, turn unchanged (free)', async () => { const { db } = await freshFile() await buildWorldFixture(db) const id = await insertMission(db, { type: 'DELIVER', description: 'Jump to Outpost', targetSystemId: 2 }) const [before] = await db.db.select().from(schema.saves) const res = await post(String(id)) expect(res.status).toBe(200) expect(await res.json()).toEqual(await getMissions(db)) // same board shape as today expect((await db.db.select().from(schema.missions)).find((m) => m.id === id)!.state).toBe('ACTIVE') // AVAILABLE → active expect((await db.db.select().from(schema.saves))[0].turn).toBe(before.turn) // free — no turn cost }) it('non-integer id → 400, nothing changes', async () => { const { db } = await freshFile() await buildWorldFixture(db) const [before] = await db.db.select().from(schema.saves) expect((await post('abc')).status).toBe(400) expect((await db.db.select().from(schema.saves))[0].turn).toBe(before.turn) }) it('unknown id → 404, nothing changes', async () => { const { db } = await freshFile() await buildWorldFixture(db) const [before] = await db.db.select().from(schema.saves) expect((await post('999')).status).toBe(404) expect((await db.db.select().from(schema.saves))[0].turn).toBe(before.turn) }) it('not AVAILABLE → 409 (existing code), nothing changes', async () => { const { db } = await freshFile() await buildWorldFixture(db) const id = await insertMission(db, { type: 'EXPLORE', description: 'Scan 2 bodies', progressTarget: 2, state: 'ACTIVE' }) const [before] = await db.db.select().from(schema.saves) expect((await post(String(id))).status).toBe(409) expect((await db.db.select().from(schema.saves))[0].turn).toBe(before.turn) }) it('a mission already active → 409 (existing code), the target stays AVAILABLE', async () => { const { db } = await freshFile() await buildWorldFixture(db) await insertMission(db, { type: 'FIND', description: 'Find a part while scanning', conditionKind: 'part', state: 'ACTIVE' }) const id = await insertMission(db, { type: 'EXPLORE', description: 'Scan 2 bodies', progressTarget: 2 }) const [before] = await db.db.select().from(schema.saves) expect((await post(String(id))).status).toBe(409) expect((await db.db.select().from(schema.saves))[0].turn).toBe(before.turn) expect((await db.db.select().from(schema.missions)).find((m) => m.id === id)!.state).toBe('AVAILABLE') }) })