Merge branch 'codex/boat-environment-core' into codex/boat-environment

* codex/boat-environment-core:
  test: synchronize signoff retries and touch geometry
This commit is contained in:
Dotta committed 2026-10-11 03:43:07 -05:00
commit 06906ab358
2 files changed
+37 -3

No files matched your search

+33 -3
View File
@@ -21,13 +21,23 @@ async function swipeToLastOption(page: Page, picker: Locator, list = picker.getB
}).toBeLessThanOrEqual(page.viewportSize()!.height);
expect((await picker.boundingBox())!.y).toBeGreaterThanOrEqual(0);
const session = await page.context().newCDPSession(page);
const targetHandle = await last.elementHandle();
expect(targetHandle).not.toBeNull();
try {
expect(await list.evaluate((element) => element.scrollHeight > element.clientHeight)).toBe(true);
for (let attempt = 0; attempt < 20; attempt += 1) {
const bounds = (await list.boundingBox())!;
const target = (await last.boundingBox())!;
// Momentum can finish between protocol round trips. Observe geometry and
// scrollTop together so a stale target cannot trigger a swipe at the end.
const { bounds, target, before } = await list.evaluate((element, option) => {
const container = element.getBoundingClientRect();
const item = option!.getBoundingClientRect();
return {
bounds: { x: container.x, y: container.y, width: container.width, height: container.height },
target: { y: item.y, height: item.height },
before: element.scrollTop,
};
}, targetHandle);
if (target.y >= bounds.y && target.y + target.height <= bounds.y + bounds.height) break;
const before = await list.evaluate((element) => element.scrollTop);
const x = bounds.x + bounds.width / 2;
const y = bounds.y + bounds.height - 20;
const distance = Math.min(180, bounds.height - 40);
@@ -49,6 +59,7 @@ async function swipeToLastOption(page: Page, picker: Locator, list = picker.getB
expect(target.y).toBeGreaterThanOrEqual(bounds.y);
expect(target.y + target.height).toBeLessThanOrEqual(bounds.y + bounds.height + 1);
} finally {
await targetHandle?.dispose();
await session.detach();
}
return last;
@@ -56,6 +67,25 @@ async function swipeToLastOption(page: Page, picker: Locator, list = picker.getB
test.use({ viewport: { width: 390, height: 844 }, hasTouch: true, isMobile: true });
test("touch scroll observation stays valid when momentum finishes between browser reads", async ({ page, browserName }) => {
test.skip(browserName !== "chromium", "Native touch drags use Chromium's input protocol.");
await page.setContent(`<div role="dialog" style="position:absolute;top:20px;left:20px;width:300px">
<div role="listbox" style="height:150px;overflow-y:auto;touch-action:pan-y">
${Array.from({ length: 12 }, (_, index) => `<div role="option" style="height:40px">Option ${index}</div>`).join("")}
</div></div>`);
const picker = page.getByRole("dialog"), list = picker.getByRole("listbox"), last = list.getByRole("option").last();
const boundingBox = last.boundingBox.bind(last);
// Reproduce the observed compositor interleaving: a returned target rectangle
// is stale by the next scrollTop read because inertia reached the list end.
last.boundingBox = async (options) => {
const beforeMomentum = await boundingBox(options);
await list.evaluate((element) => { element.scrollTop = element.scrollHeight - element.clientHeight; });
return beforeMomentum;
};
await swipeToLastOption(page, picker, list, last);
await expect(picker).toBeVisible();
});
test("new-task assignee, model, and project sheets scroll by touch and retain the selected values", async ({ page, request, browserName }, testInfo) => {
test.skip(browserName !== "chromium", "Native touch drags use Chromium's input protocol.");
const company = await json(await request.post("/api/companies", {
+4
View File
@@ -309,6 +309,9 @@ async function retryAgentPatchWithCurrentLockOnConflict(
if (issueRunLock.assigneeAgentId !== agent.agentId) return res;
const lockedRunId = issueRunLock.checkoutRunId ?? issueRunLock.executionRunId ?? fallbackRunId;
// A new lock can belong to a newly promoted fixture run, not the completed
// run we first invoked. Let that exact verified run settle before PATCH.
await waitForFixtureRun(board, agent.agentId, issueId, lockedRunId);
res = await agent.request.patch(`${BASE_URL}/api/issues/${issueId}`, {
headers: { "X-Paperclip-Run-Id": lockedRunId },
data: patchData,
@@ -365,6 +368,7 @@ async function agentPatch(
// run-lock race — leave it for the caller to assert on.
if (issueRunLock.assigneeAgentId !== agent.agentId) break;
const retryRunId = issueRunLock.checkoutRunId ?? issueRunLock.executionRunId ?? runId;
await waitForFixtureRun(board, agent.agentId, issueId, retryRunId);
res = await patchWith(retryRunId);
}
return res;