Rebrands the product from MobilityOps to Fleet Ops across the UI, backend defaults and knowledge base, and makes nl-BE/en-GB/fr-BE full first-class languages: i18next with eager-bundled per-namespace resources, a persisted accessible language switcher (topbar and mobile drawer), locale-aware date/number formatting, and a coverage test that fails the build on any missing or empty translation key. Backend dynamic content (demo scenarios, blocked-reason text, integration status) moves from fixed English/Dutch prose to stable message codes + params so the frontend can localize it; the demo knowledge base gains a fully translated NL/EN/FR procedure corpus (11 documents each) with per-language retrieval and localized evidence-state messages. The Demo Guide becomes breakpoint-adaptive: a docked rail on extra-wide desktop, a floating panel that auto-collapses to a persistent, closable progress chip on standard desktop/tablet, and a collapsed/half/full bottom sheet on mobile -- with scroll+focus+ highlight on "go to this step", Escape handling, and reduced-motion support. The Data Quality Workbench gets accessible choice-card decisions with a clear primary/ secondary/tertiary action hierarchy; the Automation ledger groups repeated successes and uses meaningful short refs; the Audit trail groups events by correlation id with human action labels and readable before/after diffs. Attention Queue, Today's movements, Vehicles, Bookings and Data Quality rows are fully clickable (stretched-link pattern) with independent secondary links, keyboard support and mobile touch targets. Fixes a topbar overflow on mobile caused by the new language switcher (moved into the mobile drawer at <=960px) and two dangling aria-labelledby references introduced this session. Updates all affected Playwright specs for the new nl-BE default and the new Audit/DemoGuide DOM structure, and adds new i18n-coverage, demo-guide-adaptive and clickable-rows specs. 131 backend tests, Ruff and mypy, and 71 Playwright tests pass. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
130 lines
4.5 KiB
Python
130 lines
4.5 KiB
Python
from __future__ import annotations
|
|
|
|
import httpx
|
|
|
|
from app.services.knowledge.demo import DemoKnowledgeProvider
|
|
from app.services.knowledge.ragcore import RAGcoreKnowledgeProvider
|
|
|
|
|
|
def test_s6_damage_question_is_grounded_with_expected_sources():
|
|
provider = DemoKnowledgeProvider()
|
|
answer = provider.ask(
|
|
"What must I do when a vehicle returns with damage?", "test-correlation-1"
|
|
)
|
|
assert answer.evidence_state == "grounded"
|
|
document_ids = {s.document_id for s in answer.sources}
|
|
assert "damage-procedure" in document_ids
|
|
assert "vehicle-return-procedure" in document_ids
|
|
assert answer.answer # never empty for a grounded answer
|
|
for source in answer.sources:
|
|
assert source.excerpt
|
|
|
|
|
|
def test_unrelated_question_is_insufficient():
|
|
provider = DemoKnowledgeProvider()
|
|
answer = provider.ask("What is the capital of France?", "test-correlation-2")
|
|
assert answer.evidence_state == "insufficient"
|
|
assert "France" not in answer.answer # never fabricates an answer beyond the procedures
|
|
|
|
|
|
def test_demo_provider_health_reports_document_count():
|
|
provider = DemoKnowledgeProvider()
|
|
health = provider.health()
|
|
assert health.provider == "demo"
|
|
assert health.available is True
|
|
assert health.document_count == 11
|
|
|
|
|
|
def test_demo_provider_health_reports_document_count_per_language():
|
|
provider = DemoKnowledgeProvider()
|
|
for language in ("nl-BE", "en-GB", "fr-BE"):
|
|
assert provider.health(language).document_count == 11
|
|
|
|
|
|
def test_demo_provider_grounds_damage_question_in_dutch():
|
|
provider = DemoKnowledgeProvider()
|
|
answer = provider.ask(
|
|
"Wat moet ik doen als een voertuig terugkomt met schade?",
|
|
"test-correlation-nl",
|
|
"nl-BE",
|
|
)
|
|
assert answer.evidence_state == "grounded"
|
|
document_ids = {s.document_id for s in answer.sources}
|
|
assert "damage-procedure" in document_ids
|
|
|
|
|
|
def test_demo_provider_grounds_damage_question_in_french():
|
|
provider = DemoKnowledgeProvider()
|
|
answer = provider.ask(
|
|
"Que dois-je faire quand un véhicule revient avec des dommages ?",
|
|
"test-correlation-fr",
|
|
"fr-BE",
|
|
)
|
|
assert answer.evidence_state == "grounded"
|
|
document_ids = {s.document_id for s in answer.sources}
|
|
assert "damage-procedure" in document_ids
|
|
|
|
|
|
def test_demo_provider_insufficient_evidence_message_is_localized():
|
|
provider = DemoKnowledgeProvider()
|
|
nl_answer = provider.ask(
|
|
"Wat is de hoofdstad van Frankrijk?", "test-correlation-nl-2", "nl-BE"
|
|
)
|
|
fr_answer = provider.ask(
|
|
"Quelle est la capitale de la France ?", "test-correlation-fr-2", "fr-BE"
|
|
)
|
|
assert nl_answer.evidence_state == "insufficient"
|
|
assert fr_answer.evidence_state == "insufficient"
|
|
assert nl_answer.answer != fr_answer.answer
|
|
assert "France" not in nl_answer.answer
|
|
assert "France" not in fr_answer.answer
|
|
|
|
|
|
def test_ask_question_endpoint_grounded(ops_client):
|
|
response = ops_client.post(
|
|
"/api/v1/knowledge/questions",
|
|
json={"question": "What must I do when a vehicle returns with damage?"},
|
|
)
|
|
assert response.status_code == 200
|
|
body = response.json()
|
|
assert body["evidence_state"] == "grounded"
|
|
assert body["provider"] == "demo"
|
|
assert len(body["sources"]) > 0
|
|
|
|
|
|
def test_ask_question_requires_authentication(client):
|
|
response = client.post("/api/v1/knowledge/questions", json={"question": "Anything?"})
|
|
assert response.status_code == 401
|
|
|
|
|
|
def test_ask_question_is_audited_without_leaking_full_text(ops_client):
|
|
ops_client.post(
|
|
"/api/v1/knowledge/questions",
|
|
json={"question": "What must I do when a vehicle returns with damage?"},
|
|
)
|
|
events = ops_client.get(
|
|
"/api/v1/audit", params={"action": "knowledge_question_asked"}
|
|
).json()
|
|
assert len(events) >= 1
|
|
metadata = events[0]["metadata"]
|
|
assert "evidence_state" in metadata
|
|
assert "source_ids" in metadata
|
|
assert "question" not in metadata
|
|
|
|
|
|
def test_knowledge_status_endpoint(ops_client):
|
|
response = ops_client.get("/api/v1/knowledge/status")
|
|
assert response.status_code == 200
|
|
assert response.json()["provider"] == "demo"
|
|
|
|
|
|
def test_ragcore_provider_degrades_to_unavailable(monkeypatch):
|
|
def fake_client(*args, **kwargs):
|
|
raise httpx.ConnectError("no ragcore in this environment")
|
|
|
|
provider = RAGcoreKnowledgeProvider()
|
|
monkeypatch.setattr(provider, "_client", fake_client)
|
|
answer = provider.ask("Anything?", "test-correlation-3")
|
|
assert answer.evidence_state == "unavailable"
|
|
assert answer.sources == []
|