From 0a8bbdcce4f114bafb9beb7c17d7c32ff1c30d2b Mon Sep 17 00:00:00 2001 From: razzant Date: Sat, 19 Sep 2026 02:13:12 +0300 Subject: [PATCH] fix(onboarding): do not latch Finish after a refused Main reviewer recovery A refused "Use Main for reviewers" preview left skipSubscriptionPresets set, so the ordinary Finish path stayed blocked for the rest of the session and the packaged setup window has no reload to escape. Roll the flag back when the preview is refused, keep the recovery button offered, and pin the path with a browser test that refuses once, checks Finish is not latched, then retries successfully. Co-authored-by: Ouroboros <311266734+ouroboros-agent@users.noreply.github.com> --- tests/test_subscription_setup_browser.py | 38 ++++++++++++++++++++++++ web/modules/onboarding_wizard.js | 10 ++++++- 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/tests/test_subscription_setup_browser.py b/tests/test_subscription_setup_browser.py index 84313067a..dc46dc7d5 100644 --- a/tests/test_subscription_setup_browser.py +++ b/tests/test_subscription_setup_browser.py @@ -76,6 +76,11 @@ def subscription_ui(): result = backend["preview_client"].post(path, json=payload) route.fulfill(status=result.status_code, content_type="application/json", body=result.text) return + if backend.get("recovery_error") and payload.get("skipSubscriptionPresets"): + route.fulfill(status=400, content_type="application/json", body=json.dumps({ + "ok": False, "error": "Reviewer recovery unavailable.", "detail": backend["recovery_error"], "saved": False, + })) + return if backend.get("preview_error") and not payload.get("skipSubscriptionPresets"): route.fulfill(status=503, content_type="application/json", body=json.dumps({ "ok": False, "error": "Automatic assignments unavailable.", "code": "models_unavailable", @@ -431,6 +436,39 @@ def test_failed_preview_allows_manual_main_and_visible_reviewer_recovery_before_ assert row['route']['profile_id'] == 'personal' +def test_failed_main_reviewer_recovery_does_not_latch_finish(subscription_ui): + # A refused recovery preview must leave the wizard on its ordinary path: + # the packaged setup window has no reload, so the owner needs both the + # normal Finish and a working Use Main retry after the backend refuses once. + ui, page = subscription_ui, subscription_ui['page'] + ui['backend'].update(preview_error='Agent model discovery unavailable.', + recovery_error='A Main account pin requires a managed model source.') + page.goto(ui['url'] + '/onboarding') + page.wait_for_selector('#next-btn:not([disabled])') + page.click('#next-btn') + main = page.locator('[data-model-role="main"]') + main.locator('[data-model-role-source]').select_option('subscription:codex') + main.locator('[data-model-role-model]').fill('owner-main') + for _ in range(3): + page.click('#next-btn') + page.wait_for_selector('#skip-presets-btn:not([hidden])') + page.click('#skip-presets-btn') + page.wait_for_function("() => document.querySelector('.wizard-error').textContent.includes('managed model source')") + page.wait_for_selector('#skip-presets-btn:not([hidden])') + page.click('#next-btn') + page.wait_for_function("() => document.querySelector('.wizard-error').textContent") + assert 'Use Main for reviewers to prepare' not in page.locator('.wizard-error').inner_text() + assert not any(path == '/api/onboarding/complete' for path, _ in ui['posts']) + ui['backend'].pop('recovery_error') + page.click('#skip-presets-btn') + page.wait_for_function("() => document.querySelector('.wizard-inline-note')?.textContent.includes('Reviewers were assigned to Main')") + page.click('#next-btn') + page.wait_for_url(ui['url'] + '/') + bodies = [body for path, body in ui['posts'] if path == '/api/onboarding/complete'] + assert len(bodies) == 1 and bodies[0]['skipSubscriptionPresets'] is True + assert bodies[0]['OUROBOROS_MODEL'] == 'claudexor::codex=owner-main' + + @pytest.mark.parametrize('http_status', [200, 500]) def test_accounts_catalog_failure_is_visible_and_retry_keeps_the_draft(subscription_ui, http_status): ui, page = subscription_ui, subscription_ui['page'] diff --git a/web/modules/onboarding_wizard.js b/web/modules/onboarding_wizard.js index 7a6a4f63b..284076de5 100644 --- a/web/modules/onboarding_wizard.js +++ b/web/modules/onboarding_wizard.js @@ -1492,8 +1492,16 @@ import { accountRowFacts } from './harness_accounts.js'; if (disposed) return; state.preparingRecovery = false; state.recoveryPrepared = Boolean(ready); - if (ready) state.recoveryMain = mainBinding(); state.error = ready ? '' : agentsStep?.previewError || 'Reviewer assignments could not be prepared. Retry before saving.'; + if (ready) { + state.recoveryMain = mainBinding(); + } else { + // A failed recovery must not latch the wizard out of its ordinary + // path: the desktop setup window has no reload, so the owner keeps + // the normal Finish and the still-offered Use Main retry. + state.skipSubscriptionPresets = false; + void agentsStep?.setSkipPresets(false); + } render(); // Show the recovered assignments; only the next explicit Start saves. }