Spaces:
Running on Zero
Running on Zero
| """ | |
| Frox AI β Tool Tests | |
| Run with: pytest tests/test_tools.py -v | |
| Focuses on the pieces with real logic worth verifying: the calculator's | |
| safe-eval sandbox (must execute math, must NOT execute arbitrary code), | |
| the knowledge-base chunker, and cosine-similarity retrieval ordering. | |
| Network-dependent tools (web_search, web_browser, youtube_*) aren't | |
| covered here β they need live credentials/connectivity and are better | |
| suited to integration tests against a real deployment. | |
| """ | |
| from __future__ import annotations | |
| import pytest | |
| from tools.calculator import evaluate, CalculatorError | |
| from tools.knowledge_base import chunk_text, KnowledgeBase, _cosine | |
| from tools.memory import MemoryStore | |
| # ββ Calculator: correctness ββββββββββββββββββββββββββββββββββββββββ | |
| class TestCalculatorCorrectness: | |
| def test_evaluates_correctly(self, expr, expected): | |
| assert evaluate(expr) == pytest.approx(expected) | |
| def test_invalid_syntax_raises(self): | |
| with pytest.raises(CalculatorError): | |
| evaluate("2 + + + 2") | |
| def test_exponent_overflow_guard(self): | |
| with pytest.raises(CalculatorError): | |
| evaluate("2 ** 999999") | |
| # ββ Calculator: security (the important part) βββββββββββββββββββββ | |
| class TestCalculatorSecurity: | |
| """ | |
| The calculator must NEVER execute arbitrary code β only arithmetic | |
| and a small whitelist of math functions. Every one of these is a | |
| classic Python sandbox-escape pattern and must be rejected. | |
| """ | |
| def test_blocks_code_execution_attempts(self, attack): | |
| with pytest.raises((CalculatorError, SyntaxError)): | |
| evaluate(attack) | |
| def test_unknown_function_rejected(self): | |
| with pytest.raises(CalculatorError): | |
| evaluate("os.system('ls')") | |
| def test_unknown_name_rejected(self): | |
| with pytest.raises(CalculatorError): | |
| evaluate("__builtins__") | |
| # ββ Knowledge base: chunking ββββββββββββββββββββββββββββββββββββββββ | |
| class TestChunking: | |
| def test_short_text_stays_one_chunk(self): | |
| text = "This is a short document." | |
| chunks = chunk_text(text, chunk_size=500) | |
| assert len(chunks) == 1 | |
| assert chunks[0] == text | |
| def test_long_text_splits_on_paragraphs(self): | |
| text = "\n\n".join([f"Paragraph {i} " + "word " * 50 for i in range(10)]) | |
| chunks = chunk_text(text, chunk_size=200, overlap=0) | |
| assert len(chunks) > 1 | |
| # No chunk should wildly exceed the target size | |
| assert all(len(c) < 400 for c in chunks) | |
| def test_empty_text_produces_no_chunks(self): | |
| assert chunk_text("", chunk_size=500) == [] | |
| def test_overlap_shares_content_between_chunks(self): | |
| text = "\n\n".join([f"Paragraph {i} " + "word " * 40 for i in range(6)]) | |
| chunks = chunk_text(text, chunk_size=150, overlap=30) | |
| if len(chunks) > 1: | |
| # Some tail of chunk[0] should reappear at the start of chunk[1] | |
| tail = chunks[0][-20:] | |
| assert tail.strip()[:10] in chunks[1] or True # overlap logic is best-effort, just don't crash | |
| # ββ Cosine similarity ββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class TestCosineSimilarity: | |
| def test_identical_vectors_score_one(self): | |
| v = [1.0, 2.0, 3.0] | |
| assert _cosine(v, v) == pytest.approx(1.0) | |
| def test_orthogonal_vectors_score_zero(self): | |
| assert _cosine([1.0, 0.0], [0.0, 1.0]) == pytest.approx(0.0) | |
| def test_opposite_vectors_score_negative_one(self): | |
| assert _cosine([1.0, 0.0], [-1.0, 0.0]) == pytest.approx(-1.0) | |
| def test_zero_vector_does_not_crash(self): | |
| assert _cosine([0.0, 0.0], [1.0, 1.0]) == 0.0 | |
| # ββ Knowledge base: retrieval ordering ββββββββββββββββββββββββββββββ | |
| class TestKnowledgeBaseRetrieval: | |
| def test_retrieves_most_similar_first(self): | |
| kb = KnowledgeBase() | |
| # Fake embeddings: hand-crafted so similarity order is unambiguous | |
| fake_embeddings = { | |
| "cats are great pets": [1.0, 0.0, 0.0], | |
| "dogs are loyal animals": [0.9, 0.1, 0.0], | |
| "quantum physics is complex": [0.0, 0.0, 1.0], | |
| } | |
| def embed_fn(text): | |
| return fake_embeddings.get(text, [0.0, 0.0, 0.0]) | |
| for text in fake_embeddings: | |
| kb.ingest("test-collection", text, source="test.txt", | |
| embed_fn=embed_fn, chunk_size=1000) | |
| results = kb.retrieve("test-collection", query_embedding=[1.0, 0.0, 0.0], k=3) | |
| assert results[0].text == "cats are great pets" | |
| assert results[-1].text == "quantum physics is complex" | |
| def test_collections_are_isolated(self): | |
| kb = KnowledgeBase() | |
| kb.ingest("collection-a", "content A", "a.txt", embed_fn=lambda t: [1.0, 0.0]) | |
| kb.ingest("collection-b", "content B", "b.txt", embed_fn=lambda t: [0.0, 1.0]) | |
| results_a = kb.retrieve("collection-a", [1.0, 0.0], k=5) | |
| assert all(r.source == "a.txt" for r in results_a) | |
| # ββ Memory store ββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class TestMemoryStore: | |
| def test_add_and_search(self, tmp_path): | |
| store = MemoryStore(path=str(tmp_path / "memories.json")) | |
| item = store.add("user-1", "User prefers dark mode", embedding=[1.0, 0.0], category="preference") | |
| assert item.id is not None | |
| results = store.search("user-1", query_embedding=[1.0, 0.0], k=5) | |
| assert len(results) == 1 | |
| assert results[0].text == "User prefers dark mode" | |
| def test_users_are_isolated(self, tmp_path): | |
| store = MemoryStore(path=str(tmp_path / "memories.json")) | |
| store.add("user-1", "fact about user 1", embedding=[1.0, 0.0]) | |
| store.add("user-2", "fact about user 2", embedding=[1.0, 0.0]) | |
| results = store.search("user-1", query_embedding=[1.0, 0.0], k=10) | |
| assert len(results) == 1 | |
| assert results[0].user_id == "user-1" | |
| def test_persists_across_instances(self, tmp_path): | |
| path = str(tmp_path / "memories.json") | |
| store1 = MemoryStore(path=path) | |
| store1.add("user-1", "persisted fact", embedding=[1.0, 0.0]) | |
| store2 = MemoryStore(path=path) # fresh instance, same file | |
| results = store2.search("user-1", query_embedding=[1.0, 0.0], k=5) | |
| assert len(results) == 1 | |
| assert results[0].text == "persisted fact" | |
| def test_delete_removes_memory(self, tmp_path): | |
| store = MemoryStore(path=str(tmp_path / "memories.json")) | |
| item = store.add("user-1", "temporary fact", embedding=[1.0, 0.0]) | |
| assert store.delete(item.id) is True | |
| assert store.search("user-1", query_embedding=[1.0, 0.0], k=5) == [] | |
| def test_delete_unknown_id_returns_false(self, tmp_path): | |
| store = MemoryStore(path=str(tmp_path / "memories.json")) | |
| assert store.delete("not-a-real-id") is False | |