release: public alpha 0.6.0 baseline
This commit is contained in:
@@ -0,0 +1,292 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
||||
SDK_SRC = PROJECT_ROOT / "packages" / "jwebot-sdk" / "src"
|
||||
KERNEL_SRC = PROJECT_ROOT / "packages" / "jwebot-kernel" / "src"
|
||||
HOST_SRC = PROJECT_ROOT / "packages" / "jwebot-extension-host" / "src"
|
||||
sys.path[:0] = [str(SDK_SRC), str(KERNEL_SRC), str(HOST_SRC)]
|
||||
|
||||
from jwebot_extension_host import ( # noqa: E402
|
||||
ExtensionExecutionError,
|
||||
ExtensionRuntime,
|
||||
HostError,
|
||||
process_request,
|
||||
)
|
||||
from jwebot_kernel import build_release, install_release, parse_manifest # noqa: E402
|
||||
from jwebot_sdk import Event, PlanKind # noqa: E402
|
||||
|
||||
|
||||
def manifest(commands=None, service_profiles=None):
|
||||
return parse_manifest(
|
||||
{
|
||||
"id": "hello-command",
|
||||
"author": "tester",
|
||||
"description": "Say hello",
|
||||
"help": ["/hello [name]"],
|
||||
"version": "1.0.0",
|
||||
"api_version": "1",
|
||||
"entrypoint": "hello_command:plugin",
|
||||
"type": "command",
|
||||
"capabilities": {
|
||||
"commands": commands or ["/hello"],
|
||||
"storage": False,
|
||||
"network_profiles": [],
|
||||
"service_profiles": service_profiles or [],
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def normal_source() -> str:
|
||||
return "\n".join(
|
||||
(
|
||||
"from jwebot_sdk import ExtensionResult, Fact, ReplyPlan",
|
||||
"",
|
||||
"def handle(event, arguments):",
|
||||
" return ExtensionResult(",
|
||||
" reply=ReplyPlan.text(f'hello {event.sender} {arguments}'.strip()),",
|
||||
" facts=(Fact(kind='topic', text='used hello command'),),",
|
||||
" )",
|
||||
"",
|
||||
"def plugin(context):",
|
||||
" context.commands.register('/hello', handle)",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class ExtensionHostTests(unittest.TestCase):
|
||||
def runtime(
|
||||
self,
|
||||
source: str,
|
||||
*,
|
||||
plugin_manifest=None,
|
||||
allow_unsigned=True,
|
||||
service_broker=None,
|
||||
):
|
||||
temporary = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(temporary.cleanup)
|
||||
base = Path(temporary.name)
|
||||
extension = base / "extension"
|
||||
extension.mkdir()
|
||||
(extension / "src").mkdir()
|
||||
(extension / "plugin.toml").write_text(
|
||||
'id = "hello-command"\n',
|
||||
encoding="utf-8",
|
||||
)
|
||||
(extension / "src" / "hello_command.py").write_text(
|
||||
source,
|
||||
encoding="utf-8",
|
||||
)
|
||||
plugin_manifest = plugin_manifest or manifest()
|
||||
artifact, record_path, _ = build_release(
|
||||
extension,
|
||||
base / "dist",
|
||||
manifest=plugin_manifest,
|
||||
)
|
||||
release_dir = install_release(
|
||||
artifact,
|
||||
record_path,
|
||||
base / "state",
|
||||
allow_unsigned=True,
|
||||
)
|
||||
return ExtensionRuntime.load(
|
||||
release_dir,
|
||||
manifest=plugin_manifest,
|
||||
allow_unsigned=allow_unsigned,
|
||||
service_broker=service_broker,
|
||||
)
|
||||
|
||||
def event(self):
|
||||
return Event(
|
||||
event_id="event-1",
|
||||
observed_at="2026-10-14T10:00:00+08:00",
|
||||
sender="Alice",
|
||||
text="@JWeBot /hello",
|
||||
group_fingerprint="group-a",
|
||||
mention_bot=True,
|
||||
)
|
||||
|
||||
def test_verified_extension_returns_structured_result(self):
|
||||
runtime = self.runtime(normal_source())
|
||||
result = runtime.invoke_command("/hello", self.event(), "world")
|
||||
self.assertEqual(result.reply.kind, PlanKind.TEXT)
|
||||
self.assertEqual(result.reply.message, "hello Alice world")
|
||||
self.assertEqual(result.facts[0].kind, "topic")
|
||||
self.assertEqual(list(runtime.release_dir.rglob("*.pyc")), [])
|
||||
|
||||
def test_host_rejects_unsigned_release_by_default(self):
|
||||
with self.assertRaisesRegex(HostError, "signed extension release"):
|
||||
self.runtime(normal_source(), allow_unsigned=False)
|
||||
|
||||
def test_request_protocol_serializes_sdk_result(self):
|
||||
runtime = self.runtime(normal_source())
|
||||
response = process_request(
|
||||
runtime,
|
||||
{
|
||||
"op": "invoke_command",
|
||||
"command": "/hello",
|
||||
"arguments": "",
|
||||
"event": self.event().to_mapping(),
|
||||
},
|
||||
)
|
||||
self.assertEqual(response["status"], "ok")
|
||||
self.assertEqual(response["plugin_id"], "hello-command")
|
||||
self.assertEqual(
|
||||
response["result"]["reply"]["text"],
|
||||
"hello Alice",
|
||||
)
|
||||
|
||||
def test_health_reports_frozen_manifest_inventory(self):
|
||||
runtime = self.runtime(
|
||||
normal_source(),
|
||||
plugin_manifest=manifest(service_profiles=["hello-service"]),
|
||||
)
|
||||
response = process_request(runtime, {"op": "health"})
|
||||
self.assertEqual(response["status"], "ok")
|
||||
self.assertEqual(response["plugin_id"], "hello-command")
|
||||
self.assertEqual(response["description"], "Say hello")
|
||||
self.assertEqual(response["help"], ["/hello [name]"])
|
||||
self.assertEqual(response["commands"], ["/hello"])
|
||||
self.assertEqual(response["service_profiles"], ["hello-service"])
|
||||
|
||||
def test_extension_cannot_register_undeclared_command(self):
|
||||
source = normal_source().replace(
|
||||
"context.commands.register('/hello', handle)",
|
||||
"context.commands.register('/other', handle)",
|
||||
)
|
||||
with self.assertRaisesRegex(HostError, "undeclared command"):
|
||||
self.runtime(source)
|
||||
|
||||
def test_extension_must_register_every_declared_command(self):
|
||||
with self.assertRaisesRegex(HostError, "did not register"):
|
||||
self.runtime(normal_source(), plugin_manifest=manifest(["/hello", "/other"]))
|
||||
|
||||
def test_invalid_handler_result_fails_closed(self):
|
||||
source = normal_source().replace(
|
||||
"return ExtensionResult(",
|
||||
"return 'invalid' #",
|
||||
1,
|
||||
)
|
||||
# Replace the remaining multi-line expression with a valid unreachable value.
|
||||
source = source.replace(
|
||||
" reply=ReplyPlan.text(f'hello {event.sender} {arguments}'.strip()),\n"
|
||||
" facts=(Fact(kind='topic', text='used hello command'),),\n"
|
||||
" )",
|
||||
"",
|
||||
)
|
||||
runtime = self.runtime(source)
|
||||
with self.assertRaisesRegex(ExtensionExecutionError, "invalid result"):
|
||||
runtime.invoke_command("/hello", self.event(), "")
|
||||
|
||||
def test_unconfigured_http_capability_fails_closed(self):
|
||||
source = "\n".join(
|
||||
(
|
||||
"from jwebot_sdk import ExtensionResult, ReplyPlan",
|
||||
"",
|
||||
"def plugin(context):",
|
||||
" def handle(event, arguments):",
|
||||
" context.http.request('missing', method='GET', path='/')",
|
||||
" return ExtensionResult(reply=ReplyPlan.no_reply())",
|
||||
" context.commands.register('/hello', handle)",
|
||||
)
|
||||
)
|
||||
runtime = self.runtime(source)
|
||||
with self.assertRaisesRegex(ExtensionExecutionError, "command failed"):
|
||||
runtime.invoke_command("/hello", self.event(), "")
|
||||
|
||||
def test_unconfigured_service_capability_fails_closed(self):
|
||||
source = "\n".join(
|
||||
(
|
||||
"from jwebot_sdk import ExtensionResult, ReplyPlan",
|
||||
"",
|
||||
"def plugin(context):",
|
||||
" def handle(event, arguments):",
|
||||
" context.services.call(",
|
||||
" 'missing', operation='execute', payload={}",
|
||||
" )",
|
||||
" return ExtensionResult(reply=ReplyPlan.no_reply())",
|
||||
" context.commands.register('/hello', handle)",
|
||||
)
|
||||
)
|
||||
runtime = self.runtime(source)
|
||||
with self.assertRaisesRegex(ExtensionExecutionError, "command failed"):
|
||||
runtime.invoke_command("/hello", self.event(), "")
|
||||
|
||||
def test_declared_service_profile_uses_kernel_broker(self):
|
||||
source = "\n".join(
|
||||
(
|
||||
"from jwebot_sdk import ExtensionResult, ReplyPlan",
|
||||
"",
|
||||
"def plugin(context):",
|
||||
" def handle(event, arguments):",
|
||||
" response = context.services.call(",
|
||||
" 'hello-service',",
|
||||
" operation='execute',",
|
||||
" payload={'sender': event.sender, 'value': arguments},",
|
||||
" )",
|
||||
" return ExtensionResult(",
|
||||
" reply=ReplyPlan.text(response['text'])",
|
||||
" )",
|
||||
" context.commands.register('/hello', handle)",
|
||||
)
|
||||
)
|
||||
calls = []
|
||||
|
||||
def broker(profile, operation, payload):
|
||||
calls.append((profile, operation, dict(payload)))
|
||||
return {"text": "brokered reply"}
|
||||
|
||||
runtime = self.runtime(
|
||||
source,
|
||||
plugin_manifest=manifest(service_profiles=["hello-service"]),
|
||||
service_broker=broker,
|
||||
)
|
||||
result = runtime.invoke_command("/hello", self.event(), "value")
|
||||
|
||||
self.assertEqual(result.reply.message, "brokered reply")
|
||||
self.assertEqual(
|
||||
calls,
|
||||
[
|
||||
(
|
||||
"hello-service",
|
||||
"execute",
|
||||
{"sender": "Alice", "value": "value"},
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
def test_logger_does_not_emit_plugin_text_or_field_names(self):
|
||||
runtime = self.runtime(normal_source())
|
||||
with self.assertLogs(level="INFO") as captured:
|
||||
runtime.context.logger.info(
|
||||
"private message body",
|
||||
**{"private-field-name": "private value"},
|
||||
)
|
||||
output = "\n".join(captured.output)
|
||||
self.assertNotIn("private message body", output)
|
||||
self.assertNotIn("private-field-name", output)
|
||||
self.assertNotIn("private value", output)
|
||||
|
||||
def test_outgoing_event_is_rejected_by_request_protocol(self):
|
||||
runtime = self.runtime(normal_source())
|
||||
event = self.event().to_mapping()
|
||||
event["side"] = "outgoing"
|
||||
with self.assertRaisesRegex(HostError, "SDK contract"):
|
||||
process_request(
|
||||
runtime,
|
||||
{
|
||||
"op": "invoke_command",
|
||||
"command": "/hello",
|
||||
"event": event,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user