mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Python: Durable Support for Workflows (#3630)
* Add workflow support for Azure Functions * fix compatability with latest framework changes and add integration tests * refactor code * remove white space Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * align help text with actual port used Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * replace instance id with a place holder Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * remove unused import Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * remove redundant typing import and fix SIM115 * fix latest breaking changes * fix mypy issues * clean up imports * define source marker strings as constants * fix json module name * refactor _extract_message_content_from_dict * refactor serialization * add helper method for error response construction and remove _extract_message_content_from_dict since it is not needed * use strict tpe checking for edges * change how duplicate agent registrations are handled * cancel approval_task on HITL timeout * update docstring * fix: align azurefunctions package with core API changes after rebase - State.import_state/export_state are now sync (removed await) - Add State.commit() before export_state() in activity execution - Rename executor parameter shared_state -> state - Rename ctx.set_shared_state/get_shared_state -> set_state/get_state (sync) - WorkflowBuilder now takes start_executor as constructor kwarg - Update WorkflowOutputEvent -> WorkflowEvent with type='output' - Update RequestInfoEvent -> WorkflowEvent[Any] - Update SharedState -> State in test imports - Update duplicate agent name tests to match new warning behavior - Update sample README API references * fix sample check errors * fix mypy issues * fix trailing white spaces * fix test imports * feat: add durable workflow samples and adapt to main branch changes - Add workflow samples 09-12 to 04-hosting/azure_functions/ - Adapt to ChatMessage -> Message rename from main - Adapt to pickle-based checkpoint encoding from main - Simplify _serialization.py to delegate to core encode/decode - Fix Message -> WorkflowMessage disambiguation in _context.py - Remove non-existent _checkpoint_summary import * fix: update create_checkpoint signature to match superclass * fix: correct relative link in HITL sample README * fix: resolve import breakage after rebase (State, DurableAgentThread, get_logger) --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
Unverified
parent
9a369c69c0
commit
bb3d3c2efc
+95
@@ -0,0 +1,95 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
"""
|
||||
Integration Tests for Workflow Shared State Sample
|
||||
|
||||
Tests the workflow shared state sample for conditional email processing
|
||||
with shared state management.
|
||||
|
||||
The function app is automatically started by the test fixture.
|
||||
|
||||
Prerequisites:
|
||||
- Azure OpenAI credentials configured (see packages/azurefunctions/tests/integration_tests/.env.example)
|
||||
- Azurite running for durable orchestrations (or Azure Storage account configured)
|
||||
|
||||
Usage:
|
||||
# Start Azurite (if not already running)
|
||||
azurite &
|
||||
|
||||
# Run tests
|
||||
uv run pytest packages/azurefunctions/tests/integration_tests/test_09_workflow_shared_state.py -v
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
# Module-level markers - applied to all tests in this file
|
||||
pytestmark = [
|
||||
pytest.mark.sample("09_workflow_shared_state"),
|
||||
pytest.mark.usefixtures("function_app_for_test"),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.orchestration
|
||||
class TestWorkflowSharedState:
|
||||
"""Tests for 09_workflow_shared_state sample."""
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _setup(self, base_url: str, sample_helper) -> None:
|
||||
"""Provide the helper and base URL for each test."""
|
||||
self.base_url = base_url
|
||||
self.helper = sample_helper
|
||||
|
||||
def test_workflow_with_spam_email(self) -> None:
|
||||
"""Test workflow with spam email content - should be detected and handled as spam."""
|
||||
spam_content = "URGENT! You have won $1,000,000! Click here to claim your prize now before it expires!"
|
||||
|
||||
# Start orchestration with spam email
|
||||
response = self.helper.post_json(f"{self.base_url}/api/workflow/run", spam_content)
|
||||
assert response.status_code == 202
|
||||
data = response.json()
|
||||
assert "instanceId" in data
|
||||
assert "statusQueryGetUri" in data
|
||||
|
||||
# Wait for completion
|
||||
status = self.helper.wait_for_orchestration_with_output(data["statusQueryGetUri"])
|
||||
assert status["runtimeStatus"] == "Completed"
|
||||
assert "output" in status
|
||||
|
||||
def test_workflow_with_legitimate_email(self) -> None:
|
||||
"""Test workflow with legitimate email content - should generate response."""
|
||||
legitimate_content = (
|
||||
"Hi team, just a reminder about the sprint planning meeting tomorrow at 10 AM. "
|
||||
"Please review the agenda items in Jira before the call."
|
||||
)
|
||||
|
||||
# Start orchestration with legitimate email
|
||||
response = self.helper.post_json(f"{self.base_url}/api/workflow/run", legitimate_content)
|
||||
assert response.status_code == 202
|
||||
data = response.json()
|
||||
assert "instanceId" in data
|
||||
assert "statusQueryGetUri" in data
|
||||
|
||||
# Wait for completion
|
||||
status = self.helper.wait_for_orchestration_with_output(data["statusQueryGetUri"])
|
||||
assert status["runtimeStatus"] == "Completed"
|
||||
assert "output" in status
|
||||
|
||||
def test_workflow_with_phishing_email(self) -> None:
|
||||
"""Test workflow with phishing email - should be detected as spam."""
|
||||
phishing_content = (
|
||||
"Dear Customer, Your account has been compromised! "
|
||||
"Click this link immediately to secure your account: http://totallylegit.suspicious.com/secure"
|
||||
)
|
||||
|
||||
# Start orchestration with phishing email
|
||||
response = self.helper.post_json(f"{self.base_url}/api/workflow/run", phishing_content)
|
||||
assert response.status_code == 202
|
||||
data = response.json()
|
||||
assert "instanceId" in data
|
||||
|
||||
# Wait for completion
|
||||
status = self.helper.wait_for_orchestration_with_output(data["statusQueryGetUri"])
|
||||
assert status["runtimeStatus"] == "Completed"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__, "-v"])
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
"""
|
||||
Integration Tests for Workflow No Shared State Sample
|
||||
|
||||
Tests the workflow sample that runs without shared state,
|
||||
demonstrating conditional routing with spam detection and email response.
|
||||
|
||||
The function app is automatically started by the test fixture.
|
||||
|
||||
Prerequisites:
|
||||
- Azure OpenAI credentials configured (see packages/azurefunctions/tests/integration_tests/.env.example)
|
||||
- Azurite running for durable orchestrations (or Azure Storage account configured)
|
||||
|
||||
Usage:
|
||||
# Start Azurite (if not already running)
|
||||
azurite &
|
||||
|
||||
# Run tests
|
||||
uv run pytest packages/azurefunctions/tests/integration_tests/test_10_workflow_no_shared_state.py -v
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
# Module-level markers - applied to all tests in this file
|
||||
pytestmark = [
|
||||
pytest.mark.sample("10_workflow_no_shared_state"),
|
||||
pytest.mark.usefixtures("function_app_for_test"),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.orchestration
|
||||
class TestWorkflowNoSharedState:
|
||||
"""Tests for 10_workflow_no_shared_state sample."""
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _setup(self, base_url: str, sample_helper) -> None:
|
||||
"""Provide the helper and base URL for each test."""
|
||||
self.base_url = base_url
|
||||
self.helper = sample_helper
|
||||
|
||||
def test_workflow_with_spam_email(self) -> None:
|
||||
"""Test workflow with spam email - should detect and handle as spam."""
|
||||
payload = {
|
||||
"email_id": "email-test-001",
|
||||
"email_content": (
|
||||
"URGENT! You've won $1,000,000! Click here immediately to claim your prize! "
|
||||
"Limited time offer - act now!"
|
||||
),
|
||||
}
|
||||
|
||||
# Start orchestration
|
||||
response = self.helper.post_json(f"{self.base_url}/api/workflow/run", payload)
|
||||
assert response.status_code == 202
|
||||
data = response.json()
|
||||
assert "instanceId" in data
|
||||
assert "statusQueryGetUri" in data
|
||||
|
||||
# Wait for completion
|
||||
status = self.helper.wait_for_orchestration_with_output(data["statusQueryGetUri"])
|
||||
assert status["runtimeStatus"] == "Completed"
|
||||
assert "output" in status
|
||||
|
||||
def test_workflow_with_legitimate_email(self) -> None:
|
||||
"""Test workflow with legitimate email - should draft a response."""
|
||||
payload = {
|
||||
"email_id": "email-test-002",
|
||||
"email_content": (
|
||||
"Hi team, just a reminder about our sprint planning meeting tomorrow at 10 AM. "
|
||||
"Please review the agenda in Jira."
|
||||
),
|
||||
}
|
||||
|
||||
# Start orchestration
|
||||
response = self.helper.post_json(f"{self.base_url}/api/workflow/run", payload)
|
||||
assert response.status_code == 202
|
||||
data = response.json()
|
||||
assert "instanceId" in data
|
||||
assert "statusQueryGetUri" in data
|
||||
|
||||
# Wait for completion
|
||||
status = self.helper.wait_for_orchestration_with_output(data["statusQueryGetUri"])
|
||||
assert status["runtimeStatus"] == "Completed"
|
||||
assert "output" in status
|
||||
|
||||
def test_workflow_status_endpoint(self) -> None:
|
||||
"""Test that the status endpoint works correctly."""
|
||||
payload = {
|
||||
"email_id": "email-test-003",
|
||||
"email_content": "Quick question: When is the next team meeting scheduled?",
|
||||
}
|
||||
|
||||
# Start orchestration
|
||||
response = self.helper.post_json(f"{self.base_url}/api/workflow/run", payload)
|
||||
assert response.status_code == 202
|
||||
data = response.json()
|
||||
instance_id = data["instanceId"]
|
||||
|
||||
# Check status using the workflow status endpoint
|
||||
status_response = self.helper.get(f"{self.base_url}/api/workflow/status/{instance_id}")
|
||||
assert status_response.status_code == 200
|
||||
status = status_response.json()
|
||||
assert "instanceId" in status
|
||||
assert status["instanceId"] == instance_id
|
||||
assert "runtimeStatus" in status
|
||||
|
||||
# Wait for completion to clean up
|
||||
self.helper.wait_for_orchestration(data["statusQueryGetUri"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__, "-v"])
|
||||
@@ -0,0 +1,138 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
"""
|
||||
Integration Tests for Parallel Workflow Sample
|
||||
|
||||
Tests the parallel workflow execution sample demonstrating:
|
||||
- Two executors running concurrently (fan-out to activities)
|
||||
- Two agents running concurrently (fan-out to entities)
|
||||
- Mixed agent + executor running concurrently
|
||||
|
||||
The function app is automatically started by the test fixture.
|
||||
|
||||
Prerequisites:
|
||||
- Azure OpenAI credentials configured (see packages/azurefunctions/tests/integration_tests/.env.example)
|
||||
- Azurite running for durable orchestrations (or Azure Storage account configured)
|
||||
|
||||
Usage:
|
||||
# Start Azurite (if not already running)
|
||||
azurite &
|
||||
|
||||
# Run tests
|
||||
uv run pytest packages/azurefunctions/tests/integration_tests/test_11_workflow_parallel.py -v
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
# Module-level markers - applied to all tests in this file
|
||||
pytestmark = [
|
||||
pytest.mark.sample("11_workflow_parallel"),
|
||||
pytest.mark.usefixtures("function_app_for_test"),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.orchestration
|
||||
class TestWorkflowParallel:
|
||||
"""Tests for 11_workflow_parallel sample."""
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _setup(self, base_url: str, sample_helper) -> None:
|
||||
"""Provide the helper and base URL for each test."""
|
||||
self.base_url = base_url
|
||||
self.helper = sample_helper
|
||||
|
||||
def test_parallel_workflow_document_analysis(self) -> None:
|
||||
"""Test parallel workflow with a standard document."""
|
||||
payload = {
|
||||
"document_id": "doc-test-001",
|
||||
"content": (
|
||||
"The quarterly earnings report shows strong growth in our cloud services division. "
|
||||
"Revenue increased by 25% compared to last year, driven by enterprise adoption. "
|
||||
"Customer satisfaction remains high at 92%. However, we face challenges in the "
|
||||
"mobile segment where competition is intense. Overall, the outlook is positive "
|
||||
"with expected continued growth in the coming quarters."
|
||||
),
|
||||
}
|
||||
|
||||
# Start orchestration
|
||||
response = self.helper.post_json(f"{self.base_url}/api/workflow/run", payload)
|
||||
assert response.status_code == 202
|
||||
data = response.json()
|
||||
assert "instanceId" in data
|
||||
assert "statusQueryGetUri" in data
|
||||
|
||||
# Wait for completion - parallel workflows may take longer
|
||||
status = self.helper.wait_for_orchestration_with_output(
|
||||
data["statusQueryGetUri"],
|
||||
max_wait=300, # 5 minutes for parallel execution
|
||||
)
|
||||
assert status["runtimeStatus"] == "Completed"
|
||||
assert "output" in status
|
||||
|
||||
def test_parallel_workflow_short_document(self) -> None:
|
||||
"""Test parallel workflow with a short document."""
|
||||
payload = {
|
||||
"document_id": "doc-test-002",
|
||||
"content": "Quick update: Project completed successfully. Team performance exceeded expectations.",
|
||||
}
|
||||
|
||||
# Start orchestration
|
||||
response = self.helper.post_json(f"{self.base_url}/api/workflow/run", payload)
|
||||
assert response.status_code == 202
|
||||
data = response.json()
|
||||
assert "instanceId" in data
|
||||
assert "statusQueryGetUri" in data
|
||||
|
||||
# Wait for completion
|
||||
status = self.helper.wait_for_orchestration_with_output(data["statusQueryGetUri"], max_wait=300)
|
||||
assert status["runtimeStatus"] == "Completed"
|
||||
assert "output" in status
|
||||
|
||||
def test_parallel_workflow_technical_document(self) -> None:
|
||||
"""Test parallel workflow with a technical document."""
|
||||
payload = {
|
||||
"document_id": "doc-test-003",
|
||||
"content": (
|
||||
"The new microservices architecture has been deployed to production. "
|
||||
"Key improvements include: reduced latency by 40%, improved scalability "
|
||||
"to handle 10x traffic spikes, and enhanced monitoring with distributed tracing. "
|
||||
"The Kubernetes cluster is now running on version 1.28 with auto-scaling enabled. "
|
||||
"Next steps include implementing service mesh and improving CI/CD pipelines."
|
||||
),
|
||||
}
|
||||
|
||||
# Start orchestration
|
||||
response = self.helper.post_json(f"{self.base_url}/api/workflow/run", payload)
|
||||
assert response.status_code == 202
|
||||
data = response.json()
|
||||
assert "instanceId" in data
|
||||
|
||||
# Wait for completion
|
||||
status = self.helper.wait_for_orchestration_with_output(data["statusQueryGetUri"], max_wait=300)
|
||||
assert status["runtimeStatus"] == "Completed"
|
||||
|
||||
def test_workflow_status_endpoint(self) -> None:
|
||||
"""Test that the workflow status endpoint works correctly."""
|
||||
payload = {
|
||||
"document_id": "doc-test-004",
|
||||
"content": "Brief status update for testing purposes.",
|
||||
}
|
||||
|
||||
# Start orchestration
|
||||
response = self.helper.post_json(f"{self.base_url}/api/workflow/run", payload)
|
||||
assert response.status_code == 202
|
||||
data = response.json()
|
||||
instance_id = data["instanceId"]
|
||||
|
||||
# Check status
|
||||
status_response = self.helper.get(f"{self.base_url}/api/workflow/status/{instance_id}")
|
||||
assert status_response.status_code == 200
|
||||
status = status_response.json()
|
||||
assert "instanceId" in status
|
||||
assert status["instanceId"] == instance_id
|
||||
|
||||
# Wait for completion
|
||||
self.helper.wait_for_orchestration(data["statusQueryGetUri"], max_wait=300)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__, "-v"])
|
||||
@@ -0,0 +1,214 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
"""
|
||||
Integration Tests for Workflow Human-in-the-Loop (HITL) Sample
|
||||
|
||||
Tests the workflow HITL sample demonstrating content moderation with human approval
|
||||
using the MAF request_info / @response_handler pattern.
|
||||
|
||||
The function app is automatically started by the test fixture.
|
||||
|
||||
Prerequisites:
|
||||
- Azure OpenAI credentials configured (see packages/azurefunctions/tests/integration_tests/.env.example)
|
||||
- Azurite running for durable orchestrations (or Azure Storage account configured)
|
||||
|
||||
Usage:
|
||||
# Start Azurite (if not already running)
|
||||
azurite &
|
||||
|
||||
# Run tests
|
||||
uv run pytest packages/azurefunctions/tests/integration_tests/test_12_workflow_hitl.py -v
|
||||
"""
|
||||
|
||||
import time
|
||||
|
||||
import pytest
|
||||
|
||||
# Module-level markers - applied to all tests in this file
|
||||
pytestmark = [
|
||||
pytest.mark.sample("12_workflow_hitl"),
|
||||
pytest.mark.usefixtures("function_app_for_test"),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.orchestration
|
||||
class TestWorkflowHITL:
|
||||
"""Tests for 12_workflow_hitl sample."""
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _setup(self, base_url: str, sample_helper) -> None:
|
||||
"""Provide the helper and base URL for each test."""
|
||||
self.base_url = base_url
|
||||
self.helper = sample_helper
|
||||
|
||||
def _wait_for_hitl_request(self, instance_id: str, timeout: int = 40) -> dict:
|
||||
"""Polls for a pending HITL request."""
|
||||
start_time = time.time()
|
||||
while time.time() - start_time < timeout:
|
||||
status_response = self.helper.get(f"{self.base_url}/api/workflow/status/{instance_id}")
|
||||
if status_response.status_code == 200:
|
||||
status = status_response.json()
|
||||
pending_requests = status.get("pendingHumanInputRequests", [])
|
||||
if pending_requests:
|
||||
return status
|
||||
time.sleep(2)
|
||||
raise AssertionError(f"Timed out waiting for HITL request for instance {instance_id}")
|
||||
|
||||
def test_hitl_workflow_approval(self) -> None:
|
||||
"""Test HITL workflow with human approval."""
|
||||
payload = {
|
||||
"content_id": "article-test-001",
|
||||
"title": "Introduction to AI in Healthcare",
|
||||
"body": (
|
||||
"Artificial intelligence is revolutionizing healthcare by enabling faster diagnosis, "
|
||||
"personalized treatment plans, and improved patient outcomes. Machine learning algorithms "
|
||||
"can analyze medical images with remarkable accuracy."
|
||||
),
|
||||
"author": "Dr. Jane Smith",
|
||||
}
|
||||
|
||||
# Start orchestration
|
||||
response = self.helper.post_json(f"{self.base_url}/api/workflow/run", payload)
|
||||
assert response.status_code == 202
|
||||
data = response.json()
|
||||
assert "instanceId" in data
|
||||
assert "statusQueryGetUri" in data
|
||||
instance_id = data["instanceId"]
|
||||
|
||||
# Wait for the workflow to reach the HITL pause point
|
||||
status = self._wait_for_hitl_request(instance_id)
|
||||
|
||||
# Confirm status is valid
|
||||
assert status["runtimeStatus"] in ["Running", "Pending"]
|
||||
|
||||
# Get the request ID from pending requests
|
||||
pending_requests = status.get("pendingHumanInputRequests", [])
|
||||
assert len(pending_requests) > 0, "Expected pending HITL request"
|
||||
request_id = pending_requests[0]["requestId"]
|
||||
|
||||
# Send approval
|
||||
approval_response = self.helper.post_json(
|
||||
f"{self.base_url}/api/workflow/respond/{instance_id}/{request_id}",
|
||||
{"approved": True, "reviewer_notes": "Content is appropriate and well-written."},
|
||||
)
|
||||
assert approval_response.status_code == 200
|
||||
|
||||
# Wait for orchestration to complete
|
||||
final_status = self.helper.wait_for_orchestration(data["statusQueryGetUri"])
|
||||
assert final_status["runtimeStatus"] == "Completed"
|
||||
assert "output" in final_status
|
||||
|
||||
def test_hitl_workflow_rejection(self) -> None:
|
||||
"""Test HITL workflow with human rejection."""
|
||||
payload = {
|
||||
"content_id": "article-test-002",
|
||||
"title": "Get Rich Quick Scheme",
|
||||
"body": (
|
||||
"Click here NOW to make $10,000 overnight! This SECRET method is GUARANTEED to work! "
|
||||
"Limited time offer - act NOW before it's too late!"
|
||||
),
|
||||
"author": "Definitely Not Spam",
|
||||
}
|
||||
|
||||
# Start orchestration
|
||||
response = self.helper.post_json(f"{self.base_url}/api/workflow/run", payload)
|
||||
assert response.status_code == 202
|
||||
data = response.json()
|
||||
instance_id = data["instanceId"]
|
||||
|
||||
# Wait for the workflow to reach the HITL pause point
|
||||
status = self._wait_for_hitl_request(instance_id)
|
||||
|
||||
# Get the request ID from pending requests
|
||||
pending_requests = status.get("pendingHumanInputRequests", [])
|
||||
assert len(pending_requests) > 0, "Expected pending HITL request"
|
||||
request_id = pending_requests[0]["requestId"]
|
||||
|
||||
# Send rejection
|
||||
rejection_response = self.helper.post_json(
|
||||
f"{self.base_url}/api/workflow/respond/{instance_id}/{request_id}",
|
||||
{"approved": False, "reviewer_notes": "Content appears to be spam/scam material."},
|
||||
)
|
||||
assert rejection_response.status_code == 200
|
||||
|
||||
# Wait for orchestration to complete
|
||||
final_status = self.helper.wait_for_orchestration(data["statusQueryGetUri"])
|
||||
assert final_status["runtimeStatus"] == "Completed"
|
||||
assert "output" in final_status
|
||||
# The output should indicate rejection
|
||||
output = final_status["output"]
|
||||
assert "rejected" in str(output).lower()
|
||||
|
||||
def test_hitl_workflow_status_endpoint(self) -> None:
|
||||
"""Test that the workflow status endpoint shows pending HITL requests."""
|
||||
payload = {
|
||||
"content_id": "article-test-003",
|
||||
"title": "Test Article",
|
||||
"body": "This is a test article for checking status endpoint functionality.",
|
||||
"author": "Test Author",
|
||||
}
|
||||
|
||||
# Start orchestration
|
||||
response = self.helper.post_json(f"{self.base_url}/api/workflow/run", payload)
|
||||
assert response.status_code == 202
|
||||
data = response.json()
|
||||
instance_id = data["instanceId"]
|
||||
|
||||
# Wait for HITL pause
|
||||
status = self._wait_for_hitl_request(instance_id)
|
||||
|
||||
# Check status
|
||||
assert "instanceId" in status
|
||||
assert status["instanceId"] == instance_id
|
||||
assert "runtimeStatus" in status
|
||||
assert "pendingHumanInputRequests" in status
|
||||
|
||||
# Clean up: approve to complete
|
||||
pending_requests = status.get("pendingHumanInputRequests", [])
|
||||
if pending_requests:
|
||||
request_id = pending_requests[0]["requestId"]
|
||||
self.helper.post_json(
|
||||
f"{self.base_url}/api/workflow/respond/{instance_id}/{request_id}",
|
||||
{"approved": True, "reviewer_notes": ""},
|
||||
)
|
||||
|
||||
# Wait for completion
|
||||
self.helper.wait_for_orchestration(data["statusQueryGetUri"])
|
||||
|
||||
def test_hitl_workflow_with_neutral_content(self) -> None:
|
||||
"""Test HITL workflow with neutral content that should get medium risk."""
|
||||
payload = {
|
||||
"content_id": "article-test-004",
|
||||
"title": "Product Review",
|
||||
"body": (
|
||||
"This product works as advertised. The build quality is average and the price "
|
||||
"is reasonable. I would recommend it for basic use cases but not for professional work."
|
||||
),
|
||||
"author": "Regular User",
|
||||
}
|
||||
|
||||
# Start orchestration
|
||||
response = self.helper.post_json(f"{self.base_url}/api/workflow/run", payload)
|
||||
assert response.status_code == 202
|
||||
data = response.json()
|
||||
instance_id = data["instanceId"]
|
||||
|
||||
# Wait for HITL pause
|
||||
status = self._wait_for_hitl_request(instance_id)
|
||||
|
||||
pending_requests = status.get("pendingHumanInputRequests", [])
|
||||
assert len(pending_requests) > 0
|
||||
request_id = pending_requests[0]["requestId"]
|
||||
|
||||
# Approve
|
||||
self.helper.post_json(
|
||||
f"{self.base_url}/api/workflow/respond/{instance_id}/{request_id}",
|
||||
{"approved": True, "reviewer_notes": "Approved after review."},
|
||||
)
|
||||
|
||||
# Wait for completion
|
||||
final_status = self.helper.wait_for_orchestration(data["statusQueryGetUri"])
|
||||
assert final_status["runtimeStatus"] == "Completed"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__, "-v"])
|
||||
Reference in New Issue
Block a user