170 lines
6.7 KiB
Python
170 lines
6.7 KiB
Python
"""Gate H — prove that a plugin contributes ALL extension points without
|
|
changing a single core file.
|
|
|
|
Covers:
|
|
1. Agent tool -> app.ai.tool_registry (core layer)
|
|
2. Workflow step type -> app.workflows.step_handlers
|
|
3. Fallback intent -> app.ai.action_mapper
|
|
4. Deactivate symmetry -> everything removed again
|
|
5. Re-activate -> present again, NO duplicates
|
|
|
|
The plugin under test is defined INLINE in this file — if this test passes,
|
|
no core file had to be touched to extend the platform.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
import pytest
|
|
|
|
# ─── The Gate-H test plugin (inline — zero core changes) ─────────────────────
|
|
|
|
|
|
def _build_gate_h_plugin():
|
|
from app.plugins.base import BasePlugin
|
|
from app.plugins.manifest import PluginManifest
|
|
|
|
class GateHTestPlugin(BasePlugin):
|
|
"""Minimal plugin exercising every Block-H contribution point."""
|
|
|
|
manifest = PluginManifest(
|
|
name="gate_h_test",
|
|
version="1.0.0",
|
|
display_name="Gate H Test",
|
|
description="Proves plugin contributions without core changes.",
|
|
dependencies=[],
|
|
routes=[],
|
|
events=[],
|
|
migrations=[],
|
|
permissions=["gate_h_test:read"],
|
|
)
|
|
|
|
async def on_activate(self, db, service_container, event_bus) -> None:
|
|
await super().on_activate(db, service_container, event_bus)
|
|
|
|
# 1) Agent tool via CORE registry
|
|
from app.ai.tool_registry import get_tool_registry
|
|
|
|
async def _echo_handler(arguments: dict[str, Any], context: dict[str, Any]) -> str:
|
|
return f"gate-h says: {arguments.get('text', '')}"
|
|
|
|
get_tool_registry().register(
|
|
name="gate_h_echo",
|
|
description="Echo text back (Gate H proof).",
|
|
parameters={
|
|
"type": "object",
|
|
"properties": {"text": {"type": "string"}},
|
|
"required": ["text"],
|
|
},
|
|
handler=_echo_handler,
|
|
plugin_name=self.manifest.name,
|
|
required_permission=None,
|
|
category="test",
|
|
)
|
|
|
|
# 2) Workflow step type
|
|
from app.workflows.step_handlers import StepResult, register_step_type
|
|
|
|
@register_step_type("gate_h_noop")
|
|
async def _handle_gate_h_noop(db, tenant_id, instance, step): # noqa: ANN001
|
|
return StepResult(output={"gate_h": True})
|
|
|
|
# 3) Fallback intent pattern
|
|
from app.ai.action_mapper import register_intent_pattern
|
|
|
|
def _invoice_intent(query: str, context: dict[str, Any] | None):
|
|
return [
|
|
{
|
|
"method": "POST",
|
|
"path": "/api/v1/gate-h/invoices",
|
|
"body": {},
|
|
"description": "Create invoice (Gate H proof)",
|
|
"confidence": 0.9,
|
|
}
|
|
]
|
|
|
|
_invoice_intent.owner_tag = self.manifest.name
|
|
register_intent_pattern(r"\b(create|new)\b.*\binvoice\b", _invoice_intent, owner=self.manifest.name)
|
|
|
|
async def on_deactivate(self, db, service_container, event_bus) -> None:
|
|
# Symmetry: remove everything registered above
|
|
from app.ai.tool_registry import get_tool_registry
|
|
from app.workflows.step_handlers import unregister_step_type
|
|
from app.ai.action_mapper import unregister_intent_patterns
|
|
|
|
get_tool_registry().unregister_plugin(self.manifest.name)
|
|
unregister_step_type("gate_h_noop")
|
|
unregister_intent_patterns(self.manifest.name)
|
|
await super().on_deactivate(db, service_container, event_bus)
|
|
|
|
return GateHTestPlugin
|
|
|
|
|
|
# ─── The proof ────────────────────────────────────────────────────────────────
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_gate_h_full_contribution_lifecycle():
|
|
"""Activate -> contributed everywhere; deactivate -> gone; reactivate -> back, no dupes."""
|
|
from app.ai.tool_registry import get_tool_registry
|
|
from app.workflows.step_handlers import get_step_handler
|
|
from app.ai.action_mapper import map_query_to_actions
|
|
|
|
plugin_cls = _build_gate_h_plugin()
|
|
plugin = plugin_cls()
|
|
|
|
# ── Before activation: nothing registered ──
|
|
assert get_tool_registry().get("gate_h_echo") is None
|
|
assert get_step_handler("gate_h_noop") is None
|
|
assert map_query_to_actions("create invoice for Acme") == []
|
|
|
|
# ── Activate ──
|
|
await plugin.on_activate(db=None, service_container=None, event_bus=None)
|
|
|
|
# 1) Agent tool present in CORE registry
|
|
tool = get_tool_registry().get("gate_h_echo")
|
|
assert tool is not None
|
|
assert tool.plugin_name == "gate_h_test"
|
|
result = await tool.handler({"text": "hello"}, {})
|
|
assert result == "gate-h says: hello"
|
|
|
|
# 2) Workflow step type resolvable
|
|
handler = get_step_handler("gate_h_noop")
|
|
assert handler is not None
|
|
|
|
# 3) Intent contributes actions for a FACHMODUL term unknown to core
|
|
actions = map_query_to_actions("create invoice for Acme")
|
|
assert len(actions) == 1
|
|
assert actions[0]["path"] == "/api/v1/gate-h/invoices"
|
|
|
|
# ── Deactivate: full symmetry ──
|
|
await plugin.on_deactivate(db=None, service_container=None, event_bus=None)
|
|
assert get_tool_registry().get("gate_h_echo") is None
|
|
assert get_step_handler("gate_h_noop") is None
|
|
assert map_query_to_actions("create invoice for Acme") == []
|
|
|
|
# ── Re-activate: back AND no duplicates ──
|
|
await plugin.on_activate(db=None, service_container=None, event_bus=None)
|
|
tool = get_tool_registry().get("gate_h_echo")
|
|
assert tool is not None # single entry (dict keyed by name — overwrite, not dupe)
|
|
assert get_step_handler("gate_h_noop") is not None
|
|
actions = map_query_to_actions("create invoice for Acme")
|
|
assert len(actions) == 1 # exactly one contributed intent, not two
|
|
|
|
# Cleanup so other tests are unaffected
|
|
await plugin.on_deactivate(db=None, service_container=None, event_bus=None)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_gate_h_agent_runtime_survives_ai_assistant_absence():
|
|
"""The core tool registry works even though ai_assistant plugin module is never imported."""
|
|
from app.ai.tool_registry import ToolRegistry, get_tool_registry
|
|
|
|
reg = get_tool_registry()
|
|
assert isinstance(reg, ToolRegistry)
|
|
# And it must be THE same singleton the shim exposes
|
|
from app.plugins.builtins.ai_assistant.tool_registry import get_tool_registry as shim_get
|
|
|
|
assert shim_get() is reg
|