From cdfac8e997eba9fd77de95dedee530ae341c56fa Mon Sep 17 00:00:00 2001 From: Zach Hannum Date: Wed, 22 Jul 2026 10:08:39 -0400 Subject: [PATCH] fix(workbench): reactivate Positron console tab before concluding no console On Workbench 2026.07.1 (GA), running a command in the Positron Terminal (as terminal_run does before its console readback) removes .positron-console from the DOM while the Terminal tab is focused. ensure_positron_console then saw no console panel, and because a console session already exists its "Start New Console Session" button is also gone, so it returned False -- surfacing as "No Positron console could be started (no R/Python interpreter resolved)" and a terminal_run 120s timeout on every retry. This was intermittent (it depends on whether the Terminal held focus at readback time) and hit the GA image far more often than the daily/preview build. ensure_positron_console now activates the Console tab and briefly polls for the panel to re-render before falling through to the start-a-new-console path, so an existing (backgrounded) console is detected instead of being missed. The poll draws from the same shared timeout budget, so the total wait stays bounded. Verified against ghcr.io/posit-dev/workbench:2026.07.1-147.pro6 (GA, arm64): test_clone_positron passed 12/12 (previously ~1-in-3 failed), alongside test_clone_rstudio, test_clone_vscode and test_launch_positron. --- src/vip_tests/workbench/exec.py | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/src/vip_tests/workbench/exec.py b/src/vip_tests/workbench/exec.py index 0ce19d9c..0804d604 100644 --- a/src/vip_tests/workbench/exec.py +++ b/src/vip_tests/workbench/exec.py @@ -277,6 +277,10 @@ def rstudio_eval(page: Page, expr: str, timeout: int = 30_000) -> str: # selecting immediately can miss R and fall back to the (less reliable) Python # console. Bounded so a genuinely R-less deployment settles quickly. _POSITRON_R_GRACE_POLLS = 10 +# Polls to let .positron-console re-render after activating the Console tab. A +# console hidden behind the Terminal is removed from the DOM on some Positron +# builds, so we activate + briefly poll before concluding no console exists. +_POSITRON_REACTIVATE_POLLS = 3 def ensure_positron_console(page: Page, timeout: int = 45_000) -> bool: @@ -302,8 +306,24 @@ def ensure_positron_console(page: Page, timeout: int = 45_000) -> bool: Returns: ``True`` if a console is running, ``False`` otherwise. """ - if page.locator(PositronSession.CONSOLE_PANEL).count() > 0: - return True + # One poll budget is shared across every phase below so the total wait is + # bounded by *timeout* rather than a multiple of it. + remaining = max(1, timeout // _POSITRON_POLL_MS) + + # A console started earlier may be hidden behind the Terminal tab (a prior + # terminal_run selects it), which removes .positron-console from the DOM on + # some Positron builds. Activate the Console tab first and give the panel a + # moment to re-render, so an existing console is detected instead of being + # missed -- once a session exists its "Start New Console Session" button is + # gone, so a missed console would otherwise look like "no console" and fail. + _activate_positron_console(page) + reactivate = min(remaining, _POSITRON_REACTIVATE_POLLS) + while reactivate > 0: + if page.locator(PositronSession.CONSOLE_PANEL).count() > 0: + return True + page.wait_for_timeout(_POSITRON_POLL_MS) + reactivate -= 1 + remaining -= 1 start = page.locator(_POSITRON_START_BUTTON) if start.count() == 0: @@ -313,10 +333,6 @@ def ensure_positron_console(page: Page, timeout: int = 45_000) -> bool: except Exception: return False - # A single poll budget is shared across both phases below so the total wait - # is bounded by *timeout* rather than ~2x it. - remaining = max(1, timeout // _POSITRON_POLL_MS) - # Phase 1: poll the interpreter quickpick — discovery lags the click on a # cold session (~10s live). while page.locator(_POSITRON_QUICKPICK_ROW).count() == 0 and remaining > 0: