"""Property-Based Tests für Context Routing und Category Mapping. **Validates: Requirements 1.4, 2.3, 3.2, 3.5, 9.6** Property 2: Context Derivation from Path *For any* file path that resides under a context folder (bahn/, dhive/, privat/), the context derivation function SHALL return the correct context string matching the top-level folder name. Property 6: Context Routing Determinism *For any* valid SourceConfig with an assigned context, the ContextRouter SHALL always route to that context. For any working directory within a context folder, the router SHALL derive the same context as the path-based derivation. Property 8: Category to Folder Mapping *For any* valid category string from the set {meeting, decision, project, reference, link, inbox}, the folder mapping function SHALL return the corresponding plural folder name. For any unknown category, it SHALL default to inbox/. """ from __future__ import annotations from pathlib import Path from hypothesis import given, settings, assume from hypothesis import strategies as st from monorepo.knowledge.ingestion.router import ( CATEGORY_FOLDER_MAP, VALID_CONTEXTS, ContextRouter, category_to_folder, ) from monorepo.knowledge.ingestion.config import SourceConfig # --------------------------------------------------------------------------- # Strategies # --------------------------------------------------------------------------- # Valid contexts sampled from the canonical set context_st = st.sampled_from(VALID_CONTEXTS) # Known categories (the valid set) known_category_st = st.sampled_from(list(CATEGORY_FOLDER_MAP.keys())) # Unknown categories: strings that are NOT in the known set unknown_category_st = st.text( alphabet=st.characters( whitelist_categories=("Ll", "Lu"), whitelist_characters="_-", ), min_size=1, max_size=30, ).filter(lambda s: s.lower() not in CATEGORY_FOLDER_MAP) # Subdirectory path segments (simulating nested paths under a context folder) # IMPORTANT: segments must NOT be valid context names, otherwise the router # will match the wrong context (it scans for /{context}/ in order). path_segment_st = st.text( alphabet=st.characters( whitelist_categories=("Ll", "Lu", "Nd"), whitelist_characters="-_.", ), min_size=1, max_size=20, ).filter(lambda s: s.lower() not in VALID_CONTEXTS) # List of subdirectory segments to form a path suffix path_suffix_st = st.lists(path_segment_st, min_size=0, max_size=4) # Monorepo root paths (Unix and Windows style) root_path_st = st.sampled_from([ "/home/user/monorepo", "/workspace/project", "C:/Users/user/Coden/Orchestrator", ]) # --------------------------------------------------------------------------- # Property 2: Context Derivation from Path # --------------------------------------------------------------------------- class TestContextDerivationFromPath: """**Validates: Requirements 1.4, 3.2** Property 2: For any file path under a context folder, determine_context SHALL return the correct context string. """ @given( root=root_path_st, context=context_st, subdirs=path_suffix_st, ) @settings(max_examples=200) def test_path_under_context_returns_correct_context( self, root: str, context: str, subdirs: list[str], ) -> None: """Any path containing /{context}/ derives to that context.""" router = ContextRouter(Path(root)) # Build a path like /root/{context}/sub1/sub2/... path_parts = [root, context] + subdirs cwd = Path("/".join(path_parts)) result = router.determine_context(cwd=cwd) assert result == context @given( root=root_path_st, context=context_st, ) @settings(max_examples=100) def test_path_ending_with_context_returns_correct_context( self, root: str, context: str, ) -> None: """A path ending exactly with the context name also resolves correctly.""" router = ContextRouter(Path(root)) cwd = Path(f"{root}/{context}") result = router.determine_context(cwd=cwd) assert result == context @given( root=root_path_st, context=context_st, subdirs=path_suffix_st, filename=path_segment_st, ) @settings(max_examples=200) def test_file_path_under_context_derives_context( self, root: str, context: str, subdirs: list[str], filename: str, ) -> None: """Even deeply nested file paths under a context derive correctly.""" router = ContextRouter(Path(root)) parts = [root, context, "knowledge"] + subdirs + [f"{filename}.md"] file_path = Path("/".join(parts)) result = router.determine_context(cwd=file_path) assert result == context # --------------------------------------------------------------------------- # Property 6: Context Routing Determinism # --------------------------------------------------------------------------- class TestContextRoutingDeterminism: """**Validates: Requirements 2.3** Property 6: For any valid SourceConfig with an assigned context, the ContextRouter SHALL always route to that context. For any working directory within a context folder, the router SHALL derive the same context as the path-based derivation. """ @given( context=context_st, source_type=st.sampled_from(["confluence", "jira", "email", "file", "link"]), source_name=st.text(min_size=1, max_size=20, alphabet="abcdefghijklmnop"), ) @settings(max_examples=200) def test_source_config_with_context_always_routes_to_that_context( self, context: str, source_type: str, source_name: str, ) -> None: """SourceConfig.params['context'] always determines the routing outcome.""" router = ContextRouter(Path("/monorepo")) config = SourceConfig( type=source_type, name=source_name, params={"context": context}, ) # Call multiple times to verify determinism result1 = router.determine_context(source_config=config) result2 = router.determine_context(source_config=config) result3 = router.determine_context(source_config=config) assert result1 == context assert result2 == context assert result3 == context @given( root=root_path_st, context=context_st, subdirs=path_suffix_st, ) @settings(max_examples=200) def test_cwd_derivation_matches_path_based_derivation( self, root: str, context: str, subdirs: list[str], ) -> None: """For any cwd within a context folder, repeated derivation yields the same result.""" router = ContextRouter(Path(root)) cwd = Path("/".join([root, context] + subdirs)) # Path-based derivation via cwd result_cwd = router.determine_context(cwd=cwd) # Derive again (determinism) result_cwd_again = router.determine_context(cwd=cwd) assert result_cwd == context assert result_cwd_again == context @given( context=context_st, subdirs=path_suffix_st, ) @settings(max_examples=200) def test_source_config_context_matches_cwd_derivation( self, context: str, subdirs: list[str], ) -> None: """SourceConfig context and cwd under same context agree.""" router = ContextRouter(Path("/monorepo")) config = SourceConfig( type="file", name="test-source", params={"context": context}, ) cwd = Path("/".join(["/monorepo", context] + subdirs)) result_config = router.determine_context(source_config=config) result_cwd = router.determine_context(cwd=cwd) assert result_config == result_cwd == context # --------------------------------------------------------------------------- # Property 8: Category to Folder Mapping # --------------------------------------------------------------------------- class TestCategoryToFolderMapping: """**Validates: Requirements 3.5, 9.6** Property 8: For any valid category string from the known set, the folder mapping returns the corresponding plural name. For unknown categories, it defaults to inbox/. """ @given(category=known_category_st) @settings(max_examples=100) def test_known_categories_map_to_plural_folder( self, category: str, ) -> None: """Every known category maps to its expected plural folder name.""" result = category_to_folder(category) expected = CATEGORY_FOLDER_MAP[category] assert result == expected @given(category=unknown_category_st) @settings(max_examples=200) def test_unknown_categories_default_to_inbox( self, category: str, ) -> None: """Any category not in the known set defaults to 'inbox'.""" result = category_to_folder(category) assert result == "inbox" @given(category=known_category_st) @settings(max_examples=100) def test_case_insensitive_known_categories( self, category: str, ) -> None: """Category mapping is case-insensitive for known categories.""" result_upper = category_to_folder(category.upper()) result_lower = category_to_folder(category.lower()) result_title = category_to_folder(category.title()) expected = CATEGORY_FOLDER_MAP[category] assert result_upper == expected assert result_lower == expected assert result_title == expected @given( category=known_category_st, context=context_st, filename=path_segment_st, ) @settings(max_examples=100) def test_resolve_target_path_uses_category_mapping( self, category: str, context: str, filename: str, ) -> None: """resolve_target_path produces path with the mapped folder name.""" router = ContextRouter(Path("/monorepo")) result_path = router.resolve_target_path(context, category, f"{filename}.md") expected_folder = CATEGORY_FOLDER_MAP[category] expected_path = Path(f"/monorepo/{context}/knowledge/{expected_folder}/{filename}.md") assert result_path == expected_path