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.
Files changed (161) hide show
  1. julee/__init__.py +3 -0
  2. julee/api/__init__.py +20 -0
  3. julee/api/app.py +180 -0
  4. julee/api/dependencies.py +257 -0
  5. julee/api/requests.py +175 -0
  6. julee/api/responses.py +43 -0
  7. julee/api/routers/__init__.py +43 -0
  8. julee/api/routers/assembly_specifications.py +212 -0
  9. julee/api/routers/documents.py +182 -0
  10. julee/api/routers/knowledge_service_configs.py +79 -0
  11. julee/api/routers/knowledge_service_queries.py +293 -0
  12. julee/api/routers/system.py +137 -0
  13. julee/api/routers/workflows.py +234 -0
  14. julee/api/services/__init__.py +20 -0
  15. julee/api/services/system_initialization.py +214 -0
  16. julee/api/tests/__init__.py +14 -0
  17. julee/api/tests/routers/__init__.py +17 -0
  18. julee/api/tests/routers/test_assembly_specifications.py +749 -0
  19. julee/api/tests/routers/test_documents.py +301 -0
  20. julee/api/tests/routers/test_knowledge_service_configs.py +234 -0
  21. julee/api/tests/routers/test_knowledge_service_queries.py +738 -0
  22. julee/api/tests/routers/test_system.py +179 -0
  23. julee/api/tests/routers/test_workflows.py +393 -0
  24. julee/api/tests/test_app.py +285 -0
  25. julee/api/tests/test_dependencies.py +245 -0
  26. julee/api/tests/test_requests.py +250 -0
  27. julee/domain/__init__.py +22 -0
  28. julee/domain/models/__init__.py +49 -0
  29. julee/domain/models/assembly/__init__.py +17 -0
  30. julee/domain/models/assembly/assembly.py +103 -0
  31. julee/domain/models/assembly/tests/__init__.py +0 -0
  32. julee/domain/models/assembly/tests/factories.py +37 -0
  33. julee/domain/models/assembly/tests/test_assembly.py +430 -0
  34. julee/domain/models/assembly_specification/__init__.py +24 -0
  35. julee/domain/models/assembly_specification/assembly_specification.py +172 -0
  36. julee/domain/models/assembly_specification/knowledge_service_query.py +123 -0
  37. julee/domain/models/assembly_specification/tests/__init__.py +0 -0
  38. julee/domain/models/assembly_specification/tests/factories.py +78 -0
  39. julee/domain/models/assembly_specification/tests/test_assembly_specification.py +490 -0
  40. julee/domain/models/assembly_specification/tests/test_knowledge_service_query.py +310 -0
  41. julee/domain/models/custom_fields/__init__.py +0 -0
  42. julee/domain/models/custom_fields/content_stream.py +68 -0
  43. julee/domain/models/custom_fields/tests/__init__.py +0 -0
  44. julee/domain/models/custom_fields/tests/test_custom_fields.py +53 -0
  45. julee/domain/models/document/__init__.py +17 -0
  46. julee/domain/models/document/document.py +150 -0
  47. julee/domain/models/document/tests/__init__.py +0 -0
  48. julee/domain/models/document/tests/factories.py +76 -0
  49. julee/domain/models/document/tests/test_document.py +297 -0
  50. julee/domain/models/knowledge_service_config/__init__.py +17 -0
  51. julee/domain/models/knowledge_service_config/knowledge_service_config.py +86 -0
  52. julee/domain/models/policy/__init__.py +15 -0
  53. julee/domain/models/policy/document_policy_validation.py +220 -0
  54. julee/domain/models/policy/policy.py +203 -0
  55. julee/domain/models/policy/tests/__init__.py +0 -0
  56. julee/domain/models/policy/tests/factories.py +47 -0
  57. julee/domain/models/policy/tests/test_document_policy_validation.py +420 -0
  58. julee/domain/models/policy/tests/test_policy.py +546 -0
  59. julee/domain/repositories/__init__.py +27 -0
  60. julee/domain/repositories/assembly.py +45 -0
  61. julee/domain/repositories/assembly_specification.py +52 -0
  62. julee/domain/repositories/base.py +146 -0
  63. julee/domain/repositories/document.py +49 -0
  64. julee/domain/repositories/document_policy_validation.py +52 -0
  65. julee/domain/repositories/knowledge_service_config.py +54 -0
  66. julee/domain/repositories/knowledge_service_query.py +44 -0
  67. julee/domain/repositories/policy.py +49 -0
  68. julee/domain/use_cases/__init__.py +17 -0
  69. julee/domain/use_cases/decorators.py +107 -0
  70. julee/domain/use_cases/extract_assemble_data.py +649 -0
  71. julee/domain/use_cases/initialize_system_data.py +842 -0
  72. julee/domain/use_cases/tests/__init__.py +7 -0
  73. julee/domain/use_cases/tests/test_extract_assemble_data.py +548 -0
  74. julee/domain/use_cases/tests/test_initialize_system_data.py +455 -0
  75. julee/domain/use_cases/tests/test_validate_document.py +1228 -0
  76. julee/domain/use_cases/validate_document.py +736 -0
  77. julee/fixtures/assembly_specifications.yaml +70 -0
  78. julee/fixtures/documents.yaml +178 -0
  79. julee/fixtures/knowledge_service_configs.yaml +37 -0
  80. julee/fixtures/knowledge_service_queries.yaml +27 -0
  81. julee/repositories/__init__.py +17 -0
  82. julee/repositories/memory/__init__.py +31 -0
  83. julee/repositories/memory/assembly.py +84 -0
  84. julee/repositories/memory/assembly_specification.py +125 -0
  85. julee/repositories/memory/base.py +227 -0
  86. julee/repositories/memory/document.py +149 -0
  87. julee/repositories/memory/document_policy_validation.py +104 -0
  88. julee/repositories/memory/knowledge_service_config.py +123 -0
  89. julee/repositories/memory/knowledge_service_query.py +120 -0
  90. julee/repositories/memory/policy.py +87 -0
  91. julee/repositories/memory/tests/__init__.py +0 -0
  92. julee/repositories/memory/tests/test_document.py +212 -0
  93. julee/repositories/memory/tests/test_document_policy_validation.py +161 -0
  94. julee/repositories/memory/tests/test_policy.py +443 -0
  95. julee/repositories/minio/__init__.py +31 -0
  96. julee/repositories/minio/assembly.py +103 -0
  97. julee/repositories/minio/assembly_specification.py +170 -0
  98. julee/repositories/minio/client.py +570 -0
  99. julee/repositories/minio/document.py +530 -0
  100. julee/repositories/minio/document_policy_validation.py +120 -0
  101. julee/repositories/minio/knowledge_service_config.py +187 -0
  102. julee/repositories/minio/knowledge_service_query.py +211 -0
  103. julee/repositories/minio/policy.py +106 -0
  104. julee/repositories/minio/tests/__init__.py +0 -0
  105. julee/repositories/minio/tests/fake_client.py +213 -0
  106. julee/repositories/minio/tests/test_assembly.py +374 -0
  107. julee/repositories/minio/tests/test_assembly_specification.py +391 -0
  108. julee/repositories/minio/tests/test_client_protocol.py +57 -0
  109. julee/repositories/minio/tests/test_document.py +591 -0
  110. julee/repositories/minio/tests/test_document_policy_validation.py +192 -0
  111. julee/repositories/minio/tests/test_knowledge_service_config.py +374 -0
  112. julee/repositories/minio/tests/test_knowledge_service_query.py +438 -0
  113. julee/repositories/minio/tests/test_policy.py +559 -0
  114. julee/repositories/temporal/__init__.py +38 -0
  115. julee/repositories/temporal/activities.py +114 -0
  116. julee/repositories/temporal/activity_names.py +34 -0
  117. julee/repositories/temporal/proxies.py +159 -0
  118. julee/services/__init__.py +18 -0
  119. julee/services/knowledge_service/__init__.py +48 -0
  120. julee/services/knowledge_service/anthropic/__init__.py +12 -0
  121. julee/services/knowledge_service/anthropic/knowledge_service.py +331 -0
  122. julee/services/knowledge_service/anthropic/tests/test_knowledge_service.py +318 -0
  123. julee/services/knowledge_service/factory.py +138 -0
  124. julee/services/knowledge_service/knowledge_service.py +160 -0
  125. julee/services/knowledge_service/memory/__init__.py +13 -0
  126. julee/services/knowledge_service/memory/knowledge_service.py +278 -0
  127. julee/services/knowledge_service/memory/test_knowledge_service.py +345 -0
  128. julee/services/knowledge_service/test_factory.py +112 -0
  129. julee/services/temporal/__init__.py +38 -0
  130. julee/services/temporal/activities.py +86 -0
  131. julee/services/temporal/activity_names.py +22 -0
  132. julee/services/temporal/proxies.py +41 -0
  133. julee/util/__init__.py +0 -0
  134. julee/util/domain.py +119 -0
  135. julee/util/repos/__init__.py +0 -0
  136. julee/util/repos/minio/__init__.py +0 -0
  137. julee/util/repos/minio/file_storage.py +213 -0
  138. julee/util/repos/temporal/__init__.py +11 -0
  139. julee/util/repos/temporal/client_proxies/file_storage.py +68 -0
  140. julee/util/repos/temporal/data_converter.py +123 -0
  141. julee/util/repos/temporal/minio_file_storage.py +12 -0
  142. julee/util/repos/temporal/proxies/__init__.py +0 -0
  143. julee/util/repos/temporal/proxies/file_storage.py +58 -0
  144. julee/util/repositories.py +55 -0
  145. julee/util/temporal/__init__.py +22 -0
  146. julee/util/temporal/activities.py +123 -0
  147. julee/util/temporal/decorators.py +473 -0
  148. julee/util/tests/__init__.py +1 -0
  149. julee/util/tests/test_decorators.py +770 -0
  150. julee/util/validation/__init__.py +29 -0
  151. julee/util/validation/repository.py +100 -0
  152. julee/util/validation/type_guards.py +369 -0
  153. julee/worker.py +211 -0
  154. julee/workflows/__init__.py +26 -0
  155. julee/workflows/extract_assemble.py +215 -0
  156. julee/workflows/validate_document.py +228 -0
  157. julee-0.1.0.dist-info/METADATA +195 -0
  158. julee-0.1.0.dist-info/RECORD +161 -0
  159. julee-0.1.0.dist-info/WHEEL +5 -0
  160. julee-0.1.0.dist-info/licenses/LICENSE +674 -0
  161. 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"