Add deterministic article search to MCP

This commit is contained in:
2026-09-25 15:13:02 +02:00
parent 6f224a1831
commit 1169df96a8
5 changed files with 284 additions and 2 deletions
+123
View File
@@ -7,6 +7,7 @@ from roll_calculation import (
CalculationRequest,
CalculationState,
_canonical_name,
article_production_site,
calculate_roll,
get_article,
modify_calculation,
@@ -446,6 +447,128 @@ class RollCalculationTests(unittest.TestCase):
)
self.assertEqual(result["status"], "success")
def test_search_exact_visible_name_keeps_duplicate_article_numbers(self):
result = ArticleRepository.load().search("Bfix NSP 4900, 5,00 x 40 m")
self.assertEqual(result["status"], "search_results")
self.assertEqual(result["total_matches"], 4)
self.assertEqual(result["candidates"][:2], [
{
"article_number": "180205",
"name": "Bfix NSP 4900, 5,00 x 40 m",
"width_m": 5.0,
"production_site": None,
},
{
"article_number": "8180205",
"name": "Bfix NSP 4900, 5,00 x 40 m",
"width_m": 5.0,
"production_site": "Malaysia",
},
])
def test_article_number_production_site_rule_only_identifies_malaysia(self):
self.assertEqual(article_production_site("8180205"), "Malaysia")
self.assertIsNone(article_production_site("180205"))
def test_search_candidates_expose_site_without_changing_order_or_resolve(self):
repository = ArticleRepository.load()
candidates = repository.search(
"Bfix NSP 4900, 5,00 x 40 m"
)["candidates"]
self.assertEqual(
[candidate["article_number"] for candidate in candidates],
["180205", "8180205", "182815", "206900"],
)
self.assertEqual(
[candidate["production_site"] for candidate in candidates],
[None, "Malaysia", None, None],
)
resolved = repository.resolve("8180205", None)
self.assertEqual(resolved["status"], "resolved")
self.assertNotIn("production_site", resolved["article"])
def test_search_bentofix_alias_matches_bfix_without_resolving_one_article(self):
repository = ArticleRepository.load()
full_name = repository.search("Bentofix NSP 4900")
abbreviated = repository.search("Bfix NSP 4900")
self.assertEqual(full_name["candidates"], abbreviated["candidates"])
self.assertGreater(full_name["total_matches"], 2)
self.assertIn("180205", [
candidate["article_number"] for candidate in full_name["candidates"]
])
def test_search_partial_name_returns_related_products_without_resolving_one(self):
result = ArticleRepository.load().search("NSP 4900")
self.assertEqual(result["status"], "search_results")
self.assertGreater(result["total_matches"], 4)
self.assertIn("180205", [
candidate["article_number"] for candidate in result["candidates"]
])
self.assertIn("206900", [
candidate["article_number"] for candidate in result["candidates"]
])
def test_search_qualifiers_and_dimensions_narrow_real_data_results(self):
repository = ArticleRepository.load()
base = repository.search("Bfix NSP 4900")
dimensions = repository.search("Bfix NSP 4900, 5,00 x 40 m")
qualified = repository.search("Bfix NSP 4900 GRI GCL3")
self.assertLess(dimensions["total_matches"], base["total_matches"])
self.assertEqual(
[candidate["article_number"] for candidate in qualified["candidates"]],
["206900"],
)
def test_search_existing_family_aliases_are_equivalent(self):
repository = ArticleRepository.load()
self.assertEqual(
repository.search("Secutex R 1801")["candidates"],
repository.search("StEx R 1801")["candidates"],
)
self.assertEqual(
repository.search("Secugrid 40/40 Q6")["candidates"],
repository.search("Sgrid 40/40 Q6")["candidates"],
)
def test_search_is_stable_and_handles_empty_and_unknown_queries(self):
repository = ArticleRepository.load()
self.assertEqual(
repository.search("NSP 4900"), repository.search("NSP 4900")
)
self.assertEqual(repository.search(" "), {
"status": "search_not_found",
"query": "",
"total_matches": 0,
"candidates": [],
})
self.assertEqual(
repository.search("not a RollCalc product")["status"],
"search_not_found",
)
def test_search_reports_total_when_the_stable_result_list_is_capped(self):
result = ArticleRepository.load().search("Secugrid")
self.assertEqual(result["status"], "search_results")
self.assertGreater(result["total_matches"], len(result["candidates"]))
self.assertEqual(len(result["candidates"]), 20)
def test_search_does_not_change_existing_resolve_ambiguity(self):
repository = ArticleRepository.load()
self.assertEqual(
repository.resolve(None, "Bentofix NSP 4900")["status"],
"article_ambiguous",
)
def test_name_only_matching_uses_deterministic_bfix_alias(self):
result = self.calculate_fixture({
"article_name_hint": "Bentofix Alias Product",