Files
PaperClipAI/ui/src/pages/Inbox.test.tsx
T
DottaandPaperclip aa58fa1073 perf(ui): restore fast Coordinator task and inbox loading (#18323)
## Thinking Path

> - Paperclip lets people manage work done by agents.
> - Tasks, Inbox, and Needs You must stay usable as work history grows.
> - The Coordinator views bypassed some existing collection
optimizations.
> - Navigation fetched complete decision cards for a count, and Tasks
mounted every loaded row.
> - This PR uses the canonical attention rules for small count and
task-ID responses. It also restores bounded rendering and the optimized
Inbox selection.
> - The benefit is less data and less initial rendering work without a
second definition of pending work.

## Linked Issues or Issue Description

**What happened?**

Coordinator navigation fetched the full attention feed on every cold
collection visit. Tasks rendered the entire loaded collection.
Coordinator could also select the old Inbox loader when Streamlined UI
was off.

**Expected behavior**

Fetch only the projection needed by each surface. Start with a bounded
set of rows. Keep complete task membership, accurate counts, current
permissions, and stable loaded rows.

**Steps to reproduce**

1. Enable Coordinator and open Tasks or Inbox with an empty browser
query cache.
2. Observe a complete attention-card download for the navigation badge.
3. Open a large task list and observe that every loaded row mounts at
once.
4. Disable Streamlined UI while keeping Coordinator enabled, then open
Mine and observe three overlapping compact task reads.

Related work: #17756, #3744, #17983, and #18312.

## What Changed

- Add board-only attention count and complete task-ID projections. Reuse
canonical company, privacy, addressee, deduplication, dismissal, snooze,
and archive rules. Skip card bodies, plan/image reads, and presentation
roster lookup.
- Share the small count read across desktop and mobile navigation. Reuse
the page-owned response on Needs You and Needs attention. Keep the
newest known count through navigation, including zero.
- Use the task-ID index for Needs attention and keep task row hydration
in batches of at most 100.
- Render 100 task rows initially, across the whole expanded tree. Reveal
150 more at a time through scrolling or a button. Keep existing rows
through refresh and append. Defer offscreen leaf layout with shared
tokens.
- Keep Coordinator on the optimized Inbox when Streamlined UI is off.
Use index reads for expanded project recents.
- Add real-database, component, and production-build browser regression
checks. Update the access and feature maps.

## Verification

- All required CI checks pass on
`1c68ac8957490a6c8585ba0939355f536c8120bf`: typecheck, build,
server/workspace tests, serialized database suites, and eight E2E
shards. Greptile is **5/5**, with its row-retention finding fixed and
the review thread resolved. Storybook checks were intentionally skipped;
Paperclip Review completed neutral.
- Workspace typecheck and production build passed locally. The final
rebased source also passed UI typecheck and production build. Token
gates and map checks passed.
- The attention service suite passed all 34 tests on disposable
PostgreSQL. Final targeted checks cover compact projections,
complete-set counts, quality approvals, snoozes, private tasks, and
board/company access. Database response capture confirms compact
projections do not read plans or images. All 31 OpenAPI tests passed
after registering both endpoint contracts.
- Six production-build browser journeys passed: desktop, mobile,
Coordinator Inbox with Streamlined UI off, Needs You, Needs attention,
and an older server page followed by task open/Back. They verify the
initial 100-row budget, scrolling, retained DOM identity/height, one
owner per attention read, complete task membership, 220 retained rows on
Back, and less than 2 px of return-position difference.
- The row-budget and Inbox regressions fail against original source and
pass with this change. The final three affected UI suites passed 40
tests, and the broader Inbox/IssuesList suites passed 101 tests
serially.

### Controlled production-build comparison

Baseline `b31ec0a2dc079e32272a8f618425fe92b68bbaec` versus PR head
`1c68ac8957490a6c8585ba0939355f536c8120bf`. Isolated baseline asset
`index-CS9uPxMx.js`; candidate asset `index-BxJeTQ30.js`. Both UI builds
used the same warm disposable API server and database, with 220 tasks
and 110 pending board approvals. Chromium **151.0.7922.34**, 1440 × 900
viewport, **4× CPU throttle**, loopback with no added network latency,
alternating baseline/candidate order. Cold cases use a new browser
context with network cache disabled; warm cases navigate within the same
context. Other host workloads are uncontrolled.

Readiness requires the destination collection's first visible task row,
then two animation frames. Inbox readiness waits for its canonical Mine
route so previous-page rows cannot satisfy the measurement. Five paired
samples per case; cold Inbox has a second five-pair confirmation wave
because the first result was slower. Times below are **milliseconds**;
parentheses show min–max. Paired savings are the median of individual
before-minus-after pairs, not the difference between medians.

| Journey | Pairs | Baseline median (range) | Candidate median (range) |
Median paired saving | Faster pairs |
| --- | ---: | ---: | ---: | ---: | ---: |
| Cold Tasks | 5 | 2752 (2542–3025) | 2300 (2275–2524) | 328 | 5/5 |
| Cold Inbox | 10 | 2536 (2283–2927) | 2582 (2117–2856) | 37 | 5/10 |
| Warm Tasks navigation | 5 | 2054 (1826–2479) | 1448 (1419–1548) | 635
| 5/5 |
| Warm Inbox navigation | 5 | 2020 (1485–2254) | 1568 (1289–1848) | 350
| 5/5 |
| Task → Back to Tasks | 5 | 1747 (1609–1948) | 1327 (1238–1355) | 472 |
5/5 |

Cold Tasks, warm Tasks, warm Inbox, and Back improved in every measured
pair. **Cold Inbox latency is inconclusive:** the two waves pointed in
opposite directions, with the candidate faster in 5/10 pairs. This PR
claims less badge work there, not a proven cold Inbox latency
improvement.

The canonical badge response shrank from **200,039 to 359 decoded
bytes** (encoded HTTP transfer: **8,125 to 598 bytes**). The compact
response also skips card payloads, plans, images, and presentation-only
roster work. These are measured mechanism reductions, separate from
end-to-end latency. Needs You still owns its full card feed; Needs
attention owns its complete task-ID projection. Reproduction setup and
assertions are in `tests/perf/collections/README.md`.

<details>
<summary>Individual paired readiness measurements (ms)</summary>

| Journey | Pair | Baseline | Candidate |
| --- | ---: | ---: | ---: |
| Cold Tasks | 1 | 3025 | 2283 |
| Cold Tasks | 2 | 2542 | 2275 |
| Cold Tasks | 3 | 2752 | 2424 |
| Cold Tasks | 4 | 2783 | 2300 |
| Cold Tasks | 5 | 2641 | 2524 |
| Cold Inbox | 1 | 2927 | 2702 |
| Cold Inbox | 2 | 2422 | 2629 |
| Cold Inbox | 3 | 2409 | 2606 |
| Cold Inbox | 4 | 2621 | 2769 |
| Cold Inbox | 5 | 2539 | 2856 |
| Cold Inbox | 6 | 2283 | 2117 |
| Cold Inbox | 7 | 2466 | 2558 |
| Cold Inbox | 8 | 2674 | 2286 |
| Cold Inbox | 9 | 2696 | 2389 |
| Cold Inbox | 10 | 2533 | 2183 |
| Warm Tasks navigation | 1 | 2479 | 1423 |
| Warm Tasks navigation | 2 | 2228 | 1548 |
| Warm Tasks navigation | 3 | 2054 | 1419 |
| Warm Tasks navigation | 4 | 1826 | 1448 |
| Warm Tasks navigation | 5 | 1954 | 1518 |
| Warm Inbox navigation | 1 | 2020 | 1661 |
| Warm Inbox navigation | 2 | 2198 | 1848 |
| Warm Inbox navigation | 3 | 1733 | 1497 |
| Warm Inbox navigation | 4 | 2254 | 1568 |
| Warm Inbox navigation | 5 | 1485 | 1289 |
| Task → Back to Tasks | 1 | 1721 | 1330 |
| Task → Back to Tasks | 2 | 1747 | 1275 |
| Task → Back to Tasks | 3 | 1948 | 1355 |
| Task → Back to Tasks | 4 | 1609 | 1327 |
| Task → Back to Tasks | 5 | 1790 | 1238 |

</details>

### Failed attempts and limits

- The local full repository command hit embedded PostgreSQL startup
failures and 16 failures in the untouched native session executor suite;
it was stopped. The full local suite is not claimed green. CI on the
final revision provides the broad test result.
- A concurrent local UI run hit timing failures under host contention.
Inbox/IssuesList subsequently passed all 101 tests serially. No tests
were weakened to address these timeouts.
- CI caught two integration gaps introduced during this work: missing
OpenAPI entries for the new endpoints, and an attention fixture
incompatible with the routine filters added on master. Both were fixed;
the affected 31 OpenAPI and 40 UI tests pass.
- The final revision's first CI attempt failed two untouched tests:
scheduled-retry batch cancellation and the tool-access audience/archive
race. Both failed jobs passed when rerun without changing the revision.
Their original failed attempts remain visible in [the workflow
history](https://github.com/paperclipai/paperclip/actions/runs/41083379875).
- Browser evidence uses synthetic disposable data. This PR is not
deployed staging or production verification.

## Risks

- New attention sources must preserve canonical eligibility. Both
projections call the existing collector, so they share its permissions
and complete-set behavior.
- The count and task-ID caches must not stand in for full decision
cards. Each projection has its own key under the existing attention
invalidation prefix.
- A row budget can omit descendants if applied per parent. The tests
cover one global budget across a large expanded tree, refresh, append,
reset, and incremental loading.
- Local browser evidence uses synthetic data and is not staging
deployment proof.

## Model Used

OpenAI Codex, GPT-6 family, with reasoning, tool use, and code
execution. The exact backend model deployment ID and context window were
not exposed in this session.

## 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 (affected suites; broad-run
limitations are listed above)
- [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>
2026-10-30 11:58:53 -05:00

1781 lines
67 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// @vitest-environment jsdom
import { CoordinatorPresentation } from "../components/coordinator/CoordinatorPresentation";
import { queryKeys } from "../lib/queryKeys";
import type { ComponentProps } from "react";
import { flushSync } from "react-dom";
import { createRoot } from "react-dom/client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import type { Approval, HeartbeatRun, Issue } from "@paperclipai/shared";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { CompanyJoinRequest } from "../api/access";
import {
clearLocalInboxArchive,
getLocalInboxArchiveIssueIds,
} from "../lib/inboxArchiveCache";
import { taskCollectionPreferencesStorageKey } from "../lib/task-collection-preferences";
const routerMock = vi.hoisted(() => ({
location: { pathname: "/", search: "", hash: "" },
navigate: vi.fn(),
}));
const companyState = vi.hoisted(() => ({ selectedCompanyId: "company-1" as string | null }));
const externalObjectMocks = vi.hoisted(() => ({
summaries: new Map(),
error: null as Error | null,
retry: vi.fn(),
}));
const apiMocks = vi.hoisted(() => ({
approvalsList: vi.fn(),
joinRequestsList: vi.fn(),
userDirectoryList: vi.fn(),
authSession: vi.fn(),
dashboardSummary: vi.fn(),
executionWorkspaceSummaries: vi.fn(),
issuesList: vi.fn(),
issuesCount: vi.fn(),
issueLabels: vi.fn(),
retryTask: vi.fn(),
archiveFromInbox: vi.fn(),
unarchiveFromInbox: vi.fn(),
agentsList: vi.fn(),
heartbeatRunsList: vi.fn(),
liveRunsForCompany: vi.fn(),
experimentalSettings: vi.fn(),
projectsList: vi.fn(),
}));
vi.mock("../api/approvals", () => ({
approvalsApi: { list: apiMocks.approvalsList },
}));
vi.mock("../api/access", async () => {
const actual = await vi.importActual<typeof import("../api/access")>("../api/access");
return {
...actual,
accessApi: {
listJoinRequests: apiMocks.joinRequestsList,
listUserDirectory: apiMocks.userDirectoryList,
},
};
});
vi.mock("../api/auth", () => ({
authApi: { getSession: apiMocks.authSession },
}));
vi.mock("../api/dashboard", () => ({
dashboardApi: { summary: apiMocks.dashboardSummary },
}));
vi.mock("../api/execution-workspaces", () => ({
executionWorkspacesApi: { listSummaries: apiMocks.executionWorkspaceSummaries },
}));
vi.mock("../api/issues", () => ({
issuesApi: {
list: apiMocks.issuesList,
listCompact: apiMocks.issuesList,
count: apiMocks.issuesCount,
listLabels: apiMocks.issueLabels,
retryTask: apiMocks.retryTask,
markRead: vi.fn(),
markUnread: vi.fn(),
archiveFromInbox: apiMocks.archiveFromInbox,
unarchiveFromInbox: apiMocks.unarchiveFromInbox,
},
}));
vi.mock("../api/agents", () => ({
agentsApi: { list: apiMocks.agentsList },
}));
vi.mock("../api/heartbeats", () => ({
heartbeatsApi: {
list: apiMocks.heartbeatRunsList,
liveRunsForCompany: apiMocks.liveRunsForCompany,
},
}));
vi.mock("../api/instanceSettings", () => ({
instanceSettingsApi: { getExperimental: apiMocks.experimentalSettings },
}));
vi.mock("../api/projects", () => ({
projectsApi: { list: apiMocks.projectsList },
}));
vi.mock("../context/CompanyContext", () => ({
useCompany: () => companyState,
}));
vi.mock("../context/BreadcrumbContext", () => ({
useBreadcrumbs: () => ({ setBreadcrumbs: vi.fn() }),
}));
vi.mock("../context/ToastContext", () => ({
useToastActions: () => ({ pushToast: vi.fn() }),
}));
vi.mock("../context/DialogContext", () => ({
useDialogActions: () => ({ openNewIssue: vi.fn() }),
}));
vi.mock("../context/SidebarContext", () => ({
useSidebar: () => ({ isMobile: false }),
}));
vi.mock("../hooks/useInboxBadge", () => ({
useDismissedInboxAlerts: () => ({ dismissed: new Set(), dismiss: vi.fn() }),
useInboxDismissals: () => ({ dismissedAtByKey: new Map(), dismiss: vi.fn() }),
useReadInboxItems: () => ({
readItems: new Set(),
markRead: vi.fn(),
markUnread: vi.fn(),
}),
}));
vi.mock("../hooks/useIssueExternalObjects", () => ({
useIssueExternalObjectSummaries: () => ({
summaries: externalObjectMocks.summaries,
isLoading: false,
isReady: !externalObjectMocks.error,
error: externalObjectMocks.error,
refetch: externalObjectMocks.retry,
}),
}));
import {
FailedRunInboxRow,
Inbox,
InboxGroupHeader,
InboxIssueMetaLeading,
InboxIssueTrailingColumns,
formatJoinRequestInboxLabel,
} from "./Inbox";
vi.mock("@/lib/router", () => ({
Link: ({ children, className, to, ...props }: ComponentProps<"a"> & { to?: string }) => (
<a className={className} href={to} {...props}>{children}</a>
),
useLocation: () => routerMock.location,
useNavigate: () => routerMock.navigate,
}));
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true;
async function act(callback: () => void | Promise<void>) {
let result: void | Promise<void> = undefined;
flushSync(() => {
result = callback();
});
await result;
}
// jsdom doesn't implement scrollIntoView; the inbox calls it from a passive effect.
if (typeof Element !== "undefined" && !Element.prototype.scrollIntoView) {
Element.prototype.scrollIntoView = () => {};
}
function createIssue(overrides: Partial<Issue> = {}): Issue {
return {
id: "issue-1",
identifier: "PAP-904",
companyId: "company-1",
projectId: null,
projectWorkspaceId: null,
goalId: null,
parentId: null,
title: "Inbox item",
description: null,
status: "todo",
priority: "medium",
reviewPolicy: null,
assigneeAgentId: null,
assigneeUserId: null,
responsibleUserId: null,
createdByAgentId: null,
createdByUserId: null,
issueNumber: 904,
requestDepth: 0,
billingCode: null,
assigneeAdapterOverrides: null,
executionWorkspaceId: null,
executionWorkspacePreference: null,
executionWorkspaceSettings: null,
checkoutRunId: null,
executionRunId: null,
executionAgentNameKey: null,
executionLockedAt: null,
startedAt: null,
completedAt: null,
cancelledAt: null,
hiddenAt: null,
createdAt: new Date("2026-03-11T00:00:00.000Z"),
updatedAt: new Date("2026-03-11T00:00:00.000Z"),
labels: [],
labelIds: [],
myLastTouchAt: null,
lastExternalCommentAt: null,
lastActivityAt: new Date("2026-03-11T00:00:00.000Z"),
isUnreadForMe: false,
...overrides,
workMode: overrides.workMode ?? "standard",
};
}
function createDeferred<T>() {
let resolve!: (value: T) => void;
let reject!: (reason?: unknown) => void;
const promise = new Promise<T>((innerResolve, innerReject) => {
resolve = innerResolve;
reject = innerReject;
});
return { promise, resolve, reject };
}
function createJoinRequest(
overrides: Partial<CompanyJoinRequest> = {},
): CompanyJoinRequest {
return {
id: "join-1",
inviteId: "invite-1",
companyId: "company-1",
requestType: "human",
status: "pending_approval",
requestIp: "127.0.0.1",
requestingUserId: "user-1",
requestEmailSnapshot: "joiner@example.com",
agentName: null,
adapterType: null,
capabilities: null,
agentDefaultsPayload: null,
claimSecretExpiresAt: null,
claimSecretConsumedAt: null,
createdAgentId: null,
approvedByUserId: null,
approvedAt: null,
rejectedByUserId: null,
rejectedAt: null,
createdAt: new Date("2026-03-11T00:00:00.000Z"),
updatedAt: new Date("2026-03-11T00:00:00.000Z"),
requesterUser: {
id: "user-1",
name: "Jordan Example",
email: "joiner@example.com",
image: null,
},
approvedByUser: null,
rejectedByUser: null,
invite: null,
...overrides,
};
}
function createApproval(overrides: Partial<Approval> = {}): Approval {
return {
id: "approval-1",
companyId: "company-1",
type: "hire_agent",
requestedByAgentId: null,
requestedByUserId: "local-board",
status: "pending",
payload: { name: "New teammate" },
decisionNote: null,
decidedByUserId: null,
decidedAt: null,
createdAt: new Date("2026-03-11T00:00:00.000Z"),
updatedAt: new Date("2026-03-11T00:00:00.000Z"),
...overrides,
};
}
function createFailedRun(overrides: Partial<HeartbeatRun> = {}): HeartbeatRun {
return {
id: "run-1",
issueId: null,
scopeKind: "company",
companyId: "company-1",
agentId: "agent-1",
responsibleUserId: null,
invocationSource: "assignment",
triggerDetail: null,
status: "failed",
error: "boom",
wakeupRequestId: null,
exitCode: null,
signal: null,
usageJson: null,
resultJson: null,
sessionIdBefore: null,
sessionIdAfter: null,
logStore: null,
logRef: null,
logBytes: null,
logSha256: null,
logCompressed: false,
stdoutExcerpt: null,
stderrExcerpt: null,
errorCode: null,
externalRunId: null,
processPid: null,
processGroupId: null,
processStartedAt: null,
lastOutputAt: null,
lastOutputSeq: 0,
lastOutputStream: null,
lastOutputBytes: null,
retryOfRunId: null,
processLossRetryCount: 0,
livenessState: null,
livenessReason: null,
continuationAttempt: 0,
lastUsefulActionAt: null,
nextAction: null,
contextSnapshot: null,
startedAt: new Date("2026-03-11T00:00:00.000Z"),
finishedAt: new Date("2026-03-11T00:01:00.000Z"),
createdAt: new Date("2026-03-11T00:00:00.000Z"),
updatedAt: new Date("2026-03-11T00:01:00.000Z"),
...overrides,
};
}
function resetInboxApiMocks() {
companyState.selectedCompanyId = "company-1";
for (const mock of Object.values(apiMocks)) mock.mockReset();
externalObjectMocks.summaries.clear();
externalObjectMocks.error = null;
externalObjectMocks.retry.mockReset();
routerMock.location.pathname = "/";
routerMock.location.search = "";
routerMock.location.hash = "";
routerMock.navigate.mockReset();
apiMocks.approvalsList.mockResolvedValue([]);
apiMocks.joinRequestsList.mockResolvedValue([]);
apiMocks.userDirectoryList.mockResolvedValue({ users: [] });
apiMocks.authSession.mockResolvedValue({
user: { id: "local-board" },
session: { userId: "local-board" },
});
apiMocks.dashboardSummary.mockResolvedValue({
agents: { error: 0 },
costs: { monthBudgetCents: 0, monthUtilizationPercent: 0 },
});
apiMocks.executionWorkspaceSummaries.mockResolvedValue([]);
apiMocks.issuesList.mockResolvedValue([]);
apiMocks.issuesCount.mockResolvedValue({ count: 0 });
apiMocks.issueLabels.mockResolvedValue([]);
apiMocks.retryTask.mockReset().mockResolvedValue({ accepted: true, requestId: "retry-intent", presentation: null });
apiMocks.archiveFromInbox.mockResolvedValue({ id: "issue-1", archivedAt: new Date() });
apiMocks.unarchiveFromInbox.mockResolvedValue({ id: "issue-1", archivedAt: new Date() });
apiMocks.agentsList.mockResolvedValue([]);
apiMocks.heartbeatRunsList.mockResolvedValue([]);
apiMocks.liveRunsForCompany.mockResolvedValue([]);
apiMocks.experimentalSettings.mockResolvedValue({
enableIsolatedWorkspaces: false,
enableStreamlinedUi: true,
});
apiMocks.projectsList.mockResolvedValue([]);
}
describe("Inbox toolbar", () => {
let container: HTMLDivElement;
beforeEach(() => {
localStorage.clear();
resetInboxApiMocks();
container = document.createElement("div");
document.body.appendChild(container);
});
afterEach(() => {
for (const issueId of getLocalInboxArchiveIssueIds("company-1")) {
clearLocalInboxArchive("company-1", issueId);
}
container.remove();
});
it.each([
{ tab: "mine", userId: "user-1", visible: ["own-failure"], hidden: ["other-failure", "unowned-failure"] },
{ tab: "mine", userId: "user-2", visible: ["other-failure"], hidden: ["own-failure", "unowned-failure"] },
{ tab: "mine", userId: "local-board", visible: ["unowned-failure"], hidden: ["own-failure", "other-failure"] },
{ tab: "mine", userId: null, visible: [], hidden: ["own-failure", "other-failure", "unowned-failure"] },
{ tab: "all", userId: "user-1", visible: ["own-failure", "other-failure", "unowned-failure"], hidden: [] },
].flatMap((scenario) => [true, false].map((streamlined) => ({ ...scenario, streamlined }))))(
"scopes failed runs on $tab for $userId (streamlined=$streamlined)",
async ({ tab, userId, visible, hidden, streamlined }) => {
apiMocks.experimentalSettings.mockResolvedValue({ enableIsolatedWorkspaces: false, enableStreamlinedUi: streamlined });
routerMock.location.pathname = `/inbox/${tab}`;
apiMocks.authSession.mockResolvedValue(userId ? { user: { id: userId }, session: { userId } } : null);
apiMocks.heartbeatRunsList.mockResolvedValue([
createFailedRun({ id: "own-failure", agentId: "agent-1", responsibleUserId: "user-1" }),
createFailedRun({ id: "other-failure", agentId: "agent-2", responsibleUserId: "user-2", status: "timed_out" }),
createFailedRun({ id: "unowned-failure", agentId: "agent-3" }),
]);
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false, staleTime: Infinity, gcTime: 0 } },
});
const root = createRoot(container);
try {
await act(async () => {
root.render(<QueryClientProvider client={queryClient}><Inbox /></QueryClientProvider>);
});
await vi.waitFor(() => {
expect(apiMocks.heartbeatRunsList).toHaveBeenCalled();
expect(queryClient.isFetching()).toBe(0);
for (const id of visible) expect(container.querySelector(`a[href$="/runs/${id}"]`)).not.toBeNull();
for (const id of hidden) expect(container.querySelector(`a[href$="/runs/${id}"]`)).toBeNull();
});
} finally {
act(() => root.unmount());
queryClient.clear();
}
},
);
it("restores the legacy toolbar and issue-row presentation when Streamlined UI is off", async () => {
routerMock.location.pathname = "/inbox/mine";
apiMocks.experimentalSettings.mockResolvedValue({
enableIsolatedWorkspaces: false,
enableStreamlinedUi: false,
});
apiMocks.issuesList.mockResolvedValue([
createIssue({ id: "legacy-row", identifier: "PAP-1904", title: "Legacy inbox task" }),
]);
localStorage.setItem(
taskCollectionPreferencesStorageKey({ companyId: "company-1", collectionKey: "inbox" }),
JSON.stringify({
version: 1,
companyId: "company-1",
collectionKey: "inbox",
viewState: {},
columns: [],
}),
);
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false, staleTime: 0, gcTime: 0 } },
});
const root = createRoot(container);
await act(async () => {
root.render(
<QueryClientProvider client={queryClient}>
<Inbox />
</QueryClientProvider>,
);
});
await vi.waitFor(() => expect(container.textContent).toContain("Legacy inbox task"));
expect(container.querySelector('[data-slot="collection-toolbar"]')).toBeNull();
expect(container.querySelector('[role="toolbar"][aria-label="Inbox controls"]')).toBeNull();
expect(container.textContent).toContain("Mine");
expect(container.querySelector('[data-slot="task-row"]')).toBeNull();
const issueLink = container.querySelector('[data-inbox-issue-link]');
// Production reads the legacy Inbox column key and must ignore the
// Streamlined collection envelope above when the experiment is disabled.
expect(issueLink?.parentElement?.textContent).toContain("PAP-1904");
act(() => root.unmount());
});
it("does not render external-object summaries in inbox rows", async () => {
routerMock.location.pathname = "/inbox/mine";
const issue = createIssue({ title: "Inbox row without external object column" });
apiMocks.issuesList.mockResolvedValue([issue]);
externalObjectMocks.summaries.set(issue.id, {
total: 1,
highestSeverity: "failed",
byStatusCategory: { failed: 1 },
objects: [],
});
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false, staleTime: 0, gcTime: 0 } },
});
const root = createRoot(container);
await act(async () => {
root.render(
<QueryClientProvider client={queryClient}>
<Inbox />
</QueryClientProvider>,
);
});
await vi.waitFor(() => {
expect(container.textContent).toContain(issue.title);
});
expect(container.querySelector('[aria-label^="External objects:"]')).toBeNull();
act(() => root.unmount());
});
it("keeps archive hover actions and swipe targets on every unread non-task Mine row", async () => {
routerMock.location.pathname = "/inbox/mine";
localStorage.setItem("paperclip:inbox:group-by", "none");
apiMocks.approvalsList.mockResolvedValue([createApproval()]);
apiMocks.heartbeatRunsList.mockResolvedValue([createFailedRun()]);
apiMocks.joinRequestsList.mockResolvedValue([createJoinRequest()]);
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false, staleTime: 0, gcTime: 0 } },
});
const root = createRoot(container);
await act(async () => {
root.render(
<QueryClientProvider client={queryClient}>
<Inbox />
</QueryClientProvider>,
);
});
await vi.waitFor(() => {
expect(container.textContent).toContain("Hire Agent: New teammate");
expect(container.textContent).toContain("Failed run");
expect(container.textContent).toContain("Jordan Example");
});
const rowFor = (text: string) =>
[...container.querySelectorAll("[data-inbox-item]")]
.find((row) => row.textContent?.includes(text));
for (const text of ["Hire Agent: New teammate", "Failed run", "Jordan Example"]) {
const row = rowFor(text);
expect(row, `missing inbox row for ${text}`).toBeDefined();
expect(row?.querySelector('button[aria-label="Mark as read"]')).not.toBeNull();
const archiveButton = row?.querySelector<HTMLButtonElement>('button[aria-label="Archive"]');
expect(archiveButton).not.toBeNull();
expect(archiveButton?.className).toContain("opacity-0");
expect(archiveButton?.className).toContain("group-hover:opacity-100");
expect(row?.querySelector("[data-inbox-row-surface]")).not.toBeNull();
}
const approvalRow = rowFor("Hire Agent: New teammate");
const approvalActions = [...(approvalRow?.querySelectorAll("button") ?? [])]
.filter((button) => button.textContent === "Approve" || button.textContent === "Reject");
expect(approvalActions.length).toBeGreaterThanOrEqual(2);
approvalActions.forEach((button) => {
expect(button.className).toContain("h-8");
expect(button.className).toContain("min-w-(--sz-64px)");
expect(button.className).toContain("justify-center");
});
act(() => root.unmount());
});
it("restores folded and unfolded sub-tasks across remounts", async () => {
routerMock.location.pathname = "/inbox/mine";
const storageKey = "paperclip:inbox:collapsed-parents:company-1";
localStorage.removeItem(storageKey);
const parent = createIssue({
id: "parent-issue",
identifier: "PAP-1001",
title: "Parent inbox task",
});
const child = createIssue({
id: "child-issue",
identifier: "PAP-1002",
parentId: parent.id,
title: "Nested inbox task",
});
const grandchild = createIssue({
id: "grandchild-issue",
identifier: "PAP-1003",
parentId: child.id,
title: "Deeply nested inbox task",
});
apiMocks.issuesList.mockResolvedValue([parent, child, grandchild]);
const mountInbox = async () => {
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false, staleTime: 0, gcTime: 0 } },
});
const root = createRoot(container);
await act(async () => {
root.render(
<QueryClientProvider client={queryClient}>
<Inbox />
</QueryClientProvider>,
);
});
await vi.waitFor(() => {
expect(container.textContent).toContain(parent.title);
});
return root;
};
const parentToggle = () => {
const parentRow = Array.from(container.querySelectorAll("[data-inbox-item]"))
.find((row) => row.textContent?.includes(parent.title));
return parentRow?.querySelector<HTMLButtonElement>('button[data-slot="icon-button"]') ?? null;
};
let root = await mountInbox();
try {
expect(container.textContent).toContain(child.title);
expect(container.textContent).toContain(grandchild.title);
const taskRowFor = (title: string) =>
Array.from(container.querySelectorAll('[data-slot="task-row"]'))
.find((row) => row.textContent?.includes(title));
const childTaskRow = taskRowFor(child.title);
const grandchildTaskRow = taskRowFor(grandchild.title);
expect(childTaskRow?.querySelectorAll('[data-slot="task-row-tree-guide"]')).toHaveLength(1);
expect(childTaskRow?.querySelector('button[aria-label="Collapse sub-tasks"]')).not.toBeNull();
expect(grandchildTaskRow?.querySelectorAll('[data-slot="task-row-tree-guide"]')).toHaveLength(2);
expect(grandchildTaskRow?.querySelector('[data-slot="task-row-disclosure-spacer"]')).not.toBeNull();
await act(async () => {
parentToggle()?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
await vi.waitFor(() => {
expect(container.textContent).not.toContain(child.title);
expect(container.textContent).not.toContain(grandchild.title);
});
expect(JSON.parse(localStorage.getItem(storageKey) ?? "[]")).toEqual([parent.id]);
act(() => root.unmount());
root = await mountInbox();
expect(container.textContent).not.toContain(child.title);
await act(async () => {
parentToggle()?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
await vi.waitFor(() => {
expect(container.textContent).toContain(child.title);
expect(container.textContent).toContain(grandchild.title);
});
expect(JSON.parse(localStorage.getItem(storageKey) ?? "[]")).toEqual([]);
act(() => root.unmount());
root = await mountInbox();
expect(container.textContent).toContain(child.title);
expect(container.textContent).toContain(grandchild.title);
} finally {
localStorage.removeItem(storageKey);
act(() => root.unmount());
}
});
it("shows blocked toolbar controls on the Blocked tab", async () => {
routerMock.location.pathname = "/inbox/blocked";
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false, staleTime: 0, gcTime: 0 } },
});
const root = createRoot(container);
await act(async () => {
root.render(
<QueryClientProvider client={queryClient}>
<Inbox />
</QueryClientProvider>,
);
});
expect(container.querySelector('input[placeholder="Search inbox…"]')).not.toBeNull();
expect(container.querySelector('[data-testid="inbox-blocked-tab-badge"]')).toBeNull();
expect(container.querySelector('button[title="Filter"]')).not.toBeNull();
expect(container.querySelector('button[title="Group"]')).not.toBeNull();
expect(container.querySelector('button[title="Columns"]')).not.toBeNull();
expect(container.querySelector('button[title="Sort"]')).not.toBeNull();
expect(container.querySelector('button[title="Enable parent-child nesting"]')).toBeNull();
expect(container.textContent).not.toContain("Mark all as read");
act(() => {
root.unmount();
});
});
it("keeps Mine, Recent, Unread, Blocked, and All in the shared toolbar geometry", async () => {
for (const tab of ["mine", "recent", "unread", "blocked", "all"] as const) {
routerMock.location.pathname = `/inbox/${tab}`;
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false, staleTime: 0, gcTime: 0 } },
});
const root = createRoot(container);
await act(async () => {
root.render(
<QueryClientProvider client={queryClient}>
<Inbox />
</QueryClientProvider>,
);
});
const toolbar = container.querySelector('[data-slot="collection-toolbar"]');
expect(toolbar?.getAttribute("aria-label")).toBe("Inbox controls");
expect(toolbar?.querySelector('[data-slot="collection-toolbar-context"]')).not.toBeNull();
expect(toolbar?.querySelector('[data-slot="collection-toolbar-search"] input[placeholder="Search inbox…"]')).not.toBeNull();
expect(toolbar?.querySelector('[data-slot="collection-toolbar-controls"]')).not.toBeNull();
expect(container.querySelectorAll('input[placeholder="Search inbox…"]')).toHaveLength(1);
act(() => root.unmount());
container.replaceChildren();
}
});
it("explains that live-run filtering is different from active task statuses", async () => {
routerMock.location.pathname = "/inbox/mine";
localStorage.setItem("paperclip:inbox:filters:company-1", JSON.stringify({
allCategoryFilter: "everything",
allApprovalFilter: "all",
issueFilters: { liveOnly: true },
}));
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false, staleTime: 0, gcTime: 0 } },
});
const root = createRoot(container);
await act(async () => {
root.render(
<QueryClientProvider client={queryClient}>
<Inbox />
</QueryClientProvider>,
);
});
expect(container.querySelector('[data-testid="inbox-filter-scope-feedback"]')?.textContent)
.toBe("Live runs only — tasks currently connected to an agent run.");
const migrated = JSON.parse(
localStorage.getItem("paperclip:task-collection:v1:company-1:inbox") ?? "null",
) as { companyId?: string; collectionKey?: string } | null;
expect(migrated).toMatchObject({ companyId: "company-1", collectionKey: "inbox" });
act(() => root.unmount());
});
it("groups ungrouped attention items by Today, Yesterday, and Earlier", async () => {
routerMock.location.pathname = "/inbox/mine";
const now = new Date();
const localNoon = (daysAgo: number) =>
new Date(now.getFullYear(), now.getMonth(), now.getDate() - daysAgo, 12, 0, 0);
apiMocks.issuesList.mockResolvedValue([
createIssue({ id: "today", title: "Today task", lastActivityAt: localNoon(0) }),
createIssue({ id: "yesterday", title: "Yesterday task", lastActivityAt: localNoon(1) }),
createIssue({ id: "earlier", title: "Earlier task", lastActivityAt: localNoon(3) }),
]);
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false, staleTime: 0, gcTime: 0 } },
});
const root = createRoot(container);
await act(async () => {
root.render(
<QueryClientProvider client={queryClient}>
<Inbox />
</QueryClientProvider>,
);
});
await vi.waitFor(() => expect(container.textContent).toContain("Earlier task"));
const separators = [...container.querySelectorAll('[data-testid="inbox-date-group"]')];
expect(separators.map((node) => node.textContent?.trim()))
.toEqual(["Today", "Yesterday", "Earlier"]);
expect(separators.every((separator) => (
separator.querySelectorAll("[data-date-group-rule]").length === 2
))).toBe(true);
expect(separators.every((separator) => (
separator.querySelector("[data-date-group-label]")?.classList.contains("text-muted-foreground/70")
))).toBe(true);
act(() => root.unmount());
});
it("honors the saved Columns option for hiding date group separators", async () => {
routerMock.location.pathname = "/inbox/mine";
localStorage.setItem(
taskCollectionPreferencesStorageKey({
companyId: "company-1",
collectionKey: "inbox",
}),
JSON.stringify({
version: 1,
companyId: "company-1",
collectionKey: "inbox",
viewState: { showDateGroupSeparators: false },
columns: ["status", "id", "updated"],
}),
);
const now = new Date();
const localNoon = (daysAgo: number) =>
new Date(now.getFullYear(), now.getMonth(), now.getDate() - daysAgo, 12, 0, 0);
apiMocks.issuesList.mockResolvedValue([
createIssue({ id: "today", title: "Today task", lastActivityAt: localNoon(0) }),
createIssue({ id: "earlier", title: "Earlier task", lastActivityAt: localNoon(3) }),
]);
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false, staleTime: 0, gcTime: 0 } },
});
const root = createRoot(container);
await act(async () => {
root.render(
<QueryClientProvider client={queryClient}>
<Inbox />
</QueryClientProvider>,
);
});
await vi.waitFor(() => expect(container.textContent).toContain("Earlier task"));
expect(container.querySelector('[data-testid="inbox-date-group"]')).toBeNull();
act(() => root.unmount());
});
it("shows the resolved isolated workspace name in canonical task metadata", async () => {
routerMock.location.pathname = "/inbox/mine";
localStorage.setItem("paperclip:inbox:issue-columns", JSON.stringify(["status", "id", "workspace", "updated"]));
apiMocks.experimentalSettings.mockResolvedValue({ enableIsolatedWorkspaces: true });
apiMocks.executionWorkspaceSummaries.mockResolvedValue([{
id: "execution-workspace-1",
name: "Workspace Aurora",
mode: "isolated_workspace",
projectWorkspaceId: "project-workspace-1",
}]);
apiMocks.projectsList.mockResolvedValue([{
id: "project-1",
name: "Launch",
color: null,
workspaces: [{ id: "project-workspace-1", name: "Main" }],
executionWorkspacePolicy: { defaultProjectWorkspaceId: "project-workspace-1" },
primaryWorkspace: null,
}]);
apiMocks.issuesList.mockResolvedValue([createIssue({
projectId: "project-1",
executionWorkspaceId: "execution-workspace-1",
title: "Workspace-aware task",
})]);
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false, staleTime: 0, gcTime: 0 } },
});
const root = createRoot(container);
await act(async () => {
root.render(
<QueryClientProvider client={queryClient}>
<Inbox />
</QueryClientProvider>,
);
});
// Workspace metadata resolves independently of the task list.
await vi.waitFor(() => {
expect(container.querySelector('[data-slot="task-row"]')?.textContent).toContain("Workspace Aurora");
});
const taskRow = container.querySelector('[data-slot="task-row"]');
const identifier = taskRow?.querySelector('[data-slot="task-row-identifier"]');
const timestamp = taskRow?.querySelector('[data-slot="task-row-timestamp"]');
expect(taskRow?.textContent).toContain("Workspace Aurora");
expect(identifier?.textContent).toBe("PAP-904");
expect(timestamp).not.toBeNull();
if (!identifier || !timestamp) throw new Error("Expected canonical identifier and timestamp columns");
expect(identifier.compareDocumentPosition(timestamp) & Node.DOCUMENT_POSITION_FOLLOWING).not.toBe(0);
act(() => root.unmount());
});
it("hides workspace grouping when isolated workspaces are disabled", async () => {
routerMock.location.pathname = "/inbox/mine";
apiMocks.experimentalSettings.mockResolvedValue({ enableIsolatedWorkspaces: false });
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false, staleTime: 0, gcTime: 0 } },
});
const root = createRoot(container);
await act(async () => {
root.render(
<QueryClientProvider client={queryClient}>
<Inbox />
</QueryClientProvider>,
);
});
const groupButton = container.querySelector<HTMLButtonElement>('button[title="Group"]');
expect(groupButton).not.toBeNull();
await act(async () => {
groupButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
const groupOptions = Array.from(document.body.querySelectorAll("button")).map((button) => button.textContent);
expect(groupOptions).not.toContain("Workspace");
act(() => {
root.unmount();
});
});
it("waits for the task index before revealing faster failed-run rows", async () => {
routerMock.location.pathname = "/inbox/mine";
let release: (rows: Issue[]) => void = () => undefined;
apiMocks.issuesList.mockImplementation((_company, filters) => filters.projection === "index"
? new Promise<Issue[]>((resolve) => { release = resolve; }) : Promise.resolve([]));
apiMocks.heartbeatRunsList.mockResolvedValue([createFailedRun()]);
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false, gcTime: 0 } } });
const root = createRoot(container);
await act(async () => root.render(<QueryClientProvider client={queryClient}><Inbox /></QueryClientProvider>));
await vi.waitFor(() => expect(apiMocks.heartbeatRunsList).toHaveBeenCalled());
expect(container.querySelector('[aria-label="Loading tasks"]')).not.toBeNull();
expect(container.querySelector('[data-inbox-item]')).toBeNull();
await act(async () => release([createIssue({ title: "Delayed task index row" })]));
await vi.waitFor(() => expect(container.textContent).toContain("Delayed task index row"));
expect(container.querySelector('[aria-label="Loading tasks"]')).toBeNull();
act(() => root.unmount());
});
it("can select a company after a cold inbox mount", async () => {
companyState.selectedCompanyId = null;
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false, gcTime: 0 } } });
const root = createRoot(container);
const render = () => root.render(<QueryClientProvider client={queryClient}><Inbox /></QueryClientProvider>);
await act(render);
await vi.waitFor(() => expect(container.textContent).toContain("Select an organization"));
companyState.selectedCompanyId = "company-1";
apiMocks.issuesList.mockResolvedValue([createIssue({ title: "Cold company task" })]);
await act(render);
await vi.waitFor(() => expect(container.textContent).toContain("Cold company task"));
act(() => root.unmount());
});
it("keeps more than 25 description-only matches in Mine with archive actions", async () => {
routerMock.location.pathname = "/inbox/mine";
const rows = Array.from({ length: 30 }, (_, index) => createIssue({ id: `source-${index}`, title: `Source task ${index}`, identifier: `PAP-${index}`, listProjection: "index" }));
apiMocks.issuesList.mockImplementation((_company, filters) => Promise.resolve(filters.q && !filters.ids ? [] : rows));
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false, gcTime: 0 } } });
const root = createRoot(container);
await act(() => root.render(<QueryClientProvider client={queryClient}><Inbox /></QueryClientProvider>));
await vi.waitFor(() => expect(container.querySelectorAll('[data-inbox-item]')).toHaveLength(30));
await act(() => {
const input = container.querySelector<HTMLInputElement>('input[type="search"]')!;
Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value")!.set!.call(input, "only-in-description");
input.dispatchEvent(new Event("input", { bubbles: true }));
});
await vi.waitFor(() => expect(apiMocks.issuesList).toHaveBeenCalledWith("company-1", expect.objectContaining({ q: "only-in-description", ids: expect.arrayContaining(rows.map((row) => row.id)), projection: "index", limit: 100 }), expect.anything()));
await vi.waitFor(() => expect(container.querySelectorAll('[data-inbox-item]')).toHaveLength(30));
expect(container.querySelectorAll('button[aria-label="Archive"]')).toHaveLength(30);
expect(container.textContent).not.toContain("Other matching tasks");
act(() => root.unmount());
});
it("keeps rows readable and offers retry when optional details fail", async () => {
routerMock.location.pathname = "/inbox/mine";
let failDetails = true;
apiMocks.issuesList.mockImplementation((_company, filters) => filters.projection === "rows" && failDetails
? Promise.reject(new Error("Details unavailable")) : Promise.resolve([createIssue({ title: "Readable task" })]));
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false, gcTime: 0 } } });
const root = createRoot(container);
await act(() => root.render(<QueryClientProvider client={queryClient}><Inbox /></QueryClientProvider>));
await vi.waitFor(() => expect(document.body.textContent).toContain("Some task details couldn’t load."));
expect(container.textContent).toContain("Readable task");
failDetails = false;
await act(() => Array.from(document.body.querySelectorAll('button')).find((button) => button.textContent === "Retry details")!.click());
await vi.waitFor(() => expect(document.body.textContent).not.toContain("Some task details couldn’t load."));
expect(container.textContent).toContain("Readable task");
act(() => root.unmount());
});
it("refreshes diagnostics when a visible index row changes without changing its ID", async () => {
routerMock.location.pathname = "/inbox/mine";
const row = createIssue({ title: "Live task", listProjection: "index" });
apiMocks.issuesList.mockResolvedValue([row]);
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false, gcTime: 0 } } });
const root = createRoot(container);
await act(() => root.render(<QueryClientProvider client={queryClient}><Inbox /></QueryClientProvider>));
const detailsReads = () => apiMocks.issuesList.mock.calls.filter(([, filters]) => filters.projection === "rows").length;
await vi.waitFor(() => expect(detailsReads()).toBe(1));
const updated = { ...row, updatedAt: new Date(new Date(row.updatedAt).getTime() + 1000) };
apiMocks.issuesList.mockResolvedValue([updated]);
await act(() => {
queryClient.setQueriesData({ predicate: (query) => query.queryKey.includes("index") }, [updated]);
});
await vi.waitFor(() => expect(detailsReads()).toBe(2));
expect(container.textContent).toContain("Live task");
act(() => root.unmount());
});
it.each(["approvalsList", "joinRequestsList", "heartbeatRunsList"] as const)(
"keeps available tasks visible when %s fails and during retry", async (source) => {
routerMock.location.pathname = "/inbox/mine";
apiMocks.issuesList.mockResolvedValue([createIssue({ title: "Available task" })]);
apiMocks[source].mockRejectedValue(new Error("Optional source unavailable"));
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false, gcTime: 0 } } });
const root = createRoot(container);
await act(() => root.render(<QueryClientProvider client={queryClient}><Inbox /></QueryClientProvider>));
await vi.waitFor(() => expect(container.textContent).toContain("Available task"));
expect(document.body.textContent).toContain("Some inbox items couldn’t load.");
let resolveRetry: (rows: []) => void = () => undefined;
apiMocks[source].mockImplementation(() => new Promise((resolve) => { resolveRetry = resolve; }));
await act(() => Array.from(document.body.querySelectorAll('button')).find((button) => button.textContent === "Retry missing items")!.click());
await vi.waitFor(() => expect(apiMocks[source]).toHaveBeenCalledTimes(2));
expect(container.textContent).toContain("Available task");
expect(container.querySelector('[aria-label="Loading tasks"]')).toBeNull();
await act(() => resolveRetry([]));
await vi.waitFor(() => expect(document.body.textContent).not.toContain("Some inbox items couldn’t load."));
act(() => root.unmount());
},
);
it("shows an error and retry instead of an endless skeleton when a required external status filter fails", async () => {
routerMock.location.pathname = "/inbox/mine";
localStorage.setItem(taskCollectionPreferencesStorageKey({ companyId: "company-1", collectionKey: "inbox" }), JSON.stringify({
version: 1, companyId: "company-1", collectionKey: "inbox", columns: [],
viewState: { issueFilters: { externalObjectStatuses: ["failed"] } },
}));
apiMocks.experimentalSettings.mockResolvedValue({ enableStreamlinedUi: true, enableExternalObjects: true });
apiMocks.issuesList.mockResolvedValue([createIssue()]);
externalObjectMocks.error = new Error("External statuses unavailable");
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false, gcTime: 0 } } });
const root = createRoot(container);
await act(() => root.render(<QueryClientProvider client={queryClient}><Inbox /></QueryClientProvider>));
await vi.waitFor(() => expect(container.textContent).toContain("External statuses unavailable"));
expect(container.querySelector('[aria-label="Loading tasks"]')).toBeNull();
await act(() => Array.from(container.querySelectorAll('button')).find((button) => button.textContent === "Retry")!.click());
expect(externalObjectMocks.retry).toHaveBeenCalledOnce();
act(() => root.unmount());
});
it("requests live descendant summaries for issue rows", async () => {
routerMock.location.pathname = "/inbox/mine";
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false, staleTime: 0, gcTime: 0 } },
});
const root = createRoot(container);
await act(async () => {
root.render(
<QueryClientProvider client={queryClient}>
<Inbox />
</QueryClientProvider>,
);
});
await vi.waitFor(() => expect(apiMocks.issuesList).toHaveBeenCalledTimes(1));
expect(apiMocks.issuesList).toHaveBeenCalledWith("company-1", expect.objectContaining({
touchedByUserId: "me", inboxArchivedByUserId: "me", projection: "index",
includeLiveDescendantSummary: true, limit: 100, sortField: "updated", sortDir: "desc",
}));
act(() => {
root.unmount();
});
});
it("paints row hover via CSS only, without moving React selection state", async () => {
routerMock.location.pathname = "/inbox/mine";
const issueA = createIssue({ id: "issue-a", identifier: "PAP-1001", title: "First inbox row" });
const issueB = createIssue({ id: "issue-b", identifier: "PAP-1002", title: "Second inbox row" });
apiMocks.issuesList.mockResolvedValue([issueA, issueB]);
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false, staleTime: 0, gcTime: 0 } },
});
const root = createRoot(container);
await act(async () => {
root.render(
<QueryClientProvider client={queryClient}>
<Inbox />
</QueryClientProvider>,
);
});
await vi.waitFor(() => {
expect(container.querySelectorAll("[data-inbox-item]").length).toBeGreaterThanOrEqual(2);
});
const rows = container.querySelectorAll("[data-inbox-item]");
// The hover wash lives on the IssueRow root band (the overlay link's
// parent), not the overlay link itself.
const bandOf = (row: Element): HTMLElement | null =>
row.querySelector<HTMLAnchorElement>("a[data-inbox-issue-link]")?.parentElement ?? null;
// Nothing selected before hover — both rows show the hover-accent class.
expect(bandOf(rows[0]!)?.className).toContain("hover:bg-accent/50");
expect(bandOf(rows[1]!)?.className).toContain("hover:bg-accent/50");
// Hovering paints via CSS `:hover` only — it must NOT flip a row into the
// state-selected band (which would swap to hover:bg-transparent). Coupling
// hover to React state was the per-hover re-render storm behind the lag;
// scrubbing the list must not touch selection state. (Keyboard nav that
// continues from the hovered row is exercised in live/e2e verification.)
await act(async () => {
rows[1]!.dispatchEvent(new MouseEvent("mouseover", { bubbles: true }));
rows[1]!.dispatchEvent(new MouseEvent("mouseenter", { bubbles: false }));
});
expect(bandOf(rows[0]!)?.className).toContain("hover:bg-accent/50");
expect(bandOf(rows[1]!)?.className).toContain("hover:bg-accent/50");
expect(bandOf(rows[1]!)?.className).not.toContain("hover:bg-transparent");
act(() => {
root.unmount();
});
});
it("does not indent unread rows: the mark-read dot overlays the shared task-row gutter", async () => {
routerMock.location.pathname = "/inbox/mine";
// Two sibling leaf rows, one unread and one read, so their leading columns
// are directly comparable.
const unread = createIssue({
id: "issue-unread",
identifier: "PAP-2001",
title: "Unread inbox row",
isUnreadForMe: true,
});
const read = createIssue({
id: "issue-read",
identifier: "PAP-2002",
title: "Read inbox row",
isUnreadForMe: false,
});
apiMocks.issuesList.mockResolvedValue([unread, read]);
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false, staleTime: 0, gcTime: 0 } },
});
const root = createRoot(container);
await act(async () => {
root.render(
<QueryClientProvider client={queryClient}>
<Inbox />
</QueryClientProvider>,
);
});
await vi.waitFor(() => {
expect(container.textContent).toContain("Unread inbox row");
expect(container.textContent).toContain("Read inbox row");
});
const rows = Array.from(container.querySelectorAll("[data-inbox-item]"));
const rowFor = (text: string) => rows.find((row) => row.textContent?.includes(text));
const markReadButton = (row: Element) => row.querySelector('button[aria-label="Mark as read"]');
// The canonical spacer reserves the disclosure column on every leaf row.
const hasLeadingSpacer = (row: Element) =>
!!row.querySelector('[data-slot="task-row-disclosure-spacer"]');
// The overlay anchor is present on read AND unread Inbox rows without
// consuming a layout column.
const dotSlot = (row: Element) =>
row.querySelector('[data-testid="issue-row-unread-slot"]');
const unreadRow = rowFor("Unread inbox row")!;
const readRow = rowFor("Read inbox row")!;
// Both rows share the canonical task-row geometry. The dot is absolutely
// positioned in the row gutter, so Inbox does not gain a column that Tasks
// lacks and unread state cannot shift status/title alignment.
const unreadSlot = dotSlot(unreadRow);
const readSlot = dotSlot(readRow);
expect(unreadSlot).not.toBeNull();
expect(readSlot).not.toBeNull();
expect(unreadSlot?.className).toContain("absolute");
// Only the unread row carries the dot button; the read slot is empty.
expect(markReadButton(unreadSlot!)).not.toBeNull();
expect(readSlot?.querySelector('button[aria-label="Mark as read"]')).toBeNull();
expect(hasLeadingSpacer(unreadRow)).toBe(true);
// Read rows keep the same spacer, so both rows line up.
expect(hasLeadingSpacer(readRow)).toBe(true);
act(() => {
root.unmount();
});
});
it("keeps hover→j/k selection in sync after the list reshapes (PAP-9679)", async () => {
routerMock.location.pathname = "/inbox/mine";
const issueA = createIssue({ id: "issue-a", identifier: "PAP-2001", title: "Sync row A" });
const issueB = createIssue({ id: "issue-b", identifier: "PAP-2002", title: "Sync row B" });
const issueC = createIssue({ id: "issue-c", identifier: "PAP-2003", title: "Sync row C" });
apiMocks.issuesList.mockResolvedValue([issueA, issueB, issueC]);
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false, staleTime: 0, gcTime: 0 } },
});
const root = createRoot(container);
// Canonical task rows put the selected wash on the root band (the overlay
// link's parent); find the row with the standalone selected utility.
const bandOf = (row: Element): HTMLElement | null =>
row.querySelector<HTMLAnchorElement>("a[data-inbox-issue-link]")?.parentElement ?? null;
const selectedRowIndex = () =>
[...container.querySelectorAll("[data-inbox-item]")].findIndex((row) =>
bandOf(row)?.className.split(/\s+/).includes("bg-accent/50"),
);
try {
await act(async () => {
root.render(
<QueryClientProvider client={queryClient}>
<Inbox />
</QueryClientProvider>,
);
});
await vi.waitFor(() => {
expect(container.querySelectorAll("[data-inbox-item]").length).toBeGreaterThanOrEqual(3);
});
// Pointer physically moves, then hovers the middle row (index 1).
await act(async () => {
window.dispatchEvent(new MouseEvent("mousemove", { bubbles: true }));
const rows = container.querySelectorAll("[data-inbox-item]");
rows[1]!.dispatchEvent(new MouseEvent("mouseover", { bubbles: true }));
rows[1]!.dispatchEvent(new MouseEvent("mouseenter", { bubbles: false }));
});
// A poll reshapes the list (row B's title changes → new nav array) before
// the keypress. This is what used to null the hovered index and strand
// j/k back at the top.
apiMocks.issuesList.mockResolvedValue([issueA, { ...issueB, title: "Sync row B (updated)" }, issueC]);
await act(async () => {
await queryClient.invalidateQueries();
});
await vi.waitFor(() => {
expect(container.textContent).toContain("Sync row B (updated)");
});
// j must continue from the hovered row (index 1) → index 2, not jump to
// the top of the list.
await act(async () => {
document.body.dispatchEvent(new KeyboardEvent("keydown", { key: "j", bubbles: true }));
});
expect(selectedRowIndex()).toBe(2);
} finally {
act(() => {
root.unmount();
});
}
});
it("holds the inbox order across a reordering poll, then re-sorts at an attention boundary (PAP-16015)", async () => {
routerMock.location.pathname = "/inbox/mine";
const base = new Date("2026-03-11T00:00:00.000Z").getTime();
const issueA = createIssue({
id: "issue-a",
identifier: "PAP-3001",
title: "Pin row A",
lastActivityAt: new Date(base + 3000),
});
const issueB = createIssue({
id: "issue-b",
identifier: "PAP-3002",
title: "Pin row B",
lastActivityAt: new Date(base + 2000),
});
const issueC = createIssue({
id: "issue-c",
identifier: "PAP-3003",
title: "Pin row C",
lastActivityAt: new Date(base + 1000),
});
apiMocks.issuesList.mockResolvedValue([issueA, issueB, issueC]);
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false, staleTime: 0, gcTime: 0 } },
});
const root = createRoot(container);
// Collapse each displayed row to its A/B/C identity so we can assert order.
const orderOf = () =>
[...container.querySelectorAll("[data-inbox-item]")].flatMap((row) => {
const text = row.textContent ?? "";
if (text.includes("Pin row A")) return ["A"];
if (text.includes("Pin row B")) return ["B"];
if (text.includes("Pin row C")) return ["C"];
return [];
});
const visibilityDescriptor = Object.getOwnPropertyDescriptor(document, "visibilityState");
const setVisibility = (state: DocumentVisibilityState) => {
Object.defineProperty(document, "visibilityState", { configurable: true, get: () => state });
};
let nowValue = base + 1_000_000;
const nowSpy = vi.spyOn(Date, "now").mockImplementation(() => nowValue);
try {
await act(async () => {
root.render(
<QueryClientProvider client={queryClient}>
<Inbox />
</QueryClientProvider>,
);
});
await vi.waitFor(() => {
expect(container.querySelectorAll("[data-inbox-item]").length).toBeGreaterThanOrEqual(3);
});
expect(orderOf()).toEqual(["A", "B", "C"]);
// A poll makes row C the most-recently-active: the fresh sort is now [C, A, B].
apiMocks.issuesList.mockResolvedValue([
{ ...issueA },
{ ...issueB },
{ ...issueC, lastActivityAt: new Date(base + 9000) },
]);
await act(async () => {
await queryClient.invalidateQueries();
});
await vi.waitFor(() => {
expect(container.textContent).toContain("Pin row C");
});
// No attention boundary has fired, so the displayed order is held, not reshuffled.
expect(orderOf()).toEqual(["A", "B", "C"]);
// The tab is hidden long enough to lose attention, then regains focus: that
// visibility boundary is a commit point, so the inbox adopts the fresh order.
await act(async () => {
setVisibility("hidden");
document.dispatchEvent(new Event("visibilitychange"));
});
nowValue += 31_000;
await act(async () => {
setVisibility("visible");
document.dispatchEvent(new Event("visibilitychange"));
});
await vi.waitFor(() => {
expect(orderOf()).toEqual(["C", "A", "B"]);
});
} finally {
nowSpy.mockRestore();
if (visibilityDescriptor) {
Object.defineProperty(document, "visibilityState", visibilityDescriptor);
} else {
setVisibility("visible");
}
act(() => {
root.unmount();
});
}
});
it("keeps other issue archive controls enabled while one archive is pending", async () => {
routerMock.location.pathname = "/inbox/mine";
const issueA = createIssue({ id: "issue-a", identifier: "PAP-1001", title: "First inbox row" });
const issueB = createIssue({ id: "issue-b", identifier: "PAP-1002", title: "Second inbox row" });
apiMocks.issuesList.mockResolvedValue([issueA, issueB]);
const archiveA = createDeferred<{ id: string; archivedAt: Date }>();
apiMocks.archiveFromInbox.mockImplementation((id: string) =>
id === "issue-a" ? archiveA.promise : Promise.resolve({ id, archivedAt: new Date() }),
);
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false, staleTime: 0, gcTime: 0 } },
});
const root = createRoot(container);
await act(async () => {
root.render(
<QueryClientProvider client={queryClient}>
<Inbox />
</QueryClientProvider>,
);
});
await vi.waitFor(() => {
expect(container.textContent).toContain("First inbox row");
expect(container.textContent).toContain("Second inbox row");
});
const initialArchiveButtons = Array.from(
container.querySelectorAll<HTMLButtonElement>('button[aria-label="Archive"]'),
);
expect(initialArchiveButtons.length).toBeGreaterThanOrEqual(2);
await act(async () => {
initialArchiveButtons[0]!.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true }));
});
await vi.waitFor(() => {
expect(apiMocks.archiveFromInbox).toHaveBeenCalledWith("issue-a");
expect(container.textContent).not.toContain("First inbox row");
expect(container.textContent).toContain("Second inbox row");
});
const remainingArchiveButton = container.querySelector<HTMLButtonElement>(
'button[aria-label="Archive"]',
);
expect(remainingArchiveButton).not.toBeNull();
expect(remainingArchiveButton?.disabled).toBe(false);
await act(async () => {
remainingArchiveButton!.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true }));
});
await vi.waitFor(() => {
expect(apiMocks.archiveFromInbox).toHaveBeenCalledWith("issue-b");
});
await act(async () => {
archiveA.resolve({ id: "issue-a", archivedAt: new Date() });
});
act(() => {
root.unmount();
});
});
it("keeps a successful archive hidden when stale query data arrives", async () => {
routerMock.location.pathname = "/inbox/mine";
const archivedIssue = createIssue({
id: "issue-a",
identifier: "PAP-1001",
title: "Archived inbox row",
});
apiMocks.issuesList.mockResolvedValue([archivedIssue]);
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false, staleTime: 0, gcTime: 0 } },
});
const root = createRoot(container);
await act(async () => {
root.render(
<QueryClientProvider client={queryClient}>
<Inbox />
</QueryClientProvider>,
);
});
await vi.waitFor(() => {
expect(container.textContent).toContain("Archived inbox row");
});
const archiveButton = container.querySelector<HTMLButtonElement>('button[aria-label="Archive"]');
expect(archiveButton).not.toBeNull();
await act(async () => {
archiveButton!.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true }));
});
await vi.waitFor(() => {
expect(apiMocks.archiveFromInbox).toHaveBeenCalledWith("issue-a");
expect(container.textContent).not.toContain("Archived inbox row");
});
await act(async () => {
queryClient.setQueriesData<Issue[]>(
{ queryKey: ["issues", "company-1", "mine-by-me"] },
[archivedIssue],
);
});
expect(container.textContent).not.toContain("Archived inbox row");
act(() => {
root.unmount();
});
});
it("restores a locally hidden archive when undo is pressed", async () => {
routerMock.location.pathname = "/inbox/mine";
const archivedIssue = createIssue({
id: "issue-a",
identifier: "PAP-1001",
title: "Undoable inbox row",
});
apiMocks.issuesList.mockResolvedValue([archivedIssue]);
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false, staleTime: 0, gcTime: 0 } },
});
const root = createRoot(container);
try {
await act(async () => {
root.render(
<QueryClientProvider client={queryClient}>
<Inbox />
</QueryClientProvider>,
);
});
await vi.waitFor(() => {
expect(container.textContent).toContain("Undoable inbox row");
});
const archiveButton = container.querySelector<HTMLButtonElement>('button[aria-label="Archive"]');
expect(archiveButton).not.toBeNull();
await act(async () => {
archiveButton!.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true }));
});
await vi.waitFor(() => {
expect(container.textContent).not.toContain("Undoable inbox row");
expect(queryClient.isMutating()).toBe(0);
});
await act(async () => {
document.body.dispatchEvent(new KeyboardEvent("keydown", { key: "u", bubbles: true }));
});
await vi.waitFor(() => {
expect(apiMocks.unarchiveFromInbox).toHaveBeenCalledWith("issue-a");
expect(container.textContent).toContain("Undoable inbox row");
});
} finally {
act(() => root.unmount());
}
});
});
describe("FailedRunInboxRow", () => {
let container: HTMLDivElement;
beforeEach(() => {
container = document.createElement("div");
document.body.appendChild(container);
});
afterEach(() => {
container.remove();
});
it("uses task presentation instead of infrastructure errors and keeps Retry in the task", () => {
const root = createRoot(container);
const issue = createIssue({ interruption: { intentId: "saved-input", responsibility: "system", state: "failed",
nextRetryAt: null, canRetry: true, userAction: null, sourceRunIds: ["run-1"], observedRunId: "run-1" } });
act(() => root.render(<FailedRunInboxRow run={createFailedRun({ issueId: issue.id, error: "Internal process identity missing" })}
issueById={new Map([[issue.id, issue]])} agentName="Worker" issueLinkState={null}
onDismiss={() => {}} onRetry={() => {}} isRetrying={false} />));
expect(container.textContent).toContain("Couldn’t finish this attempt. Your message is saved.");
expect(container.textContent).not.toContain("Internal process identity missing");
expect(container.querySelector("a")?.getAttribute("href")).toBe(`/issues/${issue.id}`);
expect(container.textContent).toContain("Retry");
act(() => root.unmount());
});
it("suppresses accent hover styling when selected", () => {
const root = createRoot(container);
const run = createFailedRun();
act(() => {
root.render(
<FailedRunInboxRow
run={run}
issueById={new Map()}
agentName="Agent"
issueLinkState={null}
onDismiss={() => {}}
onRetry={() => {}}
isRetrying={false}
selected
/>,
);
});
const link = container.querySelector("a");
expect(link).not.toBeNull();
expect(link?.className).toContain("hover:bg-transparent");
expect(link?.className).not.toContain("hover:bg-accent/50");
act(() => {
root.unmount();
});
});
});
describe("InboxIssueMetaLeading", () => {
let container: HTMLDivElement;
beforeEach(() => {
container = document.createElement("div");
document.body.appendChild(container);
});
afterEach(() => {
container.remove();
});
it("keeps status and live accents visible", () => {
const root = createRoot(container);
act(() => {
root.render(<InboxIssueMetaLeading issue={createIssue()} isLive />);
});
// The status glyph is an <svg> coloured from its --status-task-icon-* var.
const statusIcon = Array.from(container.querySelectorAll("svg")).find((svg) =>
(svg.getAttribute("style") ?? "").includes("--status-task-icon"),
);
const liveBadge = container.querySelector('span[class*="px-1.5"][class*="bg-blue-500/10"]');
const liveBadgeLabel = Array.from(container.querySelectorAll("span")).find(
// The pill chassis is a Badge (itself a span with textContent "Live");
// the label is the inner span without the rounded-full chassis class.
(node) => node.textContent === "Live" && node.className.includes("text-") && !node.className.includes("rounded-full"),
);
const liveDot = container.querySelector('span[class*="bg-blue-500"]');
const pulseRing = container.querySelector('span[class*="animate-pulse"]');
expect(statusIcon).not.toBeUndefined();
// Status accent stays visible — not neutralized to muted.
expect(statusIcon?.getAttribute("class") ?? "").not.toContain("!text-muted-foreground");
expect(liveBadge).not.toBeNull();
expect(liveBadge?.className).toContain("bg-blue-500/10");
expect(liveBadgeLabel).not.toBeNull();
expect(liveBadgeLabel?.className).toContain("text-blue-600");
expect(liveDot).not.toBeNull();
expect(pulseRing).not.toBeNull();
act(() => {
root.unmount();
});
});
});
describe("InboxIssueTrailingColumns", () => {
let container: HTMLDivElement;
beforeEach(() => {
container = document.createElement("div");
document.body.appendChild(container);
});
afterEach(() => {
container.remove();
});
it("renders an empty tags cell when an issue has no labels", () => {
const root = createRoot(container);
act(() => {
root.render(
<InboxIssueTrailingColumns
issue={createIssue({ labels: [], labelIds: [] })}
columns={["labels"]}
projectName={null}
projectColor={null}
workspaceName={null}
assigneeName={null}
currentUserId={null}
parentIdentifier={null}
parentTitle={null}
/>,
);
});
expect(container.textContent).toBe("");
act(() => {
root.unmount();
});
});
it("leaves the workspace cell blank when no explicit workspace label should be shown", () => {
const root = createRoot(container);
act(() => {
root.render(
<InboxIssueTrailingColumns
issue={createIssue()}
columns={["workspace"]}
projectName={null}
projectColor={null}
workspaceName={null}
assigneeName={null}
currentUserId={null}
parentIdentifier={null}
parentTitle={null}
/>,
);
});
expect(container.textContent).toBe("");
act(() => {
root.unmount();
});
});
});
describe("formatJoinRequestInboxLabel", () => {
it("shows the human requester's name and email when available", () => {
expect(formatJoinRequestInboxLabel(createJoinRequest())).toBe(
"Jordan Example (joiner@example.com)",
);
});
it("falls back to the email snapshot when the requester profile is missing", () => {
expect(
formatJoinRequestInboxLabel(
createJoinRequest({
requesterUser: null,
requestEmailSnapshot: "snapshot@example.com",
requestingUserId: null,
}),
),
).toBe("snapshot@example.com");
});
});
describe("InboxGroupHeader", () => {
let container: HTMLDivElement;
beforeEach(() => {
container = document.createElement("div");
document.body.appendChild(container);
});
afterEach(() => {
container.remove();
});
it("shows a left caret and expanded state for collapsible mobile headers", () => {
const root = createRoot(container);
act(() => {
root.render(<InboxGroupHeader label="Primary workspace (default)" collapsible collapsed={false} />);
});
const button = container.querySelector("button");
expect(button).not.toBeNull();
expect(button?.getAttribute("aria-expanded")).toBe("true");
expect(button?.textContent).toContain("Primary workspace (default)");
const caret = container.querySelector("svg");
expect(caret?.className.baseVal).toContain("rotate-90");
act(() => {
root.unmount();
});
});
it("keeps the caret collapsed when the mobile group is hidden", () => {
const root = createRoot(container);
act(() => {
root.render(<InboxGroupHeader label="Feature Branch" collapsible collapsed />);
});
const button = container.querySelector("button");
expect(button?.getAttribute("aria-expanded")).toBe("false");
const caret = container.querySelector("svg");
expect(caret?.className.baseVal).not.toContain("rotate-90");
act(() => {
root.unmount();
});
});
});
describe("Coordinator Inbox collection reads", () => {
it.each([true, false])("keeps optimized reads with Coordinator and Streamlined=%s", async (streamlined) => {
resetInboxApiMocks(); localStorage.clear();
routerMock.location.pathname = "/inbox/mine";
const settings = { enableIsolatedWorkspaces: false, enableStreamlinedUi: streamlined };
apiMocks.experimentalSettings.mockResolvedValue(settings);
const client = new QueryClient({ defaultOptions: { queries: { retry: false, staleTime: Infinity, gcTime: 0 } } });
client.setQueryData(queryKeys.instance.experimentalSettings, settings);
const container = document.createElement("div"); document.body.appendChild(container);
const root = createRoot(container);
try {
await act(async () => root.render(<QueryClientProvider client={client}><CoordinatorPresentation.Provider value={true}><Inbox /></CoordinatorPresentation.Provider></QueryClientProvider>));
await vi.waitFor(() => expect(apiMocks.heartbeatRunsList).toHaveBeenCalled());
await act(async () => { await new Promise(resolve => setTimeout(resolve, 30)); });
const sources = apiMocks.issuesList.mock.calls.filter(call => call[1]?.projection !== "rows").map(call => call[1]);
expect(sources).toHaveLength(1);
expect(sources.every(source => source.projection === "index")).toBe(true);
expect(apiMocks.dashboardSummary).toHaveBeenCalledTimes(0);
expect(apiMocks.heartbeatRunsList.mock.calls[0]?.[3]?.latestFailuresOnly === true).toBe(true);
} finally { await act(async () => root.unmount()); client.clear(); container.remove(); }
});
});