add CLI parity commands for credential, block, and browser (#4793)
This commit is contained in:
@@ -468,3 +468,311 @@ class TestWorkflowCommands:
|
||||
)
|
||||
|
||||
tool.assert_not_called()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# PR C parity command behavior
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestCredentialParityCommands:
|
||||
def test_credential_list_maps_options(self, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture) -> None:
|
||||
from skyvern.cli import credential as credential_cmd
|
||||
|
||||
tool = AsyncMock(
|
||||
return_value={
|
||||
"ok": True,
|
||||
"action": "skyvern_credential_list",
|
||||
"browser_context": {"mode": "none", "session_id": None, "cdp_url": None},
|
||||
"data": {"credentials": [], "page": 2, "page_size": 25, "count": 0, "has_more": False},
|
||||
"artifacts": [],
|
||||
"timing_ms": {},
|
||||
"warnings": [],
|
||||
"error": None,
|
||||
}
|
||||
)
|
||||
monkeypatch.setattr(credential_cmd, "tool_credential_list", tool)
|
||||
|
||||
credential_cmd.credential_list(page=2, page_size=25, json_output=True)
|
||||
|
||||
assert tool.await_args.kwargs == {"page": 2, "page_size": 25}
|
||||
parsed = json.loads(capsys.readouterr().out)
|
||||
assert parsed["ok"] is True
|
||||
assert parsed["action"] == "skyvern_credential_list"
|
||||
|
||||
def test_credential_get_json_error_exits_nonzero(
|
||||
self, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture
|
||||
) -> None:
|
||||
from skyvern.cli import credential as credential_cmd
|
||||
|
||||
tool = AsyncMock(
|
||||
return_value={
|
||||
"ok": False,
|
||||
"action": "skyvern_credential_get",
|
||||
"browser_context": {"mode": "none", "session_id": None, "cdp_url": None},
|
||||
"data": None,
|
||||
"artifacts": [],
|
||||
"timing_ms": {},
|
||||
"warnings": [],
|
||||
"error": {
|
||||
"code": "INVALID_INPUT",
|
||||
"message": "Invalid credential_id format: 'bad'",
|
||||
"hint": "Credential IDs start with cred_",
|
||||
"details": {},
|
||||
},
|
||||
}
|
||||
)
|
||||
monkeypatch.setattr(credential_cmd, "tool_credential_get", tool)
|
||||
|
||||
with pytest.raises(SystemExit, match="1"):
|
||||
credential_cmd.credential_get(credential_id="bad", json_output=True)
|
||||
|
||||
parsed = json.loads(capsys.readouterr().out)
|
||||
assert parsed["ok"] is False
|
||||
assert "Invalid credential_id format" in parsed["error"]["message"]
|
||||
|
||||
def test_credential_delete_maps_options(
|
||||
self, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture
|
||||
) -> None:
|
||||
from skyvern.cli import credential as credential_cmd
|
||||
|
||||
tool = AsyncMock(
|
||||
return_value={
|
||||
"ok": True,
|
||||
"action": "skyvern_credential_delete",
|
||||
"browser_context": {"mode": "none", "session_id": None, "cdp_url": None},
|
||||
"data": {"credential_id": "cred_123", "deleted": True},
|
||||
"artifacts": [],
|
||||
"timing_ms": {},
|
||||
"warnings": [],
|
||||
"error": None,
|
||||
}
|
||||
)
|
||||
monkeypatch.setattr(credential_cmd, "tool_credential_delete", tool)
|
||||
|
||||
credential_cmd.credential_delete(credential_id="cred_123", json_output=True)
|
||||
|
||||
assert tool.await_args.kwargs == {"credential_id": "cred_123"}
|
||||
parsed = json.loads(capsys.readouterr().out)
|
||||
assert parsed["ok"] is True
|
||||
assert parsed["data"]["deleted"] is True
|
||||
|
||||
|
||||
class TestBlockParityCommands:
|
||||
def test_block_schema_passes_block_type(
|
||||
self, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture
|
||||
) -> None:
|
||||
from skyvern.cli import block as block_cmd
|
||||
|
||||
tool = AsyncMock(
|
||||
return_value={
|
||||
"ok": True,
|
||||
"action": "skyvern_block_schema",
|
||||
"browser_context": {"mode": "none", "session_id": None, "cdp_url": None},
|
||||
"data": {"block_type": "navigation", "schema": {"type": "object"}},
|
||||
"artifacts": [],
|
||||
"timing_ms": {},
|
||||
"warnings": [],
|
||||
"error": None,
|
||||
}
|
||||
)
|
||||
monkeypatch.setattr(block_cmd, "tool_block_schema", tool)
|
||||
|
||||
block_cmd.block_schema(block_type="navigation", json_output=True)
|
||||
|
||||
assert tool.await_args.kwargs == {"block_type": "navigation"}
|
||||
parsed = json.loads(capsys.readouterr().out)
|
||||
assert parsed["ok"] is True
|
||||
assert parsed["data"]["block_type"] == "navigation"
|
||||
|
||||
def test_block_validate_reads_json_from_file(
|
||||
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture
|
||||
) -> None:
|
||||
from skyvern.cli import block as block_cmd
|
||||
|
||||
block_file = tmp_path / "block.json"
|
||||
block_file.write_text('{"block_type":"navigation","label":"step1","navigation_goal":"Go to page"}')
|
||||
|
||||
tool = AsyncMock(
|
||||
return_value={
|
||||
"ok": True,
|
||||
"action": "skyvern_block_validate",
|
||||
"browser_context": {"mode": "none", "session_id": None, "cdp_url": None},
|
||||
"data": {"valid": True, "block_type": "navigation", "label": "step1"},
|
||||
"artifacts": [],
|
||||
"timing_ms": {},
|
||||
"warnings": [],
|
||||
"error": None,
|
||||
}
|
||||
)
|
||||
monkeypatch.setattr(block_cmd, "tool_block_validate", tool)
|
||||
|
||||
block_cmd.block_validate(block_json=f"@{block_file}", json_output=True)
|
||||
|
||||
assert tool.await_args.kwargs == {
|
||||
"block_json": '{"block_type":"navigation","label":"step1","navigation_goal":"Go to page"}'
|
||||
}
|
||||
parsed = json.loads(capsys.readouterr().out)
|
||||
assert parsed["ok"] is True
|
||||
assert parsed["data"]["valid"] is True
|
||||
|
||||
|
||||
class TestBrowserPRCCommands:
|
||||
def test_run_task_uses_resolved_connection(
|
||||
self, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture
|
||||
) -> None:
|
||||
from skyvern.cli.commands import browser as browser_cmd
|
||||
|
||||
monkeypatch.setattr(
|
||||
browser_cmd,
|
||||
"_resolve_connection",
|
||||
lambda _session, _cdp: browser_cmd.ConnectionTarget(mode="cloud", session_id="pbs_123"),
|
||||
)
|
||||
tool = AsyncMock(
|
||||
return_value={
|
||||
"ok": True,
|
||||
"action": "skyvern_run_task",
|
||||
"browser_context": {"mode": "cloud", "session_id": "pbs_123", "cdp_url": None},
|
||||
"data": {"run_id": "run_123", "status": "completed"},
|
||||
"artifacts": [],
|
||||
"timing_ms": {},
|
||||
"warnings": [],
|
||||
"error": None,
|
||||
}
|
||||
)
|
||||
monkeypatch.setattr(browser_cmd, "tool_run_task", tool)
|
||||
|
||||
browser_cmd.run_task(
|
||||
prompt="Find the latest headline",
|
||||
session=None,
|
||||
cdp=None,
|
||||
url="https://news.ycombinator.com",
|
||||
data_extraction_schema='{"type":"object"}',
|
||||
max_steps=5,
|
||||
timeout_seconds=240,
|
||||
json_output=True,
|
||||
)
|
||||
|
||||
assert tool.await_args.kwargs == {
|
||||
"prompt": "Find the latest headline",
|
||||
"session_id": "pbs_123",
|
||||
"cdp_url": None,
|
||||
"url": "https://news.ycombinator.com",
|
||||
"data_extraction_schema": '{"type":"object"}',
|
||||
"max_steps": 5,
|
||||
"timeout_seconds": 240,
|
||||
}
|
||||
parsed = json.loads(capsys.readouterr().out)
|
||||
assert parsed["ok"] is True
|
||||
assert parsed["data"]["run_id"] == "run_123"
|
||||
|
||||
def test_login_json_error_exits_nonzero(
|
||||
self, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture
|
||||
) -> None:
|
||||
from skyvern.cli.commands import browser as browser_cmd
|
||||
|
||||
monkeypatch.setattr(
|
||||
browser_cmd,
|
||||
"_resolve_connection",
|
||||
lambda _session, _cdp: browser_cmd.ConnectionTarget(mode="cloud", session_id="pbs_abc"),
|
||||
)
|
||||
tool = AsyncMock(
|
||||
return_value={
|
||||
"ok": False,
|
||||
"action": "skyvern_login",
|
||||
"browser_context": {"mode": "cloud", "session_id": "pbs_abc", "cdp_url": None},
|
||||
"data": None,
|
||||
"artifacts": [],
|
||||
"timing_ms": {},
|
||||
"warnings": [],
|
||||
"error": {
|
||||
"code": "INVALID_INPUT",
|
||||
"message": "Missing required fields for credential_type='skyvern': credential_id",
|
||||
"hint": "Provide: credential_id",
|
||||
"details": {},
|
||||
},
|
||||
}
|
||||
)
|
||||
monkeypatch.setattr(browser_cmd, "tool_login", tool)
|
||||
|
||||
with pytest.raises(SystemExit, match="1"):
|
||||
browser_cmd.login(
|
||||
credential_type="skyvern",
|
||||
session=None,
|
||||
cdp=None,
|
||||
credential_id=None,
|
||||
json_output=True,
|
||||
)
|
||||
|
||||
kwargs = tool.await_args.kwargs
|
||||
assert kwargs["session_id"] == "pbs_abc"
|
||||
assert kwargs["credential_type"] == "skyvern"
|
||||
parsed = json.loads(capsys.readouterr().out)
|
||||
assert parsed["ok"] is False
|
||||
assert "Missing required fields" in parsed["error"]["message"]
|
||||
|
||||
|
||||
class TestParityErrorFormatting:
|
||||
def test_credential_emit_tool_result_handles_none_message_and_hint(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
from skyvern.cli import credential as credential_cmd
|
||||
|
||||
captured: dict[str, str | bool] = {}
|
||||
|
||||
def _fake_output_error(message: str, *, hint: str = "", json_mode: bool = False, exit_code: int = 1) -> None:
|
||||
captured["message"] = message
|
||||
captured["hint"] = hint
|
||||
captured["json_mode"] = json_mode
|
||||
raise SystemExit(exit_code)
|
||||
|
||||
monkeypatch.setattr(credential_cmd, "output_error", _fake_output_error)
|
||||
|
||||
with pytest.raises(SystemExit, match="1"):
|
||||
credential_cmd._emit_tool_result(
|
||||
{"ok": False, "error": {"message": None, "hint": None}},
|
||||
json_output=False,
|
||||
)
|
||||
|
||||
assert captured == {"message": "Unknown error", "hint": "", "json_mode": False}
|
||||
|
||||
def test_block_emit_tool_result_handles_none_message_and_hint(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
from skyvern.cli import block as block_cmd
|
||||
|
||||
captured: dict[str, str | bool] = {}
|
||||
|
||||
def _fake_output_error(message: str, *, hint: str = "", json_mode: bool = False, exit_code: int = 1) -> None:
|
||||
captured["message"] = message
|
||||
captured["hint"] = hint
|
||||
captured["json_mode"] = json_mode
|
||||
raise SystemExit(exit_code)
|
||||
|
||||
monkeypatch.setattr(block_cmd, "output_error", _fake_output_error)
|
||||
|
||||
with pytest.raises(SystemExit, match="1"):
|
||||
block_cmd._emit_tool_result(
|
||||
{"ok": False, "error": {"message": None, "hint": None}},
|
||||
json_output=False,
|
||||
)
|
||||
|
||||
assert captured == {"message": "Unknown error", "hint": "", "json_mode": False}
|
||||
|
||||
def test_browser_emit_tool_result_handles_none_message_and_hint(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
from skyvern.cli.commands import browser as browser_cmd
|
||||
|
||||
captured: dict[str, str | bool] = {}
|
||||
|
||||
def _fake_output_error(message: str, *, hint: str = "", json_mode: bool = False, exit_code: int = 1) -> None:
|
||||
captured["message"] = message
|
||||
captured["hint"] = hint
|
||||
captured["json_mode"] = json_mode
|
||||
raise SystemExit(exit_code)
|
||||
|
||||
monkeypatch.setattr(browser_cmd, "output_error", _fake_output_error)
|
||||
|
||||
with pytest.raises(SystemExit, match="1"):
|
||||
browser_cmd._emit_tool_result(
|
||||
{"ok": False, "error": {"message": None, "hint": None}},
|
||||
json_output=False,
|
||||
action="login",
|
||||
)
|
||||
|
||||
assert captured == {"message": "Unknown error", "hint": "", "json_mode": False}
|
||||
|
||||
Reference in New Issue
Block a user