feat(FN-5314): add broad-scope advisory chip to TaskCard and banner to Task
Adds broad-scope advisory chips to TaskCard and banner to TaskDetailModal with comprehensive test coverage and a dashboard guide entry. TaskCard gains an inline advisory chip (with styles), TaskDetailModal gets a matching banner, and both components have dedicated test suites covering the new UI ele Fusion-Task-Id: FN-5314 Fusion-Task-Lineage: cbc88f36-d029-4e4b-ac9c-32a77560b8b9
This commit is contained in:
committed by
gsxdsm
parent
22d59b5f9a
commit
c9ff2078b5
@@ -628,6 +628,28 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.card-broad-scope-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-xs);
|
||||
font-size: 0.625rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-warning);
|
||||
background: color-mix(in srgb, var(--color-warning) 18%, transparent);
|
||||
border: var(--btn-border-width) solid color-mix(in srgb, var(--color-warning) 35%, transparent);
|
||||
padding: calc(var(--space-xs) / 4) calc(var(--space-sm) - (var(--space-xs) / 4));
|
||||
border-radius: var(--radius-pill);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.card-broad-scope-chip-text {
|
||||
min-width: 0;
|
||||
flex: 0 1 auto;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@container task-card (max-width: 240px) {
|
||||
.card-agent-badge-text {
|
||||
display: none;
|
||||
@@ -639,6 +661,9 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
.card-broad-scope-chip-text {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.card-agent-badge--loading {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import "./TaskCard.css";
|
||||
import { memo, useCallback, useState, useRef, useEffect, useMemo } from "react";
|
||||
import { Link, Clock, Layers, Pencil, ChevronDown, Folder, Target, Bot, Trash2, RotateCw, Zap, GitBranch, GitPullRequest } from "lucide-react";
|
||||
import { Link, Clock, Layers, Pencil, ChevronDown, Folder, Target, Bot, Trash2, RotateCw, Zap, GitBranch, GitPullRequest, AlertTriangle } from "lucide-react";
|
||||
import type { Task, TaskDetail, Column, PrInfo, IssueInfo, TaskPriority, GithubIssueAction } from "@fusion/core";
|
||||
import {
|
||||
COLUMN_LABELS,
|
||||
@@ -346,6 +346,57 @@ function getIssueUrlFromMetadata(metadata: Task["sourceMetadata"]): string | und
|
||||
return typeof issueUrl === "string" && issueUrl.length > 0 ? issueUrl : undefined;
|
||||
}
|
||||
|
||||
const BROAD_SCOPE_REASON_LABELS: Record<string, string> = {
|
||||
"size-l": "Size L",
|
||||
"steps-high": "many steps",
|
||||
"file-scope-high": "large file scope",
|
||||
"failing-file-mentions-high": "many failing files mentioned",
|
||||
"size-l-with-many-steps": "Size L + many steps",
|
||||
};
|
||||
|
||||
function getBroadScopeFlag(sourceMetadata: Task["sourceMetadata"]): {
|
||||
score: number;
|
||||
reasons: string[];
|
||||
signals?: {
|
||||
size?: string | null;
|
||||
stepCount?: number;
|
||||
fileScopeCount?: number;
|
||||
failingFileMentions?: number;
|
||||
};
|
||||
} | null {
|
||||
const candidate = sourceMetadata?.broadScopeFlag;
|
||||
if (!candidate || typeof candidate !== "object" || Array.isArray(candidate)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const score = (candidate as { score?: unknown }).score;
|
||||
const reasons = (candidate as { reasons?: unknown }).reasons;
|
||||
const signals = (candidate as { signals?: unknown }).signals;
|
||||
|
||||
if (typeof score !== "number" || !Number.isFinite(score) || !Array.isArray(reasons) || !reasons.every((reason) => typeof reason === "string")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (signals != null && (typeof signals !== "object" || Array.isArray(signals))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
score,
|
||||
reasons,
|
||||
signals: signals as {
|
||||
size?: string | null;
|
||||
stepCount?: number;
|
||||
fileScopeCount?: number;
|
||||
failingFileMentions?: number;
|
||||
} | undefined,
|
||||
};
|
||||
}
|
||||
|
||||
function formatBroadScopeReasons(reasons: string[]): string {
|
||||
return reasons.map((reason) => BROAD_SCOPE_REASON_LABELS[reason] ?? reason).join(", ");
|
||||
}
|
||||
|
||||
function parseGithubIssueUrl(url?: string): { owner: string; repo: string; number: number } | null {
|
||||
if (!url) return null;
|
||||
const match = url.match(/^https?:\/\/github\.com\/([^/]+)\/([^/]+)\/issues\/(\d+)(?:$|[/?#])/i);
|
||||
@@ -492,6 +543,7 @@ function areTaskCardPropsEqual(previous: TaskCardProps, next: TaskCardProps): bo
|
||||
previousTask.sourceAgentId === nextTask.sourceAgentId &&
|
||||
previousTask.sourceMetadata?.issueUrl === nextTask.sourceMetadata?.issueUrl &&
|
||||
previousTask.sourceMetadata?.agentName === nextTask.sourceMetadata?.agentName &&
|
||||
previousTask.sourceMetadata?.broadScopeFlag === nextTask.sourceMetadata?.broadScopeFlag &&
|
||||
previousTask.stalledReview?.reason === nextTask.stalledReview?.reason &&
|
||||
previousTask.stalledReview?.heuristic === nextTask.stalledReview?.heuristic &&
|
||||
previousTask.stalledReview?.matchCount === nextTask.stalledReview?.matchCount &&
|
||||
@@ -847,6 +899,10 @@ function TaskCardComponent({
|
||||
const isAgentCreated = isAgentCreatedTask(task);
|
||||
const sourceAgentName = getSourceAgentName(task);
|
||||
const agentCreatedTitle = sourceAgentName ? `Created by agent: ${sourceAgentName}` : "Created by agent";
|
||||
const broadScopeFlag = getBroadScopeFlag(task.sourceMetadata);
|
||||
const broadScopeTitle = broadScopeFlag
|
||||
? `Broad-scope advisory (score ${broadScopeFlag.score}): ${formatBroadScopeReasons(broadScopeFlag.reasons)}`
|
||||
: null;
|
||||
const isAgentNameLoading = Boolean(task.assignedAgentId && agentName === null);
|
||||
const taskProviders = useMemo(() => {
|
||||
const providers: string[] = [];
|
||||
@@ -1722,6 +1778,13 @@ function TaskCardComponent({
|
||||
{abbreviateMissionTitle(missionTitle ?? task.missionId)}
|
||||
</span>
|
||||
)}
|
||||
{broadScopeFlag && broadScopeTitle && (
|
||||
<span className="card-broad-scope-chip" title={broadScopeTitle} aria-label={broadScopeTitle}>
|
||||
<AlertTriangle aria-hidden="true" />
|
||||
<span className="card-broad-scope-chip-text" aria-hidden="true">Broad scope</span>
|
||||
<span className="visually-hidden">{broadScopeTitle}</span>
|
||||
</span>
|
||||
)}
|
||||
<div className="card-header-actions">
|
||||
{canEdit && (
|
||||
<button
|
||||
|
||||
@@ -188,7 +188,46 @@
|
||||
color: var(--text-dim);
|
||||
}
|
||||
|
||||
.detail-broad-scope-banner {
|
||||
display: flex;
|
||||
gap: var(--space-sm);
|
||||
align-items: flex-start;
|
||||
padding: var(--space-sm) var(--space-md);
|
||||
margin-top: var(--space-sm);
|
||||
border-radius: var(--radius-md);
|
||||
color: var(--text);
|
||||
background: color-mix(in srgb, var(--color-warning) 12%, transparent);
|
||||
border: var(--btn-border-width) solid color-mix(in srgb, var(--color-warning) 32%, transparent);
|
||||
}
|
||||
|
||||
.detail-broad-scope-banner-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: calc(var(--space-xs) / 2);
|
||||
min-width: 0;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.detail-broad-scope-banner-heading {
|
||||
font-weight: 600;
|
||||
color: var(--color-warning);
|
||||
font-size: 0.8125rem;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.detail-broad-scope-banner-note {
|
||||
color: var(--text-muted);
|
||||
font-size: 0.6875rem;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.detail-broad-scope-banner {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.detail-broad-scope-banner-content {
|
||||
gap: var(--space-xs);
|
||||
}
|
||||
.detail-provenance {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import "./TaskDetailModal.css";
|
||||
import React, { Suspense, lazy, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { Pencil, Bot, X, ChevronDown, ChevronRight, GitBranch, ArrowLeft, Zap, Loader2 } from "lucide-react";
|
||||
import { Pencil, Bot, X, ChevronDown, ChevronRight, GitBranch, ArrowLeft, Zap, Loader2, AlertTriangle } from "lucide-react";
|
||||
import { useModalResizePersist } from "../hooks/useModalResizePersist";
|
||||
import { useMobileScrollLock } from "../hooks/useMobileScrollLock";
|
||||
import { useOverlayDismiss } from "../hooks/useOverlayDismiss";
|
||||
@@ -375,6 +375,57 @@ function parseGithubIssueLabel(url: string): { label: string; href: string } | n
|
||||
};
|
||||
}
|
||||
|
||||
const BROAD_SCOPE_REASON_LABELS: Record<string, string> = {
|
||||
"size-l": "Size L",
|
||||
"steps-high": "many steps",
|
||||
"file-scope-high": "large file scope",
|
||||
"failing-file-mentions-high": "many failing files mentioned",
|
||||
"size-l-with-many-steps": "Size L + many steps",
|
||||
};
|
||||
|
||||
function getBroadScopeFlag(sourceMetadata: Task["sourceMetadata"]): {
|
||||
score: number;
|
||||
reasons: string[];
|
||||
signals?: {
|
||||
size?: string | null;
|
||||
stepCount?: number;
|
||||
fileScopeCount?: number;
|
||||
failingFileMentions?: number;
|
||||
};
|
||||
} | null {
|
||||
const candidate = sourceMetadata?.broadScopeFlag;
|
||||
if (!candidate || typeof candidate !== "object" || Array.isArray(candidate)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const score = (candidate as { score?: unknown }).score;
|
||||
const reasons = (candidate as { reasons?: unknown }).reasons;
|
||||
const signals = (candidate as { signals?: unknown }).signals;
|
||||
|
||||
if (typeof score !== "number" || !Number.isFinite(score) || !Array.isArray(reasons) || !reasons.every((reason) => typeof reason === "string")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (signals != null && (typeof signals !== "object" || Array.isArray(signals))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
score,
|
||||
reasons,
|
||||
signals: signals as {
|
||||
size?: string | null;
|
||||
stepCount?: number;
|
||||
fileScopeCount?: number;
|
||||
failingFileMentions?: number;
|
||||
} | undefined,
|
||||
};
|
||||
}
|
||||
|
||||
function formatBroadScopeReasons(reasons: string[]): string {
|
||||
return reasons.map((reason) => BROAD_SCOPE_REASON_LABELS[reason] ?? reason).join(", ");
|
||||
}
|
||||
|
||||
function getResearchContextInfo(metadata: Task["sourceMetadata"]): string | undefined {
|
||||
const findingLabel = metadata?.findingLabel;
|
||||
if (typeof findingLabel === "string" && findingLabel.length > 0) {
|
||||
@@ -546,6 +597,7 @@ export function TaskDetailContent({
|
||||
const provenanceDisplay = getProvenanceLabel(workingTask, {
|
||||
sourceAgentName: sourceAgent?.name,
|
||||
});
|
||||
const broadScopeFlag = getBroadScopeFlag(workingTask.sourceMetadata);
|
||||
|
||||
// Sync activeTab when the caller changes initialTab (e.g. opening a different tab)
|
||||
useEffect(() => {
|
||||
@@ -2439,6 +2491,26 @@ export function TaskDetailContent({
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{broadScopeFlag && (
|
||||
<div className="detail-broad-scope-banner" aria-label="Triage broad-scope advisory">
|
||||
<AlertTriangle aria-hidden="true" />
|
||||
<div className="detail-broad-scope-banner-content">
|
||||
<div className="detail-broad-scope-banner-heading">Triage broad-scope advisory</div>
|
||||
<div>{`Score ${broadScopeFlag.score} · ${formatBroadScopeReasons(broadScopeFlag.reasons)}`}</div>
|
||||
<div>
|
||||
{[
|
||||
broadScopeFlag.signals?.size ? `Size: ${broadScopeFlag.signals.size}` : null,
|
||||
typeof broadScopeFlag.signals?.stepCount === "number" ? `Steps: ${broadScopeFlag.signals.stepCount}` : null,
|
||||
typeof broadScopeFlag.signals?.fileScopeCount === "number" ? `File scope: ${broadScopeFlag.signals.fileScopeCount}` : null,
|
||||
typeof broadScopeFlag.signals?.failingFileMentions === "number"
|
||||
? `Failing-file mentions: ${broadScopeFlag.signals.failingFileMentions}`
|
||||
: null,
|
||||
].filter(Boolean).join(" · ")}
|
||||
</div>
|
||||
<div className="detail-broad-scope-banner-note">Advisory only — task lifecycle is unaffected.</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{(task.prInfo?.number || task.mergeDetails?.prNumber) && (
|
||||
<div className="detail-provenance detail-pr-link-row">
|
||||
<GitBranch aria-hidden="true" />
|
||||
|
||||
@@ -19,6 +19,7 @@ vi.mock("lucide-react", () => ({
|
||||
Trash2: () => null,
|
||||
RotateCw: () => null,
|
||||
Zap: () => null,
|
||||
AlertTriangle: () => null,
|
||||
}));
|
||||
|
||||
vi.mock("../ProviderIcon", () => ({
|
||||
|
||||
@@ -21,6 +21,7 @@ vi.mock("lucide-react", () => ({
|
||||
Trash2: () => null,
|
||||
RotateCw: () => null,
|
||||
Zap: () => null,
|
||||
AlertTriangle: () => null,
|
||||
}));
|
||||
|
||||
vi.mock("../ProviderIcon", () => ({
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import type { Task } from "@fusion/core";
|
||||
import { TaskCard } from "../TaskCard";
|
||||
import { loadAllAppCss } from "../../test/cssFixture";
|
||||
|
||||
vi.mock("lucide-react", () => ({
|
||||
Link: () => <svg />,
|
||||
GitBranch: () => <svg />,
|
||||
Clock: () => <svg />,
|
||||
Pencil: () => <svg />,
|
||||
Layers: () => <svg />,
|
||||
ChevronDown: () => <svg />,
|
||||
Folder: () => <svg />,
|
||||
GitPullRequest: () => <svg />,
|
||||
CircleDot: () => <svg />,
|
||||
Target: () => <svg />,
|
||||
Bot: () => <svg />,
|
||||
Trash2: () => <svg />,
|
||||
RotateCw: () => <svg />,
|
||||
Zap: () => <svg />,
|
||||
AlertTriangle: () => <svg />,
|
||||
}));
|
||||
|
||||
vi.mock("../../hooks/useTaskDiffStats", () => ({
|
||||
useTaskDiffStats: () => ({ stats: null, loading: false }),
|
||||
}));
|
||||
|
||||
vi.mock("../../hooks/useBadgeWebSocket", () => ({
|
||||
useBadgeWebSocket: () => ({
|
||||
badgeUpdates: new Map(),
|
||||
isConnected: true,
|
||||
subscribeToBadge: vi.fn(),
|
||||
unsubscribeFromBadge: vi.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("../../hooks/useBatchBadgeFetch", () => ({
|
||||
getFreshBatchData: vi.fn(() => null),
|
||||
}));
|
||||
|
||||
vi.mock("../../api", () => ({
|
||||
fetchTaskDetail: vi.fn(),
|
||||
uploadAttachment: vi.fn(),
|
||||
fetchMission: vi.fn(),
|
||||
fetchAgent: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("../../hooks/useConfirm", () => ({
|
||||
useConfirm: () => ({ confirm: vi.fn(async () => true) }),
|
||||
}));
|
||||
|
||||
function makeTask(overrides: Partial<Task> = {}): Task {
|
||||
return {
|
||||
id: "FN-5314",
|
||||
title: "Broad scope test",
|
||||
description: "",
|
||||
column: "todo",
|
||||
status: undefined,
|
||||
steps: [],
|
||||
dependencies: [],
|
||||
...overrides,
|
||||
} as Task;
|
||||
}
|
||||
|
||||
const noop = () => {};
|
||||
|
||||
function mountCss(): () => void {
|
||||
const style = document.createElement("style");
|
||||
style.textContent = loadAllAppCss();
|
||||
document.head.appendChild(style);
|
||||
return () => style.remove();
|
||||
}
|
||||
|
||||
describe("TaskCard broad-scope advisory chip", () => {
|
||||
it("renders chip for well-formed broadScopeFlag", () => {
|
||||
render(
|
||||
<TaskCard
|
||||
task={makeTask({
|
||||
sourceMetadata: {
|
||||
broadScopeFlag: {
|
||||
score: 5,
|
||||
reasons: ["size-l", "steps-high"],
|
||||
signals: { size: "L", stepCount: 13, fileScopeCount: 22, failingFileMentions: 0 },
|
||||
},
|
||||
},
|
||||
})}
|
||||
onOpenDetail={noop}
|
||||
addToast={noop}
|
||||
/>,
|
||||
);
|
||||
|
||||
const label = "Broad-scope advisory (score 5): Size L, many steps";
|
||||
const chip = screen.getByLabelText(label);
|
||||
expect(chip).toBeInTheDocument();
|
||||
expect(chip).toHaveAttribute("title", label);
|
||||
expect(chip.tagName).toBe("SPAN");
|
||||
});
|
||||
|
||||
it("uses warning color + pill radius tokenized styles", () => {
|
||||
const unmountCss = mountCss();
|
||||
document.documentElement.style.setProperty("--color-warning", "rgb(210, 120, 0)");
|
||||
document.documentElement.style.setProperty("--radius-pill", "9999px");
|
||||
|
||||
render(
|
||||
<TaskCard
|
||||
task={makeTask({
|
||||
sourceMetadata: {
|
||||
broadScopeFlag: {
|
||||
score: 5,
|
||||
reasons: ["size-l"],
|
||||
},
|
||||
},
|
||||
})}
|
||||
onOpenDetail={noop}
|
||||
addToast={noop}
|
||||
/>,
|
||||
);
|
||||
|
||||
const chip = screen.getByLabelText("Broad-scope advisory (score 5): Size L");
|
||||
const styles = window.getComputedStyle(chip);
|
||||
expect(styles.borderRadius).toBe("var(--radius-pill)");
|
||||
expect(styles.color).toBe("var(--color-warning)");
|
||||
|
||||
unmountCss();
|
||||
document.documentElement.style.removeProperty("--color-warning");
|
||||
document.documentElement.style.removeProperty("--radius-pill");
|
||||
});
|
||||
|
||||
it.each([
|
||||
["missing sourceMetadata", undefined],
|
||||
["missing broadScopeFlag", {}],
|
||||
["non-numeric score", { broadScopeFlag: { score: "5", reasons: ["size-l"] } }],
|
||||
["non-array reasons", { broadScopeFlag: { score: 5, reasons: "size-l" } }],
|
||||
])("does not render chip when %s", (_label, sourceMetadata) => {
|
||||
render(
|
||||
<TaskCard
|
||||
task={makeTask({ sourceMetadata: sourceMetadata as Task["sourceMetadata"] })}
|
||||
onOpenDetail={noop}
|
||||
addToast={noop}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.queryByText("Broad scope")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -19,6 +19,7 @@ vi.mock("lucide-react", () => ({
|
||||
Trash2: () => <svg />,
|
||||
RotateCw: () => <svg />,
|
||||
Zap: () => <svg />,
|
||||
AlertTriangle: () => <svg />,
|
||||
}));
|
||||
|
||||
vi.mock("../../hooks/useTaskDiffStats", () => ({
|
||||
|
||||
@@ -21,6 +21,7 @@ vi.mock("lucide-react", () => ({
|
||||
Trash2: () => <svg />,
|
||||
RotateCw: () => <svg />,
|
||||
Zap: () => <svg />,
|
||||
AlertTriangle: () => <svg />,
|
||||
}));
|
||||
|
||||
vi.mock("../ProviderIcon", () => ({
|
||||
|
||||
@@ -20,6 +20,7 @@ vi.mock("lucide-react", () => ({
|
||||
Trash2: () => null,
|
||||
RotateCw: () => null,
|
||||
Zap: () => <svg data-testid="icon-zap" />,
|
||||
AlertTriangle: () => null,
|
||||
}));
|
||||
|
||||
vi.mock("../ProviderIcon", () => ({
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { render, screen, within } from "@testing-library/react";
|
||||
import { TaskDetailModal } from "../TaskDetailModal";
|
||||
import {
|
||||
makeTask,
|
||||
noop,
|
||||
noopDelete,
|
||||
noopMerge,
|
||||
noopMove,
|
||||
noopOpenDetail,
|
||||
setupTaskDetailModalHooks,
|
||||
} from "./TaskDetailModal.test-helpers";
|
||||
|
||||
setupTaskDetailModalHooks();
|
||||
|
||||
describe("TaskDetailModal broad-scope advisory banner", () => {
|
||||
it("renders banner details for well-formed broadScopeFlag and keeps provenance", () => {
|
||||
render(
|
||||
<TaskDetailModal
|
||||
task={makeTask({
|
||||
sourceType: "dashboard_ui",
|
||||
sourceMetadata: {
|
||||
broadScopeFlag: {
|
||||
score: 4,
|
||||
reasons: ["size-l", "steps-high", "file-scope-high"],
|
||||
signals: {
|
||||
size: "L",
|
||||
stepCount: 13,
|
||||
fileScopeCount: 21,
|
||||
failingFileMentions: 32,
|
||||
},
|
||||
},
|
||||
},
|
||||
})}
|
||||
onClose={noop}
|
||||
onMoveTask={noopMove}
|
||||
onDeleteTask={noopDelete}
|
||||
onMergeTask={noopMerge}
|
||||
onOpenDetail={noopOpenDetail}
|
||||
addToast={noop}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText("Created via Dashboard")).toBeInTheDocument();
|
||||
|
||||
const banner = screen.getByLabelText("Triage broad-scope advisory");
|
||||
expect(banner.tagName).toBe("DIV");
|
||||
expect(within(banner).queryByRole("button")).not.toBeInTheDocument();
|
||||
expect(within(banner).getByText("Triage broad-scope advisory")).toBeInTheDocument();
|
||||
expect(within(banner).getByText("Score 4 · Size L, many steps, large file scope")).toBeInTheDocument();
|
||||
expect(within(banner).getByText("Size: L · Steps: 13 · File scope: 21 · Failing-file mentions: 32")).toBeInTheDocument();
|
||||
expect(within(banner).getByText("Advisory only — task lifecycle is unaffected.")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it.each([
|
||||
["missing sourceMetadata", undefined],
|
||||
["missing broadScopeFlag", {}],
|
||||
["non-numeric score", { broadScopeFlag: { score: "4", reasons: ["size-l"] } }],
|
||||
["non-array reasons", { broadScopeFlag: { score: 4, reasons: "size-l" } }],
|
||||
])("does not render banner when %s", (_label, sourceMetadata) => {
|
||||
render(
|
||||
<TaskDetailModal
|
||||
task={makeTask({ sourceMetadata: sourceMetadata as any })}
|
||||
onClose={noop}
|
||||
onMoveTask={noopMove}
|
||||
onDeleteTask={noopDelete}
|
||||
onMergeTask={noopMerge}
|
||||
onOpenDetail={noopOpenDetail}
|
||||
addToast={noop}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.queryByLabelText("Triage broad-scope advisory")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -23,9 +23,12 @@ function initRepo(dir: string): void {
|
||||
|
||||
const created = new Set<string>();
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
for (const dir of created) rmSync(dir, { recursive: true, force: true });
|
||||
created.clear();
|
||||
try {
|
||||
for (const dir of created) rmSync(dir, { recursive: true, force: true });
|
||||
created.clear();
|
||||
} finally {
|
||||
vi.restoreAllMocks();
|
||||
}
|
||||
});
|
||||
|
||||
function mkRepo(): string {
|
||||
|
||||
Reference in New Issue
Block a user