Files
lda-wf/tests/wf_mcp/test_workflow_wrapper_hints.py
T

268 lines
9.2 KiB
Python

from __future__ import annotations
from wf_api.wrapper_hints import (
MissingDecision,
MissingDecisionKind,
OutcomeCandidate,
OutcomeCandidateKind,
WrapperAuthoringHints,
WrapperHintConfidence,
WrapperOutcomePolicy,
wrapper_hints_for_capability,
)
def test_wrapper_hint_models_serialize_enum_fields_as_strings() -> None:
hints = WrapperAuthoringHints(
capability_name="demo.personal.echo_tool",
confidence=WrapperHintConfidence.MEDIUM,
declared_outcomes=["ok", "error"],
suggested_wrapper_outcomes=["ok", "error"],
outcome_policy=WrapperOutcomePolicy.PRESERVE_DECLARED,
input_schema={"type": "object", "properties": {}},
state_schema={"type": "object", "properties": {}},
output_schema={"type": "object", "properties": {}},
input_map={},
output_map={},
outcome_candidates=[
OutcomeCandidate(
kind=OutcomeCandidateKind.BOOLEAN_CONTROL_FIELD,
source="output.success",
candidate_outcomes=["success", "failure"],
confidence=WrapperHintConfidence.MEDIUM,
reason="top-level boolean field with control-like name",
automatic=False,
)
],
missing_decisions=[
MissingDecision(
kind=MissingDecisionKind.CONFIRM_BOOLEAN_OUTCOMES,
message="Confirm whether output.success should control routing.",
)
],
notes=["Hints are scaffolding, not semantic guarantees."],
)
dumped = hints.model_dump(mode="json")
assert dumped["confidence"] == "medium"
assert dumped["outcome_policy"] == "preserve_declared"
assert dumped["outcome_candidates"][0]["kind"] == "boolean_control_field"
assert dumped["missing_decisions"][0]["kind"] == "confirm_boolean_outcomes"
def test_wrapper_hints_scaffold_simple_object_input_and_output() -> None:
hints = wrapper_hints_for_capability(
capability_name="demo.personal.echo_tool",
input_schema={
"type": "object",
"properties": {"text": {"type": "string"}},
"required": ["text"],
},
output_schema={
"type": "object",
"properties": {"echoed": {"type": "string"}},
"required": ["echoed"],
},
outcomes=["ok"],
)
dumped = hints.model_dump(mode="json")
assert dumped["confidence"] == "high"
assert dumped["declared_outcomes"] == ["ok"]
assert dumped["suggested_wrapper_outcomes"] == ["ok"]
assert dumped["outcome_policy"] == "preserve_declared"
assert dumped["input_map"] == {"input.text": "text"}
assert dumped["output_map"] == {"echoed": "state.echoed"}
assert dumped["state_schema"]["properties"]["echoed"]["type"] == "string"
assert dumped["output_schema"]["properties"]["echoed"]["type"] == "string"
assert dumped["missing_decisions"] == []
def test_wrapper_hints_offer_boolean_outcome_candidates_without_auto_mapping() -> None:
hints = wrapper_hints_for_capability(
capability_name="demo.personal.submit",
input_schema={"type": "object", "properties": {"text": {"type": "string"}}},
output_schema={
"type": "object",
"properties": {
"success": {"type": "boolean"},
"message": {"type": "string"},
},
},
outcomes=["ok"],
)
dumped = hints.model_dump(mode="json")
candidate = dumped["outcome_candidates"][0]
assert dumped["confidence"] == "medium"
assert candidate["kind"] == "boolean_control_field"
assert candidate["source"] == "output.success"
assert candidate["candidate_outcomes"] == ["success", "failure"]
assert candidate["automatic"] is False
assert dumped["outcome_policy"] == "preserve_declared"
assert dumped["suggested_wrapper_outcomes"] == ["ok"]
assert dumped["missing_decisions"][0]["kind"] == "confirm_boolean_outcomes"
def test_wrapper_hints_do_not_treat_arbitrary_booleans_as_outcomes() -> None:
hints = wrapper_hints_for_capability(
capability_name="demo.personal.profile",
input_schema={"type": "object", "properties": {"user_id": {"type": "string"}}},
output_schema={
"type": "object",
"properties": {
"is_admin": {"type": "boolean"},
"name": {"type": "string"},
},
},
outcomes=["ok"],
)
dumped = hints.model_dump(mode="json")
assert dumped["confidence"] == "high"
assert dumped["outcome_candidates"] == []
assert dumped["missing_decisions"] == []
def test_wrapper_hints_mark_nested_outputs_as_low_confidence() -> None:
hints = wrapper_hints_for_capability(
capability_name="demo.personal.search",
input_schema={"type": "object", "properties": {"query": {"type": "string"}}},
output_schema={
"type": "object",
"properties": {
"results": {
"type": "array",
"items": {
"type": "object",
"properties": {"title": {"type": "string"}},
},
}
},
},
outcomes=["ok"],
)
dumped = hints.model_dump(mode="json")
assert dumped["confidence"] == "low"
assert dumped["missing_decisions"][0]["kind"] == "review_nested_output"
assert dumped["output_map"] == {"results": "state.results"}
def test_wrapper_hints_preserve_local_defs_for_mapped_output_refs() -> None:
hints = wrapper_hints_for_capability(
capability_name="local.browser_click.open_click_page",
input_schema={"type": "object", "properties": {}},
output_schema={
"type": "object",
"$defs": {
"Snapshot": {
"type": "object",
"properties": {"clicked": {"type": "boolean"}},
}
},
"properties": {
"before": {"$ref": "#/$defs/Snapshot"},
"session_id": {"type": "string"},
},
"required": ["before", "session_id"],
},
outcomes=["ok"],
)
dumped = hints.model_dump(mode="json")
assert dumped["state_schema"]["properties"]["before"] == {
"$ref": "#/$defs/Snapshot"
}
assert dumped["state_schema"]["$defs"]["Snapshot"]["properties"]["clicked"] == {
"type": "boolean"
}
assert dumped["output_schema"]["$defs"] == dumped["state_schema"]["$defs"]
def test_wrapper_hints_do_not_auto_map_raw_mcp_content_blocks() -> None:
hints = wrapper_hints_for_capability(
capability_name="everything.default.echo",
input_schema={
"type": "object",
"properties": {"message": {"type": "string"}},
"required": ["message"],
},
output_schema={
"type": "object",
"properties": {
"content": {"type": "array"},
},
},
outcomes=["ok", "error"],
)
dumped = hints.model_dump(mode="json")
assert dumped["confidence"] == "low"
assert dumped["output_map"] == {}
assert "content" not in dumped["state_schema"]["properties"]
assert dumped["missing_decisions"][0]["kind"] == "review_nested_output"
assert "Raw MCP content blocks" in dumped["notes"][2]
def test_wrapper_hints_keep_content_only_mcp_output_explicit() -> None:
hints = wrapper_hints_for_capability(
capability_name="everything.default.echo",
input_schema={
"type": "object",
"properties": {"message": {"type": "string"}},
"required": ["message"],
},
output_schema={
"type": "object",
"properties": {"content": {"type": "array"}},
"required": ["content"],
},
outcomes=["ok", "error"],
)
dumped = hints.model_dump(mode="json")
assert dumped["confidence"] == "low"
assert dumped["output_map"] == {}
assert "content" not in dumped["state_schema"]["properties"]
assert dumped["output_schema"]["properties"] == {}
assert dumped["missing_decisions"][0]["kind"] == "review_nested_output"
assert "TextContent" in dumped["notes"][2]
def test_wrapper_hints_mark_empty_output_schema_as_low_confidence() -> None:
hints = wrapper_hints_for_capability(
capability_name="demo.personal.no_output",
input_schema={
"type": "object",
"properties": {"text": {"type": "string"}},
"required": ["text"],
},
output_schema={"type": "object", "properties": {}},
outcomes=["ok"],
)
dumped = hints.model_dump(mode="json")
assert dumped["confidence"] == "low"
assert dumped["input_map"] == {"input.text": "text"}
assert dumped["output_map"] == {}
assert dumped["missing_decisions"][0]["kind"] == "choose_output_fields"
def test_workflow_surface_wrapper_hints_shim_reexports_canonical_helper() -> None:
from wf_api.wrapper_hints import wrapper_hints_for_capability
from wf_mcp.workflow_surface.wrapper_hints import (
wrapper_hints_for_capability as wrapper_hints_for_capability_shim,
)
assert wrapper_hints_for_capability_shim is wrapper_hints_for_capability