julee 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- julee/__init__.py +3 -0
- julee/api/__init__.py +20 -0
- julee/api/app.py +180 -0
- julee/api/dependencies.py +257 -0
- julee/api/requests.py +175 -0
- julee/api/responses.py +43 -0
- julee/api/routers/__init__.py +43 -0
- julee/api/routers/assembly_specifications.py +212 -0
- julee/api/routers/documents.py +182 -0
- julee/api/routers/knowledge_service_configs.py +79 -0
- julee/api/routers/knowledge_service_queries.py +293 -0
- julee/api/routers/system.py +137 -0
- julee/api/routers/workflows.py +234 -0
- julee/api/services/__init__.py +20 -0
- julee/api/services/system_initialization.py +214 -0
- julee/api/tests/__init__.py +14 -0
- julee/api/tests/routers/__init__.py +17 -0
- julee/api/tests/routers/test_assembly_specifications.py +749 -0
- julee/api/tests/routers/test_documents.py +301 -0
- julee/api/tests/routers/test_knowledge_service_configs.py +234 -0
- julee/api/tests/routers/test_knowledge_service_queries.py +738 -0
- julee/api/tests/routers/test_system.py +179 -0
- julee/api/tests/routers/test_workflows.py +393 -0
- julee/api/tests/test_app.py +285 -0
- julee/api/tests/test_dependencies.py +245 -0
- julee/api/tests/test_requests.py +250 -0
- julee/domain/__init__.py +22 -0
- julee/domain/models/__init__.py +49 -0
- julee/domain/models/assembly/__init__.py +17 -0
- julee/domain/models/assembly/assembly.py +103 -0
- julee/domain/models/assembly/tests/__init__.py +0 -0
- julee/domain/models/assembly/tests/factories.py +37 -0
- julee/domain/models/assembly/tests/test_assembly.py +430 -0
- julee/domain/models/assembly_specification/__init__.py +24 -0
- julee/domain/models/assembly_specification/assembly_specification.py +172 -0
- julee/domain/models/assembly_specification/knowledge_service_query.py +123 -0
- julee/domain/models/assembly_specification/tests/__init__.py +0 -0
- julee/domain/models/assembly_specification/tests/factories.py +78 -0
- julee/domain/models/assembly_specification/tests/test_assembly_specification.py +490 -0
- julee/domain/models/assembly_specification/tests/test_knowledge_service_query.py +310 -0
- julee/domain/models/custom_fields/__init__.py +0 -0
- julee/domain/models/custom_fields/content_stream.py +68 -0
- julee/domain/models/custom_fields/tests/__init__.py +0 -0
- julee/domain/models/custom_fields/tests/test_custom_fields.py +53 -0
- julee/domain/models/document/__init__.py +17 -0
- julee/domain/models/document/document.py +150 -0
- julee/domain/models/document/tests/__init__.py +0 -0
- julee/domain/models/document/tests/factories.py +76 -0
- julee/domain/models/document/tests/test_document.py +297 -0
- julee/domain/models/knowledge_service_config/__init__.py +17 -0
- julee/domain/models/knowledge_service_config/knowledge_service_config.py +86 -0
- julee/domain/models/policy/__init__.py +15 -0
- julee/domain/models/policy/document_policy_validation.py +220 -0
- julee/domain/models/policy/policy.py +203 -0
- julee/domain/models/policy/tests/__init__.py +0 -0
- julee/domain/models/policy/tests/factories.py +47 -0
- julee/domain/models/policy/tests/test_document_policy_validation.py +420 -0
- julee/domain/models/policy/tests/test_policy.py +546 -0
- julee/domain/repositories/__init__.py +27 -0
- julee/domain/repositories/assembly.py +45 -0
- julee/domain/repositories/assembly_specification.py +52 -0
- julee/domain/repositories/base.py +146 -0
- julee/domain/repositories/document.py +49 -0
- julee/domain/repositories/document_policy_validation.py +52 -0
- julee/domain/repositories/knowledge_service_config.py +54 -0
- julee/domain/repositories/knowledge_service_query.py +44 -0
- julee/domain/repositories/policy.py +49 -0
- julee/domain/use_cases/__init__.py +17 -0
- julee/domain/use_cases/decorators.py +107 -0
- julee/domain/use_cases/extract_assemble_data.py +649 -0
- julee/domain/use_cases/initialize_system_data.py +842 -0
- julee/domain/use_cases/tests/__init__.py +7 -0
- julee/domain/use_cases/tests/test_extract_assemble_data.py +548 -0
- julee/domain/use_cases/tests/test_initialize_system_data.py +455 -0
- julee/domain/use_cases/tests/test_validate_document.py +1228 -0
- julee/domain/use_cases/validate_document.py +736 -0
- julee/fixtures/assembly_specifications.yaml +70 -0
- julee/fixtures/documents.yaml +178 -0
- julee/fixtures/knowledge_service_configs.yaml +37 -0
- julee/fixtures/knowledge_service_queries.yaml +27 -0
- julee/repositories/__init__.py +17 -0
- julee/repositories/memory/__init__.py +31 -0
- julee/repositories/memory/assembly.py +84 -0
- julee/repositories/memory/assembly_specification.py +125 -0
- julee/repositories/memory/base.py +227 -0
- julee/repositories/memory/document.py +149 -0
- julee/repositories/memory/document_policy_validation.py +104 -0
- julee/repositories/memory/knowledge_service_config.py +123 -0
- julee/repositories/memory/knowledge_service_query.py +120 -0
- julee/repositories/memory/policy.py +87 -0
- julee/repositories/memory/tests/__init__.py +0 -0
- julee/repositories/memory/tests/test_document.py +212 -0
- julee/repositories/memory/tests/test_document_policy_validation.py +161 -0
- julee/repositories/memory/tests/test_policy.py +443 -0
- julee/repositories/minio/__init__.py +31 -0
- julee/repositories/minio/assembly.py +103 -0
- julee/repositories/minio/assembly_specification.py +170 -0
- julee/repositories/minio/client.py +570 -0
- julee/repositories/minio/document.py +530 -0
- julee/repositories/minio/document_policy_validation.py +120 -0
- julee/repositories/minio/knowledge_service_config.py +187 -0
- julee/repositories/minio/knowledge_service_query.py +211 -0
- julee/repositories/minio/policy.py +106 -0
- julee/repositories/minio/tests/__init__.py +0 -0
- julee/repositories/minio/tests/fake_client.py +213 -0
- julee/repositories/minio/tests/test_assembly.py +374 -0
- julee/repositories/minio/tests/test_assembly_specification.py +391 -0
- julee/repositories/minio/tests/test_client_protocol.py +57 -0
- julee/repositories/minio/tests/test_document.py +591 -0
- julee/repositories/minio/tests/test_document_policy_validation.py +192 -0
- julee/repositories/minio/tests/test_knowledge_service_config.py +374 -0
- julee/repositories/minio/tests/test_knowledge_service_query.py +438 -0
- julee/repositories/minio/tests/test_policy.py +559 -0
- julee/repositories/temporal/__init__.py +38 -0
- julee/repositories/temporal/activities.py +114 -0
- julee/repositories/temporal/activity_names.py +34 -0
- julee/repositories/temporal/proxies.py +159 -0
- julee/services/__init__.py +18 -0
- julee/services/knowledge_service/__init__.py +48 -0
- julee/services/knowledge_service/anthropic/__init__.py +12 -0
- julee/services/knowledge_service/anthropic/knowledge_service.py +331 -0
- julee/services/knowledge_service/anthropic/tests/test_knowledge_service.py +318 -0
- julee/services/knowledge_service/factory.py +138 -0
- julee/services/knowledge_service/knowledge_service.py +160 -0
- julee/services/knowledge_service/memory/__init__.py +13 -0
- julee/services/knowledge_service/memory/knowledge_service.py +278 -0
- julee/services/knowledge_service/memory/test_knowledge_service.py +345 -0
- julee/services/knowledge_service/test_factory.py +112 -0
- julee/services/temporal/__init__.py +38 -0
- julee/services/temporal/activities.py +86 -0
- julee/services/temporal/activity_names.py +22 -0
- julee/services/temporal/proxies.py +41 -0
- julee/util/__init__.py +0 -0
- julee/util/domain.py +119 -0
- julee/util/repos/__init__.py +0 -0
- julee/util/repos/minio/__init__.py +0 -0
- julee/util/repos/minio/file_storage.py +213 -0
- julee/util/repos/temporal/__init__.py +11 -0
- julee/util/repos/temporal/client_proxies/file_storage.py +68 -0
- julee/util/repos/temporal/data_converter.py +123 -0
- julee/util/repos/temporal/minio_file_storage.py +12 -0
- julee/util/repos/temporal/proxies/__init__.py +0 -0
- julee/util/repos/temporal/proxies/file_storage.py +58 -0
- julee/util/repositories.py +55 -0
- julee/util/temporal/__init__.py +22 -0
- julee/util/temporal/activities.py +123 -0
- julee/util/temporal/decorators.py +473 -0
- julee/util/tests/__init__.py +1 -0
- julee/util/tests/test_decorators.py +770 -0
- julee/util/validation/__init__.py +29 -0
- julee/util/validation/repository.py +100 -0
- julee/util/validation/type_guards.py +369 -0
- julee/worker.py +211 -0
- julee/workflows/__init__.py +26 -0
- julee/workflows/extract_assemble.py +215 -0
- julee/workflows/validate_document.py +228 -0
- julee-0.1.0.dist-info/METADATA +195 -0
- julee-0.1.0.dist-info/RECORD +161 -0
- julee-0.1.0.dist-info/WHEEL +5 -0
- julee-0.1.0.dist-info/licenses/LICENSE +674 -0
- julee-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tests for the system API router.
|
|
3
|
+
|
|
4
|
+
This module provides tests for system-level endpoints including health checks
|
|
5
|
+
and other operational endpoints.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
import time
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
from typing import Generator
|
|
12
|
+
from unittest.mock import patch
|
|
13
|
+
from fastapi.testclient import TestClient
|
|
14
|
+
from fastapi import FastAPI
|
|
15
|
+
|
|
16
|
+
from julee.api.routers.system import router
|
|
17
|
+
from julee.api.responses import ServiceStatus
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@pytest.fixture
|
|
21
|
+
def app_with_router() -> FastAPI:
|
|
22
|
+
"""Create a FastAPI app with just the system router."""
|
|
23
|
+
app = FastAPI()
|
|
24
|
+
|
|
25
|
+
# Include the router (system routes are typically at root level)
|
|
26
|
+
app.include_router(router, tags=["System"])
|
|
27
|
+
|
|
28
|
+
return app
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@pytest.fixture
|
|
32
|
+
def client(
|
|
33
|
+
app_with_router: FastAPI,
|
|
34
|
+
) -> Generator[TestClient, None, None]:
|
|
35
|
+
"""Create a test client with the system router app."""
|
|
36
|
+
with (
|
|
37
|
+
patch("julee.api.routers.system.check_temporal_health") as mock_temporal,
|
|
38
|
+
patch("julee.api.routers.system.check_storage_health") as mock_storage,
|
|
39
|
+
):
|
|
40
|
+
# Mock health checks to return UP status
|
|
41
|
+
mock_temporal.return_value = ServiceStatus.UP
|
|
42
|
+
mock_storage.return_value = ServiceStatus.UP
|
|
43
|
+
|
|
44
|
+
with TestClient(app_with_router) as test_client:
|
|
45
|
+
yield test_client
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class TestHealthEndpoint:
|
|
49
|
+
"""Test the health check endpoint."""
|
|
50
|
+
|
|
51
|
+
def test_health_check(self, client: TestClient) -> None:
|
|
52
|
+
"""Test that health check returns expected response."""
|
|
53
|
+
response = client.get("/health")
|
|
54
|
+
|
|
55
|
+
assert response.status_code == 200
|
|
56
|
+
data = response.json()
|
|
57
|
+
assert data["status"] in ["healthy", "degraded", "unhealthy"]
|
|
58
|
+
assert "timestamp" in data
|
|
59
|
+
assert "services" in data
|
|
60
|
+
|
|
61
|
+
def test_health_check_response_structure(self, client: TestClient) -> None:
|
|
62
|
+
"""Test that health check response has correct structure."""
|
|
63
|
+
response = client.get("/health")
|
|
64
|
+
|
|
65
|
+
assert response.status_code == 200
|
|
66
|
+
data = response.json()
|
|
67
|
+
|
|
68
|
+
# Verify all required fields are present
|
|
69
|
+
required_fields = ["status", "timestamp", "services"]
|
|
70
|
+
for field in required_fields:
|
|
71
|
+
assert field in data, f"Missing required field: {field}"
|
|
72
|
+
|
|
73
|
+
# Verify field types
|
|
74
|
+
assert isinstance(data["status"], str)
|
|
75
|
+
assert isinstance(data["timestamp"], str)
|
|
76
|
+
assert isinstance(data["services"], dict)
|
|
77
|
+
|
|
78
|
+
# Verify status value
|
|
79
|
+
assert data["status"] in ["healthy", "degraded", "unhealthy"]
|
|
80
|
+
|
|
81
|
+
# Verify services structure
|
|
82
|
+
services = data["services"]
|
|
83
|
+
required_services = ["api", "temporal", "storage"]
|
|
84
|
+
for service in required_services:
|
|
85
|
+
assert service in services, f"Missing service: {service}"
|
|
86
|
+
assert services[service] in [
|
|
87
|
+
"up",
|
|
88
|
+
"down",
|
|
89
|
+
], f"Invalid status for {service}: {services[service]}"
|
|
90
|
+
|
|
91
|
+
def test_health_check_timestamp_format(self, client: TestClient) -> None:
|
|
92
|
+
"""Test that health check timestamp is in ISO format."""
|
|
93
|
+
|
|
94
|
+
response = client.get("/health")
|
|
95
|
+
assert response.status_code == 200
|
|
96
|
+
|
|
97
|
+
data = response.json()
|
|
98
|
+
timestamp_str = data["timestamp"]
|
|
99
|
+
|
|
100
|
+
# Should be able to parse as ISO format datetime
|
|
101
|
+
try:
|
|
102
|
+
parsed_timestamp = datetime.fromisoformat(
|
|
103
|
+
timestamp_str.replace("Z", "+00:00")
|
|
104
|
+
)
|
|
105
|
+
assert parsed_timestamp is not None
|
|
106
|
+
except ValueError:
|
|
107
|
+
pytest.fail(f"Timestamp '{timestamp_str}' is not in valid ISO format")
|
|
108
|
+
|
|
109
|
+
def test_health_check_services_status(self, client: TestClient) -> None:
|
|
110
|
+
"""Test that health check includes all service statuses."""
|
|
111
|
+
response = client.get("/health")
|
|
112
|
+
assert response.status_code == 200
|
|
113
|
+
|
|
114
|
+
data = response.json()
|
|
115
|
+
services = data["services"]
|
|
116
|
+
|
|
117
|
+
# API should always be up since we're responding
|
|
118
|
+
assert services["api"] == "up"
|
|
119
|
+
|
|
120
|
+
# Temporal and storage may be up or down depending on environment
|
|
121
|
+
assert services["temporal"] in ["up", "down"]
|
|
122
|
+
assert services["storage"] in ["up", "down"]
|
|
123
|
+
|
|
124
|
+
def test_health_check_overall_status_logic(self, client: TestClient) -> None:
|
|
125
|
+
"""Test that overall status reflects service health correctly."""
|
|
126
|
+
response = client.get("/health")
|
|
127
|
+
assert response.status_code == 200
|
|
128
|
+
|
|
129
|
+
data = response.json()
|
|
130
|
+
overall_status = data["status"]
|
|
131
|
+
services = data["services"]
|
|
132
|
+
|
|
133
|
+
# Count up services
|
|
134
|
+
up_services = sum(1 for status in services.values() if status == "up")
|
|
135
|
+
total_services = len(services)
|
|
136
|
+
|
|
137
|
+
# Validate logic
|
|
138
|
+
if up_services == total_services:
|
|
139
|
+
assert overall_status == "healthy"
|
|
140
|
+
elif up_services > 0:
|
|
141
|
+
assert overall_status == "degraded"
|
|
142
|
+
else:
|
|
143
|
+
assert overall_status == "unhealthy"
|
|
144
|
+
|
|
145
|
+
def test_health_check_multiple_calls_consistent(self, client: TestClient) -> None:
|
|
146
|
+
"""Test multiple health check calls return consistent structure."""
|
|
147
|
+
# Make multiple calls
|
|
148
|
+
responses = [client.get("/health") for _ in range(3)]
|
|
149
|
+
|
|
150
|
+
# All should be successful
|
|
151
|
+
for response in responses:
|
|
152
|
+
assert response.status_code == 200
|
|
153
|
+
|
|
154
|
+
# All should have the same structure
|
|
155
|
+
data_list = [response.json() for response in responses]
|
|
156
|
+
|
|
157
|
+
for data in data_list:
|
|
158
|
+
assert data["status"] in ["healthy", "degraded", "unhealthy"]
|
|
159
|
+
assert "timestamp" in data
|
|
160
|
+
assert "services" in data
|
|
161
|
+
|
|
162
|
+
# Services structure should be consistent
|
|
163
|
+
services = data["services"]
|
|
164
|
+
required_services = ["api", "temporal", "storage"]
|
|
165
|
+
for service in required_services:
|
|
166
|
+
assert service in services
|
|
167
|
+
assert services[service] in ["up", "down"]
|
|
168
|
+
|
|
169
|
+
def test_health_check_response_time(self, client: TestClient) -> None:
|
|
170
|
+
"""Test that health check responds quickly."""
|
|
171
|
+
|
|
172
|
+
start_time = time.time()
|
|
173
|
+
response = client.get("/health")
|
|
174
|
+
end_time = time.time()
|
|
175
|
+
|
|
176
|
+
assert response.status_code == 200
|
|
177
|
+
# Health check should complete within 10 seconds even with external
|
|
178
|
+
# service checks
|
|
179
|
+
assert end_time - start_time < 10.0
|
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tests for workflows API router.
|
|
3
|
+
|
|
4
|
+
This module provides unit tests for the workflows API endpoints,
|
|
5
|
+
focusing on workflow triggering, status monitoring, and error handling.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
from typing import Generator
|
|
10
|
+
from unittest.mock import AsyncMock, MagicMock
|
|
11
|
+
from fastapi.testclient import TestClient
|
|
12
|
+
from fastapi import FastAPI
|
|
13
|
+
from fastapi_pagination import add_pagination
|
|
14
|
+
|
|
15
|
+
from julee.api.routers.workflows import router
|
|
16
|
+
from julee.api.dependencies import get_temporal_client
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@pytest.fixture
|
|
20
|
+
def mock_temporal_client() -> MagicMock:
|
|
21
|
+
"""Create mock Temporal client."""
|
|
22
|
+
mock_client = MagicMock()
|
|
23
|
+
mock_client.start_workflow = AsyncMock()
|
|
24
|
+
mock_client.get_workflow_handle = MagicMock() # Synchronous method
|
|
25
|
+
return mock_client
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@pytest.fixture
|
|
29
|
+
def app_with_router(mock_temporal_client: MagicMock) -> FastAPI:
|
|
30
|
+
"""Create a FastAPI app with just the workflows router."""
|
|
31
|
+
app = FastAPI()
|
|
32
|
+
|
|
33
|
+
# Override the dependency with our mock temporal client
|
|
34
|
+
app.dependency_overrides[get_temporal_client] = lambda: mock_temporal_client
|
|
35
|
+
|
|
36
|
+
# Add pagination support (required for potential future endpoints)
|
|
37
|
+
add_pagination(app)
|
|
38
|
+
|
|
39
|
+
app.include_router(router, prefix="/workflows", tags=["Workflows"])
|
|
40
|
+
|
|
41
|
+
return app
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@pytest.fixture
|
|
45
|
+
def client(
|
|
46
|
+
app_with_router: FastAPI,
|
|
47
|
+
) -> Generator[TestClient, None, None]:
|
|
48
|
+
"""Create a test client with the workflows router app."""
|
|
49
|
+
with TestClient(app_with_router) as test_client:
|
|
50
|
+
yield test_client
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class TestStartExtractAssembleWorkflow:
|
|
54
|
+
"""Test cases for the start extract-assemble workflow endpoint."""
|
|
55
|
+
|
|
56
|
+
def test_start_workflow_success_with_auto_generated_id(
|
|
57
|
+
self,
|
|
58
|
+
client: TestClient,
|
|
59
|
+
mock_temporal_client: MagicMock,
|
|
60
|
+
) -> None:
|
|
61
|
+
"""Test successful workflow start with auto-generated workflow ID."""
|
|
62
|
+
# Setup mock
|
|
63
|
+
mock_handle = MagicMock()
|
|
64
|
+
mock_handle.run_id = "test-run-id-123"
|
|
65
|
+
mock_temporal_client.start_workflow.return_value = mock_handle
|
|
66
|
+
|
|
67
|
+
# Make request
|
|
68
|
+
request_data = {
|
|
69
|
+
"document_id": "doc-123",
|
|
70
|
+
"assembly_specification_id": "spec-456",
|
|
71
|
+
}
|
|
72
|
+
response = client.post("/workflows/extract-assemble", json=request_data)
|
|
73
|
+
|
|
74
|
+
# Assertions
|
|
75
|
+
assert response.status_code == 200
|
|
76
|
+
data = response.json()
|
|
77
|
+
|
|
78
|
+
assert data["run_id"] == "test-run-id-123"
|
|
79
|
+
assert data["status"] == "RUNNING"
|
|
80
|
+
assert data["message"] == "Workflow started successfully"
|
|
81
|
+
assert "extract-assemble-doc-123-spec-456" in data["workflow_id"]
|
|
82
|
+
|
|
83
|
+
# Verify temporal client was called correctly
|
|
84
|
+
mock_temporal_client.start_workflow.assert_called_once()
|
|
85
|
+
call_args = mock_temporal_client.start_workflow.call_args
|
|
86
|
+
|
|
87
|
+
# Check positional arguments
|
|
88
|
+
assert call_args[1]["args"] == ["doc-123", "spec-456"]
|
|
89
|
+
assert call_args[1]["task_queue"] == "julee-extract-assemble-queue"
|
|
90
|
+
assert "extract-assemble-doc-123-spec-456" in call_args[1]["id"]
|
|
91
|
+
|
|
92
|
+
def test_start_workflow_success_with_custom_id(
|
|
93
|
+
self,
|
|
94
|
+
client: TestClient,
|
|
95
|
+
mock_temporal_client: MagicMock,
|
|
96
|
+
) -> None:
|
|
97
|
+
"""Test successful workflow start with custom workflow ID."""
|
|
98
|
+
# Setup mock
|
|
99
|
+
mock_handle = MagicMock()
|
|
100
|
+
mock_handle.run_id = "custom-run-id"
|
|
101
|
+
mock_temporal_client.start_workflow.return_value = mock_handle
|
|
102
|
+
|
|
103
|
+
# Make request
|
|
104
|
+
request_data = {
|
|
105
|
+
"document_id": "doc-789",
|
|
106
|
+
"assembly_specification_id": "spec-101",
|
|
107
|
+
"workflow_id": "my-custom-workflow-id",
|
|
108
|
+
}
|
|
109
|
+
response = client.post("/workflows/extract-assemble", json=request_data)
|
|
110
|
+
|
|
111
|
+
# Assertions
|
|
112
|
+
assert response.status_code == 200
|
|
113
|
+
data = response.json()
|
|
114
|
+
|
|
115
|
+
assert data["workflow_id"] == "my-custom-workflow-id"
|
|
116
|
+
assert data["run_id"] == "custom-run-id"
|
|
117
|
+
assert data["status"] == "RUNNING"
|
|
118
|
+
|
|
119
|
+
# Verify temporal client was called with custom ID
|
|
120
|
+
mock_temporal_client.start_workflow.assert_called_once()
|
|
121
|
+
call_args = mock_temporal_client.start_workflow.call_args
|
|
122
|
+
assert call_args[1]["id"] == "my-custom-workflow-id"
|
|
123
|
+
|
|
124
|
+
def test_start_workflow_missing_document_id(self, client: TestClient) -> None:
|
|
125
|
+
"""Test workflow start with missing document_id."""
|
|
126
|
+
request_data = {
|
|
127
|
+
"assembly_specification_id": "spec-456",
|
|
128
|
+
}
|
|
129
|
+
response = client.post("/workflows/extract-assemble", json=request_data)
|
|
130
|
+
|
|
131
|
+
assert response.status_code == 422 # Validation error
|
|
132
|
+
data = response.json()
|
|
133
|
+
assert "document_id" in str(data["detail"])
|
|
134
|
+
|
|
135
|
+
def test_start_workflow_missing_assembly_specification_id(
|
|
136
|
+
self, client: TestClient
|
|
137
|
+
) -> None:
|
|
138
|
+
"""Test workflow start with missing assembly_specification_id."""
|
|
139
|
+
request_data = {
|
|
140
|
+
"document_id": "doc-123",
|
|
141
|
+
}
|
|
142
|
+
response = client.post("/workflows/extract-assemble", json=request_data)
|
|
143
|
+
|
|
144
|
+
assert response.status_code == 422 # Validation error
|
|
145
|
+
data = response.json()
|
|
146
|
+
assert "assembly_specification_id" in str(data["detail"])
|
|
147
|
+
|
|
148
|
+
def test_start_workflow_empty_string_ids(
|
|
149
|
+
self,
|
|
150
|
+
client: TestClient,
|
|
151
|
+
mock_temporal_client: MagicMock,
|
|
152
|
+
) -> None:
|
|
153
|
+
"""Test workflow start with empty string IDs."""
|
|
154
|
+
# Setup mock (though it shouldn't be called due to validation)
|
|
155
|
+
mock_handle = MagicMock()
|
|
156
|
+
mock_handle.run_id = "should-not-be-called"
|
|
157
|
+
mock_temporal_client.start_workflow.return_value = mock_handle
|
|
158
|
+
|
|
159
|
+
request_data = {
|
|
160
|
+
"document_id": "",
|
|
161
|
+
"assembly_specification_id": "",
|
|
162
|
+
}
|
|
163
|
+
response = client.post("/workflows/extract-assemble", json=request_data)
|
|
164
|
+
|
|
165
|
+
assert response.status_code == 422 # Validation error
|
|
166
|
+
|
|
167
|
+
def test_start_workflow_temporal_client_error(
|
|
168
|
+
self,
|
|
169
|
+
client: TestClient,
|
|
170
|
+
mock_temporal_client: MagicMock,
|
|
171
|
+
) -> None:
|
|
172
|
+
"""Test workflow start when Temporal client raises exception."""
|
|
173
|
+
# Setup mock to raise exception
|
|
174
|
+
mock_temporal_client.start_workflow.side_effect = Exception(
|
|
175
|
+
"Temporal connection failed"
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
# Make request
|
|
179
|
+
request_data = {
|
|
180
|
+
"document_id": "doc-123",
|
|
181
|
+
"assembly_specification_id": "spec-456",
|
|
182
|
+
}
|
|
183
|
+
response = client.post("/workflows/extract-assemble", json=request_data)
|
|
184
|
+
|
|
185
|
+
# Assertions
|
|
186
|
+
assert response.status_code == 500
|
|
187
|
+
data = response.json()
|
|
188
|
+
assert "Failed to start workflow" in data["detail"]
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
class TestGetWorkflowStatus:
|
|
192
|
+
"""Test cases for the get workflow status endpoint."""
|
|
193
|
+
|
|
194
|
+
def test_get_workflow_status_success(
|
|
195
|
+
self,
|
|
196
|
+
client: TestClient,
|
|
197
|
+
mock_temporal_client: MagicMock,
|
|
198
|
+
) -> None:
|
|
199
|
+
"""Test successful workflow status retrieval."""
|
|
200
|
+
# Setup mocks
|
|
201
|
+
mock_handle = MagicMock()
|
|
202
|
+
mock_description = MagicMock()
|
|
203
|
+
mock_description.run_id = "test-run-123"
|
|
204
|
+
mock_description.status.name = "RUNNING"
|
|
205
|
+
|
|
206
|
+
mock_handle.describe = AsyncMock(return_value=mock_description)
|
|
207
|
+
mock_handle.query = AsyncMock(
|
|
208
|
+
side_effect=[
|
|
209
|
+
"extracting_data", # current_step
|
|
210
|
+
"assembly-789", # assembly_id
|
|
211
|
+
]
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
mock_temporal_client.get_workflow_handle.return_value = mock_handle
|
|
215
|
+
|
|
216
|
+
# Make request
|
|
217
|
+
response = client.get("/workflows/test-workflow-id/status")
|
|
218
|
+
|
|
219
|
+
# Assertions
|
|
220
|
+
assert response.status_code == 200
|
|
221
|
+
data = response.json()
|
|
222
|
+
|
|
223
|
+
assert data["workflow_id"] == "test-workflow-id"
|
|
224
|
+
assert data["run_id"] == "test-run-123"
|
|
225
|
+
assert data["status"] == "RUNNING"
|
|
226
|
+
assert data["current_step"] == "extracting_data"
|
|
227
|
+
assert data["assembly_id"] == "assembly-789"
|
|
228
|
+
|
|
229
|
+
# Verify temporal client calls
|
|
230
|
+
mock_temporal_client.get_workflow_handle.assert_called_once_with(
|
|
231
|
+
"test-workflow-id"
|
|
232
|
+
)
|
|
233
|
+
mock_handle.describe.assert_called_once()
|
|
234
|
+
assert mock_handle.query.call_count == 2
|
|
235
|
+
|
|
236
|
+
def test_get_workflow_status_completed(
|
|
237
|
+
self,
|
|
238
|
+
client: TestClient,
|
|
239
|
+
mock_temporal_client: MagicMock,
|
|
240
|
+
) -> None:
|
|
241
|
+
"""Test workflow status for completed workflow."""
|
|
242
|
+
# Setup mocks
|
|
243
|
+
mock_handle = MagicMock()
|
|
244
|
+
mock_description = MagicMock()
|
|
245
|
+
mock_description.run_id = "completed-run-456"
|
|
246
|
+
mock_description.status.name = "COMPLETED"
|
|
247
|
+
|
|
248
|
+
mock_handle.describe = AsyncMock(return_value=mock_description)
|
|
249
|
+
mock_handle.query = AsyncMock(
|
|
250
|
+
side_effect=[
|
|
251
|
+
"completed", # current_step
|
|
252
|
+
"final-assembly", # assembly_id
|
|
253
|
+
]
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
mock_temporal_client.get_workflow_handle.return_value = mock_handle
|
|
257
|
+
|
|
258
|
+
# Make request
|
|
259
|
+
response = client.get("/workflows/completed-workflow/status")
|
|
260
|
+
|
|
261
|
+
# Assertions
|
|
262
|
+
assert response.status_code == 200
|
|
263
|
+
data = response.json()
|
|
264
|
+
|
|
265
|
+
assert data["workflow_id"] == "completed-workflow"
|
|
266
|
+
assert data["status"] == "COMPLETED"
|
|
267
|
+
assert data["current_step"] == "completed"
|
|
268
|
+
assert data["assembly_id"] == "final-assembly"
|
|
269
|
+
|
|
270
|
+
def test_get_workflow_status_query_failure(
|
|
271
|
+
self,
|
|
272
|
+
client: TestClient,
|
|
273
|
+
mock_temporal_client: MagicMock,
|
|
274
|
+
) -> None:
|
|
275
|
+
"""Test workflow status when queries fail (returns basic status)."""
|
|
276
|
+
# Setup mocks
|
|
277
|
+
mock_handle = MagicMock()
|
|
278
|
+
mock_description = MagicMock()
|
|
279
|
+
mock_description.run_id = "no-query-run"
|
|
280
|
+
mock_description.status.name = "RUNNING"
|
|
281
|
+
|
|
282
|
+
mock_handle.describe = AsyncMock(return_value=mock_description)
|
|
283
|
+
mock_handle.query = AsyncMock(side_effect=Exception("Query not supported"))
|
|
284
|
+
|
|
285
|
+
mock_temporal_client.get_workflow_handle.return_value = mock_handle
|
|
286
|
+
|
|
287
|
+
# Make request
|
|
288
|
+
response = client.get("/workflows/no-query-workflow/status")
|
|
289
|
+
|
|
290
|
+
# Assertions
|
|
291
|
+
assert response.status_code == 200
|
|
292
|
+
data = response.json()
|
|
293
|
+
|
|
294
|
+
assert data["workflow_id"] == "no-query-workflow"
|
|
295
|
+
assert data["status"] == "RUNNING"
|
|
296
|
+
assert data["current_step"] is None # Query failed gracefully
|
|
297
|
+
assert data["assembly_id"] is None # Query failed gracefully
|
|
298
|
+
|
|
299
|
+
def test_get_workflow_status_not_found(
|
|
300
|
+
self,
|
|
301
|
+
client: TestClient,
|
|
302
|
+
mock_temporal_client: MagicMock,
|
|
303
|
+
) -> None:
|
|
304
|
+
"""Test workflow status for non-existent workflow."""
|
|
305
|
+
# Setup mock to raise a generic Exception (workflow not found)
|
|
306
|
+
mock_temporal_client.get_workflow_handle.side_effect = Exception(
|
|
307
|
+
"Workflow not found"
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
# Make request
|
|
311
|
+
response = client.get("/workflows/non-existent-workflow/status")
|
|
312
|
+
|
|
313
|
+
# Assertions
|
|
314
|
+
assert response.status_code == 404
|
|
315
|
+
data = response.json()
|
|
316
|
+
assert "not found" in data["detail"].lower()
|
|
317
|
+
|
|
318
|
+
def test_get_workflow_status_temporal_error(
|
|
319
|
+
self,
|
|
320
|
+
client: TestClient,
|
|
321
|
+
mock_temporal_client: MagicMock,
|
|
322
|
+
) -> None:
|
|
323
|
+
"""Test workflow status when Temporal client raises exception."""
|
|
324
|
+
# Setup mock to raise exception
|
|
325
|
+
mock_temporal_client.get_workflow_handle.side_effect = Exception(
|
|
326
|
+
"Temporal service unavailable"
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
# Make request
|
|
330
|
+
response = client.get("/workflows/error-workflow/status")
|
|
331
|
+
|
|
332
|
+
# Assertions
|
|
333
|
+
assert response.status_code == 500
|
|
334
|
+
data = response.json()
|
|
335
|
+
assert "Failed to retrieve workflow handle" in data["detail"]
|
|
336
|
+
|
|
337
|
+
def test_get_workflow_status_describe_error(
|
|
338
|
+
self,
|
|
339
|
+
client: TestClient,
|
|
340
|
+
mock_temporal_client: MagicMock,
|
|
341
|
+
) -> None:
|
|
342
|
+
"""Test workflow status when describe fails."""
|
|
343
|
+
# Setup mocks
|
|
344
|
+
mock_handle = MagicMock()
|
|
345
|
+
mock_handle.describe = AsyncMock(side_effect=Exception("Describe failed"))
|
|
346
|
+
mock_temporal_client.get_workflow_handle.return_value = mock_handle
|
|
347
|
+
|
|
348
|
+
# Make request
|
|
349
|
+
response = client.get("/workflows/describe-error-workflow/status")
|
|
350
|
+
|
|
351
|
+
# Assertions
|
|
352
|
+
assert response.status_code == 500
|
|
353
|
+
data = response.json()
|
|
354
|
+
assert "Failed to retrieve workflow description" in data["detail"]
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
class TestWorkflowValidation:
|
|
358
|
+
"""Test cases for workflow request validation."""
|
|
359
|
+
|
|
360
|
+
def test_start_workflow_invalid_json(self, client: TestClient) -> None:
|
|
361
|
+
"""Test workflow start with invalid JSON."""
|
|
362
|
+
response = client.post(
|
|
363
|
+
"/workflows/extract-assemble",
|
|
364
|
+
content="invalid json",
|
|
365
|
+
headers={"Content-Type": "application/json"},
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
assert response.status_code == 422
|
|
369
|
+
|
|
370
|
+
def test_start_workflow_extra_fields_ignored(
|
|
371
|
+
self,
|
|
372
|
+
client: TestClient,
|
|
373
|
+
mock_temporal_client: MagicMock,
|
|
374
|
+
) -> None:
|
|
375
|
+
"""Test that extra fields in request are ignored."""
|
|
376
|
+
# Setup mock
|
|
377
|
+
mock_handle = MagicMock()
|
|
378
|
+
mock_handle.run_id = "extra-fields-run"
|
|
379
|
+
mock_temporal_client.start_workflow.return_value = mock_handle
|
|
380
|
+
|
|
381
|
+
# Make request with extra fields
|
|
382
|
+
request_data = {
|
|
383
|
+
"document_id": "doc-123",
|
|
384
|
+
"assembly_specification_id": "spec-456",
|
|
385
|
+
"extra_field": "should_be_ignored",
|
|
386
|
+
"another_extra": 42,
|
|
387
|
+
}
|
|
388
|
+
response = client.post("/workflows/extract-assemble", json=request_data)
|
|
389
|
+
|
|
390
|
+
# Should succeed and ignore extra fields
|
|
391
|
+
assert response.status_code == 200
|
|
392
|
+
data = response.json()
|
|
393
|
+
assert data["status"] == "RUNNING"
|