fix(api): guard task-queue name resolution against corrupted rows (#44)
This commit is contained in:
@@ -550,6 +550,20 @@ describe('snapshot task queue (world / system / crew)', () => {
|
||||
// buildWorldFixture (4a): a save (seed '42') + Origin (s0, current) / Outpost (s1) +
|
||||
// Alder (p0) / Basalt (p1) + the route pair — no crew, no tasks; this block adds its own.
|
||||
|
||||
// insertTaskRow JSON-encodes its arguments, so the corruption tests seed raw strings straight
|
||||
// into the tasks table (same row shape, no encoding) to reach states the encoder can't.
|
||||
async function insertRawTaskRow(
|
||||
db: GameDb,
|
||||
over: { type: string; payload: string; helpers: string; leadCrewId: number; duration: number; remaining: number; status: string; queuedAtTurn: number },
|
||||
): Promise<number> {
|
||||
const [save] = await db.db.select().from(schema.saves).limit(1)
|
||||
const [row] = await db.db
|
||||
.insert(schema.tasks)
|
||||
.values({ saveId: save.id, ...over })
|
||||
.returning({ id: schema.tasks.id })
|
||||
return row.id
|
||||
}
|
||||
|
||||
it('empty queue → every GET snapshot carries tasks: []', async () => {
|
||||
const { db } = await freshFile()
|
||||
await buildWorldFixture(db)
|
||||
@@ -585,6 +599,41 @@ describe('snapshot task queue (world / system / crew)', () => {
|
||||
expect(snap.tasks).toHaveLength(1)
|
||||
expect(snap.tasks[0].type).toBe('EXPLORE') // the DONE row is filtered out, not reordered to the back
|
||||
})
|
||||
|
||||
it('a corrupted payload degrades to targetLabel `unknown` — the snapshot still builds', async () => {
|
||||
const { db } = await freshFile()
|
||||
const f = await buildWorldFixture(db)
|
||||
const [c1, c2] = await addCrew(db, 2)
|
||||
const badId = await insertRawTaskRow(db, { type: 'EXPLORE', payload: 'not-json', helpers: '[]', leadCrewId: c1, duration: 1, remaining: 1, status: 'ACTIVE', queuedAtTurn: 2 })
|
||||
const exploreId = await insertTaskRow(db, { type: 'EXPLORE', payload: { planetId: f.p0 }, leadCrewId: c2, helpers: [], duration: 1, remaining: 1, status: 'ACTIVE', queuedAtTurn: 3 })
|
||||
const snap = await getWorldState(db) // must not throw out of the read
|
||||
expect(snap.tasks).toEqual([
|
||||
{ id: badId, type: 'EXPLORE', targetLabel: 'unknown', leadName: 'CREW 1', helperNames: [], remaining: 1 },
|
||||
{ id: exploreId, type: 'EXPLORE', targetLabel: 'Alder', leadName: 'CREW 2', helperNames: [], remaining: 1 }, // the well-formed sibling still resolves
|
||||
])
|
||||
})
|
||||
|
||||
it('a corrupted helpers blob degrades to helperNames [] — no throw', async () => {
|
||||
const { db } = await freshFile()
|
||||
const f = await buildWorldFixture(db)
|
||||
const [c1] = await addCrew(db, 1)
|
||||
const badId = await insertRawTaskRow(db, { type: 'JUMP', payload: JSON.stringify({ systemId: f.sys1, hops: 1 }), helpers: '[not-json', leadCrewId: c1, duration: 1, remaining: 1, status: 'ACTIVE', queuedAtTurn: 3 })
|
||||
const snap = await getWorldState(db)
|
||||
expect(snap.tasks).toEqual([
|
||||
{ id: badId, type: 'JUMP', targetLabel: 'Outpost', leadName: 'CREW 1', helperNames: [], remaining: 1 },
|
||||
])
|
||||
})
|
||||
|
||||
it('a corrupted type string degrades to targetLabel `unknown` — no throw', async () => {
|
||||
const { db } = await freshFile()
|
||||
const f = await buildWorldFixture(db)
|
||||
const [c1] = await addCrew(db, 1)
|
||||
const badId = await insertRawTaskRow(db, { type: 'TELEPORT', payload: JSON.stringify({ systemId: f.sys1, hops: 1 }), helpers: '[]', leadCrewId: c1, duration: 1, remaining: 1, status: 'ACTIVE', queuedAtTurn: 3 })
|
||||
const snap = await getWorldState(db)
|
||||
expect(snap.tasks).toEqual([
|
||||
{ id: badId, type: 'TELEPORT', targetLabel: 'unknown', leadName: 'CREW 1', helperNames: [], remaining: 1 },
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('backfill (old saves)', () => {
|
||||
|
||||
Reference in New Issue
Block a user