75 lines
2.6 KiB
Python
75 lines
2.6 KiB
Python
"""Meta-skill executors honor the operator gate (codex BLOCKER #2).
|
|
|
|
A meta-skill that composes ``code-task`` as a step must NOT be able to reach it
|
|
while coding mode is off — the gate lives in the executors right after the skill
|
|
is resolved from the loader, so it covers both the sub-Agent and wrapped-CLI
|
|
(skill_exec) composition styles.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from types import SimpleNamespace
|
|
|
|
import pytest
|
|
|
|
from opensquilla.skills import eligibility
|
|
from opensquilla.skills.meta.executors.agent import run_step_with_skill_text_only
|
|
from opensquilla.skills.meta.executors.skill_exec import run_skill_exec_step
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _reset_gate():
|
|
saved = eligibility._live_skills_cfg_getter
|
|
yield
|
|
eligibility.set_live_skills_config_getter(saved)
|
|
|
|
|
|
class _Loader:
|
|
def get_by_name(self, name):
|
|
# Non-None, non-meta spec with NO entrypoint: the operator gate fires
|
|
# before the entrypoint is examined, so a gated skill raises the gate
|
|
# error while an allowed skill falls through to the missing-entrypoint
|
|
# error — letting us distinguish "blocked by gate" from "blocked later".
|
|
return SimpleNamespace(name=name, kind="skill", entrypoint=None)
|
|
|
|
|
|
def _gate(coding_mode: bool, disabled=None):
|
|
eligibility.set_live_skills_config_getter(
|
|
lambda: SimpleNamespace(disabled=disabled or [], coding_mode=coding_mode)
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_skill_exec_step_refuses_gated_codetask():
|
|
_gate(coding_mode=False)
|
|
step = SimpleNamespace(id="s1")
|
|
with pytest.raises(RuntimeError, match="gated by operator config"):
|
|
await run_skill_exec_step(
|
|
step, "code-task", {}, {}, skill_loader=_Loader()
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_text_only_step_refuses_gated_codetask():
|
|
_gate(coding_mode=False)
|
|
step = SimpleNamespace(id="s2")
|
|
|
|
async def _llm(_s, _u): # never reached
|
|
return ""
|
|
|
|
with pytest.raises(ValueError, match="gated by operator config"):
|
|
await run_step_with_skill_text_only(
|
|
step, "code-task", {}, {}, llm_chat=_llm, skill_loader=_Loader()
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_executor_allows_other_skill_when_codetask_gated():
|
|
# Gating code-task must not gate an unrelated skill: the gate passes and the
|
|
# step proceeds past the check (then fails later on the absent entrypoint).
|
|
_gate(coding_mode=False)
|
|
step = SimpleNamespace(id="s3")
|
|
with pytest.raises(RuntimeError) as exc:
|
|
await run_skill_exec_step(step, "git-diff", {}, {}, skill_loader=_Loader())
|
|
assert "gated by operator config" not in str(exc.value)
|