Files
PaperClipAI/ui/src/components/IssueChatThreadSystemNotice.test.tsx
T
b4a7a12985 feat: make recovery updates quieter (#10542)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The recovery subsystem restores work after an agent run stops or
loses state
> - Recovery notices currently use the same visual weight as normal work
comments
> - Recovery agents can also post long narratives that obscure the
useful hand-off
> - The server must identify recovery output because agents cannot set
presentation controls
> - This pull request adds compact recovery notices, structured action
references, and brief recovery prompts
> - The benefit is a quieter issue thread that still keeps recovery
state inspectable

## Linked Issues or Issue Description

**Subsystem affected**

Cross-cutting: `server/`, `packages/shared`, and
`packages/adapter-utils`.

**Problem or motivation**

Recovery notices and recovery-run comments can dominate an issue thread.
Operators must scan routine recovery narration before they find the work
hand-off.

**Proposed solution**

Give routine recovery output a compact system-notice presentation.
Derive the presentation on the server so agents cannot hide arbitrary
comments. Keep the successful missing-state summary fully visible
because that comment is the recovery deliverable.

**Alternatives considered**

The UI could detect recovery text. That approach is fragile and does not
provide structured action references. Agents could also set presentation
directly, but that would weaken the current board-only security
boundary.

**Roadmap alignment**

This change refines the completed “Self-healing runs & automatic
recovery” and “Enforced Outcomes” roadmap areas. It does not add a
competing roadmap capability.

**Additional context**

The scope covers shared comment validation, server recovery notices,
agent-comment derivation, and recovery prompt text. No database
migration is needed because presentation data already uses JSON.

## What Changed

- Add the `compact` issue-comment presentation density to shared
constants, types, and validation.
- Give recovery escalation, waiting, and in-place notices compact titles
and structured recovery-action metadata.
- Use recovery-action metadata for notice deduplication, with the legacy
text marker as a compatibility fallback.
- Derive compact presentation for comments from recovery-scoped runs
while preserving the board-only presentation boundary.
- Keep successful missing-state recovery summaries fully visible.
- Ask recovery participants to record outcomes in `resolutionNote` and
keep source-issue comments brief.
- Add shared, route, service, and prompt tests for the new behavior and
exceptions.

## Verification

- `pnpm -r typecheck`
- Focused Vitest coverage: 320 tests passed across shared validators,
adapter prompts, issue comments, recovery actions, and heartbeat
recovery.
- Full server phase: 292 files passed, 3,094 tests passed, and 2 tests
skipped.
- Full UI phase: 386 files passed and 3,182 tests passed.
- `pnpm build`
- Known master baseline: `cli/src/__tests__/secrets.test.ts` expects
`pass`, but the current implementation returns `warn` when strict secret
mode is disabled for Postgres. This branch does not change CLI secrets
code.

## Risks

- Low migration risk. The presentation column is JSON and needs no
database migration.
- Recovery-run detection depends on the persisted run context snapshot.
- Structured metadata becomes the primary deduplication key. The
existing body marker remains as a fallback for older comments.

> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.

## Model Used

- OpenAI Codex with `gpt-5.6-sol`. The runtime did not expose the
context-window size. The model used agentic reasoning, repository tools,
code execution, and test execution.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-31 09:55:01 -07:00

656 lines
23 KiB
TypeScript

// @vitest-environment jsdom
import { act } from "react";
import type { ReactNode } from "react";
import { createRoot } from "react-dom/client";
import { MemoryRouter } from "react-router-dom";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { IssueChatThread } from "./IssueChatThread";
import type { IssueChatComment } from "../lib/issue-chat-messages";
import type { Agent, SuccessfulRunHandoffState } from "@paperclipai/shared";
vi.mock("@assistant-ui/react", () => ({
AssistantRuntimeProvider: ({ children }: { children: ReactNode }) => <div>{children}</div>,
useAui: () => ({ thread: () => ({ append: async () => undefined }) }),
}));
vi.mock("./transcript/useLiveRunTranscripts", () => ({
useLiveRunTranscripts: () => ({
transcriptByRun: new Map(),
hasOutputForRun: () => false,
}),
}));
vi.mock("./MarkdownBody", () => ({
MarkdownBody: ({ children }: { children: ReactNode }) => <div>{children}</div>,
}));
vi.mock("./MarkdownEditor", () => ({
MarkdownEditor: () => <textarea aria-label="Issue chat editor" />,
}));
vi.mock("./InlineEntitySelector", () => ({ InlineEntitySelector: () => null }));
vi.mock("./Identity", () => ({ Identity: ({ name }: { name: string }) => <span>{name}</span> }));
vi.mock("./OutputFeedbackButtons", () => ({ OutputFeedbackButtons: () => null }));
vi.mock("@/components/ui/tooltip", () => ({
Tooltip: ({ children }: { children: ReactNode }) => <>{children}</>,
TooltipContent: ({ children }: { children: ReactNode }) => <div>{children}</div>,
TooltipTrigger: ({ children }: { children: ReactNode }) => <>{children}</>,
}));
vi.mock("./AgentIconPicker", () => ({ AgentIcon: () => null }));
vi.mock("./StatusBadge", () => ({ StatusBadge: ({ status }: { status: string }) => <span>{status}</span> }));
vi.mock("./IssueLinkQuicklook", () => ({
IssueLinkQuicklook: ({
children,
to,
}: {
children: ReactNode;
to: string;
}) => <a href={to}>{children}</a>,
}));
vi.mock("../hooks/usePaperclipIssueRuntime", () => ({
usePaperclipIssueRuntime: () => ({}),
}));
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true;
let container: HTMLDivElement;
let root: ReturnType<typeof createRoot>;
beforeEach(() => {
container = document.createElement("div");
document.body.appendChild(container);
window.scrollTo = vi.fn();
root = createRoot(container);
});
afterEach(() => {
act(() => root?.unmount());
container.remove();
});
function renderThread(
comments: IssueChatComment[],
options: {
agentMap?: Map<string, Agent>;
issueStatus?: string;
successfulRunHandoff?: SuccessfulRunHandoffState | null;
} = {},
) {
act(() => {
root.render(
<MemoryRouter>
<IssueChatThread
comments={comments}
linkedRuns={[]}
timelineEvents={[]}
liveRuns={[]}
onAdd={async () => {}}
showComposer={false}
enableLiveTranscriptPolling={false}
agentMap={options.agentMap}
issueStatus={options.issueStatus}
successfulRunHandoff={options.successfulRunHandoff}
/>
</MemoryRouter>,
);
});
}
const baseTimestamps = {
createdAt: new Date("2026-05-04T16:32:00.000Z"),
updatedAt: new Date("2026-05-04T16:32:00.000Z"),
};
describe("IssueChatThread system notice routing", () => {
it("renders authorType=system comments as a SystemNotice rather than a user bubble", () => {
const comment: IssueChatComment = {
id: "comment-system",
companyId: "company-1",
issueId: "issue-1",
authorType: "system",
authorAgentId: null,
authorUserId: null,
body: "Paperclip needs a disposition before this issue can continue.",
presentation: {
kind: "system_notice",
tone: "warning",
title: "Missing issue disposition",
detailsDefaultOpen: false,
},
metadata: {
version: 1,
sections: [
{
title: "Required action",
rows: [
{ type: "issue_link", label: "Source issue", issueId: "i1", identifier: "PAP-3440", title: "Recovery" },
{ type: "key_value", label: "Status before", value: "in_progress" },
],
},
],
},
...baseTimestamps,
};
renderThread([comment]);
const row = container.querySelector('[data-message-role="system"]');
expect(row).not.toBeNull();
const status = row?.querySelector('[role="status"]');
expect(status?.getAttribute("aria-label")).toBe("Missing issue disposition");
expect(container.textContent).toContain("Paperclip needs a disposition");
// collapsed by default — metadata identifier should not be visible
expect(container.textContent).not.toContain("PAP-3440");
const toggle = row?.querySelector("button[aria-expanded]") as HTMLButtonElement | null;
expect(toggle?.getAttribute("aria-expanded")).toBe("false");
expect(container.querySelectorAll('[data-message-role="user"]').length).toBe(0);
});
it("expands metadata when detailsDefaultOpen is true", () => {
const comment: IssueChatComment = {
id: "comment-system-open",
companyId: "company-1",
issueId: "issue-1",
authorType: "system",
authorAgentId: null,
authorUserId: null,
body: "Recovery escalated.",
presentation: {
kind: "system_notice",
tone: "danger",
title: null,
detailsDefaultOpen: true,
},
metadata: {
version: 1,
sections: [
{
rows: [
{ type: "agent_link", label: "Owner", agentId: "agent-cto", name: "CTO" },
],
},
],
},
...baseTimestamps,
};
renderThread([comment]);
const status = container.querySelector('[role="status"]');
expect(status?.getAttribute("aria-label")).toBe("System alert");
expect(container.textContent).toContain("CTO");
const toggle = container.querySelector("button[aria-expanded]");
expect(toggle?.getAttribute("aria-expanded")).toBe("true");
});
it("falls back to legacy user bubble + handoff callout for old text-only comments", () => {
const comment: IssueChatComment = {
id: "comment-legacy",
companyId: "company-1",
issueId: "issue-1",
authorType: "user",
authorAgentId: null,
authorUserId: "user-1",
body: "## Successful run missing issue disposition\n\nFix this.",
presentation: null,
metadata: null,
...baseTimestamps,
};
renderThread([comment]);
expect(container.querySelector('[role="status"]')).toBeNull();
const userRow = container.querySelector('[data-message-role="user"]');
expect(userRow).not.toBeNull();
expect(container.textContent).toContain("Successful run missing issue disposition");
});
it("keeps regular user comments rendering as user bubbles", () => {
const comment: IssueChatComment = {
id: "comment-user",
companyId: "company-1",
issueId: "issue-1",
authorType: "user",
authorAgentId: null,
authorUserId: "user-1",
body: "Standard user message.",
presentation: null,
metadata: null,
...baseTimestamps,
};
renderThread([comment]);
expect(container.querySelector('[role="status"]')).toBeNull();
expect(container.querySelector('[data-message-role="user"]')).not.toBeNull();
expect(container.textContent).toContain("Standard user message.");
});
it("keeps agent-authored comments rendering as assistant bubbles even with system_notice presentation absent", () => {
const comment: IssueChatComment = {
id: "comment-agent",
companyId: "company-1",
issueId: "issue-1",
authorType: "agent",
authorAgentId: "agent-1",
authorUserId: null,
body: "Agent reply",
presentation: null,
metadata: null,
...baseTimestamps,
};
renderThread([comment]);
expect(container.querySelector('[role="status"]')).toBeNull();
expect(container.querySelector('[data-message-role="assistant"]')).not.toBeNull();
});
it("labels system notice source as the originating run agent name when runAgentId is available", () => {
const codexAgent = {
id: "agent-codex",
name: "CodexCoder",
} as unknown as Agent;
const agentMap = new Map<string, Agent>([[codexAgent.id, codexAgent]]);
const comment: IssueChatComment = {
id: "comment-system-runagent",
companyId: "company-1",
issueId: "issue-1",
authorType: "system",
authorAgentId: null,
authorUserId: null,
runId: "run-issue-chat-01",
runAgentId: "agent-codex",
body: "Paperclip needs a disposition before this issue can continue.",
presentation: {
kind: "system_notice",
tone: "warning",
title: "Missing issue disposition",
detailsDefaultOpen: false,
},
metadata: null,
...baseTimestamps,
};
renderThread([comment], { agentMap });
const status = container.querySelector('[role="status"]');
expect(status).not.toBeNull();
const sourceLink = status?.querySelector('a[href^="/agents/"]') as HTMLAnchorElement | null;
expect(sourceLink?.getAttribute("href")).toBe("/agents/agent-codex/runs/run-issue-chat-01");
expect(sourceLink?.textContent).toBe("CodexCoder");
expect(sourceLink?.textContent).not.toBe("You");
});
it("shows copy-link feedback on the link button only", async () => {
const writeText = vi.fn(async () => undefined);
const originalSecureContext = Object.getOwnPropertyDescriptor(window, "isSecureContext");
Object.defineProperty(window, "isSecureContext", {
configurable: true,
value: true,
});
Object.defineProperty(navigator, "clipboard", {
configurable: true,
value: { writeText },
});
const comment: IssueChatComment = {
id: "comment-copy-link",
companyId: "company-1",
issueId: "issue-1",
authorType: "system",
authorAgentId: null,
authorUserId: null,
body: "System recovery completed.",
presentation: {
kind: "system_notice",
tone: "success",
title: null,
detailsDefaultOpen: false,
},
metadata: null,
...baseTimestamps,
};
renderThread([comment]);
const copyLink = container.querySelector('button[aria-label="Copy link to system notice"]') as HTMLButtonElement;
const copyText = container.querySelector('button[aria-label="Copy system notice"]') as HTMLButtonElement;
await act(async () => {
copyLink.click();
await Promise.resolve();
});
expect(writeText).toHaveBeenCalledWith(expect.stringContaining("#comment-comment-copy-link"));
expect(copyLink.querySelector(".lucide-check")).not.toBeNull();
expect(copyText.querySelector(".lucide-check")).toBeNull();
if (originalSecureContext) {
Object.defineProperty(window, "isSecureContext", originalSecureContext);
} else {
// @ts-expect-error test cleanup for optional browser API
delete window.isSecureContext;
}
});
it("labels system notice source as Paperclip when no run agent can be resolved", () => {
const comment: IssueChatComment = {
id: "comment-system-no-author",
companyId: "company-1",
issueId: "issue-1",
authorType: "system",
authorAgentId: null,
authorUserId: null,
runId: null,
runAgentId: null,
body: "System recovery completed.",
presentation: {
kind: "system_notice",
tone: "info",
title: null,
detailsDefaultOpen: false,
},
metadata: null,
...baseTimestamps,
};
renderThread([comment]);
const status = container.querySelector('[role="status"]');
expect(status).not.toBeNull();
expect(status?.textContent).toContain("Paperclip");
expect(status?.textContent).not.toContain("You");
});
it("falls back to Paperclip in the system notice header when run agent is unknown to agentMap", () => {
const comment: IssueChatComment = {
id: "comment-system-unknown-agent",
companyId: "company-1",
issueId: "issue-1",
authorType: "system",
authorAgentId: null,
authorUserId: null,
runId: "run-xyz",
runAgentId: "agent-unknown",
body: "Disposition required.",
presentation: {
kind: "system_notice",
tone: "warning",
title: null,
detailsDefaultOpen: false,
},
metadata: null,
...baseTimestamps,
};
renderThread([comment]);
const status = container.querySelector('[role="status"]');
const sourceLink = status?.querySelector('a[href^="/agents/"]') as HTMLAnchorElement | null;
expect(sourceLink?.getAttribute("href")).toBe("/agents/agent-unknown/runs/run-xyz");
expect(sourceLink?.textContent).toBe("Paperclip");
});
it("routes agent-authored comments to the notice renderer when presentation requests system_notice", () => {
const owner = { id: "agent-1", name: "ClaudeCoder" } as unknown as Agent;
const agentMap = new Map<string, Agent>([[owner.id, owner]]);
const comment: IssueChatComment = {
id: "comment-agent-system",
companyId: "company-1",
issueId: "issue-1",
authorType: "agent",
authorAgentId: "agent-1",
authorUserId: null,
runId: "run-owner",
runAgentId: "agent-1",
body: "Reassigned to ClaudeFixer.",
presentation: {
kind: "system_notice",
tone: "neutral",
title: "Recovery reassignment",
detailsDefaultOpen: false,
},
metadata: null,
...baseTimestamps,
};
renderThread([comment], { agentMap });
// No longer an assistant bubble — it collapses like a system notice.
expect(container.querySelector('[data-message-role="assistant"]')).toBeNull();
const row = container.querySelector('[data-message-role="system"]');
expect(row).not.toBeNull();
const status = row?.querySelector('[role="status"]');
expect(status?.getAttribute("aria-label")).toBe("Recovery reassignment");
// The real author's name + run link stay visible on the notice.
const sourceLink = status?.querySelector('a[href^="/agents/"]') as HTMLAnchorElement | null;
expect(sourceLink?.getAttribute("href")).toBe("/agents/agent-1/runs/run-owner");
expect(sourceLink?.textContent).toBe("ClaudeCoder");
});
it("collapses compact system notices to a single quiet row and expands to the full card", () => {
const comment: IssueChatComment = {
id: "comment-compact",
companyId: "company-1",
issueId: "issue-1",
authorType: "system",
authorAgentId: null,
authorUserId: null,
runId: "run-recovery",
runAgentId: "agent-codex",
body: "Recovery escalated to the CTO after repeated stalls.",
presentation: {
kind: "system_notice",
tone: "warning",
title: "Recovery escalated",
detailsDefaultOpen: false,
density: "compact",
},
metadata: {
version: 1,
sections: [
{
title: "Escalation",
rows: [{ type: "agent_link", label: "Owner", agentId: "agent-cto", name: "CTO" }],
},
],
},
...baseTimestamps,
};
renderThread([comment]);
const row = container.querySelector('[data-testid="compact-system-notice"]');
expect(row).not.toBeNull();
// Collapsed: title + timestamp + chevron visible, but body/card hidden.
expect(row?.textContent).toContain("Recovery escalated");
expect(row?.querySelector('[data-testid="compact-system-notice-time"]')).not.toBeNull();
expect(row?.querySelector(".lucide-chevron-down")).not.toBeNull();
const toggle = row?.querySelector("button[aria-expanded]") as HTMLButtonElement;
expect(toggle.getAttribute("aria-expanded")).toBe("false");
const detailsId = toggle.getAttribute("aria-controls");
const details = detailsId ? container.ownerDocument.getElementById(detailsId) : null;
expect(details).not.toBeNull();
expect(details).toHaveProperty("hidden", true);
// The full SystemNotice card lives inside the collapsed region.
expect(details?.querySelector('[role="status"]')).not.toBeNull();
act(() => {
toggle.click();
});
expect(toggle.getAttribute("aria-expanded")).toBe("true");
expect(details).toHaveProperty("hidden", false);
expect(details?.textContent).toContain("Recovery escalated to the CTO");
});
it("renders a compact notice already expanded when detailsDefaultOpen is true", () => {
const comment: IssueChatComment = {
id: "comment-compact-open",
companyId: "company-1",
issueId: "issue-1",
authorType: "system",
authorAgentId: null,
authorUserId: null,
body: "Recovery escalated to the CTO.",
presentation: {
kind: "system_notice",
tone: "danger",
title: "Recovery escalated",
detailsDefaultOpen: true,
density: "compact",
},
metadata: null,
...baseTimestamps,
};
renderThread([comment]);
const row = container.querySelector('[data-testid="compact-system-notice"]');
expect(row).not.toBeNull();
const toggle = row?.querySelector("button[aria-expanded]") as HTMLButtonElement;
expect(toggle.getAttribute("aria-expanded")).toBe("true");
const detailsId = toggle.getAttribute("aria-controls");
const details = detailsId ? container.ownerDocument.getElementById(detailsId) : null;
expect(details).toHaveProperty("hidden", false);
expect(details?.querySelector('[role="status"]')).not.toBeNull();
expect(details?.textContent).toContain("Recovery escalated to the CTO.");
});
it("keeps the author visible on a compact agent-authored notice row", () => {
const owner = { id: "agent-1", name: "ClaudeCoder" } as unknown as Agent;
const agentMap = new Map<string, Agent>([[owner.id, owner]]);
const comment: IssueChatComment = {
id: "comment-compact-agent",
companyId: "company-1",
issueId: "issue-1",
authorType: "agent",
authorAgentId: "agent-1",
authorUserId: null,
body: "Picked this back up after the wake.",
presentation: {
kind: "system_notice",
tone: "neutral",
title: "Recovery owner update",
detailsDefaultOpen: false,
density: "compact",
},
metadata: null,
...baseTimestamps,
};
renderThread([comment], { agentMap });
const row = container.querySelector('[data-testid="compact-system-notice"]');
expect(row).not.toBeNull();
expect(row?.textContent).toContain("Recovery owner update");
// Author name shows on the collapsed row itself.
expect(row?.querySelector("button[aria-expanded]")?.textContent).toContain("ClaudeCoder");
});
it("keeps notices without density rendering as the full card (graceful fallback)", () => {
const comment: IssueChatComment = {
id: "comment-no-density",
companyId: "company-1",
issueId: "issue-1",
authorType: "system",
authorAgentId: null,
authorUserId: null,
body: "Recovery completed.",
presentation: {
kind: "system_notice",
tone: "success",
title: "Recovery completed",
detailsDefaultOpen: false,
},
metadata: null,
...baseTimestamps,
};
renderThread([comment]);
expect(container.querySelector('[data-testid="compact-system-notice"]')).toBeNull();
const status = container.querySelector('[role="status"]');
expect(status?.getAttribute("aria-label")).toBe("Recovery completed");
// Body is visible immediately on the full card.
expect(status?.textContent).toContain("Recovery completed.");
});
it("folds stale successful-run disposition warnings into the activity log disclosure style", () => {
const comment: IssueChatComment = {
id: "comment-stale-disposition-warning",
companyId: "company-1",
issueId: "issue-1",
authorType: "system",
authorAgentId: null,
authorUserId: null,
runId: "run-stale",
runAgentId: "agent-codex",
body: "Paperclip needs a disposition before this issue can continue.",
presentation: {
kind: "system_notice",
tone: "warning",
title: "Missing issue disposition",
detailsDefaultOpen: false,
},
metadata: {
version: 1,
sourceRunId: "run-stale",
sections: [
{
title: "Run evidence",
rows: [
{ type: "run_link", label: "Completed run", runId: "run-stale", title: "succeeded" },
{ type: "key_value", label: "Normalized cause", value: "successful_run_missing_state" },
],
},
],
},
...baseTimestamps,
};
renderThread([comment], {
issueStatus: "done",
successfulRunHandoff: {
state: "resolved",
required: false,
hasLiveContinuation: false,
sourceRunId: "run-stale",
correctiveRunId: "run-corrective",
assigneeAgentId: "agent-codex",
detectedProgressSummary: null,
createdAt: new Date("2026-05-04T17:00:00.000Z"),
},
});
const row = container.querySelector('[data-testid="stale-disposition-warning"]');
expect(row).not.toBeNull();
expect(row?.querySelector('span[aria-hidden="true"]')?.className).toContain("size-6");
const toggle = row?.querySelector("button[aria-expanded]") as HTMLButtonElement;
expect(toggle.className).toContain("w-full");
expect(toggle.className).toContain("py-0.5");
expect(row?.querySelector('[role="status"]')).toBeNull();
expect(row?.querySelector(".lucide-triangle-alert")).toBeNull();
expect(row?.querySelector(".lucide-chevron-down")).not.toBeNull();
expect(row?.querySelector('[data-testid="stale-disposition-warning-time"]')?.parentElement?.className).toContain("ml-auto");
expect(row?.textContent).toContain("Stale disposition warning");
expect(row?.textContent).not.toContain("This disposition warning is stale because the issue now has a newer disposition.");
expect(row?.textContent).not.toContain("Paperclip needs a disposition before this issue can continue.");
expect(toggle.getAttribute("aria-expanded")).toBe("false");
const detailsId = toggle.getAttribute("aria-controls");
expect(detailsId).toBeTruthy();
const details = detailsId ? container.ownerDocument.getElementById(detailsId) : null;
expect(details).not.toBeNull();
expect(details?.textContent).toContain("run-stale");
expect(details).toHaveProperty("hidden", true);
act(() => {
toggle.click();
});
expect(toggle.getAttribute("aria-expanded")).toBe("true");
expect(details).toHaveProperty("hidden", false);
expect(container.textContent).toContain("run-stale");
});
});