open-swe/tests/dashboard/test_user_mappings.py

137 lines
4.8 KiB
Python
Raw Normal View History

feat: Store-backed GitHub/Slack user mapping (self-service + admin) (#1369) * Replace hardcoded GitHub-email map with Store-backed user mapping Move the static GITHUB_USER_EMAIL_MAP to a Store-backed bidirectional mapping (GitHub login <-> work email <-> optional Slack ID) with an in-process cache, self-service onboarding, and admin management. - agent/dashboard/user_mappings.py: Store CRUD + login/email/slack-id indexes, sync cache readers for hot paths, async fallthrough, and a bulk_import that preserves existing richer records. - Migrate all read sites (auth.py, agent_overrides.py, authorship.py, github_comments.py, webapp.py x2) off the dict. - Unmapped Slack tags now run on the GitHub App installation token (use_installation_token_fallback) and get an ephemeral "link your GitHub account" prompt carrying the Slack id + email via a signed account-link token threaded through the OAuth state. - OAuth callback completes a self-service (org-gated) mapping from that token, falling back to the verified GitHub email. - Admin CRUD endpoints + one-time legacy import; dashboard UI section. - Legacy dict retained only as the import payload (no longer read). Tests: mapping store, account-link round-trip + completion, mapped vs unmapped Slack flows; existing trust-gate tests updated to prime cache. * Address review: cold-cache email resolution + stale alias de-indexing - agent_overrides: add resolve_login_from_email_async that falls through to the Store on a cold cache; use it at the async repo-resolution call sites (Slack repo config, Linear comment, owner-metadata) so a mapped user still resolves to their GitHub login + dashboard default_repo on a fresh worker. - user_mappings.upsert_mapping: de-index the existing login before re-indexing so a changed email/Slack id no longer leaves stale aliases resolving to the login in-process. - Tests for both fixes; update Slack repo-config test to patch the async resolver.
2026-06-01 14:37:19 -07:00
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:
fix: use Slack OIDC mappings for Slack thread ownership (#1410) * 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
2026-06-04 12:58:30 -07:00
await um.upsert_mapping(github_login="dev", work_email="dev@x.com")
feat: Store-backed GitHub/Slack user mapping (self-service + admin) (#1369) * Replace hardcoded GitHub-email map with Store-backed user mapping Move the static GITHUB_USER_EMAIL_MAP to a Store-backed bidirectional mapping (GitHub login <-> work email <-> optional Slack ID) with an in-process cache, self-service onboarding, and admin management. - agent/dashboard/user_mappings.py: Store CRUD + login/email/slack-id indexes, sync cache readers for hot paths, async fallthrough, and a bulk_import that preserves existing richer records. - Migrate all read sites (auth.py, agent_overrides.py, authorship.py, github_comments.py, webapp.py x2) off the dict. - Unmapped Slack tags now run on the GitHub App installation token (use_installation_token_fallback) and get an ephemeral "link your GitHub account" prompt carrying the Slack id + email via a signed account-link token threaded through the OAuth state. - OAuth callback completes a self-service (org-gated) mapping from that token, falling back to the verified GitHub email. - Admin CRUD endpoints + one-time legacy import; dashboard UI section. - Legacy dict retained only as the import payload (no longer read). Tests: mapping store, account-link round-trip + completion, mapped vs unmapped Slack flows; existing trust-gate tests updated to prime cache. * Address review: cold-cache email resolution + stale alias de-indexing - agent_overrides: add resolve_login_from_email_async that falls through to the Store on a cold cache; use it at the async repo-resolution call sites (Slack repo config, Linear comment, owner-metadata) so a mapped user still resolves to their GitHub login + dashboard default_repo on a fresh worker. - user_mappings.upsert_mapping: de-index the existing login before re-indexing so a changed email/Slack id no longer leaves stale aliases resolving to the login in-process. - Tests for both fixes; update Slack repo-config test to patch the async resolver.
2026-06-01 14:37:19 -07:00
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:
fix: use Slack OIDC mappings for Slack thread ownership (#1410) * 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
2026-06-04 12:58:30 -07:00
await um.upsert_mapping(github_login="newbie", work_email="n@x.com", status="pending")
feat: Store-backed GitHub/Slack user mapping (self-service + admin) (#1369) * Replace hardcoded GitHub-email map with Store-backed user mapping Move the static GITHUB_USER_EMAIL_MAP to a Store-backed bidirectional mapping (GitHub login <-> work email <-> optional Slack ID) with an in-process cache, self-service onboarding, and admin management. - agent/dashboard/user_mappings.py: Store CRUD + login/email/slack-id indexes, sync cache readers for hot paths, async fallthrough, and a bulk_import that preserves existing richer records. - Migrate all read sites (auth.py, agent_overrides.py, authorship.py, github_comments.py, webapp.py x2) off the dict. - Unmapped Slack tags now run on the GitHub App installation token (use_installation_token_fallback) and get an ephemeral "link your GitHub account" prompt carrying the Slack id + email via a signed account-link token threaded through the OAuth state. - OAuth callback completes a self-service (org-gated) mapping from that token, falling back to the verified GitHub email. - Admin CRUD endpoints + one-time legacy import; dashboard UI section. - Legacy dict retained only as the import payload (no longer read). Tests: mapping store, account-link round-trip + completion, mapped vs unmapped Slack flows; existing trust-gate tests updated to prime cache. * Address review: cold-cache email resolution + stale alias de-indexing - agent_overrides: add resolve_login_from_email_async that falls through to the Store on a cold cache; use it at the async repo-resolution call sites (Slack repo config, Linear comment, owner-metadata) so a mapped user still resolves to their GitHub login + dashboard default_repo on a fresh worker. - user_mappings.upsert_mapping: de-index the existing login before re-indexing so a changed email/Slack id no longer leaves stale aliases resolving to the login in-process. - Tests for both fixes; update Slack repo-config test to patch the async resolver.
2026-06-01 14:37:19 -07:00
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:
fix: use Slack OIDC mappings for Slack thread ownership (#1410) * 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
2026-06-04 12:58:30 -07:00
await um.upsert_mapping(github_login="gone", work_email="g@x.com")
feat: Store-backed GitHub/Slack user mapping (self-service + admin) (#1369) * Replace hardcoded GitHub-email map with Store-backed user mapping Move the static GITHUB_USER_EMAIL_MAP to a Store-backed bidirectional mapping (GitHub login <-> work email <-> optional Slack ID) with an in-process cache, self-service onboarding, and admin management. - agent/dashboard/user_mappings.py: Store CRUD + login/email/slack-id indexes, sync cache readers for hot paths, async fallthrough, and a bulk_import that preserves existing richer records. - Migrate all read sites (auth.py, agent_overrides.py, authorship.py, github_comments.py, webapp.py x2) off the dict. - Unmapped Slack tags now run on the GitHub App installation token (use_installation_token_fallback) and get an ephemeral "link your GitHub account" prompt carrying the Slack id + email via a signed account-link token threaded through the OAuth state. - OAuth callback completes a self-service (org-gated) mapping from that token, falling back to the verified GitHub email. - Admin CRUD endpoints + one-time legacy import; dashboard UI section. - Legacy dict retained only as the import payload (no longer read). Tests: mapping store, account-link round-trip + completion, mapped vs unmapped Slack flows; existing trust-gate tests updated to prime cache. * Address review: cold-cache email resolution + stale alias de-indexing - agent_overrides: add resolve_login_from_email_async that falls through to the Store on a cold cache; use it at the async repo-resolution call sites (Slack repo config, Linear comment, owner-metadata) so a mapped user still resolves to their GitHub login + dashboard default_repo on a fresh worker. - user_mappings.upsert_mapping: de-index the existing login before re-indexing so a changed email/Slack id no longer leaves stale aliases resolving to the login in-process. - Tests for both fixes; update Slack repo-config test to patch the async resolver.
2026-06-01 14:37:19 -07:00
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)
fix: use Slack OIDC mappings for Slack thread ownership (#1410) * 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
2026-06-04 12:58:30 -07:00
await um.upsert_mapping(github_login="cold", work_email="cold@x.com")
feat: Store-backed GitHub/Slack user mapping (self-service + admin) (#1369) * Replace hardcoded GitHub-email map with Store-backed user mapping Move the static GITHUB_USER_EMAIL_MAP to a Store-backed bidirectional mapping (GitHub login <-> work email <-> optional Slack ID) with an in-process cache, self-service onboarding, and admin management. - agent/dashboard/user_mappings.py: Store CRUD + login/email/slack-id indexes, sync cache readers for hot paths, async fallthrough, and a bulk_import that preserves existing richer records. - Migrate all read sites (auth.py, agent_overrides.py, authorship.py, github_comments.py, webapp.py x2) off the dict. - Unmapped Slack tags now run on the GitHub App installation token (use_installation_token_fallback) and get an ephemeral "link your GitHub account" prompt carrying the Slack id + email via a signed account-link token threaded through the OAuth state. - OAuth callback completes a self-service (org-gated) mapping from that token, falling back to the verified GitHub email. - Admin CRUD endpoints + one-time legacy import; dashboard UI section. - Legacy dict retained only as the import payload (no longer read). Tests: mapping store, account-link round-trip + completion, mapped vs unmapped Slack flows; existing trust-gate tests updated to prime cache. * Address review: cold-cache email resolution + stale alias de-indexing - agent_overrides: add resolve_login_from_email_async that falls through to the Store on a cold cache; use it at the async repo-resolution call sites (Slack repo config, Linear comment, owner-metadata) so a mapped user still resolves to their GitHub login + dashboard default_repo on a fresh worker. - user_mappings.upsert_mapping: de-index the existing login before re-indexing so a changed email/Slack id no longer leaves stale aliases resolving to the login in-process. - Tests for both fixes; update Slack repo-config test to patch the async resolver.
2026-06-01 14:37:19 -07:00
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:
fix: use Slack OIDC mappings for Slack thread ownership (#1410) * 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
2026-06-04 12:58:30 -07:00
await um.upsert_mapping(github_login="zeta", work_email="z@x.com")
await um.upsert_mapping(github_login="alpha", work_email="a@x.com")
feat: Store-backed GitHub/Slack user mapping (self-service + admin) (#1369) * Replace hardcoded GitHub-email map with Store-backed user mapping Move the static GITHUB_USER_EMAIL_MAP to a Store-backed bidirectional mapping (GitHub login <-> work email <-> optional Slack ID) with an in-process cache, self-service onboarding, and admin management. - agent/dashboard/user_mappings.py: Store CRUD + login/email/slack-id indexes, sync cache readers for hot paths, async fallthrough, and a bulk_import that preserves existing richer records. - Migrate all read sites (auth.py, agent_overrides.py, authorship.py, github_comments.py, webapp.py x2) off the dict. - Unmapped Slack tags now run on the GitHub App installation token (use_installation_token_fallback) and get an ephemeral "link your GitHub account" prompt carrying the Slack id + email via a signed account-link token threaded through the OAuth state. - OAuth callback completes a self-service (org-gated) mapping from that token, falling back to the verified GitHub email. - Admin CRUD endpoints + one-time legacy import; dashboard UI section. - Legacy dict retained only as the import payload (no longer read). Tests: mapping store, account-link round-trip + completion, mapped vs unmapped Slack flows; existing trust-gate tests updated to prime cache. * Address review: cold-cache email resolution + stale alias de-indexing - agent_overrides: add resolve_login_from_email_async that falls through to the Store on a cold cache; use it at the async repo-resolution call sites (Slack repo config, Linear comment, owner-metadata) so a mapped user still resolves to their GitHub login + dashboard default_repo on a fresh worker. - user_mappings.upsert_mapping: de-index the existing login before re-indexing so a changed email/Slack id no longer leaves stale aliases resolving to the login in-process. - Tests for both fixes; update Slack repo-config test to patch the async resolver.
2026-06-01 14:37:19 -07:00
listed = await um.list_mappings()
assert [m["github_login"] for m in listed] == ["alpha", "zeta"]