1277 lines
53 KiB
Python
1277 lines
53 KiB
Python
"""Static-asset smoke tests for onboarding-aware WebUI views."""
|
|
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parents[2] / "src/opensquilla/gateway"
|
|
VIEWS = ROOT / "static/js/views"
|
|
TEMPLATE = ROOT / "templates/index.html"
|
|
APP = ROOT / "static/js/app.js"
|
|
VUE_WEB_ROUTES = Path("opensquilla-webui/src/router/webRoutes.ts")
|
|
VUE_SETTINGS_VIEW = Path("opensquilla-webui/src/views/web/SettingsView.vue")
|
|
|
|
|
|
def test_channels_view_is_read_only_status_surface():
|
|
txt = (VIEWS / "channels.js").read_text(encoding="utf-8")
|
|
assert "channels.status" in txt
|
|
assert "onboarding.catalog" not in txt
|
|
assert "onboarding.channel.upsert" not in txt
|
|
assert "onboarding.channel.remove" not in txt
|
|
assert "onboarding.channel.enable" not in txt
|
|
assert "onboarding.channel.disable" not in txt
|
|
assert "Add channel" not in txt
|
|
assert "Save channel" not in txt
|
|
assert "data-ch-remove" not in txt
|
|
assert "data-ch-toggle" not in txt
|
|
assert "data-ch-logout" not in txt
|
|
assert "channels.logout" not in txt
|
|
assert "channels.restart" not in txt
|
|
|
|
|
|
def test_channels_view_points_configuration_to_cli_onboarding():
|
|
txt = (VIEWS / "channels.js").read_text(encoding="utf-8")
|
|
assert "opensquilla channels list" in txt
|
|
assert "opensquilla onboard configure channels" in txt
|
|
|
|
|
|
def test_channels_stats_do_not_report_attention_states_as_healthy():
|
|
txt = (VIEWS / "channels.js").read_text(encoding="utf-8")
|
|
assert "all healthy" not in txt
|
|
assert "need attention" in txt
|
|
assert "restarting" in txt
|
|
assert "exhausted" in txt
|
|
|
|
|
|
def test_channels_view_filters_to_configured_channels():
|
|
txt = (VIEWS / "channels.js").read_text(encoding="utf-8")
|
|
assert "configured !== false" in txt
|
|
|
|
|
|
def test_channels_load_stops_if_view_is_destroyed_while_waiting_for_rpc():
|
|
txt = (VIEWS / "channels.js").read_text(encoding="utf-8")
|
|
start = txt.index("async function _loadData()")
|
|
end = txt.index(" function _renderStats", start)
|
|
body = txt[start:end]
|
|
|
|
assert "const rpc = _rpc;" in body
|
|
assert "await rpc.waitForConnection();" in body
|
|
assert "if (!_el || _rpc !== rpc) return;" in body
|
|
|
|
|
|
def test_setup_view_loads_catalog_and_status():
|
|
txt = (VIEWS / "setup.js").read_text(encoding="utf-8")
|
|
assert "onboarding.catalog" in txt
|
|
assert "onboarding.status" in txt
|
|
assert "config.get" in txt
|
|
assert "onboarding.provider.configure" in txt
|
|
assert "onboarding.search.configure" in txt
|
|
assert "onboarding.imageGeneration.configure" in txt
|
|
assert "onboarding.audio.configure" in txt
|
|
assert "imageGenerationProviders" in txt
|
|
assert "audioProviders" in txt
|
|
assert "onboarding.memory_embedding.configure" in txt
|
|
assert "Fallback API key" in txt
|
|
assert "data-memory-api-key-label" in txt
|
|
assert "effectiveProvider" in txt
|
|
assert "current.mode" in txt
|
|
|
|
|
|
def test_setup_view_is_available_and_uses_canonical_cli_fallbacks():
|
|
txt = (VIEWS / "setup.js").read_text(encoding="utf-8")
|
|
assert "SETUP_UI_AVAILABLE" not in txt
|
|
assert "opensquilla onboard" in txt
|
|
assert "opensquilla onboard catalog providers" in txt
|
|
assert "opensquilla providers configure" not in txt
|
|
assert "onboarding.router.configure" in txt
|
|
assert "onboarding.channel.probe" in txt
|
|
assert "channels.status" in txt
|
|
assert "Connected" in txt
|
|
|
|
|
|
def test_setup_view_locks_image_model_image_support():
|
|
txt = (VIEWS / "setup.js").read_text(encoding="utf-8")
|
|
|
|
assert "const isImageModel = name === 'image_model';" in txt
|
|
assert "isImageModel ? ' checked disabled' :" in txt
|
|
assert "tier.supportsImage = true;" in txt
|
|
assert "tier.image_only = true;" in txt
|
|
|
|
|
|
def test_setup_finish_cli_commands_target_active_config_path():
|
|
txt = (VIEWS / "setup.js").read_text(encoding="utf-8")
|
|
start = txt.index("function _renderFinishStep()")
|
|
end = txt.index(" function _renderReadinessSummary", start)
|
|
body = txt[start:end]
|
|
|
|
assert "const configArg = _configCliArg(_status.configPath)" in body
|
|
assert "function _configCliArg" in txt
|
|
assert "function _shellArg" in txt
|
|
assert "Guided CLI" in body
|
|
assert "`opensquilla onboard --if-needed${configArg}`" in body
|
|
assert "Check status" in body
|
|
assert "`opensquilla onboard status${configArg}`" in body
|
|
assert "Provider options" in body
|
|
assert "`opensquilla onboard catalog providers${configArg}`" in body
|
|
assert "Router tiers" in body
|
|
assert "`opensquilla onboard catalog router${configArg}`" in body
|
|
assert "Search options" in body
|
|
assert "`opensquilla onboard catalog search${configArg}`" in body
|
|
assert "Channel options" in body
|
|
assert "`opensquilla onboard catalog channels${configArg}`" in body
|
|
assert "Image options" in body
|
|
assert "`opensquilla onboard catalog image${configArg}`" in body
|
|
assert "Memory options" in body
|
|
assert "`opensquilla onboard catalog memory${configArg}`" in body
|
|
assert "Catalog overview" not in body
|
|
assert "`opensquilla onboard catalog${configArg}`" not in body
|
|
assert "Channel field guide" not in body
|
|
assert "opensquilla channels describe <type>" not in body
|
|
assert "<type>" not in body
|
|
assert "const envRecoveryCommands = Array.isArray(_status.envRecoveryCommands)" in body
|
|
assert "const fixCommands = _envFixCommands(envRecoveryCommands, configArg)" in body
|
|
assert "const handoffCommands = [" in body
|
|
assert "const recipeCommands = [" in body
|
|
assert "_renderCliCommandGroup('Fix now', fixCommands)" in body
|
|
assert "_renderCliCommandGroup('CLI handoff', handoffCommands)" in body
|
|
assert "_renderCliCommandGroup('CLI recipes', recipeCommands)" in body
|
|
|
|
|
|
def test_setup_finish_groups_recovery_commands_before_cli_reference():
|
|
txt = (VIEWS / "setup.js").read_text(encoding="utf-8")
|
|
css = (ROOT / "static/css/views/setup.css").read_text(encoding="utf-8")
|
|
start = txt.index("function _renderFinishStep()")
|
|
end = txt.index(" function _renderCliCommand", start)
|
|
body = txt[start:end]
|
|
|
|
assert "const fixCommands = _envFixCommands(envRecoveryCommands, configArg)" in body
|
|
assert "const handoffCommands = [" in body
|
|
assert "const recipeCommands = [" in body
|
|
assert "Fix now" in body
|
|
assert "CLI handoff" in body
|
|
assert "CLI recipes" in body
|
|
assert (
|
|
"_renderCliCommandGroup('Fix now', fixCommands)"
|
|
in body
|
|
)
|
|
assert (
|
|
"_renderCliCommandGroup('CLI handoff', handoffCommands)"
|
|
in body
|
|
)
|
|
assert (
|
|
"_renderCliCommandGroup('CLI recipes', recipeCommands)"
|
|
in body
|
|
)
|
|
assert body.index("_renderCliCommandGroup('Fix now'") < body.index(
|
|
"_renderCliCommandGroup('CLI handoff'"
|
|
)
|
|
assert body.index("_renderCliCommandGroup('CLI handoff'") < body.index(
|
|
"_renderCliCommandGroup('CLI recipes'"
|
|
)
|
|
assert "function _renderCliCommandGroup" in txt
|
|
assert "setup-cli__group" in txt
|
|
assert ".setup-cli__group" in css
|
|
assert ".setup-cli__group-head" in css
|
|
|
|
|
|
def test_setup_finish_prioritizes_cli_actions_before_configuration_details():
|
|
txt = (VIEWS / "setup.js").read_text(encoding="utf-8")
|
|
start = txt.index("function _renderFinishStep()")
|
|
end = txt.index(" function _renderCliCommand", start)
|
|
body = txt[start:end]
|
|
|
|
assert body.index('<div class="setup-cli">') < body.index('<div class="setup-summary">')
|
|
assert body.index('<div class="setup-cli">') < body.index("_renderReadinessSummary()")
|
|
|
|
|
|
def test_setup_finish_pairs_env_recovery_with_gateway_restart_command():
|
|
txt = (VIEWS / "setup.js").read_text(encoding="utf-8")
|
|
start = txt.index("function _renderFinishStep()")
|
|
end = txt.index(" function _renderReadinessSummary", start)
|
|
body = txt[start:end]
|
|
|
|
assert "const fixCommands = _envFixCommands(envRecoveryCommands, configArg)" in body
|
|
assert "function _envFixCommands" in txt
|
|
assert "if (!envRecoveryCommands.length) return [];" in txt
|
|
assert "label: 'Restart gateway after env fix'" in txt
|
|
assert "command: `opensquilla gateway restart${configArg}`" in txt
|
|
assert body.index("const envRecoveryCommands = Array.isArray") < body.index(
|
|
"const fixCommands = _envFixCommands(envRecoveryCommands, configArg)"
|
|
)
|
|
|
|
|
|
def test_setup_finish_cli_commands_are_provider_neutral():
|
|
txt = (VIEWS / "setup.js").read_text(encoding="utf-8")
|
|
start = txt.index("function _renderFinishStep()")
|
|
end = txt.index(" function _renderReadinessSummary", start)
|
|
body = txt[start:end]
|
|
|
|
assert "opensquilla onboard catalog providers" in body
|
|
assert "opensquilla onboard catalog search" in body
|
|
assert "opensquilla onboard catalog image" in body
|
|
assert "opensquilla onboard catalog memory" in body
|
|
assert "--search-provider <provider>" not in body
|
|
assert "--image-provider <provider>" not in body
|
|
assert "--memory-provider <provider>" not in body
|
|
assert "--api-key-env <ENV_NAME>" not in body
|
|
assert "--search-provider brave" not in body
|
|
assert "--image-provider openrouter" not in body
|
|
assert "--memory-provider openai" not in body
|
|
assert "OPENAI_API_KEY" not in body
|
|
assert "BRAVE_SEARCH_API_KEY" not in body
|
|
|
|
|
|
def test_setup_view_starts_on_most_relevant_step():
|
|
txt = (VIEWS / "setup.js").read_text(encoding="utf-8")
|
|
render_start = txt.index("async function render(el)")
|
|
render_end = txt.index(" async function _load()", render_start)
|
|
render_body = txt[render_start:render_end]
|
|
destroy_start = txt.index("function destroy()")
|
|
destroy_body = txt[destroy_start:]
|
|
|
|
assert "let _hasAutoSelectedStep = false" in txt
|
|
assert "function _selectInitialStep()" in txt
|
|
assert "function _initialStepFromStatus()" in txt
|
|
assert "await _load();\n _selectInitialStep();" in render_body
|
|
assert render_body.index("await _load();") < render_body.index("_selectInitialStep();")
|
|
assert txt.index("const entry = sectionSteps.find") < txt.index(
|
|
"if (_status.needsOnboarding === false) return 'finish';"
|
|
)
|
|
assert "if (_status.needsOnboarding === false) return 'finish';" in txt
|
|
assert "detail.actionRequired" in txt
|
|
assert "['llm', 'provider']" in txt
|
|
assert "['router', 'router']" in txt
|
|
assert "['search', 'extras']" in txt
|
|
assert "['image_generation', 'extras']" in txt
|
|
assert "['audio', 'extras']" in txt
|
|
assert "['memory_embedding', 'extras']" in txt
|
|
assert "_step = 'provider';" in destroy_body
|
|
assert "_hasAutoSelectedStep = false;" in destroy_body
|
|
|
|
|
|
def test_setup_header_tracks_optional_action_required_sections():
|
|
txt = (VIEWS / "setup.js").read_text(encoding="utf-8")
|
|
draw_start = txt.index("function _draw()")
|
|
draw_end = txt.index(" function _renderOnboardingReasons()", draw_start)
|
|
draw_body = txt[draw_start:draw_end]
|
|
reasons_start = txt.index("function _onboardingReasons()")
|
|
reasons_end = txt.index(" function _renderCurrentStep()", reasons_start)
|
|
reasons_body = txt[reasons_start:reasons_end]
|
|
|
|
assert "function _hasSetupAction()" in txt
|
|
assert "const setupAction = _hasSetupAction();" in draw_body
|
|
assert "${setupAction ? 'Action needed' : 'Ready to run'}" in draw_body
|
|
assert "setup__status ${setupAction ? 'is-warn' : 'is-ok'}" in draw_body
|
|
assert "${setupAction ? 'Action needed' : 'Ready'}" in draw_body
|
|
assert "if (!_hasSetupAction()) return [];" in reasons_body
|
|
assert "if (!detail.blocking && !detail.actionRequired)" in reasons_body
|
|
|
|
|
|
def test_setup_finish_summarizes_provider_proxy_only_when_present():
|
|
txt = (VIEWS / "setup.js").read_text(encoding="utf-8")
|
|
start = txt.index("function _renderFinishStep()")
|
|
end = txt.index(" function _renderReadinessSummary", start)
|
|
body = txt[start:end]
|
|
|
|
assert (
|
|
"const providerProxy = configuredProvider "
|
|
"? ((_config.llm || {}).proxy || '').trim() : ''"
|
|
) in body
|
|
assert "providerProxy ?" in body
|
|
assert "<span>Proxy</span>" in body
|
|
|
|
|
|
def test_setup_finish_cli_commands_are_copyable():
|
|
txt = (VIEWS / "setup.js").read_text(encoding="utf-8")
|
|
css = (ROOT / "static/css/views/setup.css").read_text(encoding="utf-8")
|
|
|
|
assert "function _renderCliCommand" in txt
|
|
assert "const copyLabel = `Copy ${label} command`" in txt
|
|
assert "data-setup-copy-command" in txt
|
|
assert 'title="${_esc(copyLabel)}"' in txt
|
|
assert 'aria-label="${_esc(copyLabel)}"' in txt
|
|
assert 'title="Copy command"' not in txt
|
|
assert 'aria-label="Copy command"' not in txt
|
|
assert "navigator.clipboard.writeText" in txt
|
|
assert "Copied command" in txt
|
|
assert "setup-cli__copy" in txt
|
|
assert "setup-cli__label" in txt
|
|
assert "${icons.copy()}" in txt
|
|
assert ".setup-cli__copy" in css
|
|
assert ".setup-cli__label" in css
|
|
|
|
|
|
def test_setup_finish_cli_commands_stack_labels_on_mobile():
|
|
css = (ROOT / "static/css/views/setup.css").read_text(encoding="utf-8")
|
|
mobile = css.split("@media (max-width: 560px)", 1)[1]
|
|
|
|
assert ".setup-cli__row" in mobile
|
|
assert "grid-template-columns: minmax(0, 1fr) 34px" in mobile
|
|
assert ".setup-cli__label" in mobile
|
|
assert "grid-column: 1 / -1" in mobile
|
|
|
|
|
|
def test_setup_view_keeps_channel_fields_in_config_shape():
|
|
txt = (VIEWS / "setup.js").read_text(encoding="utf-8")
|
|
assert "scope === 'channel' ? label.dataset.name : _camel" in txt
|
|
|
|
|
|
def test_setup_view_renders_catalog_field_descriptions():
|
|
txt = (VIEWS / "setup.js").read_text(encoding="utf-8")
|
|
assert "field.description" in txt
|
|
assert "setup-field-desc" in txt
|
|
|
|
|
|
def test_setup_view_surfaces_catalog_requirements_before_fields():
|
|
txt = (VIEWS / "setup.js").read_text(encoding="utf-8")
|
|
|
|
assert "function _renderNeedList" in txt
|
|
assert "setup-need-list" in txt
|
|
assert "spec.whatYouNeed" in txt
|
|
assert "searchSpec.whatYouNeed" in txt
|
|
assert "_memoryNeedList(memorySpec" in txt
|
|
assert "imageSpec.whatYouNeed" in txt
|
|
assert "channelSpec.whatYouNeed" in txt
|
|
|
|
|
|
def test_setup_view_ties_need_lists_to_selected_env_keys():
|
|
txt = (VIEWS / "setup.js").read_text(encoding="utf-8")
|
|
|
|
assert "function _credentialNeedList" in txt
|
|
assert "_credentialNeedList(searchSpec.whatYouNeed, searchEnv || searchSpec.envKey)" in txt
|
|
assert "_credentialNeedList(imageSpec.whatYouNeed, imageEnv || imageSpec.envKey)" in txt
|
|
assert "envInput.value = spec.requiresApiKey ? (spec.envKey || '') : ''" in txt
|
|
assert "_credentialNeedList(spec.whatYouNeed, envInput?.value || spec.envKey)" in txt
|
|
|
|
|
|
def test_setup_provider_step_is_neutral_before_provider_choice():
|
|
txt = (VIEWS / "setup.js").read_text(encoding="utf-8")
|
|
css = (ROOT / "static/css/views/setup.css").read_text(encoding="utf-8")
|
|
start = txt.index("function _renderProviderStep()")
|
|
end = txt.index(" function _providerConfigFor", start)
|
|
body = txt[start:end]
|
|
sync_start = txt.index("function _drawProviderFields")
|
|
sync_end = txt.index(" function _drawChannelFields", sync_start)
|
|
sync_body = txt[sync_start:sync_end]
|
|
save_start = txt.index("async function _saveProvider()")
|
|
save_end = txt.index(" async function _saveRouter()", save_start)
|
|
save_body = txt[save_start:save_end]
|
|
|
|
assert "const selected = _effectiveProvider();" in body
|
|
assert "Choose from ${providers.length} supported providers" in body
|
|
assert (
|
|
'<option value="" disabled${selected ? \'\' : \' selected\'}>'
|
|
"Choose a provider</option>"
|
|
) in body
|
|
assert "const spec = selected ? providers.find" in body
|
|
assert "selected ? spec.whatYouNeed : ['Choose a provider to see required fields.']" in body
|
|
assert "data-save-provider${selected ? '' : ' disabled'}" in body
|
|
assert 'data-next="router"${selected ? \'\' : \' disabled\'}' in body
|
|
assert "saveButton.disabled = !providerId" in sync_body
|
|
assert 'const nextButton = _el.querySelector(\'[data-next="router"]\')' in sync_body
|
|
assert "if (nextButton) nextButton.disabled = !providerId" in sync_body
|
|
assert "if (!providerId)" in save_body
|
|
assert "Choose a provider before saving." in save_body
|
|
assert ".setup-btn:disabled" in css
|
|
assert "cursor: not-allowed" in css
|
|
|
|
|
|
def test_setup_view_declutters_memory_auto_requirements():
|
|
txt = (VIEWS / "setup.js").read_text(encoding="utf-8")
|
|
|
|
assert "function _memoryNeedList" in txt
|
|
assert "_memoryNeedList(memorySpec, effectiveProvider, memoryEnv)" in txt
|
|
assert "!/remote fallback credentials/i.test(item)" in txt
|
|
|
|
|
|
def test_setup_view_collapses_search_advanced_options_by_default():
|
|
txt = (VIEWS / "setup.js").read_text(encoding="utf-8")
|
|
|
|
assert "const searchAdvancedOpen" in txt
|
|
assert 'details class="setup-mini__advanced" data-search-advanced${searchAdvancedOpen}' in txt
|
|
assert "<summary>Advanced search options</summary>" in txt
|
|
assert "setup-mini__advanced-body" in txt
|
|
assert "searchFallbackPolicy !== 'off'" in txt
|
|
assert "setup_search_proxy" in txt
|
|
|
|
|
|
def test_setup_view_collapses_provider_connection_options_by_default():
|
|
txt = (VIEWS / "setup.js").read_text(encoding="utf-8")
|
|
|
|
assert "const providerCoreFields" in txt
|
|
assert "const providerAdvancedFields" in txt
|
|
assert "const providerAdvancedOpen" in txt
|
|
assert "_isProviderAdvancedField(field, spec)" in txt
|
|
assert (
|
|
"_renderProviderAdvancedFields(providerAdvancedFields, values, providerAdvancedOpen)"
|
|
in txt
|
|
)
|
|
assert 'details class="setup-mini__advanced" data-provider-advanced${openAttr}' in txt
|
|
assert "<summary>Advanced provider connection</summary>" in txt
|
|
assert "setup-provider-fields" in txt
|
|
assert "Provider connection" in txt
|
|
assert "base_url" in txt
|
|
assert "proxy" in txt
|
|
|
|
|
|
def test_setup_provider_advanced_opens_for_required_or_custom_connection():
|
|
txt = (VIEWS / "setup.js").read_text(encoding="utf-8")
|
|
|
|
assert "function _providerAdvancedOpen" in txt
|
|
assert "if (field.required) return true;" in txt
|
|
assert "value !== defaultValue" in txt
|
|
assert "return value.length > 0;" in txt
|
|
|
|
|
|
def test_setup_view_moves_optional_router_model_into_provider_advanced_connection():
|
|
txt = (VIEWS / "setup.js").read_text(encoding="utf-8")
|
|
|
|
assert "_isProviderAdvancedField(field, spec)" in txt
|
|
assert "field.name === 'model'" in txt
|
|
assert "spec.routerSupported === true" in txt
|
|
assert "field.required !== true" in txt
|
|
assert (
|
|
"providerCoreFields = providerFields.filter(field => "
|
|
"!_isProviderAdvancedField(field, spec))"
|
|
) in txt
|
|
assert (
|
|
"providerAdvancedFields = providerFields.filter(field => "
|
|
"_isProviderAdvancedField(field, spec))"
|
|
) in txt
|
|
assert "if (defaultValue) return value !== defaultValue;" in txt
|
|
|
|
|
|
def test_setup_view_keeps_required_provider_model_in_core_fields():
|
|
txt = (VIEWS / "setup.js").read_text(encoding="utf-8")
|
|
|
|
model_start = txt.index("if (field.name === 'model')")
|
|
model_clause = txt[model_start : txt.index("return false;", model_start)]
|
|
assert "field.required !== true" in model_clause
|
|
assert "spec.routerSupported === true" in model_clause
|
|
|
|
|
|
def test_setup_provider_switch_redraws_core_and_advanced_fields_together():
|
|
txt = (VIEWS / "setup.js").read_text(encoding="utf-8")
|
|
|
|
start = txt.index("function _drawProviderFields")
|
|
body = txt[start : txt.index(" function _drawChannelFields", start)]
|
|
assert "const providerFields = spec.fields || []" in body
|
|
assert "const providerCoreFields" in body
|
|
assert "const providerAdvancedFields" in body
|
|
assert "box.innerHTML = _renderProviderFields(providerCoreFields" in body
|
|
assert "advanced.innerHTML = _renderProviderAdvancedFields(" in body
|
|
assert "_renderProviderFields(spec," not in body
|
|
|
|
|
|
def test_setup_view_warns_when_env_key_is_not_visible_to_gateway():
|
|
txt = (VIEWS / "setup.js").read_text(encoding="utf-8")
|
|
css = (ROOT / "static/css/views/setup.css").read_text(encoding="utf-8")
|
|
|
|
assert "missing_env" in txt
|
|
assert "not visible to this gateway process" in txt
|
|
assert "Set it before starting or restarting the gateway" in txt
|
|
assert "if (_providerEnvMissing())" in txt
|
|
assert "function _providerEnvRecoveryCommand()" in txt
|
|
assert "Array.isArray(_status.envRecoveryCommands)" in txt
|
|
assert "return _envRecoveryCommand('llm')" in txt
|
|
assert "_providerEnvRecoveryCommand()" in txt
|
|
assert "setup-warning__command" in txt
|
|
assert 'data-setup-copy-command="${safeCommand}"' in txt
|
|
assert "Copy set provider key command" in txt
|
|
assert ".setup-warning__command" in css
|
|
assert ".setup-warning__command code" in css
|
|
|
|
|
|
def test_setup_view_does_not_default_env_key_over_stored_provider_key():
|
|
txt = (VIEWS / "setup.js").read_text(encoding="utf-8")
|
|
assert "current.api_key ? '' : field.default" in txt
|
|
|
|
|
|
def test_setup_provider_form_can_submit_api_key_env():
|
|
txt = (VIEWS / "setup.js").read_text(encoding="utf-8")
|
|
assert "api_key_env" in txt
|
|
assert "_camel(label.dataset.name)" in txt
|
|
assert "onboarding.provider.configure" in txt
|
|
|
|
|
|
def test_setup_provider_controls_have_browser_field_names():
|
|
txt = (VIEWS / "setup.js").read_text(encoding="utf-8")
|
|
assert 'name="setup_provider"' in txt
|
|
assert "const fieldName = `setup_${scope}_${rawName}`" in txt
|
|
assert 'for="${_esc(fieldId)}"' in txt
|
|
assert 'id="${_esc(fieldId)}"' in txt
|
|
assert 'name="${_esc(fieldName)}"' in txt
|
|
|
|
|
|
def test_setup_provider_switch_refreshes_provider_defaults():
|
|
txt = (VIEWS / "setup.js").read_text(encoding="utf-8")
|
|
assert "function _providerConfigFor(providerId)" in txt
|
|
assert "current.provider === providerId ? current : {}" in txt
|
|
assert 'data-provider-summary' in txt
|
|
assert "_drawProviderFields({ rememberDraft: true })" in txt
|
|
|
|
|
|
def test_setup_all_wizard_controls_have_browser_field_names():
|
|
txt = (VIEWS / "setup.js").read_text(encoding="utf-8")
|
|
|
|
for name in (
|
|
"setup_router_mode",
|
|
"setup_router_default_tier",
|
|
"setup_channel_type",
|
|
"setup_search_provider",
|
|
"setup_search_max_results",
|
|
"setup_search_api_key",
|
|
"setup_search_api_key_env",
|
|
"setup_search_proxy",
|
|
"setup_search_use_env_proxy",
|
|
"setup_search_fallback_policy",
|
|
"setup_search_diagnostics",
|
|
"setup_memory_provider",
|
|
"setup_memory_model",
|
|
"setup_memory_api_key",
|
|
"setup_memory_api_key_env",
|
|
"setup_memory_base_url",
|
|
"setup_memory_onnx_dir",
|
|
"setup_image_provider",
|
|
"setup_image_primary",
|
|
"setup_image_api_key",
|
|
"setup_image_api_key_env",
|
|
"setup_image_base_url",
|
|
"setup_image_enabled",
|
|
):
|
|
assert f'name="{name}"' in txt
|
|
|
|
assert "const tierFieldName = `setup_router_${name}_${field}`" in txt
|
|
assert 'name="${_esc(tierFieldName)}"' in txt
|
|
assert 'aria-label="${_esc(tierLabel)}' in txt
|
|
|
|
|
|
def test_setup_view_bounds_search_max_results_control():
|
|
txt = (VIEWS / "setup.js").read_text(encoding="utf-8")
|
|
start = txt.index('id="setup-search-max-results"')
|
|
end = txt.index('data-search-field="max_results"', start)
|
|
control = txt[start:end]
|
|
|
|
assert 'type="number"' in control
|
|
assert 'min="1"' in control
|
|
assert 'step="1"' in control
|
|
assert 'inputmode="numeric"' in control
|
|
|
|
|
|
def test_setup_view_preserves_selected_channel_type_while_redrawing_fields():
|
|
txt = (VIEWS / "setup.js").read_text(encoding="utf-8")
|
|
assert "let _channelType" in txt
|
|
assert "_channelType = type" in txt
|
|
assert "channels.some(c => c.type === _channelType)" in txt
|
|
|
|
|
|
def test_setup_view_rebinds_conditional_fields_after_dynamic_redraw():
|
|
txt = (VIEWS / "setup.js").read_text(encoding="utf-8")
|
|
assert "function _bindConditionalSelects" in txt
|
|
assert "_bindConditionalSelects(_el)" in txt
|
|
assert "_bindConditionalSelects(box || _el)" in txt
|
|
|
|
|
|
def test_setup_view_is_loaded_and_registered_but_not_sidebar_primary():
|
|
app = APP.read_text(encoding="utf-8")
|
|
web_routes = VUE_WEB_ROUTES.read_text(encoding="utf-8")
|
|
|
|
# Legacy static onboarding UI still registers the Setup view at /setup.
|
|
assert "_renderStandardView(SetupView, el)" in app
|
|
assert "Router.register('/setup'" in app
|
|
assert 'data-path="/setup"' not in app
|
|
# The Vue console consolidated setup into SettingsView: /settings renders it,
|
|
# and the legacy /setup deep link redirects into the settings overlay rather
|
|
# than registering a primary sidebar destination.
|
|
assert "SettingsView.vue" in web_routes
|
|
assert "path: '/settings'" in web_routes
|
|
assert "name: 'settings'" in web_routes
|
|
assert "redirect: '/settings/auto'" in web_routes
|
|
assert VUE_SETTINGS_VIEW.exists()
|
|
|
|
|
|
def test_setup_view_marks_unsupported_providers_disabled():
|
|
txt = (VIEWS / "setup.js").read_text(encoding="utf-8")
|
|
assert "runtimeSupported" in txt
|
|
|
|
|
|
def test_setup_view_validates_visible_required_channel_fields_before_save():
|
|
txt = (VIEWS / "setup.js").read_text(encoding="utf-8")
|
|
start = txt.index("function _saveChannel()")
|
|
end = txt.index(" async function _saveMemory()", start)
|
|
body = txt[start:end]
|
|
|
|
assert 'data-required="${field.required ? \'true\' : \'false\'}"' in txt
|
|
assert "function _validateScopedRequiredFields(scope)" in txt
|
|
assert "function _canKeepExistingSecret(scope)" in txt
|
|
assert "input.dataset.secret === 'true' && _canKeepExistingSecret(scope)" in txt
|
|
assert "row.configured !== false" in txt
|
|
assert "String(row.type || '') === String(type)" in txt
|
|
assert "String(row.name || '') === String(name).trim()" in txt
|
|
assert "_validateScopedRequiredFields('channel')" in body
|
|
assert "if (missing)" in body
|
|
assert "is required." in body
|
|
assert "return;" in body
|
|
|
|
|
|
def test_setup_view_treats_image_configure_as_capability_enable_action():
|
|
txt = (VIEWS / "setup.js").read_text(encoding="utf-8")
|
|
assert "field.default !== false" in txt
|
|
assert "imageGenerationEnabled === false" in txt
|
|
|
|
|
|
def test_setup_view_explains_image_generation_tool_visibility():
|
|
txt = (VIEWS / "setup.js").read_text(encoding="utf-8")
|
|
start = txt.index("function _imageGenerationStatusText")
|
|
end = txt.index(" function _memoryEmbeddingStatusText", start)
|
|
body = txt[start:end]
|
|
|
|
assert "Image generation is hidden from agents" in body
|
|
assert "Image generation will be available in new turns" in body
|
|
assert "_status.imageGenerationSource === 'missing_env'" in body
|
|
assert "_status.imageGenerationEnvKey" in body
|
|
assert "image_generate" not in body
|
|
assert "imageGenerationSource === 'llm_fallback'" in txt
|
|
assert "same provider key" in txt
|
|
|
|
|
|
def test_setup_view_uses_product_language_for_search_status_text():
|
|
txt = (VIEWS / "setup.js").read_text(encoding="utf-8")
|
|
start = txt.index("function _searchStatusText")
|
|
end = txt.index(" function _imageGenerationStatusText", start)
|
|
body = txt[start:end]
|
|
|
|
assert "Web search is off until a provider is selected." in body
|
|
assert "Web search is ready for new turns." in body
|
|
assert "_status.searchSource === 'missing_env'" in body
|
|
assert "_status.searchEnvKey" in body
|
|
assert "Web search is selected but still needs a visible provider key." in body
|
|
assert "web_search" not in body
|
|
|
|
|
|
def test_setup_view_names_missing_env_key_for_memory_embedding_status_text():
|
|
txt = (VIEWS / "setup.js").read_text(encoding="utf-8")
|
|
start = txt.index("function _memoryEmbeddingStatusText")
|
|
end = txt.index(" function _toastEnvReferenceSave", start)
|
|
body = txt[start:end]
|
|
|
|
assert "_status.memoryEmbeddingSource === 'missing_env'" in body
|
|
assert "_status.memoryEmbeddingEnvKey" in body
|
|
assert "Remote memory embeddings need a visible provider key before they can run." in body
|
|
|
|
|
|
def test_setup_capability_cards_offer_copyable_env_recovery_commands():
|
|
txt = (VIEWS / "setup.js").read_text(encoding="utf-8")
|
|
start = txt.index("function _renderExtrasStep()")
|
|
end = txt.index(" function _capabilitySaveButtonClass", start)
|
|
body = txt[start:end]
|
|
|
|
assert "function _capabilityEnvRecoveryCommand(section)" in txt
|
|
assert "function _renderCapabilityEnvRecoveryCommand(section)" in txt
|
|
assert "Array.isArray(_status.envRecoveryCommands)" in txt
|
|
assert "entry.section === section" in txt
|
|
assert "_renderCapabilityEnvRecoveryCommand('search')" in body
|
|
assert "_renderCapabilityEnvRecoveryCommand('memory_embedding')" in body
|
|
assert "_renderCapabilityEnvRecoveryCommand('image_generation')" in body
|
|
assert "_renderCapabilityEnvRecoveryCommand('audio')" in body
|
|
assert "Copy set search key command" in txt
|
|
assert "Copy set memory key command" in txt
|
|
assert "Copy set image key command" in txt
|
|
assert "Copy set audio key command" in txt
|
|
assert 'data-setup-copy-command="${safeCommand}"' in txt
|
|
|
|
|
|
def test_setup_view_exposes_image_generation_env_key_config():
|
|
txt = (VIEWS / "setup.js").read_text(encoding="utf-8")
|
|
assert "imageProviders.find(p => p.providerId === imageProviderSelected)" in txt
|
|
assert "imageProviderConfig.api_key_env" in txt
|
|
assert 'data-image-field="api_key_env"' in txt
|
|
assert "setup_image_api_key_env" in txt
|
|
assert "imageSpec.envKey" in txt
|
|
assert "_syncImageProviderDefaults" in txt
|
|
assert "[data-image-provider]')?.addEventListener('change', _syncImageProviderDefaults)" in txt
|
|
assert "primaryInput.value = spec.defaultModel || primaryInput.value" in txt
|
|
|
|
|
|
def test_setup_view_exposes_audio_provider_config():
|
|
txt = (VIEWS / "setup.js").read_text(encoding="utf-8")
|
|
assert "audioProviders.find(p => p.providerId === audioProviderSelected)" in txt
|
|
assert "audioProviderConfig.api_key_env" in txt
|
|
assert 'data-audio-field="api_key_env"' in txt
|
|
assert 'data-audio-field="tts_voice"' in txt
|
|
assert 'data-audio-field="tts_model"' in txt
|
|
assert 'data-audio-field="language_code"' in txt
|
|
assert "setup_audio_api_key_env" in txt
|
|
assert "audioSpec.envKey" in txt
|
|
assert "_syncAudioProviderDefaults" in txt
|
|
assert "[data-audio-provider]')?.addEventListener('change', _syncAudioProviderDefaults)" in txt
|
|
assert "const res = await _rpc.call('onboarding.audio.configure', params)" in txt
|
|
|
|
|
|
def test_setup_image_generation_hides_provider_fields_until_enabled():
|
|
txt = (VIEWS / "setup.js").read_text(encoding="utf-8")
|
|
start = txt.index("function _renderExtrasStep()")
|
|
end = txt.index(" function _capabilitySaveButtonClass", start)
|
|
body = txt[start:end]
|
|
|
|
assert "const imageConfigHidden = imageEnabledDefault ? '' : ' hidden'" in body
|
|
assert 'data-image-config-fields${imageConfigHidden}' in body
|
|
assert "[data-image-enabled]')?.addEventListener('change', _syncImageProviderDefaults)" in txt
|
|
|
|
sync_start = txt.index("function _syncImageProviderDefaults()")
|
|
sync_end = txt.index(" function _bindConditionalSelects", sync_start)
|
|
sync_body = txt[sync_start:sync_end]
|
|
assert "const enabledInput = _el.querySelector('[data-image-enabled]')" in sync_body
|
|
assert "const imageConfigFields = _el.querySelector('[data-image-config-fields]')" in sync_body
|
|
assert "imageConfigFields.hidden = !imageEnabled" in sync_body
|
|
|
|
|
|
def test_setup_view_exposes_image_generation_base_url_config():
|
|
txt = (VIEWS / "setup.js").read_text(encoding="utf-8")
|
|
|
|
assert "imageProviderConfig.base_url" in txt
|
|
assert "imageSpec.defaultBaseUrl" in txt
|
|
assert 'data-image-field="base_url"' in txt
|
|
assert "setup_image_base_url" in txt
|
|
assert "baseInput.value = spec.defaultBaseUrl || baseInput.value" in txt
|
|
|
|
|
|
def test_setup_view_preserves_selected_image_generation_provider():
|
|
txt = (VIEWS / "setup.js").read_text(encoding="utf-8")
|
|
assert "imageProviderSelected" in txt
|
|
assert "imageGenerationProvider" in txt
|
|
assert "imageGenerationPrimary || '').split('/')[0]" in txt
|
|
|
|
|
|
def test_setup_router_controls_use_user_facing_labels():
|
|
txt = (VIEWS / "setup.js").read_text(encoding="utf-8")
|
|
assert "SquillaRouter" in txt
|
|
assert "OpenRouter mix" not in txt
|
|
assert "Route c1" in txt
|
|
assert "Route c2" in txt
|
|
|
|
|
|
def test_setup_view_preserves_unsaved_form_values_across_step_navigation():
|
|
txt = (VIEWS / "setup.js").read_text(encoding="utf-8")
|
|
assert "const _drafts" in txt
|
|
assert "function _rememberDraft" in txt
|
|
assert "function _restoreDraft" in txt
|
|
assert "function _setStep" in txt
|
|
assert "data-next" in txt
|
|
assert "_setStep(btn.dataset.next)" in txt
|
|
|
|
|
|
def test_setup_view_reconciles_search_key_state_after_restoring_draft_provider():
|
|
txt = (VIEWS / "setup.js").read_text(encoding="utf-8")
|
|
start = txt.index("function _restoreDynamicDraftFields()")
|
|
end = txt.index(" function _fieldKey", start)
|
|
body = txt[start:end]
|
|
|
|
assert "function _syncSearchProviderKeyControls" in txt
|
|
assert "if (_step === 'extras' && _drafts.has('extras'))" in body
|
|
assert "_syncSearchProviderKeyControls({ refreshEnv: false })" in body
|
|
|
|
|
|
def test_setup_view_does_not_redraw_dirty_channel_form_during_status_poll():
|
|
txt = (VIEWS / "setup.js").read_text(encoding="utf-8")
|
|
assert "let _channelDirty" in txt
|
|
assert "data-channel-dirty-root" in txt
|
|
assert "if (_channelDirty) return;" in txt
|
|
|
|
|
|
def test_setup_view_surfaces_action_needed_reasons():
|
|
txt = (VIEWS / "setup.js").read_text(encoding="utf-8")
|
|
assert "function _onboardingReasons" in txt
|
|
assert "setup-reasons" in txt
|
|
assert "Connect a model provider" in txt
|
|
assert "detail.status === 'missing'" in txt
|
|
assert "Provider action required" not in txt
|
|
assert "sectionDetails" in txt
|
|
assert "detail.blocking" in txt
|
|
assert "(_status.channelCount || 0) === 0" not in txt
|
|
|
|
|
|
def test_setup_stepper_surfaces_readiness_for_each_setup_area():
|
|
txt = (VIEWS / "setup.js").read_text(encoding="utf-8")
|
|
css = (ROOT / "static/css/views/setup.css").read_text(encoding="utf-8")
|
|
draw_start = txt.index("function _draw()")
|
|
draw_end = txt.index(" function _renderOnboardingReasons()", draw_start)
|
|
draw_body = txt[draw_start:draw_end]
|
|
|
|
assert "STEPS.map(_renderStepButton).join('')" in draw_body
|
|
assert "function _renderStepButton" in txt
|
|
assert "function _stepStatus" in txt
|
|
assert "function _aggregateStepStatus" in txt
|
|
assert "setup-stepper__state" in txt
|
|
assert "setup-stepper__label" in txt
|
|
assert "setup-stepper__num" in txt
|
|
assert (
|
|
"_aggregateStepStatus(['search', 'image_generation', 'audio', "
|
|
"'memory_embedding'])"
|
|
) in txt
|
|
assert "detail.blocking || detail.actionRequired" in txt
|
|
assert "detail.status === 'missing' || detail.status === 'degraded'" in txt
|
|
assert "aria-label=\"${_esc(`${s.label}: ${status.label}`)}\"" in txt
|
|
assert ".setup-stepper__state" in css
|
|
assert ".setup-stepper__state.is-ok" in css
|
|
assert ".setup-stepper__state.is-warn" in css
|
|
assert ".setup-stepper__label" in css
|
|
|
|
|
|
def test_setup_stepper_names_router_provider_prerequisite():
|
|
txt = (VIEWS / "setup.js").read_text(encoding="utf-8")
|
|
start = txt.index("function _stepStatus")
|
|
body = txt[start : txt.index(" function _aggregateStepStatus", start)]
|
|
|
|
assert "if (stepId === 'router' && !_effectiveProvider())" in body
|
|
assert "Provider first" in body
|
|
|
|
|
|
def test_setup_header_reasons_name_missing_env_keys_for_capabilities():
|
|
txt = (VIEWS / "setup.js").read_text(encoding="utf-8")
|
|
start = txt.index("function _onboardingReasons()")
|
|
end = txt.index(" function _hasSetupAction()", start)
|
|
body = txt[start:end]
|
|
|
|
assert "function _setupActionReason" in txt
|
|
assert "const missingEnvPrefix = 'env key not visible: '" in txt
|
|
assert "return `${envKey} is not visible`" in txt
|
|
assert "_setupActionReason(name, detail)" in body
|
|
assert "Memory embedding setup needed" not in body
|
|
|
|
|
|
def test_setup_provider_first_run_summary_does_not_headline_default_provider():
|
|
txt = (VIEWS / "setup.js").read_text(encoding="utf-8")
|
|
start = txt.index("function _renderProviderStep()")
|
|
end = txt.index(" function _providerConfigFor", start)
|
|
body = txt[start:end]
|
|
|
|
assert "const selected = _effectiveProvider();" in body
|
|
assert (
|
|
"const providerSummary = selected\n"
|
|
" ? (spec.label || selected)\n"
|
|
" : `Choose from ${providers.length} supported providers`"
|
|
) in body
|
|
assert '<p data-provider-summary>${_esc(providerSummary)}</p>' in body
|
|
assert '<p data-provider-summary>${_esc(selected || \'not configured\')}</p>' not in body
|
|
|
|
|
|
def test_setup_provider_step_surfaces_squillarouter_tier_support():
|
|
txt = (VIEWS / "setup.js").read_text(encoding="utf-8")
|
|
start = txt.index("function _renderProviderStep()")
|
|
end = txt.index(" function _providerConfigFor", start)
|
|
body = txt[start:end]
|
|
sync_start = txt.index("function _drawProviderFields")
|
|
sync_end = txt.index(" function _drawChannelFields", sync_start)
|
|
sync_body = txt[sync_start:sync_end]
|
|
|
|
assert "function _providerRouterSupportText" in txt
|
|
assert "function _providerRouterSupportTone" in txt
|
|
assert "spec.routerSupported === true" in txt
|
|
assert "'SquillaRouter ready'" in txt
|
|
assert "'Direct only'" in txt
|
|
assert "'available'" not in body
|
|
assert "'direct model only'" not in body
|
|
assert "data-provider-router-support" in body
|
|
assert "data-provider-router-support-label" in body
|
|
assert "setup-provider-meta__badge" in body
|
|
assert "SquillaRouter tiers" in body
|
|
assert "_providerRouterSupportText(selected ? spec : null)" in body
|
|
assert "_providerRouterSupportTone(selected ? spec : null)" in body
|
|
assert "routerSupport.textContent = _providerRouterSupportText(spec);" in sync_body
|
|
assert "routerSupport.className = `setup-provider-meta__badge" in sync_body
|
|
assert "_providerRouterSupportTone(spec)" in sync_body
|
|
|
|
|
|
def test_setup_provider_router_support_badge_has_distinct_tones():
|
|
css = (ROOT / "static/css/views/setup.css").read_text(encoding="utf-8")
|
|
|
|
assert ".setup-provider-meta__badge" in css
|
|
assert ".setup-provider-meta__badge.is-ready" in css
|
|
assert ".setup-provider-meta__badge.is-direct" in css
|
|
assert ".setup-provider-meta__badge.is-neutral" in css
|
|
|
|
|
|
def test_setup_provider_switch_summary_uses_provider_label():
|
|
txt = (VIEWS / "setup.js").read_text(encoding="utf-8")
|
|
start = txt.index("function _drawProviderFields")
|
|
end = txt.index(" function _drawChannelFields", start)
|
|
body = txt[start:end]
|
|
|
|
assert "summary.textContent = spec.label || providerId || 'not configured';" in body
|
|
|
|
|
|
def test_setup_router_step_uses_effective_provider_without_hardcoded_fallback():
|
|
txt = (VIEWS / "setup.js").read_text(encoding="utf-8")
|
|
start = txt.index("function _renderRouterStep()")
|
|
end = txt.index(" function _tierRow", start)
|
|
body = txt[start:end]
|
|
|
|
assert "const provider = _effectiveProvider();" in body
|
|
assert "const canSaveRouter = provider && provider === _configuredProvider();" in body
|
|
assert "const routerSummary = provider" in body
|
|
assert "'Choose a provider first'" in body
|
|
assert "profiles.find(p => p.profileId === 'openrouter')" not in body
|
|
assert "const tiers = provider ?" in body
|
|
assert "data-router-provider-needed" in body
|
|
assert "data-router-provider-unsaved" in body
|
|
assert "data-save-router${saveDisabled}" in body
|
|
|
|
save_start = txt.index("async function _saveRouter()")
|
|
save_end = txt.index(" async function _saveChannel()", save_start)
|
|
save_body = txt[save_start:save_end]
|
|
assert "const provider = _effectiveProvider();" in save_body
|
|
assert "const configuredProvider = _configuredProvider();" in save_body
|
|
assert "Choose a provider before saving router tiers." in save_body
|
|
assert "Save the provider before saving router tiers." in save_body
|
|
|
|
|
|
def test_setup_effective_provider_accepts_runtime_config_but_not_bare_defaults():
|
|
txt = (VIEWS / "setup.js").read_text(encoding="utf-8")
|
|
start = txt.index("function _configuredProvider()")
|
|
end = txt.index(" function _renderProviderFields", start)
|
|
body = txt[start:end]
|
|
|
|
assert "if (_status.hasConfig !== false) return provider;" in body
|
|
assert "if (_status.llmConfigured === true) return provider;" in body
|
|
assert "['explicit', 'env', 'not_required'].includes(_status.llmSource)" in body
|
|
assert "providerDraft['provider:selected']" in body
|
|
assert "return (includeDraft ? _draftProvider() : '') || _configuredProvider();" in body
|
|
|
|
|
|
def test_setup_view_exposes_search_in_capability_center():
|
|
txt = (VIEWS / "setup.js").read_text(encoding="utf-8")
|
|
assert "Capability Center" in txt
|
|
assert "label: 'Capabilities'" in txt
|
|
assert "label: 'Extras'" not in txt
|
|
assert "function _renderCapabilityBadge" in txt
|
|
assert "setup-badge" in txt
|
|
assert "data-search-provider" in txt
|
|
assert "data-save-search" in txt
|
|
assert "_syncSearchProviderEnvHint" in txt
|
|
|
|
|
|
def test_setup_capability_center_names_each_capability_action():
|
|
txt = (VIEWS / "setup.js").read_text(encoding="utf-8")
|
|
start = txt.index("function _renderExtrasStep()")
|
|
end = txt.index(" function _capabilitySaveButtonClass", start)
|
|
body = txt[start:end]
|
|
|
|
assert "Web search · Memory recall · Image generation" in body
|
|
assert "search / memory / image" not in body
|
|
assert "Save web search" in body
|
|
assert "Save memory embedding" in body
|
|
assert "Save image generation" in body
|
|
assert "Save Memory" not in body
|
|
assert "Save Image" not in body
|
|
|
|
|
|
def test_setup_capability_save_buttons_prioritize_action_required_card():
|
|
txt = (VIEWS / "setup.js").read_text(encoding="utf-8")
|
|
|
|
assert "function _capabilitySaveButtonClass" in txt
|
|
start = txt.index("function _capabilitySaveButtonClass")
|
|
end = txt.index(" function _renderCapabilityBadge", start)
|
|
body = txt[start:end]
|
|
|
|
assert "detail.blocking || detail.actionRequired" in body
|
|
assert "'setup-btn setup-btn--primary'" in body
|
|
assert "'setup-btn'" in body
|
|
assert 'class="${_capabilitySaveButtonClass(\'search\')}"' in txt
|
|
assert 'class="${_capabilitySaveButtonClass(\'memory_embedding\')}"' in txt
|
|
assert 'class="${_capabilitySaveButtonClass(\'image_generation\')}"' in txt
|
|
|
|
|
|
def test_setup_view_exposes_all_search_configuration_controls():
|
|
txt = (VIEWS / "setup.js").read_text(encoding="utf-8")
|
|
|
|
for name in (
|
|
"setup_search_proxy",
|
|
"setup_search_use_env_proxy",
|
|
"setup_search_fallback_policy",
|
|
"setup_search_diagnostics",
|
|
):
|
|
assert f'name="{name}"' in txt
|
|
|
|
assert "_config.search_proxy" in txt
|
|
assert "_config.search_use_env_proxy" in txt
|
|
assert "_config.search_fallback_policy" in txt
|
|
assert "_config.search_diagnostics" in txt
|
|
assert 'data-search-field="proxy"' in txt
|
|
assert 'data-search-field="use_env_proxy"' in txt
|
|
assert 'data-search-field="fallback_policy"' in txt
|
|
assert 'data-search-field="diagnostics"' in txt
|
|
|
|
|
|
def test_setup_view_treats_search_provider_keys_as_conditional():
|
|
txt = (VIEWS / "setup.js").read_text(encoding="utf-8")
|
|
|
|
assert "searchSpec.requiresApiKey === true" in txt
|
|
assert "searchRequiresKey ? '' : ' disabled'" in txt
|
|
assert "not required for this provider" in txt
|
|
assert "const searchKeyHidden = searchRequiresKey ? '' : ' hidden'" in txt
|
|
assert 'data-search-key-fields${searchKeyHidden}' in txt
|
|
assert "keyInput.disabled = !requiresKey" in txt
|
|
assert "envInput.disabled = !requiresKey" in txt
|
|
assert "keyFields.hidden = !requiresKey" in txt
|
|
|
|
|
|
def test_setup_view_treats_memory_remote_fields_as_provider_conditional():
|
|
txt = (VIEWS / "setup.js").read_text(encoding="utf-8")
|
|
|
|
assert "memorySpec.requiresApiKey === true" in txt
|
|
assert "memoryApiKeyEnabled" in txt
|
|
assert "memoryRemoteControlEnabled" in txt
|
|
assert "function _syncMemoryProviderControls" in txt
|
|
assert (
|
|
"[data-memory-provider]')?.addEventListener('change', "
|
|
"_syncMemoryProviderControls)"
|
|
) in txt
|
|
assert "apiKeyInput.disabled = !apiKeyEnabled" in txt
|
|
assert "envInput.disabled = !apiKeyEnabled" in txt
|
|
assert "baseInput.disabled = !remoteControlEnabled" in txt
|
|
assert "modelInput.disabled = !remoteControlEnabled" in txt
|
|
assert "remoteOptions.hidden = !hasRemoteOptions" in txt
|
|
assert "remoteOptions.open = providerId !== 'auto' && hasRemoteOptions" in txt
|
|
assert 'data-memory-field="api_key_env"' in txt
|
|
|
|
|
|
def test_setup_memory_embedding_exposes_local_onnx_dir_without_cluttering_auto():
|
|
txt = (VIEWS / "setup.js").read_text(encoding="utf-8")
|
|
start = txt.index("function _renderExtrasStep()")
|
|
end = txt.index(" function _capabilitySaveButtonClass", start)
|
|
body = txt[start:end]
|
|
|
|
assert "const memoryLocal = current.local || {}" in body
|
|
assert "const memoryLocalControlEnabled = effectiveProvider === 'local'" in body
|
|
assert "const memoryRemoteOptionsSummary" in body
|
|
assert "Remote fallback options" in body
|
|
assert "Connection options" in body
|
|
assert (
|
|
"data-memory-remote-options"
|
|
"${memoryRemoteOptionsOpen}${memoryRemoteOptionsHidden}"
|
|
) in body
|
|
assert 'data-memory-local-field${memoryLocalHidden}' in body
|
|
assert 'name="setup_memory_onnx_dir"' in body
|
|
assert 'data-memory-field="onnx_dir"' in body
|
|
assert "memoryLocal.onnx_dir || ''" in body
|
|
|
|
|
|
def test_setup_view_surfaces_env_reference_save_feedback():
|
|
txt = (VIEWS / "setup.js").read_text(encoding="utf-8")
|
|
memory_start = txt.index("async function _saveMemory()")
|
|
memory_end = txt.index(" async function _saveSearch()", memory_start)
|
|
memory_body = txt[memory_start:memory_end]
|
|
image_start = txt.index("async function _saveImage()")
|
|
image_end = txt.index(" async function _saveAudio()", image_start)
|
|
image_body = txt[image_start:image_end]
|
|
audio_start = txt.index("async function _saveAudio()")
|
|
audio_end = txt.index(" async function _loadChannelStatus()", audio_start)
|
|
audio_body = txt[audio_start:audio_end]
|
|
|
|
assert "function _toastEnvReferenceSave" in txt
|
|
assert (
|
|
"const res = await _rpc.call('onboarding.memory_embedding.configure', params)"
|
|
in memory_body
|
|
)
|
|
assert "_toastEnvReferenceSave(" in memory_body
|
|
assert "'Memory embedding'" in memory_body
|
|
assert "remote.api_key_env" in memory_body
|
|
assert "restartRequired" in memory_body
|
|
assert (
|
|
"const res = await _rpc.call('onboarding.imageGeneration.configure', params)"
|
|
in image_body
|
|
)
|
|
assert "_toastEnvReferenceSave(" in image_body
|
|
assert "'Image generation'" in image_body
|
|
assert "entry.api_key_source" in image_body
|
|
assert (
|
|
"const res = await _rpc.call('onboarding.audio.configure', params)"
|
|
in audio_body
|
|
)
|
|
assert "_toastEnvReferenceSave(" in audio_body
|
|
assert "'Voice audio'" in audio_body
|
|
assert "entry.api_key_source" in audio_body
|
|
|
|
|
|
def test_setup_view_explains_memory_embedding_provider_modes():
|
|
txt = (VIEWS / "setup.js").read_text(encoding="utf-8")
|
|
|
|
assert "function _memoryEmbeddingStatusText" in txt
|
|
assert 'data-memory-status-text' in txt
|
|
assert "Local-first memory search" in txt
|
|
assert "Uses local BGE embeddings" in txt
|
|
assert "Uses your Ollama server" in txt
|
|
assert "Keyword search stays available; embeddings are disabled." in txt
|
|
assert "savedProvider" in txt
|
|
assert "provider === savedProvider" in txt
|
|
assert "statusText.textContent = _memoryEmbeddingStatusText(providerId)" in txt
|
|
|
|
|
|
def test_setup_view_does_not_submit_disabled_memory_fields():
|
|
txt = (VIEWS / "setup.js").read_text(encoding="utf-8")
|
|
start = txt.index("async function _saveMemory()")
|
|
end = txt.index(" async function _saveSearch()", start)
|
|
body = txt[start:end]
|
|
|
|
assert "if (input.disabled) return;" in body
|
|
|
|
|
|
def test_setup_view_saves_search_checkboxes_as_booleans():
|
|
txt = (VIEWS / "setup.js").read_text(encoding="utf-8")
|
|
start = txt.index("async function _saveSearch()")
|
|
end = txt.index(" async function _saveImage()", start)
|
|
body = txt[start:end]
|
|
|
|
assert "input.type === 'checkbox'" in body
|
|
assert "params[key] = input.checked" in body
|
|
|
|
|
|
def test_setup_finish_groups_required_and_optional_readiness():
|
|
txt = (VIEWS / "setup.js").read_text(encoding="utf-8")
|
|
assert "function _renderReadinessGroup" in txt
|
|
assert "Required setup" in txt
|
|
assert "Optional capabilities" in txt
|
|
assert "setup-readiness__group" in txt
|
|
|
|
|
|
def test_setup_finish_readiness_rows_render_detail_text():
|
|
txt = (VIEWS / "setup.js").read_text(encoding="utf-8")
|
|
start = txt.index("function _renderReadinessGroup")
|
|
end = txt.index(" function _readinessTone", start)
|
|
body = txt[start:end]
|
|
|
|
assert "detail.detail" in body
|
|
assert "setup-readiness__detail" in body
|
|
|
|
|
|
def test_setup_finish_readiness_rows_offer_direct_config_actions():
|
|
txt = (VIEWS / "setup.js").read_text(encoding="utf-8")
|
|
css = (ROOT / "static/css/views/setup.css").read_text(encoding="utf-8")
|
|
start = txt.index("function _renderReadinessGroup")
|
|
end = txt.index(" function _readinessTone", start)
|
|
body = txt[start:end]
|
|
|
|
assert "function _setupStepForSection" in txt
|
|
assert "function _readinessActionLabel" in txt
|
|
assert "const step = _setupStepForSection(name, detail)" in body
|
|
assert "const action = _renderReadinessAction(step, detail, name)" in body
|
|
assert "function _renderReadinessAction" in txt
|
|
assert "const actionAriaLabel = _readinessActionAriaLabel(detail, name)" in body
|
|
assert 'class="setup-readiness__action"' in body
|
|
assert "aria-label=\"${_esc(actionAriaLabel)}\"" in body
|
|
assert "title=\"${_esc(actionAriaLabel)}\"" in body
|
|
assert "data-step=\"${_esc(step)}\"" in body
|
|
assert "_readinessActionLabel(detail, name)" in body
|
|
assert "function _readinessActionAriaLabel" in txt
|
|
assert ".setup-readiness__action" in css
|
|
assert "grid-template-columns: minmax(0, 1fr) auto auto auto" in css
|
|
|
|
|
|
def test_setup_finish_readiness_names_router_provider_prerequisite():
|
|
txt = (VIEWS / "setup.js").read_text(encoding="utf-8")
|
|
|
|
assert "function _routerNeedsProvider" in txt
|
|
assert "uses SquillaRouter after provider setup" in txt
|
|
|
|
start = txt.index("function _setupStepForSection")
|
|
end = txt.index(" function _readinessActionLabel", start)
|
|
setup_body = txt[start:end]
|
|
assert "if (_routerNeedsProvider(detail, name)) return 'provider'" in setup_body
|
|
|
|
start = txt.index("function _readinessActionLabel")
|
|
end = txt.index(" function _renderReadinessAction", start)
|
|
action_body = txt[start:end]
|
|
assert "if (_routerNeedsProvider(detail, name)) return 'Choose provider'" in action_body
|
|
|
|
start = txt.index("function _readinessActionAriaLabel")
|
|
aria_body = txt[start : txt.index(" function _readinessTone", start)]
|
|
assert "return `Choose provider for ${label}`" in aria_body
|
|
|
|
start = txt.index("function _readinessStatusLabel")
|
|
status_body = txt[start : txt.index(" function _fieldHtml", start)]
|
|
assert "if (_routerNeedsProvider(detail, name)) return 'Provider first'" in status_body
|
|
|
|
|
|
def test_setup_finish_summary_stays_neutral_before_provider_is_configured():
|
|
txt = (VIEWS / "setup.js").read_text(encoding="utf-8")
|
|
start = txt.index("function _renderFinishStep()")
|
|
end = txt.index(" function _renderCliCommand", start)
|
|
body = txt[start:end]
|
|
|
|
assert "const configuredProvider = _configuredProvider();" in body
|
|
assert "const providerSummary = configuredProvider || 'not configured'" in body
|
|
assert (
|
|
"const modelSummary = configuredProvider"
|
|
"\n ? ((_config.llm || {}).model || 'SquillaRouter defaults')"
|
|
"\n : 'not configured'"
|
|
) in body
|
|
assert (
|
|
"const routerSummary = configuredProvider"
|
|
"\n ? (router.enabled === false ? 'disabled' : 'SquillaRouter')"
|
|
"\n : 'choose a provider first'"
|
|
) in body
|
|
assert "router.tier_profile || 'openrouter-mix'" not in body
|
|
assert "<strong>${_esc(providerSummary)}</strong>" in body
|
|
assert "<strong>${_esc(modelSummary)}</strong>" in body
|
|
assert "<strong>${_esc(routerSummary)}</strong>" in body
|
|
|
|
|
|
def test_setup_panel_avoids_dense_background_rule_lines():
|
|
css = (ROOT / "static/css/views/setup.css").read_text(encoding="utf-8")
|
|
panel_block = css.split(".setup-panel {", 1)[1].split("}", 1)[0]
|
|
assert "repeating-linear-gradient" not in panel_block
|
|
|
|
|
|
def test_setup_capability_cards_keep_controls_top_aligned():
|
|
css = (ROOT / "static/css/views/setup.css").read_text(encoding="utf-8")
|
|
mini_block = css.split(".setup-mini {", 1)[1].split("}", 1)[0]
|
|
|
|
assert "align-content: start" in mini_block
|
|
|
|
|
|
def test_setup_hidden_controls_remain_hidden_after_label_layout_rules():
|
|
css = (ROOT / "static/css/views/setup.css").read_text(encoding="utf-8")
|
|
|
|
assert ".setup [hidden]" in css
|
|
hidden_block = css.split(".setup [hidden]", 1)[1].split("}", 1)[0]
|
|
assert "display: none !important" in hidden_block
|
|
|
|
|
|
def test_setup_search_advanced_details_are_styled_as_secondary_controls():
|
|
css = (ROOT / "static/css/views/setup.css").read_text(encoding="utf-8")
|
|
|
|
assert ".setup-mini__advanced > summary" in css
|
|
assert ".setup-mini__advanced-body" in css
|
|
assert "details.setup-mini__advanced[open] > summary::after" in css
|
|
|
|
|
|
def test_config_view_exposes_memory_tab_and_restart_notice():
|
|
txt = (VIEWS / "config.js").read_text(encoding="utf-8")
|
|
assert "label: 'Memory'" in txt
|
|
assert "memory.embedding.provider" in txt
|
|
assert "Gateway restart required for the change to take effect" in txt
|
|
|
|
|
|
def test_config_view_links_to_guided_setup():
|
|
txt = (VIEWS / "config.js").read_text(encoding="utf-8")
|
|
assert "Guided setup" in txt
|
|
assert "Router.navigate('/setup')" in txt
|
|
|
|
|
|
def test_channels_view_remains_status_only_but_links_guided_setup():
|
|
txt = (VIEWS / "channels.js").read_text(encoding="utf-8")
|
|
assert "Runtime status" in txt
|
|
assert "Guided setup" in txt
|
|
assert "Router.navigate('/setup')" in txt
|
|
assert "onboarding.channel.upsert" not in txt
|
|
assert "channels.restart" not in txt
|
|
|
|
|
|
def test_example_config_does_not_advertise_local_embedding_model_override():
|
|
txt = (ROOT.parents[2] / "opensquilla.toml.example").read_text(encoding="utf-8")
|
|
local_section = txt.split("# [memory.embedding.local]", 1)[1].split(
|
|
"# [memory.embedding.remote]",
|
|
1,
|
|
)[0]
|
|
assert "model =" not in local_section
|
|
assert "onnx_dir" in local_section
|