Files
invoke-ai--invokeai/tests/app/routers/test_session_queue_workflow_call.py
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

431 lines
16 KiB
Python

"""Tests for session queue API behavior with workflow-call queue items."""
import logging
import uuid
from typing import Any
from unittest.mock import MagicMock
import pytest
from fastapi.testclient import TestClient
from invokeai.app.api.dependencies import ApiDependencies
from invokeai.app.api_app import app
from invokeai.app.services.config.config_default import InvokeAIAppConfig
from invokeai.app.services.invocation_services import InvocationServices
from invokeai.app.services.invoker import Invoker
from invokeai.app.services.session_processor.session_processor_common import SessionProcessorStatus
from invokeai.app.services.session_queue.session_queue_sqlite import SqliteSessionQueue
from invokeai.app.services.shared.graph import Graph, GraphExecutionState
from invokeai.app.services.users.users_common import UserCreateRequest
from invokeai.app.services.workflow_records.workflow_records_sqlite import SqliteWorkflowRecordsStorage
from invokeai.backend.util.logging import InvokeAILogger
from tests.fixtures.sqlite_database import create_mock_sqlite_database
class MockApiDependencies(ApiDependencies):
invoker: Invoker
def __init__(self, invoker: Invoker) -> None:
self.invoker = invoker
@pytest.fixture
def setup_jwt_secret():
from invokeai.app.services.auth.token_service import set_jwt_secret
set_jwt_secret("test-secret-key-for-unit-tests-only-do-not-use-in-production")
@pytest.fixture
def client():
return TestClient(app)
@pytest.fixture
def mock_services() -> InvocationServices:
from invokeai.app.services.board_image_records.board_image_records_sqlite import SqliteBoardImageRecordStorage
from invokeai.app.services.board_records.board_records_sqlite import SqliteBoardRecordStorage
from invokeai.app.services.boards.boards_default import BoardService
from invokeai.app.services.bulk_download.bulk_download_default import BulkDownloadService
from invokeai.app.services.client_state_persistence.client_state_persistence_sqlite import (
ClientStatePersistenceSqlite,
)
from invokeai.app.services.image_records.image_records_sqlite import SqliteImageRecordStorage
from invokeai.app.services.images.images_default import ImageService
from invokeai.app.services.invocation_cache.invocation_cache_memory import MemoryInvocationCache
from invokeai.app.services.invocation_stats.invocation_stats_default import InvocationStatsService
from invokeai.app.services.users.users_default import UserService
from tests.test_nodes import TestEventService
configuration = InvokeAIAppConfig(use_memory_db=True, node_cache_size=0)
logger = InvokeAILogger.get_logger()
db = create_mock_sqlite_database(configuration, logger)
return InvocationServices(
board_image_records=SqliteBoardImageRecordStorage(db=db),
board_images=None, # type: ignore
board_records=SqliteBoardRecordStorage(db=db),
boards=BoardService(),
bulk_download=BulkDownloadService(),
configuration=configuration,
events=TestEventService(),
image_files=None, # type: ignore
image_records=SqliteImageRecordStorage(db=db),
images=ImageService(),
invocation_cache=MemoryInvocationCache(max_cache_size=0),
logger=logging, # type: ignore
model_images=None, # type: ignore
model_manager=None, # type: ignore
download_queue=None, # type: ignore
names=None, # type: ignore
performance_statistics=InvocationStatsService(),
session_processor=None, # type: ignore
session_queue=None, # type: ignore
urls=None, # type: ignore
workflow_records=SqliteWorkflowRecordsStorage(db=db),
tensors=None, # type: ignore
conditioning=None, # type: ignore
style_preset_records=None, # type: ignore
style_preset_image_files=None, # type: ignore
workflow_thumbnails=None, # type: ignore
model_relationship_records=None, # type: ignore
model_relationships=None, # type: ignore
client_state_persistence=ClientStatePersistenceSqlite(db=db),
users=UserService(db),
external_generation=None, # type: ignore
)
@pytest.fixture
def mock_invoker(mock_services: InvocationServices) -> Invoker:
invoker = Invoker(services=mock_services)
queue = SqliteSessionQueue(db=mock_services.board_records._db)
mock_services.session_queue = queue
mock_services.session_processor = MagicMock()
mock_services.session_processor.get_status.return_value = SessionProcessorStatus(
is_started=True, is_processing=False
)
queue.start(invoker)
return invoker
def _create_user(mock_invoker: Invoker, email: str, display_name: str, is_admin: bool = False) -> str:
user = mock_invoker.services.users.create(
UserCreateRequest(email=email, display_name=display_name, password="TestPass123", is_admin=is_admin)
)
return user.user_id
def _login(client: TestClient, email: str) -> str:
response = client.post("/api/v1/auth/login", json={"email": email, "password": "TestPass123", "remember_me": False})
assert response.status_code == 200
return response.json()["token"]
def _auth(token: str) -> dict[str, str]:
return {"Authorization": f"Bearer {token}"}
@pytest.fixture
def enable_multiuser(monkeypatch: Any, mock_invoker: Invoker):
mock_invoker.services.configuration.multiuser = True
mock_deps = MockApiDependencies(mock_invoker)
monkeypatch.setattr("invokeai.app.api.routers.auth.ApiDependencies", mock_deps)
monkeypatch.setattr("invokeai.app.api.auth_dependencies.ApiDependencies", mock_deps)
monkeypatch.setattr("invokeai.app.api.routers.session_queue.ApiDependencies", mock_deps)
yield
@pytest.fixture
def admin_token(setup_jwt_secret: None, enable_multiuser: Any, mock_invoker: Invoker, client: TestClient):
_create_user(mock_invoker, "admin@test.com", "Admin", is_admin=True)
return _login(client, "admin@test.com")
@pytest.fixture
def user1_token(enable_multiuser: Any, mock_invoker: Invoker, client: TestClient, admin_token: str):
_create_user(mock_invoker, "user1@test.com", "User One")
return _login(client, "user1@test.com")
@pytest.fixture
def user2_token(enable_multiuser: Any, mock_invoker: Invoker, client: TestClient, admin_token: str):
_create_user(mock_invoker, "user2@test.com", "User Two")
return _login(client, "user2@test.com")
def _insert_queue_item(
session_queue: SqliteSessionQueue,
*,
queue_id: str = "default",
user_id: str,
status: str,
session: GraphExecutionState | None = None,
workflow_call_id: str | None = None,
parent_item_id: int | None = None,
parent_session_id: str | None = None,
root_item_id: int | None = None,
workflow_call_depth: int | None = None,
) -> int:
session = session or GraphExecutionState(graph=Graph())
with session_queue._db.transaction() as cursor:
cursor.execute(
"""--sql
INSERT INTO session_queue (
queue_id, session, session_id, batch_id, field_values, priority, workflow, origin, destination,
retried_from_item_id, user_id, status, workflow_call_id, parent_item_id, parent_session_id,
root_item_id, workflow_call_depth
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
queue_id,
session.model_dump_json(warnings=False),
session.id,
str(uuid.uuid4()),
None,
0,
None,
None,
None,
None,
user_id,
status,
workflow_call_id,
parent_item_id,
parent_session_id,
root_item_id,
workflow_call_depth,
),
)
return cursor.lastrowid
def test_get_queue_status_reports_waiting_for_owner(
client: TestClient, mock_invoker: Invoker, user1_token: str, user2_token: str
) -> None:
user1 = mock_invoker.services.users.get_by_email("user1@test.com")
user2 = mock_invoker.services.users.get_by_email("user2@test.com")
assert user1 is not None and user2 is not None
_insert_queue_item(mock_invoker.services.session_queue, user_id=user1.user_id, status="waiting")
_insert_queue_item(mock_invoker.services.session_queue, user_id=user2.user_id, status="pending")
response = client.get("/api/v1/queue/default/status", headers=_auth(user1_token))
assert response.status_code == 200
payload = response.json()
assert payload["queue"]["waiting"] == 1
assert payload["queue"]["pending"] == 1
assert payload["queue"]["in_progress"] == 0
assert payload["queue"]["total"] == 2
assert payload["queue"]["user_pending"] == 0
assert payload["queue"]["user_in_progress"] == 0
assert payload["queue"]["item_id"] is None
def test_get_queue_item_sanitizes_workflow_call_metadata_for_non_owner(
client: TestClient, mock_invoker: Invoker, user1_token: str, user2_token: str
) -> None:
user1 = mock_invoker.services.users.get_by_email("user1@test.com")
assert user1 is not None
item_id = _insert_queue_item(
mock_invoker.services.session_queue,
user_id=user1.user_id,
status="waiting",
workflow_call_id="workflow-call-1",
parent_item_id=41,
parent_session_id="parent-session-1",
root_item_id=17,
workflow_call_depth=2,
)
response = client.get(f"/api/v1/queue/default/i/{item_id}", headers=_auth(user2_token))
assert response.status_code == 200
payload = response.json()
assert payload["status"] == "waiting"
assert payload["item_id"] == item_id
assert payload["user_id"] == "redacted"
assert payload["batch_id"] == "redacted"
assert payload["session_id"] == "redacted"
assert payload.get("workflow_call_id") is None
assert payload.get("parent_item_id") is None
assert payload.get("parent_session_id") is None
assert payload.get("root_item_id") is None
assert payload.get("workflow_call_depth") is None
def test_retry_items_by_id_normalizes_child_to_root_at_router(
client: TestClient, mock_invoker: Invoker, user1_token: str
) -> None:
user1 = mock_invoker.services.users.get_by_email("user1@test.com")
assert user1 is not None
root_item_id = _insert_queue_item(mock_invoker.services.session_queue, user_id=user1.user_id, status="failed")
child_item_id = _insert_queue_item(
mock_invoker.services.session_queue,
user_id=user1.user_id,
status="failed",
workflow_call_id="workflow-call-1",
parent_item_id=root_item_id,
parent_session_id="parent-session-1",
root_item_id=root_item_id,
workflow_call_depth=1,
)
response = client.put("/api/v1/queue/default/retry_items_by_id", headers=_auth(user1_token), json=[child_item_id])
assert response.status_code == 200
assert response.json()["retried_item_ids"] == [root_item_id]
def test_retry_items_by_id_rejects_items_from_other_queue(
client: TestClient, mock_invoker: Invoker, user1_token: str
) -> None:
user1 = mock_invoker.services.users.get_by_email("user1@test.com")
assert user1 is not None
item_id = _insert_queue_item(
mock_invoker.services.session_queue,
queue_id="secondary",
user_id=user1.user_id,
status="failed",
)
response = client.put("/api/v1/queue/default/retry_items_by_id", headers=_auth(user1_token), json=[item_id])
assert response.status_code == 404
def test_retry_items_by_id_authorizes_workflow_call_root_owner(
client: TestClient, mock_invoker: Invoker, user1_token: str, user2_token: str
) -> None:
user1 = mock_invoker.services.users.get_by_email("user1@test.com")
user2 = mock_invoker.services.users.get_by_email("user2@test.com")
assert user1 is not None and user2 is not None
root_item_id = _insert_queue_item(mock_invoker.services.session_queue, user_id=user2.user_id, status="failed")
child_item_id = _insert_queue_item(
mock_invoker.services.session_queue,
user_id=user1.user_id,
status="failed",
workflow_call_id="workflow-call-1",
parent_item_id=root_item_id,
parent_session_id="parent-session-1",
root_item_id=root_item_id,
workflow_call_depth=1,
)
response = client.put("/api/v1/queue/default/retry_items_by_id", headers=_auth(user1_token), json=[child_item_id])
assert response.status_code == 403
assert mock_invoker.services.session_queue.get_queue_item(root_item_id).status == "failed"
def test_retry_items_by_id_skips_missing_items_and_retries_valid_items(
client: TestClient, mock_invoker: Invoker, user1_token: str
) -> None:
user1 = mock_invoker.services.users.get_by_email("user1@test.com")
assert user1 is not None
root_item_id = _insert_queue_item(mock_invoker.services.session_queue, user_id=user1.user_id, status="failed")
response = client.put(
"/api/v1/queue/default/retry_items_by_id",
headers=_auth(user1_token),
json=[root_item_id + 9999, root_item_id],
)
assert response.status_code == 200
assert response.json()["retried_item_ids"] == [root_item_id]
def test_cancel_queue_item_cascades_to_waiting_parent_via_router(
client: TestClient, mock_invoker: Invoker, user1_token: str
) -> None:
user1 = mock_invoker.services.users.get_by_email("user1@test.com")
assert user1 is not None
parent_item_id = _insert_queue_item(mock_invoker.services.session_queue, user_id=user1.user_id, status="waiting")
child_item_id = _insert_queue_item(
mock_invoker.services.session_queue,
user_id=user1.user_id,
status="pending",
workflow_call_id="workflow-call-1",
parent_item_id=parent_item_id,
parent_session_id="parent-session-1",
root_item_id=parent_item_id,
workflow_call_depth=1,
)
response = client.put(f"/api/v1/queue/default/i/{child_item_id}/cancel", headers=_auth(user1_token))
assert response.status_code == 200
assert response.json()["status"] == "canceled"
assert mock_invoker.services.session_queue.get_queue_item(parent_item_id).status == "canceled"
assert mock_invoker.services.session_queue.get_queue_item(child_item_id).status == "canceled"
def test_cancel_queue_item_rejects_item_from_other_queue(
client: TestClient, mock_invoker: Invoker, user1_token: str
) -> None:
user1 = mock_invoker.services.users.get_by_email("user1@test.com")
assert user1 is not None
item_id = _insert_queue_item(
mock_invoker.services.session_queue,
queue_id="secondary",
user_id=user1.user_id,
status="pending",
)
response = client.put(f"/api/v1/queue/default/i/{item_id}/cancel", headers=_auth(user1_token))
assert response.status_code == 404
def test_delete_queue_item_rejects_item_from_other_queue(
client: TestClient, mock_invoker: Invoker, user1_token: str
) -> None:
user1 = mock_invoker.services.users.get_by_email("user1@test.com")
assert user1 is not None
item_id = _insert_queue_item(
mock_invoker.services.session_queue,
queue_id="secondary",
user_id=user1.user_id,
status="failed",
)
response = client.delete(f"/api/v1/queue/default/i/{item_id}", headers=_auth(user1_token))
assert response.status_code == 404
def test_delete_queue_item_authorizes_workflow_call_root_owner(
client: TestClient, mock_invoker: Invoker, user1_token: str, user2_token: str
) -> None:
user1 = mock_invoker.services.users.get_by_email("user1@test.com")
user2 = mock_invoker.services.users.get_by_email("user2@test.com")
assert user1 is not None and user2 is not None
root_item_id = _insert_queue_item(mock_invoker.services.session_queue, user_id=user2.user_id, status="failed")
child_item_id = _insert_queue_item(
mock_invoker.services.session_queue,
user_id=user1.user_id,
status="failed",
workflow_call_id="workflow-call-1",
parent_item_id=root_item_id,
parent_session_id="parent-session-1",
root_item_id=root_item_id,
workflow_call_depth=1,
)
response = client.delete(f"/api/v1/queue/default/i/{child_item_id}", headers=_auth(user1_token))
assert response.status_code == 403
assert mock_invoker.services.session_queue.get_queue_item(root_item_id).status == "failed"
assert mock_invoker.services.session_queue.get_queue_item(child_item_id).status == "failed"