NeonCharlie-24 commited on
Commit
ae1b6a8
·
unverified ·
1 Parent(s): f735d03

Refactor/unittests (#33)

Browse files

* reorganize /tests directory and convert to unittest.TestCase

* cleaned up old test files and updated GHA command.

* moved test_auto_colors.py into the tests/unit directory.

.github/workflows/unit_tests.yml CHANGED
@@ -27,7 +27,7 @@ jobs:
27
  - name: Run unit tests
28
  working-directory: multi_llm_chatbot_backend
29
  run: |
30
- pytest app/tests/test_persona_config.py app/tests/test_auto_colors.py app/tests/test_lucide_icons.py --junitxml=test-results/results.xml
31
 
32
  - name: Upload test results
33
  if: always()
 
27
  - name: Run unit tests
28
  working-directory: multi_llm_chatbot_backend
29
  run: |
30
+ pytest app/tests/unit/ --junitxml=test-results/results.xml
31
 
32
  - name: Upload test results
33
  if: always()
multi_llm_chatbot_backend/app/tests/conftest.py DELETED
@@ -1,10 +0,0 @@
1
- import pytest
2
- import app.config as config_module
3
-
4
- @pytest.fixture(autouse=True)
5
- def reset_config_singleton():
6
- """Clear the cached settings so each test gets a fresh load."""
7
- config_module._settings = None
8
- yield
9
- config_module._settings = None
10
-
 
 
 
 
 
 
 
 
 
 
 
multi_llm_chatbot_backend/app/tests/integration/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+
multi_llm_chatbot_backend/app/tests/{test_context_propagation.py → integration/test_context_propagation.py} RENAMED
File without changes
multi_llm_chatbot_backend/app/tests/{test_document_upload.py → integration/test_document_upload.py} RENAMED
File without changes
multi_llm_chatbot_backend/app/tests/{test_rag_system.py → integration/test_rag_system.py} RENAMED
File without changes
multi_llm_chatbot_backend/app/tests/{test_mistral.py → scripts/check_mistral.py} RENAMED
File without changes
multi_llm_chatbot_backend/app/tests/{debug_rag.py → scripts/debug_rag.py} RENAMED
File without changes
multi_llm_chatbot_backend/app/tests/test_lucide_icons.py DELETED
@@ -1,74 +0,0 @@
1
- import pytest
2
- from pydantic import ValidationError
3
- from app.utils.lucide_icons import get_valid_icon_names
4
- from app.config import FeatureConfig, ExampleCategory, PersonaItemConfig
5
-
6
-
7
- @pytest.fixture(autouse=True)
8
- def _clear_icon_cache():
9
- get_valid_icon_names.cache_clear()
10
- yield
11
- get_valid_icon_names.cache_clear()
12
-
13
-
14
- # ---------------------------------------------------------------------------
15
- # Icon registry tests
16
- # ---------------------------------------------------------------------------
17
-
18
-
19
- def test_returns_known_icons():
20
- icons = get_valid_icon_names()
21
- assert len(icons) > 1500
22
- assert "HelpCircle" in icons
23
- assert "BookOpen" in icons
24
-
25
-
26
- def test_rejects_invalid_icon():
27
- icons = get_valid_icon_names()
28
- assert "NotARealIcon" not in icons
29
-
30
-
31
- # ---------------------------------------------------------------------------
32
- # Icon validation tests
33
- # ---------------------------------------------------------------------------
34
-
35
-
36
- def test_feature_config_accepts_valid_icon():
37
- feature = FeatureConfig(title="Test", description="desc", icon="BookOpen")
38
- assert feature.icon == "BookOpen"
39
-
40
-
41
- def test_example_category_accepts_valid_icon():
42
- example = ExampleCategory(title="Test", icon="Brain")
43
- assert example.icon == "Brain"
44
-
45
-
46
- def test_persona_item_accepts_valid_icon():
47
- persona = PersonaItemConfig(id="test", name="Test", icon="Heart")
48
- assert persona.icon == "Heart"
49
-
50
-
51
- def test_default_icons_are_valid():
52
- feature = FeatureConfig(title="Test", description="desc")
53
- assert feature.icon == "HelpCircle"
54
-
55
- example = ExampleCategory(title="Test")
56
- assert example.icon == "BookOpen"
57
-
58
- persona = PersonaItemConfig(id="test", name="Test")
59
- assert persona.icon == "HelpCircle"
60
-
61
-
62
- def test_feature_config_rejects_invalid_icon():
63
- with pytest.raises(ValidationError, match="Unknown icon"):
64
- FeatureConfig(title="Test", description="desc", icon="NotARealIcon")
65
-
66
-
67
- def test_example_category_rejects_invalid_icon():
68
- with pytest.raises(ValidationError, match="Unknown icon"):
69
- ExampleCategory(title="Test", icon="TotallyFake")
70
-
71
-
72
- def test_persona_item_rejects_invalid_icon():
73
- with pytest.raises(ValidationError, match="Unknown icon"):
74
- PersonaItemConfig(id="test", name="Test", icon="Nope")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
multi_llm_chatbot_backend/app/tests/test_persona_config.py DELETED
@@ -1,143 +0,0 @@
1
- import pytest
2
- import os, yaml
3
- import app.config
4
- from app.config import load_settings, load_personas_from_dir, PersonasConfig
5
-
6
-
7
- @pytest.fixture(autouse=True)
8
- def _reset_settings_singleton():
9
- app.config._settings = None
10
- yield
11
- app.config._settings = None
12
-
13
-
14
- def _write_config(tmp_path, data: dict) -> str:
15
- path = os.path.join(str(tmp_path), "config.yaml")
16
- with open(path, "w") as f:
17
- yaml.dump(data, f)
18
- return path
19
-
20
-
21
- def _write_persona(directory, filename, data: dict):
22
- path = os.path.join(str(directory), filename)
23
- with open(path, "w") as f:
24
- yaml.dump(data, f)
25
-
26
-
27
- # ---------------------------------------------------------------------------
28
- # load_settings tests
29
- # ---------------------------------------------------------------------------
30
-
31
- def test_loads_personas_from_main_config(tmp_path):
32
- cfg_path = _write_config(tmp_path, {
33
- "personas": {
34
- "base_prompt": "Be helpful.",
35
- "items": [
36
- {"id": "test1", "name": "Test One", "persona_prompt": "You are test1."},
37
- ]
38
- }
39
- })
40
- settings = load_settings(cfg_path)
41
- assert len(settings.personas.items) == 1
42
- assert settings.personas.items[0].id == "test1"
43
-
44
-
45
- def test_load_settings_uses_personas_dir(tmp_path):
46
- """Test that load_settings loads personas from a directory when personas_dir is set."""
47
- # Create a personas subdirectory inside the temp dir
48
- personas_dir = os.path.join(str(tmp_path), "personas")
49
- os.makedirs(personas_dir)
50
-
51
- # Write two persona files into it
52
- _write_persona(personas_dir, "one.yaml", {"id": "one", "name": "One"})
53
- _write_persona(personas_dir, "two.yaml", {"id": "two", "name": "Two"})
54
-
55
- # Write a main config that points to the directory
56
- cfg_path = _write_config(tmp_path, {
57
- "personas": {
58
- "base_prompt": "Be helpful.",
59
- "personas_dir": "personas",
60
- }
61
- })
62
-
63
- settings = load_settings(cfg_path)
64
- assert len(settings.personas.items) == 2
65
- ids = {p.id for p in settings.personas.items}
66
- assert ids == {"one", "two"}
67
-
68
-
69
- def test_bad_persona_does_not_crash_everything(tmp_path):
70
- """Validates that a bad persona in the inline items list causes a
71
- validation error — the directory loader solves this for file-based configs."""
72
- cfg_path = _write_config(tmp_path, {
73
- "personas": {
74
- "items": [
75
- {"id": "good", "name": "Good"},
76
- {"not_an_id": "bad"}, # missing required 'id' and 'name'
77
- ]
78
- }
79
- })
80
- with pytest.raises(Exception):
81
- load_settings(cfg_path)
82
-
83
-
84
- # ---------------------------------------------------------------------------
85
- # load_personas_from_dir tests
86
- # ---------------------------------------------------------------------------
87
-
88
- def test_loads_personas_from_directory(tmp_path):
89
- _write_persona(tmp_path, "one.yaml", {"id": "one", "name": "One"})
90
- _write_persona(tmp_path, "two.yaml", {"id": "two", "name": "Two"})
91
- result = load_personas_from_dir(str(tmp_path))
92
- assert len(result) == 2
93
- ids = {p.id for p in result}
94
- assert ids == {"one", "two"}
95
-
96
-
97
- def test_personas_config_validator_loads_from_dir(tmp_path):
98
- """Test that PersonasConfig's model_validator loads personas automatically."""
99
- _write_persona(tmp_path, "one.yaml", {"id": "one", "name": "One"})
100
- _write_persona(tmp_path, "two.yaml", {"id": "two", "name": "Two"})
101
-
102
- config = PersonasConfig(personas_dir=str(tmp_path))
103
- assert len(config.items) == 2
104
- ids = {p.id for p in config.items}
105
- assert ids == {"one", "two"}
106
-
107
-
108
- def test_skips_invalid_persona_files(tmp_path):
109
- _write_persona(tmp_path, "good.yaml", {"id": "good", "name": "Good"})
110
- _write_persona(tmp_path, "bad.yaml", {"not_id": "bad"})
111
- result = load_personas_from_dir(str(tmp_path))
112
- assert len(result) == 1
113
- assert result[0].id == "good"
114
-
115
-
116
- def test_disabled_persona_excluded(tmp_path):
117
- _write_persona(tmp_path, "on.yaml", {"id": "on", "name": "On"})
118
- _write_persona(tmp_path, "off.yaml", {"id": "off", "name": "Off", "enabled": False})
119
- result = load_personas_from_dir(str(tmp_path))
120
- assert len(result) == 1
121
- assert result[0].id == "on"
122
-
123
-
124
- def test_duplicate_id_rejected(tmp_path):
125
- _write_persona(tmp_path, "a.yaml", {"id": "same", "name": "First"})
126
- _write_persona(tmp_path, "b.yaml", {"id": "same", "name": "Second"})
127
- result = load_personas_from_dir(str(tmp_path))
128
- assert len(result) == 1
129
- assert result[0].name == "First"
130
-
131
-
132
- def test_duplicate_name_rejected(tmp_path):
133
- _write_persona(tmp_path, "a.yaml", {"id": "id_a", "name": "Same Name"})
134
- _write_persona(tmp_path, "b.yaml", {"id": "id_b", "name": "Same Name"})
135
- result = load_personas_from_dir(str(tmp_path))
136
- assert len(result) == 1
137
- assert result[0].id == "id_a"
138
-
139
-
140
- def test_missing_directory_returns_empty(tmp_path):
141
- result = load_personas_from_dir(os.path.join(str(tmp_path), "nonexistent"))
142
- assert result == []
143
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
multi_llm_chatbot_backend/app/tests/unit/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+
multi_llm_chatbot_backend/app/tests/{test_auto_colors.py → unit/test_auto_colors.py} RENAMED
File without changes
multi_llm_chatbot_backend/app/tests/unit/test_lucide_icons.py ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import unittest
2
+ from pydantic import ValidationError
3
+ from app.utils.lucide_icons import get_valid_icon_names
4
+ from app.config import FeatureConfig, ExampleCategory, PersonaItemConfig
5
+
6
+
7
+ class TestIconRegistry(unittest.TestCase):
8
+
9
+ def setUp(self):
10
+ get_valid_icon_names.cache_clear()
11
+
12
+ def tearDown(self):
13
+ get_valid_icon_names.cache_clear()
14
+
15
+ def test_returns_known_icons(self):
16
+ icons = get_valid_icon_names()
17
+ self.assertGreater(len(icons), 1500)
18
+ self.assertIn("HelpCircle", icons)
19
+ self.assertIn("BookOpen", icons)
20
+
21
+ def test_rejects_invalid_icon(self):
22
+ icons = get_valid_icon_names()
23
+ self.assertNotIn("NotARealIcon", icons)
24
+
25
+
26
+ class TestIconValidation(unittest.TestCase):
27
+
28
+ def setUp(self):
29
+ get_valid_icon_names.cache_clear()
30
+
31
+ def tearDown(self):
32
+ get_valid_icon_names.cache_clear()
33
+
34
+ def test_feature_config_accepts_valid_icon(self):
35
+ feature = FeatureConfig(title="Test", description="desc", icon="BookOpen")
36
+ self.assertEqual(feature.icon, "BookOpen")
37
+
38
+ def test_example_category_accepts_valid_icon(self):
39
+ example = ExampleCategory(title="Test", icon="Brain")
40
+ self.assertEqual(example.icon, "Brain")
41
+
42
+ def test_persona_item_accepts_valid_icon(self):
43
+ persona = PersonaItemConfig(id="test", name="Test", icon="Heart")
44
+ self.assertEqual(persona.icon, "Heart")
45
+
46
+ def test_default_icons_are_valid(self):
47
+ feature = FeatureConfig(title="Test", description="desc")
48
+ self.assertEqual(feature.icon, "HelpCircle")
49
+
50
+ example = ExampleCategory(title="Test")
51
+ self.assertEqual(example.icon, "BookOpen")
52
+
53
+ persona = PersonaItemConfig(id="test", name="Test")
54
+ self.assertEqual(persona.icon, "HelpCircle")
55
+
56
+ def test_feature_config_rejects_invalid_icon(self):
57
+ with self.assertRaisesRegex(ValidationError, "Unknown icon"):
58
+ FeatureConfig(title="Test", description="desc", icon="NotARealIcon")
59
+
60
+ def test_example_category_rejects_invalid_icon(self):
61
+ with self.assertRaisesRegex(ValidationError, "Unknown icon"):
62
+ ExampleCategory(title="Test", icon="TotallyFake")
63
+
64
+ def test_persona_item_rejects_invalid_icon(self):
65
+ with self.assertRaisesRegex(ValidationError, "Unknown icon"):
66
+ PersonaItemConfig(id="test", name="Test", icon="Nope")
multi_llm_chatbot_backend/app/tests/unit/test_persona_config.py ADDED
@@ -0,0 +1,140 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import unittest
2
+ import os
3
+ import tempfile
4
+ import yaml
5
+ import app.config
6
+ from app.config import load_settings, load_personas_from_dir, PersonasConfig
7
+
8
+
9
+ def _write_config(tmp_path, data: dict) -> str:
10
+ path = os.path.join(tmp_path, "config.yaml")
11
+ with open(path, "w") as f:
12
+ yaml.dump(data, f)
13
+ return path
14
+
15
+
16
+ def _write_persona(directory, filename, data: dict):
17
+ path = os.path.join(directory, filename)
18
+ with open(path, "w") as f:
19
+ yaml.dump(data, f)
20
+
21
+
22
+ class TestLoadSettings(unittest.TestCase):
23
+
24
+ def setUp(self):
25
+ app.config._settings = None
26
+ self._tmp = tempfile.TemporaryDirectory()
27
+ self.tmp_path = self._tmp.name
28
+
29
+ def tearDown(self):
30
+ app.config._settings = None
31
+ self._tmp.cleanup()
32
+
33
+ def test_loads_personas_from_main_config(self):
34
+ cfg_path = _write_config(self.tmp_path, {
35
+ "personas": {
36
+ "base_prompt": "Be helpful.",
37
+ "items": [
38
+ {"id": "test1", "name": "Test One", "persona_prompt": "You are test1."},
39
+ ]
40
+ }
41
+ })
42
+ settings = load_settings(cfg_path)
43
+ self.assertEqual(len(settings.personas.items), 1)
44
+ self.assertEqual(settings.personas.items[0].id, "test1")
45
+
46
+ def test_uses_personas_dir(self):
47
+ """Test that load_settings loads personas from a directory when personas_dir is set."""
48
+ personas_dir = os.path.join(self.tmp_path, "personas")
49
+ os.makedirs(personas_dir)
50
+
51
+ _write_persona(personas_dir, "one.yaml", {"id": "one", "name": "One"})
52
+ _write_persona(personas_dir, "two.yaml", {"id": "two", "name": "Two"})
53
+
54
+ cfg_path = _write_config(self.tmp_path, {
55
+ "personas": {
56
+ "base_prompt": "Be helpful.",
57
+ "personas_dir": "personas",
58
+ }
59
+ })
60
+
61
+ settings = load_settings(cfg_path)
62
+ self.assertEqual(len(settings.personas.items), 2)
63
+ ids = {p.id for p in settings.personas.items}
64
+ self.assertEqual(ids, {"one", "two"})
65
+
66
+ def test_bad_persona_does_not_crash_everything(self):
67
+ """Validates that a bad persona in the inline items list causes a
68
+ validation error -- the directory loader solves this for file-based configs."""
69
+ cfg_path = _write_config(self.tmp_path, {
70
+ "personas": {
71
+ "items": [
72
+ {"id": "good", "name": "Good"},
73
+ {"not_an_id": "bad"},
74
+ ]
75
+ }
76
+ })
77
+ with self.assertRaises(Exception):
78
+ load_settings(cfg_path)
79
+
80
+
81
+ class TestLoadPersonasFromDir(unittest.TestCase):
82
+
83
+ def setUp(self):
84
+ app.config._settings = None
85
+ self._tmp = tempfile.TemporaryDirectory()
86
+ self.tmp_path = self._tmp.name
87
+
88
+ def tearDown(self):
89
+ app.config._settings = None
90
+ self._tmp.cleanup()
91
+
92
+ def test_loads_personas_from_directory(self):
93
+ _write_persona(self.tmp_path, "one.yaml", {"id": "one", "name": "One"})
94
+ _write_persona(self.tmp_path, "two.yaml", {"id": "two", "name": "Two"})
95
+ result = load_personas_from_dir(self.tmp_path)
96
+ self.assertEqual(len(result), 2)
97
+ ids = {p.id for p in result}
98
+ self.assertEqual(ids, {"one", "two"})
99
+
100
+ def test_personas_config_validator_loads_from_dir(self):
101
+ """Test that PersonasConfig's model_validator loads personas automatically."""
102
+ _write_persona(self.tmp_path, "one.yaml", {"id": "one", "name": "One"})
103
+ _write_persona(self.tmp_path, "two.yaml", {"id": "two", "name": "Two"})
104
+
105
+ config = PersonasConfig(personas_dir=self.tmp_path)
106
+ self.assertEqual(len(config.items), 2)
107
+ ids = {p.id for p in config.items}
108
+ self.assertEqual(ids, {"one", "two"})
109
+
110
+ def test_skips_invalid_persona_files(self):
111
+ _write_persona(self.tmp_path, "good.yaml", {"id": "good", "name": "Good"})
112
+ _write_persona(self.tmp_path, "bad.yaml", {"not_id": "bad"})
113
+ result = load_personas_from_dir(self.tmp_path)
114
+ self.assertEqual(len(result), 1)
115
+ self.assertEqual(result[0].id, "good")
116
+
117
+ def test_disabled_persona_excluded(self):
118
+ _write_persona(self.tmp_path, "on.yaml", {"id": "on", "name": "On"})
119
+ _write_persona(self.tmp_path, "off.yaml", {"id": "off", "name": "Off", "enabled": False})
120
+ result = load_personas_from_dir(self.tmp_path)
121
+ self.assertEqual(len(result), 1)
122
+ self.assertEqual(result[0].id, "on")
123
+
124
+ def test_duplicate_id_rejected(self):
125
+ _write_persona(self.tmp_path, "a.yaml", {"id": "same", "name": "First"})
126
+ _write_persona(self.tmp_path, "b.yaml", {"id": "same", "name": "Second"})
127
+ result = load_personas_from_dir(self.tmp_path)
128
+ self.assertEqual(len(result), 1)
129
+ self.assertEqual(result[0].name, "First")
130
+
131
+ def test_duplicate_name_rejected(self):
132
+ _write_persona(self.tmp_path, "a.yaml", {"id": "id_a", "name": "Same Name"})
133
+ _write_persona(self.tmp_path, "b.yaml", {"id": "id_b", "name": "Same Name"})
134
+ result = load_personas_from_dir(self.tmp_path)
135
+ self.assertEqual(len(result), 1)
136
+ self.assertEqual(result[0].id, "id_a")
137
+
138
+ def test_missing_directory_returns_empty(self):
139
+ result = load_personas_from_dir(os.path.join(self.tmp_path, "nonexistent"))
140
+ self.assertEqual(result, [])