mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-09-30 11:33:14 +00:00
* fix: tag Slack threads with stored identity so they surface in web process_slack_mention gated the run on mapped_login (resolved from the stable Slack user id), but upsert_agent_thread_owner_metadata independently re-resolved the GitHub login from the Slack profile email. When that email differs from the user's mapping email (e.g. a personal vs work address), the lookup returned None, so github_login was never stamped on the thread and the thread never surfaced in the web Agents UI (which searches by github_login / triggering_user_email). Resolve the GitHub user from the store via the Slack id, pass that login through to the owner metadata, and use the mapping's stored work email (falling back to the Slack profile email for unmapped users) for both the run config and the thread tagging, so Slack-started threads reliably appear in web. * fix: stamp github_login on Slack threads so they surface in web process_slack_mention gated the run on mapped_login (resolved from the stable Slack user id) but upsert_agent_thread_owner_metadata re-resolved the login from the Slack profile email; when that email isn't the user's mapping email the lookup returns None and github_login is never stamped, so the thread is invisible in the web Agents UI (which searches by github_login / triggering_user_email). Pass the already-resolved mapped_login through to the owner metadata. The dashboard match keys on the stable GitHub login, so this is sufficient; the triggering email stays the live Slack profile value. * fix: preserve Slack email during account mapping * fix: require Slack OIDC for email mappings * chore: format Slack OIDC mapping cleanup
136 lines
4.8 KiB
Python
136 lines
4.8 KiB
Python
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
import pytest
|
|
|
|
from agent.dashboard import user_mappings as um
|
|
|
|
|
|
class _FakeStore:
|
|
"""Minimal in-memory stand-in for the LangGraph Store."""
|
|
|
|
def __init__(self) -> None:
|
|
self.items: dict[tuple[tuple[str, ...], str], dict[str, Any]] = {}
|
|
|
|
async def get_item(self, namespace: list[str], key: str):
|
|
value = self.items.get((tuple(namespace), key))
|
|
return {"value": value} if value is not None else None
|
|
|
|
async def put_item(self, namespace: list[str], key: str, value: dict[str, Any]) -> None:
|
|
self.items[(tuple(namespace), key)] = value
|
|
|
|
async def delete_item(self, namespace: list[str], key: str) -> None:
|
|
self.items.pop((tuple(namespace), key), None)
|
|
|
|
async def search_items(self, namespace: list[str], *, limit: int = 1000):
|
|
ns = tuple(namespace)
|
|
items = [{"value": v} for (n, _k), v in self.items.items() if n == ns]
|
|
return {"items": items[:limit]}
|
|
|
|
|
|
class _FakeClient:
|
|
def __init__(self, store: _FakeStore) -> None:
|
|
self.store = store
|
|
|
|
|
|
@pytest.fixture()
|
|
def fake_store(monkeypatch: pytest.MonkeyPatch) -> _FakeStore:
|
|
store = _FakeStore()
|
|
monkeypatch.setattr(um, "_client", lambda: _FakeClient(store))
|
|
um.clear_cache()
|
|
return store
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_upsert_and_bidirectional_lookup(fake_store: _FakeStore) -> None:
|
|
await um.upsert_mapping(
|
|
github_login="Octocat",
|
|
work_email="OCTO@example.com",
|
|
slack_user_id="U123",
|
|
)
|
|
# Login lookups are case-insensitive; email is normalized to lowercase.
|
|
assert await um.email_for_login("octocat") == "octo@example.com"
|
|
assert await um.login_for_email("octo@example.com") == "Octocat"
|
|
assert await um.login_for_slack_id("U123") == "Octocat"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_cache_readers_after_refresh(fake_store: _FakeStore) -> None:
|
|
await um.upsert_mapping(github_login="dev", work_email="dev@x.com")
|
|
um.clear_cache()
|
|
await um.refresh_cache()
|
|
assert um.cached_email_for_login("dev") == "dev@x.com"
|
|
assert um.cached_login_for_email("dev@x.com") == "dev"
|
|
assert um.is_login_mapped("dev") is True
|
|
assert um.is_login_mapped("ghost") is False
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_pending_status_not_trusted(fake_store: _FakeStore) -> None:
|
|
await um.upsert_mapping(github_login="newbie", work_email="n@x.com", status="pending")
|
|
um.clear_cache()
|
|
await um.refresh_cache()
|
|
assert um.is_login_mapped("newbie") is False
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_delete_removes_record_and_indexes(fake_store: _FakeStore) -> None:
|
|
await um.upsert_mapping(github_login="gone", work_email="g@x.com")
|
|
assert await um.email_for_login("gone") == "g@x.com"
|
|
deleted = await um.delete_mapping("gone")
|
|
assert deleted is True
|
|
assert um.cached_email_for_login("gone") is None
|
|
assert await um.get_mapping("gone") is None
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_resolve_login_from_email_async_cold_cache(
|
|
fake_store: _FakeStore, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
# Mapped user must resolve even on a cold worker (cache not yet primed),
|
|
# because repo-resolution call sites run before the cache is refreshed.
|
|
from agent.dashboard import agent_overrides
|
|
|
|
monkeypatch.setattr(agent_overrides, "login_for_email", um.login_for_email)
|
|
await um.upsert_mapping(github_login="cold", work_email="cold@x.com")
|
|
um.clear_cache()
|
|
|
|
assert await agent_overrides.resolve_login_from_email_async("cold@x.com") == "cold"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_update_deindexes_stale_email_and_slack_id(fake_store: _FakeStore) -> None:
|
|
# An update that changes the email/slack id must not leave the old aliases
|
|
# resolving to this login in the in-process cache.
|
|
await um.upsert_mapping(
|
|
github_login="mover",
|
|
work_email="old@x.com",
|
|
slack_user_id="UOLD",
|
|
)
|
|
await um.upsert_mapping(
|
|
github_login="mover",
|
|
work_email="new@x.com",
|
|
slack_user_id="UNEW",
|
|
)
|
|
|
|
assert um.cached_login_for_email("old@x.com") is None
|
|
assert um.cached_login_for_slack_id("UOLD") is None
|
|
assert um.cached_login_for_email("new@x.com") == "mover"
|
|
assert um.cached_login_for_slack_id("UNEW") == "mover"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_upsert_requires_login_and_email(fake_store: _FakeStore) -> None:
|
|
with pytest.raises(ValueError):
|
|
await um.upsert_mapping(github_login="", work_email="x@x.com")
|
|
with pytest.raises(ValueError):
|
|
await um.upsert_mapping(github_login="x", work_email="")
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_list_mappings_sorted(fake_store: _FakeStore) -> None:
|
|
await um.upsert_mapping(github_login="zeta", work_email="z@x.com")
|
|
await um.upsert_mapping(github_login="alpha", work_email="a@x.com")
|
|
listed = await um.list_mappings()
|
|
assert [m["github_login"] for m in listed] == ["alpha", "zeta"]
|