db+game: schema_version on saves with ordered idempotent backfill (v2 bodies, v3 crew) — no more DB wipes
This commit is contained in:
+66
-1
@@ -2,10 +2,11 @@ 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 { eq } from 'drizzle-orm'
|
||||
import { openGameDb, type GameDb } from '@/lib/db/client'
|
||||
import * as schema from '@/lib/db/schema'
|
||||
import { generateStartingCrew, rollCrewMember, startingCrewRng } from './crew'
|
||||
import { getCrewState, getSystemState, explorePlanet, getWorldState, jumpToSystem, type WorldState } from './save'
|
||||
import { SCHEMA_VERSION, getCrewState, getSystemState, explorePlanet, getWorldState, jumpToSystem, type WorldState } from './save'
|
||||
import { bodyRng, explorationRng, generatePlanets, rollExploration } from './world'
|
||||
|
||||
const dirs: string[] = []
|
||||
@@ -300,3 +301,67 @@ describe('explorePlanet crew finds', () => {
|
||||
expect((await getCrewState(db)).length).toBe(before.length + 1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('backfill (old saves)', () => {
|
||||
// Planets/moons of one system in insertion order, nested by parent — ids are excluded so a
|
||||
// regenerated set compares equal to the original even though autoincrement moved on.
|
||||
function bodyShape(rows: { id: number; name: string; type: string; hazard: boolean; parentId: number | null }[]) {
|
||||
return rows
|
||||
.filter((r) => r.parentId === null)
|
||||
.map((p) => ({ name: p.name, type: p.type, hazard: p.hazard, moons: rows.filter((m) => m.parentId === p.id).map((m) => [m.name, m.type, m.hazard]) }))
|
||||
}
|
||||
|
||||
it('upgrades a v1 save (no bodies, no crew) in place — deterministic and idempotent', async () => {
|
||||
const { db } = await freshFile()
|
||||
await getWorldState(db) // new game: everything seeded at the current version
|
||||
|
||||
const systems = await db.db.select().from(schema.solarSystems)
|
||||
const rowsFor = (systemId: number) => db.db.select().from(schema.planets).where(eq(schema.planets.systemId, systemId))
|
||||
const beforeBodies: Record<number, unknown> = {}
|
||||
for (const s of systems) {
|
||||
const rows = await rowsFor(s.id)
|
||||
beforeBodies[s.id] = bodyShape(rows.sort((a, b) => a.id - b.id))
|
||||
}
|
||||
const crewOf = (rows: { id: number; name: string; role: string; traitGood: string; traitBad: string; origin: string }[]) =>
|
||||
rows.map((c) => ({ name: c.name, role: c.role, traitGood: c.traitGood, traitBad: c.traitBad, origin: c.origin })) // ids excluded — re-seed moves autoincrement on
|
||||
const beforeCrew = crewOf(await db.db.select().from(schema.crew))
|
||||
|
||||
// Simulate a save created by pre-v0.4 code: no bodies, no crew, no version stamp (NULL).
|
||||
await db.db.delete(schema.planets)
|
||||
await db.db.delete(schema.crew)
|
||||
await db.db.update(schema.saves).set({ schemaVersion: null })
|
||||
|
||||
const world = await getWorldState(db) // ensureSave → backfill runs
|
||||
expect(world.systems).toHaveLength(7)
|
||||
|
||||
for (const s of systems) {
|
||||
const rows = await rowsFor(s.id)
|
||||
expect(bodyShape(rows.sort((a, b) => a.id - b.id))).toEqual(beforeBodies[s.id]) // identical to fresh generation
|
||||
}
|
||||
expect(crewOf(await db.db.select().from(schema.crew))).toEqual(beforeCrew)
|
||||
|
||||
const [save] = await db.db.select().from(schema.saves)
|
||||
expect(save.schemaVersion).toBe(SCHEMA_VERSION)
|
||||
|
||||
// Idempotent: a second pass changes nothing.
|
||||
const planetCount = (await db.db.select().from(schema.planets)).length
|
||||
await getWorldState(db)
|
||||
expect((await db.db.select().from(schema.planets)).length).toBe(planetCount)
|
||||
expect((await db.db.select().from(schema.crew)).length).toBe(beforeCrew.length)
|
||||
for (const s of systems) {
|
||||
const rows = await rowsFor(s.id)
|
||||
expect(bodyShape(rows.sort((a, b) => a.id - b.id))).toEqual(beforeBodies[s.id])
|
||||
}
|
||||
})
|
||||
|
||||
it('leaves an up-to-date save untouched', async () => {
|
||||
const { db } = await freshFile()
|
||||
await getWorldState(db)
|
||||
const planetCount = (await db.db.select().from(schema.planets)).length
|
||||
const [save] = await db.db.select().from(schema.saves)
|
||||
expect(save.schemaVersion).toBe(SCHEMA_VERSION)
|
||||
|
||||
await getWorldState(db) // second read: no backfill work to do
|
||||
expect((await db.db.select().from(schema.planets)).length).toBe(planetCount)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user