feat(SKY-8879) copilot-stack/07: enforcement + overflow recovery (#5519)
Some checks are pending
Auto Create GitHub Release on Version Change / check-version-change (push) Waiting to run
Auto Create GitHub Release on Version Change / create-release (push) Blocked by required conditions
Run tests and pre-commit / Run tests and pre-commit hooks (push) Waiting to run
Run tests and pre-commit / Frontend Lint and Build (push) Waiting to run
Publish Fern Docs / run (push) Waiting to run
Build Skyvern SDK and publish to PyPI / check-version-change (push) Waiting to run
Build Skyvern SDK and publish to PyPI / run-ci (push) Blocked by required conditions
Build Skyvern SDK and publish to PyPI / build-sdk (push) Blocked by required conditions
zizmor / Audit GitHub Actions (push) Waiting to run

This commit is contained in:
Andrew Neilson 2026-04-15 22:21:36 -07:00 committed by GitHub
parent faa2b233cb
commit 7c29ab9d1f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 1572 additions and 9 deletions

View file

@ -500,6 +500,7 @@ def test_discover_switch_targets_finds_claude_code_and_codex(
monkeypatch.setattr("skyvern.cli.mcp_commands._cursor_config_path", lambda: tmp_path / "missing-cursor.json")
monkeypatch.setattr("skyvern.cli.mcp_commands._windsurf_config_path", lambda: tmp_path / "missing-windsurf.json")
monkeypatch.setattr("skyvern.cli.mcp_commands._codex_config_path", lambda: codex_config)
monkeypatch.setattr("skyvern.cli.mcp_commands._hermes_config_path", lambda: tmp_path / "missing-hermes.yaml")
discovered, missing = _discover_switch_targets()
@ -509,4 +510,4 @@ def test_discover_switch_targets_finds_claude_code_and_codex(
assert "Codex" in discovered_by_name
assert discovered_by_name["Codex"].config_format == "codex_toml"
assert discovered_by_name["Codex"].entry_key == "skyvern"
assert {name for name, _ in missing} == {"Claude Desktop", "Cursor", "Windsurf"}
assert {name for name, _ in missing} == {"Claude Desktop", "Cursor", "Windsurf", "Hermes"}

View file

@ -130,8 +130,9 @@ def test_run_mcp_http_transport_wires_auth_middleware(monkeypatch: pytest.Monkey
assert kwargs["path"] == "/mcp"
assert kwargs["stateless_http"] is True
middleware = kwargs["middleware"]
assert len(middleware) == 1
assert middleware[0].cls is run_commands.MCPAPIKeyMiddleware
assert len(middleware) == 2
assert middleware[0].cls is run_commands._ServerCardMiddleware
assert middleware[1].cls is run_commands.MCPAPIKeyMiddleware
set_stateless.assert_has_calls([call(True), call(False)])
cleanup_blocking.assert_called_once()

View file

@ -0,0 +1,130 @@
"""Tests for skyvern setup hermes command."""
from __future__ import annotations
from pathlib import Path
import pytest
from typer.testing import CliRunner
from skyvern.cli.setup_commands import (
_load_yaml_config,
_save_yaml_config,
setup_app,
)
runner = CliRunner()
@pytest.fixture()
def hermes_home(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
"""Create a fake ~/.hermes with global config + 2 profiles."""
home = tmp_path / ".hermes"
home.mkdir()
_save_yaml_config(home / "config.yaml", {"model": {"default": "gpt-4"}})
for name in ("profile-a", "profile-b"):
p = home / "profiles" / name
p.mkdir(parents=True)
_save_yaml_config(p / "config.yaml", {"mcp_servers": {"exa": {"url": "https://exa.ai"}}})
monkeypatch.setattr("skyvern.cli.setup_commands.Path.home", lambda: tmp_path)
monkeypatch.setenv("SKYVERN_API_KEY", "test-key-1234567890")
monkeypatch.setenv("SKYVERN_BASE_URL", "https://api.skyvern.com")
return home
def test_setup_hermes_updates_global_and_profiles(hermes_home: Path) -> None:
"""Remote mode updates global + all profile configs."""
result = runner.invoke(setup_app, ["hermes", "--yes"])
assert result.exit_code == 0, result.output
for config_path in [
hermes_home / "config.yaml",
hermes_home / "profiles" / "profile-a" / "config.yaml",
hermes_home / "profiles" / "profile-b" / "config.yaml",
]:
data = _load_yaml_config(config_path)
assert data is not None
assert "skyvern" in data["mcp_servers"]
assert data["mcp_servers"]["skyvern"]["url"] == "https://api.skyvern.com/mcp/"
assert data["mcp_servers"]["skyvern"]["headers"]["x-api-key"] == "test-key-1234567890"
# Existing exa entry preserved in profiles
profile_a = _load_yaml_config(hermes_home / "profiles" / "profile-a" / "config.yaml")
assert profile_a["mcp_servers"]["exa"]["url"] == "https://exa.ai"
def test_setup_hermes_skips_malformed_profile(hermes_home: Path) -> None:
"""Bad YAML in one profile is skipped, others still updated."""
bad_profile = hermes_home / "profiles" / "profile-a" / "config.yaml"
bad_profile.write_text("{{{{invalid yaml", encoding="utf-8")
result = runner.invoke(setup_app, ["hermes", "--yes"])
assert result.exit_code == 0, result.output
assert "Skipping" in result.output
# profile-b still updated
data = _load_yaml_config(hermes_home / "profiles" / "profile-b" / "config.yaml")
assert data is not None
assert "skyvern" in data["mcp_servers"]
def test_setup_hermes_case_insensitive_key(hermes_home: Path) -> None:
"""Existing 'Skyvern' key (capitalized) is reused, not duplicated."""
config_path = hermes_home / "config.yaml"
data = _load_yaml_config(config_path)
data["mcp_servers"] = {"Skyvern": {"url": "https://old.example.com"}}
_save_yaml_config(config_path, data)
result = runner.invoke(setup_app, ["hermes", "--yes"])
assert result.exit_code == 0, result.output
updated = _load_yaml_config(config_path)
assert updated is not None
# Should reuse 'Skyvern' key, not create a new 'skyvern'
assert "Skyvern" in updated["mcp_servers"]
assert updated["mcp_servers"]["Skyvern"]["url"] == "https://api.skyvern.com/mcp/"
# No duplicate lowercase key
keys = [k for k in updated["mcp_servers"] if k.lower() == "skyvern"]
assert len(keys) == 1
def test_setup_hermes_local_fails_without_base_url(hermes_home: Path, monkeypatch: pytest.MonkeyPatch) -> None:
"""Local mode exits with error when SKYVERN_BASE_URL is missing."""
monkeypatch.delenv("SKYVERN_BASE_URL", raising=False)
monkeypatch.setenv("SKYVERN_API_KEY", "test-key-1234567890")
# Prevent dotenv from providing a base URL
monkeypatch.setattr("skyvern.cli.setup_commands._get_local_env_credentials", lambda: ("test-key", ""))
result = runner.invoke(setup_app, ["hermes", "--local", "--yes"])
assert result.exit_code == 1
assert "SKYVERN_BASE_URL" in result.output
def test_setup_hermes_dry_run_masks_secrets(hermes_home: Path) -> None:
"""Dry-run output does not contain raw API keys."""
result = runner.invoke(setup_app, ["hermes", "--dry-run"])
assert result.exit_code == 0, result.output
assert "test-key-1234567890" not in result.output
# Masked key should appear
assert "****" in result.output
def test_setup_hermes_idempotent_no_backup(hermes_home: Path) -> None:
"""Running setup twice with same config produces no backup on second run."""
# First run
result1 = runner.invoke(setup_app, ["hermes", "--yes"])
assert result1.exit_code == 0
# Count backups
backups_before = list(hermes_home.rglob("*.bak"))
# Second run with same key/url — should be a no-op (exit 0, no error)
result2 = runner.invoke(setup_app, ["hermes", "--yes"])
assert result2.exit_code == 0, result2.output
assert "up to date" in result2.output
backups_after = list(hermes_home.rglob("*.bak"))
# No new backups created on second run
assert len(backups_after) == len(backups_before)