## Backend: agent module GRACE-Poly compliance - Split app.py (749→~280 lines) into _tool_resolver, _confirmation, _persistence - All 18 naked functions wrapped in #region/#endregion contracts - Fixed @DEFGROUP→@defgroup typos; added @DATA_CONTRACT, @SIDE_EFFECT, CoT logs - Conversation list API: added last_role, has_tool_calls, has_error, risk_level fields - Message state detection: Russian/English error patterns (недоступен, unavailable) - State field preserved in save_conversation messages - HITL titles: descriptive tool names instead of generic "HITL resume" ## Backend: conversation title generation (two-layer) - Layer 1: clean_title() — rule-based, strips file markers, pre-fetch blocks, JSON/CSV, URLs, code; truncates at 80 chars word boundary (25 unit tests, all edge cases) - Layer 2: generate_llm_title() — async best-effort LLM titling via /v1/chat/completions with per-conversation lock, graceful degradation on failure ## Frontend: conversation list indicators (orthogonal system) - Status dot (green/yellow/red/blue) per conversation state - Icon column: tool activity, errors, waiting, completed - Risk stripe (left border accent) + message count badge + relative time - Fixed group labels: "Сегодня"/"Вчера" instead of "3 ч"/"5 ч" - Hide "Окружение: —" when env is empty ## Frontend: guardrails card verification + fixes - Confirmed all interaction modes: Enter/click confirm, Escape/click deny - Auto-populate envId from environmentContextStore in DashboardDetailModel - Better error message: missing_context_hint with recovery guidance ## Design system: semantic tokens - Added category-* gradient tokens to tailwind.config.js - Sidebar + Breadcrumbs use semantic tokens (10 categories) - Raw Tailwind reduced from ~50 to 6 occurrences - Added skip-to-content link in root layout (+layout.svelte) - Added aria-label on DashboardDataGrid row checkboxes ## Protocol: INV_7 pragmatic exception - Modules may exceed 400 lines when contract-dense (every function has #region) - Recorded in semantics-core SKILL.md with rationale Total: 5841+ contracts, 2993+ edges, backend 41/41, frontend 2501/2501
130 lines
5.6 KiB
Python
130 lines
5.6 KiB
Python
# backend/tests/agent/test_title_cleaning.py
|
||
# #region Test.Agent.Persistence.CleanTitle [C:2] [TYPE Module] [SEMANTICS test,agent,persistence,title]
|
||
# @BRIEF Unit tests for rule-based conversation title cleaning — all edge cases.
|
||
# @RELATION BINDS_TO -> [AgentChat.Persistence.CleanTitle]
|
||
# @TEST_EDGE: empty_input -> "Новый диалог"
|
||
# @TEST_EDGE: whitespace_only -> "Новый диалог"
|
||
# @TEST_EDGE: file_markers -> stripped before first marker
|
||
# @TEST_EDGE: prefetch_markers -> stripped before first marker
|
||
# @TEST_EDGE: hitl_title -> preserved as-is
|
||
# @TEST_EDGE: json_input -> "Данные: {...}"
|
||
# @TEST_EDGE: url_input -> domain extracted
|
||
# @TEST_EDGE: long_text -> truncated at 80 with word boundary
|
||
# @TEST_EDGE: code_input -> first line preserved
|
||
import pytest
|
||
from src.agent._persistence import clean_title
|
||
|
||
|
||
class TestCleanTitle:
|
||
"""Rule-based title cleaning — all edge cases from the spec matrix."""
|
||
|
||
# ── Empty / whitespace ──
|
||
def test_empty_string(self):
|
||
assert clean_title("") == "Новый диалог"
|
||
|
||
def test_whitespace_only(self):
|
||
assert clean_title(" \n \t ") == "Новый диалог"
|
||
|
||
def test_none_input(self):
|
||
assert clean_title(None) == "Новый диалог"
|
||
|
||
# ── Short / normal ──
|
||
def test_short_greeting(self):
|
||
assert clean_title("Привет") == "Привет"
|
||
|
||
def test_emoji_only(self):
|
||
assert clean_title("🔥") == "🔥"
|
||
|
||
def test_normal_russian(self):
|
||
assert clean_title("Покажи доступные дашборды") == "Покажи доступные дашборды"
|
||
|
||
def test_normal_english(self):
|
||
assert clean_title("Show me all dashboards in prod") == "Show me all dashboards in prod"
|
||
|
||
# ── File markers ──
|
||
def test_file_marker_stripped(self):
|
||
result = clean_title("Покажи дашборды\n--- Uploaded file content ---\nid,name\n1,A")
|
||
assert result == "Покажи дашборды"
|
||
|
||
def test_only_file_marker(self):
|
||
result = clean_title("--- Uploaded file content ---\nid,name\n1,A")
|
||
assert result == "Новый диалог"
|
||
|
||
# ── Pre-fetch markers ──
|
||
def test_prefetch_marker_stripped(self):
|
||
result = clean_title("Мигрируй 42\n[PRE-FETCHED DATA]\nAvailable dashboards...")
|
||
assert result == "Мигрируй 42"
|
||
|
||
def test_prefetch_end_marker_stripped(self):
|
||
result = clean_title("Поиск\n[/PRE-FETCHED DATA]\nextra")
|
||
assert result == "Поиск"
|
||
|
||
# ── HITL titles (preserved) ──
|
||
def test_hitl_confirm_preserved(self):
|
||
assert clean_title("✅ get_health_summary") == "✅ get_health_summary"
|
||
|
||
def test_hitl_deny_preserved(self):
|
||
assert clean_title("⏹️ show_capabilities") == "⏹️ show_capabilities"
|
||
|
||
# ── JSON / CSV / URL detection ──
|
||
def test_json_input(self):
|
||
result = clean_title('{"id": 1, "name": "Dashboard A"}')
|
||
assert result.startswith("Данные: ")
|
||
|
||
def test_csv_input(self):
|
||
result = clean_title("id,name,status\n1,Dashboard A,active")
|
||
# CSV under 80 chars, first line kept as-is
|
||
assert result == "id,name,status"
|
||
|
||
def test_url_input(self):
|
||
result = clean_title("https://superset.example.com/dashboard/42")
|
||
# Should extract domain
|
||
assert "superset.example.com" in result
|
||
|
||
# ── Sentence cut ──
|
||
def test_cut_at_first_sentence(self):
|
||
result = clean_title(
|
||
"Чтобы проанализировать систему, мне нужно проверить health. "
|
||
"Для начала покажи список дашбордов."
|
||
)
|
||
assert "Чтобы проанализировать систему, мне нужно проверить health." in result
|
||
assert "Для начала" not in result
|
||
|
||
def test_cut_at_exclamation(self):
|
||
result = clean_title("Срочно! Проверь статус системы.")
|
||
assert result == "Срочно!"
|
||
|
||
def test_cut_at_question(self):
|
||
result = clean_title("Как работает миграция? Нужно подробное описание.")
|
||
assert result == "Как работает миграция?"
|
||
|
||
# ── Long text truncation ──
|
||
def test_long_text_truncated(self):
|
||
long_text = "Это очень длинное сообщение про миграцию дашбордов из окружения dev в prod с заменой конфигов и фиксом кросс-фильтров"
|
||
result = clean_title(long_text)
|
||
assert len(result) <= 80
|
||
assert result.endswith("…")
|
||
|
||
def test_long_text_no_spaces(self):
|
||
result = clean_title("A" * 200)
|
||
assert len(result) <= 80
|
||
|
||
# ── Code detection ──
|
||
def test_code_def(self):
|
||
result = clean_title("def migrate(): pass\n\nMore text here")
|
||
assert result == "def migrate(): pass"
|
||
|
||
def test_code_import(self):
|
||
result = clean_title("import os\nimport sys\n\nShow dashboards")
|
||
assert result == "import os"
|
||
|
||
# ── Already clean ──
|
||
def test_already_clean_title(self):
|
||
assert clean_title("CSV-файл: Анализ дашбордов") == "CSV-файл: Анализ дашбордов"
|
||
|
||
# ── Edge: no dot at end ──
|
||
def test_single_sentence_no_punctuation(self):
|
||
result = clean_title("Проверь статус системы ss-dev")
|
||
assert result == "Проверь статус системы ss-dev"
|
||
# #endregion Test.Agent.Persistence.CleanTitle
|