Files
Flight-Ops/lib/db/client.test.ts
T

132 lines
4.8 KiB
TypeScript

import fs from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import initSqlJs from 'sql.js';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
import { openGameDb, persist } from './client';
import * as schema from './schema';
// The v0.6.0 DDL — used to build a "legacy" DB file for the migration test.
const LEGACY_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,
contract_id TEXT,
lat REAL NOT NULL,
lon REAL NOT NULL
);
CREATE TABLE contracts (
id TEXT PRIMARY KEY NOT NULL,
origin_icao TEXT NOT NULL,
dest_icao TEXT NOT NULL,
reward REAL NOT NULL,
cargo_type TEXT NOT NULL,
cargo_count INTEGER NOT NULL,
deadline_sim_time INTEGER NOT NULL,
status TEXT NOT NULL,
plane_id TEXT,
created_sim_time INTEGER NOT NULL
);
CREATE TABLE account (
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
);
`;
let dir: string;
beforeAll(async () => {
dir = await fs.mkdtemp(path.join(os.tmpdir(), 'flight-db-'));
});
afterAll(async () => {
await fs.rm(dir, { recursive: true, force: true });
});
describe('openGameDb', () => {
it('seeds a fresh DB file on first open and persists it to disk', async () => {
const file = path.join(dir, 'fresh.db');
const inst = await openGameDb(file);
const planes = await inst.db.select().from(schema.planes);
expect(planes).toHaveLength(1);
expect(planes[0].id).toBe('plane-starter');
expect(planes[0].status).toBe('available');
expect(planes[0].locationIcao).toBe('EDDF'); // Frankfurt hub from the CSV
expect(planes[0].fuel).toBe(planes[0].maxFuel);
const contracts = await inst.db.select().from(schema.contracts);
expect(contracts.length).toBeGreaterThanOrEqual(8);
expect(contracts.every((c) => c.status === 'open')).toBe(true);
const account = (await inst.db.select().from(schema.account))[0];
expect(account?.cash).toBeGreaterThan(0);
const sim = (await inst.db.select().from(schema.simState))[0];
expect(sim?.paused).toBe(false);
expect(sim?.timeScale).toBe(1);
// The seed was durably written: the file exists and is non-empty.
const st = await fs.stat(file);
expect(st.size).toBeGreaterThan(0);
});
it('does not re-seed an existing DB — state survives reopen', async () => {
const file = path.join(dir, 'reopen.db');
const first = await openGameDb(file);
const contracts1 = (await first.db.select().from(schema.contracts)).map((c) => c.id).sort();
const account1 = (await first.db.select().from(schema.account))[0];
// Simulate a process restart: brand-new instance over the same file.
const second = await openGameDb(file);
const contracts2 = (await second.db.select().from(schema.contracts)).map((c) => c.id).sort();
expect(contracts2).toEqual(contracts1);
expect((await second.db.select().from(schema.account))[0]).toEqual(account1);
});
it('persist() durably saves writes made after open', async () => {
const file = path.join(dir, 'write.db');
const inst = await openGameDb(file);
await inst.db.update(schema.account).set({ cash: 12345 });
await persist(inst);
// A fresh instance over the same file sees the update.
const reloaded = await openGameDb(file);
expect((await reloaded.db.select().from(schema.account))[0]?.cash).toBe(12345);
});
it('migrates a pre-v0.7.0 database: adds new columns and fixes last_real_tick_ms', async () => {
// Build a legacy file with the v0.6.0 schema (no departure_sim_time / last_real_tick_ms).
const SQL = await initSqlJs();
const raw = new SQL.Database();
raw.exec(LEGACY_DDL);
raw.exec(`INSERT INTO planes VALUES ('plane-starter', 'atr72', 'available', 6300, 6300, 'EDDF', NULL, 50.03, 8.57)`);
raw.exec(`INSERT INTO account VALUES (20000, 10)`);
raw.exec('INSERT INTO sim_state VALUES (1750000000000, 1, 0)');
const file = path.join(dir, 'legacy.db');
await fs.writeFile(file, Buffer.from(raw.export()));
const inst = await openGameDb(file);
// Not re-seeded (account row exists)…
expect((await inst.db.select().from(schema.account))[0]?.cash).toBe(20000);
// …but the new columns exist and are usable…
const plane = (await inst.db.select().from(schema.planes))[0];
expect(plane?.departureSimTime ?? null).toBeNull();
const sim = (await inst.db.select().from(schema.simState))[0];
expect(sim?.clockMs).toBe(1_750_000_000_000);
// …and the ALTER-default 0 was fixed up to a real timestamp so the first
// tick-on-read resumes cleanly instead of taking one clamped catch-up jump.
expect(sim?.lastRealTickMs).toBeGreaterThan(0);
});
});