nte-optimizer/tests/import.test.ts
goober 91952b1745 feat: NTE gear optimizer web app
Client-side optimizer for Neverness to Everness console builds. Imports a
gear export from nte-history-exporter, keeps it in IndexedDB, and solves for
the best module placement per character or across a team.

- domain/: pure ports of the Python research scripts (shapes, cartridges,
  board tiling, stats, scoring, set bonuses, Arc effects, sheet prediction).
  No DOM, no storage imports, so they stay worker-safe and Node-testable.
- solver/: single-character solve with branch and bound, plus a three-phase
  team solve (portfolio, leximin, column generation) over disjoint item sets.
  Runs in a Worker when one can be constructed, inline otherwise.
- db/: three storage tiers selected by use rather than feature detection,
  fail-closed import validation, and undo that restores both sides of a steal.
- ui/: React 19 views for items, characters and teams; the board is drawn as
  the deliverable rather than a score.
- Two build targets: a hosted bundle and a single self-contained nte.html
  with no external requests.

Values the model cannot measure are reported as unavailable rather than
guessed: cartridge set bonuses, the level 80 base-stat multiplier, and the
distinction between "optimal for this packing" and globally optimal.

145 tests, tsc clean, both build targets clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 11:38:17 +03:00

171 lines
6.2 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { emptyCharacter } from "../src/db/schema.ts";
import { mergeCharacters, parseGearExport } from "../src/db/import.ts";
const item = (over: Record<string, unknown> = {}) => ({
instance: "a1",
kind: "module",
item_id: "cell3_style6_1_Orange",
shape: "cell3_style6",
level: 20,
rarity: "orange",
main_stats: [{ stat: "HPMaxAdd", value: 840 }],
substats: [{ stat: "CritBase", value: 0.06 }],
owner_group: null,
...over,
});
const gearExport = (over: Record<string, unknown> = {}) => ({
format: "nte-gear-export",
format_version: 1,
scan: { cartridges: 0, modules: 1, characters: 0, characters_with_loadouts: 0, warnings: [] },
exporter: { name: "nte-history-exporter", version: "0.4.0" },
user_uid: "100200300",
characters: [],
items: [item()],
...over,
});
describe("gear export import", () => {
it("accepts a well-formed export", () => {
const result = parseGearExport(gearExport());
expect(result.ok).toBe(true);
expect(result.report.parsed).toBe(1);
expect(result.report.rejected).toBe(0);
expect(result.rows!.items[0]!.cells).toBe(3);
expect(result.rows!.userUid).toBe("100200300");
expect(result.rows!.exporterVersion).toBe("0.4.0");
});
it("refuses a file that is not a gear export", () => {
expect(parseGearExport({ format: "nte-achievement-export" }).ok).toBe(false);
expect(parseGearExport(gearExport({ format_version: 2 })).ok).toBe(false);
expect(parseGearExport("not json at all").ok).toBe(false);
});
it("writes nothing at all when one record is bad", () => {
// Fail closed: a half-imported account is worse than a refused import.
const result = parseGearExport(
gearExport({
scan: { cartridges: 0, modules: 2 },
items: [item(), item({ instance: "a2", level: 99 })],
}),
);
expect(result.ok).toBe(false);
expect(result.rows).toBeUndefined();
expect(result.report.rejected).toBe(1);
expect(result.report.problems.map((p) => p.code)).toContain("bad_level");
});
it("reports a diff rather than a silent count", () => {
const result = parseGearExport(
gearExport({ scan: { cartridges: 0, modules: 3 }, items: [item()] }),
);
expect(result.ok).toBe(false);
expect(result.report.expected).toBe(3);
expect(result.report.parsed).toBe(1);
expect(result.report.problems.map((p) => p.code)).toContain("count_mismatch");
});
it("rejects a duplicate instance instead of overwriting it", () => {
// `instance` is the primary key, so a duplicate would silently vanish.
const result = parseGearExport(
gearExport({ scan: { cartridges: 0, modules: 2 }, items: [item(), item()] }),
);
expect(result.ok).toBe(false);
expect(result.report.problems.map((p) => p.code)).toContain("duplicate_instance");
});
it("rejects a stat the model has no slot for", () => {
const result = parseGearExport(
gearExport({ items: [item({ substats: [{ stat: "MadeUpStat", value: 1 }] })] }),
);
expect(result.ok).toBe(false);
expect(result.report.problems.map((p) => p.code)).toContain("unknown_stat");
});
it("allows a null main-stat value but not a null substat", () => {
// The game never transmits a main stat's displayed value.
expect(
parseGearExport(gearExport({ items: [item({ main_stats: [{ stat: "HPMaxAdd", value: null }] })] }))
.ok,
).toBe(true);
expect(
parseGearExport(gearExport({ items: [item({ substats: [{ stat: "CritBase", value: null }] })] }))
.ok,
).toBe(false);
});
it("rejects a module whose shape is unknown", () => {
const result = parseGearExport(gearExport({ items: [item({ shape: "cell9_style9" })] }));
expect(result.ok).toBe(false);
expect(result.report.problems.map((p) => p.code)).toContain("unknown_shape");
});
it("resolves a cartridge's set from its packet id", () => {
const result = parseGearExport(
gearExport({
scan: { cartridges: 1, modules: 0 },
items: [
item({
instance: "c1",
kind: "cartridge",
item_id: "Psyche_orange",
shape: undefined,
main_stats: [{ stat: "CritDamageBase", value: 0.6 }],
}),
],
}),
);
expect(result.ok).toBe(true);
// Psyche, not Blood: the id the game actually uses.
expect(result.rows!.items[0]!.set).toBe("Devil's Blood: Curse");
});
it("turns an owner group into an equipment row without naming a character", () => {
const result = parseGearExport(gearExport({ items: [item({ owner_group: "0000000d0d" })] }));
expect(result.ok).toBe(true);
const [row] = result.rows!.equipment;
expect(row!.ownerGroup).toBe("0000000d0d");
// The name attached to a group is a guess until the player makes it.
expect(row!.characterId).toBeNull();
expect(row!.origin).toBe("imported");
});
it("leaves unequipped items out of the equipment store", () => {
const result = parseGearExport(gearExport());
expect(result.rows!.equipment).toEqual([]);
});
});
describe("character merge", () => {
it("refreshes level and ascension but keeps the player's configuration", () => {
const stored = {
...emptyCharacter("Zankou"),
level: 60,
breakthroughs: 3,
arcId: "fork_vine",
arcRefinement: 2,
useCustom: true,
customTargets: [{ stat: "CritBase", target: 0.7, weight: 3 }],
};
const [merged] = mergeCharacters(
[stored],
[{ ...emptyCharacter("Zankou"), level: 70, breakthroughs: 5 }],
);
expect(merged!.level).toBe(70);
expect(merged!.breakthroughs).toBe(5);
expect(merged!.arcId).toBe("fork_vine");
expect(merged!.arcRefinement).toBe(2);
expect(merged!.useCustom).toBe(true);
expect(merged!.customTargets).toHaveLength(1);
});
it("keeps a character the capture did not mention", () => {
const stored = { ...emptyCharacter("Oneiroi"), arcId: "fork_dustbin" };
const merged = mergeCharacters([stored], [emptyCharacter("Zankou")]);
expect(merged.map((row) => row.characterId).sort()).toEqual(["Oneiroi", "Zankou"]);
expect(merged.find((row) => row.characterId === "Oneiroi")!.arcId).toBe("fork_dustbin");
});
});