feat: OpenMesh 基础平台与 MD/PDF 转换技能
- 后端: coworker 智能体框架, WS API, 文件上传, 附件处理 - 前端: Open WebUI, 文件全量走 upload API (含 MD/TXT/JSON 等文本类) - 技能: md-to-office (pandoc + wkhtmltopdf) - 修复: 上传文件路径丢失, Agent 搜索浪费, 输出文件跑到 uploads/ - 打包: PyInstaller one-dir, 预打包 pandoc/wkhtmltopdf/chromium
This commit is contained in:
227
tests/test_slack_status.py
Normal file
227
tests/test_slack_status.py
Normal file
@@ -0,0 +1,227 @@
|
||||
"""Slack connection health (M3.6 Step 2) — three honest layers: the
|
||||
desktop↔relay socket, the cloud sign-in, and per-workspace bot tokens.
|
||||
The endpoint aggregates what the adapter observed; it never invents a
|
||||
Slack↔cloud claim (that leg is invisible from the desktop).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from coworker.connectors.base import SendResult
|
||||
from coworker.connectors.relay_client import SlackRelayAdapter
|
||||
from coworker.server import SessionManager, create_app
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client(tmp_path, monkeypatch):
|
||||
monkeypatch.setenv("COWORKER_STATE_DIR", str(tmp_path / "state"))
|
||||
manager = SessionManager(workspace=tmp_path)
|
||||
app = create_app(manager)
|
||||
with TestClient(app) as c:
|
||||
c.manager = manager
|
||||
yield c
|
||||
|
||||
|
||||
class _StubAdapter:
|
||||
"""Quacks like SlackRelayAdapter.status() without a socket."""
|
||||
|
||||
def __init__(self, state="live", teams=None):
|
||||
self._state = state
|
||||
self._teams = teams or {}
|
||||
|
||||
def status(self):
|
||||
return {
|
||||
"state": self._state,
|
||||
"reconnects": 2,
|
||||
"last_event_at": 1751970000.0,
|
||||
"last_error": "" if self._state == "live" else "boom",
|
||||
"teams": self._teams,
|
||||
}
|
||||
|
||||
|
||||
async def _noop_stop():
|
||||
return None
|
||||
|
||||
|
||||
def _gateway_with(adapter) -> SimpleNamespace:
|
||||
# `stop` because app shutdown tears the gateway down.
|
||||
return SimpleNamespace(
|
||||
_adapters={"slack": adapter} if adapter else {}, stop=_noop_stop
|
||||
)
|
||||
|
||||
|
||||
# --- endpoint aggregation ----------------------------------------------------
|
||||
|
||||
|
||||
def test_status_live_relay_with_teams(client):
|
||||
client.manager.secrets.put("slack:default", {"mode": "relay", "enabled": True})
|
||||
client.manager.secrets.put(
|
||||
"cloud:auth", {"access_token": "jwt", "account": "rohit@x.com"}
|
||||
)
|
||||
client.manager.gateway = _gateway_with(
|
||||
_StubAdapter("live", {"T1": {"token_ok": True}, "T2": {"token_ok": False}})
|
||||
)
|
||||
data = client.get("/v1/connectors/slack/status").json()
|
||||
assert data["mode"] == "relay"
|
||||
assert data["signed_in"] is True
|
||||
assert data["relay"]["state"] == "live"
|
||||
assert data["relay"]["last_event_at"] == 1751970000.0
|
||||
assert data["teams"]["T1"]["token_ok"] is True
|
||||
assert data["teams"]["T2"]["token_ok"] is False
|
||||
assert "teams" not in data["relay"] # folded up, not duplicated
|
||||
|
||||
|
||||
def test_status_reconnecting_carries_last_error(client):
|
||||
client.manager.secrets.put("slack:default", {"mode": "relay", "enabled": True})
|
||||
client.manager.gateway = _gateway_with(_StubAdapter("reconnecting"))
|
||||
data = client.get("/v1/connectors/slack/status").json()
|
||||
assert data["relay"]["state"] == "reconnecting"
|
||||
assert data["relay"]["last_error"] == "boom"
|
||||
assert data["signed_in"] is False
|
||||
|
||||
|
||||
def test_status_offline_when_no_adapter(client):
|
||||
# Relay mode configured but the gateway has no slack adapter (e.g. signed
|
||||
# out at startup so it never built) → offline, not a crash.
|
||||
client.manager.secrets.put("slack:default", {"mode": "relay", "enabled": True})
|
||||
client.manager.gateway = _gateway_with(None)
|
||||
data = client.get("/v1/connectors/slack/status").json()
|
||||
assert data["relay"]["state"] == "offline"
|
||||
assert data["teams"] == {}
|
||||
|
||||
|
||||
def test_status_manual_socket_mode_has_no_relay_layer(client):
|
||||
# Socket-Mode adapters expose no status(); the endpoint still answers.
|
||||
client.manager.secrets.put("slack:default", {"bot_token": "xoxb", "enabled": True})
|
||||
client.manager.gateway = None
|
||||
data = client.get("/v1/connectors/slack/status").json()
|
||||
assert data["mode"] == "" # not relay
|
||||
assert data["relay"]["state"] == "offline"
|
||||
|
||||
|
||||
# --- adapter state tracking --------------------------------------------------
|
||||
|
||||
|
||||
def _adapter(**kw) -> SlackRelayAdapter:
|
||||
return SlackRelayAdapter(
|
||||
"wss://relay.test/ws",
|
||||
token_provider=lambda: "jwt",
|
||||
teams={"T1": {"bot_token": "xoxb-1", "bot_user_id": "UBOT"}},
|
||||
reconnect_delay=0.0,
|
||||
**kw,
|
||||
)
|
||||
|
||||
|
||||
def test_adapter_status_offline_before_connect():
|
||||
st = _adapter().status()
|
||||
assert st["state"] == "offline"
|
||||
assert st["last_event_at"] is None
|
||||
assert st["teams"] == {"T1": {"token_ok": True}} # unknown ⇒ assumed good
|
||||
|
||||
|
||||
async def test_adapter_stamps_last_event_and_reports_live():
|
||||
class _Blocking:
|
||||
async def open(self):
|
||||
pass
|
||||
|
||||
async def recv(self):
|
||||
await asyncio.Event().wait()
|
||||
|
||||
async def close(self):
|
||||
pass
|
||||
|
||||
adapter = _adapter(transport_factory=lambda: _Blocking())
|
||||
assert await adapter.connect() is True
|
||||
try:
|
||||
assert adapter.status()["state"] == "live"
|
||||
assert adapter.status()["last_event_at"] is None
|
||||
await adapter._dispatch_slack_event(
|
||||
"T1",
|
||||
{
|
||||
"type": "app_mention",
|
||||
"user": "U1",
|
||||
"channel": "C1",
|
||||
"text": "x",
|
||||
"ts": "1",
|
||||
},
|
||||
)
|
||||
assert adapter.status()["last_event_at"] is not None
|
||||
finally:
|
||||
await adapter.disconnect()
|
||||
assert adapter.status()["state"] == "offline"
|
||||
|
||||
|
||||
async def test_adapter_connect_failure_records_last_error():
|
||||
class _Broken:
|
||||
async def open(self):
|
||||
raise OSError("relay unreachable")
|
||||
|
||||
async def recv(self):
|
||||
return None
|
||||
|
||||
async def close(self):
|
||||
pass
|
||||
|
||||
adapter = _adapter(transport_factory=lambda: _Broken())
|
||||
assert await adapter.connect() is False
|
||||
assert "relay unreachable" in adapter.status()["last_error"]
|
||||
|
||||
|
||||
async def test_send_error_marks_token_dead_and_recovers(monkeypatch):
|
||||
from coworker.connectors import relay_client
|
||||
|
||||
results = iter(
|
||||
[SendResult(False, error="invalid_auth"), SendResult(True, message_id="ts")]
|
||||
)
|
||||
monkeypatch.setattr(relay_client, "_send_slack", lambda *a, **k: next(results))
|
||||
adapter = _adapter()
|
||||
|
||||
await adapter.send("T1/C1", "hi")
|
||||
assert adapter.status()["teams"]["T1"]["token_ok"] is False
|
||||
await adapter.send("T1/C1", "hi again")
|
||||
assert adapter.status()["teams"]["T1"]["token_ok"] is True
|
||||
|
||||
|
||||
async def test_non_token_send_error_does_not_flag_token(monkeypatch):
|
||||
from coworker.connectors import relay_client
|
||||
|
||||
monkeypatch.setattr(
|
||||
relay_client,
|
||||
"_send_slack",
|
||||
lambda *a, **k: SendResult(False, error="channel_not_found"),
|
||||
)
|
||||
adapter = _adapter()
|
||||
await adapter.send("T1/C1", "hi")
|
||||
assert adapter.status()["teams"]["T1"]["token_ok"] is True
|
||||
|
||||
|
||||
async def test_slack_get_token_error_marks_team(monkeypatch):
|
||||
adapter = _adapter()
|
||||
|
||||
class _Resp:
|
||||
def json(self):
|
||||
return {"ok": False, "error": "account_inactive"}
|
||||
|
||||
class _Http:
|
||||
def __init__(self, *a, **k):
|
||||
pass
|
||||
|
||||
async def __aenter__(self):
|
||||
return self
|
||||
|
||||
async def __aexit__(self, *a):
|
||||
return False
|
||||
|
||||
async def get(self, *a, **k):
|
||||
return _Resp()
|
||||
|
||||
import httpx
|
||||
|
||||
monkeypatch.setattr(httpx, "AsyncClient", _Http)
|
||||
assert await adapter._slack_get("T1", "users.info", {"user": "U1"}) is None
|
||||
assert adapter.status()["teams"]["T1"]["token_ok"] is False
|
||||
Reference in New Issue
Block a user