Skyvern/tests/unit/test_copilot_make_agent_result_backfill.py
Celal Zamanoğlu 9949fb0ded
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
feat(SKY-11988): add credentialPrompt signal to copilot narrative payload (#7200)
Co-authored-by: AronPerez <aperez0295@gmail.com>
2026-07-08 18:24:49 +03:00

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