Add regression corpus and opt-in catalog test route
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from decimal import Decimal
|
||||
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
|
||||
from app.matcher import CatalogMatcher
|
||||
|
||||
|
||||
class CatalogItemInfo(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
sku: str
|
||||
name: str
|
||||
unit: str
|
||||
price: Decimal
|
||||
|
||||
|
||||
def register_test_info_route(
|
||||
application: FastAPI, matcher: CatalogMatcher
|
||||
) -> None:
|
||||
items_by_sku = {item.sku: item for item in matcher.items}
|
||||
|
||||
@application.get(
|
||||
"/get_info_by/{sku}",
|
||||
response_model=CatalogItemInfo,
|
||||
include_in_schema=False,
|
||||
)
|
||||
def get_info_by(sku: str) -> CatalogItemInfo:
|
||||
item = items_by_sku.get(sku.strip().upper())
|
||||
if item is None:
|
||||
raise HTTPException(status_code=404, detail="SKU not found")
|
||||
return CatalogItemInfo(
|
||||
sku=item.sku,
|
||||
name=item.name,
|
||||
unit=item.unit,
|
||||
price=item.price,
|
||||
)
|
||||
+42
-11
@@ -4,6 +4,7 @@ import os
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
|
||||
from app.matcher import CatalogMatcher
|
||||
from app.models import MatchRequest, MatchResponse
|
||||
@@ -11,18 +12,48 @@ from app.models import MatchRequest, MatchResponse
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
CATALOG_PATH = Path(os.environ.get("CATALOG_PATH", BASE_DIR / "catalog_excel.csv"))
|
||||
MATCHER = CatalogMatcher(CATALOG_PATH)
|
||||
|
||||
# The task asks for one application endpoint. OpenAPI/Swagger routes are disabled
|
||||
# so the running service exposes only POST /match.
|
||||
app = FastAPI(
|
||||
title="Offline catalog matcher",
|
||||
version="0.2.0",
|
||||
docs_url=None,
|
||||
redoc_url=None,
|
||||
openapi_url=None,
|
||||
)
|
||||
TEST_INFO_ROUTE_ENV = "ENABLE_TEST_INFO_ROUTE"
|
||||
|
||||
|
||||
@app.post("/match", response_model=MatchResponse)
|
||||
def match(request: MatchRequest) -> MatchResponse:
|
||||
return MatchResponse(results=[MATCHER.match(message) for message in request.messages])
|
||||
|
||||
|
||||
def create_app(*, enable_test_info_route: bool = False) -> FastAPI:
|
||||
# OpenAPI/Swagger routes are disabled so the production application exposes
|
||||
# exactly the single endpoint required by the task.
|
||||
application = FastAPI(
|
||||
title="Offline catalog matcher",
|
||||
version="0.2.0",
|
||||
docs_url=None,
|
||||
redoc_url=None,
|
||||
openapi_url=None,
|
||||
)
|
||||
application.add_api_route(
|
||||
"/match", match, methods=["POST"], response_model=MatchResponse
|
||||
)
|
||||
|
||||
if enable_test_info_route:
|
||||
from app.dev_routes import register_test_info_route
|
||||
|
||||
application.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_methods=["GET", "POST"],
|
||||
allow_headers=["Content-Type"],
|
||||
)
|
||||
register_test_info_route(application, MATCHER)
|
||||
|
||||
return application
|
||||
|
||||
|
||||
def _test_info_route_enabled() -> bool:
|
||||
return os.environ.get(TEST_INFO_ROUTE_ENV, "").strip().lower() in {
|
||||
"1",
|
||||
"true",
|
||||
"yes",
|
||||
"on",
|
||||
}
|
||||
|
||||
|
||||
app = create_app(enable_test_info_route=_test_info_route_enabled())
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
добрый день, саморезы по дереву 3.5х25 пачка 200 штук
|
||||
саморез гкл 3,5 на 25 кг
|
||||
нужны черные саморезы 4.2x75 упаковка 1000
|
||||
самарезы па дереву 3.5х45 200шт
|
||||
саморезы 99х999
|
||||
дюбель гвоздь 6x40
|
||||
дюбеля 8 на 80 есть?
|
||||
анкерный болт м10
|
||||
гайка м8 оцинкованная
|
||||
шайбы м12 сколько стоят
|
||||
болт м6х30 50 штук
|
||||
шпилька м10 один метр
|
||||
крепеж какой-нибудь
|
||||
кабель ввг нг ls 3*2,5
|
||||
провод пвс 2х1.5 нужен
|
||||
ШВВП 2 X 0,75, цена за метр?
|
||||
кабель кг 3х2,5
|
||||
кабель для чайника
|
||||
3х1.5
|
||||
ввгнг 20х20
|
||||
труба профильная 40x20x2
|
||||
профтруба 60 40 3
|
||||
труба квадратная двадцать на двадцать
|
||||
труба 35х35х2
|
||||
нужна труба длиной до луны
|
||||
гипсокартон 12,5 влагостойкий
|
||||
гкл обычный 9.5 мм
|
||||
лист гипса зеленый
|
||||
гипсокортан 12.5
|
||||
фанера 18 мм
|
||||
дрель prowerk pw-750
|
||||
аккумуляторная дрель 18в
|
||||
шуруповёрт 12 вольт самый дешёвый
|
||||
перфоратор sds plus 800 вт
|
||||
болгарка 125
|
||||
ушм 230 мм 2200 вт
|
||||
лобзик посоветуйте
|
||||
makita df333 есть?
|
||||
инструмент чтобы сверлить стену
|
||||
дрэл 9999 ват
|
||||
бур sds-plus 8x160
|
||||
бур 10 на 210 sds+
|
||||
бур SDS MAX 20х400
|
||||
сверло по металлу 6 мм
|
||||
сверло по дереву 10x120
|
||||
бита ph2 25
|
||||
набор бит torx
|
||||
бита t30 50 мм
|
||||
бита t99
|
||||
насадка на шуруповерт
|
||||
диск отрезной 125х1,0 металл
|
||||
круг по металлу 230 на 2.5
|
||||
пильный диск 190x48t
|
||||
алмазный диск 125 бетон
|
||||
диск на болгарку
|
||||
круг квадратный 12х34
|
||||
наждачная бумага p80
|
||||
шкурка нулёвка
|
||||
круг лепестковый 125 р40
|
||||
лента фум 19 мм
|
||||
изолента синяя 15 мм
|
||||
хомут 4,8x300 черный
|
||||
стяжки пластиковые
|
||||
перчатки нитриловые размер l
|
||||
перчатки хб с пвх
|
||||
мешки для мусора 120 литров
|
||||
пена монтажная зимняя
|
||||
герметик силиконовый прозрачный
|
||||
клей жидкие гвозди
|
||||
WD-40 400 мл
|
||||
здравствуйте
|
||||
вы сегодня открыты?
|
||||
какой адрес
|
||||
есть доставка в химки?
|
||||
можно вернуть товар без чека
|
||||
примите заказ 12345
|
||||
где мой заказ 000001
|
||||
оплатил, но денег нет
|
||||
спасибо большое!
|
||||
позовите человека
|
||||
1234567890
|
||||
000000000000000000
|
||||
42
|
||||
qwerty asdf zxcv
|
||||
йцукен фывапр олдж
|
||||
абракадабра крокодил вертолёт
|
||||
!@#$%^&*()
|
||||
..........
|
||||
купить купить купить
|
||||
null
|
||||
None
|
||||
SELECT * FROM catalog;
|
||||
<script>alert(1)</script>
|
||||
../../etc/passwd
|
||||
🤖🔩🔨
|
||||
САМОБРЕЗЫ ГКЛ 3.5Х25???
|
||||
ну эээ диск этот круглый короче
|
||||
мне два метра того красного
|
||||
хочу всё и сразу за сто рублей
|
||||
статус заказа 4512 и бур 8х160
|
||||
диск
|
||||
+45
-1
@@ -2,7 +2,7 @@ from __future__ import annotations
|
||||
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from app.main import app
|
||||
from app.main import app, create_app
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
@@ -47,3 +47,47 @@ def test_empty_message_list_is_valid() -> None:
|
||||
def test_unknown_request_fields_are_rejected() -> None:
|
||||
response = client.post("/match", json={"messages": [], "debug": True})
|
||||
assert response.status_code == 422
|
||||
|
||||
|
||||
def test_opt_in_info_route_returns_catalog_item() -> None:
|
||||
test_app = create_app(enable_test_info_route=True)
|
||||
test_client = TestClient(test_app)
|
||||
|
||||
assert [route.path for route in test_app.routes] == [
|
||||
"/match",
|
||||
"/get_info_by/{sku}",
|
||||
]
|
||||
|
||||
response = test_client.get("/get_info_by/bit-0073")
|
||||
assert response.status_code == 200
|
||||
assert set(response.json()) == {"sku", "name", "unit", "price"}
|
||||
assert response.json()["sku"] == "BIT-0073"
|
||||
assert response.json()["name"]
|
||||
assert response.json()["unit"]
|
||||
assert float(response.json()["price"]) > 0
|
||||
|
||||
|
||||
def test_opt_in_info_route_returns_404_for_unknown_sku() -> None:
|
||||
test_client = TestClient(create_app(enable_test_info_route=True))
|
||||
|
||||
response = test_client.get("/get_info_by/UNKNOWN-0000")
|
||||
|
||||
assert response.status_code == 404
|
||||
assert response.json() == {"detail": "SKU not found"}
|
||||
|
||||
|
||||
def test_opt_in_routes_allow_requests_from_local_html_page() -> None:
|
||||
test_client = TestClient(create_app(enable_test_info_route=True))
|
||||
response = test_client.options(
|
||||
"/match",
|
||||
headers={
|
||||
"Origin": "null",
|
||||
"Access-Control-Request-Method": "POST",
|
||||
"Access-Control-Request-Headers": "content-type",
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.headers["access-control-allow-origin"] == "*"
|
||||
assert "POST" in response.headers["access-control-allow-methods"]
|
||||
assert "GET" in response.headers["access-control-allow-methods"]
|
||||
|
||||
@@ -126,6 +126,20 @@ def test_product_evidence_has_priority_over_service_words(matcher: CatalogMatche
|
||||
assert result.candidates[0].sku == "BIT-0077"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("brand", ["makita", "макита", "макиты", "макиту"])
|
||||
def test_makita_transliterations_share_the_same_brand_alias(
|
||||
matcher: CatalogMatcher, brand: str
|
||||
) -> None:
|
||||
result = matcher.match(f"шуруповерт как у {brand}, только дешевле")
|
||||
|
||||
assert result.status == "ambiguous"
|
||||
assert [candidate.sku for candidate in result.candidates] == [
|
||||
"INS-0010",
|
||||
"INS-0017",
|
||||
"INS-0003",
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("query", ["шурпуоверт на 12в", "шураыввавёрт на 12в"])
|
||||
def test_product_type_survives_typos(matcher: CatalogMatcher, query: str) -> None:
|
||||
result = matcher.match(query)
|
||||
|
||||
@@ -0,0 +1,234 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Literal
|
||||
|
||||
import pytest
|
||||
|
||||
from app.matcher import CatalogMatcher
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
MESSAGE_PATHS = (ROOT / "messages.txt", ROOT / "more_messages.txt")
|
||||
MatchStatus = Literal["matched", "ambiguous", "not_found"]
|
||||
ExpectedResult = tuple[MatchStatus, tuple[str, ...]]
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class MessageCase:
|
||||
id: str
|
||||
source: str
|
||||
line_number: int
|
||||
message: str
|
||||
|
||||
|
||||
def _load_message_cases() -> tuple[MessageCase, ...]:
|
||||
cases: list[MessageCase] = []
|
||||
for path in MESSAGE_PATHS:
|
||||
for line_number, raw_line in enumerate(
|
||||
path.read_text(encoding="utf-8").splitlines(), start=1
|
||||
):
|
||||
message = raw_line.strip()
|
||||
if not message:
|
||||
continue
|
||||
cases.append(
|
||||
MessageCase(
|
||||
id=f"{path.name}:{line_number}",
|
||||
source=path.name,
|
||||
line_number=line_number,
|
||||
message=message,
|
||||
)
|
||||
)
|
||||
return tuple(cases)
|
||||
|
||||
|
||||
MESSAGE_CASES = _load_message_cases()
|
||||
|
||||
# Baseline approved for the current corpus. The key is the message text so adding
|
||||
# or moving unrelated lines does not invalidate every expectation below.
|
||||
EXPECTED_RESULTS: dict[str, ExpectedResult] = {
|
||||
"здравствуйте, есть саморезы гкл 3.5х25?": ("ambiguous", ("SAM-0063", "SAM-0061", "SAM-0062")),
|
||||
"дрель ударная prowerk pw-750 в наличии?": ("matched", ("INS-0008",)),
|
||||
"кабель шввп 2х0.5 сколько за метр": ("matched", ("KAB-0017",)),
|
||||
"диск пильный 190 на 48 зубьев": ("matched", ("DSK-0034",)),
|
||||
"уровень 1000 мм есть?": ("matched", ("RIN-0024",)),
|
||||
"гайки м10 нужны": ("matched", ("KRP-0020",)),
|
||||
"лента фум 12 мм": ("matched", ("RAS-0060",)),
|
||||
"перчатки нитриловые есть?": ("matched", ("PER-0002",)),
|
||||
"проф труба 20х20 стенка полтора": ("matched", ("TRB-0002",)),
|
||||
"шурик на 12в недорогой": ("ambiguous", ("INS-0010", "INS-0017", "INS-0003")),
|
||||
"болгарка на 230 какая есть": ("ambiguous", ("INS-0014", "INS-0028", "INS-0007")),
|
||||
"гкл 9.5 сколько лист": ("matched", ("GKL-0001",)),
|
||||
"хомуты пластиковые 4.8х400": ("matched", ("RAS-0014",)),
|
||||
"сдс бур 6 на 110": ("matched", ("BIT-0074",)),
|
||||
"наждачка р120 листами": ("matched", ("RAS-0050",)),
|
||||
"круг зачистной на 125": ("matched", ("DSK-0018",)),
|
||||
"бита ph2 50 мм": ("matched", ("BIT-0005",)),
|
||||
"саморезы по дереву 3.5х45 пачка 200": ("matched", ("SAM-0010",)),
|
||||
"труба профильная 20х20х2": ("matched", ("TRB-0003",)),
|
||||
"ввгнг лс 3х1.5": ("matched", ("KAB-0010",)),
|
||||
"диск отрезной по металлу 115х1.2": ("matched", ("DSK-0002",)),
|
||||
"бур sds 10х210": ("matched", ("BIT-0080",)),
|
||||
"дюбель-гвоздь 6х60": ("matched", ("KRP-0048",)),
|
||||
"саморезы по металлу 4.2х70": ("not_found", ()),
|
||||
"труба профильная 35х35": ("not_found", ()),
|
||||
"кабель ввгнг 4х2.5": ("not_found", ()),
|
||||
"ушм на 150": ("not_found", ()),
|
||||
"бита t50": ("not_found", ()),
|
||||
"шпилька м16 метровая": ("not_found", ()),
|
||||
"нужен кабель": ("ambiguous", ("KAB-0004", "KAB-0005", "KAB-0008")),
|
||||
"дайте дюбелей": ("ambiguous", ("KRP-0050", "KRP-0047", "KRP-0048")),
|
||||
"сверло нужно": ("ambiguous", ("BIT-0060", "BIT-0061", "BIT-0062")),
|
||||
"какие есть диски": ("ambiguous", ("DSK-0011", "DSK-0012", "DSK-0013")),
|
||||
"перфоратор посоветуйте": ("ambiguous", ("INS-0012", "INS-0026", "INS-0005")),
|
||||
"шуруповерт как у макиты, только дешевле": ("ambiguous", ("INS-0010", "INS-0017", "INS-0003")),
|
||||
"здравствуйте, вы до скольки работаете?": ("not_found", ()),
|
||||
"можно оплатить картой при получении?": ("not_found", ()),
|
||||
"где находится ваш магазин": ("not_found", ()),
|
||||
"статус заказа 4512 подскажите": ("not_found", ()),
|
||||
"спасибо, заказ получил, все отлично": ("not_found", ()),
|
||||
"добрый день, саморезы по дереву 3.5х25 пачка 200 штук": ("matched", ("SAM-0001",)),
|
||||
"саморез гкл 3,5 на 25 кг": ("matched", ("SAM-0063",)),
|
||||
"нужны черные саморезы 4.2x75 упаковка 1000": ("matched", ("SAM-0023",)),
|
||||
"самарезы па дереву 3.5х45 200шт": ("ambiguous", ("SAM-0010", "SAM-0012", "SAM-0011")),
|
||||
"саморезы 99х999": ("not_found", ()),
|
||||
"дюбель гвоздь 6x40": ("matched", ("KRP-0047",)),
|
||||
"дюбеля 8 на 80 есть?": ("matched", ("KRP-0049",)),
|
||||
"анкерный болт м10": ("ambiguous", ("KRP-0009", "KRP-0010", "KRP-0011")),
|
||||
"гайка м8 оцинкованная": ("matched", ("KRP-0019",)),
|
||||
"шайбы м12 сколько стоят": ("ambiguous", ("KRP-0032", "KRP-0031")),
|
||||
"болт м6х30 50 штук": ("ambiguous", ("KRP-0002", "KRP-0001", "KRP-0003")),
|
||||
"шпилька м10 один метр": ("matched", ("KRP-0034",)),
|
||||
"крепеж какой-нибудь": ("not_found", ()),
|
||||
"кабель ввг нг ls 3*2,5": ("matched", ("KAB-0011",)),
|
||||
"провод пвс 2х1.5 нужен": ("not_found", ()),
|
||||
"ШВВП 2 X 0,75, цена за метр?": ("matched", ("KAB-0018",)),
|
||||
"кабель кг 3х2,5": ("not_found", ()),
|
||||
"кабель для чайника": ("ambiguous", ("KAB-0004", "KAB-0005", "KAB-0008")),
|
||||
"3х1.5": ("not_found", ()),
|
||||
"ввгнг 20х20": ("not_found", ()),
|
||||
"труба профильная 40x20x2": ("not_found", ()),
|
||||
"профтруба 60 40 3": ("ambiguous", ("TRB-0009", "TRB-0010", "TRB-0015")),
|
||||
"труба квадратная двадцать на двадцать": ("ambiguous", ("TRB-0003", "TRB-0007", "TRB-0008")),
|
||||
"труба 35х35х2": ("not_found", ()),
|
||||
"нужна труба длиной до луны": ("ambiguous", ("TRB-0003", "TRB-0007", "TRB-0008")),
|
||||
"гипсокартон 12,5 влагостойкий": ("matched", ("GKL-0003",)),
|
||||
"гкл обычный 9.5 мм": ("matched", ("GKL-0001",)),
|
||||
"лист гипса зеленый": ("not_found", ()),
|
||||
"гипсокортан 12.5": ("ambiguous", ("GKL-0002", "GKL-0004", "GKL-0003")),
|
||||
"фанера 18 мм": ("not_found", ()),
|
||||
"дрель prowerk pw-750": ("matched", ("INS-0008",)),
|
||||
"аккумуляторная дрель 18в": ("not_found", ()),
|
||||
"шуруповёрт 12 вольт самый дешёвый": ("ambiguous", ("INS-0010", "INS-0017", "INS-0003")),
|
||||
"перфоратор sds plus 800 вт": ("not_found", ()),
|
||||
"болгарка 125": ("ambiguous", ("INS-0013", "INS-0027", "INS-0006")),
|
||||
"ушм 230 мм 2200 вт": ("not_found", ()),
|
||||
"лобзик посоветуйте": ("ambiguous", ("INS-0031", "INS-0029")),
|
||||
"makita df333 есть?": ("not_found", ()),
|
||||
"инструмент чтобы сверлить стену": ("ambiguous", ("BIT-0067", "BIT-0068", "BIT-0069")),
|
||||
"дрэл 9999 ват": ("not_found", ()),
|
||||
"бур sds-plus 8x160": ("matched", ("BIT-0077",)),
|
||||
"бур 10 на 210 sds+": ("matched", ("BIT-0080",)),
|
||||
"бур SDS MAX 20х400": ("not_found", ()),
|
||||
"сверло по металлу 6 мм": ("matched", ("BIT-0045",)),
|
||||
"сверло по дереву 10x120": ("not_found", ()),
|
||||
"бита ph2 25": ("matched", ("BIT-0004",)),
|
||||
"набор бит torx": ("not_found", ()),
|
||||
"бита t30 50 мм": ("matched", ("BIT-0029",)),
|
||||
"бита t99": ("not_found", ()),
|
||||
"насадка на шуруповерт": ("ambiguous", ("INS-0010", "INS-0011", "INS-0024")),
|
||||
"диск отрезной 125х1,0 металл": ("matched", ("DSK-0003",)),
|
||||
"круг по металлу 230 на 2.5": ("not_found", ()),
|
||||
"пильный диск 190x48t": ("ambiguous", ("DSK-0034", "DSK-0032", "DSK-0033")),
|
||||
"алмазный диск 125 бетон": ("ambiguous", ("DSK-0025", "DSK-0026", "DSK-0027")),
|
||||
"диск на болгарку": ("ambiguous", ("DSK-0011", "DSK-0012", "DSK-0013")),
|
||||
"круг квадратный 12х34": ("not_found", ()),
|
||||
"наждачная бумага p80": ("matched", ("RAS-0049",)),
|
||||
"шкурка нулёвка": ("ambiguous", ("RAS-0049", "RAS-0048", "RAS-0052")),
|
||||
"круг лепестковый 125 р40": ("matched", ("RAS-0054",)),
|
||||
"лента фум 19 мм": ("not_found", ()),
|
||||
"изолента синяя 15 мм": ("not_found", ()),
|
||||
"хомут 4,8x300 черный": ("not_found", ()),
|
||||
"стяжки пластиковые": ("ambiguous", ("RAS-0009", "RAS-0011", "RAS-0012")),
|
||||
"перчатки нитриловые размер l": ("matched", ("PER-0002",)),
|
||||
"перчатки хб с пвх": ("matched", ("PER-0001",)),
|
||||
"мешки для мусора 120 литров": ("ambiguous", ("RAS-0035", "RAS-0034")),
|
||||
"пена монтажная зимняя": ("ambiguous", ("RAS-0015", "RAS-0016")),
|
||||
"герметик силиконовый прозрачный": ("ambiguous", ("RAS-0018", "RAS-0017")),
|
||||
"клей жидкие гвозди": ("ambiguous", ("KRP-0043", "KRP-0044", "KRP-0045")),
|
||||
"WD-40 400 мл": ("not_found", ()),
|
||||
"здравствуйте": ("not_found", ()),
|
||||
"вы сегодня открыты?": ("not_found", ()),
|
||||
"какой адрес": ("not_found", ()),
|
||||
"есть доставка в химки?": ("not_found", ()),
|
||||
"можно вернуть товар без чека": ("not_found", ()),
|
||||
"примите заказ 12345": ("not_found", ()),
|
||||
"где мой заказ 000001": ("not_found", ()),
|
||||
"оплатил, но денег нет": ("not_found", ()),
|
||||
"спасибо большое!": ("not_found", ()),
|
||||
"позовите человека": ("not_found", ()),
|
||||
"1234567890": ("not_found", ()),
|
||||
"000000000000000000": ("not_found", ()),
|
||||
"42": ("not_found", ()),
|
||||
"qwerty asdf zxcv": ("not_found", ()),
|
||||
"йцукен фывапр олдж": ("not_found", ()),
|
||||
"абракадабра крокодил вертолёт": ("not_found", ()),
|
||||
"!@#$%^&*()": ("not_found", ()),
|
||||
"..........": ("not_found", ()),
|
||||
"купить купить купить": ("not_found", ()),
|
||||
"null": ("not_found", ()),
|
||||
"None": ("not_found", ()),
|
||||
"SELECT * FROM catalog;": ("not_found", ()),
|
||||
"<script>alert(1)</script>": ("not_found", ()),
|
||||
"../../etc/passwd": ("not_found", ()),
|
||||
"🤖🔩🔨": ("not_found", ()),
|
||||
"САМОБРЕЗЫ ГКЛ 3.5Х25???": ("not_found", ()),
|
||||
"ну эээ диск этот круглый короче": ("ambiguous", ("DSK-0011", "DSK-0012", "DSK-0013")),
|
||||
"мне два метра того красного": ("not_found", ()),
|
||||
"хочу всё и сразу за сто рублей": ("not_found", ()),
|
||||
"статус заказа 4512 и бур 8х160": ("matched", ("BIT-0077",)),
|
||||
"диск": ("ambiguous", ("DSK-0011", "DSK-0012", "DSK-0013")),
|
||||
}
|
||||
|
||||
|
||||
def test_message_files_are_present_and_have_expected_size() -> None:
|
||||
counts = {
|
||||
path.name: sum(
|
||||
bool(line.strip())
|
||||
for line in path.read_text(encoding="utf-8").splitlines()
|
||||
)
|
||||
for path in MESSAGE_PATHS
|
||||
}
|
||||
assert counts["messages.txt"] > 0
|
||||
assert counts["more_messages.txt"] >= 100
|
||||
|
||||
messages = [case.message for case in MESSAGE_CASES]
|
||||
assert len(messages) == len(set(messages)), "message texts must be unique"
|
||||
assert set(EXPECTED_RESULTS) == set(messages)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("case", MESSAGE_CASES, ids=lambda case: case.id)
|
||||
def test_every_message_can_be_matched(
|
||||
matcher: CatalogMatcher, case: MessageCase
|
||||
) -> None:
|
||||
result = matcher.match(case.message)
|
||||
|
||||
assert result.message == case.message
|
||||
assert result.status in {"matched", "ambiguous", "not_found"}
|
||||
assert len(result.candidates) <= 3
|
||||
assert len({candidate.sku for candidate in result.candidates}) == len(
|
||||
result.candidates
|
||||
)
|
||||
assert all(0.0 <= candidate.confidence <= 1.0 for candidate in result.candidates)
|
||||
|
||||
if result.status == "matched":
|
||||
assert len(result.candidates) == 1
|
||||
elif result.status == "ambiguous":
|
||||
assert 2 <= len(result.candidates) <= 3
|
||||
else:
|
||||
assert result.candidates == []
|
||||
|
||||
expected_status, expected_skus = EXPECTED_RESULTS[case.message]
|
||||
assert result.status == expected_status
|
||||
assert tuple(candidate.sku for candidate in result.candidates) == expected_skus
|
||||
Reference in New Issue
Block a user