File size: 13,053 Bytes
122c643
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
"""
Comprehensive integration tests for the ReadBookMom app.

Tests cover:
  - Module imports and structure
  - Story loading and text processing
  - HTML rendering functions (dashboard, library, voices, story text)
  - Gradio component wiring (inputs/outputs match handler signatures)
  - State machine transitions (book select β†’ play β†’ pause β†’ ask β†’ resume)
  - XSS escaping in all HTML renderers
  - Voice profile integration in player
  - TTS module chunk splitting
  - Q&A answer rendering safety
"""
import os
import sys
import html as html_module
import inspect

sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))

PASS = 0
FAIL = 0


def check(label, condition, detail=""):
    global PASS, FAIL
    if condition:
        PASS += 1
        print(f"  [OK] {label}")
    else:
        FAIL += 1
        print(f"  [FAIL] {label} -- {detail}")


# ── Module imports ──────────────────────────────────────────────

def test_module_imports():
    print("\n=== Module imports ===")
    try:
        from tts import split_into_chunks, generate_audio_stream
        check("tts module imports", True)
    except Exception as e:
        check("tts module imports", False, str(e))

    try:
        from voice_clone import (
            create_voice_profile, synthesize_cloned, synthesize_cloned_preview,
            synthesize_custom_voice, has_profile, get_model_mode,
            _trim_reference_audio, _select_dtype, _select_attn_impl,
            GENERATION_PARAMS, BASE_MODEL_ID, CUSTOM_VOICE_MODEL_ID,
        )
        check("voice_clone module imports", True)
    except Exception as e:
        check("voice_clone module imports", False, str(e))

    try:
        from inference import transcribe_audio, answer_story_question
        check("inference module imports", True)
    except Exception as e:
        check("inference module imports", False, str(e))


# ── Story loading ───────────────────────────────────────────────

def test_story_loading():
    print("\n=== Story loading ===")
    from app import load_books_from_stories, load_paragraphs

    books = load_books_from_stories()
    check("Stories found", len(books) > 0, f"found {len(books)}")

    for b in books:
        required = ["id", "title", "story_path", "cover_url", "voice_name", "percentage"]
        missing = [k for k in required if k not in b]
        check(f"Book '{b['title']}' has all keys", len(missing) == 0, f"missing: {missing}")

    # Test paragraph loading
    if books:
        paras = load_paragraphs(books[0]["story_path"])
        check(f"Paragraphs loaded from '{books[0]['title']}'", len(paras) > 0, f"got {len(paras)}")

    # Test non-existent file
    paras = load_paragraphs("nonexistent_file.txt")
    check("Non-existent file returns empty list", paras == [])


# ── TTS chunk splitting ────────────────────────────────────────

def test_chunk_splitting():
    print("\n=== TTS chunk splitting ===")
    from tts import split_into_chunks

    chunks = split_into_chunks("Hello world. How are you? I am fine!")
    check("Splits on sentence boundaries", len(chunks) == 3, f"got {len(chunks)}: {chunks}")

    chunks = split_into_chunks("Single sentence without ending period")
    check("Single sentence stays intact", len(chunks) == 1, f"got {len(chunks)}")

    chunks = split_into_chunks("")
    check("Empty text returns empty list", len(chunks) == 0)

    chunks = split_into_chunks("   ")
    check("Whitespace-only returns empty list", len(chunks) == 0)

    long_text = "First sentence. Second sentence! Third? Fourth; Fifth."
    chunks = split_into_chunks(long_text)
    check("Splits on .!?; delimiters", len(chunks) == 5, f"got {len(chunks)}: {chunks}")


# ── HTML rendering ──────────────────────────────────────────────

def test_html_rendering():
    print("\n=== HTML rendering ===")
    from app import (
        generate_dashboard_html, generate_library_html,
        render_cloned_voices_html, render_story_text, mock_books, mock_voices,
    )

    # Dashboard
    dash_html = generate_dashboard_html(mock_books)
    check("Dashboard renders without error", isinstance(dash_html, str) and len(dash_html) > 100)
    check("Dashboard contains book titles", any(b["title"] in dash_html for b in mock_books))

    # Library
    lib_html = generate_library_html(mock_books)
    check("Library renders without error", isinstance(lib_html, str) and len(lib_html) > 100)
    check("Library contains Tap to Play", "Tap to Play" in lib_html)

    # Library with search filter
    if mock_books:
        first_title = mock_books[0]["title"]
        filtered = generate_library_html(mock_books, query=first_title)
        check(f"Library filters to '{first_title}'", first_title in filtered)

    # Library empty result
    empty = generate_library_html(mock_books, query="xyznonexistent999")
    check("Library shows no-match message", "No audiobooks match" in empty)

    # Cloned voices
    voices_html = render_cloned_voices_html(mock_voices)
    check("Voices grid renders", isinstance(voices_html, str) and len(voices_html) > 50)

    # Story text
    paras = ["Paragraph one.", "Paragraph two.", "Paragraph three."]
    story_html = render_story_text(paras, 1)
    check("Story text highlights current paragraph", "border-left: 3px solid #f5841f" in story_html)
    check("Story text renders all paragraphs", "Paragraph one" in story_html and "Paragraph three" in story_html)

    # Empty paragraphs
    check("Empty paragraphs returns empty string", render_story_text([], 0) == "")


# ── XSS escaping ───────────────────────────────────────────────

def test_xss_escaping():
    print("\n=== XSS escaping ===")
    from app import render_story_text, render_cloned_voices_html

    # Story text with HTML injection
    xss_paras = ['<script>alert("xss")</script>', 'Normal text']
    result = render_story_text(xss_paras, 0)
    check("Story text escapes <script>", "<script>" not in result)
    check("Story text contains escaped version", "&lt;script&gt;" in result)

    # Voices with HTML injection
    xss_voices = [{
        "id": "v1", "name": '<img onerror="alert(1)" src=x>',
        "avatar_url": "πŸ‘©", "description": "test", "gender": "Female",
        "created_date": "2026-01-01", "status": "ready"
    }]
    result = render_cloned_voices_html(xss_voices)
    check("Voice name escapes HTML", "<img onerror" not in result)
    check("Voice name contains escaped version", "&lt;img" in result)


# ── Gradio component wiring ────────────────────────────────────

def test_gradio_wiring():
    print("\n=== Gradio component wiring ===")
    from app import demo

    # Verify the demo has the expected blocks
    blocks = demo.blocks
    check("Demo has blocks", len(blocks) > 0, f"found {len(blocks)}")

    # Check that no component references a deleted 'loading_spinner'
    source = open("app.py", encoding="utf-8").read()
    check("No loading_spinner references", "loading_spinner" not in source)
    check("No empty_state_sim references", "empty_state_sim" not in source)
    check("No offline_toggle references", "offline_toggle" not in source)
    check("No system_error_modal references", "system_error_modal" not in source)
    check("No narrative_subtitles references", "narrative_subtitles" not in source)


# ── Handler signatures vs outputs ──────────────────────────────

def test_handler_signatures():
    print("\n=== Handler signatures ===")
    source = open("app.py", encoding="utf-8").read()

    # Verify key handler functions exist
    handlers = [
        "def filter_books_shelf",
        "def handle_book_select",
        "def animate_cloning_pipeline",
        "def save_new_cloned_voice",
        "def stream_tts",
        "def enter_paused_state",
        "def update_story_highlight",
        "def enter_asking_state",
        "def handle_question_submit",
        "def enter_resume_state",
    ]
    for h in handlers:
        check(f"Handler {h.split('def ')[1]} exists", h in source)

    # Verify removed handlers are gone
    check("check_story_finished removed (inline in stream_tts)", "def check_story_finished" not in source)
    check("toggle_outage_sandbox removed", "def toggle_outage_sandbox" not in source)


# ── Voice profile integration ──────────────────────────────────

def test_voice_profile_integration():
    print("\n=== Voice profile integration ===")
    from voice_clone import has_profile

    check("has_profile(None) is False", has_profile(None) is False)
    check("has_profile('') is False", has_profile("") is False)
    check("has_profile('nonexistent') is False", has_profile("nonexistent") is False)

    # Verify handle_book_select accepts profile_id
    source = open("app.py", encoding="utf-8").read()
    check("handle_book_select takes profile_id",
          "def handle_book_select(title_chosen, current_inventory, profile_id)" in source)
    check("book_selector inputs include voice_profile_state",
          "inputs=[book_selector, books_state, voice_profile_state]" in source)


# ── State machine consistency ──────────────────────────────────

def test_state_machine():
    print("\n=== State machine consistency ===")
    source = open("app.py", encoding="utf-8").read()

    # Play β†’ shows pause_btn, hides play_btn
    check("stream_tts hides play_btn during playback",
          "gr.Button(visible=False), gr.Button(visible=True), gr.Button(visible=True)" in source)

    # Pause β†’ shows play_btn, hides pause_btn
    check("enter_paused_state shows play_btn",
          "gr.Button(visible=True),\n            gr.Button(visible=False)," in source or
          ("enter_paused_state" in source and "gr.Button(visible=True)" in source))

    # Resume β†’ shows play_btn (ready state), hides resume_btn
    check("enter_resume_state shows ready state",
          "READY" in source and "Tap Play to continue" in source)

    # Ask β†’ shows resume_btn, hides play/pause/ask
    check("enter_asking_state shows resume_btn",
          "PAUSED" in source and "ASKING" in source)

    # stream_tts updates timeline_slider
    check("stream_tts outputs include timeline_slider",
          "timeline_slider, story_text_display, story_finished_panel" in source)


# ── App launch readiness ───────────────────────────────────────

def test_app_launch():
    print("\n=== App launch readiness ===")
    from app import demo

    check("Demo object exists", demo is not None)
    check("Demo is a gr.Blocks instance", "Blocks" in type(demo).__name__)

    # Check allowed_paths in launch call
    source = open("app.py", encoding="utf-8").read()
    check("Launch has allowed_paths for assets", 'allowed_paths' in source)
    check("Launch uses .queue()", '.queue()' in source)


# ── File structure ──────────────────────────────────────────────

def test_file_structure():
    print("\n=== File structure ===")
    required_files = [
        "app.py", "tts.py", "voice_clone.py", "inference.py",
        "requirements.txt", "static/style.css",
    ]
    for f in required_files:
        path = os.path.join(os.path.dirname(os.path.dirname(__file__)), f)
        check(f"File exists: {f}", os.path.exists(path))

    # Check stories directory has content
    stories_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), "stories")
    if os.path.isdir(stories_dir):
        txt_files = [f for f in os.listdir(stories_dir) if f.endswith(".txt")]
        check("Stories directory has .txt files", len(txt_files) > 0, f"found {len(txt_files)}")
    else:
        check("Stories directory exists", False, "stories/ not found")


if __name__ == "__main__":
    print("=" * 60)
    print("Comprehensive Integration Tests β€” ReadBookMom")
    print("=" * 60)

    test_module_imports()
    test_story_loading()
    test_chunk_splitting()
    test_html_rendering()
    test_xss_escaping()
    test_gradio_wiring()
    test_handler_signatures()
    test_voice_profile_integration()
    test_state_machine()
    test_app_launch()
    test_file_structure()

    print("\n" + "=" * 60)
    print(f"Results: {PASS} passed, {FAIL} failed")
    print("=" * 60)
    sys.exit(1 if FAIL > 0 else 0)