From 057aa6d88eba82195d9685b95d066cdc7038b6b5 Mon Sep 17 00:00:00 2001 From: Ryan Wong Date: Mon, 10 Aug 2026 18:56:21 +0100 Subject: [PATCH] fix(raiderio): treat an empty customization as absent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Raider.IO sends discord_profile: "" for a player who never set one. The schema required a non-empty string, so the parse threw, the client raised schema_drift, and the run failed with non-retryable upstream_schema_changed — "The search could not be completed" for every such character, permanently. An empty value is an absent value, not structural change to the payload. Accept it and normalize it to no profile guess, so nothing downstream searches on an empty alias. Verified against the live eu/draenor/shurkle payload that produced the failure in the test environment. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Qh8Zb2HnaxebWrRLUMoAiv --- packages/raiderio/src/client.test.ts | 12 +++++++++++ packages/raiderio/src/normalize.ts | 7 +++++-- .../raiderio/character-empty-discord.json | 21 +++++++++++++++++++ 3 files changed, 38 insertions(+), 2 deletions(-) create mode 100644 tests/fixtures/raiderio/character-empty-discord.json 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 + } + } + } +}