diff --git a/packages/views/issues/components/attribution-badge.test.tsx b/packages/views/issues/components/attribution-badge.test.tsx index 75dcd538f1..4221592b57 100644 --- a/packages/views/issues/components/attribution-badge.test.tsx +++ b/packages/views/issues/components/attribution-badge.test.tsx @@ -35,7 +35,7 @@ describe("AttributionBadge", () => { expect(screen.getByTitle("Direct member action")).toBeInTheDocument(); }); - it("marks degraded (non-precise) attribution with a warning tone", () => { + it("marks a fallback guess (owner_fallback) with a warning tone", () => { const attribution: TaskAttribution = { source: "owner_fallback", precise: false, @@ -51,6 +51,40 @@ describe("AttributionBadge", () => { .toBeInTheDocument(); }); + it("shows a backfilled attribution in the normal tone, not a warning (MUL-4768)", () => { + // Backfill is non-precise for the coverage metric, but it names a human the + // same waterfall resolved — just after the fact — so it must read like any + // other confident attribution rather than a yellow warning. + const attribution: TaskAttribution = { + source: "backfill", + precise: false, + initiator: { id: "u5", name: "Bohan" }, + }; + const { container } = renderWithI18n( + , + ); + + expect(screen.getByText("on behalf of Bohan")).toBeInTheDocument(); + // No warning tone — the confusing yellow that flagged backfilled runs is gone. + expect(container.querySelector(".text-warning")).toBeNull(); + expect(container.querySelector(".text-muted-foreground")).not.toBeNull(); + // The "backfilled" nuance still lives in the tooltip for anyone who looks. + expect(screen.getByTitle("Backfilled attribution")).toBeInTheDocument(); + }); + + it("avatar variant shows no warning ring for a backfilled attribution (MUL-4768)", () => { + const attribution: TaskAttribution = { + source: "backfill", + precise: false, + initiator: { id: "u5", name: "Bohan" }, + }; + const { container } = renderWithI18n( + , + ); + + expect(container.querySelector(".ring-warning\\/60")).toBeNull(); + }); + it("falls back to a generic name when the initiator has no display name", () => { const attribution: TaskAttribution = { source: "delegation", diff --git a/packages/views/issues/components/attribution-badge.tsx b/packages/views/issues/components/attribution-badge.tsx index 91ab2a4b74..d84f18d6da 100644 --- a/packages/views/issues/components/attribution-badge.tsx +++ b/packages/views/issues/components/attribution-badge.tsx @@ -23,8 +23,11 @@ function initialsOf(name: string): string { /** * AttributionBadge renders who an agent run is accountable to (MUL-4302 §9): - * the "on behalf of " provenance, with the resolution source and a - * distinct warning tone for degraded (non-precise) attribution. + * the "on behalf of " provenance, with the resolution source in a + * tooltip and a cautionary tone ONLY when the named human is a fallback guess + * (owner_fallback) rather than a confidently resolved one. A backfilled + * attribution is confident (resolved after the fact by the same waterfall), so + * it reads like any other attribution instead of a warning (MUL-4768). * * Two shapes, both silent when no responsible member resolved (MUL-4765): * - `variant="badge"` (default): the full "on behalf of " chip. Renders @@ -82,9 +85,20 @@ export function AttributionBadge({ sourceLabel = attribution.source; } - // Degraded attribution (owner_fallback / backfill / unattributed) is marked - // distinctly so it never reads as a compliance-grade "who is responsible". - const degraded = attribution.precise === false; + // The backend's `precise` flag is an attribution-*coverage* health bit: + // owner_fallback, backfill, and unattributed all fail it. But coverage is an + // ops metric, not a reader-facing signal. The only thing a viewer of "on + // behalf of " cares about is whether that named human might NOT actually + // be who's responsible — true only for a fallback guess (owner_fallback: + // nothing resolved, so we defaulted to the agent owner). A backfilled + // attribution names a human the same waterfall resolved, just after the fact, + // so it is confident and must read like any other attribution, never as a + // warning (MUL-4768). The cautionary tone therefore fires for any non-precise + // source EXCEPT backfill; keeping the `precise === false` base means a future + // unknown degraded source still warns (fail-safe) instead of silently reading + // as confident. + const uncertain = + attribution.precise === false && attribution.source !== "backfill"; const initiator = attribution.initiator; // Avatar-only shape: just the accountable member's face, with the name + @@ -99,9 +113,9 @@ export function AttributionBadge({ @@ -122,7 +136,7 @@ export function AttributionBadge({ {sourceLabel} @@ -145,7 +159,7 @@ export function AttributionBadge({ variant="outline" className={cn( "max-w-40 min-w-0 gap-1 font-normal", - degraded ? "text-warning" : "text-muted-foreground", + uncertain ? "text-warning" : "text-muted-foreground", className )} title={sourceLabel}