mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-11 05:11:49 +01:00
test(restart-chaos): wait for any visible Retry, not the first match (#20616)
## 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 <noreply@anthropic.com>
This commit is contained in:
1 parent
28063474d2
commit
90dd864bff
2 files changed
+11
-1
No files matched your search
@@ -11,6 +11,8 @@ const pages: Record<string, string> = {
|
|||||||
"/issues/CHA-4": `<section><p>Couldn’t load the subtasks.</p><button onclick="window.reloaded=true">Retry</button></section>
|
"/issues/CHA-4": `<section><p>Couldn’t load the subtasks.</p><button onclick="window.reloaded=true">Retry</button></section>
|
||||||
<div role="status">Couldn’t finish this attempt. <button onclick="${retry("i4")}">Retry</button></div>`,
|
<div role="status">Couldn’t finish this attempt. <button onclick="${retry("i4")}">Retry</button></div>`,
|
||||||
"/issues/CHA-5": `<div role="status">Couldn’t finish this attempt. <button onclick="${retry("other")}">Retry</button></div>`,
|
"/issues/CHA-5": `<div role="status">Couldn’t finish this attempt. <button onclick="${retry("other")}">Retry</button></div>`,
|
||||||
|
"/issues/CHA-6": `<button style="width:0;height:0;padding:0;border:0;overflow:hidden">Retry</button>
|
||||||
|
<div role="status">Couldn’t finish this attempt. Your message is saved. <button onclick="${retry("i6")}">Retry</button></div>`,
|
||||||
};
|
};
|
||||||
let server: Server;
|
let server: Server;
|
||||||
let baseUrl: string;
|
let baseUrl: string;
|
||||||
@@ -65,6 +67,13 @@ describe("BoardUi.clickRetry", () => {
|
|||||||
expect(posts).toContain("/api/issues/i4/retry");
|
expect(posts).toContain("/api/issues/i4/retry");
|
||||||
}, 60_000);
|
}, 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 () => {
|
it("does not count a retry request for a different issue", async () => {
|
||||||
await withBoard(async board => {
|
await withBoard(async board => {
|
||||||
expect(await board.clickRetry("CHA-5", "i5", 1_500, 500)).toMatchObject({ clicked: true, error: UI_RETRY_NO_REQUEST });
|
expect(await board.clickRetry("CHA-5", "i5", 1_500, 500)).toMatchObject({ clicked: true, error: UI_RETRY_NO_REQUEST });
|
||||||
|
|||||||
@@ -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;
|
return texts.map(text => text.trim()).filter(Boolean).join(" | ").slice(0, 500) || null;
|
||||||
};
|
};
|
||||||
try {
|
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 {
|
} catch {
|
||||||
return { clicked: false, httpStatus: null, accepted: null, error: UI_RETRY_BUTTON_MISSING, bannerText: await banner() };
|
return { clicked: false, httpStatus: null, accepted: null, error: UI_RETRY_BUTTON_MISSING, bannerText: await banner() };
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user