CI / ruff · pytest (pull_request) Successful in 22s
Freeze interfaces between lanes A (bot), B (backend), C (dashboard) so three developers work in parallel without touching each other's files: dialog Event/Reply + engine registry with echo stub, integration protocols with in-memory defaults, MonitoringService with noop stub, /api/v1 schemas with fixture-backed routes, frozen main.py wiring, config/modules.yaml + due_scales(), DAR-5, tzdata, Andrey trajectory, lanes in WORKFLOW, ADR-010. Refs #34 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
198 lines
9.4 KiB
Python
198 lines
9.4 KiB
Python
"""Tests for app.core.privacy (issue #2, guardrail #1: only scrubbed text reaches the LLM)."""
|
||
|
||
import pytest
|
||
|
||
from app.core.privacy import pseudo_id, scrub
|
||
|
||
# ---------------------------------------------------------------------------------------------------
|
||
# scrub(): identifiers are replaced
|
||
# ---------------------------------------------------------------------------------------------------
|
||
|
||
|
||
@pytest.mark.parametrize(
|
||
("text", "expected"),
|
||
[
|
||
# phones
|
||
("Мой телефон +7 (916) 123-45-67, звоните", "Мой телефон [PHONE], звоните"),
|
||
("номер 8-916-123-45-67", "номер [PHONE]"),
|
||
("номер 89161234567", "номер [PHONE]"),
|
||
("+79161234567", "[PHONE]"),
|
||
("горячая линия 8 800 2000 122", "горячая линия [PHONE]"),
|
||
("мобильный 916 123 45 67", "мобильный [PHONE]"),
|
||
("домашний 123-45-67", "домашний [PHONE]"),
|
||
# e-mail, links, handles
|
||
("пишите на ivan.petrov@mail.ru", "пишите на [EMAIL]"),
|
||
("или почта@мэйл.рф", "или [EMAIL]"),
|
||
("см. https://example.com/page?x=1.", "см. [URL]."),
|
||
("сайт www.clinic.ru", "сайт [URL]"),
|
||
("мой канал t.me/ivan_99", "мой канал [HANDLE]"),
|
||
("страница https://vk.com/id123", "страница [HANDLE]"),
|
||
("ник @ivan_petrov в телеге", "ник [HANDLE] в телеге"),
|
||
# documents
|
||
("СНИЛС 112-233-445 95", "СНИЛС [SNILS]"),
|
||
("СНИЛС 11223344595", "СНИЛС [SNILS]"),
|
||
("паспорт серия 4510 номер 123456", "паспорт [PASSPORT]"),
|
||
("паспорт 45 10 123456", "паспорт [PASSPORT]"),
|
||
("паспорт 4510 123456", "паспорт [PASSPORT]"),
|
||
("ИНН 7707083893", "ИНН [INN]"),
|
||
("ИНН 500100732259", "ИНН [INN]"),
|
||
("карта 4276 3800 1234 5678", "карта [CARD]"),
|
||
("карта 4276-3800-1234-5678", "карта [CARD]"),
|
||
("карта 4276380012345678", "карта [CARD]"),
|
||
# dates of birth
|
||
("родился 12.03.1985", "родился [DATE]"),
|
||
("д.р. 1985-03-12", "д.р. [DATE]"),
|
||
("родилась 5 мая 1988 г.", "родилась [DATE]"),
|
||
("1985 г.р.", "[DATE]"),
|
||
("дата рождения: 1990", "дата рождения: [DATE]"),
|
||
("родился в 1990 году", "родился в [DATE] году"),
|
||
# addresses
|
||
("живу: г. Москва, ул. Ленина, д. 5, кв. 12", "живу: [ADDRESS]"),
|
||
("улица Тверская 7", "[ADDRESS]"),
|
||
("Ленинский проспект, 32", "[ADDRESS]"),
|
||
("пр-т Мира, д. 10, корп. 2, кв. 45", "[ADDRESS]"),
|
||
("ул. 8 Марта 3", "[ADDRESS]"),
|
||
("а потом дом 7 кв. 3", "а потом [ADDRESS]"),
|
||
],
|
||
)
|
||
def test_scrub_identifiers(text: str, expected: str) -> None:
|
||
assert scrub(text) == expected
|
||
|
||
|
||
@pytest.mark.parametrize(
|
||
("text", "expected"),
|
||
[
|
||
("Меня зовут Алексей Петров", "Меня зовут [NAME]"),
|
||
("меня зовут алексей", "меня зовут [NAME]"),
|
||
("Иванов Иван Иванович пришёл", "[NAME] пришёл"),
|
||
("врач д-р Смирнова Е. А., психиатр", "врач д-р [NAME], психиатр"),
|
||
("Е.А. Смирнова назначила", "[NAME] назначила"),
|
||
("Сегодня Саша опять не спала", "Сегодня [NAME] опять не спала"),
|
||
("мой брат саша приехал", "мой брат [NAME] приехал"),
|
||
("звонил Алексею вчера", "звонил [NAME] вчера"),
|
||
("встретил Ирину Сергеевну", "встретил [NAME]"),
|
||
("был у Петрова", "был у [NAME]"),
|
||
("Вера Ивановна сказала", "[NAME] сказала"),
|
||
("Вчера звонила Вера.", "Вчера звонила [NAME]."),
|
||
("Надежда Петрова пришла", "[NAME] пришла"),
|
||
("встретил Петрова Ивана", "встретил [NAME]"),
|
||
("его зовут Тимофей", "его зовут [NAME]"),
|
||
("Анна-Мария приехала", "[NAME] приехала"),
|
||
("с Павлом и Леной", "с [NAME] и [NAME]"),
|
||
],
|
||
)
|
||
def test_scrub_names(text: str, expected: str) -> None:
|
||
assert scrub(text) == expected
|
||
|
||
|
||
def test_scrub_full_message() -> None:
|
||
text = (
|
||
"Здравствуйте, я Алексей Смирнов, СНИЛС 112-233-445 95, тел. +7 916 123 45 67.\n"
|
||
"Живу на ул. Ленина, д. 5, кв. 12. Сплю плохо, тошнота 3 дня."
|
||
)
|
||
assert scrub(text) == (
|
||
"Здравствуйте, я [NAME], СНИЛС [SNILS], тел. [PHONE].\nЖиву на [ADDRESS]. Сплю плохо, тошнота 3 дня."
|
||
)
|
||
|
||
|
||
# ---------------------------------------------------------------------------------------------------
|
||
# scrub(): ordinary clinical text is not mangled (false-positive guards)
|
||
# ---------------------------------------------------------------------------------------------------
|
||
|
||
|
||
@pytest.mark.parametrize(
|
||
"text",
|
||
[
|
||
"сплю плохо, кошмары почти каждую ночь",
|
||
"Сплю плохо, кошмары почти каждую ночь",
|
||
"выпил 2 бутылки пива",
|
||
"сертралин 50 мг 1 р/сут утром после еды",
|
||
"PHQ-9 = 14, п.9 = 0",
|
||
"GAD-7: 9, PCL-5 = 45",
|
||
"тошнота 3 дня подряд",
|
||
"принял таблетку в 08:00, уснул в 23:30",
|
||
"сплю 4-6 часов",
|
||
"давление 120/80, пульс 90, вес 75.5 кг",
|
||
"0.5 мг на ночь, полтаблетки 1/2",
|
||
"12 марта был у врача",
|
||
"в 2010 г. Мне стало хуже",
|
||
"и т.д. 5 раз за ночь",
|
||
"на улице страшно, проезд оплатил",
|
||
"Снова не спал. Сегодня лучше.",
|
||
"Слава богу, лучше. Роман читаю.",
|
||
"Надежда есть.",
|
||
"Любой ценой. Света нет, боюсь темноты.",
|
||
"кости ломит, коли укол",
|
||
"Принимаю Феварин и Глицин",
|
||
"Не могу уснуть без пива.",
|
||
"Всё бессмысленно, устал от всего",
|
||
"От таблеток только хуже, брошу",
|
||
"Был в ПНД, сказали прийти через 4 недели",
|
||
"день 13, настроение 2 из 10",
|
||
],
|
||
)
|
||
def test_scrub_keeps_clinical_text(text: str) -> None:
|
||
assert scrub(text) == text
|
||
|
||
|
||
# ---------------------------------------------------------------------------------------------------
|
||
# scrub(): idempotency and type
|
||
# ---------------------------------------------------------------------------------------------------
|
||
|
||
|
||
@pytest.mark.parametrize(
|
||
"text",
|
||
[
|
||
"Меня зовут Алексей Петров, тел. 8 (916) 123-45-67, ivan@mail.ru",
|
||
"СНИЛС 112-233-445 95, паспорт 4510 123456, ИНН 7707083893, карта 4276 3800 1234 5678",
|
||
"г. Москва, ул. Ленина, д. 5, кв. 12; родился 12.03.1985; t.me/ivan; @ivan_petrov",
|
||
"д-р Смирнова Е. А. и Иванов Иван Иванович",
|
||
"сертралин 50 мг, PHQ-9 = 14, сплю 4-6 часов",
|
||
"[NAME] [PHONE] [EMAIL]",
|
||
],
|
||
)
|
||
def test_scrub_is_idempotent(text: str) -> None:
|
||
once = scrub(text)
|
||
assert scrub(once) == once
|
||
|
||
|
||
def test_scrub_returns_str_and_handles_empty() -> None:
|
||
assert scrub("") == ""
|
||
assert isinstance(scrub("Алексей"), str)
|
||
|
||
|
||
# ---------------------------------------------------------------------------------------------------
|
||
# pseudo_id()
|
||
# ---------------------------------------------------------------------------------------------------
|
||
|
||
KEY = "test-key-0123456789abcdef0123456789abcdef"
|
||
|
||
|
||
def test_pseudo_id_format_and_determinism() -> None:
|
||
token = pseudo_id("112-233-445 95", key=KEY)
|
||
assert len(token) == 32
|
||
assert all(c in "0123456789abcdef" for c in token)
|
||
assert pseudo_id("112-233-445 95", key=KEY) == token
|
||
|
||
|
||
@pytest.mark.parametrize("variant", ["11223344595", "112-233-445 95", "112 233 445 95", " 112-233-445-95 "])
|
||
def test_pseudo_id_normalises_snils(variant: str) -> None:
|
||
assert pseudo_id(variant, key=KEY) == pseudo_id("11223344595", key=KEY)
|
||
|
||
|
||
def test_pseudo_id_depends_on_key_and_snils() -> None:
|
||
assert pseudo_id("11223344595", key=KEY) != pseudo_id("11223344595", key=KEY + "x")
|
||
assert pseudo_id("11223344595", key=KEY) != pseudo_id("11223344596", key=KEY)
|
||
|
||
|
||
def test_pseudo_id_default_key_from_settings() -> None:
|
||
from app.config import get_settings
|
||
|
||
assert pseudo_id("11223344595") == pseudo_id("11223344595", key=get_settings().pseudo_key)
|
||
|
||
|
||
@pytest.mark.parametrize("bad", ["", "1234567890", "123456789012", "112-233-445 9X", "11223344595"])
|
||
def test_pseudo_id_rejects_invalid_snils(bad: str) -> None:
|
||
with pytest.raises(ValueError):
|
||
pseudo_id(bad, key=KEY)
|