Files
opensandbox-group--opensandbox/cli/tests/test_skills.py
T
wehub-resource-sync e0e362d700
Deploy Docs Pages / build (push) Waiting to run
Deploy Docs Pages / deploy (push) Blocked by required conditions
Real E2E Tests / Real E2E CI (push) Blocked by required conditions
SDK Tests / SDK CI (push) Blocked by required conditions
SDK Tests / changes (push) Successful in 2m29s
Real E2E Tests / changes (push) Successful in 2m29s
Real E2E Tests / Python E2E (docker bridge) (push) Waiting to run
Real E2E Tests / C# E2E (docker bridge) (push) Waiting to run
Real E2E Tests / Go E2E (docker bridge) (push) Waiting to run
Real E2E Tests / Java E2E (docker bridge) (push) Waiting to run
Real E2E Tests / JavaScript E2E (docker bridge) (push) Waiting to run
SDK Tests / Python SDK Tests (sandbox) (push) Waiting to run
SDK Tests / CLI Quality (push) Waiting to run
SDK Tests / CLI Tests (push) Waiting to run
SDK Tests / Python SDK Quality (code-interpreter) (push) Waiting to run
SDK Tests / Python SDK Quality (sandbox) (push) Waiting to run
SDK Tests / Python SDK Tests (code-interpreter) (push) Waiting to run
SDK Tests / JavaScript SDK Quality And Tests (code-interpreter) (push) Waiting to run
SDK Tests / JavaScript SDK Quality And Tests (sandbox) (push) Waiting to run
SDK Tests / Kotlin SDK Quality And Tests (sandbox) (push) Waiting to run
SDK Tests / Kotlin SDK Quality And Tests (code-interpreter) (push) Waiting to run
SDK Tests / C# SDK Quality And Tests (code-interpreter) (push) Waiting to run
SDK Tests / C# SDK Quality And Tests (sandbox) (push) Waiting to run
SDK Tests / Go SDK Quality And Tests (push) Waiting to run
chore: import upstream snapshot with attribution
2026-07-13 13:39:33 +08:00

672 lines
26 KiB
Python

# Copyright 2026 Alibaba Group Holding Ltd.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Tests for bundled skill install/list/show/uninstall flows."""
from __future__ import annotations
import importlib.resources
import json
from pathlib import Path
from types import SimpleNamespace
import pytest
from click import Command, Group, Option
from click.testing import CliRunner
from opensandbox_cli.commands.skills import _TARGETS, skills_group
from opensandbox_cli.main import cli
from opensandbox_cli.output import OutputFormatter
from opensandbox_cli.skill_registry import list_builtin_skills
@pytest.fixture()
def isolated_skill_targets(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
patched = {
"claude": {
**_TARGETS["claude"],
"scopes": {
"project": {
**_TARGETS["claude"]["scopes"]["project"],
"dest_dir": tmp_path / ".claude" / "skills",
},
"global": {
**_TARGETS["claude"]["scopes"]["global"],
"dest_dir": tmp_path / "home" / ".claude" / "skills",
},
},
},
"cursor": {
**_TARGETS["cursor"],
"scopes": {
"project": {
**_TARGETS["cursor"]["scopes"]["project"],
"dest_dir": tmp_path / ".cursor" / "rules",
},
"global": {
**_TARGETS["cursor"]["scopes"]["global"],
"dest_dir": tmp_path / "home" / ".cursor" / "rules",
},
},
},
"codex": {
**_TARGETS["codex"],
"scopes": {
"project": {
**_TARGETS["codex"]["scopes"]["project"],
"dest_dir": tmp_path / ".codex" / "skills",
},
"global": {
**_TARGETS["codex"]["scopes"]["global"],
"dest_dir": tmp_path / "home" / ".codex" / "skills",
},
},
},
"copilot": {
**_TARGETS["copilot"],
"scopes": {
"project": {
**_TARGETS["copilot"]["scopes"]["project"],
"dest_file": tmp_path / ".github" / "copilot-instructions.md",
},
"global": {
**_TARGETS["copilot"]["scopes"]["global"],
"dest_file": tmp_path / "home" / ".github" / "copilot-instructions.md",
},
},
},
"windsurf": {
**_TARGETS["windsurf"],
"scopes": {
"project": {
**_TARGETS["windsurf"]["scopes"]["project"],
"dest_file": tmp_path / ".windsurfrules",
},
"global": {
**_TARGETS["windsurf"]["scopes"]["global"],
"dest_file": tmp_path / "home" / ".windsurfrules",
},
},
},
"cline": {
**_TARGETS["cline"],
"scopes": {
"project": {
**_TARGETS["cline"]["scopes"]["project"],
"dest_file": tmp_path / ".clinerules",
},
"global": {
**_TARGETS["cline"]["scopes"]["global"],
"dest_file": tmp_path / "home" / ".clinerules",
},
},
},
"opencode": {
**_TARGETS["opencode"],
"scopes": {
"project": {
**_TARGETS["opencode"]["scopes"]["project"],
"dest_dir": tmp_path / ".agents" / "skills",
},
"global": {
**_TARGETS["opencode"]["scopes"]["global"],
"dest_dir": tmp_path / "home" / ".agents" / "skills",
},
},
},
}
monkeypatch.setattr("opensandbox_cli.commands.skills._TARGETS", patched)
class TestSkillsCommands:
def test_list_supports_json_output(
self,
runner: CliRunner,
isolated_skill_targets: None,
) -> None:
result = runner.invoke(
skills_group,
["list"],
obj=SimpleNamespace(output=OutputFormatter("json", color=False)),
)
assert result.exit_code == 0
data = json.loads(result.output)
assert "skills" in data
assert "targets" in data
assert any(skill["slug"] == "sandbox-troubleshooting" for skill in data["skills"])
assert any(skill["slug"] == "sandbox-lifecycle" for skill in data["skills"])
def test_show_supports_json_output(
self,
runner: CliRunner,
isolated_skill_targets: None,
) -> None:
result = runner.invoke(
skills_group,
["show", "sandbox-lifecycle"],
obj=SimpleNamespace(output=OutputFormatter("json", color=False)),
)
assert result.exit_code == 0
data = json.loads(result.output)
assert data["skill"] == "sandbox-lifecycle"
assert data["title"] == "OpenSandbox Sandbox Lifecycle"
assert '"defaultAction": "deny"' in data["json_shapes"]
def test_install_without_args_prints_guidance_and_does_not_install(
self,
runner: CliRunner,
isolated_skill_targets: None,
tmp_path: Path,
) -> None:
result = runner.invoke(skills_group, ["install"])
assert result.exit_code != 0
assert "Install guidance:" in result.output
assert "osb skills install <skill-name> --target <tool> --scope <scope>" in result.output
assert not (tmp_path / ".claude" / "skills" / "sandbox-troubleshooting.md").exists()
def test_install_with_skill_but_without_target_prints_guidance(
self,
runner: CliRunner,
isolated_skill_targets: None,
) -> None:
result = runner.invoke(skills_group, ["install", "sandbox-troubleshooting"])
assert result.exit_code != 0
assert "Install guidance:" in result.output
assert "Missing required option '--target'" in result.output
def test_install_with_all_builtins_but_without_target_prints_guidance(
self,
runner: CliRunner,
isolated_skill_targets: None,
) -> None:
result = runner.invoke(skills_group, ["install", "--all-builtins"])
assert result.exit_code != 0
assert "Install guidance:" in result.output
assert "Missing required option '--target'" in result.output
def test_install_copy_target_creates_named_skill_file(
self,
runner: CliRunner,
isolated_skill_targets: None,
tmp_path: Path,
) -> None:
result = runner.invoke(
skills_group,
["install", "sandbox-troubleshooting", "--target", "claude", "--scope", "project"],
)
assert result.exit_code == 0
dest = tmp_path / ".claude" / "skills" / "sandbox-troubleshooting.md"
assert dest.exists()
content = dest.read_text(encoding="utf-8")
assert content.startswith("---\nname: sandbox-troubleshooting")
def test_install_codex_creates_skill_directory_with_frontmatter(
self,
runner: CliRunner,
isolated_skill_targets: None,
tmp_path: Path,
) -> None:
result = runner.invoke(
skills_group,
["install", "sandbox-troubleshooting", "--target", "codex", "--scope", "project"],
)
assert result.exit_code == 0
dest = tmp_path / ".codex" / "skills" / "sandbox-troubleshooting" / "SKILL.md"
content = dest.read_text(encoding="utf-8")
assert content.startswith("---\nname: sandbox-troubleshooting")
assert "# OpenSandbox Sandbox Troubleshooting" in content
def test_install_reports_already_present_without_prompt(
self,
runner: CliRunner,
isolated_skill_targets: None,
tmp_path: Path,
) -> None:
first = runner.invoke(
skills_group,
["install", "sandbox-troubleshooting", "--target", "codex", "--scope", "project"],
)
assert first.exit_code == 0
second = runner.invoke(
skills_group,
["install", "sandbox-troubleshooting", "--target", "codex", "--scope", "project"],
)
assert second.exit_code == 0
assert "already_present" in second.output
dest = tmp_path / ".codex" / "skills" / "sandbox-troubleshooting" / "SKILL.md"
assert dest.exists()
def test_install_all_builtins_to_codex_creates_skill_directories(
self,
runner: CliRunner,
isolated_skill_targets: None,
tmp_path: Path,
) -> None:
result = runner.invoke(
skills_group,
["install", "--all-builtins", "--target", "codex", "--scope", "project"],
)
assert result.exit_code == 0
assert "Install plan:" in result.output
assert "install one file per skill" in result.output
for skill in list_builtin_skills():
dest = tmp_path / ".codex" / "skills" / skill.slug / "SKILL.md"
assert dest.exists()
def test_install_supports_json_output(
self,
runner: CliRunner,
isolated_skill_targets: None,
) -> None:
result = runner.invoke(
skills_group,
["install", "network-egress", "--target", "codex", "--scope", "project"],
obj=SimpleNamespace(output=OutputFormatter("json", color=False)),
)
assert result.exit_code == 0
data = json.loads(result.output)
assert data["requires_restart"] is True
assert data["operations"][0]["skill"] == "network-egress"
assert data["operations"][0]["status"] == "installed"
def test_install_rejects_skill_name_and_all_builtins_together(
self,
runner: CliRunner,
isolated_skill_targets: None,
) -> None:
result = runner.invoke(
skills_group,
["install", "sandbox-troubleshooting", "--all-builtins"],
)
assert result.exit_code != 0
assert "either a skill name or --all-builtins" in result.output
def test_install_to_global_codex_uses_global_path(
self,
runner: CliRunner,
isolated_skill_targets: None,
tmp_path: Path,
) -> None:
result = runner.invoke(
skills_group,
["install", "sandbox-troubleshooting", "--target", "codex", "--scope", "global"],
)
assert result.exit_code == 0
assert (tmp_path / "home" / ".codex" / "skills" / "sandbox-troubleshooting" / "SKILL.md").exists()
def test_install_to_project_opencode_creates_skill_directory(
self,
runner: CliRunner,
isolated_skill_targets: None,
tmp_path: Path,
) -> None:
result = runner.invoke(
skills_group,
["install", "network-egress", "--target", "opencode", "--scope", "project"],
)
assert result.exit_code == 0
dest = tmp_path / ".agents" / "skills" / "network-egress" / "SKILL.md"
assert dest.exists()
assert dest.read_text(encoding="utf-8").startswith("---\nname: network-egress")
def test_show_prints_skill_metadata_and_content(
self,
runner: CliRunner,
isolated_skill_targets: None,
) -> None:
result = runner.invoke(skills_group, ["show", "file-operations"])
assert result.exit_code == 0
assert "Skill: file-operations" in result.output
assert "Title: OpenSandbox File Operations" in result.output
assert "When To Use:" in result.output
assert "Quick Start:" in result.output
assert "Minimal Closed Loops:" in result.output
assert "Full Skill:" in result.output
assert "osb file cat" in result.output
def test_show_supports_new_network_egress_skill(
self,
runner: CliRunner,
isolated_skill_targets: None,
) -> None:
result = runner.invoke(skills_group, ["show", "network-egress"])
assert result.exit_code == 0
assert "Skill: network-egress" in result.output
assert "Quick Start:" in result.output
assert "osb egress patch" in result.output
def test_show_supports_credential_vault_skill(
self,
runner: CliRunner,
isolated_skill_targets: None,
) -> None:
result = runner.invoke(skills_group, ["show", "credential-vault"])
assert result.exit_code == 0
assert "Skill: credential-vault" in result.output
assert "Credential Vault" in result.output
assert "osb credential-vault create" in result.output
def test_show_surfaces_json_shapes_for_lifecycle_skill(
self,
runner: CliRunner,
isolated_skill_targets: None,
) -> None:
result = runner.invoke(skills_group, ["show", "sandbox-lifecycle"])
assert result.exit_code == 0
assert "JSON Shapes:" in result.output
assert '"defaultAction": "deny"' in result.output
assert '"mountPath": "/workspace/data"' in result.output
def test_list_reports_all_builtins(
self,
runner: CliRunner,
isolated_skill_targets: None,
) -> None:
result = runner.invoke(skills_group, ["list"])
assert result.exit_code == 0
assert "aggregate into one instructions file" in result.output
assert "install one file per skill" in result.output
for skill in list_builtin_skills():
assert skill.slug in result.output
def test_list_reports_not_installed_when_append_target_file_exists_without_marker(
self,
runner: CliRunner,
isolated_skill_targets: None,
tmp_path: Path,
) -> None:
dest = tmp_path / ".github" / "copilot-instructions.md"
dest.parent.mkdir(parents=True, exist_ok=True)
dest.write_text("user custom instructions\n", encoding="utf-8")
result = runner.invoke(skills_group, ["list"])
assert result.exit_code == 0
assert "copilot" in result.output
assert "not installed" in result.output
def test_uninstall_append_target_preserves_non_skill_content(
self,
runner: CliRunner,
isolated_skill_targets: None,
tmp_path: Path,
) -> None:
install_result = runner.invoke(
skills_group,
["install", "sandbox-troubleshooting", "--target", "copilot", "--scope", "project"],
)
assert install_result.exit_code == 0
dest = tmp_path / ".github" / "copilot-instructions.md"
original = dest.read_text(encoding="utf-8")
dest.write_text("team rules\n\n" + original, encoding="utf-8")
uninstall_result = runner.invoke(
skills_group,
["uninstall", "sandbox-troubleshooting", "--target", "copilot", "--scope", "project"],
)
assert uninstall_result.exit_code == 0
assert dest.read_text(encoding="utf-8") == "team rules\n"
def test_uninstall_supports_json_output(
self,
runner: CliRunner,
isolated_skill_targets: None,
) -> None:
install_result = runner.invoke(
skills_group,
["install", "sandbox-troubleshooting", "--target", "codex", "--scope", "project"],
)
assert install_result.exit_code == 0
uninstall_result = runner.invoke(
skills_group,
["uninstall", "sandbox-troubleshooting", "--target", "codex", "--scope", "project"],
obj=SimpleNamespace(output=OutputFormatter("json", color=False)),
)
assert uninstall_result.exit_code == 0
data = json.loads(uninstall_result.output)
assert data["operations"][0]["status"] == "removed"
def test_reinstall_append_target_does_not_duplicate_skill_block(
self,
runner: CliRunner,
isolated_skill_targets: None,
tmp_path: Path,
) -> None:
first = runner.invoke(
skills_group,
["install", "sandbox-troubleshooting", "--target", "copilot", "--scope", "project"],
)
assert first.exit_code == 0
second = runner.invoke(
skills_group,
["install", "sandbox-troubleshooting", "--target", "copilot", "--scope", "project", "--force"],
)
assert second.exit_code == 0
dest = tmp_path / ".github" / "copilot-instructions.md"
content = dest.read_text(encoding="utf-8")
assert content.count("<!-- BEGIN opensandbox-sandbox-troubleshooting -->") == 1
def test_install_all_builtins_to_copy_target_creates_new_skill_files(
self,
runner: CliRunner,
isolated_skill_targets: None,
tmp_path: Path,
) -> None:
result = runner.invoke(
skills_group,
["install", "--all-builtins", "--target", "claude", "--scope", "project"],
)
assert result.exit_code == 0
assert "Install plan:" in result.output
assert "install one file per skill" in result.output
assert (tmp_path / ".claude" / "skills" / "network-egress.md").exists()
assert (tmp_path / ".claude" / "skills" / "sandbox-troubleshooting.md").exists()
def _read_builtin_skill(package_file: str) -> str:
resource = importlib.resources.files("opensandbox_cli") / "skills" / package_file
return Path(str(resource)).read_text(encoding="utf-8")
def _command(path: list[str]) -> Command:
current: Command = cli
for part in path:
assert isinstance(current, Group), f"{current.name} is not a command group"
current = current.commands[part]
return current
def _option_names(command: Command) -> set[str]:
names: set[str] = set()
for param in command.params:
if isinstance(param, Option):
names.update(param.opts)
names.update(param.secondary_opts)
return names
class TestSkillContentQuality:
def test_sandbox_troubleshooting_keeps_triage_and_diagnostics_contract(self) -> None:
content = _read_builtin_skill("opensandbox-sandbox-troubleshooting.md")
assert "## Triage Order" in content
assert "osb sandbox get <sandbox-id> -o json" in content
assert "osb diagnostics events <sandbox-id> --scope lifecycle -o raw" in content
assert "osb diagnostics events <sandbox-id> --scope runtime -o raw" in content
assert "osb diagnostics logs <sandbox-id> --scope container -o raw" in content
assert "## Diagnostics Streams" in content
assert "## Evidence Semantics" in content
assert "## URL Delivery" in content
assert "truncated: true" in content
assert "osb devops" not in content
assert "## Symptom To Command Mapping" in content
def test_builtin_skills_do_not_reference_legacy_devops_diagnostics(self) -> None:
skills_dir = Path("src/opensandbox_cli/skills")
forbidden = (
"osb devops inspect",
"osb devops summary",
"devops inspect",
"devops summary",
)
violations: list[str] = []
for skill_path in sorted(skills_dir.glob("*.md")):
content = skill_path.read_text(encoding="utf-8")
for term in forbidden:
if term in content:
violations.append(f"{skill_path.name}: {term}")
assert violations == []
def test_lifecycle_skill_keeps_json_shapes_and_health_guidance(self) -> None:
content = _read_builtin_skill("opensandbox-sandbox-lifecycle.md")
assert "## JSON Shapes" in content
assert '"defaultAction": "deny"' in content
assert '"mountPath": "/workspace/data"' in content
assert "Prefer `health` over assuming readiness from `create` output alone" in content
def test_sandbox_troubleshooting_keeps_cli_first_and_http_fallback_guidance(self) -> None:
content = _read_builtin_skill("opensandbox-sandbox-troubleshooting.md")
assert "## Operating Rules" in content
assert "use CLI commands when `osb` is available" in content
assert "use HTTP only when the CLI is unavailable" in content
assert "Use raw HTTP only after domain, protocol, and API key expectations are explicit." in content
assert "## Symptom To Command Mapping" in content
class TestSkillCliAlignment:
def test_command_execution_skill_matches_command_cli(self) -> None:
run_cmd = _command(["command", "run"])
session_run_cmd = _command(["command", "session", "run"])
logs_cmd = _command(["command", "logs"])
session_delete_cmd = _command(["command", "session", "delete"])
assert {"-d", "--background", "-w", "--workdir", "-t", "--timeout", "-o", "--output"} <= _option_names(run_cmd)
assert {"-w", "--workdir", "-t", "--timeout", "-o", "--output"} <= _option_names(session_run_cmd)
assert {"--cursor", "-o", "--output"} <= _option_names(logs_cmd)
assert {"-o", "--output"} <= _option_names(session_delete_cmd)
def test_sandbox_lifecycle_skill_matches_sandbox_cli(self) -> None:
create_cmd = _command(["sandbox", "create"])
resume_cmd = _command(["sandbox", "resume"])
endpoint_cmd = _command(["sandbox", "endpoint"])
metrics_cmd = _command(["sandbox", "metrics"])
assert {
"-i", "--image", "-t", "--timeout", "--entrypoint", "--network-policy-file",
"--credential-proxy", "--volumes-file", "--skip-health-check",
"--ready-timeout", "-o", "--output",
} <= _option_names(create_cmd)
assert {"--skip-health-check", "--resume-timeout", "-o", "--output"} <= _option_names(resume_cmd)
assert {"-p", "--port", "-o", "--output"} <= _option_names(endpoint_cmd)
assert {"--watch", "-o", "--output"} <= _option_names(metrics_cmd)
def test_file_operations_skill_matches_file_cli(self) -> None:
expected_subcommands = {
"cat", "write", "upload", "download", "rm", "mv", "mkdir",
"rmdir", "search", "info", "chmod", "replace",
}
file_group = _command(["file"])
assert isinstance(file_group, Group)
assert expected_subcommands <= set(file_group.commands)
assert {"-c", "--content", "--encoding", "--mode", "--owner", "--group", "-o", "--output"} <= _option_names(
_command(["file", "write"])
)
assert {"-p", "--pattern", "-o", "--output"} <= _option_names(_command(["file", "search"]))
assert {"--mode", "--owner", "--group", "-o", "--output"} <= _option_names(_command(["file", "chmod"]))
assert {"--old", "--new", "-o", "--output"} <= _option_names(_command(["file", "replace"]))
def test_network_egress_diagnostics_and_devops_skills_match_cli(self) -> None:
patch_cmd = _command(["egress", "patch"])
diag_logs_cmd = _command(["diagnostics", "logs"])
diag_events_cmd = _command(["diagnostics", "events"])
logs_cmd = _command(["devops", "logs"])
events_cmd = _command(["devops", "events"])
summary_cmd = _command(["devops", "summary"])
assert {"--rule", "-o", "--output"} <= _option_names(patch_cmd)
assert {"--scope", "-s", "-o", "--output"} <= _option_names(diag_logs_cmd)
assert {"--scope", "-s", "-o", "--output"} <= _option_names(diag_events_cmd)
assert {"--tail", "-n", "--since", "-s", "-o", "--output"} <= _option_names(logs_cmd)
assert {"--limit", "-l", "-o", "--output"} <= _option_names(events_cmd)
assert {"--tail", "-n", "--event-limit", "-o", "--output"} <= _option_names(summary_cmd)
def test_credential_vault_skill_matches_cli(self) -> None:
vault_group = _command(["credential-vault"])
assert isinstance(vault_group, Group)
assert {"create", "get", "patch", "delete", "credential", "binding"} <= set(vault_group.commands)
assert {"--file", "-o", "--output"} <= _option_names(_command(["credential-vault", "create"]))
assert {"--file", "-o", "--output"} <= _option_names(_command(["credential-vault", "patch"]))
assert {"-o", "--output"} <= _option_names(_command(["credential-vault", "get"]))
assert {"-o", "--output"} <= _option_names(_command(["credential-vault", "delete"]))
assert {"-o", "--output"} <= _option_names(_command(["credential-vault", "credential", "list"]))
assert {"-o", "--output"} <= _option_names(_command(["credential-vault", "binding", "get"]))
def test_skill_osb_examples_use_explicit_output_formats(self) -> None:
allowed_without_output = {
"osb --version",
}
skills_dir = Path("src/opensandbox_cli/skills")
missing_output: list[str] = []
for skill_path in sorted(skills_dir.glob("*.md")):
in_block = False
for line in skill_path.read_text(encoding="utf-8").splitlines():
stripped = line.strip()
if stripped.startswith("```bash"):
in_block = True
continue
if in_block and stripped == "```":
in_block = False
continue
if not in_block or not stripped.startswith("osb "):
continue
if stripped in allowed_without_output:
continue
if " -o " not in stripped:
missing_output.append(f"{skill_path.name}: {stripped}")
assert missing_output == []