fix(#1195): an unrelated page change never cancels the hidden Widgets retention check

Review round 4 advisory (gpt-6-astra): widgets.js aborted the owned list
requests on EVERY non-Widgets page-shown event. A skill disabled while Widgets
was hidden started the retention read; navigating between other pages during
that read cancelled it, and the disabled skill's kept-running frame stayed
alive until the next visit. Leaving Widgets still cancels its own sync; once
hidden, other navigation leaves the retention check alone. The retain browser
case now holds the read across a dashboard -> skills navigation before letting
it land (Chromium + WebKit pass; fails without the fix).
This commit is contained in:
Ouroboros 2026-09-22 21:29:32 +03:00
parent b6e82b095f
commit c6a1e4139a
2 changed files with 14 additions and 1 deletions

View file

@ -15,6 +15,7 @@ import json
import os
import pathlib
import textwrap
import time
import pytest
@ -848,11 +849,21 @@ def test_ui_smoke_widget_retain_keeps_running_across_pages(direct_server_with_da
# Leave, then disable the skill while Widgets is hidden: the kept frame
# is force-stopped without a visit; the return finds the cards gone.
# The read is HELD across dashboard -> skills: unrelated navigation must not cancel it.
_click_nav(page, "dashboard")
wait_active(page, False)
page.wait_for_timeout(300)
assert frame_count(page, "kept") == 1
held = []
page.route("**/api/widgets", lambda route: held.append(route))
assert toggle(page, False) == 200
deadline = time.monotonic() + 10
while not held and time.monotonic() < deadline: page.wait_for_timeout(50)
assert held, "the disable did not start the hidden retention read"
_click_nav(page, "skills")
page.wait_for_timeout(300)
assert frame_count(page, "kept") == 1, "the read is still held; nothing may stop yet"
[route.continue_() for route in held]; page.unroute("**/api/widgets")
wait_frame(page, "kept", False, timeout=15_000)
wait_active(page, False)
_click_nav(page, "widgets")

View file

@ -1576,7 +1576,9 @@ export function initWidgets(ctx = {}) {
render();
} else {
cardMenus.close();
listRequests.abortAll();
// Leaving Widgets cancels ITS sync; once hidden, an unrelated page change
// must not cancel a skill-disable retention check (its kept frame would run on).
if (widgetsVisible) listRequests.abortAll();
// Leaving disposes the mounted work — except the frames the owner
// keeps running, which stay mounted in the hidden page — and stops
// stale paints; the cards stay in the DOM so the next entry mounts