| from __future__ import annotations |
|
|
| import sys |
| import time |
| from pathlib import Path |
|
|
| from fastapi.testclient import TestClient |
|
|
| BACKEND_DIR = Path(__file__).resolve().parents[1] |
| if str(BACKEND_DIR) not in sys.path: |
| sys.path.insert(0, str(BACKEND_DIR)) |
|
|
| from app import main as main_mod |
|
|
|
|
| class StubUnifiedCore: |
| def normalize_query(self, message: str, mode: str = "auto"): |
| raw = str(message or "") |
| normalized = raw.replace("repalcement", "replacement") |
| return { |
| "original_message": raw, |
| "normalized_message": normalized, |
| "changed": raw != normalized, |
| "corrections": [{"from": "repalcement", "to": "replacement"}] if raw != normalized else [], |
| } |
|
|
| def handle_message(self, message: str, state: dict, mode: str = "auto", audience: str = "auto", show_citations: bool = True): |
| return { |
| "assistant": "**Result**\n\nUnified stub answer.", |
| "state": {"mode": mode, "pending": {}}, |
| "prompt_version": "unified-1.0", |
| "sources": [ |
| { |
| "id": "S1", |
| "domain": "router_docs", |
| "doc": "XR60-Datasheet.pdf", |
| "relative_path": "/router_rag_files/01_documents/XR60-Datasheet.pdf", |
| "chunk_id": "chunk-1", |
| "excerpt": "XR60 spec excerpt", |
| } |
| ] |
| if show_citations |
| else [], |
| "files": ["/router_rag_files/01_documents/XR60-Datasheet.pdf"], |
| "effective_audience": "external", |
| "meta": {"domain": "router_docs", "domain_label": "Router docs/specs"}, |
| } |
|
|
| def list_files(self): |
| return {"router_docs": ["01_documents/XR60-Datasheet.pdf"]} |
|
|
| def health(self): |
| return {"ok": True, "domains": {"router_docs": {"ok": True}}} |
|
|
| def record_router_workbook_feedback( |
| self, |
| *, |
| request_id: str, |
| verdict: str, |
| detail: str = "", |
| meta: dict | None = None, |
| ): |
| return { |
| "ok": True, |
| "feedback": { |
| "request_id": request_id, |
| "verdict": verdict, |
| "detail": detail, |
| "retrieval_mode": str((meta or {}).get("retrieval_mode") or ""), |
| }, |
| } |
|
|
| def handle_router_inventory_import( |
| self, |
| *, |
| file_bytes: bytes | None, |
| filename: str, |
| pasted_text: str, |
| mode: str = "router_lifecycle", |
| audience: str = "auto", |
| show_citations: bool = True, |
| ): |
| return { |
| "assistant": "**Result**\n\nWorkbook-backed inventory import analyzed 2 row(s).", |
| "state": {}, |
| "prompt_version": "unified-1.0", |
| "sources": [], |
| "files": ["router_workbook.xlsx"], |
| "effective_audience": "external", |
| "meta": { |
| "domain": "router_lifecycle", |
| "domain_label": "Router lifecycle", |
| "retrieval_mode": "deterministic_router_workbook_inventory_import", |
| "router_fleet_view": { |
| "title": "Workbook fleet normalization", |
| "subtitle": "Per-row workbook normalization, lifecycle, and replacement confidence.", |
| "source_label": "Uploaded inventory", |
| "uploaded_filename": filename or "fleet.csv", |
| "row_count": 2, |
| "matched_count": 1, |
| "unmatched_count": 1, |
| "review_required_count": 1, |
| "shown_row_count": 2, |
| "truncated_count": 0, |
| "rows": [], |
| }, |
| }, |
| } |
|
|
|
|
| def test_knowledgebase_message_endpoint_happy_path(monkeypatch) -> None: |
| monkeypatch.setattr(main_mod, "_get_knowledgebase_core", lambda: StubUnifiedCore()) |
|
|
| with TestClient(main_mod.app) as client: |
| res = client.post( |
| "/api/knowledgebase/message", |
| json={ |
| "message": "Compare RV50X vs XR60 from docs only", |
| "state": {}, |
| "mode": "router_docs", |
| "audience": "auto", |
| "show_citations": True, |
| "request_id": "kb-req-123", |
| }, |
| headers={"x-request-id": "kb-req-header"}, |
| ) |
|
|
| assert res.status_code == 200 |
| body = res.json() |
| assert "Unified stub answer" in str(body.get("assistant") or "") |
| assert body.get("meta", {}).get("domain") == "router_docs" |
| assert body.get("meta", {}).get("request_id") == "kb-req-header" |
| assert res.headers.get("x-request-id") == "kb-req-header" |
| assert isinstance(body.get("suggested_replies"), list) |
| assert isinstance(body.get("response_shell"), list) |
|
|
|
|
| def test_knowledgebase_tabs_default_to_unified() -> None: |
| with TestClient(main_mod.app) as client: |
| res = client.get("/api/ui/tabs") |
| assert res.status_code == 200 |
| body = res.json() |
| assert body.get("knowledgebase") is True |
| assert body.get("pots_pricing_lite") is False |
|
|
|
|
| def test_knowledgebase_tabs_expose_pots_pricing_lite_with_estimator_fallback(monkeypatch) -> None: |
| monkeypatch.setenv("MASTERS_TOOLKIT_TAB_POTS_SAVINGS_ESTIMATOR_ENABLED", "true") |
|
|
| with TestClient(main_mod.app) as client: |
| res = client.get("/api/ui/tabs") |
|
|
| assert res.status_code == 200 |
| body = res.json() |
| assert body.get("pots_estimator") is True |
| assert body.get("pots_pricing_lite") is True |
|
|
|
|
| def test_knowledgebase_message_hard_timeout_returns_guidance(monkeypatch) -> None: |
| class SlowUnifiedCore: |
| def handle_message(self, message: str, state: dict, mode: str = "auto", audience: str = "auto", show_citations: bool = True): |
| time.sleep(0.05) |
| return { |
| "assistant": "late answer", |
| "state": {}, |
| "sources": [], |
| "files": [], |
| "meta": {"domain": "knowledgebase"}, |
| } |
|
|
| monkeypatch.setattr(main_mod, "_get_knowledgebase_core", lambda: SlowUnifiedCore()) |
| monkeypatch.setenv("MASTERS_TOOLKIT_CHAT_HARD_TIMEOUT_S", "0.01") |
| monkeypatch.setenv("UNIFIED_KB_FALLBACK_EXTRA_BUDGET_S", "0") |
|
|
| with TestClient(main_mod.app) as client: |
| res = client.post( |
| "/api/knowledgebase/message", |
| json={ |
| "message": "very long synthesis request", |
| "state": {"mode": "auto"}, |
| "mode": "auto", |
| "audience": "auto", |
| "show_citations": True, |
| }, |
| ) |
|
|
| assert res.status_code == 200 |
| body = res.json() |
| assert "paused this response" in str(body.get("assistant") or "").lower() |
| assert body.get("meta", {}).get("retrieval_mode") == "hard_timeout_guidance" |
| assert body.get("state", {}).get("pending", {}).get("type") == "clarify_speed" |
|
|
|
|
| def test_knowledgebase_normalize_endpoint(monkeypatch) -> None: |
| monkeypatch.setattr(main_mod, "_get_knowledgebase_core", lambda: StubUnifiedCore()) |
| with TestClient(main_mod.app) as client: |
| res = client.post( |
| "/api/knowledgebase/normalize", |
| json={"message": "need repalcement options", "mode": "auto"}, |
| ) |
| assert res.status_code == 200 |
| body = res.json() |
| assert body.get("changed") is True |
| assert "replacement" in str(body.get("normalized_message") or "") |
|
|
|
|
| def test_router_lifecycle_normalize_chat_message_preserves_distinct_customer_names() -> None: |
| main_mod._get_knowledgebase_core.cache_clear() |
| msg = "McDonalds 24 AER2200\nMcDonald's 12 MG52\nMcDonald 49 XR60" |
| normalized, meta = main_mod._normalize_chat_message(msg, mode_hint="router_lifecycle") |
| assert normalized == msg |
| assert meta.get("changed") is False |
| assert meta.get("corrections") == [] |
|
|
|
|
| def test_router_lifecycle_normalize_chat_message_skips_instruction_wrapped_inventory_spellcheck() -> None: |
| main_mod._get_knowledgebase_core.cache_clear() |
| msg = ( |
| "Normalize this list and give row-by-row lifecycle plus replacements: " |
| "McDonalds 24 AER2200, McDonald's 12 MG52, McDonald 49 XR60" |
| ) |
| normalized, meta = main_mod._normalize_chat_message(msg, mode_hint="router_lifecycle") |
| assert normalized == msg |
| assert meta.get("changed") is False |
| assert meta.get("corrections") == [] |
|
|
|
|
| def test_knowledgebase_control_reset_endpoint(monkeypatch) -> None: |
| monkeypatch.setattr(main_mod, "_get_knowledgebase_core", lambda: StubUnifiedCore()) |
| with TestClient(main_mod.app) as client: |
| res = client.post( |
| "/api/knowledgebase/control", |
| json={"action": "reset", "state": {"pending": {"type": "x"}}, "mode": "auto"}, |
| headers={"x-request-id": "kb-control-1"}, |
| ) |
| assert res.status_code == 200 |
| body = res.json() |
| assert "Unified stub answer" in str(body.get("assistant") or "") |
| assert body.get("meta", {}).get("request_id") == "kb-control-1" |
| assert body.get("meta", {}).get("control_action") == "reset" |
| assert res.headers.get("x-request-id") == "kb-control-1" |
|
|
|
|
| def test_knowledgebase_router_inventory_import_endpoint(monkeypatch) -> None: |
| monkeypatch.setattr(main_mod, "_get_knowledgebase_core", lambda: StubUnifiedCore()) |
| with TestClient(main_mod.app) as client: |
| res = client.post( |
| "/api/knowledgebase/router_workbook/inventory/import", |
| files={"file": ("fleet.csv", b"Customer,Model,Quantity\nDarden,Current-500,5\n", "text/csv")}, |
| data={ |
| "mode": "router_lifecycle", |
| "audience": "auto", |
| "show_citations": "true", |
| }, |
| ) |
| assert res.status_code == 200 |
| body = res.json() |
| assert "Workbook-backed inventory import analyzed" in str(body.get("assistant") or "") |
| assert body.get("meta", {}).get("retrieval_mode") == "deterministic_router_workbook_inventory_import" |
| assert body.get("meta", {}).get("router_fleet_view", {}).get("uploaded_filename") == "fleet.csv" |
|
|
|
|
| def test_knowledgebase_router_workbook_feedback_endpoint(monkeypatch) -> None: |
| monkeypatch.setattr(main_mod, "_get_knowledgebase_core", lambda: StubUnifiedCore()) |
| with TestClient(main_mod.app) as client: |
| res = client.post( |
| "/api/knowledgebase/router_workbook/feedback", |
| json={ |
| "request_id": "kb-req-123", |
| "verdict": "needs_followup", |
| "detail": "This answer still needs a stronger compare explanation.", |
| "meta": { |
| "retrieval_mode": "deterministic_router_workbook_compare", |
| "router_intelligence_intent": "compare", |
| }, |
| }, |
| ) |
| assert res.status_code == 200 |
| body = res.json() |
| assert body.get("ok") is True |
| assert body.get("feedback", {}).get("request_id") == "kb-req-123" |
| assert body.get("feedback", {}).get("verdict") == "needs_followup" |
|
|
|
|
| def test_knowledgebase_telemetry_tracks_control_and_messages(monkeypatch) -> None: |
| monkeypatch.setattr(main_mod, "_get_knowledgebase_core", lambda: StubUnifiedCore()) |
| with TestClient(main_mod.app) as client: |
| msg = client.post( |
| "/api/knowledgebase/message", |
| json={"message": "hello", "state": {}, "mode": "auto", "request_id": "telemetry-msg"}, |
| ) |
| ctl = client.post( |
| "/api/knowledgebase/control", |
| json={"action": "reset", "state": {}, "mode": "auto", "request_id": "telemetry-ctl"}, |
| ) |
| tel = client.get("/api/knowledgebase/telemetry") |
| assert msg.status_code == 200 |
| assert ctl.status_code == 200 |
| assert tel.status_code == 200 |
| body = tel.json() |
| assert int(body.get("message_count") or 0) >= 1 |
| assert int(body.get("control_count") or 0) >= 1 |
|
|
|
|
| def test_get_knowledgebase_core_wires_rapid_router_catalog_provider(monkeypatch) -> None: |
| import app.knowledgebase.core as kb_core_mod |
|
|
| captured: dict = {} |
|
|
| class FakeUnifiedCore: |
| def __init__(self, **kwargs): |
| captured.update(kwargs) |
|
|
| class StubRapidRouterCore: |
| def get_store_for_client(self): |
| return {"config": {}, "products": [{"id": "stub_router"}]} |
|
|
| def get_catalog_status(self): |
| return {"loaded": True, "product_count": 1, "latest_import": {"filename": "stub_router_catalog.xlsx"}} |
|
|
| monkeypatch.setattr(main_mod, "_get_router_rag_core", lambda: object()) |
| monkeypatch.setattr(main_mod, "_get_router_core", lambda: object()) |
| monkeypatch.setattr(main_mod, "_get_masters_core", lambda: object()) |
| monkeypatch.setattr(main_mod, "_get_pots_core", lambda: object()) |
| monkeypatch.setattr(main_mod, "_get_rapid_router_core", lambda: StubRapidRouterCore()) |
| monkeypatch.setattr(kb_core_mod, "UnifiedKnowledgebaseCore", FakeUnifiedCore) |
|
|
| main_mod._get_knowledgebase_core.cache_clear() |
| try: |
| core = main_mod._get_knowledgebase_core() |
| assert isinstance(core, FakeUnifiedCore) |
| provider = captured.get("rapid_router_catalog_provider") |
| assert callable(provider) |
| data = provider() |
| assert isinstance(data, dict) |
| assert data.get("products", [{}])[0].get("id") == "stub_router" |
| workbook_provider = captured.get("rapid_router_intelligence_provider") |
| assert callable(workbook_provider) |
| workbook_core = workbook_provider() |
| assert isinstance(workbook_core, StubRapidRouterCore) |
| assert workbook_core.get_catalog_status().get("loaded") is True |
| finally: |
| main_mod._get_knowledgebase_core.cache_clear() |
|
|
|
|
| def test_maybe_bootstrap_rapid_router_catalog_loads_detected_workbook(tmp_path, monkeypatch) -> None: |
| workbook_path = tmp_path / "device_master_source_of_truth_v26_site_survey_integrated_export.xlsx" |
| workbook_path.write_bytes(b"fake-workbook") |
|
|
| loaded: dict[str, object] = {} |
|
|
| class StubRapidRouterCore: |
| def get_catalog_status(self): |
| return {"loaded": False} |
|
|
| def load_catalog_workbook(self, *, workbook_bytes: bytes, filename: str): |
| loaded["bytes"] = workbook_bytes |
| loaded["filename"] = filename |
| return {"ok": True} |
|
|
| monkeypatch.setattr( |
| main_mod, |
| "_find_rapid_router_catalog_workbook_candidate", |
| lambda: { |
| "found": True, |
| "path": workbook_path, |
| "filename": workbook_path.name, |
| "searched": True, |
| "searched_roots": [str(workbook_path.parent)], |
| }, |
| ) |
| main_mod._set_rapid_router_catalog_bootstrap_status( |
| found=False, |
| path="", |
| filename="", |
| searched=False, |
| load_attempted=False, |
| load_succeeded=False, |
| already_loaded=False, |
| error="", |
| ) |
|
|
| main_mod._maybe_bootstrap_rapid_router_catalog(StubRapidRouterCore()) |
|
|
| assert loaded["bytes"] == b"fake-workbook" |
| assert loaded["filename"] == workbook_path.name |
| status = main_mod._rapid_router_catalog_bootstrap_status() |
| assert status["found"] is True |
| assert status["path"] == str(workbook_path) |
| assert status["load_attempted"] is True |
| assert status["load_succeeded"] is True |
|
|