ouroboros/tests/test_page_chrome_static.py
Anton Razzhigaev 8f70cd56c1 Decide viewport restore from geometry, not mutation flags
The stable-viewport boundary now takes its restore/follow decision from the
pre-mutation scroll geometry: follow happens on an actual scrollHeight delta
(or an explicit force-follow), an away reader is re-anchored unconditionally
(a no-op at zero delta), and the restore path runs even when the mutation
throws. The mutate() boolean protocol keeps its remaining duties - the
remote-activity marker and physical write idempotence - but a lying change
flag can no longer strand the reader (owner decision Q8=B, verified by the
two-engine geometry experiment).

Also: legacy progress frames without delegation_role fall through to the
ordinary card path again instead of being swallowed; first history load pins
to the newest row explicitly and the removed source asserts return; a
collapsed mermaid fence measures its visible ancestor instead of the 320px
floor; returning to the browser tab re-pins only a visible instance; unread
increments are gated on an actual visible change in every branch.

cleanupUploadedAttachments moves to chat_media.js and stampNodeTimestamp to
chat_activity.js (their domains); web/modules/chat.js ends at 208519 bytes,
below its recorded 208717 debt, with the manifest regenerated.

Co-authored-by: Ouroboros <311266734+ouroboros-agent@users.noreply.github.com>
2026-09-01 11:30:00 +00:00

252 lines
12 KiB
Python

"""Static UI contracts for page chrome: shared header helper, scroll regions,
and evolution/consciousness wiring.
Consolidated in v5.15.x from three small files that each guarded one slice
of the SPA page-chrome layer:
- ``test_page_header_ui_static.py`` — renderPageHeader / renderTabStrip SSOT
- ``test_settings_and_page_layout_static.py`` — secrets generality + scroll regions
- ``test_evolution_ui_guards.py`` — evolution page + server runtime-state wiring
"""
from __future__ import annotations
import pathlib
import re
ROOT = pathlib.Path(__file__).resolve().parents[1]
def _read(rel: str) -> str:
return (ROOT / rel).read_text(encoding="utf-8")
# ---------------------------------------------------------------------------
# Shared page header helper (renderPageHeader / renderTabStrip SSOT)
# ---------------------------------------------------------------------------
def test_shared_page_header_helper_has_no_inline_styles():
source = _read("web/modules/page_header.js")
assert "export function renderPageHeader" in source
assert "export function renderTabStrip" in source
assert "style=" not in source
assert "app-page-header" in source
assert "app-tab-strip" in source
def test_primary_pages_use_shared_header_helper():
for rel in [
"web/modules/settings_ui.js",
"web/modules/dashboard.js",
"web/modules/skills.js",
"web/modules/widgets.js",
"web/modules/files.js",
"web/modules/chat.js",
]:
source = _read(rel)
assert "page_header.js" in source
assert "renderPageHeader" in source
# ---------------------------------------------------------------------------
# Settings secrets layout + skills/widgets scroll regions
# ---------------------------------------------------------------------------
def test_settings_secrets_are_generic_and_integrations_tab_removed():
ui = _read("web/modules/settings_ui.js")
settings = _read("web/modules/settings.js")
assert "Integrations" not in ui
assert "TELEGRAM_" not in ui
assert "TELEGRAM_" not in settings
assert "skill-requested-secrets" in ui
assert "custom-secrets-list" in ui
assert "Source Control" in ui
def test_settings_scope_review_effort_round_trips():
"""6.3 moved the Review/Scope efforts off the Behavior tab onto the Models
page as PER-SLOT dropdowns (red on cxi/p6-ui-v2's own head — the branch
moved the carrier and left this pin behind): the owner-facing carrier is now
reviewer_slots.js, where an EMPTY slot effort inherits the surface default
(OUROBOROS_EFFORT_SCOPE_REVIEW backend-side) and the advisory row defaults
low (D14). The mode-guard filter in settings.js is unchanged."""
slots_ui = _read("web/modules/reviewer_slots.js")
settings = _read("web/modules/settings.js")
assert "scope review effort" in slots_ui # per-slot surface-default wording
assert "review effort" in slots_ui
assert "effort: 'low'" in slots_ui # the advisory default (D14)
assert "key !== 'OUROBOROS_RUNTIME_MODE' && key !== 'OUROBOROS_CONTEXT_MODE'" in settings
def test_settings_mutative_subagents_toggle_round_trips():
"""The owner-facing master switch for mutative ("acting") subagents must exist
as an owner-facing Off/Auto/On control wired through the
settings load/save path, not only as a settings.json/env key.
The CARRIER moved twice, and the invariant is the same each time: the write
permission sits inside the Delegation section (Agents tab, beside Review
lanes) together with the delegation route and the subagent counts, so one
subagent story is told in one place. Two controls over one setting would have
carried two drafts, so settings_ui.js must NOT keep a second copy."""
ui = _read("web/modules/subagents_settings.js")
settings = _read("web/modules/settings.js")
assert "s-allow-mutative-subagents" not in _read("web/modules/settings_ui.js")
assert "s-allow-mutative-subagents" in ui
assert "renderSubagentsSection" in _read("web/modules/settings_ui.js")
section_start = ui.index("<h3>Available subagents</h3>")
section = ui[section_start:]
# Segmented control is generated by the renderSegmentedField SSOT (C7.1): the
# options live in its `options` array, not inline button markup. The control
# is tri-state again (SC-4): the v6.22.1 binary Off/On display was truthful
# only while the unset default was all-or-nothing per mode; the surface-aware
# light default (unset = external_workspace/genesis ON, self_worktree off)
# made "Off" a false claim for unset-in-light, so unset there displays as
# an explicit "Auto" state instead.
assert "renderSegmentedField" in section
assert "target: 's-allow-mutative-subagents'" in section
assert "value: 'auto'" in section
assert "value: 'off'" in section
assert "value: 'on'" in section
# The note must describe the surface-aware light default, not the pre-SC-4
# "off in Light" claim.
assert "off in Light" not in section
assert "isolated worktree, external workspace, or" in section
assert "Auto follows runtime" in section
# load + save mapping wired in settings.js (outside VALUE_FIELDS): unset in
# light displays Auto (never a false "Off"); unset in advanced/pro keeps the
# truthful effective "On"; an untouched control preserves the empty value.
assert "OUROBOROS_ALLOW_MUTATIVE_SUBAGENTS" in settings
assert "s-allow-mutative-subagents" in settings
assert "runtimeMode === 'light' ? 'auto' : 'on'" in settings
assert "dataset.rawValue = rawMutative" in settings
assert "mutativeTouched" in settings
def test_onboarding_compact_access_step_keeps_default_width_two_column():
css = _read("web/onboarding.css")
assert "@media (max-height: 820px), (max-width: 900px)" in css
assert ".field-note,\n .footer-copy" in css
assert "@media (max-width: 760px)" in css
def test_settings_more_providers_collapse_keeps_inputs_mounted():
"""Rarely used provider cards (Cloud.ru, MiniMax, GigaChat) collapse under a
"More providers" details wrapper, but their inputs must stay mounted:
settings.js applyInputValue has no null guard, so a missing input id
breaks settings load. The wrapper auto-opens when configured."""
ui = _read("web/modules/settings_ui.js")
settings = _read("web/modules/settings.js")
css = _read("web/settings.css")
assert 'id="settings-more-providers"' in ui
assert ui.count("advanced: true") == 3
assert "PROVIDER_CARDS.filter((card) => !card.advanced)" in ui
assert "PROVIDER_CARDS.filter((card) => card.advanced)" in ui
assert "syncMoreProvidersDisclosure" in settings
assert ".settings-more-providers > summary" in css
# About tab footer stays removed (v6.82 cosmetic pass).
assert "Joi Lab" not in ui
def test_subagent_write_surface_badge_in_both_card_paths():
"""The write=<surface> badge must render on BOTH the Logs path
(summarizeLogEvent) and the Chat live-card path (summarizeChatLiveEvent),
not just one — the surface field is plumbed through history/contracts."""
log_events = _read("web/modules/log_events.js")
# both summarize functions reference the badge
assert log_events.count("write=${evt.write_surface}") >= 2
api_types = _read("web/modules/api_types.js")
assert "write_surface" in api_types
def test_skills_and_widgets_use_inner_scroll_regions():
skills = _read("web/modules/skills.js")
widgets = _read("web/modules/widgets.js")
css = _read("web/style.css")
assert 'class="skills-scroll scroll-fade-y"' in skills
assert 'class="widgets-scroll scroll-fade-y"' in widgets
assert ".skills-scroll" in css and "overflow-y: auto" in css
assert ".widgets-scroll" in css and "overflow-y: auto" in css
# ---------------------------------------------------------------------------
# Evolution / consciousness UI wiring
# ---------------------------------------------------------------------------
def test_evolution_page_supports_refresh_and_runtime_state():
source = _read("web/modules/evolution.js")
assert 'id="evo-refresh"' in source
assert "Runtime Status" in source
assert "apiFetch(`/api/evolution-data${suffix}`" in source
assert "ws.on('open', () => {" in source
assert "window.addEventListener('ouro:page-shown'" in source
assert "document.addEventListener('visibilitychange'" in source
assert "renderRuntimeState(runtime, data.generated_at || '');" in source
assert "evolution_state" in source
assert "bg_consciousness_state" in source
def test_server_navigation_and_chat_static_contracts():
server_source = _read("server.py")
state_source = _read("ouroboros/gateway/state.py")
control_source = _read("ouroboros/gateway/control.py")
app_source = _read("web/app.js")
evo_source = _read("web/modules/evolution.js")
chat_source = _read("web/modules/chat.js")
css = _read("web/style.css")
ui = _read("web/modules/settings_ui.js")
settings = _read("web/modules/settings.js")
costs = _read("web/modules/costs.js")
assert "def _describe_bg_consciousness_state(requested_enabled: bool) -> dict:" in server_source
assert '"evolution_state": evolution_state,' in state_source
assert '"bg_consciousness_state": bg_state,' in state_source
assert 'request.query_params.get("force")' in control_source
assert "window.dispatchEvent(new CustomEvent('ouro:page-shown', { detail: { page: pageName } }));" in app_source
assert "evo-runtime-detail" in evo_source
assert "data?.evolution_state?.detail" in chat_source
assert "data?.bg_consciousness_state?.detail" in chat_source
assert re.search(r'<input[^>]+id="chat-file-input"[^>]+multiple', chat_source)
assert "MAX_PENDING_ATTACHMENTS = 10" in chat_source
assert "MAX_ATTACHMENT_FILE_BYTES = 50 * 1024 * 1024" in chat_source
assert "MAX_PENDING_ATTACHMENT_BYTES = 100 * 1024 * 1024" in chat_source
assert "pendingAttachments" in chat_source
assert "attachmentsUploading" in chat_source
assert "setAttachmentUploadState" in chat_source
assert "attachBtn.classList.toggle('uploading', uploading)" in chat_source
assert "input.disabled = uploading;" in chat_source
assert "cleanupUploadedAttachments" in chat_source
assert "await cleanupUploadedAttachments(uploaded);" in chat_source
assert "await cleanupUploadedAttachments(uploadedAttachments);" in chat_source
assert "ws.send({" in chat_source and "{ queue: false }" in chat_source
assert "result?.status !== 'sent'" in chat_source
assert "data-attachment-remove" in chat_source
media_source = _read("web/modules/chat_media.js")
assert "export async function cleanupUploadedAttachments" in media_source
assert "method: 'DELETE'" in media_source
assert "Promise.allSettled" in media_source
assert '>Loading…</span>' in chat_source
assert "syncHeaderControlState({ accounting: { available: false } });" in chat_source
assert "budget_text: 'Connecting...'" not in chat_source
assert "send(msg, options = {})" in _read("web/modules/ws.js")
assert "options.queue === false" in _read("web/modules/ws.js")
assert ".chat-attachment-preview" in css and "flex-wrap" in css
assert 'id="s-total-budget"' in ui
assert 'id="s-settings-per-task-cost"' in ui
assert "setupContract.budgetFields" in settings
assert "'anthropic/claude-sonnet-5'" in settings
assert "'anthropic::claude-sonnet-5'" in settings
assert "currentSettings?.[field.settingKey]" in settings
assert "window.addEventListener('ouro:settings-updated'" in settings
assert "source: 'settings'" in settings
assert "Budget values must be at least 0.01." in costs
assert "COST_BUDGET_INPUTS" in costs
assert "s?._meta?.setup_contract?.budgetFields" in costs
assert "const MIN_BUDGET_VALUE" not in costs
assert "new CustomEvent('ouro:settings-updated'" in costs
assert "source: 'costs'" in costs
assert "event.detail?.source === 'costs'" in costs