diff --git a/src/Web/autopilot-monitor-web/app/sessions/[sessionId]/components/EventTimeline.tsx b/src/Web/autopilot-monitor-web/app/sessions/[sessionId]/components/EventTimeline.tsx index 47471280..3315e346 100644 --- a/src/Web/autopilot-monitor-web/app/sessions/[sessionId]/components/EventTimeline.tsx +++ b/src/Web/autopilot-monitor-web/app/sessions/[sessionId]/components/EventTimeline.tsx @@ -3,7 +3,7 @@ import { useState, useMemo } from "react"; import { EnrollmentEvent, Session } from "@/types"; import { normalizeEventDataForDisplay, shortenBuildHashInMessage } from "../utils/eventHelpers"; -import { getErrorCodeEntry, formatErrorCode } from "@/utils/errorCodeMap"; +import { getErrorCodeEntry, getEnrichedOrLookup, formatErrorCode } from "@/utils/errorCodeMap"; interface EventTimelineProps { filteredEvents: EnrollmentEvent[]; @@ -447,8 +447,10 @@ function EventRow({ event, showScriptOutput }: { event: EnrollmentEvent; showScr const hr = event.data?.hresultFromWin32 ?? event.data?.hresult_from_win32; const hasNonZero = (ec && String(ec) !== "0") || (hr && String(hr) !== "0"); if (!hasNonZero) return null; - const ecEntry = ec ? getErrorCodeEntry(String(ec)) : null; - const hrEntry = hr ? getErrorCodeEntry(String(hr)) : null; + // Prefer backend-enriched *Info sibling, fall back to local lookup for older + // responses that pre-date the backend ErrorCodeEnricher. + const ecEntry = ec ? getEnrichedOrLookup(event.data?.exitCodeInfo, String(ec)) : null; + const hrEntry = hr ? getEnrichedOrLookup(event.data?.hresultFromWin32Info, String(hr)) : null; return (
{ec && String(ec) !== "0" && ( @@ -489,7 +491,7 @@ function EventRow({ event, showScriptOutput }: { event: EnrollmentEvent; showScr const isAdvisory = event.eventType === "esp_failure_advisory"; if (!code && !isAdvisory) return null; const codeStr = code ? String(code) : null; - const entry = codeStr ? getErrorCodeEntry(codeStr) : null; + const entry = codeStr ? getEnrichedOrLookup(event.data?.errorCodeInfo, codeStr) : null; // Advisory path uses warning-color palette (amber); the device continued past the // failure via ContinueAnyway, so this is not a hard error. PR1 Session 4fa5a2d4. const badgeBg = isAdvisory ? "bg-amber-100 text-amber-800" : "bg-red-100 text-red-800"; diff --git a/src/Web/autopilot-monitor-web/utils/__tests__/errorCodeMap.test.ts b/src/Web/autopilot-monitor-web/utils/__tests__/errorCodeMap.test.ts index 0633c863..ecc30285 100644 --- a/src/Web/autopilot-monitor-web/utils/__tests__/errorCodeMap.test.ts +++ b/src/Web/autopilot-monitor-web/utils/__tests__/errorCodeMap.test.ts @@ -2,6 +2,7 @@ import { describe, it, expect } from "vitest"; import { getErrorCodeDescription, getErrorCodeEntry, + getEnrichedOrLookup, formatErrorCode, } from "../errorCodeMap"; import catalogFile from "../error-codes.json"; @@ -67,6 +68,43 @@ describe("errorCodeMap", () => { }); }); + describe("getEnrichedOrLookup", () => { + it("prefers backend-enriched sibling when present", () => { + const enriched = { + description: "Backend-provided description", + confidence: "high" as const, + source: "Backend Catalog v2", + }; + const result = getEnrichedOrLookup(enriched, "0x80070005"); + expect(result).toBe(enriched); + expect(result?.description).toBe("Backend-provided description"); + }); + + it("falls back to local catalog when info is null", () => { + const result = getEnrichedOrLookup(null, "0x80070005"); + expect(result).not.toBeNull(); + expect(result?.description).toContain("Access denied"); + }); + + it("falls back to local catalog when info is undefined", () => { + const result = getEnrichedOrLookup(undefined, "0x80070005"); + expect(result).not.toBeNull(); + expect(result?.description).toContain("Access denied"); + }); + + it("returns null when both info and lookup miss", () => { + const result = getEnrichedOrLookup(null, "0xDEADBEEF"); + expect(result).toBeNull(); + }); + + it("ignores malformed info objects and falls back to lookup", () => { + // Backend would never emit this but guard against bad clients. + const malformed = { description: 42 } as unknown as { description: string; confidence: "high"; source: string }; + const result = getEnrichedOrLookup(malformed, "0x80070005"); + expect(result?.description).toContain("Access denied"); + }); + }); + describe("formatErrorCode", () => { it("uppercases hex codes", () => { expect(formatErrorCode("0x80070005")).toBe("0X80070005"); diff --git a/src/Web/autopilot-monitor-web/utils/errorCodeMap.ts b/src/Web/autopilot-monitor-web/utils/errorCodeMap.ts index a7f13572..23365c63 100644 --- a/src/Web/autopilot-monitor-web/utils/errorCodeMap.ts +++ b/src/Web/autopilot-monitor-web/utils/errorCodeMap.ts @@ -95,6 +95,26 @@ export function getErrorCodeEntry(code: string | number | null | undefined): Err return lookupEntry(code); } +/** + * Backend-enriched info entry. The backend ErrorCodeEnricher injects sibling + * {codeKey}Info properties into event Data containing the same shape as + * . When present we prefer the server-side value (uses the + * authoritative catalog version + Confidence-level), and only fall back to the local + * lookup for older responses that don't carry the enrichment yet. + * + * Example: an event with { errorCode: "0x80070005", errorCodeInfo: {...} } + * passes the info through without a second lookup. + */ +export function getEnrichedOrLookup( + info: ErrorCodeEntry | null | undefined, + code: string | number | null | undefined +): ErrorCodeEntry | null { + if (info && typeof info === "object" && typeof info.description === "string") { + return info; + } + return lookupEntry(code); +} + /** * Format a raw numeric error code for display. * Signed-decimal HRESULTs are converted to hex notation.