"""Real UI modules against controlled subscription API responses, without runtime startup.""" from __future__ import annotations import copy import json import os from datetime import datetime, timedelta, timezone from functools import partial from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer from pathlib import Path from threading import Thread from urllib.parse import parse_qs, urlparse import pytest from tests.test_onboarding_complete_endpoint import ( LIVE_SNAPSHOT, _profile, _profile_account, onboarding as onboarding, # explicit re-export of the real atomic settings fixture ) pytestmark = [pytest.mark.ui_browser, pytest.mark.serial] WEB = Path(__file__).resolve().parents[1] / "web" @pytest.fixture def subscription_ui(): if os.environ.get("OUROBOROS_RUN_UI_SMOKE") != "1": pytest.skip("Set OUROBOROS_RUN_UI_SMOKE=1 to run the browser flow") playwright = pytest.importorskip("playwright.sync_api") bootstrap = json.loads((WEB / "tests/fixtures/onboarding_bootstrap.json").read_text(encoding="utf-8")) bootstrap["freshInstall"] = True fixture = json.loads((WEB / "tests/fixtures/subscription_setup.json").read_text(encoding="utf-8")) fixture['status']['quota'] = [{ 'subject': {'harness': 'codex', 'subject_id': 'personal'}, 'freshness': 'fresh', 'constraints': [{'id': 'window', 'used_ratio': 0.38, 'resets_at': (datetime.now(timezone.utc) + timedelta(hours=2)).isoformat()}], }] posts = [] reads = [] page_errors = [] backend = {} settings = { **fixture["preview"]["model_settings"], "OUROBOROS_SUBAGENTS": fixture["preview"]["available_subagents"], "OUROBOROS_REVIEWER_SLOTS": json.dumps(fixture["preview"]["reviewer_slots"]), "OUROBOROS_RUNTIME_MODE": "advanced", "OUROBOROS_CONTEXT_MODE": "max", "_meta": {"setup_contract": bootstrap["contract"]}, } class Handler(SimpleHTTPRequestHandler): def log_message(self, *_): pass def do_GET(self): if self.path == "/onboarding": body = (WEB / "onboarding_template.html").read_text(encoding="utf-8").replace( "__ONBOARDING_BOOTSTRAP__", json.dumps(bootstrap)) self.send_response(200) self.send_header("Content-Type", "text/html") self.end_headers() self.wfile.write(body.encode()) return if self.path.startswith("/api/tasks/") and backend.get("task_gateway"): response = backend["task_gateway"].get(self.path) self.send_response(response.status_code) for key in ("content-type", "content-disposition", "content-length"): if key in response.headers: self.send_header(key, response.headers[key]) self.end_headers() self.wfile.write(response.content) return self.path = self.path.removeprefix("/static") super().do_GET() def respond(route): request = route.request path = urlparse(request.url).path reads.append(request.url) body = {} if request.method == "POST": payload = request.post_data_json or {} posts.append((path, payload)) if path == "/api/onboarding/subagents/preview": if backend.get("preview_client"): 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", "detail": backend["preview_error"], "can_skip": True, "saved": False, })) return body = copy.deepcopy(fixture["preview"]) if not payload.get('subscriptionsConnected'): body['model_settings'] = {key: payload.get(key, '') for key in body['model_settings']} body['available_subagents'] = {'enabled': True, 'items': []} body['reviewer_slots'] = { 'triad': [{'slot_id': 'triad_1', 'route': {'kind': 'api_chat', 'target_id': payload.get('OUROBOROS_MODEL')}}], 'scope': [{'slot_id': 'scope_1', 'route': {'kind': 'api_chat', 'target_id': payload.get('OUROBOROS_MODEL')}}], 'advisory': {'enabled': True, 'route': {'kind': 'api_chat', 'target_id': payload.get('OUROBOROS_MODEL')}}, } if payload.get("skipSubscriptionPresets"): body["model_settings"] = {key: payload.get(key, '') for key in body["model_settings"]} body["available_subagents"] = payload.get("OUROBOROS_SUBAGENTS") or {"enabled": True, "items": []} slots = json.loads(payload["OUROBOROS_REVIEWER_SLOTS"]) if payload.get("OUROBOROS_REVIEWER_SLOTS") else body["reviewer_slots"] for value in slots.values(): for row in value if isinstance(value, list) else [value]: if not isinstance(row, dict): continue row.pop("subagent_id", None) row["route"] = {"kind": "api_chat", "target_id": payload["OUROBOROS_MODEL"]} pin = payload.get("OUROBOROS_MODEL_ACCOUNTS", {}).get("main") if pin: row["route"]["profile_id"] = pin body["reviewer_slots"] = slots body["reviewer_slots"] = json.dumps(body["reviewer_slots"]) elif path == "/api/onboarding/complete": assert isinstance(payload["OUROBOROS_REVIEWER_SLOTS"], str) assert isinstance(json.loads(payload["OUROBOROS_REVIEWER_SLOTS"]), dict) if backend.get("client"): result = backend["client"].post(path, json=payload) route.fulfill(status=result.status_code, content_type="application/json", body=result.text) return body = {"ok": True, "runtime_mode": "advanced", "restart_required": False} elif path == "/api/settings": body = {"ok": True, "saved": True} elif path == "/api/claudexor/status": body = fixture["status"] elif path == "/api/settings": body = settings elif path == "/api/reviewer-slots": body = fixture["preview"]["reviewer_slots"] elif path == "/api/model-catalog": body = copy.deepcopy(backend.get("catalog_response", fixture["catalog"])) if backend.get("catalog_status"): route.fulfill(status=backend["catalog_status"], content_type="application/json", body=json.dumps(body)) return profile = parse_qs(urlparse(request.url).query).get("credential_profile_id", [""])[0] if profile == "work": body["items"][0].update(credential_profile_id="work", max_context_window=500000) elif path == "/api/onboarding": route.fulfill(status=204) return elif path == "/api/state": body = {"supervisor_ready": True, "active_chat_activities": [], "projects": []} elif path == "/api/projects": body = {"projects": []} elif path == "/api/chat/history": body = {"messages": [], "progress": []} elif path == "/api/health": body = {"ok": True, "version": "fixture"} route.fulfill(content_type="application/json", body=json.dumps(body)) server = ThreadingHTTPServer(("127.0.0.1", 0), partial(Handler, directory=str(WEB))) thread = Thread(target=server.serve_forever, daemon=True) thread.start() try: with playwright.sync_playwright() as pw: engine = os.environ.get("OUROBOROS_UI_BROWSER_ENGINE", "chromium") if engine not in {"chromium", "webkit", "firefox"}: raise ValueError(f"Unsupported browser engine: {engine}") browser = getattr(pw, engine).launch(headless=True) try: page = browser.new_page(viewport={"width": 1360, "height": 900}, has_touch=os.environ.get("OUROBOROS_UI_HAS_TOUCH") == "1") page.route_web_socket('**/ws', lambda ws: ws.send(json.dumps({"type": "heartbeat"}))) page.route("**/api/**", respond) page.on("pageerror", lambda error: page_errors.append(str(error))) yield {"page": page, "url": f"http://127.0.0.1:{server.server_port}", "posts": posts, "reads": reads, "fixture": fixture, "settings": settings, "errors": page_errors, "backend": backend, "bootstrap": bootstrap} assert not page_errors finally: browser.close() finally: server.shutdown() server.server_close() thread.join(timeout=5) def capture(page, name): root = os.environ.get("OUROBOROS_UI_EVIDENCE_DIR") if root: target = Path(root) target.mkdir(parents=True, exist_ok=True) page.screenshot(path=str(target / f"{name}.png"), animations="disabled") def test_codex_quick_setup_computes_skipped_steps_and_finishes_once(subscription_ui, onboarding): ui = subscription_ui ui['backend']['client'] = onboarding.client onboarding.calls['snapshot_payload'] = { **LIVE_SNAPSHOT, 'harnesses': [LIVE_SNAPSHOT['harnesses'][1]], 'profiles': {'harnessAccounts': [_profile_account('codex', 'personal')], 'profiles': [_profile('codex', 'personal')]}, 'model_catalog': ui['fixture']['catalog']['items'], } ui['fixture']['status']['profiles']['profiles'] = ui['fixture']['status']['profiles']['profiles'][:1] page = ui["page"] page.goto(ui["url"] + "/onboarding") page.wait_for_selector('#quick-start-btn:not([hidden])') assert page.locator('.wizard-step').count() == 5 capture(page, "accounts-connected") page.click('#quick-start-btn') page.wait_for_selector('.summary-card') assert "gpt-test" in page.locator('.summary-card').inner_text() assert "Deep self-review" in page.locator('.summary-card').inner_text() capture(page, "summary-quick") page.click('#next-btn') page.wait_for_url(ui["url"] + "/") writes = [body for path, body in ui["posts"] if path == '/api/onboarding/complete'] assert len(writes) == 1 assert writes[0]["OPENROUTER_API_KEY"] == "" assert writes[0]["OUROBOROS_MODEL"] == 'claudexor::codex=gpt-test' assert json.loads(writes[0]["OUROBOROS_REVIEWER_SLOTS"])["deep_review"]["subagent_id"] == "codex" assert not any(path == '/api/settings' for path, _ in ui["posts"]) saved = onboarding.saved() assert saved['OUROBOROS_MODEL'] == writes[0]['OUROBOROS_MODEL'] assert saved['OUROBOROS_REVIEWER_SLOTS'] == writes[0]['OUROBOROS_REVIEWER_SLOTS'] assert not saved['OPENAI_API_KEY'] and not saved['OPENROUTER_API_KEY'] assert onboarding.calls['supervisor'] == 1 @pytest.mark.parametrize("credential_harness, expected", [("codex", True), ("claude", False)]) def test_quick_start_requires_a_model_source_for_the_connected_harness(subscription_ui, credential_harness, expected): ui, page = subscription_ui, subscription_ui["page"] ui["fixture"]["catalog"]["model_sources"] = [ {"id": "opaque-source", "label": "Managed models", "credentialHarness": credential_harness}, ] with page.expect_response("**/api/model-catalog"): page.goto(ui["url"] + "/onboarding") page.wait_for_function( "expected => document.querySelector('#quick-start-btn').hidden !== expected", arg=expected) assert page.locator("#quick-start-btn").is_visible() is expected def test_model_roles_pin_context_fallback_and_manual_draft_survive_preview(subscription_ui): ui = subscription_ui page = ui["page"] page.goto(ui["url"] + "/onboarding") page.wait_for_selector('#quick-start-btn:not([hidden])') page.click('#next-btn') page.wait_for_selector('[data-model-role="main"]') main = page.locator('[data-model-role="main"]') light = page.locator('[data-model-role="light"]') main.locator('[data-model-role-account]').select_option('personal') held_catalog = [] page.route('**/api/model-catalog?*credential_profile_id=work', lambda route: held_catalog.append(route)) light.locator('[data-model-role-account]').select_option('work') main.locator('summary').click() light.locator('summary').click() assert 'not known' in light.locator('[data-model-context-note]').inner_text() page.wait_for_function("() => document.querySelector('[data-model-role=light] [data-model-role-account]').value === 'work'") assert held_catalog work_catalog = copy.deepcopy(ui['fixture']['catalog']) work_catalog['items'][0].update(credential_profile_id='work', max_context_window=500000) for route in held_catalog: route.fulfill(content_type='application/json', body=json.dumps(work_catalog)) page.unroute('**/api/model-catalog?*credential_profile_id=work') page.wait_for_function("() => document.querySelector('[data-model-role=light] [data-model-context-note]').textContent.includes('500,000')") assert '872,000' in main.locator('[data-model-context-note]').inner_text() main.locator('[data-model-role-context]').fill('1000000') assert 'set by you' in main.locator('[data-model-context-note]').inner_text() page.locator('[data-model-role="vision"] [data-model-role-account]').select_option('work') page.click('[data-model-add]') fallback = page.locator('[data-model-role-group="fallback"]') fallback.locator('[data-model-role-source]').select_option('subscription:codex') fallback.locator('[data-model-role-model]').fill('first') fallback.locator('[data-model-role-account]').select_option('work') page.click('[data-model-add]') fallback.locator('[data-model-role-model]').last.fill('openai::second') fallback.locator('[data-model-up]').last.click() main.locator('[data-model-role-model]').fill('owner-model') page.evaluate('window.scrollTo(0, 0)') capture(page, "models-desktop") page.click('#next-btn') page.wait_for_selector('#reviewer-slots-section', state='attached') page.locator('details').filter(has=page.locator('#reviewer-slots-section')).locator(':scope > summary').click() assert page.locator('#reviewer-deep-review-row select').count() > 0 capture(page, "review-editable") deep_review = page.locator('#reviewer-deep-review-row') deep_review.locator('[data-deep-review-effort]').select_option('high') deep_review.scroll_into_view_if_needed() capture(page, "deep-review-editable") page.click('#next-btn') page.wait_for_selector('[data-collapse="api-budget"]') assert not page.locator('[data-collapse="api-budget"]').evaluate('(el) => el.open') capture(page, "budget-subscription") page.click('#next-btn') page.wait_for_selector('.summary-card') page.wait_for_function("() => !document.querySelector('.wizard-error').textContent") summary = page.locator('.summary-card') assert '1,000,000 tokens, set by you' in summary.inner_text() vision = summary.locator('.summary-kv').filter(has=page.get_by_text('Vision', exact=True)) assert 'Uses Main' in vision.inner_text() assert 'Account: work' in vision.inner_text() deep_summary = summary.locator('.summary-kv').filter(has=page.get_by_text('Deep self-review', exact=True)) assert 'Effort: high' in deep_summary.inner_text() capture(page, "summary-manual") page.click('#next-btn') page.wait_for_url(ui["url"] + "/") body = [body for path, body in ui["posts"] if path == '/api/onboarding/complete'][0] assert body['OUROBOROS_MODEL'] == 'claudexor::codex=owner-model' assert body['OUROBOROS_MODEL_LIGHT'] == 'claudexor::codex=gpt-test' assert body['OUROBOROS_MODEL_ACCOUNTS']['main'] == 'personal' assert body['OUROBOROS_MODEL_ACCOUNTS']['light'] == 'work' assert body['OUROBOROS_MODEL_ACCOUNTS']['vision'] == 'work' assert body['OUROBOROS_MODEL_ACCOUNTS']['fallback'] == ['', 'work'] assert body['OUROBOROS_MODEL_CONTEXT_WINDOWS']['main'] == 1000000 assert body['OUROBOROS_MODEL_FALLBACKS'] == 'openai::second, claudexor::codex=first' assert json.loads(body['OUROBOROS_REVIEWER_SLOTS'])['deep_review']['effort'] == 'high' def test_settings_accounts_and_model_roles_use_the_same_compact_components(subscription_ui): ui = subscription_ui page = ui['page'] page.goto(ui['url'] + '/#settings') page.wait_for_selector('#settings-model-roles .model-role-row', state='attached') assert page.locator('[data-settings-tab="providers"]').inner_text() == 'Accounts' assert page.locator('[data-settings-panel="providers"] #harness-accounts-section').count() == 1 assert page.locator('[data-settings-panel="agents"] #harness-accounts-section').count() == 0 page.click('[data-settings-tab="models"]') page.wait_for_function("() => document.querySelector('[data-settings-tab=models]').getAttribute('aria-selected') === 'true'") main = page.locator('[data-model-role="main"]') main.locator('[data-model-role-account]').select_option('work') inputs = main.locator('.model-role-controls > input, .model-role-controls > select') tops = inputs.evaluate_all('(els) => els.filter(el => !el.hidden).map(el => el.getBoundingClientRect().top)') assert max(tops) - min(tops) <= 2 assert main.locator('input').first.evaluate('(el) => getComputedStyle(el).fontSize') == '14px' capture(page, 'settings-models-desktop') page.set_viewport_size({'width': 390, 'height': 844}) page.wait_for_function("() => document.querySelector('#primary-sidebar').getBoundingClientRect().right <= 1") assert page.evaluate('document.documentElement.scrollWidth <= window.innerWidth') capture(page, 'settings-models-narrow') def test_cursor_only_does_not_claim_model_access_and_api_only_finishes(subscription_ui): ui = subscription_ui ui['fixture']['status']['profiles']['profiles'] = [{ 'profile': {'profile_id': 'cursor-only', 'harness_id': 'cursor', 'enabled': True}, 'status': {'verification': 'passed'}, }] page = ui['page'] page.goto(ui['url'] + '/onboarding') page.wait_for_function("() => document.querySelector('[data-agent-family=cursor]').textContent.includes('Connected')") assert page.locator('#quick-start-btn').is_hidden() assert page.locator('#next-btn').is_disabled() assert 'Main still needs Codex' in page.locator('#agents-outcome').inner_text() # Removing the optional agent gives the API-only path the same five steps. ui['fixture']['status']['profiles']['profiles'] = [] page.reload() page.locator('[data-collapse="api-access"] > summary').click() page.locator('#openai-key').fill('fixture-api-credential') page.click('#next-btn') page.wait_for_selector('#main-model') assert page.locator('#main-model').input_value() for _ in range(3): page.click('#next-btn') page.wait_for_selector('.summary-card') assert 'no API key' not in page.locator('.summary-card').inner_text() page.click('#next-btn') page.wait_for_url(ui['url'] + '/') body = [body for path, body in ui['posts'] if path == '/api/onboarding/complete'][0] assert body['OPENAI_API_KEY'] == 'fixture-api-credential' assert body['OUROBOROS_MODEL'].startswith('openai::') assert not body['subscriptionsConnected'] @pytest.mark.parametrize('edit_after_recovery', [False, True, 'main']) def test_failed_preview_allows_manual_main_and_visible_reviewer_recovery_before_save(subscription_ui, onboarding, edit_after_recovery): ui, page = subscription_ui, subscription_ui['page'] ui['backend'].update(preview_client=onboarding.client, client=onboarding.client) onboarding.calls['snapshot_payload'] = { **LIVE_SNAPSHOT, 'harnesses': [{**LIVE_SNAPSHOT['harnesses'][1], 'models': []}], 'profiles': {'harnessAccounts': [_profile_account('codex', 'personal')], 'profiles': [_profile('codex', 'personal')]}, 'model_catalog': ui['fixture']['catalog']['items'], } page.goto(ui['url'] + '/onboarding') page.wait_for_selector('#next-btn:not([disabled])') page.wait_for_function("() => document.querySelector('#onboarding-access-note').textContent.includes('listed no models')") assert not any(path == '/api/onboarding/complete' for path, _ in ui['posts']) page.set_viewport_size({'width': 390, 'height': 844}) assert page.evaluate('document.documentElement.scrollWidth <= window.innerWidth') capture(page, 'accounts-preview-failure-narrow') page.set_viewport_size({'width': 1360, 'height': 900}) page.click('#next-btn') main = page.locator('[data-model-role="main"]') assert main.locator('[data-model-role-model]').input_value() == '' assert page.locator('#next-btn').is_disabled() main.locator('[data-model-role-source]').select_option('subscription:codex') main.locator('[data-model-role-model]').fill('owner-main') main.locator('[data-model-role-account]').select_option('personal') for _ in range(3): page.click('#next-btn') page.wait_for_selector('.summary-card') page.wait_for_selector('#skip-presets-btn:not([hidden])') page.click('#skip-presets-btn') page.wait_for_function("() => document.querySelector('.wizard-inline-note')?.textContent.includes('Reviewers were assigned to Main')") assert not any(path == '/api/onboarding/complete' for path, _ in ui['posts']), 'Recovery is a preview, not a write' assert not onboarding.settings_path.exists() for label in ['Triad review', 'Scope review', 'Advisory review', 'Deep self-review']: row = page.locator('.summary-kv').filter(has=page.get_by_text(label, exact=True)) assert 'claudexor::codex=owner-main' in row.inner_text() capture(page, 'manual-main-reviewer-recovery') if edit_after_recovery == 'main': for _ in range(3): page.click('#back-btn') page.locator('[data-model-role="main"] [data-model-role-model]').fill('new-main') for _ in range(3): page.click('#next-btn') page.wait_for_selector('#skip-presets-btn:not([hidden])') assert page.locator('#next-btn').is_enabled() assert 'Main changed; reviewers keep the assignments shown above' in page.locator('.wizard-inline-note').inner_text() assert 'owner-main' in page.locator('.summary-kv').filter(has=page.get_by_text('Triad review', exact=True)).inner_text() elif edit_after_recovery: page.click('#back-btn') page.click('#back-btn') page.locator('[data-collapse="reviewers"] > summary').click() page.locator('[data-deep-review-api-model]').fill('owner-deep') page.click('#next-btn') page.click('#next-btn') page.wait_for_function("() => !document.querySelector('.wizard-error').textContent") deep = page.locator('.summary-kv').filter(has=page.get_by_text('Deep self-review', exact=True)) assert 'owner-deep' in deep.inner_text() 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=new-main' if edit_after_recovery == 'main' else 'claudexor::codex=owner-main') assert json.loads(onboarding.saved()['OUROBOROS_REVIEWER_SLOTS']) == json.loads(bodies[0]['OUROBOROS_REVIEWER_SLOTS']) assert onboarding.calls['supervisor'] == 1 for kind, value in json.loads(bodies[0]['OUROBOROS_REVIEWER_SLOTS']).items(): for row in value if isinstance(value, list) else [value]: if isinstance(row, dict): expected = 'owner-deep' if edit_after_recovery is True and kind == 'deep_review' else 'owner-main' assert row['route']['target_id'] == f'claudexor::codex={expected}' 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'] ui['backend'].update(preview_error='Agent model discovery unavailable.', catalog_status=http_status, catalog_response={'error': 'Raw model service unavailable.'}) page.goto(ui['url'] + '/onboarding') page.wait_for_function("() => document.querySelector('#onboarding-access-note').textContent.includes('Raw model service unavailable')") assert page.locator('#next-btn').is_disabled() page.locator('[data-collapse="api-access"] > summary').click() page.locator('#openai-key').fill('owner-draft-credential') ui['backend'].pop('catalog_status') ui['backend'].pop('catalog_response') page.click('#onboarding-access-retry') page.wait_for_selector('#quick-start-btn:not([hidden])') assert page.locator('#openai-key').input_value() == 'owner-draft-credential' assert page.locator('[data-collapse="api-access"]').evaluate('(el) => el.open') assert not any(path == '/api/onboarding/complete' for path, _ in ui['posts']) @pytest.mark.parametrize('fresh_install', [False, True]) def test_declared_source_with_failed_inventory_preserves_stored_or_owner_authored_defaults(subscription_ui, fresh_install): ui, page = subscription_ui, subscription_ui['page'] ui['bootstrap']['freshInstall'] = fresh_install initial = ui['bootstrap']['initialState']['mainModel'] ui['backend'].update(preview_error='Agent inventory unavailable.', catalog_response={ 'items': [], 'model_sources': ui['fixture']['catalog']['model_sources'], 'read_state': 'partial', 'errors': [{'error': 'Account model inventory unavailable.'}], }) page.goto(ui['url'] + '/onboarding') page.wait_for_selector('#next-btn:not([disabled])') page.click('#next-btn') main = page.locator('[data-model-role="main"]') if fresh_install: assert main.locator('[data-model-role-model]').input_value() == '' main.locator('[data-model-role-model]').fill(initial) page.click('#back-btn') page.click('#onboarding-access-retry') page.wait_for_selector('#onboarding-access-retry:not([disabled])') page.click('#next-btn') assert main.locator('[data-model-role-model]').input_value() == initial assert page.locator('#next-btn').is_disabled() assert 'Main uses OpenRouter' in page.locator('.wizard-error').inner_text() main.locator('[data-model-role-source]').select_option('subscription:codex') main.locator('[data-model-role-model]').fill('typed-without-inventory') assert page.locator('#next-btn').is_enabled() def test_late_catalog_does_not_restore_access_after_the_account_disconnects(subscription_ui): ui, page = subscription_ui, subscription_ui['page'] ui['backend']['preview_error'] = 'Agent inventory unavailable.' held = [] page.route('**/api/model-catalog', lambda route: held.append(route)) page.goto(ui['url'] + '/onboarding') page.wait_for_function("() => document.querySelector('#onboarding-access-note').textContent.includes('Checking model sources')") assert held ui['fixture']['status']['profiles']['profiles'] = [] page.evaluate("async () => (await import('/static/modules/claudexor_status_store.js')).claudexorStatus.refresh()") page.wait_for_function("() => !document.querySelector('[data-agent-family=codex]').textContent.includes('Connected')") for route in held: route.fulfill(content_type='application/json', body=json.dumps(ui['fixture']['catalog'])) page.unroute('**/api/model-catalog') assert page.locator('#next-btn').is_disabled() assert page.locator('#quick-start-btn').is_hidden()