mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-27 11:20:42 +01:00
fix(ui): label the deliberate-wait recovery kind from the recovery classification change
the recovery classification change added `deliberate_wait_without_target` to `ISSUE_RECOVERY_ACTION_KINDS` without updating its UI consumer. The two `Record<IssueRecoveryActionKind, string>` maps in the recovery card are exhaustive, so the two branches typechecked in isolation but broke the moment they met: IssueRecoveryActionCard.tsx(117,7): error TS2741: Property 'deliberate_wait_without_target' is missing ... Give the kind first-class copy instead of letting it fall through to the generic `missing_disposition` headline. Per the spec this kind means a run stopped to wait on something that does not exist, and the original owner gets bounded disposition-repair attempts — so while one of those attempts is live or queued the display derivation already resolves it to the recessed `in_progress` state, which is exactly this task's thesis. No derivation change was needed; the liveness logic is kind-agnostic. Prevention: the break was invisible to both branches' own test runs, so add a guard that renders every kind in the shared union and asserts each gets distinct, non-empty copy. Verified by mutation — pointing the new kind at the generic headline fails the guard. Verified: tsc clean, 218 UI tests across 9 affected suites, the recovery back end's disposition-repair server suite, and all three token gates clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
843c218e25
commit
a88bde1def
3 files changed
+47
No files matched your search
@@ -4,6 +4,7 @@ import { createRoot } from "react-dom/client";
|
||||
import { flushSync } from "react-dom";
|
||||
import type { AnchorHTMLAttributes, ReactElement } from "react";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { ISSUE_RECOVERY_ACTION_KINDS } from "@paperclipai/shared";
|
||||
import type { Agent, IssueRecoveryAction } from "@paperclipai/shared";
|
||||
import { IssueRecoveryActionCard, deriveRecoveryCardState } from "./IssueRecoveryActionCard";
|
||||
|
||||
@@ -168,6 +169,30 @@ describe("IssueRecoveryActionCard", () => {
|
||||
);
|
||||
});
|
||||
|
||||
// PAP-17058: PAP-17057 added a recovery action kind without a UI label, which
|
||||
// only surfaced as a tsc failure once both branches met. Assert every kind in
|
||||
// the shared union renders its own copy so a new kind cannot land unlabelled
|
||||
// or silently share another kind's headline.
|
||||
it("gives every shared recovery action kind a distinct label and headline", () => {
|
||||
const headlines = new Map<string, string>();
|
||||
for (const kind of ISSUE_RECOVERY_ACTION_KINDS) {
|
||||
const node = render(<IssueRecoveryActionCard action={buildAction({ kind })} />);
|
||||
const text = node.textContent ?? "";
|
||||
expect(text.trim(), `${kind} rendered no copy`).not.toBe("");
|
||||
headlines.set(kind, text);
|
||||
act(() => root?.unmount());
|
||||
container?.remove();
|
||||
root = null;
|
||||
container = null;
|
||||
}
|
||||
expect(headlines.size).toBe(ISSUE_RECOVERY_ACTION_KINDS.length);
|
||||
const deliberateWait = headlines.get("deliberate_wait_without_target") ?? "";
|
||||
expect(deliberateWait).toContain("Waiting On Nothing");
|
||||
expect(deliberateWait).toContain("no reviewer, blocker, or open subtask to wait on");
|
||||
// The generic missing_disposition copy must not be what a named kind shows.
|
||||
expect(deliberateWait).not.toContain("no next step was chosen");
|
||||
});
|
||||
|
||||
it("explains issue_graph_liveness in plain language", () => {
|
||||
const node = render(
|
||||
<IssueRecoveryActionCard
|
||||
|
||||
@@ -116,6 +116,7 @@ export interface IssueRecoveryActionCardProps {
|
||||
|
||||
const KIND_LABEL: Record<IssueRecoveryActionKind, string> = {
|
||||
missing_disposition: "Missing Disposition",
|
||||
deliberate_wait_without_target: "Waiting On Nothing",
|
||||
stranded_assigned_issue: "Stranded Task",
|
||||
workspace_validation: "Workspace Validation",
|
||||
configuration_validation: "Configuration Validation",
|
||||
@@ -126,6 +127,8 @@ const KIND_LABEL: Record<IssueRecoveryActionKind, string> = {
|
||||
const KIND_HEADLINE: Record<IssueRecoveryActionKind, string> = {
|
||||
missing_disposition:
|
||||
"This task's run finished, but no next step was chosen. Choose what happens next — try the task again, mark it done, or send it for review.",
|
||||
deliberate_wait_without_target:
|
||||
"This task's run stopped to wait for something, but there is no reviewer, blocker, or open subtask to wait on. Paperclip is asking the original owner to pick a real next step.",
|
||||
stranded_assigned_issue:
|
||||
"Paperclip retried this task's last run, but there is still no queued run, reviewer, blocker, or other next owner. To get it moving, choose what happens next — try the task again, mark it done, or send it for review.",
|
||||
workspace_validation:
|
||||
|
||||
@@ -103,6 +103,25 @@ describe("deriveRecoveryDisplayState", () => {
|
||||
expect(deriveRecoveryDisplayState(base, {})).toBe("needed");
|
||||
});
|
||||
|
||||
// PAP-17057 added this kind: a run stopped to wait on nothing, so the original
|
||||
// owner gets bounded disposition-repair attempts. While one of those attempts
|
||||
// is live or queued the state is recessed, not a prominent call to action.
|
||||
it("reads a bounded repair attempt on deliberate_wait_without_target as in progress", () => {
|
||||
const action = { ...base, kind: "deliberate_wait_without_target" as const };
|
||||
expect(
|
||||
deriveRecoveryDisplayState(action, {
|
||||
ownerRun: { agentId: CTO_ID, status: "running" },
|
||||
}),
|
||||
).toBe("in_progress");
|
||||
expect(deriveRecoveryDisplayState(action, { hasQueuedWake: true })).toBe("in_progress");
|
||||
});
|
||||
|
||||
it("keeps deliberate_wait_without_target needed once repair attempts stop", () => {
|
||||
expect(
|
||||
deriveRecoveryDisplayState({ ...base, kind: "deliberate_wait_without_target" }, {}),
|
||||
).toBe("needed");
|
||||
});
|
||||
|
||||
it("prefers the server-projected display state when present", () => {
|
||||
expect(
|
||||
deriveRecoveryDisplayState({ ...base, displayState: "observe_only" } as never, {
|
||||
|
||||
Reference in new issue
Block a user