98 lines
5.6 KiB
Python
98 lines
5.6 KiB
Python
import hashlib
|
|
import json
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
WIDGETS = {
|
|
"active-projects": ROOT / "plugins" / "shd-mcp-plugin" / "widgets" / "active-projects" / "v1",
|
|
"documents": ROOT / "plugins" / "shd-mcp-plugin" / "widgets" / "documents" / "v1",
|
|
"tasks": ROOT / "plugins" / "shd-mcp-plugin" / "widgets" / "tasks" / "v1",
|
|
"finance": ROOT / "plugins" / "shd-mcp-plugin" / "widgets" / "finance" / "v1",
|
|
"crm": ROOT / "plugins" / "shd-mcp-plugin" / "widgets" / "crm" / "v1",
|
|
"discussions": ROOT / "plugins" / "shd-mcp-plugin" / "widgets" / "discussions" / "v1",
|
|
"notifications": ROOT / "plugins" / "shd-mcp-plugin" / "widgets" / "notifications" / "v1",
|
|
"scheduling": ROOT / "plugins" / "shd-mcp-plugin" / "widgets" / "scheduling" / "v1",
|
|
"inventory": ROOT / "plugins" / "shd-mcp-plugin" / "widgets" / "inventory" / "v1",
|
|
"agents": ROOT / "plugins" / "shd-mcp-plugin" / "widgets" / "agents" / "v1",
|
|
"status-page": ROOT / "plugins" / "shd-mcp-plugin" / "widgets" / "status-page" / "v1",
|
|
"project-db": ROOT / "plugins" / "shd-mcp-plugin" / "widgets" / "project-db" / "v1",
|
|
"notes": ROOT / "plugins" / "shd-mcp-plugin" / "widgets" / "notes" / "v1",
|
|
"project-overview": ROOT / "plugins" / "shd-mcp-plugin" / "widgets" / "project-overview" / "v1",
|
|
"files": ROOT / "plugins" / "shd-mcp-plugin" / "widgets" / "files" / "v1",
|
|
"proposals": ROOT / "plugins" / "shd-mcp-plugin" / "widgets" / "proposals" / "v1",
|
|
"terms": ROOT / "plugins" / "shd-mcp-plugin" / "widgets" / "terms" / "v1",
|
|
"activity": ROOT / "plugins" / "shd-mcp-plugin" / "widgets" / "activity" / "v1",
|
|
"organizations": ROOT / "plugins" / "shd-mcp-plugin" / "widgets" / "organizations" / "v1",
|
|
"gitea": ROOT / "plugins" / "shd-mcp-plugin" / "widgets" / "gitea" / "v1",
|
|
"schemes": ROOT / "plugins" / "shd-mcp-plugin" / "widgets" / "schemes" / "v1",
|
|
"task-kanban": ROOT / "plugins" / "shd-mcp-plugin" / "widgets" / "task-kanban" / "v1",
|
|
"team-workload": ROOT / "plugins" / "shd-mcp-plugin" / "widgets" / "team-workload" / "v1",
|
|
"project-timeline": ROOT / "plugins" / "shd-mcp-plugin" / "widgets" / "project-timeline" / "v1",
|
|
"crm-funnel": ROOT / "plugins" / "shd-mcp-plugin" / "widgets" / "crm-funnel" / "v1",
|
|
"finance-dashboard": ROOT / "plugins" / "shd-mcp-plugin" / "widgets" / "finance-dashboard" / "v1",
|
|
"proposal-approvals": ROOT / "plugins" / "shd-mcp-plugin" / "widgets" / "proposal-approvals" / "v1",
|
|
"documents-completeness": ROOT / "plugins" / "shd-mcp-plugin" / "widgets" / "documents-completeness" / "v1",
|
|
"agents-health": ROOT / "plugins" / "shd-mcp-plugin" / "widgets" / "agents-health" / "v1",
|
|
"schedule-calendar": ROOT / "plugins" / "shd-mcp-plugin" / "widgets" / "schedule-calendar" / "v1",
|
|
"acl-matrix": ROOT / "plugins" / "shd-mcp-plugin" / "widgets" / "acl-matrix" / "v1",
|
|
"inventory-warnings": ROOT / "plugins" / "shd-mcp-plugin" / "widgets" / "inventory-warnings" / "v1",
|
|
"gitea-board": ROOT / "plugins" / "shd-mcp-plugin" / "widgets" / "gitea-board" / "v1",
|
|
"schemes-progress": ROOT / "plugins" / "shd-mcp-plugin" / "widgets" / "schemes-progress" / "v1",
|
|
"notes-tree": ROOT / "plugins" / "shd-mcp-plugin" / "widgets" / "notes-tree" / "v1",
|
|
"project-db-preview": ROOT / "plugins" / "shd-mcp-plugin" / "widgets" / "project-db-preview" / "v1",
|
|
}
|
|
|
|
|
|
class WidgetArtifactTests(unittest.TestCase):
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
cls.artifacts = {}
|
|
for name, widget in WIDGETS.items():
|
|
manifest = json.loads((widget / "manifest.json").read_text(encoding="utf-8"))
|
|
resource = next(item for item in manifest["resources"] if item["file"] == "index.html")
|
|
cls.artifacts[name] = {
|
|
"widget": widget,
|
|
"manifest": manifest,
|
|
"resource": resource,
|
|
"html": (widget / "index.html").read_text(encoding="utf-8"),
|
|
}
|
|
|
|
def test_manifest_and_checksums_match_artifact(self):
|
|
for name, artifact in self.artifacts.items():
|
|
widget = artifact["widget"]
|
|
manifest = artifact["manifest"]
|
|
resource = artifact["resource"]
|
|
html = artifact["html"]
|
|
digest = hashlib.sha256(html.encode("utf-8")).hexdigest()
|
|
self.assertEqual(resource["uri"], f"ui://shd/{name}/v1.html", name)
|
|
self.assertEqual(resource["mediaType"], "text/html;profile=mcp-app", name)
|
|
self.assertEqual(resource["bytes"], len(html.encode("utf-8")), name)
|
|
self.assertEqual(resource["sha256"], digest, name)
|
|
|
|
checksum = (widget / "SHA256SUMS").read_text(encoding="utf-8").strip().split()
|
|
self.assertEqual(checksum, [digest, "index.html"], name)
|
|
|
|
license_path = (widget / manifest["license"]).resolve()
|
|
self.assertEqual(license_path, (ROOT / "LICENSE").resolve(), name)
|
|
source_path = (ROOT / manifest["source"]["path"]).resolve()
|
|
self.assertEqual(source_path, (widget / "index.html").resolve(), name)
|
|
|
|
def test_widget_uses_portable_bridge_and_no_external_script(self):
|
|
for name, artifact in self.artifacts.items():
|
|
for marker in (
|
|
"ui/initialize",
|
|
"ui/notifications/initialized",
|
|
"ui/notifications/tool-input",
|
|
"ui/notifications/tool-result",
|
|
"tools/call",
|
|
"window.openai",
|
|
):
|
|
self.assertIn(marker, artifact["html"], f"{name}: {marker}")
|
|
self.assertNotIn("<script src=", artifact["html"].lower(), name)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|