Files
wehub-resource-sync 4b6817381b
Benchmark image — build + push to ECR (any adapter) / build + push (push) Waiting to run
CI / quality (ubuntu-latest) (push) Waiting to run
CI / test (tools-runtime) (push) Waiting to run
CI / test (e2e-general) (push) Waiting to run
CI / test (cli-runtime) (push) Waiting to run
CI / test (e2e-provider-and-openclaw) (push) Waiting to run
CI / test (integrations-and-misc) (push) Waiting to run
CI / coverage-report (push) Blocked by required conditions
CI / test-kubernetes (push) Waiting to run
CI / should-run-thorough (push) Waiting to run
CI / test-thorough (cloudwatch-demo) (push) Blocked by required conditions
CI / test-thorough (flink-ecs) (push) Blocked by required conditions
CI / test-thorough (upstream-lambda) (push) Blocked by required conditions
CI / test-thorough (prefect-ecs-fargate) (push) Blocked by required conditions
CodeQL / Analyze (python) (push) Waiting to run
Release / build-binaries (zip, opensre.exe, onefile, windows-latest, windows-x64) (push) Blocked by required conditions
Release / publish-release (push) Blocked by required conditions
Release / publish-main-release (push) Blocked by required conditions
Release / prepare (push) Waiting to run
Release / verify (push) Blocked by required conditions
Release / build-python-dist (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, macos-15-intel, darwin-x64) (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, macos-latest, darwin-arm64) (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04, linux-x64) (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04-arm, linux-arm64) (push) Blocked by required conditions
Synthetic Deterministic Tests / Synthetic offline (deterministic) (push) Waiting to run
Interactive Shell Live (PR + post-merge) / turn-checks (no-LLM) (push) Waiting to run
Interactive Shell Live (PR + post-merge) / turn-live shard ${{ matrix.shard_index }} (push) Waiting to run
CI (OpenClaw E2E) / openclaw test (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:10:45 +08:00

230 lines
8.4 KiB
Python

"""Tests for interactive-shell action tool registry."""
from __future__ import annotations
import re
from rich.console import Console
from core.agent_harness.tools.action_tools import (
get_action_tool,
get_action_tools_from_integrations_context,
)
from core.agent_harness.tools.tool_context import (
ActionToolContext,
)
from surfaces.cli.wizard.config import PROVIDER_BY_VALUE
from surfaces.interactive_shell.command_registry import SLASH_COMMANDS
from surfaces.interactive_shell.session import Session
from tools.interactive_shell.action_names import TOOL_KIND_TO_NAME
def _action_tools(
session: Session,
*,
resolved_integrations: dict[str, dict[str, str]] | None = None,
) -> list[object]:
ctx = ActionToolContext(session=session, console=Console(force_terminal=False))
return get_action_tools_from_integrations_context(
ctx, resolved_integrations=resolved_integrations
)
def _tool_specs(
session: Session,
*,
resolved_integrations: dict[str, dict[str, str]] | None = None,
) -> list[dict[str, object]]:
return [
{
"name": tool.name,
"description": tool.description,
"input_schema": tool.public_input_schema,
}
for tool in _action_tools(session, resolved_integrations=resolved_integrations)
]
# OpenAI's Chat Completions API rejects any tool name that does not match
# this pattern with HTTP 400. Every OpenAI-compatible provider (OpenRouter,
# Gemini, Nvidia, Minimax, Ollama, etc.) enforces the same rule. Anthropic
# is more permissive, but using the OpenAI subset keeps the planner working
# across all providers without per-provider name munging.
_OPENAI_TOOL_NAME_RE = re.compile(r"^[a-zA-Z0-9_-]+$")
def test_action_kind_mapping_targets_registered_tools() -> None:
for tool_name in TOOL_KIND_TO_NAME.values():
assert get_action_tool(tool_name) is not None
def test_tool_specs_include_required_fields() -> None:
specs = _tool_specs(Session())
assert specs
for spec in specs:
assert spec["name"]
assert spec["description"]
assert "input_schema" in spec
def test_action_kind_to_tool_names_are_openai_compatible() -> None:
"""Guard against the dotted-name regression that broke all 56 live
planner scenarios on OpenAI-style providers (HTTP 400 on
``tools[0].function.name``)."""
for kind, tool_name in TOOL_KIND_TO_NAME.items():
assert _OPENAI_TOOL_NAME_RE.match(tool_name), (
f"TOOL_KIND_TO_NAME[{kind!r}] = {tool_name!r} must match "
f"OpenAI's tool-name pattern ^[a-zA-Z0-9_-]+$"
)
def test_registered_tool_specs_are_openai_compatible() -> None:
"""Same guarantee, but exercised through the spec builder the LLM
planner actually feeds to the provider."""
specs = _tool_specs(Session())
assert specs
for spec in specs:
name = spec["name"]
assert _OPENAI_TOOL_NAME_RE.match(name), (
f"Registered tool spec name {name!r} must match "
f"OpenAI's tool-name pattern ^[a-zA-Z0-9_-]+$"
)
def test_tool_schemas_are_closed_objects() -> None:
specs = _tool_specs(Session())
assert specs
for spec in specs:
schema = spec["input_schema"]
assert schema["type"] == "object"
assert schema["additionalProperties"] is False
def test_required_properties_have_descriptions() -> None:
specs = _tool_specs(Session())
assert specs
for spec in specs:
schema = spec["input_schema"]
required = schema.get("required", [])
properties = schema.get("properties", {})
for required_name in required:
prop = properties.get(required_name)
assert isinstance(prop, dict), (
f"{spec['name']} required property {required_name!r} missing from properties"
)
assert str(prop.get("description", "")).strip(), (
f"{spec['name']} required property {required_name!r} must include description"
)
def test_llm_set_provider_schema_enum_matches_runtime_providers() -> None:
spec = next(tool for tool in _tool_specs(Session()) if tool["name"] == "llm_set_provider")
target = spec["input_schema"]["properties"]["target"]
target_variants = target.get("oneOf", [])
enum_variant = next(
variant for variant in target_variants if isinstance(variant, dict) and "enum" in variant
)
assert set(enum_variant["enum"]) == set(PROVIDER_BY_VALUE.keys())
def test_slash_invoke_schema_enum_matches_registered_commands() -> None:
spec = next(tool for tool in _tool_specs(Session()) if tool["name"] == "slash_invoke")
command = spec["input_schema"]["properties"]["command"]
assert set(command["enum"]) == set(SLASH_COMMANDS.keys())
def test_tools_hidden_when_capabilities_are_explicitly_empty() -> None:
session = Session(
available_capabilities={
"slash_commands": (),
"cli_commands": (),
"synthetic_suites": (),
"shell_commands": (),
"implementation": (),
"llm_provider": (),
}
)
names = {spec["name"] for spec in _tool_specs(session)}
assert "slash_invoke" not in names
assert "cli_exec" not in names
assert "synthetic_run" not in names
assert "shell_run" not in names
assert "code_implement" not in names
assert "llm_set_provider" not in names
def test_telegram_send_message_offered_when_telegram_is_configured() -> None:
session = Session()
session.configured_integrations = ("telegram",)
session.configured_integrations_known = True
names = {
spec["name"]
for spec in _tool_specs(
session,
resolved_integrations={"telegram": {"bot_token": "token"}},
)
}
assert "telegram_send_message" in names
def test_telegram_send_message_hidden_when_telegram_is_not_configured() -> None:
names = {spec["name"] for spec in _tool_specs(Session())}
assert "telegram_send_message" not in names
def test_llm_set_provider_offered_by_default() -> None:
"""With no capability constraints (the production default), the planner is
still offered the provider-switch tool."""
names = {spec["name"] for spec in _tool_specs(Session())}
assert "llm_set_provider" in names
def test_registry_agent_tools_exclude_unavailable_tool() -> None:
session = Session(available_capabilities={"slash_commands": ()})
ctx = ActionToolContext(session=session, console=Console(force_terminal=False))
names = {tool.name for tool in get_action_tools_from_integrations_context(ctx)}
assert "slash_invoke" not in names
def test_investigation_offered_to_planner() -> None:
"""``investigation_start`` is always offered to the planner so diagnostic
prompts can trigger the RCA pipeline from the REPL."""
names = {spec["name"] for spec in _tool_specs(Session())}
assert "investigation_start" in names
def test_investigation_tool_description_preserves_compound_slash_guidance() -> None:
entry = get_action_tool("investigation_start")
assert entry is not None
description = entry.description.lower()
assert "run /remote and then investigate" in description
assert "placeholder quoted text like 'hello world'" in description
assert "separate second tool call" in description
assert "never drop the quoted investigation" in description
def test_assistant_handoff_description_preserves_bare_alert_guidance() -> None:
entry = get_action_tool("assistant_handoff")
assert entry is not None
description = entry.description.lower()
assert "bare pasted alert json/yaml/key-value blob" in description
assert "did not explicitly ask to investigate" in description
def test_slash_tool_description_preserves_compound_followup_guidance() -> None:
entry = get_action_tool("slash_invoke")
assert entry is not None
description = entry.description.lower()
assert "only the slash-command clause" in description
assert "run /remote and then investigate" in description
assert "investigation_start" in description
def test_synthetic_tool_description_preserves_numeric_id_guidance() -> None:
entry = get_action_tool("synthetic_run")
assert entry is not None
description = entry.description.lower()
assert '"005" -> "005-failover"' in description
assert '"004" -> "004-cpu-saturation-bad-query"' in description
assert "never substitute a neighboring numbered scenario" in description