Files
ss-tools/backend/tests/agent/test_title_cleaning.py
busya 3b4ac807a5 feat(agent+ui): fullstack agent module refactoring + UI/UX improvements
## 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
2026-06-30 15:21:05 +03:00

130 lines
5.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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