3602 lines
172 KiB
Python
3602 lines
172 KiB
Python
from pathlib import Path
|
|
|
|
CHAT_JS = Path("src/opensquilla/gateway/static/js/views/chat.js")
|
|
CHAT_CSS = Path("src/opensquilla/gateway/static/css/views/chat.css")
|
|
CHAT_VIEW_VUE = Path("opensquilla-webui/src/views/ChatView.vue")
|
|
CHAT_SEND_TS = Path("opensquilla-webui/src/composables/chat/useChatSend.ts")
|
|
CHAT_MESSAGE_ACTIONS_TS = Path("opensquilla-webui/src/composables/chat/useChatMessageActions.ts")
|
|
CHAT_ROUTER_FX_VUE = Path("opensquilla-webui/src/components/chat/RouterFxStrip.vue")
|
|
APP_JS = Path("src/opensquilla/gateway/static/js/app.js")
|
|
RPC_JS = Path("src/opensquilla/gateway/static/js/rpc.js")
|
|
SAVINGS_FX_JS = Path("src/opensquilla/gateway/static/js/components/savings-fx.js")
|
|
TASK_RUNTIME_PY = Path("src/opensquilla/gateway/task_runtime.py")
|
|
|
|
|
|
def test_global_topbar_does_not_render_duplicate_chat_title() -> None:
|
|
source = APP_JS.read_text(encoding="utf-8")
|
|
topbar_start = source.index('<header class="topbar"')
|
|
topbar_end = source.index('<main class="content"', topbar_start)
|
|
topbar_markup = source[topbar_start:topbar_end]
|
|
|
|
assert 'id="topbar-title"' not in topbar_markup
|
|
assert ">Chat</h1>" not in topbar_markup
|
|
assert 'id="conn-pill"' in topbar_markup
|
|
assert 'id="theme-toggle"' in topbar_markup
|
|
|
|
|
|
def test_chat_history_passes_subagent_completion_provenance_to_renderer() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
|
|
assert "provenanceSourceTool: msg.provenance_source_tool || ''" in source
|
|
assert "provenanceSourceSessionKey: msg.provenance_source_session_key || ''" in source
|
|
|
|
|
|
def test_chat_suppresses_internal_provider_retry_warning_toasts() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
|
|
warning_start = source.index("_rpc.on('session.event.warning'")
|
|
warning_end = source.index(" // Track session epoch", warning_start)
|
|
warning_handler = source[warning_start:warning_end]
|
|
|
|
assert "const silentWarningCodes = new Set([" in warning_handler
|
|
assert "'provider_reasoning_only_retry'" in warning_handler
|
|
assert "if (silentWarningCodes.has(code)) return;" in warning_handler
|
|
silent_warning_index = warning_handler.index("if (silentWarningCodes.has(code)) return;")
|
|
toast_index = warning_handler.index("UI.toast(msg, 'warn', 5000);")
|
|
assert silent_warning_index < toast_index
|
|
|
|
|
|
def test_chat_toolbar_has_no_tool_compress_selector() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
|
|
assert "Tool " + "Compress" not in source
|
|
assert "tool" + "Compress" not in source
|
|
assert "tool-compress" not in source
|
|
|
|
|
|
def test_chat_slash_menu_is_part_of_composer_and_width_capped() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
css = CHAT_CSS.read_text(encoding="utf-8")
|
|
composer_start = source.index('<div class="chat-composer" id="chat-composer">')
|
|
composer_end = source.index('<input type="file" id="chat-file-input"', composer_start)
|
|
composer_markup = source[composer_start:composer_end]
|
|
|
|
assert 'id="chat-slash"' in composer_markup
|
|
assert composer_markup.index('id="chat-slash"') < composer_markup.index(
|
|
'class="chat-input-bar"'
|
|
)
|
|
slash_css = css[css.index(".chat-slash {") : css.index(".chat-slash-item {")]
|
|
assert "var(--chat-measure)" in slash_css
|
|
|
|
slash_desc_start = css.index(".chat-slash-desc {")
|
|
slash_desc = css[
|
|
slash_desc_start : css.index("/* ─── Per-bubble", slash_desc_start)
|
|
]
|
|
assert "min-width: 0;" in slash_desc
|
|
assert "overflow: hidden;" in slash_desc
|
|
assert "text-overflow: ellipsis;" in slash_desc
|
|
assert "white-space: nowrap;" in slash_desc
|
|
|
|
|
|
def test_chat_pending_attachment_preview_is_composer_width_capped() -> None:
|
|
css = CHAT_CSS.read_text(encoding="utf-8")
|
|
start = css.index(".chat-attachments {")
|
|
end = css.index(".chat-attachments.hidden", start)
|
|
block = css[start:end]
|
|
|
|
assert "box-sizing: border-box;" in block
|
|
assert "width: min(calc(100% - var(--sp-8)), var(--chat-measure));" in block
|
|
assert "margin: 0 auto var(--sp-2);" in block
|
|
assert "overflow-x: auto;" in block
|
|
|
|
|
|
def test_chat_day_separator_stays_on_centered_chat_axis() -> None:
|
|
css = CHAT_CSS.read_text(encoding="utf-8")
|
|
start = css.index(".chat-day-sep {")
|
|
end = css.index(".chat-day-sep::before", start)
|
|
block = css[start:end]
|
|
|
|
assert "width: 100%;" in block
|
|
assert "max-width: var(--chat-measure);" in block
|
|
assert "margin: var(--sp-2) auto;" in block
|
|
|
|
|
|
def test_chat_user_bubble_text_uses_reading_direction_alignment() -> None:
|
|
css = CHAT_CSS.read_text(encoding="utf-8")
|
|
start = css.index(".chat-msg--user .chat-msg-text,")
|
|
end = css.index("/* Assistant", start)
|
|
block = css[start:end]
|
|
|
|
assert "display: flex;" in block
|
|
assert "align-items: center;" in block
|
|
assert "justify-content: flex-start;" in block
|
|
assert "text-align: start;" in block
|
|
|
|
|
|
def test_chat_sent_attachment_images_render_as_separate_thumbnail_attachments() -> None:
|
|
css = CHAT_CSS.read_text(encoding="utf-8")
|
|
body_start = css.index(".msg.user .msg-body.msg-body--has-attachments {")
|
|
body_end = css.index(".msg.user .msg-body--has-attachments .msg-attachment-text", body_start)
|
|
body_block = css[body_start:body_end]
|
|
text_start = css.index(".msg.user .msg-body--has-attachments .msg-attachment-text {")
|
|
text_end = css.index(".msg.user .msg-body--has-attachments .msg-attachments", text_start)
|
|
text_block = css[text_start:text_end]
|
|
attachments_start = css.index(".msg.user .msg-body--has-attachments .msg-attachments {")
|
|
attachments_end = css.index(
|
|
".msg.user .msg-body--has-attachments .msg-thumb",
|
|
attachments_start,
|
|
)
|
|
attachments_block = css[attachments_start:attachments_end]
|
|
thumb_start = css.index(".msg.user .msg-body--has-attachments .msg-thumb {")
|
|
thumb_end = css.index("/* ─── Pending Queue", thumb_start)
|
|
thumb_block = css[thumb_start:thumb_end]
|
|
|
|
assert "max-width: min(360px, 78%);" in body_block
|
|
assert "border: 0;" in body_block
|
|
assert "background: transparent;" in body_block
|
|
assert "max-width: min(360px, 100%);" in text_block
|
|
assert "max-width: 100%;" in attachments_block
|
|
assert "user-select: text;" in attachments_block
|
|
assert "width: min(260px, 42vw);" in thumb_block
|
|
assert "height: auto;" in thumb_block
|
|
assert "min-height:" not in thumb_block
|
|
assert "object-fit: contain;" in thumb_block
|
|
assert "background: transparent;" in thumb_block
|
|
assert "border-color: color-mix(in srgb, var(--border) 65%, transparent);" in thumb_block
|
|
assert "box-shadow: none;" in thumb_block
|
|
assert "var(--accent)" not in thumb_block
|
|
|
|
|
|
def test_chat_user_message_bubbles_preserve_multiline_text() -> None:
|
|
css = CHAT_CSS.read_text(encoding="utf-8")
|
|
user_start = css.index(".chat-msg--user .chat-msg-text,")
|
|
user_end = css.index("/* Assistant", user_start)
|
|
user_block = css[user_start:user_end]
|
|
text_start = css.index(".msg.user .msg-body--has-attachments .msg-attachment-text {")
|
|
text_end = css.index(".msg.user .msg-body--has-attachments .msg-attachments", text_start)
|
|
attachment_text_block = css[text_start:text_end]
|
|
|
|
assert "white-space: pre-wrap;" in user_block
|
|
assert "overflow-wrap: anywhere;" in user_block
|
|
assert "text-align: start;" in user_block
|
|
assert "white-space: pre-wrap;" in attachment_text_block
|
|
assert "overflow-wrap: anywhere;" in attachment_text_block
|
|
assert "text-align: start;" in attachment_text_block
|
|
|
|
|
|
def test_chat_non_image_message_attachments_render_download_links_when_data_exists() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
render_start = source.index("function _renderMessageAttachmentHtml(att)")
|
|
render_end = source.index("function _renderAttachmentPreview()", render_start)
|
|
render_body = source[render_start:render_end]
|
|
|
|
assert "function _escAttr(s)" in source
|
|
assert "function _attachmentDownloadHref(att, mime)" in source
|
|
assert "function _attachmentDownloadName(att)" in source
|
|
assert '<a class="msg-file-chip msg-file-chip--download"' in render_body
|
|
assert 'download="${_escAttr(downloadName)}"' in render_body
|
|
assert 'href="${_escAttr(downloadHref)}"' in render_body
|
|
assert '<span class="msg-file-chip msg-file-chip--disabled"' in render_body
|
|
|
|
|
|
def test_chat_file_attachment_download_links_keep_chip_styling() -> None:
|
|
css = CHAT_CSS.read_text(encoding="utf-8")
|
|
chip_start = css.index(".msg-file-chip {")
|
|
chip_end = css.index(".msg-file-chip__icon", chip_start)
|
|
chip_block = css[chip_start:chip_end]
|
|
download_start = css.index(".msg-file-chip--download {")
|
|
download_end = css.index(".msg-file-chip--download:hover", download_start)
|
|
download_block = css[download_start:download_end]
|
|
|
|
assert "text-decoration: none;" in chip_block
|
|
assert "cursor: default;" in chip_block
|
|
assert "cursor: pointer;" in download_block
|
|
assert ".msg-file-chip--download:hover" in css
|
|
assert ".msg-file-chip--download:focus-visible" in css
|
|
assert ".msg-file-chip--disabled" in css
|
|
|
|
|
|
def test_chat_tool_display_map_does_not_reference_removed_wrapper_tools() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
start = source.index("const _TOOL_EMOJI = {")
|
|
end = source.index(" function _toolEmoji", start)
|
|
tool_display_map = source[start:end]
|
|
|
|
assert "generate_image" not in tool_display_map
|
|
assert "spawn_subagent" not in tool_display_map
|
|
assert "send_message" not in tool_display_map
|
|
# Display-only mappings for owner-visible or historical tool payloads may remain.
|
|
assert "http_request" in tool_display_map
|
|
|
|
|
|
def test_system_messages_are_not_all_rendered_as_subagent_disclosures() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
|
|
assert "_isSubagentCompletionMessage(role, text, options)" in source
|
|
assert "body.appendChild(_renderSubagentDisclosure(visibleText));" in source
|
|
assert "body.textContent = visibleText;" in source
|
|
|
|
|
|
def test_live_subagent_completion_event_uses_same_renderer() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
|
|
assert "session.event.subagent_completion" in source
|
|
assert "_appendSubagentCompletion(payload)" in source
|
|
|
|
def test_chat_renders_live_and_historical_artifacts_as_header_auth_downloads() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
|
|
assert "session.event.artifact" in source
|
|
assert "_appendArtifact(payload)" in source
|
|
assert "_renderArtifacts(msg.artifacts || [])" in source
|
|
assert "data-artifact-download" in source
|
|
assert "headers['x-opensquilla-session-key'] = _sessionKey" in source
|
|
assert "url.searchParams.delete('sessionKey')" in source
|
|
assert "fetch(downloadUrl" in source
|
|
assert "Authorization" in source
|
|
|
|
|
|
def test_chat_artifact_download_url_carries_query_fallback_auth() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
helper_start = source.index("function _artifactAuthenticatedDownloadUrl(raw, token)")
|
|
helper_end = source.index(" function _renderArtifacts", helper_start)
|
|
helper_body = source[helper_start:helper_end]
|
|
download_start = source.index("async function _downloadArtifact(artifact)")
|
|
download_end = source.index(" function _reconstructToolCalls", download_start)
|
|
download_body = source[download_start:download_end]
|
|
|
|
assert "_artifactAuthenticatedDownloadUrl(downloadUrl, token)" in download_body
|
|
assert "url.searchParams.set('sessionKey', _sessionKey)" in helper_body
|
|
assert "url.searchParams.set('token', token)" in helper_body
|
|
assert "headers['x-opensquilla-session-key'] = _sessionKey" in download_body
|
|
assert "headers['Authorization'] = `Bearer ${token}`" in download_body
|
|
|
|
|
|
def test_chat_artifact_downloads_use_direct_links_to_preserve_user_activation() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
bind_start = source.index("function _bindHoverActions()")
|
|
bind_end = source.index(" function _truncate", bind_start)
|
|
bind_body = source[bind_start:bind_end]
|
|
render_start = source.index("function _renderArtifacts(artifacts)")
|
|
render_end = source.index(" async function _downloadArtifact", render_start)
|
|
render_body = source[render_start:render_end]
|
|
|
|
assert "if (artifactBtn.tagName === 'A') return;" in bind_body
|
|
assert (
|
|
"const downloadHref = _artifactAuthenticatedDownloadUrl(downloadUrl, token);"
|
|
in render_body
|
|
)
|
|
assert '<a class="msg-artifact-card msg-artifact-card--image"' in render_body
|
|
assert '<a class="msg-artifact-chip"' in render_body
|
|
assert 'href="${_escAttr(downloadHref)}"' in render_body
|
|
assert 'download="${_escAttr(name)}"' in render_body
|
|
|
|
|
|
def test_chat_message_actions_do_not_stick_after_history_rebuild_or_click() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
assert "function _clearMessageActionFocus(reason = '') {" in source
|
|
|
|
clear_start = source.index("function _clearMessageActionFocus(reason = '') {")
|
|
clear_end = source.index("function _attachHoverActions", clear_start)
|
|
clear_body = source[clear_start:clear_end]
|
|
assert "const active = document.activeElement;" in clear_body
|
|
assert "active.closest('.msg-actions')" in clear_body
|
|
assert "active.blur();" in clear_body
|
|
assert "_chatDiag('message_actions.focus_cleared'" in clear_body
|
|
|
|
hover_start = source.index("function _bindHoverActions()")
|
|
hover_end = source.index(" function _truncate", hover_start)
|
|
hover_body = source[hover_start:hover_end]
|
|
assert "btn.blur();" in hover_body
|
|
assert "const action = btn.dataset.action;" in hover_body
|
|
assert hover_body.index("btn.blur();") < hover_body.index("const action = btn.dataset.action;")
|
|
|
|
history_start = source.index("function _renderHistoryMessages(messages, opts = {})")
|
|
history_end = source.index("function _historyLiveTailAnchor", history_start)
|
|
history_body = source[history_start:history_end]
|
|
assert "_clearMessageActionFocus('history_rebuild');" in history_body
|
|
|
|
|
|
def test_chat_artifact_images_render_as_preview_cards_and_refresh_on_done() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
css = CHAT_CSS.read_text(encoding="utf-8")
|
|
preview_start = source.index("function _artifactPreviewUrl(artifact)")
|
|
preview_end = source.index(" function _renderArtifacts", preview_start)
|
|
preview_body = source[preview_start:preview_end]
|
|
render_start = source.index("function _renderArtifacts(artifacts)")
|
|
render_end = source.index(" async function _downloadArtifact", render_start)
|
|
render_body = source[render_start:render_end]
|
|
done_start = source.index("if (event.endsWith('.done') || event === 'chat.done') {")
|
|
done_end = source.index(" // On natural completion", done_start)
|
|
done_body = source[done_start:done_end]
|
|
|
|
assert "function _isImageArtifact(artifact)" in source
|
|
assert "function _artifactCategory(artifact)" in source
|
|
assert "function _artifactPreviewUrl(artifact)" in source
|
|
assert "url.searchParams.set('sessionKey', _sessionKey)" in preview_body
|
|
assert "const token = (App.getAuthToken && App.getAuthToken()) || '';" in preview_body
|
|
assert "url.searchParams.set('token', token)" in preview_body
|
|
assert "msg-artifact-gallery" in render_body
|
|
assert "msg-artifact-files" in render_body
|
|
assert "class=\"msg-artifact-card msg-artifact-card--image\"" in render_body
|
|
assert "<img class=\"msg-artifact-preview\"" in render_body
|
|
assert "data-artifact-download" in render_body
|
|
assert "data-artifact-category" in render_body
|
|
assert "_scheduleHistorySync();" in done_body
|
|
assert ".msg-artifact-gallery" in css
|
|
assert ".msg-artifact-files" in css
|
|
assert ".msg-artifact-card--image" in css
|
|
assert ".msg-artifact-preview" in css
|
|
|
|
|
|
def test_chat_audio_artifacts_render_inline_players_with_download_fallback() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
css = CHAT_CSS.read_text(encoding="utf-8")
|
|
render_start = source.index("function _renderArtifacts(artifacts)")
|
|
render_end = source.index(" async function _downloadArtifact", render_start)
|
|
render_body = source[render_start:render_end]
|
|
|
|
assert "function _isAudioArtifact(artifact)" in source
|
|
assert "_isAudioArtifact(artifact)" in render_body
|
|
assert '<div class="msg-artifact-card msg-artifact-card--audio"' in render_body
|
|
assert '<audio class="msg-artifact-audio" controls preload="metadata"' in render_body
|
|
assert 'src="${_escAttr(downloadHref)}"' in render_body
|
|
assert '<a class="msg-artifact-card__action"' in render_body
|
|
assert 'download="${_escAttr(name)}"' in render_body
|
|
assert ".msg-artifact-card--audio" in css
|
|
assert ".msg-artifact-audio" in css
|
|
|
|
|
|
def test_chat_final_text_reconciliation_preserves_live_artifacts() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
start = source.index("function _replaceStreamText(finalText)")
|
|
end = source.index(" function _reconcileFinalStreamText", start)
|
|
body = source[start:end]
|
|
|
|
assert "_renderStreamArtifacts();" in body
|
|
|
|
|
|
def test_chat_markdown_export_includes_artifact_download_entries() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
start = source.index("function _exportMarkdown()")
|
|
end = source.index(" /* ── Pending Queue", start)
|
|
export_body = source[start:end]
|
|
|
|
assert "artifacts: msg.artifacts || []" in source
|
|
assert "_artifactMarkdownLines(msg.artifacts || [])" in export_body
|
|
assert "[Download" in source
|
|
|
|
|
|
def test_chat_resets_stream_timeout_on_run_heartbeat() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
|
|
assert "session.event.run_heartbeat" in source
|
|
assert "_resetStreamIdleTimer();" in source
|
|
assert "_DEFAULT_STREAM_IDLE_TIMEOUT_MS = 210000" in source
|
|
assert "webui_stream_idle_grace_ms" in source
|
|
|
|
|
|
def test_chat_shows_awaiting_model_hint_after_tool_result_heartbeat() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
css = CHAT_CSS.read_text(encoding="utf-8")
|
|
heartbeat_start = source.index("_rpc.on('session.event.run_heartbeat'")
|
|
heartbeat_end = source.index(" }));", heartbeat_start)
|
|
heartbeat_body = source[heartbeat_start:heartbeat_end]
|
|
tool_result_start = source.index("function _appendToolResult(payload)")
|
|
tool_result_end = source.index(" // ── PR5", tool_result_start)
|
|
tool_result_body = source[tool_result_start:tool_result_end]
|
|
|
|
assert "const _AWAITING_MODEL_CLASS = 'awaiting-model';" in source
|
|
assert "function _showAwaitingModelHintAfterToolResult()" in source
|
|
assert "_markVisibleStreamEvent('tool_result');" in tool_result_body
|
|
assert "_showAwaitingModelHintAfterToolResult();" in heartbeat_body
|
|
assert "if (_streamBubble)" in heartbeat_body
|
|
assert "_showThinkingIndicator();" in heartbeat_body
|
|
assert ".msg.streaming.awaiting-model::after" in css
|
|
assert "content: 'waiting for model response...';" in css
|
|
assert "textContent = 'waiting for model response" not in source
|
|
assert "document.createTextNode('waiting for model response" not in source
|
|
|
|
|
|
def test_chat_streaming_indicator_uses_delayed_bottom_dock() -> None:
|
|
css = CHAT_CSS.read_text(encoding="utf-8")
|
|
|
|
assert ".msg.streaming.streaming-active-mark:not(.awaiting-model) .msg-body::after" in css
|
|
assert "padding-bottom: calc(var(--sp-2) + 24px);" in css
|
|
# Mark is corner-aligned to the content-left edge and centered in the
|
|
# reserved footer band, so the orbiting ring clears the tool card above
|
|
# and the bubble's bottom edge — no overlap, no rightward-inset orphan.
|
|
assert "left: var(--sp-4);" in css
|
|
assert "bottom: var(--sp-2);" in css
|
|
assert "left: calc(var(--sp-4) - 4px);" in css
|
|
assert "bottom: calc(var(--sp-2) - 4px);" in css
|
|
assert "left: calc(var(--sp-4) + 16px);" not in css
|
|
assert "background-image: url('../../img/opensquilla-mark.png');" in css
|
|
assert "width: 16px;" in css
|
|
assert "height: 16px;" in css
|
|
assert "background-size: 11px 11px;" in css
|
|
assert "border-radius: 50%;" in css
|
|
assert "0 0 0 1px color-mix(in srgb, var(--accent) 7%, transparent)" in css
|
|
# Orbital-sweep design: the figurative squilla mark stays UPRIGHT and
|
|
# legible (no spin on ::after); the motion lives in an orbiting ring
|
|
# rendered on ::before. The mark must not tumble.
|
|
assert "animation: squilla-activity-spin 1.6s linear infinite;" not in css
|
|
assert ".msg.streaming.streaming-active-mark:not(.awaiting-model) .msg-body::before" in css
|
|
assert "background: conic-gradient(from 0deg," in css
|
|
assert "var(--accent-secondary) 320deg," in css
|
|
assert (
|
|
"mask: radial-gradient(farthest-side, transparent calc(100% - 2px), "
|
|
"#000 calc(100% - 2px));"
|
|
in css
|
|
)
|
|
assert "animation: squilla-activity-spin 1.15s linear infinite;" in css
|
|
assert "@keyframes squilla-activity-spin" in css
|
|
assert "@media (prefers-reduced-motion: reduce)" in css
|
|
assert ".chat-tools-collapse--running > .chat-tools-summary .chat-tools-icon" not in css
|
|
assert ".msg-text-seg:not(:empty):last-of-type > :last-child::after" not in css
|
|
assert "streaming-harbor" not in css
|
|
assert "animation: squilla-harbor-peek" not in css
|
|
assert "@keyframes squilla-harbor-peek" not in css
|
|
assert "animation: squilla-harbor-spin" not in css
|
|
assert "@keyframes squilla-harbor-spin" not in css
|
|
assert "animation: squilla-harbor-wave" not in css
|
|
assert "@keyframes squilla-harbor-wave" not in css
|
|
assert "animation: squilla-dock-patrol" not in css
|
|
assert "@keyframes squilla-dock-patrol" not in css
|
|
assert "animation: shrimp-roll" not in css
|
|
assert "@keyframes shrimp-roll" not in css
|
|
|
|
|
|
def test_chat_streaming_active_mark_reveals_after_visible_bubble_delay() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
|
|
assert "const _STREAM_ACTIVE_MARK_CLASS = 'streaming-active-mark';" in source
|
|
assert "const _STREAM_ACTIVE_MARK_DELAY_MS = 3500;" in source
|
|
assert "let _streamActiveMarkVisibleStartedAt = 0;" in source
|
|
assert "function _beginStreamActiveMarkRevealWindow()" in source
|
|
|
|
start_stream_start = source.index("function _startStreaming()")
|
|
start_stream_end = source.index(" function _ensureStreamBubble", start_stream_start)
|
|
start_stream_body = source[start_stream_start:start_stream_end]
|
|
assert "_scheduleStreamActiveMarkReveal();" not in start_stream_body
|
|
assert "_beginStreamActiveMarkRevealWindow();" not in start_stream_body
|
|
|
|
ensure_start = source.index("function _ensureStreamBubble()")
|
|
ensure_end = source.index(" /** Create a new .msg-text-seg", ensure_start)
|
|
ensure_body = source[ensure_start:ensure_end]
|
|
assert "_beginStreamActiveMarkRevealWindow();" in ensure_body
|
|
assert "_maybeRevealStreamActiveMark();" in ensure_body
|
|
|
|
reveal_start = source.index("function _maybeRevealStreamActiveMark()")
|
|
reveal_end = source.index(" function _scheduleStreamActiveMarkReveal", reveal_start)
|
|
reveal_body = source[reveal_start:reveal_end]
|
|
assert (
|
|
"_streamActiveMarkVisibleStartedAt ? Date.now() - _streamActiveMarkVisibleStartedAt : 0"
|
|
in reveal_body
|
|
)
|
|
|
|
end_stream_start = source.index("function _endStreaming(opts)")
|
|
end_stream_end = source.index(" function _hasViewLocalStreamState", end_stream_start)
|
|
end_stream_body = source[end_stream_start:end_stream_end]
|
|
assert "_clearStreamActiveMarkReveal();" in end_stream_body
|
|
|
|
|
|
def test_chat_clears_awaiting_model_hint_on_next_visible_event_and_stream_reset() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
append_delta_start = source.index("function _appendDelta(text)")
|
|
append_delta_end = source.index(" function _flushPendingTextSegment", append_delta_start)
|
|
append_delta_body = source[append_delta_start:append_delta_end]
|
|
append_tool_start = source.index("function _appendToolCall(payload)")
|
|
append_tool_end = source.index(" function _appendToolResult(payload)", append_tool_start)
|
|
append_tool_body = source[append_tool_start:append_tool_end]
|
|
append_artifact_start = source.index("function _appendArtifact(payload)")
|
|
append_artifact_end = source.index(" function _renderStreamArtifacts", append_artifact_start)
|
|
append_artifact_body = source[append_artifact_start:append_artifact_end]
|
|
end_stream_start = source.index("function _endStreaming(opts)")
|
|
end_stream_end = source.index(" function _hasViewLocalStreamState", end_stream_start)
|
|
end_stream_body = source[end_stream_start:end_stream_end]
|
|
clear_state_start = source.index("function _clearViewLocalStreamState(reason)")
|
|
clear_state_end = source.index(" function _updateSendButton", clear_state_start)
|
|
clear_state_body = source[clear_state_start:clear_state_end]
|
|
|
|
assert "_markVisibleStreamEvent('text_delta');" in append_delta_body
|
|
assert "_markVisibleStreamEvent('tool_use_start');" in append_tool_body
|
|
assert "_markVisibleStreamEvent('artifact');" in append_artifact_body
|
|
assert "_clearAwaitingModelHint();" in end_stream_body
|
|
assert "_lastVisibleStreamEvent = '';" in end_stream_body
|
|
assert "_lastVisibleStreamEvent = '';" in clear_state_body
|
|
|
|
|
|
def test_chat_tool_results_use_execution_status_for_state() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
|
|
assert "function _toolExecutionStatus(payload)" in source
|
|
assert "function _toolResultIsError(payload)" in source
|
|
assert "function _toolResultStateClass(payload)" in source
|
|
assert "chat-tools-collapse--unknown" in source
|
|
assert "_toolResultIsTruncated(payload)," in source
|
|
assert "_toolResultIsTruncated(seg)," in source
|
|
|
|
|
|
def test_chat_publish_artifact_tool_cards_show_target_filename() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
build_start = source.index("function _buildToolCallDOM")
|
|
build_end = source.index(" function _findToolDetailsById", build_start)
|
|
build_body = source[build_start:build_end]
|
|
|
|
assert "function _toolDisplayName(name, input)" in source
|
|
assert "function _publishArtifactTargetName(input)" in source
|
|
assert "name === 'publish_artifact'" in source
|
|
assert "input.name || input.path" in source
|
|
assert "summary.appendChild(document.createTextNode(' ' + displayName));" in build_body
|
|
assert "_buildToolCallDOM(name, toolId, input, true)" in source
|
|
assert (
|
|
"_buildToolCallDOM(seg.name || 'tool', seg.tool_use_id || '', seg.input || '', false)"
|
|
in source
|
|
)
|
|
|
|
|
|
def test_chat_meta_step_cards_explain_long_running_work() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
css = CHAT_CSS.read_text(encoding="utf-8")
|
|
build_start = source.index("function _buildToolCallDOM")
|
|
build_end = source.index(" function _findToolDetailsById", build_start)
|
|
build_body = source[build_start:build_end]
|
|
|
|
assert "function _metaStepRunningHint(name)" in source
|
|
assert "stepId.endsWith('_video')" in source
|
|
assert "may take several minutes" in source
|
|
assert "chat-meta-step-running-hint" in build_body
|
|
assert "_metaStepRunningHint(name)" in build_body
|
|
assert ".chat-meta-step-running-hint" in css
|
|
|
|
|
|
def test_chat_memory_search_results_surface_sources() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
css = CHAT_CSS.read_text(encoding="utf-8")
|
|
|
|
assert "function _memorySearchSourceRows(content)" in source
|
|
assert "function _buildMemorySearchSourceDOM(content)" in source
|
|
assert "toolName === 'memory_search'" in source
|
|
assert "data-tool-name" in source
|
|
assert "chat-memory-source-badge--sessions" in css
|
|
assert "chat-memory-source-citation" in css
|
|
|
|
|
|
def test_chat_live_tool_result_provider_badge_is_web_search_only() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
start = source.index("function _appendToolResult(payload)")
|
|
end = source.index(" // Only show result preview", start)
|
|
live_result_body = source[start:end]
|
|
|
|
guard_start = live_result_body.index("if (toolName === 'web_search'")
|
|
block_start = live_result_body.index("{", guard_start)
|
|
depth = 0
|
|
block_end = -1
|
|
for idx in range(block_start, len(live_result_body)):
|
|
if live_result_body[idx] == "{":
|
|
depth += 1
|
|
elif live_result_body[idx] == "}":
|
|
depth -= 1
|
|
if depth == 0:
|
|
block_end = idx
|
|
break
|
|
assert block_end != -1
|
|
|
|
guarded_block = live_result_body[block_start:block_end]
|
|
assert live_result_body.count("_toolResultProvider(payload, content)") == 1
|
|
assert live_result_body.count("_injectProviderBadge") == 1
|
|
assert "_toolResultProvider(payload, content)" in guarded_block
|
|
assert "_injectProviderBadge" in guarded_block
|
|
|
|
|
|
def test_chat_tool_result_can_retitle_coerced_tool_cards() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
live_start = source.index("function _appendToolResult(payload)")
|
|
live_end = source.index(" // Only show result preview", live_start)
|
|
live_body = source[live_start:live_end]
|
|
history_start = source.index("function _reconstructToolCalls(bubbleDiv, segments)")
|
|
history_end = source.index(" function _renderMessageTags", history_start)
|
|
history_body = source[history_start:history_end]
|
|
|
|
assert "function _retitleToolCallDOM(details, name, input)" in source
|
|
assert (
|
|
"_retitleToolCallDOM(details, toolName, payload.arguments || payload.input || '')"
|
|
in live_body
|
|
)
|
|
assert "const resultToolName = seg.name || _toolNameById[toolId] || '';" in history_body
|
|
assert "_retitleToolCallDOM(details, resultToolName, seg.input || '')" in history_body
|
|
|
|
|
|
def test_chat_tool_result_full_view_uses_stable_modal_class() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
css = CHAT_CSS.read_text(encoding="utf-8")
|
|
build_start = source.index("function _buildToolResultDOM")
|
|
build_end = source.index(" function _appendToolCall", build_start)
|
|
build_body = source[build_start:build_end]
|
|
|
|
assert "class=\"chat-tool-result-full\"" in build_body
|
|
assert "style=\"white-space:pre-wrap" not in build_body
|
|
assert "viewBtn.type = 'button';" in build_body
|
|
assert "event.stopPropagation();" in build_body
|
|
assert ".chat-tool-result-full" in css
|
|
assert "overflow-wrap: anywhere;" in css
|
|
assert "max-width: min(" in css
|
|
|
|
|
|
def test_chat_historical_tool_results_stringify_non_string_payloads() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
history_start = source.index("function _reconstructToolCalls(bubbleDiv, segments)")
|
|
history_end = source.index(" function _renderMessageTags", history_start)
|
|
history_body = source[history_start:history_end]
|
|
|
|
assert "function _toolResultContent" in source
|
|
assert "const content = _toolResultContent(seg);" in history_body
|
|
assert "const content = _toolResultContent(payload);" in source
|
|
assert "JSON.stringify(raw, null, 2)" in source
|
|
|
|
|
|
def test_router_control_is_hidden_from_regular_tool_cards() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
append_tool_start = source.index("function _appendToolCall(payload)")
|
|
append_tool_end = source.index(" function _appendToolResult(payload)", append_tool_start)
|
|
append_tool_body = source[append_tool_start:append_tool_end]
|
|
append_result_start = source.index("function _appendToolResult(payload)")
|
|
append_result_end = source.index(
|
|
" // \u2500\u2500 PR5: meta-skill user_input form rendering",
|
|
append_result_start,
|
|
)
|
|
append_result_body = source[append_result_start:append_result_end]
|
|
history_start = source.index("function _reconstructToolCalls(bubbleDiv, segments)")
|
|
history_end = source.index(" function _renderMessageTags", history_start)
|
|
history_body = source[history_start:history_end]
|
|
|
|
assert "function _isControlPlaneToolName(name)" in source
|
|
assert "return name === 'router_control';" in source
|
|
assert "if (_isControlPlaneToolName(name))" in append_tool_body
|
|
assert "tool_call.append.skip_control_plane" in append_tool_body
|
|
assert "if (_isControlPlaneToolName(toolName))" in append_result_body
|
|
assert "tool_result.append.skip_control_plane" in append_result_body
|
|
assert "if (_isControlPlaneToolName(seg.name || '')) continue;" in history_body
|
|
assert "if (_isControlPlaneToolName(resultToolName)) continue;" in history_body
|
|
|
|
|
|
def test_chat_search_provider_badge_updates_running_web_search_cards() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
|
|
assert "let badge = summary.querySelector('.chat-tool-provider');" in source
|
|
assert "badge = document.createElement('span');" in source
|
|
assert "function _refreshRunningSearchProviderBadges(provider)" in source
|
|
assert (
|
|
'.chat-tools-collapse--running[data-tool-name="web_search"] .chat-tools-summary'
|
|
in source
|
|
)
|
|
assert "_setSearchProvider(res.provider)" in source
|
|
assert "_setSearchProvider(provider, { refreshRunning: false })" in source
|
|
|
|
|
|
def test_chat_url_agent_query_resolves_default_webchat_session() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
|
|
assert "function _readAgentFromUrl()" in source
|
|
assert "function _webchatSessionKey(agentId, suffix = 'default')" in source
|
|
assert "const urlAgent = _readAgentFromUrl();" in source
|
|
assert "urlSession || (urlAgent ? _webchatSessionKey(urlAgent) : storedSession)" in source
|
|
assert "url.searchParams.delete('agent');" in source
|
|
|
|
|
|
def test_chat_subscribe_does_not_advance_cursor_before_replayed_events() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
start = source.index("async function _subscribeSession()")
|
|
end = source.index(" async function _unsubscribeSession()", start)
|
|
body = source[start:end]
|
|
|
|
assert "Number(res.replayed_count || 0) <= 0" in body
|
|
assert "replayed_count" in body
|
|
|
|
|
|
def test_chat_stream_seq_drops_only_seen_duplicates_not_late_unique_events() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
|
|
assert "const _streamSeqSeenBySession = new Map();" in source
|
|
assert "const _STREAM_SEQ_SEEN_WINDOW = 800;" in source
|
|
assert "function _markSessionStreamSeqSeen(key, seq)" in source
|
|
|
|
start = source.index("function _acceptStreamSeq(payload)")
|
|
end = source.index("function _eventHasSpecificSessionHandler(event)", start)
|
|
body = source[start:end]
|
|
|
|
assert "return _markSessionStreamSeqSeen(key, seq);" in body
|
|
assert "seq <= lastSeq" not in body
|
|
|
|
marker_start = source.index("function _markSessionStreamSeqSeen(key, seq)")
|
|
marker_end = source.index("function _syncLastStreamSeqFromSession(key)", marker_start)
|
|
marker_body = source[marker_start:marker_end]
|
|
assert "if (seen.has(seq)) return false;" in marker_body
|
|
assert "seen.add(seq);" in marker_body
|
|
assert "_setSessionStreamSeq(key, seq);" in marker_body
|
|
assert "value < pruneBefore" in marker_body
|
|
|
|
|
|
def test_chat_new_session_uses_current_agent_namespace() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
click_start = source.index("newBtn.addEventListener('click', () => {")
|
|
click_end = source.index(" // Export", click_start)
|
|
click_body = source[click_start:click_end]
|
|
slash_start = source.index(" case 'new_chat':")
|
|
slash_end = source.index(" case 'reset_session':", slash_start)
|
|
slash_body = source[slash_start:slash_end]
|
|
|
|
assert "function _agentIdFromSessionKey(key)" in source
|
|
assert "return _webchatSessionKey(_agentIdFromSessionKey(_sessionKey)," in source
|
|
assert 'title="New chat session in the current agent"' in source
|
|
assert "New chat session in the current agent: " in source
|
|
assert "_loadHistory(" not in click_body
|
|
assert "_loadHistory(" not in slash_body
|
|
|
|
|
|
def test_chat_slash_menu_loads_web_chat_catalog_from_rpc() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
|
|
assert "const _SLASH_CMDS = [" not in source
|
|
assert "let _slashCmds = [];" in source
|
|
assert "let _slashCommandMap = new Map();" in source
|
|
assert "let _slashCatalogLoaded = false;" in source
|
|
assert "async function _loadSlashCommands()" in source
|
|
assert "_rpc.call('commands.list_for_surface', { surface: 'web_chat' })" in source
|
|
assert "_slashCommandMap.set(_slashCommandKey(cmd.name), cmd);" in source
|
|
assert "cmd.aliases || []" in source
|
|
assert "_loadSlashCommands();" in source
|
|
|
|
|
|
def test_chat_slash_input_supports_literal_slash_escape() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
send_start = source.index("async function _onSend()")
|
|
send_end = source.index(" // Reset abort flag for new message", send_start)
|
|
send_prefix = source[send_start:send_end]
|
|
|
|
assert "let isLiteralSlash = false;" in send_prefix
|
|
assert "if (text.startsWith('//')) {" in send_prefix
|
|
assert "isLiteralSlash = true;" in send_prefix
|
|
assert "text = text.slice(1);" in send_prefix
|
|
literal_idx = send_prefix.index("if (text.startsWith('//')) {")
|
|
normalize_idx = send_prefix.index("const normalized = await _normalizeOutgoingComposerPayload(")
|
|
slash_exec_guard_idx = send_prefix.index("if (!isLiteralSlash && text.startsWith('/')) {")
|
|
assert literal_idx < normalize_idx < slash_exec_guard_idx
|
|
assert "{ allowSlashCommand: isSlashCommand }" in send_prefix
|
|
assert "!!(await _lookupSlashCommand(text))" in send_prefix
|
|
assert "await _executeSlashCommand(text)" in send_prefix
|
|
assert "if (val.startsWith('//')) { _closeSlashMenu(); return; }" in source
|
|
|
|
|
|
def test_chat_slash_commands_are_blocked_while_streaming_after_literal_escape() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
send_start = source.index("async function _onSend()")
|
|
send_end = source.index(" // Reset abort flag for new message", send_start)
|
|
send_prefix = source[send_start:send_end]
|
|
|
|
flag_idx = send_prefix.index("let isLiteralSlash = false;")
|
|
literal_idx = send_prefix.index("if (text.startsWith('//')) {")
|
|
flag_set_idx = send_prefix.index("isLiteralSlash = true;")
|
|
normalize_idx = send_prefix.index("const normalized = await _normalizeOutgoingComposerPayload(")
|
|
streaming_idx = send_prefix.index(
|
|
"if (_isStreaming || _isCompactInFlightForCurrentSession()) {"
|
|
)
|
|
execute_idx = send_prefix.index("await _executeSlashCommand(text)")
|
|
streaming_guard = "if (isSlashCommand) {"
|
|
real_slash_guard = "if (!isLiteralSlash && text.startsWith('/')) {"
|
|
streaming_block_end = send_prefix.index(f"\n\n {real_slash_guard}", streaming_idx)
|
|
streaming_block = send_prefix[streaming_idx:streaming_block_end]
|
|
|
|
assert flag_idx < literal_idx < normalize_idx < streaming_idx
|
|
assert literal_idx < flag_set_idx < normalize_idx
|
|
assert "text = text.slice(1);" in send_prefix[literal_idx:normalize_idx]
|
|
assert "text = normalized.text;" in send_prefix[normalize_idx:streaming_idx]
|
|
assert (
|
|
"attachmentsForSend = normalized.attachments;"
|
|
in send_prefix[normalize_idx:streaming_idx]
|
|
)
|
|
assert streaming_guard in streaming_block
|
|
assert "const waitReason = _isCompactInFlightForCurrentSession()" in streaming_block
|
|
assert "Wait for ${waitReason} before running" in streaming_block
|
|
assert "_executeSlashCommand" not in streaming_block
|
|
assert streaming_idx < execute_idx
|
|
assert real_slash_guard in send_prefix[streaming_idx:execute_idx]
|
|
|
|
|
|
def test_chat_slash_executor_lets_unknown_slash_input_fall_through() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
lookup_start = source.index("async function _lookupSlashCommand(text)")
|
|
lookup_end = source.index("async function _executeSlashCommand(text)", lookup_start)
|
|
exec_start = source.index("async function _executeSlashCommand(text)")
|
|
exec_end = source.index(" /* ── Send Message", exec_start)
|
|
lookup = source[lookup_start:lookup_end]
|
|
executor = source[exec_start:exec_end]
|
|
|
|
assert "await _loadSlashCommands()" in lookup
|
|
assert "_slashCommandMap.get(_slashCommandKey(cmdText)) || null" in lookup
|
|
assert "const cmd = await _lookupSlashCommand(text);" in executor
|
|
assert "Unsupported command" not in executor
|
|
assert "return false;" in executor
|
|
assert "chat.send" not in executor
|
|
|
|
|
|
def test_chat_image_paste_prevents_default_only_after_attachment_acceptance() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
paste_start = source.index("const pasteHandler = (e) => {")
|
|
paste_end = source.index(" document.addEventListener('paste', pasteHandler);", paste_start)
|
|
paste_body = source[paste_start:paste_end]
|
|
|
|
assert "let consumedAttachment = false;" in paste_body
|
|
assert "if (file && _addAttachment(file)) consumedAttachment = true;" in paste_body
|
|
assert "if (consumedAttachment) e.preventDefault();" in paste_body
|
|
assert paste_body.index("_addAttachment(file)") < paste_body.index("e.preventDefault()")
|
|
|
|
|
|
def test_chat_add_attachment_reports_acceptance_for_paste_handler() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
add_start = source.index("function _addAttachment(file)")
|
|
add_end = source.index(
|
|
" async function _uploadAttachmentStaged(file, mime, localId)",
|
|
add_start,
|
|
)
|
|
add_body = source[add_start:add_end]
|
|
|
|
assert "return false;" in add_body[
|
|
add_body.index("if (!_isAllowedAttachmentMime(mime))") :
|
|
add_body.index("const hardCap = _attachmentHardCapBytes(mime);")
|
|
]
|
|
assert "return false;" in add_body[
|
|
add_body.index("if (file.size > hardCap)") :
|
|
add_body.index("const localId = _nextAttachmentId++;")
|
|
]
|
|
assert "reader.readAsDataURL(file);" in add_body
|
|
assert "reader.readAsDataURL(file);\n return true;" in add_body
|
|
assert "_uploadAttachmentStaged(file, mime, localId).catch" in add_body
|
|
assert add_body.rstrip().endswith("return true;\n }")
|
|
|
|
|
|
def test_chat_usage_slash_commands_call_usage_rpcs() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
select_start = source.index("function _selectSlashCmd(cmd, args = '')")
|
|
select_end = source.index(" async function _executeSlashCommand(text)", select_start)
|
|
selector = source[select_start:select_end]
|
|
|
|
assert "case 'usage_status':" in selector
|
|
assert (
|
|
"const usageMethod = args.trim().toLowerCase() === 'cost' "
|
|
"? 'usage.cost' : 'usage.status';"
|
|
) in selector
|
|
assert "_rpc.call(usageMethod)" in selector
|
|
assert "Usage cost" in source
|
|
|
|
|
|
def test_chat_usage_slash_status_reads_top_level_and_totals_fields() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
select_start = source.index("function _selectSlashCmd(cmd, args = '')")
|
|
usage_start = source.index("case 'usage_status':", select_start)
|
|
usage_end = source.index(" .catch((err) => UI.toast('Usage failed:", usage_start)
|
|
usage_block = source[usage_start:usage_end]
|
|
|
|
for field_name in (
|
|
"result?.totalTokens",
|
|
"result?.total_tokens",
|
|
"result?.totalCostUsd",
|
|
"result?.total_cost_usd",
|
|
"totals.tokens",
|
|
"totals.total_tokens",
|
|
"totals.cost",
|
|
"totals.cost_usd",
|
|
):
|
|
assert field_name in usage_block
|
|
|
|
|
|
def test_chat_switching_existing_session_does_not_mark_new_chat_intent() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
switch_start = source.index("function _switchToSession(key)")
|
|
switch_end = source.index(" function _bindSessionChip()", switch_start)
|
|
switch_body = source[switch_start:switch_end]
|
|
|
|
assert "_pendingSessionIntent = 'new_chat'" not in switch_body
|
|
assert source.count("_pendingSessionIntent = 'new_chat'") == 2
|
|
assert (
|
|
"const sessionIntentForSend = textOverride !== null ? null : _pendingSessionIntent;"
|
|
in source
|
|
)
|
|
assert "params.intent = sessionIntentForSend;" in source
|
|
|
|
|
|
def test_chat_regenerate_targets_clicked_assistant_bubble() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
hover_start = source.index("function _bindHoverActions()")
|
|
hover_end = source.index(" function _truncate", hover_start)
|
|
hover_body = source[hover_start:hover_end]
|
|
regen_start = source.index("function _regenerateAssistantBubble(bubble)")
|
|
regen_end = source.index(" // Pop the user message back into the textarea", regen_start)
|
|
regen_body = source[regen_start:regen_end]
|
|
|
|
assert "_regenerateAssistantBubble(bubble);" in hover_body
|
|
assert "_regenerateLastTurn" not in source
|
|
assert "querySelectorAll(':scope > .msg.assistant')" in regen_body
|
|
assert "const assistantOrdinal = assistantBubbles.indexOf(bubble);" in regen_body
|
|
assert "assistantSeen === assistantOrdinal" in regen_body
|
|
assert "_messages.splice(userIdx + 1);" in regen_body
|
|
|
|
|
|
def test_legacy_chat_middle_edit_sends_fork_before_message_id() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
switch_start = source.index("function _switchToSession(key)")
|
|
switch_end = source.index(" function _bindSessionChip()", switch_start)
|
|
switch_body = source[switch_start:switch_end]
|
|
regen_start = source.index("function _regenerateAssistantBubble(bubble)")
|
|
regen_end = source.index(" // Pop the user message back into the textarea", regen_start)
|
|
regen_body = source[regen_start:regen_end]
|
|
edit_start = source.index("function _editUserBubble(bubble)")
|
|
edit_end = source.index(" function _bindHoverActions()", edit_start)
|
|
edit_body = source[edit_start:edit_end]
|
|
history_start = source.index("async function _loadHistory(opts = {}) {")
|
|
history_end = source.index(" /* ── Send Message", history_start)
|
|
history_body = source[history_start:history_end]
|
|
send_start = source.index("async function _onSend()")
|
|
send_end = source.index(" /* ── Streaming", send_start)
|
|
send_body = source[send_start:send_end]
|
|
|
|
assert "let _pendingForkBeforeMessageId = null;" in source
|
|
assert "messageId: stableIdentity," in history_body
|
|
assert "_pendingForkBeforeMessageId = _messages[userIdx].messageId || null;" in regen_body
|
|
assert "_pendingForkBeforeMessageId = _messages[cutIdx].messageId || null;" in edit_body
|
|
assert "_pendingForkBeforeMessageId = null;" in switch_body
|
|
assert (
|
|
"const forkBeforeMessageIdForSend = textOverride !== null "
|
|
"? null : _pendingForkBeforeMessageId;"
|
|
) in send_body
|
|
assert "params.forkBeforeMessageId = forkBeforeMessageIdForSend;" in send_body
|
|
assert "_pendingForkBeforeMessageId = null;" in send_body
|
|
|
|
|
|
def test_chat_maps_task_terminal_events_during_migration() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
|
|
assert "function _taskTerminalAsSessionEvent(event, payload)" in source
|
|
assert "task.failed" in source
|
|
assert "task.timeout" in source
|
|
assert "task.abandoned" in source
|
|
assert "task.cancelled" in source
|
|
assert "function _taskTerminalMessage(status, payload)" in source
|
|
assert "function _sessionErrorMessage(payload)" in source
|
|
assert "payload?.terminal_message" in source
|
|
terminal_mapper = source[
|
|
source.index("function _taskTerminalAsSessionEvent(event, payload)") :
|
|
source.index("function _taskTerminalMessage(status, payload)")
|
|
]
|
|
assert "Gateway task" not in terminal_mapper
|
|
error_start = source.index("} else if (event.endsWith('.error'))")
|
|
error_end = source.index("if (_activeTaskGroups.size > 0)", error_start)
|
|
error_handler = source[error_start:error_end]
|
|
assert "_sessionErrorMessage(payload)" in error_handler
|
|
|
|
|
|
def test_chat_succeeded_task_without_done_falls_back_to_history_sync() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
|
|
wildcard_start = source.index("const terminalStatus = _taskTerminalStatus(rawEvent);")
|
|
wildcard_end = source.index(
|
|
"const normalized = _taskTerminalAsSessionEvent(rawEvent, rawPayload);"
|
|
)
|
|
terminal_handler = source[wildcard_start:wildcard_end]
|
|
fallback_start = source.index("function _scheduleSucceededTaskTerminalSync(payload = {})")
|
|
fallback_end = source.index(" function _taskTerminalAsSessionEvent", fallback_start)
|
|
fallback = source[fallback_start:fallback_end]
|
|
|
|
assert "rawEvent === 'task.succeeded'" in terminal_handler
|
|
assert "_scheduleSucceededTaskTerminalSync(rawPayload);" in terminal_handler
|
|
assert "let _streamGeneration = 0;" in source
|
|
assert "_streamGeneration += 1;" in source
|
|
assert "const streamGeneration = _streamGeneration;" in fallback
|
|
assert "_scheduleHistorySync();" in fallback
|
|
assert "if (_isStreaming && _streamGeneration === streamGeneration)" in fallback
|
|
assert "_endStreaming();" in fallback
|
|
|
|
|
|
def test_chat_reconciles_terminal_session_changed_events() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
|
|
assert "if (value === 'killed') return 'cancelled';" in source
|
|
assert "function _sessionChangeIsTerminal(payload)" in source
|
|
assert "function _syncTerminalSessionChange(payload = {})" in source
|
|
sessions_changed = source[
|
|
source.index("_rpc.on('sessions.changed'") :
|
|
source.index("_rpc.on('task.queued'", source.index("_rpc.on('sessions.changed'"))
|
|
]
|
|
assert "_sessionChangeIsTerminal(payload)" in sessions_changed
|
|
assert "_syncTerminalSessionChange(payload);" in sessions_changed
|
|
assert "_applySessionRunState(payload);" in sessions_changed
|
|
done_handler = source[
|
|
source.index("const _doneWasAborted = payload?.reason === 'aborted';") :
|
|
source.index("} else if (event.endsWith('.error'))")
|
|
]
|
|
assert "run_status: 'cancelled'" in done_handler
|
|
assert "status: 'cancelled'" in done_handler
|
|
|
|
|
|
def test_chat_failed_task_message_prefers_payload_error_detail() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
|
|
start = source.index("function _taskTerminalMessage(status, payload)")
|
|
end = source.index(" function _sessionErrorMessage(payload)", start)
|
|
body = source[start:end]
|
|
|
|
assert "const failedDetail = _payloadErrorDetail(payload);" in body
|
|
assert "if (failedDetail) return failedDetail;" in body
|
|
assert "function _payloadErrorDetail(payload)" in source
|
|
for field_name in ("error", "message", "error_message", "detail"):
|
|
assert f"payload?.{field_name}" in source
|
|
|
|
|
|
def test_chat_error_event_refreshes_from_persisted_transcript() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
|
|
error_start = source.index("} else if (event.endsWith('.error'))")
|
|
error_end = source.index(" }", source.index("_applySessionRunState({", error_start))
|
|
error_body = source[error_start:error_end]
|
|
|
|
assert "_addMessage('error', _sessionErrorMessage(payload));" in error_body
|
|
assert "_scheduleHistorySync();" in error_body
|
|
|
|
|
|
def test_chat_subscribe_failure_is_visible() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
|
|
assert "Session stream subscription failed:" in source
|
|
assert "No subscription manager available" in source
|
|
|
|
|
|
def test_chat_subscribe_uses_stream_replay_cursor() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
start = source.index("async function _subscribeSession() {")
|
|
end = source.index(" async function _unsubscribeSession()", start)
|
|
body = source[start:end]
|
|
|
|
assert "let _lastStreamSeq = 0;" in source
|
|
assert "const _streamSeqBySession = new Map();" in source
|
|
assert "params.since_stream_seq = _sessionStreamSeq(subscribeKey);" in source
|
|
assert "params.since_stream_seq = _lastStreamSeq;" not in source
|
|
assert "if (_lastStreamSeq > 0) params.since_stream_seq = _lastStreamSeq;" not in body
|
|
assert "function _acceptStreamSeq(payload)" in source
|
|
assert "function _replayGapShouldWarn(reason)" in source
|
|
assert "Session stream gap detected; reloading transcript." not in body
|
|
|
|
|
|
def test_chat_stream_handlers_drop_replayed_duplicate_frames() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
start = source.index("function _subscribeRpcEvents() {")
|
|
end = source.index(" /* ── Chat History", start)
|
|
body = source[start:end]
|
|
|
|
assert "function _acceptStreamSeq(payload)" in source
|
|
assert "if (!_acceptStreamSeq(payload)) return;" in body
|
|
assert "_noteStreamSeq(payload);" not in body
|
|
|
|
|
|
def test_chat_replayed_wait_events_do_not_bootstrap_live_thinking() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
start = source.index("function _subscribeRpcEvents() {")
|
|
end = source.index(" /* ── Chat History", start)
|
|
body = source[start:end]
|
|
|
|
assert "function _dropReplayedLiveWaitEvent(meta, payload, eventName)" in source
|
|
assert "_rpc.on('session.event.state_change', (payload, meta = {}) =>" in body
|
|
assert "_rpc.on('session.event.run_heartbeat', (payload, meta = {}) =>" in body
|
|
assert "_dropReplayedLiveWaitEvent(meta, payload, 'event.state_change')" in body
|
|
assert "_dropReplayedLiveWaitEvent(meta, payload, 'event.run_heartbeat')" in body
|
|
|
|
subscribe_start = source.index("async function _subscribeSession() {")
|
|
subscribe_end = source.index(" async function _unsubscribeSession()", subscribe_start)
|
|
subscribe_body = source[subscribe_start:subscribe_end]
|
|
assert "const subscribedState = _sessionRunStatus(res);" in subscribe_body
|
|
assert "_runStatusIsActive(subscribedState.status)" in subscribe_body
|
|
|
|
|
|
def test_chat_stream_seq_cursor_is_scoped_per_session() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
start = source.index("function _acceptStreamSeq(payload)")
|
|
end = source.index(" function _showThinkingIndicator()", start)
|
|
body = source[start:end]
|
|
|
|
assert "function _sessionStreamSeq(key)" in source
|
|
assert "function _setSessionStreamSeq(key, seq)" in source
|
|
assert "function _syncLastStreamSeqFromSession(key)" in source
|
|
assert "function _markSessionStreamSeqSeen(key, seq)" in source
|
|
assert "const key = _sessionKeyFromPayload(payload) || _sessionKey || '';" in body
|
|
assert "return _markSessionStreamSeqSeen(key, seq);" in body
|
|
assert "seq <= lastSeq" not in body
|
|
assert "_syncLastStreamSeqFromSession(canonicalKey);" in source
|
|
|
|
|
|
def test_chat_surfaces_persisted_run_state_in_header_and_session_picker() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
css = CHAT_CSS.read_text(encoding="utf-8")
|
|
|
|
assert 'id="chat-run-status"' in source
|
|
assert "function _sessionRunStatus(source)" in source
|
|
assert "function _applySessionRunState(source)" in source
|
|
assert "_applySessionRunState(res);" in source
|
|
assert "_applySessionRunState({ run_status: 'running'" in source
|
|
assert "chat-session-popover-item-run" in source
|
|
# Run-status pill renders as a shared .chip with a color modifier picked
|
|
# by the _runStatusChipClass helper (see components.css for .chip styling).
|
|
assert "_runStatusChipClass" in source
|
|
assert ".chat-session-popover-item-run" in css
|
|
|
|
|
|
def test_chat_resets_replay_cursor_after_stream_gap() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
start = source.index("async function _subscribeSession() {")
|
|
end = source.index(" async function _unsubscribeSession()", start)
|
|
body = source[start:end]
|
|
|
|
assert "if (res && res.replay_complete === false)" in body
|
|
assert "_setSessionStreamSeq(subscribeKey, res.current_stream_seq);" in body
|
|
assert "const replayGapReason = res.replay_gap_reason || res.replayGapReason || '';" in body
|
|
assert "if (_replayGapShouldWarn(replayGapReason))" in body
|
|
assert "_loadHistory(_historyRefreshScrollOptions());" in body
|
|
assert body.index("_setSessionStreamSeq(subscribeKey, res.current_stream_seq);") < body.index(
|
|
"_loadHistory(_historyRefreshScrollOptions());"
|
|
)
|
|
assert body.index("if (_replayGapShouldWarn(replayGapReason))") < body.index(
|
|
"_loadHistory(_historyRefreshScrollOptions());"
|
|
)
|
|
|
|
|
|
def test_chat_replay_gap_history_refresh_preserves_reader_scroll() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
|
|
assert "function _historyRefreshScrollOptions()" in source
|
|
start = source.index("function _historyRefreshScrollOptions()")
|
|
end = source.index("function _scheduleHistorySync", start)
|
|
body = source[start:end]
|
|
|
|
assert "!_thread || !_historyHasRendered" in body
|
|
assert "_thread.scrollHeight - _thread.scrollTop - _thread.clientHeight" in body
|
|
assert "if (gap < 60) return {};" in body
|
|
assert "preserveScroll: true" in body
|
|
assert "previousScrollHeight: _thread.scrollHeight" in body
|
|
assert "previousScrollTop: _thread.scrollTop" in body
|
|
|
|
|
|
def test_chat_empty_history_preserves_live_stream_bubble() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
start = source.index("function _renderHistoryMessages(messages")
|
|
end = source.index(" const existingByStableIdentity = new Map();", start)
|
|
body = source[start:end]
|
|
|
|
live_guard = "if (_isStreaming && ("
|
|
assert "const liveUserAnchor = _currentSessionLiveUserAnchor(_sessionKey || '');" in body
|
|
assert (
|
|
"const liveThinking = _isCurrentSessionThinkingIndicator(_thinkingEl) "
|
|
"? _thinkingEl : null;"
|
|
) in body
|
|
assert live_guard in body
|
|
assert "_isCurrentSessionStreamBubble(_streamBubble)" in body
|
|
assert "|| liveRouterStrips.length > 0" in body
|
|
assert "|| liveUserAnchor" in body
|
|
assert "|| liveThinking" in body
|
|
assert (
|
|
"if (liveUserAnchor && !liveUserAnchor.isConnected) "
|
|
"_thread.appendChild(liveUserAnchor);"
|
|
) in body
|
|
assert "_thread.appendChild(_streamBubble);" in body
|
|
assert "return;" in body[body.index(live_guard) :]
|
|
|
|
|
|
def test_chat_live_stream_bubble_is_session_scoped() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
|
|
assert "let _streamSessionKey = '';" in source
|
|
assert "function _isCurrentSessionStreamBubble(el)" in source
|
|
assert "streamKey === currentKey" in source
|
|
assert (
|
|
"_streamBubble.dataset.streamSessionKey = "
|
|
"_streamSessionKey || _sessionKey || '';"
|
|
) in source
|
|
assert "if (_isStreaming && el === _streamBubble) return;" not in source
|
|
|
|
|
|
def test_chat_session_switch_parks_and_restores_live_stream_state() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
start = source.index("function _switchToSession(key)")
|
|
end = source.index(" function _bindSessionChip()", start)
|
|
body = source[start:end]
|
|
|
|
assert "const _liveStreamStateBySession = new Map();" in source
|
|
assert "_parkCurrentSessionStreamState('session_switch');" in body
|
|
assert "_restoreLiveStreamStateForSession(_sessionKey);" in body
|
|
assert body.index("_parkCurrentSessionStreamState('session_switch');") < body.index(
|
|
"_persistSession(key);"
|
|
)
|
|
assert body.index("_restoreLiveStreamStateForSession(_sessionKey);") < body.index(
|
|
"_subscribeSession();"
|
|
)
|
|
assert "function _parkCurrentSessionStreamState(reason)" in source
|
|
assert "function _restoreLiveStreamStateForSession(key)" in source
|
|
assert "function _clearViewLocalStreamState(reason)" in source
|
|
assert "function _currentSessionLiveRouterStrips(key = _sessionKey || '')" in source
|
|
assert "function _currentSessionLiveUserAnchor(key = _sessionKey || '')" in source
|
|
assert "routerStrips," in source
|
|
assert "liveUserAnchor," in source
|
|
assert "_routerFxPauseScanTimers(el);" in source
|
|
assert "_routerFxResumeLiveStrip(el);" in source
|
|
|
|
|
|
def test_chat_session_switch_preserves_live_router_strip_without_stream_bubble() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
|
|
has_start = source.index("function _hasViewLocalStreamState()")
|
|
has_end = source.index("function _parkCurrentSessionStreamState(reason)", has_start)
|
|
has_body = source[has_start:has_end]
|
|
assert (
|
|
"_currentSessionLiveRouterStrips("
|
|
"_streamSessionKey || _sessionKey || '').length"
|
|
) in has_body
|
|
assert "_thinkingEl" in has_body
|
|
assert "_thinkingDelayTimer" in has_body
|
|
|
|
restore_start = source.index("function _restoreLiveStreamStateForSession(key)")
|
|
restore_end = source.index("function _clearViewLocalStreamState(reason)", restore_start)
|
|
restore_body = source[restore_start:restore_end]
|
|
assert (
|
|
"const routerStrips = Array.isArray(state.routerStrips) "
|
|
"? state.routerStrips : [];"
|
|
) in restore_body
|
|
assert "const liveUserAnchor = state.liveUserAnchor || null;" in restore_body
|
|
assert "if (_thread && liveUserAnchor && !liveUserAnchor.isConnected)" in restore_body
|
|
assert "_insertLiveRouterStripForAnchor(el, liveUserAnchor, _streamBubble);" in restore_body
|
|
assert "_routerFxResumeLiveStrip(el);" in restore_body
|
|
|
|
|
|
def test_chat_empty_history_keeps_live_router_only_running_view() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
history_start = source.index("function _renderHistoryMessages(messages")
|
|
history_end = source.index("const existingByStableIdentity = new Map();", history_start)
|
|
history_body = source[history_start:history_end]
|
|
|
|
assert (
|
|
"const liveRouterStrips = "
|
|
"_currentSessionLiveRouterStrips(_sessionKey || '');"
|
|
) in history_body
|
|
assert (
|
|
"const liveUserAnchor = _currentSessionLiveUserAnchor(_sessionKey || '');"
|
|
) in history_body
|
|
assert (
|
|
"_isCurrentSessionStreamBubble(_streamBubble)"
|
|
) in history_body
|
|
assert "|| liveRouterStrips.length > 0" in history_body
|
|
assert "|| liveUserAnchor" in history_body
|
|
assert "|| liveThinking" in history_body
|
|
assert "_chatDiag('history.empty.keep_live_stream_view'" in history_body
|
|
|
|
|
|
def test_chat_session_switch_preserves_live_user_anchor_for_router_restore() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
|
|
helper_start = source.index("function _currentSessionLiveUserAnchor(")
|
|
helper_end = source.index("function _insertLiveRouterStripForAnchor(", helper_start)
|
|
helper_body = source[helper_start:helper_end]
|
|
assert "const routerStrips = _currentSessionLiveRouterStrips(key);" in helper_body
|
|
assert "if (_isUserMessageElement(prev)) return prev;" in helper_body
|
|
assert "const streamAnchor = _routerFxUserMessageForAssistant(_streamBubble);" in helper_body
|
|
assert "return _isStreaming ? _routerFxLastUserMessage() : null;" in helper_body
|
|
|
|
park_start = source.index("function _parkCurrentSessionStreamState(reason)")
|
|
park_end = source.index("function _restoreLiveStreamStateForSession(key)", park_start)
|
|
park_body = source[park_start:park_end]
|
|
assert "const liveUserAnchor = _currentSessionLiveUserAnchor(key);" in park_body
|
|
assert "liveUserAnchor," in park_body
|
|
assert "if (liveUserAnchor && liveUserAnchor.parentNode) liveUserAnchor.remove();" in park_body
|
|
|
|
restore_start = source.index("function _restoreLiveStreamStateForSession(key)")
|
|
restore_end = source.index("function _clearViewLocalStreamState(reason)", restore_start)
|
|
restore_body = source[restore_start:restore_end]
|
|
assert "const liveUserAnchor = state.liveUserAnchor || null;" in restore_body
|
|
assert "_thread.appendChild(liveUserAnchor);" in restore_body
|
|
assert "_insertLiveRouterStripForAnchor(el, liveUserAnchor, _streamBubble);" in restore_body
|
|
|
|
|
|
def test_chat_history_cleanup_keeps_live_user_anchor_during_streaming() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
history_start = source.index("function _renderHistoryMessages(messages")
|
|
history_end = source.index("function _appendHistoryDaySeparator", history_start)
|
|
history_body = source[history_start:history_end]
|
|
|
|
assert (
|
|
"const liveUserAnchor = _currentSessionLiveUserAnchor(_sessionKey || '');"
|
|
) in history_body
|
|
assert "if (_isStreaming && el === liveUserAnchor) return;" in history_body
|
|
|
|
|
|
def test_chat_router_restore_does_not_auto_settle_without_cached_decision() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
|
|
resume_start = source.index("function _routerFxResumeLiveStrip(wrap)")
|
|
resume_end = source.index(" // When output begins", resume_start)
|
|
resume_body = source[resume_start:resume_end]
|
|
assert "if (wrap._fxDecision) {" in resume_body
|
|
scan_cap = (
|
|
"wrap._fxScanCap = setTimeout(() => _routerFxFinishScan(wrap), "
|
|
"_ROUTER_FX_SCAN_MS);"
|
|
)
|
|
assert scan_cap in resume_body
|
|
assert "_chatDiag('router_scan.resume_without_decision'" in resume_body
|
|
assert resume_body.index("if (wrap._fxDecision) {") < resume_body.index(
|
|
"wrap._fxScanCap = setTimeout"
|
|
)
|
|
|
|
settle_start = source.index("function _routerFxSettleForOutput()")
|
|
settle_end = source.index(" // Lock an in-flight scanning strip", settle_start)
|
|
settle_body = source[settle_start:settle_end]
|
|
assert "if (wrap._fxDecision) {" in settle_body
|
|
assert "_routerFxFinishScan(wrap);" in settle_body
|
|
assert "_chatDiag('router_scan.keep_scanning_without_decision_on_output'" in settle_body
|
|
|
|
|
|
def test_chat_session_events_drop_foreign_session_before_stream_seq_acceptance() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
|
|
assert "function _dropForeignSessionPayload(event, payload)" in source
|
|
for event_name in (
|
|
"session.event.router_decision",
|
|
"session.event.text_delta",
|
|
"session.event.tool_use_start",
|
|
"session.event.tool_result",
|
|
"session.event.state_change",
|
|
"session.event.run_heartbeat",
|
|
):
|
|
start = source.index(f"_rpc.on('{event_name}'")
|
|
end = source.index(" }));", start)
|
|
body = source[start:end]
|
|
assert "_dropForeignSessionPayload(" in body
|
|
assert body.index("_dropForeignSessionPayload(") < body.index("_acceptStreamSeq(payload)")
|
|
|
|
|
|
def test_chat_task_succeeded_clears_run_state_without_session_done() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
|
|
assert "function _taskTerminalStatus(event)" in source
|
|
assert "['succeeded', 'failed', 'timeout', 'abandoned', 'cancelled']" in source
|
|
assert "terminalStatus === 'succeeded' ? 'idle'" in source
|
|
|
|
|
|
def test_chat_tracks_background_task_groups_as_active_run_state() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
|
|
assert "let _activeTaskGroups = new Set();" in source
|
|
assert "function _clearActiveTaskGroups()" in source
|
|
assert "function _noteTaskGroupActive(payload)" in source
|
|
assert "function _noteTaskGroupTerminal(payload, terminalStatus)" in source
|
|
assert "session.event.task_group.waiting" in source
|
|
assert "session.event.task_group.synthesizing" in source
|
|
assert "session.event.task_group.done" in source
|
|
assert "session.event.task_group.failed" in source
|
|
assert "if (event.startsWith('session.event.task_group.')) return;" in source
|
|
assert "_activeTaskGroups.size > 0" in source
|
|
|
|
|
|
def test_chat_surfaces_compaction_lifecycle_status_and_exception_toasts() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
compact_block = source[
|
|
source.index("case 'compact_context':") : source.index("case 'usage_status':")
|
|
]
|
|
start = source.index("function _showCompactionToast(payload, meta = {})")
|
|
end = source.index(" /* ── RPC Event Subscriptions", start)
|
|
body = source[start:end]
|
|
|
|
assert "function _showCompactionToast(payload, meta = {})" in source
|
|
assert "_setCompactInFlight(true, compactKey);" in compact_block
|
|
assert "_syncCompactionSeparator(" in compact_block
|
|
assert "context compacting" in source
|
|
assert "_compactionStatusLabel(payload || {}, source, status)" in source
|
|
assert "Already within context budget; no compact was applied." in source
|
|
assert "Context compaction could not be applied" in source
|
|
assert "No compactable chat history yet." in source
|
|
assert "Context was left unchanged because no usable summary was produced." in source
|
|
assert "if (compactKey !== _sessionKey) return;" in compact_block
|
|
assert (
|
|
"_showCompactionToast({ ...(result || {}), key: compactKey, source: 'manual'"
|
|
in compact_block
|
|
)
|
|
assert "session.event.compaction" in source
|
|
assert "Context compacted older messages to keep this session within budget" not in source
|
|
assert "Continuing with temporary context compaction" in source
|
|
assert "Continuing with temporary context compaction for this turn" in body
|
|
assert "Compact cancelled" in source
|
|
assert "function _compactionUserVisible(payload, source, status)" in source
|
|
assert "!_compactionUserVisible(payload || {}, source, status)" in source
|
|
assert "structured content noop" not in source.lower()
|
|
|
|
|
|
def test_chat_compaction_uses_single_in_thread_separator_surface() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
css = CHAT_CSS.read_text(encoding="utf-8")
|
|
start = source.index("function _showCompactionToast(payload, meta = {})")
|
|
end = source.index(" /* ── RPC Event Subscriptions", start)
|
|
body = source[start:end]
|
|
|
|
# The composer pill (#chat-compact-status) is gone; the in-thread context
|
|
# separator is the single compaction surface for every lifecycle state.
|
|
assert 'id="chat-compact-status"' not in source
|
|
assert "_compactStatusEl" not in source
|
|
assert "function _setCompactStatus(" not in source
|
|
assert "function _hideCompactStatus(" not in source
|
|
|
|
# Lightweight separator state + renderer remain; no metrics, phase cards,
|
|
# lifecycle chips, or rail panel are constructed.
|
|
assert "let _compactionSeparatorEl = null;" in source
|
|
assert "let _compactionSeparatorTimer = null;" in source
|
|
assert "function _syncCompactionSeparator(payload, status, source, overrides = {})" in source
|
|
assert "function _hideCompactionSeparator()" in source
|
|
assert "chat-context-rail__panel" not in source
|
|
assert "function _compactionLifecycleSteps(" not in source
|
|
assert "function _compactionMetricItems(" not in source
|
|
|
|
# Every lifecycle event renders through the one rail call; the branches below
|
|
# only drive non-UI side effects + the corner toasts.
|
|
assert "_syncCompactionSeparator(payload || {}, status, source);" in body
|
|
assert "function _compactionSkipMessage(payload, source)" in source
|
|
assert "if (_INTERNAL_COMPACTION_SKIP_REASONS.has(reason)) return '';" in source
|
|
assert "Request-scoped; session history was not rewritten" in source
|
|
assert "No usable summary was produced" in source
|
|
assert "status === 'emergency_ephemeral'" in body
|
|
assert "status === 'observed'" in body
|
|
|
|
# destroy() tears down the separator (the only surface) — no pill teardown remains.
|
|
assert "_hideCompactionSeparator();" in source[source.index("function destroy()") :]
|
|
assert "_hideCompactStatus(" not in source
|
|
|
|
# Pill/rail CSS removed; separator CSS is the only compaction thread surface.
|
|
assert ".chat-compact-status" not in css
|
|
assert ".chat-context-rail" not in css
|
|
assert ".chat-context-separator" in css
|
|
assert ".chat-context-separator::before" in css
|
|
assert ".chat-context-separator::after" in css
|
|
assert ".chat-context-separator--live span" in css
|
|
assert "contextSeparatorShimmer 2.2s linear infinite" in css
|
|
assert "@keyframes contextSeparatorShimmer" in css
|
|
assert "prefers-reduced-motion: reduce" in css
|
|
assert "contextPulse" not in css
|
|
|
|
|
|
def test_chat_compaction_summary_separator_anchors_to_transcript_ids() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
sync_start = source.index(
|
|
"function _syncCompactionSeparator(payload, status, source, overrides = {})"
|
|
)
|
|
sync_end = source.index("function _compactFailureBlocksPending", sync_start)
|
|
sync_body = source[sync_start:sync_end]
|
|
history_start = source.index("async function _loadHistory(opts = {}) {")
|
|
history_end = source.index("_chatDiag('history.done'", history_start)
|
|
history_body = source[history_start:history_end]
|
|
|
|
assert "function _renderCompactionSummarySeparators(messages)" in source
|
|
assert "function _clearCompactionSummarySeparators()" in source
|
|
assert "if (inserted > 0) _hideCompactionSeparator();" in sync_body
|
|
assert "covered_through_id" in source
|
|
assert "transcript_id" in source
|
|
assert "_renderCompactionSummarySeparators(messages);" in history_body
|
|
assert "history.scroll.compaction_summary_anchor" not in history_body
|
|
assert "_scrollHistoryAnchorIntoView(" not in history_body
|
|
assert "_placeCompactionRail(" not in history_body
|
|
assert "function _scheduleCompactionSeparatorRemoval(delayMs = 4500)" in source
|
|
assert "_scheduleCompactionSeparatorRemoval();" in sync_body
|
|
assert "status === 'skipped'" in sync_body
|
|
|
|
|
|
def test_chat_current_compaction_separator_bottom_anchors_and_autoscrolls() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
css = CHAT_CSS.read_text(encoding="utf-8")
|
|
sync_start = source.index(
|
|
"function _syncCompactionSeparator(payload, status, source, overrides = {})"
|
|
)
|
|
sync_end = source.index("function _clearCompactionSummarySeparators", sync_start)
|
|
sync_body = source[sync_start:sync_end]
|
|
|
|
assert "chat-context-separator--session" in sync_body
|
|
assert "if (_autoScroll) _scrollToBottom();" in sync_body
|
|
assert ".chat-context-separator--session" in css
|
|
assert "margin-top: auto;" in css
|
|
|
|
|
|
def test_chat_compaction_separator_does_not_render_token_details() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
# The in-thread separator is intentionally terse; token/debug details stay
|
|
# out of the chat transcript visual surface.
|
|
assert "function _compactionTokenStats(" not in source
|
|
assert "function _compactionMetricItems(" not in source
|
|
assert "function _compactionLifecycleSteps(" not in source
|
|
sync_start = source.index(
|
|
"function _syncCompactionSeparator(payload, status, source, overrides = {})"
|
|
)
|
|
sync_end = source.index("function _compactFailureBlocksPending", sync_start)
|
|
sync_body = source[sync_start:sync_end]
|
|
assert "tokens_before" not in sync_body
|
|
assert "tokens_after" not in sync_body
|
|
assert "remaining_budget_tokens" not in sync_body
|
|
|
|
start = source.index("function _showCompactionToast(payload, meta = {})")
|
|
end = source.index(" /* ── RPC Event Subscriptions", start)
|
|
body = source[start:end]
|
|
skipped_start = body.index("if (status === 'skipped')")
|
|
skipped_end = body.index("const semanticNotice", skipped_start)
|
|
skipped_block = body[skipped_start:skipped_end]
|
|
# Skips settle silently — no token figures, no skip-message toast spam.
|
|
assert "_compactionTokenStats" not in skipped_block
|
|
assert "tokens_after" not in skipped_block
|
|
assert "UI.toast(" not in skipped_block
|
|
assert "_scheduleCompactionSeparatorRemoval();" in skipped_block
|
|
|
|
|
|
def test_chat_semantic_memory_degraded_is_non_blocking_and_path_safe() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
start = source.index("function _showCompactionToast(payload, meta = {})")
|
|
end = source.index(" /* ── RPC Event Subscriptions", start)
|
|
body = source[start:end]
|
|
|
|
assert "function _compactSemanticMemoryNotice(payload)" in source
|
|
assert "payload.semanticMemory || payload.semantic_memory" in source
|
|
assert "Memory saved; organizing" in source
|
|
assert "_syncCompactionSeparator(payload || {}, 'completed', source, {" in body
|
|
assert "label: 'context compacted'," in body
|
|
assert "UI.toast(semanticNotice" not in body
|
|
assert body.index("const semanticNotice = _compactSemanticMemoryNotice") < body.index(
|
|
"if (status === 'failed' || status === 'error')"
|
|
)
|
|
assert "function _compactSafeMessageDetail(payload)" in source
|
|
assert "[memory checkpoint]" in source
|
|
assert "const safe = _compactSafeMessageDetail(payload || {});" in body
|
|
assert "payload.message ? ': ' + payload.message" not in body
|
|
|
|
|
|
def test_chat_distill_failures_do_not_surface_as_blocking_memory_errors() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
start = source.index("function _showCompactionToast(payload, meta = {})")
|
|
end = source.index(" /* ── RPC Event Subscriptions", start)
|
|
body = source[start:end]
|
|
semantic_notice = body.index("const semanticNotice = _compactSemanticMemoryNotice")
|
|
failure_branch = body.index("if (status === 'failed' || status === 'error')")
|
|
|
|
assert semantic_notice < failure_branch
|
|
assert "Memory saved; organizing" in source
|
|
assert "flush failed" not in source.lower()
|
|
assert "bad json" not in source.lower()
|
|
|
|
|
|
def test_chat_compact_inflight_uses_pending_queue_and_safe_terminal_drain() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
send_start = source.index("async function _onSend()")
|
|
send_end = source.index(" /* ── Streaming", send_start)
|
|
send_body = source[send_start:send_end]
|
|
toast_start = source.index("function _showCompactionToast(payload, meta = {})")
|
|
toast_end = source.index(" /* ── RPC Event Subscriptions", toast_start)
|
|
toast_body = source[toast_start:toast_end]
|
|
|
|
assert "let _compactInFlight = false;" in source
|
|
assert "function _isCompactInFlightForCurrentSession()" in source
|
|
assert "if (_isStreaming || _isCompactInFlightForCurrentSession())" in send_body
|
|
assert "const waitReason = _isCompactInFlightForCurrentSession()" in send_body
|
|
assert "_enqueuePendingInput(" in send_body
|
|
assert "'Message queued until compaction finishes'" in send_body
|
|
assert "'context compaction'" in send_body
|
|
assert "Wait for ${waitReason} or clear." in source
|
|
assert "_settleCompactInFlight(payload || {});" in toast_body
|
|
assert "status === 'completed'" in source
|
|
assert "status === 'skipped'" in source
|
|
assert "_schedulePendingDrainAfterTerminal();" in source
|
|
assert "status === 'failed' || status === 'error'" in toast_body
|
|
assert "status === 'cancelled'" in toast_body
|
|
assert "_settleCompactInFlight(payload || {}, { recoverPending: true })" in toast_body
|
|
assert "_schedulePendingDrainAfterTerminal();" not in toast_body[
|
|
toast_body.index("if (status === 'failed' || status === 'error')") :
|
|
toast_body.index("if (status === 'cancelled')")
|
|
]
|
|
assert "_schedulePendingDrainAfterTerminal();" not in toast_body[
|
|
toast_body.index("if (status === 'cancelled')") :
|
|
toast_body.index("if (status !== 'completed')")
|
|
]
|
|
|
|
|
|
def test_chat_large_paste_guard_runs_before_queue_and_rpc_send() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
send_start = source.index("async function _onSend()")
|
|
send_end = source.index(" /* ── Streaming", send_start)
|
|
send_body = source[send_start:send_end]
|
|
normalize_start = source.index("async function _normalizeOutgoingComposerPayload(")
|
|
normalize_end = source.index(" function _addAttachment(file)", normalize_start)
|
|
normalize_body = source[normalize_start:normalize_end]
|
|
|
|
assert "const LARGE_PASTE_CHARS = 20_000;" in source
|
|
assert "const PAGE_DUMP_CHARS = 8_000;" in source
|
|
assert "function _normalizeOutgoingComposerPayload(" in source
|
|
assert "function _inputNormalizationProvenanceFromAttachments(" in source
|
|
normalize_call = "await _normalizeOutgoingComposerPayload("
|
|
assert normalize_call in send_body
|
|
assert send_body.count(normalize_call) == 1
|
|
assert send_body.index(normalize_call) < send_body.index(
|
|
"if (_isStreaming || _isCompactInFlightForCurrentSession())"
|
|
)
|
|
assert send_body.index(normalize_call) < send_body.index("_enqueuePendingInput(")
|
|
assert send_body.index(normalize_call) < send_body.index(
|
|
"await _executeSlashCommand(text)"
|
|
)
|
|
assert send_body.index(normalize_call) < send_body.index("const params = { message:")
|
|
provenance_call = (
|
|
"const normalizationProvenance = "
|
|
"_inputNormalizationProvenanceFromAttachments(attachmentsForSend);"
|
|
)
|
|
assert provenance_call in send_body
|
|
assert (
|
|
"if (normalizationProvenance) params.inputProvenance = normalizationProvenance;"
|
|
in send_body
|
|
)
|
|
assert send_body.index("const params = { message:") < send_body.index(provenance_call)
|
|
assert send_body.index(provenance_call) < send_body.index("_rpc.call('chat.send', params)")
|
|
assert "inputNormalization: {" in normalize_body
|
|
assert "materialEstimatedTokens," in normalize_body
|
|
assert "guardAction: 'generated_text_attachment'," in normalize_body
|
|
|
|
|
|
def test_chat_pending_queue_can_store_normalized_attachments_explicitly() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
enqueue_start = source.index("function _enqueuePendingInput(")
|
|
enqueue_end = source.index(" function _drainQueueHead()", enqueue_start)
|
|
enqueue_body = source[enqueue_start:enqueue_end]
|
|
|
|
assert "attachmentsOverride = null" in source
|
|
assert "const queuedAttachments = attachmentsOverride || _pendingAttachments" in enqueue_body
|
|
assert "attachments: queuedAttachments.map((a) => ({ ...a }))" in enqueue_body
|
|
|
|
|
|
def test_chat_manual_pending_enqueue_normalizes_large_pastes() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
current_start = source.index("async function _enqueueCurrentInput()")
|
|
current_end = source.index(" function _updateStopButton()", current_start)
|
|
current_body = source[current_start:current_end]
|
|
|
|
normalize_idx = current_body.index(
|
|
"const normalized = await _normalizeOutgoingComposerPayload("
|
|
)
|
|
enqueue_idx = current_body.index("_enqueuePendingInput(")
|
|
|
|
assert "let text = _textarea.value.trim();" in current_body
|
|
assert "let isLiteralSlash = false;" in current_body
|
|
assert "if (text.startsWith('//')) {" in current_body
|
|
assert "isLiteralSlash = true;" in current_body
|
|
assert "text = text.slice(1);" in current_body
|
|
assert "const isSlashCommand = !isLiteralSlash && text.startsWith('/')" in current_body
|
|
assert "!!(await _lookupSlashCommand(text))" in current_body
|
|
assert "{ allowSlashCommand: isSlashCommand }" in current_body
|
|
assert normalize_idx < enqueue_idx
|
|
assert "_pendingAttachments = normalized.attachments;" in current_body
|
|
assert (
|
|
"_enqueuePendingInput(text, null, 'the current response', normalized.attachments)"
|
|
in current_body
|
|
)
|
|
|
|
|
|
def test_chat_large_paste_attachment_base64_is_computed_once() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
normalize_start = source.index("async function _normalizeOutgoingComposerPayload(")
|
|
normalize_end = source.index(" function _addAttachment(file)", normalize_start)
|
|
normalize_body = source[normalize_start:normalize_end]
|
|
|
|
assert "function _bytesToBase64(bytes)" in source
|
|
assert "const encoded = _bytesToBase64(bytes);" in normalize_body
|
|
assert "data: encoded," in normalize_body
|
|
assert "dataUrl: `data:text/plain;base64,${encoded}`" in normalize_body
|
|
assert "_encodeUtf8Base64(raw)" not in normalize_body
|
|
|
|
|
|
def test_chat_compact_blocking_failure_preserves_pending_queue() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
toast_start = source.index("function _showCompactionToast(payload, meta = {})")
|
|
toast_end = source.index(" /* ── RPC Event Subscriptions", toast_start)
|
|
toast_body = source[toast_start:toast_end]
|
|
settle_start = source.index("function _settleCompactInFlight(payload = {}, options = {})")
|
|
settle_end = source.index(" // Programmatic textarea write", settle_start)
|
|
settle_body = source[settle_start:settle_end]
|
|
|
|
assert "function _compactFailureBlocksPending(payload)" in source
|
|
assert "compaction_insufficient" in source
|
|
assert "compaction_flush_failed" in source
|
|
assert "const preservePending = _compactFailureBlocksPending(payload || {});" in toast_body
|
|
assert (
|
|
"const keepPendingQueued = preservePending || (source !== 'manual' && _isStreaming);"
|
|
in toast_body
|
|
)
|
|
assert "preservePending: keepPendingQueued" in toast_body
|
|
assert "options && options.preservePending" in settle_body
|
|
assert "_popAllPendingIntoComposer();" in settle_body
|
|
assert "recovered = _pendingQueue.length > 0;" in settle_body
|
|
|
|
|
|
def test_chat_clears_background_task_groups_on_state_reset_paths() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
|
|
reset_idx = source.index("case '/reset':")
|
|
epoch_idx = source.index("_rpc.on('session.epoch_changed'")
|
|
destroy_idx = source.index("function destroy()")
|
|
|
|
assert source.index("_clearActiveTaskGroups();", reset_idx) < source.index(
|
|
"UI.toast('Session reset'",
|
|
reset_idx,
|
|
)
|
|
assert source.index("_clearActiveTaskGroups();", epoch_idx) < source.index(
|
|
"_currentEpoch = ep;",
|
|
epoch_idx,
|
|
)
|
|
assert source.index("_clearActiveTaskGroups();", destroy_idx) > destroy_idx
|
|
|
|
|
|
def test_rpc_client_detects_frame_gaps_and_tick_timeout() -> None:
|
|
source = RPC_JS.read_text(encoding="utf-8")
|
|
|
|
assert "this._lastSeq = 0;" in source
|
|
assert "_noteIncomingFrame(data)" in source
|
|
assert "seq !== this._lastSeq + 1" in source
|
|
assert "reason: 'tick_timeout'" in source
|
|
assert "this._startTickWatch();" in source
|
|
|
|
|
|
def test_subagent_completion_has_distinct_chat_styles() -> None:
|
|
source = CHAT_CSS.read_text(encoding="utf-8")
|
|
|
|
assert ".msg.subagent" in source
|
|
assert ".chat-subagent-disclosure" in source
|
|
|
|
|
|
def test_subagent_disclosure_renders_expand_chevron() -> None:
|
|
source = CHAT_CSS.read_text(encoding="utf-8")
|
|
|
|
assert ".chat-subagent-disclosure-summary::after" in source
|
|
assert ".chat-subagent-disclosure[open] > .chat-subagent-disclosure-summary::after" in source
|
|
|
|
|
|
def test_savings_popup_suppresses_only_the_model_switch_turn() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
|
|
assert "_savingsPopupSuppressUntil" not in source
|
|
assert "let _lastSavingsPopupIdentity = '';" in source
|
|
assert "const cacheHit = !!(u.cache_hit_active || (u.cached_tokens || 0) > 0);" in source
|
|
assert "const identityModel = u.routed_model || u.model || '';" in source
|
|
assert (
|
|
"const identity = identityModel ? `${identityModel}|${u.routed_tier || ''}` : '';"
|
|
in source
|
|
)
|
|
assert "let suppressPopup = false;" in source
|
|
assert "const identityChanged =" in source
|
|
assert "suppressPopup = true;" in source
|
|
assert "const _savingsPopupTsByIdentity = new Map();" in source
|
|
assert (
|
|
"if (!cacheHit && now - _identityLastTs < _SAVINGS_POPUP_COOLDOWN_MS) return;"
|
|
in source
|
|
)
|
|
assert source.index("let suppressPopup = false;") < source.index(
|
|
"window.SavingsFX.noteTurn(u);"
|
|
)
|
|
|
|
|
|
def test_savings_popup_persists_cache_hit_active_to_turn_meta() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
|
|
assert "cache_hit_active: !!u.cache_hit_active," in source
|
|
assert "model: u.model || _usageModel || null," in source
|
|
assert "routed_model: u.routed_model || null," in source
|
|
assert "__savings_ui_suppressed: !!u.__savings_ui_suppressed," in source
|
|
|
|
|
|
def test_savings_fx_cleanup_removes_floating_labels() -> None:
|
|
source = SAVINGS_FX_JS.read_text(encoding="utf-8")
|
|
|
|
assert "const _labels = new Set();" in source
|
|
assert "_labels.add(el);" in source
|
|
assert "_labels.delete(el);" in source
|
|
assert "for (const el of _labels)" in source
|
|
assert "window.SavingsFX.cleanup();" in CHAT_JS.read_text(encoding="utf-8")
|
|
|
|
|
|
def test_turn_meta_and_router_share_model_display_normalization() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
attach_start = source.index("function _attachTurnMeta(")
|
|
attach_end = source.index(" if (hasTokens)", attach_start)
|
|
attach_body = source[attach_start:attach_end]
|
|
router_strip_start = source.index("function _routerFxStripProvider(name)")
|
|
router_strip_end = source.index(" // Promise resolved", router_strip_start)
|
|
router_strip_body = source[router_strip_start:router_strip_end]
|
|
|
|
assert "function _modelDisplayName(name)" in source
|
|
assert r"return stripped.replace(/-\d{8}$/, '');" in source
|
|
assert "const displayModel = _modelDisplayName(model);" in attach_body
|
|
assert "span.textContent = displayModel;" in attach_body
|
|
assert "return _modelDisplayName(name);" in router_strip_body
|
|
|
|
|
|
def test_router_fx_header_names_ai_model_router() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
|
|
assert '<span class="title">AI model router</span>' in source
|
|
assert '<span class="title">model router</span>' not in source
|
|
|
|
|
|
def test_router_fx_replay_without_live_strip_renders_settled_history() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
handler_start = source.index("async function _handleRouterDecision(payload) {")
|
|
handler_end = source.index(" // History-load entry point", handler_start)
|
|
handler_body = source[handler_start:handler_end]
|
|
subscription_start = source.index("function _subscribeRpcEvents() {")
|
|
subscription_end = source.index(
|
|
" // Text delta: accumulate into streaming bubble", subscription_start
|
|
)
|
|
subscription_body = source[subscription_start:subscription_end]
|
|
|
|
assert "function _routerFxShouldAnimateIdentity" not in source
|
|
assert "shouldAnimate" not in handler_body
|
|
assert "_rpc.on('session.event.router_decision'" in subscription_body
|
|
assert "_handleRouterDecision(payload);" in subscription_body
|
|
assert (
|
|
"const liveStrip = _thread.querySelector('.router-fx[data-live=\"true\"]');"
|
|
in handler_body
|
|
)
|
|
assert "liveStrip._fxDecision = payload;" in handler_body
|
|
assert "_routerFxLock(liveStrip, payload);" in handler_body
|
|
assert "preSettled: true," in handler_body
|
|
assert "renderMode: 'history'," in handler_body
|
|
assert "_routerFxNormalizeSettledStrip(wrap, 'history', payload);" in handler_body
|
|
assert "_animateRouterFx(wrap, winnerIdx)" not in handler_body
|
|
assert "_animateRouterFxCloud(wrap)" not in handler_body
|
|
assert "router_decision.inserted_settled_strip" in handler_body
|
|
|
|
|
|
def test_chat_diag_captures_stream_router_and_history_state() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
|
|
assert "const _CHAT_DIAG_KEY = 'opensquilla.chat.debugLog';" in source
|
|
assert "window.OpenSquillaChatDiag" in source
|
|
assert "function _chatDiagDomSnapshot() {" in source
|
|
for marker in [
|
|
"send.start",
|
|
"event.router_decision",
|
|
"event.text_delta",
|
|
"router_scan.started",
|
|
"router_decision.cached_on_live_strip",
|
|
"stream.bubble.created",
|
|
"stream.flush.done",
|
|
"stream.end.start",
|
|
"history.loaded",
|
|
"history.done",
|
|
]:
|
|
assert marker in source
|
|
|
|
|
|
def test_chat_diag_is_opt_in_by_default() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
start = source.index("function _chatDiagEnabled() {")
|
|
end = source.index("function _chatDiagShortText", start)
|
|
body = source[start:end]
|
|
|
|
assert "window.localStorage.getItem(_CHAT_DIAG_ENABLED_KEY) === '1';" in body
|
|
assert "window.localStorage.getItem(_CHAT_DIAG_ENABLED_KEY) !== '0';" not in body
|
|
assert "return false;" in body
|
|
assert "window.localStorage.setItem(_CHAT_DIAG_ENABLED_KEY, '1');" in source
|
|
|
|
|
|
def test_chat_history_requests_active_paginated_metadata() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
history_start = source.index("async function _loadHistory(opts = {}) {")
|
|
history_end = source.index("async function _loadEarlierHistory()", history_start)
|
|
history_body = source[history_start:history_end]
|
|
earlier_start = source.index("async function _loadEarlierHistory() {")
|
|
earlier_end = source.index("function _renderHistoryMessages(", earlier_start)
|
|
earlier_body = source[earlier_start:earlier_end]
|
|
|
|
assert "const CHAT_HISTORY_PAGE_SIZE = 50;" in source
|
|
assert "const requestSessionKey = _sessionKey;" in history_body
|
|
assert "const requestSeq = ++_historyRequestSeq;" in history_body
|
|
assert "sessionKey: requestSessionKey" in history_body
|
|
assert "limit: CHAT_HISTORY_PAGE_SIZE" in history_body
|
|
assert "includeCanonical: false" in history_body
|
|
assert "includeCanonical: false" in earlier_body
|
|
assert "includeCanonical: true" not in history_body
|
|
assert "includeCanonical: true" not in earlier_body
|
|
assert "includeSummaries: true" in history_body
|
|
assert "requestSessionKey !== _sessionKey || requestSeq !== _historyRequestSeq" in history_body
|
|
assert "_chatDiag('history.stale_response.drop'" in history_body
|
|
|
|
|
|
def test_chat_history_load_earlier_uses_cursor_and_preserves_scroll() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
start = source.index("async function _loadEarlierHistory() {")
|
|
end = source.index("function _renderHistoryMessages(", start)
|
|
body = source[start:end]
|
|
|
|
assert "!_historyOldestCursor || _historyLoadingEarlier" in body
|
|
assert "const previousScrollHeight = _thread.scrollHeight;" in body
|
|
assert "const previousScrollTop = _thread.scrollTop;" in body
|
|
assert "before: _historyOldestCursor" in body
|
|
assert "_mergeHistoryMessagePages(olderMessages, _historyLoadedMessages)" in body
|
|
assert "preserveScroll: true" in body
|
|
assert "_chatDiag('history.load_earlier.stale_response.drop'" in body
|
|
|
|
|
|
def test_chat_history_scope_row_surfaces_partial_compacted_and_error_states() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
css = CHAT_CSS.read_text(encoding="utf-8")
|
|
start = source.index("function _renderHistoryScopeRow() {")
|
|
end = source.index("async function _loadHistory(opts = {})", start)
|
|
body = source[start:end]
|
|
|
|
assert "chat-history-scope" in body
|
|
assert "Showing latest ${_historyLoadedMessages.length} messages." in body
|
|
assert "Older history is available." in body
|
|
assert "Older context was compacted for the model." in body
|
|
assert "Export the session for exact text." in body
|
|
assert "Load earlier" in body
|
|
assert "Retry history" in body
|
|
assert "btn.addEventListener('click', () => _loadEarlierHistory());" in body
|
|
assert ".chat-history-scope--partial" in css
|
|
assert ".chat-history-scope--compacted" in css
|
|
assert ".chat-history-scope--error" in css
|
|
|
|
|
|
def test_chat_history_render_preserves_visible_messages_on_errors() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
initial_start = source.index("async function _loadHistory(opts = {}) {")
|
|
initial_end = source.index("async function _loadEarlierHistory()", initial_start)
|
|
initial_body = source[initial_start:initial_end]
|
|
earlier_start = source.index("async function _loadEarlierHistory() {")
|
|
earlier_end = source.index("function _renderHistoryMessages(", earlier_start)
|
|
earlier_body = source[earlier_start:earlier_end]
|
|
|
|
assert "_historyError = 'Could not load chat history.'" in initial_body
|
|
assert "_renderHistoryScopeRow();" in initial_body
|
|
assert "_thread.innerHTML = ''" not in initial_body
|
|
assert "_historyError = 'Could not load earlier history.'" in earlier_body
|
|
assert "_renderHistoryScopeRow();" in earlier_body
|
|
assert "_thread.innerHTML = ''" not in earlier_body
|
|
|
|
|
|
def test_chat_history_scope_row_is_not_a_message_node() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
start = source.index("function _renderHistoryScopeRow() {")
|
|
end = source.index("async function _loadHistory(opts = {})", start)
|
|
body = source[start:end]
|
|
|
|
assert "row.className = `chat-history-scope chat-history-scope--${tone}`;" in body
|
|
assert "row.className = `msg" not in body
|
|
assert "_thread.querySelectorAll('.chat-history-scope').forEach((el) => el.remove());" in source
|
|
|
|
|
|
def test_router_fx_history_reuses_settled_strip_for_same_turn_identity() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
history_start = source.index("async function _loadHistory(opts = {}) {")
|
|
history_end = source.index(" /* ── Send Message", history_start)
|
|
history_body = source[history_start:history_end]
|
|
|
|
assert "function _routerFxUserMessageForAssistant(referenceAssistant) {" in source
|
|
assert "const userMsg = _routerFxUserMessageForAssistant(div);" in history_body
|
|
assert "const userMsg = _routerFxLastUserMessage();" not in history_body
|
|
assert "el.dataset.sessionKey === (_sessionKey || '') && el.dataset.turnIndex" in source
|
|
assert "const routerIdentity = _routerFxUsageIdentity(savedUsage);" in source
|
|
assert "existingStrip.dataset.routerIdentity === routerIdentity" in source
|
|
assert "if (existingStrip) _routerFxRemoveStrip(existingStrip);" in source
|
|
assert "existingStrip.dataset.live" not in history_body
|
|
assert "routerStrip.dataset.turnIndex = String(_histUserIdx);" in source
|
|
|
|
|
|
def test_router_fx_history_reanchors_stranded_strip() -> None:
|
|
# _appendHistoryElementInOrder re-appends only .msg elements, stranding any
|
|
# strip already rendered for a turn. The rebuild must match this turn's
|
|
# strip(s) by (session, turn index) and router identity — NOT by the live
|
|
# flag — keep the one whose identity matches, drop extras, and re-anchor the
|
|
# survivor beneath its user message, so the first chat never shows two grids.
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
history_start = source.index("async function _loadHistory(opts = {}) {")
|
|
history_end = source.index(" /* ── Send Message", history_start)
|
|
history_body = source[history_start:history_end]
|
|
|
|
# Match by (session, turnIndex), independent of the data-live flag.
|
|
assert (
|
|
"const ownStrips = Array.from(_thread.querySelectorAll('.router-fx')).filter("
|
|
in history_body
|
|
)
|
|
assert "el.dataset.turnIndex === String(_histUserIdx)" in history_body
|
|
assert "el.dataset.routerIdentity === routerIdentity" in history_body
|
|
# Drop duplicates, re-anchor the survivor, normalize it into history state.
|
|
assert (
|
|
"ownStrips.forEach((el) => { if (el !== keep) _routerFxRemoveStrip(el); });"
|
|
in history_body
|
|
)
|
|
assert "_thread.insertBefore(keep, userMsg.nextSibling);" in history_body
|
|
assert "_routerFxNormalizeSettledStrip(keep, 'history', savedUsage);" in history_body
|
|
# The consolidation must precede the existingStrip probe so the relocated
|
|
# strip is what the reuse check sees.
|
|
assert history_body.index("const ownStrips =") < history_body.index(
|
|
"const placed = userMsg && userMsg.nextSibling;"
|
|
)
|
|
# Positional orphan backstop: outside an active stream, any strip not
|
|
# sitting directly beneath a user message (turn-index skew) is dropped so
|
|
# a stranded grid can never linger at the top.
|
|
assert "if (!_isStreaming) {" in history_body
|
|
assert "const prev = el.previousElementSibling;" in history_body
|
|
assert "if (!anchored) _routerFxRemoveStrip(el);" in history_body
|
|
|
|
|
|
def test_router_fx_uses_only_effective_real_candidates() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
builder_start = source.index(
|
|
"function _routerFxBuildCandidateGridCells(realEntries, seedKey) {",
|
|
)
|
|
builder_end = source.index(" function _routerFxBuildLegacyGridCells", builder_start)
|
|
builder_body = source[builder_start:builder_end]
|
|
|
|
assert "function _routerFxResolveLayoutSeed(sessionKey, hintTimestamp)" in source
|
|
assert "function _routerFxVisualEntries(requestKind, decision) {" in source
|
|
assert "const cachedSeed = _routerFxResolveLayoutSeed(_sessionKey, hint);" in source
|
|
assert "const orderedRealEntries = realEntries.slice().sort" in builder_body
|
|
assert "return orderedRealEntries.map((entry) => ({" in builder_body
|
|
assert "kind: 'real'," in builder_body
|
|
assert "kind: 'decoy'" not in builder_body
|
|
assert "return _routerFxShuffle(cells, seedKey);" not in builder_body
|
|
|
|
|
|
def test_router_fx_legacy_grid_is_config_driven_visual_only() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
css = CHAT_CSS.read_text(encoding="utf-8")
|
|
|
|
assert "Router panel" not in source
|
|
assert "data-router-panel" not in source
|
|
assert "const _ROUTER_FX_DECOY_POOL = [" in source
|
|
for model in (
|
|
"gpt-5.5",
|
|
"claude-opus-4.8",
|
|
"gemini-3.5-flash",
|
|
"qwen3-coder-plus",
|
|
"grok-4.3",
|
|
"deepseek-v3.2",
|
|
"kimi-k2.6",
|
|
"mistral-medium-3.5",
|
|
"command-a-plus",
|
|
"glm-4.6",
|
|
):
|
|
assert model in source
|
|
for stale_model in (
|
|
"gpt-4.1-mini",
|
|
"gpt-4o-mini",
|
|
"o4-mini",
|
|
"deepseek-chat",
|
|
"mistral-medium',",
|
|
"grok-code-fast",
|
|
"deepseek-v4-pro",
|
|
"glm-5.2",
|
|
"qwen3-coder',",
|
|
):
|
|
assert stale_model not in source
|
|
assert "const _ROUTER_FX_GRID_COLS = 5;" in source
|
|
assert "const _ROUTER_FX_GRID_ROWS = 3;" in source
|
|
assert "const _ROUTER_FX_REAL_ANCHOR_CELLS" in source
|
|
assert "function _routerFxNormalizeVisualMode(mode) {" in source
|
|
assert "function _routerFxShuffle(items, seedKey) {" in source
|
|
assert "return 'legacy_grid';" in source
|
|
assert "_routerFxApplyConfigVisualMode(cfg?.squilla_router?.visual_mode);" in source
|
|
assert "saved.visualMode" not in source
|
|
assert "saved.panel" not in source
|
|
save_start = source.index("function _routerFxSavePref() {")
|
|
save_end = source.index("function _routerFxSortTiers(", save_start)
|
|
assert "visualMode:" not in source[save_start:save_end]
|
|
|
|
legacy_start = source.index("function _routerFxBuildLegacyGridCells(realEntries, seedKey) {")
|
|
legacy_end = source.index(" function _routerFxBuildGridCells", legacy_start)
|
|
legacy_body = source[legacy_start:legacy_end]
|
|
assert "Array.from({ length: _ROUTER_FX_GRID_CELLS }" in legacy_body
|
|
assert "cells[idx] = {" in legacy_body
|
|
assert "kind: 'real'," in legacy_body
|
|
assert "kind: 'decoy'," in legacy_body
|
|
assert "realNames.add(entry.displayName);" in legacy_body
|
|
|
|
builder_start = source.index(
|
|
"function _routerFxBuildGridCells(realEntries, seedKey, visualMode) {",
|
|
)
|
|
builder_end = source.index(" function _buildRouterFxElement", builder_start)
|
|
builder_body = source[builder_start:builder_end]
|
|
assert "_routerFxNormalizeVisualMode(visualMode) === 'legacy_grid'" in builder_body
|
|
assert "_routerFxBuildLegacyGridCells(realEntries, seedKey)" in builder_body
|
|
assert "_routerFxBuildCandidateGridCells(realEntries, seedKey)" in builder_body
|
|
assert "wrap.dataset.panel = _routerFxPanelDataset(visualMode);" in source
|
|
assert "_routerFxBuildGridCells(realEntries, seedKey || undefined, visualMode);" in source
|
|
assert "const isLegacyGrid = visualMode === 'legacy_grid';" in source
|
|
assert "const cols = isLegacyGrid ? _ROUTER_FX_GRID_COLS" in source
|
|
assert "const rows = isLegacyGrid ? _ROUTER_FX_GRID_ROWS : 1;" in source
|
|
assert "cells[i].kind === 'real' && cells[i].entry.tiers.indexOf(norm) >= 0" in source
|
|
assert '.router-fx[data-panel="legacy-grid"] .router-fx-grid' in css
|
|
assert "repeat(var(--router-fx-cols, 5), minmax(0, 1fr));" in css
|
|
assert "repeat(var(--router-fx-rows, 3), 30px);" in css
|
|
assert "max-width: min(100%, 820px);" not in css
|
|
|
|
|
|
def test_router_fx_cells_render_plain_model_names_only() -> None:
|
|
# Cells show only the user-facing model name: no S/M/L/XL, no provider
|
|
# labels, no thinking badges, and no DOM roster metadata beyond the cell
|
|
# index needed for the selector.
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
css = CHAT_CSS.read_text(encoding="utf-8")
|
|
vue = CHAT_ROUTER_FX_VUE.read_text(encoding="utf-8")
|
|
|
|
assert "cell.dataset.kind" not in source
|
|
assert "cell.dataset.tiers" not in source
|
|
assert ":data-kind" not in vue
|
|
assert ":data-tiers" not in vue
|
|
assert '[data-kind="real"]' not in vue
|
|
assert '[data-kind="decoy"]' not in vue
|
|
assert '[data-kind="real"]' not in css
|
|
assert '[data-kind="decoy"]' not in css
|
|
assert "router-fx-dot-idle" not in css
|
|
cell_start = css.index(".router-fx-cell {")
|
|
cell_end = css.index("}", cell_start)
|
|
assert "color: var(--text);" in css[cell_start:cell_end]
|
|
assert ".router-fx-cell.win {" in css
|
|
win_after = css.index(".router-fx-cell.win::after {")
|
|
win_after_end = css.index("}", win_after)
|
|
assert "content: '';" in css[win_after:win_after_end]
|
|
assert "const cells = wrap._fxGridCells || [];" in source
|
|
assert "cells[i].kind === 'real'" in source
|
|
assert "cell.dataset.cellIdx = String(i);" in source
|
|
assert "cell.dataset.provider" not in source
|
|
assert "cell.dataset.thinking" not in source
|
|
for size_label in (">S<", ">M<", ">L<", ">XL<"):
|
|
assert size_label not in source
|
|
|
|
|
|
def test_router_fx_config_caches_text_and_image_capability_flags() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
load_start = source.index("async function _loadFeatureToggles() {")
|
|
load_end = source.index(" /* ── Session Chip", load_start)
|
|
body = source[load_start:load_end]
|
|
|
|
assert "const _routerFxTierConfigs = {};" in source
|
|
assert "model: typeof rawTier?.model === 'string' ? rawTier.model : ''," in body
|
|
assert "supportsImage: rawTier?.supports_image === true," in body
|
|
assert "imageOnly: rawTier?.image_only === true," in body
|
|
assert "_routerFxTierConfigs[lower] = tierConfig;" in body
|
|
assert "delete _routerFxTierConfigs[tier];" in body
|
|
|
|
|
|
def test_router_fx_filters_text_and_image_candidates_by_request_kind() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
|
|
assert "function _routerFxRequestKindFromAttachments(attachments) {" in source
|
|
request_start = source.index("function _routerFxRequestKindFromAttachments(attachments) {")
|
|
request_end = source.index("function _routerFxTierMatchesRequestKind", request_start)
|
|
request_body = source[request_start:request_end]
|
|
assert "return 'image';" in request_body
|
|
assert "return 'text';" in request_body
|
|
|
|
match_start = source.index("function _routerFxTierMatchesRequestKind")
|
|
match_end = source.index("function _routerFxVisualEntries", match_start)
|
|
match_body = source[match_start:match_end]
|
|
assert "return !!(tierConfig.supportsImage || tierConfig.imageOnly);" in match_body
|
|
assert "return !tierConfig.imageOnly;" in match_body
|
|
|
|
send_start = source.index("async function _onSend()")
|
|
send_end = source.index(" // Send", send_start)
|
|
send_body = source[send_start:send_end]
|
|
assert (
|
|
"const routerFxRequestKind = "
|
|
"_routerFxRequestKindFromAttachments(params.attachments || []);"
|
|
) in send_body
|
|
assert "requestKind: routerFxRequestKind" in send_body
|
|
|
|
|
|
def test_router_fx_single_visual_candidate_renders_nothing_live_or_history() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
|
|
builder_start = source.index("function _buildRouterFxElement(decision, opts) {")
|
|
builder_end = source.index(" function _routerFxWinnerCellIndex", builder_start)
|
|
builder_body = source[builder_start:builder_end]
|
|
assert "if (realEntries.length <= 1) return null;" in builder_body
|
|
|
|
schedule_start = source.index("function _scheduleRouterFxBeginScan(anchorDiv, seedKey, opts) {")
|
|
schedule_end = source.index(" // Render the routing visualisation", schedule_start)
|
|
schedule_body = source[schedule_start:schedule_end]
|
|
assert (
|
|
"if (_routerFxConfigTiers !== null "
|
|
"&& !_routerFxHasMultipleCandidates(requestKind, null)) {"
|
|
in schedule_body
|
|
)
|
|
assert "_chatDiag('router_scan.schedule.skip.single_candidate'" in schedule_body
|
|
|
|
pending_start = source.index("async function _finishPendingRouterFxScan() {")
|
|
pending_end = source.index(
|
|
"function _scheduleRouterFxBeginScan(anchorDiv, seedKey, opts) {",
|
|
pending_start,
|
|
)
|
|
pending_body = source[pending_start:pending_end]
|
|
assert pending_body.index("await _routerFxAwaitConfig();") < pending_body.index(
|
|
"_routerFxBeginScan(pending.anchorDiv, pending.seedKey"
|
|
)
|
|
|
|
begin_start = source.index("function _routerFxBeginScan(anchorDiv, seedKey, opts) {")
|
|
begin_end = source.index("function _routerFxScanRoam(", begin_start)
|
|
begin_body = source[begin_start:begin_end]
|
|
assert "if (!wrap) {" in begin_body
|
|
assert "_chatDiag('router_scan.skip.single_candidate'" in begin_body
|
|
|
|
history_start = source.index("function _buildRouterFxFromUsage(usage, seedKey, opts) {")
|
|
history_end = source.index(" /* ── RPC Event Subscriptions", history_start)
|
|
history_body = source[history_start:history_end]
|
|
assert "requestKind: requestKind" in history_body
|
|
assert "return _buildRouterFxElement(decision, {" in history_body
|
|
|
|
|
|
def test_router_fx_grid_labels_shrink_to_fit() -> None:
|
|
# Long model names (e.g. "gemini-3.1-flash-lite") must show in full, not
|
|
# clip at the cell edges: a post-insert measure shrinks the label font to
|
|
# fit its cell. Runs for every inserted strip via _routerFxInsertAnchored.
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
|
|
assert "function _routerFxFitLabels(wrap) {" in source
|
|
measure_start = source.index("function _routerFxMeasureLabels(wrap) {")
|
|
measure_end = source.index("function _routerFxScheduleLabelFit(", measure_start)
|
|
measure = source[measure_start:measure_end]
|
|
assert "wrap.querySelectorAll('.router-fx-cell')" in measure
|
|
assert "const w = nm.scrollWidth;" in measure
|
|
assert "nm.style.fontSize = Math.max(7, base * (avail / w)).toFixed(1) + 'px';" in measure
|
|
fit_start = source.index("function _routerFxInstallLabelFit(wrap) {")
|
|
fit_end = source.index("function _routerFxInsertAnchored(", fit_start)
|
|
fit = source[fit_start:fit_end]
|
|
assert "new ResizeObserver(() => _routerFxScheduleLabelFit(wrap))" in fit
|
|
assert "document.fonts.ready" in fit
|
|
assert "_routerFxScheduleLabelFit(wrap);" in fit
|
|
# Invoked from the shared insert path so both live and history strips fit.
|
|
insert_start = source.index("function _routerFxInsertAnchored(")
|
|
insert_end = source.index("}", source.index("_thread.appendChild(wrap);", insert_start))
|
|
assert "_routerFxFitLabels(wrap);" in source[insert_start:insert_end]
|
|
|
|
|
|
def test_router_fx_scan_does_not_hide_waiting_indicator() -> None:
|
|
# The "squilla · Watching · N.Ns" indicator must remain visible even while
|
|
# the router panel is scanning. Provider first-byte waits can be minutes
|
|
# long, so router animation must not suppress the only user-facing progress
|
|
# signal.
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
# No longer suppressed.
|
|
assert "if (_routerFx.enabled && _routerFeatureEnabled) return;" not in source
|
|
now_start = source.index("function _showThinkingIndicatorNow() {")
|
|
now_end = source.index("function _hideThinkingIndicator", now_start)
|
|
body = source[now_start:now_end]
|
|
assert "_thread.querySelector('.router-fx[data-scanning=\"true\"]')" not in body
|
|
assert "thinking.defer.router_scan" not in body
|
|
# The verb list (incl. "Watching") is unchanged.
|
|
assert "const SQUILLA_VERBS = ['Watching'," in source
|
|
|
|
|
|
def test_router_fx_scan_to_lock_fills_the_wait() -> None:
|
|
# The routing visualisation starts shortly after SEND and animates
|
|
# continuously (JS-driven, ~170ms class/position swaps) until the decision
|
|
# locks it onto the winner. The small delay lets request-time compaction
|
|
# suppress the panel before a competing router flash appears.
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
css = CHAT_CSS.read_text(encoding="utf-8")
|
|
|
|
for fn in ("_routerFxBeginScan", "_routerFxScanRoam", "_routerFxStopScan",
|
|
"_routerFxLock", "_routerFxLockGrid"):
|
|
assert f"function {fn}(" in source
|
|
# Scan is gated on BOTH the viz pref and routing actually being on.
|
|
begin_start = source.index("function _routerFxBeginScan(")
|
|
begin_end = source.index("function _routerFxScanRoam(", begin_start)
|
|
begin = source[begin_start:begin_end]
|
|
assert "if (!_thread || !_routerFx.enabled || !_routerFeatureEnabled) {" in begin
|
|
assert "_chatDiag('router_scan.skip'" in begin
|
|
assert "return false;" in begin
|
|
assert "_routerFxHasMultipleCandidates(requestKind, null)" in begin
|
|
# Scheduled from the send path.
|
|
assert (
|
|
"const routerScanStarted = _scheduleRouterFxBeginScan("
|
|
"userDiv, _routerFxResolveLayoutSeed(_sessionKey), {"
|
|
) in source
|
|
# The scan runs for a FIXED, hard-capped window (≤1s), then locks — not
|
|
# "roam until the decision WS event lands".
|
|
assert "const _ROUTER_FX_SCAN_MS = 600;" in source
|
|
cap = "wrap._fxScanCap = setTimeout(() => _routerFxFinishScan(wrap), _ROUTER_FX_SCAN_MS);"
|
|
assert cap in source
|
|
assert "function _routerFxFinishScan(wrap) {" in source
|
|
# The decision is CACHED on the in-flight strip; the cap (or output) locks it.
|
|
assert "liveStrip._fxDecision = payload;" in source
|
|
assert "if (liveStrip._fxFinished) {" in source
|
|
assert "_routerFxLock(wrap, wrap._fxDecision);" in source # finish locks the cached winner
|
|
# Roam is JS-driven discrete selector hops across the real candidate cells.
|
|
roam_start = source.index("function _routerFxScanRoam(")
|
|
roam_end = source.index("function _routerFxStopScan(", roam_start)
|
|
roam = source[roam_start:roam_end]
|
|
assert "const grid = wrap.querySelector('.router-fx-grid');" in roam
|
|
assert "const targets = grid.querySelectorAll('.router-fx-cell');" in roam
|
|
assert "wrap._fxScanTimer = setTimeout(step, 190);" in roam
|
|
assert ".router-fx-mote" not in css
|
|
|
|
|
|
def test_chat_compaction_suppresses_current_turn_router_wait_panel() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
|
|
assert "let _compactSuppressedRouterTurnIndex = '';" in source
|
|
assert "function _suppressRouterFxForCompaction(payload = {})" in source
|
|
assert "const _ROUTER_FX_START_DELAY_MS = 280;" in source
|
|
assert "function _scheduleRouterFxBeginScan(anchorDiv, seedKey, opts)" in source
|
|
assert "function _cancelPendingRouterFxScan(reason = '')" in source
|
|
|
|
toast_start = source.index("function _showCompactionToast(payload, meta = {})")
|
|
started_start = source.index("if (status === 'started') {", toast_start)
|
|
started_end = source.index("if (status === 'observed') {", started_start)
|
|
started_body = source[started_start:started_end]
|
|
assert "_suppressRouterFxForCompaction(payload || {});" in started_body
|
|
|
|
observed_start = source.index("if (status === 'observed') {", toast_start)
|
|
observed_end = source.index("if (status === 'emergency_ephemeral') {", observed_start)
|
|
observed_body = source[observed_start:observed_end]
|
|
assert "_suppressRouterFxForCompaction(payload || {});" in observed_body
|
|
|
|
suppress_start = source.index("function _suppressRouterFxForCompaction(payload = {})")
|
|
suppress_end = source.index("function _showCompactionToast(payload, meta = {})", suppress_start)
|
|
suppress_body = source[suppress_start:suppress_end]
|
|
assert "_cancelPendingRouterFxScan('compaction');" in suppress_body
|
|
|
|
send_start = source.index("async function _onSend()")
|
|
send_end = source.index(" // Send", send_start)
|
|
send_body = source[send_start:send_end]
|
|
assert (
|
|
"_scheduleRouterFxBeginScan(userDiv, _routerFxResolveLayoutSeed(_sessionKey), {"
|
|
in send_body
|
|
)
|
|
assert "_routerFxBeginScan(userDiv, _routerFxResolveLayoutSeed(_sessionKey))" not in send_body
|
|
|
|
begin_start = source.index("function _routerFxBeginScan(")
|
|
begin_end = source.index("function _routerFxScanRoam(", begin_start)
|
|
begin_body = source[begin_start:begin_end]
|
|
assert "_routerFxIsSuppressedForCompactionTurn(_routerFxCountUserMessages())" in begin_body
|
|
|
|
handler_start = source.index("async function _handleRouterDecision(payload) {")
|
|
handler_end = source.index(" // History-load entry point", handler_start)
|
|
handler_body = source[handler_start:handler_end]
|
|
assert "_routerFxIsSuppressedForCompactionTurn(turnIndex)" in handler_body
|
|
assert "router_decision.skip.compaction_suppressed" in handler_body
|
|
|
|
|
|
def test_chat_compaction_suppresses_current_turn_thinking_indicator() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
|
|
show_start = source.index("function _showThinkingIndicatorNow() {")
|
|
show_end = source.index("function _hideThinkingIndicator", show_start)
|
|
show_body = source[show_start:show_end]
|
|
assert "thinking.defer.compaction_in_flight" in show_body
|
|
assert "_isCompactInFlightForCurrentSession()" in show_body
|
|
|
|
toast_start = source.index("function _showCompactionToast(payload, meta = {})")
|
|
started_start = source.index("if (status === 'started') {", toast_start)
|
|
started_end = source.index("if (status === 'observed') {", started_start)
|
|
started_body = source[started_start:started_end]
|
|
assert "_hideThinkingIndicator();" in started_body
|
|
|
|
observed_start = source.index("if (status === 'observed') {", toast_start)
|
|
observed_end = source.index("if (status === 'emergency_ephemeral') {", observed_start)
|
|
observed_body = source[observed_start:observed_end]
|
|
assert "_hideThinkingIndicator();" in observed_body
|
|
|
|
settle_start = source.index("function _settleCompactInFlight(payload = {}, options = {})")
|
|
settle_end = source.index(" // Programmatic textarea write", settle_start)
|
|
settle_body = source[settle_start:settle_end]
|
|
assert "if (_isStreaming && !_streamBubble) _showThinkingIndicator();" in settle_body
|
|
|
|
|
|
def test_chat_history_refresh_preserves_active_thinking_indicator() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
|
|
assert "function _isCurrentSessionThinkingIndicator(el)" in source
|
|
assert "_thinkingEl.dataset.sessionKey = _streamSessionKey || _sessionKey || '';" in source
|
|
|
|
render_start = source.index("function _renderHistoryMessages(messages, opts = {})")
|
|
render_end = source.index("function _appendHistoryDaySeparator", render_start)
|
|
render_body = source[render_start:render_end]
|
|
assert (
|
|
"const liveThinking = _isCurrentSessionThinkingIndicator(_thinkingEl) "
|
|
"? _thinkingEl : null;"
|
|
) in render_body
|
|
assert (
|
|
"if (el !== _streamBubble && el !== liveUserAnchor && el !== liveThinking) "
|
|
"el.remove();"
|
|
) in render_body
|
|
assert (
|
|
"if (liveThinking && !liveThinking.isConnected) "
|
|
"_thread.appendChild(liveThinking);"
|
|
) in render_body
|
|
assert "if (_isStreaming && _isCurrentSessionThinkingIndicator(el)) return;" in render_body
|
|
|
|
|
|
def test_chat_history_reorders_before_live_thinking_indicator() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
|
|
assert "function _historyLiveTailAnchor()" in source
|
|
helper_start = source.index("function _historyLiveTailAnchor()")
|
|
helper_end = source.index("function _appendHistoryDaySeparator", helper_start)
|
|
helper_body = source[helper_start:helper_end]
|
|
assert "if (!_isStreaming) return null;" in helper_body
|
|
assert "if (_isCurrentSessionStreamBubble(_streamBubble)) return _streamBubble;" in helper_body
|
|
assert "if (_isCurrentSessionThinkingIndicator(_thinkingEl)) return _thinkingEl;" in helper_body
|
|
|
|
day_start = source.index("function _appendHistoryDaySeparator(timestamp)")
|
|
day_end = source.index("function _appendHistoryElementInOrder(div)", day_start)
|
|
day_body = source[day_start:day_end]
|
|
assert "const liveTail = _historyLiveTailAnchor();" in day_body
|
|
assert "_thread.insertBefore(sep, liveTail);" in day_body
|
|
|
|
append_start = source.index("function _appendHistoryElementInOrder(div)")
|
|
append_end = source.index("function _historyStableMessageIdentity", append_start)
|
|
append_body = source[append_start:append_end]
|
|
assert "const liveTail = _historyLiveTailAnchor();" in append_body
|
|
assert "if (liveTail && div !== liveTail)" in append_body
|
|
assert "_thread.insertBefore(div, liveTail);" in append_body
|
|
|
|
|
|
def test_router_fx_strip_survives_multistep_turn() -> None:
|
|
# History reorder moves .msg nodes. The router strip is a sibling, so the
|
|
# root fix is to move an attached strip together with its user message
|
|
# instead of preserving a separate live-strip path inside _loadHistory().
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
assert "delete settledStrip.dataset.live;" not in source
|
|
assert "_routerFxFindAttachedStrip" not in source
|
|
history_start = source.index("async function _loadHistory(opts = {}) {")
|
|
history_end = source.index(" /* ── Send Message", history_start)
|
|
history_body = source[history_start:history_end]
|
|
assert "ownStrips.find((el) => el.dataset.live === 'true')" not in history_body
|
|
assert "if (_isStreaming && el.dataset.live === 'true') return;" not in history_body
|
|
assert "if (el.dataset.live === 'true') return;" not in history_body
|
|
# The reorder path moves a user message's attached router strip with that
|
|
# user message, so a long multi-step turn can't swap their positions.
|
|
append_start = source.index("function _appendHistoryElementInOrder(div)")
|
|
append_end = source.index("function _historyStableMessageIdentity", append_start)
|
|
append_body = source[append_start:append_end]
|
|
assert "const attachedRouterStrip = _routerFxStripImmediatelyAfterUser(div);" in append_body
|
|
assert "_restoreRouterFxAfterHistoryUser(div, attachedRouterStrip);" in append_body
|
|
assert "_thread.insertBefore(routerStrip, div.nextSibling);" in append_body
|
|
assert (
|
|
"const keep = ownStrips.find((el) => "
|
|
"el.dataset.routerIdentity === routerIdentity)"
|
|
) in history_body
|
|
assert "_routerFxNormalizeSettledStrip(keep, 'history', savedUsage);" in history_body
|
|
|
|
|
|
def test_router_decision_without_anchor_is_cached_for_history_replay() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
assert "const _pendingRouterDecisions = new Map();" in source
|
|
assert "let _historyHydrating = false;" in source
|
|
assert "let _historyHasRendered = false;" in source
|
|
assert "function _cachePendingRouterDecision(payload)" in source
|
|
assert "function _flushPendingRouterDecisions()" in source
|
|
|
|
handler_start = source.index("async function _handleRouterDecision(payload)")
|
|
handler_end = source.index(" // No matching live scan means this decision", handler_start)
|
|
handler_body = source[handler_start:handler_end]
|
|
assert "if (!_historyHasRendered || _historyHydrating) {" in handler_body
|
|
assert "_chatDiag('router_decision.cached_during_history_hydration'" in handler_body
|
|
assert "_cachePendingRouterDecision(payload);" in handler_body
|
|
assert "_chatDiag('router_decision.cached_pending_anchor'" in source
|
|
assert "_chatDiag('router_decision.skip.no_anchor_user'" not in handler_body
|
|
|
|
history_start = source.index("async function _loadHistory(opts = {}) {")
|
|
history_end = source.index("function _appendHistoryDaySeparator", history_start)
|
|
history_body = source[history_start:history_end]
|
|
assert "_historyHydrating = true;" in history_body
|
|
assert "_historyHydrating = false;" in history_body
|
|
assert "_historyHasRendered = true;" in history_body
|
|
assert history_body.index("_historyHasRendered = true;") < history_body.index(
|
|
"_flushPendingRouterDecisions();"
|
|
)
|
|
assert "_flushPendingRouterDecisions();" in history_body
|
|
|
|
|
|
def test_router_fx_settled_cleanup_clears_live_animation_state() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
assert "function _routerFxNormalizeSettledStrip(wrap, renderMode, decision) {" in source
|
|
start = source.index("function _routerFxNormalizeSettledStrip(wrap, renderMode, decision) {")
|
|
end = source.index("function _routerFxDisconnectLabelFit(wrap) {", start)
|
|
body = source[start:end]
|
|
|
|
assert "_routerFxStopScan(wrap);" in body
|
|
assert "_routerFxClearAnimationTimers(wrap);" in body
|
|
assert "_routerFxClearVisualResidue(wrap);" in body
|
|
assert "wrap.dataset.state = 'settled';" in body
|
|
assert "delete wrap.dataset.live;" in body
|
|
assert "delete wrap.dataset.scanning;" in body
|
|
assert "_routerFxApplySettledSemantics(wrap, decision, wrap.dataset.renderMode);" in body
|
|
assert "_routerFxFitLabels(wrap);" in body
|
|
|
|
clear_start = source.index("function _routerFxClearVisualResidue(wrap) {")
|
|
clear_end = source.index("function _routerFxNormalizeSettledStrip", clear_start)
|
|
clear = source[clear_start:clear_end]
|
|
assert "selector.classList.remove('visible', 'lock', 'lock-impact')" in clear
|
|
assert ".router-fx-cell.pinging" in clear
|
|
assert ".router-fx-burst" in clear
|
|
|
|
settle_start = source.index("function _settleRouterFxImmediate(wrap, winnerIdx, opts) {")
|
|
settle_end = source.index("function _routerFxFireBurst(grid, cell) {", settle_start)
|
|
settle = source[settle_start:settle_end]
|
|
assert "delete wrap.dataset.live;" in settle
|
|
assert "delete wrap.dataset.scanning;" in settle
|
|
|
|
|
|
def test_router_fx_config_refresh_prunes_stale_model_cache() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
start = source.index("async function _loadFeatureToggles() {")
|
|
end = source.index(" /* ── Session Chip", start)
|
|
body = source[start:end]
|
|
|
|
assert "Object.keys(_routerFxModels).forEach((tier) => {" in body
|
|
assert "if (!configTierSet.has(tier)) delete _routerFxModels[tier];" in body
|
|
assert "if (!configTierSet.has(tier)) delete _routerFxTierConfigs[tier];" in body
|
|
assert "_routerFxConfigTiers = configTierSet;" in body
|
|
|
|
|
|
def test_router_fx_settled_semantics_expose_render_mode_and_result() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
assert (
|
|
"wrap.dataset.renderMode = opts.renderMode || (opts.preSettled ? 'history' : 'live');"
|
|
in source
|
|
)
|
|
start = source.index("function _routerFxApplySettledSemantics(wrap, decision, renderMode) {")
|
|
end = source.index("function _routerFxClearVisualResidue(wrap) {", start)
|
|
body = source[start:end]
|
|
|
|
assert "wrap.dataset.renderMode = mode;" in body
|
|
assert "wrap.setAttribute('role', mode === 'live' ? 'status' : 'group');" in body
|
|
assert "wrap.setAttribute('aria-live', mode === 'live' ? 'polite' : 'off');" in body
|
|
assert "winnerName ? `Router selected ${winnerName}` : 'Router settled'" in body
|
|
|
|
|
|
def test_done_stream_bubble_survives_until_history_persists_assistant() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
assert "let _pendingFinalizedAssistantBubble = null;" in source
|
|
assert "function _markPendingFinalizedAssistantBubble(bubble, text)" in source
|
|
assert "function _isPendingFinalizedAssistantBubble(el)" in source
|
|
assert "function _historyStillWaitingForAssistant(messages)" in source
|
|
|
|
end_start = source.index("function _endStreaming(opts)")
|
|
end_end = source.index("_attachHoverActions(_streamBubble, 'assistant');", end_start)
|
|
end_body = source[end_start:end_end]
|
|
assert "_markPendingFinalizedAssistantBubble(_streamBubble, cleanedText);" in end_body
|
|
stamp_marker = "_stampHistoryElement(_streamBubble, '', 'assistant', cleanedText);"
|
|
mark_marker = "_markPendingFinalizedAssistantBubble(_streamBubble, cleanedText);"
|
|
assert end_body.index(stamp_marker) < end_body.index(mark_marker)
|
|
|
|
history_start = source.index("async function _loadHistory(opts = {}) {")
|
|
history_end = source.index("function _appendHistoryDaySeparator", history_start)
|
|
history_body = source[history_start:history_end]
|
|
assert "_chatDiag('history.empty.keep_pending_finalized_assistant'" in history_body
|
|
assert (
|
|
"if (_isPendingFinalizedAssistantBubble(el) && "
|
|
"_historyStillWaitingForAssistant(messages)) return;"
|
|
) in history_body
|
|
assert "_clearPendingFinalizedAssistantBubble();" in history_body
|
|
|
|
|
|
def test_generic_duplicate_stream_seq_is_classified_as_exact_handler_replay() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
assert "function _eventHasSpecificSessionHandler(event)" in source
|
|
wildcard_start = source.index("_unsubs.push(_rpc.on('*', (rawEvent, rawPayload")
|
|
wildcard_end = source.index(
|
|
" if (event.startsWith('session.event.task_group.')) return;",
|
|
wildcard_start,
|
|
)
|
|
wildcard_body = source[wildcard_start:wildcard_end]
|
|
assert "_eventHasSpecificSessionHandler(event)" in wildcard_body
|
|
assert "_chatDiag('event.generic.skip.specific_handler_stream_seq'" in wildcard_body
|
|
assert "_chatDiag('event.generic.drop.stream_seq'" in wildcard_body
|
|
|
|
|
|
def test_savings_popup_does_not_fire_for_replayed_done_frames() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
wildcard_start = source.index("_unsubs.push(_rpc.on('*'")
|
|
wildcard_end = source.index(" // Connection state changes", wildcard_start)
|
|
body = source[wildcard_start:wildcard_end]
|
|
|
|
assert "(rawEvent, rawPayload, rawMeta = {})" in body
|
|
assert "const isReplayedFrame = !!(rawMeta && rawMeta.replayed);" in body
|
|
assert "_maybeFireSavingsPopup(_finishedBubble, u, { animate: !isReplayedFrame });" in body
|
|
assert body.index("isReplayedFrame") < body.index("_maybeFireSavingsPopup(")
|
|
|
|
|
|
def test_tool_summary_exposes_visible_running_status() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
assert "function _setToolSummaryStatus(details, status)" in source
|
|
assert "function _visibleToolSummaryStatus(status)" in source
|
|
assert "return status === 'running' ? 'running' : '';" in source
|
|
build_start = source.index("function _buildToolCallDOM(")
|
|
build_end = source.index("function _retitleToolCallDOM", build_start)
|
|
build_body = source[build_start:build_end]
|
|
assert "statusSpan.className = 'chat-tools-status';" in build_body
|
|
assert "_applyToolSummaryStatus(statusSpan, isRunning ? 'running' : '');" in build_body
|
|
result_start = source.index("function _appendToolResult(payload)")
|
|
result_end = source.index(
|
|
" // \u2500\u2500 PR5: meta-skill user_input form rendering",
|
|
result_start,
|
|
)
|
|
result_body = source[result_start:result_end]
|
|
assert "_setToolSummaryStatus(details, isError ? 'error' : 'done');" in result_body
|
|
assert "statusSpan.hidden = !visibleStatus;" in source
|
|
|
|
|
|
def test_live_clarify_form_closes_matching_tool_card_before_return() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
result_start = source.index("function _appendToolResult(payload)")
|
|
clarify_start = source.index("if (\n _args", result_start)
|
|
clarify_end = source.index(" // Skipped user_input step", clarify_start)
|
|
clarify_body = source[clarify_start:clarify_end]
|
|
|
|
assert "_settleToolResultCard(payload, false);" in clarify_body
|
|
assert clarify_body.index("_settleToolResultCard(payload, false);") < clarify_body.index(
|
|
"_appendClarifyForm(payload, _args);"
|
|
)
|
|
|
|
|
|
def test_historical_clarify_tool_results_reconstruct_form_not_raw_card() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
history_start = source.index("function _reconstructToolCalls(bubbleDiv, segments)")
|
|
history_end = source.index(" function _renderMessageTags", history_start)
|
|
history_body = source[history_start:history_end]
|
|
|
|
assert "const clarifyArgs = _clarifyArgsFromHistoricalSegment(seg, details);" in history_body
|
|
assert "_appendClarifyFormToBody(body, seg, clarifyArgs);" in history_body
|
|
clarify_form_index = history_body.index(
|
|
"_appendClarifyFormToBody(body, seg, clarifyArgs);"
|
|
)
|
|
raw_result_index = history_body.index("const resultDiv = _buildToolResultDOM(")
|
|
assert clarify_form_index < raw_result_index
|
|
|
|
|
|
def test_clarify_form_renders_progress_summary_and_next_steps() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
css = CHAT_CSS.read_text(encoding="utf-8")
|
|
|
|
form_start = source.index("function _appendClarifyFormToBody(body, payload, args)")
|
|
form_end = source.index(" function _appendClarifySkipSummary", form_start)
|
|
form_body = source[form_start:form_end]
|
|
|
|
assert "const requiredFields = fields.filter((field) => field && field.required);" in form_body
|
|
assert "clarify-form-progress" in form_body
|
|
assert "clarify-form-progress-metric" in form_body
|
|
assert "clarify-form-progress-next" in form_body
|
|
assert "已收集" in form_body
|
|
assert "补充" in form_body
|
|
assert "Suggested next steps" in form_body
|
|
|
|
assert ".clarify-form-progress" in css
|
|
assert ".clarify-form-progress-metric" in css
|
|
assert ".clarify-form-progress-next" in css
|
|
|
|
|
|
def test_clarify_form_keeps_internal_prompt_out_of_primary_labels() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
css = CHAT_CSS.read_text(encoding="utf-8")
|
|
|
|
form_start = source.index("function _appendClarifyFormToBody(body, payload, args)")
|
|
form_end = source.index(" function _appendClarifySkipSummary", form_start)
|
|
form_body = source[form_start:form_end]
|
|
|
|
assert "const clarifyFieldDisplayLabel = (field) => {" in form_body
|
|
assert "script draft" in form_body
|
|
assert "verbatim reply" in form_body
|
|
assert "回复内容" in form_body
|
|
assert "Reply" in form_body
|
|
assert "missingRequiredFields.map(clarifyFieldDisplayLabel)" in form_body
|
|
assert "label.textContent = clarifyFieldDisplayLabel(field) + requiredFlag;" in form_body
|
|
assert "field.prompt && clarifyFieldDisplayLabel(field) !== fieldPromptText" in form_body
|
|
assert "clarify-form-field-help" in form_body
|
|
assert "填写要求" in form_body
|
|
assert "Requirements" in form_body
|
|
|
|
assert ".clarify-form-field-help" in css
|
|
assert ".clarify-form-field-help-summary" in css
|
|
assert ".clarify-form-field-help-body" in css
|
|
|
|
|
|
def test_clarify_form_submit_waits_for_connection_and_shows_pending_feedback() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
css = CHAT_CSS.read_text(encoding="utf-8")
|
|
|
|
form_start = source.index("function _appendClarifyFormToBody(body, payload, args)")
|
|
form_end = source.index(" function _appendClarifySkipSummary", form_start)
|
|
form_body = source[form_start:form_end]
|
|
call_index = form_body.index("_rpc.call('chat.clarify_submit'")
|
|
|
|
assert "_rpc.waitForConnection()" in form_body
|
|
assert form_body.index("_rpc.waitForConnection()") < call_index
|
|
assert "clarify-form--submitting" in form_body
|
|
assert "clarify-form--submitted" in form_body
|
|
assert "clarify-form-submit-status" in form_body
|
|
assert "已提交,正在继续流程" in form_body
|
|
assert "Submitted, continuing" in form_body
|
|
|
|
assert ".clarify-form--submitting" in css
|
|
assert ".clarify-form-submit-status" in css
|
|
|
|
|
|
def test_clarify_submit_button_has_direct_click_fallback() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
|
|
form_start = source.index("function _appendClarifyFormToBody(body, payload, args)")
|
|
form_end = source.index(" function _appendClarifySkipSummary", form_start)
|
|
form_body = source[form_start:form_end]
|
|
|
|
assert "let clarifySubmitInFlight = false;" in form_body
|
|
assert "const submitClarifyForm = () => {" in form_body
|
|
assert "const form = document.createElement('div');" in form_body
|
|
assert "form.setAttribute('role', 'form');" in form_body
|
|
assert "submitBtn.type = 'button';" in form_body
|
|
assert "submitBtn.addEventListener('click', (evt) => {" in form_body
|
|
assert "form.addEventListener('keydown', (evt) => {" in form_body
|
|
assert "evt.key !== 'Enter'" in form_body
|
|
assert "evt.preventDefault();" in form_body
|
|
assert "submitClarifyForm();" in form_body
|
|
|
|
|
|
def test_clarify_submit_connection_wait_has_visible_timeout() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
|
|
form_start = source.index("function _appendClarifyFormToBody(body, payload, args)")
|
|
form_end = source.index(" function _appendClarifySkipSummary", form_start)
|
|
form_body = source[form_start:form_end]
|
|
|
|
assert "const waitForClarifySubmitConnection = () => Promise.race([" in form_body
|
|
assert "_rpc.waitForConnection()," in form_body
|
|
assert "setTimeout(() => reject(new Error(clarifyText(" in form_body
|
|
assert "连接超时,请刷新页面后重试。" in form_body
|
|
assert "waitForClarifySubmitConnection()" in form_body
|
|
|
|
|
|
def test_router_fx_settles_but_preserves_winner_animation_when_output_begins() -> None:
|
|
# The moment output renders, the panel should stop roaming, but the winner
|
|
# lock/settle animation must remain visible instead of becoming a static
|
|
# empty frame.
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
css = CHAT_CSS.read_text(encoding="utf-8")
|
|
|
|
assert "function _routerFxSettleForOutput() {" in source
|
|
fz_start = source.index("function _routerFxSettleForOutput() {")
|
|
fz = source[fz_start:source.index("// Lock an in-flight scanning strip", fz_start)]
|
|
assert "_routerFxFinishScan(wrap);" in fz
|
|
assert "_routerFxStopScan(wrap);" not in fz
|
|
assert "wrap.dataset.frozen = 'true';" not in fz
|
|
# It is invoked at the top of the stream-bubble (output) path.
|
|
esb = source[source.index("function _ensureStreamBubble() {"):]
|
|
esb = esb[:esb.index("function _newTextSegment")]
|
|
assert "_routerFxSettleForOutput();" in esb
|
|
assert '.router-fx[data-frozen="true"]' not in css
|
|
|
|
|
|
def test_router_fx_history_mode_has_no_motion_effects() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
css = CHAT_CSS.read_text(encoding="utf-8")
|
|
|
|
assert "function _routerFxStaticizeCompletedStrips(sessionKey) {" in source
|
|
end_start = source.index("function _endStreaming(opts) {")
|
|
end_body = source[end_start:source.index("function _hasViewLocalStreamState()", end_start)]
|
|
assert "_routerFxStaticizeCompletedStrips(_streamSessionKey || _sessionKey || '');" in end_body
|
|
|
|
assert '.router-fx[data-render-mode="history"]' in css
|
|
history_motion_start = css.index('.router-fx[data-render-mode="history"],')
|
|
history_motion = css[history_motion_start:css.index("}", history_motion_start)]
|
|
assert "animation: none !important;" in history_motion
|
|
assert "transition: none !important;" in history_motion
|
|
|
|
|
|
def test_router_fx_history_and_turn_meta_preserve_observe_rollout_state() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
history_start = source.index("function _buildRouterFxFromUsage(usage, seedKey, opts) {")
|
|
history_end = source.index(" /* ── RPC Event Subscriptions", history_start)
|
|
history_body = source[history_start:history_end]
|
|
store_start = source.index("_storeTurnMeta(_sessionKey, _metaIdx")
|
|
store_end = source.index(" });", store_start)
|
|
store_body = source[store_start:store_end]
|
|
|
|
assert "routing_applied: usage.routing_applied !== false," in history_body
|
|
assert "rollout_phase: usage.rollout_phase || 'full'," in history_body
|
|
assert "const observeMode = decision && decision.routing_applied === false;" in source
|
|
assert "routing_applied: u.routing_applied !== false," in store_body
|
|
assert "rollout_phase: u.rollout_phase || 'full'," in store_body
|
|
|
|
|
|
def test_router_fx_mobile_grid_uses_dynamic_candidate_columns() -> None:
|
|
"""Mobile router-fx grid follows actual candidate count, not a fixed wall."""
|
|
css = CHAT_CSS.read_text(encoding="utf-8")
|
|
mobile_start = css.index("@media (max-width: 640px)")
|
|
tiny_start = css.index("@media (max-width: 380px)")
|
|
mobile_body = css[mobile_start:tiny_start]
|
|
tiny_body = css[tiny_start:]
|
|
|
|
assert (
|
|
"grid-template-columns: "
|
|
"repeat(var(--router-fx-mobile-cols, var(--router-fx-cols, 2)), 1fr);"
|
|
) in mobile_body
|
|
assert "grid-template-rows: none;" in mobile_body
|
|
assert "grid-template-columns: repeat(var(--router-fx-mobile-cols, 2), 1fr);" in tiny_body
|
|
assert "grid-template-rows: none;" in tiny_body
|
|
|
|
|
|
def test_router_fx_visualisation_pref_is_client_side_localstorage() -> None:
|
|
# The router-fx ON/OFF state is a per-browser preference (theme.js style),
|
|
# NOT a gateway config write — distinct from the operator squilla_router
|
|
# toggle. Persisted under an opensquilla-* localStorage key, default ON.
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
|
|
assert "const _ROUTER_FX_PREF_KEY = 'opensquilla-router-fx';" in source
|
|
assert (
|
|
"const _routerFx = { enabled: true, visualMode: 'real_candidates', "
|
|
"variant: 'default' };"
|
|
) in source
|
|
assert "function _routerFxLoadPref() {" in source
|
|
assert "function _routerFxSavePref() {" in source
|
|
assert "localStorage.getItem(_ROUTER_FX_PREF_KEY)" in source
|
|
assert "localStorage.setItem(_ROUTER_FX_PREF_KEY" in source
|
|
# Hydrated + switch synced on every render (inherits visibility/focus refresh).
|
|
assert "_routerFxLoadPref();" in source
|
|
assert "if (routerFxToggle) routerFxToggle.checked = _routerFx.enabled;" in source
|
|
|
|
# Load path: a stored pref must actually be PARSED and APPLIED back onto
|
|
# _routerFx (a no-op loader would silently reset a saved OFF to default-ON),
|
|
# validating types, and must swallow localStorage/JSON throws (private mode).
|
|
load_start = source.index("function _routerFxLoadPref() {")
|
|
load_end = source.index("function _routerFxSavePref() {", load_start)
|
|
load_body = source[load_start:load_end]
|
|
assert "const saved = JSON.parse(raw);" in load_body
|
|
assert "if (typeof saved.enabled === 'boolean') _routerFx.enabled = saved.enabled;" in load_body
|
|
assert "saved.visualMode" not in load_body
|
|
assert "saved.panel" not in load_body
|
|
assert "saved.variant" not in load_body
|
|
assert "_routerFx.variant = 'default';" in load_body
|
|
assert "} catch { /* keep defaults */ }" in load_body
|
|
|
|
# Save path: serialize the live pref and swallow quota/availability throws.
|
|
save_start = load_end
|
|
save_end = source.index("function _routerFxSortTiers(", save_start)
|
|
save_body = source[save_start:save_end]
|
|
assert "localStorage.setItem(_ROUTER_FX_PREF_KEY, JSON.stringify({" in save_body
|
|
assert "enabled: _routerFx.enabled," in save_body
|
|
assert "visualMode:" not in save_body
|
|
assert "panel:" not in save_body
|
|
assert "variant:" not in save_body
|
|
assert "} catch { /* preference is best-effort */ }" in save_body
|
|
|
|
# The toggle handler is client-side: it saves the pref and does NOT write
|
|
# gateway config (no config.patch.safe in the router-fx handler). ON re-renders
|
|
# historical strips via the rebuild; both branches give toast feedback.
|
|
fx_start = source.index("const routerFxToggle = _el.querySelector('#toggle-router-fx');")
|
|
fx_end = source.index("// Re-pull router config", fx_start)
|
|
fx_body = source[fx_start:fx_end]
|
|
assert "_routerFx.enabled = routerFxToggle.checked;" in fx_body
|
|
assert "_routerFxSavePref();" in fx_body
|
|
assert "config.patch.safe" not in fx_body
|
|
assert "_scheduleHistorySync();" in fx_body
|
|
assert "if (window.SavingsFX) window.SavingsFX.setEnabled(_routerFx.enabled);" in fx_body
|
|
assert "UI.toast('Visual effects: '" in fx_body
|
|
|
|
|
|
def test_router_effects_default_on_and_cloud_choice_hidden() -> None:
|
|
chat_source = CHAT_JS.read_text(encoding="utf-8")
|
|
savings_source = (
|
|
Path("src/opensquilla/gateway/static/js/components/savings-fx.js")
|
|
.read_text(encoding="utf-8")
|
|
)
|
|
|
|
assert (
|
|
"const _routerFx = { enabled: true, visualMode: 'real_candidates', "
|
|
"variant: 'default' };"
|
|
) in chat_source
|
|
assert (
|
|
"try { return window.localStorage.getItem(_PREF_KEY) !== '0'; } catch { return true; }"
|
|
in savings_source
|
|
)
|
|
assert "if (window.SavingsFX) window.SavingsFX.setEnabled(_routerFx.enabled);" in chat_source
|
|
assert 'id="toggle-savings-fx"' not in chat_source
|
|
assert "Savings FX" not in chat_source
|
|
assert "Visual effects" in chat_source
|
|
assert "Show router and savings effects" in chat_source
|
|
assert "Router effects" not in chat_source
|
|
assert "Router animation" not in chat_source
|
|
assert "Cloud view" not in chat_source
|
|
|
|
|
|
def test_router_fx_render_gated_in_both_live_and_history_paths() -> None:
|
|
# The visualisation pref gates RENDER in both the live (router_decision) and
|
|
# history (rebuild) paths, ahead of all test-pinned lines. Tier/model
|
|
# bookkeeping stays warm in the live path; the gate sits after it and before
|
|
# the config await so a disabled panel short-circuits the 1.5s wait too.
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
handler_start = source.index("async function _handleRouterDecision(payload) {")
|
|
handler_end = source.index(" // History-load entry point", handler_start)
|
|
handler_body = source[handler_start:handler_end]
|
|
|
|
pre_gate = (
|
|
"if (!_routerFx.enabled) {\n"
|
|
" _chatDiag('router_decision.skip.disabled_pre_config'"
|
|
)
|
|
post_gate = (
|
|
"if (!_routerFx.enabled) {\n"
|
|
" _chatDiag('router_decision.skip.disabled_post_config'"
|
|
)
|
|
assert pre_gate in handler_body
|
|
assert post_gate in handler_body
|
|
assert handler_body.index("_routerFxRememberTierDecision(tier, payload.model || '');") < \
|
|
handler_body.index(pre_gate)
|
|
assert handler_body.index(pre_gate) < \
|
|
handler_body.index("await _routerFxAwaitConfig();")
|
|
# Re-checked AFTER the await as well — the user may flip OFF during the
|
|
# cold-start config wait, so the gate is symmetric on both sides.
|
|
assert handler_body.count("if (!_routerFx.enabled) {") >= 2
|
|
assert handler_body.index(post_gate) > \
|
|
handler_body.index("await _routerFxAwaitConfig();")
|
|
# History path gate returns null ahead of the operator gate (caller null-checks).
|
|
assert "if (!_routerFx.enabled) return null;" in source
|
|
assert source.index("if (!_routerFx.enabled) return null;") < \
|
|
source.index("if (_routerFxConfigTiers !== null && !_routerFeatureEnabled) return null;")
|
|
|
|
|
|
def test_router_fx_disable_removes_all_strips_without_live_spare_path() -> None:
|
|
# Hiding the panel is a user-visible preference. The disabled path should
|
|
# remove router visuals directly instead of preserving a separate live-strip
|
|
# path that competes with history reorder repair.
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
|
|
assert (
|
|
"_thread.querySelectorAll('.router-fx').forEach((n) => _routerFxRemoveStrip(n));"
|
|
in source
|
|
)
|
|
assert ".router-fx:not([data-live=\"true\"])" not in source
|
|
history_start = source.index("async function _loadHistory(opts = {}) {")
|
|
history_end = source.index(" /* ── Send Message", history_start)
|
|
history_body = source[history_start:history_end]
|
|
assert "if (!_routerFx.enabled) {" in history_body
|
|
sweep = history_body[history_body.index("if (!_routerFx.enabled) {"):]
|
|
sweep = sweep[: sweep.index("_lastSavingsPopupIdentity")]
|
|
assert (
|
|
"_thread.querySelectorAll('.router-fx').forEach((el) => _routerFxRemoveStrip(el));"
|
|
in sweep
|
|
)
|
|
assert "dataset.live" not in sweep
|
|
|
|
|
|
def test_router_toggle_off_immediately_stops_router_visuals() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
start = source.index("const routerToggle = _el.querySelector('#toggle-router');")
|
|
end = source.index("// Router-fx visualisation toggle", start)
|
|
body = source[start:end]
|
|
|
|
assert "const previousRouterFeatureEnabled = _routerFeatureEnabled;" in body
|
|
assert "_routerFeatureEnabled = enabled;" in body
|
|
assert "if (!enabled) _clearRouterFxVisuals('router_disabled');" in body
|
|
assert "_routerFeatureEnabled = previousRouterFeatureEnabled;" in body
|
|
assert "_clearRouterFxVisuals('router_patch_reverted');" in body
|
|
assert "_scheduleHistorySync();" in body
|
|
|
|
assert "function _clearRouterFxVisuals(reason = '') {" in source
|
|
clear_start = source.index("function _clearRouterFxVisuals(reason = '') {")
|
|
clear_end = source.index("async function _finishPendingRouterFxScan", clear_start)
|
|
clear_body = source[clear_start:clear_end]
|
|
assert "_cancelPendingRouterFxScan(reason || 'clear_visuals');" in clear_body
|
|
assert (
|
|
"_thread.querySelectorAll('.router-fx').forEach((el) => "
|
|
"_routerFxRemoveStrip(el));"
|
|
in clear_body
|
|
)
|
|
|
|
|
|
def test_router_fx_variant_seam_stamps_data_variant() -> None:
|
|
# data-variant on the .router-fx root is the style-variant seam (same idiom
|
|
# as data-state/source/observe). Only non-'default' is stamped, leaving the
|
|
# base look attribute-free. A documented CSS scaffold marks the extension
|
|
# point; no variant block ships yet.
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
css = CHAT_CSS.read_text(encoding="utf-8")
|
|
builder_start = source.index("function _buildRouterFxElement(decision, opts) {")
|
|
builder_end = source.index(" function ", builder_start + 1)
|
|
builder_body = source[builder_start:builder_end]
|
|
|
|
assert ("const variant = (opts.variant != null ? opts.variant : _routerFx.variant)"
|
|
" || 'default';") in builder_body
|
|
assert "if (variant && variant !== 'default') wrap.dataset.variant = variant;" in builder_body
|
|
assert "Router-fx style variants" in css
|
|
assert '.router-fx[data-variant="<name>"]' in css
|
|
|
|
|
|
def test_router_fx_visualisation_toggle_markup_reuses_switch() -> None:
|
|
# The user-facing switch lives in the composer 'Composer settings' popover,
|
|
# reusing the existing .toggle-switch recipe verbatim (no fifth toggle CSS).
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
popover_start = source.index('id="chat-toolbar-popover"')
|
|
popover_end = source.index('chat-input-wrap', popover_start)
|
|
popover = source[popover_start:popover_end]
|
|
|
|
assert 'id="toggle-router-fx"' in popover
|
|
assert "Visual effects" in popover
|
|
assert "Show router and savings effects" in popover
|
|
assert "Router effects" not in popover
|
|
assert 'id="toggle-savings-fx"' not in popover
|
|
assert "Savings FX" not in popover
|
|
assert 'class="toggle-switch"' in popover
|
|
assert popover.count('class="toggle-track"') >= 2
|
|
|
|
|
|
def test_chat_history_replays_turn_meta_to_restore_combo_streak() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
start = source.index("async function _loadHistory(opts = {}) {")
|
|
end = source.index(" /* ── Send Message", start)
|
|
body = source[start:end]
|
|
|
|
assert "function _historyTurnMeta(msg) {" in source
|
|
assert "function _savedUsageFromMeta(meta) {" in source
|
|
assert "function _turnSavingsIdentity(u) {" in source
|
|
assert "if (window.SavingsFX) window.SavingsFX.resetStreak();" in body
|
|
assert "let historySavingsIdentity = '';" in body
|
|
assert "const m = _historyTurnMeta(msg) || _recallTurnMeta(_sessionKey, _histAsstIdx);" in body
|
|
assert "const savedUsage = _savedUsageFromMeta(m);" in body
|
|
assert "const identity = _turnSavingsIdentity(savedUsage);" in body
|
|
assert "if (identityChanged) savedUsage.__savings_ui_suppressed = true;" in body
|
|
assert "window.SavingsFX.noteTurn(savedUsage);" in body
|
|
assert body.index("window.SavingsFX.noteTurn(savedUsage);") < body.index(
|
|
"_attachTurnMeta(div, m.model, m.input, m.output, savedUsage || undefined);"
|
|
)
|
|
assert "_lastSavingsPopupIdentity = historySavingsIdentity;" in body
|
|
|
|
|
|
def test_chat_turn_meta_replaces_existing_footer_before_append() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
start = source.index("function _attachTurnMeta(")
|
|
end = source.index(" function _normalizeAgentId", start)
|
|
body = source[start:end]
|
|
|
|
assert "bubble.querySelectorAll(':scope > .msg-meta')" in body
|
|
assert "forEach((el) => el.remove())" in body
|
|
assert body.index("bubble.querySelectorAll(':scope > .msg-meta')") < body.index(
|
|
"bubble.appendChild(meta);"
|
|
)
|
|
|
|
|
|
def test_chat_done_event_reconciles_final_text_before_ending_stream() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
start = source.index("if (event.endsWith('.done') || event === 'chat.done') {")
|
|
end = source.index(" // Populate savings indicator", start)
|
|
body = source[start:end]
|
|
|
|
assert "const finalText = typeof u.text === 'string' ? u.text : '';" in body
|
|
assert "if (finalText && finalText !== _streamRaw)" in body
|
|
# _endStreaming now takes an optional {reason} so abort-vs-natural can be
|
|
# distinguished; the ordering invariant (reconcile before end) is preserved.
|
|
end_call_marker = "_endStreaming(_doneWasAborted ? { reason: 'aborted' } : undefined);"
|
|
assert end_call_marker in body
|
|
assert body.index("if (finalText && finalText !== _streamRaw)") < body.index(end_call_marker)
|
|
|
|
|
|
def test_chat_turn_complete_event_schedules_history_sync() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
start = source.index("_rpc.on('sessions.changed'")
|
|
end = source.index("_rpc.on('task.queued'", start)
|
|
body = source[start:end]
|
|
|
|
assert "function _scheduleHistorySync()" in source
|
|
assert "reason === 'turn_complete'" in source
|
|
assert "_sessionChangeIsTerminal(payload)" in body
|
|
helper = source[
|
|
source.index("function _syncTerminalSessionChange(payload = {})") :
|
|
source.index(
|
|
" function _activeTaskGroupRunState",
|
|
source.index("function _syncTerminalSessionChange(payload = {})"),
|
|
)
|
|
]
|
|
assert "_scheduleHistorySync();" in helper
|
|
|
|
|
|
def test_chat_turn_complete_event_schedules_pending_queue_drain_fallback() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
helper = source[
|
|
source.index("function _syncTerminalSessionChange(payload = {})") :
|
|
source.index(
|
|
" function _activeTaskGroupRunState",
|
|
source.index("function _syncTerminalSessionChange(payload = {})"),
|
|
)
|
|
]
|
|
scheduler = source[
|
|
source.index("function _schedulePendingDrainAfterTerminal()") :
|
|
source.index(
|
|
" // Programmatic textarea write",
|
|
source.index("function _schedulePendingDrainAfterTerminal()"),
|
|
)
|
|
]
|
|
|
|
assert "_schedulePendingDrainAfterTerminal();" in helper
|
|
assert "const recoverPending =" in helper
|
|
assert "_recoverPendingAfterTerminal(state.status);" in helper
|
|
assert (
|
|
"if (_isStreaming || _isCompactInFlightForCurrentSession() || "
|
|
"_pendingQueue.length === 0) return;"
|
|
in scheduler
|
|
)
|
|
assert "_drainQueueHead();" in scheduler
|
|
|
|
|
|
def test_chat_failed_terminal_events_recover_pending_queue_to_composer() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
error_start = source.index("} else if (event.endsWith('.error')) {")
|
|
error_end = source.index(" }\n }));", error_start)
|
|
error_body = source[error_start:error_end]
|
|
terminal_start = source.index("const terminalStatus = _taskTerminalStatus(rawEvent);")
|
|
terminal_end = source.index(" const normalized =", terminal_start)
|
|
terminal_body = source[terminal_start:terminal_end]
|
|
|
|
assert "function _recoverPendingAfterTerminal(status = 'failed')" in source
|
|
assert "_recoverPendingAfterTerminal(_normalizeRunStatus" in error_body
|
|
assert "_recoverPendingAfterTerminal(terminalRunStatus);" in terminal_body
|
|
|
|
|
|
def test_chat_approval_pending_has_distinct_run_status() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
approval_start = source.index("function _setStreamIdlePausedForApproval(paused)")
|
|
approval_end = source.index(" function _resetStreamIdleTimer()", approval_start)
|
|
approval_body = source[approval_start:approval_end]
|
|
|
|
assert "approval_pending: 'Waiting for approval'" in source
|
|
assert "approval_pending: 'chip-warn'" in source
|
|
assert "_approvalPendingForCurrentSession" in source
|
|
assert "run_status: 'approval_pending'" in approval_body
|
|
assert "active_task: { status: 'approval_pending'" in approval_body
|
|
|
|
|
|
def test_chat_replayed_compaction_terminal_restores_separator_without_toast() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
start = source.index("function _showCompactionToast(payload, meta = {})")
|
|
end = source.index(" /* ── RPC Event Subscriptions", start)
|
|
body = source[start:end]
|
|
|
|
assert "function _showCompactionToast(payload, meta = {})" in source
|
|
assert "function _compactionTerminalStatus(status)" in source
|
|
assert "const isReplay = !!(meta && meta.replayed);" in body
|
|
assert "if (isReplay && !_compactionTerminalStatus(status)) return;" in body
|
|
assert "if (meta && meta.replayed) return;" not in body
|
|
assert "if (!isReplay) UI.toast('Compact failed'" in body
|
|
assert "if (!isReplay) {\n UI.toast(\n 'Compact cancelled'" in body
|
|
|
|
|
|
def test_chat_terminal_compaction_separator_persists_for_completed_manual_and_auto() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
sync_start = source.index(
|
|
"function _syncCompactionSeparator(payload, status, source, overrides = {})"
|
|
)
|
|
sync_end = source.index("function _clearCompactionSummarySeparators", sync_start)
|
|
sync_body = source[sync_start:sync_end]
|
|
|
|
assert "function _compactionSeparatorAnimated(status, overrides = {})" in source
|
|
assert "function _shouldPersistCompactionSeparator(status, source, overrides = {})" in source
|
|
assert "return status === 'completed';" in source
|
|
assert "const liveClass = _compactionSeparatorAnimated(status, overrides)" in sync_body
|
|
assert "filter(Boolean)" in sync_body
|
|
assert "if (_shouldPersistCompactionSeparator(status, source, overrides)) return;" in sync_body
|
|
assert "_scheduleCompactionSeparatorRemoval();" in sync_body
|
|
|
|
|
|
def test_rpc_client_passes_event_meta_without_polluting_payload() -> None:
|
|
source = RPC_JS.read_text(encoding="utf-8")
|
|
event_start = source.index("} else if (data.type === 'event') {")
|
|
event_end = source.index(" };\n", event_start)
|
|
event_body = source[event_start:event_end]
|
|
|
|
assert "const meta = data.meta || {};" in event_body
|
|
assert "h(data.payload, meta)" in event_body
|
|
assert "h(data.event, data.payload, meta)" in event_body
|
|
|
|
|
|
def test_chat_history_reconciles_by_message_identity_without_clear_replace() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
start = source.index("async function _loadHistory(opts = {}) {")
|
|
end = source.index(" /* ── Send Message", start)
|
|
body = source[start:end]
|
|
|
|
assert "function _historyStableMessageIdentity(msg)" in source
|
|
assert "function _historyElementFallbackIdentity(el)" in source
|
|
assert "function _historyFallbackText(role, text)" in source
|
|
assert "const existingByStableIdentity = new Map();" in body
|
|
assert "const existingByFallbackIdentity = new Map();" in body
|
|
assert "const consumedHistoryElements = new Set();" in body
|
|
assert "data-message-id" in source
|
|
assert (
|
|
"_stampHistoryElement(div, stableIdentity, msg.role, displayText, "
|
|
"_messageTranscriptId(msg));"
|
|
) in body
|
|
assert "let div = stableIdentity ? existingByStableIdentity.get(stableIdentity) : null;" in body
|
|
assert "consumedHistoryElements," in body
|
|
assert "consumedHistoryElements.add(div);" in body
|
|
assert body.index("const messages = data.messages || [];") < body.index(
|
|
"const existingByStableIdentity = new Map();"
|
|
)
|
|
assert " _thread.innerHTML = '';" not in body[: body.index("if (messages.length === 0)")]
|
|
assert "if (_isStreaming && _isCurrentSessionStreamBubble(el)) return;" in body
|
|
assert "if (!consumedHistoryElements.has(el)) el.remove();" in body
|
|
|
|
|
|
def test_chat_view_wires_middle_edit_branch_fork_id() -> None:
|
|
view = CHAT_VIEW_VUE.read_text(encoding="utf-8")
|
|
send = CHAT_SEND_TS.read_text(encoding="utf-8")
|
|
actions = CHAT_MESSAGE_ACTIONS_TS.read_text(encoding="utf-8")
|
|
|
|
assert "const pendingForkBeforeMessageId = ref<string | null>(null)" in view
|
|
|
|
actions_start = view.index("const chatMessageActions = useChatMessageActions({")
|
|
actions_end = view.index("})\nconst {", actions_start)
|
|
assert "pendingForkBeforeMessageId," in view[actions_start:actions_end]
|
|
|
|
send_start = view.index("const chatSend = useChatSend({")
|
|
send_end = view.index("})\nconst { onSend", send_start)
|
|
assert "pendingForkBeforeMessageId," in view[send_start:send_end]
|
|
|
|
session_watch_start = view.index("watch(sessionKey, () => {")
|
|
session_watch_end = view.index("})", session_watch_start)
|
|
assert "pendingForkBeforeMessageId.value = null" in view[session_watch_start:session_watch_end]
|
|
|
|
assert "pendingForkBeforeMessageId: Ref<string | null>" in send
|
|
assert "params.forkBeforeMessageId = forkBeforeMessageId" in send
|
|
assert "options.pendingForkBeforeMessageId.value = null" in send
|
|
assert (
|
|
"options.pendingForkBeforeMessageId.value = "
|
|
"options.messages.value[userMsgIndex]?.messageId || null"
|
|
) in actions
|
|
assert (
|
|
"options.pendingForkBeforeMessageId.value = "
|
|
"options.messages.value[msgIndex]?.messageId || null"
|
|
) in actions
|
|
|
|
|
|
def test_chat_history_reorders_reused_nodes_to_match_transcript_order() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
start = source.index("async function _loadHistory(opts = {}) {")
|
|
end = source.index(" /* ── Send Message", start)
|
|
body = source[start:end]
|
|
|
|
assert "_thread.querySelectorAll('.chat-day-sep').forEach((el) => el.remove());" in body
|
|
assert "function _appendHistoryElementInOrder(div)" in source
|
|
assert "const liveTail = _historyLiveTailAnchor();" in source
|
|
assert "if (liveTail && div !== liveTail)" in source
|
|
assert "_thread.insertBefore(div, liveTail);" in source
|
|
assert "_thread.appendChild(div);" in source
|
|
stamp_idx = body.index(
|
|
"_stampHistoryElement(div, stableIdentity, msg.role, displayText, "
|
|
"_messageTranscriptId(msg));"
|
|
)
|
|
reorder_idx = body.index("_appendHistoryElementInOrder(div);")
|
|
assert stamp_idx < reorder_idx
|
|
|
|
|
|
def test_chat_history_fallback_identity_consumes_duplicate_elements() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
|
|
assert "function _pushIdentityElement(map, identity, el)" in source
|
|
assert "function _shiftIdentityElement(map, identity, consumedElements = null)" in source
|
|
assert "elements.push(el);" in source
|
|
assert "if (!consumedElements || !consumedElements.has(el)) return el;" in source
|
|
|
|
|
|
def test_chat_history_fallback_identity_normalizes_assistant_directives() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
start = source.index("function _historyFallbackText")
|
|
end = source.index(" function _pushIdentityElement", start)
|
|
body = source[start:end]
|
|
|
|
assert (
|
|
"if (role === 'assistant') return "
|
|
"_stripProtocolTextLeak("
|
|
"_stripDirectiveTags(_stripGeneratedArtifactMarkers(text || ''))"
|
|
").trim();"
|
|
in body
|
|
)
|
|
|
|
|
|
def test_chat_first_delta_marks_render_dirty_before_flush() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
start = source.index("function _appendDelta(text) {")
|
|
end = source.index(" function _flushRender()", start)
|
|
body = source[start:end]
|
|
|
|
assert "_renderDirty = true;\n _flushRender();" in body
|
|
|
|
|
|
def test_chat_flushes_pending_text_before_tool_segment_boundary() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
helper_start = source.index("function _flushPendingTextSegment() {")
|
|
helper_end = source.index(" function _flushRender()", helper_start)
|
|
helper_body = source[helper_start:helper_end]
|
|
tool_start = source.index("function _appendToolCall(payload) {")
|
|
tool_end = source.index(" function _appendToolResult(payload) {", tool_start)
|
|
tool_body = source[tool_start:tool_end]
|
|
|
|
assert "if (!_renderDirty) return;" in helper_body
|
|
assert "_flushRender();" in helper_body
|
|
assert tool_body.index("_flushPendingTextSegment();") < tool_body.index(
|
|
"_newTextSegment();"
|
|
)
|
|
|
|
|
|
def test_chat_history_replacement_preserves_message_body_rendering() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
replace_start = source.index("function _replaceHistoryMessage")
|
|
replace_end = source.index(" /* ── Send Message", replace_start)
|
|
replace_body = source[replace_start:replace_end]
|
|
render_start = source.index("function _renderMessageBody")
|
|
render_end = source.index(" function _scrollToBottom", render_start)
|
|
render_body = source[render_start:render_end]
|
|
|
|
assert "_renderMessageBody(body, role, text, options);" in replace_body
|
|
visible_text_assignment = (
|
|
"const visibleText = role === 'assistant' "
|
|
"? _stripGeneratedArtifactMarkers(text) : text;"
|
|
)
|
|
markdown_render = (
|
|
"Markdown.render(_stripProtocolTextLeak(_stripDirectiveTags(visibleText)))"
|
|
)
|
|
assert visible_text_assignment in render_body
|
|
assert markdown_render in render_body
|
|
assert "Markdown.bindHighlight(body);" in render_body
|
|
|
|
|
|
def test_chat_history_replacement_rebuilds_role_header() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
assert "function _syncMessageHeader(div, displayRole, timestamp, options = {}) {" in source
|
|
|
|
helper_start = source.index(
|
|
"function _syncMessageHeader(div, displayRole, timestamp, options = {}) {"
|
|
)
|
|
helper_end = source.index("function _replaceHistoryMessage", helper_start)
|
|
helper_body = source[helper_start:helper_end]
|
|
assert "const existing = div.querySelector(':scope > .msg-header');" in helper_body
|
|
assert "_displayRoleLabel(displayRole)" in helper_body
|
|
assert "_renderMessageTags(options)" in helper_body
|
|
assert "div.insertBefore(header, div.firstChild);" in helper_body
|
|
assert "if (sameGroup) {" in helper_body
|
|
assert "if (existing) existing.remove();" in helper_body
|
|
|
|
replace_start = source.index("function _replaceHistoryMessage")
|
|
replace_end = source.index(" function _replaceStreamText", replace_start)
|
|
replace_body = source[replace_start:replace_end]
|
|
assert (
|
|
"_syncMessageHeader(div, displayRole, options.timestamp || null, options);"
|
|
in replace_body
|
|
)
|
|
|
|
history_start = source.index("const msgOptions = {")
|
|
history_end = source.index("_messages.push({", history_start)
|
|
history_body = source[history_start:history_end]
|
|
assert "timestamp: msg.timestamp || msg.ts || null," in history_body
|
|
|
|
|
|
def test_chat_streaming_text_strips_generated_artifact_markers() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
flush_start = source.index("function _flushRender()")
|
|
flush_end = source.index(" function _endStreaming", flush_start)
|
|
flush_body = source[flush_start:flush_end]
|
|
end_start = source.index("function _endStreaming")
|
|
end_end = source.index(" /* ── Attachments", end_start)
|
|
end_body = source[end_start:end_end]
|
|
|
|
assert "function _stripGeneratedArtifactMarkers(text)" in source
|
|
assert "_stripGeneratedArtifactMarkers(_activeTextRaw)" in flush_body
|
|
assert "_stripGeneratedArtifactMarkers(_streamRaw)" in end_body
|
|
assert "_stripGeneratedArtifactMarkers(seg.raw)" in end_body
|
|
|
|
|
|
def test_chat_history_text_segments_use_protocol_leak_guard() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
reconstruct_start = source.index("function _reconstructToolCalls")
|
|
reconstruct_end = source.index(" /* ── Message Rendering", reconstruct_start)
|
|
reconstruct_body = source[reconstruct_start:reconstruct_end]
|
|
render_start = source.index("function _renderMessageBody")
|
|
render_end = source.index(" function _scrollToBottom", render_start)
|
|
render_body = source[render_start:render_end]
|
|
|
|
assert "function _stripProtocolTextLeak" in source
|
|
assert "_stripProtocolTextLeak(seg.text || '')" in reconstruct_body
|
|
assert "_stripProtocolTextLeak(_stripDirectiveTags(visibleText))" in render_body
|
|
assert "View areas around line" in source
|
|
assert "effect_calls" in source
|
|
assert "angle\\s+brackets" in source
|
|
|
|
|
|
def test_approval_monitor_uses_adaptive_timeout_backoff() -> None:
|
|
source = Path("src/opensquilla/gateway/static/js/approval_monitor.js").read_text(
|
|
encoding="utf-8"
|
|
)
|
|
|
|
assert "const POLL_MAX_MS = 30000;" in source
|
|
assert "let _pollDelayMs = POLL_MS;" in source
|
|
assert "function _schedulePoll(delayMs = _pollDelayMs)" in source
|
|
assert "setTimeout(async () =>" in source
|
|
assert "_increasePollBackoff();" in source
|
|
assert "setInterval(_poll, POLL_MS)" not in source
|
|
|
|
|
|
def test_approval_monitor_sends_auth_headers() -> None:
|
|
source = Path("src/opensquilla/gateway/static/js/approval_monitor.js").read_text(
|
|
encoding="utf-8"
|
|
)
|
|
|
|
assert "function _authHeaders(extra)" in source
|
|
assert "App.getAuthToken" in source
|
|
assert "window.App && App.getAuthToken" not in source
|
|
assert "typeof App !== 'undefined'" in source
|
|
assert "headers['Authorization'] = `Bearer ${token}`;" in source
|
|
assert "headers: _authHeaders()," in source
|
|
assert "headers: _authHeaders({ 'Content-Type': 'application/json' })" in source
|
|
|
|
|
|
def test_session_api_token_totals_load_independently_of_token_widget() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
start = source.index("async function _loadCurrentSessionUsage() {")
|
|
end = source.index(" function _relTime", start)
|
|
body = source[start:end]
|
|
|
|
assert "OPENSQUILLA_FEATURES?.tokenViz" not in body
|
|
assert "const usage = await _rpc.call('usage.status', { sessionKey: _sessionKey });" in body
|
|
assert "Turn — input:" in source
|
|
|
|
|
|
def test_chat_context_warning_uses_backend_context_status_not_lifetime_usage() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
start = source.index("function _updateCtxWarning()")
|
|
end = source.index(" /* ── Chat History", start)
|
|
body = source[start:end]
|
|
|
|
assert "const _CTX_WARN_THRESHOLD" not in source
|
|
assert "_totalTokens > _CTX_WARN_THRESHOLD" not in source
|
|
assert "Context > 85%" not in source
|
|
assert "_contextStatus" in body
|
|
assert "contextTokens" in body
|
|
assert "context_window_tokens" in body
|
|
assert "Request ctx" in body
|
|
|
|
|
|
def test_chat_usage_status_applies_current_session_context_status() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
start = source.index("async function _loadCurrentSessionUsage() {")
|
|
end = source.index(" function _relTime", start)
|
|
body = source[start:end]
|
|
|
|
assert "_applyContextStatus(current.contextStatus || current.context_status || null);" in body
|
|
assert "_clearContextStatus();" in body
|
|
|
|
|
|
def test_combo_display_requires_current_saved_turn_but_suppressed_savings_can_count() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
savings_source = SAVINGS_FX_JS.read_text(encoding="utf-8")
|
|
|
|
assert "u.__savings_ui_suppressed = true;" in source
|
|
assert "const savingsDetailSuppressed = !!u.__savings_ui_suppressed;" in source
|
|
assert "const hasSaved = !savingsDetailSuppressed && hasTier && turnSavedPct > 0;" in source
|
|
assert "const hasCombo = hasSaved && streak >= 2;" in source
|
|
assert source.index("const hasSaved =") < source.index(
|
|
"const hasCombo = hasSaved && streak >= 2;"
|
|
)
|
|
assert "if (suppressPopup) return;" in source
|
|
assert source.index("window.SavingsFX.noteTurn(u);") < source.index(
|
|
"if (suppressPopup) return;"
|
|
)
|
|
assert "let _streakIdentity = '';" in savings_source
|
|
assert "function _turnIdentity(u) {" in savings_source
|
|
assert "function _isComboTier(tier) {" in savings_source
|
|
assert "if (numeric) return Number(numeric[1]) < 3;" in savings_source
|
|
assert "_streak = (_streakIdentity === identity) ? _streak + 1 : 1;" in savings_source
|
|
assert "_streakIdentity = identity;" in savings_source
|
|
assert (
|
|
"if (hasTier && savePct > 0 && identity && _isComboTier(u.routed_tier))"
|
|
in savings_source
|
|
)
|
|
|
|
|
|
def test_savings_fx_only_vibrates_after_browser_user_activation() -> None:
|
|
savings_source = SAVINGS_FX_JS.read_text(encoding="utf-8")
|
|
|
|
assert "function _canVibrate()" in savings_source
|
|
assert "navigator.userActivation" in savings_source
|
|
assert "activation.hasBeenActive || activation.isActive" in savings_source
|
|
assert "if (_canVibrate()) {" in savings_source
|
|
|
|
|
|
def test_savings_fx_scores_prefer_comprehensive_totals() -> None:
|
|
source = SAVINGS_FX_JS.read_text(encoding="utf-8")
|
|
|
|
assert "const savingsUsd = (typeof u.total_savings_usd === 'number')" in source
|
|
assert (
|
|
"const rawPct = (typeof u.total_savings_pct === 'number' && u.total_savings_pct > 0)"
|
|
in source
|
|
)
|
|
|
|
|
|
def test_chat_streaming_bubble_has_polite_live_region() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
start = source.index("function _ensureStreamBubble()")
|
|
end = source.index("function ", start + 1)
|
|
body = source[start:end]
|
|
assert "_streamBubble.setAttribute('aria-live', 'polite');" in body
|
|
|
|
|
|
def test_chat_thread_does_not_duplicate_composer_bottom_clearance() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
css = CHAT_CSS.read_text(encoding="utf-8")
|
|
|
|
assert "padding-bottom: max(var(--composer-h" not in css
|
|
assert "padding-bottom: var(--composer-h" not in css
|
|
assert "document.documentElement.style.setProperty('--composer-h'" in source
|
|
|
|
|
|
def test_chat_input_bar_tightens_desktop_bottom_padding_but_keeps_mobile_safe_area() -> None:
|
|
css = CHAT_CSS.read_text(encoding="utf-8")
|
|
desktop_padding = "padding: var(--sp-2) var(--sp-4) var(--sp-1);"
|
|
mobile_safe_area = (
|
|
"padding-bottom: calc(var(--sp-2) + env(safe-area-inset-bottom, 0px));"
|
|
)
|
|
|
|
assert ".content:has(> .chat)" in css
|
|
assert "padding-bottom: 0;" in css
|
|
assert desktop_padding in css
|
|
assert mobile_safe_area in css
|
|
assert css.rfind(mobile_safe_area) > css.index(desktop_padding)
|
|
|
|
|
|
def test_chat_task_lifecycle_events_are_session_scoped() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
runtime = TASK_RUNTIME_PY.read_text(encoding="utf-8")
|
|
|
|
queued_start = source.index("_rpc.on('task.queued'")
|
|
queued_end = source.index("_rpc.on('task.running'", queued_start)
|
|
queued_body = source[queued_start:queued_end]
|
|
running_start = queued_end
|
|
running_end = source.index("_rpc.on('session.event.task_group.waiting'", running_start)
|
|
running_body = source[running_start:running_end]
|
|
terminal_start = source.index("const terminalStatus = _taskTerminalStatus(rawEvent);")
|
|
terminal_end = source.index(" const normalized =", terminal_start)
|
|
terminal_body = source[terminal_start:terminal_end]
|
|
|
|
assert "if (!_isCurrentSessionPayload(payload)) return;" in queued_body
|
|
assert "_currentRunStatus === 'running'" in queued_body
|
|
assert "_currentRunStatus === 'approval_pending'" in queued_body
|
|
assert "if (!_isCurrentSessionPayload(payload)) return;" in running_body
|
|
assert "if (!_isCurrentSessionPayload(rawPayload)) return;" in terminal_body
|
|
queued_emit_start = runtime.index('await self._emit(\n envelope.session_key,')
|
|
queued_emit_end = runtime.index(" return TaskHandle", queued_emit_start)
|
|
queued_emit = runtime[queued_emit_start:queued_emit_end]
|
|
running_emit_start = runtime.index('await self._emit(\n task.envelope.session_key,')
|
|
running_emit_end = runtime.index(
|
|
" await self._notify_task_lifecycle",
|
|
running_emit_start,
|
|
)
|
|
running_emit = runtime[running_emit_start:running_emit_end]
|
|
|
|
assert '"task.queued"' in queued_emit
|
|
assert '"session_key": envelope.session_key' in queued_emit
|
|
assert '"task.running"' in running_emit
|
|
assert '"session_key": task.envelope.session_key' in running_emit
|
|
assert '"session_key": task.envelope.session_key' in runtime
|
|
|
|
|
|
def test_chat_queue_drain_preserves_draft_typed_during_stream() -> None:
|
|
source = CHAT_JS.read_text(encoding="utf-8")
|
|
start = source.index("function _drainQueueHead()")
|
|
end = source.index(" function _popPendingTail", start)
|
|
body = source[start:end]
|
|
|
|
assert "const draftText = _textarea.value;" in body
|
|
assert "const draftAttachments = _pendingAttachments.map" in body
|
|
assert "const draftIntent = _pendingSessionIntent;" in body
|
|
assert "_onSend();" in body
|
|
assert "if (draftText.trim() || draftAttachments.length || draftIntent) {" in body
|
|
assert "_textarea.value = draftText;" in body
|
|
assert "_pendingAttachments = draftAttachments;" in body
|
|
assert "_pendingSessionIntent = draftIntent;" in body
|
|
assert body.index("_onSend();") < body.index("_textarea.value = draftText;")
|