Files
lda-wf/tests/authoring/test_ops.py
T

453 lines
13 KiB
Python

from __future__ import annotations
import pytest
from pydantic import ValidationError
from wf_authoring import (
WorkflowBuilder,
build_registry,
coalesce,
concat,
constant,
default_if_none,
extract_field,
extract_text_content,
filter_items,
filter_items_present,
first_item,
first_item_maybe,
first_item_or_none,
input_from,
is_empty,
last_item,
last_item_or_none,
length,
output_to,
pick_key,
pick_path,
project_fields,
rename_fields,
runtime_error,
state_path,
truthy,
)
from wf_core import (
RunStatus,
RuntimeContext,
SchemaRef,
StateSchema,
WorkflowExecutionError,
execute_workflow,
)
def _build_first_workflow(use_safe_first: bool = False):
spec = first_item_or_none if use_safe_first else first_item
builder = WorkflowBuilder(
name="first_demo",
input_schema=SchemaRef(type="object"),
state_schema=StateSchema.model_validate(
{
"type": "object",
"properties": {
"items": {"type": "array"},
"item": {"type": ["string", "null"]},
},
}
),
output_schema=SchemaRef(type="object"),
start="pick_first",
)
node = builder.use(
spec,
id="pick_first",
input=[input_from(state_path("items"), "items")],
output=[output_to("item", state_path("item"))],
)
builder.connect(node, "ok", "__end__")
return builder.compile(), build_registry(spec)
def _build_first_maybe_workflow():
builder = WorkflowBuilder(
name="first_maybe_demo",
input_schema=SchemaRef(type="object"),
state_schema=StateSchema.model_validate(
{
"type": "object",
"properties": {
"items": {"type": "array"},
"item": {"type": ["string", "null"]},
"missing": {"type": "boolean"},
},
}
),
output_schema=SchemaRef(type="object"),
start="pick_first",
)
pick_first = builder.use(
first_item_maybe,
id="pick_first",
input=[input_from(state_path("items"), "items")],
output=[output_to("item", state_path("item"))],
)
mark_missing = builder.use(
first_item_or_none,
id="mark_missing",
input=[input_from(state_path("items"), "items")],
output=[output_to("item", state_path("item"))],
)
builder.connect(pick_first, "found", "__end__")
builder.connect(pick_first, "missing", mark_missing)
builder.connect(mark_missing, "ok", "__end__")
return builder.compile(), build_registry(first_item_maybe, first_item_or_none)
def test_first_item_selects_first_value_through_workflow() -> None:
workflow, registry = _build_first_workflow()
run = execute_workflow(workflow, {"items": ["a", "b"]}, registry)
assert run.status == RunStatus.COMPLETED
assert run.state["item"] == "a"
def test_first_item_fails_on_empty_sequence() -> None:
workflow, registry = _build_first_workflow()
with pytest.raises(
WorkflowExecutionError,
match=r"node input for pick_first\['items'\]: \[\] should be non-empty",
):
execute_workflow(workflow, {"items": []}, registry)
def test_first_item_or_none_returns_none_for_empty_sequence() -> None:
workflow, registry = _build_first_workflow(use_safe_first=True)
run = execute_workflow(workflow, {"items": []}, registry)
assert run.status == RunStatus.COMPLETED
assert run.state["item"] is None
def test_first_item_maybe_routes_found_outcome() -> None:
workflow, registry = _build_first_maybe_workflow()
run = execute_workflow(workflow, {"items": ["a", "b"]}, registry)
assert run.status == RunStatus.COMPLETED
assert run.state["item"] == "a"
assert run.trace[0].outcome == "found"
def test_first_item_maybe_routes_missing_outcome() -> None:
workflow, registry = _build_first_maybe_workflow()
run = execute_workflow(workflow, {"items": []}, registry)
assert run.status == RunStatus.COMPLETED
assert run.state["item"] is None
assert run.trace[0].outcome == "missing"
def test_last_item_selects_last_value() -> None:
registry = build_registry(last_item)
result = registry["authoring.last_item"](
{"items": ["a", "b"]},
RuntimeContext(current_node_id="last"),
)
assert result == {"outcome": "ok", "output": {"item": "b"}}
def test_last_item_fails_on_empty_sequence() -> None:
registry = build_registry(last_item)
with pytest.raises(ValidationError, match="List should have at least 1 item"):
registry["authoring.last_item"](
{"items": []},
RuntimeContext(current_node_id="last"),
)
def test_last_item_or_none_returns_none_for_empty_sequence() -> None:
registry = build_registry(last_item_or_none)
result = registry["authoring.last_item_or_none"](
{"items": []},
RuntimeContext(current_node_id="last"),
)
assert result == {"outcome": "ok", "output": {"item": None}}
def test_length_counts_items() -> None:
registry = build_registry(length)
result = registry["authoring.length"](
{"items": ["a", "b", "c"]},
RuntimeContext(current_node_id="length"),
)
assert result == {"outcome": "ok", "output": {"count": 3}}
def test_is_empty_detects_empty_sequence() -> None:
registry = build_registry(is_empty)
result = registry["authoring.is_empty"](
{"items": []},
RuntimeContext(current_node_id="is_empty"),
)
assert result == {"outcome": "ok", "output": {"value": True}}
def test_coalesce_returns_value_or_fallback() -> None:
registry = build_registry(coalesce)
ctx = RuntimeContext(current_node_id="coalesce")
present = registry["authoring.coalesce"](
{"value": "x", "fallback": "fallback"},
ctx,
)
missing = registry["authoring.coalesce"](
{"value": None, "fallback": "fallback"},
ctx,
)
assert present == {"outcome": "ok", "output": {"value": "x"}}
assert missing == {"outcome": "ok", "output": {"value": "fallback"}}
def test_default_if_none_is_coalesce_alias() -> None:
registry = build_registry(default_if_none)
result = registry["authoring.default_if_none"](
{"value": None, "fallback": "fallback"},
RuntimeContext(current_node_id="default_if_none"),
)
assert default_if_none.name == "authoring.default_if_none"
assert default_if_none.fn is coalesce.fn
assert result == {"outcome": "ok", "output": {"value": "fallback"}}
def test_constant_returns_configured_value() -> None:
registry = build_registry(constant)
result = registry["authoring.constant"](
{"value": {"source": "fixture"}},
RuntimeContext(current_node_id="constant"),
)
assert result == {"outcome": "ok", "output": {"value": {"source": "fixture"}}}
def test_pick_key_selects_value_from_mapping() -> None:
registry = build_registry(pick_key)
result = registry["authoring.pick_key"](
{"mapping": {"name": "Ada", "age": 36}, "key": "name"},
RuntimeContext(current_node_id="pick_key"),
)
assert result == {"outcome": "ok", "output": {"value": "Ada"}}
def test_pick_key_returns_none_when_missing() -> None:
registry = build_registry(pick_key)
result = registry["authoring.pick_key"](
{"mapping": {"name": "Ada"}, "key": "missing"},
RuntimeContext(current_node_id="pick_key"),
)
assert result == {"outcome": "ok", "output": {"value": None}}
def test_pick_path_selects_nested_value() -> None:
registry = build_registry(pick_path)
result = registry["authoring.pick_path"](
{"mapping": {"result": {"status": "done"}}, "path": "result.status"},
RuntimeContext(current_node_id="pick_path"),
)
assert result == {"outcome": "ok", "output": {"value": "done"}}
def test_project_fields_selects_named_keys() -> None:
registry = build_registry(project_fields)
result = registry["authoring.project_fields"](
{
"mapping": {"status": "done", "message": "ok", "debug": True},
"fields": ["status", "message"],
},
RuntimeContext(current_node_id="project_fields"),
)
assert result == {
"outcome": "ok",
"output": {"mapping": {"status": "done", "message": "ok"}},
}
def test_rename_fields_remaps_existing_keys() -> None:
registry = build_registry(rename_fields)
result = registry["authoring.rename_fields"](
{
"mapping": {"provider_status": "done", "provider_message": "ok"},
"renames": {"provider_status": "status", "provider_message": "message"},
},
RuntimeContext(current_node_id="rename_fields"),
)
assert result == {
"outcome": "ok",
"output": {"mapping": {"status": "done", "message": "ok"}},
}
def test_filter_items_selects_mapping_items_by_exact_match() -> None:
registry = build_registry(filter_items)
result = registry["authoring.filter_items"](
{
"items": [
{"type": "text", "text": "a"},
{"type": "image", "url": "img://1"},
{"type": "text", "text": "b"},
],
"key": "type",
"value": "text",
},
RuntimeContext(current_node_id="filter_items"),
)
output = result["output"]
assert result["outcome"] == "ok"
assert output["items"][0]["text"] == "a"
assert output["items"][1]["text"] == "b"
def test_filter_items_present_selects_mapping_items_containing_key() -> None:
registry = build_registry(filter_items_present)
result = registry["authoring.filter_items_present"](
{
"items": [
{"text": "a"},
{"url": "img://1"},
{"text": None},
],
"key": "text",
},
RuntimeContext(current_node_id="filter_items_present"),
)
output = result["output"]
assert result["outcome"] == "ok"
assert output["items"][0]["text"] == "a"
assert output["items"][1]["text"] is None
def test_extract_field_returns_existing_field_values() -> None:
registry = build_registry(extract_field)
result = registry["authoring.extract_field"](
{
"items": [
{"text": "a"},
{"url": "img://1"},
{"text": "b"},
],
"field": "text",
},
RuntimeContext(current_node_id="extract_field"),
)
assert result["outcome"] == "ok"
assert result["output"]["values"] == ["a", "b"]
def test_concat_joins_strings_with_separator() -> None:
registry = build_registry(concat)
result = registry["authoring.concat"](
{"items": ["a", "b", "c"], "separator": "\n"},
RuntimeContext(current_node_id="concat"),
)
assert result["outcome"] == "ok"
assert result["output"]["text"] == "a\nb\nc"
def test_non_empty_sequence_models_publish_min_items_without_changing_empty_aware_ops() -> (
None
):
assert (
first_item.input_model.model_json_schema()["properties"]["items"]["minItems"]
== 1
)
assert (
last_item.input_model.model_json_schema()["properties"]["items"]["minItems"]
== 1
)
assert (
"minItems"
not in first_item_maybe.input_model.model_json_schema()["properties"]["items"]
)
assert (
"minItems"
not in first_item_or_none.input_model.model_json_schema()["properties"]["items"]
)
assert (
"minItems"
not in last_item_or_none.input_model.model_json_schema()["properties"]["items"]
)
def test_extract_text_content_recipe_filters_extracts_and_joins_text_blocks() -> None:
registry = build_registry(extract_text_content)
result = registry["authoring.extract_text_content"](
{
"content": [
{"type": "text", "text": "hello"},
{"type": "image", "url": "img://1"},
{"type": "text", "text": "world"},
],
"separator": " ",
},
RuntimeContext(current_node_id="extract_text_content"),
)
assert result["outcome"] == "ok"
assert result["output"]["text"] == "hello world"
def test_truthy_routes_truthy_and_falsey_outcomes() -> None:
registry = build_registry(truthy)
ctx = RuntimeContext(current_node_id="truthy")
truthy_result = registry["authoring.truthy"]({"value": "yes"}, ctx)
falsey_result = registry["authoring.truthy"]({"value": ""}, ctx)
assert truthy_result == {"outcome": "truthy", "output": {"value": True}}
assert falsey_result == {"outcome": "falsey", "output": {"value": False}}
def test_runtime_error_raises_with_message_and_details() -> None:
registry = build_registry(runtime_error)
with pytest.raises(RuntimeError, match="bad branch"):
registry["authoring.runtime_error"](
{"message": "bad branch", "details": {"step": "demo"}},
RuntimeContext(current_node_id="runtime_error"),
)