test(block-h): gate H proof - plugin contributes all extension points without core changes
This commit is contained in:
@@ -61,6 +61,11 @@ def register_step_type(step_type: str):
|
||||
return decorator
|
||||
|
||||
|
||||
def unregister_step_type(step_type: str) -> None:
|
||||
"""Remove a step handler (plugin deactivation lifecycle symmetry)."""
|
||||
_HANDLERS.pop(step_type, None)
|
||||
|
||||
|
||||
def get_step_handler(step_type: str) -> StepHandler | None:
|
||||
return _HANDLERS.get(step_type)
|
||||
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user