-
Notifications
You must be signed in to change notification settings - Fork 7.6k
test: expand coverage from 20 to 92 tests, migrate to uv #131
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,169 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| conftest.py — Shared fixtures for MemPalace tests. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Provides isolated palace and knowledge graph instances so tests never | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| touch the user's real data or leak temp files on failure. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| HOME is redirected to a temp directory at module load time — before any | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| mempalace imports — so that module-level initialisations (e.g. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ``_kg = KnowledgeGraph()`` in mcp_server) write to a throwaway location | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| instead of the real user profile. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import os | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import shutil | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import tempfile | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # ── Isolate HOME before any mempalace imports ────────────────────────── | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _original_env = {} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _session_tmp = tempfile.mkdtemp(prefix="mempalace_session_") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for _var in ("HOME", "USERPROFILE", "HOMEDRIVE", "HOMEPATH"): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _original_env[_var] = os.environ.get(_var) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| os.environ["HOME"] = _session_tmp | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| os.environ["USERPROFILE"] = _session_tmp | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| os.environ["HOMEDRIVE"] = os.path.splitdrive(_session_tmp)[0] or "C:" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| os.environ["HOMEPATH"] = os.path.splitdrive(_session_tmp)[1] or _session_tmp | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Now it is safe to import mempalace modules that trigger initialisation. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import chromadb # noqa: E402 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import pytest # noqa: E402 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from mempalace.config import MempalaceConfig # noqa: E402 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from mempalace.knowledge_graph import KnowledgeGraph # noqa: E402 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+32
to
+36
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from mempalace.config import MempalaceConfig | |
| from mempalace.knowledge_graph import KnowledgeGraph | |
| _TEST_HOME_DIR = tempfile.mkdtemp(prefix="mempalace_home_") | |
| _ORIGINAL_HOME_ENV = { | |
| "HOME": os.environ.get("HOME"), | |
| "USERPROFILE": os.environ.get("USERPROFILE"), | |
| "HOMEDRIVE": os.environ.get("HOMEDRIVE"), | |
| "HOMEPATH": os.environ.get("HOMEPATH"), | |
| } | |
| os.environ["HOME"] = _TEST_HOME_DIR | |
| os.environ["USERPROFILE"] = _TEST_HOME_DIR | |
| drive, path = os.path.splitdrive(_TEST_HOME_DIR) | |
| os.environ["HOMEDRIVE"] = drive or "" | |
| os.environ["HOMEPATH"] = path or _TEST_HOME_DIR | |
| from mempalace.config import MempalaceConfig | |
| from mempalace.knowledge_graph import KnowledgeGraph | |
| @pytest.fixture(scope="session", autouse=True) | |
| def isolated_home_dir(): | |
| """Redirect user-home expansion to a temp directory for the test session.""" | |
| yield _TEST_HOME_DIR | |
| for env_name, original_value in _ORIGINAL_HOME_ENV.items(): | |
| if original_value is None: | |
| os.environ.pop(env_name, None) | |
| else: | |
| os.environ[env_name] = original_value | |
| shutil.rmtree(_TEST_HOME_DIR, ignore_errors=True) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,157 @@ | ||
| """ | ||
| test_dialect.py — Tests for the AAAK Dialect compression system. | ||
|
|
||
| Covers plain text compression, entity detection, emotion detection, | ||
| topic extraction, key sentence extraction, zettel encoding, and stats. | ||
| """ | ||
|
|
||
| from mempalace.dialect import Dialect | ||
|
|
||
|
|
||
| class TestPlainTextCompression: | ||
| def test_compress_basic(self): | ||
| d = Dialect() | ||
| result = d.compress("We decided to use GraphQL instead of REST for the API layer.") | ||
| assert isinstance(result, str) | ||
| assert len(result) > 0 | ||
| # AAAK format uses pipe-separated fields | ||
| assert "|" in result | ||
|
|
||
| def test_compress_with_metadata(self): | ||
| d = Dialect() | ||
| result = d.compress( | ||
| "Authentication now uses JWT tokens.", | ||
| metadata={"wing": "project", "room": "backend", "source_file": "auth.py"}, | ||
| ) | ||
| assert "project" in result | ||
| assert "backend" in result | ||
|
|
||
| def test_compress_produces_entity_codes(self): | ||
| d = Dialect(entities={"Alice": "ALC", "Bob": "BOB"}) | ||
| result = d.compress("Alice told Bob about the new deployment strategy.") | ||
| assert "ALC" in result or "BOB" in result | ||
|
|
||
| def test_compress_empty_text(self): | ||
| d = Dialect() | ||
| result = d.compress("") | ||
| assert isinstance(result, str) | ||
|
|
||
|
|
||
| class TestEntityDetection: | ||
| def test_known_entities(self): | ||
| d = Dialect(entities={"Alice": "ALC"}) | ||
| found = d._detect_entities_in_text("Alice went to the store.") | ||
| assert "ALC" in found | ||
|
|
||
| def test_auto_code_unknown_entities(self): | ||
| d = Dialect() | ||
| found = d._detect_entities_in_text("I spoke with Bernardo about the project today.") | ||
| assert any(code for code in found if len(code) == 3) | ||
|
|
||
| def test_skip_names(self): | ||
| d = Dialect(entities={"Gandalf": "GAN"}, skip_names=["Gandalf"]) | ||
| code = d.encode_entity("Gandalf") | ||
| assert code is None | ||
|
|
||
|
|
||
| class TestEmotionDetection: | ||
| def test_detect_emotions(self): | ||
| d = Dialect() | ||
| emotions = d._detect_emotions("I'm really excited and happy about this breakthrough!") | ||
| assert len(emotions) > 0 | ||
|
|
||
| def test_max_three_emotions(self): | ||
| d = Dialect() | ||
| text = "I feel scared, happy, angry, surprised, disgusted, and confused." | ||
| emotions = d._detect_emotions(text) | ||
| assert len(emotions) <= 3 | ||
|
|
||
|
|
||
| class TestTopicExtraction: | ||
| def test_extract_topics(self): | ||
| d = Dialect() | ||
| topics = d._extract_topics( | ||
| "The Python authentication server uses PostgreSQL for storage " | ||
| "and Redis for caching sessions." | ||
| ) | ||
| assert len(topics) > 0 | ||
| assert len(topics) <= 3 | ||
|
|
||
| def test_boosts_technical_terms(self): | ||
| d = Dialect() | ||
| topics = d._extract_topics("GraphQL vs REST: we chose GraphQL for the new API endpoint.") | ||
| # "graphql" should appear since it's mentioned twice + capitalized | ||
| topic_lower = [t.lower() for t in topics] | ||
| assert "graphql" in topic_lower | ||
|
|
||
|
|
||
| class TestKeySentenceExtraction: | ||
| def test_extract_key_sentence(self): | ||
| d = Dialect() | ||
| text = ( | ||
| "The server runs on port 3000. " | ||
| "We decided to use PostgreSQL instead of MongoDB. " | ||
| "The config file needs updating." | ||
| ) | ||
| key = d._extract_key_sentence(text) | ||
| assert "decided" in key.lower() or "instead" in key.lower() | ||
|
|
||
| def test_truncates_long_sentences(self): | ||
| d = Dialect() | ||
| text = "a " * 100 # very long | ||
| key = d._extract_key_sentence(text) | ||
| assert len(key) <= 55 | ||
|
|
||
|
|
||
| class TestCompressionStats: | ||
| def test_stats(self): | ||
| d = Dialect() | ||
| original = "We decided to use GraphQL instead of REST. " * 10 | ||
| compressed = d.compress(original) | ||
| stats = d.compression_stats(original, compressed) | ||
| assert stats["ratio"] > 1 | ||
| assert stats["original_chars"] > stats["compressed_chars"] | ||
|
|
||
| def test_count_tokens(self): | ||
| assert Dialect.count_tokens("hello world") == len("hello world") // 3 | ||
|
|
||
|
|
||
| class TestZettelEncoding: | ||
| def test_encode_zettel(self): | ||
| d = Dialect(entities={"Alice": "ALC"}) | ||
| zettel = { | ||
| "id": "zettel-001", | ||
| "people": ["Alice"], | ||
| "topics": ["memory", "ai"], | ||
| "content": 'She said "I want to remember everything"', | ||
| "emotional_weight": 0.9, | ||
| "emotional_tone": ["joy"], | ||
| "origin_moment": False, | ||
| "sensitivity": "", | ||
| "notes": "", | ||
| "origin_label": "", | ||
| "title": "Test - Memory Discussion", | ||
| } | ||
| result = d.encode_zettel(zettel) | ||
| assert "ALC" in result | ||
| assert "memory" in result | ||
|
|
||
| def test_encode_tunnel(self): | ||
| d = Dialect() | ||
| tunnel = {"from": "zettel-001", "to": "zettel-002", "label": "follows: temporal"} | ||
| result = d.encode_tunnel(tunnel) | ||
| assert "T:" in result | ||
| assert "001" in result | ||
| assert "002" in result | ||
|
|
||
|
|
||
| class TestDecode: | ||
| def test_decode_roundtrip(self): | ||
| d = Dialect() | ||
| encoded = ( | ||
| '001|ALC+BOB|2025-01-01|test_title\nARC:journey\n001:ALC|memory_ai|"test quote"|0.9|joy' | ||
| ) | ||
| decoded = d.decode(encoded) | ||
| assert decoded["header"]["file"] == "001" | ||
| assert decoded["arc"] == "journey" | ||
| assert len(decoded["zettels"]) == 1 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Dropping
[project.optional-dependencies].devin favor of[dependency-groups].devbreaks the documented contributor install commandpip install -e ".[dev]"(seeCONTRIBUTING.md:10). Consider either updating the docs or keeping adevextra (possibly mirroring the dependency-group) so non-uv workflows still work.