diff --git a/CHANGELOG.d/2.12.6-provider-error-boundary.md b/CHANGELOG.d/2.12.6-provider-error-boundary.md index 99a2fbd4d..927fad08e 100644 --- a/CHANGELOG.d/2.12.6-provider-error-boundary.md +++ b/CHANGELOG.d/2.12.6-provider-error-boundary.md @@ -1,4 +1,5 @@ ## Fixed - Keep contextual-orchestrator, OIDC, RankWeave, TEPP, and durable-ingestion diagnostics behind stable product error boundaries while retaining the original exception for server-side chaining. +- Route browser transport and tenant-settings failures through the same boundary so 5xx provider details never reach buyer-facing error text. - Keep browser 5xx and transport failures behind the same stable client error boundary. diff --git a/docs/adr/0123-provider-error-boundary.md b/docs/adr/0123-provider-error-boundary.md index 48610ebeb..5cc6289d1 100644 --- a/docs/adr/0123-provider-error-boundary.md +++ b/docs/adr/0123-provider-error-boundary.md @@ -24,9 +24,9 @@ cannot escape as raw ``KeyError`` or type-error payloads from a library boundary. The browser API client is a second trust boundary: HTTP 5xx details are -discarded, and transport failures become a stable status-0 client error -before any UI handler can render them. Client-error details remain available -only for actionable validation or authorization responses. +discarded, and transport failures become a stable status-0 client error before +any UI handler can render them. Client-error details remain available only for +actionable validation or authorization responses. Missing or malformed evidence remains unavailable; it is never converted into a fabricated negative result. Existing input-validation errors outside a diff --git a/frontend/src/PostBody.test.tsx b/frontend/src/PostBody.test.tsx index 1a9b00c4a..86beb01c5 100644 --- a/frontend/src/PostBody.test.tsx +++ b/frontend/src/PostBody.test.tsx @@ -295,6 +295,44 @@ describe("PostBody", () => { expect(screen.getByText("Panel")).toBeInTheDocument(); }); + it("keeps escaped pipes and does not invent headers for unmarked region OCR", () => { + render( + '} + imageContent={[ + { + unit_index: 0, + mime_type: "image/png", + status_code: "described", + extracted_text: "| Item | State |\n| --- | --- |\n| Review \\| approve | Ready |", + caption: "A table image", + tags: [], + regions: [ + { + region_index: 0, + x_ratio: 0, + y_ratio: 0, + width_ratio: 1, + height_ratio: 1, + status_code: "described", + extracted_text: "| 1 | Panel |\n| 2 | Ready |", + caption: "An OCR region without a Markdown header marker.", + tags: [], + }, + ], + }, + ]} + />, + ); + + expect(screen.getByText("Review | approve")).toBeInTheDocument(); + expect(screen.getByRole("columnheader", { name: "Item" })).toBeInTheDocument(); + expect(screen.queryByRole("columnheader", { name: "1" })).not.toBeInTheDocument(); + expect(document.querySelector(".post-image-region-text p")?.textContent).toBe( + "| 1 | Panel |\n| 2 | Ready |", + ); + }); + it("keeps source-image placement while showing persisted OCR and caption evidence", () => { render( { - const cells = row.split("|").map((cell) => cell.trim()); + const cells = row.split(/(? cell.trim().replace(/\\\|/g, "|")); if (cells[0] === "") cells.shift(); if (cells[cells.length - 1] === "") cells.pop(); return cells; - }) + }); + if (!parsedRows.some((row) => row.every((cell) => /^:?-{3,}:?$/.test(cell)))) return null; + const rows = parsedRows .filter((row) => !row.every((cell) => /^:?-{3,}:?$/.test(cell))) .filter((row) => row.length > 1 && row.some(Boolean)); if (rows.length < 2 || rows.some((row) => row.length !== rows[0].length)) return null; @@ -22,10 +24,20 @@ function parsePipeDelimitedTable(text: string): string[][] | null { function renderImageText(text: string) { const rows = parsePipeDelimitedTable(text); if (!rows) return

{text}

; + const [header, ...bodyRows] = rows; return ( + + + {header.map((cell, cellIndex) => ( + + ))} + + - {rows.map((row, rowIndex) => ( + {bodyRows.map((row, rowIndex) => ( {row.map((cell, cellIndex) => ( @@ -72,7 +84,14 @@ function renderImageEvidence(
    {imageContent.regions.map((region) => (
  1. - {region.caption || region.extracted_text || t("Unknown")} + {region.caption ?

    {region.caption}

    : null} + {region.extracted_text ? ( +
    + {renderImageText(region.extracted_text)} +
    + ) : region.caption ? null : ( + t("Unknown") + )} {region.tags.length ? ( {t("Image tags")}: {region.tags.join(", ")}
+ {cell} +
{cell}