diff --git a/backend/autometabuilder/workflow/plugin_map.json b/backend/autometabuilder/workflow/plugin_map.json index 270d329..8965a64 100644 --- a/backend/autometabuilder/workflow/plugin_map.json +++ b/backend/autometabuilder/workflow/plugin_map.json @@ -89,5 +89,29 @@ "var.delete": "autometabuilder.workflow.plugins.var.var_delete.run", "var.exists": "autometabuilder.workflow.plugins.var.var_exists.run", "var.get": "autometabuilder.workflow.plugins.var.var_get.run", - "var.set": "autometabuilder.workflow.plugins.var.var_set.run" + "var.set": "autometabuilder.workflow.plugins.var.var_set.run", + "web.build_context": "autometabuilder.workflow.plugins.web.web_build_context.run", + "web.build_prompt_yaml": "autometabuilder.workflow.plugins.web.web_build_prompt_yaml.run", + "web.create_flask_app": "autometabuilder.workflow.plugins.web.web_create_flask_app.run", + "web.create_translation": "autometabuilder.workflow.plugins.web.web_create_translation.run", + "web.delete_translation": "autometabuilder.workflow.plugins.web.web_delete_translation.run", + "web.get_env_vars": "autometabuilder.workflow.plugins.web.web_get_env_vars.run", + "web.get_navigation_items": "autometabuilder.workflow.plugins.web.web_get_navigation_items.run", + "web.get_prompt_content": "autometabuilder.workflow.plugins.web.web_get_prompt_content.run", + "web.get_recent_logs": "autometabuilder.workflow.plugins.web.web_get_recent_logs.run", + "web.get_ui_messages": "autometabuilder.workflow.plugins.web.web_get_ui_messages.run", + "web.get_workflow_content": "autometabuilder.workflow.plugins.web.web_get_workflow_content.run", + "web.list_translations": "autometabuilder.workflow.plugins.web.web_list_translations.run", + "web.load_messages": "autometabuilder.workflow.plugins.web.web_load_messages.run", + "web.load_translation": "autometabuilder.workflow.plugins.web.web_load_translation.run", + "web.load_workflow_packages": "autometabuilder.workflow.plugins.web.web_load_workflow_packages.run", + "web.persist_env_vars": "autometabuilder.workflow.plugins.web.web_persist_env_vars.run", + "web.read_json": "autometabuilder.workflow.plugins.web.web_read_json.run", + "web.register_blueprint": "autometabuilder.workflow.plugins.web.web_register_blueprint.run", + "web.start_server": "autometabuilder.workflow.plugins.web.web_start_server.run", + "web.summarize_workflow_packages": "autometabuilder.workflow.plugins.web.web_summarize_workflow_packages.run", + "web.update_translation": "autometabuilder.workflow.plugins.web.web_update_translation.run", + "web.write_messages_dir": "autometabuilder.workflow.plugins.web.web_write_messages_dir.run", + "web.write_prompt": "autometabuilder.workflow.plugins.web.web_write_prompt.run", + "web.write_workflow": "autometabuilder.workflow.plugins.web.web_write_workflow.run" } diff --git a/backend/autometabuilder/workflow/plugins/web/__init__.py b/backend/autometabuilder/workflow/plugins/web/__init__.py new file mode 100644 index 0000000..4c0553d --- /dev/null +++ b/backend/autometabuilder/workflow/plugins/web/__init__.py @@ -0,0 +1 @@ +"""Web data access workflow plugins.""" diff --git a/backend/autometabuilder/workflow/plugins/web/web_build_context.py b/backend/autometabuilder/workflow/plugins/web/web_build_context.py new file mode 100644 index 0000000..ae627c6 --- /dev/null +++ b/backend/autometabuilder/workflow/plugins/web/web_build_context.py @@ -0,0 +1,17 @@ +"""Workflow plugin: build context for API.""" +import os +from ....web.routes.context import build_context + + +def run(_runtime, _inputs): + """ + Build the complete context object for the web UI. + + This includes logs, env vars, translations, metadata, navigation, + prompt, workflow, packages, messages, and status. + + Returns: + dict: Complete context object + """ + context = build_context() + return {"result": context} diff --git a/backend/autometabuilder/workflow/plugins/web/web_build_prompt_yaml.py b/backend/autometabuilder/workflow/plugins/web/web_build_prompt_yaml.py new file mode 100644 index 0000000..3383e18 --- /dev/null +++ b/backend/autometabuilder/workflow/plugins/web/web_build_prompt_yaml.py @@ -0,0 +1,12 @@ +"""Workflow plugin: build prompt YAML.""" +from ....web.data.prompt import build_prompt_yaml + + +def run(_runtime, inputs): + """Build prompt YAML from system and user content.""" + system_content = inputs.get("system_content") + user_content = inputs.get("user_content") + model = inputs.get("model") + + yaml_content = build_prompt_yaml(system_content, user_content, model) + return {"result": yaml_content} diff --git a/backend/autometabuilder/workflow/plugins/web/web_create_flask_app.py b/backend/autometabuilder/workflow/plugins/web/web_create_flask_app.py new file mode 100644 index 0000000..e4ff37b --- /dev/null +++ b/backend/autometabuilder/workflow/plugins/web/web_create_flask_app.py @@ -0,0 +1,28 @@ +"""Workflow plugin: create Flask app.""" +from flask import Flask + + +def run(runtime, inputs): + """ + Create a Flask application instance. + + Inputs: + name: Application name (default: __name__) + config: Dictionary of Flask configuration options + + Returns: + dict: Contains the Flask app in result + """ + name = inputs.get("name", "__main__") + config = inputs.get("config", {}) + + app = Flask(name) + + # Apply configuration + for key, value in config.items(): + app.config[key] = value + + # Store app in runtime context for other plugins to use + runtime.context["flask_app"] = app + + return {"result": app, "message": "Flask app created"} diff --git a/backend/autometabuilder/workflow/plugins/web/web_create_translation.py b/backend/autometabuilder/workflow/plugins/web/web_create_translation.py new file mode 100644 index 0000000..9233182 --- /dev/null +++ b/backend/autometabuilder/workflow/plugins/web/web_create_translation.py @@ -0,0 +1,12 @@ +"""Workflow plugin: create translation.""" +from ....web.data.translations import create_translation + + +def run(_runtime, inputs): + """Create a new translation.""" + lang = inputs.get("lang") + if not lang: + return {"error": "lang is required"} + + created = create_translation(lang) + return {"result": created} diff --git a/backend/autometabuilder/workflow/plugins/web/web_delete_translation.py b/backend/autometabuilder/workflow/plugins/web/web_delete_translation.py new file mode 100644 index 0000000..76e7eb0 --- /dev/null +++ b/backend/autometabuilder/workflow/plugins/web/web_delete_translation.py @@ -0,0 +1,12 @@ +"""Workflow plugin: delete translation.""" +from ....web.data.translations import delete_translation + + +def run(_runtime, inputs): + """Delete a translation.""" + lang = inputs.get("lang") + if not lang: + return {"error": "lang is required"} + + deleted = delete_translation(lang) + return {"result": deleted} diff --git a/backend/autometabuilder/workflow/plugins/web/web_get_env_vars.py b/backend/autometabuilder/workflow/plugins/web/web_get_env_vars.py new file mode 100644 index 0000000..10f6c9e --- /dev/null +++ b/backend/autometabuilder/workflow/plugins/web/web_get_env_vars.py @@ -0,0 +1,8 @@ +"""Workflow plugin: get environment variables.""" +from ....web.data.env import get_env_vars + + +def run(_runtime, _inputs): + """Get environment variables from .env file.""" + env_vars = get_env_vars() + return {"result": env_vars} diff --git a/backend/autometabuilder/workflow/plugins/web/web_get_navigation_items.py b/backend/autometabuilder/workflow/plugins/web/web_get_navigation_items.py new file mode 100644 index 0000000..b663727 --- /dev/null +++ b/backend/autometabuilder/workflow/plugins/web/web_get_navigation_items.py @@ -0,0 +1,8 @@ +"""Workflow plugin: get navigation items.""" +from ....web.data.navigation import get_navigation_items + + +def run(_runtime, _inputs): + """Get navigation items.""" + items = get_navigation_items() + return {"result": items} diff --git a/backend/autometabuilder/workflow/plugins/web/web_get_prompt_content.py b/backend/autometabuilder/workflow/plugins/web/web_get_prompt_content.py new file mode 100644 index 0000000..5cdf72f --- /dev/null +++ b/backend/autometabuilder/workflow/plugins/web/web_get_prompt_content.py @@ -0,0 +1,8 @@ +"""Workflow plugin: get prompt content.""" +from ....web.data.prompt import get_prompt_content + + +def run(_runtime, _inputs): + """Get prompt content from prompt file.""" + content = get_prompt_content() + return {"result": content} diff --git a/backend/autometabuilder/workflow/plugins/web/web_get_recent_logs.py b/backend/autometabuilder/workflow/plugins/web/web_get_recent_logs.py new file mode 100644 index 0000000..979902b --- /dev/null +++ b/backend/autometabuilder/workflow/plugins/web/web_get_recent_logs.py @@ -0,0 +1,9 @@ +"""Workflow plugin: get recent logs.""" +from ....web.data.logs import get_recent_logs + + +def run(_runtime, inputs): + """Get recent log entries.""" + lines = inputs.get("lines", 50) + logs = get_recent_logs(lines) + return {"result": logs} diff --git a/backend/autometabuilder/workflow/plugins/web/web_get_ui_messages.py b/backend/autometabuilder/workflow/plugins/web/web_get_ui_messages.py new file mode 100644 index 0000000..8e761a1 --- /dev/null +++ b/backend/autometabuilder/workflow/plugins/web/web_get_ui_messages.py @@ -0,0 +1,17 @@ +"""Workflow plugin: get UI messages.""" +from ....web.data.translations import get_ui_messages + + +def run(_runtime, inputs): + """ + Get UI messages for a specific language with fallback to English. + + Inputs: + lang: Language code (default: en) + + Returns: + dict: UI messages with __lang key indicating the language + """ + lang = inputs.get("lang", "en") + messages = get_ui_messages(lang) + return {"result": messages} diff --git a/backend/autometabuilder/workflow/plugins/web/web_get_workflow_content.py b/backend/autometabuilder/workflow/plugins/web/web_get_workflow_content.py new file mode 100644 index 0000000..9b53ade --- /dev/null +++ b/backend/autometabuilder/workflow/plugins/web/web_get_workflow_content.py @@ -0,0 +1,8 @@ +"""Workflow plugin: get workflow content.""" +from ....web.data.workflow import get_workflow_content + + +def run(_runtime, _inputs): + """Get workflow content from workflow file.""" + content = get_workflow_content() + return {"result": content} diff --git a/backend/autometabuilder/workflow/plugins/web/web_list_translations.py b/backend/autometabuilder/workflow/plugins/web/web_list_translations.py new file mode 100644 index 0000000..2bdc80f --- /dev/null +++ b/backend/autometabuilder/workflow/plugins/web/web_list_translations.py @@ -0,0 +1,8 @@ +"""Workflow plugin: list translations.""" +from ....web.data.translations import list_translations + + +def run(_runtime, _inputs): + """List all available translations.""" + translations = list_translations() + return {"result": translations} diff --git a/backend/autometabuilder/workflow/plugins/web/web_load_messages.py b/backend/autometabuilder/workflow/plugins/web/web_load_messages.py new file mode 100644 index 0000000..83e8993 --- /dev/null +++ b/backend/autometabuilder/workflow/plugins/web/web_load_messages.py @@ -0,0 +1,13 @@ +"""Workflow plugin: load messages.""" +from pathlib import Path +from ....web.data.messages_io import load_messages + + +def run(_runtime, inputs): + """Load translation messages from path.""" + path = inputs.get("path") + if not path: + return {"error": "path is required"} + + messages = load_messages(Path(path)) + return {"result": messages} diff --git a/backend/autometabuilder/workflow/plugins/web/web_load_translation.py b/backend/autometabuilder/workflow/plugins/web/web_load_translation.py new file mode 100644 index 0000000..99088ac --- /dev/null +++ b/backend/autometabuilder/workflow/plugins/web/web_load_translation.py @@ -0,0 +1,9 @@ +"""Workflow plugin: load translation.""" +from ....web.data.translations import load_translation + + +def run(_runtime, inputs): + """Load translation for a specific language.""" + lang = inputs.get("lang", "en") + translation = load_translation(lang) + return {"result": translation} diff --git a/backend/autometabuilder/workflow/plugins/web/web_load_workflow_packages.py b/backend/autometabuilder/workflow/plugins/web/web_load_workflow_packages.py new file mode 100644 index 0000000..b6e1d9b --- /dev/null +++ b/backend/autometabuilder/workflow/plugins/web/web_load_workflow_packages.py @@ -0,0 +1,8 @@ +"""Workflow plugin: load workflow packages.""" +from ....web.data.workflow import load_workflow_packages + + +def run(_runtime, _inputs): + """Load all workflow packages.""" + packages = load_workflow_packages() + return {"result": packages} diff --git a/backend/autometabuilder/workflow/plugins/web/web_persist_env_vars.py b/backend/autometabuilder/workflow/plugins/web/web_persist_env_vars.py new file mode 100644 index 0000000..22c5886 --- /dev/null +++ b/backend/autometabuilder/workflow/plugins/web/web_persist_env_vars.py @@ -0,0 +1,9 @@ +"""Workflow plugin: persist environment variables.""" +from ....web.data.env import persist_env_vars + + +def run(_runtime, inputs): + """Persist environment variables to .env file.""" + updates = inputs.get("updates", {}) + persist_env_vars(updates) + return {"result": "Environment variables persisted"} diff --git a/backend/autometabuilder/workflow/plugins/web/web_read_json.py b/backend/autometabuilder/workflow/plugins/web/web_read_json.py new file mode 100644 index 0000000..8ca1bdf --- /dev/null +++ b/backend/autometabuilder/workflow/plugins/web/web_read_json.py @@ -0,0 +1,13 @@ +"""Workflow plugin: read JSON file.""" +from pathlib import Path +from ....web.data.json_utils import read_json + + +def run(_runtime, inputs): + """Read JSON file.""" + path = inputs.get("path") + if not path: + return {"error": "path is required"} + + json_data = read_json(Path(path)) + return {"result": json_data} diff --git a/backend/autometabuilder/workflow/plugins/web/web_register_blueprint.py b/backend/autometabuilder/workflow/plugins/web/web_register_blueprint.py new file mode 100644 index 0000000..50c56f9 --- /dev/null +++ b/backend/autometabuilder/workflow/plugins/web/web_register_blueprint.py @@ -0,0 +1,29 @@ +"""Workflow plugin: register Flask blueprint.""" + + +def run(runtime, inputs): + """ + Register a Flask blueprint with the Flask app. + + Inputs: + blueprint_path: Dotted path to the blueprint (e.g., "autometabuilder.web.routes.context.context_bp") + + Returns: + dict: Success indicator + """ + from ....loaders.callable_loader import load_callable + + app = runtime.context.get("flask_app") + if not app: + return {"error": "Flask app not found in context. Run web.create_flask_app first."} + + blueprint_path = inputs.get("blueprint_path") + if not blueprint_path: + return {"error": "blueprint_path is required"} + + try: + blueprint = load_callable(blueprint_path) + app.register_blueprint(blueprint) + return {"result": f"Blueprint {blueprint_path} registered"} + except Exception as e: + return {"error": f"Failed to register blueprint: {str(e)}"} diff --git a/backend/autometabuilder/workflow/plugins/web/web_start_server.py b/backend/autometabuilder/workflow/plugins/web/web_start_server.py new file mode 100644 index 0000000..10e2c82 --- /dev/null +++ b/backend/autometabuilder/workflow/plugins/web/web_start_server.py @@ -0,0 +1,27 @@ +"""Workflow plugin: start Flask server.""" + + +def run(runtime, inputs): + """ + Start the Flask web server. + + Inputs: + host: Host address (default: 0.0.0.0) + port: Port number (default: 8000) + debug: Enable debug mode (default: False) + + Returns: + dict: Success indicator (note: this blocks until server stops) + """ + app = runtime.context.get("flask_app") + if not app: + return {"error": "Flask app not found in context. Run web.create_flask_app first."} + + host = inputs.get("host", "0.0.0.0") + port = inputs.get("port", 8000) + debug = inputs.get("debug", False) + + # This will block until the server is stopped + app.run(host=host, port=port, debug=debug) + + return {"result": "Server stopped"} diff --git a/backend/autometabuilder/workflow/plugins/web/web_summarize_workflow_packages.py b/backend/autometabuilder/workflow/plugins/web/web_summarize_workflow_packages.py new file mode 100644 index 0000000..4f6e4a3 --- /dev/null +++ b/backend/autometabuilder/workflow/plugins/web/web_summarize_workflow_packages.py @@ -0,0 +1,9 @@ +"""Workflow plugin: summarize workflow packages.""" +from ....web.data.workflow import summarize_workflow_packages + + +def run(_runtime, inputs): + """Summarize workflow packages.""" + packages = inputs.get("packages", []) + summary = summarize_workflow_packages(packages) + return {"result": summary} diff --git a/backend/autometabuilder/workflow/plugins/web/web_update_translation.py b/backend/autometabuilder/workflow/plugins/web/web_update_translation.py new file mode 100644 index 0000000..5c995a9 --- /dev/null +++ b/backend/autometabuilder/workflow/plugins/web/web_update_translation.py @@ -0,0 +1,14 @@ +"""Workflow plugin: update translation.""" +from ....web.data.translations import update_translation + + +def run(_runtime, inputs): + """Update an existing translation.""" + lang = inputs.get("lang") + payload = inputs.get("payload", {}) + + if not lang: + return {"error": "lang is required"} + + updated = update_translation(lang, payload) + return {"result": updated} diff --git a/backend/autometabuilder/workflow/plugins/web/web_write_messages_dir.py b/backend/autometabuilder/workflow/plugins/web/web_write_messages_dir.py new file mode 100644 index 0000000..4d02855 --- /dev/null +++ b/backend/autometabuilder/workflow/plugins/web/web_write_messages_dir.py @@ -0,0 +1,15 @@ +"""Workflow plugin: write messages directory.""" +from pathlib import Path +from ....web.data.messages_io import write_messages_dir + + +def run(_runtime, inputs): + """Write messages to directory.""" + base_dir = inputs.get("base_dir") + payload_content = inputs.get("payload_content", {}) + + if not base_dir: + return {"error": "base_dir is required"} + + write_messages_dir(Path(base_dir), payload_content) + return {"result": "Messages written successfully"} diff --git a/backend/autometabuilder/workflow/plugins/web/web_write_prompt.py b/backend/autometabuilder/workflow/plugins/web/web_write_prompt.py new file mode 100644 index 0000000..12f56e6 --- /dev/null +++ b/backend/autometabuilder/workflow/plugins/web/web_write_prompt.py @@ -0,0 +1,9 @@ +"""Workflow plugin: write prompt.""" +from ....web.data.prompt import write_prompt + + +def run(_runtime, inputs): + """Write prompt content to file.""" + content = inputs.get("content", "") + write_prompt(content) + return {"result": "Prompt written successfully"} diff --git a/backend/autometabuilder/workflow/plugins/web/web_write_workflow.py b/backend/autometabuilder/workflow/plugins/web/web_write_workflow.py new file mode 100644 index 0000000..a9dba0c --- /dev/null +++ b/backend/autometabuilder/workflow/plugins/web/web_write_workflow.py @@ -0,0 +1,9 @@ +"""Workflow plugin: write workflow.""" +from ....web.data.workflow import write_workflow + + +def run(_runtime, inputs): + """Write workflow content to file.""" + content = inputs.get("content", "") + write_workflow(content) + return {"result": "Workflow written successfully"} diff --git a/backend/tests/test_web_plugins.py b/backend/tests/test_web_plugins.py new file mode 100644 index 0000000..eb0226b --- /dev/null +++ b/backend/tests/test_web_plugins.py @@ -0,0 +1,242 @@ +"""Tests for web workflow plugins.""" +import os +import tempfile +from pathlib import Path +from autometabuilder.workflow.plugin_registry import PluginRegistry, load_plugin_map +from autometabuilder.workflow.runtime import WorkflowRuntime + + +class MockLogger: + """Mock logger for testing.""" + def info(self, *args, **kwargs): + pass + + def debug(self, *args, **kwargs): + pass + + def error(self, *args, **kwargs): + pass + + +def create_test_runtime(): + """Create a test runtime with empty context.""" + logger = MockLogger() + return WorkflowRuntime(context={}, store={}, tool_runner=None, logger=logger) + + +def test_plugin_map_includes_web_plugins(): + """Test that plugin map includes all new web plugins.""" + plugin_map = load_plugin_map() + + # Test web data plugins + assert "web.get_env_vars" in plugin_map + assert "web.persist_env_vars" in plugin_map + assert "web.get_recent_logs" in plugin_map + assert "web.read_json" in plugin_map + assert "web.load_messages" in plugin_map + assert "web.write_messages_dir" in plugin_map + assert "web.get_navigation_items" in plugin_map + assert "web.get_prompt_content" in plugin_map + assert "web.write_prompt" in plugin_map + assert "web.build_prompt_yaml" in plugin_map + assert "web.get_workflow_content" in plugin_map + assert "web.write_workflow" in plugin_map + assert "web.load_workflow_packages" in plugin_map + assert "web.summarize_workflow_packages" in plugin_map + + # Test translation plugins + assert "web.load_translation" in plugin_map + assert "web.list_translations" in plugin_map + assert "web.create_translation" in plugin_map + assert "web.update_translation" in plugin_map + assert "web.delete_translation" in plugin_map + assert "web.get_ui_messages" in plugin_map + + # Test Flask/server plugins + assert "web.create_flask_app" in plugin_map + assert "web.register_blueprint" in plugin_map + assert "web.start_server" in plugin_map + assert "web.build_context" in plugin_map + + +def test_web_read_json_plugin(): + """Test web.read_json plugin.""" + plugin_map = load_plugin_map() + registry = PluginRegistry(plugin_map) + runtime = create_test_runtime() + + plugin = registry.get("web.read_json") + assert plugin is not None + + # Test with non-existent file (should return empty dict) + with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f: + f.write('{"test": "value"}') + temp_path = f.name + + try: + result = plugin(runtime, {"path": temp_path}) + assert "result" in result + assert result["result"]["test"] == "value" + finally: + os.unlink(temp_path) + + +def test_web_build_prompt_yaml_plugin(): + """Test web.build_prompt_yaml plugin.""" + plugin_map = load_plugin_map() + registry = PluginRegistry(plugin_map) + runtime = create_test_runtime() + + plugin = registry.get("web.build_prompt_yaml") + assert plugin is not None + + result = plugin(runtime, { + "system_content": "You are a helpful assistant", + "user_content": "Help me with coding", + "model": "openai/gpt-4o" + }) + + assert "result" in result + yaml_content = result["result"] + assert "messages:" in yaml_content + assert "role: system" in yaml_content + assert "role: user" in yaml_content + assert "model: openai/gpt-4o" in yaml_content + + +def test_web_create_flask_app_plugin(): + """Test web.create_flask_app plugin.""" + plugin_map = load_plugin_map() + registry = PluginRegistry(plugin_map) + runtime = create_test_runtime() + + plugin = registry.get("web.create_flask_app") + assert plugin is not None + + result = plugin(runtime, { + "name": "test_app", + "config": {"JSON_SORT_KEYS": False} + }) + + assert "result" in result + assert runtime.context.get("flask_app") is not None + + app = runtime.context["flask_app"] + assert app.config["JSON_SORT_KEYS"] is False + + +def test_web_register_blueprint_plugin(): + """Test web.register_blueprint plugin.""" + plugin_map = load_plugin_map() + registry = PluginRegistry(plugin_map) + runtime = create_test_runtime() + + # First create a Flask app + create_app_plugin = registry.get("web.create_flask_app") + create_app_plugin(runtime, {"name": "test_app"}) + + # Now test registering a blueprint + plugin = registry.get("web.register_blueprint") + assert plugin is not None + + result = plugin(runtime, { + "blueprint_path": "autometabuilder.web.routes.context.context_bp" + }) + + assert "result" in result + assert "registered" in result["result"] + + +def test_web_get_ui_messages_plugin(): + """Test web.get_ui_messages plugin.""" + plugin_map = load_plugin_map() + registry = PluginRegistry(plugin_map) + runtime = create_test_runtime() + + plugin = registry.get("web.get_ui_messages") + assert plugin is not None + + result = plugin(runtime, {"lang": "en"}) + + assert "result" in result + assert isinstance(result["result"], dict) + assert result["result"].get("__lang") == "en" + + +def test_web_list_translations_plugin(): + """Test web.list_translations plugin.""" + plugin_map = load_plugin_map() + registry = PluginRegistry(plugin_map) + runtime = create_test_runtime() + + plugin = registry.get("web.list_translations") + assert plugin is not None + + result = plugin(runtime, {}) + + assert "result" in result + assert isinstance(result["result"], dict) + + +def test_web_load_workflow_packages_plugin(): + """Test web.load_workflow_packages plugin.""" + plugin_map = load_plugin_map() + registry = PluginRegistry(plugin_map) + runtime = create_test_runtime() + + plugin = registry.get("web.load_workflow_packages") + assert plugin is not None + + result = plugin(runtime, {}) + + assert "result" in result + assert isinstance(result["result"], list) + + +def test_web_summarize_workflow_packages_plugin(): + """Test web.summarize_workflow_packages plugin.""" + plugin_map = load_plugin_map() + registry = PluginRegistry(plugin_map) + runtime = create_test_runtime() + + plugin = registry.get("web.summarize_workflow_packages") + assert plugin is not None + + packages = [ + { + "id": "test_pkg", + "name": "Test Package", + "description": "A test package", + "version": "1.0.0" + } + ] + + result = plugin(runtime, {"packages": packages}) + + assert "result" in result + assert isinstance(result["result"], list) + assert len(result["result"]) == 1 + assert result["result"][0]["id"] == "test_pkg" + + +def test_web_build_context_plugin(): + """Test web.build_context plugin.""" + plugin_map = load_plugin_map() + registry = PluginRegistry(plugin_map) + runtime = create_test_runtime() + + plugin = registry.get("web.build_context") + assert plugin is not None + + result = plugin(runtime, {}) + + assert "result" in result + context = result["result"] + + # Verify expected keys in context + assert "logs" in context + assert "env_vars" in context + assert "translations" in context + assert "metadata" in context + assert "navigation" in context + assert "status" in context