diff --git a/tests/test_account_catalog_browser.py b/tests/test_account_catalog_browser.py index 6630e3079..353e7a441 100644 --- a/tests/test_account_catalog_browser.py +++ b/tests/test_account_catalog_browser.py @@ -215,8 +215,10 @@ def test_partial_account_refresh_keeps_draft_and_only_failed_account_history(acc assert "not in discovery" in suggestions(page, field)[DRAFT_MODEL] account.select_option("work") assert "not checked" in suggestions(page, field)[DRAFT_MODEL] - assert row.locator("[data-subagent-status]").inner_text() == "Draft · Not checked" - assert "model list could not be read" in row.locator("[data-subagent-meta]").inner_text() + status = row.locator("[data-subagent-status]") + assert status.inner_text() == "Draft · Not checked" + # The unread account catalog is disclosed on the row's own status sentence. + assert "model list could not be read" in status.get_attribute("title") account.select_option("") roles.capture(page, "account-catalog-partial-" + consumer.lower().replace(" ", "-")) assert_saved(ui, consumer, DRAFT_MODEL, "") diff --git a/tests/test_model_chooser_browser.py b/tests/test_model_chooser_browser.py index 8558e97de..90441077c 100644 --- a/tests/test_model_chooser_browser.py +++ b/tests/test_model_chooser_browser.py @@ -20,6 +20,9 @@ def catalog(ui): def select_model_field(ui, consumer): roles.configure_mixed(ui) + # The catalog above is unprefixed, so every consumer is put on the same + # OpenRouter API lane the suggestions belong to. + lane = roles.api_lane(ui) if consumer in ['Scope', 'Deep']: slots = ui['fixture']['preview']['reviewer_slots'] if consumer == 'Scope': slots['scope'] = [{'slot_id': 'scope_1', 'route': {'kind': 'api_chat', 'target_id': 'owner/model'}}] @@ -33,15 +36,16 @@ def select_model_field(ui, consumer): page.locator('[data-settings-tab="models"]').click() if consumer == 'Fallback': page.locator('[data-model-add]').click() group = page.locator('[data-model-role="main"]') if consumer == 'Models' else page.locator('[data-model-role-group="fallback"] .model-role-row').first - group.locator('[data-model-role-source]').select_option('openrouter') + group.locator('[data-model-role-source]').select_option(lane) return page, group.locator('[data-model-role-model]') if consumer == "Actor": row = page.locator('[data-subagent-row]').nth(1) + row.locator('[data-subagent-field="route"]').select_option(lane) return page, row.locator('[data-subagent-field="model"]') if consumer == 'Scope': return page, page.locator('[data-slot-id="scope_1"] [data-slot-custom-api]') if consumer == 'Deep': return page, page.locator('[data-deep-review-api-model]') row = page.locator('[data-slot-id="triad_1"]') if consumer == "Triad" else page.locator('[data-advisory-row]') - row.locator('[data-slot-route], [data-advisory-route]').select_option('api') + row.locator('[data-slot-route], [data-advisory-route]').select_option(lane) return page, row.locator('[data-slot-custom-api], [data-advisory-api-model]') diff --git a/tests/test_model_wait_browser.py b/tests/test_model_wait_browser.py index 9305b470a..3dfafae44 100644 --- a/tests/test_model_wait_browser.py +++ b/tests/test_model_wait_browser.py @@ -13,12 +13,15 @@ capture = setup_browser.capture pytestmark = [pytest.mark.ui_browser, pytest.mark.serial] TASK = "analysis-task" +# The wait picker offers an API lane per provider whose credential is stored. +API_LANE = "api:openai" @pytest.fixture def waiting_ui(subscription_ui): ui = subscription_ui page = ui["page"] + ui["settings"]["OPENAI_API_KEY"] = "***set***" rows = { key: {"wait_id": key, "revision": 1, "task_attempt": 1, "role": role, "model": "claudexor::codex=gpt-test", "source": "codex", @@ -162,7 +165,7 @@ def test_multiple_waits_toggle_and_exact_role_switch_wait_for_application(waitin page.wait_for_selector('[data-wait-id="light-wait"] [data-wait-change]:not([disabled])') assert not light.locator('[data-wait-auto]').is_checked() light.locator('[data-wait-change]').click() - light.locator('[data-model-role-source]').select_option('openai') + light.locator('[data-model-role-source]').select_option(API_LANE) light.locator('[data-model-role-model]').fill('owner-model') assert not light.locator('[data-wait-persist]').is_checked() light.locator('[data-wait-apply]').click() @@ -320,7 +323,7 @@ def test_wait_reconnect_preserves_unsubmitted_form_without_context_or_animation( field = row.locator('[data-model-role-model]') field.wait_for() assert row.locator('.model-role-details').count() == 0 - row.locator('[data-model-role-source]').select_option('openai') + row.locator('[data-model-role-source]').select_option(API_LANE) assert row.locator('.model-role-details').count() == 0 field.fill('unfinished-owner-model') row.locator('[data-wait-persist]').check() diff --git a/tests/test_model_wait_recovery_browser.py b/tests/test_model_wait_recovery_browser.py index 7489e46fd..847885ca5 100644 --- a/tests/test_model_wait_recovery_browser.py +++ b/tests/test_model_wait_recovery_browser.py @@ -9,7 +9,7 @@ from ouroboros import model_wait, owner_mailbox from ouroboros.task_results import load_task_result from tests.test_llm_claudexor import setup as subscription_transport from tests.test_model_wait import live_wait as wait_fixture -from tests.test_model_wait_browser import TASK, waiting_ui as waiting_fixture +from tests.test_model_wait_browser import API_LANE, TASK, waiting_ui as waiting_fixture from tests.test_subscription_setup_browser import subscription_ui as ui_fixture, capture setup = subscription_transport @@ -97,7 +97,7 @@ def test_fallback_local_change_discloses_task_only_and_keeps_other_roles(waiting waiter = page.locator('[data-wait-id="light-wait"]') waiter.locator('[data-wait-role]').filter(has_text="Fallback 2").wait_for() waiter.locator('[data-wait-change]').click() - waiter.locator('[data-model-role-source]').select_option("openai") + waiter.locator('[data-model-role-source]').select_option(API_LANE) waiter.locator('[data-model-role-model]').fill("replacement") local = waiter.locator('[data-model-local]') persist = waiter.locator('[data-wait-persist]') diff --git a/tests/test_processing_preferences_browser.py b/tests/test_processing_preferences_browser.py index 24ce0731e..d6de10621 100644 --- a/tests/test_processing_preferences_browser.py +++ b/tests/test_processing_preferences_browser.py @@ -80,7 +80,7 @@ def test_onboarding_processing_draft_returns_and_reaches_finish(subscription_ui) main.locator('summary').click() main.locator('[data-model-role-processing]').select_option('standard') page.locator('#next-btn').click() - page.wait_for_selector('#reviewer-slots-section') + page.wait_for_selector('[data-collapse="reviewers"]') page.locator('#back-btn').click() page.wait_for_selector('[data-global-processing]') assert page.locator('[data-global-processing]').input_value() == 'fast' diff --git a/tests/test_subscription_role_routes_browser.py b/tests/test_subscription_role_routes_browser.py index 182af8506..bfdd222bc 100644 --- a/tests/test_subscription_role_routes_browser.py +++ b/tests/test_subscription_role_routes_browser.py @@ -9,6 +9,15 @@ from tests import test_subscription_setup_browser as setup_browser pytestmark = [pytest.mark.ui_browser, pytest.mark.serial] subscription_ui = setup_browser.subscription_ui capture = setup_browser.capture +# Every route picker offers an API lane per provider whose credential is +# stored, so a fixture that selects one advertises that provider's key first. +API_KEYS = {"openrouter": "OPENROUTER_API_KEY", "openai": "OPENAI_API_KEY"} + + +def api_lane(ui, provider="openrouter"): + """Store `provider`'s key in the served settings and return its route choice.""" + ui["settings"][API_KEYS[provider]] = "***set***" + return f"api:{provider}" @pytest.fixture @@ -72,6 +81,7 @@ def open_agents(ui): def test_reviewer_source_roundtrip_restores_its_own_model_and_account(role_ui): configure_mixed(role_ui) + lane = api_lane(role_ui, 'openai') page = open_agents(role_ui) for selector, model_field, account_field in [ ('[data-slot-id="triad_1"]', '[data-slot-custom-api]', '[data-slot-profile]'), @@ -79,13 +89,15 @@ def test_reviewer_source_roundtrip_restores_its_own_model_and_account(role_ui): ]: row = page.locator(selector) route = row.locator('[data-slot-route], [data-advisory-route]') - route.select_option('api') - row.locator(model_field).fill('openai::other-choice') + route.select_option(lane) + # The chooser holds the model alone; the source select names the provider. + row.locator(model_field).fill('other-choice') route.select_option('subscription:opaque-source') assert row.locator(model_field).input_value() == 'gpt-test' assert row.locator(account_field).input_value() == 'personal' - route.select_option('api') - assert row.locator(model_field).input_value() == 'openai::other-choice' + route.select_option(lane) + assert row.locator(model_field).input_value() == 'other-choice' + assert route.input_value() == lane route.select_option('subscription:opaque-source') page.locator('[data-advisory-row]').scroll_into_view_if_needed() capture(page, "reviewer-source-roundtrip-restored") @@ -93,7 +105,7 @@ def test_reviewer_source_roundtrip_restores_its_own_model_and_account(role_ui): with page.expect_response('**/api/reviewer-slots'): page.locator('#btn-reload-settings').click() page.get_by_role('button', name='Discard and continue', exact=True).click() - page.locator('[data-advisory-route]').select_option('api') + page.locator('[data-advisory-route]').select_option(lane) assert page.locator('[data-advisory-api-model]').input_value() == '' @@ -164,9 +176,10 @@ def test_subscription_accounts_roundtrip_existing_editors(role_ui, width): def test_source_switch_and_catalog_refresh_keep_focus_and_draft(role_ui): ui = role_ui configure_mixed(ui) + lane = api_lane(ui, 'openai') page = open_agents(ui) triad = page.locator('[data-slot-id="triad_1"]') - triad.locator('[data-slot-route]').select_option("api") + triad.locator('[data-slot-route]').select_option(lane) assert triad.locator('[data-slot-profile]').count() == 0 triad.locator('[data-slot-custom-api]').fill("openai::gpt-api") triad.locator('[data-slot-route]').select_option("subscription:opaque-source") diff --git a/tests/test_subscription_wait_integration_browser.py b/tests/test_subscription_wait_integration_browser.py index 81e0adf4a..414821fa5 100644 --- a/tests/test_subscription_wait_integration_browser.py +++ b/tests/test_subscription_wait_integration_browser.py @@ -63,6 +63,8 @@ def integrated_wait(subscription_ui, live_wait, monkeypatch): config.SETTINGS_PATH.write_text(json.dumps(settings), encoding="utf-8") original_settings = config.SETTINGS_PATH.read_bytes() ui["settings"].update(settings) + # The wait picker offers an API lane per provider whose credential is stored. + ui["settings"]["OPENAI_API_KEY"] = "***set***" source_entered, source_release = threading.Event(), threading.Event() catalog_entered, catalog_release = threading.Event(), threading.Event() @@ -265,7 +267,7 @@ def test_browser_wait_controls_apply_once_and_only_to_light(integrated_wait, per assert not row.locator("[data-wait-auto]").is_checked() row.locator("[data-wait-change]").click() - row.locator("[data-model-role-source]").select_option("openai") + row.locator("[data-model-role-source]").select_option("api:openai") row.locator("[data-model-role-model]").fill("owner-model") assert not row.locator("[data-wait-persist]").is_checked() if persist_role: diff --git a/tests/test_ui_smoke_large_artifacts.py b/tests/test_ui_smoke_large_artifacts.py index 2fe6f998a..3ca7b5249 100644 --- a/tests/test_ui_smoke_large_artifacts.py +++ b/tests/test_ui_smoke_large_artifacts.py @@ -99,12 +99,29 @@ def test_large_attachment_returns_through_real_document_handler_and_download( "name": "send_file", "arguments": json.dumps({"file_path": str(staged[0]), "caption": "Complete large dataset"}), }, }]} + finish = "tool_calls" if message.get("tool_calls") else "stop" payload = {"id": "mock-large-file", "object": "chat.completion", - "choices": [{"message": message, "finish_reason": "tool_calls" if message.get("tool_calls") else "stop"}], + "model": request.get("model") or "mock-model", + "choices": [{"index": 0, "message": message, "finish_reason": finish}], "usage": {"prompt_tokens": 1, "completion_tokens": 1, "total_tokens": 2}} - data = json.dumps(payload).encode() + content_type = "application/json" + if request.get("stream"): + # The main loop streams every completion: answer in SSE frames with + # the terminal framing the assembler requires. + content_type = "text/event-stream" + delta = dict(message) + if delta.get("tool_calls"): + delta["tool_calls"] = [dict(call, index=index) + for index, call in enumerate(delta["tool_calls"])] + common = {"id": payload["id"], "model": payload["model"], "object": "chat.completion.chunk"} + frames = [{**common, "choices": [{"index": 0, "delta": delta, "finish_reason": finish}]}, + {**common, "choices": [], "usage": payload["usage"]}] + data = ("".join("data: " + json.dumps(frame) + "\n\n" for frame in frames) + + "data: [DONE]\n\n").encode() + else: + data = json.dumps(payload).encode() handler.send_response(200) - handler.send_header("Content-Type", "application/json") + handler.send_header("Content-Type", content_type) handler.send_header("Content-Length", str(len(data))) handler.end_headers() handler.wfile.write(data) diff --git a/tests/test_ui_smoke_settings_drafts.py b/tests/test_ui_smoke_settings_drafts.py index 60f228e4a..5b1257e2f 100644 --- a/tests/test_ui_smoke_settings_drafts.py +++ b/tests/test_ui_smoke_settings_drafts.py @@ -211,6 +211,8 @@ def test_initial_settings_document_survives_early_edit_while_enrichment_waits(su ui, pending = subscription_ui, {"reviewers": [], "status": [], "catalog": []} page = ui["page"] ui["settings"]["OUROBOROS_MODEL"] = "claudexor::opaque-source=gpt-test" + # An API lane is offered per provider whose credential is stored. + ui["settings"]["OPENROUTER_API_KEY"] = "***set***" ui["fixture"]["catalog"]["model_sources"] = [ {"id": "opaque-source", "label": "Managed models", "credentialHarness": "codex"}, ] @@ -225,7 +227,7 @@ def test_initial_settings_document_survives_early_edit_while_enrichment_waits(su assert pending["status"], "the status read must still be pending" assert page.locator('#btn-save-settings').is_enabled(), "the known document can be saved before enrichment" source = main.locator('[data-model-role-source]') - source.select_option("openrouter") + source.select_option("api:openrouter") model = main.locator('[data-model-role-model]') model.fill("owner-kept-model") model.evaluate("element => { window.__earlyModel = element; element.setSelectionRange(4, 4); }") @@ -245,7 +247,7 @@ def test_initial_settings_document_survives_early_edit_while_enrichment_waits(su page.wait_for_function("""() => document.querySelector('[data-model-role="main"] [data-model-role-source]') .querySelector('option[value="subscription:opaque-source"]')""") expect(model).to_have_value("owner-kept-model") - assert source.input_value() == "openrouter" + assert source.input_value() == "api:openrouter" assert model.evaluate("element => element === window.__earlyModel && element.selectionStart === 4") expect(page.locator('#settings-unsaved-indicator')).to_have_class(re.compile('is-visible')) expect(page.locator('#btn-save-settings')).to_be_enabled()