"""UI-Refresh cross-cutting acceptance — the merge gate (UI-REFRESH-VERIFICATION "Cross-cutting acceptance, end-to-end, via FakeSlack"). One scripted scenario that exercises Phases 1-4 together against the **real** SlackAdapter / slack_bolt stack driven by FakeSlack (no network, no tokens, no Slack app console): 1. Connect Slack against FakeSlack through the manager's gateway; allow a user; subscribe an Ops "incident" session to a channel. 2. Post a channel message -> it reaches the session as a structured connector message: the persisted user message carries a `source` with **resolved** channel/sender names (Phase 2), the live turn_start carries that same source, and the provider gets framed text WITHOUT `source`. 3. The agent proposes a tool needing approval; the (Unattended) session routes approvals to the channel -> FakeSlack receives a Block Kit card; injecting the Approve button click resumes the turn and a reply posts back to the origin channel. 4. Mute Slack for the session -> a further channel post does NOT wake it (but is still buffered). 5. `GET /v1/sessions/{id}/connections` `attention` == the persona's unconnected recommends count. State is fully isolated: `COWORKER_STATE_DIR` redirects the SecretStore and the manager's data dir lives under `tmp_path`, so the machine-global secrets/config are never touched. """ from __future__ import annotations import asyncio import time from fastapi.testclient import TestClient from coworker.interactions import decode from coworker.providers import ( AssistantTurn, ModelCapabilities, ProviderClient, ToolCall, ) from coworker.server import create_app from coworker.server.manager import SessionManager from coworker.sessions import SessionRecord SID = "incident" CHANNEL = "C_OPS" CHANNEL_NAME = "ops-incidents" USER = "U1" SENDER_DISPLAY = "Alice Display" ALERT = "\U0001f6a8 deploy to prod failed at 14:03" REPLY = "Acknowledged - investigating the deploy now." class E2EProvider(ProviderClient): """Returns queued turns and records the messages handed to it on each call, so the test can assert exactly what the provider saw (framed text, never a `source` sidecar).""" def __init__(self, turns): self._turns = list(turns) self.calls: list[list[dict]] = [] def complete(self, *, model, messages, tools=None, **settings): self.calls.append([dict(m) for m in messages]) return self._turns.pop(0) def capabilities(self, model): return ModelCapabilities() def _tool(name, args, call_id): return AssistantTurn(tool_calls=[ToolCall(id=call_id, name=name, arguments=args)]) def _text(text): return AssistantTurn(text=text, finish_reason="stop") async def _wait_until(predicate, *, timeout: float = 8.0, interval: float = 0.02): """Poll `predicate` until it returns truthy (or the timeout elapses); return the last value.""" deadline = time.monotonic() + timeout val = predicate() while not val and time.monotonic() < deadline: await asyncio.sleep(interval) val = predicate() return val def _find_card(outbound): """The mirrored approval card = a chat.postMessage carrying Block Kit blocks.""" for o in outbound: if o["method"] == "chat.postMessage" and o.get("blocks"): return o return None def _approve_button(card): """The first action button (`ocw_0` = Approve) of a mirrored approval card.""" for block in card.get("blocks") or []: if block.get("type") == "actions": for el in block.get("elements") or []: if el.get("action_id") == "ocw_0": return el return None def _find_reply(outbound, channel, text): """A plain (no-blocks) chat.postMessage = the agent's send_message reply.""" return [ o for o in outbound if o["method"] == "chat.postMessage" and not o.get("blocks") and o["channel"] == channel and o["text"] == text ] async def test_ui_refresh_cross_cutting_e2e(fake_slack, tmp_path, monkeypatch): # Isolate the SecretStore (machine-global otherwise) so "is slack connected?" is decided only # by what this test writes; the manager's own data dir lives under tmp_path/.coworker. monkeypatch.setenv("COWORKER_STATE_DIR", str(tmp_path / "state")) ws = tmp_path / "ops_ws" ws.mkdir() provider = E2EProvider( [ # turn 1: a write needing approval -> mirrored to the channel as a Block Kit card. _tool( "write_file", {"path": str(ws / "incident-note.txt"), "content": "ok"}, "call_w", ), # turn 2: reply to the origin channel (pre-allowed so it doesn't ask a second time). _tool( "send_message", {"target": f"slack:{CHANNEL}", "text": REPLY}, "call_s" ), # turn 3: wrap up. _text("Posted the acknowledgement to the channel."), ] ) mgr = SessionManager(workspace=tmp_path, provider=provider) try: # -- Step 1: connect Slack (real adapter -> FakeSlack via the manager's gateway) ---------- mgr.secrets.put( "slack:default", {"bot_token": "xoxb-test", "app_token": "xapp-test", "enabled": True}, ) live = ( await mgr.start_gateway() ) # builds the gateway + connects the SlackAdapter assert "slack" in live await fake_slack.wait_socket() # register the user + channel in FakeSlack so users.info / conversations.info resolve names fake_slack.add_user(USER, "alice", display_name=SENDER_DISPLAY) fake_slack.add_channel(CHANNEL, CHANNEL_NAME) # allow-list: the inbound gate drops unknown senders unless allowed assert mgr.allow_user("slack", USER)["ok"] is True assert mgr.set_slack_approval_owner(USER, add=True)["ok"] is True # an Ops "incident" session subscribed to the channel, Unattended, approvals -> the channel mgr.session_store.save( SessionRecord( session_id=SID, workspace=str(ws), model="gpt-5.5", mode="interactive", agent="ops", ) ) mgr.subscriptions.subscribe(SID, f"slack:{CHANNEL}") assert mgr.set_inbox_binding( "ops-incidents", channel="slack", target=CHANNEL )["ok"] mgr.inbox_routing.set_session_override(SID, "ops-incidents") mgr.unattended.set(SID, True) # pre-build the engine + pre-allow the reply tool so the reply (step 3) doesn't itself ask. engine = mgr.get_engine(SID) assert engine is not None engine.permissions.allow_tool_for_session("send_message") # observe the live turn stream for this session (the "card shows live" assertion). ws_events: list[dict] = [] async def _client_cb(msg): ws_events.append(msg) mgr.register_session_client(SID, _client_cb) client = TestClient( create_app(mgr) ) # not entered as a CM -> no second gateway start # -- Step 2 + 3a: post a channel message; it becomes a structured connector message and the # agent's approval is mirrored as a Block Kit card -------------------------------------- await fake_slack.inbound(channel=CHANNEL, user=USER, text=ALERT) card = await _wait_until(lambda: _find_card(fake_slack.outbound())) assert ( card is not None ), f"approval card never mirrored: {fake_slack.outbound()}" # (Step 2) persisted user message carries the structured, RESOLVED source sidecar. msgs = client.get(f"/v1/sessions/{SID}/messages").json()["messages"] user_msgs = [m for m in msgs if m.get("role") == "user"] assert user_msgs, msgs last = user_msgs[-1] src = last["source"] assert src["connector"] == "slack" assert src["kind"] == "channel" assert src["channel_id"] == CHANNEL assert ( src["channel_name"] == CHANNEL_NAME ) # resolved via conversations.info (Phase 2) assert src["sender_id"] == USER assert src["sender_name"] == SENDER_DISPLAY # resolved via users.info (Phase 2) assert src["text"] == ALERT # the RAW message (what the card renders) # the model-facing content is the FRAMED text, not the raw message assert "subscribed" in last["content"] and last["content"] != ALERT # (Step 2) the provider saw the framed text and NO source / unknown keys, on every call. assert provider.calls, "provider was never invoked" first_users = [m for m in provider.calls[0] if m.get("role") == "user"] assert first_users sent = first_users[-1] assert "source" not in sent assert "subscribed" in sent["content"] assert all("source" not in m for call in provider.calls for m in call) # (Step 2) the live turn_start carried the same resolved source (card shows live). starts = [e for e in ws_events if e.get("type") == "turn_start"] assert starts, ws_events live_src = starts[0]["data"]["source"] assert live_src["channel_name"] == CHANNEL_NAME assert live_src["sender_name"] == SENDER_DISPLAY assert live_src["text"] == ALERT # (Step 3a) the card is a real Block Kit approval, posted to the origin channel. assert card["channel"] == CHANNEL assert "write_file" in (card["text"] or "") button = _approve_button(card) assert button is not None and button["value"] item_id, resolution = decode(button["value"]) assert resolution == "allow" item = mgr.inbox.get(item_id) assert item is not None and item.kind == "approval" and item.state == "pending" # -- Step 3b: inject the Approve click -> turn resumes, reply posts to the origin channel -- await fake_slack.interaction( channel=CHANNEL, user=USER, username="alice", message_ts=card["ts"], action_id=button["action_id"], value=button["value"], ) replies = await _wait_until( lambda: _find_reply(fake_slack.outbound(), CHANNEL, REPLY) ) assert replies, f"reply never posted back: {fake_slack.outbound()}" assert await _wait_until(lambda: not mgr.is_running(SID)) assert mgr.inbox.get(item_id).state == "resolved" # -- Step 4: mute Slack for the session -> a further post does NOT wake it, still buffered - # The turn's mark_idle fired an auto-title completion (fire-and-forget, on a worker # thread); let it settle so its provider call can't land between the snapshot below # and the "provider not re-invoked" assertion. assert await _wait_until(lambda: SID not in mgr._autotitle_inflight) msgcount_before = len(mgr.session_messages(SID)) calls_before = len(provider.calls) resp = client.post( f"/v1/sessions/{SID}/connections", json={"connector": "slack", "enabled": False}, ).json() assert resp["ok"] is True assert "slack" not in mgr.effective_connectors(SID, "ops") muted_text = "second alert while muted" await fake_slack.inbound(channel=CHANNEL, user=USER, text=muted_text) # the message is buffered for catch-up even though it's not delivered. assert await _wait_until( lambda: any( m["text"] == muted_text for m in mgr.channel_buffer.recent(f"slack:{CHANNEL}") ) ), "muted message was not buffered" # the skip-delivery decision is synchronous with the buffering above (no await between); # a short settle guards against any stray scheduled delivery, then assert nothing woke. await asyncio.sleep(0.1) assert len(mgr.session_messages(SID)) == msgcount_before # no new turn/message assert len(provider.calls) == calls_before # provider not re-invoked assert not mgr.is_running(SID) # -- Step 5: attention == the persona's account-unconnected connector recommends ---------- detail = client.get("/v1/personas/ops").json() unconnected = [ r for r in detail["recommends"] if r["kind"] == "connector" and not r["connected"] ] view = client.get(f"/v1/sessions/{SID}/connections").json() assert view["attention"] == len(unconnected) # only slack is account-connected here -> github/datadog/pagerduty remain assert view["attention"] == 3 assert {r["connector"] for r in view["recommended"]} == { "github", "datadog", "pagerduty", } finally: await mgr.aclose() # State-dir isolation held: the SecretStore resolved to the tmp_path-scoped path, never the # machine-global ~/.config/coworker (so this run cannot mutate the real secrets hash). assert str(tmp_path) in str(mgr.secrets.path)