Files
shandGitHub 68916aa90a feat: add simplex-support-bot-light (#7378)
* feat: add simplex-support-bot-light

* feat: package simplex-support-bot-light for docker

* docs: document simplex-support-bot-light

* fix: make a missing config actionable and bounded

* docs: note the attached compose Ctrl+C behaviour

* refactor: use the library for startup and custom data

* refactor: drop the display-name workaround

* style: shorten the startup comment

* build: build the core and the library into the image

* docs: correct where the image type rule comes from

* fix: log what the core said a command got wrong

* build: run the container as the operator's uid

* docs: keep the container uid in .env

* docs: tighten the README
2026-08-17 09:12:47 +01:00

155 lines
5.4 KiB
Python

from support_bot_light import roster
from tests.conftest import USER_ID, make_contact
def test_entry_of_reads_active_mark():
contact = make_contact(
7, "sh", {"supportBotLight": {"roster": "active", "since": "2026-08-13"}}
)
entry = roster.entry_of(contact)
assert entry is not None
assert (entry.contact_id, entry.name, entry.state, entry.since) == (
7,
"sh",
"active",
"2026-08-13",
)
def test_entry_of_returns_none_without_custom_data():
assert roster.entry_of(make_contact(7, "sh")) is None
def test_entry_of_ignores_other_namespaces():
assert roster.entry_of(make_contact(7, "sh", {"otherBot": {"roster": "active"}})) is None
def test_entry_of_ignores_unknown_state():
contact = make_contact(7, "sh", {"supportBotLight": {"roster": "banned"}})
assert roster.entry_of(contact) is None
def test_entry_of_ignores_non_dict_mark():
assert roster.entry_of(make_contact(7, "sh", {"supportBotLight": "oops"})) is None
async def test_mark_preserves_other_keys(api):
contact = make_contact(7, "sh", {"otherBot": {"keep": 1}})
api.contacts.append(contact)
await roster.mark(api, contact, "active", "2026-08-13T09:00:00+00:00")
contact_id, data = api.custom_data[-1]
assert contact_id == 7
assert data["otherBot"] == {"keep": 1}
assert data["supportBotLight"] == {"roster": "active", "since": "2026-08-13T09:00:00+00:00"}
async def test_mark_does_not_mutate_the_callers_contact(api):
original = {"otherBot": {"keep": 1}}
contact = make_contact(7, "sh", original)
api.contacts.append(contact)
await roster.mark(api, contact, "active", "2026-08-13T09:00:00+00:00")
# mark() builds a new blob; the caller's dict must be untouched.
assert original == {"otherBot": {"keep": 1}}
assert "supportBotLight" not in original
async def test_unmark_removes_only_our_key(api):
contact = make_contact(
7, "sh", {"supportBotLight": {"roster": "active"}, "otherBot": {"keep": 1}}
)
api.contacts.append(contact)
await roster.unmark(api, contact)
assert api.custom_data[-1] == (7, {"otherBot": {"keep": 1}})
async def test_unmark_clears_blob_when_nothing_left(api):
contact = make_contact(7, "sh", {"supportBotLight": {"roster": "active"}})
api.contacts.append(contact)
await roster.unmark(api, contact)
# None clears the column rather than writing an empty object.
assert api.custom_data[-1] == (7, None)
async def test_unmark_of_an_unmarked_contact_takes_nothing_away(api):
contact = make_contact(7, "sh", {"otherBot": {"keep": 1}})
api.contacts.append(contact)
await roster.unmark(api, contact)
assert api.custom_data[-1] == (7, {"otherBot": {"keep": 1}})
async def test_load_returns_marked_contacts_sorted_case_insensitively(api):
api.contacts += [
make_contact(1, "Zoe", {"supportBotLight": {"roster": "active", "since": "x"}}),
make_contact(2, "bob", {"supportBotLight": {"roster": "pending", "since": "x"}}),
make_contact(3, "unmarked"),
]
entries = await roster.load(api, USER_ID)
assert [e.name for e in entries] == ["bob", "Zoe"]
async def test_active_filters_pending(api):
api.contacts += [
make_contact(
1, "a", {"supportBotLight": {"roster": "active", "since": "x"}}, connected=True
),
make_contact(2, "b", {"supportBotLight": {"roster": "pending", "since": "x"}}),
]
assert [e.contact_id for e in await roster.active(api, USER_ID)] == [1]
async def test_find_contact_returns_none_when_absent(api):
assert await roster.find_contact(api, USER_ID, 99) is None
async def test_find_contact_returns_match(api):
api.contacts.append(make_contact(7, "sh"))
found = await roster.find_contact(api, USER_ID, 7)
assert found is not None and found["contactId"] == 7
def test_utc_now_is_iso_with_offset():
now = roster.utc_now()
assert now.endswith("+00:00") and "T" in now
async def test_active_excludes_a_marked_contact_that_is_no_longer_usable(api):
# The person deleted the bot: the mark survives but api_add_member would
# fail for them on every business chat.
api.contacts += [
make_contact(
1, "live", {"supportBotLight": {"roster": "active", "since": "x"}}, connected=True
),
make_contact(2, "dead", {"supportBotLight": {"roster": "active", "since": "x"}}),
]
assert [e.contact_id for e in await roster.active(api, USER_ID)] == [1]
def test_entry_name_is_sanitised():
contact = make_contact(7, "a\nb", {"supportBotLight": {"roster": "active", "since": "x"}})
entry = roster.entry_of(contact)
assert entry is not None
assert entry.name == "a b"
def test_entry_of_survives_a_contact_with_no_profile():
entry = roster.entry_of(
{"contactId": 7, "customData": {"supportBotLight": {"roster": "active", "since": "x"}}}
)
assert entry is not None
assert entry.name == "(unnamed)"
def test_contact_name_prefers_the_local_display_name():
# The core makes localDisplayName unique per user; two peers calling
# themselves "sh" render as "sh" and "sh_1", which is what the roster and
# the log must show.
contact = make_contact(1, "sh_1")
contact["profile"]["displayName"] = "sh"
assert roster.contact_name(contact) == "sh_1"
def test_contact_name_falls_back_to_the_profile():
contact = make_contact(1, "sh")
del contact["localDisplayName"]
assert roster.contact_name(contact) == "sh"