f0815dd126
CI / lint (pull_request) Has been cancelled
CI / typecheck (pull_request) Has been cancelled
CI / security (pull_request) Has been cancelled
CI / quality (pull_request) Has been cancelled
CI / unit_tests (pull_request) Has been cancelled
CI / integration_tests (pull_request) Has been cancelled
CI / coverage (pull_request) Has been cancelled
CI / build (pull_request) Has been cancelled
CI / docker (pull_request) Has been cancelled
216 lines
6.3 KiB
Python
216 lines
6.3 KiB
Python
"""Robot Framework helper for skill resolution smoke tests.
|
|
|
|
Provides a CLI-style interface for Robot to invoke skill resolution
|
|
logic and validate resolver output ordering, de-duplication, and
|
|
inline tool handling.
|
|
|
|
Usage:
|
|
python robot/helper_skill_resolution.py resolve-flat
|
|
python robot/helper_skill_resolution.py resolve-dedup
|
|
python robot/helper_skill_resolution.py resolve-inline
|
|
python robot/helper_skill_resolution.py resolve-cycle
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
# Ensure the src directory is on the import path.
|
|
_SRC = str(Path(__file__).resolve().parents[1] / "src")
|
|
if _SRC not in sys.path:
|
|
sys.path.insert(0, _SRC)
|
|
|
|
from cleveragents.domain.models.core.skill import ( # noqa: E402
|
|
Skill,
|
|
SkillInclude,
|
|
SkillInlineTool,
|
|
SkillMcpSource,
|
|
SkillResolver,
|
|
)
|
|
from cleveragents.domain.models.core.tool import ToolSource # noqa: E402
|
|
|
|
|
|
def _resolve_flat() -> int:
|
|
"""Resolve a skill with flat includes and verify output ordering."""
|
|
base = Skill(
|
|
name="local/base-tools",
|
|
description="Base tools",
|
|
tool_refs=["local/edit-file", "local/read-file"],
|
|
)
|
|
root = Skill(
|
|
name="local/code-tools",
|
|
description="Code editing tools",
|
|
tool_refs=["local/format"],
|
|
includes=[SkillInclude(name="local/base-tools")],
|
|
)
|
|
registry = {"local/base-tools": base, "local/code-tools": root}
|
|
resolver = SkillResolver()
|
|
resolved = resolver.resolve_tools(root, registry)
|
|
names = [e.name for e in resolved]
|
|
|
|
# Include tools should come first (depth-first), then own refs
|
|
expected = ["local/edit-file", "local/read-file", "local/format"]
|
|
if names != expected:
|
|
print(f"skill-resolve-fail: expected {expected}, got {names}")
|
|
return 1
|
|
|
|
print(f"skill-resolve-flat-ok: {json.dumps(names)}")
|
|
return 0
|
|
|
|
|
|
def _resolve_dedup() -> int:
|
|
"""Resolve overlapping includes and verify de-duplication."""
|
|
skill_a = Skill(
|
|
name="local/a",
|
|
description="Skill A",
|
|
tool_refs=["local/shared-tool", "local/a-only"],
|
|
)
|
|
skill_b = Skill(
|
|
name="local/b",
|
|
description="Skill B",
|
|
tool_refs=["local/shared-tool", "local/b-only"],
|
|
)
|
|
root = Skill(
|
|
name="local/top",
|
|
description="Top skill",
|
|
includes=[
|
|
SkillInclude(name="local/a"),
|
|
SkillInclude(name="local/b"),
|
|
],
|
|
)
|
|
registry = {
|
|
"local/a": skill_a,
|
|
"local/b": skill_b,
|
|
"local/top": root,
|
|
}
|
|
resolver = SkillResolver()
|
|
resolved = resolver.resolve_tools(root, registry)
|
|
names = [e.name for e in resolved]
|
|
|
|
# shared-tool appears once (position from first occurrence in a),
|
|
# but entry metadata is from b (last-wins)
|
|
if names.count("local/shared-tool") != 1:
|
|
print(f"skill-resolve-fail: shared-tool should appear once, got {names}")
|
|
return 1
|
|
|
|
# The shared-tool entry should come from skill b (last-wins)
|
|
shared_entry = next(e for e in resolved if e.name == "local/shared-tool")
|
|
if shared_entry.source_skill != "local/b":
|
|
print(
|
|
f"skill-resolve-fail: shared-tool source should be local/b, "
|
|
f"got {shared_entry.source_skill}"
|
|
)
|
|
return 1
|
|
|
|
print(f"skill-resolve-dedup-ok: {json.dumps(names)}")
|
|
return 0
|
|
|
|
|
|
def _resolve_inline() -> int:
|
|
"""Resolve a skill with inline tools and verify keying."""
|
|
root = Skill(
|
|
name="local/inline-skill",
|
|
description="Skill with inline tools",
|
|
tool_refs=["local/ref-tool"],
|
|
anonymous_tools=[
|
|
SkillInlineTool(
|
|
description="First inline",
|
|
source=ToolSource.CUSTOM,
|
|
code="return 1",
|
|
timeout=300,
|
|
),
|
|
SkillInlineTool(
|
|
description="Second inline",
|
|
source=ToolSource.CUSTOM,
|
|
code="return 2",
|
|
timeout=300,
|
|
),
|
|
],
|
|
mcp_servers=[
|
|
SkillMcpSource(
|
|
server="npx @mcp/test-server",
|
|
tools=["mcp-read"],
|
|
),
|
|
],
|
|
)
|
|
resolver = SkillResolver()
|
|
resolved = resolver.resolve_tools(root, {})
|
|
names = [e.name for e in resolved]
|
|
|
|
expected = [
|
|
"local/ref-tool",
|
|
"local/inline-skill/_anon_0",
|
|
"local/inline-skill/_anon_1",
|
|
"mcp:npx @mcp/test-server/mcp-read",
|
|
]
|
|
if names != expected:
|
|
print(f"skill-resolve-fail: expected {expected}, got {names}")
|
|
return 1
|
|
|
|
# Check that inline entries are marked correctly
|
|
anon_entries = [e for e in resolved if e.is_inline]
|
|
if len(anon_entries) != 2:
|
|
print(f"skill-resolve-fail: expected 2 inline entries, got {len(anon_entries)}")
|
|
return 1
|
|
|
|
print(f"skill-resolve-inline-ok: {json.dumps(names)}")
|
|
return 0
|
|
|
|
|
|
def _resolve_cycle() -> int:
|
|
"""Attempt cyclic resolution and verify error."""
|
|
skill_a = Skill(
|
|
name="local/cycle-a",
|
|
description="Cycle A",
|
|
includes=[SkillInclude(name="local/cycle-b")],
|
|
)
|
|
skill_b = Skill(
|
|
name="local/cycle-b",
|
|
description="Cycle B",
|
|
includes=[SkillInclude(name="local/cycle-a")],
|
|
)
|
|
registry = {"local/cycle-a": skill_a, "local/cycle-b": skill_b}
|
|
resolver = SkillResolver()
|
|
try:
|
|
resolver.resolve_tools(skill_a, registry)
|
|
print("skill-resolve-fail: expected cycle error but resolution succeeded")
|
|
return 1
|
|
except ValueError as exc:
|
|
if "ycle" not in str(exc):
|
|
print(f"skill-resolve-fail: expected cycle error, got: {exc}")
|
|
return 1
|
|
print(f"skill-resolve-cycle-ok: {exc}")
|
|
return 0
|
|
|
|
|
|
def main() -> int:
|
|
"""Entry point called by Robot Framework ``Run Process``."""
|
|
if len(sys.argv) < 2:
|
|
print(
|
|
"Usage: helper_skill_resolution.py "
|
|
"<resolve-flat|resolve-dedup|resolve-inline|resolve-cycle>"
|
|
)
|
|
return 1
|
|
|
|
command = sys.argv[1]
|
|
|
|
commands = {
|
|
"resolve-flat": _resolve_flat,
|
|
"resolve-dedup": _resolve_dedup,
|
|
"resolve-inline": _resolve_inline,
|
|
"resolve-cycle": _resolve_cycle,
|
|
}
|
|
|
|
handler = commands.get(command)
|
|
if handler is None:
|
|
print(f"Unknown command: {command}")
|
|
return 1
|
|
|
|
return handler()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|