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>
This commit is contained in:
razzant 2026-09-19 02:13:12 +03:00
parent 472f8c5cc7
commit 0a8bbdcce4
2 changed files with 47 additions and 1 deletions

View file

@ -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']

View file

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