"""Talk rules end to end: stored, edited, and obeyed by the roster, a friend and a crowd. The table of what `decide` answers is `test_talk_decide.py`. This file is where those answers reach something: the roster a model is told, the friend it may ask, the crowd picker's two lists, a member added by hand that must still speak, and a rule that lets a model from another data group in. """ from __future__ import annotations import pytest from sqlalchemy import select from lembas.db.models import ( DEFAULT_GROUP, Chat, Connection, CrowdMember, DataGroup, Model, TalkRule, User, ) from lembas.services import chat as chat_service from lembas.services import crowd, settings_store, talk from lembas.services import subagent as subagent_service from lembas.services.crypto import encrypt @pytest.fixture def owner(db, registered) -> User: """The first account, demoted: an administrator holds `rules.override`.""" user = db.scalars(select(User).order_by(User.created_at)).first() user.role = "user" db.commit() return user @pytest.fixture def setup(db, owner): settings_store.update(db, {"enabled": True}, key=settings_store.CROWD) db.add(DataGroup(id="hosted", name="Hosted")) local = Connection(name="Local", base_url="http://127.0.0.1:1", api_key_encrypted=encrypt("")) cloud = Connection( name="Cloud", base_url="http://127.0.0.1:2", api_key_encrypted=encrypt(""), data_group_id="hosted", ) db.add_all([local, cloud]) db.flush() for position, (connection, name) in enumerate( [(local, "gpt-oss"), (local, "qwen38"), (local, "bonsai"), (cloud, "deepseek")] ): db.add(Model(connection_id=connection.id, model_id=name, position=position)) chat = Chat( user_id=owner.id, model_id="gpt-oss", connection_id=local.id, data_group_id=DEFAULT_GROUP, title="t", ) db.add(chat) db.commit() return chat def _ids(models) -> list[str]: return [m.model_id for m in models] def _grant_override(db): settings_store.update(db, {"default_permissions": {talk.PERMISSION: True}}) # --- The roster and the friend ------------------------------------------------------ def test_an_instance_deny_takes_a_model_off_the_roster(db, owner, setup): talk.set_rule(db, None, "gpt-oss", "qwen38", "deny") roster = chat_service.roster_block(db, owner, exclude="gpt-oss", group=DEFAULT_GROUP) assert "qwen38" not in roster and "bonsai" in roster def test_a_friend_the_rules_forbid_is_refused_with_the_reason(db, owner, setup): talk.set_rule(db, None, "gpt-oss", "qwen38", "deny") friend, refusal = subagent_service._resolve_friend( db, owner, "qwen38", asking="gpt-oss", group=DEFAULT_GROUP ) assert friend is None listed = refusal.split("These are the ones you can:")[-1] assert "qwen38" not in listed def test_the_rule_is_read_from_the_main_model_only(db, owner, setup): """bonsai may not talk to qwen38 -- which says nothing about gpt-oss's chats.""" talk.set_rule(db, None, "bonsai", "qwen38", "deny") assert "qwen38" in _ids(talk.offered(db, owner, "gpt-oss", DEFAULT_GROUP)) assert "qwen38" not in _ids(talk.offered(db, owner, "bonsai", DEFAULT_GROUP)) def test_a_closed_instance_offers_only_what_is_allowed(db, owner, setup): settings_store.update(db, {"mode": "closed"}, key=settings_store.RULES) talk.set_rule(db, None, "gpt-oss", "bonsai", "allow") assert _ids(talk.offered(db, owner, "gpt-oss", DEFAULT_GROUP)) == ["bonsai"] # --- Other data groups ----------------------------------------------------------------- def test_another_group_is_not_offered_until_a_rule_allows_it(db, owner, setup): assert "deepseek" not in _ids(talk.offered(db, owner, "gpt-oss", DEFAULT_GROUP)) talk.set_rule(db, None, "gpt-oss", "deepseek", "allow") assert "deepseek" in _ids(talk.offered(db, owner, "gpt-oss", DEFAULT_GROUP)) def test_a_member_from_another_group_reads_its_own_groups_data(db, owner, setup): from lembas.services import data_groups speaker = chat_service.Speaker("deepseek", None) assert data_groups.for_speaker(db, owner, setup, speaker) == "hosted" # --- The crowd: offered, and by hand ------------------------------------------------------- def test_the_picker_names_what_it_holds_back_and_why(client, db, owner, setup): talk.set_rule(db, None, "gpt-oss", "qwen38", "deny") talk.set_rule(db, owner, "gpt-oss", "bonsai", "deny") page = client.get(f"/chat/{setup.id}").text held = page.split("Not offered to this model")[1] assert "qwen38" in held and "bonsai" in held assert "The instance's rule" in held or "The instance's rule" in held assert "You may still add it yourself." in held def test_a_persons_own_rule_is_soft_for_them(client, db, owner, setup): talk.set_rule(db, owner, "gpt-oss", "bonsai", "deny") client.patch(f"/api/chats/{setup.id}", data={"crowd_model_ids": ["bonsai"]}) assert [m.model_id for m in db.scalars(select(CrowdMember))] == ["bonsai"] def test_an_instance_rule_is_hard_without_the_override(client, db, owner, setup): talk.set_rule(db, None, "gpt-oss", "qwen38", "deny") client.patch(f"/api/chats/{setup.id}", data={"crowd_model_ids": ["qwen38"]}) assert list(db.scalars(select(CrowdMember))) == [] def test_with_the_override_an_instance_rule_can_be_passed_by_hand(client, db, owner, setup): _grant_override(db) talk.set_rule(db, None, "gpt-oss", "qwen38", "deny") client.patch(f"/api/chats/{setup.id}", data={"crowd_model_ids": ["qwen38"]}) assert [m.model_id for m in db.scalars(select(CrowdMember))] == ["qwen38"] def test_a_member_added_by_hand_still_speaks(db, owner, setup): """Send time asks `addable`, not `offered`, or the choice would be undone.""" talk.set_rule(db, owner, "gpt-oss", "bonsai", "deny") db.add(CrowdMember(chat_id=setup.id, model_id="bonsai")) db.commit() assert _ids(crowd.member_speakers(db, setup, owner)) == ["gpt-oss", "bonsai"] def test_a_member_the_instance_later_forbids_is_skipped_and_named(db, owner, setup): db.add(CrowdMember(chat_id=setup.id, model_id="qwen38")) db.commit() talk.set_rule(db, None, "gpt-oss", "qwen38", "deny") assert _ids(crowd.member_speakers(db, setup, owner)) == ["gpt-oss"] assert crowd.unreachable_members(db, setup, owner) == ["qwen38"] # --- Storing and editing ------------------------------------------------------------------------ def test_both_directions_writes_two_rules_and_a_second_write_replaces(db, setup): talk.set_rule(db, None, "a", "b", "deny", both=True) assert {(r.from_model, r.to_model) for r in db.scalars(select(TalkRule))} == { ("a", "b"), ("b", "a"), } talk.set_rule(db, None, "a", "b", "allow") assert db.scalar(select(TalkRule).where(TalkRule.from_model == "a")).effect == "allow" def test_a_person_cannot_delete_an_instance_rule(db, owner, setup): talk.set_rule(db, None, "a", "b", "deny") rule = db.scalar(select(TalkRule)) assert talk.delete_rule(db, owner, rule.id) is False assert talk.delete_rule(db, None, rule.id) is True def test_the_admin_page_adds_a_rule_and_draws_the_matrix(client, db, registered, setup): # The fixture demoted the first account; the page is an administrator's. user = db.scalars(select(User).order_by(User.created_at)).first() user.role = "admin" db.commit() client.post("/admin/rules", data={"from_model": "gpt-oss", "to_model": "bonsai", "effect": "deny"}) page = client.get("/admin/rules").text assert "Who may talk to whom" in page assert page.count("talk-matrix__no") >= 1 def test_the_settings_card_sets_a_persons_mode_and_rules(client, db, owner, setup): client.post("/api/preferences/talk-mode", data={"mode": "closed"}) client.post("/api/preferences/talk-rules", data={"from_model": "gpt-oss", "to_model": "bonsai", "effect": "allow"}) db.expire_all() user = db.get(User, owner.id) assert talk.user_mode(user) == "closed" assert _ids(talk.offered(db, user, "gpt-oss", DEFAULT_GROUP)) == ["bonsai"]