Viet Tran a38d04c3d5
feat(search): overhaul relevance and curated design data
Overhaul BM25 relevance, reasoning and data-quality contracts; refresh UI styles and framework guidance; add resilient text, chip, badge and micro-interaction guidance; strengthen release, provenance and catalog refresh gates; update bilingual documentation.
2026-08-14 00:08:23 +07:00

344 lines
16 KiB
Python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Stdlib-only regression tests for core.py / design_system.py (unittest, not
pytest -- this project ships with zero external dependencies and the tests
shouldn't add one).
Run with:
python -m unittest discover -s scripts/tests -v
or directly:
python scripts/tests/test_core.py
"""
import os
import json
import subprocess
import sys
import tempfile
import unittest
from unittest.mock import patch
from pathlib import Path
SCRIPTS_DIR = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(SCRIPTS_DIR))
import core
from core import BM25, detect_domain, search, search_stack, CSV_CONFIG, AVAILABLE_STACKS
from design_system import DesignSystemGenerator, generate_design_system
class TestTokenizer(unittest.TestCase):
def test_short_domain_terms_are_kept(self):
bm25 = BM25()
tokens = bm25.tokenize("UI and UX design with 3D and AI")
self.assertIn("ui", tokens)
self.assertIn("3d", tokens)
self.assertIn("ai", tokens)
def test_stopwords_removed(self):
bm25 = BM25()
tokens = bm25.tokenize("this is for the team to do")
for stopword in ("is", "for", "the", "to", "do"):
self.assertNotIn(stopword, tokens)
def test_synonym_normalization(self):
bm25 = BM25()
self.assertEqual(bm25.tokenize("e-commerce store"), bm25.tokenize("ecommerce store"))
self.assertEqual(bm25.tokenize("dark-mode toggle"), bm25.tokenize("dark toggle"))
def test_boundary_safe_nav_normalization_preserves_existing_words(self):
bm25 = BM25()
tokens = bm25.tokenize("nav navigation navbar")
self.assertIn("navigation", tokens)
self.assertIn("navbar", tokens)
self.assertNotIn("navigationigation", tokens)
self.assertNotIn("navigationbar", tokens)
def test_punctuation_and_uk_variants_normalize_to_canonical_tokens(self):
bm25 = BM25()
tokens = bm25.tokenize("colour, organisation; behaviour customisation")
for expected in ("color", "organization", "behavior", "customization"):
self.assertIn(expected, tokens)
class TestBm25CoreBehavior(unittest.TestCase):
def test_empty_documents_produce_no_scores_or_vocab(self):
bm25 = BM25()
bm25.fit([])
self.assertEqual(bm25.score("anything"), [])
self.assertEqual(bm25.vocabulary(), [])
def test_bm25_cache_rebuilds_after_file_mtime_changes(self):
with tempfile.TemporaryDirectory() as tmp:
path = Path(tmp) / "search.csv"
path.write_text("Name,Keywords\nAlpha,alpha token\n", encoding="utf-8")
results_a, bm25_a = core._search_csv(path, ["Name", "Keywords"], ["Name"], "alpha", 1)
self.assertEqual(results_a[0]["Name"], "Alpha")
path.write_text("Name,Keywords\nBeta,beta token\n", encoding="utf-8")
stat = path.stat()
os.utime(path, ns=(stat.st_atime_ns + 1_000_000_000, stat.st_mtime_ns + 1_000_000_000))
results_b, bm25_b = core._search_csv(path, ["Name", "Keywords"], ["Name"], "beta", 1)
self.assertEqual(results_b[0]["Name"], "Beta")
self.assertIsNot(bm25_a, bm25_b)
def test_search_uses_one_verified_rows_and_index_snapshot(self):
with tempfile.TemporaryDirectory() as tmp:
path = Path(tmp) / "search.csv"
path.write_text("Name,Keywords\nAlpha,alpha token\n", encoding="utf-8")
original_get_bm25 = core._get_bm25
replaced = False
def replace_after_read(filepath, search_cols, data, signature=None,
cache_variant=""):
nonlocal replaced
if not replaced:
path.write_text("Name,Keywords\nBeta,beta token\n", encoding="utf-8")
replaced = True
return original_get_bm25(
filepath, search_cols, data, signature, cache_variant)
with patch.object(core, "_get_bm25", side_effect=replace_after_read):
results, _, _ = core._search_csv_detailed(
path, ["Name", "Keywords"], ["Name"], "alpha", 1)
self.assertEqual(results[0]["Name"], "Alpha")
results, _, _ = core._search_csv_detailed(
path, ["Name", "Keywords"], ["Name"], "beta", 1)
self.assertEqual(results[0]["Name"], "Beta")
class TestSearchDomains(unittest.TestCase):
def test_read_failure_is_not_reported_as_a_search_result(self):
failure = UnicodeDecodeError("utf-8", b"\xff", 0, 1, "invalid")
with patch("core._load_csv_snapshot", side_effect=failure):
domain = search("palette", domain="color", max_results=1)
stack = search_stack("component", "react", max_results=1)
for result in (domain, stack):
self.assertEqual(0, result["count"])
self.assertEqual([], result["results"])
self.assertRegex(result["error"], r"^Unable to read search data:")
self.assertNotIn("invalid", result["error"])
def test_ui_is_searchable_in_style_domain(self):
result = search("ui minimalism", domain="style", max_results=1)
self.assertGreater(result["count"], 0, "literal 'ui' token must be searchable, not filtered by tokenizer")
def test_accessibility_query_hits_ux(self):
result = search("accessibility contrast wcag keyboard", domain="ux", max_results=3)
self.assertGreater(result["count"], 0)
def test_zero_result_query_reports_suggestions_not_error(self):
result = search("zzqqxx totally made up gibberish", domain="ux", max_results=2)
self.assertEqual(result["count"], 0)
self.assertIn("suggestions", result)
self.assertNotIn("error", result)
def test_hard_negative_query_abstains_across_registered_domains_and_stacks(self):
query = "sourdough starter crumb fermentation"
for domain in CSV_CONFIG:
with self.subTest(kind="domain", name=domain):
self.assertEqual(search(query, domain=domain, max_results=1)["count"], 0)
for stack in AVAILABLE_STACKS:
with self.subTest(kind="stack", name=stack):
self.assertEqual(search_stack(query, stack, max_results=1)["count"], 0)
def test_typo_suggestions_are_deterministic_and_retryable(self):
first = search("testimonal", domain="landing", max_results=3)
second = search("testimonal", domain="landing", max_results=3)
self.assertEqual(first["count"], 0)
self.assertEqual(first.get("suggestions"), second.get("suggestions"))
self.assertTrue(first["suggestions"], "typo path should return at least one deterministic suggestion")
retry = search(first["suggestions"][0], domain="landing", max_results=3)
self.assertGreater(retry["count"], 0)
def test_suggestions_never_repeat_the_input_or_offer_a_dead_first_retry(self):
pricing = search("pricing", domain="landing", max_results=3)
self.assertNotIn("pricing", pricing.get("suggestions", []))
minimal = search("minimal", domain="style", max_results=3)
self.assertEqual(1, minimal["count"])
self.assertEqual(
"minimalism-and-swiss-style", minimal["results"][0]["Style ID"]
)
def test_unknown_programmatic_domain_keeps_legacy_style_fallback(self):
result = search("minimalism", domain="unknown", max_results=1)
self.assertEqual(result["domain"], "unknown")
self.assertEqual(result["file"], CSV_CONFIG["style"]["file"])
self.assertGreater(result["count"], 0)
def test_unsupported_icon_library_abstains_instead_of_returning_other_library(self):
result = search("lucide icon", diagnostics=True)
self.assertEqual(result["domain"], "icons")
self.assertEqual(result["count"], 0)
self.assertEqual(result["diagnostics"]["reason"], "unsupported-library")
def test_every_configured_domain_file_exists_and_is_searchable(self):
for domain, config in CSV_CONFIG.items():
with self.subTest(domain=domain):
result = search("design", domain=domain, max_results=1)
self.assertNotIn("error", result, f"domain '{domain}' failed: {result.get('error')}")
def test_chart_output_keeps_legacy_grade_during_risk_migration(self):
result = search("time series chart", domain="chart", max_results=1)
self.assertEqual(result["count"], 1)
self.assertEqual(
result["results"][0]["Accessibility Grade"],
"deprecated: use Accessibility Risk",
)
self.assertIn("Accessibility Risk", result["results"][0])
def test_every_stack_file_exists_and_is_searchable(self):
for stack in AVAILABLE_STACKS:
with self.subTest(stack=stack):
result = search_stack("performance", stack, max_results=1)
self.assertNotIn("error", result, f"stack '{stack}' failed: {result.get('error')}")
class TestDomainDetection(unittest.TestCase):
def test_style_keywords_route_to_style(self):
self.assertEqual(detect_domain("glassmorphism dark ui"), "style")
def test_accessibility_keywords_route_to_ux(self):
self.assertEqual(detect_domain("accessibility contrast wcag"), "ux")
def test_ambiguous_query_returns_runner_up(self):
domain, _ = detect_domain("font pairing elegant crypto", return_scores=True)
self.assertIsNotNone(domain)
def test_empty_query_falls_back_to_style(self):
self.assertEqual(detect_domain("...!!!???"), "style")
def test_router_prioritizes_color_intent_over_generic_product_terms(self):
self.assertEqual(detect_domain("semantic color tokens palette"), "color")
def test_router_prioritizes_icons_when_icon_library_and_icon_intent_present(self):
self.assertEqual(detect_domain("lucide search icon outline"), "icons")
def test_router_prioritizes_typography_for_font_pairing_queries(self):
self.assertEqual(detect_domain("font pairing elegant serif body font"), "typography")
def test_router_prioritizes_chart_queries_over_generic_product_keywords(self):
self.assertEqual(detect_domain("time series chart forecast"), "chart")
def test_hash_only_routes_color_for_a_valid_hex_literal(self):
self.assertNotEqual(detect_domain("C# WPF desktop app"), "color")
self.assertEqual(detect_domain("use #ff00aa as the accent"), "color")
def test_product_router_keeps_high_signal_service_aliases(self):
self.assertEqual(detect_domain("beauty spa"), "product")
self.assertEqual(detect_domain("salon booking"), "product")
def test_native_drag_intent_beats_generic_react_token(self):
self.assertEqual(detect_domain("drag reorder react native"), "web")
def test_every_router_term_is_searchable_or_has_a_corpus_rewrite(self):
for domain, keywords in core._domain_keywords().items():
config = CSV_CONFIG[domain]
path = core.DATA_DIR / config["file"]
index = core._get_bm25(path, config["search_cols"], core._load_csv(path))
vocabulary = set(index.vocabulary())
for keyword in keywords:
with self.subTest(domain=domain, keyword=keyword):
searchable = bool(set(index.tokenize(keyword)) & vocabulary)
explicitly_routing_only = keyword in core._DOMAIN_QUERY_REWRITES.get(domain, {})
self.assertTrue(searchable or explicitly_routing_only)
class TestPersistence(unittest.TestCase):
def test_concurrent_non_force_persist_has_one_writer(self):
with tempfile.TemporaryDirectory() as tmp:
search_script = SCRIPTS_DIR / "search.py"
processes = [subprocess.Popen(
[sys.executable, str(search_script), f"saas dashboard {index}",
"--design-system", "--persist", "--project-name", "Race Probe",
"--output-dir", tmp, "--json"],
stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True,
) for index in range(8)]
statuses = []
for process in processes:
stdout, stderr = process.communicate(timeout=30)
self.assertEqual(process.returncode, 0, stderr)
statuses.append(json.loads(stdout)["persistence"]["status"])
self.assertEqual(statuses.count("success"), 1)
self.assertEqual(statuses.count("skipped_exists"), 7)
def test_persist_then_skip_then_force(self):
with tempfile.TemporaryDirectory() as tmp:
result = generate_design_system("saas dashboard", "Test Project", persist=True, output_dir=tmp)
self.assertEqual(result["persistence"]["status"], "success")
master = Path(result["persistence"]["master_file"])
self.assertTrue(master.exists())
original_content = master.read_text(encoding="utf-8")
# Second persist without force must not overwrite.
result2 = generate_design_system("saas dashboard", "Test Project", persist=True, output_dir=tmp)
self.assertEqual(result2["persistence"]["status"], "skipped_exists")
self.assertEqual(master.read_text(encoding="utf-8"), original_content)
# A new page override may be added without rewriting the existing Master.
page_result = generate_design_system(
"checkout form", "Test Project", persist=True, page="Checkout", output_dir=tmp
)
self.assertEqual(page_result["persistence"]["status"], "success")
self.assertEqual(master.read_text(encoding="utf-8"), original_content)
page_file = Path(tmp) / "design-system" / "test-project" / "pages" / "checkout.md"
self.assertEqual(page_result["persistence"]["created_files"], [str(page_file)])
self.assertTrue(page_file.exists())
# Existing page overrides are protected by the same default no-overwrite rule.
page_content = page_file.read_text(encoding="utf-8")
page_result2 = generate_design_system(
"different checkout", "Test Project", persist=True, page="Checkout", output_dir=tmp
)
self.assertEqual(page_result2["persistence"]["status"], "skipped_exists")
self.assertEqual(page_file.read_text(encoding="utf-8"), page_content)
# With force=True it must overwrite.
result3 = generate_design_system("ecommerce luxury", "Test Project", persist=True, output_dir=tmp, force=True)
self.assertEqual(result3["persistence"]["status"], "success")
def test_persist_writes_only_under_output_dir(self):
with tempfile.TemporaryDirectory() as tmp:
generate_design_system("saas dashboard", "Scoped Project", persist=True, output_dir=tmp)
expected = Path(tmp) / "design-system" / "scoped-project" / "MASTER.md"
self.assertTrue(expected.exists())
class TestReasoningMatch(unittest.TestCase):
def test_known_category_matches_exactly(self):
gen = DesignSystemGenerator()
rule = gen._find_reasoning_rule("SaaS (General)")
self.assertTrue(rule, "exact-match category lookup should not fall through to fuzzy matching")
def test_unknown_category_falls_back_gracefully(self):
gen = DesignSystemGenerator()
rule = gen._find_reasoning_rule("Totally Unknown Category XYZ")
# Should not raise; may return {} which _apply_reasoning handles with defaults.
self.assertIsInstance(rule, dict)
class TestDiagnosticsContracts(unittest.TestCase):
def test_diagnostics_opt_in_is_additive_for_domain_search(self):
baseline = search("minimalism", domain="style", max_results=1)
diagnosed = search("minimalism", domain="style", max_results=1, diagnostics=True)
self.assertEqual(set(baseline.keys()), set(diagnosed.keys()) - {"diagnostics"})
self.assertIn("diagnostics", diagnosed)
self.assertIn("top_score", diagnosed["diagnostics"])
self.assertIn("query_rewrites", diagnosed["diagnostics"])
def test_diagnostics_opt_in_is_additive_for_stack_search(self):
baseline = search_stack("performance", "react", max_results=1)
diagnosed = search_stack("performance", "react", max_results=1, diagnostics=True)
self.assertEqual(set(baseline.keys()), set(diagnosed.keys()) - {"diagnostics"})
self.assertIn("diagnostics", diagnosed)
if __name__ == "__main__":
unittest.main()