fishertools 0.2.1__py3-none-any.whl → 0.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fishertools/__init__.py +16 -5
- fishertools/errors/__init__.py +11 -3
- fishertools/errors/exception_types.py +282 -0
- fishertools/errors/explainer.py +87 -1
- fishertools/errors/models.py +73 -1
- fishertools/errors/patterns.py +40 -0
- fishertools/examples/cli_example.py +156 -0
- fishertools/examples/learn_example.py +65 -0
- fishertools/examples/logger_example.py +176 -0
- fishertools/examples/menu_example.py +101 -0
- fishertools/examples/storage_example.py +175 -0
- fishertools/input_utils.py +185 -0
- fishertools/learn/__init__.py +19 -2
- fishertools/learn/examples.py +88 -1
- fishertools/learn/knowledge_engine.py +321 -0
- fishertools/learn/repl/__init__.py +19 -0
- fishertools/learn/repl/cli.py +31 -0
- fishertools/learn/repl/code_sandbox.py +229 -0
- fishertools/learn/repl/command_handler.py +544 -0
- fishertools/learn/repl/command_parser.py +165 -0
- fishertools/learn/repl/engine.py +479 -0
- fishertools/learn/repl/models.py +121 -0
- fishertools/learn/repl/session_manager.py +284 -0
- fishertools/learn/repl/test_code_sandbox.py +261 -0
- fishertools/learn/repl/test_code_sandbox_pbt.py +148 -0
- fishertools/learn/repl/test_command_handler.py +224 -0
- fishertools/learn/repl/test_command_handler_pbt.py +189 -0
- fishertools/learn/repl/test_command_parser.py +160 -0
- fishertools/learn/repl/test_command_parser_pbt.py +100 -0
- fishertools/learn/repl/test_engine.py +190 -0
- fishertools/learn/repl/test_session_manager.py +310 -0
- fishertools/learn/repl/test_session_manager_pbt.py +182 -0
- fishertools/learn/test_knowledge_engine.py +241 -0
- fishertools/learn/test_knowledge_engine_pbt.py +180 -0
- fishertools/patterns/__init__.py +46 -0
- fishertools/patterns/cli.py +175 -0
- fishertools/patterns/logger.py +140 -0
- fishertools/patterns/menu.py +99 -0
- fishertools/patterns/storage.py +127 -0
- fishertools/readme_transformer.py +631 -0
- fishertools/safe/__init__.py +6 -1
- fishertools/safe/files.py +329 -1
- fishertools/transform_readme.py +105 -0
- fishertools-0.4.0.dist-info/METADATA +104 -0
- fishertools-0.4.0.dist-info/RECORD +131 -0
- {fishertools-0.2.1.dist-info → fishertools-0.4.0.dist-info}/WHEEL +1 -1
- tests/test_documentation_properties.py +329 -0
- tests/test_documentation_structure.py +349 -0
- tests/test_errors/test_exception_types.py +446 -0
- tests/test_errors/test_exception_types_pbt.py +333 -0
- tests/test_errors/test_patterns.py +52 -0
- tests/test_input_utils/__init__.py +1 -0
- tests/test_input_utils/test_input_utils.py +65 -0
- tests/test_learn/test_examples.py +179 -1
- tests/test_learn/test_explain_properties.py +307 -0
- tests/test_patterns_cli.py +611 -0
- tests/test_patterns_docstrings.py +473 -0
- tests/test_patterns_logger.py +465 -0
- tests/test_patterns_menu.py +440 -0
- tests/test_patterns_storage.py +447 -0
- tests/test_readme_enhancements_v0_3_1.py +2036 -0
- tests/test_readme_transformer/__init__.py +1 -0
- tests/test_readme_transformer/test_readme_infrastructure.py +1023 -0
- tests/test_readme_transformer/test_transform_readme_integration.py +431 -0
- tests/test_safe/test_files.py +726 -1
- fishertools-0.2.1.dist-info/METADATA +0 -256
- fishertools-0.2.1.dist-info/RECORD +0 -81
- {fishertools-0.2.1.dist-info → fishertools-0.4.0.dist-info}/licenses/LICENSE +0 -0
- {fishertools-0.2.1.dist-info → fishertools-0.4.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Property-based tests for CodeSandbox using Hypothesis.
|
|
3
|
+
|
|
4
|
+
**Validates: Requirements 4.1, 4.4**
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
from hypothesis import given, strategies as st, assume
|
|
9
|
+
from fishertools.learn.repl.code_sandbox import CodeSandbox
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TestCodeSandboxProperties:
|
|
13
|
+
"""Property-based tests for code execution safety."""
|
|
14
|
+
|
|
15
|
+
@given(st.text(min_size=1))
|
|
16
|
+
def test_execution_always_returns_tuple(self, code):
|
|
17
|
+
"""
|
|
18
|
+
For any code input, execute should always return a tuple of (bool, str).
|
|
19
|
+
|
|
20
|
+
**Validates: Requirements 4.1, 4.4**
|
|
21
|
+
"""
|
|
22
|
+
sandbox = CodeSandbox()
|
|
23
|
+
result = sandbox.execute(code)
|
|
24
|
+
|
|
25
|
+
assert isinstance(result, tuple), "Result should be a tuple"
|
|
26
|
+
assert len(result) == 2, "Result should have 2 elements"
|
|
27
|
+
assert isinstance(result[0], bool), "First element should be bool"
|
|
28
|
+
assert isinstance(result[1], str), "Second element should be str"
|
|
29
|
+
|
|
30
|
+
@given(st.text(min_size=1))
|
|
31
|
+
def test_execution_never_crashes(self, code):
|
|
32
|
+
"""
|
|
33
|
+
For any code input, execute should never crash the sandbox.
|
|
34
|
+
|
|
35
|
+
**Validates: Requirements 4.1, 4.4**
|
|
36
|
+
"""
|
|
37
|
+
sandbox = CodeSandbox()
|
|
38
|
+
try:
|
|
39
|
+
result = sandbox.execute(code)
|
|
40
|
+
# Should always return successfully
|
|
41
|
+
assert result is not None
|
|
42
|
+
except Exception as e:
|
|
43
|
+
pytest.fail(f"Sandbox crashed with: {e}")
|
|
44
|
+
|
|
45
|
+
@given(st.text(min_size=1))
|
|
46
|
+
def test_no_file_operations_allowed(self, code):
|
|
47
|
+
"""
|
|
48
|
+
For any code containing file operations, execution should fail safely.
|
|
49
|
+
|
|
50
|
+
**Validates: Requirements 4.1, 4.4**
|
|
51
|
+
"""
|
|
52
|
+
sandbox = CodeSandbox()
|
|
53
|
+
|
|
54
|
+
# Check if code contains file operations
|
|
55
|
+
dangerous_patterns = ["open(", "read(", "write(", "file("]
|
|
56
|
+
has_dangerous = any(pattern in code.lower() for pattern in dangerous_patterns)
|
|
57
|
+
|
|
58
|
+
if has_dangerous:
|
|
59
|
+
success, output = sandbox.execute(code)
|
|
60
|
+
# Should either fail or not actually perform file operations
|
|
61
|
+
# We can't guarantee it fails because the code might have syntax errors
|
|
62
|
+
# But we can verify the sandbox doesn't crash
|
|
63
|
+
assert isinstance(success, bool)
|
|
64
|
+
assert isinstance(output, str)
|
|
65
|
+
|
|
66
|
+
@given(st.text(min_size=1))
|
|
67
|
+
def test_no_imports_allowed(self, code):
|
|
68
|
+
"""
|
|
69
|
+
For any code containing imports, execution should fail safely.
|
|
70
|
+
|
|
71
|
+
**Validates: Requirements 4.1, 4.4**
|
|
72
|
+
"""
|
|
73
|
+
sandbox = CodeSandbox()
|
|
74
|
+
|
|
75
|
+
# Check if code contains imports
|
|
76
|
+
import_patterns = ["import ", "from "]
|
|
77
|
+
has_import = any(pattern in code.lower() for pattern in import_patterns)
|
|
78
|
+
|
|
79
|
+
if has_import:
|
|
80
|
+
success, output = sandbox.execute(code)
|
|
81
|
+
# Should either fail or not actually perform imports
|
|
82
|
+
assert isinstance(success, bool)
|
|
83
|
+
assert isinstance(output, str)
|
|
84
|
+
|
|
85
|
+
@given(st.text(min_size=1))
|
|
86
|
+
def test_execution_is_deterministic(self, code):
|
|
87
|
+
"""
|
|
88
|
+
For any code input, executing it twice should produce the same result.
|
|
89
|
+
|
|
90
|
+
**Validates: Requirements 4.1, 4.4**
|
|
91
|
+
"""
|
|
92
|
+
sandbox = CodeSandbox()
|
|
93
|
+
|
|
94
|
+
# Skip code that might have non-deterministic behavior
|
|
95
|
+
if any(x in code.lower() for x in ["random", "time", "datetime"]):
|
|
96
|
+
return
|
|
97
|
+
|
|
98
|
+
result1 = sandbox.execute(code)
|
|
99
|
+
result2 = sandbox.execute(code)
|
|
100
|
+
|
|
101
|
+
# Results should be identical
|
|
102
|
+
assert result1 == result2, f"Execution not deterministic for: {code}"
|
|
103
|
+
|
|
104
|
+
@given(st.text(min_size=1))
|
|
105
|
+
def test_output_is_string(self, code):
|
|
106
|
+
"""
|
|
107
|
+
For any code execution, the output should always be a string.
|
|
108
|
+
|
|
109
|
+
**Validates: Requirements 4.1, 4.4**
|
|
110
|
+
"""
|
|
111
|
+
sandbox = CodeSandbox()
|
|
112
|
+
success, output = sandbox.execute(code)
|
|
113
|
+
|
|
114
|
+
assert isinstance(output, str), "Output should always be a string"
|
|
115
|
+
# Output should not be None
|
|
116
|
+
assert output is not None, "Output should not be None"
|
|
117
|
+
|
|
118
|
+
@given(st.text(min_size=1))
|
|
119
|
+
def test_success_flag_is_boolean(self, code):
|
|
120
|
+
"""
|
|
121
|
+
For any code execution, the success flag should always be a boolean.
|
|
122
|
+
|
|
123
|
+
**Validates: Requirements 4.1, 4.4**
|
|
124
|
+
"""
|
|
125
|
+
sandbox = CodeSandbox()
|
|
126
|
+
success, output = sandbox.execute(code)
|
|
127
|
+
|
|
128
|
+
assert isinstance(success, bool), "Success flag should be boolean"
|
|
129
|
+
assert success in [True, False], "Success should be True or False"
|
|
130
|
+
|
|
131
|
+
@given(st.text(min_size=1))
|
|
132
|
+
def test_dangerous_functions_blocked(self, code):
|
|
133
|
+
"""
|
|
134
|
+
For any code containing dangerous functions, execution should fail safely.
|
|
135
|
+
|
|
136
|
+
**Validates: Requirements 4.1, 4.4**
|
|
137
|
+
"""
|
|
138
|
+
sandbox = CodeSandbox()
|
|
139
|
+
|
|
140
|
+
# Check if code contains dangerous functions
|
|
141
|
+
dangerous_funcs = ["exec(", "eval(", "compile(", "globals(", "locals("]
|
|
142
|
+
has_dangerous = any(func in code.lower() for func in dangerous_funcs)
|
|
143
|
+
|
|
144
|
+
if has_dangerous:
|
|
145
|
+
success, output = sandbox.execute(code)
|
|
146
|
+
# Should fail or not execute the dangerous function
|
|
147
|
+
assert isinstance(success, bool)
|
|
148
|
+
assert isinstance(output, str)
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Unit tests for the CommandHandler class.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
import tempfile
|
|
7
|
+
from fishertools.learn.knowledge_engine import KnowledgeEngine
|
|
8
|
+
from fishertools.learn.repl.command_handler import CommandHandler
|
|
9
|
+
from fishertools.learn.repl.session_manager import SessionManager
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@pytest.fixture
|
|
13
|
+
def engine():
|
|
14
|
+
"""Fixture to provide a Knowledge Engine instance."""
|
|
15
|
+
return KnowledgeEngine()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@pytest.fixture
|
|
19
|
+
def session_manager():
|
|
20
|
+
"""Fixture to provide a SessionManager instance."""
|
|
21
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
22
|
+
yield SessionManager(tmpdir)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@pytest.fixture
|
|
26
|
+
def handler(engine, session_manager):
|
|
27
|
+
"""Fixture to provide a CommandHandler instance."""
|
|
28
|
+
return CommandHandler(engine, session_manager)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class TestCommandHandlerList:
|
|
32
|
+
"""Test /list command."""
|
|
33
|
+
|
|
34
|
+
def test_handle_list_returns_string(self, handler):
|
|
35
|
+
"""Test that /list returns a string."""
|
|
36
|
+
output = handler.handle_list()
|
|
37
|
+
assert isinstance(output, str)
|
|
38
|
+
assert len(output) > 0
|
|
39
|
+
|
|
40
|
+
def test_handle_list_contains_topics(self, handler):
|
|
41
|
+
"""Test that /list output contains topics."""
|
|
42
|
+
output = handler.handle_list()
|
|
43
|
+
assert "Available Topics" in output or "📚" in output
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class TestCommandHandlerSearch:
|
|
47
|
+
"""Test /search command."""
|
|
48
|
+
|
|
49
|
+
def test_handle_search_empty_keyword(self, handler):
|
|
50
|
+
"""Test /search with empty keyword."""
|
|
51
|
+
output = handler.handle_search("")
|
|
52
|
+
assert "Please provide" in output or "keyword" in output.lower()
|
|
53
|
+
|
|
54
|
+
def test_handle_search_valid_keyword(self, handler, engine):
|
|
55
|
+
"""Test /search with valid keyword."""
|
|
56
|
+
# Get a topic to search for
|
|
57
|
+
topics = engine.list_topics()
|
|
58
|
+
if topics:
|
|
59
|
+
keyword = topics[0].lower()
|
|
60
|
+
output = handler.handle_search(keyword)
|
|
61
|
+
assert isinstance(output, str)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class TestCommandHandlerRandom:
|
|
65
|
+
"""Test /random command."""
|
|
66
|
+
|
|
67
|
+
def test_handle_random_returns_string(self, handler):
|
|
68
|
+
"""Test that /random returns a string."""
|
|
69
|
+
output = handler.handle_random()
|
|
70
|
+
assert isinstance(output, str)
|
|
71
|
+
assert "Random" in output or "🎲" in output
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class TestCommandHandlerCategories:
|
|
75
|
+
"""Test /categories command."""
|
|
76
|
+
|
|
77
|
+
def test_handle_categories_returns_string(self, handler):
|
|
78
|
+
"""Test that /categories returns a string."""
|
|
79
|
+
output = handler.handle_categories()
|
|
80
|
+
assert isinstance(output, str)
|
|
81
|
+
assert "Categories" in output or "📂" in output
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class TestCommandHandlerCategory:
|
|
85
|
+
"""Test /category command."""
|
|
86
|
+
|
|
87
|
+
def test_handle_category_empty_name(self, handler):
|
|
88
|
+
"""Test /category with empty name."""
|
|
89
|
+
output = handler.handle_category("")
|
|
90
|
+
assert "Please provide" in output or "category" in output.lower()
|
|
91
|
+
|
|
92
|
+
def test_handle_category_valid_name(self, handler, engine):
|
|
93
|
+
"""Test /category with valid category name."""
|
|
94
|
+
categories = list(engine.categories.keys())
|
|
95
|
+
if categories:
|
|
96
|
+
output = handler.handle_category(categories[0])
|
|
97
|
+
assert isinstance(output, str)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class TestCommandHandlerPath:
|
|
101
|
+
"""Test /path command."""
|
|
102
|
+
|
|
103
|
+
def test_handle_path_returns_string(self, handler):
|
|
104
|
+
"""Test that /path returns a string."""
|
|
105
|
+
output = handler.handle_path()
|
|
106
|
+
assert isinstance(output, str)
|
|
107
|
+
assert "Learning Path" in output or "🛤️" in output
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class TestCommandHandlerProgress:
|
|
111
|
+
"""Test /progress command."""
|
|
112
|
+
|
|
113
|
+
def test_handle_progress_returns_string(self, handler):
|
|
114
|
+
"""Test that /progress returns a string."""
|
|
115
|
+
output = handler.handle_progress()
|
|
116
|
+
assert isinstance(output, str)
|
|
117
|
+
assert "Progress" in output or "📊" in output
|
|
118
|
+
|
|
119
|
+
def test_handle_progress_shows_stats(self, handler, session_manager):
|
|
120
|
+
"""Test that /progress shows statistics."""
|
|
121
|
+
session_manager.mark_topic_viewed("Lists")
|
|
122
|
+
output = handler.handle_progress()
|
|
123
|
+
assert "Topics viewed" in output or "viewed" in output.lower()
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class TestCommandHandlerStats:
|
|
127
|
+
"""Test /stats command."""
|
|
128
|
+
|
|
129
|
+
def test_handle_stats_returns_string(self, handler):
|
|
130
|
+
"""Test that /stats returns a string."""
|
|
131
|
+
output = handler.handle_stats()
|
|
132
|
+
assert isinstance(output, str)
|
|
133
|
+
assert "Statistics" in output or "📈" in output
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class TestCommandHandlerHelp:
|
|
137
|
+
"""Test /help command."""
|
|
138
|
+
|
|
139
|
+
def test_handle_help_returns_string(self, handler):
|
|
140
|
+
"""Test that /help returns a string."""
|
|
141
|
+
output = handler.handle_help()
|
|
142
|
+
assert isinstance(output, str)
|
|
143
|
+
assert "Commands" in output or "help" in output.lower()
|
|
144
|
+
|
|
145
|
+
def test_handle_help_specific_command(self, handler):
|
|
146
|
+
"""Test /help for specific command."""
|
|
147
|
+
output = handler.handle_help("list")
|
|
148
|
+
assert isinstance(output, str)
|
|
149
|
+
assert "list" in output.lower()
|
|
150
|
+
|
|
151
|
+
def test_handle_help_invalid_command(self, handler):
|
|
152
|
+
"""Test /help for invalid command."""
|
|
153
|
+
output = handler.handle_help("invalid_command_xyz")
|
|
154
|
+
assert isinstance(output, str)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class TestCommandHandlerCommands:
|
|
158
|
+
"""Test /commands command."""
|
|
159
|
+
|
|
160
|
+
def test_handle_commands_returns_string(self, handler):
|
|
161
|
+
"""Test that /commands returns a string."""
|
|
162
|
+
output = handler.handle_commands()
|
|
163
|
+
assert isinstance(output, str)
|
|
164
|
+
assert "Commands" in output or "📋" in output
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
class TestCommandHandlerAbout:
|
|
168
|
+
"""Test /about command."""
|
|
169
|
+
|
|
170
|
+
def test_handle_about_returns_string(self, handler):
|
|
171
|
+
"""Test that /about returns a string."""
|
|
172
|
+
output = handler.handle_about()
|
|
173
|
+
assert isinstance(output, str)
|
|
174
|
+
assert "About" in output or "ℹ️" in output
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
class TestCommandHandlerHint:
|
|
178
|
+
"""Test /hint command."""
|
|
179
|
+
|
|
180
|
+
def test_handle_hint_no_topic(self, handler):
|
|
181
|
+
"""Test /hint with no current topic."""
|
|
182
|
+
output = handler.handle_hint(None)
|
|
183
|
+
assert "No current topic" in output or "❌" in output
|
|
184
|
+
|
|
185
|
+
def test_handle_hint_invalid_topic(self, handler):
|
|
186
|
+
"""Test /hint with invalid topic."""
|
|
187
|
+
output = handler.handle_hint("NonexistentTopic123")
|
|
188
|
+
assert "not found" in output.lower() or "❌" in output
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
class TestCommandHandlerTip:
|
|
192
|
+
"""Test /tip command."""
|
|
193
|
+
|
|
194
|
+
def test_handle_tip_returns_string(self, handler):
|
|
195
|
+
"""Test that /tip returns a string."""
|
|
196
|
+
output = handler.handle_tip()
|
|
197
|
+
assert isinstance(output, str)
|
|
198
|
+
assert "💡" in output
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
class TestCommandHandlerRelated:
|
|
202
|
+
"""Test /related command."""
|
|
203
|
+
|
|
204
|
+
def test_handle_related_no_topic(self, handler):
|
|
205
|
+
"""Test /related with no current topic."""
|
|
206
|
+
output = handler.handle_related(None)
|
|
207
|
+
assert "No current topic" in output or "❌" in output
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
class TestCommandHandlerFormatTopic:
|
|
211
|
+
"""Test topic formatting."""
|
|
212
|
+
|
|
213
|
+
def test_format_topic_display_invalid_topic(self, handler):
|
|
214
|
+
"""Test formatting invalid topic."""
|
|
215
|
+
output = handler.format_topic_display("NonexistentTopic123")
|
|
216
|
+
assert "not found" in output.lower() or "❌" in output
|
|
217
|
+
|
|
218
|
+
def test_format_topic_display_valid_topic(self, handler, engine):
|
|
219
|
+
"""Test formatting valid topic."""
|
|
220
|
+
topics = engine.list_topics()
|
|
221
|
+
if topics:
|
|
222
|
+
output = handler.format_topic_display(topics[0])
|
|
223
|
+
assert isinstance(output, str)
|
|
224
|
+
assert topics[0] in output
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Property-based tests for CommandHandler using Hypothesis.
|
|
3
|
+
|
|
4
|
+
**Validates: Requirements 2.1, 2.2**
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
import tempfile
|
|
9
|
+
from hypothesis import given, strategies as st, assume, settings, HealthCheck
|
|
10
|
+
from fishertools.learn.knowledge_engine import KnowledgeEngine
|
|
11
|
+
from fishertools.learn.repl.command_handler import CommandHandler
|
|
12
|
+
from fishertools.learn.repl.session_manager import SessionManager
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class TestCommandHandlerProperties:
|
|
16
|
+
"""Property-based tests for command handler."""
|
|
17
|
+
|
|
18
|
+
def test_search_result_relevance(self):
|
|
19
|
+
"""
|
|
20
|
+
Property 3: Search Result Relevance
|
|
21
|
+
|
|
22
|
+
For any search keyword and topic database, all returned search results
|
|
23
|
+
should contain the keyword in either the topic name or description (case-insensitive).
|
|
24
|
+
|
|
25
|
+
**Validates: Requirements 2.2**
|
|
26
|
+
"""
|
|
27
|
+
engine = KnowledgeEngine()
|
|
28
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
29
|
+
manager = SessionManager(tmpdir)
|
|
30
|
+
handler = CommandHandler(engine, manager)
|
|
31
|
+
|
|
32
|
+
# Get all topics to search for
|
|
33
|
+
all_topics = engine.list_topics()
|
|
34
|
+
if len(all_topics) > 0:
|
|
35
|
+
# Use a topic name as keyword
|
|
36
|
+
keyword = all_topics[0].lower()
|
|
37
|
+
output = handler.handle_search(keyword)
|
|
38
|
+
|
|
39
|
+
# Output should be a string
|
|
40
|
+
assert isinstance(output, str)
|
|
41
|
+
# If results are found, they should contain the keyword
|
|
42
|
+
if "Found" in output and "0 topic" not in output:
|
|
43
|
+
# Results were found, verify they contain the keyword
|
|
44
|
+
assert keyword in output.lower() or "search" in output.lower()
|
|
45
|
+
|
|
46
|
+
def test_topic_list_completeness(self):
|
|
47
|
+
"""
|
|
48
|
+
Property 11: Topic List Completeness
|
|
49
|
+
|
|
50
|
+
For any /list command, all topics from the Knowledge Engine should appear
|
|
51
|
+
in the output organized by their categories.
|
|
52
|
+
|
|
53
|
+
**Validates: Requirements 2.1**
|
|
54
|
+
"""
|
|
55
|
+
engine = KnowledgeEngine()
|
|
56
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
57
|
+
manager = SessionManager(tmpdir)
|
|
58
|
+
handler = CommandHandler(engine, manager)
|
|
59
|
+
|
|
60
|
+
output = handler.handle_list()
|
|
61
|
+
|
|
62
|
+
# Output should be a string
|
|
63
|
+
assert isinstance(output, str)
|
|
64
|
+
|
|
65
|
+
# Get all topics
|
|
66
|
+
all_topics = engine.list_topics()
|
|
67
|
+
|
|
68
|
+
# Output should mention the total number of topics
|
|
69
|
+
assert "Total topics" in output or "topics" in output.lower()
|
|
70
|
+
|
|
71
|
+
# Output should contain category information
|
|
72
|
+
categories = list(engine.categories.keys())
|
|
73
|
+
if categories:
|
|
74
|
+
# At least one category should be mentioned
|
|
75
|
+
assert any(cat in output for cat in categories) or "📂" in output
|
|
76
|
+
|
|
77
|
+
@given(st.text(min_size=1, max_size=100))
|
|
78
|
+
@settings(suppress_health_check=[HealthCheck.function_scoped_fixture])
|
|
79
|
+
def test_search_always_returns_string(self, keyword):
|
|
80
|
+
"""
|
|
81
|
+
For any search keyword, the search handler should always return a string.
|
|
82
|
+
|
|
83
|
+
**Validates: Requirements 2.2**
|
|
84
|
+
"""
|
|
85
|
+
engine = KnowledgeEngine()
|
|
86
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
87
|
+
manager = SessionManager(tmpdir)
|
|
88
|
+
handler = CommandHandler(engine, manager)
|
|
89
|
+
|
|
90
|
+
output = handler.handle_search(keyword)
|
|
91
|
+
assert isinstance(output, str)
|
|
92
|
+
assert len(output) > 0
|
|
93
|
+
|
|
94
|
+
@given(st.text(min_size=1, max_size=100))
|
|
95
|
+
@settings(suppress_health_check=[HealthCheck.function_scoped_fixture])
|
|
96
|
+
def test_category_always_returns_string(self, category_name):
|
|
97
|
+
"""
|
|
98
|
+
For any category name, the category handler should always return a string.
|
|
99
|
+
|
|
100
|
+
**Validates: Requirements 2.1**
|
|
101
|
+
"""
|
|
102
|
+
engine = KnowledgeEngine()
|
|
103
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
104
|
+
manager = SessionManager(tmpdir)
|
|
105
|
+
handler = CommandHandler(engine, manager)
|
|
106
|
+
|
|
107
|
+
output = handler.handle_category(category_name)
|
|
108
|
+
assert isinstance(output, str)
|
|
109
|
+
assert len(output) > 0
|
|
110
|
+
|
|
111
|
+
def test_list_output_structure(self):
|
|
112
|
+
"""
|
|
113
|
+
For /list command, output should have consistent structure.
|
|
114
|
+
|
|
115
|
+
**Validates: Requirements 2.1**
|
|
116
|
+
"""
|
|
117
|
+
engine = KnowledgeEngine()
|
|
118
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
119
|
+
manager = SessionManager(tmpdir)
|
|
120
|
+
handler = CommandHandler(engine, manager)
|
|
121
|
+
|
|
122
|
+
output = handler.handle_list()
|
|
123
|
+
|
|
124
|
+
# Should contain header
|
|
125
|
+
assert "Available Topics" in output or "📚" in output
|
|
126
|
+
|
|
127
|
+
# Should contain instructions
|
|
128
|
+
assert "Type" in output or "topic" in output.lower()
|
|
129
|
+
|
|
130
|
+
def test_search_output_structure(self):
|
|
131
|
+
"""
|
|
132
|
+
For /search command, output should have consistent structure.
|
|
133
|
+
|
|
134
|
+
**Validates: Requirements 2.2**
|
|
135
|
+
"""
|
|
136
|
+
engine = KnowledgeEngine()
|
|
137
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
138
|
+
manager = SessionManager(tmpdir)
|
|
139
|
+
handler = CommandHandler(engine, manager)
|
|
140
|
+
|
|
141
|
+
# Get a valid keyword
|
|
142
|
+
topics = engine.list_topics()
|
|
143
|
+
if topics:
|
|
144
|
+
keyword = topics[0].lower()
|
|
145
|
+
output = handler.handle_search(keyword)
|
|
146
|
+
|
|
147
|
+
# Should contain search indicator
|
|
148
|
+
assert "search" in output.lower() or "🔍" in output
|
|
149
|
+
|
|
150
|
+
# Should contain result count
|
|
151
|
+
assert "Found" in output or "topic" in output.lower()
|
|
152
|
+
|
|
153
|
+
def test_categories_output_structure(self):
|
|
154
|
+
"""
|
|
155
|
+
For /categories command, output should have consistent structure.
|
|
156
|
+
|
|
157
|
+
**Validates: Requirements 2.1**
|
|
158
|
+
"""
|
|
159
|
+
engine = KnowledgeEngine()
|
|
160
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
161
|
+
manager = SessionManager(tmpdir)
|
|
162
|
+
handler = CommandHandler(engine, manager)
|
|
163
|
+
|
|
164
|
+
output = handler.handle_categories()
|
|
165
|
+
|
|
166
|
+
# Should contain header
|
|
167
|
+
assert "Categories" in output or "📂" in output
|
|
168
|
+
|
|
169
|
+
# Should contain count information
|
|
170
|
+
assert "Total categories" in output or "categories" in output.lower()
|
|
171
|
+
|
|
172
|
+
def test_path_output_structure(self):
|
|
173
|
+
"""
|
|
174
|
+
For /path command, output should have consistent structure.
|
|
175
|
+
|
|
176
|
+
**Validates: Requirements 2.1**
|
|
177
|
+
"""
|
|
178
|
+
engine = KnowledgeEngine()
|
|
179
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
180
|
+
manager = SessionManager(tmpdir)
|
|
181
|
+
handler = CommandHandler(engine, manager)
|
|
182
|
+
|
|
183
|
+
output = handler.handle_path()
|
|
184
|
+
|
|
185
|
+
# Should contain header
|
|
186
|
+
assert "Learning Path" in output or "🛤️" in output
|
|
187
|
+
|
|
188
|
+
# Should contain topic count
|
|
189
|
+
assert "Total topics" in output or "topics" in output.lower()
|