304 lines
10 KiB
Python
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
|