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

422 lines
14 KiB
Python

from types import SimpleNamespace
from unittest.mock import Mock
import pytest
from invokeai.app.services.users.users_common import UserDTO
from invokeai.app.services.workflow_records.workflow_records_common import (
Workflow,
WorkflowCategory,
WorkflowMeta,
WorkflowNotFoundError,
WorkflowRecordDTO,
WorkflowWithoutIDValidator,
)
def build_workflow_record_dto(
*,
workflow_id: str = "workflow-123",
user_id: str = "owner-1",
category: WorkflowCategory = WorkflowCategory.User,
is_public: bool = False,
) -> WorkflowRecordDTO:
workflow = Workflow(
id=workflow_id,
name="Saved Workflow",
author="Tester",
description="",
version="1.0.0",
contact="",
tags="",
notes="",
exposedFields=[],
meta=WorkflowMeta(version="1.0.0", category=category),
nodes=[],
edges=[],
form=None,
)
return WorkflowRecordDTO(
workflow_id=workflow_id,
workflow=workflow,
name=workflow.name,
created_at="2026-04-08T00:00:00Z",
updated_at="2026-04-08T00:00:00Z",
opened_at=None,
user_id=user_id,
is_public=is_public,
)
def build_user_dto(*, user_id: str = "user-1", is_admin: bool = False) -> UserDTO:
return UserDTO(
user_id=user_id,
email=f"{user_id}@example.test",
display_name=user_id,
is_admin=is_admin,
is_active=True,
created_at="2026-04-08T00:00:00Z",
updated_at="2026-04-08T00:00:00Z",
last_login_at=None,
)
def build_context(
*,
workflow_record: WorkflowRecordDTO | None = None,
queue_user_id: str = "owner-1",
multiuser: bool = False,
user_is_admin: bool = False,
user_exists: bool = True,
workflow_not_found: bool = False,
):
services = SimpleNamespace(
configuration=SimpleNamespace(multiuser=multiuser),
users=Mock(),
workflow_records=Mock(),
)
services.users.get.return_value = (
build_user_dto(user_id=queue_user_id, is_admin=user_is_admin) if user_exists else None
)
if workflow_not_found:
services.workflow_records.get.side_effect = WorkflowNotFoundError("missing")
else:
services.workflow_records.get.return_value = workflow_record or build_workflow_record_dto()
context = Mock()
context._services = services
context._data = SimpleNamespace(queue_item=SimpleNamespace(user_id=queue_user_id))
return context
def test_call_saved_workflow_invocation_contract():
from invokeai.app.invocations.call_saved_workflow import CallSavedWorkflowInvocation
from invokeai.app.invocations.workflow_return import WorkflowReturnOutput
invocation = CallSavedWorkflowInvocation(id="test-node", workflow_id="workflow-123")
assert invocation.get_type() == "call_saved_workflow"
assert invocation.workflow_id == "workflow-123"
output = invocation.invoke(build_context())
assert isinstance(output, WorkflowReturnOutput)
assert output.values == {}
def test_call_saved_workflow_invocation_raises_when_workflow_id_is_empty():
from invokeai.app.invocations.call_saved_workflow import CallSavedWorkflowInvocation
invocation = CallSavedWorkflowInvocation(id="test-node")
with pytest.raises(ValueError, match="saved workflow must be selected"):
invocation.invoke(build_context())
def test_call_saved_workflow_invocation_raises_when_workflow_does_not_exist():
from invokeai.app.invocations.call_saved_workflow import CallSavedWorkflowInvocation
invocation = CallSavedWorkflowInvocation(id="test-node", workflow_id="missing-workflow")
with pytest.raises(ValueError, match="could not be found"):
invocation.invoke(build_context(workflow_not_found=True))
def test_call_saved_workflow_invocation_raises_when_workflow_is_not_accessible():
from invokeai.app.invocations.call_saved_workflow import CallSavedWorkflowInvocation
invocation = CallSavedWorkflowInvocation(id="test-node", workflow_id="private-workflow")
with pytest.raises(ValueError, match="is not accessible"):
invocation.invoke(
build_context(
workflow_record=build_workflow_record_dto(
workflow_id="private-workflow",
user_id="owner-1",
category=WorkflowCategory.User,
is_public=False,
),
queue_user_id="other-user",
multiuser=True,
user_is_admin=False,
)
)
def test_call_saved_workflow_invocation_allows_shared_workflow_for_non_owner():
from invokeai.app.invocations.call_saved_workflow import CallSavedWorkflowInvocation
invocation = CallSavedWorkflowInvocation(id="test-node", workflow_id="shared-workflow")
output = invocation.invoke(
build_context(
workflow_record=build_workflow_record_dto(
workflow_id="shared-workflow",
user_id="owner-1",
category=WorkflowCategory.User,
is_public=True,
),
queue_user_id="other-user",
multiuser=True,
user_is_admin=False,
)
)
assert output.values == {}
def test_call_saved_workflow_invocation_allows_default_workflow_for_non_owner():
from invokeai.app.invocations.call_saved_workflow import CallSavedWorkflowInvocation
invocation = CallSavedWorkflowInvocation(id="test-node", workflow_id="default-workflow")
output = invocation.invoke(
build_context(
workflow_record=build_workflow_record_dto(
workflow_id="default-workflow",
user_id="system",
category=WorkflowCategory.Default,
is_public=False,
),
queue_user_id="other-user",
multiuser=True,
user_is_admin=False,
)
)
assert output.values == {}
def test_call_saved_workflow_invocation_allows_admin_to_access_private_workflow():
from invokeai.app.invocations.call_saved_workflow import CallSavedWorkflowInvocation
invocation = CallSavedWorkflowInvocation(id="test-node", workflow_id="private-workflow")
output = invocation.invoke(
build_context(
workflow_record=build_workflow_record_dto(
workflow_id="private-workflow",
user_id="owner-1",
category=WorkflowCategory.User,
is_public=False,
),
queue_user_id="admin-user",
multiuser=True,
user_is_admin=True,
)
)
assert output.values == {}
def test_call_saved_workflow_invocation_raises_when_private_workflow_user_record_is_missing():
from invokeai.app.invocations.call_saved_workflow import CallSavedWorkflowInvocation
invocation = CallSavedWorkflowInvocation(id="test-node", workflow_id="private-workflow")
with pytest.raises(ValueError, match="is not accessible"):
invocation.invoke(
build_context(
workflow_record=build_workflow_record_dto(
workflow_id="private-workflow",
user_id="owner-1",
category=WorkflowCategory.User,
is_public=False,
),
queue_user_id="other-user",
multiuser=True,
user_exists=False,
)
)
def test_call_saved_workflow_invocation_schema_declares_saved_workflow_ui_type():
from invokeai.app.invocations.call_saved_workflow import CallSavedWorkflowInvocation
schema = CallSavedWorkflowInvocation.model_json_schema()
workflow_id = schema["properties"]["workflow_id"]
workflow_inputs = schema["properties"]["workflow_inputs"]
assert workflow_id["default"] == ""
assert workflow_id["input"] == "any"
assert workflow_id["ui_type"] == "SavedWorkflowField"
assert workflow_inputs["default"] == {}
assert workflow_inputs["ui_hidden"] is True
def test_workflow_return_invocation_contract():
from invokeai.app.invocations.workflow_return import (
WorkflowReturnInvocation,
WorkflowReturnOutput,
WorkflowReturnValueField,
)
invocation = WorkflowReturnInvocation(
id="return-node",
values=[
WorkflowReturnValueField(key="prompt", value="a"),
WorkflowReturnValueField(key="count", value=1),
WorkflowReturnValueField(key="metadata", value={"x": True}),
],
)
assert invocation.get_type() == "workflow_return"
output = invocation.invoke(build_context())
assert isinstance(output, WorkflowReturnOutput)
assert output.values == {"prompt": "a", "count": 1, "metadata": {"x": True}}
assert not hasattr(output, "collection")
def test_workflow_return_invocation_accepts_single_return_value():
from invokeai.app.invocations.workflow_return import WorkflowReturnInvocation, WorkflowReturnValueField
invocation = WorkflowReturnInvocation(id="return-node", values=WorkflowReturnValueField(key="sum", value=3))
output = invocation.invoke(build_context())
assert output.values == {"sum": 3}
def test_workflow_return_values_schema_preserves_single_or_list_cardinality():
from invokeai.app.invocations.workflow_return import WorkflowReturnInvocation
values_schema = WorkflowReturnInvocation.model_json_schema()["properties"]["values"]
assert values_schema["anyOf"] == [
{"$ref": "#/$defs/WorkflowReturnValueField"},
{"items": {"$ref": "#/$defs/WorkflowReturnValueField"}, "type": "array"},
]
assert values_schema.get("ui_type") != "CollectionField"
def test_workflow_return_value_invocation_contract():
from invokeai.app.invocations.workflow_return import WorkflowReturnValueField, WorkflowReturnValueInvocation
invocation = WorkflowReturnValueInvocation(id="return-value-node", key="image", value={"image_name": "image-a"})
output = invocation.invoke(build_context())
assert output.value == WorkflowReturnValueField(key="image", value={"image_name": "image-a"})
def test_workflow_return_value_field_survives_exclude_none_session_roundtrip():
from invokeai.app.invocations.workflow_return import (
WorkflowReturnInvocation,
WorkflowReturnValueField,
WorkflowReturnValueOutput,
)
from invokeai.app.services.shared.graph import Graph, GraphExecutionState
graph = Graph()
graph.add_node(
WorkflowReturnInvocation(id="return-node", values=WorkflowReturnValueField(key="nullable", value=None))
)
session = GraphExecutionState(graph=graph)
session.execution_graph.add_node(
WorkflowReturnInvocation(id="return-node", values=WorkflowReturnValueField(key="nullable", value=None))
)
session.results["return-value-node"] = WorkflowReturnValueOutput(
value=WorkflowReturnValueField(key="nullable", value=None)
)
reloaded = GraphExecutionState.model_validate_json(session.model_dump_json(warnings=False, exclude_none=True))
assert reloaded.execution_graph.nodes["return-node"].values == WorkflowReturnValueField(key="nullable", value=None)
assert reloaded.results["return-value-node"].value == WorkflowReturnValueField(key="nullable", value=None)
def test_workflow_return_invocation_rejects_duplicate_keys():
from invokeai.app.invocations.workflow_return import WorkflowReturnInvocation, WorkflowReturnValueField
invocation = WorkflowReturnInvocation(
id="return-node",
values=[
WorkflowReturnValueField(key="image", value="image-a"),
WorkflowReturnValueField(key="image", value="image-b"),
],
)
with pytest.raises(ValueError, match="Duplicate workflow return key 'image'"):
invocation.invoke(build_context())
def test_workflow_return_get_invocation_contract():
from invokeai.app.invocations.workflow_return import WorkflowReturnGetInvocation
invocation = WorkflowReturnGetInvocation(id="return-get-node", values={"image": "image-a"}, key="image")
output = invocation.invoke(build_context())
assert output.value == "image-a"
def test_workflow_return_get_invocation_rejects_missing_key():
from invokeai.app.invocations.workflow_return import WorkflowReturnGetInvocation
invocation = WorkflowReturnGetInvocation(id="return-get-node", values={"image": "image-a"}, key="mask")
with pytest.raises(ValueError, match="Workflow return key 'mask' was not found"):
invocation.invoke(build_context())
def test_workflow_return_get_invocation_rejects_empty_key():
from invokeai.app.invocations.workflow_return import WorkflowReturnGetInvocation
invocation = WorkflowReturnGetInvocation(id="return-get-node", values={"image": "image-a"}, key=" ")
with pytest.raises(ValueError, match="Workflow return key must not be empty"):
invocation.invoke(build_context())
def test_workflow_return_invocation_schema_declares_named_values_contract():
from invokeai.app.invocations.workflow_return import WorkflowReturnGetInvocation, WorkflowReturnInvocation
schema = WorkflowReturnInvocation.model_json_schema()
assert "collection" not in schema["properties"]
values = schema["properties"]["values"]
assert values["input"] == "connection"
assert "ui_type" not in values
get_schema = WorkflowReturnGetInvocation.model_json_schema()
get_values = get_schema["properties"]["values"]
assert get_values["input"] == "connection"
assert get_values["ui_type"] == "AnyField"
def test_workflow_without_id_validator_rejects_duplicate_workflow_return_nodes():
with pytest.raises(ValueError, match="workflow_return"):
WorkflowWithoutIDValidator.validate_python(
{
"name": "Workflow With Duplicate Returns",
"author": "Tester",
"description": "",
"version": "1.0.0",
"contact": "",
"tags": "",
"notes": "",
"exposedFields": [],
"meta": {"version": "1.0.0", "category": "user"},
"nodes": [
{
"id": "return-1",
"type": "invocation",
"data": {"id": "return-1", "type": "workflow_return"},
"position": {"x": 0, "y": 0},
},
{
"id": "return-2",
"type": "invocation",
"data": {"id": "return-2", "type": "workflow_return"},
"position": {"x": 100, "y": 0},
},
],
"edges": [],
"form": None,
}
)