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

188 lines
6.9 KiB
Python

"""Property-Based Tests für Quick Capture.
**Validates: Requirements 3.3, 8.3**
Property 7: Filename Pattern from Title
*For any* non-empty title string, the Quick Capture filename generator SHALL
produce a filename matching the pattern
`\\d{4}-\\d{2}-\\d{2}-\\d{2}-\\d{2}-[a-z0-9-]+\\.md`
(date-time prefix + slugified title + .md extension).
Property 11: URL Detection in Quick Capture
*For any* string that is a valid HTTP or HTTPS URL, the Quick Capture input
type detector SHALL classify it as type "url". For any string that is not a
valid HTTP(S) URL, it SHALL NOT classify it as "url".
"""
from __future__ import annotations
import re
from pathlib import Path
from hypothesis import given, settings, assume
from hypothesis import strategies as st
from monorepo.knowledge.ingestion.capture import QuickCapture
# ---------------------------------------------------------------------------
# Shared fixtures / helpers
# ---------------------------------------------------------------------------
FILENAME_PATTERN = re.compile(r"^\d{4}-\d{2}-\d{2}-\d{2}-\d{2}-[a-z0-9-]+\.md$")
def make_capture() -> QuickCapture:
"""Create a QuickCapture instance with a dummy root path."""
return QuickCapture(Path("/tmp"))
# ---------------------------------------------------------------------------
# Strategies
# ---------------------------------------------------------------------------
# Non-empty title strings with various characters (letters, digits, punctuation, spaces).
# Filtered to contain at least one alphanumeric char, since the slugifier strips
# all non-alnum chars — a title like "/" would yield an empty slug. In practice,
# _derive_title always produces titles with alphanumeric content (from URLs,
# file stems, or first 60 chars of text).
title_st = st.text(
alphabet=st.characters(
whitelist_categories=("L", "N", "P", "Z"),
blacklist_characters="\x00",
),
min_size=1,
max_size=200,
).filter(lambda s: s.strip() != "" and any(c.isalnum() for c in s))
# Valid HTTP/HTTPS URLs: protocol + domain + optional path
_domain_st = st.from_regex(r"[a-z][a-z0-9]{1,20}\.[a-z]{2,6}", fullmatch=True)
_path_segment_st = st.from_regex(r"[a-z0-9\-]{1,15}", fullmatch=True)
_url_path_st = st.lists(_path_segment_st, min_size=0, max_size=3).map(
lambda parts: "/" + "/".join(parts) if parts else ""
)
_protocol_st = st.sampled_from(["http://", "https://"])
valid_url_st = st.builds(
lambda proto, domain, path: f"{proto}{domain}{path}",
_protocol_st,
_domain_st,
_url_path_st,
)
# Non-URL strings: plain text that does NOT start with http:// or https://
non_url_st = st.text(
alphabet=st.characters(
whitelist_categories=("L", "N", "P", "Z"),
blacklist_characters="\x00",
),
min_size=1,
max_size=100,
).filter(
lambda s: not s.strip().startswith("http://")
and not s.strip().startswith("https://")
)
# ---------------------------------------------------------------------------
# Property 7: Filename Pattern from Title
# ---------------------------------------------------------------------------
class TestFilenamePatternFromTitle:
"""**Validates: Requirements 3.3**
Property 7: For any non-empty title string, _generate_filename SHALL produce
a filename matching the expected pattern with timestamp prefix, slugified
title, and .md extension.
"""
@given(title=title_st)
@settings(max_examples=300)
def test_filename_matches_pattern(self, title: str) -> None:
"""Any non-empty title produces a filename matching the date-slug pattern."""
capture = make_capture()
filename = capture._generate_filename(title)
assert FILENAME_PATTERN.match(filename), (
f"Filename '{filename}' from title '{title!r}' "
f"does not match pattern {FILENAME_PATTERN.pattern}"
)
@given(title=title_st)
@settings(max_examples=300)
def test_slug_portion_max_50_chars(self, title: str) -> None:
"""The slug portion of the filename never exceeds 50 characters."""
capture = make_capture()
filename = capture._generate_filename(title)
# Filename format: YYYY-MM-DD-HH-MM-{slug}.md
# Timestamp prefix is exactly "YYYY-MM-DD-HH-MM-" = 17 chars
# Suffix is ".md" = 3 chars
slug = filename[17:-3] # strip timestamp prefix and .md suffix
assert len(slug) <= 50, (
f"Slug '{slug}' has {len(slug)} chars, exceeds 50 limit"
)
@given(title=title_st)
@settings(max_examples=300)
def test_filename_ends_with_md(self, title: str) -> None:
"""Every generated filename ends with .md."""
capture = make_capture()
filename = capture._generate_filename(title)
assert filename.endswith(".md")
@given(title=title_st)
@settings(max_examples=200)
def test_slug_has_no_trailing_hyphen(self, title: str) -> None:
"""The slug portion does not end with a trailing hyphen."""
capture = make_capture()
filename = capture._generate_filename(title)
# Remove .md suffix, then check the last char before .md
without_md = filename[:-3]
assert not without_md.endswith("-"), (
f"Filename '{filename}' has trailing hyphen in slug"
)
# ---------------------------------------------------------------------------
# Property 11: URL Detection in Quick Capture
# ---------------------------------------------------------------------------
class TestURLDetectionInQuickCapture:
"""**Validates: Requirements 8.3**
Property 11: For any valid HTTP(S) URL, _detect_input_type SHALL return "url".
For any non-URL string, it SHALL NOT return "url".
"""
@given(url=valid_url_st)
@settings(max_examples=300)
def test_valid_urls_classified_as_url(self, url: str) -> None:
"""Any string starting with http:// or https:// followed by a domain is 'url'."""
capture = make_capture()
result = capture._detect_input_type(url)
assert result == "url", (
f"URL '{url}' was classified as '{result}', expected 'url'"
)
@given(text=non_url_st)
@settings(max_examples=300)
def test_non_urls_not_classified_as_url(self, text: str) -> None:
"""Any string that is NOT a valid HTTP(S) URL SHALL NOT be classified as 'url'."""
capture = make_capture()
result = capture._detect_input_type(text)
assert result != "url", (
f"Non-URL text '{text!r}' was incorrectly classified as 'url'"
)
@given(url=valid_url_st)
@settings(max_examples=200)
def test_urls_with_leading_whitespace_still_detected(self, url: str) -> None:
"""URLs with leading/trailing whitespace are still classified as 'url'."""
capture = make_capture()
padded = f" {url} "
result = capture._detect_input_type(padded)
assert result == "url", (
f"Padded URL '{padded}' was classified as '{result}', expected 'url'"
)