import { eq } from 'drizzle-orm'; import { getGameDb, persist } from '@/lib/db/client'; import { LOCAL_PLAYER } from '@/lib/db/localPlayer'; import * as schema from '@/lib/db/schema'; import { getStateSnapshot } from '@/lib/game/state'; import { tickNow } from '@/lib/sim/engine'; // TEST-ONLY hook (v1.7.0, ISS-116) — deletes every plane owned by a player so the e2e suite can // remove planes no API path can get rid of: a minted second account's onboarding starter is its // LAST plane, and the per-owner last-plane guard (v1.2.0, ISS-109) makes it unsellable. Leftover // foreign planes would break later specs' single-fleet self-heal (two-planes.spec.ts sells every // non-starter as local → 403). Disabled unless FLIGHT_TEST_HOOKS=1; sibling of /api/test/set-fuel — // same gate, same conventions. // // POST /api/test/delete-plane // body { username: string } — required. Deletes all planes owned by that player's row (any bound // packages are unbound first) and returns the full state snapshot after ticking, like siblings. // Idempotent: an unknown username deletes nothing and still 200s, so cleanup can run unconditionally. export async function POST(req: Request) { if (process.env.FLIGHT_TEST_HOOKS !== '1') { return Response.json({ error: 'test hooks disabled' }, { status: 404 }); } let body: unknown = {}; try { const text = await req.text(); body = text ? JSON.parse(text) : {}; } catch { return Response.json({ error: 'invalid JSON body' }, { status: 400 }); } if (typeof body !== 'object' || body === null || Array.isArray(body)) { return Response.json({ error: 'body must be a JSON object' }, { status: 400 }); } const input = body as Record; for (const key of Object.keys(input)) { if (key !== 'username') { return Response.json({ error: `unknown field "${key}"` }, { status: 400 }); } } const username = input.username; if (typeof username !== 'string' || username.length === 0) { return Response.json({ error: 'username is required and must be a non-empty string' }, { status: 400 }); } try { const inst = await getGameDb(); await tickNow(); // clock is current before the snapshot we return (same as siblings) const players = await inst.db.select().from(schema.players).where(eq(schema.players.username, username)); let deleted = 0; if (players[0]) { const owned = await inst.db.select().from(schema.planes).where(eq(schema.planes.ownerId, players[0].id)); for (const plane of owned) { // Free any bound packages' slots — a deleted plane must never leave dangling planeIds. await inst.db.update(schema.packages).set({ planeId: null }).where(eq(schema.packages.planeId, plane.id)); await inst.db.delete(schema.planes).where(eq(schema.planes.id, plane.id)); deleted++; } } if (deleted > 0) { await persist(inst); } // v1.3.0 (ISS-110): admin-style hook — no session; the snapshot reports the seeded local account. return Response.json({ deleted, ...(await getStateSnapshot(LOCAL_PLAYER.id)) }); } catch (err) { console.error('test hook failed', err); return Response.json({ error: 'test hook failed' }, { status: 500 }); } }