Stop reporting a silent workflow as a completed task
n8n answers 200 with an empty body when a node throws before the Respond node runs — a refused token exchange is the common way in. The adapter read that as a completed task carrying "(the workflow returned nothing)", which tells the caller the task succeeded: the one thing certainly false. Off-contract replies get the same treatment. They are still returned raw so drift is visible, but a body that is not the contract cannot be read as agreement. Also removes a backlog section that described the events as unbuilt and said an event should carry sub and azp from the caller's token — the design the user overruled. Anyone reading it would have built the wrong consumer. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -11,7 +11,7 @@ import httpx
|
||||
import pytest
|
||||
|
||||
from a2a_n8n_adapter.config import Settings
|
||||
from a2a_n8n_adapter.server import _read, build_agent_card
|
||||
from a2a_n8n_adapter.server import Outcome, _read, build_agent_card
|
||||
|
||||
|
||||
def settings(**over) -> Settings:
|
||||
@@ -35,27 +35,35 @@ def response(payload, status=200) -> httpx.Response:
|
||||
|
||||
class TestReadingTheWorkflow:
|
||||
def test_the_contracted_shape(self) -> None:
|
||||
assert _read(response({"answer": "yes", "escalate": False})) == ("yes", False)
|
||||
assert _read(response({"answer": "yes", "escalate": False})) == ("yes", Outcome.ANSWERED)
|
||||
|
||||
def test_n8n_wraps_its_last_node_in_an_array(self) -> None:
|
||||
assert _read(response([{"answer": "yes"}])) == ("yes", False)
|
||||
assert _read(response([{"answer": "yes"}])) == ("yes", Outcome.ANSWERED)
|
||||
|
||||
def test_escalation_is_carried_not_swallowed(self) -> None:
|
||||
# An autonomous agent meeting a rule that needs approval must escalate.
|
||||
# Losing this flag would turn "a human must look" into "done".
|
||||
assert _read(response({"answer": "needs approval", "escalate": True})) == (
|
||||
"needs approval",
|
||||
True,
|
||||
Outcome.ESCALATED,
|
||||
)
|
||||
|
||||
def test_an_off_contract_reply_is_returned_raw(self) -> None:
|
||||
# Visible rather than reinterpreted: a workflow that has drifted should
|
||||
# look wrong, not be quietly guessed at.
|
||||
text, escalate = _read(response("plain text"))
|
||||
assert text == "plain text" and escalate is False
|
||||
# Off contract is not agreement. Returned raw so the drift is visible,
|
||||
# but never as a completed answer.
|
||||
text, outcome = _read(response("plain text"))
|
||||
assert text == "plain text" and outcome is Outcome.OFF_CONTRACT
|
||||
|
||||
def test_an_empty_reply_says_so(self) -> None:
|
||||
assert _read(response("")) == ("(the workflow returned nothing)", False)
|
||||
# HTTP 200 with an empty body is what n8n returns when a node throws
|
||||
# before the reply is built — a refused token exchange, usually. Reading
|
||||
# it as a completed answer told the caller the task succeeded, which was
|
||||
# the one thing certainly false.
|
||||
text, outcome = _read(response(""))
|
||||
assert outcome is Outcome.SILENT
|
||||
assert "no answer" in text
|
||||
|
||||
|
||||
class TestAgentCard:
|
||||
|
||||
Reference in New Issue
Block a user