From 90dd864bfff1f069c68c7528ae6a6a16ad715b3d Mon Sep 17 00:00:00 2001 From: Dotta <34892728+cryppadotta@users.noreply.github.com> Date: Tue, 10 Nov 2026 13:40:31 -0600 Subject: [PATCH] test(restart-chaos): wait for any visible Retry, not the first match (#20616) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work > - The restart chaos harness checks that a task never needs a human Retry after a control-plane restart, and that every Retry the board shows works > - When a task rests with a Retry offer, the harness opens the task page and clicks the Retry button like a user > - The harness waited for the first `Retry` button in document order to become visible > - A page can hold an earlier `Retry` button that is in the accessibility tree but has no box. The harness then waited on that button for the full timeout and reported `ui_retry_button_missing`, although the user saw a Retry in the status banner > - This pull request makes the harness wait for any visible Retry button, banner first > - The benefit is that a chaos grade reports a missing Retry only when the user cannot see one ## Linked Issues or Issue Description **What happened?** A Daytona chaos wave graded one task `ui_retry_button_missing`. The captured status banner text was "Couldn't finish this attempt. Your message is saved. Retry", so the page showed a Retry button. The harness waited on `getByRole("button", { name: "Retry", exact: true }).first()`. That locator selects the first match in document order. When that match is not visible, the wait times out even when another Retry is visible. The click loop after the wait already used only visible buttons, but the wait itself did not. **Expected behavior** The harness reports `ui_retry_button_missing` only when no visible Retry button exists before the timeout. When a visible Retry exists, the harness clicks it, banner first. **Steps to reproduce** 1. Serve a page with a zero-size `Retry` button before a visible status banner that holds a second `Retry` button. 2. Call `BoardUi.clickRetry` for that page. 3. Before this change, the result is `ui_retry_button_missing`. After it, the harness clicks the banner Retry and sees the retry request. Separate finding, not changed here: in that wave the Retry was also a dead end on the server side. The run had failed with `native_session_cleanup_quarantined`. The server build that ran the wave still projected `canRetry: true` for that run, and the retry route refuses that code with 409. Current master already removes that Retry offer (#20572). ## What Changed - `tests/restart-chaos/board-ui.ts`: wait on the `Retry` role locator filtered to visible elements (`filter({ visible: true })`), instead of the first match in document order. After the wait, re-query the visible buttons and click them banner first, as before. - `tests/restart-chaos/board-ui.test.ts`: add a page with an earlier zero-size `Retry` and a visible banner `Retry`. The test fails before the change (`ui_retry_button_missing`) and passes after it. ## Verification - `pnpm exec vitest run --config tests/restart-chaos/vitest.config.ts tests/restart-chaos/board-ui.test.ts tests/restart-chaos/triggers.test.ts`: 43 passed. With the old `board-ui.ts`, the new test fails with `ui_retry_button_missing`. - `pnpm exec tsc --noEmit -p tests/restart-chaos`: passes. ## Risks - Low risk. The change is test-harness code only. No product code changes. - The timeout and the "missing" result do not change. The locator `visible` filter needs Playwright 1.51 or later; the repo uses 1.62. > 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 - Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, used through Claude Code with tool use and code 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 - [ ] All Paperclip CI gates are green - [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude --- tests/restart-chaos/board-ui.test.ts | 9 +++++++++ tests/restart-chaos/board-ui.ts | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/tests/restart-chaos/board-ui.test.ts b/tests/restart-chaos/board-ui.test.ts index 6cfd43267e1..6d0f994c7e8 100644 --- a/tests/restart-chaos/board-ui.test.ts +++ b/tests/restart-chaos/board-ui.test.ts @@ -11,6 +11,8 @@ const pages: Record = { "/issues/CHA-4": `

Couldn’t load the subtasks.

Couldn’t finish this attempt.
`, "/issues/CHA-5": `
Couldn’t finish this attempt.
`, + "/issues/CHA-6": ` +
Couldn’t finish this attempt. Your message is saved.
`, }; let server: Server; let baseUrl: string; @@ -65,6 +67,13 @@ describe("BoardUi.clickRetry", () => { expect(posts).toContain("/api/issues/i4/retry"); }, 60_000); + it("finds the visible banner Retry when an earlier Retry match has no box", async () => { + await withBoard(async board => { + expect(await board.clickRetry("CHA-6", "i6", 3_000, 500)).toMatchObject({ clicked: true, httpStatus: 202, accepted: true, error: null }); + }); + expect(posts).toContain("/api/issues/i6/retry"); + }, 60_000); + it("does not count a retry request for a different issue", async () => { await withBoard(async board => { expect(await board.clickRetry("CHA-5", "i5", 1_500, 500)).toMatchObject({ clicked: true, error: UI_RETRY_NO_REQUEST }); diff --git a/tests/restart-chaos/board-ui.ts b/tests/restart-chaos/board-ui.ts index 029f2cba161..96343025eb7 100644 --- a/tests/restart-chaos/board-ui.ts +++ b/tests/restart-chaos/board-ui.ts @@ -37,7 +37,8 @@ export async function clickRetryOnPage(page: Page, url: string, issueRefs: reado return texts.map(text => text.trim()).filter(Boolean).join(" | ").slice(0, 500) || null; }; try { - await page.getByRole("button", { name: "Retry", exact: true }).first().waitFor({ state: "visible", timeout: timeoutMs }); + await page.getByRole("button", { name: "Retry", exact: true }).filter({ visible: true }).first() + .waitFor({ state: "visible", timeout: timeoutMs }); } catch { return { clicked: false, httpStatus: null, accepted: null, error: UI_RETRY_BUTTON_MISSING, bannerText: await banner() }; }