fix(browser): wait for extension tabs after relay drop (#32331)#32461
fix(browser): wait for extension tabs after relay drop (#32331)#32461steipete merged 2 commits intoopenclaw:mainfrom
Conversation
Greptile SummaryThis PR adds a short polling retry (up to 3 s, 200 ms cadence) inside Key points:
Confidence Score: 4/5
Last reviewed commit: 4fc6f98 |
| while (tabs1.length === 0 && Date.now() < deadlineAt) { | ||
| await new Promise((resolve) => setTimeout(resolve, 200)); | ||
| tabs1 = await listTabs(); | ||
| } |
There was a problem hiding this comment.
Deadline check precedes sleep — loop can overrun by one interval
The while condition evaluates Date.now() < deadlineAt before the 200 ms setTimeout and the subsequent listTabs() call. This means the last iteration can start with only 1 ms remaining on the deadline, sleep 200 ms, call listTabs(), and resolve up to ~200 ms + network time past the 3 s deadline before the throw is reached. In most cases the overrun is harmless, but it slightly weakens the "fail within 3 s" guarantee.
A common fix is to also check the deadline after waking, or to measure elapsed time with an AbortSignal/clearTimeout pattern. As a minimal fix, moving the deadline check to after the sleep would ensure the timeout is observed more faithfully:
| while (tabs1.length === 0 && Date.now() < deadlineAt) { | |
| await new Promise((resolve) => setTimeout(resolve, 200)); | |
| tabs1 = await listTabs(); | |
| } | |
| while (tabs1.length === 0) { | |
| await new Promise((resolve) => setTimeout(resolve, 200)); | |
| if (Date.now() >= deadlineAt) break; | |
| tabs1 = await listTabs(); | |
| } |
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/browser/server-context.selection.ts
Line: 41-44
Comment:
**Deadline check precedes sleep — loop can overrun by one interval**
The `while` condition evaluates `Date.now() < deadlineAt` *before* the 200 ms `setTimeout` and the subsequent `listTabs()` call. This means the last iteration can start with only 1 ms remaining on the deadline, sleep 200 ms, call `listTabs()`, and resolve up to ~200 ms + network time *past* the 3 s deadline before the throw is reached. In most cases the overrun is harmless, but it slightly weakens the "fail within 3 s" guarantee.
A common fix is to also check the deadline after waking, or to measure elapsed time with an `AbortSignal`/`clearTimeout` pattern. As a minimal fix, moving the deadline check to after the sleep would ensure the timeout is observed more faithfully:
```suggestion
while (tabs1.length === 0) {
await new Promise((resolve) => setTimeout(resolve, 200));
if (Date.now() >= deadlineAt) break;
tabs1 = await listTabs();
}
```
How can I resolve this? If you propose a fix, please make it concise.| it("waits briefly for extension tabs to reappear when a previous target exists", async () => { | ||
| vi.useFakeTimers(); | ||
| try { | ||
| const responses = [ | ||
| // First call: select tab A and store lastTargetId. | ||
| [{ id: "A", type: "page", url: "https://a.example", webSocketDebuggerUrl: "ws://x/a" }], | ||
| [{ id: "A", type: "page", url: "https://a.example", webSocketDebuggerUrl: "ws://x/a" }], | ||
| // Second call: transient drop, then the extension re-announces attached tab A. | ||
| [], | ||
| [{ id: "A", type: "page", url: "https://a.example", webSocketDebuggerUrl: "ws://x/a" }], | ||
| [{ id: "A", type: "page", url: "https://a.example", webSocketDebuggerUrl: "ws://x/a" }], | ||
| ]; | ||
| stubChromeJsonList(responses); | ||
| const state = makeBrowserState(); | ||
|
|
||
| const ctx = createBrowserRouteContext({ getState: () => state }); | ||
| const chrome = ctx.forProfile("chrome"); | ||
| const first = await chrome.ensureTabAvailable(); | ||
| expect(first.targetId).toBe("A"); | ||
|
|
||
| const secondPromise = chrome.ensureTabAvailable(); | ||
| await vi.advanceTimersByTimeAsync(250); | ||
| const second = await secondPromise; | ||
| expect(second.targetId).toBe("A"); | ||
| } finally { | ||
| vi.useRealTimers(); | ||
| } | ||
| }); |
There was a problem hiding this comment.
Deadline-exceeded path is not tested
The new test only covers the happy path (tabs reappear within one polling interval). The complementary case — lastTargetId is set, /json/list stays empty throughout the 3 s window, and ensureTabAvailable must still throw with the "no attached Chrome tabs" message — has no automated test.
This is the code path the PR description says "was not verified end-to-end," and it's also the path that ensures the retry doesn't silently swallow the error on a genuinely broken relay. A test like the one below would complete near-instantly with fake timers:
it("throws after deadline when extension tabs never reappear (lastTargetId set)", async () => {
vi.useFakeTimers();
try {
// Provide enough empty responses to cover the retry window plus one first call
const responses = [
[{ id: "A", type: "page", url: "https://a.example", webSocketDebuggerUrl: "ws://x/a" }],
[{ id: "A", type: "page", url: "https://a.example", webSocketDebuggerUrl: "ws://x/a" }],
// All subsequent polls return empty
...Array(20).fill([]),
];
stubChromeJsonList(responses);
const state = makeBrowserState();
const ctx = createBrowserRouteContext({ getState: () => state });
const chrome = ctx.forProfile("chrome");
await chrome.ensureTabAvailable(); // sets lastTargetId = "A"
const failPromise = chrome.ensureTabAvailable();
await vi.advanceTimersByTimeAsync(4_000); // past the 3 s deadline
await expect(failPromise).rejects.toThrow(/no attached Chrome tabs/i);
} finally {
vi.useRealTimers();
}
});Prompt To Fix With AI
This is a comment left during a code review.
Path: src/browser/server-context.ensure-tab-available.prefers-last-target.test.ts
Line: 126-153
Comment:
**Deadline-exceeded path is not tested**
The new test only covers the happy path (tabs reappear within one polling interval). The complementary case — `lastTargetId` is set, `/json/list` stays empty throughout the 3 s window, and `ensureTabAvailable` must still throw with the "no attached Chrome tabs" message — has no automated test.
This is the code path the PR description says "was not verified end-to-end," and it's also the path that ensures the retry doesn't silently swallow the error on a genuinely broken relay. A test like the one below would complete near-instantly with fake timers:
```ts
it("throws after deadline when extension tabs never reappear (lastTargetId set)", async () => {
vi.useFakeTimers();
try {
// Provide enough empty responses to cover the retry window plus one first call
const responses = [
[{ id: "A", type: "page", url: "https://a.example", webSocketDebuggerUrl: "ws://x/a" }],
[{ id: "A", type: "page", url: "https://a.example", webSocketDebuggerUrl: "ws://x/a" }],
// All subsequent polls return empty
...Array(20).fill([]),
];
stubChromeJsonList(responses);
const state = makeBrowserState();
const ctx = createBrowserRouteContext({ getState: () => state });
const chrome = ctx.forProfile("chrome");
await chrome.ensureTabAvailable(); // sets lastTargetId = "A"
const failPromise = chrome.ensureTabAvailable();
await vi.advanceTimersByTimeAsync(4_000); // past the 3 s deadline
await expect(failPromise).rejects.toThrow(/no attached Chrome tabs/i);
} finally {
vi.useRealTimers();
}
});
```
How can I resolve this? If you propose a fix, please make it concise.4fc6f98 to
79c96b0
Compare
|
Landed via temp rebase onto main. Thanks @AaronWander! |
…AaronWander) (#1317) Co-authored-by: Peter Steinberger <[email protected]>
Summary
Change Type (select all)
Scope (select all touched areas)
Linked Issue/PR
User-visible / Behavior Changes
Security Impact (required)
Repro + Verification
Environment
Steps
Expected
Actual
Evidence
Human Verification (required)
Compatibility / Migration
Failure Recovery (if this breaks)
Risks and Mitigations
None.