release: public alpha 0.6.0 baseline

This commit is contained in:
JWeBot Contributors
2026-10-15 03:58:31 +08:00
commit d4242ae07c
238 changed files with 37724 additions and 0 deletions
+303
View File
@@ -0,0 +1,303 @@
#!/usr/bin/env python3
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path
from unittest import mock
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"
OFFICIAL_SRC = (
PROJECT_ROOT
/ "packages"
/ "jwebot-official-extensions"
/ "src"
)
try:
import tomli
TOML_EXTRA = [str(Path(tomli.__file__).resolve().parent.parent)]
except ModuleNotFoundError:
TOML_EXTRA = []
sys.path[:0] = [
str(SDK_SRC),
str(OFFICIAL_SRC),
str(KERNEL_SRC),
]
from jwebot_kernel import ( # noqa: E402
ExtensionHostClient,
build_release,
install_release,
sign_release_record,
)
from jwebot_official_extensions import LOCAL_EXTENSIONS # noqa: E402
from tests.test_jwebot_edge import MODULE, config, event # noqa: E402
PUBLISHER = "test-publisher"
@unittest.skipUnless(
Path("/usr/bin/ssh-keygen").is_file(),
"/usr/bin/ssh-keygen is unavailable",
)
class HostedCommandParityTests(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.workspace = tempfile.TemporaryDirectory()
cls.root = Path(cls.workspace.name)
cls.private_key = cls.root / "publisher"
result = subprocess.run(
[
"/usr/bin/ssh-keygen",
"-q",
"-t",
"ed25519",
"-N",
"",
"-f",
str(cls.private_key),
],
capture_output=True,
check=False,
)
if result.returncode != 0:
raise RuntimeError("cannot generate test publisher key")
cls.allowed_signers = cls.root / "allowed_signers"
cls.allowed_signers.write_text(
f"{PUBLISHER} "
+ cls.private_key.with_suffix(".pub").read_text(
encoding="utf-8"
),
encoding="utf-8",
)
cls.wrapper = cls.root / "host-wrapper.py"
cls.wrapper.write_text(
"\n".join(
(
"import runpy, sys",
"sys.path[:0] = [",
f" {str(SDK_SRC)!r},",
f" {str(KERNEL_SRC)!r},",
f" {str(HOST_SRC)!r},",
*(
f" {path!r},"
for path in TOML_EXTRA
),
"]",
"runpy.run_module('jwebot_extension_host', run_name='__main__')",
)
)
+ "\n",
encoding="utf-8",
)
cls.state_root = cls.root / "extensions"
health_client = cls.client()
for descriptor in LOCAL_EXTENSIONS:
source = (
PROJECT_ROOT
/ "extensions"
/ descriptor.directory
/ descriptor.extension_id
)
artifact, record_path, _ = build_release(
source,
cls.root / "dist" / descriptor.extension_id,
)
signature_path, _ = sign_release_record(
record_path,
cls.private_key,
PUBLISHER,
)
install_release(
artifact,
record_path,
cls.state_root / descriptor.extension_id,
signature=signature_path,
allowed_signers=cls.allowed_signers,
expected_publisher=PUBLISHER,
health_check=lambda release, client=health_client: client.invoke(
release,
{"op": "health"},
),
)
@classmethod
def tearDownClass(cls):
cls.workspace.cleanup()
@classmethod
def client(cls):
return ExtensionHostClient(
command=(sys.executable, str(cls.wrapper)),
timeout_seconds=5,
allowed_signers=cls.allowed_signers,
expected_publisher=PUBLISHER,
)
@classmethod
def factory(cls, plugin_id, release_dir):
del plugin_id
client = cls.client()
return lambda request, service: client.invoke(
release_dir,
request,
service_handler=service,
)
def setUp(self):
self.hosted_temp = tempfile.TemporaryDirectory()
self.compat_temp = tempfile.TemporaryDirectory()
self.hosted = MODULE.EdgeApplication(
config(
Path(self.hosted_temp.name),
extension_mode="host-only",
extension_transport="process",
extension_state_root=self.state_root,
extension_allowed_signers=self.allowed_signers,
extension_publisher=PUBLISHER,
),
host_invoker_factory=self.factory,
)
self.compat = MODULE.EdgeApplication(
config(
Path(self.compat_temp.name),
local_extensions_enabled=False,
)
)
def tearDown(self):
self.hosted.close()
self.compat.close()
self.hosted_temp.cleanup()
self.compat_temp.cleanup()
def assert_command_equal(
self,
event_id,
text,
*,
sender="Alice",
):
incoming = event(event_id, text, sender=sender)
hosted = self.hosted.accept(dict(incoming))
compat = self.compat.accept(dict(incoming))
self.assertEqual(hosted["route"], compat["route"])
self.assertEqual(hosted["text"], compat["text"])
return hosted
def test_all_plugin_families_match_direct_compatibility_path(self):
stateless = (
("dice", "@JWeBot /丢骰子5d20"),
("coin", "@JWeBot /翻硬币"),
("random", "@JWeBot /随机数 -10 100"),
("lot", "@JWeBot /抽签"),
("fortune", "@JWeBot /今日运势"),
("memory-help", "@JWeBot /记忆帮助"),
("scan-empty", "@JWeBot /扫码排行"),
("repeat-empty", "@JWeBot /复读榜"),
("history-empty", "@JWeBot /查记录 测试"),
)
for event_id, text in stateless:
with self.subTest(command=text):
self.assert_command_equal(event_id, text)
memory = (
("remember", "@JWeBot /记住 我喜欢家庭服务器"),
("my-memory", "@JWeBot /我的记忆"),
("person-memory", "@JWeBot /人物记忆 Alice"),
("forget-memory", "@JWeBot /忘记 全部"),
)
for event_id, text in memory:
with self.subTest(command=text):
self.assert_command_equal(event_id, text)
challenge = (
("challenge-start", "@JWeBot /丢骰子挑战 开始 5d20", "Alice"),
("challenge-alice", "@JWeBot /丢骰子挑战", "Alice"),
("challenge-gb", "@JWeBot /丢骰子挑战", "Bob"),
("challenge-state", "@JWeBot /丢骰子挑战 状态", "Alice"),
("challenge-settle", "@JWeBot /丢骰子挑战 结算", "Alice"),
)
for event_id, text, sender in challenge:
with self.subTest(command=text):
self.assert_command_equal(event_id, text, sender=sender)
self.assert_command_equal(
"guess-start",
"@JWeBot /猜数字 开始 50",
)
target = self.hosted.ledger.local_game(
"group-a",
"guess_number",
)["target"]
self.assertEqual(
target,
self.compat.ledger.local_game(
"group-a",
"guess_number",
)["target"],
)
self.assert_command_equal("guess-status", "@JWeBot /猜数字 状态")
self.assert_command_equal("guess-win", f"@JWeBot /猜 {target}")
rps = (
("rps-start", "@JWeBot /石头剪刀布 开始", "Alice"),
("rps-join", "@JWeBot /石头剪刀布 加入", "Bob"),
("rps-first", "@JWeBot /石头剪刀布 出 石头", "Alice"),
("rps-second", "@JWeBot /石头剪刀布 出 剪刀", "Bob"),
("rps-end", "@JWeBot /石头剪刀布 结束", "Alice"),
)
for event_id, text, sender in rps:
with self.subTest(command=text):
self.assert_command_equal(event_id, text, sender=sender)
for application in (self.hosted, self.compat):
application.local.steam = mock.Mock()
application.local.steam.render.return_value = (
"Steam:yr 正在玩 Dota 2"
)
self.assert_command_equal("steam", "@JWeBot /steam")
hosted_row = self.hosted.ledger.row("steam")
compat_row = self.compat.ledger.row("steam")
self.assertEqual(
self.hosted.generate_steam_reply(hosted_row),
self.compat.generate_steam_reply(compat_row),
)
self.assert_command_equal(
"restart-guess",
"@JWeBot /猜数字 开始 30",
)
expected_target = self.hosted.ledger.local_game(
"group-a",
"guess_number",
)["target"]
self.hosted.close()
self.hosted = MODULE.EdgeApplication(
config(
Path(self.hosted_temp.name),
extension_mode="host-only",
extension_transport="process",
extension_state_root=self.state_root,
extension_allowed_signers=self.allowed_signers,
extension_publisher=PUBLISHER,
),
host_invoker_factory=self.factory,
)
restored = self.hosted.ledger.local_game(
"group-a",
"guess_number",
)
self.assertEqual(restored["target"], expected_target)
self.assertEqual(restored["upper"], 30)
if __name__ == "__main__":
unittest.main()