Modify ChatConversationApi to include pause state in status_count (vibe-kanban 4f35bf71)

If the workflow generating a message is paused, the status\_count should count it as paused.
This commit is contained in:
QuantumGhost
2026-01-27 18:12:13 +08:00
parent d289cf4824
commit 6cd62971c8
5 changed files with 230 additions and 1 deletions

View File

@ -90,6 +90,7 @@ status_count_model = console_ns.model(
"success": fields.Integer,
"failed": fields.Integer,
"partial_success": fields.Integer,
"paused": fields.Integer,
},
)

View File

@ -92,7 +92,12 @@ message_detail_fields = {
}
feedback_stat_fields = {"like": fields.Integer, "dislike": fields.Integer}
status_count_fields = {"success": fields.Integer, "failed": fields.Integer, "partial_success": fields.Integer}
status_count_fields = {
"success": fields.Integer,
"failed": fields.Integer,
"partial_success": fields.Integer,
"paused": fields.Integer,
}
model_config_fields = {
"opening_statement": fields.String,
"suggested_questions": fields.Raw,

View File

@ -868,6 +868,7 @@ class Conversation(Base):
WorkflowExecutionStatus.FAILED: 0,
WorkflowExecutionStatus.STOPPED: 0,
WorkflowExecutionStatus.PARTIAL_SUCCEEDED: 0,
WorkflowExecutionStatus.PAUSED: 0,
}
for message in messages:
@ -888,6 +889,7 @@ class Conversation(Base):
"success": status_counts[WorkflowExecutionStatus.SUCCEEDED],
"failed": status_counts[WorkflowExecutionStatus.FAILED],
"partial_success": status_counts[WorkflowExecutionStatus.PARTIAL_SUCCEEDED],
"paused": status_counts[WorkflowExecutionStatus.PAUSED],
}
@property

View File

@ -0,0 +1,166 @@
"""TestContainers integration tests for ChatConversationApi status_count behavior."""
import json
import uuid
from flask.testing import FlaskClient
from sqlalchemy.orm import Session
from configs import dify_config
from constants import HEADER_NAME_CSRF_TOKEN
from core.workflow.enums import WorkflowExecutionStatus
from libs.datetime_utils import naive_utc_now
from libs.token import _real_cookie_name, generate_csrf_token
from models import Account, DifySetup, Tenant, TenantAccountJoin
from models.account import AccountStatus, TenantAccountRole
from models.enums import CreatorUserRole
from models.model import App, AppMode, Conversation, Message
from models.workflow import WorkflowRun
from services.account_service import AccountService
def _create_account_and_tenant(db_session: Session) -> tuple[Account, Tenant]:
account = Account(
email=f"test-{uuid.uuid4()}@example.com",
name="Test User",
interface_language="en-US",
status=AccountStatus.ACTIVE,
)
account.initialized_at = naive_utc_now()
db_session.add(account)
db_session.commit()
tenant = Tenant(name="Test Tenant", status="normal")
db_session.add(tenant)
db_session.commit()
join = TenantAccountJoin(
tenant_id=tenant.id,
account_id=account.id,
role=TenantAccountRole.OWNER,
current=True,
)
db_session.add(join)
db_session.commit()
account.set_tenant_id(tenant.id)
account.timezone = "UTC"
db_session.commit()
dify_setup = DifySetup(version=dify_config.project.version)
db_session.add(dify_setup)
db_session.commit()
return account, tenant
def _create_app(db_session: Session, tenant_id: str, account_id: str) -> App:
app = App(
tenant_id=tenant_id,
name="Test Chat App",
mode=AppMode.CHAT,
enable_site=True,
enable_api=True,
created_by=account_id,
)
db_session.add(app)
db_session.commit()
return app
def _create_conversation(db_session: Session, app_id: str, account_id: str) -> Conversation:
conversation = Conversation(
app_id=app_id,
name="Test Conversation",
inputs={},
status="normal",
mode=AppMode.CHAT,
from_source=CreatorUserRole.ACCOUNT,
from_account_id=account_id,
)
db_session.add(conversation)
db_session.commit()
return conversation
def _create_workflow_run(db_session: Session, app_id: str, tenant_id: str, account_id: str) -> WorkflowRun:
workflow_run = WorkflowRun(
tenant_id=tenant_id,
app_id=app_id,
workflow_id=str(uuid.uuid4()),
type="chat",
triggered_from="app-run",
version="1.0.0",
graph=json.dumps({"nodes": [], "edges": []}),
inputs=json.dumps({"query": "test"}),
status=WorkflowExecutionStatus.PAUSED,
outputs=json.dumps({}),
elapsed_time=0.0,
total_tokens=0,
total_steps=0,
created_by_role=CreatorUserRole.ACCOUNT,
created_by=account_id,
created_at=naive_utc_now(),
)
db_session.add(workflow_run)
db_session.commit()
return workflow_run
def _create_message(
db_session: Session, app_id: str, conversation_id: str, workflow_run_id: str, account_id: str
) -> Message:
message = Message(
app_id=app_id,
conversation_id=conversation_id,
query="Hello",
message={"type": "text", "content": "Hello"},
answer="Hi there",
message_tokens=1,
answer_tokens=1,
message_unit_price=0.001,
answer_unit_price=0.001,
message_price_unit=0.001,
answer_price_unit=0.001,
currency="USD",
status="normal",
from_source=CreatorUserRole.ACCOUNT,
from_account_id=account_id,
workflow_run_id=workflow_run_id,
inputs={"query": "Hello"},
)
db_session.add(message)
db_session.commit()
return message
def test_chat_conversation_status_count_includes_paused(
db_session_with_containers: Session,
test_client_with_containers: FlaskClient,
):
account, tenant = _create_account_and_tenant(db_session_with_containers)
app = _create_app(db_session_with_containers, tenant.id, account.id)
conversation = _create_conversation(db_session_with_containers, app.id, account.id)
conversation_id = conversation.id
workflow_run = _create_workflow_run(db_session_with_containers, app.id, tenant.id, account.id)
_create_message(db_session_with_containers, app.id, conversation.id, workflow_run.id, account.id)
access_token = AccountService.get_account_jwt_token(account)
csrf_token = generate_csrf_token(account.id)
cookie_name = _real_cookie_name("csrf_token")
test_client_with_containers.set_cookie(cookie_name, csrf_token, domain="localhost")
response = test_client_with_containers.get(
f"/console/api/apps/{app.id}/chat-conversations",
headers={
"Authorization": f"Bearer {access_token}",
HEADER_NAME_CSRF_TOKEN: csrf_token,
},
)
assert response.status_code == 200
payload = response.get_json()
assert payload is not None
assert payload["total"] == 1
assert payload["data"][0]["id"] == conversation_id
assert payload["data"][0]["status_count"]["paused"] == 1

View File

@ -1296,6 +1296,7 @@ class TestConversationStatusCount:
assert result["success"] == 1 # One SUCCEEDED
assert result["failed"] == 1 # One FAILED
assert result["partial_success"] == 1 # One PARTIAL_SUCCEEDED
assert result["paused"] == 0
def test_status_count_app_id_filtering(self):
"""Test that status_count filters workflow runs by app_id for security."""
@ -1350,6 +1351,7 @@ class TestConversationStatusCount:
assert result["success"] == 0
assert result["failed"] == 0
assert result["partial_success"] == 0
assert result["paused"] == 0
def test_status_count_handles_invalid_workflow_status(self):
"""Test that status_count gracefully handles invalid workflow status values."""
@ -1404,3 +1406,56 @@ class TestConversationStatusCount:
assert result["success"] == 0
assert result["failed"] == 0
assert result["partial_success"] == 0
assert result["paused"] == 0
def test_status_count_paused(self):
"""Test status_count includes paused workflow runs."""
# Arrange
from core.workflow.enums import WorkflowExecutionStatus
app_id = str(uuid4())
conversation_id = str(uuid4())
workflow_run_id = str(uuid4())
conversation = Conversation(
app_id=app_id,
mode=AppMode.CHAT,
name="Test Conversation",
status="normal",
from_source="api",
)
conversation.id = conversation_id
mock_messages = [
MagicMock(
conversation_id=conversation_id,
workflow_run_id=workflow_run_id,
),
]
mock_workflow_runs = [
MagicMock(
id=workflow_run_id,
status=WorkflowExecutionStatus.PAUSED.value,
app_id=app_id,
),
]
with patch("models.model.db.session.scalars") as mock_scalars:
def mock_scalars_side_effect(query):
mock_result = MagicMock()
if "messages" in str(query):
mock_result.all.return_value = mock_messages
elif "workflow_runs" in str(query):
mock_result.all.return_value = mock_workflow_runs
else:
mock_result.all.return_value = []
return mock_result
mock_scalars.side_effect = mock_scalars_side_effect
# Act
result = conversation.status_count
# Assert
assert result["paused"] == 1