Spaces:
Sleeping
Sleeping
| import pytest | |
| pytest.importorskip("gradio") | |
| from backend.modal_api import ParseResult, TranscriptionResult | |
| from voiceledger.ui import gradio_app | |
| def test_create_app_builds_blocks(tmp_path) -> None: | |
| app = gradio_app.create_app(tmp_path / "voiceledger.sqlite3") | |
| try: | |
| assert app is not None | |
| finally: | |
| app.close() | |
| def test_transcribe_and_parse_audio_handles_transcription_error(monkeypatch) -> None: | |
| def fail_transcription(_: object) -> str: | |
| raise RuntimeError("test failure") | |
| monkeypatch.setattr(gradio_app.modal_api, "transcribe_audio_result", lambda audio, fallback, **kwargs: fail_transcription(audio)) | |
| transcript, structured, state, status, review_card = gradio_app._transcribe_and_parse_audio("audio.wav") | |
| assert transcript == "" | |
| assert structured["transaction_type"] == "unknown" | |
| assert state is None | |
| assert status == "Transcription failed: test failure" | |
| assert "Transaction review" in review_card | |
| def test_transcribe_and_parse_audio_parses_transcript(monkeypatch) -> None: | |
| monkeypatch.setattr( | |
| gradio_app.modal_api, | |
| "transcribe_audio_result", | |
| lambda audio, fallback, **kwargs: TranscriptionResult( | |
| transcript="Sold 12 mangoes, 20 each", | |
| source="modal", | |
| message="Transcribed by Modal faster-whisper endpoint.", | |
| ), | |
| ) | |
| monkeypatch.setattr( | |
| gradio_app.modal_api, | |
| "parse_transaction_result", | |
| lambda text, fallback, **kwargs: ParseResult( | |
| transaction=fallback(text), | |
| source="local", | |
| message="Parsed locally with the rule parser after Modal failed.", | |
| fallback_reason="test fallback", | |
| ), | |
| ) | |
| transcript, structured, state, status, review_card = gradio_app._transcribe_and_parse_audio("audio.wav") | |
| assert transcript == "Sold 12 mangoes, 20 each" | |
| assert structured["transaction_type"] == "sale" | |
| assert structured["amount"] == 240 | |
| assert state == structured | |
| assert "sale" in status | |
| assert "Modal faster-whisper" in status | |
| assert "test fallback" in status | |
| assert "Review transaction" in review_card | |
| def test_parse_note_surfaces_modal_source(monkeypatch) -> None: | |
| monkeypatch.setattr( | |
| gradio_app.modal_api, | |
| "parse_transaction_result", | |
| lambda text, fallback, **kwargs: ParseResult( | |
| transaction=fallback(text), | |
| source="modal", | |
| message="Parsed by Modal using NVIDIA Nemotron.", | |
| ), | |
| ) | |
| structured, state, status, review_card = gradio_app._parse_note("Paid 500 for supplies") | |
| assert structured["transaction_type"] == "expense" | |
| assert state == structured | |
| assert "NVIDIA Nemotron" in status | |
| assert "Safe to save" in review_card | |
| def test_parse_note_local_mode_surfaces_local_fallback(monkeypatch) -> None: | |
| captured = {} | |
| def fake_parse(text, fallback, **kwargs): | |
| captured.update(kwargs) | |
| return ParseResult( | |
| transaction=fallback(text), | |
| source="local", | |
| message="Parsed locally with the rule parser.", | |
| fallback_reason="Cloud AI is disabled for local-first mode.", | |
| ) | |
| monkeypatch.setattr(gradio_app.modal_api, "parse_transaction_result", fake_parse) | |
| structured, state, status, review_card = gradio_app._parse_note("Amit owes 100", ai_mode="Local fallback only") | |
| assert captured["force_local"] is True | |
| assert structured["transaction_type"] == "customer_credit" | |
| assert state == structured | |
| assert "Local fallback" in status | |
| assert "local-first mode" in status | |
| assert "Safe to save" in review_card | |
| def test_parse_note_surfaces_language_confidence_chip(monkeypatch) -> None: | |
| monkeypatch.setattr( | |
| gradio_app.modal_api, | |
| "parse_transaction_result", | |
| lambda text, fallback, **kwargs: ParseResult( | |
| transaction=fallback(text), | |
| source="local", | |
| message="Parsed locally with the rule parser.", | |
| ), | |
| ) | |
| _, _, status, _ = gradio_app._parse_note("Vendí 12 mangos, 20 cada uno") | |
| assert "Spanish" in status | |
| assert "High confidence" in status | |
| def test_high_contrast_demo_panels_are_rendered() -> None: | |
| panel = gradio_app._info_panel( | |
| "Demo Health", | |
| "The Space calls Modal first for speech and NVIDIA Nemotron parsing, with local fallback for reliability.", | |
| ) | |
| assert "vl-info-panel" in panel | |
| assert "Demo Health" in panel | |
| assert "NVIDIA Nemotron" in panel | |
| def test_judge_demo_panel_surfaces_submission_flow() -> None: | |
| panel = gradio_app._judge_demo_panel() | |
| assert "Judge Demo Flow" in panel | |
| assert "1. Seed demo data" in panel | |
| assert "2. Record/type" in panel | |
| assert "3. Save" in panel | |
| assert "4. View dashboard/reports" in panel | |
| assert "NVIDIA Nemotron parser" in panel | |
| def test_submission_story_surfaces_pipeline_and_small_model_fit() -> None: | |
| story = gradio_app._submission_story_panel() | |
| pipeline = gradio_app._ai_pipeline_strip() | |
| small_model = gradio_app._small_model_fit_card() | |
| assert "Built for a real informal seller" in story | |
| assert "Small Model Intelligence" in pipeline | |
| assert "Nemotron 4B" in pipeline | |
| assert "Fallback" in pipeline | |
| assert "Database" in pipeline | |
| assert "Why Small Models Win Here" in small_model | |
| assert "Speed & Latency" in small_model | |
| assert "Localization" in small_model | |
| def test_demo_health_placeholder_includes_nemotron_status() -> None: | |
| placeholder = gradio_app._demo_health_placeholder() | |
| assert "NVIDIA Nemotron parser" in set(placeholder["check"]) | |
| def test_show_page_makes_exactly_one_section_visible() -> None: | |
| updates = gradio_app._show_page("ledger") | |
| assert len(updates) == 10 | |
| assert updates[-1]["visible"] is True | |
| assert sum(1 for update in updates if update["visible"]) == 1 | |
| def test_review_warnings_flag_missing_and_low_confidence() -> None: | |
| transaction = gradio_app.Transaction(transaction_type="unknown", confidence=0.2) | |
| warnings = gradio_app._review_warnings(transaction, None) | |
| assert "Unknown type" in warnings | |
| assert "Low confidence" in warnings | |
| def test_review_card_surfaces_needs_review_for_missing_fields() -> None: | |
| transaction = gradio_app.Transaction(transaction_type="customer_credit", confidence=0.8) | |
| review_card = gradio_app._review_card(transaction, "Parsed locally.", gradio_app._review_warnings(transaction, None)) | |
| assert "Needs review" in review_card | |
| assert "Missing amount" in review_card | |
| assert "Missing customer" in review_card | |
| def test_parse_note_for_editing_populates_review_fields(monkeypatch) -> None: | |
| monkeypatch.setattr( | |
| gradio_app.modal_api, | |
| "parse_transaction_result", | |
| lambda text, fallback, **kwargs: ParseResult( | |
| transaction=fallback(text), | |
| source="local", | |
| message="Parsed locally with the rule parser.", | |
| ), | |
| ) | |
| result = gradio_app._parse_note_for_editing("Sold 12 mangoes, 20 each") | |
| assert result[0]["transaction_type"] == "sale" | |
| assert result[4] == "sale" | |
| assert result[5] == "mangoes" | |
| assert result[6] == 12 | |
| assert result[8] == 240 | |
| def test_price_memory_fills_missing_sale_price(tmp_path, monkeypatch) -> None: | |
| db_path = tmp_path / "voiceledger.sqlite3" | |
| gradio_app.add_transaction(gradio_app.local_parse_transaction("Sold 12 mangoes, 20 each"), db_path) | |
| monkeypatch.setattr( | |
| gradio_app.modal_api, | |
| "parse_transaction_result", | |
| lambda text, fallback, **kwargs: ParseResult( | |
| transaction=fallback(text), | |
| source="local", | |
| message="Parsed locally with the rule parser.", | |
| ), | |
| ) | |
| result = gradio_app._parse_note_for_editing("Sold 5 mangoes", db_path) | |
| assert result[0]["unit_price"] == 20 | |
| assert result[0]["amount"] == 100 | |
| assert result[7] == 20 | |
| assert result[8] == 100 | |
| assert "Price memory used" in result[3] | |
| def test_price_memory_does_not_overwrite_explicit_price(tmp_path) -> None: | |
| db_path = tmp_path / "voiceledger.sqlite3" | |
| gradio_app.add_transaction(gradio_app.local_parse_transaction("Sold 12 mangoes, 20 each"), db_path) | |
| transaction = gradio_app.local_parse_transaction("Sold 5 mangoes, 30 each") | |
| updated, used = gradio_app.suggest_price_from_history(transaction, db_path) | |
| assert used is False | |
| assert updated.unit_price == 30 | |
| assert updated.amount == 150 | |
| def test_apply_review_edits_updates_payload_and_review_card(tmp_path) -> None: | |
| db_path = tmp_path / "voiceledger.sqlite3" | |
| parsed = gradio_app.local_parse_transaction("Sold 12 mangoes, 20 each").model_dump() | |
| payload, state, status, review_card = gradio_app._apply_review_edits( | |
| parsed, | |
| "sale", | |
| "mangoes", | |
| 10, | |
| 25, | |
| 250, | |
| None, | |
| "paid", | |
| "Sold 10 mangoes, 25 each", | |
| 0.95, | |
| db_path, | |
| ) | |
| assert payload["quantity"] == 10 | |
| assert payload["amount"] == 250 | |
| assert state == payload | |
| assert "Review updated" in status | |
| assert "250" in review_card | |
| correction_log = gradio_app.get_correction_log(db_path) | |
| assert len(correction_log) == 1 | |
| assert "quantity" in correction_log.iloc[0]["changed_fields"] | |
| def test_receipt_card_summarizes_saved_sale(tmp_path) -> None: | |
| transaction = gradio_app.local_parse_transaction("Sold 12 mangoes, 20 each") | |
| receipt = gradio_app._receipt_card(transaction, 4, tmp_path / "voiceledger.sqlite3") | |
| assert "Saved just now" in receipt | |
| assert "Transaction #4" in receipt | |
| assert "Stock reduced" in receipt | |
| def test_daily_closeout_returns_exports(tmp_path) -> None: | |
| db_path = tmp_path / "voiceledger.sqlite3" | |
| gradio_app.add_transaction(gradio_app.local_parse_transaction("Sold 12 mangoes, 20 each"), db_path) | |
| summary, pdf_path, csv_path, whatsapp, status = gradio_app._run_daily_closeout(db_path) | |
| assert "Daily Closeout Ready" in summary | |
| assert pdf_path is not None | |
| assert csv_path is not None | |
| assert "VoiceLedger Seller Daily Summary" in whatsapp | |
| assert "Daily closeout complete" in status | |
| def test_command_center_and_seller_setup_use_settings(tmp_path) -> None: | |
| db_path = tmp_path / "voiceledger.sqlite3" | |
| gradio_app.update_business_settings( | |
| business_name="Mango Cart", | |
| currency_symbol="Rs ", | |
| low_stock_threshold=3, | |
| language_style="English + Hinglish", | |
| db_path=db_path, | |
| ) | |
| gradio_app.add_transaction(gradio_app.local_parse_transaction("Sold 12 mangoes, 20 each"), db_path) | |
| command_center = gradio_app._command_center(db_path) | |
| assert "Mango Cart Command Center" in command_center | |
| assert "Rs240" in command_center | |
| def test_customer_followup_and_reorder_helpers(tmp_path) -> None: | |
| db_path = tmp_path / "voiceledger.sqlite3" | |
| gradio_app.add_transaction(gradio_app.local_parse_transaction("Amit owes 100"), db_path) | |
| gradio_app.add_transaction(gradio_app.local_parse_transaction("Amit paid 40"), db_path) | |
| gradio_app.add_transaction(gradio_app.local_parse_transaction("Bought 3 onions"), db_path) | |
| followup = gradio_app.generate_customer_followup("Amit", db_path) | |
| reorder, message = gradio_app.generate_reorder_list(db_path) | |
| assert "Amit" in followup | |
| assert "₹60" in followup | |
| assert "onions" in set(reorder["item"]) | |
| assert "Onions" in message | |
| def test_debt_reminder_queue_sorts_positive_balances(tmp_path) -> None: | |
| db_path = tmp_path / "voiceledger.sqlite3" | |
| gradio_app.add_transaction(gradio_app.local_parse_transaction("Amit owes 100"), db_path) | |
| gradio_app.add_transaction(gradio_app.local_parse_transaction("Ramesh owes 250"), db_path) | |
| gradio_app.add_transaction(gradio_app.local_parse_transaction("Amit paid 40"), db_path) | |
| queue = gradio_app.get_debt_reminder_queue(db_path) | |
| reminder = gradio_app.generate_debt_reminder("Ramesh", db_path) | |
| assert list(queue["customer"]) == ["Ramesh", "Amit"] | |
| assert "₹250" in queue.iloc[0]["suggested_message"] | |
| assert "Ramesh" in reminder | |
| def test_reorder_intelligence_flags_low_and_fast_stock(tmp_path) -> None: | |
| db_path = tmp_path / "voiceledger.sqlite3" | |
| gradio_app.update_business_settings(low_stock_threshold=5, db_path=db_path) | |
| gradio_app.add_transaction(gradio_app.local_parse_transaction("Bought 3 onions"), db_path) | |
| gradio_app.add_transaction(gradio_app.local_parse_transaction("Bought 20 mangoes"), db_path) | |
| gradio_app.add_transaction(gradio_app.local_parse_transaction("Sold 12 mangoes, 20 each"), db_path) | |
| recommendations = gradio_app.generate_reorder_intelligence(db_path) | |
| table, message = gradio_app._generate_reorder_intelligence_for_ui(db_path) | |
| statuses = dict(zip(recommendations["item"], recommendations["status"], strict=True)) | |
| assert statuses["onions"] == "Low stock" | |
| assert statuses["mangoes"] == "Selling fast" | |
| assert "recent_sold" in table.columns | |
| assert "Mangoes" in message | |
| def test_currency_presets_and_voice_commands(tmp_path) -> None: | |
| db_path = tmp_path / "voiceledger.sqlite3" | |
| gradio_app.add_transaction(gradio_app.local_parse_transaction("Amit owes 100"), db_path) | |
| gradio_app.add_transaction(gradio_app.local_parse_transaction("Bought 3 onions"), db_path) | |
| assert gradio_app._currency_symbol_for_preset("Brazil - BRL (R$)") == "R$" | |
| assert gradio_app._currency_preset_for_symbol("€") == "European Union - EUR (€)" | |
| assert "Amit" in gradio_app._run_voice_command("show Amit", db_path) | |
| assert "onions" in gradio_app._run_voice_command("stock onions", db_path) | |
| assert "Daily Closeout Ready" in gradio_app._run_voice_command("close today", db_path) | |
| def test_insight_coach_surfaces_credit_and_stock_actions(tmp_path) -> None: | |
| db_path = tmp_path / "voiceledger.sqlite3" | |
| gradio_app.add_transaction(gradio_app.local_parse_transaction("Amit owes 100"), db_path) | |
| gradio_app.add_transaction(gradio_app.local_parse_transaction("Bought 3 onions"), db_path) | |
| coach = gradio_app._insight_coach(db_path) | |
| assert "Insight Coach" in coach | |
| assert "Follow up with Amit" in coach | |
| assert "Restock onions" in coach | |
| def test_field_test_evidence_persists_checklist_and_notes(tmp_path) -> None: | |
| db_path = tmp_path / "voiceledger.sqlite3" | |
| status = gradio_app._save_field_test_evidence( | |
| ["Record sale", "Export report"], | |
| "Local snack seller", | |
| "Voice sale and PDF export", | |
| "Moved corrections before save", | |
| db_path, | |
| ) | |
| settings = gradio_app.get_business_settings(db_path) | |
| assert "2 checklist item" in status | |
| assert settings["field_test_who"] == "Local snack seller" | |
| assert gradio_app._field_test_checklist_values(settings) == ["Record sale", "Export report"] | |
| def test_field_test_evidence_persists_real_user_details(tmp_path) -> None: | |
| db_path = tmp_path / "voiceledger.sqlite3" | |
| status = gradio_app._save_field_test_evidence( | |
| ["Record sale"], | |
| "Local seller", | |
| "Voice note", | |
| "Old change note", | |
| db_path, | |
| pain_point="Paper notes get lost", | |
| before="Used memory", | |
| after="Checks dashboard", | |
| useful="Debt reminders", | |
| changed_after_feedback="Added price memory", | |
| ) | |
| settings = gradio_app.get_business_settings(db_path) | |
| assert "Paper notes get lost" in status | |
| assert settings["field_pain_point"] == "Paper notes get lost" | |
| assert settings["field_before"] == "Used memory" | |
| assert settings["field_after"] == "Checks dashboard" | |
| assert settings["field_useful_moments"] == "Debt reminders" | |
| assert settings["field_changed_after_feedback"] == "Added price memory" | |
| def test_guided_demo_status_reflects_progress(tmp_path) -> None: | |
| db_path = tmp_path / "voiceledger.sqlite3" | |
| empty_status = gradio_app.get_guided_demo_status(db_path) | |
| gradio_app.add_transaction(gradio_app.local_parse_transaction("Sold 12 mangoes, 20 each"), db_path) | |
| progressed_status = gradio_app.get_guided_demo_status(db_path) | |
| assert "Guided Judge Mode" in empty_status | |
| assert "Seed Demo Data" in progressed_status | |
| assert "Done" in progressed_status | |