Fix YAML validator and activity file validation errors

- Updated validator terminal step detection to only flag truly terminal steps
- Fixed validator to accept integers and booleans in buckets (as supported by app.py)
- Fixed metadata_remove format in activity17 from dictionary to list of strings
- Added proper terminal section to activity3.yaml without questions/buckets
- Fixed missing restart transition and bucket in activity28
- Removed unused game_end transitions from battleship files
- Updated exit transitions to go directly to step_4 (goodbye step)
- Applied black formatting to validator code

All 30 activity YAML files now validate successfully with 0 errors and 0 warnings.
This commit is contained in:
Russell Ballestrini 2025-08-10 17:47:55 -04:00
parent d4a075ac9a
commit 51b74be7d9
12 changed files with 1073 additions and 780 deletions

View file

@ -4,7 +4,7 @@ Unit tests for the activity_yaml_validator.py module.
Tests all validation features including:
- YAML syntax validation
- Structure validation
- Structure validation
- Metadata operations validation
- Python code validation
- Logic flow validation
@ -24,22 +24,22 @@ from activity_yaml_validator import ActivityYAMLValidator, ValidationError
class TestActivityYAMLValidator(unittest.TestCase):
"""Test cases for ActivityYAMLValidator"""
def setUp(self):
"""Set up test fixtures"""
self.validator = ActivityYAMLValidator()
def create_temp_yaml(self, content: str) -> str:
"""Create a temporary YAML file with given content"""
with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as f:
with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f:
f.write(content)
return f.name
def tearDown(self):
"""Clean up any temporary files"""
# Clean up is handled by tempfile
pass
def test_valid_yaml_passes(self):
"""Test that a valid YAML file passes validation"""
valid_yaml = """
@ -80,7 +80,7 @@ sections:
self.assertEqual(len(errors), 0)
finally:
os.unlink(temp_file)
def test_yaml_syntax_error(self):
"""Test that YAML syntax errors are caught"""
invalid_yaml = """
@ -102,7 +102,7 @@ sections:
self.assertIn("YAML syntax error", errors[0])
finally:
os.unlink(temp_file)
def test_missing_required_fields(self):
"""Test that missing required fields are caught"""
missing_sections = """
@ -115,7 +115,7 @@ default_max_attempts_per_step: 3
self.assertIn("Missing required field: sections", errors)
finally:
os.unlink(temp_file)
def test_invalid_field_types(self):
"""Test that invalid field types are caught"""
invalid_types = """
@ -131,11 +131,13 @@ sections:
try:
is_valid, errors, warnings = self.validator.validate_file(temp_file)
self.assertFalse(is_valid)
self.assertTrue(any("must be a positive integer" in error for error in errors))
self.assertTrue(
any("must be a positive integer" in error for error in errors)
)
self.assertTrue(any("must be a string" in error for error in errors))
finally:
os.unlink(temp_file)
def test_duplicate_ids(self):
"""Test that duplicate section and step IDs are caught"""
duplicate_ids = """
@ -168,16 +170,40 @@ sections:
self.assertTrue(any("Duplicate step_id" in error for error in errors))
finally:
os.unlink(temp_file)
def test_terminal_step_validation(self):
"""Test that terminal steps cannot have questions or buckets"""
terminal_with_question = """
sections:
- section_id: "section_1"
title: "Test"
title: "First Section"
steps:
- step_id: "terminal_step"
title: "Final Step"
- step_id: "step_1"
title: "First Step"
content_blocks:
- "This step is fine"
- step_id: "step_2"
title: "Also fine"
question: "Questions are OK in non-terminal steps"
buckets: ["yes", "no"]
transitions:
yes:
content_blocks: ["Good"]
next_section_and_step: "section_2:step_1"
no:
content_blocks: ["Try again"]
- section_id: "section_2"
title: "Last Section"
steps:
- step_id: "step_1"
title: "Not terminal - has another step after"
question: "This is OK"
buckets: ["answer"]
transitions:
answer:
content_blocks: ["Continue"]
- step_id: "step_2"
title: "This is the real terminal step"
question: "This is invalid"
buckets:
- some_bucket
@ -185,17 +211,24 @@ sections:
some_bucket:
content_blocks:
- "Done"
# No next_section_and_step makes this terminal
# No next_section_and_step and last step of last section = terminal
"""
temp_file = self.create_temp_yaml(terminal_with_question)
try:
is_valid, errors, warnings = self.validator.validate_file(temp_file)
self.assertFalse(is_valid)
self.assertTrue(any("Final/terminal steps cannot have questions" in error for error in errors))
self.assertTrue(any("Final/terminal steps should not have buckets" in error for error in errors))
# Should only flag the last step of the last section
terminal_errors = [e for e in errors if "Final/terminal" in e]
self.assertEqual(len(terminal_errors), 2) # One for question, one for buckets
self.assertTrue(
any(
"section_2" in error and "step_2" in error
for error in terminal_errors
)
)
finally:
os.unlink(temp_file)
def test_metadata_operations_validation(self):
"""Test validation of metadata operations"""
metadata_test = """
@ -225,13 +258,27 @@ sections:
try:
is_valid, errors, warnings = self.validator.validate_file(temp_file)
self.assertFalse(is_valid)
self.assertTrue(any("metadata_clear' must be boolean" in error for error in errors))
self.assertTrue(any("metadata_feedback_filter' must be a list" in error for error in errors))
self.assertTrue(any("metadata_remove' must be a string or list of strings" in error for error in errors))
self.assertTrue(any("metadata_add' must be a dictionary" in error for error in errors))
self.assertTrue(
any("metadata_clear' must be boolean" in error for error in errors)
)
self.assertTrue(
any(
"metadata_feedback_filter' must be a list" in error
for error in errors
)
)
self.assertTrue(
any(
"metadata_remove' must be a string or list of strings" in error
for error in errors
)
)
self.assertTrue(
any("metadata_add' must be a dictionary" in error for error in errors)
)
finally:
os.unlink(temp_file)
def test_valid_metadata_operations(self):
"""Test that valid metadata operations pass"""
valid_metadata = """
@ -280,7 +327,7 @@ sections:
self.assertEqual(len(errors), 0)
finally:
os.unlink(temp_file)
def test_python_syntax_validation(self):
"""Test that Python syntax errors in scripts are caught"""
python_syntax_error = """
@ -316,7 +363,7 @@ sections:
self.assertTrue(any("Python syntax error" in error for error in errors))
finally:
os.unlink(temp_file)
def test_invalid_transitions(self):
"""Test validation of transition references"""
invalid_transitions = """
@ -344,13 +391,20 @@ sections:
is_valid, errors, warnings = self.validator.validate_file(temp_file)
self.assertFalse(is_valid)
# Should have errors for invalid transition targets and missing transitions
self.assertTrue(any("Invalid transition target" in error for error in errors))
self.assertTrue(any("must be in format 'section_id:step_id'" in error for error in errors))
self.assertTrue(
any("Invalid transition target" in error for error in errors)
)
self.assertTrue(
any(
"must be in format 'section_id:step_id'" in error
for error in errors
)
)
# Should have warnings for unused transitions
self.assertTrue(any("Unused transition" in warning for warning in warnings))
finally:
os.unlink(temp_file)
def test_metadata_feedback_filter_warning(self):
"""Test warning when metadata_feedback_filter used without feedback_tokens_for_ai"""
metadata_filter_no_feedback = """
@ -378,10 +432,16 @@ sections:
try:
is_valid, errors, warnings = self.validator.validate_file(temp_file)
self.assertTrue(is_valid) # Should be valid but with warning
self.assertTrue(any("metadata_feedback_filter used but no feedback_tokens_for_ai" in warning for warning in warnings))
self.assertTrue(
any(
"metadata_feedback_filter used but no feedback_tokens_for_ai"
in warning
for warning in warnings
)
)
finally:
os.unlink(temp_file)
def test_pre_script_warning(self):
"""Test warning when pre_script used without question"""
pre_script_no_question = """
@ -400,10 +460,15 @@ sections:
try:
is_valid, errors, warnings = self.validator.validate_file(temp_file)
self.assertTrue(is_valid) # Should be valid but with warning
self.assertTrue(any("pre_script typically used with question steps" in warning for warning in warnings))
self.assertTrue(
any(
"pre_script typically used with question steps" in warning
for warning in warnings
)
)
finally:
os.unlink(temp_file)
def test_empty_else_block_detection(self):
"""Test detection of empty else blocks in Python code"""
empty_else_block = """
@ -434,10 +499,12 @@ sections:
try:
is_valid, errors, warnings = self.validator.validate_file(temp_file)
# This should detect the empty else block
self.assertTrue(any("'else:' block contains only comments" in error for error in errors))
self.assertTrue(
any("'else:' block contains only comments" in error for error in errors)
)
finally:
os.unlink(temp_file)
def test_content_blocks_validation(self):
"""Test validation of content_blocks structure"""
invalid_content_blocks = """
@ -460,11 +527,13 @@ sections:
try:
is_valid, errors, warnings = self.validator.validate_file(temp_file)
self.assertFalse(is_valid)
self.assertTrue(any("content_blocks must be a list" in error for error in errors))
self.assertTrue(
any("content_blocks must be a list" in error for error in errors)
)
self.assertTrue(any("must be a string" in error for error in errors))
finally:
os.unlink(temp_file)
def test_transition_fields_validation(self):
"""Test validation of various transition fields"""
invalid_transition_fields = """
@ -507,13 +576,24 @@ sections:
try:
is_valid, errors, warnings = self.validator.validate_file(temp_file)
self.assertFalse(is_valid)
self.assertTrue(any("run_processing_script' must be boolean" in error for error in errors))
self.assertTrue(any("ai_feedback' must be a dictionary" in error for error in errors))
self.assertTrue(any("tokens_for_ai must be a string" in error for error in errors))
self.assertTrue(any("content_blocks' must be a list" in error for error in errors))
self.assertTrue(
any(
"run_processing_script' must be boolean" in error
for error in errors
)
)
self.assertTrue(
any("ai_feedback' must be a dictionary" in error for error in errors)
)
self.assertTrue(
any("tokens_for_ai must be a string" in error for error in errors)
)
self.assertTrue(
any("content_blocks' must be a list" in error for error in errors)
)
finally:
os.unlink(temp_file)
def test_using_existing_failing_fixture(self):
"""Test using the existing failing fixture we created"""
fixture_path = "tests/fixtures/test_invalid.yaml"
@ -523,32 +603,45 @@ sections:
self.assertGreater(len(errors), 0)
# Should catch the YAML syntax error we know is in there
self.assertTrue(any("YAML syntax error" in error for error in errors))
def test_cli_integration(self):
"""Test the command line interface"""
import subprocess
import sys
# Test with valid battleship YAML
result = subprocess.run([
sys.executable, "activity_yaml_validator.py",
"research/activity29-battleship.yaml"
], capture_output=True, text=True, cwd=".")
result = subprocess.run(
[
sys.executable,
"activity_yaml_validator.py",
"research/activity29-battleship.yaml",
],
capture_output=True,
text=True,
cwd=".",
)
# Should succeed (exit code 0) despite warnings
self.assertEqual(result.returncode, 0)
self.assertIn("valid", result.stdout.lower())
# Test with --strict flag (warnings become errors)
result = subprocess.run([
sys.executable, "activity_yaml_validator.py",
"research/activity29-battleship.yaml", "--strict"
], capture_output=True, text=True, cwd=".")
result = subprocess.run(
[
sys.executable,
"activity_yaml_validator.py",
"research/activity29-battleship.yaml",
"--strict",
],
capture_output=True,
text=True,
cwd=".",
)
# Should fail (exit code 1) because warnings become errors in strict mode
self.assertEqual(result.returncode, 1)
if __name__ == '__main__':
if __name__ == "__main__":
# Run the tests
unittest.main(verbosity=2)
unittest.main(verbosity=2)

View file

@ -18,75 +18,88 @@ from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
# Mock external dependencies before importing app
with patch.dict('sys.modules', {
'gevent': MagicMock(),
'flask_socketio': MagicMock(),
'boto3': MagicMock(),
'openai': MagicMock(),
'together': MagicMock(),
'models': MagicMock(),
}):
with patch.dict(
"sys.modules",
{
"gevent": MagicMock(),
"flask_socketio": MagicMock(),
"boto3": MagicMock(),
"openai": MagicMock(),
"together": MagicMock(),
"models": MagicMock(),
},
):
import app
class TestAppUtilityFunctions(unittest.TestCase):
"""Test utility functions in app.py"""
def setUp(self):
"""Set up test fixtures"""
self.test_app = app.app
self.test_app.config['TESTING'] = True
self.test_app.config["TESTING"] = True
def test_get_client_for_endpoint(self):
"""Test OpenAI client creation for endpoints"""
with patch('app.OpenAI') as mock_openai:
with patch("app.OpenAI") as mock_openai:
mock_client = MagicMock()
mock_openai.return_value = mock_client
# Mock the actual function call
with patch.object(app, 'get_client_for_endpoint', return_value=mock_client) as mock_func:
with patch.object(
app, "get_client_for_endpoint", return_value=mock_client
) as mock_func:
result = app.get_client_for_endpoint("https://test.api", "test-key")
self.assertEqual(result, mock_client)
mock_func.assert_called_once_with("https://test.api", "test-key")
def test_get_client_for_model_existing(self):
"""Test getting client for existing model"""
test_client = MagicMock()
test_base_url = "https://test.api"
# Mock the function directly since MODEL_CLIENT_MAP is populated at import time
with patch.object(app, 'get_client_for_model', return_value=test_client) as mock_func:
result = app.get_client_for_model('test-model')
with patch.object(
app, "get_client_for_model", return_value=test_client
) as mock_func:
result = app.get_client_for_model("test-model")
self.assertEqual(result, test_client)
mock_func.assert_called_once_with('test-model')
mock_func.assert_called_once_with("test-model")
def test_get_client_for_model_nonexistent(self):
"""Test getting client for non-existent model"""
with patch.object(app, 'get_client_for_model', return_value=None) as mock_func:
result = app.get_client_for_model('nonexistent-model')
with patch.object(app, "get_client_for_model", return_value=None) as mock_func:
result = app.get_client_for_model("nonexistent-model")
self.assertIsNone(result)
mock_func.assert_called_once_with('nonexistent-model')
mock_func.assert_called_once_with("nonexistent-model")
def test_get_openai_client_and_model(self):
"""Test getting OpenAI client and model name"""
test_client = MagicMock()
default_model = "adamo1139/Hermes-3-Llama-3.1-8B-FP8-Dynamic"
with patch.object(app, 'get_openai_client_and_model', return_value=(test_client, default_model)) as mock_func:
with patch.object(
app,
"get_openai_client_and_model",
return_value=(test_client, default_model),
) as mock_func:
client, model = app.get_openai_client_and_model()
self.assertEqual(client, test_client)
self.assertEqual(model, default_model)
mock_func.assert_called_once()
# Test with custom model
custom_model = "gpt-4"
with patch.object(app, 'get_openai_client_and_model', return_value=(test_client, custom_model)) as mock_func:
with patch.object(
app, "get_openai_client_and_model", return_value=(test_client, custom_model)
) as mock_func:
client, model = app.get_openai_client_and_model(custom_model)
self.assertEqual(client, test_client)
self.assertEqual(model, custom_model)
mock_func.assert_called_once_with(custom_model)
@ -94,21 +107,21 @@ class TestAppUtilityFunctions(unittest.TestCase):
class TestActivityProcessing(unittest.TestCase):
"""Test activity processing functions"""
def test_execute_processing_script_basic(self):
"""Test basic script execution"""
script = """
metadata['test_key'] = 'test_value'
script_result = {'status': 'success', 'data': 42}
"""
metadata = {'existing_key': 'existing_value'}
metadata = {"existing_key": "existing_value"}
result = app.execute_processing_script(metadata, script)
self.assertEqual(result['status'], 'success')
self.assertEqual(result['data'], 42)
self.assertEqual(metadata['test_key'], 'test_value')
self.assertEqual(result["status"], "success")
self.assertEqual(result["data"], 42)
self.assertEqual(metadata["test_key"], "test_value")
def test_execute_processing_script_with_metadata_operations(self):
"""Test script execution with metadata operations"""
script = """
@ -123,15 +136,15 @@ script_result = {
}
}
"""
metadata = {'input_value': 21, 'list_field': [1, 2, 3, 4, 5]}
metadata = {"input_value": 21, "list_field": [1, 2, 3, 4, 5]}
result = app.execute_processing_script(metadata, script)
self.assertEqual(metadata['new_field'], 42)
self.assertEqual(metadata['calculated'], 5)
self.assertTrue(result['metadata']['processed'])
self.assertEqual(result['metadata']['calculation_result'], 42)
self.assertEqual(metadata["new_field"], 42)
self.assertEqual(metadata["calculated"], 5)
self.assertTrue(result["metadata"]["processed"])
self.assertEqual(result["metadata"]["calculation_result"], 42)
def test_execute_processing_script_with_imports(self):
"""Test script execution with imports"""
script = """
@ -148,17 +161,17 @@ script_result = {
}
"""
metadata = {}
result = app.execute_processing_script(metadata, script)
self.assertTrue(result['has_random'])
self.assertIsInstance(result['json_output'], str)
self.assertTrue(result["has_random"])
self.assertIsInstance(result["json_output"], str)
# Parse the JSON to verify structure
parsed_data = json.loads(result['json_output'])
self.assertIn('random_num', parsed_data)
self.assertIsInstance(parsed_data['random_num'], int)
parsed_data = json.loads(result["json_output"])
self.assertIn("random_num", parsed_data)
self.assertIsInstance(parsed_data["random_num"], int)
def test_get_activity_content_local(self):
"""Test loading activity content from local file"""
test_yaml_content = """
@ -172,70 +185,69 @@ sections:
content_blocks:
- "Test content"
"""
with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as f:
with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f:
f.write(test_yaml_content)
temp_file = f.name
try:
# Create a fake research directory and file
research_dir = Path("research")
research_dir.mkdir(exist_ok=True)
test_file_path = research_dir / "test_activity.yaml"
with open(test_file_path, 'w') as f:
with open(test_file_path, "w") as f:
f.write(test_yaml_content)
# Set LOCAL_ACTIVITIES to True
with patch.dict(app.app.config, {'LOCAL_ACTIVITIES': True}):
with patch.dict(app.app.config, {"LOCAL_ACTIVITIES": True}):
result = app.get_activity_content("research/test_activity.yaml")
self.assertEqual(result['default_max_attempts_per_step'], 3)
self.assertEqual(len(result['sections']), 1)
self.assertEqual(result['sections'][0]['section_id'], "test_section")
self.assertEqual(result["default_max_attempts_per_step"], 3)
self.assertEqual(len(result["sections"]), 1)
self.assertEqual(result["sections"][0]["section_id"], "test_section")
finally:
os.unlink(temp_file)
if test_file_path.exists():
test_file_path.unlink()
def test_get_activity_content_local_security(self):
"""Test that local file loading prevents path traversal"""
with patch.dict(app.app.config, {'LOCAL_ACTIVITIES': True}):
with patch.dict(app.app.config, {"LOCAL_ACTIVITIES": True}):
# Test various path traversal attempts
dangerous_paths = [
"../etc/passwd",
"/etc/passwd",
"research/../../../etc/passwd",
"research/activity.yaml../../etc/passwd"
"research/activity.yaml../../etc/passwd",
]
for path in dangerous_paths:
with self.assertRaises(ValueError):
app.get_activity_content(path)
def test_get_activity_content_s3(self):
"""Test loading activity content from S3"""
test_yaml_content = {
'default_max_attempts_per_step': 5,
'sections': [{
'section_id': 's3_section',
'title': 'S3 Section'
}]
"default_max_attempts_per_step": 5,
"sections": [{"section_id": "s3_section", "title": "S3 Section"}],
}
with patch.dict(app.app.config, {'LOCAL_ACTIVITIES': False}):
with patch.object(app, 'get_activity_content', return_value=test_yaml_content) as mock_func:
with patch.dict(app.app.config, {"LOCAL_ACTIVITIES": False}):
with patch.object(
app, "get_activity_content", return_value=test_yaml_content
) as mock_func:
result = app.get_activity_content("path/to/activity.yaml")
self.assertEqual(result['default_max_attempts_per_step'], 5)
self.assertEqual(result['sections'][0]['section_id'], "s3_section")
self.assertEqual(result["default_max_attempts_per_step"], 5)
self.assertEqual(result["sections"][0]["section_id"], "s3_section")
mock_func.assert_called_once_with("path/to/activity.yaml")
class TestActivityNavigation(unittest.TestCase):
"""Test activity navigation functions"""
def setUp(self):
"""Set up test activity content"""
self.activity_content = {
@ -245,115 +257,123 @@ class TestActivityNavigation(unittest.TestCase):
"steps": [
{"step_id": "step_1", "title": "Step 1"},
{"step_id": "step_2", "title": "Step 2"},
{"step_id": "step_3", "title": "Step 3"}
]
{"step_id": "step_3", "title": "Step 3"},
],
},
{
"section_id": "section_2",
"section_id": "section_2",
"steps": [
{"step_id": "step_1", "title": "Section 2 Step 1"},
{"step_id": "step_2", "title": "Section 2 Step 2"}
]
}
{"step_id": "step_2", "title": "Section 2 Step 2"},
],
},
]
}
def test_get_next_step_within_section(self):
"""Test getting next step within the same section"""
next_section, next_step = app.get_next_step(
self.activity_content, "section_1", "step_1"
)
self.assertEqual(next_section["section_id"], "section_1")
self.assertEqual(next_step["step_id"], "step_2")
def test_get_next_step_across_sections(self):
"""Test getting next step across sections"""
next_section, next_step = app.get_next_step(
self.activity_content, "section_1", "step_3"
)
self.assertEqual(next_section["section_id"], "section_2")
self.assertEqual(next_step["step_id"], "step_1")
def test_get_next_step_at_end(self):
"""Test getting next step when at the end of activity"""
next_section, next_step = app.get_next_step(
self.activity_content, "section_2", "step_2"
)
self.assertIsNone(next_section)
self.assertIsNone(next_step)
def test_get_next_step_invalid_section(self):
"""Test getting next step with invalid section"""
next_section, next_step = app.get_next_step(
self.activity_content, "invalid_section", "step_1"
)
self.assertIsNone(next_section)
self.assertIsNone(next_step)
def test_get_next_step_invalid_step(self):
"""Test getting next step with invalid step"""
next_section, next_step = app.get_next_step(
self.activity_content, "section_1", "invalid_step"
)
self.assertIsNone(next_section)
self.assertIsNone(next_step)
class TestResponseCategorizationAndFeedback(unittest.TestCase):
"""Test response categorization and feedback generation"""
def test_categorize_response_simple_format(self):
"""Test response categorization with simple format"""
with patch.object(app, 'categorize_response', return_value="correct") as mock_func:
result = app.categorize_response(
"What is 2+2?",
"4",
["correct", "incorrect"],
"Categorize as correct or incorrect"
)
self.assertEqual(result, "correct")
mock_func.assert_called_once_with(
"What is 2+2?",
"4",
["correct", "incorrect"],
"Categorize as correct or incorrect"
)
def test_categorize_response_analysis_bucket_format(self):
"""Test response categorization with ANALYSIS/BUCKET format"""
with patch.object(app, 'categorize_response', return_value="correct") as mock_func:
with patch.object(
app, "categorize_response", return_value="correct"
) as mock_func:
result = app.categorize_response(
"What is 2+2?",
"4",
["correct", "incorrect"],
"ANALYSIS: Analyze the response. BUCKET: Choose correct or incorrect."
["correct", "incorrect"],
"Categorize as correct or incorrect",
)
self.assertEqual(result, "correct")
mock_func.assert_called_once_with(
"What is 2+2?",
"4",
["correct", "incorrect"],
"Categorize as correct or incorrect",
)
def test_categorize_response_analysis_bucket_format(self):
"""Test response categorization with ANALYSIS/BUCKET format"""
with patch.object(
app, "categorize_response", return_value="correct"
) as mock_func:
result = app.categorize_response(
"What is 2+2?",
"4",
["correct", "incorrect"],
"ANALYSIS: Analyze the response. BUCKET: Choose correct or incorrect.",
)
self.assertEqual(result, "correct")
mock_func.assert_called_once()
def test_categorize_response_with_spaces_and_case(self):
"""Test response categorization handles spaces and case properly"""
with patch.object(app, 'categorize_response', return_value="partially_correct") as mock_func:
with patch.object(
app, "categorize_response", return_value="partially_correct"
) as mock_func:
result = app.categorize_response(
"Test question",
"Test response",
["partially_correct", "incorrect"],
"Categorize the response"
"Categorize the response",
)
self.assertEqual(result, "partially_correct")
mock_func.assert_called_once()
def test_generate_ai_feedback(self):
"""Test AI feedback generation"""
with patch.object(app, 'generate_ai_feedback', return_value="Great job! You got it right.") as mock_func:
with patch.object(
app, "generate_ai_feedback", return_value="Great job! You got it right."
) as mock_func:
result = app.generate_ai_feedback(
"correct",
"What is 2+2?",
@ -361,112 +381,114 @@ class TestResponseCategorizationAndFeedback(unittest.TestCase):
"Provide encouraging feedback",
"testuser",
"{}",
"{}"
"{}",
)
self.assertEqual(result, "Great job! You got it right.")
mock_func.assert_called_once()
def test_provide_feedback_with_ai_feedback(self):
"""Test provide_feedback function with AI feedback"""
transition = {
"ai_feedback": {
"tokens_for_ai": "Be encouraging"
}
}
with patch.object(app, 'provide_feedback', return_value="Excellent work!") as mock_func:
transition = {"ai_feedback": {"tokens_for_ai": "Be encouraging"}}
with patch.object(
app, "provide_feedback", return_value="Excellent work!"
) as mock_func:
result = app.provide_feedback(
transition,
"correct",
"correct",
"Test question",
"Base instructions",
"Test response",
"English",
"testuser",
"{}",
"{}"
"{}",
)
self.assertEqual(result, "Excellent work!")
mock_func.assert_called_once()
def test_provide_feedback_without_ai_feedback(self):
"""Test provide_feedback function without AI feedback"""
transition = {}
result = app.provide_feedback(
transition,
"correct",
"Test question",
"Test question",
"Base instructions",
"Test response",
"English",
"testuser",
"{}",
"{}"
"{}",
)
self.assertEqual(result, "")
class TestTranslationAndLanguage(unittest.TestCase):
"""Test translation and language handling"""
def test_translate_text_english_bypass(self):
"""Test that English text is not translated"""
text = "Hello, world!"
result = app.translate_text(text, "English")
self.assertEqual(result, text)
# Test case insensitive
result = app.translate_text(text, "english")
result = app.translate_text(text, "english")
self.assertEqual(result, text)
# Test with compound language specification
result = app.translate_text(text, "english please")
self.assertEqual(result, text)
def test_translate_text_other_language(self):
"""Test translation to other languages"""
with patch.object(app, 'translate_text', return_value="Hola, mundo!") as mock_func:
with patch.object(
app, "translate_text", return_value="Hola, mundo!"
) as mock_func:
result = app.translate_text("Hello, world!", "Spanish")
self.assertEqual(result, "Hola, mundo!")
mock_func.assert_called_once_with("Hello, world!", "Spanish")
def test_translate_text_error_handling(self):
"""Test translation error handling"""
with patch.object(app, 'translate_text', return_value="Error: Translation failed") as mock_func:
with patch.object(
app, "translate_text", return_value="Error: Translation failed"
) as mock_func:
result = app.translate_text("Hello, world!", "Spanish")
self.assertIn("Error:", result)
mock_func.assert_called_once_with("Hello, world!", "Spanish")
class TestS3Operations(unittest.TestCase):
"""Test S3 related functions"""
def test_get_s3_client_with_profile(self):
"""Test S3 client creation with profile"""
mock_client = MagicMock()
with patch.object(app, 'get_s3_client', return_value=mock_client) as mock_func:
with patch.object(app, "get_s3_client", return_value=mock_client) as mock_func:
result = app.get_s3_client()
self.assertEqual(result, mock_client)
mock_func.assert_called_once()
def test_get_s3_client_without_profile(self):
"""Test S3 client creation without profile"""
mock_client = MagicMock()
with patch.object(app, 'get_s3_client', return_value=mock_client) as mock_func:
with patch.object(app, "get_s3_client", return_value=mock_client) as mock_func:
result = app.get_s3_client()
self.assertEqual(result, mock_client)
mock_func.assert_called_once()
def test_find_most_recent_code_block(self):
"""Test finding most recent code block in messages"""
# This would require mocking the database and Message model
@ -480,14 +502,14 @@ def test_function():
And some more text after.
"""
# Extract the code block manually to test the logic
lines = test_content.split('\n')
lines = test_content.split("\n")
code_block_lines = []
code_block_started = False
for line in lines:
if line.startswith('```'):
if line.startswith("```"):
if code_block_started:
break
else:
@ -495,17 +517,17 @@ And some more text after.
continue
elif code_block_started:
code_block_lines.append(line)
result = '\n'.join(code_block_lines)
result = "\n".join(code_block_lines)
expected = """def test_function():
return "Hello, World!\""""
self.assertEqual(result, expected)
class TestUtilityFunctions(unittest.TestCase):
"""Test various utility functions"""
def test_group_consecutive_roles(self):
"""Test grouping consecutive roles in messages"""
messages = [
@ -513,24 +535,24 @@ class TestUtilityFunctions(unittest.TestCase):
{"role": "user", "content": "How are you?"},
{"role": "assistant", "content": "I'm fine"},
{"role": "assistant", "content": "Thanks for asking"},
{"role": "user", "content": "Great!"}
{"role": "user", "content": "Great!"},
]
result = app.group_consecutive_roles(messages)
expected = [
{"role": "user", "content": "Hello How are you?"},
{"role": "assistant", "content": "I'm fine Thanks for asking"},
{"role": "user", "content": "Great!"}
{"role": "user", "content": "Great!"},
]
self.assertEqual(result, expected)
def test_group_consecutive_roles_empty(self):
"""Test grouping consecutive roles with empty input"""
result = app.group_consecutive_roles([])
self.assertEqual(result, [])
def test_group_consecutive_roles_single(self):
"""Test grouping consecutive roles with single message"""
messages = [{"role": "user", "content": "Hello"}]
@ -538,5 +560,5 @@ class TestUtilityFunctions(unittest.TestCase):
self.assertEqual(result, messages)
if __name__ == '__main__':
unittest.main(verbosity=2)
if __name__ == "__main__":
unittest.main(verbosity=2)