mirror of
https://github.com/Skyvern-AI/skyvern.git
synced 2026-04-28 03:30:10 +00:00
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
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:
parent
faa2b233cb
commit
7c29ab9d1f
12 changed files with 1572 additions and 9 deletions
|
|
@ -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"}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
130
tests/unit/test_setup_hermes.py
Normal file
130
tests/unit/test_setup_hermes.py
Normal 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)
|
||||
Loading…
Add table
Add a link
Reference in a new issue