File size: 7,111 Bytes
79df050
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
def test_typewriter_avoids_interval_and_full_markdown_rerender_per_tick():
    source = open("static/frontend/js/ui.js", encoding="utf-8").read()
    start = source.index("function typewriterEffect")
    block = source[start:source.index("const agentProgressSteps")]

    assert "setInterval(" not in block
    assert "requestAnimationFrame(" in block
    assert "renderAgentMarkdown(text.slice(0, index))" not in block
    assert "typewriterTextNode" in source
    assert "typewriterFrameId" in source
    assert "setTypewriterContent" in source


def test_streaming_output_skips_redundant_full_repaint_when_text_unchanged():
    source = open("static/frontend/js/ui.js", encoding="utf-8").read()

    assert "let lastRenderedAgentText = '';" in source
    assert "if (lastRenderedAgentText === nextText)" in source
    assert "lastRenderedAgentText = nextText;" in source


def test_final_typewriter_waits_for_speech_completion_before_idle():
    ui_source = open("static/frontend/js/ui.js", encoding="utf-8").read()
    tts_source = open("static/frontend/js/tts.js", encoding="utf-8").read()

    assert "const awaitSpeechCompletion = options.awaitSpeechCompletion === true || !!enableTTS;" in ui_source
    assert "window.agentOutputAwaitingSpeechCompletion = awaitSpeechCompletion;" in ui_source
    assert "if (window.agentOutputAwaitingSpeechCompletion) {" in ui_source
    assert "function maybeFinalizeSpeechPlayback()" in tts_source
    assert "window.agentOutputAwaitingSpeechCompletion = false;" in tts_source
    assert "maybeFinalizeSpeechPlayback();" in tts_source


def test_progress_updates_do_not_override_active_answer_stream():
    ui_source = open("static/frontend/js/ui.js", encoding="utf-8").read()
    ws_source = open("static/frontend/js/websocket.js", encoding="utf-8").read()

    assert "window.agentOutputHasAnswerStream = false;" in ui_source
    assert "if (options.progress === true && window.agentOutputHasAnswerStream) {" in ui_source
    assert "window.agentOutputHasAnswerStream = true;" in ui_source
    assert "if (typeof currentState !== 'undefined' && currentState === 'speaking') {" in ws_source
    assert "setState('speaking');" in ws_source


def test_stt_idle_does_not_reset_agent_while_speaking():
    ws_source = open("static/frontend/js/websocket.js", encoding="utf-8").read()

    assert "else if (data.status === 'idle' || data.status === 'error') {" in ws_source
    assert "if (typeof currentState !== 'undefined' && currentState === 'speaking') {" in ws_source
    assert "resetAgent();" in ws_source


def test_final_output_can_wait_for_streaming_speech_completion_without_full_tts():
    ui_source = open("static/frontend/js/ui.js", encoding="utf-8").read()
    tts_source = open("static/frontend/js/tts.js", encoding="utf-8").read()
    ws_source = open("static/frontend/js/websocket.js", encoding="utf-8").read()

    assert "const awaitSpeechCompletion = options.awaitSpeechCompletion === true || !!enableTTS;" in ui_source
    assert "function hasPendingStreamingSpeech()" in tts_source
    assert "|| !!ttsStreamSocket || _ttsActiveSources.length > 0" in tts_source
    assert "hasPendingStreamingSpeech() || useFullTtsFallback" in ws_source
    assert "finishAgentOutput(data.message, useFullTtsFallback, {" in ws_source


def test_tts_debug_logging_and_stop_reason_contracts_exist():
    tts_source = open("static/frontend/js/tts.js", encoding="utf-8").read()
    agent_source = open("static/frontend/js/agent.js", encoding="utf-8").read()

    assert "function logTtsDebug(event, extra = {})" in tts_source
    assert "if (!window.DEBUG_MODE) {" in tts_source
    assert "logTtsDebug('socket_close'" in tts_source
    assert "function stopSpeaking(clearPending = true, reason = 'unspecified')" in tts_source
    assert "stopSpeaking(true, 'state_idle');" in agent_source
    assert "stopSpeaking(true, 'reset_agent');" in agent_source


def test_complete_typewriter_refuses_idle_while_streaming_speech_still_pending():
    ui_source = open("static/frontend/js/ui.js", encoding="utf-8").read()

    assert "if (typeof hasPendingStreamingSpeech === 'function' && hasPendingStreamingSpeech()) {" in ui_source
    assert "window.agentOutputAwaitingSpeechCompletion = true;" in ui_source


def test_tool_cards_container_is_an_interactive_fixed_overlay():
    source = open("static/frontend/index.html", encoding="utf-8").read()

    assert "#tool-cards-container {" in source
    assert "position: fixed;" in source
    assert "pointer-events: none;" in source
    assert "#tool-cards-container .voice-tool-card {" in source
    assert "pointer-events: auto;" in source


def test_tool_card_visual_dividers_and_scrollbar_are_refined():
    source = open("static/frontend/index.html", encoding="utf-8").read()

    assert "border: 1px solid rgba(15, 23, 42, 0.06);" in source
    assert "border-bottom: 1px solid rgba(15, 23, 42, 0.05);" in source
    assert ".tool-drawer-content::-webkit-scrollbar {" in source
    assert "width: 3px;" in source
    assert "background: rgba(15, 23, 42, 0.12);" in source


def test_speaking_petals_reuse_idle_rotation_and_layer_interleave():
    source = open("static/frontend/index.html", encoding="utf-8").read()

    assert ".voice-mic-container.speaking .bloom-petal-group.upper .bloom-petal:nth-child(1)" in source
    assert "animation: petalBloom 1.6s ease-in-out infinite 0s;" in source
    assert ".voice-mic-container.speaking .bloom-petal-group.lower .bloom-petal:nth-child(1)" in source
    assert "animation: petalBloomInner 1.6s ease-in-out infinite 0.1s;" in source
    assert "--speaking-angle" not in source
    assert "speakingPetalFloat" not in source
    assert "speakingPetalFloatInner" not in source


def test_websocket_updates_conversation_language_from_bot_messages():
    ws_source = open("static/frontend/js/websocket.js", encoding="utf-8").read()

    assert "if (data.language) {" in ws_source
    assert "window.currentConversationLanguage = data.language;" in ws_source
    assert "window.currentSpeechLanguage = data.language;" in ws_source


def test_new_thinking_cycle_clears_stale_tts_language_before_next_stream():
    ws_source = open("static/frontend/js/websocket.js", encoding="utf-8").read()

    assert "window.currentConversationLanguage = null;" in ws_source
    assert "window.currentSpeechLanguage = null;" in ws_source


def test_bot_delta_carries_language_for_streaming_tts_switch():
    source = open("app.py", encoding="utf-8").read()

    assert "\"language\": _preferred_language_from_text(stream_accumulator[\"text\"]) or resolved_language" in source


def test_frontend_preloads_conversation_language_on_initial_input():
    ws_source = open("static/frontend/js/websocket.js", encoding="utf-8").read()

    assert "function inferConversationLanguage(text, fallback = null)" in ws_source
    assert "const inferredLanguage = window.inferConversationLanguage(text, navigator.language || 'zh-TW');" in ws_source
    assert "window.currentConversationLanguage = inferredLanguage;" in ws_source
    assert "window.currentSpeechLanguage = inferredLanguage;" in ws_source