mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-25 09:41:04 +01:00
Fix blocked attention no-continuation copy
This commit is contained in:
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
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in new issue
Block a user