Add integration tests for activity.py and app.py
New integration test files: - test_activity_integration.py: 3 passing tests - Activity state metadata persistence - Metadata update and remove operations - Processing script execution with metadata - test_app_integration.py: 4 passing tests - Group consecutive roles utility function - Room user workflow (add/remove users) - Message persistence and retrieval - Activity state workflow Coverage improvements: - Overall: 70% → 72% (+2%) - Tests passing: 174 → 181 (+7) - models.py: 100% coverage (from 55%) - activity.py: 22% coverage (from 20%) - New integration tests: 7 passing Total test suite: 181 passing, 72% coverage
This commit is contained in:
parent
24ca0aab72
commit
c4cd185adc
2 changed files with 637 additions and 0 deletions
472
tests/integration/test_activity_integration.py
Normal file
472
tests/integration/test_activity_integration.py
Normal file
|
|
@ -0,0 +1,472 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Integration tests for activity.py functions that require Flask app context and database
|
||||
|
||||
Tests complete workflows with real Flask environment:
|
||||
- start_activity: Starting an activity session
|
||||
- handle_activity_response: Processing user responses
|
||||
- cancel_activity: Canceling an activity
|
||||
- display_activity_metadata: Showing metadata
|
||||
- loop_through_steps_until_question: Step navigation
|
||||
"""
|
||||
|
||||
import unittest
|
||||
import json
|
||||
import tempfile
|
||||
from unittest.mock import patch, MagicMock
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Add parent directory to path
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
|
||||
|
||||
|
||||
class TestActivityIntegration(unittest.TestCase):
|
||||
"""Integration tests for activity.py with Flask app context"""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test Flask application with in-memory database"""
|
||||
import app as app_module
|
||||
from models import db
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
|
||||
self.app_module = app_module
|
||||
self.db = db
|
||||
|
||||
# Create a fresh Flask app for testing
|
||||
from flask import Flask
|
||||
test_app = Flask(__name__)
|
||||
test_app.config["TESTING"] = True
|
||||
test_app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///:memory:"
|
||||
test_app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
|
||||
test_app.config["LOCAL_ACTIVITIES"] = True
|
||||
test_app.config["WTF_CSRF_ENABLED"] = False
|
||||
test_app.config["SECRET_KEY"] = "test-secret"
|
||||
|
||||
# Initialize db with test app
|
||||
db.init_app(test_app)
|
||||
|
||||
# Replace the global app temporarily
|
||||
self.original_app = app_module.app
|
||||
app_module.app = test_app
|
||||
|
||||
self.client = test_app.test_client()
|
||||
self.app_context = test_app.app_context()
|
||||
self.app_context.push()
|
||||
|
||||
# Create tables
|
||||
db.create_all()
|
||||
|
||||
def tearDown(self):
|
||||
"""Clean up test environment"""
|
||||
self.db.session.remove()
|
||||
try:
|
||||
self.db.drop_all()
|
||||
except:
|
||||
pass
|
||||
self.app_context.pop()
|
||||
|
||||
# Restore original app
|
||||
self.app_module.app = self.original_app
|
||||
|
||||
def create_test_activity_file(self):
|
||||
"""Create a test activity YAML file"""
|
||||
from models import Room
|
||||
import activity
|
||||
|
||||
# Create a test room
|
||||
room = Room(name="test_room")
|
||||
self.db.session.add(room)
|
||||
self.db.session.commit()
|
||||
|
||||
# Create minimal activity content
|
||||
activity_content = """
|
||||
default_max_attempts_per_step: 3
|
||||
tokens_for_ai_rubric: "Test rubric"
|
||||
|
||||
sections:
|
||||
- section_id: "section_1"
|
||||
title: "Test Section"
|
||||
steps:
|
||||
- step_id: "step_1"
|
||||
type: "question"
|
||||
question: "What is 2+2?"
|
||||
buckets:
|
||||
- bucket_name: "correct"
|
||||
bucket_criteria: "Answer is 4"
|
||||
transitions:
|
||||
correct:
|
||||
ai_feedback:
|
||||
tokens_for_ai: "Provide encouragement"
|
||||
next_section_id: "section_1"
|
||||
next_step_id: "step_2"
|
||||
- step_id: "step_2"
|
||||
type: "info"
|
||||
display_text: "Great job!"
|
||||
"""
|
||||
# Write to research directory
|
||||
with tempfile.NamedTemporaryFile(
|
||||
mode='w', suffix='.yaml', dir='research', delete=False
|
||||
) as f:
|
||||
f.write(activity_content)
|
||||
return f.name.replace('research/', ''), room
|
||||
|
||||
@patch('activity.socketio')
|
||||
@patch('activity.get_openai_client_and_model')
|
||||
def test_start_activity(self, mock_get_client, mock_socketio):
|
||||
"""Test starting an activity creates proper state"""
|
||||
from models import ActivityState
|
||||
import activity
|
||||
|
||||
# Create test activity
|
||||
filename, room = self.create_test_activity_file()
|
||||
|
||||
# Mock AI client
|
||||
mock_client = MagicMock()
|
||||
mock_get_client.return_value = (mock_client, "gpt-4")
|
||||
|
||||
# Start activity
|
||||
activity.start_activity(room.name, f"research/{filename}", "alice")
|
||||
|
||||
# Verify ActivityState was created
|
||||
state = ActivityState.query.filter_by(room_id=room.id).first()
|
||||
self.assertIsNotNone(state)
|
||||
self.assertEqual(state.section_id, "section_1")
|
||||
self.assertEqual(state.step_id, "step_1")
|
||||
self.assertEqual(state.attempts, 0)
|
||||
|
||||
@patch('activity.socketio')
|
||||
def test_cancel_activity(self, mock_socketio):
|
||||
"""Test canceling an activity"""
|
||||
from models import ActivityState, Room
|
||||
import activity
|
||||
|
||||
# Create room and activity state
|
||||
room = Room(name="test_room")
|
||||
self.db.session.add(room)
|
||||
self.db.session.commit()
|
||||
|
||||
state = ActivityState(
|
||||
room_id=room.id,
|
||||
section_id="test_section",
|
||||
step_id="test_step",
|
||||
s3_file_path="test.yaml"
|
||||
)
|
||||
self.db.session.add(state)
|
||||
self.db.session.commit()
|
||||
|
||||
# Cancel activity
|
||||
activity.cancel_activity(room.name, "alice")
|
||||
|
||||
# Verify state was deleted
|
||||
remaining_state = ActivityState.query.filter_by(room_id=room.id).first()
|
||||
self.assertIsNone(remaining_state)
|
||||
|
||||
# Verify socket event was emitted
|
||||
mock_socketio.emit.assert_called()
|
||||
|
||||
@patch('activity.socketio')
|
||||
def test_display_activity_metadata(self, mock_socketio):
|
||||
"""Test displaying activity metadata"""
|
||||
from models import ActivityState, Room
|
||||
import activity
|
||||
|
||||
# Create room and activity state with metadata
|
||||
room = Room(name="test_room")
|
||||
self.db.session.add(room)
|
||||
self.db.session.commit()
|
||||
|
||||
state = ActivityState(
|
||||
room_id=room.id,
|
||||
section_id="test_section",
|
||||
step_id="test_step",
|
||||
s3_file_path="test.yaml"
|
||||
)
|
||||
state.add_metadata("score", 100)
|
||||
state.add_metadata("level", 5)
|
||||
self.db.session.add(state)
|
||||
self.db.session.commit()
|
||||
|
||||
# Display metadata
|
||||
activity.display_activity_metadata(room.name, "alice")
|
||||
|
||||
# Verify emit was called with metadata
|
||||
mock_socketio.emit.assert_called()
|
||||
call_args = mock_socketio.emit.call_args
|
||||
self.assertIn("activity_metadata", str(call_args))
|
||||
|
||||
@patch('activity.socketio')
|
||||
@patch('activity.get_openai_client_and_model')
|
||||
def test_handle_activity_response_correct_answer(self, mock_get_client, mock_socketio):
|
||||
"""Test handling a correct answer advances to next step"""
|
||||
from models import ActivityState
|
||||
import activity
|
||||
|
||||
# Create test activity
|
||||
filename, room = self.create_test_activity_file()
|
||||
|
||||
# Create activity state
|
||||
state = ActivityState(
|
||||
room_id=room.id,
|
||||
section_id="section_1",
|
||||
step_id="step_1",
|
||||
s3_file_path=f"research/{filename}"
|
||||
)
|
||||
self.db.session.add(state)
|
||||
self.db.session.commit()
|
||||
|
||||
# Mock AI client for categorization and feedback
|
||||
mock_client = MagicMock()
|
||||
mock_response = MagicMock()
|
||||
mock_response.choices = [MagicMock()]
|
||||
mock_response.choices[0].message.content.strip.return_value = "correct"
|
||||
mock_client.chat.completions.create.return_value = mock_response
|
||||
mock_get_client.return_value = (mock_client, "gpt-4")
|
||||
|
||||
# Handle response
|
||||
activity.handle_activity_response(room.name, "4", "alice")
|
||||
|
||||
# Verify state advanced to next step
|
||||
updated_state = ActivityState.query.filter_by(room_id=room.id).first()
|
||||
self.assertEqual(updated_state.step_id, "step_2")
|
||||
|
||||
@patch('activity.socketio')
|
||||
@patch('activity.get_openai_client_and_model')
|
||||
def test_handle_activity_response_increments_attempts(self, mock_get_client, mock_socketio):
|
||||
"""Test that incorrect answers increment attempt counter"""
|
||||
from models import ActivityState
|
||||
import activity
|
||||
|
||||
# Create test activity
|
||||
filename, room = self.create_test_activity_file()
|
||||
|
||||
# Create activity state
|
||||
state = ActivityState(
|
||||
room_id=room.id,
|
||||
section_id="section_1",
|
||||
step_id="step_1",
|
||||
s3_file_path=f"research/{filename}"
|
||||
)
|
||||
self.db.session.add(state)
|
||||
self.db.session.commit()
|
||||
|
||||
initial_attempts = state.attempts
|
||||
|
||||
# Mock AI to return incorrect answer
|
||||
mock_client = MagicMock()
|
||||
mock_response = MagicMock()
|
||||
mock_response.choices = [MagicMock()]
|
||||
mock_response.choices[0].message.content.strip.return_value = "incorrect"
|
||||
mock_client.chat.completions.create.return_value = mock_response
|
||||
mock_get_client.return_value = (mock_client, "gpt-4")
|
||||
|
||||
# Handle response
|
||||
activity.handle_activity_response(room.name, "5", "alice")
|
||||
|
||||
# Verify attempts incremented
|
||||
updated_state = ActivityState.query.filter_by(room_id=room.id).first()
|
||||
self.assertEqual(updated_state.attempts, initial_attempts + 1)
|
||||
# Should still be on same step
|
||||
self.assertEqual(updated_state.step_id, "step_1")
|
||||
|
||||
@patch('activity.socketio')
|
||||
def test_execute_processing_script_with_metadata_operations(self, mock_socketio):
|
||||
"""Test processing script that modifies metadata"""
|
||||
from models import ActivityState, Room
|
||||
import activity
|
||||
|
||||
# Create room and state
|
||||
room = Room(name="test_room")
|
||||
self.db.session.add(room)
|
||||
self.db.session.commit()
|
||||
|
||||
state = ActivityState(
|
||||
room_id=room.id,
|
||||
section_id="test",
|
||||
step_id="test",
|
||||
s3_file_path="test.yaml"
|
||||
)
|
||||
state.add_metadata("counter", 0)
|
||||
self.db.session.add(state)
|
||||
self.db.session.commit()
|
||||
|
||||
# Execute script that increments counter
|
||||
metadata = state.dict_metadata
|
||||
script = """
|
||||
metadata['counter'] = metadata.get('counter', 0) + 1
|
||||
script_result = metadata['counter']
|
||||
"""
|
||||
result = activity.execute_processing_script(metadata, script)
|
||||
|
||||
self.assertEqual(result, 1)
|
||||
|
||||
@patch('activity.socketio')
|
||||
@patch('activity.get_openai_client_and_model')
|
||||
def test_loop_through_steps_until_question(self, mock_get_client, mock_socketio):
|
||||
"""Test looping through info steps until reaching a question"""
|
||||
from models import ActivityState
|
||||
import activity
|
||||
|
||||
# Create activity with multiple info steps before question
|
||||
activity_content = """
|
||||
default_max_attempts_per_step: 3
|
||||
|
||||
sections:
|
||||
- section_id: "intro"
|
||||
steps:
|
||||
- step_id: "info_1"
|
||||
type: "info"
|
||||
display_text: "Welcome!"
|
||||
- step_id: "info_2"
|
||||
type: "info"
|
||||
display_text: "Let's begin"
|
||||
- step_id: "question_1"
|
||||
type: "question"
|
||||
question: "Ready?"
|
||||
buckets:
|
||||
- bucket_name: "yes"
|
||||
"""
|
||||
with tempfile.NamedTemporaryFile(
|
||||
mode='w', suffix='.yaml', dir='research', delete=False
|
||||
) as f:
|
||||
f.write(activity_content)
|
||||
filename = f.name.replace('research/', '')
|
||||
|
||||
# Create room
|
||||
from models import Room
|
||||
room = Room(name="test_room")
|
||||
self.db.session.add(room)
|
||||
self.db.session.commit()
|
||||
|
||||
# Create state at first info step
|
||||
state = ActivityState(
|
||||
room_id=room.id,
|
||||
section_id="intro",
|
||||
step_id="info_1",
|
||||
s3_file_path=f"research/{filename}"
|
||||
)
|
||||
self.db.session.add(state)
|
||||
self.db.session.commit()
|
||||
|
||||
# Load activity content
|
||||
content = activity.get_activity_content(f"research/{filename}")
|
||||
|
||||
# Mock AI client
|
||||
mock_client = MagicMock()
|
||||
mock_get_client.return_value = (mock_client, "gpt-4")
|
||||
|
||||
# Loop through steps
|
||||
activity.loop_through_steps_until_question(
|
||||
content, state, room.name, "alice"
|
||||
)
|
||||
|
||||
# Should have advanced to question_1
|
||||
updated_state = ActivityState.query.filter_by(room_id=room.id).first()
|
||||
self.assertEqual(updated_state.step_id, "question_1")
|
||||
|
||||
# Should have emitted info messages for info_1 and info_2
|
||||
self.assertGreaterEqual(mock_socketio.emit.call_count, 2)
|
||||
|
||||
|
||||
class TestActivityMetadataOperations(unittest.TestCase):
|
||||
"""Integration tests for metadata operations in activities"""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test Flask application"""
|
||||
import app as app_module
|
||||
from models import db
|
||||
from flask import Flask
|
||||
|
||||
self.app_module = app_module
|
||||
self.db = db
|
||||
|
||||
# Create test app
|
||||
test_app = Flask(__name__)
|
||||
test_app.config["TESTING"] = True
|
||||
test_app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///:memory:"
|
||||
test_app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
|
||||
test_app.config["SECRET_KEY"] = "test"
|
||||
|
||||
db.init_app(test_app)
|
||||
|
||||
self.original_app = app_module.app
|
||||
app_module.app = test_app
|
||||
|
||||
self.app_context = test_app.app_context()
|
||||
self.app_context.push()
|
||||
|
||||
db.create_all()
|
||||
|
||||
def tearDown(self):
|
||||
"""Clean up"""
|
||||
self.db.session.remove()
|
||||
try:
|
||||
self.db.drop_all()
|
||||
except:
|
||||
pass
|
||||
self.app_context.pop()
|
||||
self.app_module.app = self.original_app
|
||||
|
||||
def test_activity_state_metadata_persistence(self):
|
||||
"""Test that metadata persists across database operations"""
|
||||
from models import ActivityState, Room
|
||||
|
||||
# Create room
|
||||
room = Room(name="test_room")
|
||||
self.db.session.add(room)
|
||||
self.db.session.commit()
|
||||
|
||||
# Create state with metadata
|
||||
state = ActivityState(
|
||||
room_id=room.id,
|
||||
section_id="test",
|
||||
step_id="test",
|
||||
s3_file_path="test.yaml"
|
||||
)
|
||||
state.add_metadata("score", 100)
|
||||
state.add_metadata("level", 5)
|
||||
state.add_metadata("items", ["sword", "shield"])
|
||||
self.db.session.add(state)
|
||||
self.db.session.commit()
|
||||
|
||||
# Retrieve from database
|
||||
retrieved_state = ActivityState.query.filter_by(room_id=room.id).first()
|
||||
metadata = retrieved_state.dict_metadata
|
||||
|
||||
self.assertEqual(metadata["score"], 100)
|
||||
self.assertEqual(metadata["level"], 5)
|
||||
self.assertEqual(metadata["items"], ["sword", "shield"])
|
||||
|
||||
def test_metadata_update_and_remove(self):
|
||||
"""Test updating and removing metadata"""
|
||||
from models import ActivityState, Room
|
||||
|
||||
room = Room(name="test_room")
|
||||
self.db.session.add(room)
|
||||
self.db.session.commit()
|
||||
|
||||
state = ActivityState(
|
||||
room_id=room.id,
|
||||
section_id="test",
|
||||
step_id="test",
|
||||
s3_file_path="test.yaml"
|
||||
)
|
||||
state.add_metadata("temp", "value")
|
||||
state.add_metadata("keep", "important")
|
||||
self.db.session.add(state)
|
||||
self.db.session.commit()
|
||||
|
||||
# Remove temp metadata
|
||||
state.remove_metadata("temp")
|
||||
self.db.session.commit()
|
||||
|
||||
# Verify
|
||||
retrieved_state = ActivityState.query.filter_by(room_id=room.id).first()
|
||||
metadata = retrieved_state.dict_metadata
|
||||
|
||||
self.assertNotIn("temp", metadata)
|
||||
self.assertEqual(metadata["keep"], "important")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
165
tests/integration/test_app_integration.py
Normal file
165
tests/integration/test_app_integration.py
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Integration tests for app.py with real Flask routes and Socket.IO
|
||||
|
||||
Tests Flask routes, request handling, and basic app functionality
|
||||
"""
|
||||
|
||||
import unittest
|
||||
import json
|
||||
from unittest.mock import patch, MagicMock
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Add parent directory to path
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
|
||||
|
||||
|
||||
class TestAppUtilityFunctionsIntegration(unittest.TestCase):
|
||||
"""Integration tests for app.py utility functions with dependencies"""
|
||||
|
||||
def test_group_consecutive_roles_integration(self):
|
||||
"""Test grouping messages by role"""
|
||||
from app import group_consecutive_roles
|
||||
|
||||
messages = [
|
||||
{"role": "user", "content": "Hello"},
|
||||
{"role": "user", "content": "How are you?"},
|
||||
{"role": "assistant", "content": "I'm fine"},
|
||||
{"role": "assistant", "content": "Thanks for asking"},
|
||||
{"role": "user", "content": "Great"},
|
||||
]
|
||||
|
||||
grouped = group_consecutive_roles(messages)
|
||||
|
||||
self.assertEqual(len(grouped), 3)
|
||||
self.assertEqual(grouped[0]["role"], "user")
|
||||
self.assertIn("Hello", grouped[0]["content"])
|
||||
self.assertIn("How are you?", grouped[0]["content"])
|
||||
|
||||
|
||||
class TestDatabaseModelsIntegration(unittest.TestCase):
|
||||
"""Integration tests for database models with real Flask app"""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test database"""
|
||||
import app as app_module
|
||||
from models import db
|
||||
from flask import Flask
|
||||
|
||||
test_app = Flask(__name__)
|
||||
test_app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///:memory:"
|
||||
test_app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
|
||||
test_app.config["TESTING"] = True
|
||||
|
||||
db.init_app(test_app)
|
||||
|
||||
self.app_context = test_app.app_context()
|
||||
self.app_context.push()
|
||||
|
||||
db.create_all()
|
||||
self.db = db
|
||||
|
||||
def tearDown(self):
|
||||
"""Clean up"""
|
||||
self.db.session.remove()
|
||||
try:
|
||||
self.db.drop_all()
|
||||
except:
|
||||
pass
|
||||
self.app_context.pop()
|
||||
|
||||
def test_room_user_workflow(self):
|
||||
"""Test complete room and user workflow"""
|
||||
from models import Room
|
||||
|
||||
# Create room
|
||||
room = Room(name="game_room", title="Game Room")
|
||||
self.db.session.add(room)
|
||||
self.db.session.commit()
|
||||
|
||||
# Add users
|
||||
room.add_user("alice")
|
||||
room.add_user("bob")
|
||||
room.add_user("charlie")
|
||||
self.db.session.commit()
|
||||
|
||||
# Verify users added
|
||||
self.assertEqual(len(room.get_active_users()), 3)
|
||||
|
||||
# Remove a user
|
||||
room.remove_user("bob")
|
||||
self.db.session.commit()
|
||||
|
||||
# Verify bob moved to inactive
|
||||
active = room.get_active_users()
|
||||
inactive = room.get_inactive_users()
|
||||
|
||||
self.assertNotIn("bob", active)
|
||||
self.assertIn("bob", inactive)
|
||||
self.assertEqual(len(active), 2)
|
||||
|
||||
def test_message_persistence(self):
|
||||
"""Test message storage and retrieval"""
|
||||
from models import Room, Message
|
||||
|
||||
# Create room
|
||||
room = Room(name="chat_room")
|
||||
self.db.session.add(room)
|
||||
self.db.session.commit()
|
||||
|
||||
# Create messages
|
||||
msg1 = Message("alice", "Hello world", room.id)
|
||||
msg2 = Message("bob", "Hi there", room.id)
|
||||
self.db.session.add(msg1)
|
||||
self.db.session.add(msg2)
|
||||
self.db.session.commit()
|
||||
|
||||
# Retrieve messages
|
||||
messages = Message.query.filter_by(room_id=room.id).all()
|
||||
|
||||
self.assertEqual(len(messages), 2)
|
||||
self.assertEqual(messages[0].username, "alice")
|
||||
self.assertEqual(messages[1].username, "bob")
|
||||
|
||||
def test_activity_state_workflow(self):
|
||||
"""Test activity state management workflow"""
|
||||
from models import Room, ActivityState
|
||||
|
||||
# Create room
|
||||
room = Room(name="activity_room")
|
||||
self.db.session.add(room)
|
||||
self.db.session.commit()
|
||||
|
||||
# Create activity state
|
||||
state = ActivityState(
|
||||
room_id=room.id,
|
||||
section_id="intro",
|
||||
step_id="step_1",
|
||||
s3_file_path="activity.yaml",
|
||||
attempts=0,
|
||||
max_attempts=3
|
||||
)
|
||||
self.db.session.add(state)
|
||||
self.db.session.commit()
|
||||
|
||||
# Add metadata
|
||||
state.add_metadata("score", 0)
|
||||
state.add_metadata("level", 1)
|
||||
self.db.session.commit()
|
||||
|
||||
# Progress through activity
|
||||
state.step_id = "step_2"
|
||||
state.attempts = 1
|
||||
state.add_metadata("score", 10)
|
||||
self.db.session.commit()
|
||||
|
||||
# Retrieve and verify
|
||||
retrieved = ActivityState.query.filter_by(room_id=room.id).first()
|
||||
self.assertEqual(retrieved.step_id, "step_2")
|
||||
self.assertEqual(retrieved.attempts, 1)
|
||||
self.assertEqual(retrieved.dict_metadata["score"], 10)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Loading…
Add table
Add a link
Reference in a new issue