feat: implement channel runtime connectors
This commit is contained in:
@ -1,4 +1,5 @@
|
||||
import json
|
||||
import asyncio
|
||||
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
@ -46,6 +47,44 @@ def test_load_config_reads_current_instance_shape(tmp_path) -> None:
|
||||
assert target["extra_headers"] == {"X-Test": "1"}
|
||||
|
||||
|
||||
def test_config_loader_reads_channels(tmp_path) -> None:
|
||||
config_path = tmp_path / "config.json"
|
||||
config_path.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"agents": {"defaults": {"model": "openai/gpt-5"}},
|
||||
"channels": {
|
||||
"webhook-dev": {
|
||||
"enabled": True,
|
||||
"kind": "webhook",
|
||||
"mode": "webhook",
|
||||
"accountId": "local",
|
||||
"displayName": "Webhook Dev",
|
||||
"config": {
|
||||
"responseTimeoutSeconds": 1800,
|
||||
"dedupeRetentionHours": 48,
|
||||
},
|
||||
"secrets": {"ignored_for_status": "secret-value"},
|
||||
}
|
||||
},
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
config = load_config(config_path=config_path)
|
||||
|
||||
channel = config.channels["webhook-dev"]
|
||||
assert channel.enabled is True
|
||||
assert channel.kind == "webhook"
|
||||
assert channel.mode == "webhook"
|
||||
assert channel.account_id == "local"
|
||||
assert channel.display_name == "Webhook Dev"
|
||||
assert channel.config["response_timeout_seconds"] == 1800
|
||||
assert channel.config["dedupe_retention_hours"] == 48
|
||||
assert channel.secrets == {"ignored_for_status": "secret-value"}
|
||||
|
||||
|
||||
def test_provider_resolution_ignores_custom_and_disabled_overrides(tmp_path) -> None:
|
||||
config_path = tmp_path / "config.json"
|
||||
config_path.write_text(
|
||||
@ -163,6 +202,58 @@ def test_reload_agent_config_updates_booted_loop_config(tmp_path) -> None:
|
||||
service.close()
|
||||
|
||||
|
||||
def test_reload_agent_config_keeps_running_service_when_old_mcp_close_fails(tmp_path) -> None:
|
||||
async def run_case() -> None:
|
||||
workspace = tmp_path / "workspace"
|
||||
config_path = tmp_path / "config.json"
|
||||
config_path.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"agents": {"defaults": {"workspace": str(workspace), "model": "old-model"}},
|
||||
"providers": {"openai": {"apiKey": "sk-test", "apiBase": "https://old.example.com/v1"}},
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
service = AgentService(config_path=config_path)
|
||||
await service.start()
|
||||
|
||||
class FailingMCPManager:
|
||||
async def close(self) -> None:
|
||||
raise RuntimeError("Attempted to exit cancel scope in a different task than it was entered in")
|
||||
|
||||
loaded = service.create_loop().boot()
|
||||
loaded.mcp_manager = FailingMCPManager()
|
||||
config_path.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"agents": {"defaults": {"workspace": str(workspace), "model": "new-model"}},
|
||||
"providers": {"openai": {"apiKey": "sk-test", "apiBase": "https://new.example.com/v1"}},
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
loop = asyncio.get_running_loop()
|
||||
unhandled: list[dict[str, object]] = []
|
||||
previous_handler = loop.get_exception_handler()
|
||||
loop.set_exception_handler(lambda _loop, context: unhandled.append(context))
|
||||
try:
|
||||
_reload_agent_config(service, config_path)
|
||||
await asyncio.sleep(0)
|
||||
|
||||
target = service.create_loop().boot().config.resolve_provider_target()
|
||||
assert service.is_running is True
|
||||
assert target["model"] == "new-model"
|
||||
assert target["api_base"] == "https://new.example.com/v1"
|
||||
assert unhandled == []
|
||||
finally:
|
||||
loop.set_exception_handler(previous_handler)
|
||||
await service.shutdown(force=True)
|
||||
|
||||
asyncio.run(run_case())
|
||||
|
||||
|
||||
def test_agent_defaults_include_runtime_controls(tmp_path) -> None:
|
||||
config_path = tmp_path / "config.json"
|
||||
config_path.write_text(
|
||||
@ -245,6 +336,67 @@ def test_agent_config_api_accepts_zero_temperature_and_iterations(tmp_path) -> N
|
||||
service.close()
|
||||
|
||||
|
||||
def test_channel_config_api_persists_and_masks_secrets(tmp_path) -> None:
|
||||
config_path = tmp_path / "config.json"
|
||||
config_path.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"agents": {"defaults": {"model": "openai/gpt-5"}},
|
||||
"channels": {
|
||||
"telegram-main": {
|
||||
"enabled": False,
|
||||
"kind": "telegram",
|
||||
"mode": "polling",
|
||||
"accountId": "bot-main",
|
||||
"displayName": "Telegram Main",
|
||||
"secrets": {"botToken": "1234567890abcdef"},
|
||||
"config": {"requireMentionInGroups": True},
|
||||
}
|
||||
},
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
service = AgentService(config_path=config_path)
|
||||
app = create_app(service=service, manage_service_lifecycle=False)
|
||||
|
||||
with TestClient(app) as client:
|
||||
before = client.get("/api/channels/telegram-main/config")
|
||||
response = client.post(
|
||||
"/api/channels/telegram-main/config",
|
||||
json={
|
||||
"enabled": True,
|
||||
"kind": "telegram",
|
||||
"mode": "polling",
|
||||
"account_id": "bot-main",
|
||||
"display_name": "Telegram Primary",
|
||||
"secrets": {"botToken": ""},
|
||||
"config": {
|
||||
"requireMentionInGroups": False,
|
||||
"allowFrom": ["1001", "1002"],
|
||||
"maxMessageChars": 3000,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
saved = json.loads(config_path.read_text(encoding="utf-8"))
|
||||
channel = saved["channels"]["telegram-main"]
|
||||
|
||||
assert before.status_code == 200
|
||||
assert before.json()["secrets"] == {"botToken": "1234••••cdef"}
|
||||
assert response.status_code == 200
|
||||
assert response.json()["ok"] is True
|
||||
assert response.json()["restart_required"] is True
|
||||
assert response.json()["channel"]["display_name"] == "Telegram Primary"
|
||||
assert response.json()["channel"]["secrets"] == {"botToken": "1234••••cdef"}
|
||||
assert channel["enabled"] is True
|
||||
assert channel["displayName"] == "Telegram Primary"
|
||||
assert channel["secrets"]["botToken"] == "1234567890abcdef"
|
||||
assert channel["config"]["allowFrom"] == ["1001", "1002"]
|
||||
assert load_config(config_path=config_path).channels["telegram-main"].enabled is True
|
||||
service.close()
|
||||
|
||||
|
||||
def test_openai_compatible_qwen_config_keeps_openai_provider() -> None:
|
||||
bundle = make_provider_bundle(
|
||||
model="qwen-plus",
|
||||
|
||||
Reference in New Issue
Block a user