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
+345
View File
@@ -0,0 +1,345 @@
#!/usr/bin/env python3
from __future__ import annotations
import json
import os
import sys
import tarfile
import tempfile
import unittest
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parents[1]
CONFIG_SRC = PROJECT_ROOT / "packages" / "jwebot-config" / "src"
sys.path.insert(0, str(CONFIG_SRC))
from jwebot_config import ( # noqa: E402
DEFAULT_PROFILE_FILES,
ConfigError,
ConfigLayout,
activate_profile,
archive_legacy_material,
compile_profile,
export_profile,
import_profile,
load_active_environment,
load_private_context,
migrate_legacy_context,
validate_profile,
write_default_profile,
)
class ConfigProfileTests(unittest.TestCase):
def layout(self, root: Path) -> ConfigLayout:
return ConfigLayout(
config_root=root / "config",
state_root=root / "state",
)
def test_repository_template_matches_generated_defaults(self):
template = PROJECT_ROOT / "config" / "templates" / "default"
for relative, expected in DEFAULT_PROFILE_FILES.items():
with self.subTest(path=relative):
self.assertEqual(
(template / relative).read_text(encoding="utf-8"),
expected,
)
def test_default_profile_compiles_and_activates_atomically(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
layout = self.layout(root)
profile = write_default_profile("workstation", layout)
self.assertEqual(validate_profile(profile), "workstation")
generated = compile_profile(profile, layout)
activate_profile("workstation", layout)
self.assertTrue(layout.current_link.is_symlink())
self.assertEqual(layout.active_profile(), profile.resolve())
self.assertEqual(
os.stat(generated / "runtime.env").st_mode & 0o777,
0o600,
)
for directory_path in (
layout.config_root,
layout.profiles_root,
layout.state_root,
layout.state_root / "profiles",
layout.profile_state("workstation"),
):
self.assertEqual(
os.stat(directory_path).st_mode & 0o777,
0o700,
)
environment = load_active_environment(layout)
self.assertEqual(environment["JWEBOT_CONFIG_PROFILE"], "workstation")
self.assertEqual(environment["JWEBOT_EDGE_ENABLED"], "1")
self.assertEqual(environment["JWEBOT_EDGE_STEAM_API_KEY"], "")
self.assertEqual(
load_private_context(layout)["bot"]["mention"],
"JWeBot",
)
def test_private_context_and_component_values_are_compiled_separately(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
layout = self.layout(root)
profile = write_default_profile("custom", layout)
(profile / "context" / "people.json").write_text(
json.dumps(
{
"people": [
{
"canonical_name": "Alice",
"aliases": ["alice-ocr"],
"prefixes": [],
"identity_fact": "",
"display_id": "alice",
"steam_id64": "10000000000000001",
"steam_aliases": ["alice-steam"],
}
],
"clock_scan_targets": [],
}
),
encoding="utf-8",
)
(profile / "secrets" / "runtime.env").write_text(
"JWEBOT_EDGE_DEEPSEEK_API_KEY=test-secret\n"
"JWEBOT_EDGE_STEAM_API_KEY=\n",
encoding="utf-8",
)
generated = compile_profile(profile, layout)
environment = (generated / "runtime.env").read_text(encoding="utf-8")
context = json.loads(
(generated / "private-context.json").read_text(encoding="utf-8")
)
self.assertIn("JWEBOT_EDGE_DEEPSEEK_API_KEY=test-secret", environment)
self.assertNotIn("alice-ocr", environment)
self.assertEqual(context["people"][0]["display_id"], "alice")
self.assertEqual(
context["people"][0]["steam_id64"],
"10000000000000001",
)
self.assertEqual(
context["people"][0]["steam_aliases"],
["alice-steam"],
)
self.assertNotIn("test-secret", json.dumps(context))
def test_redacted_export_removes_secrets_and_people(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
layout = self.layout(root)
profile = write_default_profile("shareable", layout)
(profile / "secrets" / "runtime.env").write_text(
"JWEBOT_EDGE_DEEPSEEK_API_KEY=test-secret\n"
"JWEBOT_EDGE_STEAM_API_KEY=another-secret\n",
encoding="utf-8",
)
(profile / "context" / "people.json").write_text(
'{"people":[{"canonical_name":"Alice","aliases":["a"],'
'"prefixes":[],"identity_fact":"","display_id":"a"}],'
'"clock_scan_targets":[]}\n',
encoding="utf-8",
)
output = root / "shareable.tar.gz"
export_profile("shareable", output, layout)
with tarfile.open(output, "r:gz") as archive:
secret_text = archive.extractfile(
"shareable/secrets/runtime.env"
).read().decode()
context_text = archive.extractfile(
"shareable/context/people.json"
).read().decode()
self.assertNotIn("test-secret", secret_text)
self.assertNotIn("another-secret", secret_text)
self.assertEqual(json.loads(context_text)["people"], [])
def test_private_context_rejects_duplicate_steam_ids(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
layout = self.layout(root)
profile = write_default_profile("duplicate-steam", layout)
people = []
for name in ("Alice", "Bob"):
people.append(
{
"canonical_name": name,
"aliases": [],
"prefixes": [],
"identity_fact": "",
"display_id": name.casefold(),
"steam_id64": "10000000000000001",
"steam_aliases": [],
}
)
(profile / "context" / "people.json").write_text(
json.dumps(
{"people": people, "clock_scan_targets": []}
),
encoding="utf-8",
)
with self.assertRaisesRegex(ConfigError, "must be unique"):
validate_profile(profile)
def test_import_rejects_archive_path_traversal(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
layout = self.layout(root)
archive_path = root / "unsafe.tar"
payload = root / "payload"
payload.write_text("value", encoding="utf-8")
with tarfile.open(archive_path, "w") as archive:
archive.add(payload, arcname="../outside")
with self.assertRaisesRegex(ConfigError, "unsafe path"):
import_profile(archive_path, layout)
def test_active_profile_rejects_symlink_outside_profile_root(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
layout = self.layout(root)
layout.config_root.mkdir(parents=True)
outside = root / "outside"
outside.mkdir()
layout.current_link.symlink_to(outside)
with self.assertRaisesRegex(ConfigError, "outside"):
layout.active_profile()
def test_legacy_debug_material_is_archived_outside_the_repository(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
repository = root / "repository"
repository.mkdir()
(repository / ".dbg").mkdir()
(repository / ".dbg" / "session.env").write_text(
"DEBUG_VALUE=local\n",
encoding="utf-8",
)
(repository / "debug-incident.md").write_text(
"private incident",
encoding="utf-8",
)
layout = self.layout(root)
write_default_profile("migrated", layout)
archive = archive_legacy_material(
repository,
"migrated",
layout,
)
self.assertTrue((archive / ".dbg" / "session.env").is_file())
self.assertTrue((archive / "debug-incident.md").is_file())
self.assertEqual(
os.stat(archive / ".dbg" / "session.env").st_mode & 0o777,
0o600,
)
def test_legacy_context_is_migrated_into_private_profile(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
repository = root / "repository"
source = (
repository
/ "packages"
/ "jwebot-kernel"
/ "src"
/ "jwebot_kernel"
)
source.mkdir(parents=True)
(source / "edge_runtime.py").write_text(
"\n".join(
(
'BOT_IDENTITY_NOTE = "ExampleBot identity"',
"PERSON_ALIAS_TEMPLATES = (",
" {",
' "canonical_name": "Alice",',
' "aliases": ("alice-ocr",),',
' "prefixes": (),',
' "identity_fact": "Alice is a test user",',
" },",
" {",
' "canonical_name": "Bob",',
' "aliases": ("bob-ocr",),',
' "prefixes": (),',
' "identity_fact": "Bob is a test user",',
" },",
")",
'PERSON_DISPLAY_IDS = {"Alice": "alice", "Bob": "bob"}',
'CLOCK_SCAN_TARGETS = (("Alice", "alice"), ("Bob", "bob"))',
)
)
+ "\n",
encoding="utf-8",
)
steam_source = (
repository
/ "packages"
/ "jwebot-official-extensions"
/ "src"
/ "jwebot_official_extensions"
)
steam_source.mkdir(parents=True)
(steam_source / "steam.py").write_text(
"\n".join(
(
'STEAM_GROUP_ROSTER = (("bob", "10000000000000002"),)',
"STEAM_ALIAS_LABELS = {",
' "alice-steam": "alice",',
' "bob-steam": "bob",',
"}",
)
)
+ "\n",
encoding="utf-8",
)
environment = root / "environment"
environment.write_text(
"JWEBOT_EDGE_STEAM_ID64=10000000000000001\n",
encoding="utf-8",
)
layout = self.layout(root)
profile = write_default_profile("migrated", layout)
self.assertTrue(
migrate_legacy_context(
repository,
"migrated",
layout,
environment=environment,
)
)
context = json.loads(
(profile / "context" / "people.json").read_text(encoding="utf-8")
)
bot = (profile / "components" / "bot.toml").read_text(
encoding="utf-8"
)
self.assertEqual(context["people"][0]["display_id"], "alice")
self.assertEqual(context["clock_scan_targets"][0]["display_id"], "alice")
self.assertEqual(
[person["steam_id64"] for person in context["people"]],
["10000000000000001", "10000000000000002"],
)
self.assertEqual(
[person["steam_aliases"] for person in context["people"]],
[["alice-steam"], ["bob-steam"]],
)
self.assertIn('mention = "ExampleBot"', bot)
if __name__ == "__main__":
unittest.main()
+135
View File
@@ -0,0 +1,135 @@
#!/usr/bin/env python3
import importlib.util
import py_compile
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"
sys.path[:0] = [str(SDK_SRC), str(KERNEL_SRC)]
SPEC = importlib.util.spec_from_file_location(
"create_plugin",
PROJECT_ROOT / "tools" / "create-plugin.py",
)
MODULE = importlib.util.module_from_spec(SPEC)
assert SPEC.loader is not None
SPEC.loader.exec_module(MODULE)
class CreatePluginTests(unittest.TestCase):
def test_generator_creates_a_bounded_command_extension(self):
with tempfile.TemporaryDirectory() as directory:
destination = MODULE.create_plugin(
Path(directory),
"weather-command",
"/weather",
"Friend",
"查询天气",
)
self.assertTrue((destination / "plugin.toml").is_file())
manifest = (destination / "plugin.toml").read_text(
encoding="utf-8"
)
self.assertIn('description = "查询天气"', manifest)
self.assertIn('help = ["/weather"]', manifest)
module = destination / "src" / "weather_command.py"
self.assertIn(
'context.commands.register("/weather", handle)',
module.read_text(encoding="utf-8"),
)
self.assertNotIn(
"subprocess",
module.read_text(encoding="utf-8"),
)
py_compile.compile(str(module), doraise=True)
py_compile.compile(
str(destination / "tests" / "test_plugin.py"),
doraise=True,
)
def test_generator_refuses_to_overwrite_existing_plugin(self):
with tempfile.TemporaryDirectory() as directory:
output = Path(directory)
MODULE.create_plugin(
output,
"weather-command",
"/weather",
"Friend",
"查询天气",
)
with self.assertRaises(FileExistsError):
MODULE.create_plugin(
output,
"weather-command",
"/weather",
"Friend",
"查询天气",
)
def test_generator_rejects_path_traversal_plugin_id(self):
with tempfile.TemporaryDirectory() as directory:
with self.assertRaisesRegex(ValueError, "invalid format"):
MODULE.create_plugin(
Path(directory),
"../outside",
"/weather",
"Friend",
"查询天气",
)
def test_generator_rejects_invalid_command(self):
with tempfile.TemporaryDirectory() as directory:
with self.assertRaisesRegex(ValueError, "invalid value"):
MODULE.create_plugin(
Path(directory),
"weather-command",
"weather",
"Friend",
"查询天气",
"/weather",
)
def test_generator_rejects_command_template_injection(self):
with tempfile.TemporaryDirectory() as directory:
with self.assertRaisesRegex(ValueError, "invalid value"):
MODULE.create_plugin(
Path(directory),
"weather-command",
'/"\\nentrypoint = "attacker:run',
"Friend",
"查询天气",
"/weather",
)
def test_generator_rejects_invalid_author(self):
with tempfile.TemporaryDirectory() as directory:
with self.assertRaisesRegex(ValueError, "author has an invalid format"):
MODULE.create_plugin(
Path(directory),
"weather-command",
"/weather",
"../Friend",
"查询天气",
)
def test_generator_rejects_invalid_help_description(self):
with tempfile.TemporaryDirectory() as directory:
with self.assertRaisesRegex(ValueError, "invalid display"):
MODULE.create_plugin(
Path(directory),
"weather-command",
"/weather",
"Friend",
"查询天气",
"plain text",
)
if __name__ == "__main__":
unittest.main()
+205
View File
@@ -0,0 +1,205 @@
#!/usr/bin/env python3
import os
import subprocess
import unittest
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parents[1]
USER_INSTALLER = (
PROJECT_ROOT / "deploy" / "scripts" / "install-wechat-health-watch.sh"
)
PRODUCTION_INSTALLER = (
PROJECT_ROOT / "deploy" / "scripts" / "deploy-jwebot-production.sh"
)
BOOTSTRAP = PROJECT_ROOT / "deploy" / "scripts" / "bootstrap-jwebot.sh"
LEGACY_INSTALLER = (
PROJECT_ROOT / "deploy" / "scripts" / "install-wechat-local-ocr.sh"
)
LEGACY_UPGRADER = (
PROJECT_ROOT / "deploy" / "scripts" / "upgrade-wechat-local-ocr.sh"
)
ADAPTIVE_UNIT = (
PROJECT_ROOT
/ "deploy"
/ "systemd"
/ "user"
/ "wechat-ocr-adaptive-recorder.service"
)
PACKAGE_GATE = PROJECT_ROOT / "tools" / "check-packages.sh"
LOCAL_GATE = PROJECT_ROOT / "tools" / "check-local.sh"
class DeploymentScriptTests(unittest.TestCase):
def test_installers_are_executable_and_parse_with_their_shells(self):
for path, shell in (
(BOOTSTRAP, "bash"),
(USER_INSTALLER, "sh"),
(PRODUCTION_INSTALLER, "bash"),
):
self.assertTrue(os.access(path, os.X_OK), path)
result = subprocess.run(
[shell, "-n", str(path)],
text=True,
capture_output=True,
check=False,
)
self.assertEqual(result.returncode, 0, result.stderr)
def test_user_installer_owns_the_complete_adaptive_runtime(self):
source = USER_INSTALLER.read_text(encoding="utf-8")
for required in (
"wechat_local_ocr.py",
"wechat_health_watch.py",
"wechat_adaptive_recorder.py",
"wechat_adaptive_gui_bridge.py",
"jwebot_edge.py",
"jwebot_edge_delivery.py",
"jwebot_config",
"jwebot_sdk",
"jwebot_kernel",
"jwebot_official_extensions",
):
self.assertIn(required, source)
def test_user_installer_has_rollback_and_stability_gates(self):
source = USER_INSTALLER.read_text(encoding="utf-8")
for required in (
"systemd-user-state.tsv",
"restore_unit_states",
"legacy system OCR is active",
'{"op":"health"}',
"EDGE_HEALTH_TIMEOUT_SECONDS:-45",
"wait_for_edge_health",
"ADAPTIVE_READY_TIMEOUT_SECONDS:-90",
"wait_for_adaptive_ready",
"adaptive-start.marker",
'value.get("initialized") is True',
'captured_at.timestamp() >= int(sys.argv[2])',
"NRestarts",
'test "$restarts_before" = "$restarts_after"',
"[JWeBot] %s",
'jwebot_log "edge starting"',
'jwebot_log "edge active health=ok"',
'jwebot_log "adaptive starting"',
'jwebot_log "adaptive active"',
'jwebot_log "adaptive ready baseline=captured"',
'jwebot_log "user runtime ready"',
):
self.assertIn(required, source)
self.assertLess(
source.index('jwebot_log "adaptive active"'),
source.index('jwebot_log "adaptive ready baseline=captured"'),
)
self.assertLess(
source.index('jwebot_log "adaptive ready baseline=captured"'),
source.index('jwebot_log "user runtime ready"'),
)
def test_root_installer_enforces_the_production_cutover_contract(self):
source = PRODUCTION_INSTALLER.read_text(encoding="utf-8")
for required in (
"production deploy must run as root",
"assert_queues_empty",
"assert_wechat_ready",
'[[ "${title}" == "Weixin" ]]',
"WECHAT_READY_TIMEOUT_SECONDS:-30",
"/usr/sbin/runuser",
"/usr/bin/xdotool search --onlyvisible --class wechat",
"/usr/bin/xdotool search --onlyvisible --name '^Weixin$'",
'jwebot_log "WeChat readiness starting timeout=',
'source.backup(target)',
"restore_enable_state",
"restore_active_state",
"install-wechat-health-watch.sh",
"systemctl disable --now",
"wechat-local-ocr-recorder.service",
"wechat-local-ocr-responder.service",
"sha256sum -c SHA256SUMS",
"[JWeBot] %s",
'jwebot_log "preflight starting"',
'jwebot_log "runtime install ready"',
'jwebot_log "edge active"',
'jwebot_log "adaptive active"',
'jwebot_log "deployment ready version=',
'cd "${target_home}"',
):
self.assertIn(required, source)
self.assertEqual(
source.count("runuser"),
source.count("/usr/sbin/runuser"),
)
self.assertEqual(
source.count("xdotool"),
source.count("/usr/bin/xdotool"),
)
def test_production_reports_bounded_progress(self):
source = PRODUCTION_INSTALLER.read_text(encoding="utf-8")
self.assertIn("jwebot_progress()", source)
self.assertIn('jwebot_progress 1 8 "preflight"', source)
self.assertIn('jwebot_progress 8 8 "deployment ready"', source)
self.assertNotIn("pixel_art", source)
def test_production_preflight_verifies_rendered_runtime_units(self):
source = PRODUCTION_INSTALLER.read_text(encoding="utf-8")
render = 'render_root_configuration "${preflight_rendered_dir}"'
verify = (
'systemd-analyze verify \\\n'
' "${preflight_rendered_dir}/wechat-local-ocr-recorder.service"'
)
self.assertIn("render_root_configuration()", source)
self.assertIn("preflight_rendered_dir=$(mktemp -d)", source)
self.assertIn(render, source)
self.assertIn(verify, source)
self.assertLess(source.index(render), source.index(verify))
self.assertNotIn(
'systemd-analyze verify \\\n'
' "${source_dir}/deploy/systemd/system/'
'wechat-local-ocr-recorder.service"',
source,
)
def test_bootstrap_has_prepare_verify_and_deploy_stages(self):
source = BOOTSTRAP.read_text(encoding="utf-8")
for required in (
"prepare()",
"verify()",
"deploy()",
"requirements-runtime.txt",
"jwebot_config",
"tools/check-secrets.py",
"xdotool search --onlyvisible --class wechat",
"deploy-jwebot-production.sh",
"progress 6 6",
):
self.assertIn(required, source)
def test_legacy_maintenance_never_enables_services_implicitly(self):
installer = LEGACY_INSTALLER.read_text(encoding="utf-8")
upgrader = LEGACY_UPGRADER.read_text(encoding="utf-8")
self.assertIn("ENABLE_LEGACY_OCR:-0", installer)
self.assertIn("restore_service_state", installer)
self.assertIn("restore_service_state", upgrader)
self.assertNotIn(
'systemctl restart "$recorder_service" "$responder_service"',
upgrader,
)
def test_adaptive_unit_bounds_real_crash_restarts(self):
unit = ADAPTIVE_UNIT.read_text(encoding="utf-8")
self.assertIn("Restart=on-failure", unit)
self.assertIn("StartLimitIntervalSec=300", unit)
self.assertIn("StartLimitBurst=6", unit)
def test_release_gates_are_repeatable_without_ripgrep(self):
package_gate = PACKAGE_GATE.read_text(encoding="utf-8")
local_gate = LOCAL_GATE.read_text(encoding="utf-8")
self.assertIn("PYTHONPYCACHEPREFIX", package_gate)
self.assertIn("command -v rg", package_gate)
self.assertIn("command -v rg", local_gate)
if __name__ == "__main__":
unittest.main()
+46
View File
@@ -0,0 +1,46 @@
#!/usr/bin/env python3
import subprocess
import sys
import unittest
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(PROJECT_ROOT))
import jwebot_edge # noqa: E402
from jwebot_kernel import edge_runtime # noqa: E402
class EdgeCompatibilityTests(unittest.TestCase):
def test_import_alias_is_the_packaged_runtime_module(self):
self.assertIs(jwebot_edge, edge_runtime)
def test_legacy_module_mutation_affects_runtime_globals(self):
original = edge_runtime.CLOUD_HOST if hasattr(edge_runtime, "CLOUD_HOST") else None
jwebot_edge.COMPATIBILITY_TEST_VALUE = "value"
try:
self.assertEqual(edge_runtime.COMPATIBILITY_TEST_VALUE, "value")
finally:
del edge_runtime.COMPATIBILITY_TEST_VALUE
self.assertEqual(
edge_runtime.CLOUD_HOST if hasattr(edge_runtime, "CLOUD_HOST") else None,
original,
)
def test_root_cli_still_exposes_help(self):
result = subprocess.run(
[sys.executable, str(PROJECT_ROOT / "jwebot_edge.py"), "--help"],
text=True,
capture_output=True,
timeout=10,
check=False,
)
self.assertEqual(result.returncode, 0)
self.assertIn("Local, low-latency router", result.stdout)
if __name__ == "__main__":
unittest.main()
+351
View File
@@ -0,0 +1,351 @@
#!/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"
try:
import tomli
TOML_EXTRA = [str(Path(tomli.__file__).resolve().parent.parent)]
except ModuleNotFoundError:
TOML_EXTRA = []
sys.path[:0] = [
str(SDK_SRC),
str(PROJECT_ROOT / "packages" / "jwebot-official-extensions" / "src"),
str(KERNEL_SRC),
]
from jwebot_kernel import ( # noqa: E402
ExtensionHostClient,
build_release,
install_release,
sign_release_record,
)
from jwebot_sdk import ExtensionResult, ReplyPlan # noqa: E402
from tests.test_jwebot_edge import MODULE, config, event # noqa: E402
PUBLISHER = "test-publisher"
def publisher_key(root: Path):
private_key = root / "publisher"
result = subprocess.run(
[
"/usr/bin/ssh-keygen",
"-q",
"-t",
"ed25519",
"-N",
"",
"-f",
str(private_key),
],
capture_output=True,
check=False,
)
if result.returncode != 0:
raise RuntimeError("cannot generate test publisher key")
allowed_signers = root / "allowed_signers"
allowed_signers.write_text(
f"{PUBLISHER} "
+ private_key.with_suffix(".pub").read_text(encoding="utf-8"),
encoding="utf-8",
)
return private_key, allowed_signers
def install_coin(
root: Path,
private_key: Path,
allowed_signers: Path,
) -> Path:
source = PROJECT_ROOT / "extensions" / "jwebot" / "coin"
artifact, record_path, _ = build_release(source, root / "coin-dist")
signature_path, _ = sign_release_record(
record_path,
private_key,
PUBLISHER,
)
state_root = root / "extensions"
install_release(
artifact,
record_path,
state_root / "coin",
signature=signature_path,
allowed_signers=allowed_signers,
expected_publisher=PUBLISHER,
health_check=lambda release: None,
)
return state_root
@unittest.skipUnless(
Path("/usr/bin/ssh-keygen").is_file(),
"/usr/bin/ssh-keygen is unavailable",
)
class EdgeHostedExtensionTests(unittest.TestCase):
def setup_signed_coin(self):
temporary = tempfile.TemporaryDirectory()
self.addCleanup(temporary.cleanup)
root = Path(temporary.name)
private_key, allowed_signers = publisher_key(root)
state_root = install_coin(root, private_key, allowed_signers)
return root, state_root, allowed_signers
@staticmethod
def healthy_coin_factory(calls, *, fail_invoke=False):
def factory(plugin_id, release_dir):
del release_dir
def invoke(request, service_handler):
calls.append((plugin_id, dict(request)))
if request["op"] == "health":
return {
"status": "ok",
"api_version": "1",
"plugin_id": "coin",
"author": "jwebot",
"description": "抛一枚硬币",
"help": ["/翻硬币"],
"commands": ["/翻硬币"],
"service_profiles": ["coin"],
}
if fail_invoke:
raise RuntimeError("host failed")
response = service_handler(
"coin",
"execute",
{
"command": request["command"],
"arguments": request["arguments"],
"event": request["event"],
},
)
return {
"status": "ok",
"api_version": "1",
"plugin_id": "coin",
"result": ExtensionResult(
reply=ReplyPlan.text(response["text"])
).to_mapping(),
}
return invoke
return factory
def test_preferred_host_executes_signed_coin_through_service_broker(self):
root, state_root, allowed_signers = self.setup_signed_coin()
calls = []
application = MODULE.EdgeApplication(
config(
root,
extension_mode="prefer-host",
extension_state_root=state_root,
extension_allowed_signers=allowed_signers,
extension_publisher=PUBLISHER,
),
host_invoker_factory=self.healthy_coin_factory(calls),
)
self.addCleanup(application.close)
response = application.accept(event("host-coin", "@JWeBot /翻硬币"))
self.assertEqual(response["route"], "local")
self.assertIn("硬币落地", response["text"])
self.assertEqual(
[request["op"] for _, request in calls],
["health", "invoke_command"],
)
self.assertEqual(application.health()["hosted_extensions"], 1)
def test_preferred_host_uses_in_process_for_missing_release(self):
root, state_root, allowed_signers = self.setup_signed_coin()
application = MODULE.EdgeApplication(
config(
root,
extension_mode="prefer-host",
extension_state_root=state_root,
extension_allowed_signers=allowed_signers,
extension_publisher=PUBLISHER,
),
host_invoker_factory=self.healthy_coin_factory([]),
)
self.addCleanup(application.close)
response = application.accept(
event("fallback-dice", "@JWeBot /丢骰子 1d6")
)
self.assertEqual(response["route"], "local")
self.assertIn("掷出了", response["text"])
def test_unhealthy_host_is_excluded_before_runtime_fallback(self):
root, state_root, allowed_signers = self.setup_signed_coin()
def factory(plugin_id, release_dir):
del plugin_id, release_dir
return lambda request, service: {"status": "error"}
application = MODULE.EdgeApplication(
config(
root,
extension_mode="prefer-host",
extension_state_root=state_root,
extension_allowed_signers=allowed_signers,
extension_publisher=PUBLISHER,
),
host_invoker_factory=factory,
)
self.addCleanup(application.close)
response = application.accept(
event("fallback-coin", "@JWeBot /翻硬币")
)
self.assertIn("硬币落地", response["text"])
self.assertEqual(application.health()["hosted_extension_failures"], 1)
def test_active_host_failure_does_not_replay_stateful_command(self):
root, state_root, allowed_signers = self.setup_signed_coin()
application = MODULE.EdgeApplication(
config(
root,
extension_mode="prefer-host",
extension_state_root=state_root,
extension_allowed_signers=allowed_signers,
extension_publisher=PUBLISHER,
),
host_invoker_factory=self.healthy_coin_factory(
[],
fail_invoke=True,
),
)
self.addCleanup(application.close)
application.extensions.handle = mock.Mock(side_effect=AssertionError)
response = application.accept(
event("host-failed", "@JWeBot /翻硬币")
)
self.assertIn("隔离插件暂时不可用", response["text"])
application.extensions.handle.assert_not_called()
self.assertEqual(
application.ledger.row("host-failed")["status"],
"local_rejected",
)
def test_host_only_requires_every_signed_plugin(self):
root, state_root, allowed_signers = self.setup_signed_coin()
with self.assertRaisesRegex(MODULE.EdgeError, "startup failed"):
MODULE.EdgeApplication(
config(
root,
extension_mode="host-only",
extension_state_root=state_root,
extension_allowed_signers=allowed_signers,
extension_publisher=PUBLISHER,
),
host_invoker_factory=self.healthy_coin_factory([]),
)
def test_preferred_mode_rejects_tampered_installed_release(self):
root, state_root, allowed_signers = self.setup_signed_coin()
release_dir = (state_root / "coin" / "current").resolve()
(release_dir / "README.md").write_text(
"tampered\n",
encoding="utf-8",
)
with self.assertRaisesRegex(MODULE.EdgeError, "startup failed"):
MODULE.EdgeApplication(
config(
root,
extension_mode="prefer-host",
extension_state_root=state_root,
extension_allowed_signers=allowed_signers,
extension_publisher=PUBLISHER,
extension_fallback_enabled=True,
),
host_invoker_factory=self.healthy_coin_factory([]),
)
def test_edge_to_real_host_process_to_service_broker_round_trip(self):
root, state_root, allowed_signers = self.setup_signed_coin()
wrapper = root / "host-wrapper.py"
wrapper.write_text(
"\n".join(
(
"import runpy, sys",
"sys.path[:0] = [",
f" {str(SDK_SRC)!r},",
f" {str(KERNEL_SRC)!r},",
*(
f" {path!r},"
for path in TOML_EXTRA
),
" "
+ repr(
str(
PROJECT_ROOT
/ "packages"
/ "jwebot-extension-host"
/ "src"
)
)
+ ",",
"]",
"runpy.run_module('jwebot_extension_host', run_name='__main__')",
)
)
+ "\n",
encoding="utf-8",
)
def factory(plugin_id, release_dir):
del plugin_id
client = ExtensionHostClient(
command=(sys.executable, str(wrapper)),
timeout_seconds=5,
allowed_signers=allowed_signers,
expected_publisher=PUBLISHER,
)
return lambda request, service: client.invoke(
release_dir,
request,
service_handler=service,
)
application = MODULE.EdgeApplication(
config(
root,
extension_mode="prefer-host",
extension_transport="process",
extension_state_root=state_root,
extension_allowed_signers=allowed_signers,
extension_publisher=PUBLISHER,
),
host_invoker_factory=factory,
)
self.addCleanup(application.close)
response = application.accept(
event("real-host-coin", "@JWeBot /翻硬币")
)
self.assertEqual(response["route"], "local")
self.assertIn("硬币落地", response["text"])
self.assertEqual(application.health()["hosted_extensions"], 1)
if __name__ == "__main__":
unittest.main()
+54
View File
@@ -0,0 +1,54 @@
#!/usr/bin/env python3
import importlib.util
import sys
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"
EXAMPLE_SRC = PROJECT_ROOT / "examples" / "hello-command" / "src"
sys.path[:0] = [str(SDK_SRC), str(EXAMPLE_SRC)]
from jwebot_sdk import Event, PlanKind # noqa: E402
SPEC = importlib.util.spec_from_file_location(
"hello_command",
EXAMPLE_SRC / "hello_command.py",
)
EXAMPLE = importlib.util.module_from_spec(SPEC)
assert SPEC.loader is not None
SPEC.loader.exec_module(EXAMPLE)
class HelloCommandTests(unittest.TestCase):
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_handler_returns_a_structured_reply(self):
result = EXAMPLE.handle_hello(self.event(), "world")
self.assertEqual(result.reply.kind, PlanKind.TEXT)
self.assertEqual(result.reply.message, "hello Alice world")
def test_plugin_registers_only_its_declared_command(self):
context = mock.Mock()
EXAMPLE.plugin(context)
context.commands.register.assert_called_once_with(
"/hello",
EXAMPLE.handle_hello,
)
if __name__ == "__main__":
unittest.main()
+292
View File
@@ -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()
+58
View File
@@ -0,0 +1,58 @@
#!/usr/bin/env python3
import unittest
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parents[1]
SERVICE = (
PROJECT_ROOT
/ "deploy"
/ "systemd"
/ "system"
/ "jwebot-extension-host@.service"
)
SOCKET = (
PROJECT_ROOT
/ "deploy"
/ "systemd"
/ "system"
/ "jwebot-extension-host@.socket"
)
class ExtensionSandboxUnitTests(unittest.TestCase):
def test_service_has_required_isolation_controls(self):
text = SERVICE.read_text(encoding="utf-8")
for setting in (
"User=jwebot-ext",
"NoNewPrivileges=true",
"CapabilityBoundingSet=",
"PrivateNetwork=true",
"ProtectSystem=strict",
"ProtectHome=true",
"MemoryDenyWriteExecute=true",
"RestrictAddressFamilies=AF_UNIX",
"RuntimeMaxSec=12s",
"MemoryMax=128M",
"TasksMax=16",
"LimitCORE=0",
):
with self.subTest(setting=setting):
self.assertIn(setting, text)
def test_socket_is_private_to_the_edge_user(self):
text = SOCKET.read_text(encoding="utf-8")
self.assertIn("SocketUser=jwebot", text)
self.assertIn("SocketMode=0600", text)
self.assertIn("RemoveOnStop=true", text)
def test_service_has_no_write_allowlist(self):
text = SERVICE.read_text(encoding="utf-8")
self.assertNotIn("ReadWritePaths=", text)
self.assertIn("ReadOnlyPaths=/var/lib/jwebot-extensions/%i", text)
if __name__ == "__main__":
unittest.main()
+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()
File diff suppressed because it is too large Load Diff
+62
View File
@@ -0,0 +1,62 @@
#!/usr/bin/env python3
import importlib.util
import json
import socket
import sys
import tempfile
import threading
import unittest
from pathlib import Path
from unittest import mock
PROJECT_ROOT = Path(__file__).resolve().parents[1]
MODULE_PATH = PROJECT_ROOT / "jwebot_edge_delivery.py"
SPEC = importlib.util.spec_from_file_location("jwebot_edge_delivery", MODULE_PATH)
MODULE = importlib.util.module_from_spec(SPEC)
assert SPEC.loader is not None
sys.modules[SPEC.name] = MODULE
SPEC.loader.exec_module(MODULE)
class DeliveryHelperTests(unittest.TestCase):
def test_submit_gui_sender_uses_unix_socket_protocol(self):
with tempfile.TemporaryDirectory() as temporary:
socket_path = Path(temporary) / "gui.sock"
received = {}
def server():
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as listener:
listener.bind(str(socket_path))
listener.listen(1)
connection, _ = listener.accept()
with connection:
raw = connection.recv(4096).split(b"\n", 1)[0]
received.update(json.loads(raw.decode("utf-8")))
connection.sendall(b'{"ok":true,"state":"sent"}\n')
thread = threading.Thread(target=server, daemon=True)
thread.start()
for _ in range(200):
if socket_path.exists():
break
MODULE.time.sleep(0.01)
with mock.patch.object(MODULE, "GUI_SENDER_SOCKET", socket_path):
response = MODULE.submit_gui_sender(
{
"op": "send",
"event_id": "event-1",
"group_fingerprint": "group-a",
"reply": "reply",
"source": "delivery-helper",
}
)
self.assertEqual(response["state"], "sent")
self.assertEqual(received["op"], "send")
if __name__ == "__main__":
unittest.main()
+229
View File
@@ -0,0 +1,229 @@
#!/usr/bin/env python3
import os
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"
sys.path[:0] = [str(SDK_SRC), str(KERNEL_SRC)]
from jwebot_kernel import ( # noqa: E402
ActivationError,
ReleaseError,
build_release,
install_release,
parse_manifest,
rollback_release,
)
def manifest():
return parse_manifest(
{
"id": "hello-command",
"author": "tester",
"description": "Say hello",
"help": ["/hello"],
"version": "1.0.0",
"api_version": "1",
"entrypoint": "hello_command:plugin",
"type": "command",
"capabilities": {
"commands": ["/hello"],
"storage": False,
"network_profiles": [],
},
}
)
def extension(root: Path, reply: str) -> Path:
root.mkdir()
(root / "src").mkdir()
(root / "plugin.toml").write_text(
'id = "hello-command"\n',
encoding="utf-8",
)
(root / "src" / "hello_command.py").write_text(
"\n".join(
(
"from jwebot_sdk import ExtensionResult, ReplyPlan",
"",
"def handle(event, arguments):",
f" return ExtensionResult(reply=ReplyPlan.text({reply!r}))",
)
),
encoding="utf-8",
)
return root
class ActivationTests(unittest.TestCase):
def build(self, base: Path, name: str, reply: str):
return build_release(
extension(base / name, reply),
base / f"{name}-dist",
manifest=manifest(),
)
def test_install_creates_verified_release_and_current_pointer(self):
with tempfile.TemporaryDirectory() as directory:
base = Path(directory)
artifact, record_path, record = self.build(base, "first", "one")
release_dir = install_release(
artifact,
record_path,
base / "state",
allow_unsigned=True,
)
self.assertEqual(release_dir.name, record.source_digest)
self.assertEqual(
os.readlink(base / "state" / "current"),
f"releases/{record.source_digest}",
)
self.assertTrue((release_dir / ".release.json").is_file())
self.assertEqual(
(release_dir / "src" / "hello_command.py").stat().st_mode & 0o777,
0o600,
)
def test_install_rejects_unsigned_release_by_default(self):
with tempfile.TemporaryDirectory() as directory:
base = Path(directory)
artifact, record_path, _ = self.build(base, "first", "one")
state = base / "state"
with self.assertRaisesRegex(
ActivationError,
"signed release activation is required",
):
install_release(artifact, record_path, state)
self.assertFalse(state.exists())
def test_unsigned_local_install_rejects_publisher_policy(self):
with tempfile.TemporaryDirectory() as directory:
base = Path(directory)
artifact, record_path, _ = self.build(base, "first", "one")
with self.assertRaisesRegex(
ActivationError,
"cannot include signature inputs",
):
install_release(
artifact,
record_path,
base / "state",
expected_publisher="test-publisher",
allow_unsigned=True,
)
def test_second_install_preserves_previous_and_rollback_swaps(self):
with tempfile.TemporaryDirectory() as directory:
base = Path(directory)
first_artifact, first_record_path, first_record = self.build(
base,
"first",
"one",
)
second_artifact, second_record_path, second_record = self.build(
base,
"second",
"two",
)
state = base / "state"
install_release(
first_artifact,
first_record_path,
state,
allow_unsigned=True,
)
install_release(
second_artifact,
second_record_path,
state,
allow_unsigned=True,
)
self.assertEqual(
os.readlink(state / "previous"),
f"releases/{first_record.source_digest}",
)
with self.assertRaisesRegex(
ActivationError,
"signed release rollback is required",
):
rollback_release(state)
rollback_release(state, allow_unsigned=True)
self.assertEqual(
os.readlink(state / "current"),
f"releases/{first_record.source_digest}",
)
self.assertEqual(
os.readlink(state / "previous"),
f"releases/{second_record.source_digest}",
)
def test_tampered_artifact_does_not_create_activation_state(self):
with tempfile.TemporaryDirectory() as directory:
base = Path(directory)
artifact, record_path, _ = self.build(base, "first", "one")
artifact.write_bytes(artifact.read_bytes() + b"tampered")
state = base / "state"
with self.assertRaises(ReleaseError):
install_release(
artifact,
record_path,
state,
allow_unsigned=True,
)
self.assertFalse((state / "current").exists())
def test_existing_release_content_is_reverified(self):
with tempfile.TemporaryDirectory() as directory:
base = Path(directory)
artifact, record_path, _ = self.build(base, "first", "one")
state = base / "state"
release_dir = install_release(
artifact,
record_path,
state,
allow_unsigned=True,
)
(release_dir / "src" / "hello_command.py").write_text(
"tampered\n",
encoding="utf-8",
)
with self.assertRaisesRegex(ActivationError, "source digest mismatch"):
install_release(
artifact,
record_path,
state,
allow_unsigned=True,
)
def test_rollback_requires_a_previous_verified_release(self):
with tempfile.TemporaryDirectory() as directory:
state = Path(directory) / "state"
with self.assertRaisesRegex(ActivationError, "no verified previous"):
rollback_release(state)
def test_unsafe_current_pointer_is_rejected(self):
with tempfile.TemporaryDirectory() as directory:
base = Path(directory)
artifact, record_path, _ = self.build(base, "first", "one")
state = base / "state"
state.mkdir()
(state / "current").symlink_to("/tmp")
with self.assertRaisesRegex(ActivationError, "unsafe target"):
install_release(
artifact,
record_path,
state,
allow_unsigned=True,
)
if __name__ == "__main__":
unittest.main()
+93
View File
@@ -0,0 +1,93 @@
#!/usr/bin/env python3
import sys
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"
sys.path[:0] = [str(SDK_SRC), str(KERNEL_SRC)]
from jwebot_kernel import ( # noqa: E402
CatalogError,
ExtensionCatalog,
parse_manifest,
)
def manifest(plugin_id: str, command: str):
module_name = plugin_id.replace("-", "_")
return parse_manifest(
{
"id": plugin_id,
"author": "tester",
"description": f"Handle {command}",
"help": [command],
"version": "1.0.0",
"api_version": "1",
"entrypoint": f"{module_name}:plugin",
"type": "command",
"capabilities": {
"commands": [command],
"storage": False,
"network_profiles": [],
},
}
)
class ExtensionCatalogTests(unittest.TestCase):
def test_registration_is_open_then_frozen(self):
catalog = ExtensionCatalog()
catalog.register(manifest("hello-command", "/hello"))
self.assertFalse(catalog.frozen)
catalog.freeze()
self.assertTrue(catalog.frozen)
with self.assertRaisesRegex(CatalogError, "frozen"):
catalog.register(manifest("weather-command", "/weather"))
def test_duplicate_plugin_id_is_rejected(self):
catalog = ExtensionCatalog()
catalog.register(manifest("hello-command", "/hello"))
with self.assertRaisesRegex(CatalogError, "duplicate plugin id"):
catalog.register(manifest("hello-command", "/hello2"))
def test_command_collision_is_rejected_case_insensitively(self):
catalog = ExtensionCatalog()
catalog.register(manifest("hello-command", "/Hello"))
with self.assertRaisesRegex(CatalogError, "already belongs"):
catalog.register(manifest("other-command", "/hello"))
def test_routing_requires_a_frozen_catalog(self):
catalog = ExtensionCatalog()
with self.assertRaisesRegex(CatalogError, "must be frozen"):
catalog.resolve_command("/hello")
def test_frozen_catalog_resolves_arguments(self):
catalog = ExtensionCatalog()
catalog.register(manifest("hello-command", "/hello"))
catalog.freeze()
match = catalog.resolve_command("/hello world")
self.assertIsNotNone(match)
assert match is not None
self.assertEqual(match.plugin_id, "hello-command")
self.assertEqual(match.command, "/hello")
self.assertEqual(match.arguments, "world")
def test_command_name_does_not_match_a_longer_token(self):
catalog = ExtensionCatalog()
catalog.register(manifest("hello-command", "/hello"))
catalog.freeze()
self.assertIsNone(catalog.resolve_command("/hello-world"))
def test_catalog_views_are_read_only(self):
catalog = ExtensionCatalog()
catalog.register(manifest("hello-command", "/hello"))
with self.assertRaises(TypeError):
catalog.commands["/other"] = "other-command" # type: ignore[index]
if __name__ == "__main__":
unittest.main()
+246
View File
@@ -0,0 +1,246 @@
#!/usr/bin/env python3
import shutil
import subprocess
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"
sys.path[:0] = [str(SDK_SRC), str(KERNEL_SRC)]
from jwebot_kernel import ( # noqa: E402
ExpectedExtension,
HostedExtensionError,
HostedExtensionRuntime,
SignedExtensionInventory,
build_release,
install_release,
sign_release_record,
)
from jwebot_sdk import ExtensionResult, ReplyPlan # noqa: E402
PUBLISHER = "test-publisher"
def publisher_key(root: Path):
private_key = root / "publisher"
result = subprocess.run(
[
"/usr/bin/ssh-keygen",
"-q",
"-t",
"ed25519",
"-N",
"",
"-f",
str(private_key),
],
capture_output=True,
check=False,
)
if result.returncode != 0:
raise RuntimeError("cannot generate test publisher key")
allowed_signers = root / "allowed_signers"
allowed_signers.write_text(
f"{PUBLISHER} "
+ private_key.with_suffix(".pub").read_text(encoding="utf-8"),
encoding="utf-8",
)
return private_key, allowed_signers
def install_example(base: Path, private_key: Path, allowed_signers: Path):
source = base / "source"
shutil.copytree(PROJECT_ROOT / "examples" / "hello-command", source)
artifact, record_path, _ = build_release(source, base / "dist")
signature_path, _ = sign_release_record(
record_path,
private_key,
PUBLISHER,
)
state_root = base / "state"
release_dir = install_release(
artifact,
record_path,
state_root / "hello-command",
signature=signature_path,
allowed_signers=allowed_signers,
expected_publisher=PUBLISHER,
health_check=lambda release: None,
)
return state_root, release_dir
def expected(author: str = "example"):
return ExpectedExtension(
plugin_id="hello-command",
author=author,
commands=("/hello",),
service_profiles=(),
)
@unittest.skipUnless(
Path("/usr/bin/ssh-keygen").is_file(),
"/usr/bin/ssh-keygen is unavailable",
)
class SignedInventoryTests(unittest.TestCase):
def test_signed_inventory_loads_and_freezes_current_release(self):
with tempfile.TemporaryDirectory() as directory:
base = Path(directory)
private_key, allowed_signers = publisher_key(base)
state_root, release_dir = install_example(
base,
private_key,
allowed_signers,
)
inventory = SignedExtensionInventory.load(
state_root,
allowed_signers,
[expected()],
expected_publisher=PUBLISHER,
require_all=True,
)
self.assertTrue(inventory.catalog.frozen)
self.assertEqual(
inventory.releases["hello-command"].release_dir,
release_dir,
)
match = inventory.catalog.resolve_command("/hello world")
self.assertEqual(match.plugin_id, "hello-command")
self.assertEqual(match.arguments, "world")
def test_signed_inventory_rejects_expected_author_mismatch(self):
with tempfile.TemporaryDirectory() as directory:
base = Path(directory)
private_key, allowed_signers = publisher_key(base)
state_root, _ = install_example(
base,
private_key,
allowed_signers,
)
with self.assertRaisesRegex(
HostedExtensionError,
"inventory mismatch",
):
SignedExtensionInventory.load(
state_root,
allowed_signers,
[expected(author="wrong")],
expected_publisher=PUBLISHER,
)
def test_signed_inventory_can_require_every_release(self):
with tempfile.TemporaryDirectory() as directory:
base = Path(directory)
_, allowed_signers = publisher_key(base)
with self.assertRaisesRegex(
HostedExtensionError,
"are missing",
):
SignedExtensionInventory.load(
base / "state",
allowed_signers,
[expected()],
require_all=True,
)
def test_hosted_runtime_health_gates_and_invokes_result(self):
with tempfile.TemporaryDirectory() as directory:
base = Path(directory)
private_key, allowed_signers = publisher_key(base)
state_root, _ = install_example(
base,
private_key,
allowed_signers,
)
inventory = SignedExtensionInventory.load(
state_root,
allowed_signers,
[expected()],
expected_publisher=PUBLISHER,
require_all=True,
)
calls = []
def invoke(request, service_handler):
calls.append((dict(request), service_handler))
if request["op"] == "health":
return {
"status": "ok",
"api_version": "1",
"plugin_id": "hello-command",
"author": "example",
"description": "Return a local greeting",
"help": ["/hello [name]"],
"commands": ["/hello"],
"service_profiles": [],
}
return {
"status": "ok",
"api_version": "1",
"plugin_id": "hello-command",
"result": ExtensionResult(
reply=ReplyPlan.text("hosted")
).to_mapping(),
}
runtime = HostedExtensionRuntime(
inventory,
{"hello-command": invoke},
lambda profile, operation, payload: {},
require_all_healthy=True,
)
result = runtime.invoke(
"hello-command",
"/hello",
"world",
{
"event_id": "event-1",
"observed_at": "2026-10-14T10:00:00+08:00",
"sender": "Alice",
"text": "@JWeBot /hello world",
"group_fingerprint": "group-a",
"mention_bot": True,
},
)
self.assertEqual(result.reply.message, "hosted")
self.assertEqual(calls[1][0]["arguments"], "world")
def test_unhealthy_host_is_not_active_in_preferred_mode(self):
with tempfile.TemporaryDirectory() as directory:
base = Path(directory)
private_key, allowed_signers = publisher_key(base)
state_root, _ = install_example(
base,
private_key,
allowed_signers,
)
inventory = SignedExtensionInventory.load(
state_root,
allowed_signers,
[expected()],
expected_publisher=PUBLISHER,
)
runtime = HostedExtensionRuntime(
inventory,
{"hello-command": lambda request, service: {"status": "error"}},
lambda profile, operation, payload: {},
)
self.assertFalse(runtime.available("hello-command"))
self.assertIn("hello-command", runtime.failures)
if __name__ == "__main__":
unittest.main()
+326
View File
@@ -0,0 +1,326 @@
#!/usr/bin/env python3
import json
import os
import socket
import sys
import tempfile
import threading
import time
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"
sys.path[:0] = [str(SDK_SRC), str(KERNEL_SRC)]
from jwebot_kernel import ( # noqa: E402
ExtensionHostClient,
HostClientError,
HostTimeout,
RuntimeTelemetry,
UnixExtensionHostClient,
)
class HostClientTests(unittest.TestCase):
def helper(self, source: str):
temporary = tempfile.TemporaryDirectory()
self.addCleanup(temporary.cleanup)
root = Path(temporary.name)
release = root / "release"
release.mkdir()
script = root / "host.py"
script.write_text(source, encoding="utf-8")
return root, release, script
def test_client_uses_minimal_environment_and_parses_response(self):
root, release, script = self.helper(
"\n".join(
(
"import json, os, sys",
"request = json.loads(sys.stdin.readline())",
"print(json.dumps({",
" 'status': 'ok',",
" 'event_id': request['event_id'],",
" 'secret_present': 'JWEBOT_TEST_SECRET' in os.environ,",
" 'dont_write_bytecode': sys.dont_write_bytecode,",
"}), flush=True)",
)
)
)
del root
os.environ["JWEBOT_TEST_SECRET"] = "must-not-cross"
self.addCleanup(os.environ.pop, "JWEBOT_TEST_SECRET", None)
response = ExtensionHostClient(
command=(sys.executable, str(script)),
).invoke(release, {"event_id": "event-1"})
self.assertEqual(response["event_id"], "event-1")
self.assertFalse(response["secret_present"])
self.assertTrue(response["dont_write_bytecode"])
def test_client_rejects_invalid_json(self):
_, release, script = self.helper("print('not-json')\n")
with self.assertRaisesRegex(HostClientError, "invalid JSON"):
ExtensionHostClient(
command=(sys.executable, str(script)),
).invoke(release, {"event_id": "event-1"})
def test_client_rejects_host_error_status(self):
_, release, script = self.helper(
"print('{\"status\":\"error\",\"error\":\"ExtensionFailed\"}')\n"
)
with self.assertRaisesRegex(HostClientError, "returned an error"):
ExtensionHostClient(
command=(sys.executable, str(script)),
).invoke(release, {"event_id": "event-1"})
def test_client_enforces_request_limit(self):
_, release, script = self.helper("print('{\"status\":\"ok\"}')\n")
with self.assertRaisesRegex(HostClientError, "size limit"):
ExtensionHostClient(
command=(sys.executable, str(script)),
).invoke(release, {"value": "x" * (256 * 1024)})
def test_timeout_kills_the_host_process_group(self):
root, release, script = self.helper(
"\n".join(
(
"import subprocess, sys, time",
"marker = sys.argv[1]",
"subprocess.Popen([",
" sys.executable, '-c',",
" 'import pathlib,time,sys; time.sleep(0.3); '",
" 'pathlib.Path(sys.argv[1]).write_text(\"escaped\")',",
" marker,",
"])",
"time.sleep(10)",
)
)
)
marker = root / "escaped.txt"
client = ExtensionHostClient(
command=(sys.executable, str(script), str(marker)),
timeout_seconds=0.05,
)
with self.assertRaises(HostTimeout):
client.invoke(release, {"event_id": "event-1"})
time.sleep(0.5)
self.assertFalse(marker.exists())
def test_client_services_one_bounded_broker_request(self):
_, release, script = self.helper(
"\n".join(
(
"import json, sys",
"request = json.loads(sys.stdin.readline())",
"print(json.dumps({",
" 'status': 'service_request',",
" 'call_id': '1',",
" 'profile': 'coin',",
" 'operation': 'execute',",
" 'payload': {'command': '/翻硬币'},",
"}), flush=True)",
"service = json.loads(sys.stdin.readline())",
"print(json.dumps({",
" 'status': 'ok',",
" 'plugin_id': 'coin',",
" 'request_op': request['op'],",
" 'text': service['result']['text'],",
"}), flush=True)",
)
)
)
calls = []
observed = []
def service(profile, operation, payload):
calls.append((profile, operation, payload))
return {"text": "硬币落地:正面喵"}
response = ExtensionHostClient(
command=(sys.executable, str(script)),
plugin_id="coin",
telemetry=RuntimeTelemetry(
component="host-client",
observer=observed.append,
logger=mock.Mock(),
),
).invoke(
release,
{"op": "invoke_command"},
service_handler=service,
)
self.assertEqual(response["text"], "硬币落地:正面喵")
self.assertEqual(
calls,
[
(
"coin",
"execute",
{"command": "/翻硬币"},
)
],
)
names = [value["name"] for value in observed]
self.assertIn("host.process.spawn", names)
self.assertIn("host.service.request", names)
self.assertIn("host.service.complete", names)
self.assertIn("host.invoke", names)
def test_service_handler_cannot_exceed_host_deadline(self):
_, release, script = self.helper(
"\n".join(
(
"import json, sys",
"json.loads(sys.stdin.readline())",
"print(json.dumps({",
" 'status': 'service_request',",
" 'call_id': '1',",
" 'profile': 'coin',",
" 'operation': 'execute',",
" 'payload': {},",
"}), flush=True)",
"sys.stdin.readline()",
)
)
)
def slow_service(profile, operation, payload):
del profile, operation, payload
time.sleep(0.65)
return {"text": "late"}
with self.assertRaisesRegex(HostTimeout, "service exceeded"):
ExtensionHostClient(
command=(sys.executable, str(script)),
timeout_seconds=0.5,
).invoke(
release,
{"op": "invoke_command"},
service_handler=slow_service,
)
class UnixHostClientTests(unittest.TestCase):
def server(self, response: bytes, *, delay: float = 0.0):
temporary = tempfile.TemporaryDirectory()
self.addCleanup(temporary.cleanup)
path = Path(temporary.name) / "host.sock"
listener = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
listener.bind(str(path))
listener.listen(1)
received: list[bytes] = []
def serve():
try:
connection, _ = listener.accept()
with connection:
stream = connection.makefile("rwb", buffering=0)
received.append(stream.readline(256 * 1024 + 1))
if delay:
time.sleep(delay)
try:
stream.write(response)
except OSError:
pass
stream.close()
finally:
listener.close()
thread = threading.Thread(target=serve, daemon=True)
thread.start()
self.addCleanup(thread.join, 1)
return path, received
def test_unix_client_round_trip(self):
path, received = self.server(
b'{"status":"ok","plugin_id":"hello-command"}\n'
)
response = UnixExtensionHostClient(path).invoke(
{"op": "invoke_command", "command": "/hello"}
)
self.assertEqual(response["plugin_id"], "hello-command")
self.assertEqual(
json.loads(received[0]),
{"command": "/hello", "op": "invoke_command"},
)
def test_unix_client_timeout_is_bounded(self):
path, _ = self.server(
b'{"status":"ok"}\n',
delay=0.3,
)
with self.assertRaises(HostTimeout):
UnixExtensionHostClient(
path,
timeout_seconds=0.05,
).invoke({"op": "invoke_command"})
def test_unix_client_rejects_error_response(self):
path, _ = self.server(
b'{"status":"error","error":"ExtensionExecutionError"}\n'
)
with self.assertRaisesRegex(HostClientError, "returned an error"):
UnixExtensionHostClient(path).invoke({"op": "invoke_command"})
def test_unix_client_fails_closed_when_socket_is_missing(self):
with tempfile.TemporaryDirectory() as directory:
path = Path(directory) / "missing.sock"
with self.assertRaisesRegex(HostClientError, "unavailable"):
UnixExtensionHostClient(path).invoke(
{"op": "invoke_command"}
)
def test_unix_client_services_broker_request(self):
temporary = tempfile.TemporaryDirectory()
self.addCleanup(temporary.cleanup)
path = Path(temporary.name) / "host.sock"
listener = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
listener.bind(str(path))
listener.listen(1)
received = []
def serve():
try:
connection, _ = listener.accept()
with connection:
stream = connection.makefile("rwb", buffering=0)
received.append(json.loads(stream.readline()))
stream.write(
b'{"status":"service_request","call_id":"1",'
b'"profile":"coin","operation":"execute",'
b'"payload":{"command":"/\\u7ffb\\u786c\\u5e01"}}\n'
)
received.append(json.loads(stream.readline()))
stream.write(
b'{"status":"ok","plugin_id":"coin"}\n'
)
stream.close()
finally:
listener.close()
thread = threading.Thread(target=serve, daemon=True)
thread.start()
self.addCleanup(thread.join, 1)
calls = []
response = UnixExtensionHostClient(path).invoke(
{"op": "invoke_command", "command": "/翻硬币"},
service_handler=lambda profile, operation, payload: (
calls.append((profile, operation, payload))
or {"text": "ok"}
),
)
self.assertEqual(response["plugin_id"], "coin")
self.assertEqual(received[1]["op"], "service_result")
self.assertEqual(calls[0][0:2], ("coin", "execute"))
if __name__ == "__main__":
unittest.main()
+173
View File
@@ -0,0 +1,173 @@
#!/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"
sys.path[:0] = [str(SDK_SRC), str(KERNEL_SRC)]
from jwebot_kernel import ( # noqa: E402
ManifestError,
PluginType,
load_manifest,
parse_manifest,
)
from jwebot_kernel import manifest as manifest_module # noqa: E402
class ManifestTests(unittest.TestCase):
def manifest(self, **overrides):
value = {
"id": "hello-command",
"author": "jwebot",
"description": "Say hello",
"help": ["/hello [name]"],
"version": "1.0.0",
"api_version": "1",
"entrypoint": "hello_command:plugin",
"type": "command",
"capabilities": {
"commands": ["/hello"],
"storage": False,
"network_profiles": [],
"service_profiles": [],
},
}
value.update(overrides)
return value
def test_command_manifest_is_parsed(self):
manifest = parse_manifest(self.manifest())
self.assertEqual(manifest.plugin_id, "hello-command")
self.assertEqual(manifest.author, "jwebot")
self.assertEqual(manifest.description, "Say hello")
self.assertEqual(manifest.help_entries, ("/hello [name]",))
self.assertEqual(manifest.plugin_type, PluginType.COMMAND)
self.assertEqual(manifest.capabilities.commands, ("/hello",))
def test_author_is_required_and_bounded(self):
value = self.manifest()
del value["author"]
with self.assertRaisesRegex(ManifestError, "author must"):
parse_manifest(value)
with self.assertRaisesRegex(ManifestError, "author has an invalid format"):
parse_manifest(self.manifest(author="../Alice"))
def test_unknown_capability_is_rejected(self):
value = self.manifest()
value["capabilities"]["shell"] = True
with self.assertRaisesRegex(ManifestError, "unknown capabilities"):
parse_manifest(value)
def test_description_and_help_are_declared_together(self):
value = self.manifest()
del value["help"]
with self.assertRaisesRegex(
ManifestError,
"declared together",
):
parse_manifest(value)
def test_command_manifest_requires_visible_help_metadata(self):
value = self.manifest()
del value["description"]
del value["help"]
with self.assertRaisesRegex(
ManifestError,
"must declare description",
):
parse_manifest(value)
def test_help_entries_are_bounded_command_usage(self):
with self.assertRaisesRegex(ManifestError, "invalid display"):
parse_manifest(self.manifest(help=["plain prose"]))
with self.assertRaisesRegex(ManifestError, "display boundary"):
parse_manifest(self.manifest(description="x" * 121))
def test_command_extension_requires_a_namespace(self):
value = self.manifest()
value["capabilities"]["commands"] = []
with self.assertRaisesRegex(ManifestError, "at least one command"):
parse_manifest(value)
def test_non_command_extension_cannot_claim_a_command(self):
with self.assertRaisesRegex(ManifestError, "only command extensions"):
parse_manifest(self.manifest(type="enrichment"))
def test_unsupported_api_version_is_rejected(self):
with self.assertRaisesRegex(ManifestError, "unsupported api_version"):
parse_manifest(self.manifest(api_version="2"))
def test_entrypoint_must_use_plugin_namespace(self):
with self.assertRaisesRegex(ManifestError, "plugin module namespace"):
parse_manifest(self.manifest(entrypoint="json:plugin"))
def test_service_profiles_are_bounded_and_parsed(self):
value = self.manifest()
value["capabilities"]["service_profiles"] = ["coin"]
self.assertEqual(
parse_manifest(value).capabilities.service_profiles,
("coin",),
)
def test_duplicate_commands_are_rejected(self):
value = self.manifest()
value["capabilities"]["commands"] = ["/hello", "/hello"]
with self.assertRaisesRegex(ManifestError, "duplicates"):
parse_manifest(value)
def test_case_insensitive_duplicate_commands_are_rejected(self):
value = self.manifest()
value["capabilities"]["commands"] = ["/Hello", "/hello"]
with self.assertRaisesRegex(ManifestError, "case-insensitive duplicates"):
parse_manifest(value)
def test_command_rejects_manifest_breaking_characters(self):
value = self.manifest()
value["capabilities"]["commands"] = ['/"quoted']
with self.assertRaisesRegex(ManifestError, "invalid value"):
parse_manifest(value)
def test_command_accepts_chinese_names(self):
value = self.manifest()
value["capabilities"]["commands"] = ["/天气"]
self.assertEqual(
parse_manifest(value).capabilities.commands,
("/天气",),
)
def test_load_manifest_reads_toml(self):
if manifest_module.tomllib is None:
self.skipTest("tomli is not installed under local Python")
with tempfile.TemporaryDirectory() as directory:
path = Path(directory) / "plugin.toml"
path.write_text(
"\n".join(
(
'id = "hello-command"',
'author = "Alice"',
'description = "Say hello"',
'help = ["/hello"]',
'version = "1.0.0"',
'api_version = "1"',
'entrypoint = "hello_command:plugin"',
'type = "command"',
"",
"[capabilities]",
'commands = ["/hello"]',
"storage = false",
"network_profiles = []",
)
),
encoding="utf-8",
)
self.assertEqual(load_manifest(path).plugin_id, "hello-command")
if __name__ == "__main__":
unittest.main()
+215
View File
@@ -0,0 +1,215 @@
#!/usr/bin/env python3
import gzip
import hashlib
import io
import os
import sys
import tarfile
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"
sys.path[:0] = [str(SDK_SRC), str(KERNEL_SRC)]
from jwebot_kernel import ( # noqa: E402
ReleaseError,
ReleaseRecord,
build_release,
parse_manifest,
verify_release,
)
def manifest():
return parse_manifest(
{
"id": "hello-command",
"author": "tester",
"description": "Say hello",
"help": ["/hello"],
"version": "1.0.0",
"api_version": "1",
"entrypoint": "hello_command:plugin",
"type": "command",
"capabilities": {
"commands": ["/hello"],
"storage": False,
"network_profiles": [],
},
}
)
def extension(root: Path) -> Path:
root.mkdir()
(root / "src").mkdir()
(root / "plugin.toml").write_text(
'id = "hello-command"\n',
encoding="utf-8",
)
(root / "README.md").write_text("# hello\n", encoding="utf-8")
(root / "src" / "hello_command.py").write_text(
"\n".join(
(
"from jwebot_sdk import ExtensionResult, ReplyPlan",
"",
"def handle(event, arguments):",
" return ExtensionResult(reply=ReplyPlan.text('hello'))",
)
),
encoding="utf-8",
)
return root
class ReleaseTests(unittest.TestCase):
def test_release_is_deterministic_across_mtime_changes(self):
with tempfile.TemporaryDirectory() as directory:
base = Path(directory)
source = extension(base / "extension")
first, _, first_record = build_release(
source,
base / "first",
manifest=manifest(),
)
os.utime(source / "src" / "hello_command.py", (123456789, 123456789))
second, _, second_record = build_release(
source,
base / "second",
manifest=manifest(),
)
self.assertEqual(first.read_bytes(), second.read_bytes())
self.assertEqual(first_record.source_digest, second_record.source_digest)
self.assertEqual(
first_record.artifact_sha256,
second_record.artifact_sha256,
)
def test_built_release_verifies_without_extraction(self):
with tempfile.TemporaryDirectory() as directory:
base = Path(directory)
artifact, _, record = build_release(
extension(base / "extension"),
base / "output",
manifest=manifest(),
)
verify_release(artifact, record)
self.assertEqual(record.author, "tester")
self.assertEqual(
record.files,
("README.md", "plugin.toml", "src/hello_command.py"),
)
def test_artifact_tampering_is_rejected(self):
with tempfile.TemporaryDirectory() as directory:
base = Path(directory)
artifact, _, record = build_release(
extension(base / "extension"),
base / "output",
manifest=manifest(),
)
content = bytearray(artifact.read_bytes())
content[-1] ^= 0x01
artifact.write_bytes(content)
with self.assertRaisesRegex(ReleaseError, "digest mismatch"):
verify_release(artifact, record)
def test_release_record_rejects_invalid_author(self):
with tempfile.TemporaryDirectory() as directory:
base = Path(directory)
_, _, record = build_release(
extension(base / "extension"),
base / "output",
manifest=manifest(),
)
value = record.to_mapping()
value["author"] = "../tester"
with self.assertRaisesRegex(ReleaseError, "invalid release record"):
ReleaseRecord.from_mapping(value)
def test_unsupported_source_file_is_rejected(self):
with tempfile.TemporaryDirectory() as directory:
base = Path(directory)
source = extension(base / "extension")
(source / "src" / "payload.bin").write_bytes(b"payload")
with self.assertRaisesRegex(ReleaseError, "unsupported release file"):
build_release(
source,
base / "output",
manifest=manifest(),
)
def test_interpreter_cache_is_excluded_from_release(self):
with tempfile.TemporaryDirectory() as directory:
base = Path(directory)
source = extension(base / "extension")
cache = source / "src" / "__pycache__"
cache.mkdir()
(cache / "hello_command.cpython-313.pyc").write_bytes(b"cache")
_, _, record = build_release(
source,
base / "output",
manifest=manifest(),
)
self.assertEqual(
record.files,
("README.md", "plugin.toml", "src/hello_command.py"),
)
def test_immutable_artifact_cannot_be_replaced(self):
with tempfile.TemporaryDirectory() as directory:
base = Path(directory)
source = extension(base / "extension")
artifact, _, _ = build_release(
source,
base / "output",
manifest=manifest(),
)
artifact.write_bytes(b"tampered")
with self.assertRaisesRegex(ReleaseError, "immutable release"):
build_release(
source,
base / "output",
manifest=manifest(),
)
def test_unsafe_archive_member_is_rejected(self):
with tempfile.TemporaryDirectory() as directory:
artifact = Path(directory) / "unsafe.tar.gz"
with artifact.open("wb") as raw:
with gzip.GzipFile(
filename="",
mode="wb",
fileobj=raw,
mtime=0,
) as compressed:
with tarfile.open(fileobj=compressed, mode="w") as archive:
content = b"unsafe"
info = tarfile.TarInfo("plugin/../outside.py")
info.size = len(content)
archive.addfile(info, io.BytesIO(content))
artifact_bytes = artifact.read_bytes()
record = ReleaseRecord(
plugin_id="hello-command",
author="tester",
plugin_version="1.0.0",
api_version="1",
source_digest="0" * 64,
artifact_name=artifact.name,
artifact_sha256=hashlib.sha256(artifact_bytes).hexdigest(),
artifact_size=len(artifact_bytes),
files=("outside.py",),
)
with self.assertRaisesRegex(ReleaseError, "unsafe member"):
verify_release(artifact, record)
if __name__ == "__main__":
unittest.main()
+64
View File
@@ -0,0 +1,64 @@
#!/usr/bin/env python3
import sys
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"
sys.path[:0] = [str(SDK_SRC), str(KERNEL_SRC)]
from jwebot_kernel import ( # noqa: E402
ExecutionPath,
RouteKind,
classify_question,
parse_local_command,
)
ALIASES = {
"功能列表": "feature_list",
"丢骰子": "dice",
"丢骰子挑战": "dice_challenge",
"steam": "steam_friends",
}
class KernelRoutingTests(unittest.TestCase):
def test_longest_command_prefix_wins(self):
self.assertEqual(
parse_local_command("/丢骰子挑战5d20", ALIASES),
("dice_challenge", "5d20"),
)
def test_unknown_slash_command_stays_local(self):
decision = classify_question("/unknown", ALIASES)
self.assertEqual(decision.route, RouteKind.LOCAL)
self.assertEqual(
decision.execution_path,
ExecutionPath.LOCAL_COMMAND,
)
self.assertEqual(decision.reason, "local_unknown")
def test_hash_request_routes_to_cloud(self):
decision = classify_question("#status", ALIASES)
self.assertEqual(decision.route, RouteKind.CLOUD)
self.assertEqual(decision.execution_path, ExecutionPath.OPENCLAW)
self.assertEqual(decision.reason, "hash_command")
def test_ordinary_chat_routes_direct(self):
decision = classify_question("hello", ALIASES)
self.assertEqual(decision.route, RouteKind.CHAT)
self.assertEqual(decision.execution_path, ExecutionPath.DEEPSEEK)
self.assertEqual(decision.reason, "direct_chat")
def test_command_arguments_are_preserved(self):
decision = classify_question("/steam 在线", ALIASES)
self.assertEqual(decision.command_kind, "steam_friends")
self.assertEqual(decision.arguments, "在线")
if __name__ == "__main__":
unittest.main()
+510
View File
@@ -0,0 +1,510 @@
#!/usr/bin/env python3
import base64
import json
import subprocess
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"
sys.path[:0] = [str(SDK_SRC), str(KERNEL_SRC)]
from jwebot_kernel import ( # noqa: E402
ActivationError,
ReleaseError,
SignatureError,
build_release,
install_release,
parse_manifest,
rollback_release,
sign_release_record,
verify_installed_release_signature,
verify_release_signature,
)
SSH_KEYGEN = Path("/usr/bin/ssh-keygen")
PUBLISHER = "test-publisher"
def manifest():
return parse_manifest(
{
"id": "hello-command",
"author": "tester",
"description": "Say hello",
"help": ["/hello"],
"version": "1.0.0",
"api_version": "1",
"entrypoint": "hello_command:plugin",
"type": "command",
"capabilities": {
"commands": ["/hello"],
"storage": False,
"network_profiles": [],
},
}
)
def extension(root: Path, reply: str = "hello") -> Path:
root.mkdir()
(root / "src").mkdir()
(root / "plugin.toml").write_text(
'id = "hello-command"\n',
encoding="utf-8",
)
(root / "src" / "hello_command.py").write_text(
"\n".join(
(
"from jwebot_sdk import ExtensionResult, ReplyPlan",
"",
"def handle(event, arguments):",
f" return ExtensionResult(reply=ReplyPlan.text({reply!r}))",
)
),
encoding="utf-8",
)
return root
def publisher_key(root: Path, publisher: str = PUBLISHER):
root.mkdir(parents=True, exist_ok=True)
private_key = root / "publisher"
result = subprocess.run(
[
str(SSH_KEYGEN),
"-q",
"-t",
"ed25519",
"-N",
"",
"-f",
str(private_key),
],
capture_output=True,
check=False,
)
if result.returncode != 0:
raise RuntimeError("cannot generate test publisher key")
allowed_signers = root / "allowed_signers"
public_key = private_key.with_suffix(".pub").read_text(
encoding="utf-8"
).strip()
allowed_signers.write_text(
f"{publisher} {public_key}\n",
encoding="utf-8",
)
return private_key, allowed_signers
@unittest.skipUnless(
SSH_KEYGEN.is_file(),
"/usr/bin/ssh-keygen is unavailable",
)
class ReleaseSigningTests(unittest.TestCase):
def build(
self,
base: Path,
name: str = "extension",
reply: str = "hello",
):
return build_release(
extension(base / name, reply),
base / f"{name}-dist",
manifest=manifest(),
)
def test_valid_signature_is_deterministic_and_verifies(self):
with tempfile.TemporaryDirectory() as directory:
base = Path(directory)
_, record_path, _ = self.build(base)
private_key, allowed_signers = publisher_key(base)
first_path, first = sign_release_record(
record_path,
private_key,
PUBLISHER,
)
first_bytes = first_path.read_bytes()
second_path, second = sign_release_record(
record_path,
private_key,
PUBLISHER,
)
self.assertEqual(first, second)
self.assertEqual(first_bytes, second_path.read_bytes())
verified = verify_release_signature(
record_path,
first_path,
allowed_signers,
expected_publisher=PUBLISHER,
)
self.assertEqual(verified.publisher, PUBLISHER)
def test_signature_rejects_release_record_tampering(self):
with tempfile.TemporaryDirectory() as directory:
base = Path(directory)
_, record_path, _ = self.build(base)
private_key, allowed_signers = publisher_key(base)
signature_path, _ = sign_release_record(
record_path,
private_key,
PUBLISHER,
)
value = json.loads(record_path.read_text(encoding="utf-8"))
value["artifact_size"] += 1
record_path.write_text(
json.dumps(
value,
ensure_ascii=False,
sort_keys=True,
separators=(",", ":"),
)
+ "\n",
encoding="utf-8",
)
with self.assertRaisesRegex(
SignatureError,
"signed release digest mismatch",
):
verify_release_signature(
record_path,
signature_path,
allowed_signers,
)
def test_signing_rejects_tampered_artifact(self):
with tempfile.TemporaryDirectory() as directory:
base = Path(directory)
artifact, record_path, _ = self.build(base)
private_key, _ = publisher_key(base)
artifact.write_bytes(artifact.read_bytes() + b"tampered")
with self.assertRaisesRegex(ReleaseError, "size mismatch"):
sign_release_record(
record_path,
private_key,
PUBLISHER,
)
def test_signature_rejects_cryptographic_tampering(self):
with tempfile.TemporaryDirectory() as directory:
base = Path(directory)
_, record_path, _ = self.build(base)
private_key, allowed_signers = publisher_key(base)
signature_path, _ = sign_release_record(
record_path,
private_key,
PUBLISHER,
)
value = json.loads(signature_path.read_text(encoding="utf-8"))
raw = bytearray(base64.b64decode(value["signature_base64"]))
raw[len(raw) // 2] ^= 0x01
value["signature_base64"] = base64.b64encode(raw).decode("ascii")
signature_path.write_text(
json.dumps(
value,
ensure_ascii=False,
sort_keys=True,
separators=(",", ":"),
)
+ "\n",
encoding="utf-8",
)
with self.assertRaisesRegex(
SignatureError,
"signature verification failed",
):
verify_release_signature(
record_path,
signature_path,
allowed_signers,
)
def test_signature_rejects_untrusted_publisher_key(self):
with tempfile.TemporaryDirectory() as directory:
base = Path(directory)
_, record_path, _ = self.build(base)
private_key, _ = publisher_key(base / "trusted")
untrusted_root = base / "untrusted"
untrusted_root.mkdir()
_, wrong_allowed_signers = publisher_key(untrusted_root)
signature_path, _ = sign_release_record(
record_path,
private_key,
PUBLISHER,
)
with self.assertRaisesRegex(
SignatureError,
"signature verification failed",
):
verify_release_signature(
record_path,
signature_path,
wrong_allowed_signers,
)
def test_trust_file_rejects_non_ed25519_entries(self):
with tempfile.TemporaryDirectory() as directory:
base = Path(directory)
_, record_path, _ = self.build(base)
private_key, allowed_signers = publisher_key(base)
signature_path, _ = sign_release_record(
record_path,
private_key,
PUBLISHER,
)
allowed_signers.write_text(
allowed_signers.read_text(encoding="utf-8").replace(
"ssh-ed25519",
"ssh-rsa",
),
encoding="utf-8",
)
with self.assertRaisesRegex(SignatureError, "simple Ed25519"):
verify_release_signature(
record_path,
signature_path,
allowed_signers,
)
def test_signature_binds_publisher_even_when_key_has_two_principals(self):
with tempfile.TemporaryDirectory() as directory:
base = Path(directory)
_, record_path, _ = self.build(base)
private_key, allowed_signers = publisher_key(base)
public_key = private_key.with_suffix(".pub").read_text(
encoding="utf-8"
).strip()
with allowed_signers.open("a", encoding="utf-8") as stream:
stream.write(f"other-publisher {public_key}\n")
signature_path, _ = sign_release_record(
record_path,
private_key,
PUBLISHER,
)
value = json.loads(signature_path.read_text(encoding="utf-8"))
value["publisher"] = "other-publisher"
signature_path.write_text(
json.dumps(
value,
ensure_ascii=False,
sort_keys=True,
separators=(",", ":"),
)
+ "\n",
encoding="utf-8",
)
with self.assertRaisesRegex(
SignatureError,
"signature verification failed",
):
verify_release_signature(
record_path,
signature_path,
allowed_signers,
)
def test_signing_rejects_noncanonical_release_record(self):
with tempfile.TemporaryDirectory() as directory:
base = Path(directory)
_, record_path, _ = self.build(base)
private_key, _ = publisher_key(base)
record_path.write_bytes(b" " + record_path.read_bytes())
with self.assertRaisesRegex(SignatureError, "not canonical"):
sign_release_record(
record_path,
private_key,
PUBLISHER,
)
def test_signed_activation_persists_and_reverifies_signature(self):
with tempfile.TemporaryDirectory() as directory:
base = Path(directory)
artifact, record_path, _ = self.build(base)
private_key, allowed_signers = publisher_key(base)
signature_path, _ = sign_release_record(
record_path,
private_key,
PUBLISHER,
)
release_dir = install_release(
artifact,
record_path,
base / "state",
signature=signature_path,
allowed_signers=allowed_signers,
expected_publisher=PUBLISHER,
health_check=lambda release: None,
)
self.assertTrue((release_dir / ".release.sig.json").is_file())
record, signature = verify_installed_release_signature(
release_dir,
allowed_signers,
expected_publisher=PUBLISHER,
)
self.assertEqual(record.plugin_id, "hello-command")
self.assertEqual(signature.publisher, PUBLISHER)
def test_signed_activation_requires_passing_health_check(self):
with tempfile.TemporaryDirectory() as directory:
base = Path(directory)
artifact, record_path, _ = self.build(base)
private_key, allowed_signers = publisher_key(base)
signature_path, _ = sign_release_record(
record_path,
private_key,
PUBLISHER,
)
state = base / "state"
with self.assertRaisesRegex(
ActivationError,
"health check is required",
):
install_release(
artifact,
record_path,
state,
signature=signature_path,
allowed_signers=allowed_signers,
expected_publisher=PUBLISHER,
)
self.assertFalse(state.exists())
def test_failed_health_check_does_not_switch_current(self):
with tempfile.TemporaryDirectory() as directory:
base = Path(directory)
artifact, record_path, record = self.build(base)
private_key, allowed_signers = publisher_key(base)
signature_path, _ = sign_release_record(
record_path,
private_key,
PUBLISHER,
)
state = base / "state"
with self.assertRaisesRegex(
ActivationError,
"health check failed",
):
install_release(
artifact,
record_path,
state,
signature=signature_path,
allowed_signers=allowed_signers,
expected_publisher=PUBLISHER,
health_check=lambda release: (_ for _ in ()).throw(
RuntimeError("unhealthy")
),
)
self.assertFalse((state / "current").exists())
self.assertFalse(
(state / "releases" / record.source_digest).exists()
)
def test_signed_activation_rejects_wrong_publisher_policy(self):
with tempfile.TemporaryDirectory() as directory:
base = Path(directory)
artifact, record_path, _ = self.build(base)
private_key, allowed_signers = publisher_key(base)
signature_path, _ = sign_release_record(
record_path,
private_key,
PUBLISHER,
)
state = base / "state"
with self.assertRaisesRegex(
SignatureError,
"publisher does not match policy",
):
install_release(
artifact,
record_path,
state,
signature=signature_path,
allowed_signers=allowed_signers,
expected_publisher="other-publisher",
)
self.assertFalse(state.exists())
def test_signed_rollback_reverifies_previous_release(self):
with tempfile.TemporaryDirectory() as directory:
base = Path(directory)
private_key, allowed_signers = publisher_key(base)
first_artifact, first_record_path, first_record = self.build(
base,
"first",
"one",
)
first_signature, _ = sign_release_record(
first_record_path,
private_key,
PUBLISHER,
)
second_artifact, second_record_path, second_record = self.build(
base,
"second",
"two",
)
second_signature, _ = sign_release_record(
second_record_path,
private_key,
PUBLISHER,
)
state = base / "state"
install_release(
first_artifact,
first_record_path,
state,
signature=first_signature,
allowed_signers=allowed_signers,
expected_publisher=PUBLISHER,
health_check=lambda release: None,
)
install_release(
second_artifact,
second_record_path,
state,
signature=second_signature,
allowed_signers=allowed_signers,
expected_publisher=PUBLISHER,
health_check=lambda release: None,
)
destination = rollback_release(
state,
allowed_signers=allowed_signers,
expected_publisher=PUBLISHER,
)
self.assertEqual(destination.name, first_record.source_digest)
self.assertEqual(
(state / "current").resolve().name,
first_record.source_digest,
)
self.assertEqual(
(state / "previous").resolve().name,
second_record.source_digest,
)
if __name__ == "__main__":
unittest.main()
+106
View File
@@ -0,0 +1,106 @@
#!/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"
sys.path[:0] = [str(SDK_SRC), str(KERNEL_SRC)]
from jwebot_kernel import ( # noqa: E402
SourcePolicyError,
parse_manifest,
validate_source_tree,
)
def manifest():
return parse_manifest(
{
"id": "test-command",
"author": "tester",
"description": "Run a test command",
"help": ["/test"],
"version": "1.0.0",
"api_version": "1",
"entrypoint": "test_command:plugin",
"type": "command",
"capabilities": {
"commands": ["/test"],
"storage": False,
"network_profiles": [],
},
}
)
class SourcePolicyTests(unittest.TestCase):
def validate(self, source: str):
temporary = tempfile.TemporaryDirectory()
self.addCleanup(temporary.cleanup)
root = Path(temporary.name)
source_root = root / "src"
source_root.mkdir()
(source_root / "test_command.py").write_text(source, encoding="utf-8")
return validate_source_tree(root, manifest())
def test_sdk_and_pure_stdlib_imports_are_allowed(self):
paths = self.validate(
"\n".join(
(
"from dataclasses import dataclass",
"from jwebot_sdk import ReplyPlan",
"",
"@dataclass",
"class Value:",
" number: int",
"",
"def reply():",
" return ReplyPlan.text(str(Value(1).number))",
)
)
)
self.assertEqual(len(paths), 1)
def test_subprocess_import_is_rejected(self):
with self.assertRaisesRegex(SourcePolicyError, "JWP001"):
self.validate("import subprocess\n")
def test_socket_import_is_rejected(self):
with self.assertRaisesRegex(SourcePolicyError, "JWP001"):
self.validate("import socket\n")
def test_open_is_rejected(self):
with self.assertRaisesRegex(SourcePolicyError, "JWP002"):
self.validate('open("/tmp/value")\n')
def test_dynamic_import_is_rejected(self):
with self.assertRaisesRegex(SourcePolicyError, "JWP002"):
self.validate('__import__("os")\n')
def test_dunder_attribute_access_is_rejected(self):
with self.assertRaisesRegex(SourcePolicyError, "JWP003"):
self.validate("value.__class__\n")
def test_relative_local_import_is_allowed(self):
paths = self.validate("from .helper import value\n")
self.assertEqual(len(paths), 1)
def test_symlinked_source_is_rejected(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
source_root = root / "src"
source_root.mkdir()
target = root / "outside.py"
target.write_text("value = 1\n", encoding="utf-8")
(source_root / "test_command.py").symlink_to(target)
with self.assertRaisesRegex(SourcePolicyError, "JWP006"):
validate_source_tree(root, manifest())
if __name__ == "__main__":
unittest.main()
+102
View File
@@ -0,0 +1,102 @@
#!/usr/bin/env python3
import sys
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"
sys.path[:0] = [str(SDK_SRC), str(KERNEL_SRC)]
from jwebot_kernel.telemetry import RuntimeTelemetry # noqa: E402
class TelemetryTests(unittest.TestCase):
def test_event_hashes_identifiers_and_drops_unknown_fields(self):
observed = []
logger = mock.Mock()
telemetry = RuntimeTelemetry(
component="edge",
observer=observed.append,
logger=logger,
)
value = telemetry.emit(
"edge.route.classified",
event_id="private-event-id",
group_id="private-group",
route="local",
reason="local_coin",
component="attacker",
message_body="must-not-appear",
)
self.assertNotIn("private-event-id", str(dict(value)))
self.assertNotIn("private-group", str(dict(value)))
self.assertNotIn("message_body", value)
self.assertEqual(value["route"], "local")
self.assertEqual(value["component"], "edge")
self.assertEqual(len(observed), 1)
logger.info.assert_called_once()
def test_elapsed_updates_low_cardinality_snapshot(self):
values = iter((10.0, 10.025))
telemetry = RuntimeTelemetry(
component="host-client",
logger=mock.Mock(),
monotonic=lambda: next(values),
)
started = telemetry.monotonic()
telemetry.elapsed(
"host.invoke",
started,
status="ok",
plugin_id="coin",
)
snapshot = telemetry.snapshot()
self.assertEqual(snapshot["counts"]["host.invoke:ok"], 1)
self.assertEqual(
snapshot["latencies"]["host.invoke"]["average_ms"],
25.0,
)
def test_span_records_error_type_without_swallowing(self):
observed = []
values = iter((1.0, 1.01))
telemetry = RuntimeTelemetry(
component="edge",
observer=observed.append,
logger=mock.Mock(),
monotonic=lambda: next(values),
)
with self.assertRaisesRegex(RuntimeError, "failed"):
with telemetry.span("edge.local.execute", route="local"):
raise RuntimeError("failed")
self.assertEqual(observed[0]["status"], "error")
self.assertEqual(observed[0]["error_type"], "RuntimeError")
self.assertNotIn("failed", str(dict(observed[0])))
def test_disabled_telemetry_has_no_side_effects(self):
observer = mock.Mock()
logger = mock.Mock()
telemetry = RuntimeTelemetry(
component="edge",
enabled=False,
observer=observer,
logger=logger,
)
self.assertEqual(dict(telemetry.emit("edge.test")), {})
observer.assert_not_called()
logger.info.assert_not_called()
self.assertEqual(telemetry.snapshot()["counts"], {})
if __name__ == "__main__":
unittest.main()
+193
View File
@@ -0,0 +1,193 @@
#!/usr/bin/env python3
import tempfile
import unittest
from pathlib import Path
from unittest import mock
from tests.test_jwebot_edge import MODULE, config, event
class LocalCommandParityTests(unittest.TestCase):
def setUp(self):
self.enabled_temp = tempfile.TemporaryDirectory()
self.compat_temp = tempfile.TemporaryDirectory()
self.enabled = MODULE.EdgeApplication(
config(
Path(self.enabled_temp.name),
local_extensions_enabled=True,
)
)
self.compat = MODULE.EdgeApplication(
config(
Path(self.compat_temp.name),
local_extensions_enabled=False,
)
)
def tearDown(self):
self.enabled.close()
self.compat.close()
self.enabled_temp.cleanup()
self.compat_temp.cleanup()
def assert_command_equal(
self,
event_id: str,
text: str,
*,
sender: str = "Alice",
):
incoming = event(
event_id,
text,
sender=sender,
)
enabled = self.enabled.accept(dict(incoming))
compat = self.compat.accept(dict(incoming))
self.assertEqual(enabled["route"], compat["route"])
self.assertEqual(enabled["text"], compat["text"])
return enabled, compat
def test_stateless_command_families_match(self):
commands = (
("help", "@JWeBot /功能列表"),
("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 commands:
with self.subTest(command=text):
self.assert_command_equal(event_id, text)
def test_memory_subcommands_match(self):
commands = (
("remember", "@JWeBot /记住 我喜欢家庭服务器"),
("my-memory", "@JWeBot /我的记忆"),
("person-memory", "@JWeBot /人物记忆 Alice"),
("forget-keyword", "@JWeBot /忘记 家庭服务器"),
("remember-again", "@JWeBot /记住 我喜欢技术细节"),
("forget-all", "@JWeBot /忘记 全部"),
)
for event_id, text in commands:
with self.subTest(command=text):
self.assert_command_equal(event_id, text)
def test_dice_challenge_subcommands_match(self):
commands = (
("challenge-start", "@JWeBot /丢骰子挑战 开始 5d20", "Alice"),
("challenge-alice", "@JWeBot /丢骰子挑战", "Alice"),
("challenge-gb", "@JWeBot /丢骰子挑战", "Bob"),
("challenge-state", "@JWeBot /丢骰子挑战 状态", "Alice"),
("challenge-settle", "@JWeBot /丢骰子挑战 结算", "Alice"),
("challenge-record", "@JWeBot /丢骰子挑战 战绩", "Alice"),
)
for event_id, text, sender in commands:
with self.subTest(command=text, sender=sender):
self.assert_command_equal(event_id, text, sender=sender)
def test_guess_number_subcommands_match(self):
self.assert_command_equal(
"guess-start",
"@JWeBot /猜数字 开始 50",
)
enabled_state = self.enabled.ledger.local_game(
"group-a",
"guess_number",
)
compat_state = self.compat.ledger.local_game(
"group-a",
"guess_number",
)
self.assertEqual(enabled_state["target"], compat_state["target"])
target = enabled_state["target"]
self.assert_command_equal(
"guess-status",
"@JWeBot /猜数字 状态",
)
self.assert_command_equal(
"guess-win",
f"@JWeBot /猜 {target}",
)
def test_rps_subcommands_match(self):
commands = (
("rps-start", "@JWeBot /石头剪刀布 开始", "Alice"),
("rps-join", "@JWeBot /石头剪刀布 加入", "Bob"),
("rps-first", "@JWeBot /石头剪刀布 出 石头", "Alice"),
("rps-status", "@JWeBot /石头剪刀布 状态", "Alice"),
("rps-second", "@JWeBot /石头剪刀布 出 剪刀", "Bob"),
("rps-end", "@JWeBot /石头剪刀布 结束", "Alice"),
)
for event_id, text, sender in commands:
with self.subTest(command=text, sender=sender):
self.assert_command_equal(event_id, text, sender=sender)
def test_status_balance_and_steam_match(self):
balance = [
{
"currency": "CNY",
"total_balance": "10.00",
"granted_balance": "2.00",
"topped_up_balance": "8.00",
}
]
for application in (self.enabled, self.compat):
application.local.steam = mock.Mock()
application.local.steam.render.return_value = (
"Steam:yr 正在玩 Dota 2"
)
with mock.patch.object(
MODULE.LocalCommandEngine,
"service_state",
return_value="active",
), mock.patch.object(
MODULE.DeepSeekClient,
"balance",
return_value=balance,
):
self.assert_command_equal("status", "@JWeBot /状态")
self.assert_command_equal("server-status", "@JWeBot /status")
self.assert_command_equal("jwebot-version", "@JWeBot /version")
self.assert_command_equal("debug-telemetry", "@JWeBot /debug")
self.assert_command_equal("balance", "@JWeBot /余额")
self.assert_command_equal("steam", "@JWeBot /steam")
enabled_row = self.enabled.ledger.row("steam")
compat_row = self.compat.ledger.row("steam")
self.assertEqual(
self.enabled.generate_steam_reply(enabled_row),
self.compat.generate_steam_reply(compat_row),
)
def test_extension_game_state_survives_application_restart(self):
response = self.enabled.accept(
event("restart-game", "@JWeBot /猜数字 开始 30")
)
self.assertIn("猜数字开局", response["text"])
expected = self.enabled.ledger.local_game(
"group-a",
"guess_number",
)
self.enabled.close()
self.enabled = MODULE.EdgeApplication(
config(
Path(self.enabled_temp.name),
local_extensions_enabled=True,
)
)
restored = self.enabled.ledger.local_game(
"group-a",
"guess_number",
)
self.assertEqual(restored["target"], expected["target"])
self.assertEqual(restored["upper"], 30)
if __name__ == "__main__":
unittest.main()
+136
View File
@@ -0,0 +1,136 @@
#!/usr/bin/env python3
import sys
import unittest
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parents[1]
OFFICIAL_SRC = (
PROJECT_ROOT
/ "packages"
/ "jwebot-official-extensions"
/ "src"
)
SDK_SRC = PROJECT_ROOT / "packages" / "jwebot-sdk" / "src"
sys.path[:0] = [str(SDK_SRC), str(OFFICIAL_SRC)]
from jwebot_official_extensions import ( # noqa: E402
DIRECT_KERNEL_COMMAND_KINDS,
JWEBOT_AUTHOR,
LOCAL_COMMAND_ALIASES,
LOCAL_EXTENSIONS,
OFFICIAL_AUTHOR,
extension_for_kind,
render_feature_list,
)
class LocalExtensionInventoryTests(unittest.TestCase):
def test_every_command_kind_is_direct_or_has_exactly_one_owner(self):
expected = set(LOCAL_COMMAND_ALIASES.values())
owned = [
kind
for extension in LOCAL_EXTENSIONS
for kind in extension.command_kinds
]
self.assertEqual(
set(owned) | set(DIRECT_KERNEL_COMMAND_KINDS),
expected,
)
self.assertEqual(len(owned), len(set(owned)))
self.assertFalse(set(owned) & set(DIRECT_KERNEL_COMMAND_KINDS))
def test_every_alias_is_exposed_by_its_owner(self):
for alias, kind in LOCAL_COMMAND_ALIASES.items():
with self.subTest(alias=alias, kind=kind):
owner = extension_for_kind(kind)
if kind in DIRECT_KERNEL_COMMAND_KINDS:
self.assertIsNone(owner)
continue
self.assertIsNotNone(owner)
assert owner is not None
self.assertIn(f"/{alias}", owner.commands)
def test_extension_ids_and_commands_do_not_collide(self):
ids = [extension.extension_id for extension in LOCAL_EXTENSIONS]
commands = [
command.casefold()
for extension in LOCAL_EXTENSIONS
for command in extension.commands
]
self.assertEqual(len(ids), len(set(ids)))
self.assertEqual(len(commands), len(set(commands)))
def test_help_is_generated_from_every_plugin_description(self):
help_text = render_feature_list()
self.assertLessEqual(len(help_text), 900)
for extension in LOCAL_EXTENSIONS:
with self.subTest(extension=extension.extension_id):
self.assertTrue(extension.description)
self.assertTrue(extension.help_entries)
self.assertIn(extension.extension_id, help_text)
self.assertIn(extension.description, help_text)
for entry in extension.help_entries:
self.assertIn(entry, help_text)
def test_expected_granular_plugins_and_authors_are_present(self):
self.assertEqual(
{
extension.extension_id: extension.author
for extension in LOCAL_EXTENSIONS
},
{
"person-memory": OFFICIAL_AUTHOR,
"server-status": OFFICIAL_AUTHOR,
"jwebot-version": OFFICIAL_AUTHOR,
"debug-telemetry": OFFICIAL_AUTHOR,
"dice": JWEBOT_AUTHOR,
"coin": JWEBOT_AUTHOR,
"random-number": JWEBOT_AUTHOR,
"draw-lot": JWEBOT_AUTHOR,
"daily-fortune": JWEBOT_AUTHOR,
"guess-number": JWEBOT_AUTHOR,
"rock-paper-scissors": JWEBOT_AUTHOR,
"scan-ranking": JWEBOT_AUTHOR,
"repeat-ranking": JWEBOT_AUTHOR,
"history-search": JWEBOT_AUTHOR,
"steam-presence": JWEBOT_AUTHOR,
},
)
def test_only_approved_collections_combine_function_kinds(self):
combined = {
extension.extension_id: extension.command_kinds
for extension in LOCAL_EXTENSIONS
if len(extension.command_kinds) > 1
}
self.assertEqual(
combined,
{
"person-memory": (
"remember",
"my_memory",
"forget_memory",
"person_memory",
"memory_help",
),
"server-status": ("local_status", "server_status"),
"dice": ("dice", "dice_challenge"),
},
)
def test_kernel_direct_commands_are_not_plugins(self):
self.assertEqual(
DIRECT_KERNEL_COMMAND_KINDS,
frozenset(
{
"feature_list",
"deepseek_balance",
}
),
)
if __name__ == "__main__":
unittest.main()
+52
View File
@@ -0,0 +1,52 @@
#!/usr/bin/env python3
import sys
import unittest
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parents[1]
SDK_SRC = PROJECT_ROOT / "packages" / "jwebot-sdk" / "src"
OFFICIAL_SRC = (
PROJECT_ROOT
/ "packages"
/ "jwebot-official-extensions"
/ "src"
)
sys.path[:0] = [str(SDK_SRC), str(OFFICIAL_SRC)]
from jwebot_official_extensions.local_commands import ( # noqa: E402
LocalCommandEngine,
)
class TelemetryDebugTests(unittest.TestCase):
def test_parses_journal_lines_and_renders_p50_p95(self):
text = "\n".join(
[
'telemetry {"name":"gui.send.total","status":"ok",'
'"elapsed_ms":100}',
'telemetry {"name":"gui.send.total","status":"ok",'
'"elapsed_ms":200}',
'telemetry {"name":"gui.send.total","status":"ok",'
'"elapsed_ms":300}',
'telemetry {"name":"provider.direct","status":"error",'
'"elapsed_ms":9999}',
'not telemetry',
]
)
rows = LocalCommandEngine.telemetry_rows_from_text(text)
rendered = LocalCommandEngine.render_telemetry_summary(rows)
self.assertEqual(len(rows), 4)
self.assertIn("调试埋点 p50/p95", rendered)
self.assertIn("gui.send.total", rendered)
self.assertIn("n=3", rendered)
self.assertIn("p50=200.0ms", rendered)
self.assertIn("p95=290.0ms", rendered)
self.assertNotIn("9999", rendered)
if __name__ == "__main__":
unittest.main()
+153
View File
@@ -0,0 +1,153 @@
#!/usr/bin/env python3
import importlib.util
import sys
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"
OFFICIAL_SRC = (
PROJECT_ROOT
/ "packages"
/ "jwebot-official-extensions"
/ "src"
)
sys.path[:0] = [str(SDK_SRC), str(KERNEL_SRC), str(OFFICIAL_SRC)]
from jwebot_kernel import parse_manifest, validate_source_tree # noqa: E402
from jwebot_official_extensions import LOCAL_EXTENSIONS # noqa: E402
from jwebot_sdk import Event, PlanKind # noqa: E402
class _Registry:
def __init__(self):
self.handlers = {}
def register(self, command, handler):
if command in self.handlers:
raise AssertionError(f"duplicate command: {command}")
self.handlers[command] = handler
class _Services:
def __init__(self):
self.calls = []
def call(self, profile, *, operation, payload):
self.calls.append((profile, operation, payload))
return {"text": f"{profile}:{payload['command']}"}
class _Context:
def __init__(self):
self.commands = _Registry()
self.services = _Services()
class LocalPluginPackageTests(unittest.TestCase):
def event(self):
return Event(
event_id="event-1",
observed_at="2026-10-14T10:00:00+08:00",
sender="Alice",
text="@JWeBot test",
group_fingerprint="group-a",
mention_bot=True,
)
def test_every_local_package_validates_registers_and_invokes(self):
for descriptor in LOCAL_EXTENSIONS:
with self.subTest(extension=descriptor.extension_id):
module_name = descriptor.extension_id.replace("-", "_")
root = (
PROJECT_ROOT
/ "extensions"
/ descriptor.directory
/ descriptor.extension_id
)
manifest = parse_manifest(
{
"id": descriptor.extension_id,
"author": descriptor.author,
"description": descriptor.description,
"help": list(descriptor.help_entries),
"version": "1.0.0",
"api_version": "1",
"entrypoint": f"{module_name}:plugin",
"type": "command",
"capabilities": {
"commands": list(descriptor.commands),
"storage": False,
"network_profiles": [],
"service_profiles": [descriptor.extension_id],
},
}
)
self.assertEqual(manifest.plugin_id, descriptor.extension_id)
self.assertEqual(manifest.author, descriptor.author)
self.assertEqual(
manifest.description,
descriptor.description,
)
self.assertEqual(
manifest.help_entries,
descriptor.help_entries,
)
self.assertEqual(
manifest.capabilities.commands,
descriptor.commands,
)
self.assertEqual(
manifest.capabilities.service_profiles,
(descriptor.extension_id,),
)
validate_source_tree(root, manifest)
spec = importlib.util.spec_from_file_location(
f"official_{module_name}",
root / "src" / f"{module_name}.py",
)
module = importlib.util.module_from_spec(spec)
assert spec.loader is not None
spec.loader.exec_module(module)
context = _Context()
module.plugin(context)
self.assertEqual(
set(context.commands.handlers),
set(descriptor.commands),
)
first = descriptor.commands[0]
result = context.commands.handlers[first](
self.event(),
"argument",
)
self.assertEqual(result.reply.kind, PlanKind.TEXT)
self.assertEqual(
context.services.calls[0][0],
descriptor.extension_id,
)
self.assertEqual(
context.services.calls[0][2]["command"],
first,
)
self.assertEqual(
context.services.calls[0][2]["arguments"],
"argument",
)
def test_generated_local_packages_are_in_sync(self):
spec = importlib.util.spec_from_file_location(
"sync_local_extensions",
PROJECT_ROOT / "tools" / "sync-local-extensions.py",
)
module = importlib.util.module_from_spec(spec)
assert spec.loader is not None
spec.loader.exec_module(module)
self.assertEqual(module.synchronize(check=True), [])
if __name__ == "__main__":
unittest.main()
+87
View File
@@ -0,0 +1,87 @@
#!/usr/bin/env python3
import importlib.util
import sys
import unittest
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parents[1]
MODULE_PATH = PROJECT_ROOT / "tools" / "performance-baseline.py"
SPEC = importlib.util.spec_from_file_location(
"performance_baseline",
MODULE_PATH,
)
MODULE = importlib.util.module_from_spec(SPEC)
assert SPEC.loader is not None
sys.modules[SPEC.name] = MODULE
SPEC.loader.exec_module(MODULE)
class StatisticsTests(unittest.TestCase):
def test_percentile_interpolates_sorted_values(self):
self.assertEqual(MODULE.percentile([40, 10, 30, 20], 50), 25)
self.assertEqual(MODULE.percentile([10, 20], 95), 19.5)
def test_percentile_rejects_empty_samples(self):
with self.assertRaisesRegex(ValueError, "at least one"):
MODULE.percentile([], 50)
def test_summary_exposes_required_latency_percentiles(self):
value = MODULE.summarize_ms([1.0, 2.0, 3.0, 20.0])
self.assertEqual(value["samples"], 4)
self.assertEqual(value["p50_ms"], 2.5)
self.assertEqual(value["max_ms"], 20.0)
class TelemetryAnalysisTests(unittest.TestCase):
def test_prefixed_telemetry_line_is_parsed(self):
value = MODULE.parse_telemetry_line(
'2026-10-14 INFO telemetry '
'{"name":"ocr.scan","status":"ok","elapsed_ms":12.5}'
)
self.assertEqual(value["name"], "ocr.scan")
self.assertEqual(value["elapsed_ms"], 12.5)
def test_unrelated_or_invalid_lines_are_ignored(self):
self.assertIsNone(MODULE.parse_telemetry_line("ordinary log line"))
self.assertIsNone(MODULE.parse_telemetry_line("telemetry not-json"))
self.assertIsNone(MODULE.parse_telemetry_line('{"elapsed_ms":12}'))
def test_summary_ranks_stages_by_p95_and_counts_statuses(self):
records = [
{
"name": "edge.ack.prepared",
"status": "ok",
"elapsed_ms": 2.0,
"event_hash": "event-a",
},
{
"name": "edge.ack.prepared",
"status": "ok",
"elapsed_ms": 4.0,
"event_hash": "event-b",
},
{
"name": "ocr.scan",
"status": "ok",
"elapsed_ms": 5000.0,
},
{
"name": "ocr.scan",
"status": "error",
"elapsed_ms": "private-invalid-value",
},
]
value = MODULE.summarize_telemetry(records)
self.assertEqual(value["records"], 4)
self.assertEqual(value["events"], 2)
self.assertEqual(value["stages"][0]["name"], "ocr.scan")
self.assertEqual(value["stages"][1]["name"], "edge.ack.prepared")
self.assertEqual(value["statuses"]["ocr.scan:error"], 1)
if __name__ == "__main__":
unittest.main()
+102
View File
@@ -0,0 +1,102 @@
#!/usr/bin/env python3
import sys
import unittest
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parents[1]
SDK_SRC = PROJECT_ROOT / "packages" / "jwebot-sdk" / "src"
sys.path.insert(0, str(SDK_SRC))
from jwebot_sdk import ( # noqa: E402
ContractViolation,
Event,
ExtensionResult,
Fact,
PlanKind,
ReplyPlan,
)
class EventContractTests(unittest.TestCase):
def event(self, **overrides):
value = {
"version": 1,
"event_id": "event-1",
"observed_at": "2026-10-14T10:00:00+08:00",
"sender": "Alice",
"text": "@JWeBot hello",
"confidence": 0.95,
"sender_confidence": 0.96,
"body_confidence": 0.97,
"side": "incoming",
"mention_bot": True,
"group_fingerprint": "group-a",
"source": "rapidocr-local",
}
value.update(overrides)
return value
def test_event_round_trip_preserves_normalized_fields(self):
event = Event.from_mapping(self.event())
self.assertEqual(event.to_mapping(), self.event())
def test_event_rejects_outgoing_messages(self):
with self.assertRaisesRegex(ContractViolation, "incoming"):
Event.from_mapping(self.event(side="outgoing"))
def test_event_rejects_out_of_range_confidence(self):
with self.assertRaisesRegex(ContractViolation, "between 0 and 1"):
Event.from_mapping(self.event(confidence=1.1))
def test_event_rejects_unsupported_version(self):
with self.assertRaisesRegex(ContractViolation, "unsupported event version"):
Event.from_mapping(self.event(version=2))
class PlanContractTests(unittest.TestCase):
def test_text_plan_has_a_bounded_serialized_shape(self):
plan = ReplyPlan.text("hello")
self.assertEqual(plan.kind, PlanKind.TEXT)
self.assertEqual(plan.to_mapping(), {"kind": "text", "text": "hello"})
def test_no_reply_plan_cannot_contain_text(self):
with self.assertRaisesRegex(ContractViolation, "cannot contain text"):
ReplyPlan(kind=PlanKind.NO_REPLY, message="unexpected")
def test_result_freezes_metadata_and_serializes_facts(self):
result = ExtensionResult(
reply=ReplyPlan.text("hello"),
facts=(Fact(kind="topic", text="likes servers", confidence=0.8),),
metadata={"source": "test"},
)
self.assertEqual(result.to_mapping()["metadata"], {"source": "test"})
with self.assertRaises(TypeError):
result.metadata["source"] = "changed" # type: ignore[index]
def test_result_rejects_unbounded_fact_count(self):
facts = tuple(Fact(kind="topic", text=f"fact-{index}") for index in range(13))
with self.assertRaisesRegex(ContractViolation, "exceeds 12 facts"):
ExtensionResult(facts=facts)
def test_result_round_trip_preserves_reply_facts_and_metadata(self):
result = ExtensionResult(
reply=ReplyPlan.text("hello"),
facts=(Fact(kind="topic", text="likes servers", confidence=0.8),),
metadata={"source": "test"},
)
restored = ExtensionResult.from_mapping(result.to_mapping())
self.assertEqual(restored.reply, result.reply)
self.assertEqual(restored.facts, result.facts)
self.assertEqual(dict(restored.metadata), dict(result.metadata))
def test_result_rejects_wrong_api_version(self):
value = ExtensionResult(reply=ReplyPlan.text("hello")).to_mapping()
value["api_version"] = "2"
with self.assertRaisesRegex(ContractViolation, "API version"):
ExtensionResult.from_mapping(value)
if __name__ == "__main__":
unittest.main()
+56
View File
@@ -0,0 +1,56 @@
#!/usr/bin/env python3
import importlib.util
import tempfile
import unittest
from pathlib import Path
from unittest import mock
PROJECT_ROOT = Path(__file__).resolve().parents[1]
MODULE_PATH = PROJECT_ROOT / "tools" / "check-secrets.py"
SPEC = importlib.util.spec_from_file_location("check_secrets", MODULE_PATH)
MODULE = importlib.util.module_from_spec(SPEC)
assert SPEC.loader is not None
SPEC.loader.exec_module(MODULE)
class SnapshotScannerTests(unittest.TestCase):
def test_snapshot_without_git_scans_regular_files(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
(root / "safe.txt").write_text("safe\n", encoding="utf-8")
(root / ".venv").mkdir()
(root / ".venv" / "ignored.txt").write_text(
"JWEBOT_EDGE_STEAM_API_KEY=not-for-scan\n",
encoding="utf-8",
)
with mock.patch.object(MODULE, "ROOT", root), mock.patch.object(
MODULE,
"has_git_repository",
return_value=False,
):
self.assertEqual(MODULE.tracked_paths(), ["safe.txt"])
self.assertEqual(MODULE.scan_tracked(), [])
def test_snapshot_scanner_reports_fingerprint_not_value(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
(root / "config.env").write_text(
"JWEBOT_EDGE_STEAM_API_KEY=local-sensitive-value\n",
encoding="utf-8",
)
with mock.patch.object(MODULE, "ROOT", root), mock.patch.object(
MODULE,
"has_git_repository",
return_value=False,
):
findings = MODULE.scan_tracked()
self.assertEqual(findings[0][0], "populated_secret")
self.assertEqual(findings[0][1], "config.env")
self.assertNotIn("local-sensitive-value", str(findings))
if __name__ == "__main__":
unittest.main()
+341
View File
@@ -0,0 +1,341 @@
#!/usr/bin/env python3
import importlib.util
import sys
import unittest
import tempfile
from pathlib import Path
from unittest import mock
from PIL import Image
PROJECT_ROOT = Path(__file__).resolve().parents[1]
MODULE_PATH = PROJECT_ROOT / "wechat_adaptive_gui_bridge.py"
SPEC = importlib.util.spec_from_file_location("wechat_adaptive_gui_bridge", MODULE_PATH)
MODULE = importlib.util.module_from_spec(SPEC)
assert SPEC.loader is not None
sys.modules[SPEC.name] = MODULE
SPEC.loader.exec_module(MODULE)
class CoordinateTests(unittest.TestCase):
def test_baseline_coordinates_are_unchanged(self):
self.assertEqual(
MODULE.scaled_point(
MODULE.BASE_DRAFT_POINT,
width=MODULE.BASE_WIDTH,
height=MODULE.BASE_HEIGHT,
),
MODULE.BASE_DRAFT_POINT,
)
def test_current_vnc_coordinates_are_scaled(self):
self.assertEqual(
MODULE.scaled_point(
MODULE.BASE_DRAFT_POINT,
width=1470,
height=694,
),
(450, 625),
)
self.assertEqual(
MODULE.scaled_point(
MODULE.BASE_SEND_POINT,
width=1470,
height=694,
),
(1419, 637),
)
self.assertEqual(
MODULE.scaled_box(
MODULE.BASE_INPUT_BOX,
width=1470,
height=694,
),
(306, 582, 1469, 693),
)
self.assertEqual(
MODULE.scaled_region(
MODULE.BASE_INPUT_TEXT_ROI,
width=1470,
height=694,
),
(327, 595, 653, 60),
)
self.assertEqual(MODULE.INPUT_TEXT_ROI_NAME, "input_text_core_v2")
class VncKeyboardTests(unittest.TestCase):
def test_vnc_key_event_has_the_expected_wire_format(self):
connection = mock.Mock()
MODULE.vnc_key_event(connection, down=True, keysym=ord("v"))
connection.sendall.assert_called_once_with(
MODULE.struct.pack(">BBHI", 4, 1, 0, ord("v"))
)
def test_x11_clipboard_rejects_a_failed_owner(self):
process = mock.Mock()
process.returncode = 1
with mock.patch.object(
MODULE.subprocess,
"Popen",
return_value=process,
):
with self.assertRaisesRegex(RuntimeError, "clipboard operation failed"):
MODULE.set_x_clipboard("测试")
def test_vnc_pointer_event_has_the_expected_wire_format(self):
connection = mock.Mock()
MODULE.vnc_pointer_event(connection, buttons=1, x=450, y=652)
connection.sendall.assert_called_once_with(
MODULE.struct.pack(">BBHH", 5, 1, 450, 652)
)
def test_vnc_select_all_and_clear_uses_vnc_key_events(self):
connection = mock.Mock()
MODULE.vnc_select_all_and_clear(connection)
payloads = [call.args[0] for call in connection.sendall.call_args_list]
self.assertEqual(
payloads,
[
MODULE.struct.pack(">BBHI", 4, 1, 0, 0xFFE3),
MODULE.struct.pack(">BBHI", 4, 1, 0, ord("a")),
MODULE.struct.pack(">BBHI", 4, 0, 0, ord("a")),
MODULE.struct.pack(">BBHI", 4, 0, 0, 0xFFE3),
MODULE.struct.pack(">BBHI", 4, 1, 0, 0xFF08),
MODULE.struct.pack(">BBHI", 4, 0, 0, 0xFF08),
],
)
def test_vnc_return_key_uses_the_standard_enter_keysym(self):
connection = mock.Mock()
MODULE.vnc_key_press(connection, 0xFF0D)
self.assertEqual(
[call.args[0] for call in connection.sendall.call_args_list],
[
MODULE.struct.pack(">BBHI", 4, 1, 0, 0xFF0D),
MODULE.struct.pack(">BBHI", 4, 0, 0, 0xFF0D),
],
)
class InputVerificationTests(unittest.TestCase):
def test_changed_pixels_ignores_a_small_cursor_change(self):
before = Image.new("RGB", (20, 20), "white")
after = before.copy()
after.putpixel((1, 1), (0, 0, 0))
self.assertEqual(MODULE.changed_pixels(before, after), 1)
def test_changed_pixels_detects_pasted_text_region(self):
before = Image.new("RGB", (20, 20), "white")
after = before.copy()
for x in range(10):
for y in range(10):
after.putpixel((x, y), (0, 0, 0))
self.assertGreaterEqual(
MODULE.changed_pixels(before, after),
MODULE.MIN_DRAFT_CHANGED_PIXELS,
)
def test_changed_pixels_rejects_an_unchanged_send_region(self):
image = Image.new("RGB", (20, 20), "white")
self.assertLess(
MODULE.changed_pixels(image, image.copy()),
MODULE.MIN_DRAFT_CHANGED_PIXELS,
)
def test_ink_pixels_detects_text_and_blank_roi(self):
blank = Image.new("RGB", (20, 20), "white")
text = blank.copy()
for x in range(8):
for y in range(8):
text.putpixel((x, y), (0, 0, 0))
self.assertLessEqual(MODULE.ink_pixels(blank), MODULE.MAX_EMPTY_ROI_INK_PIXELS)
self.assertGreater(MODULE.ink_pixels(text), MODULE.MAX_EMPTY_ROI_INK_PIXELS)
def test_wait_for_input_empty_reports_poll_count(self):
dirty = Image.new("RGB", (20, 20), "white")
for x in range(8):
for y in range(8):
dirty.putpixel((x, y), (0, 0, 0))
blank = Image.new("RGB", (20, 20), "white")
monotonic_values = iter((1.0, 1.0, 1.05, 1.05))
with mock.patch.object(
MODULE,
"input_image",
side_effect=[dirty, blank],
), mock.patch.object(
MODULE.time,
"monotonic",
side_effect=lambda: next(monotonic_values),
), mock.patch.object(MODULE.time, "sleep"):
_, ink, polls, elapsed_ms = MODULE.wait_for_input_empty(
"window",
width=1470,
height=694,
timeout_seconds=MODULE.CLEAR_CONFIRM_TIMEOUT_SECONDS,
)
self.assertLessEqual(ink, MODULE.MAX_EMPTY_ROI_INK_PIXELS)
self.assertEqual(polls, 2)
self.assertEqual(elapsed_ms, 50.0)
def test_wait_for_input_change_reports_poll_count_and_duration(self):
before = Image.new("RGB", (20, 20), "white")
after = before.copy()
for x in range(10):
for y in range(10):
after.putpixel((x, y), (0, 0, 0))
monotonic_values = iter((1.0, 1.0, 1.05))
with mock.patch.object(
MODULE,
"input_image",
return_value=after,
), mock.patch.object(
MODULE,
"changed_pixels",
return_value=MODULE.MIN_SEND_CHANGED_PIXELS,
), mock.patch.object(
MODULE.time,
"monotonic",
side_effect=lambda: next(monotonic_values),
):
_, changed, polls, elapsed_ms = MODULE.wait_for_input_change(
"window",
before,
width=1470,
height=694,
threshold=MODULE.MIN_SEND_CHANGED_PIXELS,
timeout_seconds=MODULE.SEND_CONFIRM_TIMEOUT_SECONDS,
)
self.assertEqual(changed, MODULE.MIN_SEND_CHANGED_PIXELS)
self.assertEqual(polls, 1)
self.assertEqual(elapsed_ms, 50.0)
class SendTests(unittest.TestCase):
def setUp(self):
self.temporary = tempfile.TemporaryDirectory()
self.pending_text = Path(self.temporary.name) / "pending.txt"
self.pending_token = Path(self.temporary.name) / "pending.token"
self.pending_text.write_text("reply", encoding="utf-8")
self.pending_token.write_text("token\n", encoding="utf-8")
self.patches = [
mock.patch.object(MODULE, "PENDING_TEXT", self.pending_text),
mock.patch.object(MODULE, "PENDING_TOKEN", self.pending_token),
mock.patch.object(MODULE, "window_id", return_value="window"),
mock.patch.object(MODULE, "geometry", return_value=(1470, 694)),
mock.patch.object(MODULE, "position", return_value=(0, 0)),
mock.patch.object(MODULE.time, "sleep"),
]
for patch in self.patches:
patch.start()
def tearDown(self):
for patch in reversed(self.patches):
patch.stop()
self.temporary.cleanup()
def test_send_presses_return_first_after_roi_flag(self):
connection = mock.Mock()
before = Image.new("RGB", (20, 20), "white")
after = before.copy()
with mock.patch.object(
MODULE,
"vnc_focus_input",
return_value=connection,
) as focus, mock.patch.object(
MODULE,
"input_image",
return_value=before,
), mock.patch.object(
MODULE,
"wait_for_input_change",
return_value=(after, MODULE.MIN_SEND_CHANGED_PIXELS, 1, 50.0),
) as wait:
MODULE.send("token")
focus.assert_called_once_with("window", (450, 625), origin=(0, 0))
connection.sendall.assert_any_call(
MODULE.struct.pack(">BBHI", 4, 1, 0, 0xFF0D)
)
self.assertEqual(wait.call_args.kwargs["box"], MODULE.BASE_INPUT_TEXT_ROI)
self.assertFalse(self.pending_text.exists())
self.assertFalse(self.pending_token.exists())
def test_send_uses_button_when_return_is_not_confirmed(self):
first_connection = mock.Mock()
second_connection = mock.Mock()
before = Image.new("RGB", (20, 20), "white")
after_return = before.copy()
after_click = before.copy()
with mock.patch.object(
MODULE,
"vnc_focus_input",
side_effect=[first_connection, second_connection],
) as focus, mock.patch.object(
MODULE,
"input_image",
return_value=before,
), mock.patch.object(
MODULE,
"wait_for_input_change",
side_effect=[
(after_click, 0, 18, 900.0),
(after_return, MODULE.MIN_SEND_CHANGED_PIXELS, 1, 50.0),
],
):
MODULE.send("token")
self.assertEqual(
[call.args for call in focus.call_args_list],
[("window", (450, 625)), ("window", (1419, 637))],
)
first_connection.sendall.assert_any_call(
MODULE.struct.pack(">BBHI", 4, 1, 0, 0xFF0D)
)
self.assertFalse(self.pending_text.exists())
self.assertFalse(self.pending_token.exists())
def test_deliver_text_confirms_clear_before_paste(self):
connection = mock.Mock()
blank = Image.new("RGB", (20, 20), "white")
after_paste = blank.copy()
after_send = blank.copy()
with mock.patch.object(
MODULE,
"vnc_focus_input",
return_value=connection,
), mock.patch.object(
MODULE,
"wait_for_input_empty",
return_value=(blank, 0, 1, 50.0),
) as empty, mock.patch.object(
MODULE,
"vnc_paste",
) as paste, mock.patch.object(
MODULE,
"input_image",
return_value=after_paste,
), mock.patch.object(
MODULE,
"wait_for_input_change",
side_effect=[
(after_paste, MODULE.MIN_DRAFT_CHANGED_PIXELS, 1, 50.0),
(after_send, MODULE.MIN_SEND_CHANGED_PIXELS, 1, 50.0),
],
) as wait:
payload = MODULE.deliver_text("reply")
self.assertEqual(payload["state"], "sent")
empty.assert_called_once()
paste.assert_called_once_with(connection, "reply")
self.assertEqual(len(wait.call_args_list), 2)
connection.sendall.assert_any_call(
MODULE.struct.pack(">BBHI", 4, 1, 0, 0xFF0D)
)
if __name__ == "__main__":
unittest.main()
+639
View File
@@ -0,0 +1,639 @@
#!/usr/bin/env python3
import importlib.util
import sys
import tempfile
import threading
import time
import unittest
from contextlib import contextmanager
from datetime import datetime, timezone
from pathlib import Path
from unittest import mock
PROJECT_ROOT = Path(__file__).resolve().parents[1]
if str(PROJECT_ROOT) not in sys.path:
sys.path.insert(0, str(PROJECT_ROOT))
MODULE_PATH = PROJECT_ROOT / "wechat_adaptive_recorder.py"
SPEC = importlib.util.spec_from_file_location("wechat_adaptive_recorder", MODULE_PATH)
MODULE = importlib.util.module_from_spec(SPEC)
assert SPEC.loader is not None
sys.modules[SPEC.name] = MODULE
SPEC.loader.exec_module(MODULE)
class FakeGuiUpstream:
def __init__(self) -> None:
self.events = []
self.bridge_payloads = []
self.current_group = "group-a"
self.title_captures = 0
self.full_captures = 0
@contextmanager
def gui_interaction_lock(self, *, blocking):
yield True
def capture_image(self):
self.full_captures += 1
return object()
def title_fingerprint(self, image):
return self.current_group
def capture_title_fingerprint(self):
self.title_captures += 1
return self.current_group
def telemetry_event(self, name, **kwargs):
self.events.append((name, kwargs))
def emit_bridge_timings(self, payload, **kwargs):
self.bridge_payloads.append((payload, kwargs))
class FakeGuiBridge:
def __init__(self) -> None:
self.calls = []
self.active = 0
self.max_active = 0
self.lock = threading.Lock()
def deliver_text(self, reply, *, before_send=None):
with self.lock:
self.active += 1
self.max_active = max(self.max_active, self.active)
try:
self.calls.append(reply)
if before_send is not None:
before_send()
time.sleep(0.01)
return {
"ok": True,
"state": "sent",
"timings": [{"phase": "deliver.total", "elapsed_ms": 1.0}],
}
finally:
with self.lock:
self.active -= 1
class BlockingGuiBridge(FakeGuiBridge):
def __init__(self, blocked_reply):
super().__init__()
self.blocked_reply = blocked_reply
self.blocked_started = threading.Event()
self.release_blocked = threading.Event()
def deliver_text(self, reply, *, before_send=None):
with self.lock:
self.active += 1
self.max_active = max(self.max_active, self.active)
try:
self.calls.append(reply)
if reply == self.blocked_reply:
self.blocked_started.set()
if not self.release_blocked.wait(2):
raise RuntimeError("blocked GUI job was not released")
if before_send is not None:
before_send()
return {
"ok": True,
"state": "sent",
"timings": [{"phase": "deliver.total", "elapsed_ms": 1.0}],
}
finally:
with self.lock:
self.active -= 1
class LayoutScalingTests(unittest.TestCase):
def run_gui_priority_scenario(self, queued_jobs):
upstream = FakeGuiUpstream()
bridge = BlockingGuiBridge("active-final")
with tempfile.TemporaryDirectory() as temporary:
sender = MODULE.GuiSender(
upstream,
bridge,
socket_path=Path(temporary) / "gui.sock",
)
sender.start()
errors = []
def submit(reply, source):
try:
sender.send_reply(
reply,
"group-a",
source=source,
timeout_seconds=3,
)
except BaseException as error:
errors.append(error)
threads = [
threading.Thread(
target=submit,
args=("active-final", "delivery-helper"),
)
]
threads[0].start()
self.assertTrue(bridge.blocked_started.wait(1))
for index, (reply, source) in enumerate(queued_jobs, start=1):
thread = threading.Thread(target=submit, args=(reply, source))
threads.append(thread)
thread.start()
deadline = time.monotonic() + 1
while (
sender.jobs.qsize() < index
and time.monotonic() < deadline
):
time.sleep(0.01)
self.assertEqual(sender.jobs.qsize(), index)
bridge.release_blocked.set()
for thread in threads:
thread.join(timeout=3)
self.assertFalse(thread.is_alive())
sender.stop()
self.assertEqual(errors, [])
self.assertEqual(bridge.max_active, 1)
return bridge.calls
def test_fast_ack_telemetry_keeps_legacy_and_dedicated_names(self):
source = Path(MODULE.__file__).read_text(encoding="utf-8")
for name in (
"recorder.ack.gui",
"recorder.ack.end_to_end",
"recorder.ack.confirm",
"recorder.fast_ack.gui",
"recorder.fast_ack.end_to_end",
"recorder.fast_ack.confirm",
):
self.assertIn(name, source)
def test_default_upstream_is_installed_with_the_adaptive_runtime(self):
self.assertEqual(
MODULE.UPSTREAM,
MODULE.HOME
/ ".local"
/ "lib"
/ "wechat-health-watch"
/ "wechat_local_ocr.py",
)
def test_wait_for_ready_retries_without_exiting_the_service(self):
sleep = mock.Mock()
with mock.patch.object(
MODULE,
"configure_layout",
side_effect=[
RuntimeError("login required"),
(1440, 873),
],
) as configure:
layout = MODULE.wait_for_ready(
mock.Mock(),
poll_seconds=2.5,
sleep=sleep,
monotonic=mock.Mock(return_value=10.0),
)
self.assertEqual(layout, (1440, 873))
self.assertEqual(configure.call_count, 2)
sleep.assert_called_once_with(2.5)
def test_wait_for_ready_rejects_a_busy_loop(self):
with self.assertRaisesRegex(ValueError, "positive"):
MODULE.wait_for_ready(mock.Mock(), poll_seconds=0)
def test_ready_capture_preserves_the_legacy_zero_argument_call(self):
upstream = mock.Mock()
capture = mock.Mock(return_value="image")
with mock.patch.object(MODULE, "wait_for_ready") as ready:
result = MODULE.capture_when_ready(upstream, capture)
self.assertEqual(result, "image")
ready.assert_called_once_with(upstream)
capture.assert_called_once_with()
def test_ready_capture_forwards_the_verified_window_identifier(self):
upstream = mock.Mock()
capture = mock.Mock(return_value="image")
with mock.patch.object(
MODULE,
"wait_for_ready",
) as ready, mock.patch.object(
MODULE,
"configure_layout",
) as configure:
result = MODULE.capture_when_ready(upstream, capture, "42")
self.assertEqual(result, "image")
ready.assert_not_called()
configure.assert_called_once_with(upstream, identifier="42")
capture.assert_called_once_with("42")
def test_verified_capture_layout_does_not_rediscover_the_window(self):
upstream = mock.Mock()
upstream.window_geometry.return_value = (1440, 873)
layout = MODULE.configure_layout(upstream, identifier="42")
self.assertEqual(layout, (1440, 873))
upstream.window_id.assert_not_called()
upstream.window_geometry.assert_called_once_with("42")
def test_ack_end_to_end_age_uses_the_observed_timestamp(self):
self.assertEqual(
MODULE.elapsed_since_iso_ms(
"2026-10-14T12:00:00+00:00",
now=datetime(
2026,
10,
14,
12,
0,
1,
250000,
tzinfo=timezone.utc,
),
),
1250.0,
)
self.assertIsNone(MODULE.elapsed_since_iso_ms("not-a-timestamp"))
def test_baseline_box_is_preserved(self):
self.assertEqual(
MODULE.scale_box(
MODULE.BASE_MESSAGE_BOX,
width=MODULE.BASE_WIDTH,
height=MODULE.BASE_HEIGHT,
),
MODULE.BASE_MESSAGE_BOX,
)
def test_current_vnc_box_is_scaled_and_bounded(self):
box = MODULE.scale_box(
MODULE.BASE_MESSAGE_BOX,
width=1470,
height=694,
)
self.assertEqual(box, (306, 64, 1469, 582))
self.assertLess(box[0], box[2])
self.assertLess(box[1], box[3])
def test_scaled_box_never_exceeds_image_bounds(self):
self.assertEqual(
MODULE.scale_box(
MODULE.BASE_MESSAGE_BOX,
width=320,
height=200,
),
(67, 18, 320, 168),
)
def test_capture_heartbeat_is_shorter_than_health_staleness_window(self):
self.assertLess(
MODULE.CAPTURE_HEARTBEAT_SECONDS,
180,
)
def test_openclaw_transport_retries_without_control_socket(self):
with mock.patch.dict(
MODULE.os.environ,
{"WECHAT_HEALTH_CLOUD_HOST": "cloud.example"},
clear=True,
):
command = MODULE.user_openclaw_command(
mock.Mock(SSH_KEY=Path("/tmp/key")),
agent="wechat-responder",
session="probe",
timeout_seconds=30,
)
self.assertIn("-F", command)
self.assertIn("/dev/null", command)
self.assertIn("ConnectionAttempts=3", command)
self.assertIn("ServerAliveInterval=5", command)
self.assertIn("ServerAliveCountMax=2", command)
self.assertIn("IPQoS=none", command)
self.assertNotIn("ControlMaster=auto", command)
self.assertNotIn("--model", command)
self.assertNotIn("--thinking", command)
def test_edge_reply_is_used_without_invoking_legacy_router(self):
legacy = mock.Mock(return_value="legacy")
with mock.patch.object(
MODULE.jwebot_edge,
"request_edge",
return_value={"status": "reply", "text": "本地回复喵"},
):
reply = MODULE.edge_router_reply(
{"event_id": "event-1", "text": "@JWeBot 你好"},
legacy_router=legacy,
)
self.assertEqual(reply, "本地回复喵")
legacy.assert_not_called()
def test_fast_ack_is_sent_by_callback_without_upstream_duplicate(self):
legacy = mock.Mock(return_value="legacy")
fast_ack_sender = mock.Mock(return_value=True)
incoming = {
"event_id": "event-1",
"group_fingerprint": "group-a",
"text": "@JWeBot 你好",
}
response = {
"status": "reply",
"route": "chat",
"text": MODULE.jwebot_edge.FAST_ACKNOWLEDGEMENT,
"event_id": "event-1",
"fast_ack": True,
}
with mock.patch.object(
MODULE.jwebot_edge,
"request_edge",
return_value=response,
):
reply = MODULE.edge_router_reply(
incoming,
legacy_router=legacy,
fast_ack_sender=fast_ack_sender,
)
self.assertEqual(reply, "NO_REPLY")
fast_ack_sender.assert_called_once_with(incoming, response)
legacy.assert_not_called()
def test_failed_fast_ack_does_not_fall_back_to_a_second_sender(self):
legacy = mock.Mock(return_value="legacy")
fast_ack_sender = mock.Mock(return_value=False)
response = {
"status": "reply",
"route": "chat",
"text": MODULE.jwebot_edge.FAST_ACKNOWLEDGEMENT,
"event_id": "event-1",
"fast_ack": True,
}
with mock.patch.object(
MODULE.jwebot_edge,
"request_edge",
return_value=response,
):
reply = MODULE.edge_router_reply(
{"event_id": "event-1", "text": "@JWeBot 你好"},
legacy_router=legacy,
fast_ack_sender=fast_ack_sender,
)
self.assertEqual(reply, "NO_REPLY")
fast_ack_sender.assert_called_once()
legacy.assert_not_called()
def test_hash_command_uses_legacy_router_when_edge_is_unavailable(self):
legacy = mock.Mock(return_value="legacy")
with mock.patch.object(
MODULE.jwebot_edge,
"request_edge",
side_effect=MODULE.jwebot_edge.EdgeError("unavailable"),
):
reply = MODULE.edge_router_reply(
{"event_id": "event-1", "text": "@JWeBot #状态"},
legacy_router=legacy,
)
self.assertEqual(reply, "legacy")
legacy.assert_called_once()
def test_non_hash_request_never_falls_back_to_legacy_router(self):
legacy = mock.Mock(return_value="legacy")
with mock.patch.object(
MODULE.jwebot_edge,
"request_edge",
side_effect=MODULE.jwebot_edge.EdgeError("unavailable"),
):
reply = MODULE.edge_router_reply(
{"event_id": "event-1", "text": "@JWeBot /丢骰子5d20"},
legacy_router=legacy,
)
self.assertIn("Local edge unavailable", reply)
legacy.assert_not_called()
def test_gui_sender_serializes_concurrent_jobs(self):
upstream = FakeGuiUpstream()
bridge = FakeGuiBridge()
with tempfile.TemporaryDirectory() as temporary:
sender = MODULE.GuiSender(
upstream,
bridge,
socket_path=Path(temporary) / "gui.sock",
)
sender.start()
try:
callbacks = []
threads = [
threading.Thread(
target=sender.send_reply,
args=(f"reply-{index}", "group-a"),
kwargs={
"event_id": f"event-{index}",
"before_send": lambda i=index: callbacks.append(
f"before-{i}"
),
"after_send": lambda i=index: callbacks.append(
f"after-{i}"
),
"timeout_seconds": 2,
},
)
for index in range(3)
]
for thread in threads:
thread.start()
for thread in threads:
thread.join(timeout=2)
self.assertTrue(all(not thread.is_alive() for thread in threads))
self.assertEqual(bridge.max_active, 1)
self.assertCountEqual(
bridge.calls,
["reply-0", "reply-1", "reply-2"],
)
self.assertEqual(len(callbacks), 6)
self.assertTrue(upstream.bridge_payloads)
self.assertEqual(upstream.title_captures, 6)
self.assertEqual(upstream.full_captures, 0)
finally:
sender.stop()
def test_gui_sender_prioritizes_new_ack_over_queued_final(self):
calls = self.run_gui_priority_scenario(
[
("queued-final", "delivery-helper"),
("newer-ack", "ack-local"),
]
)
self.assertEqual(
calls,
["active-final", "newer-ack", "queued-final"],
)
def test_gui_sender_prioritizes_local_reply_over_queued_final(self):
calls = self.run_gui_priority_scenario(
[
("queued-final", "delivery-helper"),
("newer-local", "ocr"),
]
)
self.assertEqual(
calls,
["active-final", "newer-local", "queued-final"],
)
def test_gui_sender_preserves_fifo_within_final_priority(self):
calls = self.run_gui_priority_scenario(
[
("first-queued-final", "delivery-helper"),
("second-queued-final", "delivery-helper"),
]
)
self.assertEqual(
calls,
[
"active-final",
"first-queued-final",
"second-queued-final",
],
)
def test_gui_sender_stop_sentinel_terminates_idle_worker(self):
with tempfile.TemporaryDirectory() as temporary:
sender = MODULE.GuiSender(
FakeGuiUpstream(),
FakeGuiBridge(),
socket_path=Path(temporary) / "gui.sock",
)
sender.start()
sender.stop()
self.assertFalse(sender.worker.is_alive())
self.assertFalse(sender.server.is_alive())
self.assertEqual(sender.jobs.unfinished_tasks, 0)
def test_gui_sender_fails_closed_on_title_region_mismatch(self):
upstream = FakeGuiUpstream()
upstream.current_group = "other-group"
bridge = FakeGuiBridge()
with tempfile.TemporaryDirectory() as temporary:
sender = MODULE.GuiSender(
upstream,
bridge,
socket_path=Path(temporary) / "gui.sock",
)
sender.start()
try:
with self.assertRaisesRegex(RuntimeError, "GUI sender failed"):
sender.send_reply(
"reply",
"group-a",
event_id="event-1",
timeout_seconds=2,
)
self.assertEqual(bridge.calls, [])
self.assertEqual(upstream.title_captures, 1)
self.assertEqual(upstream.full_captures, 0)
finally:
sender.stop()
def test_gui_sender_falls_back_to_full_window_group_checks(self):
upstream = FakeGuiUpstream()
upstream.capture_title_fingerprint = None
bridge = FakeGuiBridge()
with tempfile.TemporaryDirectory() as temporary:
sender = MODULE.GuiSender(
upstream,
bridge,
socket_path=Path(temporary) / "gui.sock",
)
sender.start()
try:
sender.send_reply(
"reply",
"group-a",
event_id="event-1",
timeout_seconds=2,
)
self.assertEqual(bridge.calls, ["reply"])
self.assertEqual(upstream.title_captures, 0)
self.assertEqual(upstream.full_captures, 2)
finally:
sender.stop()
def test_gui_sender_socket_submits_to_worker_queue(self):
upstream = FakeGuiUpstream()
bridge = FakeGuiBridge()
with tempfile.TemporaryDirectory() as temporary:
socket_path = Path(temporary) / "gui.sock"
sender = MODULE.GuiSender(upstream, bridge, socket_path=socket_path)
sender.start()
try:
deadline = time.monotonic() + 2
while not socket_path.exists() and time.monotonic() < deadline:
time.sleep(0.01)
response = MODULE.submit_gui_sender_request(
{
"op": "send",
"event_id": "event-1",
"group_fingerprint": "group-a",
"reply": "queued reply",
"source": "delivery-helper",
},
socket_path=socket_path,
timeout_seconds=2,
)
self.assertTrue(response["ok"])
self.assertEqual(response["state"], "sent")
self.assertEqual(bridge.calls, ["queued reply"])
finally:
sender.stop()
def test_edge_request_telemetry_excludes_message_body(self):
legacy = mock.Mock(return_value="legacy")
telemetry = mock.Mock()
with mock.patch.object(
MODULE,
"TELEMETRY",
telemetry,
), mock.patch.object(
MODULE.jwebot_edge,
"request_edge",
return_value={
"status": "reply",
"route": "local",
"text": "本地回复喵",
},
):
MODULE.edge_router_reply(
{
"event_id": "event-1",
"group_fingerprint": "group-a",
"text": "@JWeBot 私密正文",
},
legacy_router=legacy,
)
_, kwargs = telemetry.elapsed.call_args
self.assertEqual(kwargs["route"], "local")
self.assertNotIn("text", kwargs)
self.assertNotIn("私密正文", str(kwargs))
if __name__ == "__main__":
unittest.main()
+336
View File
@@ -0,0 +1,336 @@
#!/usr/bin/env python3
import importlib.util
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path
from unittest import mock
PROJECT_ROOT = Path(__file__).resolve().parents[1]
MODULE_PATH = PROJECT_ROOT / "wechat_health_watch.py"
SPEC = importlib.util.spec_from_file_location("wechat_health_watch", MODULE_PATH)
MODULE = importlib.util.module_from_spec(SPEC)
assert SPEC.loader is not None
sys.modules[SPEC.name] = MODULE
SPEC.loader.exec_module(MODULE)
class TransitionTests(unittest.TestCase):
def test_incident_requires_two_consecutive_unhealthy_scans(self):
state = {}
self.assertIsNone(
MODULE.update_incident(
state,
codes=["recorder_stale"],
now=100.0,
)
)
self.assertEqual(state["wechat_chain"]["consecutive_failures"], 1)
self.assertEqual(
MODULE.update_incident(
state,
codes=["recorder_stale"],
now=160.0,
),
"incident",
)
self.assertTrue(state["wechat_chain"]["active"])
self.assertTrue(state["wechat_chain"]["incident_pending"])
def test_healthy_scan_resets_the_consecutive_failure_counter(self):
state = {"wechat_chain": {"consecutive_failures": 1}}
self.assertIsNone(MODULE.update_incident(state, codes=[], now=100.0))
self.assertEqual(state["wechat_chain"]["consecutive_failures"], 0)
self.assertIsNone(
MODULE.update_incident(
state,
codes=["wechat_window"],
now=160.0,
)
)
def test_recovery_is_immediate_after_a_sent_incident(self):
state = {
"wechat_chain": {
"active": True,
"incident_sent": True,
"incident_last_notification_attempt_at": 100.0,
"consecutive_failures": 2,
}
}
self.assertEqual(
MODULE.update_incident(state, codes=[], now=101.0),
"recovery",
)
def test_failed_incident_notification_retries_after_cooldown(self):
state = {
"wechat_chain": {
"active": True,
"incident_pending": True,
"incident_last_notification_attempt_at": 100.0,
}
}
self.assertIsNone(
MODULE.update_incident(
state,
codes=["recorder_stale"],
now=100.0 + MODULE.NOTIFICATION_COOLDOWN_SECONDS - 1,
)
)
self.assertEqual(
MODULE.update_incident(
state,
codes=["recorder_stale"],
now=100.0 + MODULE.NOTIFICATION_COOLDOWN_SECONDS,
),
"incident",
)
class RecorderStateTests(unittest.TestCase):
def test_missing_recorder_state_is_unhealthy(self):
with tempfile.TemporaryDirectory() as directory, mock.patch.object(
MODULE,
"RECORDER_STATE",
Path(directory) / "missing.json",
):
self.assertEqual(
MODULE.recorder_is_fresh(100.0),
(False, "recorder_state"),
)
def test_stale_recorder_state_is_unhealthy(self):
with tempfile.TemporaryDirectory() as directory:
state_path = Path(directory) / "recorder-state.json"
state_path.write_text(
'{"last_capture_at":"1970-01-01T00:00:00+00:00"}',
encoding="utf-8",
)
with mock.patch.object(MODULE, "RECORDER_STATE", state_path):
self.assertEqual(
MODULE.recorder_is_fresh(
MODULE.RECORDER_FRESHNESS_SECONDS + 1
),
(False, "recorder_stale"),
)
def test_fresh_recorder_state_is_healthy(self):
with tempfile.TemporaryDirectory() as directory:
state_path = Path(directory) / "recorder-state.json"
state_path.write_text(
'{"last_capture_at":"1970-01-01T00:00:00+00:00"}',
encoding="utf-8",
)
with mock.patch.object(MODULE, "RECORDER_STATE", state_path):
self.assertEqual(
MODULE.recorder_is_fresh(
MODULE.RECORDER_FRESHNESS_SECONDS
),
(True, ""),
)
class HealthCodeTests(unittest.TestCase):
def test_login_sized_window_is_not_accepted_as_ready(self):
with mock.patch.object(
MODULE,
"is_active",
return_value=True,
), mock.patch.object(
MODULE,
"is_user_active",
return_value=True,
), mock.patch.object(
MODULE,
"wechat_process_present",
return_value=True,
), mock.patch.object(
MODULE,
"weixin_window_geometry",
return_value=(280, 380),
), mock.patch.object(
MODULE,
"recorder_is_fresh",
return_value=(True, ""),
), mock.patch.object(
MODULE,
"has_unacknowledged_failed_delivery",
return_value=False,
):
self.assertEqual(
MODULE.health_codes(100.0),
["wechat_login_required"],
)
def test_missing_window_is_reported(self):
with mock.patch.object(
MODULE,
"is_active",
return_value=True,
), mock.patch.object(
MODULE,
"is_user_active",
return_value=True,
), mock.patch.object(
MODULE,
"wechat_process_present",
return_value=True,
), mock.patch.object(
MODULE,
"weixin_window_geometry",
return_value=None,
), mock.patch.object(
MODULE,
"recorder_is_fresh",
return_value=(True, ""),
), mock.patch.object(
MODULE,
"has_unacknowledged_failed_delivery",
return_value=False,
):
self.assertEqual(MODULE.health_codes(100.0), ["wechat_window"])
def test_failed_reply_is_reported_as_an_incident(self):
with mock.patch.object(
MODULE,
"is_active",
return_value=True,
), mock.patch.object(
MODULE,
"is_user_active",
return_value=True,
), mock.patch.object(
MODULE,
"wechat_process_present",
return_value=True,
), mock.patch.object(
MODULE,
"weixin_window_geometry",
return_value=(1470, 694),
), mock.patch.object(
MODULE,
"recorder_is_fresh",
return_value=(True, ""),
), mock.patch.object(
MODULE,
"has_unacknowledged_failed_delivery",
return_value=True,
):
self.assertEqual(MODULE.health_codes(100.0), ["responder_failed"])
class ConfigurationTests(unittest.TestCase):
def test_notification_adapter_is_loaded_from_generated_config(self):
with tempfile.TemporaryDirectory() as directory:
config_file = Path(directory) / "notification.json"
config_file.write_text(
'{"enabled":true,"command":["/usr/bin/true"],'
'"timeout_seconds":15}',
encoding="utf-8",
)
with mock.patch.object(
MODULE,
"NOTIFICATION_CONFIG",
config_file,
):
self.assertEqual(
MODULE.notification_config(),
{
"enabled": True,
"command": ["/usr/bin/true"],
"timeout_seconds": 15,
},
)
def test_notification_message_is_sent_on_stdin_without_shell(self):
with mock.patch.object(
MODULE,
"notification_config",
return_value={
"enabled": True,
"command": ["/usr/local/bin/notifier", "--json"],
"timeout_seconds": 20,
},
), mock.patch.object(
MODULE,
"run",
return_value=subprocess.CompletedProcess([], 0, "", ""),
) as run:
MODULE.send_official_notification("health message")
run.assert_called_once_with(
["/usr/local/bin/notifier", "--json"],
timeout=20,
data="health message",
)
class NotificationFailureTests(unittest.TestCase):
def test_timeout_is_accepted_as_an_uncertain_incident(self):
state = {"wechat_chain": {"incident_pending": True}}
with mock.patch.object(
MODULE,
"send_official_notification",
side_effect=MODULE.NotificationDeliveryUncertain("delivery_timeout"),
):
self.assertTrue(
MODULE.finish_notification(
state,
"incident",
codes=["wechat_window"],
start_result="not_needed",
now=100.0,
dry_run=False,
)
)
self.assertEqual(state["wechat_chain"]["last_notification_result"], "uncertain")
self.assertFalse(state["wechat_chain"]["incident_pending"])
self.assertTrue(state["wechat_chain"]["incident_sent"])
def test_remote_error_code_has_priority(self):
result = subprocess.CompletedProcess(
[],
75,
"",
"delivery_error=provider_context_rejected",
)
self.assertEqual(
MODULE.notification_failure_code(result),
"provider_context_rejected",
)
def test_prepare_failure_is_sanitized(self):
result = subprocess.CompletedProcess(
[],
1,
"",
"sendMessage ret=-2 errmsg=prepare failed",
)
self.assertEqual(
MODULE.notification_failure_code(result),
"provider_prepare_failed",
)
def test_context_failure_is_sanitized(self):
result = subprocess.CompletedProcess(
[],
1,
"",
"context_token expired",
)
self.assertEqual(
MODULE.notification_failure_code(result),
"provider_context_rejected",
)
if __name__ == "__main__":
unittest.main()
+1231
View File
File diff suppressed because it is too large Load Diff