-
Notifications
You must be signed in to change notification settings - Fork 1
test(frontend): cover LineageDag with tests and stories #439
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
seonghobae
merged 10 commits into
worktree-fix-frontend-build-break
from
worktree-fix-lineagedag-coverage
Aug 23, 2026
Merged
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
1b431d7
test(frontend): cover LineageDag with tests and stories
seonghobae 590c6c3
fix(frontend): use OIDC return-url helpers on the login button
seonghobae 0bc3257
Revert "fix(frontend): use OIDC return-url helpers on the login button"
seonghobae 8149d37
fix(frontend): keep lineage edge evidence visible-only
seonghobae c8924d5
fix(frontend): terminate rooted lineage cycles
seonghobae d150d8a
chore(frontend): keep shared OIDC repair on #426
seonghobae bff8fac
refactor(frontend): remove unreachable cycle guard
seonghobae 5db6276
test(frontend): cover converging lineage DAGs
seonghobae d05ed9f
fix(frontend): position converging DAG nodes once
seonghobae a171811
merge: pull in App.tsx AdminPanel/oidcReturnUrl build fix from base
seonghobae File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| # Verify lineage DAG states before changing graph CSS | ||
|
|
||
| Open `Lineage/LineageDag` in Storybook and compare the empty, grouped/forked, | ||
| ungrouped, and long-title states. The graph now omits a relationship from both | ||
| the SVG and its count unless both endpoints are visible in the same reconstruct | ||
| group. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,92 @@ | ||
| import type { Meta, StoryObj } from "@storybook/react-vite"; | ||
| import { LineageDag } from "./LineageDag"; | ||
| import type { LineageGraph } from "./api"; | ||
|
|
||
| const meta = { | ||
| title: "Lineage/LineageDag", | ||
| component: LineageDag, | ||
| args: { | ||
| onSelectPost: () => undefined, | ||
| }, | ||
| } satisfies Meta<typeof LineageDag>; | ||
|
|
||
| export default meta; | ||
|
|
||
| type Story = StoryObj<typeof meta>; | ||
|
|
||
| // Edge case: no reconstructed lineage yet -- must not render an empty SVG. | ||
| export const Empty: Story = { | ||
| args: { | ||
| graph: { nodes: [], edges: [] }, | ||
| }, | ||
| }; | ||
|
|
||
| export const SingleBranch: Story = { | ||
| args: { | ||
| graph: { | ||
| nodes: [ | ||
| { id: "a1", group: "Northwind Renewal", label: "Kickoff note", occurred_at: "2026-01-01T00:00:00Z", is_root: true, is_branch_point: false }, | ||
| { id: "a2", group: "Northwind Renewal", label: "Follow-up call", occurred_at: "2026-01-04T00:00:00Z", is_root: false, is_branch_point: false }, | ||
| { id: "a3", group: "Northwind Renewal", label: "Contract signed", occurred_at: "2026-01-10T00:00:00Z", is_root: false, is_branch_point: false }, | ||
| ], | ||
| edges: [ | ||
| { source: "a1", target: "a2", fused_score: 0.86 }, | ||
| { source: "a2", target: "a3", fused_score: 0.91 }, | ||
| ], | ||
| } satisfies LineageGraph, | ||
| }, | ||
| }; | ||
|
|
||
| // The multi-branch, git-branch-style case the Ask Agent answer view relies on: | ||
| // several independent lineage threads rendered as separate figures, plus one | ||
| // thread with an actual fork (a branch point with two children). | ||
| export const MultipleGitStyleBranches: Story = { | ||
| args: { | ||
| graph: { | ||
| nodes: [ | ||
| { id: "a1", group: "Northwind Renewal", label: "Kickoff note", occurred_at: "2026-01-01T00:00:00Z", is_root: true, is_branch_point: true }, | ||
| { id: "a2", group: "Northwind Renewal", label: "Legal review", occurred_at: "2026-01-04T00:00:00Z", is_root: false, is_branch_point: false }, | ||
| { id: "a3", group: "Northwind Renewal", label: "Pricing review", occurred_at: "2026-01-04T00:00:00Z", is_root: false, is_branch_point: false }, | ||
| { id: "b1", group: "Acme Onboarding", label: "Welcome call", occurred_at: "2026-02-01T00:00:00Z", is_root: true, is_branch_point: false }, | ||
| { id: "b2", group: "Acme Onboarding", label: "Access provisioned", occurred_at: "2026-02-03T00:00:00Z", is_root: false, is_branch_point: false }, | ||
| ], | ||
| edges: [ | ||
| { source: "a1", target: "a2", fused_score: 0.78 }, | ||
| { source: "a1", target: "a3", fused_score: 0.64 }, | ||
| { source: "b1", target: "b2", fused_score: 0.9 }, | ||
| ], | ||
| } satisfies LineageGraph, | ||
| currentPostId: "a1", | ||
| }, | ||
| }; | ||
|
|
||
| // Edge case: a node with no explicit group (or a raw UUID group id from a | ||
| // partially-processed corpus) must still render, bucketed under "Ungrouped". | ||
| export const UngroupedNode: Story = { | ||
| args: { | ||
| graph: { | ||
| nodes: [{ id: "u1", group: "", label: "Standalone note with no thread yet", occurred_at: "2026-01-01T00:00:00Z", is_root: true, is_branch_point: false }], | ||
| edges: [], | ||
| } satisfies LineageGraph, | ||
| }, | ||
| }; | ||
|
|
||
| // Edge case: a very long post title must truncate in the graph without | ||
| // breaking layout, while the full title stays available to screen readers. | ||
| export const LongNodeLabel: Story = { | ||
| args: { | ||
| graph: { | ||
| nodes: [ | ||
| { | ||
| id: "a1", | ||
| group: "Northwind Renewal", | ||
| label: "Quarterly vendor renewal kickoff call with legal, procurement, and finance stakeholders", | ||
| occurred_at: "2026-01-01T00:00:00Z", | ||
| is_root: true, | ||
| is_branch_point: false, | ||
| }, | ||
| ], | ||
| edges: [], | ||
| } satisfies LineageGraph, | ||
| }, | ||
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,132 @@ | ||
| import { fireEvent, render, screen } from "@testing-library/react"; | ||
| import { describe, expect, it, vi } from "vitest"; | ||
| import { LineageDag } from "./LineageDag"; | ||
| import type { LineageGraph } from "./api"; | ||
|
|
||
| describe("LineageDag", () => { | ||
| it("shows an empty-state message instead of an empty graph", () => { | ||
| render(<LineageDag graph={{ nodes: [], edges: [] }} onSelectPost={vi.fn()} />); | ||
| expect(screen.getByText("No reconstructed lineage yet. Rebuild after seeding posts.")).toBeInTheDocument(); | ||
| expect(screen.queryByRole("img")).not.toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it("renders one branch figure per lineage group, git-branch style", () => { | ||
| const graph: LineageGraph = { | ||
| nodes: [ | ||
| { id: "a1", group: "Project Alpha", label: "Kickoff note", occurred_at: "2026-01-01T00:00:00Z", is_root: true, is_branch_point: true }, | ||
| { id: "a2", group: "Project Alpha", label: "Follow-up note", occurred_at: "2026-01-02T00:00:00Z", is_root: false, is_branch_point: false }, | ||
| { id: "b1", group: "Project Beta", label: "Beta kickoff", occurred_at: "2026-01-03T00:00:00Z", is_root: true, is_branch_point: false }, | ||
| ], | ||
| edges: [{ source: "a1", target: "a2", fused_score: 0.82 }], | ||
| }; | ||
| const { container } = render(<LineageDag graph={graph} onSelectPost={vi.fn()} />); | ||
|
|
||
| expect(screen.getByText("Project Alpha (2 records, 1 lineage edges)")).toBeInTheDocument(); | ||
| expect(screen.getByText("Project Beta (1 records, 0 lineage edges)")).toBeInTheDocument(); | ||
| expect(screen.getAllByRole("img")).toHaveLength(2); | ||
| expect(container.querySelectorAll(".lineage-dag-edge")).toHaveLength(1); | ||
| expect(container.querySelector(".lineage-dag-branch")).toBeInTheDocument(); | ||
| expect(container.querySelector(".lineage-dag-root")).toBeInTheDocument(); | ||
| expect(container.querySelector(".lineage-dag-node:not(.lineage-dag-root):not(.lineage-dag-branch)")).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it("groups a missing/UUID group id under an Ungrouped heading, sorted last", () => { | ||
| const graph: LineageGraph = { | ||
| nodes: [ | ||
| { id: "u1", group: "3f9c8b1a-1111-2222-3333-444455556666", label: "Loose note", occurred_at: "2026-01-01T00:00:00Z", is_root: true, is_branch_point: false }, | ||
| { id: "z1", group: "Zeta Corp", label: "Zeta note", occurred_at: "2026-01-01T00:00:00Z", is_root: true, is_branch_point: false }, | ||
| ], | ||
| edges: [], | ||
| }; | ||
| render(<LineageDag graph={graph} onSelectPost={vi.fn()} />); | ||
|
|
||
| const headings = screen.getAllByRole("img").map((img) => img.getAttribute("aria-label")); | ||
| expect(headings).toEqual(["Zeta Corp lineage", "Ungrouped lineage"]); | ||
| }); | ||
|
|
||
| it("reports the clicked post id", () => { | ||
| const onSelectPost = vi.fn(); | ||
| const graph: LineageGraph = { | ||
| nodes: [{ id: "a1", group: "Project Alpha", label: "Kickoff note", occurred_at: "2026-01-01T00:00:00Z", is_root: true, is_branch_point: false }], | ||
| edges: [], | ||
| }; | ||
| render(<LineageDag graph={graph} onSelectPost={onSelectPost} />); | ||
|
|
||
| fireEvent.click(screen.getByRole("button", { name: "Open post: Kickoff note" })); | ||
| expect(onSelectPost).toHaveBeenCalledWith("a1"); | ||
| }); | ||
|
|
||
| it("also selects a node via Enter and Space for keyboard users", () => { | ||
| const onSelectPost = vi.fn(); | ||
| const graph: LineageGraph = { | ||
| nodes: [{ id: "a1", group: "Project Alpha", label: "Kickoff note", occurred_at: "2026-01-01T00:00:00Z", is_root: true, is_branch_point: false }], | ||
| edges: [], | ||
| }; | ||
| render(<LineageDag graph={graph} onSelectPost={onSelectPost} />); | ||
|
|
||
| const node = screen.getByRole("button", { name: "Open post: Kickoff note" }); | ||
| fireEvent.keyDown(node, { key: "Enter" }); | ||
| fireEvent.keyDown(node, { key: " " }); | ||
| expect(onSelectPost).toHaveBeenCalledTimes(2); | ||
| }); | ||
|
|
||
| it("does not select on an unrelated key press", () => { | ||
| const onSelectPost = vi.fn(); | ||
| const graph: LineageGraph = { | ||
| nodes: [{ id: "a1", group: "Project Alpha", label: "Kickoff note", occurred_at: "2026-01-01T00:00:00Z", is_root: true, is_branch_point: false }], | ||
| edges: [], | ||
| }; | ||
| render(<LineageDag graph={graph} onSelectPost={onSelectPost} />); | ||
|
|
||
| fireEvent.keyDown(screen.getByRole("button", { name: "Open post: Kickoff note" }), { key: "Tab" }); | ||
| expect(onSelectPost).not.toHaveBeenCalled(); | ||
| }); | ||
|
|
||
| it("marks the current post distinctly from the rest", () => { | ||
| const graph: LineageGraph = { | ||
| nodes: [ | ||
| { id: "a1", group: "Project Alpha", label: "Kickoff note", occurred_at: "2026-01-01T00:00:00Z", is_root: true, is_branch_point: false }, | ||
| { id: "a2", group: "Project Alpha", label: "Follow-up note", occurred_at: "2026-01-02T00:00:00Z", is_root: false, is_branch_point: false }, | ||
| ], | ||
| edges: [{ source: "a1", target: "a2", fused_score: 0.5 }], | ||
| }; | ||
| render(<LineageDag graph={graph} onSelectPost={vi.fn()} currentPostId="a2" />); | ||
|
|
||
| expect(screen.getByRole("button", { name: "Open post: Follow-up note" })).toHaveAttribute("aria-current", "true"); | ||
| expect(screen.getByRole("button", { name: "Open post: Kickoff note" })).not.toHaveAttribute("aria-current"); | ||
| }); | ||
|
|
||
| it("truncates a very long node label instead of overflowing the graph", () => { | ||
| const longLabel = "A".repeat(60); | ||
| const graph: LineageGraph = { | ||
| nodes: [{ id: "a1", group: "Project Alpha", label: longLabel, occurred_at: "2026-01-01T00:00:00Z", is_root: true, is_branch_point: false }], | ||
| edges: [], | ||
| }; | ||
| render(<LineageDag graph={graph} onSelectPost={vi.fn()} />); | ||
|
|
||
| expect(screen.getByText(`${"A".repeat(33)}…`)).toBeInTheDocument(); | ||
| expect(screen.queryByText(longLabel)).not.toBeInTheDocument(); | ||
| // The full label is still reachable via the accessible name for screen readers. | ||
| expect(screen.getByRole("button", { name: `Open post: ${longLabel}` })).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it("does not count or render a relationship whose other endpoint is not visible", () => { | ||
| const graph: LineageGraph = { | ||
| nodes: [ | ||
| { | ||
| id: "visible-note", | ||
| group: "Project Alpha", | ||
| label: "Visible note", | ||
| occurred_at: "2026-01-01T00:00:00Z", | ||
| is_root: true, | ||
| is_branch_point: false, | ||
| }, | ||
| ], | ||
| edges: [{ source: "visible-note", target: "hidden-note", fused_score: 0.92 }], | ||
| }; | ||
| const { container } = render(<LineageDag graph={graph} onSelectPost={vi.fn()} />); | ||
|
|
||
| expect(screen.getByText("Project Alpha (1 records, 0 lineage edges)")).toBeInTheDocument(); | ||
| expect(container.querySelector(".lineage-dag-edge")).not.toBeInTheDocument(); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.