Claude commited on
Commit
29b2eb2
·
unverified ·
1 Parent(s): 7da7ce7

test: Add integration tests for chatbot RAG functionality

Browse files

New test classes:
- TestSessionState: Session creation, isolation, clear functionality
- TestRAGPipeline: Vector DB add/search, session isolation, multi-video search
- TestChatWithVideosIntegration: Context retrieval, model info, error handling
- TestHandleChat: URL detection, question handling, auth requirements
- TestGetKnowledgeStatsWithSession: Stats with session state

Total: 56 tests (20 new integration tests)

Files changed (1) hide show
  1. tests/test_app.py +454 -3
tests/test_app.py CHANGED
@@ -390,7 +390,7 @@ class TestProcessYoutube:
390
  from app import process_youtube
391
 
392
  mock_progress = MagicMock()
393
- result = process_youtube("https://youtube.com/watch?v=test", 5, None, mock_progress)
394
  assert "log in" in result.lower()
395
 
396
  def test_empty_url_returns_prompt(self):
@@ -398,7 +398,7 @@ class TestProcessYoutube:
398
  from app import process_youtube
399
 
400
  mock_progress = MagicMock()
401
- result = process_youtube("", 5, MagicMock(), mock_progress)
402
  assert "enter" in result.lower()
403
 
404
  def test_invalid_url_returns_error(self):
@@ -406,5 +406,456 @@ class TestProcessYoutube:
406
  from app import process_youtube
407
 
408
  mock_progress = MagicMock()
409
- result = process_youtube("not-a-url", 5, MagicMock(), mock_progress)
410
  assert "valid youtube url" in result.lower()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
390
  from app import process_youtube
391
 
392
  mock_progress = MagicMock()
393
+ result = process_youtube("https://youtube.com/watch?v=test", 5, None, None, mock_progress)
394
  assert "log in" in result.lower()
395
 
396
  def test_empty_url_returns_prompt(self):
 
398
  from app import process_youtube
399
 
400
  mock_progress = MagicMock()
401
+ result = process_youtube("", 5, MagicMock(), None, mock_progress)
402
  assert "enter" in result.lower()
403
 
404
  def test_invalid_url_returns_error(self):
 
406
  from app import process_youtube
407
 
408
  mock_progress = MagicMock()
409
+ result = process_youtube("not-a-url", 5, MagicMock(), None, mock_progress)
410
  assert "valid youtube url" in result.lower()
411
+
412
+
413
+ class TestSessionState:
414
+ """Tests for the SessionState class."""
415
+
416
+ def test_creates_collection_with_session_id(self):
417
+ """Test SessionState creates a collection with session ID."""
418
+ from app import SessionState
419
+
420
+ state = SessionState("test_session_123")
421
+ assert state.session_id == "test_session_123"
422
+ assert state.collection is not None
423
+
424
+ def test_auto_generates_session_id(self):
425
+ """Test SessionState generates session ID if not provided."""
426
+ from app import SessionState
427
+
428
+ state = SessionState()
429
+ assert state.session_id is not None
430
+ assert len(state.session_id) == 32 # UUID hex length
431
+
432
+ def test_clear_recreates_collection(self):
433
+ """Test clear() recreates the collection."""
434
+ from app import SessionState
435
+
436
+ state = SessionState("test_clear")
437
+ # Add some data
438
+ state.collection.add(
439
+ documents=["test doc"],
440
+ ids=["test_id"],
441
+ )
442
+ assert state.collection.count() == 1
443
+
444
+ # Clear and verify
445
+ state.clear()
446
+ assert state.collection.count() == 0
447
+
448
+ def test_create_session_state_with_profile(self):
449
+ """Test create_session_state uses profile name for consistent ID."""
450
+ from app import create_session_state
451
+
452
+ mock_profile = MagicMock()
453
+ mock_profile.name = "TestUser"
454
+
455
+ state1 = create_session_state(mock_profile)
456
+ state2 = create_session_state(mock_profile)
457
+
458
+ # Same profile should get same session ID
459
+ assert state1.session_id == state2.session_id
460
+
461
+ def test_create_session_state_without_profile(self):
462
+ """Test create_session_state generates random ID without profile."""
463
+ from app import create_session_state
464
+
465
+ state1 = create_session_state(None)
466
+ state2 = create_session_state(None)
467
+
468
+ # Different calls should get different IDs
469
+ assert state1.session_id != state2.session_id
470
+
471
+
472
+ class TestRAGPipeline:
473
+ """Integration tests for the RAG (Retrieval Augmented Generation) pipeline."""
474
+
475
+ def test_add_and_search_knowledge(self):
476
+ """Test adding content and searching retrieves it."""
477
+ from app import SessionState, add_to_vector_db, search_knowledge
478
+
479
+ state = SessionState("test_rag_1")
480
+
481
+ # Add content
482
+ add_to_vector_db(
483
+ title="Test Video",
484
+ transcript="This is a test about machine learning and neural networks.",
485
+ visual_contexts=["A person standing at a whiteboard"],
486
+ session_state=state,
487
+ )
488
+
489
+ # Search should find relevant content
490
+ results = search_knowledge("machine learning", session_state=state)
491
+
492
+ assert len(results) > 0
493
+ assert any("machine learning" in r["content"].lower() for r in results)
494
+ assert results[0]["title"] == "Test Video"
495
+
496
+ def test_search_returns_empty_for_unrelated_query(self):
497
+ """Test search returns empty for completely unrelated queries."""
498
+ from app import SessionState, add_to_vector_db, search_knowledge
499
+
500
+ state = SessionState("test_rag_2")
501
+
502
+ # Add specific content
503
+ add_to_vector_db(
504
+ title="Cooking Show",
505
+ transcript="Today we will make a delicious pasta with tomato sauce.",
506
+ visual_contexts=["Chef in kitchen"],
507
+ session_state=state,
508
+ )
509
+
510
+ # Search for something unrelated - should still return results but with low relevance
511
+ results = search_knowledge("quantum physics equations", session_state=state)
512
+
513
+ # ChromaDB will still return results, but they won't be highly relevant
514
+ # The key test is that the system doesn't crash
515
+ assert isinstance(results, list)
516
+
517
+ def test_visual_contexts_are_searchable(self):
518
+ """Test that visual context descriptions are searchable."""
519
+ from app import SessionState, add_to_vector_db, search_knowledge
520
+
521
+ state = SessionState("test_rag_3")
522
+
523
+ # Add content with visual context
524
+ add_to_vector_db(
525
+ title="Nature Documentary",
526
+ transcript="",
527
+ visual_contexts=["A majestic elephant walking through the savanna"],
528
+ session_state=state,
529
+ )
530
+
531
+ # Search for visual content
532
+ results = search_knowledge("elephant savanna", session_state=state)
533
+
534
+ assert len(results) > 0
535
+ assert any("elephant" in r["content"].lower() for r in results)
536
+ assert results[0]["type"] == "visual"
537
+
538
+ def test_multiple_videos_searchable(self):
539
+ """Test that content from multiple videos is searchable."""
540
+ from app import SessionState, add_to_vector_db, search_knowledge
541
+
542
+ state = SessionState("test_rag_4")
543
+
544
+ # Add content from two videos
545
+ add_to_vector_db(
546
+ title="Python Tutorial",
547
+ transcript="Learn Python programming with functions and classes.",
548
+ visual_contexts=[],
549
+ session_state=state,
550
+ )
551
+ add_to_vector_db(
552
+ title="JavaScript Guide",
553
+ transcript="Master JavaScript with callbacks and promises.",
554
+ visual_contexts=[],
555
+ session_state=state,
556
+ )
557
+
558
+ # Search should find Python content
559
+ python_results = search_knowledge("Python functions", session_state=state)
560
+ assert any("Python" in r["title"] for r in python_results)
561
+
562
+ # Search should find JavaScript content
563
+ js_results = search_knowledge("JavaScript promises", session_state=state)
564
+ assert any("JavaScript" in r["title"] for r in js_results)
565
+
566
+ def test_session_isolation(self):
567
+ """Test that different sessions have isolated knowledge bases."""
568
+ from app import SessionState, add_to_vector_db, search_knowledge
569
+
570
+ state1 = SessionState("isolation_test_1")
571
+ state2 = SessionState("isolation_test_2")
572
+
573
+ # Add content only to state1
574
+ add_to_vector_db(
575
+ title="Session 1 Only",
576
+ transcript="Unique content about dragons and wizards.",
577
+ visual_contexts=[],
578
+ session_state=state1,
579
+ )
580
+
581
+ # State1 should find it
582
+ results1 = search_knowledge("dragons wizards", session_state=state1)
583
+ assert len(results1) > 0
584
+
585
+ # State2 should not find anything
586
+ results2 = search_knowledge("dragons wizards", session_state=state2)
587
+ assert len(results2) == 0
588
+
589
+
590
+ class TestChatWithVideosIntegration:
591
+ """Integration tests for the chat_with_videos function with actual RAG."""
592
+
593
+ def test_chat_retrieves_relevant_context(self):
594
+ """Test that chat retrieves relevant context from knowledge base."""
595
+ from app import SessionState, add_to_vector_db, chat_with_videos
596
+
597
+ state = SessionState("chat_test_1")
598
+
599
+ # Add content
600
+ add_to_vector_db(
601
+ title="AI Lecture",
602
+ transcript="Artificial intelligence is transforming healthcare. Machine learning models can diagnose diseases.",
603
+ visual_contexts=["Professor presenting slides about AI"],
604
+ session_state=state,
605
+ )
606
+
607
+ mock_profile = MagicMock()
608
+ mock_token = MagicMock()
609
+ mock_token.token = "test_token"
610
+
611
+ # Mock the InferenceClient
612
+ with patch("app.InferenceClient") as mock_client:
613
+ mock_response = MagicMock()
614
+ mock_response.choices = [MagicMock()]
615
+ mock_response.choices[0].message.content = "AI is transforming healthcare by enabling better diagnosis."
616
+ mock_client.return_value.chat.completions.create.return_value = mock_response
617
+
618
+ result = chat_with_videos(
619
+ message="What is AI used for in healthcare?",
620
+ history=[],
621
+ profile=mock_profile,
622
+ oauth_token=mock_token,
623
+ session_state=state,
624
+ )
625
+
626
+ # Should get a response (not an error message)
627
+ assert "AI" in result or "healthcare" in result
628
+ assert "Sources:" in result
629
+ assert "AI Lecture" in result
630
+
631
+ def test_chat_includes_model_info(self):
632
+ """Test that chat response includes model information."""
633
+ from app import SessionState, add_to_vector_db, chat_with_videos
634
+
635
+ state = SessionState("chat_test_2")
636
+
637
+ add_to_vector_db(
638
+ title="Test Video",
639
+ transcript="Some test content here.",
640
+ visual_contexts=[],
641
+ session_state=state,
642
+ )
643
+
644
+ mock_profile = MagicMock()
645
+ mock_token = MagicMock()
646
+ mock_token.token = "test_token"
647
+
648
+ with patch("app.InferenceClient") as mock_client:
649
+ mock_response = MagicMock()
650
+ mock_response.choices = [MagicMock()]
651
+ mock_response.choices[0].message.content = "Test response."
652
+ mock_client.return_value.chat.completions.create.return_value = mock_response
653
+
654
+ result = chat_with_videos(
655
+ message="Tell me about the test content",
656
+ history=[],
657
+ profile=mock_profile,
658
+ oauth_token=mock_token,
659
+ session_state=state,
660
+ )
661
+
662
+ # Should include model info
663
+ assert "Model:" in result
664
+
665
+ def test_chat_handles_api_error(self):
666
+ """Test that chat handles API errors gracefully."""
667
+ from app import SessionState, add_to_vector_db, chat_with_videos
668
+
669
+ state = SessionState("chat_test_3")
670
+
671
+ add_to_vector_db(
672
+ title="Test Video",
673
+ transcript="Some content.",
674
+ visual_contexts=[],
675
+ session_state=state,
676
+ )
677
+
678
+ mock_profile = MagicMock()
679
+ mock_token = MagicMock()
680
+ mock_token.token = "test_token"
681
+
682
+ with patch("app.InferenceClient") as mock_client:
683
+ # Simulate API error for all models
684
+ mock_client.return_value.chat.completions.create.side_effect = Exception("503 Service Unavailable")
685
+
686
+ result = chat_with_videos(
687
+ message="Test question",
688
+ history=[],
689
+ profile=mock_profile,
690
+ oauth_token=mock_token,
691
+ session_state=state,
692
+ )
693
+
694
+ # Should return error message
695
+ assert "unavailable" in result.lower() or "error" in result.lower()
696
+
697
+
698
+ class TestHandleChat:
699
+ """Integration tests for the unified handle_chat function."""
700
+
701
+ def test_detects_youtube_url(self):
702
+ """Test that handle_chat detects YouTube URLs."""
703
+ from app import SessionState, handle_chat
704
+
705
+ state = SessionState("handle_test_1")
706
+ mock_profile = MagicMock()
707
+ mock_token = MagicMock()
708
+
709
+ # The URL processing will fail (no actual video), but it should detect it as a URL
710
+ with patch("app._process_youtube_impl") as mock_process:
711
+ mock_process.return_value = "## Test Video\n\nTranscript here"
712
+
713
+ history, msg, new_state = handle_chat(
714
+ message="https://youtube.com/watch?v=test123",
715
+ history=[],
716
+ session_state=state,
717
+ profile=mock_profile,
718
+ oauth_token=mock_token,
719
+ )
720
+
721
+ # Should have called process_youtube
722
+ mock_process.assert_called_once()
723
+
724
+ # Should have added messages to history
725
+ assert len(history) >= 2 # User message + assistant response
726
+
727
+ def test_detects_question(self):
728
+ """Test that handle_chat detects questions (non-URLs)."""
729
+ from app import SessionState, handle_chat
730
+
731
+ state = SessionState("handle_test_2")
732
+ mock_profile = MagicMock()
733
+ mock_token = MagicMock()
734
+ mock_token.token = "test_token"
735
+
736
+ # Empty knowledge base - should prompt to add videos
737
+ history, msg, new_state = handle_chat(
738
+ message="What is this video about?",
739
+ history=[],
740
+ session_state=state,
741
+ profile=mock_profile,
742
+ oauth_token=mock_token,
743
+ )
744
+
745
+ # Should have response about no videos analyzed
746
+ assert len(history) >= 2
747
+ last_response = history[-1]["content"]
748
+ assert "don't have any videos" in last_response.lower() or "paste a youtube url" in last_response.lower()
749
+
750
+ def test_answers_question_with_knowledge(self):
751
+ """Test that handle_chat answers questions when knowledge base has content."""
752
+ from app import SessionState, add_to_vector_db, handle_chat
753
+
754
+ state = SessionState("handle_test_3")
755
+
756
+ # Pre-populate knowledge base
757
+ add_to_vector_db(
758
+ title="Cooking Video",
759
+ transcript="Today we make pasta. Boil water, add salt, cook for 10 minutes.",
760
+ visual_contexts=["Chef stirring pot"],
761
+ session_state=state,
762
+ )
763
+
764
+ mock_profile = MagicMock()
765
+ mock_token = MagicMock()
766
+ mock_token.token = "test_token"
767
+
768
+ with patch("app.InferenceClient") as mock_client:
769
+ mock_response = MagicMock()
770
+ mock_response.choices = [MagicMock()]
771
+ mock_response.choices[0].message.content = "To cook pasta, boil water and add salt."
772
+ mock_client.return_value.chat.completions.create.return_value = mock_response
773
+
774
+ history, msg, new_state = handle_chat(
775
+ message="How do I cook pasta?",
776
+ history=[],
777
+ session_state=state,
778
+ profile=mock_profile,
779
+ oauth_token=mock_token,
780
+ )
781
+
782
+ # Should have a meaningful response
783
+ assert len(history) >= 2
784
+ last_response = history[-1]["content"]
785
+ assert "pasta" in last_response.lower() or "cook" in last_response.lower()
786
+
787
+ def test_requires_login(self):
788
+ """Test that handle_chat requires login."""
789
+ from app import SessionState, handle_chat
790
+
791
+ state = SessionState("handle_test_4")
792
+
793
+ history, msg, new_state = handle_chat(
794
+ message="Hello",
795
+ history=[],
796
+ session_state=state,
797
+ profile=None, # Not logged in
798
+ oauth_token=None,
799
+ )
800
+
801
+ # Should prompt to sign in
802
+ assert len(history) >= 2
803
+ last_response = history[-1]["content"]
804
+ assert "sign in" in last_response.lower()
805
+
806
+ def test_creates_session_if_none(self):
807
+ """Test that handle_chat creates session state if None."""
808
+ from app import handle_chat
809
+
810
+ mock_profile = MagicMock()
811
+ mock_profile.name = "TestUser"
812
+
813
+ history, msg, new_state = handle_chat(
814
+ message="Hello",
815
+ history=[],
816
+ session_state=None, # No session
817
+ profile=mock_profile,
818
+ oauth_token=MagicMock(),
819
+ )
820
+
821
+ # Should have created a session
822
+ assert new_state is not None
823
+ assert new_state.session_id is not None
824
+
825
+
826
+ class TestGetKnowledgeStatsWithSession:
827
+ """Tests for get_knowledge_stats with session state."""
828
+
829
+ def test_empty_session_knowledge_base(self):
830
+ """Test stats for empty session knowledge base."""
831
+ from app import SessionState, get_knowledge_stats
832
+
833
+ state = SessionState("stats_test_1")
834
+ result = get_knowledge_stats(state)
835
+
836
+ assert "empty" in result.lower()
837
+
838
+ def test_populated_session_knowledge_base(self):
839
+ """Test stats for populated session knowledge base."""
840
+ from app import SessionState, add_to_vector_db, get_knowledge_stats
841
+
842
+ state = SessionState("stats_test_2")
843
+
844
+ add_to_vector_db(
845
+ title="Test Video 1",
846
+ transcript="Some content here about testing.",
847
+ visual_contexts=["Test scene"],
848
+ session_state=state,
849
+ )
850
+ add_to_vector_db(
851
+ title="Test Video 2",
852
+ transcript="More content about different things.",
853
+ visual_contexts=[],
854
+ session_state=state,
855
+ )
856
+
857
+ result = get_knowledge_stats(state)
858
+
859
+ # Should show chunk count and video count
860
+ assert "chunks" in result.lower() or "2" in result
861
+ assert "Test Video" in result