mirror of
https://github.com/Skyvern-AI/skyvern.git
synced 2026-07-09 16:09:13 +00:00
Some checks failed
Auto Create GitHub Release on Version Change / check-version-change (push) Waiting to run
Auto Create GitHub Release on Version Change / create-release (push) Blocked by required conditions
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
Run tests and pre-commit / pip Package Smoke Tests (3.11) (push) Waiting to run
Run tests and pre-commit / pip Package Smoke Tests (3.13) (push) Waiting to run
Publish Fern Docs / run (push) Waiting to run
Build Skyvern SDK and publish to PyPI / check-version-change (push) Waiting to run
Build Skyvern SDK and publish to PyPI / run-ci (push) Blocked by required conditions
Build Skyvern SDK and publish to PyPI / build-sdk (push) Blocked by required conditions
Build Skyvern SDK and publish to PyPI / publish-sdk (push) Blocked by required conditions
Build Skyvern TS SDK and publish to npm / check-version-change (push) Has been cancelled
Build Skyvern TS SDK and publish to npm / build-and-publish-sdk (push) Has been cancelled
Co-authored-by: AronPerez <aperez0295@gmail.com>
162 lines
6.9 KiB
Python
162 lines
6.9 KiB
Python
"""`_make_agent_result` back-fills the typed terminal adjudication onto the
|
|
narrative payload: ``responseKind`` from ``TurnOutcome.response_kind`` and
|
|
``verifiedSuccess`` from ``enforcement.verified_goal_satisfied_context``."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
from skyvern.forge.sdk.copilot.agent import _finalize_result_with_blocker_override, _make_agent_result
|
|
from skyvern.forge.sdk.copilot.blocker_signal import CopilotToolBlockerSignal
|
|
from skyvern.forge.sdk.copilot.context import AgentResult, CopilotContext
|
|
from skyvern.forge.sdk.copilot.request_policy import RequestPolicy
|
|
from skyvern.forge.sdk.schemas.copilot_turn_outcome import ResponseKind, TurnOutcome
|
|
from tests.unit.copilot_test_helpers import make_copilot_ctx as _ctx
|
|
from tests.unit.copilot_test_helpers import make_verified_goal_contract
|
|
|
|
|
|
def _verified_goal_ctx() -> CopilotContext:
|
|
return _ctx(
|
|
last_test_ok=True,
|
|
last_full_workflow_test_ok=True,
|
|
latest_diagnosis_repair_contract=make_verified_goal_contract(),
|
|
)
|
|
|
|
|
|
def _outcome(kind: ResponseKind) -> TurnOutcome:
|
|
return TurnOutcome(response_kind=kind)
|
|
|
|
|
|
def _payload(**overrides: object) -> dict:
|
|
base: dict = {
|
|
"turnId": "turn-1",
|
|
"turnIndex": 0,
|
|
"mode": "build",
|
|
"designStarted": True,
|
|
"designEnded": True,
|
|
"draft": None,
|
|
"blocks": [],
|
|
"terminal": "response",
|
|
"terminalMessage": "done",
|
|
"narrativeSummary": None,
|
|
"priorBlockCount": None,
|
|
"designActivity": [],
|
|
"startedAt": None,
|
|
"endedAt": None,
|
|
}
|
|
base.update(overrides)
|
|
return base
|
|
|
|
|
|
def _result(ctx: CopilotContext | None, **kwargs: object):
|
|
kwargs.setdefault("user_response", "ok")
|
|
kwargs.setdefault("updated_workflow", None)
|
|
kwargs.setdefault("global_llm_context", None)
|
|
return _make_agent_result(ctx, **kwargs)
|
|
|
|
|
|
def test_backfill_writes_both_fields_together() -> None:
|
|
result = _result(_ctx(), turn_outcome=_outcome(ResponseKind.CLARIFY), narrative_payload=_payload())
|
|
assert result.narrative_payload is not None
|
|
assert result.narrative_payload["responseKind"] == "clarify"
|
|
assert result.narrative_payload["verifiedSuccess"] is False
|
|
|
|
|
|
def test_backfill_verified_success_requires_adjudicated_evidence() -> None:
|
|
# The legacy run-status conjunction still ends the turn but no longer backs
|
|
# a verified-success claim: without judge-confirmed outcome evidence the
|
|
# claim tier renders built-but-unverified.
|
|
result = _result(_verified_goal_ctx(), turn_outcome=_outcome(ResponseKind.BUILD), narrative_payload=_payload())
|
|
assert result.narrative_payload is not None
|
|
assert result.narrative_payload["responseKind"] == "build"
|
|
assert result.narrative_payload["verifiedSuccess"] is False
|
|
|
|
|
|
def test_backfill_verified_success_true_when_outcome_fully_verified() -> None:
|
|
from skyvern.forge.sdk.copilot.completion_verification import (
|
|
CompletionVerificationResult,
|
|
CriterionVerdict,
|
|
)
|
|
|
|
ctx = _verified_goal_ctx()
|
|
ctx.completion_verification_result = CompletionVerificationResult(
|
|
status="evaluated",
|
|
criterion_ids=["c0"],
|
|
verdicts=[CriterionVerdict(criterion_id="c0", state="satisfied", reason_code="evidence_confirms")],
|
|
)
|
|
result = _result(ctx, turn_outcome=_outcome(ResponseKind.BUILD), narrative_payload=_payload())
|
|
assert result.narrative_payload is not None
|
|
assert result.narrative_payload["verifiedSuccess"] is True
|
|
|
|
|
|
def test_backfill_never_overwrites_explicit_values() -> None:
|
|
payload = _payload(responseKind="refuse", verifiedSuccess=True)
|
|
result = _result(_ctx(), turn_outcome=_outcome(ResponseKind.CLARIFY), narrative_payload=payload)
|
|
assert result.narrative_payload is not None
|
|
assert result.narrative_payload["responseKind"] == "refuse"
|
|
assert result.narrative_payload["verifiedSuccess"] is True
|
|
|
|
|
|
def test_backfill_tolerates_turn_outcome_none() -> None:
|
|
result = _result(_ctx(), turn_outcome=None, narrative_payload=_payload())
|
|
assert result.narrative_payload is not None
|
|
assert "responseKind" not in result.narrative_payload
|
|
assert result.narrative_payload["verifiedSuccess"] is False
|
|
|
|
|
|
def test_backfill_tolerates_ctx_none() -> None:
|
|
result = _result(None, turn_outcome=_outcome(ResponseKind.REFUSE), narrative_payload=_payload())
|
|
assert result.narrative_payload is not None
|
|
assert result.narrative_payload["responseKind"] == "refuse"
|
|
assert "verifiedSuccess" not in result.narrative_payload
|
|
|
|
|
|
def test_backfill_tolerates_missing_payload() -> None:
|
|
with pytest.raises(ValueError, match="narrative_payload"):
|
|
_result(_ctx(), turn_outcome=_outcome(ResponseKind.BUILD), narrative_payload=None)
|
|
|
|
|
|
def test_missing_payload_is_allowed_without_ctx() -> None:
|
|
result = _result(None, turn_outcome=_outcome(ResponseKind.BUILD), narrative_payload=None)
|
|
assert result.narrative_payload is None
|
|
|
|
|
|
def test_backfill_adds_credential_prompt_for_typed_clarification_reason() -> None:
|
|
ctx = _ctx(request_policy=RequestPolicy(clarification_reason="credential_name_unresolved"))
|
|
result = _result(ctx, turn_outcome=_outcome(ResponseKind.CLARIFY), narrative_payload=_payload())
|
|
assert result.narrative_payload is not None
|
|
assert result.narrative_payload["credentialPrompt"] == {"reason": "credential_name_unresolved"}
|
|
|
|
|
|
def test_blocker_override_path_adds_credential_prompt_from_request_policy() -> None:
|
|
ctx = _ctx(request_policy=RequestPolicy(clarification_reason="workflow_credential_inputs_unbound"))
|
|
ctx.blocker_signal = CopilotToolBlockerSignal(
|
|
blocker_kind="authority_denied",
|
|
agent_steering_text="Reply to the user without updating the workflow.",
|
|
user_facing_reason="I couldn't find the required credentials for the existing workflow.",
|
|
recovery_hint="report_blocker_to_user",
|
|
internal_reason_code="turn_intent_no_mutation_run_blocked",
|
|
blocked_tool="update_workflow",
|
|
)
|
|
pre_override = AgentResult(user_response="agent reply", updated_workflow=None, global_llm_context=None)
|
|
|
|
overridden = _finalize_result_with_blocker_override(ctx, pre_override)
|
|
|
|
assert overridden.narrative_payload is not None
|
|
assert overridden.narrative_payload["credentialPrompt"] == {"reason": "workflow_credential_inputs_unbound"}
|
|
|
|
|
|
def test_backfill_adds_credential_prompt_from_text_marker_when_no_policy_signal() -> None:
|
|
result = _result(
|
|
_ctx(),
|
|
user_response="You can add one at https://app.skyvern.com/credentials.",
|
|
narrative_payload=_payload(),
|
|
)
|
|
assert result.narrative_payload is not None
|
|
assert result.narrative_payload["credentialPrompt"] == {"reason": "assistant_directed"}
|
|
|
|
|
|
def test_backfill_omits_credential_prompt_when_no_signal_present() -> None:
|
|
result = _result(_ctx(), user_response="Done, the workflow is ready.", narrative_payload=_payload())
|
|
assert result.narrative_payload is not None
|
|
assert "credentialPrompt" not in result.narrative_payload
|