174 lines
6.3 KiB
Python
174 lines
6.3 KiB
Python
#!/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()
|