Files
Orchestrator/shared/tools/monorepo-cli/tests/test_router_pbt.py
T

304 lines
10 KiB
Python

"""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