Merge branch 'main' into pr-1785
This commit is contained in:
@@ -753,8 +753,9 @@ class FeishuChannel(BaseChannel):
|
|||||||
None, self._download_file_sync, message_id, file_key, msg_type
|
None, self._download_file_sync, message_id, file_key, msg_type
|
||||||
)
|
)
|
||||||
if not filename:
|
if not filename:
|
||||||
ext = {"audio": ".opus", "media": ".mp4"}.get(msg_type, "")
|
filename = file_key[:16]
|
||||||
filename = f"{file_key[:16]}{ext}"
|
if msg_type == "audio" and not filename.endswith(".opus"):
|
||||||
|
filename = f"{filename}.opus"
|
||||||
|
|
||||||
if data and filename:
|
if data and filename:
|
||||||
file_path = media_dir / filename
|
file_path = media_dir / filename
|
||||||
|
|||||||
@@ -81,8 +81,8 @@ class SlackChannel(BaseChannel):
|
|||||||
slack_meta = msg.metadata.get("slack", {}) if msg.metadata else {}
|
slack_meta = msg.metadata.get("slack", {}) if msg.metadata else {}
|
||||||
thread_ts = slack_meta.get("thread_ts")
|
thread_ts = slack_meta.get("thread_ts")
|
||||||
channel_type = slack_meta.get("channel_type")
|
channel_type = slack_meta.get("channel_type")
|
||||||
# Only reply in thread for channel/group messages; DMs don't use threads
|
# Slack DMs don't use threads; channel/group replies may keep thread_ts.
|
||||||
thread_ts_param = thread_ts if use_thread else None
|
thread_ts_param = thread_ts if thread_ts and channel_type != "im" else None
|
||||||
|
|
||||||
# Slack rejects empty text payloads. Keep media-only messages media-only,
|
# Slack rejects empty text payloads. Keep media-only messages media-only,
|
||||||
# but send a single blank message when the bot has no text or files to send.
|
# but send a single blank message when the bot has no text or files to send.
|
||||||
@@ -278,4 +278,3 @@ class SlackChannel(BaseChannel):
|
|||||||
if parts:
|
if parts:
|
||||||
rows.append(" · ".join(parts))
|
rows.append(" · ".join(parts))
|
||||||
return "\n".join(rows)
|
return "\n".join(rows)
|
||||||
|
|
||||||
|
|||||||
@@ -310,9 +310,9 @@ def gateway(
|
|||||||
logging.basicConfig(level=logging.DEBUG)
|
logging.basicConfig(level=logging.DEBUG)
|
||||||
|
|
||||||
config = _load_runtime_config(config, workspace)
|
config = _load_runtime_config(config, workspace)
|
||||||
selected_port = port if port is not None else config.gateway.port
|
port = port if port is not None else config.gateway.port
|
||||||
|
|
||||||
console.print(f"{__logo__} Starting nanobot gateway on port {selected_port}...")
|
console.print(f"{__logo__} Starting nanobot gateway on port {port}...")
|
||||||
sync_workspace_templates(config.workspace_path)
|
sync_workspace_templates(config.workspace_path)
|
||||||
bus = MessageBus()
|
bus = MessageBus()
|
||||||
provider = _make_provider(config)
|
provider = _make_provider(config)
|
||||||
|
|||||||
@@ -327,51 +327,6 @@ def test_gateway_workspace_option_overrides_config(monkeypatch, tmp_path: Path)
|
|||||||
assert seen["workspace"] == override
|
assert seen["workspace"] == override
|
||||||
assert config.workspace_path == override
|
assert config.workspace_path == override
|
||||||
|
|
||||||
|
|
||||||
def test_gateway_uses_port_from_config_when_cli_port_is_omitted(monkeypatch, tmp_path: Path) -> None:
|
|
||||||
config_file = tmp_path / "instance" / "config.json"
|
|
||||||
config_file.parent.mkdir(parents=True)
|
|
||||||
config_file.write_text("{}")
|
|
||||||
|
|
||||||
config = Config()
|
|
||||||
config.gateway.port = 18791
|
|
||||||
|
|
||||||
monkeypatch.setattr("nanobot.config.loader.set_config_path", lambda _path: None)
|
|
||||||
monkeypatch.setattr("nanobot.config.loader.load_config", lambda _path=None: config)
|
|
||||||
monkeypatch.setattr("nanobot.cli.commands.sync_workspace_templates", lambda _path: None)
|
|
||||||
monkeypatch.setattr(
|
|
||||||
"nanobot.cli.commands._make_provider",
|
|
||||||
lambda _config: (_ for _ in ()).throw(_StopGateway("stop")),
|
|
||||||
)
|
|
||||||
|
|
||||||
result = runner.invoke(app, ["gateway", "--config", str(config_file)])
|
|
||||||
|
|
||||||
assert isinstance(result.exception, _StopGateway)
|
|
||||||
assert "Starting nanobot gateway on port 18791" in result.stdout
|
|
||||||
|
|
||||||
|
|
||||||
def test_gateway_cli_port_overrides_config_port(monkeypatch, tmp_path: Path) -> None:
|
|
||||||
config_file = tmp_path / "instance" / "config.json"
|
|
||||||
config_file.parent.mkdir(parents=True)
|
|
||||||
config_file.write_text("{}")
|
|
||||||
|
|
||||||
config = Config()
|
|
||||||
config.gateway.port = 18791
|
|
||||||
|
|
||||||
monkeypatch.setattr("nanobot.config.loader.set_config_path", lambda _path: None)
|
|
||||||
monkeypatch.setattr("nanobot.config.loader.load_config", lambda _path=None: config)
|
|
||||||
monkeypatch.setattr("nanobot.cli.commands.sync_workspace_templates", lambda _path: None)
|
|
||||||
monkeypatch.setattr(
|
|
||||||
"nanobot.cli.commands._make_provider",
|
|
||||||
lambda _config: (_ for _ in ()).throw(_StopGateway("stop")),
|
|
||||||
)
|
|
||||||
|
|
||||||
result = runner.invoke(app, ["gateway", "--config", str(config_file), "--port", "18801"])
|
|
||||||
|
|
||||||
assert isinstance(result.exception, _StopGateway)
|
|
||||||
assert "Starting nanobot gateway on port 18801" in result.stdout
|
|
||||||
|
|
||||||
|
|
||||||
def test_gateway_uses_config_directory_for_cron_store(monkeypatch, tmp_path: Path) -> None:
|
def test_gateway_uses_config_directory_for_cron_store(monkeypatch, tmp_path: Path) -> None:
|
||||||
config_file = tmp_path / "instance" / "config.json"
|
config_file = tmp_path / "instance" / "config.json"
|
||||||
config_file.parent.mkdir(parents=True)
|
config_file.parent.mkdir(parents=True)
|
||||||
@@ -400,3 +355,47 @@ def test_gateway_uses_config_directory_for_cron_store(monkeypatch, tmp_path: Pat
|
|||||||
|
|
||||||
assert isinstance(result.exception, _StopGateway)
|
assert isinstance(result.exception, _StopGateway)
|
||||||
assert seen["cron_store"] == config_file.parent / "cron" / "jobs.json"
|
assert seen["cron_store"] == config_file.parent / "cron" / "jobs.json"
|
||||||
|
|
||||||
|
|
||||||
|
def test_gateway_uses_configured_port_when_cli_flag_is_missing(monkeypatch, tmp_path: Path) -> None:
|
||||||
|
config_file = tmp_path / "instance" / "config.json"
|
||||||
|
config_file.parent.mkdir(parents=True)
|
||||||
|
config_file.write_text("{}")
|
||||||
|
|
||||||
|
config = Config()
|
||||||
|
config.gateway.port = 18791
|
||||||
|
|
||||||
|
monkeypatch.setattr("nanobot.config.loader.set_config_path", lambda _path: None)
|
||||||
|
monkeypatch.setattr("nanobot.config.loader.load_config", lambda _path=None: config)
|
||||||
|
monkeypatch.setattr("nanobot.cli.commands.sync_workspace_templates", lambda _path: None)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"nanobot.cli.commands._make_provider",
|
||||||
|
lambda _config: (_ for _ in ()).throw(_StopGateway("stop")),
|
||||||
|
)
|
||||||
|
|
||||||
|
result = runner.invoke(app, ["gateway", "--config", str(config_file)])
|
||||||
|
|
||||||
|
assert isinstance(result.exception, _StopGateway)
|
||||||
|
assert "port 18791" in result.stdout
|
||||||
|
|
||||||
|
|
||||||
|
def test_gateway_cli_port_overrides_configured_port(monkeypatch, tmp_path: Path) -> None:
|
||||||
|
config_file = tmp_path / "instance" / "config.json"
|
||||||
|
config_file.parent.mkdir(parents=True)
|
||||||
|
config_file.write_text("{}")
|
||||||
|
|
||||||
|
config = Config()
|
||||||
|
config.gateway.port = 18791
|
||||||
|
|
||||||
|
monkeypatch.setattr("nanobot.config.loader.set_config_path", lambda _path: None)
|
||||||
|
monkeypatch.setattr("nanobot.config.loader.load_config", lambda _path=None: config)
|
||||||
|
monkeypatch.setattr("nanobot.cli.commands.sync_workspace_templates", lambda _path: None)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"nanobot.cli.commands._make_provider",
|
||||||
|
lambda _config: (_ for _ in ()).throw(_StopGateway("stop")),
|
||||||
|
)
|
||||||
|
|
||||||
|
result = runner.invoke(app, ["gateway", "--config", str(config_file), "--port", "18792"])
|
||||||
|
|
||||||
|
assert isinstance(result.exception, _StopGateway)
|
||||||
|
assert "port 18792" in result.stdout
|
||||||
|
|||||||
90
tests/test_slack_channel.py
Normal file
90
tests/test_slack_channel.py
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from nanobot.bus.events import OutboundMessage
|
||||||
|
from nanobot.bus.queue import MessageBus
|
||||||
|
from nanobot.channels.slack import SlackChannel
|
||||||
|
from nanobot.config.schema import SlackConfig
|
||||||
|
|
||||||
|
|
||||||
|
class _FakeAsyncWebClient:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.chat_post_calls: list[dict[str, object | None]] = []
|
||||||
|
self.file_upload_calls: list[dict[str, object | None]] = []
|
||||||
|
|
||||||
|
async def chat_postMessage(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
channel: str,
|
||||||
|
text: str,
|
||||||
|
thread_ts: str | None = None,
|
||||||
|
) -> None:
|
||||||
|
self.chat_post_calls.append(
|
||||||
|
{
|
||||||
|
"channel": channel,
|
||||||
|
"text": text,
|
||||||
|
"thread_ts": thread_ts,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
async def files_upload_v2(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
channel: str,
|
||||||
|
file: str,
|
||||||
|
thread_ts: str | None = None,
|
||||||
|
) -> None:
|
||||||
|
self.file_upload_calls.append(
|
||||||
|
{
|
||||||
|
"channel": channel,
|
||||||
|
"file": file,
|
||||||
|
"thread_ts": thread_ts,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_send_uses_thread_for_channel_messages() -> None:
|
||||||
|
channel = SlackChannel(SlackConfig(enabled=True), MessageBus())
|
||||||
|
fake_web = _FakeAsyncWebClient()
|
||||||
|
channel._web_client = fake_web
|
||||||
|
|
||||||
|
await channel.send(
|
||||||
|
OutboundMessage(
|
||||||
|
channel="slack",
|
||||||
|
chat_id="C123",
|
||||||
|
content="hello",
|
||||||
|
media=["/tmp/demo.txt"],
|
||||||
|
metadata={"slack": {"thread_ts": "1700000000.000100", "channel_type": "channel"}},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
assert len(fake_web.chat_post_calls) == 1
|
||||||
|
assert fake_web.chat_post_calls[0]["text"] == "hello\n"
|
||||||
|
assert fake_web.chat_post_calls[0]["thread_ts"] == "1700000000.000100"
|
||||||
|
assert len(fake_web.file_upload_calls) == 1
|
||||||
|
assert fake_web.file_upload_calls[0]["thread_ts"] == "1700000000.000100"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_send_omits_thread_for_dm_messages() -> None:
|
||||||
|
channel = SlackChannel(SlackConfig(enabled=True), MessageBus())
|
||||||
|
fake_web = _FakeAsyncWebClient()
|
||||||
|
channel._web_client = fake_web
|
||||||
|
|
||||||
|
await channel.send(
|
||||||
|
OutboundMessage(
|
||||||
|
channel="slack",
|
||||||
|
chat_id="D123",
|
||||||
|
content="hello",
|
||||||
|
media=["/tmp/demo.txt"],
|
||||||
|
metadata={"slack": {"thread_ts": "1700000000.000100", "channel_type": "im"}},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
assert len(fake_web.chat_post_calls) == 1
|
||||||
|
assert fake_web.chat_post_calls[0]["text"] == "hello\n"
|
||||||
|
assert fake_web.chat_post_calls[0]["thread_ts"] is None
|
||||||
|
assert len(fake_web.file_upload_calls) == 1
|
||||||
|
assert fake_web.file_upload_calls[0]["thread_ts"] is None
|
||||||
Reference in New Issue
Block a user