test(SKY-11845): delete verified-redundant unit tests (zero coverage loss) (#7113)

Co-authored-by: Suchintan Singh <suchintan@skyvern.com>
This commit is contained in:
Suchintan 2026-07-06 11:04:47 -04:00 committed by GitHub
parent 8ab0db16d1
commit f2f3399e67
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 28 additions and 706 deletions

View file

@ -5,7 +5,6 @@ They do NOT require an LLM key or Playwright — they test infrastructure only.
"""
import sys
from pathlib import Path
import pytest
@ -32,38 +31,6 @@ def test_no_cloud_in_sys_modules() -> None:
)
@pytest.mark.asyncio
async def test_bootstrap_creates_db() -> None:
"""Embedded bootstrap creates SQLite tables, org, and token."""
from skyvern import Skyvern
skyvern = Skyvern.local(use_in_memory_db=True)
try:
workflows = await skyvern.get_workflows()
assert workflows is not None
finally:
await skyvern.aclose()
@pytest.mark.asyncio
async def test_artifact_tempdir_is_live() -> None:
"""StorageFactory points to an existing temp directory after bootstrap."""
from skyvern import Skyvern
from skyvern.library.embedded_server_factory import EmbeddedClient
skyvern = Skyvern.local(use_in_memory_db=True)
try:
await skyvern.get_workflows()
client = getattr(skyvern, "_embedded_client", None)
assert isinstance(client, EmbeddedClient)
artifact_dir = client.embedded_transport._artifact_dir
assert artifact_dir is not None
assert Path(artifact_dir).exists()
assert "skyvern-artifacts-" in artifact_dir
finally:
await skyvern.aclose()
@pytest.mark.asyncio
async def test_unique_llm_key_no_collision() -> None:
"""Sequential clients (create -> close -> create again) use different LLM keys."""

View file

@ -27,6 +27,12 @@ def _make_file_info(url: str, filename: str, checksum: str | None = None) -> Fil
return FileInfo(url=url, filename=filename, checksum=checksum)
def test_downloaded_file_signature_strips_query_params():
file_info = _make_file_info("https://files/a.pdf?sig=x", "a.pdf", "abc")
assert to_downloaded_file_signature(file_info) == ("a.pdf", "abc", "https://files/a.pdf")
def test_second_block_excludes_first_blocks_files():
"""
Simulate two sequential task blocks that each download a file.

View file

@ -127,13 +127,6 @@ def test_planning_tool_still_trips_name_only_streak() -> None:
assert "LOOP DETECTED" in msg
def test_block_running_tool_is_blocked_by_pending_reconciliation() -> None:
ctx = _ctx(pending_reconciliation_run_id="wr_123")
msg = _tool_loop_error(ctx, "update_and_run_blocks")
assert msg is not None
assert "wr_123" in msg
def test_block_running_tool_blocks_late_retry_after_failed_workflow() -> None:
ctx = _ctx(
last_failed_workflow_yaml="version: '1.0'",

View file

@ -1,270 +1,13 @@
"""Unit tests for iframe support: _locator_scope, frame_switch, frame_main, frame_list."""
"""Unit tests for iframe support."""
from __future__ import annotations
from typing import Any
from unittest.mock import AsyncMock, MagicMock
import pytest
from skyvern.cli.core.browser_ops import do_frame_list, do_frame_main, do_frame_switch
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _make_fake_frame(*, name: str = "", url: str = "about:blank") -> MagicMock:
frame = MagicMock()
frame.name = name
frame.url = url
frame.locator = MagicMock(return_value=MagicMock())
return frame
def _make_fake_page(frames: list[MagicMock] | None = None) -> MagicMock:
"""Build a mock Playwright Page with .locator(), .frames, .main_frame, .frame()."""
page = MagicMock()
all_frames = frames or [_make_fake_frame(name="main", url="https://example.com")]
page.frames = all_frames
page.main_frame = all_frames[0]
page.locator = MagicMock(return_value=MagicMock())
page.frame = MagicMock(return_value=None)
return page
class FakeSkyvernPage:
"""Minimal stand-in for SkyvernPage to test _locator_scope without Playwright."""
def __init__(self, page: Any, working_frame: Any = None) -> None:
self.page = page
self._working_frame = working_frame
@property
def _locator_scope(self) -> Any:
frame = object.__getattribute__(self, "_working_frame")
if frame is not None:
return frame
return object.__getattribute__(self, "page")
class FakeSkyvernBrowserPage(FakeSkyvernPage):
"""Minimal stand-in for SkyvernBrowserPage to test frame methods."""
async def frame_switch(
self, *, selector: str | None = None, name: str | None = None, index: int | None = None
) -> dict[str, Any]:
params = sum(p is not None for p in (selector, name, index))
if params != 1:
raise ValueError("Exactly one of selector, name, or index is required.")
frame = None
if selector is not None:
element = await self.page.query_selector(selector)
if element is None:
raise ValueError(f"Selector '{selector}' did not match any element.")
frame = await element.content_frame()
if frame is None:
raise ValueError(f"Selector '{selector}' did not resolve to an iframe.")
elif name is not None:
frame = self.page.frame(name=name)
if frame is None:
raise ValueError(f"No frame found with name '{name}'.")
elif index is not None:
frames = self.page.frames
if index < 0 or index >= len(frames):
raise ValueError(f"Frame index {index} out of range (0-{len(frames) - 1}).")
frame = frames[index]
self._working_frame = frame
return {
"name": frame.name if frame else None,
"url": frame.url if frame else None,
"selector": selector,
"frame_name": name,
"index": index,
}
def frame_main(self) -> dict[str, str]:
self._working_frame = None
return {"status": "switched_to_main_frame"}
async def frame_list(self) -> list[dict[str, Any]]:
frames = self.page.frames
return [
{"index": i, "name": f.name, "url": f.url, "is_main": f == self.page.main_frame}
for i, f in enumerate(frames)
]
# ---------------------------------------------------------------------------
# _locator_scope tests
# ---------------------------------------------------------------------------
class TestLocatorScope:
def test_returns_page_when_no_frame(self) -> None:
page = _make_fake_page()
sp = FakeSkyvernPage(page)
assert sp._locator_scope is page
def test_returns_frame_when_set(self) -> None:
page = _make_fake_page()
frame = _make_fake_frame(name="iframe1")
sp = FakeSkyvernPage(page, working_frame=frame)
assert sp._locator_scope is frame
def test_returns_page_after_clearing_frame(self) -> None:
page = _make_fake_page()
frame = _make_fake_frame()
sp = FakeSkyvernPage(page, working_frame=frame)
assert sp._locator_scope is frame
sp._working_frame = None
assert sp._locator_scope is page
def test_locator_call_delegates_to_frame(self) -> None:
page = _make_fake_page()
frame = _make_fake_frame()
sp = FakeSkyvernPage(page, working_frame=frame)
sp._locator_scope.locator("#btn")
frame.locator.assert_called_once_with("#btn")
page.locator.assert_not_called()
def test_locator_call_delegates_to_page_when_no_frame(self) -> None:
page = _make_fake_page()
sp = FakeSkyvernPage(page)
sp._locator_scope.locator("#btn")
page.locator.assert_called_once_with("#btn")
# ---------------------------------------------------------------------------
# frame_switch tests
# ---------------------------------------------------------------------------
class TestFrameSwitch:
@pytest.mark.asyncio
async def test_switch_by_selector(self) -> None:
iframe = _make_fake_frame(name="payment", url="https://payment.example.com/v3")
element_mock = MagicMock()
element_mock.content_frame = AsyncMock(return_value=iframe)
page = _make_fake_page()
page.query_selector = AsyncMock(return_value=element_mock)
sp = FakeSkyvernBrowserPage(page)
result = await sp.frame_switch(selector="#payment-frame")
assert sp._working_frame is iframe
assert result["name"] == "payment"
assert result["url"] == "https://payment.example.com/v3"
@pytest.mark.asyncio
async def test_switch_by_name(self) -> None:
iframe = _make_fake_frame(name="checkout", url="https://checkout.com")
page = _make_fake_page()
page.frame = MagicMock(return_value=iframe)
sp = FakeSkyvernBrowserPage(page)
result = await sp.frame_switch(name="checkout")
assert sp._working_frame is iframe
assert result["frame_name"] == "checkout"
@pytest.mark.asyncio
async def test_switch_by_index(self) -> None:
main = _make_fake_frame(name="main", url="https://example.com")
iframe = _make_fake_frame(name="embed", url="https://embed.com")
page = _make_fake_page([main, iframe])
sp = FakeSkyvernBrowserPage(page)
result = await sp.frame_switch(index=1)
assert sp._working_frame is iframe
assert result["index"] == 1
@pytest.mark.asyncio
async def test_switch_no_params_raises(self) -> None:
page = _make_fake_page()
sp = FakeSkyvernBrowserPage(page)
with pytest.raises(ValueError, match="Exactly one"):
await sp.frame_switch()
@pytest.mark.asyncio
async def test_switch_multiple_params_raises(self) -> None:
page = _make_fake_page()
sp = FakeSkyvernBrowserPage(page)
with pytest.raises(ValueError, match="Exactly one"):
await sp.frame_switch(selector="#x", name="y")
@pytest.mark.asyncio
async def test_switch_selector_not_iframe_raises(self) -> None:
element_mock = MagicMock()
element_mock.content_frame = AsyncMock(return_value=None)
page = _make_fake_page()
page.query_selector = AsyncMock(return_value=element_mock)
sp = FakeSkyvernBrowserPage(page)
with pytest.raises(ValueError, match="did not resolve to an iframe"):
await sp.frame_switch(selector="#not-iframe")
@pytest.mark.asyncio
async def test_switch_name_not_found_raises(self) -> None:
page = _make_fake_page()
page.frame = MagicMock(return_value=None)
sp = FakeSkyvernBrowserPage(page)
with pytest.raises(ValueError, match="No frame found"):
await sp.frame_switch(name="nonexistent")
@pytest.mark.asyncio
async def test_switch_index_out_of_range_raises(self) -> None:
page = _make_fake_page([_make_fake_frame()])
sp = FakeSkyvernBrowserPage(page)
with pytest.raises(ValueError, match="out of range"):
await sp.frame_switch(index=5)
# ---------------------------------------------------------------------------
# frame_main tests
# ---------------------------------------------------------------------------
class TestFrameMain:
def test_clears_working_frame(self) -> None:
page = _make_fake_page()
frame = _make_fake_frame()
sp = FakeSkyvernBrowserPage(page, working_frame=frame)
assert sp._working_frame is frame
sp.frame_main()
assert sp._working_frame is None
assert sp._locator_scope is page
# ---------------------------------------------------------------------------
# frame_list tests
# ---------------------------------------------------------------------------
class TestFrameList:
@pytest.mark.asyncio
async def test_lists_all_frames(self) -> None:
main = _make_fake_frame(name="", url="https://example.com")
iframe1 = _make_fake_frame(name="ads", url="https://ads.com")
iframe2 = _make_fake_frame(name="payment", url="https://payment.example.com")
page = _make_fake_page([main, iframe1, iframe2])
sp = FakeSkyvernBrowserPage(page)
frames = await sp.frame_list()
assert len(frames) == 3
assert frames[0]["is_main"] is True
assert frames[1]["name"] == "ads"
assert frames[2]["name"] == "payment"
# ---------------------------------------------------------------------------
# MCP tool tests
# ---------------------------------------------------------------------------
@ -432,7 +175,7 @@ class TestCLIStateFrame:
assert state.frame_name is None
assert state.frame_index is None
def test_frame_fields_roundtrip(self, tmp_path: Any) -> None:
def test_frame_fields_roundtrip(self, tmp_path) -> None:
import skyvern.cli.commands._state as state_mod
from skyvern.cli.commands._state import CLIState, load_state, save_state

View file

@ -79,68 +79,3 @@ def test_screenshot_attrs_reflect_post_filter_for_non_vision_config() -> None:
_set_llm_context_attrs(span, screenshots=filtered, is_speculative_step=False)
span.set_attribute.assert_any_call("screenshots_included", False)
span.set_attribute.assert_any_call("screenshot_count", 0)
def test_set_llm_context_attrs_invoked_in_all_three_handlers() -> None:
# Invariant: router, non-router, and LLMCaller handlers must each record
# context attrs; a missing call site silently drops attribution.
from skyvern.forge.sdk.api.llm import api_handler_factory as mod
tree = ast.parse(inspect.getsource(mod))
call_count = 0
for node in ast.walk(tree):
if not isinstance(node, ast.Call):
continue
target = node.func
fname = (
target.id if isinstance(target, ast.Name) else (target.attr if isinstance(target, ast.Attribute) else None)
)
if fname == "_set_llm_context_attrs":
call_count += 1
assert call_count == 3, f"Expected exactly 3 call sites, found {call_count}"
def _iter_function_defs(node: ast.AST) -> list[ast.FunctionDef | ast.AsyncFunctionDef]:
funcs: list[ast.FunctionDef | ast.AsyncFunctionDef] = []
for n in ast.walk(node):
if isinstance(n, (ast.FunctionDef, ast.AsyncFunctionDef)):
funcs.append(n)
return funcs
def _call_lineno(func: ast.FunctionDef | ast.AsyncFunctionDef, name: str) -> int | None:
for n in ast.walk(func):
if isinstance(n, ast.Call):
target = n.func
fname = (
target.id
if isinstance(target, ast.Name)
else (target.attr if isinstance(target, ast.Attribute) else None)
)
if fname == name:
return n.lineno
return None
def test_handlers_compute_screenshot_attrs_after_filter() -> None:
# Invariant: in every handler that records screenshot attrs, the
# _set_llm_context_attrs call must run AFTER _llm_screenshots_for_call so
# the attrs reflect what was actually sent to the LLM, not the raw arg.
from skyvern.forge.sdk.api.llm import api_handler_factory as mod
tree = ast.parse(inspect.getsource(mod))
matches = 0
for func in _iter_function_defs(tree):
attrs_line = _call_lineno(func, "_set_llm_context_attrs")
filter_line = _call_lineno(func, "_llm_screenshots_for_call")
if attrs_line is None or filter_line is None:
continue
assert attrs_line > filter_line, (
f"In `{func.name}` (defined at line {func.lineno}): "
f"_set_llm_context_attrs (line {attrs_line}) must come AFTER "
f"_llm_screenshots_for_call (line {filter_line})."
)
matches += 1
# ast.walk yields the 3 inner handler closures plus their two enclosing
# factory functions, so the ordering invariant must hold for >=3 matches.
assert matches >= 3, f"Expected at least 3 handlers with both calls, found {matches}"

View file

@ -1,111 +0,0 @@
import asyncio
import pytest
from skyvern.forge.sdk.schemas.files import FileInfo
from skyvern.forge.sdk.workflow.loop_download_filter import (
filter_downloaded_files_for_current_iteration,
to_downloaded_file_signature,
)
def _file(url: str, filename: str, checksum: str) -> FileInfo:
return FileInfo(url=url, filename=filename, checksum=checksum)
async def _capture_baseline(
get_files_coro: asyncio.coroutines,
timeout_seconds: float,
) -> dict | None:
"""Mirrors the exact baseline capture pattern from block.py / script_service.py."""
sigs: list = []
timed_out = False
try:
async with asyncio.timeout(timeout_seconds):
sigs = [to_downloaded_file_signature(fi) for fi in await get_files_coro]
except TimeoutError:
timed_out = True
if timed_out:
return None
return {"downloaded_file_signatures_before_iteration": sigs}
@pytest.mark.asyncio
async def test_baseline_capture_returns_none_on_timeout() -> None:
"""When get_downloaded_files hangs, the baseline must be None (not empty sigs)."""
async def _hang_forever() -> list[FileInfo]:
await asyncio.Event().wait()
return [] # never reached
result = await _capture_baseline(_hang_forever(), timeout_seconds=0.01)
assert result is None
@pytest.mark.asyncio
async def test_baseline_capture_returns_signatures_on_success() -> None:
"""Normal completion should return the signature dict."""
async def _return_files() -> list[FileInfo]:
return [
_file("https://files/a.pdf?sig=x", "a.pdf", "abc"),
_file("https://files/b.pdf?sig=y", "b.pdf", "def"),
]
result = await _capture_baseline(_return_files(), timeout_seconds=5.0)
assert result is not None
sigs = result["downloaded_file_signatures_before_iteration"]
assert len(sigs) == 2
assert sigs[0] == ("a.pdf", "abc", "https://files/a.pdf")
assert sigs[1] == ("b.pdf", "def", "https://files/b.pdf")
@pytest.mark.asyncio
async def test_baseline_capture_returns_empty_sigs_when_no_files() -> None:
"""No files is distinct from a timeout — returns empty list, not None."""
async def _return_empty() -> list[FileInfo]:
return []
result = await _capture_baseline(_return_empty(), timeout_seconds=5.0)
assert result is not None
assert result == {"downloaded_file_signatures_before_iteration": []}
@pytest.mark.asyncio
async def test_timeout_then_filter_returns_all_files_unfiltered() -> None:
"""End-to-end: timeout → None state → filter returns all files."""
async def _hang_forever() -> list[FileInfo]:
await asyncio.Event().wait()
return []
loop_internal_state = await _capture_baseline(_hang_forever(), timeout_seconds=0.01)
assert loop_internal_state is None
all_files = [
_file("https://files/a.pdf", "a.pdf", "abc"),
_file("https://files/b.pdf", "b.pdf", "def"),
]
result = filter_downloaded_files_for_current_iteration(all_files, loop_internal_state)
assert result == all_files
@pytest.mark.asyncio
async def test_success_then_filter_excludes_baseline_files() -> None:
"""End-to-end: successful capture → filter excludes baseline files."""
async def _return_baseline() -> list[FileInfo]:
return [_file("https://files/a.pdf?sig=old", "a.pdf", "abc")]
loop_internal_state = await _capture_baseline(_return_baseline(), timeout_seconds=5.0)
assert loop_internal_state is not None
all_files = [
_file("https://files/a.pdf?sig=old", "a.pdf", "abc"),
_file("https://files/b.pdf?sig=new", "b.pdf", "def"),
]
result = filter_downloaded_files_for_current_iteration(all_files, loop_internal_state)
assert len(result) == 1
assert result[0].filename == "b.pdf"

View file

@ -2,6 +2,7 @@
from pathlib import Path
from skyvern.core.script_generations.script_validators import validate_missing_selectors
from skyvern.services.script_reviewer import ScriptReviewer
_PROMPT_PATH = Path(__file__).resolve().parents[2] / "skyvern" / "forge" / "prompts" / "skyvern" / "script-reviewer.j2"
@ -404,53 +405,7 @@ class TestValidateMissingSelectors:
def setup_method(self) -> None:
self.reviewer = ScriptReviewer()
def test_fallback_with_selector_is_fine(self) -> None:
code = """
async def block_fn(page, context):
await page.click(selector='button:has-text("Submit")', ai='fallback', prompt='submit')
"""
assert self.reviewer._validate_missing_selectors(code) is None
def test_fallback_without_selector_flagged(self) -> None:
"""ai='fallback' with no selector= silently uses AI as primary path."""
code = """
async def block_fn(page, context):
await page.click(ai='fallback', prompt='Click Billing & Payments')
"""
error = self.reviewer._validate_missing_selectors(code)
assert error is not None
assert "page.click()" in error
assert "Missing selector" in error
def test_fill_without_selector_flagged(self) -> None:
code = """
async def block_fn(page, context):
await page.fill(ai='fallback', prompt='Enter username', value='test')
"""
error = self.reviewer._validate_missing_selectors(code)
assert error is not None
assert "page.fill()" in error
def test_no_ai_arg_without_selector_flagged(self) -> None:
"""Bare calls with no ai= and no selector= silently burn LLM tokens."""
code = """
async def block_fn(page, context):
await page.click(prompt='Click something')
"""
error = self.reviewer._validate_missing_selectors(code)
assert error is not None
assert "page.click()" in error
assert "no ai= argument" in error
def test_proactive_without_selector_not_flagged(self) -> None:
"""ai='proactive' without selector is intentional — AI always generates the value."""
code = """
async def block_fn(page, context):
await page.click(ai='proactive', prompt='Click something')
"""
assert self.reviewer._validate_missing_selectors(code) is None
def test_multiline_call_flagged(self) -> None:
def test_delegates_to_shared_validator(self) -> None:
code = """
async def block_fn(page, context):
await page.click(
@ -458,36 +413,9 @@ async def block_fn(page, context):
prompt='Click Billing & Payments',
)
"""
error = self.reviewer._validate_missing_selectors(code)
assert error is not None
assert "page.click()" in error
def test_multiline_with_selector_ok(self) -> None:
code = """
async def block_fn(page, context):
await page.click(
selector='a:has-text("Billing")',
ai='fallback',
prompt='Click billing link',
)
"""
assert self.reviewer._validate_missing_selectors(code) is None
def test_comments_ignored(self) -> None:
code = """
async def block_fn(page, context):
# await page.click(ai='fallback', prompt='old code')
await page.click(selector='button', ai='fallback', prompt='submit')
"""
assert self.reviewer._validate_missing_selectors(code) is None
def test_non_interaction_methods_ignored(self) -> None:
"""Methods like page.wait, page.complete are not interaction methods."""
code = """
async def block_fn(page, context):
await page.wait(ai='fallback', prompt='wait for page')
"""
assert self.reviewer._validate_missing_selectors(code) is None
result = self.reviewer._validate_missing_selectors(code)
assert result is not None
assert result == validate_missing_selectors(code)
class TestExtractCachedBlocks:

View file

@ -107,6 +107,21 @@ async def block_fn(page, context):
assert error is not None
assert "page.fill()" in error
def test_comments_ignored(self) -> None:
code = """
async def block_fn(page, context):
# await page.click(ai='fallback', prompt='old code')
await page.click(selector='button', ai='fallback', prompt='submit')
"""
assert validate_missing_selectors(code) is None
def test_non_interaction_methods_ignored(self) -> None:
code = """
async def block_fn(page, context):
await page.wait(ai='fallback', prompt='wait for page')
"""
assert validate_missing_selectors(code) is None
def test_multiple_methods_flagged(self) -> None:
code = """
async def block_fn(page, context):

View file

@ -32,86 +32,6 @@ def _output_parameter(key: str) -> OutputParameter:
)
def test_taskv2_output_includes_downloaded_files_filtered_by_loop() -> None:
"""When inside a loop, TaskV2Block output should include only THIS iteration's downloaded files."""
# Simulate baseline: iteration started with a.pdf already downloaded
loop_state = {
"downloaded_file_signatures_before_iteration": [
["a.pdf", "abc", "https://files/a.pdf"],
],
}
# Storage returns all files (a.pdf from before + b.pdf downloaded this iteration)
all_files = [
_file("https://files/a.pdf?sig=old", "a.pdf", "abc"),
_file("https://files/b.pdf?sig=new", "b.pdf", "def"),
]
# Apply the same filter that TaskV2Block now uses
filtered = filter_downloaded_files_for_current_iteration(all_files, loop_state)
# Build output dict the same way TaskV2Block does
task_v2_output = {
"task_id": "oc_test",
"status": "completed",
"summary": None,
"extracted_information": None,
"failure_reason": None,
"downloaded_files": [fi.model_dump() for fi in filtered],
"downloaded_file_urls": [fi.url for fi in filtered],
"task_screenshot_artifact_ids": [],
"workflow_screenshot_artifact_ids": [],
}
assert len(task_v2_output["downloaded_files"]) == 1
assert task_v2_output["downloaded_files"][0]["filename"] == "b.pdf"
assert task_v2_output["downloaded_file_urls"] == ["https://files/b.pdf?sig=new"]
def test_taskv2_output_includes_all_files_outside_loop() -> None:
"""Outside a loop (no loop_internal_state), all downloaded files should be included."""
all_files = [
_file("https://files/a.pdf", "a.pdf", "abc"),
_file("https://files/b.pdf", "b.pdf", "def"),
]
filtered = filter_downloaded_files_for_current_iteration(all_files, None)
task_v2_output = {
"downloaded_files": [fi.model_dump() for fi in filtered],
"downloaded_file_urls": [fi.url for fi in filtered],
}
assert len(task_v2_output["downloaded_files"]) == 2
assert task_v2_output["downloaded_file_urls"] == [
"https://files/a.pdf",
"https://files/b.pdf",
]
def test_taskv2_output_empty_when_no_new_downloads_in_iteration() -> None:
"""If no new files were downloaded in this iteration, both lists should be empty."""
loop_state = {
"downloaded_file_signatures_before_iteration": [
["a.pdf", "abc", "https://files/a.pdf"],
],
}
all_files = [
_file("https://files/a.pdf?sig=old", "a.pdf", "abc"),
]
filtered = filter_downloaded_files_for_current_iteration(all_files, loop_state)
task_v2_output = {
"downloaded_files": [fi.model_dump() for fi in filtered],
"downloaded_file_urls": [fi.url for fi in filtered],
}
assert task_v2_output["downloaded_files"] == []
assert task_v2_output["downloaded_file_urls"] == []
def test_taskv2_context_loop_state_available_after_nested_task_execution() -> None:
"""Verify loop_internal_state survives nested task execution and remains available for filtering."""
loop_state = {

View file

@ -1,74 +0,0 @@
"""Track while-loop child blocks for script caching (mirrors for-loop SKY-7751 pattern)."""
from datetime import datetime, timezone
from skyvern.forge.sdk.workflow.models.block import (
BlockType,
FileDownloadBlock,
JinjaBranchCriteria,
TaskBlock,
WhileLoopBlock,
)
from skyvern.forge.sdk.workflow.models.parameter import OutputParameter
from skyvern.forge.sdk.workflow.service import BLOCK_TYPES_THAT_SHOULD_BE_CACHED
def _make_output_param(label: str) -> OutputParameter:
now = datetime.now(tz=timezone.utc)
return OutputParameter(
key=f"{label}_output",
parameter_type="output",
output_parameter_id=f"op_{label}",
workflow_id="wf_test",
created_at=now,
modified_at=now,
)
def test_while_loop_is_cacheable() -> None:
assert BlockType.WHILE_LOOP in BLOCK_TYPES_THAT_SHOULD_BE_CACHED
def test_while_loop_child_labels_collected() -> None:
inner = TaskBlock(label="inner_task", output_parameter=_make_output_param("inner_task"), url="https://x.test")
wloop = WhileLoopBlock(
label="w",
output_parameter=_make_output_param("w"),
loop_blocks=[inner],
condition=JinjaBranchCriteria(expression="{{ true }}"),
)
script_blocks_by_label: dict[str, object] = {}
blocks_to_update: set[str] = set()
for loop_child in wloop.loop_blocks:
if (
loop_child.label
and loop_child.label not in script_blocks_by_label
and loop_child.block_type in BLOCK_TYPES_THAT_SHOULD_BE_CACHED
):
blocks_to_update.add(loop_child.label)
assert "inner_task" in blocks_to_update
def test_while_loop_file_download_child() -> None:
dl = FileDownloadBlock(
label="dl",
output_parameter=_make_output_param("dl"),
url="http://example.com",
navigation_goal="get file",
)
wloop = WhileLoopBlock(
label="w",
output_parameter=_make_output_param("w"),
loop_blocks=[dl],
condition=JinjaBranchCriteria(expression="{{ false }}"),
)
script_blocks_by_label: dict[str, object] = {}
blocks_to_update: set[str] = set()
for loop_child in wloop.loop_blocks:
if (
loop_child.label
and loop_child.label not in script_blocks_by_label
and loop_child.block_type in BLOCK_TYPES_THAT_SHOULD_BE_CACHED
):
blocks_to_update.add(loop_child.label)
assert "dl" in blocks_to_update