deadhead-space-freight-tycoon/packages/schema/test/save.test.ts

128 lines
5.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { describe, expect, it } from "vitest";
import {
applyMigrations,
CURRENT_SAVE_VERSION,
migrateSave,
saveDocumentSchema,
type SaveMigration,
} from "../src/index";
import { fullSave } from "./save-fixture";
describe("save document (Step 14.2)", () => {
it("validates a fully populated save", () => {
expect(saveDocumentSchema.safeParse(fullSave()).success).toBe(true);
expect(CURRENT_SAVE_VERSION).toBe(3);
});
it("rejects an invalid save with a readable error naming the field", () => {
const broken = fullSave() as unknown as { company: { credits: unknown } };
broken.company.credits = "lots";
const result = migrateSave(broken);
expect(result.ok).toBe(false);
if (!result.ok) {
expect(result.error).toContain("The save is damaged");
expect(result.error).toContain("company.credits");
}
});
it("rejects things that aren't saves, and saves from a newer build", () => {
expect(migrateSave("hello")).toEqual({ ok: false, error: "The save isn't a save document." });
expect(migrateSave({ company: {} })).toEqual({
ok: false,
error: "The save has no valid saveVersion.",
});
const future = migrateSave({ ...fullSave(), saveVersion: CURRENT_SAVE_VERSION + 1 });
expect(future.ok).toBe(false);
if (!future.ok) expect(future.error).toContain("newer version");
});
it("reads a current save as it is", () => {
const result = migrateSave(JSON.parse(JSON.stringify(fullSave())));
expect(result).toEqual({ ok: true, save: fullSave() });
});
it("chains migrations in order, one version at a time", () => {
const seen: number[] = [];
const migrations: SaveMigration[] = [
{ from: 2, migrate: (s) => (seen.push(2), { ...s, extra: [...(s.extra as number[]), 2] }) },
{ from: 1, migrate: (s) => (seen.push(1), { ...s, extra: [1] }) },
];
const result = applyMigrations({ saveVersion: 1 }, migrations, 3);
expect(result).toEqual({ ok: true, save: { saveVersion: 3, extra: [1, 2] } });
expect(seen).toEqual([1, 2]);
expect(applyMigrations({ saveVersion: 1 }, [], 2)).toEqual({
ok: false,
error: "No migration from save version 1.",
});
});
});
/** The fixture as a version-1 save: before fuel tanks (CR-015). */
type Loose = Record<string, unknown> & {
ships: Record<string, unknown>[];
dispatches: { record: Record<string, unknown>; resolution: Record<string, unknown> | null }[];
};
function versionOne(): Record<string, unknown> {
const save = JSON.parse(JSON.stringify(fullSave())) as Loose;
save.saveVersion = 1;
for (const ship of save.ships) {
delete ship.fuelFu;
delete ship.fuelAvgPrice;
delete ship.refuelPolicy;
}
for (const dispatch of save.dispatches) {
delete dispatch.record.fuelBurnedFu;
if (dispatch.resolution) delete dispatch.resolution.topUp;
}
return save;
}
describe("save migration 1 → 2: fuel tanks (CR-015 §5)", () => {
it("fills every ship's tank and changes nothing else (then 2 → 3 puts it on Manual)", () => {
const result = migrateSave(versionOne());
if (!result.ok) throw new Error(result.error);
const { save } = result;
const before = fullSave();
expect(save.saveVersion).toBe(3);
// The Courier has Extended Fuel Tanks 1 (40 fu × 4.8) and is in flight: the base
// price, 5 cr/fu since CR-018 (decision 0.5.110).
expect(save.ships[0]).toMatchObject({ fuelFu: 192, fuelAvgPrice: 5, refuelPolicy: "manual" });
// The Clipper is at Earth: a full 1,170 fu at Earth's 8 cr/fu.
expect(save.ships[1]).toMatchObject({ fuelFu: 1170, fuelAvgPrice: 8, refuelPolicy: "manual" });
// Earlier trips paid fuel in credits: nothing came out of a tank, nothing topped up.
for (const dispatch of save.dispatches) {
expect(dispatch.record.fuelBurnedFu).toBe(0);
if (dispatch.resolution) expect(dispatch.resolution.topUp).toBeNull();
}
// In-flight dispatches keep their arrival times; everything else is as it was.
expect(save.dispatches.map((d) => d.arrivalAt)).toEqual(
before.dispatches.map((d) => d.arrivalAt),
);
expect(save.company).toEqual(before.company);
expect(save.pilots).toEqual(before.pilots);
});
});
describe("save migration 2 → 3: Manual refueling (CR-018 §3)", () => {
it("puts every ship on Manual and leaves fuel and credits as they were", () => {
const before = fullSave();
const old = JSON.parse(JSON.stringify(before)) as Record<string, unknown> & {
ships: Record<string, unknown>[];
};
old.saveVersion = 2;
old.ships[0]!.refuelPolicy = "auto";
old.ships[1]!.refuelPolicy = "auto";
const result = migrateSave(old);
if (!result.ok) throw new Error(result.error);
const { save } = result;
expect(save.saveVersion).toBe(3);
expect(save.ships.map((s) => s.refuelPolicy)).toEqual(save.ships.map(() => "manual"));
expect(save.ships.map((s) => [s.fuelFu, s.fuelAvgPrice])).toEqual(
before.ships.map((s) => [s.fuelFu, s.fuelAvgPrice]),
);
expect(save.company).toEqual(before.company);
expect({ ...save, ships: [] }).toEqual({ ...before, ships: [] });
});
});