feat: vampire syncNewContent injection and grant-eternal-sovereignty debug endpoint

Adds vampire parity with existing goddess debug tooling:
- `SyncNewContentResponse` type gains vampire count fields
- `syncNewContent` injects missing vampire content arrays for players
  who have entered the vampire realm (achievements, bosses, equipment,
  exploration areas, quests, thralls, upgrades, zones)
- `/grant-eternal-sovereignty` debug endpoint mirrors `/grant-apotheosis`,
  setting `vampire.eternalSovereignty.count = 1` for saves that need it
- Full test coverage for all new paths in debug.spec.ts
This commit is contained in:
2026-04-16 17:46:59 -07:00
committed by Naomi Carrigan
parent e02827dbb6
commit d45b80fe4a
3 changed files with 390 additions and 1 deletions
+192
View File
@@ -1206,6 +1206,198 @@ describe("debug route", () => {
});
});
describe("POST /sync-new-content — vampire injection", () => {
const syncNewContent = () =>
app.fetch(new Request("http://localhost/debug/sync-new-content", { method: "POST" }));
const makeVampireState = (): NonNullable<GameState["vampire"]> => ({
achievements: [],
awakening: { count: 0, purchasedUpgradeIds: [], soulShards: 0, soulShardsBloodMultiplier: 1, soulShardsCombatMultiplier: 1, soulShardsMetaMultiplier: 1, soulShardsSiringIchorMultiplier: 1, soulShardsSiringThresholdMultiplier: 1 },
baseClickPower: 1,
bosses: [],
equipment: [],
eternalSovereignty: { count: 0 },
exploration: { areas: [], craftedBloodMultiplier: 1, craftedCombatMultiplier: 1, craftedIchorMultiplier: 1, craftedRecipeIds: [], materials: [] },
lastTickAt: 0,
lifetimeBloodEarned: 0,
lifetimeBossesDefeated: 0,
lifetimeQuestsCompleted: 0,
quests: [],
siring: { count: 0, ichor: 0, ichorBloodMultiplier: 1, ichorCombatMultiplier: 1, ichorThrallsMultiplier: 1, purchasedUpgradeIds: [] },
thralls: [],
totalBloodEarned: 0,
upgrades: [],
zones: [],
});
it("injects vampire content arrays when state.vampire exists with empty arrays", async () => {
const state = makeState({ vampire: makeVampireState() });
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never);
vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never);
const res = await syncNewContent();
expect(res.status).toBe(200);
const body = await res.json() as { vampireAchievementsAdded: number; vampireBossesAdded: number; vampireQuestsAdded: number; vampireZonesAdded: number };
expect(body.vampireAchievementsAdded).toBeGreaterThan(0);
expect(body.vampireBossesAdded).toBeGreaterThan(0);
expect(body.vampireQuestsAdded).toBeGreaterThan(0);
expect(body.vampireZonesAdded).toBeGreaterThan(0);
});
it("returns zero vampire counts when state.vampire is undefined", async () => {
const state = makeState();
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never);
vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never);
const res = await syncNewContent();
expect(res.status).toBe(200);
const body = await res.json() as { vampireAchievementsAdded: number; vampireBossesAdded: number; vampireEquipmentAdded: number; vampireExplorationAreasAdded: number; vampireQuestsAdded: number; vampireThrallsAdded: number; vampireUpgradesAdded: number; vampireZonesAdded: number };
expect(body.vampireAchievementsAdded).toBe(0);
expect(body.vampireBossesAdded).toBe(0);
expect(body.vampireEquipmentAdded).toBe(0);
expect(body.vampireExplorationAreasAdded).toBe(0);
expect(body.vampireQuestsAdded).toBe(0);
expect(body.vampireThrallsAdded).toBe(0);
expect(body.vampireUpgradesAdded).toBe(0);
expect(body.vampireZonesAdded).toBe(0);
});
it("injects vampire exploration areas when vampire has no areas", async () => {
const state = makeState({ vampire: makeVampireState() });
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never);
vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never);
const res = await syncNewContent();
expect(res.status).toBe(200);
const body = await res.json() as { vampireExplorationAreasAdded: number };
expect(body.vampireExplorationAreasAdded).toBeGreaterThan(0);
});
});
describe("POST /grant-eternal-sovereignty", () => {
const grantEternalSovereignty = () =>
app.fetch(new Request("http://localhost/debug/grant-eternal-sovereignty", { method: "POST" }));
it("returns 404 when no save is found", async () => {
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce(null);
const res = await grantEternalSovereignty();
expect(res.status).toBe(404);
const body = await res.json() as { error: string };
expect(body.error).toContain("No save found");
});
it("returns 200 with unchanged state when eternalSovereignty count is already >= 1", async () => {
const vampire: NonNullable<GameState["vampire"]> = {
achievements: [],
awakening: { count: 0, purchasedUpgradeIds: [], soulShards: 0, soulShardsBloodMultiplier: 1, soulShardsCombatMultiplier: 1, soulShardsMetaMultiplier: 1, soulShardsSiringIchorMultiplier: 1, soulShardsSiringThresholdMultiplier: 1 },
baseClickPower: 1,
bosses: [],
equipment: [],
eternalSovereignty: { count: 1 },
exploration: { areas: [], craftedBloodMultiplier: 1, craftedCombatMultiplier: 1, craftedIchorMultiplier: 1, craftedRecipeIds: [], materials: [] },
lastTickAt: 0,
lifetimeBloodEarned: 0,
lifetimeBossesDefeated: 0,
lifetimeQuestsCompleted: 0,
quests: [],
siring: { count: 0, ichor: 0, ichorBloodMultiplier: 1, ichorCombatMultiplier: 1, ichorThrallsMultiplier: 1, purchasedUpgradeIds: [] },
thralls: [],
totalBloodEarned: 0,
upgrades: [],
zones: [],
};
const state = makeState({ vampire });
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never);
const res = await grantEternalSovereignty();
expect(res.status).toBe(200);
const body = await res.json() as { state: GameState };
expect(body.state.vampire?.eternalSovereignty.count).toBe(1);
expect(vi.mocked(prisma.gameState.update)).not.toHaveBeenCalled();
});
it("returns 200 and grants eternal sovereignty when not yet granted", async () => {
const state = makeState();
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never);
vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never);
const res = await grantEternalSovereignty();
expect(res.status).toBe(200);
const body = await res.json() as { state: GameState };
expect(body.state.vampire?.eternalSovereignty.count).toBe(1);
});
it("returns 200 and sets eternalSovereignty when vampire exists with count 0", async () => {
const vampire: NonNullable<GameState["vampire"]> = {
achievements: [],
awakening: { count: 0, purchasedUpgradeIds: [], soulShards: 0, soulShardsBloodMultiplier: 1, soulShardsCombatMultiplier: 1, soulShardsMetaMultiplier: 1, soulShardsSiringIchorMultiplier: 1, soulShardsSiringThresholdMultiplier: 1 },
baseClickPower: 1,
bosses: [],
equipment: [],
eternalSovereignty: { count: 0 },
exploration: { areas: [], craftedBloodMultiplier: 1, craftedCombatMultiplier: 1, craftedIchorMultiplier: 1, craftedRecipeIds: [], materials: [] },
lastTickAt: 0,
lifetimeBloodEarned: 0,
lifetimeBossesDefeated: 0,
lifetimeQuestsCompleted: 0,
quests: [],
siring: { count: 0, ichor: 0, ichorBloodMultiplier: 1, ichorCombatMultiplier: 1, ichorThrallsMultiplier: 1, purchasedUpgradeIds: [] },
thralls: [],
totalBloodEarned: 0,
upgrades: [],
zones: [],
};
const state = makeState({ vampire });
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never);
vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never);
const res = await grantEternalSovereignty();
expect(res.status).toBe(200);
const body = await res.json() as { state: GameState };
expect(body.state.vampire?.eternalSovereignty.count).toBe(1);
});
it("returns 200 with HMAC signature when ANTI_CHEAT_SECRET is set", async () => {
process.env.ANTI_CHEAT_SECRET = "test_secret";
const vampire: NonNullable<GameState["vampire"]> = {
achievements: [],
awakening: { count: 0, purchasedUpgradeIds: [], soulShards: 0, soulShardsBloodMultiplier: 1, soulShardsCombatMultiplier: 1, soulShardsMetaMultiplier: 1, soulShardsSiringIchorMultiplier: 1, soulShardsSiringThresholdMultiplier: 1 },
baseClickPower: 1,
bosses: [],
equipment: [],
eternalSovereignty: { count: 1 },
exploration: { areas: [], craftedBloodMultiplier: 1, craftedCombatMultiplier: 1, craftedIchorMultiplier: 1, craftedRecipeIds: [], materials: [] },
lastTickAt: 0,
lifetimeBloodEarned: 0,
lifetimeBossesDefeated: 0,
lifetimeQuestsCompleted: 0,
quests: [],
siring: { count: 0, ichor: 0, ichorBloodMultiplier: 1, ichorCombatMultiplier: 1, ichorThrallsMultiplier: 1, purchasedUpgradeIds: [] },
thralls: [],
totalBloodEarned: 0,
upgrades: [],
zones: [],
};
const state = makeState({ vampire });
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never);
const res = await grantEternalSovereignty();
expect(res.status).toBe(200);
const body = await res.json() as { signature: string | undefined };
expect(body.signature).toBeDefined();
delete process.env.ANTI_CHEAT_SECRET;
});
it("returns 500 when DB throws an Error", async () => {
vi.mocked(prisma.gameState.findUnique).mockRejectedValueOnce(new Error("DB error"));
const res = await grantEternalSovereignty();
expect(res.status).toBe(500);
const body = await res.json() as { error: string };
expect(body.error).toContain("Internal server error");
});
it("returns 500 when DB throws a non-Error value", async () => {
vi.mocked(prisma.gameState.findUnique).mockRejectedValueOnce("raw error");
const res = await grantEternalSovereignty();
expect(res.status).toBe(500);
const body = await res.json() as { error: string };
expect(body.error).toContain("Internal server error");
});
});
describe("POST /grant-apotheosis", () => {
const grantApotheosis = () =>
app.fetch(new Request("http://localhost/debug/grant-apotheosis", { method: "POST" }));