FN-7184: render pull request review context

Render pull-request review details in the task Review tab so PR-backed tasks show actionable context.\n\n- Preserve PR review metadata when mapping legacy review API data.\n- Show PR reviewers, checks, blocking reasons, comment state, author, path, and GitHub links.\n- Add responsive Review-tab styles and coverage for empty, partial, and populated PR review states.\n\nFiles changed:\n packages/dashboard/app/api/legacy.ts               |  51 ++++++--\n .../dashboard/app/components/TaskReviewTab.css     | 101 +++++++++++++++\n .../dashboard/app/components/TaskReviewTab.tsx     |  94 +++++++++++++-\n .../components/__tests__/TaskReviewTab.test.tsx    | 143 ++++++++++++++++++++-\n 4 files changed, 368 insertions(+), 21 deletions(-)

Fusion-Task-Id: FN-7184

Fusion-Task-Lineage: 59680355-1afa-44e1-b5e1-33e26335a2c8

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-06-28 00:02:24 -07:00
parent fc958bc54b
commit 0956214009
4 changed files with 368 additions and 21 deletions

View File

@@ -6435,24 +6435,47 @@ export function acceptTaskReview(taskId: string, projectId?: string): Promise<Ta
}
function mapTaskReviewDataToLegacy(data: TaskReviewData): TaskReviewResponse {
const fetchedAt = data.fetchedAt ?? undefined;
const canonicalItems = data.items.map((item) => ({
id: item.itemId,
body: item.body,
author: { login: item.author },
createdAt: item.createdAt ?? new Date(0).toISOString(),
updatedAt: item.updatedAt ?? undefined,
path: item.filePath,
threadId: item.threadId,
htmlUrl: item.url,
state: item.reviewState ?? undefined,
summary: item.title ?? undefined,
isResolved: item.isResolved,
...(typeof item.line === "number" ? { line: item.line } : {}),
}));
return {
reviewState: {
source: data.mode,
summary: data.summary ?? undefined,
items: data.items.map((item) => ({
id: item.itemId,
body: item.body,
author: { login: item.author },
createdAt: item.createdAt ?? new Date(0).toISOString(),
updatedAt: item.updatedAt ?? undefined,
path: item.filePath,
threadId: item.threadId,
htmlUrl: item.url,
state: item.reviewState ?? undefined,
isResolved: item.isResolved,
})),
addressing: [],
lastRefreshedAt: data.fetchedAt ?? undefined,
items: canonicalItems,
addressing: data.items
.filter((item) => item.progressStatus != null)
.map((item) => ({
itemId: item.itemId,
status: item.progressStatus ?? "queued",
selectedAt: item.createdAt ?? fetchedAt ?? new Date(0).toISOString(),
snapshot: {
itemId: item.itemId,
sourceMode: item.sourceMode,
source: item.sourceMode === "pull-request" ? "pr-review" : "reviewer-agent",
summary: item.title || item.body.slice(0, 120),
body: item.body,
authorLogin: item.author,
filePath: item.filePath,
lineNumber: item.line,
threadId: item.threadId,
url: item.url,
},
})),
lastRefreshedAt: fetchedAt,
refreshStatus: "ready",
refreshSource: "initial-load",
},

View File

@@ -345,6 +345,92 @@ The Review tab mirrors the Import-from-GitHub comment provenance model: avatar,
margin-top: var(--space-xs);
}
/*
FNXC:TaskReviewTab 2026-06-27-23:38:
PR-linked tasks must expose decision-adjacent reviewers/checks/blockers plus item author/state/GitHub links without overflowing the task-detail modal. These scoped flex rows wrap using design tokens so desktop and mobile Review tabs preserve the existing hierarchy.
*/
.task-review-tab__pr-summary {
display: flex;
flex-direction: column;
gap: var(--space-md);
min-width: 0;
padding: var(--space-md);
border: var(--btn-border-width) solid var(--border);
border-radius: var(--radius-md);
background: color-mix(in srgb, var(--surface) 72%, var(--card));
}
.task-review-tab__pr-summary-section {
display: flex;
flex-direction: column;
gap: var(--space-xs);
min-width: 0;
}
.task-review-tab__pr-summary-label {
color: var(--text);
font-family: var(--font-primary);
font-size: 0.75rem;
font-weight: 700;
line-height: 1.4;
text-transform: uppercase;
letter-spacing: 0.04em;
}
.task-review-tab__pill-list,
.task-review-tab__pr-item-meta {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: var(--space-xs);
min-width: 0;
}
.task-review-tab__pill-list {
margin: 0;
padding: 0;
list-style: none;
}
.task-review-tab__pill-list-item {
display: inline-flex;
align-items: center;
gap: var(--space-xs);
min-width: 0;
max-width: 100%;
}
.task-review-tab__reviewer-login,
.task-review-tab__check-name {
min-width: 0;
max-width: 100%;
color: var(--text);
font-family: var(--font-primary);
font-size: 0.8125rem;
font-weight: 600;
line-height: 1.4;
overflow-wrap: anywhere;
}
.task-review-tab__github-link {
display: inline-flex;
align-items: center;
gap: var(--space-xs);
min-width: 0;
color: var(--color-primary);
font-family: var(--font-primary);
font-size: 0.75rem;
font-weight: 600;
line-height: 1.5;
overflow-wrap: anywhere;
}
.task-review-tab__github-link svg {
width: 1em;
height: 1em;
flex-shrink: 0;
}
.task-review-tab__empty {
overflow-wrap: anywhere;
}
@@ -418,6 +504,21 @@ The Review tab mirrors the Import-from-GitHub comment provenance model: avatar,
align-items: flex-start;
}
.task-review-tab__pr-summary,
.task-review-tab__pr-item-meta {
align-items: stretch;
}
.task-review-tab__pill-list,
.task-review-tab__pill-list-item,
.task-review-tab__pr-item-meta {
width: 100%;
}
.task-review-tab__github-link {
max-width: 100%;
}
.task-review-tab__status,
.task-review-tab__decision {
max-width: 100%;

View File

@@ -1,7 +1,7 @@
import "./TaskReviewTab.css";
import { getErrorMessage, type Task, type TaskDetail } from "@fusion/core";
import { getErrorMessage, type PrCheckStatus, type Task, type TaskDetail, type TaskReviewSummary } from "@fusion/core";
import { resolveEffectiveAutoMerge } from "../../../core/src/task-merge";
import { Bot, GitPullRequest, User } from "lucide-react";
import { Bot, ExternalLink, GitPullRequest, User } from "lucide-react";
import { useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { fetchTaskReview, refreshTaskReview, reviseTaskReviewItems, updateTask } from "../api";
@@ -84,6 +84,26 @@ function formatRefreshSource(source?: "manual" | "auto" | "initial-load", t?: (k
return t?.("taskReview.refreshSourceInitialLoad", "Initial load") ?? "Initial load";
}
function summarizeChecks(checks: PrCheckStatus[] | undefined, t: (key: string, defaultValue: string, options?: Record<string, unknown>) => string): string {
const safeChecks = checks ?? [];
if (safeChecks.length === 0) return t("taskReview.noChecksReported", "No checks reported");
const requiredChecks = safeChecks.filter((check) => check.required).length;
const successfulChecks = safeChecks.filter((check) => check.state === "success").length;
const blockingChecks = safeChecks.filter((check) => check.required && check.state !== "success" && check.state !== "skipped" && check.state !== "neutral").length;
return t("taskReview.checksSummary", "{{successful}}/{{total}} checks passing · {{required}} required · {{blocking}} blocking", {
successful: successfulChecks,
total: safeChecks.length,
required: requiredChecks,
blocking: blockingChecks,
});
}
function getCheckTone(check: PrCheckStatus): "APPROVED" | "CHANGES_REQUESTED" | "REVIEW_REQUIRED" {
if (check.state === "success" || check.state === "skipped" || check.state === "neutral") return "APPROVED";
if (check.state === "pending" || check.state === "stale") return "REVIEW_REQUIRED";
return "CHANGES_REQUESTED";
}
function getDisplayReviewItems(review: ReviewState): DisplayReviewItem[] {
const addressingById = new Map(review.addressing.map((record) => [record.itemId, record] as const));
const items = review.items.map((item) => {
@@ -144,6 +164,7 @@ export function TaskReviewTab({
const [isSavingAutoMergePreference, setIsSavingAutoMergePreference] = useState(false);
const isPrMode = review?.source === "pull-request";
const prSummary = isPrMode ? review?.summary as TaskReviewSummary | undefined : undefined;
const displayItems = useMemo(() => (review ? getDisplayReviewItems(review) : []), [review]);
const filteredDisplayItems = useMemo(() => {
if (authorTypeFilter === "all") return displayItems;
@@ -298,7 +319,7 @@ export function TaskReviewTab({
author: item.item.author?.login,
summary: item.item.summary ?? item.item.body.slice(0, 120),
body: item.item.body,
url: typeof itemRecord.url === "string" ? itemRecord.url : undefined,
url: item.item.htmlUrl ?? (typeof itemRecord.url === "string" ? itemRecord.url : undefined),
};
});
@@ -397,6 +418,51 @@ export function TaskReviewTab({
source: formatRefreshSource(review?.refreshSource, t),
})}</span>
</div>
{/*
FNXC:TaskReviewTab 2026-06-27-23:38:
PR-linked tasks need Review-tab context that is already present in the GitHub review payload: decision, reviewers, checks, blockers, and per-item author/state/GitHub links. Keep this branch gated to pull-request mode so reviewer-agent reviews retain their established direct-mode layout.
*/}
{isPrMode ? (
<section className="task-review-tab__pr-summary" aria-label={t("taskReview.prSummaryAria", "Pull request review summary")}>
<div className="task-review-tab__pr-summary-section">
<div className="task-review-tab__pr-summary-label">{t("taskReview.reviewers", "Reviewers")}</div>
{prSummary?.reviewers?.length ? (
<ul className="task-review-tab__pill-list" aria-label={t("taskReview.reviewers", "Reviewers")}>
{prSummary.reviewers.map((reviewer) => (
<li key={`${reviewer.login}-${reviewer.state}`} className="task-review-tab__pill-list-item">
<span className="task-review-tab__reviewer-login">{reviewer.login}</span>
<span className={`task-review-tab__decision task-review-tab__decision--${reviewer.state}`}>{reviewer.state}</span>
</li>
))}
</ul>
) : (
<div className="task-review-tab__meta">{t("taskReview.noReviewers", "No reviewers reported")}</div>
)}
</div>
<div className="task-review-tab__pr-summary-section">
<div className="task-review-tab__pr-summary-label">{t("taskReview.checks", "Checks")}</div>
<div className="task-review-tab__meta">{summarizeChecks(prSummary?.checks, t)}</div>
{prSummary?.checks?.length ? (
<ul className="task-review-tab__pill-list" aria-label={t("taskReview.checks", "Checks")}>
{prSummary.checks.map((check) => (
<li key={`${check.name}-${check.state}`} className="task-review-tab__pill-list-item">
<span className="task-review-tab__check-name">{check.name}</span>
<span className={`task-review-tab__decision task-review-tab__decision--${getCheckTone(check)}`}>{check.state}</span>
</li>
))}
</ul>
) : null}
</div>
{prSummary?.blockingReasons?.length ? (
<div className="task-review-tab__pr-summary-section">
<div className="task-review-tab__pr-summary-label">{t("taskReview.blockingReasons", "Blocking reasons")}</div>
<ul className="task-review-tab__blockers">
{prSummary.blockingReasons.map((reason) => <li key={reason}>{reason}</li>)}
</ul>
</div>
) : null}
</section>
) : null}
{loading ? <div className="task-review-tab__meta"><LoadingSpinner label={t("taskReview.loadingData", "Loading review data…")} /></div> : null}
{!loading && error ? <div className="task-review-tab__error">{error}</div> : null}
{!loading && !error && !isPrMode && displayItems.length === 0 ? <div className="task-review-tab__empty">{emptyMessage ?? t("taskReview.noFeedbackDirect", "No reviewer feedback yet — this task has not produced reviewer-agent feedback in direct mode.")}</div> : null}
@@ -409,6 +475,10 @@ export function TaskReviewTab({
const authorType = authorInfo.authorIsBot ? "bot" : "human";
const avatarKey = `${item.id}:${authorInfo.author}`;
const showAvatarImg = Boolean(authorInfo.authorAvatarUrl && !brokenAvatars.has(avatarKey));
const prAuthor = isPrMode ? item.item?.author?.login ?? item.addressing?.snapshot?.authorLogin : undefined;
const prState = isPrMode ? item.item?.state : undefined;
const prUrl = isPrMode ? item.item?.htmlUrl ?? item.addressing?.snapshot?.url : undefined;
const summaryPrefix = item.path && !isPrMode ? `${item.path}: ` : "";
return (
<li key={item.id} className="task-review-tab__item card" data-review-comment-author-type={authorType}>
@@ -417,7 +487,7 @@ export function TaskReviewTab({
<div className="task-review-tab__item-header">
<div className="task-review-tab__item-selection">
<input id={checkboxId} type="checkbox" checked={selected.includes(item.id)} onChange={() => toggleSelected(item.id)} />
<span className="task-review-tab__item-summary">{item.path ? `${item.path}: ` : ""}{item.summary}</span>
<span className="task-review-tab__item-summary">{summaryPrefix}{item.summary}</span>
</div>
<span className={`task-review-tab__status task-review-tab__status--${item.status}`}>{item.status}</span>
</div>
@@ -449,6 +519,22 @@ export function TaskReviewTab({
<time className="task-review-tab__comment-time" dateTime={item.createdAt} title={item.createdAt}>{formatTimestamp(item.createdAt, t)}</time>
</div>
<div className="task-review-tab__item-meta-list">
{isPrMode ? (
<div className="task-review-tab__pr-item-meta" aria-label={t("taskReview.prItemMeta", "Pull request review item metadata")}>
{prAuthor ? <span className="task-review-tab__meta">{t("taskReview.itemAuthor", "Author: {{author}}", { author: prAuthor })}</span> : null}
{prState ? <span className={`task-review-tab__decision task-review-tab__decision--${prState}`}>{prState}</span> : null}
<span className="task-review-tab__meta">{formatTimestamp(item.createdAt, t)}</span>
{item.path ? <span className="task-review-tab__meta">{item.path}</span> : null}
{prUrl ? (
<a className="task-review-tab__github-link" href={prUrl} target="_blank" rel="noopener noreferrer">
{t("taskReview.viewOnGitHub", "View on GitHub")}
<ExternalLink aria-hidden="true" />
</a>
) : null}
</div>
) : (
<div className="task-review-tab__meta">{formatTimestamp(item.createdAt, t)}</div>
)}
{item.addressing ? (
<div className="task-review-tab__meta">
{t("taskReview.selectedAt", "Selected: {{timestamp}}", { timestamp: formatTimestamp(item.addressing.selectedAt, t) })}

View File

@@ -3,7 +3,7 @@ FNXC:DashboardTests 2026-06-14-08:31:
FN-6441 rescued this orphaned component test after standalone dashboard-app execution passed without assertion, timeout, or source-code changes. Keep it registered through the app backfill lane so task-review UI regressions cannot silently fall out of quality coverage again.
*/
import { describe, it, expect, vi, beforeEach } from "vitest";
import { act, render as rtlRender, screen, fireEvent, waitFor } from "@testing-library/react";
import { act, render as rtlRender, screen, fireEvent, waitFor, within } from "@testing-library/react";
import { TaskReviewTab } from "../TaskReviewTab";
import { makeTask } from "./TaskDetailModal.test-helpers";
import { loadAllAppCss } from "../../test/cssFixture";
@@ -47,6 +47,7 @@ describe("TaskReviewTab", () => {
await renderWithAct(<TaskReviewTab task={makeTask({ reviewState: undefined })} addToast={vi.fn()} />);
expect(await screen.findByText("No reviewer feedback yet — this task has not produced reviewer-agent feedback in direct mode.")).toBeInTheDocument();
expect(screen.queryByLabelText("Pull request review summary")).not.toBeInTheDocument();
expect(screen.getByRole("button", { name: "Request revision" })).toBeDisabled();
});
@@ -67,7 +68,7 @@ describe("TaskReviewTab", () => {
await renderWithAct(<TaskReviewTab task={task} addToast={addToast} />);
fireEvent.click(await screen.findByRole("button", { name: "Refresh" }));
expect(apiMocks.refreshTaskReview).toHaveBeenCalledWith(task.id, undefined);
expect(await screen.findByText("APPROVED")).toBeInTheDocument();
expect((await screen.findAllByText("APPROVED")).length).toBeGreaterThan(0);
expect(screen.getAllByText("Looks good").length).toBeGreaterThan(0);
expect(addToast).toHaveBeenCalledWith("Review refreshed", "success");
});
@@ -127,6 +128,139 @@ describe("TaskReviewTab", () => {
await renderWithAct(<TaskReviewTab task={task} addToast={vi.fn()} />);
expect(await screen.findByText("No review items yet.")).toBeInTheDocument();
expect(screen.getByText("No reviewers reported")).toBeInTheDocument();
expect(screen.getByText("No checks reported")).toBeInTheDocument();
});
it("renders PR decision, reviewers, checks, blockers, and per-item GitHub metadata", async () => {
const task = makeTask({
reviewState: {
source: "pull-request",
summary: {
reviewDecision: "CHANGES_REQUESTED",
reviewers: [{ login: "octocat", state: "CHANGES_REQUESTED", submittedAt: "2026-06-27T12:00:00.000Z" }],
blockingReasons: ["1 required review requesting changes"],
checks: [
{ name: "lint", required: true, state: "success" },
{ name: "test", required: true, state: "failure", detailsUrl: "https://github.example/checks/test" },
],
},
items: [
{
id: "review-1",
body: "Please fix the failing branch.",
author: { login: "octocat" },
createdAt: "2026-06-27T12:00:00.000Z",
path: "src/parser.ts",
summary: "Review CHANGES_REQUESTED",
state: "CHANGES_REQUESTED",
htmlUrl: "https://github.example/reviews/1",
},
{
id: "comment-1",
body: "Nit: rename this variable.",
author: { login: "hubot" },
createdAt: "2026-06-27T12:05:00.000Z",
summary: "PR comment",
state: "COMMENTED",
htmlUrl: "https://github.example/comments/1",
},
],
addressing: [],
},
});
apiMocks.fetchTaskReview.mockResolvedValue({ reviewState: task.reviewState, automationStatus: null, emptyMessage: null });
const { container } = await renderWithAct(<TaskReviewTab task={task} addToast={vi.fn()} />);
expect(await screen.findByText("CHANGES_REQUESTED · 2 review item(s)")).toBeInTheDocument();
const summary = screen.getByLabelText("Pull request review summary");
expect(within(summary).getByText("octocat")).toBeInTheDocument();
expect(within(summary).getByText("1/2 checks passing · 2 required · 1 blocking")).toBeInTheDocument();
expect(within(summary).getByText("lint")).toBeInTheDocument();
expect(within(summary).getByText("test")).toBeInTheDocument();
expect(within(summary).getByText("1 required review requesting changes")).toBeInTheDocument();
expect(screen.getByText("Author: octocat")).toBeInTheDocument();
expect(screen.getByText("Author: hubot")).toBeInTheDocument();
expect(screen.getByText("src/parser.ts")).toBeInTheDocument();
expect(screen.getAllByText("COMMENTED").length).toBeGreaterThan(0);
const githubLinks = screen.getAllByRole("link", { name: /View on GitHub/ });
expect(githubLinks.map((link) => link.getAttribute("href"))).toEqual(["https://github.example/reviews/1", "https://github.example/comments/1"]);
expect(githubLinks[0]).toHaveAttribute("target", "_blank");
expect(githubLinks[0]).toHaveAttribute("rel", "noopener noreferrer");
expect(container.querySelectorAll(".task-review-tab__summary-group .task-review-tab__decision")).toHaveLength(1);
});
it.each([
["APPROVED", "APPROVED · 0 review item(s)", true],
["CHANGES_REQUESTED", "CHANGES_REQUESTED · 0 review item(s)", true],
["REVIEW_REQUIRED", "REVIEW_REQUIRED · 0 review item(s)", true],
[null, "REVIEW_REQUIRED · 0 review item(s)", false],
] as const)("renders PR decision state %s without undefined badges", async (decision, summaryText, hasDecisionBadge) => {
const task = makeTask({
reviewState: {
source: "pull-request",
summary: { reviewDecision: decision, reviewers: [], blockingReasons: [], checks: [] },
items: [],
addressing: [],
},
});
apiMocks.fetchTaskReview.mockResolvedValue({ reviewState: task.reviewState, automationStatus: null, emptyMessage: null });
const { container } = await renderWithAct(<TaskReviewTab task={task} addToast={vi.fn()} />);
expect(await screen.findByText(summaryText)).toBeInTheDocument();
expect(screen.queryByText("undefined")).not.toBeInTheDocument();
expect(container.querySelectorAll(".task-review-tab__summary-group .task-review-tab__decision")).toHaveLength(hasDecisionBadge ? 1 : 0);
});
it("renders PR items when optional path, state, URL, or body are missing", async () => {
const task = makeTask({
reviewState: {
source: "pull-request",
summary: { reviewDecision: "REVIEW_REQUIRED", reviewers: [], blockingReasons: [], checks: [] },
items: [
{
id: "comment-with-empty-body",
body: "",
author: { login: "code-review-bot" },
createdAt: "2026-06-27T12:10:00.000Z",
summary: "PR comment by code-review-bot",
},
],
addressing: [],
},
});
apiMocks.fetchTaskReview.mockResolvedValue({ reviewState: task.reviewState, automationStatus: null, emptyMessage: null });
await renderWithAct(<TaskReviewTab task={task} addToast={vi.fn()} />);
expect(await screen.findByText("PR comment by code-review-bot")).toBeInTheDocument();
expect(screen.getByText("Author: code-review-bot")).toBeInTheDocument();
expect(screen.queryByRole("link", { name: /View on GitHub/ })).not.toBeInTheDocument();
expect(screen.queryByText("undefined")).not.toBeInTheDocument();
});
it("toggles markdown/plain rendering for PR items without affecting selection", async () => {
const task = makeTask({
reviewState: {
source: "pull-request",
summary: { reviewDecision: "APPROVED", reviewers: [], blockingReasons: [], checks: [] },
items: [{ id: "markdown-pr-item", body: "**bold PR body**", author: { login: "octocat" }, createdAt: new Date().toISOString(), summary: "PR markdown item", state: "APPROVED" }],
addressing: [],
},
});
apiMocks.fetchTaskReview.mockResolvedValue({ reviewState: task.reviewState, automationStatus: null, emptyMessage: null });
const { container } = await renderWithAct(<TaskReviewTab task={task} addToast={vi.fn()} />);
await screen.findByText("PR markdown item");
expect(container.querySelector("strong")?.textContent).toBe("bold PR body");
const checkbox = screen.getByRole("checkbox");
fireEvent.click(screen.getByTestId("task-review-markdown-toggle"));
await waitFor(() => expect(container.querySelector("pre.task-review-tab__body")?.textContent).toContain("**bold PR body**"));
fireEvent.click(container.querySelector("pre.task-review-tab__body") as HTMLElement);
expect(checkbox).not.toBeChecked();
});
it("shows load error when initial review fetch fails", async () => {
@@ -475,7 +609,7 @@ describe("TaskReviewTab", () => {
summary: "Parser guard is missing",
threadId: "thread-1",
line: 42,
url: "https://example.test/thread/1",
htmlUrl: "https://example.test/thread/1",
},
],
addressing: [{ itemId: "ri-1", status: "queued", selectedAt: new Date().toISOString() }],
@@ -689,6 +823,9 @@ describe("TaskReviewTab", () => {
expect(baseBodyRule).toMatch(/overflow-wrap\s*:\s*anywhere\s*;/);
expect(baseBodyRule).toMatch(/overflow-x\s*:\s*auto\s*;/);
expect(taskReviewCss).toMatch(/\.task-review-tab__item\s*\{[^}]*padding\s*:\s*var\(--card-padding\)\s*;[^}]*\}/);
expect(taskReviewCss).toMatch(/\.task-review-tab__pr-summary\s*\{[^}]*gap\s*:\s*var\(--space-md\)\s*;[^}]*\}/);
expect(taskReviewCss).toMatch(/\.task-review-tab__pr-item-meta\s*\{[^}]*flex-wrap\s*:\s*wrap\s*;[^}]*\}/);
expect(mobileCss).toMatch(/\.task-review-tab__pill-list,\s*\.task-review-tab__pill-list-item,\s*\.task-review-tab__pr-item-meta\s*\{[^}]*width\s*:\s*100%\s*;[^}]*\}/);
});
it("preserves review header structure across sources and empty or populated states", async () => {