Fix blocked attention no-continuation copy

This commit is contained in:
Dotta committed 2026-06-09 22:41:45 +00:00
1 parent 6de173d35c
commit b174c8cef3
5 files changed
+125 -12

No files matched your search

@@ -223,6 +223,43 @@ describe("IssueBlockedNotice", () => {
expect(node.textContent).toBe("");
});
it("renders live-next-step copy for blocked issues needing attention with no unresolved blockers", () => {
const node = render(
<IssueBlockedNotice
issueStatus="blocked"
blockers={[]}
blockerAttention={{
state: "needs_attention",
reason: "attention_required",
unresolvedBlockerCount: 0,
coveredBlockerCount: 0,
stalledBlockerCount: 0,
attentionBlockerCount: 0,
sampleBlockerIdentifier: null,
sampleStalledBlockerIdentifier: null,
}}
/>,
);
expect(node.textContent).toContain("This task is blocked but the system has no live next step.");
expect(node.textContent).toContain("Earlier blockers resolved, and no recovery path or scheduled retry is active.");
expect(node.textContent).not.toContain("Work on this task is blocked until it is moved back to todo.");
expect(node.querySelector('[data-blocker-attention-state="needs_attention_no_blockers"]')).not.toBeNull();
});
it("keeps the moved-back-to-todo fallback for manually parked blocked issues", () => {
const node = render(
<IssueBlockedNotice
issueStatus="blocked"
blockers={[]}
blockerAttention={null}
/>,
);
expect(node.textContent).toContain("Work on this task is blocked until it is moved back to todo.");
expect(node.textContent).not.toContain("This task is blocked but the system has no live next step.");
});
it("renders a recovery indicator on a blocker chip when the blocker has an active recovery action", () => {
const node = render(
<IssueBlockedNotice
+36 -10
View File
@@ -157,6 +157,8 @@ export function IssueBlockedNotice({
return collected;
})();
const showParkedRow = parkedBlockers.length > 0;
const showNeedsAttentionNoBlockers =
blockerAttention?.state === "needs_attention" && blockers.length === 0 && issueStatus === "blocked";
const stalledLeafIdentifier =
blockerAttention?.sampleStalledBlockerIdentifier ?? blockerAttention?.sampleBlockerIdentifier ?? null;
const stalledLeafBlockers = (() => {
@@ -199,7 +201,9 @@ export function IssueBlockedNotice({
return (
<div
data-blocker-attention-state={blockerAttention?.state}
data-blocker-attention-state={
showNeedsAttentionNoBlockers ? "needs_attention_no_blockers" : blockerAttention?.state
}
data-successful-run-handoff={showSuccessfulRunHandoff ? "required" : undefined}
className="mb-3 rounded-md border border-amber-300/70 bg-amber-50/90 px-3 py-2.5 text-sm text-amber-950 shadow-sm dark:border-amber-500/40 dark:bg-amber-500/10 dark:text-amber-100"
>
@@ -257,15 +261,37 @@ export function IssueBlockedNotice({
) : null}
{blockers.length > 0 || issueStatus === "blocked" ? (
<>
<p className="leading-5">
{blockers.length > 0
? isStalled
? stalledLeafBlockers.length > 1
? <>Work on this task is blocked by {blockerLabel}, but the chain is stalled in review without a clear next step. Resolve the stalled reviews below or remove them as blockers.</>
: <>Work on this task is blocked by {blockerLabel}, but the chain is stalled in review without a clear next step. Resolve the stalled review below or remove it as a blocker.</>
: <>Work on this task is blocked by {blockerLabel} until {blockers.length === 1 ? "it is" : "they are"} complete. Comments still wake the assignee for questions or triage.</>
: <>Work on this task is blocked until it is moved back to todo. Comments still wake the assignee for questions or triage.</>}
</p>
{showNeedsAttentionNoBlockers ? (
<>
<p className="font-medium leading-5">
This task is blocked but the system has no live next step.
</p>
<p className="leading-5">
Earlier blockers resolved, and no recovery path or scheduled retry is active. The assignee
needs to either move it back to{" "}
<code className="rounded bg-amber-100 px-1 py-0.5 text-[12px] dark:bg-amber-400/15">
todo
</code>{" "}
so a fresh run is queued, or add a real blocker if work genuinely can't proceed.
</p>
{blockerAttention.coveredBlockerCount > 0 ? (
<p className="text-xs leading-5 text-amber-800 dark:text-amber-200">
Some ancestor blockers are still covered by active work — see the "Ultimately waiting on" row
below.
</p>
) : null}
</>
) : (
<p className="leading-5">
{blockers.length > 0
? isStalled
? stalledLeafBlockers.length > 1
? <>Work on this task is blocked by {blockerLabel}, but the chain is stalled in review without a clear next step. Resolve the stalled reviews below or remove them as blockers.</>
: <>Work on this task is blocked by {blockerLabel}, but the chain is stalled in review without a clear next step. Resolve the stalled review below or remove it as a blocker.</>
: <>Work on this task is blocked by {blockerLabel} until {blockers.length === 1 ? "it is" : "they are"} complete. Comments still wake the assignee for questions or triage.</>
: <>Work on this task is blocked until it is moved back to todo. Comments still wake the assignee for questions or triage.</>}
</p>
)}
{blockers.length > 0 ? (
<div className="flex flex-wrap gap-1.5">
{blockers.map(renderBlockerChip)}
+22
View File
@@ -77,6 +77,28 @@ describe("StatusIcon", () => {
expect(html).not.toContain("border-dashed");
});
it("labels attention-required blocked issues with no unresolved blockers as needing assignee action", () => {
const html = renderToStaticMarkup(
<StatusIcon
status="blocked"
blockerAttention={{
state: "needs_attention",
reason: "attention_required",
unresolvedBlockerCount: 0,
coveredBlockerCount: 0,
stalledBlockerCount: 0,
attentionBlockerCount: 0,
sampleBlockerIdentifier: null,
sampleStalledBlockerIdentifier: null,
}}
/>,
);
expect(html).toContain('data-blocker-attention-state="needs_attention"');
expect(html).toContain('aria-label="Blocked · no live continuation path — assignee needs to act"');
expect(html).toContain("border-red-600");
});
it("shows active covered work on mixed attention-required blockers", () => {
const html = renderToStaticMarkup(
<StatusIcon
+3
View File
@@ -49,6 +49,9 @@ function blockedAttentionLabel(blockerAttention: IssueBlockerAttention | null |
}
if (blockerAttention.reason === "attention_required") {
if (blockerAttention.unresolvedBlockerCount === 0) {
return "Blocked · no live continuation path — assignee needs to act";
}
const count = blockerAttention.attentionBlockerCount || blockerAttention.unresolvedBlockerCount;
const attentionCopy = `${count} ${count === 1 ? "blocker needs" : "blockers need"} attention`;
const coveredCount = blockerAttention.coveredBlockerCount;
@@ -175,6 +175,18 @@ const coveredBlockedMatrix: CoveredBlockedCell[] = [
expectedVisual: "solid red ring",
expectedCopy: "Blocked · 1 unresolved blocker needs attention",
},
{
label: "Needs attention (no unresolved blockers)",
status: "blocked",
blockerAttention: attention({
state: "needs_attention",
reason: "attention_required",
unresolvedBlockerCount: 0,
attentionBlockerCount: 0,
}),
expectedVisual: "solid red ring",
expectedCopy: "Blocked · no live continuation path — assignee needs to act",
},
{
label: "Non-blocked with prop ignored",
status: "in_progress",
@@ -220,7 +232,8 @@ function summaryBlocker(
type BlockedNoticeStateLabel =
| "Default covered"
| "Stalled (single leaf)"
| "Stalled (multiple leaves)";
| "Stalled (multiple leaves)"
| "Needs attention (no blockers)";
type BlockedNoticeFixture = {
label: BlockedNoticeStateLabel;
@@ -318,6 +331,18 @@ const blockedNoticeFixtures: BlockedNoticeFixture[] = [
sampleStalledBlockerIdentifier: "PAP-2284",
}),
},
{
label: "Needs attention (no blockers)",
caption: "Earlier blockers resolved, but no live run, recovery path, or scheduled retry remains.",
blockers: [],
blockerAttention: attention({
state: "needs_attention",
reason: "attention_required",
unresolvedBlockerCount: 0,
coveredBlockerCount: 0,
attentionBlockerCount: 0,
}),
},
];
function BlockedNoticeSurface({
@@ -340,7 +365,7 @@ function BlockedNoticeSurface({
{size} · {mode}
</span>
</div>
<div className={isMobile ? "max-w-[358px] px-3 py-3" : "min-w-[620px] px-4 py-3"}>
<div className={isMobile ? "max-w-[358px] px-3 py-3" : "max-w-[620px] px-4 py-3"}>
<IssueBlockedNotice
issueStatus="blocked"
blockers={fixture.blockers}