fix(FN-3386): restore workspace verification gates and docs

- Resolve the cli-alias merge conflict by keeping mainline-deletion semantics under smart-prefer-main
- Add TaskCard workspace verification UI behavior and regression coverage
- Update plugin-sdk exports and related task/dashboard documentation for verification workflow

Fusion-Task-Id: FN-3386
This commit is contained in:
Fusion
2026-05-04 15:43:51 -07:00
committed by gsxdsm
parent c48feb2f79
commit 05cb972212
6 changed files with 209 additions and 3 deletions

View File

@@ -451,6 +451,20 @@
white-space: nowrap;
}
.card-agent-created-badge {
display: inline-flex;
align-items: center;
gap: var(--space-xs);
font-size: 0.625rem;
font-weight: 600;
color: var(--text-muted);
background: color-mix(in srgb, var(--text-muted) 18%, transparent);
border: var(--btn-border-width) solid color-mix(in srgb, var(--text-muted) 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-agent-badge-text {
min-width: 0;
overflow: hidden;
@@ -1061,6 +1075,11 @@
white-space: nowrap;
}
.card-agent-created-badge {
font-size: 0.5625rem;
padding: calc(var(--space-xs) / 4) var(--space-sm);
}
/* Card: wrap dependency badges */
.card-dep-list {
flex-wrap: wrap;

View File

@@ -79,6 +79,23 @@ function abbreviateBadge(text: string, max: number): string {
return text.slice(0, max - 3) + "...";
}
function getSourceAgentName(task: Task): string | undefined {
const metadataAgentName = task.sourceMetadata?.agentName;
if (typeof metadataAgentName === "string" && metadataAgentName.trim().length > 0) {
return metadataAgentName.trim();
}
if (typeof task.sourceAgentId === "string" && task.sourceAgentId.trim().length > 0) {
return task.sourceAgentId.trim();
}
return undefined;
}
function isAgentCreatedTask(task: Task): boolean {
return task.sourceType === "agent_heartbeat" || task.sourceType === "automation" || Boolean(getSourceAgentName(task));
}
// ── Constants ───────────────────────────────────────────────────────────────
const EDITABLE_COLUMNS: Set<Column> = new Set(["triage", "todo"]);
@@ -441,7 +458,9 @@ function areTaskCardPropsEqual(previous: TaskCardProps, next: TaskCardProps): bo
previousTask.assignedAgentId === nextTask.assignedAgentId &&
previousTask.mergeRetries === nextTask.mergeRetries &&
previousTask.sourceType === nextTask.sourceType &&
previousTask.sourceAgentId === nextTask.sourceAgentId &&
previousTask.sourceMetadata?.issueUrl === nextTask.sourceMetadata?.issueUrl &&
previousTask.sourceMetadata?.agentName === nextTask.sourceMetadata?.agentName &&
areAttachmentsEqual(previousTask.attachments, nextTask.attachments) &&
areCommentsEqual(previousTask.comments, nextTask.comments) &&
areTaskDependenciesEqual(previousTask.dependencies, nextTask.dependencies) &&
@@ -728,6 +747,9 @@ function TaskCardComponent({
const hasGitHubBadge = Boolean(task.prInfo || task.issueInfo);
const isGitHubImportedTask = task.sourceType === "github_import";
const sourceIssueUrl = getIssueUrlFromMetadata(task.sourceMetadata);
const isAgentCreated = isAgentCreatedTask(task);
const sourceAgentName = getSourceAgentName(task);
const agentCreatedTitle = sourceAgentName ? `Created by agent: ${sourceAgentName}` : "Created by agent";
const isAgentNameLoading = Boolean(task.assignedAgentId && agentName === null);
const taskProviders = useMemo(() => {
const providers: string[] = [];
@@ -1342,6 +1364,17 @@ function TaskCardComponent({
issueInfo={liveIssueInfo}
/>
)}
{isAgentCreated && (
<span
className="card-agent-created-badge"
title={agentCreatedTitle}
aria-label={agentCreatedTitle}
>
<Bot size={11} aria-hidden="true" />
<span className="visually-hidden">{agentCreatedTitle}</span>
<span aria-hidden="true">Agent</span>
</span>
)}
{showPriorityBadge && (
<span className={`card-priority-badge card-priority-badge--${normalizedPriority}`}>
{normalizedPriority}

View File

@@ -1,6 +1,6 @@
import { afterEach, describe, it, expect, vi } from "vitest";
import { render, screen, fireEvent, waitFor, act } from "@testing-library/react";
import { TaskCard, formatElapsedDurationDone } from "../TaskCard";
import { TaskCard, formatElapsedDurationDone, __test_areTaskCardPropsEqual } from "../TaskCard";
import type { Task } from "@fusion/core";
// Mock lucide-react to avoid SVG rendering issues in test env
@@ -730,12 +730,86 @@ describe("TaskCard", () => {
expect(screen.queryByTestId("provider-icon-github")).toBeNull();
});
it("renders agent-created provenance badge for automation tasks and prefers sourceMetadata.agentName", () => {
const { container } = render(
<TaskCard
task={makeTask({
column: "todo",
sourceType: "automation",
sourceAgentId: "agent-123",
sourceMetadata: { agentName: "Task Robot" },
})}
onOpenDetail={noop}
addToast={noop}
/>,
);
const badge = container.querySelector(".card-agent-created-badge");
expect(badge).not.toBeNull();
expect(badge?.getAttribute("title")).toBe("Created by agent: Task Robot");
});
it("renders agent-created provenance badge for agent_heartbeat tasks", () => {
const { container } = render(
<TaskCard
task={makeTask({
column: "todo",
sourceType: "agent_heartbeat",
sourceAgentId: "heartbeat-agent-1",
sourceMetadata: { agentName: "Scheduler Bot" },
})}
onOpenDetail={noop}
addToast={noop}
/>,
);
const badge = container.querySelector(".card-agent-created-badge");
expect(badge).not.toBeNull();
expect(badge?.getAttribute("title")).toBe("Created by agent: Scheduler Bot");
expect(badge?.getAttribute("aria-label")).toBe("Created by agent: Scheduler Bot");
});
it("renders agent-created provenance badge for legacy sourceAgentId-only tasks", () => {
const { container } = render(
<TaskCard
task={makeTask({
column: "todo",
sourceAgentId: "legacy-agent-1",
})}
onOpenDetail={noop}
addToast={noop}
/>,
);
const badge = container.querySelector(".card-agent-created-badge");
expect(badge).not.toBeNull();
expect(badge?.getAttribute("title")).toBe("Created by agent: legacy-agent-1");
});
it("does not render agent-created provenance badge for non-agent task sources", () => {
const { container } = render(
<TaskCard
task={makeTask({
column: "todo",
sourceType: "dashboard_ui",
sourceAgentId: undefined,
sourceMetadata: undefined,
})}
onOpenDetail={noop}
addToast={noop}
/>,
);
expect(container.querySelector(".card-agent-created-badge")).toBeNull();
});
it("coexists with GitHub badge and timer metadata", () => {
const { container } = render(
<TaskCard
task={makeTask({
column: "done",
sourceType: "github_import",
sourceAgentId: "agent-42",
sourceMetadata: { issueUrl: "https://github.com/owner/repo/issues/7" },
issueInfo: {
owner: "owner",
@@ -754,6 +828,7 @@ describe("TaskCard", () => {
expect(container.querySelector(".card-github-badge")).not.toBeNull();
expect(container.querySelector(".card-source-provenance")).not.toBeNull();
expect(container.querySelector(".card-agent-created-badge")).not.toBeNull();
expect(container.querySelector(".card-time-indicator")).not.toBeNull();
});
@@ -1172,6 +1247,50 @@ describe("TaskCard provider icons on agent row", () => {
});
});
describe("TaskCard memo comparator provenance behavior", () => {
it("returns false when sourceMetadata.agentName changes", () => {
const previousTask = makeTask({ sourceType: "automation", sourceMetadata: { agentName: "Agent One" } });
const nextTask = makeTask({ sourceType: "automation", sourceMetadata: { agentName: "Agent Two" } });
const previousProps = {
task: previousTask,
onOpenDetail: noop,
addToast: noop,
};
const nextProps = {
task: nextTask,
onOpenDetail: noop,
addToast: noop,
};
expect(__test_areTaskCardPropsEqual(previousProps as any, nextProps as any)).toBe(false);
});
it("returns false when sourceType changes", () => {
const previousTask = makeTask({ sourceType: "automation", sourceMetadata: { agentName: "Agent" } });
const nextTask = makeTask({ sourceType: "dashboard_ui", sourceMetadata: { agentName: "Agent" } });
expect(
__test_areTaskCardPropsEqual(
{ task: previousTask, onOpenDetail: noop, addToast: noop } as any,
{ task: nextTask, onOpenDetail: noop, addToast: noop } as any,
),
).toBe(false);
});
it("returns false when sourceAgentId changes", () => {
const previousTask = makeTask({ sourceType: "automation", sourceAgentId: "agent-a" });
const nextTask = makeTask({ sourceType: "automation", sourceAgentId: "agent-b" });
expect(
__test_areTaskCardPropsEqual(
{ task: previousTask, onOpenDetail: noop, addToast: noop } as any,
{ task: nextTask, onOpenDetail: noop, addToast: noop } as any,
),
).toBe(false);
});
});
describe("TaskCard mission badge", () => {
// Access the internal cache reset helper
let clearCache: () => void;