feat(gateway)!: drop HTTP listener, run outbound agent only

- delete GatewayHTTPServer/GatewayHandler, create_server, /healthz and
  /v1/channel; event_channel module removed
- load_config now requires CREATOR_PLATFORM_URL + CREATOR_GATEWAY_KEY
- run() wires Gateway into OutboundAgent; SIGINT/SIGTERM stop the agent
- deploy env example, install script, systemd unit docs and deployment
  docs updated to the outbound model; NATIVE_GATEWAY_ENDPOINT dead dev
  config removed
This commit is contained in:
2026-10-09 08:25:27 +08:00
parent 469b283ac2
commit d9e57c4900
17 changed files with 199 additions and 623 deletions
+106
View File
@@ -0,0 +1,106 @@
"""Server entry tests: outbound-only config and run() wiring."""
from __future__ import annotations
import unittest
from importlib import import_module
from unittest.mock import Mock, patch
gateway_module = import_module(f"{__package__}.server.http")
load_config = gateway_module.load_config
class OutboundConfigTests(unittest.TestCase):
def test_platform_url_and_key_are_required(self) -> None:
config = load_config({
"CREATOR_PLATFORM_URL": "http://platform.example:8080",
"CREATOR_GATEWAY_KEY": "0123456789abcdef",
"BROWSER_PATH": "/bin/true",
})
self.assertEqual(config["platform_url"], "http://platform.example:8080")
self.assertEqual(config["access_key"], "0123456789abcdef")
self.assertNotIn("listen", config)
self.assertNotIn("token", config)
def test_platform_url_must_be_http(self) -> None:
with self.assertRaises(ValueError):
load_config({
"CREATOR_PLATFORM_URL": "ftp://platform.example",
"CREATOR_GATEWAY_KEY": "0123456789abcdef",
"BROWSER_PATH": "/bin/true",
})
def test_platform_url_must_not_embed_agent_path(self) -> None:
with self.assertRaises(ValueError):
load_config({
"CREATOR_PLATFORM_URL": "http://platform.example:8080/v1/agent",
"CREATOR_GATEWAY_KEY": "0123456789abcdef",
"BROWSER_PATH": "/bin/true",
})
def test_access_key_minimum_length(self) -> None:
with self.assertRaises(ValueError):
load_config({
"CREATOR_PLATFORM_URL": "http://platform.example:8080",
"CREATOR_GATEWAY_KEY": "short",
"BROWSER_PATH": "/bin/true",
})
def test_trailing_slash_platform_url_is_normalized(self) -> None:
config = load_config({
"CREATOR_PLATFORM_URL": "http://platform.example:8080/",
"CREATOR_GATEWAY_KEY": "0123456789abcdef",
"BROWSER_PATH": "/bin/true",
})
self.assertEqual(config["platform_url"], "http://platform.example:8080")
class RunWiringTests(unittest.TestCase):
def test_run_builds_agent_from_config_and_stops_on_signal(self) -> None:
config = load_config({
"CREATOR_PLATFORM_URL": "http://platform.example:8080",
"CREATOR_GATEWAY_KEY": "0123456789abcdef",
"BROWSER_PATH": "/bin/true",
})
agent_instances = []
class FakeAgent:
def __init__(self, gateway, agent_config) -> None:
self.gateway = gateway
self.config = agent_config
self.ran = False
self.stopped = False
agent_instances.append(self)
def run(self) -> None:
self.ran = True
def stop(self) -> None:
self.stopped = True
registered = {}
def record_signal(signum, handler):
registered[signum] = handler
with patch.object(gateway_module, "OutboundAgent", FakeAgent), \
patch.object(gateway_module, "NativeRuntimeManager", Mock()), \
patch.object(gateway_module.signal, "signal", record_signal):
exit_code = gateway_module.run(config)
self.assertEqual(exit_code, 0)
self.assertEqual(len(agent_instances), 1)
self.assertEqual(agent_instances[0].config.platform_url, config["platform_url"])
self.assertEqual(agent_instances[0].config.access_key, config["access_key"])
self.assertTrue(agent_instances[0].ran)
# SIGINT/SIGTERM 都注册了停止回调,触发后 agent 收到 stop
self.assertIn(gateway_module.signal.SIGINT, registered)
self.assertIn(gateway_module.signal.SIGTERM, registered)
registered[gateway_module.signal.SIGTERM](15, None)
registered[gateway_module.signal.SIGINT](2, None)
self.assertTrue(agent_instances[0].stopped)
# Gateway 的业务方法必须仍挂在 agent 上(agent.gateway)
self.assertIsNotNone(agent_instances[0].gateway.subscriptions)
if __name__ == "__main__":
unittest.main()