Files
invoke-ai--invokeai/tests/test_graph_execution_state.py
T
wehub-resource-sync cddb07a176
build container image / cpu (push) Waiting to run
build container image / cuda (push) Waiting to run
build container image / rocm (push) Waiting to run
frontend tests / frontend-tests (push) Waiting to run
openapi checks / openapi-checks (push) Waiting to run
python tests / py3.12: macos-default (push) Waiting to run
python tests / py3.11: windows-cpu (push) Waiting to run
python tests / py3.12: windows-cpu (push) Waiting to run
python tests / py3.11: linux-cpu (push) Waiting to run
python tests / py3.12: linux-cpu (push) Waiting to run
typegen checks / typegen-checks (push) Waiting to run
uv lock checks / uv-lock-checks (push) Waiting to run
frontend checks / frontend-checks (push) Waiting to run
lfs checks / lfs-check (push) Waiting to run
python checks / python-checks (push) Waiting to run
python tests / py3.11: macos-default (push) Waiting to run
docs / deploy (push) Has been cancelled
docs / changes (push) Has been cancelled
docs / check-and-build (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:22:06 +08:00

1570 lines
66 KiB
Python

from typing import Optional
from unittest.mock import Mock
import pytest
from pydantic import TypeAdapter
from invokeai.app.invocations.baseinvocation import BaseInvocation, BaseInvocationOutput, InvocationContext
from invokeai.app.invocations.collections import RangeInvocation
from invokeai.app.invocations.fields import InputField, OutputField
from invokeai.app.invocations.logic import IfInvocation, IfInvocationOutput
from invokeai.app.invocations.math import AddInvocation, MultiplyInvocation
from invokeai.app.invocations.primitives import (
BooleanCollectionInvocation,
BooleanCollectionOutput,
BooleanInvocation,
BooleanOutput,
)
from invokeai.app.services.shared.graph import (
CollectInvocation,
Graph,
GraphExecutionState,
IterateInvocation,
WorkflowCallFrame,
)
# This import must happen before other invoke imports or test in other files(!!) break
from tests.test_nodes import (
AnyTypeTestInvocation,
AnyTypeTestInvocationOutput,
PromptCollectionTestInvocation,
PromptTestInvocation,
TextToImageTestInvocation,
create_edge,
)
class IntegerCollectionTestInvocationOutput(BaseInvocationOutput):
collection: list[int] = OutputField(default=[])
class IntegerCollectionFromItemTestInvocation(BaseInvocation):
value: int = InputField(default=0)
def invoke(self, context: InvocationContext) -> IntegerCollectionTestInvocationOutput:
base = self.value * 10
return IntegerCollectionTestInvocationOutput(collection=[base, base + 1])
class IntegerCollectionPassthroughTestInvocation(BaseInvocation):
collection: list[int] = InputField(default=[])
def invoke(self, context: InvocationContext) -> IntegerCollectionTestInvocationOutput:
return IntegerCollectionTestInvocationOutput(collection=self.collection.copy())
class TwoIntegerCollectionsTestInvocation(BaseInvocation):
first: list[int] = InputField(default=[])
second: list[int] = InputField(default=[])
def invoke(self, context: InvocationContext) -> IntegerCollectionTestInvocationOutput:
return IntegerCollectionTestInvocationOutput(collection=self.first + self.second)
@pytest.fixture
def simple_graph() -> Graph:
g = Graph()
g.add_node(PromptTestInvocation(id="1", prompt="Banana sushi"))
g.add_node(TextToImageTestInvocation(id="2"))
g.add_edge(create_edge("1", "prompt", "2", "prompt"))
return g
def invoke_next(g: GraphExecutionState) -> tuple[Optional[BaseInvocation], Optional[BaseInvocationOutput]]:
n = g.next()
if n is None:
return (None, None)
print(f"invoking {n.id}: {type(n)}")
o = n.invoke(Mock(InvocationContext))
g.complete(n.id, o)
return (n, o)
def execute_all_nodes(g: GraphExecutionState) -> list[str]:
"""Execute the graph to completion and return source node ids in execution order."""
executed_source_ids: list[str] = []
while True:
invocation, _output = invoke_next(g)
if invocation is None:
break
executed_source_ids.append(g.prepared_source_mapping[invocation.id])
return executed_source_ids
def test_graph_state_executes_in_order(simple_graph: Graph):
g = GraphExecutionState(graph=simple_graph)
n1 = invoke_next(g)
n2 = invoke_next(g)
n3 = g.next()
assert g.prepared_source_mapping[n1[0].id] == "1"
assert g.prepared_source_mapping[n2[0].id] == "2"
assert n3 is None
assert g.results[n1[0].id].prompt == n1[0].prompt
assert n2[0].prompt == n1[0].prompt
def test_graph_is_complete(simple_graph: Graph):
g = GraphExecutionState(graph=simple_graph)
_ = invoke_next(g)
_ = invoke_next(g)
_ = g.next()
assert g.is_complete()
def test_graph_is_not_complete(simple_graph: Graph):
g = GraphExecutionState(graph=simple_graph)
_ = invoke_next(g)
_ = g.next()
assert not g.is_complete()
def test_graph_waiting_on_workflow_call_blocks_other_ready_nodes():
graph = Graph()
graph.add_node(PromptTestInvocation(id="prompt_a", prompt="a"))
graph.add_node(PromptTestInvocation(id="prompt_b", prompt="b"))
g = GraphExecutionState(graph=graph)
first = g.next()
assert first is not None
waiting_frame = g.build_workflow_call_frame(exec_node_id=first.id, workflow_id="workflow-a")
g.begin_waiting_on_workflow_call(waiting_frame)
assert g.next() is None
assert not g.is_complete()
assert g.is_waiting_on_workflow_call()
def test_graph_build_workflow_call_frame_uses_prepared_and_source_ids():
g = GraphExecutionState(graph=Graph())
g.execution_graph.add_node(PromptTestInvocation(id="prepared-call", prompt="a"))
g.prepared_source_mapping["prepared-call"] = "source-call"
frame = g.build_workflow_call_frame(exec_node_id="prepared-call", workflow_id="workflow-a")
assert frame.prepared_call_node_id == "prepared-call"
assert frame.source_call_node_id == "source-call"
assert frame.workflow_id == "workflow-a"
assert frame.depth == 1
def test_graph_build_workflow_call_frame_rejects_depth_over_limit():
graph = Graph()
graph.add_node(PromptTestInvocation(id="source-call", prompt="a"))
g = GraphExecutionState(
graph=graph,
workflow_call_stack=[
WorkflowCallFrame(
prepared_call_node_id=f"prepared-{i}",
source_call_node_id=f"source-{i}",
workflow_id=f"workflow-{i}",
depth=i + 1,
)
for i in range(4)
],
)
g.execution_graph.add_node(PromptTestInvocation(id="prepared-call", prompt="a"))
g.prepared_source_mapping["prepared-call"] = "source-call"
with pytest.raises(ValueError, match="Maximum workflow call depth"):
g.build_workflow_call_frame(exec_node_id="prepared-call", workflow_id="workflow-a")
def test_graph_execution_state_serializes_workflow_call_state():
graph = Graph()
graph.add_node(PromptTestInvocation(id="source-call", prompt="a"))
g = GraphExecutionState(graph=graph)
g.execution_graph.add_node(PromptTestInvocation(id="prepared-call", prompt="a"))
g.prepared_source_mapping["prepared-call"] = "source-call"
frame = g.build_workflow_call_frame(exec_node_id="prepared-call", workflow_id="workflow-a")
g.workflow_call_stack.append(frame)
g.begin_waiting_on_workflow_call(frame)
restored = GraphExecutionState.model_validate(g.model_dump(warnings=False))
assert restored.workflow_call_stack == [frame]
assert restored.waiting_workflow_call == frame
assert restored.max_workflow_call_depth == 4
def test_graph_waiting_on_workflow_call_blocks_until_suspended_node_is_completed():
graph = Graph()
graph.add_node(PromptTestInvocation(id="prompt_a", prompt="a"))
graph.add_node(PromptTestInvocation(id="prompt_b", prompt="b"))
g = GraphExecutionState(graph=graph)
first = g.next()
assert first is not None
waiting_frame = g.build_workflow_call_frame(exec_node_id=first.id, workflow_id="workflow-a")
g.begin_waiting_on_workflow_call(waiting_frame)
assert g.next() is None
g.end_waiting_on_workflow_call()
g.complete(first.id, first.invoke(Mock(InvocationContext)))
resumed = g.next()
assert resumed is not None
assert resumed.id != first.id
assert g.prepared_source_mapping[resumed.id] == "prompt_b"
def test_graph_begin_waiting_on_workflow_call_rejects_double_entry():
g = GraphExecutionState(graph=Graph())
g.execution_graph.add_node(PromptTestInvocation(id="prepared-call", prompt="a"))
g.prepared_source_mapping["prepared-call"] = "source-call"
first_frame = g.build_workflow_call_frame(exec_node_id="prepared-call", workflow_id="workflow-a")
g.begin_waiting_on_workflow_call(first_frame)
with pytest.raises(ValueError, match="already waiting"):
g.begin_waiting_on_workflow_call(first_frame)
def test_graph_build_workflow_call_frame_rejects_missing_execution_node():
g = GraphExecutionState(graph=Graph())
with pytest.raises(Exception, match="not found in execution graph"):
g.build_workflow_call_frame(exec_node_id="missing-node", workflow_id="workflow-a")
def test_graph_build_workflow_call_frame_rejects_unprepared_execution_node():
g = GraphExecutionState(graph=Graph())
g.execution_graph.add_node(PromptTestInvocation(id="prepared-call", prompt="a"))
with pytest.raises(ValueError, match="not a prepared execution node"):
g.build_workflow_call_frame(exec_node_id="prepared-call", workflow_id="workflow-a")
def test_graph_child_workflow_execution_state_inherits_stack_and_isolates_runtime_state():
parent_graph = Graph()
child_graph = Graph()
parent = GraphExecutionState(graph=parent_graph)
parent.execution_graph.add_node(PromptTestInvocation(id="prepared-parent", prompt="a"))
parent.prepared_source_mapping["prepared-parent"] = "source-parent"
parent.results["prepared-parent"] = PromptTestInvocation(id="result-node", prompt="existing").invoke(
Mock(InvocationContext)
)
parent.executed.add("prepared-parent")
root_frame = parent.build_workflow_call_frame(exec_node_id="prepared-parent", workflow_id="workflow-a")
parent.workflow_call_stack.append(root_frame)
parent.execution_graph.add_node(PromptTestInvocation(id="prepared-child", prompt="b"))
parent.prepared_source_mapping["prepared-child"] = "source-child"
child_frame = parent.build_workflow_call_frame(exec_node_id="prepared-child", workflow_id="workflow-b")
child_state = parent.create_child_workflow_execution_state(graph=child_graph, frame=child_frame)
assert child_state.graph == child_graph
assert child_state.workflow_call_stack == [root_frame, child_frame]
assert child_state.max_workflow_call_depth == parent.max_workflow_call_depth
assert child_state.waiting_workflow_call is None
assert child_state.results == {}
assert child_state.executed == set()
def test_graph_waiting_workflow_call_tracks_parent_child_metadata():
parent = GraphExecutionState(graph=Graph())
parent.execution_graph.add_node(PromptTestInvocation(id="prepared-parent", prompt="a"))
parent.prepared_source_mapping["prepared-parent"] = "source-parent"
frame = parent.build_workflow_call_frame(exec_node_id="prepared-parent", workflow_id="workflow-a")
child = parent.create_child_workflow_execution_state(graph=Graph(), frame=frame)
parent.begin_waiting_on_workflow_call(frame)
parent.attach_waiting_workflow_call_child_session(child)
assert parent.waiting_workflow_call_execution is not None
assert parent.waiting_workflow_call_execution.parent_session_id == parent.id
assert parent.waiting_workflow_call_execution.child_session_id == child.id
assert parent.waiting_workflow_call_execution.status == "running_child"
assert child.workflow_call_parent is not None
assert child.workflow_call_parent.workflow_call_id == parent.waiting_workflow_call_execution.id
assert child.workflow_call_parent.parent_session_id == parent.id
def test_graph_attach_waiting_workflow_call_child_sessions_tracks_fan_out_metadata():
parent = GraphExecutionState(graph=Graph())
parent.execution_graph.add_node(AddInvocation(id="prepared-parent", a=1, b=2))
parent.prepared_source_mapping["prepared-parent"] = "source-parent"
frame = parent.build_workflow_call_frame(exec_node_id="prepared-parent", workflow_id="workflow-a")
child_a = parent.create_child_workflow_execution_state(Graph(), frame)
child_b = parent.create_child_workflow_execution_state(Graph(), frame)
parent.begin_waiting_on_workflow_call(frame)
parent.attach_waiting_workflow_call_child_sessions([child_a, child_b])
assert parent.waiting_workflow_call_execution is not None
assert parent.waiting_workflow_call_execution.child_session_ids == [child_a.id, child_b.id]
assert parent.waiting_workflow_call_execution.expected_child_count == 2
assert parent.waiting_workflow_call_child_session is None
assert child_a.workflow_call_parent is not None
assert child_b.workflow_call_parent is not None
def test_graph_record_waiting_workflow_call_child_completion_aggregates_named_values():
parent = GraphExecutionState(graph=Graph())
parent.execution_graph.add_node(AddInvocation(id="prepared-parent", a=1, b=2))
parent.prepared_source_mapping["prepared-parent"] = "source-parent"
frame = parent.build_workflow_call_frame(exec_node_id="prepared-parent", workflow_id="workflow-a")
child_a = parent.create_child_workflow_execution_state(Graph(), frame)
child_b = parent.create_child_workflow_execution_state(Graph(), frame)
parent.begin_waiting_on_workflow_call(frame)
parent.attach_waiting_workflow_call_child_sessions([child_a, child_b])
is_complete, aggregated_values = parent.record_waiting_workflow_call_child_completion(
101, {"sum": 3, "images": "image-a"}
)
assert is_complete is False
assert aggregated_values == {"sum": [3], "images": ["image-a"]}
is_complete, aggregated_values = parent.record_waiting_workflow_call_child_completion(
102, {"sum": 7, "images": "image-b"}
)
assert is_complete is True
assert aggregated_values == {"sum": [3, 7], "images": ["image-a", "image-b"]}
assert parent.waiting_workflow_call_execution is not None
assert parent.waiting_workflow_call_execution.completed_child_item_ids == [101, 102]
def test_graph_record_waiting_workflow_call_child_completion_preserves_enqueue_order():
parent = GraphExecutionState(graph=Graph())
parent.execution_graph.add_node(AddInvocation(id="prepared-parent", a=1, b=2))
parent.prepared_source_mapping["prepared-parent"] = "source-parent"
frame = parent.build_workflow_call_frame(exec_node_id="prepared-parent", workflow_id="workflow-a")
child_a = parent.create_child_workflow_execution_state(Graph(), frame)
child_b = parent.create_child_workflow_execution_state(Graph(), frame)
parent.begin_waiting_on_workflow_call(frame)
parent.attach_waiting_workflow_call_child_sessions([child_a, child_b])
parent.set_waiting_workflow_call_child_item_ids([101, 102])
parent.record_waiting_workflow_call_child_completion(102, {"sum": 7})
is_complete, aggregated_values = parent.record_waiting_workflow_call_child_completion(101, {"sum": 3})
assert is_complete is True
assert aggregated_values == {"sum": [3, 7]}
def test_graph_end_waiting_on_workflow_call_records_lifecycle_history():
parent = GraphExecutionState(graph=Graph())
parent.execution_graph.add_node(PromptTestInvocation(id="prepared-parent", prompt="a"))
parent.prepared_source_mapping["prepared-parent"] = "source-parent"
frame = parent.build_workflow_call_frame(exec_node_id="prepared-parent", workflow_id="workflow-a")
child = parent.create_child_workflow_execution_state(graph=Graph(), frame=frame)
parent.begin_waiting_on_workflow_call(frame)
parent.attach_waiting_workflow_call_child_session(child)
parent.end_waiting_on_workflow_call(status="failed", error_message="child failed")
assert parent.waiting_workflow_call is None
assert parent.waiting_workflow_call_execution is None
assert parent.waiting_workflow_call_child_session is None
assert len(parent.workflow_call_history) == 1
assert parent.workflow_call_history[0].status == "failed"
assert parent.workflow_call_history[0].error_message == "child failed"
assert parent.workflow_call_history[0].parent_session_id == parent.id
assert parent.workflow_call_history[0].child_session_id == child.id
def test_graph_execution_state_serializes_recursive_workflow_call_stack():
g = GraphExecutionState(
graph=Graph(),
workflow_call_stack=[
WorkflowCallFrame(
prepared_call_node_id="prepared-a",
source_call_node_id="source-a",
workflow_id="workflow-a",
depth=1,
),
WorkflowCallFrame(
prepared_call_node_id="prepared-b",
source_call_node_id="source-b",
workflow_id="workflow-b",
depth=2,
),
WorkflowCallFrame(
prepared_call_node_id="prepared-a-2",
source_call_node_id="source-a-2",
workflow_id="workflow-a",
depth=3,
),
],
)
restored = GraphExecutionState.model_validate(g.model_dump(warnings=False))
assert restored.workflow_call_stack == g.workflow_call_stack
# TODO: test completion with iterators/subgraphs
def test_graph_state_expands_iterator():
graph = Graph()
graph.add_node(RangeInvocation(id="0", start=0, stop=3, step=1))
graph.add_node(IterateInvocation(id="1"))
graph.add_node(MultiplyInvocation(id="2", b=10))
graph.add_node(AddInvocation(id="3", b=1))
graph.add_edge(create_edge("0", "collection", "1", "collection"))
graph.add_edge(create_edge("1", "item", "2", "a"))
graph.add_edge(create_edge("2", "value", "3", "a"))
g = GraphExecutionState(graph=graph)
while not g.is_complete():
invoke_next(g)
prepared_add_nodes = g.source_prepared_mapping["3"]
results = {g.results[n].value for n in prepared_add_nodes}
expected = {1, 11, 21}
assert results == expected
def test_graph_state_collects():
graph = Graph()
test_prompts = ["Banana sushi", "Cat sushi"]
graph.add_node(PromptCollectionTestInvocation(id="1", collection=list(test_prompts)))
graph.add_node(IterateInvocation(id="2"))
graph.add_node(PromptTestInvocation(id="3"))
graph.add_node(CollectInvocation(id="4"))
graph.add_edge(create_edge("1", "collection", "2", "collection"))
graph.add_edge(create_edge("2", "item", "3", "prompt"))
graph.add_edge(create_edge("3", "prompt", "4", "item"))
g = GraphExecutionState(graph=graph)
_ = invoke_next(g)
_ = invoke_next(g)
_ = invoke_next(g)
_ = invoke_next(g)
_ = invoke_next(g)
n6 = invoke_next(g)
assert isinstance(n6[0], CollectInvocation)
assert sorted(g.results[n6[0].id].collection) == sorted(test_prompts)
def test_graph_state_resumes_partially_executed_session_after_json_round_trip():
graph = Graph()
graph.add_node(RangeInvocation(id="c", start=1, stop=5, step=1))
graph.add_node(IterateInvocation(id="iter"))
graph.add_node(AddInvocation(id="add", b=1))
graph.add_node(CollectInvocation(id="collect"))
graph.add_edge(create_edge("c", "collection", "iter", "collection"))
graph.add_edge(create_edge("iter", "item", "add", "a"))
graph.add_edge(create_edge("add", "value", "collect", "item"))
state = GraphExecutionState(graph=graph)
for _ in range(4):
invocation, output = invoke_next(state)
assert invocation is not None
assert output is not None
raw = state.model_dump_json(warnings=False, exclude_none=True)
resumed = TypeAdapter(GraphExecutionState).validate_json(raw, strict=False)
registry = resumed._prepared_registry()
assert all(
registry.get_iteration_path(exec_node_id) is not None for exec_node_id in resumed.prepared_source_mapping
)
executed_source_ids = execute_all_nodes(resumed)
assert executed_source_ids
assert "add" in executed_source_ids
assert "collect" in resumed.source_prepared_mapping
prepared_collect_id = next(iter(resumed.source_prepared_mapping["collect"]))
assert resumed.results[prepared_collect_id].collection == [2, 3, 4, 5]
def test_if_graph_state_resumes_resolved_branch_after_json_round_trip():
graph = Graph()
graph.add_node(BooleanInvocation(id="condition", value=True))
graph.add_node(PromptTestInvocation(id="true_value", prompt="true branch"))
graph.add_node(PromptTestInvocation(id="false_value", prompt="false branch"))
graph.add_node(IfInvocation(id="if"))
graph.add_node(PromptTestInvocation(id="selected_output"))
graph.add_edge(create_edge("condition", "value", "if", "condition"))
graph.add_edge(create_edge("true_value", "prompt", "if", "true_input"))
graph.add_edge(create_edge("false_value", "prompt", "if", "false_input"))
graph.add_edge(create_edge("if", "value", "selected_output", "prompt"))
state = GraphExecutionState(graph=graph)
for _ in range(2):
invocation, output = invoke_next(state)
assert invocation is not None
assert output is not None
raw = state.model_dump_json(warnings=False, exclude_none=True)
resumed = TypeAdapter(GraphExecutionState).validate_json(raw, strict=False)
executed_source_ids = execute_all_nodes(resumed)
prepared_selected_output_id = next(iter(resumed.source_prepared_mapping["selected_output"]))
assert resumed.results[prepared_selected_output_id].prompt == "true branch"
assert set(executed_source_ids) == {"if", "selected_output"}
assert "false_value" not in executed_source_ids
def test_graph_state_prepares_eagerly():
"""Tests that all prepareable nodes are prepared"""
graph = Graph()
test_prompts = ["Banana sushi", "Cat sushi"]
graph.add_node(PromptCollectionTestInvocation(id="prompt_collection", collection=list(test_prompts)))
graph.add_node(IterateInvocation(id="iterate"))
graph.add_node(PromptTestInvocation(id="prompt_iterated"))
graph.add_edge(create_edge("prompt_collection", "collection", "iterate", "collection"))
graph.add_edge(create_edge("iterate", "item", "prompt_iterated", "prompt"))
# separated, fully-preparable chain of nodes
graph.add_node(PromptTestInvocation(id="prompt_chain_1", prompt="Dinosaur sushi"))
graph.add_node(PromptTestInvocation(id="prompt_chain_2"))
graph.add_node(PromptTestInvocation(id="prompt_chain_3"))
graph.add_edge(create_edge("prompt_chain_1", "prompt", "prompt_chain_2", "prompt"))
graph.add_edge(create_edge("prompt_chain_2", "prompt", "prompt_chain_3", "prompt"))
g = GraphExecutionState(graph=graph)
g.next()
assert "prompt_collection" in g.source_prepared_mapping
assert "prompt_chain_1" in g.source_prepared_mapping
assert "prompt_chain_2" in g.source_prepared_mapping
assert "prompt_chain_3" in g.source_prepared_mapping
assert "iterate" not in g.source_prepared_mapping
assert "prompt_iterated" not in g.source_prepared_mapping
def test_graph_executes_depth_first():
"""Tests that the graph executes depth-first, executing a branch as far as possible before moving to the next branch"""
def assert_topo_order_and_all_executed(state: GraphExecutionState, order: list[str]):
"""
Validates:
1) Every materialized exec node executed exactly once.
2) Execution order respects all exec-graph dependencies (u→v ⇒ u before v).
"""
# order must be EXEC node ids in run order
exec_nodes = set(state.execution_graph.nodes.keys())
# 1) coverage: all exec nodes ran, and no duplicates
pos = {nid: i for i, nid in enumerate(order)}
assert set(pos.keys()) == exec_nodes, (
f"Executed {len(pos)} of {len(exec_nodes)} nodes. Missing: {sorted(exec_nodes - set(pos))[:10]}"
)
assert len(pos) == len(order), "Duplicate execution detected"
# 2) topo order: parents before children
for e in state.execution_graph.edges:
u = e.source.node_id
v = e.destination.node_id
assert pos[u] < pos[v], f"child {v} ran before parent {u}"
graph = Graph()
test_prompts = ["Banana sushi", "Cat sushi"]
graph.add_node(PromptCollectionTestInvocation(id="prompt_collection", collection=list(test_prompts)))
graph.add_node(IterateInvocation(id="iterate"))
graph.add_node(PromptTestInvocation(id="prompt_iterated"))
graph.add_node(PromptTestInvocation(id="prompt_successor"))
graph.add_edge(create_edge("prompt_collection", "collection", "iterate", "collection"))
graph.add_edge(create_edge("iterate", "item", "prompt_iterated", "prompt"))
graph.add_edge(create_edge("prompt_iterated", "prompt", "prompt_successor", "prompt"))
g = GraphExecutionState(graph=graph)
order: list[str] = []
while True:
n = g.next()
if n is None:
break
o = n.invoke(Mock(InvocationContext))
g.complete(n.id, o)
order.append(n.id)
assert_topo_order_and_all_executed(g, order)
def test_graph_scheduler_drains_active_class_before_switching():
graph = Graph()
graph.add_node(PromptTestInvocation(id="prompt_a", prompt="a"))
graph.add_node(PromptTestInvocation(id="prompt_b", prompt="b"))
graph.add_node(TextToImageTestInvocation(id="image"))
g = GraphExecutionState(graph=graph)
g.set_ready_order([PromptTestInvocation, TextToImageTestInvocation])
first = invoke_next(g)[0]
second = invoke_next(g)[0]
third = invoke_next(g)[0]
assert first is not None
assert g.prepared_source_mapping[first.id] == "prompt_a"
assert g.prepared_source_mapping[second.id] == "prompt_b"
assert g.prepared_source_mapping[third.id] == "image"
def test_graph_scheduler_skips_stale_ready_entries():
graph = Graph()
graph.add_node(PromptTestInvocation(id="prompt_a", prompt="a"))
graph.add_node(PromptTestInvocation(id="prompt_b", prompt="b"))
g = GraphExecutionState(graph=graph)
g.set_ready_order([PromptTestInvocation])
first = invoke_next(g)[0]
assert first is not None
prompt_queue = g._queue_for(PromptTestInvocation.__name__)
prompt_queue.appendleft(first.id)
second = g.next()
assert second is not None
assert second.id != first.id
assert g.prepared_source_mapping[second.id] == "prompt_b"
def test_graph_scheduler_falls_back_to_non_priority_ready_classes():
graph = Graph()
graph.add_node(TextToImageTestInvocation(id="image"))
g = GraphExecutionState(graph=graph)
g.set_ready_order([PromptTestInvocation])
next_node = g.next()
assert next_node is not None
assert g.prepared_source_mapping[next_node.id] == "image"
# Because this tests deterministic ordering, we run it multiple times
@pytest.mark.parametrize("execution_number", range(5))
def test_graph_iterate_execution_order(execution_number: int):
"""Tests that iterate nodes execution is ordered by the order of the collection"""
graph = Graph()
test_prompts = ["Banana sushi", "Cat sushi", "Strawberry Sushi", "Dinosaur Sushi"]
graph.add_node(PromptCollectionTestInvocation(id="prompt_collection", collection=list(test_prompts)))
graph.add_node(IterateInvocation(id="iterate"))
graph.add_node(PromptTestInvocation(id="prompt_iterated"))
graph.add_edge(create_edge("prompt_collection", "collection", "iterate", "collection"))
graph.add_edge(create_edge("iterate", "item", "prompt_iterated", "prompt"))
g = GraphExecutionState(graph=graph)
_ = invoke_next(g)
_ = invoke_next(g)
assert _[1].item == "Banana sushi"
_ = invoke_next(g)
assert _[1].item == "Cat sushi"
_ = invoke_next(g)
assert _[1].item == "Strawberry Sushi"
_ = invoke_next(g)
assert _[1].item == "Dinosaur Sushi"
_ = invoke_next(g)
# Because this tests deterministic ordering, we run it multiple times
@pytest.mark.parametrize("execution_number", range(5))
def test_graph_nested_iterate_execution_order(execution_number: int):
"""
Validates best-effort in-order execution for nodes expanded under nested iterators.
Expected lexicographic order by (outer_index, inner_index), subject to readiness.
"""
graph = Graph()
# Outer iterator: [0, 1]
graph.add_node(RangeInvocation(id="outer_range", start=0, stop=2, step=1))
graph.add_node(IterateInvocation(id="outer_iter"))
# Inner iterator is derived from the outer item:
# start = outer_item * 10
# stop = start + 2 => yields 2 items per outer item
graph.add_node(MultiplyInvocation(id="mul10", b=10))
graph.add_node(AddInvocation(id="stop_plus2", b=2))
graph.add_node(RangeInvocation(id="inner_range", start=0, stop=1, step=1))
graph.add_node(IterateInvocation(id="inner_iter"))
# Observe inner items (they encode outer via start=outer*10)
graph.add_node(AddInvocation(id="sum", b=0))
graph.add_edge(create_edge("outer_range", "collection", "outer_iter", "collection"))
graph.add_edge(create_edge("outer_iter", "item", "mul10", "a"))
graph.add_edge(create_edge("mul10", "value", "stop_plus2", "a"))
graph.add_edge(create_edge("mul10", "value", "inner_range", "start"))
graph.add_edge(create_edge("stop_plus2", "value", "inner_range", "stop"))
graph.add_edge(create_edge("inner_range", "collection", "inner_iter", "collection"))
graph.add_edge(create_edge("inner_iter", "item", "sum", "a"))
g = GraphExecutionState(graph=graph)
sum_values: list[int] = []
while True:
n, o = invoke_next(g)
if n is None:
break
if g.prepared_source_mapping[n.id] == "sum":
sum_values.append(o.value)
assert sum_values == [0, 1, 10, 11]
def test_graph_collector_nested_under_outer_iterator_collects_only_current_outer_iteration_items():
graph = Graph()
graph.add_node(RangeInvocation(id="outer_range", start=0, stop=2, step=1))
graph.add_node(IterateInvocation(id="outer_iter"))
graph.add_node(IntegerCollectionFromItemTestInvocation(id="inner_collection"))
graph.add_node(IterateInvocation(id="inner_iter"))
graph.add_node(AddInvocation(id="inner_item", b=0))
graph.add_node(CollectInvocation(id="collect"))
graph.add_node(IntegerCollectionPassthroughTestInvocation(id="per_outer_consumer"))
graph.add_edge(create_edge("outer_range", "collection", "outer_iter", "collection"))
graph.add_edge(create_edge("outer_iter", "item", "inner_collection", "value"))
graph.add_edge(create_edge("inner_collection", "collection", "inner_iter", "collection"))
graph.add_edge(create_edge("inner_iter", "item", "inner_item", "a"))
graph.add_edge(create_edge("inner_item", "value", "collect", "item"))
graph.add_edge(create_edge("collect", "collection", "per_outer_consumer", "collection"))
g = GraphExecutionState(graph=graph)
execute_all_nodes(g)
prepared_consumer_ids = g.source_prepared_mapping["per_outer_consumer"]
consumer_collections = sorted(g.results[node_id].collection for node_id in prepared_consumer_ids)
assert consumer_collections == [[0, 1], [10, 11]]
def test_graph_collector_reuses_outer_collection_input_for_each_nested_iterator_group():
graph = Graph()
graph.add_node(RangeInvocation(id="base_collection", start=100, stop=101, step=1))
graph.add_node(RangeInvocation(id="outer_range", start=0, stop=2, step=1))
graph.add_node(IterateInvocation(id="outer_iter"))
graph.add_node(IntegerCollectionFromItemTestInvocation(id="inner_collection"))
graph.add_node(IterateInvocation(id="inner_iter"))
graph.add_node(AddInvocation(id="inner_item", b=0))
graph.add_node(CollectInvocation(id="collect"))
graph.add_edge(create_edge("base_collection", "collection", "collect", "collection"))
graph.add_edge(create_edge("outer_range", "collection", "outer_iter", "collection"))
graph.add_edge(create_edge("outer_iter", "item", "inner_collection", "value"))
graph.add_edge(create_edge("inner_collection", "collection", "inner_iter", "collection"))
graph.add_edge(create_edge("inner_iter", "item", "inner_item", "a"))
graph.add_edge(create_edge("inner_item", "value", "collect", "item"))
g = GraphExecutionState(graph=graph)
execute_all_nodes(g)
prepared_collect_ids = g.source_prepared_mapping["collect"]
collect_results = sorted(g.results[node_id].collection for node_id in prepared_collect_ids)
assert collect_results == [[100, 0, 1], [100, 10, 11]]
def test_graph_collector_nested_under_three_iterators_preserves_outer_iteration_paths():
graph = Graph()
graph.add_node(RangeInvocation(id="outer_range", start=0, stop=2, step=1))
graph.add_node(IterateInvocation(id="outer_iter"))
graph.add_node(IntegerCollectionFromItemTestInvocation(id="middle_collection"))
graph.add_node(IterateInvocation(id="middle_iter"))
graph.add_node(IntegerCollectionFromItemTestInvocation(id="inner_collection"))
graph.add_node(IterateInvocation(id="inner_iter"))
graph.add_node(AddInvocation(id="inner_item", b=0))
graph.add_node(CollectInvocation(id="collect"))
graph.add_node(IntegerCollectionPassthroughTestInvocation(id="per_middle_consumer"))
graph.add_edge(create_edge("outer_range", "collection", "outer_iter", "collection"))
graph.add_edge(create_edge("outer_iter", "item", "middle_collection", "value"))
graph.add_edge(create_edge("middle_collection", "collection", "middle_iter", "collection"))
graph.add_edge(create_edge("middle_iter", "item", "inner_collection", "value"))
graph.add_edge(create_edge("inner_collection", "collection", "inner_iter", "collection"))
graph.add_edge(create_edge("inner_iter", "item", "inner_item", "a"))
graph.add_edge(create_edge("inner_item", "value", "collect", "item"))
graph.add_edge(create_edge("collect", "collection", "per_middle_consumer", "collection"))
g = GraphExecutionState(graph=graph)
execute_all_nodes(g)
prepared_consumer_ids = g.source_prepared_mapping["per_middle_consumer"]
consumer_collections = sorted(g.results[node_id].collection for node_id in prepared_consumer_ids)
assert consumer_collections == [[0, 1], [10, 11], [100, 101], [110, 111]]
def test_graph_collector_with_mixed_depth_item_inputs_keeps_outer_iterations_separate():
graph = Graph()
graph.add_node(RangeInvocation(id="outer_range", start=0, stop=2, step=1))
graph.add_node(IterateInvocation(id="outer_iter"))
graph.add_node(AddInvocation(id="outer_item", b=100))
graph.add_node(IntegerCollectionFromItemTestInvocation(id="inner_collection"))
graph.add_node(IterateInvocation(id="inner_iter"))
graph.add_node(AddInvocation(id="inner_item", b=0))
graph.add_node(CollectInvocation(id="collect"))
graph.add_edge(create_edge("outer_range", "collection", "outer_iter", "collection"))
graph.add_edge(create_edge("outer_iter", "item", "outer_item", "a"))
graph.add_edge(create_edge("outer_item", "value", "collect", "item"))
graph.add_edge(create_edge("outer_iter", "item", "inner_collection", "value"))
graph.add_edge(create_edge("inner_collection", "collection", "inner_iter", "collection"))
graph.add_edge(create_edge("inner_iter", "item", "inner_item", "a"))
graph.add_edge(create_edge("inner_item", "value", "collect", "item"))
g = GraphExecutionState(graph=graph)
execute_all_nodes(g)
collect_results = sorted(g.results[node_id].collection for node_id in g.source_prepared_mapping["collect"])
assert collect_results == [[100, 0, 1], [101, 10, 11]]
def test_graph_consumer_matches_collector_parents_at_different_iteration_depths():
graph = Graph()
graph.add_node(RangeInvocation(id="outer_range", start=0, stop=2, step=1))
graph.add_node(IterateInvocation(id="outer_iter"))
graph.add_node(IntegerCollectionFromItemTestInvocation(id="middle_collection"))
graph.add_node(IterateInvocation(id="middle_iter"))
graph.add_node(AddInvocation(id="shallow_item", b=0))
graph.add_node(CollectInvocation(id="shallow_collect"))
graph.add_node(IntegerCollectionFromItemTestInvocation(id="inner_collection"))
graph.add_node(IterateInvocation(id="inner_iter"))
graph.add_node(AddInvocation(id="deep_item", b=0))
graph.add_node(CollectInvocation(id="deep_collect"))
graph.add_node(TwoIntegerCollectionsTestInvocation(id="consumer"))
graph.add_edge(create_edge("outer_range", "collection", "outer_iter", "collection"))
graph.add_edge(create_edge("outer_iter", "item", "middle_collection", "value"))
graph.add_edge(create_edge("middle_collection", "collection", "middle_iter", "collection"))
graph.add_edge(create_edge("middle_iter", "item", "shallow_item", "a"))
graph.add_edge(create_edge("shallow_item", "value", "shallow_collect", "item"))
graph.add_edge(create_edge("middle_iter", "item", "inner_collection", "value"))
graph.add_edge(create_edge("inner_collection", "collection", "inner_iter", "collection"))
graph.add_edge(create_edge("inner_iter", "item", "deep_item", "a"))
graph.add_edge(create_edge("deep_item", "value", "deep_collect", "item"))
graph.add_edge(create_edge("shallow_collect", "collection", "consumer", "first"))
graph.add_edge(create_edge("deep_collect", "collection", "consumer", "second"))
g = GraphExecutionState(graph=graph)
execute_all_nodes(g)
consumer_results = sorted(g.results[node_id].collection for node_id in g.source_prepared_mapping["consumer"])
assert consumer_results == [
[0, 1, 0, 1],
[0, 1, 10, 11],
[10, 11, 100, 101],
[10, 11, 110, 111],
]
def test_graph_consumer_reuses_global_parent_for_each_nested_collector_iteration():
graph = Graph()
graph.add_node(RangeInvocation(id="global_collection", start=99, stop=100, step=1))
graph.add_node(RangeInvocation(id="outer_range", start=0, stop=2, step=1))
graph.add_node(IterateInvocation(id="outer_iter"))
graph.add_node(IntegerCollectionFromItemTestInvocation(id="inner_collection"))
graph.add_node(IterateInvocation(id="inner_iter"))
graph.add_node(AddInvocation(id="inner_item", b=0))
graph.add_node(CollectInvocation(id="collect"))
graph.add_node(TwoIntegerCollectionsTestInvocation(id="consumer"))
graph.add_edge(create_edge("global_collection", "collection", "consumer", "second"))
graph.add_edge(create_edge("outer_range", "collection", "outer_iter", "collection"))
graph.add_edge(create_edge("outer_iter", "item", "inner_collection", "value"))
graph.add_edge(create_edge("inner_collection", "collection", "inner_iter", "collection"))
graph.add_edge(create_edge("inner_iter", "item", "inner_item", "a"))
graph.add_edge(create_edge("inner_item", "value", "collect", "item"))
graph.add_edge(create_edge("collect", "collection", "consumer", "first"))
g = GraphExecutionState(graph=graph)
execute_all_nodes(g)
consumer_results = sorted(g.results[node_id].collection for node_id in g.source_prepared_mapping["consumer"])
assert consumer_results == [[0, 1, 99], [10, 11, 99]]
def test_graph_validate_self_iterator_without_collection_input_raises_invalid_edge_error():
"""Iterator nodes with no collection input should fail validation cleanly.
This test exposes the bug where validation crashes with IndexError instead of raising InvalidEdgeError.
"""
from invokeai.app.services.shared.graph import InvalidEdgeError
graph = Graph()
graph.add_node(IterateInvocation(id="iterate"))
with pytest.raises(InvalidEdgeError):
graph.validate_self()
def test_graph_validate_self_collector_without_item_inputs_raises_invalid_edge_error():
"""Collector nodes with no item inputs should fail validation cleanly.
This test exposes the bug where validation can crash (e.g. StopIteration) instead of raising InvalidEdgeError.
"""
from invokeai.app.services.shared.graph import InvalidEdgeError
graph = Graph()
graph.add_node(CollectInvocation(id="collect"))
with pytest.raises(InvalidEdgeError):
graph.validate_self()
def test_if_invocation_selects_true_input_value():
invocation = IfInvocation(id="if", condition=True, true_input="true", false_input="false")
output = invocation.invoke(Mock(InvocationContext))
assert output.value == "true"
def test_if_invocation_outputs_none_when_selected_input_is_missing():
invocation = IfInvocation(id="if", condition=False, true_input="true")
output = invocation.invoke(Mock(InvocationContext))
assert output.value is None
def test_if_invocation_output_allows_missing_value_on_deserialization():
output = IfInvocationOutput.model_validate({"type": "if_output"})
assert output.value is None
def test_if_invocation_output_connects_to_downstream_input():
graph = Graph()
graph.add_node(IfInvocation(id="if", condition=True, true_input="connected value", false_input="unused"))
graph.add_node(PromptTestInvocation(id="prompt"))
graph.add_edge(create_edge("if", "value", "prompt", "prompt"))
g = GraphExecutionState(graph=graph)
while not g.is_complete():
invoke_next(g)
prepared_prompt_nodes = g.source_prepared_mapping["prompt"]
assert len(prepared_prompt_nodes) == 1
prepared_prompt_node_id = next(iter(prepared_prompt_nodes))
assert g.results[prepared_prompt_node_id].prompt == "connected value"
@pytest.mark.xfail(strict=True, reason="Legacy eager If-node execution should no longer occur")
def test_if_graph_current_behavior_executes_both_branches_and_shared_ancestors():
graph = Graph()
graph.add_node(BooleanInvocation(id="condition", value=True))
graph.add_node(PromptTestInvocation(id="shared", prompt="shared value"))
graph.add_node(PromptTestInvocation(id="true_mid"))
graph.add_node(PromptTestInvocation(id="true_leaf"))
graph.add_node(PromptTestInvocation(id="false_mid"))
graph.add_node(PromptTestInvocation(id="false_leaf"))
graph.add_node(PromptTestInvocation(id="side_consumer"))
graph.add_node(IfInvocation(id="if"))
graph.add_node(PromptTestInvocation(id="selected_output"))
graph.add_edge(create_edge("condition", "value", "if", "condition"))
graph.add_edge(create_edge("shared", "prompt", "true_mid", "prompt"))
graph.add_edge(create_edge("true_mid", "prompt", "true_leaf", "prompt"))
graph.add_edge(create_edge("true_leaf", "prompt", "if", "true_input"))
graph.add_edge(create_edge("shared", "prompt", "false_mid", "prompt"))
graph.add_edge(create_edge("false_mid", "prompt", "false_leaf", "prompt"))
graph.add_edge(create_edge("false_leaf", "prompt", "if", "false_input"))
graph.add_edge(create_edge("shared", "prompt", "side_consumer", "prompt"))
graph.add_edge(create_edge("if", "value", "selected_output", "prompt"))
g = GraphExecutionState(graph=graph)
executed_source_ids = execute_all_nodes(g)
assert set(executed_source_ids) == {
"condition",
"shared",
"true_mid",
"true_leaf",
"false_mid",
"false_leaf",
"side_consumer",
"if",
"selected_output",
}
assert executed_source_ids.count("false_mid") == 1
assert executed_source_ids.count("false_leaf") == 1
prepared_selected_output_id = next(iter(g.source_prepared_mapping["selected_output"]))
assert g.results[prepared_selected_output_id].prompt == "shared value"
@pytest.mark.xfail(strict=True, reason="Legacy eager If-node execution should no longer occur")
def test_if_graph_current_behavior_executes_both_simple_branches():
graph = Graph()
graph.add_node(BooleanInvocation(id="condition", value=True))
graph.add_node(PromptTestInvocation(id="true_value", prompt="true branch"))
graph.add_node(PromptTestInvocation(id="false_value", prompt="false branch"))
graph.add_node(IfInvocation(id="if"))
graph.add_node(PromptTestInvocation(id="selected_output"))
graph.add_edge(create_edge("condition", "value", "if", "condition"))
graph.add_edge(create_edge("true_value", "prompt", "if", "true_input"))
graph.add_edge(create_edge("false_value", "prompt", "if", "false_input"))
graph.add_edge(create_edge("if", "value", "selected_output", "prompt"))
g = GraphExecutionState(graph=graph)
executed_source_ids = execute_all_nodes(g)
assert set(executed_source_ids) == {"condition", "true_value", "false_value", "if", "selected_output"}
prepared_selected_output_id = next(iter(g.source_prepared_mapping["selected_output"]))
assert g.results[prepared_selected_output_id].prompt == "true branch"
def test_if_graph_optimized_behavior_executes_only_selected_simple_branch():
graph = Graph()
graph.add_node(BooleanInvocation(id="condition", value=True))
graph.add_node(PromptTestInvocation(id="true_value", prompt="true branch"))
graph.add_node(PromptTestInvocation(id="false_value", prompt="false branch"))
graph.add_node(IfInvocation(id="if"))
graph.add_node(PromptTestInvocation(id="selected_output"))
graph.add_edge(create_edge("condition", "value", "if", "condition"))
graph.add_edge(create_edge("true_value", "prompt", "if", "true_input"))
graph.add_edge(create_edge("false_value", "prompt", "if", "false_input"))
graph.add_edge(create_edge("if", "value", "selected_output", "prompt"))
g = GraphExecutionState(graph=graph)
executed_source_ids = execute_all_nodes(g)
assert set(executed_source_ids) == {"condition", "true_value", "if", "selected_output"}
assert "false_value" not in executed_source_ids
def test_if_graph_optimized_behavior_records_skipped_branch_in_execution_history():
graph = Graph()
graph.add_node(BooleanInvocation(id="condition", value=True))
graph.add_node(PromptTestInvocation(id="true_value", prompt="true branch"))
graph.add_node(PromptTestInvocation(id="false_value", prompt="false branch"))
graph.add_node(IfInvocation(id="if"))
graph.add_node(PromptTestInvocation(id="selected_output"))
graph.add_edge(create_edge("condition", "value", "if", "condition"))
graph.add_edge(create_edge("true_value", "prompt", "if", "true_input"))
graph.add_edge(create_edge("false_value", "prompt", "if", "false_input"))
graph.add_edge(create_edge("if", "value", "selected_output", "prompt"))
g = GraphExecutionState(graph=graph)
execute_all_nodes(g)
assert set(g.executed_history) == {"condition", "true_value", "false_value", "if", "selected_output"}
assert g.executed_history.count("false_value") == 1
def test_if_graph_optimized_behavior_skips_unselected_branch_but_keeps_shared_ancestors():
graph = Graph()
graph.add_node(BooleanInvocation(id="condition", value=True))
graph.add_node(PromptTestInvocation(id="shared", prompt="shared value"))
graph.add_node(PromptTestInvocation(id="true_mid"))
graph.add_node(PromptTestInvocation(id="true_leaf"))
graph.add_node(PromptTestInvocation(id="false_mid"))
graph.add_node(PromptTestInvocation(id="false_leaf"))
graph.add_node(PromptTestInvocation(id="side_consumer"))
graph.add_node(IfInvocation(id="if"))
graph.add_node(PromptTestInvocation(id="selected_output"))
graph.add_edge(create_edge("condition", "value", "if", "condition"))
graph.add_edge(create_edge("shared", "prompt", "true_mid", "prompt"))
graph.add_edge(create_edge("true_mid", "prompt", "true_leaf", "prompt"))
graph.add_edge(create_edge("true_leaf", "prompt", "if", "true_input"))
graph.add_edge(create_edge("shared", "prompt", "false_mid", "prompt"))
graph.add_edge(create_edge("false_mid", "prompt", "false_leaf", "prompt"))
graph.add_edge(create_edge("false_leaf", "prompt", "if", "false_input"))
graph.add_edge(create_edge("shared", "prompt", "side_consumer", "prompt"))
graph.add_edge(create_edge("if", "value", "selected_output", "prompt"))
g = GraphExecutionState(graph=graph)
executed_source_ids = execute_all_nodes(g)
assert set(executed_source_ids) == {
"condition",
"shared",
"true_mid",
"true_leaf",
"side_consumer",
"if",
"selected_output",
}
assert "false_mid" not in executed_source_ids
assert "false_leaf" not in executed_source_ids
def test_if_graph_optimized_behavior_skips_distant_unselected_ancestors_only_when_exclusive():
graph = Graph()
graph.add_node(BooleanInvocation(id="condition", value=False))
graph.add_node(PromptTestInvocation(id="shared_root", prompt="shared value"))
graph.add_node(PromptTestInvocation(id="true_shared_mid"))
graph.add_node(PromptTestInvocation(id="true_exclusive_leaf"))
graph.add_node(PromptTestInvocation(id="false_mid"))
graph.add_node(PromptTestInvocation(id="false_leaf"))
graph.add_node(PromptTestInvocation(id="shared_observer"))
graph.add_node(IfInvocation(id="if"))
graph.add_node(PromptTestInvocation(id="selected_output"))
graph.add_edge(create_edge("condition", "value", "if", "condition"))
graph.add_edge(create_edge("shared_root", "prompt", "true_shared_mid", "prompt"))
graph.add_edge(create_edge("true_shared_mid", "prompt", "true_exclusive_leaf", "prompt"))
graph.add_edge(create_edge("true_exclusive_leaf", "prompt", "if", "true_input"))
graph.add_edge(create_edge("shared_root", "prompt", "false_mid", "prompt"))
graph.add_edge(create_edge("false_mid", "prompt", "false_leaf", "prompt"))
graph.add_edge(create_edge("false_leaf", "prompt", "if", "false_input"))
graph.add_edge(create_edge("true_shared_mid", "prompt", "shared_observer", "prompt"))
graph.add_edge(create_edge("if", "value", "selected_output", "prompt"))
g = GraphExecutionState(graph=graph)
executed_source_ids = execute_all_nodes(g)
assert set(executed_source_ids) == {
"condition",
"shared_root",
"true_shared_mid",
"false_mid",
"false_leaf",
"shared_observer",
"if",
"selected_output",
}
assert "true_exclusive_leaf" not in executed_source_ids
def test_if_graph_optimized_behavior_allows_selected_missing_branch_input():
graph = Graph()
graph.add_node(BooleanInvocation(id="condition", value=False))
graph.add_node(PromptTestInvocation(id="true_value", prompt="true branch"))
graph.add_node(IfInvocation(id="if"))
graph.add_node(AnyTypeTestInvocation(id="selected_output"))
graph.add_edge(create_edge("condition", "value", "if", "condition"))
graph.add_edge(create_edge("true_value", "prompt", "if", "true_input"))
graph.add_edge(create_edge("if", "value", "selected_output", "value"))
g = GraphExecutionState(graph=graph)
executed_source_ids = execute_all_nodes(g)
prepared_selected_output_id = next(iter(g.source_prepared_mapping["selected_output"]))
assert g.results[prepared_selected_output_id].value is None
assert set(executed_source_ids) == {"condition", "if", "selected_output"}
assert "true_value" not in executed_source_ids
def test_if_graph_optimized_behavior_does_not_cross_defer_independent_ifs():
graph = Graph()
graph.add_node(BooleanInvocation(id="condition_a", value=True))
graph.add_node(BooleanInvocation(id="condition_b", value=False))
graph.add_node(PromptTestInvocation(id="true_a", prompt="true a"))
graph.add_node(PromptTestInvocation(id="false_a", prompt="false a"))
graph.add_node(PromptTestInvocation(id="true_b", prompt="true b"))
graph.add_node(PromptTestInvocation(id="false_b", prompt="false b"))
graph.add_node(IfInvocation(id="if_a"))
graph.add_node(IfInvocation(id="if_b"))
graph.add_node(CollectInvocation(id="collect"))
graph.add_edge(create_edge("condition_a", "value", "if_a", "condition"))
graph.add_edge(create_edge("true_a", "prompt", "if_a", "true_input"))
graph.add_edge(create_edge("false_a", "prompt", "if_a", "false_input"))
graph.add_edge(create_edge("condition_b", "value", "if_b", "condition"))
graph.add_edge(create_edge("true_b", "prompt", "if_b", "true_input"))
graph.add_edge(create_edge("false_b", "prompt", "if_b", "false_input"))
graph.add_edge(create_edge("if_a", "value", "collect", "item"))
graph.add_edge(create_edge("if_b", "value", "collect", "item"))
g = GraphExecutionState(graph=graph)
executed_source_ids = execute_all_nodes(g)
prepared_collect_id = next(iter(g.source_prepared_mapping["collect"]))
assert sorted(g.results[prepared_collect_id].collection) == ["false b", "true a"]
assert set(executed_source_ids) == {
"condition_a",
"condition_b",
"true_a",
"false_b",
"if_a",
"if_b",
"collect",
}
assert "false_a" not in executed_source_ids
assert "true_b" not in executed_source_ids
def test_if_graph_optimized_behavior_supports_nested_ifs():
graph = Graph()
graph.add_node(BooleanInvocation(id="outer_condition", value=True))
graph.add_node(BooleanInvocation(id="inner_condition", value=False))
graph.add_node(PromptTestInvocation(id="outer_false", prompt="outer false"))
graph.add_node(PromptTestInvocation(id="inner_true", prompt="inner true"))
graph.add_node(PromptTestInvocation(id="inner_false", prompt="inner false"))
graph.add_node(IfInvocation(id="inner_if"))
graph.add_node(IfInvocation(id="outer_if"))
graph.add_node(PromptTestInvocation(id="selected_output"))
graph.add_edge(create_edge("inner_condition", "value", "inner_if", "condition"))
graph.add_edge(create_edge("inner_true", "prompt", "inner_if", "true_input"))
graph.add_edge(create_edge("inner_false", "prompt", "inner_if", "false_input"))
graph.add_edge(create_edge("outer_condition", "value", "outer_if", "condition"))
graph.add_edge(create_edge("inner_if", "value", "outer_if", "true_input"))
graph.add_edge(create_edge("outer_false", "prompt", "outer_if", "false_input"))
graph.add_edge(create_edge("outer_if", "value", "selected_output", "prompt"))
g = GraphExecutionState(graph=graph)
executed_source_ids = execute_all_nodes(g)
prepared_selected_output_id = next(iter(g.source_prepared_mapping["selected_output"]))
assert g.results[prepared_selected_output_id].prompt == "inner false"
assert set(executed_source_ids) == {
"outer_condition",
"inner_condition",
"inner_false",
"inner_if",
"outer_if",
"selected_output",
}
assert "inner_true" not in executed_source_ids
assert "outer_false" not in executed_source_ids
def test_if_graph_optimized_behavior_prunes_branches_per_iteration():
graph = Graph()
graph.add_node(BooleanCollectionInvocation(id="conditions", collection=[True, False, True]))
graph.add_node(IterateInvocation(id="condition_iter"))
graph.add_node(AnyTypeTestInvocation(id="true_branch"))
graph.add_node(AnyTypeTestInvocation(id="false_branch"))
graph.add_node(IfInvocation(id="if"))
graph.add_node(CollectInvocation(id="collect"))
graph.add_edge(create_edge("conditions", "collection", "condition_iter", "collection"))
graph.add_edge(create_edge("condition_iter", "item", "if", "condition"))
graph.add_edge(create_edge("condition_iter", "item", "true_branch", "value"))
graph.add_edge(create_edge("true_branch", "value", "if", "true_input"))
graph.add_edge(create_edge("condition_iter", "item", "false_branch", "value"))
graph.add_edge(create_edge("false_branch", "value", "if", "false_input"))
graph.add_edge(create_edge("if", "value", "collect", "item"))
g = GraphExecutionState(graph=graph)
executed_source_ids = execute_all_nodes(g)
prepared_collect_id = next(iter(g.source_prepared_mapping["collect"]))
assert g.results[prepared_collect_id].collection == [True, False, True]
assert executed_source_ids.count("condition_iter") == 3
assert executed_source_ids.count("true_branch") == 2
assert executed_source_ids.count("false_branch") == 1
assert executed_source_ids.count("if") == 3
def test_if_graph_optimized_behavior_keeps_shared_live_consumers_per_iteration():
graph = Graph()
graph.add_node(BooleanCollectionInvocation(id="conditions", collection=[True, False, False]))
graph.add_node(IterateInvocation(id="condition_iter"))
graph.add_node(AnyTypeTestInvocation(id="shared_branch"))
graph.add_node(AnyTypeTestInvocation(id="true_leaf"))
graph.add_node(AnyTypeTestInvocation(id="false_branch"))
graph.add_node(AnyTypeTestInvocation(id="observer"))
graph.add_node(IfInvocation(id="if"))
graph.add_node(CollectInvocation(id="selected_collect"))
graph.add_node(CollectInvocation(id="observer_collect"))
graph.add_edge(create_edge("conditions", "collection", "condition_iter", "collection"))
graph.add_edge(create_edge("condition_iter", "item", "if", "condition"))
graph.add_edge(create_edge("condition_iter", "item", "shared_branch", "value"))
graph.add_edge(create_edge("shared_branch", "value", "true_leaf", "value"))
graph.add_edge(create_edge("true_leaf", "value", "if", "true_input"))
graph.add_edge(create_edge("condition_iter", "item", "false_branch", "value"))
graph.add_edge(create_edge("false_branch", "value", "if", "false_input"))
graph.add_edge(create_edge("shared_branch", "value", "observer", "value"))
graph.add_edge(create_edge("if", "value", "selected_collect", "item"))
graph.add_edge(create_edge("observer", "value", "observer_collect", "item"))
g = GraphExecutionState(graph=graph)
executed_source_ids = execute_all_nodes(g)
prepared_selected_collect_id = next(iter(g.source_prepared_mapping["selected_collect"]))
assert g.results[prepared_selected_collect_id].collection == [True, False, False]
prepared_observer_collect_id = next(iter(g.source_prepared_mapping["observer_collect"]))
assert g.results[prepared_observer_collect_id].collection == [True, False, False]
assert executed_source_ids.count("condition_iter") == 3
assert executed_source_ids.count("shared_branch") == 3
assert executed_source_ids.count("observer") == 3
assert executed_source_ids.count("true_leaf") == 1
assert executed_source_ids.count("false_branch") == 2
def test_if_graph_optimized_behavior_handles_selected_true_branch_with_shared_false_input_ancestor():
graph = Graph()
graph.add_node(BooleanInvocation(id="condition", value=True))
graph.add_node(AnyTypeTestInvocation(id="shared_item", value="shared"))
graph.add_node(AnyTypeTestInvocation(id="true_item", value="true"))
graph.add_node(CollectInvocation(id="shared_collect"))
graph.add_node(CollectInvocation(id="true_collect"))
graph.add_node(IfInvocation(id="if"))
graph.add_node(AnyTypeTestInvocation(id="selected_output"))
graph.add_edge(create_edge("condition", "value", "if", "condition"))
graph.add_edge(create_edge("shared_item", "value", "shared_collect", "item"))
graph.add_edge(create_edge("shared_collect", "collection", "true_collect", "collection"))
graph.add_edge(create_edge("true_item", "value", "true_collect", "item"))
graph.add_edge(create_edge("shared_collect", "collection", "if", "false_input"))
graph.add_edge(create_edge("true_collect", "collection", "if", "true_input"))
graph.add_edge(create_edge("if", "value", "selected_output", "value"))
g = GraphExecutionState(graph=graph)
executed_source_ids = execute_all_nodes(g)
prepared_selected_output_id = next(iter(g.source_prepared_mapping["selected_output"]))
assert g.results[prepared_selected_output_id].value == ["shared", "true"]
assert set(executed_source_ids) == {
"condition",
"shared_item",
"true_item",
"shared_collect",
"true_collect",
"if",
"selected_output",
}
def test_if_graph_optimized_behavior_handles_selected_false_branch_with_shared_true_input_ancestor():
graph = Graph()
graph.add_node(BooleanInvocation(id="condition", value=False))
graph.add_node(AnyTypeTestInvocation(id="shared_item", value="shared"))
graph.add_node(AnyTypeTestInvocation(id="true_item", value="true"))
graph.add_node(CollectInvocation(id="shared_collect"))
graph.add_node(CollectInvocation(id="true_collect"))
graph.add_node(IfInvocation(id="if"))
graph.add_node(AnyTypeTestInvocation(id="selected_output"))
graph.add_edge(create_edge("condition", "value", "if", "condition"))
graph.add_edge(create_edge("shared_item", "value", "shared_collect", "item"))
graph.add_edge(create_edge("shared_collect", "collection", "true_collect", "collection"))
graph.add_edge(create_edge("true_item", "value", "true_collect", "item"))
graph.add_edge(create_edge("shared_collect", "collection", "if", "false_input"))
graph.add_edge(create_edge("true_collect", "collection", "if", "true_input"))
graph.add_edge(create_edge("if", "value", "selected_output", "value"))
g = GraphExecutionState(graph=graph)
executed_source_ids = execute_all_nodes(g)
prepared_selected_output_id = next(iter(g.source_prepared_mapping["selected_output"]))
assert g.results[prepared_selected_output_id].value == ["shared"]
assert set(executed_source_ids) == {
"condition",
"shared_item",
"shared_collect",
"if",
"selected_output",
}
assert "true_item" not in executed_source_ids
assert "true_collect" not in executed_source_ids
def test_prepare_if_inputs_raises_when_selected_branch_source_has_no_result():
graph = Graph()
graph.add_node(BooleanInvocation(id="condition", value=True))
graph.add_node(PromptTestInvocation(id="true_value", prompt="true branch"))
graph.add_node(IfInvocation(id="if"))
graph.add_edge(create_edge("condition", "value", "if", "condition"))
graph.add_edge(create_edge("true_value", "prompt", "if", "true_input"))
g = GraphExecutionState(graph=graph)
condition_exec_id = g._create_execution_node("condition", [])[0]
true_value_exec_id = g._create_execution_node("true_value", [])[0]
if_exec_id = g._create_execution_node(
"if",
[("condition", condition_exec_id), ("true_value", true_value_exec_id)],
)[0]
g.executed.add(condition_exec_id)
g.results[condition_exec_id] = BooleanOutput(value=True)
g.executed.add(true_value_exec_id)
g._resolved_if_exec_branches[if_exec_id] = "true_input"
if_node = g.execution_graph.get_node(if_exec_id)
with pytest.raises(RuntimeError) as exc_info:
g._prepare_inputs(if_node)
message = str(exc_info.value)
assert if_exec_id in message
assert true_value_exec_id in message
assert "iteration_path=()" in message
def test_get_collect_iteration_mapping_groups_ignores_skipped_prepared_exec_nodes():
graph = Graph()
graph.add_node(AnyTypeTestInvocation(id="parent", value="value"))
graph.add_node(CollectInvocation(id="collect"))
graph.add_edge(create_edge("parent", "value", "collect", "item"))
g = GraphExecutionState(graph=graph)
skipped_exec_id = g._create_execution_node("parent", [])[0]
active_exec_id = g._create_execution_node("parent", [])[0]
g._set_prepared_exec_state(skipped_exec_id, "skipped")
mappings = g._materializer()._get_collect_iteration_mapping_groups(graph._get_input_edges("collect"))
assert mappings == [((), [("parent", active_exec_id)])]
def test_get_iteration_node_ignores_skipped_prepared_exec_nodes():
graph = Graph()
graph.add_node(PromptTestInvocation(id="value", prompt="branch value"))
g = GraphExecutionState(graph=graph)
skipped_exec_id = g._create_execution_node("value", [])[0]
active_exec_id = g._create_execution_node("value", [])[0]
g._set_prepared_exec_state(skipped_exec_id, "skipped")
selected_exec_id = g._get_iteration_node("value", graph.nx_graph_flat(), g.execution_graph.nx_graph_flat(), [])
assert selected_exec_id == active_exec_id
def test_get_iteration_node_returns_single_active_prepared_exec_node():
graph = Graph()
graph.add_node(PromptTestInvocation(id="value", prompt="branch value"))
g = GraphExecutionState(graph=graph)
active_exec_id = g._create_execution_node("value", [])[0]
selected_exec_id = g._get_iteration_node("value", graph.nx_graph_flat(), g.execution_graph.nx_graph_flat(), [])
assert selected_exec_id == active_exec_id
def test_get_iteration_node_returns_none_when_only_skipped_prepared_exec_nodes_exist():
graph = Graph()
graph.add_node(PromptTestInvocation(id="value", prompt="branch value"))
g = GraphExecutionState(graph=graph)
skipped_exec_id = g._create_execution_node("value", [])[0]
g._set_prepared_exec_state(skipped_exec_id, "skipped")
selected_exec_id = g._get_iteration_node("value", graph.nx_graph_flat(), g.execution_graph.nx_graph_flat(), [])
assert selected_exec_id is None
def test_get_iteration_node_does_not_reuse_wrong_iterator_when_only_other_iteration_is_live():
graph = Graph()
graph.add_node(BooleanCollectionInvocation(id="conditions", collection=[True, False]))
graph.add_node(IterateInvocation(id="condition_iter"))
graph.add_node(AnyTypeTestInvocation(id="value"))
graph.add_edge(create_edge("conditions", "collection", "condition_iter", "collection"))
graph.add_edge(create_edge("condition_iter", "item", "value", "value"))
g = GraphExecutionState(graph=graph)
conditions_exec_id = g._create_execution_node("conditions", [])[0]
g.executed.add(conditions_exec_id)
g.results[conditions_exec_id] = BooleanCollectionOutput(collection=[True, False])
iterator_exec_ids = g._create_execution_node("condition_iter", [("conditions", conditions_exec_id)])
assert len(iterator_exec_ids) == 2
iterator_exec_ids_by_index = {g.execution_graph.get_node(exec_id).index: exec_id for exec_id in iterator_exec_ids}
first_iter_exec_id = iterator_exec_ids_by_index[0]
second_iter_exec_id = iterator_exec_ids_by_index[1]
value_exec_ids = []
value_exec_ids.extend(g._create_execution_node("value", [("condition_iter", first_iter_exec_id)]))
value_exec_ids.extend(g._create_execution_node("value", [("condition_iter", second_iter_exec_id)]))
assert len(value_exec_ids) == 2
for exec_id in value_exec_ids:
if g._get_iteration_path(exec_id) == (1,):
active_value_exec_id = exec_id
else:
skipped_value_exec_id = exec_id
g._set_prepared_exec_state(skipped_value_exec_id, "skipped")
selected_exec_id = g._get_iteration_node(
"value", graph.nx_graph_flat(), g.execution_graph.nx_graph_flat(), [first_iter_exec_id]
)
assert selected_exec_id is None
assert active_value_exec_id != skipped_value_exec_id
def test_mark_exec_node_skipped_does_not_hide_already_executed_results():
graph = Graph()
graph.add_node(AnyTypeTestInvocation(id="value", value="value"))
g = GraphExecutionState(graph=graph)
exec_id = g._create_execution_node("value", [])[0]
g.results[exec_id] = AnyTypeTestInvocationOutput(value="value")
g.executed.add(exec_id)
g._set_prepared_exec_state(exec_id, "executed")
g._if_scheduler().mark_exec_node_skipped(exec_id)
assert g._get_prepared_exec_metadata(exec_id).state == "executed"
assert g.results[exec_id].value == "value"
def test_mark_exec_node_skipped_is_idempotent_for_skipped_state():
graph = Graph()
graph.add_node(AnyTypeTestInvocation(id="value", value="value"))
g = GraphExecutionState(graph=graph)
exec_id = g._create_execution_node("value", [])[0]
g._if_scheduler().mark_exec_node_skipped(exec_id)
g._if_scheduler().mark_exec_node_skipped(exec_id)
assert g._get_prepared_exec_metadata(exec_id).state == "skipped"
assert g.executed_history.count("value") == 1
def test_are_connection_types_compatible_accepts_subclass_to_base():
"""A subclass output should be connectable to a base-class input.
This test exposes the bug where non-Union targets reject valid subclass connections.
"""
from invokeai.app.services.shared.graph import are_connection_types_compatible
class Base:
pass
class Child(Base):
pass
assert are_connection_types_compatible(Child, Base) is True