feat(api): ship + research endpoints queue tasks with lead + helpers; mission accept is instant and free (#44)
CI / build + unit + e2e (pull_request) Failing after 3m58s
CI / build + unit + e2e (pull_request) Failing after 3m58s
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
// 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<GameDb> }
|
||||
|
||||
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<number> {
|
||||
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')
|
||||
})
|
||||
})
|
||||
@@ -2,7 +2,8 @@ import { getGameDb } from '@/lib/db/client'
|
||||
import { ensureSave } from '@/lib/game/save'
|
||||
import { acceptMission, MissionError } from '@/lib/game/missions'
|
||||
|
||||
// Accept an AVAILABLE mission — it becomes the single active one. Costs a turn; returns the fresh board.
|
||||
// Accept an AVAILABLE mission — it becomes the single active one. Instant and free (no turn,
|
||||
// no body); returns the fresh board.
|
||||
export async function POST(_req: Request, ctx: { params: Promise<{ id: string }> }) {
|
||||
const db = await getGameDb()
|
||||
await ensureSave(db) // acceptMission assumes a save exists
|
||||
|
||||
Reference in New Issue
Block a user