mirror of
https://github.com/razzant/ouroboros.git
synced 2026-10-03 04:07:04 +00:00
225 lines
11 KiB
Python
225 lines
11 KiB
Python
"""Editable model chooser on real Settings/actor/reviewer consumers, no model calls."""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
|
|
import pytest
|
|
|
|
from tests import test_subscription_role_routes_browser as roles
|
|
|
|
pytestmark = [pytest.mark.ui_browser, pytest.mark.serial]
|
|
subscription_ui = roles.subscription_ui
|
|
role_ui = roles.role_ui
|
|
|
|
|
|
def catalog(ui):
|
|
value = ui["fixture"]["catalog"]
|
|
value["items"] = [{"value": f"choice-{i:02}", "id": f"choice-{i:02}"} for i in range(30)]
|
|
return value
|
|
|
|
|
|
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'}}]
|
|
else: slots['deep_review'] = {'route': {'kind': 'api_chat', 'target_id': 'owner/model'}}
|
|
ui['settings']['OUROBOROS_REVIEWER_SLOTS'] = json.dumps(slots)
|
|
data = catalog(ui)
|
|
page = roles.open_agents(ui)
|
|
page.evaluate("""detail => document.dispatchEvent(new CustomEvent(
|
|
'settings-model-catalog:updated', {detail}))""", data)
|
|
if consumer in ["Models", "Fallback"]:
|
|
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(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(lane)
|
|
return page, row.locator('[data-slot-custom-api], [data-advisory-api-model]')
|
|
|
|
|
|
@pytest.mark.parametrize("consumer", ["Models", "Fallback", "Actor", "Triad", "Scope", "Advisory", "Deep"])
|
|
def test_chooser_keyboard_free_values_and_catalog_identity(role_ui, consumer):
|
|
page, field = select_model_field(role_ui, consumer)
|
|
field.fill('choice')
|
|
popup = page.locator('[id="' + field.get_attribute('aria-controls') + '"]')
|
|
assert popup.is_visible()
|
|
assert popup.locator('[role="option"]').count() == 30
|
|
field.press('ArrowDown')
|
|
assert field.get_attribute('aria-activedescendant')
|
|
field.press('Escape')
|
|
assert field.input_value() == 'choice'
|
|
assert not popup.is_visible()
|
|
field.press('ArrowDown')
|
|
field.press('Enter')
|
|
assert field.input_value() == 'choice-00'
|
|
assert field.get_attribute('aria-expanded') == 'false'
|
|
field.fill('owner/new-unknown-id')
|
|
field.evaluate("""e => { window.keptModelField = e; e.setSelectionRange(3, 7);
|
|
e.dispatchEvent(new CompositionEvent('compositionstart', {bubbles:true})); }""")
|
|
page.evaluate("""() => document.dispatchEvent(new CustomEvent(
|
|
'settings-model-catalog:updated', {detail:{items:[],model_sources:[]}}))""")
|
|
assert field.evaluate('e => e === window.keptModelField && document.activeElement === e')
|
|
assert field.evaluate('e => [e.selectionStart,e.selectionEnd]') == [3, 7]
|
|
assert field.input_value() == 'owner/new-unknown-id'
|
|
field.evaluate("e => e.dispatchEvent(new CompositionEvent('compositionend', {bubbles:true}))")
|
|
field.press('Tab')
|
|
assert field.input_value() == 'owner/new-unknown-id'
|
|
assert not popup.is_visible()
|
|
roles.capture(page, f"chooser-{consumer.lower()}-unknown-kept")
|
|
|
|
|
|
def test_agent_session_uses_same_chooser_and_keeps_native_short_choices(role_ui):
|
|
roles.configure_mixed(role_ui)
|
|
page = roles.open_agents(role_ui)
|
|
row = page.locator('[data-subagent-row]').nth(2)
|
|
model = row.locator('[data-subagent-field="model"]')
|
|
assert model.evaluate('e => e.tagName') == 'INPUT'
|
|
assert model.get_attribute('role') == 'combobox'
|
|
for field in ['route', 'account', 'effort']:
|
|
assert row.locator(f'[data-subagent-field="{field}"]').evaluate('e => e.tagName') == 'SELECT'
|
|
model.fill('future-model')
|
|
page.evaluate("""detail => document.dispatchEvent(new CustomEvent(
|
|
'settings-model-catalog:updated', {detail}))""", role_ui['fixture']['catalog'])
|
|
assert model.input_value() == 'future-model'
|
|
assert row.locator('[data-subagent-field="account"]').input_value() == 'personal'
|
|
assert page.locator('[data-deep-review-route]').input_value() == 'subagent:native'
|
|
roles.capture(page, 'chooser-session-reference-preserved')
|
|
|
|
|
|
def test_incomplete_reviewer_holds_whole_settings_save_until_corrected(role_ui):
|
|
roles.configure_mixed(role_ui)
|
|
page = roles.open_agents(role_ui)
|
|
model = page.locator('[data-slot-id="triad_1"] [data-slot-custom-api]')
|
|
model.fill('')
|
|
page.locator('#btn-save-settings').click()
|
|
assert not [path for path, _ in role_ui['posts'] if path == '/api/settings']
|
|
assert model.input_value() == ''
|
|
assert model.get_attribute('aria-invalid') == 'true'
|
|
model.fill('owner-unknown-review-model')
|
|
assert model.get_attribute('aria-invalid') == 'false'
|
|
with page.expect_response('**/api/settings'):
|
|
page.locator('#btn-save-settings').click()
|
|
writes = [payload for path, payload in role_ui['posts'] if path == '/api/settings']
|
|
assert len(writes) == 1
|
|
roles.capture(page, 'chooser-reviewer-corrected-save')
|
|
|
|
|
|
@pytest.fixture
|
|
def touch_role_ui(monkeypatch, request):
|
|
monkeypatch.setenv('OUROBOROS_UI_HAS_TOUCH', '1')
|
|
return request.getfixturevalue('role_ui')
|
|
|
|
|
|
def test_chooser_touch_selection(touch_role_ui):
|
|
page, field = select_model_field(touch_role_ui, 'Triad')
|
|
page.set_viewport_size({'width': 390, 'height': 600})
|
|
field.tap()
|
|
field.fill('choice-')
|
|
popup = page.locator('[id="' + field.get_attribute('aria-controls') + '"]')
|
|
popup.locator('[data-model-value="choice-01"]').tap()
|
|
assert field.input_value() == 'choice-01'
|
|
assert field.get_attribute('aria-expanded') == 'false'
|
|
roles.capture(page, 'chooser-touch-selected')
|
|
|
|
|
|
def test_partial_numeric_context_blocks_save_and_preserves_auto(role_ui):
|
|
roles.configure_mixed(role_ui)
|
|
role_ui['settings']['OUROBOROS_MODEL_CONTEXT_WINDOWS'] = {'main': 8192}
|
|
page = roles.open_agents(role_ui)
|
|
page.locator('[data-settings-tab="models"]').click()
|
|
row = page.locator('[data-model-role="main"]')
|
|
row.locator('summary').click()
|
|
field = row.locator('[data-model-role-context]')
|
|
assert field.input_value() == '8192'
|
|
field.fill('')
|
|
field.press('1')
|
|
field.press('e')
|
|
assert field.evaluate('e => e.validity.badInput')
|
|
page.locator('#btn-save-settings').click()
|
|
assert not [path for path, _ in role_ui['posts'] if path == '/api/settings']
|
|
assert field.get_attribute('aria-invalid') == 'true'
|
|
field.fill('16384')
|
|
assert field.get_attribute('aria-invalid') == 'false'
|
|
with page.expect_response('**/api/settings'):
|
|
page.locator('#btn-save-settings').click()
|
|
saved = [payload for path, payload in role_ui['posts'] if path == '/api/settings'][-1]
|
|
assert saved['OUROBOROS_MODEL_CONTEXT_WINDOWS']['main'] == 16384
|
|
page.wait_for_function("() => !document.querySelector('#btn-save-settings').disabled")
|
|
row.locator('summary').click()
|
|
field.fill('')
|
|
assert not field.evaluate('e => e.validity.badInput')
|
|
with page.expect_response('**/api/settings'):
|
|
page.locator('#btn-save-settings').click()
|
|
saved = [payload for path, payload in role_ui['posts'] if path == '/api/settings'][-1]
|
|
assert saved['OUROBOROS_MODEL_CONTEXT_WINDOWS']['main'] == 0
|
|
roles.capture(page, 'numeric-context-corrected-auto')
|
|
page.wait_for_function("() => !document.querySelector('#btn-save-settings').disabled")
|
|
row.locator('summary').click()
|
|
field.press('1')
|
|
field.press('e')
|
|
assert field.evaluate('e => e.validity.badInput')
|
|
assert 'is-visible' in page.locator('#settings-unsaved-indicator').get_attribute('class')
|
|
|
|
|
|
def test_chooser_scrolls_with_mobile_keyboard_boundary(role_ui):
|
|
page = role_ui['page']
|
|
page.set_viewport_size({'width': 390, 'height': 844})
|
|
page.add_init_script("""(() => {
|
|
const viewport = new EventTarget();
|
|
Object.assign(viewport, {width:390,height:844,offsetLeft:0,offsetTop:0,scale:1});
|
|
Object.defineProperty(window,'visualViewport',{value:viewport,configurable:true});
|
|
window.testViewport = viewport;
|
|
})();""")
|
|
page, field = select_model_field(role_ui, 'Triad')
|
|
field.click()
|
|
field.fill('choice')
|
|
popup = page.locator('[id="' + field.get_attribute('aria-controls') + '"]')
|
|
page.evaluate("""() => { testViewport.height=400;testViewport.dispatchEvent(new Event('resize')); }""")
|
|
page.wait_for_function("document.body.classList.contains('keyboard-open')")
|
|
assert popup.evaluate('e => e.scrollHeight > e.clientHeight')
|
|
def gesture(target):
|
|
return target.evaluate("""node => {
|
|
for (const [type,y] of [['touchstart',220],['touchmove',170]]) {
|
|
const event=new Event(type,{bubbles:true,cancelable:true});
|
|
Object.defineProperty(event,'touches',{value:[{clientX:100,clientY:y}]});
|
|
node.dispatchEvent(event);
|
|
if(type==='touchmove') return event.defaultPrevented;
|
|
}
|
|
}""")
|
|
assert gesture(popup) is False
|
|
assert gesture(page.locator('#page-settings .app-page-header')) is True
|
|
assert field.evaluate('e => document.activeElement === e')
|
|
roles.capture(page, 'chooser-keyboard-touch-scroll')
|
|
|
|
|
|
@pytest.mark.parametrize("width,height", [(320, 480), (390, 600), (640, 360), (641, 540),
|
|
(760, 600), (768, 600), (980, 600), (981, 540), (1440, 900), (1920, 900)])
|
|
def test_chooser_popup_reachable_in_narrow_short_view(role_ui, width, height):
|
|
page, field = select_model_field(role_ui, 'Triad')
|
|
page.set_viewport_size({'width': width, 'height': height})
|
|
field.click()
|
|
field.fill('choice')
|
|
popup = page.locator('[id="' + field.get_attribute('aria-controls') + '"]')
|
|
assert popup.is_visible()
|
|
box = popup.bounding_box()
|
|
roles.capture(page, f'chooser-open-{width}-{height}')
|
|
assert box and box['x'] >= 0 and box['y'] >= 0
|
|
assert box['x'] + box['width'] <= width + 1
|
|
assert box['y'] + box['height'] <= height + 1, {'popup': box, 'anchor': field.bounding_box()}
|
|
option = popup.locator('[role="option"]').nth(29)
|
|
option.scroll_into_view_if_needed()
|
|
option.click()
|
|
assert field.input_value() == 'choice-29'
|
|
roles.capture(page, f'chooser-narrow-{width}-{height}')
|