mirror of
https://github.com/Skyvern-AI/skyvern.git
synced 2026-04-28 11:40:32 +00:00
Some checks failed
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
Auto Create GitHub Release on Version Change / check-version-change (push) Has been cancelled
Build Skyvern SDK and publish to PyPI / check-version-change (push) Has been cancelled
Build Skyvern TS SDK and publish to npm / check-version-change (push) Has been cancelled
Auto Create GitHub Release on Version Change / create-release (push) Has been cancelled
Build Skyvern SDK and publish to PyPI / run-ci (push) Has been cancelled
Build Skyvern SDK and publish to PyPI / build-sdk (push) Has been cancelled
Build Skyvern TS SDK and publish to npm / build-and-publish-sdk (push) Has been cancelled
235 lines
7.7 KiB
Python
235 lines
7.7 KiB
Python
"""Tests for the probable-site-block-wall detector and stop nudge.
|
|
|
|
Covers the copilot's response to a scrape-wall pattern where navigation
|
|
reports success but the page is unreadable — a symptom of site blocking
|
|
that the shared anti-bot classifier does not recognise (it routes to
|
|
``DATA_EXTRACTION_FAILURE`` rather than ``ANTI_BOT_DETECTION``). The
|
|
copilot tracks its own shape-independent streak so workflow-shape
|
|
rewrites cannot perpetually reset a repeated-frontier counter while the
|
|
site keeps refusing to load.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from types import SimpleNamespace
|
|
|
|
import pytest
|
|
|
|
from skyvern.forge.sdk.copilot.context import CopilotContext
|
|
from skyvern.forge.sdk.copilot.enforcement import (
|
|
POST_PROBABLE_SITE_BLOCK_STOP_NUDGE,
|
|
PROBABLE_SITE_BLOCK_STREAK_STOP_AT,
|
|
_needs_probable_site_block_stop_nudge,
|
|
)
|
|
from skyvern.forge.sdk.copilot.tools import (
|
|
_detect_probable_site_block_wall,
|
|
_record_run_blocks_result,
|
|
)
|
|
|
|
_SCRAPE_WALL_REASON = (
|
|
"Skyvern failed to load the website. The page may have navigated "
|
|
"unexpectedly or become unresponsive during analysis."
|
|
)
|
|
|
|
|
|
def _fresh_context() -> CopilotContext:
|
|
return CopilotContext(
|
|
organization_id="o",
|
|
workflow_id="w",
|
|
workflow_permanent_id="wp",
|
|
workflow_yaml="",
|
|
browser_session_id=None,
|
|
stream=SimpleNamespace(), # type: ignore[arg-type]
|
|
)
|
|
|
|
|
|
def _scrape_wall_result() -> dict:
|
|
return {
|
|
"ok": False,
|
|
"data": {
|
|
"blocks": [
|
|
{"block_type": "goto_url", "status": "completed"},
|
|
{
|
|
"block_type": "extraction",
|
|
"status": "failed",
|
|
"failure_reason": _SCRAPE_WALL_REASON,
|
|
},
|
|
]
|
|
},
|
|
}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _detect_probable_site_block_wall
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_detect_matches_completed_nav_plus_scrape_wall() -> None:
|
|
assert _detect_probable_site_block_wall(_scrape_wall_result()) is True
|
|
|
|
|
|
def test_detect_matches_page_navigated_unexpectedly_phrasing() -> None:
|
|
result = {
|
|
"ok": False,
|
|
"data": {
|
|
"blocks": [
|
|
{"block_type": "navigation", "status": "completed"},
|
|
{
|
|
"block_type": "extraction",
|
|
"status": "failed",
|
|
"failure_reason": "We think the page may have navigated unexpectedly during analysis.",
|
|
},
|
|
]
|
|
},
|
|
}
|
|
assert _detect_probable_site_block_wall(result) is True
|
|
|
|
|
|
def test_detect_returns_false_when_run_ok() -> None:
|
|
result = _scrape_wall_result()
|
|
result["ok"] = True
|
|
assert _detect_probable_site_block_wall(result) is False
|
|
|
|
|
|
def test_detect_returns_false_without_completed_nav() -> None:
|
|
result = {
|
|
"ok": False,
|
|
"data": {
|
|
"blocks": [
|
|
{"block_type": "goto_url", "status": "failed"},
|
|
{"block_type": "extraction", "status": "failed", "failure_reason": _SCRAPE_WALL_REASON},
|
|
]
|
|
},
|
|
}
|
|
assert _detect_probable_site_block_wall(result) is False
|
|
|
|
|
|
def test_detect_returns_false_for_other_failure_reasons() -> None:
|
|
result = {
|
|
"ok": False,
|
|
"data": {
|
|
"blocks": [
|
|
{"block_type": "goto_url", "status": "completed"},
|
|
{
|
|
"block_type": "extraction",
|
|
"status": "failed",
|
|
"failure_reason": "Timeout waiting for selector #submit",
|
|
},
|
|
]
|
|
},
|
|
}
|
|
assert _detect_probable_site_block_wall(result) is False
|
|
|
|
|
|
def test_detect_tolerates_missing_data() -> None:
|
|
assert _detect_probable_site_block_wall({"ok": False}) is False
|
|
assert _detect_probable_site_block_wall({"ok": False, "data": "not a dict"}) is False
|
|
assert _detect_probable_site_block_wall({"ok": False, "data": {}}) is False
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Streak maintenance in _record_run_blocks_result
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_streak_increments_on_consecutive_scrape_walls() -> None:
|
|
ctx = _fresh_context()
|
|
_record_run_blocks_result(ctx, _scrape_wall_result())
|
|
assert ctx.probable_site_block_streak_count == 1
|
|
_record_run_blocks_result(ctx, _scrape_wall_result())
|
|
assert ctx.probable_site_block_streak_count == 2
|
|
|
|
|
|
def test_streak_resets_on_real_success() -> None:
|
|
ctx = _fresh_context()
|
|
_record_run_blocks_result(ctx, _scrape_wall_result())
|
|
assert ctx.probable_site_block_streak_count == 1
|
|
success = {
|
|
"ok": True,
|
|
"data": {
|
|
"blocks": [
|
|
{
|
|
"block_type": "extraction",
|
|
"status": "completed",
|
|
"extracted_data": {"answer": "42"},
|
|
}
|
|
]
|
|
},
|
|
}
|
|
_record_run_blocks_result(ctx, success)
|
|
assert ctx.probable_site_block_streak_count == 0
|
|
|
|
|
|
def test_streak_resets_on_failure_without_pattern() -> None:
|
|
ctx = _fresh_context()
|
|
_record_run_blocks_result(ctx, _scrape_wall_result())
|
|
assert ctx.probable_site_block_streak_count == 1
|
|
other_failure = {
|
|
"ok": False,
|
|
"data": {
|
|
"blocks": [
|
|
{"block_type": "goto_url", "status": "completed"},
|
|
{
|
|
"block_type": "extraction",
|
|
"status": "failed",
|
|
"failure_reason": "Timeout waiting for selector #submit",
|
|
},
|
|
]
|
|
},
|
|
}
|
|
_record_run_blocks_result(ctx, other_failure)
|
|
assert ctx.probable_site_block_streak_count == 0
|
|
|
|
|
|
def test_streak_stays_zero_when_navigation_itself_failed() -> None:
|
|
# Orthogonality contract: when the navigation block did not reach
|
|
# status=completed (e.g. non-retriable nav error — DNS, SSL, invalid URL),
|
|
# the scrape-wall detector must not count the run even if a later block
|
|
# also emitted the generic load-failure template. That case belongs to
|
|
# _detect_non_retriable_nav_error, not the probable-site-block streak.
|
|
ctx = _fresh_context()
|
|
nav_failed_with_wall_text = {
|
|
"ok": False,
|
|
"data": {
|
|
"blocks": [
|
|
{
|
|
"block_type": "goto_url",
|
|
"status": "failed",
|
|
"failure_reason": (
|
|
"Failed to navigate to url https://x.invalid. Error message: net::ERR_NAME_NOT_RESOLVED"
|
|
),
|
|
},
|
|
{
|
|
"block_type": "extraction",
|
|
"status": "failed",
|
|
"failure_reason": _SCRAPE_WALL_REASON,
|
|
},
|
|
]
|
|
},
|
|
}
|
|
_record_run_blocks_result(ctx, nav_failed_with_wall_text)
|
|
assert ctx.probable_site_block_streak_count == 0
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Enforcement gate
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@pytest.mark.parametrize("streak", [0, 1])
|
|
def test_gate_does_not_fire_below_threshold(streak: int) -> None:
|
|
ctx = _fresh_context()
|
|
ctx.probable_site_block_streak_count = streak
|
|
assert not _needs_probable_site_block_stop_nudge(ctx)
|
|
|
|
|
|
def test_gate_fires_at_stop_threshold() -> None:
|
|
ctx = _fresh_context()
|
|
ctx.probable_site_block_streak_count = PROBABLE_SITE_BLOCK_STREAK_STOP_AT
|
|
assert _needs_probable_site_block_stop_nudge(ctx)
|
|
|
|
|
|
def test_nudge_text_is_stop_oriented() -> None:
|
|
# Sanity-check the stop nudge tells the agent not to retry.
|
|
assert "STOP" in POST_PROBABLE_SITE_BLOCK_STOP_NUDGE
|
|
assert "Do NOT" in POST_PROBABLE_SITE_BLOCK_STOP_NUDGE
|