188 lines
6.2 KiB
Python
188 lines
6.2 KiB
Python
"""Regression tests for findings from the final adversarial review."""
|
|
from __future__ import annotations
|
|
|
|
from concurrent.futures import ThreadPoolExecutor
|
|
|
|
from scripts.mock_llm import MockLLM
|
|
|
|
|
|
def _headers(token: str) -> dict[str, str]:
|
|
return {"Authorization": f"Bearer {token}"}
|
|
|
|
|
|
def _setup_admin(user_store, login) -> str:
|
|
user_store.complete_setup(
|
|
"admin",
|
|
"admin@example.com",
|
|
"admin-ready-password",
|
|
accepted_terms=True,
|
|
accepted_terms_at="2026-08-13T00:00:00Z",
|
|
)
|
|
return login("admin", "admin-ready-password")["token"]
|
|
|
|
|
|
def test_trainee_cannot_read_draft_group_by_id(client, user_store, login):
|
|
_setup_admin(user_store, login)
|
|
group_store = client.application.extensions["group_store"]
|
|
group = group_store.create(
|
|
org_id="org-default", creator_id="admin", title="Draft group"
|
|
)
|
|
trainee = user_store.create_user(
|
|
org_id="org-default",
|
|
username="draft-reader",
|
|
password="draft-reader-password",
|
|
name="Draft Reader",
|
|
role="user",
|
|
must_setup=False,
|
|
)
|
|
token = login(trainee["username"], "draft-reader-password")["token"]
|
|
|
|
response = client.get(f"/api/groups/{group['id']}", headers=_headers(token))
|
|
|
|
assert response.status_code == 403, response.get_json()
|
|
|
|
|
|
def test_analysis_publishes_ready_group_as_one_complete_record(
|
|
client, user_store, login, monkeypatch
|
|
):
|
|
token = _setup_admin(user_store, login)
|
|
client.application.extensions["llm"] = MockLLM()
|
|
group_store = client.application.extensions["group_store"]
|
|
group = group_store.create(
|
|
org_id="org-default", creator_id="admin", title="Atomic analysis"
|
|
)
|
|
group_store.update(
|
|
group["id"],
|
|
input={
|
|
"product": "CRM",
|
|
"segment": "SME",
|
|
"description": "sales tool",
|
|
"channel": "facebook",
|
|
"language": "th",
|
|
},
|
|
)
|
|
observed_ready = []
|
|
original_replace = group_store.groups.replace
|
|
|
|
def checked_replace(key, value):
|
|
if value.get("status") == "ready":
|
|
observed_ready.append(value)
|
|
assert value.get("personas")
|
|
assert value.get("report")
|
|
assert value.get("sales_kit")
|
|
return original_replace(key, value)
|
|
|
|
monkeypatch.setattr(group_store.groups, "replace", checked_replace)
|
|
response = client.post(
|
|
f"/api/groups/{group['id']}/analyze", headers=_headers(token)
|
|
)
|
|
|
|
assert response.status_code == 200, response.get_json()
|
|
assert len(observed_ready) == 1
|
|
|
|
|
|
def test_group_delete_holds_each_session_record_lock(
|
|
client, user_store, login, monkeypatch
|
|
):
|
|
token = _setup_admin(user_store, login)
|
|
group_store = client.application.extensions["group_store"]
|
|
group = group_store.create(
|
|
org_id="org-default", creator_id="admin", title="Delete group"
|
|
)
|
|
group_id = group["id"]
|
|
session_store = client.application.extensions["session_store"]
|
|
session_store.sessions.create(
|
|
{
|
|
"id": "session-to-delete",
|
|
"org_id": "org-default",
|
|
"user_id": "admin",
|
|
"group_id": group_id,
|
|
"persona_id": "persona-1",
|
|
"mode": "preview",
|
|
"status": "active",
|
|
},
|
|
key="session-to-delete",
|
|
)
|
|
|
|
from app.storage import store as storage_module
|
|
|
|
original_delete = session_store.sessions.delete
|
|
|
|
def checked_delete(key):
|
|
path = str(session_store.sessions._path(key))
|
|
assert path in storage_module._held_records.get()
|
|
return original_delete(key)
|
|
|
|
monkeypatch.setattr(session_store.sessions, "delete", checked_delete)
|
|
response = client.delete(f"/api/groups/{group_id}", headers=_headers(token))
|
|
|
|
assert response.status_code == 200, response.get_json()
|
|
assert session_store.sessions.get_or_none("session-to-delete") is None
|
|
|
|
|
|
def test_bootstrap_admin_is_process_safe_for_concurrent_first_start(
|
|
monkeypatch, tmp_path
|
|
):
|
|
from app.auth.users import UserStore
|
|
from app.config import Config
|
|
from app.factory import bootstrap_admin
|
|
|
|
monkeypatch.setattr(Config, "DATA_DIR", tmp_path)
|
|
monkeypatch.setattr(Config, "APP_ENV", "test")
|
|
monkeypatch.setattr(Config, "SECRET_KEY", "pytest-secret-0123456789abcdef0123456789")
|
|
monkeypatch.setattr(Config, "BOOTSTRAP_ADMIN_PASSWORD", "bootstrap-password")
|
|
stores = [UserStore(tmp_path), UserStore(tmp_path)]
|
|
|
|
with ThreadPoolExecutor(max_workers=2) as pool:
|
|
results = list(pool.map(bootstrap_admin, stores))
|
|
|
|
assert results == [None, None]
|
|
assert len(UserStore(tmp_path).users.all()) == 1
|
|
assert UserStore(tmp_path).get_user("admin")["role"] == "super_admin"
|
|
|
|
|
|
def test_analytics_hardest_personas_prioritize_losses_then_low_score(
|
|
client, user_store, login
|
|
):
|
|
token = _setup_admin(user_store, login)
|
|
trainee = user_store.create_user(
|
|
org_id="org-default",
|
|
username="hardest-personas-trainee",
|
|
password="hardest-personas-trainee-password",
|
|
name="Hardest Personas Trainee",
|
|
role="user",
|
|
must_setup=False,
|
|
)
|
|
session_store = client.application.extensions["session_store"]
|
|
analytics_group = client.application.extensions["group_store"].create(
|
|
org_id="org-default", creator_id="admin", title="Analytics group"
|
|
)
|
|
rows = [
|
|
("hard-1", "Hard persona", "lost", 20),
|
|
("hard-2", "Hard persona", "lost", 30),
|
|
("easy-1", "Easy persona", "won", 95),
|
|
]
|
|
for sid, persona_name, outcome, score in rows:
|
|
session_store.sessions.create(
|
|
{
|
|
"id": sid,
|
|
"org_id": "org-default",
|
|
"user_id": trainee["id"],
|
|
"group_id": analytics_group["id"],
|
|
"persona_id": persona_name.lower().replace(" ", "-"),
|
|
"persona_name": persona_name,
|
|
"mode": "trainee",
|
|
"status": "finished",
|
|
"outcome": outcome,
|
|
"debrief": {"score": score},
|
|
},
|
|
key=sid,
|
|
)
|
|
|
|
response = client.get("/api/analytics", headers=_headers(token))
|
|
|
|
assert response.status_code == 200, response.get_json()
|
|
assert [item["persona_name"] for item in response.get_json()["hardest_personas"][:2]] == [
|
|
"Hard persona",
|
|
"Easy persona",
|
|
] |