mirror of
https://github.com/Skyvern-AI/skyvern.git
synced 2026-04-28 19:50:42 +00:00
224 lines
8.3 KiB
Python
224 lines
8.3 KiB
Python
"""
|
|
Unit tests for WorkflowTriggerBlock support in cached scripts (SKY-8575).
|
|
|
|
WorkflowTriggerBlock makes zero LLM calls — it's pure orchestration
|
|
(template resolution, workflow dispatch, output collection). These tests
|
|
verify it generates valid code for cached script execution, especially
|
|
inside ForLoop blocks where the original bug manifested.
|
|
"""
|
|
|
|
import ast
|
|
|
|
import libcst as cst
|
|
|
|
from skyvern.core.script_generations.generate_script import (
|
|
_build_for_loop_statement,
|
|
_build_workflow_trigger_statement,
|
|
)
|
|
|
|
|
|
class TestWorkflowTriggerStatement:
|
|
"""Test that _build_workflow_trigger_statement generates valid skyvern.trigger_workflow() calls."""
|
|
|
|
def test_basic_trigger_generates_await_call(self) -> None:
|
|
"""Verify basic trigger block generates await skyvern.trigger_workflow(...)."""
|
|
block = {
|
|
"block_type": "workflow_trigger",
|
|
"label": "trigger_child",
|
|
"workflow_permanent_id": "wpid_123456",
|
|
}
|
|
|
|
result = _build_workflow_trigger_statement(block)
|
|
code = cst.Module(body=[result]).code
|
|
|
|
assert "await skyvern.trigger_workflow" in code
|
|
assert "workflow_permanent_id" in code
|
|
assert "wpid_123456" in code
|
|
assert "label" in code
|
|
assert "trigger_child" in code
|
|
|
|
def test_trigger_with_payload(self) -> None:
|
|
"""Verify payload dict is included in generated code."""
|
|
block = {
|
|
"block_type": "workflow_trigger",
|
|
"label": "trigger_with_data",
|
|
"workflow_permanent_id": "wpid_999",
|
|
"payload": {"zip_code": "90210", "state": "CA"},
|
|
}
|
|
|
|
result = _build_workflow_trigger_statement(block)
|
|
code = cst.Module(body=[result]).code
|
|
|
|
assert "payload" in code
|
|
assert "zip_code" in code
|
|
assert "90210" in code
|
|
|
|
def test_trigger_with_wait_for_completion(self) -> None:
|
|
"""Verify wait_for_completion flag is included."""
|
|
block = {
|
|
"block_type": "workflow_trigger",
|
|
"label": "sync_trigger",
|
|
"workflow_permanent_id": "wpid_sync",
|
|
"wait_for_completion": True,
|
|
}
|
|
|
|
result = _build_workflow_trigger_statement(block)
|
|
code = cst.Module(body=[result]).code
|
|
|
|
assert "wait_for_completion" in code
|
|
|
|
def test_trigger_async_fire_and_forget(self) -> None:
|
|
"""Verify async trigger generates wait_for_completion=False."""
|
|
block = {
|
|
"block_type": "workflow_trigger",
|
|
"label": "async_trigger",
|
|
"workflow_permanent_id": "wpid_async",
|
|
"wait_for_completion": False,
|
|
}
|
|
|
|
result = _build_workflow_trigger_statement(block)
|
|
code = cst.Module(body=[result]).code
|
|
|
|
assert "wait_for_completion = False" in code or "wait_for_completion=False" in code
|
|
|
|
def test_trigger_with_parent_browser_session(self) -> None:
|
|
"""Verify use_parent_browser_session is included when True."""
|
|
block = {
|
|
"block_type": "workflow_trigger",
|
|
"label": "shared_browser",
|
|
"workflow_permanent_id": "wpid_browser",
|
|
"use_parent_browser_session": True,
|
|
}
|
|
|
|
result = _build_workflow_trigger_statement(block)
|
|
code = cst.Module(body=[result]).code
|
|
|
|
assert "use_parent_browser_session" in code
|
|
|
|
def test_trigger_without_parent_browser_session_omits_it(self) -> None:
|
|
"""When use_parent_browser_session is False, it should be omitted."""
|
|
block = {
|
|
"block_type": "workflow_trigger",
|
|
"label": "no_shared_browser",
|
|
"workflow_permanent_id": "wpid_no_browser",
|
|
"use_parent_browser_session": False,
|
|
}
|
|
|
|
result = _build_workflow_trigger_statement(block)
|
|
code = cst.Module(body=[result]).code
|
|
|
|
assert "use_parent_browser_session" not in code
|
|
|
|
def test_trigger_with_explicit_browser_session_id(self) -> None:
|
|
"""Verify explicit browser_session_id is included."""
|
|
block = {
|
|
"block_type": "workflow_trigger",
|
|
"label": "explicit_session",
|
|
"workflow_permanent_id": "wpid_session",
|
|
"browser_session_id": "pbs_12345",
|
|
}
|
|
|
|
result = _build_workflow_trigger_statement(block)
|
|
code = cst.Module(body=[result]).code
|
|
|
|
assert "browser_session_id" in code
|
|
assert "pbs_12345" in code
|
|
|
|
def test_trigger_compiles_as_valid_python(self) -> None:
|
|
"""Verify the generated statement is syntactically valid Python."""
|
|
block = {
|
|
"block_type": "workflow_trigger",
|
|
"label": "compile_test",
|
|
"workflow_permanent_id": "wpid_compile",
|
|
"payload": {"key": "value"},
|
|
"wait_for_completion": True,
|
|
"use_parent_browser_session": True,
|
|
}
|
|
|
|
result = _build_workflow_trigger_statement(block)
|
|
code = cst.Module(body=[result]).code
|
|
|
|
# Wrap in async function so the await is valid
|
|
wrapped = "async def _test():\n" + "\n".join(f" {line}" for line in code.strip().splitlines())
|
|
ast.parse(wrapped) # Should not raise SyntaxError
|
|
|
|
|
|
class TestWorkflowTriggerInsideForLoop:
|
|
"""Test the key bug scenario: workflow_trigger inside a for_loop (SKY-8575)."""
|
|
|
|
def test_forloop_with_trigger_block_generates_valid_code(self) -> None:
|
|
"""A ForLoop containing a workflow_trigger block should produce a valid async for
|
|
statement with the trigger call in its body — not a no-op comment."""
|
|
forloop_block = {
|
|
"block_type": "for_loop",
|
|
"label": "iterate_zip_codes",
|
|
"loop_variable_reference": "{{ zip_codes }}",
|
|
"loop_blocks": [
|
|
{
|
|
"block_type": "workflow_trigger",
|
|
"label": "trigger_per_zip",
|
|
"workflow_permanent_id": "wpid_child_workflow",
|
|
"payload": {"zip": "{{ current_value }}"},
|
|
"wait_for_completion": True,
|
|
},
|
|
],
|
|
}
|
|
|
|
result = _build_for_loop_statement("iterate_zip_codes", forloop_block)
|
|
code = cst.Module(body=[result]).code
|
|
|
|
# The loop body should contain the trigger call, not 'Unknown block type'
|
|
assert "skyvern.trigger_workflow" in code
|
|
assert "Unknown block type" not in code
|
|
assert "wpid_child_workflow" in code
|
|
|
|
def test_forloop_with_trigger_and_extraction_generates_both(self) -> None:
|
|
"""A ForLoop with both a trigger and extraction block should include both in the body."""
|
|
forloop_block = {
|
|
"block_type": "for_loop",
|
|
"label": "multi_block_loop",
|
|
"loop_variable_reference": "{{ items }}",
|
|
"loop_blocks": [
|
|
{
|
|
"block_type": "workflow_trigger",
|
|
"label": "trigger_step",
|
|
"workflow_permanent_id": "wpid_target",
|
|
"wait_for_completion": True,
|
|
},
|
|
{
|
|
"block_type": "extraction",
|
|
"label": "extract_step",
|
|
"data_extraction_goal": "Get result",
|
|
"task_id": "task_001",
|
|
},
|
|
],
|
|
}
|
|
|
|
result = _build_for_loop_statement("multi_block_loop", forloop_block)
|
|
code = cst.Module(body=[result]).code
|
|
|
|
# Both blocks should appear in the loop body
|
|
assert "skyvern.trigger_workflow" in code
|
|
assert "extract_step" in code
|
|
|
|
def test_forloop_with_trigger_compiles(self) -> None:
|
|
"""Full compilation test: ForLoop + trigger block generates valid Python."""
|
|
forloop_block = {
|
|
"block_type": "for_loop",
|
|
"label": "compile_loop",
|
|
"loop_variable_reference": "{{ data }}",
|
|
"loop_blocks": [
|
|
{
|
|
"block_type": "workflow_trigger",
|
|
"label": "child_trigger",
|
|
"workflow_permanent_id": "wpid_test",
|
|
},
|
|
],
|
|
}
|
|
|
|
result = _build_for_loop_statement("compile_loop", forloop_block)
|
|
code = cst.Module(body=[result]).code
|
|
|
|
# Wrap in an async function for valid Python
|
|
wrapped = "async def _test():\n" + "\n".join(f" {line}" for line in code.strip().splitlines())
|
|
ast.parse(wrapped) # Should not raise SyntaxError
|