From 8ce8c9b3db5feab9bd407b7da9c15a1cb61e97b6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 21:30:47 +0900 Subject: [PATCH 1/3] test(frontend): cover FiveW1H component with tests and stories Adds Vitest coverage for loading state, empty-evidence messaging, raw-source-to-label mapping (including the unmapped fallback), and optional evidence-text/ontology-badge rendering, plus a Storybook inventory covering the loading, all-empty, grounded-answer, and unmapped-source scenarios. --- frontend/src/components/FiveW1H.stories.tsx | 90 +++++++++++ frontend/src/components/FiveW1H.test.tsx | 156 ++++++++++++++++++++ 2 files changed, 246 insertions(+) create mode 100644 frontend/src/components/FiveW1H.stories.tsx create mode 100644 frontend/src/components/FiveW1H.test.tsx diff --git a/frontend/src/components/FiveW1H.stories.tsx b/frontend/src/components/FiveW1H.stories.tsx new file mode 100644 index 000000000..adb76d322 --- /dev/null +++ b/frontend/src/components/FiveW1H.stories.tsx @@ -0,0 +1,90 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { FiveW1H } from "./FiveW1H"; + +const meta = { + title: "Ask Agent/FiveW1H", + component: FiveW1H, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Loading: Story = { + args: { slots: null }, +}; + +export const AllSlotsEmpty: Story = { + args: { + slots: (["who", "what", "when", "where", "why", "how"] as const).map((slot_code) => ({ + slot_code, + values: [], + empty_next_action_code: "none", + })), + }, +}; + +export const GroundedAnswer: Story = { + args: { + slots: [ + { + slot_code: "who", + empty_next_action_code: "none", + values: [ + { + text: "Ada West", + source: "post_summary_role", + evidence_text: "“Ada West signed off on the renewal”", + ontology_codes: [], + ontology_annotations: {}, + }, + { + text: "Northwind Logistics", + source: "post_summary_role.affiliated_organization_name", + ontology_codes: [], + ontology_annotations: {}, + }, + ], + }, + { + slot_code: "what", + empty_next_action_code: "none", + values: [ + { + text: "Renewed the vendor contract", + source: "post_summary_event", + evidence_text: "“we renewed the contract through Q4”", + ontology_codes: ["evt-42"], + ontology_annotations: { ontology_label: "Contract renewal" }, + }, + ], + }, + { + slot_code: "when", + empty_next_action_code: "none", + values: [], + }, + ], + }, +}; + +// Edge case: an evidence source with no human-label mapping yet must still +// render something readable instead of disappearing. +export const UnmappedEvidenceSource: Story = { + args: { + slots: [ + { + slot_code: "how", + empty_next_action_code: "none", + values: [ + { + text: "Filed via the vendor portal", + source: "some_future_source", + ontology_codes: [], + ontology_annotations: {}, + }, + ], + }, + ], + }, +}; diff --git a/frontend/src/components/FiveW1H.test.tsx b/frontend/src/components/FiveW1H.test.tsx new file mode 100644 index 000000000..278d656c3 --- /dev/null +++ b/frontend/src/components/FiveW1H.test.tsx @@ -0,0 +1,156 @@ +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { describe, expect, it } from "vitest"; +import { FiveW1H } from "./FiveW1H"; +import type { FiveW1HSlot } from "../api"; + +function slot(overrides: Partial): FiveW1HSlot { + return { + slot_code: "who", + values: [], + empty_next_action_code: "none", + ...overrides, + }; +} + +describe("FiveW1H", () => { + it("shows a loading state while slots have not arrived yet", () => { + render(); + expect(screen.getByText("Loading 5W1H...")).toBeInTheDocument(); + }); + + it("shows a no-evidence message for a slot with no values", () => { + render(); + expect(screen.getByText("No grounded evidence for this dimension.")).toBeInTheDocument(); + }); + + it("renders a human label for a raw evidence source instead of the dotted path", () => { + render( + , + ); + expect(screen.getByText("Ada West")).toBeInTheDocument(); + expect(screen.getByText("Extracted affiliation")).toBeInTheDocument(); + expect(screen.queryByText("post_summary_role.affiliated_organization_name")).not.toBeInTheDocument(); + }); + + it("falls back to the raw source string for an unmapped evidence source", () => { + render( + , + ); + expect(screen.getByText("some_future_source")).toBeInTheDocument(); + }); + + it("shows optional evidence text and ontology class badges only when present", async () => { + render( + , + ); + + await userEvent.click(screen.getByText("Evidence provenance")); + expect(screen.getByText("“we renewed the contract”")).toBeInTheDocument(); + expect(screen.getByText("Ontology class: Contract renewal")).toBeInTheDocument(); + }); + + it("falls back to the ontology code when no ontology label is annotated", async () => { + render( + , + ); + + await userEvent.click(screen.getByText("Evidence provenance")); + expect(screen.getByText("Ontology class: evt-42")).toBeInTheDocument(); + }); + + it("renders one definition entry per slot with its human label", () => { + render( + , + ); + expect(screen.getByText("Who")).toBeInTheDocument(); + expect(screen.getByText("When")).toBeInTheDocument(); + expect(screen.getByText("Where")).toBeInTheDocument(); + }); + + it("renders multiple values for the same slot as separate list items", () => { + render( + , + ); + expect(screen.getByText("Ada West")).toBeInTheDocument(); + expect(screen.getByText("Priya Nair")).toBeInTheDocument(); + }); + + it("exposes the section under an accessible landmark name", () => { + render(); + expect(screen.getByRole("region", { name: "5W1H" })).toBeInTheDocument(); + }); +}); From 5e54a45b85d344dccb312ff867d54593743fba14 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 21:38:26 +0900 Subject: [PATCH 2/3] fix(frontend): use OIDC return-url helpers on the login button Same shared-ancestor bug as #418/#415/#426/#427/#429/#431/#434/#435/#436: the login button built an unsanitized returnUrl inline instead of returnUrlFromLocation()/rememberOidcReturnUrl(), and removed the unreachable login-screen AdminPanel render (accessToken is always undefined pre-auth). This PR's own diff doesn't touch AdminPanel. --- frontend/src/App.test.tsx | 3 +++ frontend/src/App.tsx | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/frontend/src/App.test.tsx b/frontend/src/App.test.tsx index 7462abd2c..70eb27590 100644 --- a/frontend/src/App.test.tsx +++ b/frontend/src/App.test.tsx @@ -41,6 +41,9 @@ describe("App, unauthenticated", () => { state: expect.objectContaining({ returnUrl: expect.stringMatching(/^\//) }), }), ); + // Persisted as a fallback in case the OIDC state round-trip is dropped + // (see oidcReturnUrl.ts's restoreOidcReturnUrl, consumed in main.tsx). + expect(window.sessionStorage.getItem("lineageweave.oidc.returnUrl")).toMatch(/^\//); }); }); diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 6fba0dd41..1b5b351ab 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -4610,7 +4610,8 @@ export default function App({ showLabPanels = false }: { showLabPanels?: boolean
- {destination === "admin" ? : null}