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. }