import { describe, expect, it } from "vitest"; import { PROFILE_STORAGE_KEYS, assignRomSecondaryClass, createCharacter, createOfflineSession, listCharacters, swapRomClasses, updateCharacterActionBindings, updateCharacterProgression, type StorageLike, } from "./accountRepository"; class MemoryStorage implements StorageLike { private readonly values = new Map(); getItem(key: string) { return this.values.get(key) ?? null; } setItem(key: string, value: string) { this.values.set(key, value); } removeItem(key: string) { this.values.delete(key); } } const appearance = { skinColor: 0, face: 0, hairStyle: 0, hairColor: 0, feature: 0 }; describe("RoM dual-class persistence", () => { it("migrates v1 profiles to v2 without losing bindings or progression", () => { const storage = new MemoryStorage(); storage.setItem(PROFILE_STORAGE_KEYS.database, JSON.stringify({ version: 1, accounts: [], rosters: { "offline-roster": [{ id: "legacy", ownerId: "offline-roster", name: "Legacy", raceId: "human", classId: "priest", gender: "female", appearance, level: 42, experience: 777, talentRanks: { legacy: 3 }, actionBindings: { "primary:face-bottom": "priest-flash-heal" }, inventory: [], equipment: {}, location: "Dungeon Entrance", createdAt: 1, lastPlayedAt: 2, }], }, })); expect(listCharacters("offline-roster", storage)[0]).toMatchObject({ categoryId: "wow", secondaryClassId: null, level: 42, experience: 777, talentRanks: { legacy: 3 }, actionBindings: { "primary:face-bottom": "priest-flash-heal" }, actionLoadouts: { "priest:none": { "primary:face-bottom": "priest-flash-heal" }, }, }); expect(JSON.parse(storage.getItem(PROFILE_STORAGE_KEYS.database)!).version).toBe(7); }); it("locks selection to level ten and preserves separate bars through atomic swaps", () => { const storage = new MemoryStorage(); const ownerId = createOfflineSession().ownerId; const created = createCharacter({ ownerId, name: "Runesworn", categoryId: "rom", raceId: "rom-human", classId: "rom-warrior", gender: "male", appearance, }, storage).character!; expect(assignRomSecondaryClass(ownerId, created.id, "rom-scout", storage)).toMatchObject({ ok: false }); updateCharacterProgression(ownerId, created.id, { level: 10, experience: 81, talentRanks: {} }, storage); expect(assignRomSecondaryClass(ownerId, created.id, "rom-warden", storage)).toMatchObject({ ok: false }); expect(assignRomSecondaryClass(ownerId, created.id, "rom-scout", storage)).toMatchObject({ ok: true, character: { classId: "rom-warrior", secondaryClassId: "rom-scout" }, }); updateCharacterActionBindings(ownerId, created.id, { "primary:face-bottom": "warrior-scout-a" }, storage); expect(swapRomClasses(ownerId, created.id, storage)).toMatchObject({ ok: true, character: { classId: "rom-scout", secondaryClassId: "rom-warrior", actionBindings: {} }, }); updateCharacterActionBindings(ownerId, created.id, { "primary:face-bottom": "scout-warrior-b" }, storage); expect(swapRomClasses(ownerId, created.id, storage).character).toMatchObject({ classId: "rom-warrior", secondaryClassId: "rom-scout", level: 10, experience: 81, actionBindings: { "primary:face-bottom": "warrior-scout-a" }, }); expect(assignRomSecondaryClass(ownerId, created.id, "rom-priest", storage)).toMatchObject({ ok: false }); const saved = listCharacters(ownerId, storage)[0]!; expect(saved.actionLoadouts).toMatchObject({ "rom-warrior:rom-scout": { "primary:face-bottom": "warrior-scout-a" }, "rom-scout:rom-warrior": { "primary:face-bottom": "scout-warrior-b" }, }); }); });