Skyvern/tests/unit/test_log_artifacts.py

204 lines
8.1 KiB
Python

"""Tests for skyvern.forge.sdk.log_artifacts."""
from unittest.mock import AsyncMock, patch
import pytest
from skyvern.forge.sdk.core import skyvern_context
from skyvern.forge.sdk.log_artifacts import (
save_step_logs,
save_task_logs,
save_workflow_run_block_logs,
save_workflow_run_logs,
)
@pytest.mark.asyncio
async def test_save_workflow_run_logs_no_context_is_noop(monkeypatch: pytest.MonkeyPatch) -> None:
"""Regression test for timeout activity crash.
When ``save_workflow_run_logs`` is called from a code path that lacks a
``skyvern_context`` (e.g. the Temporal timeout activity), it must not raise.
The function's purpose is to flush the in-memory log buffer that lives on
the current context. With no context there is no buffer to flush, so the
call must degrade to a no-op rather than crash the surrounding DB update.
"""
monkeypatch.setattr("skyvern.forge.sdk.log_artifacts.settings.ENABLE_LOG_ARTIFACTS", True)
skyvern_context.reset()
assert skyvern_context.current() is None
with (
patch("skyvern.forge.sdk.log_artifacts._save_log_artifacts", new_callable=AsyncMock) as mock_save,
patch("skyvern.forge.sdk.log_artifacts.LOG") as mock_log,
):
# Must not raise RuntimeError("No skyvern context")
await save_workflow_run_logs("wr_test_no_context")
# And must not attempt to persist anything when there's nothing to flush.
mock_save.assert_not_called()
# Logged at debug — Temporal cleanup is a known routine no-context caller,
# so anything higher would be steady-state noise.
mock_log.debug.assert_called_once()
@pytest.mark.asyncio
async def test_save_workflow_run_logs_with_context_filters_by_run_id(monkeypatch: pytest.MonkeyPatch) -> None:
"""When context is present, we still filter context.log by workflow_run_id."""
monkeypatch.setattr("skyvern.forge.sdk.log_artifacts.settings.ENABLE_LOG_ARTIFACTS", True)
context = skyvern_context.SkyvernContext(
organization_id="o_test",
workflow_run_id="wr_match",
log=[
{"workflow_run_id": "wr_match", "msg": "keep"},
{"workflow_run_id": "wr_other", "msg": "drop"},
],
)
skyvern_context.reset()
skyvern_context.set(context)
try:
with patch("skyvern.forge.sdk.log_artifacts._save_log_artifacts", new_callable=AsyncMock) as mock_save:
await save_workflow_run_logs("wr_match")
mock_save.assert_awaited_once()
kwargs = mock_save.await_args.kwargs
assert kwargs["organization_id"] == "o_test"
assert kwargs["workflow_run_id"] == "wr_match"
assert kwargs["log"] == [{"workflow_run_id": "wr_match", "msg": "keep"}]
finally:
skyvern_context.reset()
@pytest.mark.asyncio
async def test_save_step_logs_no_context_is_noop(monkeypatch: pytest.MonkeyPatch) -> None:
"""save_step_logs must tolerate a missing skyvern_context."""
monkeypatch.setattr("skyvern.forge.sdk.log_artifacts.settings.ENABLE_LOG_ARTIFACTS", True)
skyvern_context.reset()
assert skyvern_context.current() is None
with (
patch("skyvern.forge.sdk.log_artifacts._save_log_artifacts", new_callable=AsyncMock) as mock_save,
patch("skyvern.forge.sdk.log_artifacts.LOG") as mock_log,
):
await save_step_logs("step_test_no_context")
mock_save.assert_not_called()
mock_log.debug.assert_called_once()
@pytest.mark.asyncio
async def test_save_step_logs_with_context_filters_by_step_id(monkeypatch: pytest.MonkeyPatch) -> None:
"""save_step_logs still filters context.log by step_id when context is present."""
monkeypatch.setattr("skyvern.forge.sdk.log_artifacts.settings.ENABLE_LOG_ARTIFACTS", True)
context = skyvern_context.SkyvernContext(
organization_id="o_test",
step_id="step_match",
log=[
{"step_id": "step_match", "msg": "keep"},
{"step_id": "step_other", "msg": "drop"},
],
)
skyvern_context.reset()
skyvern_context.set(context)
try:
with patch("skyvern.forge.sdk.log_artifacts._save_log_artifacts", new_callable=AsyncMock) as mock_save:
await save_step_logs("step_match")
mock_save.assert_awaited_once()
kwargs = mock_save.await_args.kwargs
assert kwargs["organization_id"] == "o_test"
assert kwargs["step_id"] == "step_match"
assert kwargs["log"] == [{"step_id": "step_match", "msg": "keep"}]
finally:
skyvern_context.reset()
@pytest.mark.asyncio
async def test_save_task_logs_no_context_is_noop(monkeypatch: pytest.MonkeyPatch) -> None:
"""save_task_logs must tolerate a missing skyvern_context."""
monkeypatch.setattr("skyvern.forge.sdk.log_artifacts.settings.ENABLE_LOG_ARTIFACTS", True)
skyvern_context.reset()
assert skyvern_context.current() is None
with (
patch("skyvern.forge.sdk.log_artifacts._save_log_artifacts", new_callable=AsyncMock) as mock_save,
patch("skyvern.forge.sdk.log_artifacts.LOG") as mock_log,
):
await save_task_logs("tsk_test_no_context")
mock_save.assert_not_called()
mock_log.debug.assert_called_once()
@pytest.mark.asyncio
async def test_save_task_logs_with_context_filters_by_task_id(monkeypatch: pytest.MonkeyPatch) -> None:
"""save_task_logs still filters context.log by task_id when context is present."""
monkeypatch.setattr("skyvern.forge.sdk.log_artifacts.settings.ENABLE_LOG_ARTIFACTS", True)
context = skyvern_context.SkyvernContext(
organization_id="o_test",
task_id="tsk_match",
log=[
{"task_id": "tsk_match", "msg": "keep"},
{"task_id": "tsk_other", "msg": "drop"},
],
)
skyvern_context.reset()
skyvern_context.set(context)
try:
with patch("skyvern.forge.sdk.log_artifacts._save_log_artifacts", new_callable=AsyncMock) as mock_save:
await save_task_logs("tsk_match")
mock_save.assert_awaited_once()
kwargs = mock_save.await_args.kwargs
assert kwargs["organization_id"] == "o_test"
assert kwargs["task_id"] == "tsk_match"
assert kwargs["log"] == [{"task_id": "tsk_match", "msg": "keep"}]
finally:
skyvern_context.reset()
@pytest.mark.asyncio
async def test_save_workflow_run_block_logs_no_context_is_noop(monkeypatch: pytest.MonkeyPatch) -> None:
"""save_workflow_run_block_logs must tolerate a missing skyvern_context."""
monkeypatch.setattr("skyvern.forge.sdk.log_artifacts.settings.ENABLE_LOG_ARTIFACTS", True)
skyvern_context.reset()
assert skyvern_context.current() is None
with (
patch("skyvern.forge.sdk.log_artifacts._save_log_artifacts", new_callable=AsyncMock) as mock_save,
patch("skyvern.forge.sdk.log_artifacts.LOG") as mock_log,
):
await save_workflow_run_block_logs("wrb_test_no_context")
mock_save.assert_not_called()
mock_log.debug.assert_called_once()
@pytest.mark.asyncio
async def test_save_workflow_run_block_logs_with_context_filters_by_block_id(monkeypatch: pytest.MonkeyPatch) -> None:
"""save_workflow_run_block_logs still filters context.log by workflow_run_block_id."""
monkeypatch.setattr("skyvern.forge.sdk.log_artifacts.settings.ENABLE_LOG_ARTIFACTS", True)
context = skyvern_context.SkyvernContext(
organization_id="o_test",
workflow_run_block_id="wrb_match",
log=[
{"workflow_run_block_id": "wrb_match", "msg": "keep"},
{"workflow_run_block_id": "wrb_other", "msg": "drop"},
],
)
skyvern_context.reset()
skyvern_context.set(context)
try:
with patch("skyvern.forge.sdk.log_artifacts._save_log_artifacts", new_callable=AsyncMock) as mock_save:
await save_workflow_run_block_logs("wrb_match")
mock_save.assert_awaited_once()
kwargs = mock_save.await_args.kwargs
assert kwargs["organization_id"] == "o_test"
assert kwargs["workflow_run_block_id"] == "wrb_match"
assert kwargs["log"] == [{"workflow_run_block_id": "wrb_match", "msg": "keep"}]
finally:
skyvern_context.reset()