- Demo accounts: super_admin-only provisioning into isolated DEMO_ORG_ID tenant, 30-day UTC trial on first login, revocable, one-time credential delivery via optional SES/webhook (never persisted). Adds boto3 dependency. - Analytics/report/export/privacy: shared bounded scan budget across users/groups/ sessions, tenant-consistent session/user/group joins, scalar-only CSV export (no nested persisted-value stringification). - Ownership/tenant isolation: canonical owner-tenant predicate for list/read/chat; client sees is_owned only, never owner_user_id. - Lifecycle/races: status transition validation, analyzing is an in-progress gate (no duplicate reanalysis), structured-ready publication, stale-variant revalidation. - Auth/setup/consent/JWT/OAuth/config: fail-closed consent, bounded JWT lifetime, provider-subject atomic OAuth identity, repeated-secret rejection, strict Persona trait validation. - Chat/session/privacy: pre-seller opener redaction, corrupt-session recovery, role-aware completed-chat dashboard routing. - Frontend: Training→product→personas→practice flow, demo/role/demo guards, is_owned-based ownership display, 320×568 and 500×768 responsive E2E. - 8 independent exact-five-key review scopes passed; backend 509, frontend 26, production build 1775 modules, isolated E2E 15.
166 lines
5.9 KiB
Python
166 lines
5.9 KiB
Python
"""Private variant isolation and locked-core regression tests."""
|
|
from __future__ import annotations
|
|
|
|
from scripts.mock_llm import MockLLM
|
|
|
|
|
|
class VariantLLM:
|
|
def complete_json(self, system_prompt, user_prompt, **kwargs):
|
|
return {
|
|
"name": "Fresh Identity",
|
|
"channel": "line",
|
|
"initiation_mode": "customer",
|
|
"profession": "Different profession",
|
|
"age_group": "40s",
|
|
"location": "Chiang Mai",
|
|
"background": "Different background",
|
|
"pains": [{"name": "wrong pain"}],
|
|
"objections": ["wrong objection"],
|
|
"negotiation_levers": ["wrong lever"],
|
|
"tolerance": 5,
|
|
"special": "",
|
|
"recontact": False,
|
|
"goal": "different goal",
|
|
"decision_timeline": "later",
|
|
"budget": "different budget",
|
|
"difficulty": 1,
|
|
"tier": "A",
|
|
"product_context": "different context",
|
|
}
|
|
|
|
|
|
def _headers(token: str) -> dict[str, str]:
|
|
return {"Authorization": f"Bearer {token}"}
|
|
|
|
|
|
def _setup(user_store, login):
|
|
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_private_variant_locks_core_and_does_not_mutate_shared_group(client, user_store, login):
|
|
admin_token = _setup(user_store, login)
|
|
client.application.extensions["llm"] = MockLLM()
|
|
created = client.post(
|
|
"/api/groups",
|
|
json={"product": "CRM", "segment": "SME"},
|
|
headers=_headers(admin_token),
|
|
)
|
|
gid = created.get_json()["group"]["id"]
|
|
analyzed = client.post(f"/api/groups/{gid}/analyze", headers=_headers(admin_token))
|
|
assert analyzed.status_code == 200
|
|
group_store = client.application.extensions["group_store"]
|
|
shared_before = group_store.get(gid)
|
|
source = shared_before["personas"][0]
|
|
|
|
trainee = user_store.create_user(
|
|
org_id="org-default",
|
|
username="variant-trainee",
|
|
password="variant-trainee-password",
|
|
name="Variant Trainee",
|
|
role="user",
|
|
must_setup=False,
|
|
)
|
|
trainee_token = login(trainee["username"], "variant-trainee-password")["token"]
|
|
client.application.extensions["llm"] = VariantLLM()
|
|
|
|
response = client.post(
|
|
f"/api/groups/{gid}/personas/{source['id']}/variant",
|
|
headers=_headers(trainee_token),
|
|
)
|
|
|
|
assert response.status_code == 201, response.get_json()
|
|
payload = response.get_json()
|
|
variant = payload["persona"]
|
|
assert payload["group_id"] != gid
|
|
assert variant["id"] != source["id"]
|
|
|
|
shared_after = group_store.get(gid)
|
|
assert len(shared_after["personas"]) == len(shared_before["personas"])
|
|
assert shared_after["personas"] == shared_before["personas"]
|
|
|
|
private = group_store.get(payload["group_id"])
|
|
stored_variant = private["personas"][0]
|
|
for key in ("pains", "objections", "negotiation_levers", "tolerance", "special", "recontact", "goal", "decision_timeline", "budget", "difficulty", "tier", "product_context"):
|
|
assert stored_variant[key] == source[key]
|
|
assert stored_variant["source_persona_id"] == source["id"]
|
|
|
|
|
|
def test_other_trainee_cannot_access_private_variant(client, user_store, login):
|
|
admin_token = _setup(user_store, login)
|
|
client.application.extensions["llm"] = MockLLM()
|
|
created = client.post(
|
|
"/api/groups",
|
|
json={"product": "CRM"},
|
|
headers=_headers(admin_token),
|
|
)
|
|
gid = created.get_json()["group"]["id"]
|
|
analyzed = client.post(f"/api/groups/{gid}/analyze", headers=_headers(admin_token))
|
|
source_id = analyzed.get_json()["personas"][0]["id"]
|
|
owner = user_store.create_user(
|
|
org_id="org-default",
|
|
username="variant-owner",
|
|
password="variant-owner-password",
|
|
name="Owner",
|
|
role="user",
|
|
must_setup=False,
|
|
)
|
|
other = user_store.create_user(
|
|
org_id="org-default",
|
|
username="variant-other",
|
|
password="variant-other-password",
|
|
name="Other",
|
|
role="user",
|
|
must_setup=False,
|
|
)
|
|
owner_token = login(owner["username"], "variant-owner-password")["token"]
|
|
other_token = login(other["username"], "variant-other-password")["token"]
|
|
client.application.extensions["llm"] = VariantLLM()
|
|
created_variant = client.post(
|
|
f"/api/groups/{gid}/personas/{source_id}/variant",
|
|
headers=_headers(owner_token),
|
|
)
|
|
private_gid = created_variant.get_json()["group_id"]
|
|
|
|
response = client.get(
|
|
f"/api/groups/{private_gid}/personas", headers=_headers(other_token)
|
|
)
|
|
|
|
assert response.status_code == 403
|
|
|
|
|
|
def test_reanalysis_preserves_an_accepted_variant(client, user_store, login):
|
|
admin_token = _setup(user_store, login)
|
|
client.application.extensions["llm"] = MockLLM()
|
|
created = client.post(
|
|
"/api/groups",
|
|
json={"product": "CRM", "segment": "SME"},
|
|
headers=_headers(admin_token),
|
|
)
|
|
gid = created.get_json()["group"]["id"]
|
|
analyzed = client.post(f"/api/groups/{gid}/analyze", headers=_headers(admin_token))
|
|
source_id = analyzed.get_json()["personas"][0]["id"]
|
|
|
|
client.application.extensions["llm"] = VariantLLM()
|
|
variant_response = client.post(
|
|
f"/api/groups/{gid}/personas/{source_id}/variant",
|
|
headers=_headers(admin_token),
|
|
)
|
|
assert variant_response.status_code == 201, variant_response.get_json()
|
|
variant_id = variant_response.get_json()["persona"]["id"]
|
|
|
|
client.application.extensions["llm"] = MockLLM()
|
|
reanalyzed = client.post(f"/api/groups/{gid}/analyze", headers=_headers(admin_token))
|
|
|
|
assert reanalyzed.status_code == 200, reanalyzed.get_json()
|
|
assert variant_id in {
|
|
persona["id"]
|
|
for persona in client.application.extensions["group_store"].get(gid)["personas"]
|
|
}
|