Clarify recovery false-positive controls

This commit is contained in:
Dotta committed 2026-05-10 00:59:20 -05:00
1 parent 8e16b8744b
commit f19f9766df
6 files changed
+21 -21

No files matched your search

+1 -1
View File
@@ -244,7 +244,7 @@ export const resolveIssueRecoveryActionSchema = z.object({
) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "This recovery outcome may only move the source issue to done or in_review",
message: "This recovery outcome requires sourceIssueStatus to be done or in_review",
path: ["sourceIssueStatus"],
});
}
+3 -3
View File
@@ -301,7 +301,7 @@ interface IssueChatThreadProps {
successfulRunHandoff?: SuccessfulRunHandoffState | null;
recoveryAction?: IssueRecoveryAction | null;
onResolveRecoveryAction?: (outcome: RecoveryResolveOutcome) => void;
canCancelRecoveryAction?: boolean;
canFalsePositiveRecoveryAction?: boolean;
legacyRecoverySourceIssue?: {
identifier: string | null;
href: string;
@@ -3621,7 +3621,7 @@ export function IssueChatThread({
successfulRunHandoff = null,
recoveryAction = null,
onResolveRecoveryAction,
canCancelRecoveryAction = false,
canFalsePositiveRecoveryAction = false,
legacyRecoverySourceIssue = null,
companyId,
projectId,
@@ -4263,7 +4263,7 @@ export function IssueChatThread({
action={recoveryAction}
agentMap={agentMap}
onResolve={onResolveRecoveryAction}
canCancelRecovery={canCancelRecoveryAction}
canFalsePositive={canFalsePositiveRecoveryAction}
/>
) : null}
{legacyRecoverySourceIssue ? (
@@ -182,7 +182,7 @@ describe("IssueRecoveryActionCard", () => {
it("does not offer blocked recovery resolution without a blocker selection flow", () => {
const node = render(
<IssueRecoveryActionCard action={buildAction()} onResolve={() => {}} canCancelRecovery />,
<IssueRecoveryActionCard action={buildAction()} onResolve={() => {}} canFalsePositive />,
);
click(node.querySelector("[data-testid='recovery-action-resolve-trigger']"));
@@ -193,7 +193,7 @@ describe("IssueRecoveryActionCard", () => {
expect(document.body.textContent).not.toContain("Mark blocked");
});
it("hides false-positive options unless canCancelRecovery is set", () => {
it("hides false-positive options unless canFalsePositive is set", () => {
const first = render(
<IssueRecoveryActionCard action={buildAction()} onResolve={() => {}} />,
);
@@ -207,7 +207,7 @@ describe("IssueRecoveryActionCard", () => {
const onResolve = vi.fn();
const second = render(
<IssueRecoveryActionCard action={buildAction()} onResolve={onResolve} canCancelRecovery />,
<IssueRecoveryActionCard action={buildAction()} onResolve={onResolve} canFalsePositive />,
);
click(second.querySelector("[data-testid='recovery-action-resolve-trigger']"));
expect(document.body.textContent).toContain("False positive, done");
@@ -37,8 +37,8 @@ export interface IssueRecoveryActionCardProps {
forcedState?: RecoveryCardCardState;
/** Optional click handler for resolve menu actions. If omitted, the buttons are not rendered. */
onResolve?: (outcome: RecoveryResolveOutcome) => void;
/** Whether the viewer can run destructive board-only actions (e.g. cancel recovery). */
canCancelRecovery?: boolean;
/** Whether the viewer can run destructive board-only actions (e.g. false-positive dismissal). */
canFalsePositive?: boolean;
className?: string;
}
@@ -323,7 +323,7 @@ export function IssueRecoveryActionCard({
agentMap,
forcedState,
onResolve,
canCancelRecovery = false,
canFalsePositive = false,
className,
}: IssueRecoveryActionCardProps) {
const cardState: RecoveryCardCardState = forcedState ?? deriveRecoveryCardState(action);
@@ -364,7 +364,7 @@ export function IssueRecoveryActionCard({
const showResolveActions = onResolve !== undefined && cardState !== "resolved";
const visibleResolveOptions = RESOLVE_OPTIONS.filter((option) => {
if (option.boardOnly && !canCancelRecovery) return false;
if (option.boardOnly && !canFalsePositive) return false;
return true;
});
+4 -4
View File
@@ -615,7 +615,7 @@ type IssueDetailChatTabProps = {
successfulRunHandoff: Issue["successfulRunHandoff"] | null;
recoveryAction: Issue["activeRecoveryAction"];
onResolveRecoveryAction?: (outcome: import("../components/IssueRecoveryActionCard").RecoveryResolveOutcome) => void;
canCancelRecoveryAction?: boolean;
canFalsePositiveRecoveryAction?: boolean;
legacyRecoverySourceIssue?: {
identifier: string | null;
href: string;
@@ -686,7 +686,7 @@ const IssueDetailChatTab = memo(function IssueDetailChatTab({
successfulRunHandoff,
recoveryAction,
onResolveRecoveryAction,
canCancelRecoveryAction,
canFalsePositiveRecoveryAction,
legacyRecoverySourceIssue,
comments,
locallyQueuedCommentRunIds,
@@ -896,7 +896,7 @@ const IssueDetailChatTab = memo(function IssueDetailChatTab({
successfulRunHandoff={successfulRunHandoff}
recoveryAction={recoveryAction ?? null}
onResolveRecoveryAction={onResolveRecoveryAction}
canCancelRecoveryAction={canCancelRecoveryAction}
canFalsePositiveRecoveryAction={canFalsePositiveRecoveryAction}
legacyRecoverySourceIssue={legacyRecoverySourceIssue ?? null}
companyId={companyId}
projectId={projectId}
@@ -3887,7 +3887,7 @@ export function IssueDetail() {
successfulRunHandoff={issue.successfulRunHandoff ?? null}
recoveryAction={issue.activeRecoveryAction ?? null}
onResolveRecoveryAction={handleResolveRecoveryAction}
canCancelRecoveryAction={canResolveBoardRecoveryAction}
canFalsePositiveRecoveryAction={canResolveBoardRecoveryAction}
legacyRecoverySourceIssue={legacyRecoverySourceIssue}
comments={threadComments}
locallyQueuedCommentRunIds={locallyQueuedCommentRunIds}
@@ -65,11 +65,11 @@ function buildAction(overrides: Partial<IssueRecoveryAction> = {}): IssueRecover
};
}
function CardPanel({ caption, action, forcedState, canCancelRecovery }: {
function CardPanel({ caption, action, forcedState, canFalsePositive }: {
caption: string;
action: IssueRecoveryAction;
forcedState?: React.ComponentProps<typeof IssueRecoveryActionCard>["forcedState"];
canCancelRecovery?: boolean;
canFalsePositive?: boolean;
}) {
return (
<section className="space-y-2">
@@ -81,7 +81,7 @@ function CardPanel({ caption, action, forcedState, canCancelRecovery }: {
agentMap={storybookAgentMap}
forcedState={forcedState}
onResolve={() => {}}
canCancelRecovery={canCancelRecovery}
canFalsePositive={canFalsePositive}
/>
</section>
);
@@ -90,12 +90,12 @@ function CardPanel({ caption, action, forcedState, canCancelRecovery }: {
function AllStatesPanel() {
return (
<div className="grid gap-5 lg:grid-cols-1">
<CardPanel caption="State 1 · Recovery needed (default)" action={buildAction()} canCancelRecovery />
<CardPanel caption="State 1 · Recovery needed (default)" action={buildAction()} canFalsePositive />
<CardPanel
caption="State 2 · Recovery in progress"
action={buildAction({ outcome: "delegated", attemptCount: 2 })}
forcedState="in_progress"
canCancelRecovery
canFalsePositive
/>
<CardPanel
caption="State 3 · Observing active run (watchdog)"
@@ -118,7 +118,7 @@ function AllStatesPanel() {
},
nextAction: "Board operator: assign an invokable owner or record a manual resolution.",
})}
canCancelRecovery
canFalsePositive
/>
<CardPanel
caption="State 5 · Recovery resolved"