140 lines
4.7 KiB
Python
140 lines
4.7 KiB
Python
"""Canonical session identity and one-shot scope regression tests."""
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from app.services.sessions import SessionStore
|
|
|
|
|
|
def _start(store: SessionStore, *, org: str, user: str, group: str, persona: str):
|
|
return store.start(
|
|
org_id=org,
|
|
user_id=user,
|
|
group_id=group,
|
|
persona_id=persona,
|
|
persona_name=f"Persona {persona}",
|
|
)
|
|
|
|
|
|
def test_same_user_and_persona_in_different_groups_are_independent(tmp_path: Path):
|
|
store = SessionStore(tmp_path)
|
|
|
|
first, resumed_first = _start(
|
|
store, org="org-1", user="user-1", group="group-a", persona="persona-01"
|
|
)
|
|
store.update(first["id"], status="finished", outcome="won")
|
|
|
|
second, resumed_second = _start(
|
|
store, org="org-1", user="user-1", group="group-b", persona="persona-01"
|
|
)
|
|
|
|
assert resumed_first is False
|
|
assert resumed_second is False
|
|
assert second["id"] != first["id"]
|
|
assert second["group_id"] == "group-b"
|
|
|
|
|
|
def test_finished_session_blocks_only_the_same_scope(tmp_path: Path):
|
|
store = SessionStore(tmp_path)
|
|
session, _ = _start(
|
|
store, org="org-1", user="user-1", group="group-a", persona="persona-01"
|
|
)
|
|
store.update(session["id"], status="finished", outcome="lost")
|
|
|
|
with pytest.raises(ValueError, match="already trained"):
|
|
_start(store, org="org-1", user="user-1", group="group-a", persona="persona-01")
|
|
|
|
|
|
def test_active_session_resumes_only_for_same_org_user_group_and_persona(tmp_path: Path):
|
|
store = SessionStore(tmp_path)
|
|
original, resumed = _start(
|
|
store, org="org-1", user="user-1", group="group-a", persona="persona-01"
|
|
)
|
|
assert resumed is False
|
|
|
|
same, same_resumed = _start(
|
|
store, org="org-1", user="user-1", group="group-a", persona="persona-01"
|
|
)
|
|
different_group, group_resumed = _start(
|
|
store, org="org-1", user="user-1", group="group-b", persona="persona-01"
|
|
)
|
|
different_org, org_resumed = _start(
|
|
store, org="org-2", user="user-1", group="group-a", persona="persona-01"
|
|
)
|
|
different_user, user_resumed = _start(
|
|
store, org="org-1", user="user-2", group="group-a", persona="persona-01"
|
|
)
|
|
|
|
assert same_resumed is True
|
|
assert same["id"] == original["id"]
|
|
assert group_resumed is False
|
|
assert org_resumed is False
|
|
assert user_resumed is False
|
|
assert len({different_group["id"], different_org["id"], different_user["id"]}) == 3
|
|
|
|
|
|
def test_list_for_user_is_org_scoped_and_preview_is_opt_in(tmp_path: Path):
|
|
store = SessionStore(tmp_path)
|
|
_start(store, org="org-1", user="user-1", group="group-a", persona="persona-01")
|
|
preview, _ = store.start(
|
|
org_id="org-1",
|
|
user_id="user-1",
|
|
group_id="group-a",
|
|
persona_id="persona-02",
|
|
persona_name="Preview",
|
|
mode="preview",
|
|
)
|
|
other_org, _ = _start(
|
|
store, org="org-2", user="user-1", group="group-b", persona="persona-01"
|
|
)
|
|
|
|
visible = store.list_for_user("user-1", org_id="org-1")
|
|
assert {row["id"] for row in visible} != {other_org["id"]}
|
|
assert preview["id"] not in {row["id"] for row in visible}
|
|
assert preview["mode"] == "preview"
|
|
assert preview["org_id"] == "org-1"
|
|
assert len(store.list_for_user("user-1", org_id="org-1", include_preview=True)) == 2
|
|
|
|
|
|
def test_start_publishes_seeded_session_state_atomically(tmp_path: Path):
|
|
store = SessionStore(tmp_path)
|
|
session, resumed = store.start(
|
|
org_id="org-1",
|
|
user_id="user-1",
|
|
group_id="group-a",
|
|
persona_id="persona-01",
|
|
persona_name="Customer",
|
|
persona_meta={"scenario": "f2f_call", "locale": "th"},
|
|
scenario="f2f_call",
|
|
locale="th",
|
|
messages=[{"role": "customer", "text": "สวัสดีค่ะ"}],
|
|
internal={"turns": 0, "score": 50, "signals": []},
|
|
)
|
|
|
|
assert resumed is False
|
|
assert session["status"] == "active"
|
|
assert session["scenario"] == "f2f_call"
|
|
assert session["messages"] == [{"role": "customer", "text": "สวัสดีค่ะ"}]
|
|
assert session["internal"]["turns"] == 0
|
|
|
|
|
|
def test_scope_scans_ignore_non_mapping_rows(tmp_path: Path, monkeypatch):
|
|
store = SessionStore(tmp_path)
|
|
monkeypatch.setattr(store.sessions, "where", lambda _predicate: [object()])
|
|
|
|
assert store.active_for_scope(
|
|
org_id="org-1",
|
|
user_id="user-1",
|
|
group_id="group-a",
|
|
persona_id="persona-01",
|
|
) is None
|
|
assert store.latest_for_scope(
|
|
org_id="org-1",
|
|
user_id="user-1",
|
|
group_id="group-a",
|
|
persona_id="persona-01",
|
|
) is None
|
|
assert store.list_for_user("user-1", org_id="org-1") == []
|