v1.2.0: planes.owner_id + ownership checks + per-player onboarding (ISS-108, ISS-109)
CI / build + unit + e2e (pull_request) Successful in 13m46s
CI / build + unit + e2e (pull_request) Successful in 13m46s
This commit is contained in:
@@ -2,6 +2,7 @@ import fs from 'node:fs/promises';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import initSqlJs from 'sql.js';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
||||
import { WORLD_TIME_SCALE } from '@/lib/worldClock';
|
||||
import { STARTING_CASH, STARTING_CLOCK_MS, STARTING_REPUTATION } from '@/lib/game/seed';
|
||||
@@ -88,6 +89,40 @@ CREATE TABLE sim_state (
|
||||
);
|
||||
`;
|
||||
|
||||
// The v1.1.0 DDL — the last schema before ownership (ISS-109); used to build a "pre-v1.2.0" DB
|
||||
// file for the owner_id backfill migration test.
|
||||
const PRE_OWNERSHIP_DDL = `
|
||||
CREATE TABLE planes (
|
||||
id TEXT PRIMARY KEY NOT NULL,
|
||||
model TEXT NOT NULL,
|
||||
status TEXT NOT NULL,
|
||||
fuel REAL NOT NULL,
|
||||
max_fuel REAL NOT NULL,
|
||||
location_icao TEXT NOT NULL,
|
||||
dest_icao TEXT,
|
||||
lat REAL NOT NULL,
|
||||
lon REAL NOT NULL,
|
||||
departure_sim_time INTEGER,
|
||||
queue TEXT NOT NULL DEFAULT '[]',
|
||||
health REAL NOT NULL DEFAULT 100,
|
||||
repair_until_sim_time INTEGER
|
||||
);
|
||||
CREATE TABLE players (
|
||||
id TEXT PRIMARY KEY NOT NULL,
|
||||
provider TEXT NOT NULL,
|
||||
username TEXT NOT NULL,
|
||||
cash REAL NOT NULL,
|
||||
reputation REAL NOT NULL
|
||||
);
|
||||
CREATE TABLE sim_state (
|
||||
clock_ms INTEGER NOT NULL,
|
||||
time_scale REAL NOT NULL,
|
||||
paused INTEGER NOT NULL,
|
||||
last_real_tick_ms INTEGER NOT NULL DEFAULT 0,
|
||||
last_selected_time_scale REAL NOT NULL DEFAULT 1
|
||||
);
|
||||
`;
|
||||
|
||||
let dir: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
@@ -350,4 +385,32 @@ describe('openGameDb', () => {
|
||||
expect(tables.has('account')).toBe(false);
|
||||
expect(Number(second.raw.exec('SELECT COUNT(*) FROM players')[0].values[0][0])).toBe(1);
|
||||
});
|
||||
|
||||
it('migrates a pre-v1.2.0 database: backfills planes.owner_id to the local player (one-shot)', async () => {
|
||||
// A v1.1.0-era file: two planes without owner_id, the seeded local player row, live sim clock.
|
||||
const SQL = await initSqlJs();
|
||||
const raw = new SQL.Database();
|
||||
raw.exec(PRE_OWNERSHIP_DDL);
|
||||
raw.exec(`INSERT INTO planes VALUES ('plane-starter', 'atr72', 'available', 6300, 6300, 'EDDF', NULL, 50.03, 8.57, NULL, '[]', 100, NULL)`);
|
||||
raw.exec(`INSERT INTO planes VALUES ('plane-bought', 'atr72', 'available', 6300, 6300, 'LEMD', NULL, 40.4, -3.6, NULL, '[]', 100, NULL)`);
|
||||
raw.exec(`INSERT INTO players VALUES ('local', 'local', 'kamilkrzywda', 54020.34, 62.57)`);
|
||||
raw.exec('INSERT INTO sim_state VALUES (1789706016605, 1, 0, 1789718713288, 1)');
|
||||
const file = path.join(dir, 'pre-ownership.db');
|
||||
await fs.writeFile(file, Buffer.from(raw.export()));
|
||||
|
||||
const inst = await openGameDb(file);
|
||||
|
||||
// Every legacy plane is backfilled to the local player…
|
||||
const owners = (await inst.db.select().from(schema.planes)).map((p) => p.ownerId).sort();
|
||||
expect(owners).toEqual(['local', 'local']);
|
||||
// …and the world was NOT re-seeded (the players row already existed).
|
||||
expect(await getLocalPlayer(inst)).toEqual({ ...LOCAL_PLAYER_ROW, cash: 54020.34, reputation: 62.57 });
|
||||
|
||||
// Second open is a no-op: an owner change made after the migration survives a reopen.
|
||||
await inst.db.update(schema.planes).set({ ownerId: 'someone-else' }).where(eq(schema.planes.id, 'plane-bought'));
|
||||
await persist(inst);
|
||||
const second = await openGameDb(file);
|
||||
const owners2 = (await second.db.select().from(schema.planes)).map((p) => p.ownerId).sort();
|
||||
expect(owners2).toEqual(['local', 'someone-else']);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user