diff --git a/packages/raiderio/src/client.test.ts b/packages/raiderio/src/client.test.ts index 3b83e51..29b96f2 100644 --- a/packages/raiderio/src/client.test.ts +++ b/packages/raiderio/src/client.test.ts @@ -10,6 +10,7 @@ import { createRaiderIoClient } from "./index"; type FixtureName = | "character-visible-owner" | "character-private-owner" + | "character-empty-discord" | "character-declared-main" | "character-declared-main-out-of-scope" | "character-renamed-root" @@ -166,6 +167,17 @@ describe("Raider.IO gateway", () => { expect(character.profileGuess).toBe("profile-candidate"); }); + it("treats an empty customization field as absent, not as schema drift", async () => { + // Break caught: Raider.IO sends "" for a customization a player never set. + // Rejecting it raised non-retryable schema_drift, so every character with an + // empty Discord field failed its search permanently. + const character = await clientFor("character-empty-discord").getCharacter( + sentinel + ); + + expect(character.profileGuess).toBeNull(); + }); + it("extracts and normalizes a declared-main character key", async () => { const character = await clientFor("character-declared-main").getCharacter( sentinel diff --git a/packages/raiderio/src/normalize.ts b/packages/raiderio/src/normalize.ts index 591bc1e..fdec04a 100644 --- a/packages/raiderio/src/normalize.ts +++ b/packages/raiderio/src/normalize.ts @@ -33,7 +33,10 @@ const characterResponseSchema = z.object({ .optional(), characterCustomizations: z .object({ - discord_profile: z.string().min(1).nullable().optional(), + // Raider.IO sends "" for a customization the player never set. That is + // an absent value, not structural change: rejecting it would raise + // non-retryable schema drift and permanently fail the search. + discord_profile: z.string().nullable().optional(), main_character: declaredMainSchema.nullable().optional() }) .optional() @@ -147,7 +150,7 @@ export function normalizeCharacterResponse( return { ...character, ownerId: details.user?.name ?? null, - profileGuess: customizations?.discord_profile ?? null, + profileGuess: customizations?.discord_profile?.trim() || null, declaredMain, ...(omittedMembers ? { omittedMembers: true } : {}) }; diff --git a/tests/fixtures/raiderio/character-empty-discord.json b/tests/fixtures/raiderio/character-empty-discord.json new file mode 100644 index 0000000..009f5d8 --- /dev/null +++ b/tests/fixtures/raiderio/character-empty-discord.json @@ -0,0 +1,21 @@ +{ + "status": 200, + "body": { + "characterDetails": { + "character": { + "name": "Sentinel", + "level": 90, + "class": { "name": "Paladin" }, + "realm": { "slug": "Silvermoon" }, + "region": { "slug": "EU" } + }, + "user": null, + "characterCustomizations": { + "isClaimed": false, + "biography": "", + "discord_profile": "", + "main_character": null + } + } + } +}