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,749 @@
1
+ """
2
+ Tests for the assembly specifications API router.
3
+
4
+ This module provides comprehensive tests for the assembly specifications
5
+ endpoints, focusing on testing the router behavior with proper dependency
6
+ injection and mocking patterns.
7
+ """
8
+
9
+ import pytest
10
+ from typing import Generator
11
+ from fastapi.testclient import TestClient
12
+ from fastapi import FastAPI
13
+ from fastapi_pagination import add_pagination
14
+
15
+ from julee.api.routers.assembly_specifications import router
16
+ from julee.api.dependencies import (
17
+ get_assembly_specification_repository,
18
+ )
19
+ from julee.domain.models import (
20
+ AssemblySpecification,
21
+ AssemblySpecificationStatus,
22
+ )
23
+ from julee.repositories.memory import (
24
+ MemoryAssemblySpecificationRepository,
25
+ )
26
+
27
+
28
+ @pytest.fixture
29
+ def memory_repo() -> MemoryAssemblySpecificationRepository:
30
+ """Create a memory assembly specification repository for testing."""
31
+ return MemoryAssemblySpecificationRepository()
32
+
33
+
34
+ @pytest.fixture
35
+ def app_with_router(
36
+ memory_repo: MemoryAssemblySpecificationRepository,
37
+ ) -> FastAPI:
38
+ """Create a FastAPI app with just the assembly specifications router."""
39
+ app = FastAPI()
40
+
41
+ # Override the dependency with our memory repository
42
+ app.dependency_overrides[get_assembly_specification_repository] = (
43
+ lambda: memory_repo
44
+ )
45
+
46
+ # Add pagination support (required for the paginate function)
47
+ add_pagination(app)
48
+
49
+ # Include the router with the prefix
50
+ app.include_router(
51
+ router,
52
+ prefix="/assembly_specifications",
53
+ tags=["Assembly Specifications"],
54
+ )
55
+
56
+ return app
57
+
58
+
59
+ @pytest.fixture
60
+ def client(
61
+ app_with_router: FastAPI,
62
+ ) -> Generator[TestClient, None, None]:
63
+ """Create a test client with the router app."""
64
+ with TestClient(app_with_router) as test_client:
65
+ yield test_client
66
+
67
+
68
+ @pytest.fixture
69
+ def sample_assembly_specification() -> AssemblySpecification:
70
+ """Create a sample assembly specification for testing."""
71
+ return AssemblySpecification(
72
+ assembly_specification_id="test-spec-123",
73
+ name="Meeting Minutes",
74
+ applicability="Online video meeting transcripts",
75
+ jsonschema={
76
+ "type": "object",
77
+ "properties": {
78
+ "attendees": {"type": "array", "items": {"type": "string"}},
79
+ "summary": {"type": "string"},
80
+ },
81
+ },
82
+ knowledge_service_queries={
83
+ "/properties/attendees": "query-123",
84
+ "/properties/summary": "query-456",
85
+ },
86
+ status=AssemblySpecificationStatus.ACTIVE,
87
+ version="1.0.0",
88
+ )
89
+
90
+
91
+ class TestGetAssemblySpecifications:
92
+ """Test the GET / endpoint for assembly specifications."""
93
+
94
+ def test_get_assembly_specifications_empty_list(
95
+ self,
96
+ client: TestClient,
97
+ memory_repo: MemoryAssemblySpecificationRepository,
98
+ ) -> None:
99
+ """Test getting specifications when repository is empty."""
100
+ response = client.get("/assembly_specifications/")
101
+
102
+ assert response.status_code == 200
103
+ data = response.json()
104
+
105
+ # Verify pagination structure
106
+ assert "items" in data
107
+ assert "total" in data
108
+ assert "page" in data
109
+ assert "size" in data
110
+ assert "pages" in data
111
+
112
+ # Should return empty list when repository is empty
113
+ assert data["items"] == []
114
+ assert data["total"] == 0
115
+
116
+ def test_get_assembly_specifications_with_pagination_params(
117
+ self,
118
+ client: TestClient,
119
+ memory_repo: MemoryAssemblySpecificationRepository,
120
+ ) -> None:
121
+ """Test getting specifications with pagination parameters."""
122
+ response = client.get("/assembly_specifications/?page=2&size=10")
123
+
124
+ assert response.status_code == 200
125
+ data = response.json()
126
+
127
+ # Verify pagination parameters are handled
128
+ assert "items" in data
129
+ assert "page" in data
130
+ assert "size" in data
131
+
132
+ # Even with pagination params, should work with empty repository
133
+ assert data["items"] == []
134
+
135
+ async def test_get_assembly_specifications_with_data(
136
+ self,
137
+ client: TestClient,
138
+ memory_repo: MemoryAssemblySpecificationRepository,
139
+ sample_assembly_specification: AssemblySpecification,
140
+ ) -> None:
141
+ """Test getting specifications when repository contains data."""
142
+ # Create a second specification for testing
143
+ spec2 = AssemblySpecification(
144
+ assembly_specification_id="test-spec-456",
145
+ name="Project Report",
146
+ applicability="Project documentation and status updates",
147
+ jsonschema={
148
+ "type": "object",
149
+ "properties": {
150
+ "project_name": {"type": "string"},
151
+ "status": {"type": "string"},
152
+ },
153
+ },
154
+ knowledge_service_queries={
155
+ "/properties/project_name": "query-789",
156
+ "/properties/status": "query-101",
157
+ },
158
+ )
159
+
160
+ # Save specifications to the repository
161
+ await memory_repo.save(sample_assembly_specification)
162
+ await memory_repo.save(spec2)
163
+
164
+ response = client.get("/assembly_specifications/")
165
+
166
+ assert response.status_code == 200
167
+ data = response.json()
168
+
169
+ # Verify pagination structure
170
+ assert "items" in data
171
+ assert "total" in data
172
+ assert "page" in data
173
+ assert "size" in data
174
+
175
+ # Should return both specifications
176
+ assert data["total"] == 2
177
+ assert len(data["items"]) == 2
178
+
179
+ # Verify the specifications are returned (order may vary)
180
+ returned_ids = {item["assembly_specification_id"] for item in data["items"]}
181
+ expected_ids = {
182
+ sample_assembly_specification.assembly_specification_id,
183
+ spec2.assembly_specification_id,
184
+ }
185
+ assert returned_ids == expected_ids
186
+
187
+ # Verify specification data structure
188
+ for item in data["items"]:
189
+ assert "assembly_specification_id" in item
190
+ assert "name" in item
191
+ assert "applicability" in item
192
+ assert "jsonschema" in item
193
+ assert "status" in item
194
+
195
+ async def test_get_assembly_specifications_pagination(
196
+ self,
197
+ client: TestClient,
198
+ memory_repo: MemoryAssemblySpecificationRepository,
199
+ ) -> None:
200
+ """Test pagination with multiple specifications."""
201
+ # Create several specifications
202
+ specifications = []
203
+ for i in range(5):
204
+ spec = AssemblySpecification(
205
+ assembly_specification_id=f"spec-{i:03d}",
206
+ name=f"Specification {i}",
207
+ applicability=f"Test applicability {i}",
208
+ jsonschema={"type": "object", "properties": {}},
209
+ )
210
+ specifications.append(spec)
211
+ await memory_repo.save(spec)
212
+
213
+ # Test first page with size 2
214
+ response = client.get("/assembly_specifications/?page=1&size=2")
215
+ assert response.status_code == 200
216
+ data = response.json()
217
+
218
+ assert data["total"] == 5
219
+ assert data["page"] == 1
220
+ assert data["size"] == 2
221
+ assert len(data["items"]) == 2
222
+
223
+ # Test second page
224
+ response = client.get("/assembly_specifications/?page=2&size=2")
225
+ assert response.status_code == 200
226
+ data = response.json()
227
+
228
+ assert data["total"] == 5
229
+ assert data["page"] == 2
230
+ assert data["size"] == 2
231
+ assert len(data["items"]) == 2
232
+
233
+
234
+ class TestGetAssemblySpecification:
235
+ """Test the GET /{id} endpoint for getting a specific specification."""
236
+
237
+ async def test_get_assembly_specification_success(
238
+ self,
239
+ client: TestClient,
240
+ memory_repo: MemoryAssemblySpecificationRepository,
241
+ sample_assembly_specification: AssemblySpecification,
242
+ ) -> None:
243
+ """Test successfully getting a specific assembly specification."""
244
+ # Save the specification to the repository
245
+ await memory_repo.save(sample_assembly_specification)
246
+
247
+ response = client.get(
248
+ f"/assembly_specifications/{sample_assembly_specification.assembly_specification_id}"
249
+ )
250
+
251
+ assert response.status_code == 200
252
+ data = response.json()
253
+
254
+ # Verify response structure and content
255
+ assert (
256
+ data["assembly_specification_id"]
257
+ == sample_assembly_specification.assembly_specification_id
258
+ )
259
+ assert data["name"] == sample_assembly_specification.name
260
+ assert data["applicability"] == sample_assembly_specification.applicability
261
+ assert data["jsonschema"] == sample_assembly_specification.jsonschema
262
+ assert (
263
+ data["knowledge_service_queries"]
264
+ == sample_assembly_specification.knowledge_service_queries
265
+ )
266
+ assert data["status"] == sample_assembly_specification.status.value
267
+ assert data["version"] == sample_assembly_specification.version
268
+ assert "created_at" in data
269
+ assert "updated_at" in data
270
+
271
+ def test_get_assembly_specification_not_found(
272
+ self,
273
+ client: TestClient,
274
+ memory_repo: MemoryAssemblySpecificationRepository,
275
+ ) -> None:
276
+ """Test getting a non-existent assembly specification."""
277
+ nonexistent_id = "nonexistent-spec-123"
278
+ response = client.get(f"/assembly_specifications/{nonexistent_id}")
279
+
280
+ assert response.status_code == 404
281
+ data = response.json()
282
+ assert "not found" in data["detail"].lower()
283
+ assert nonexistent_id in data["detail"]
284
+
285
+ async def test_get_assembly_specification_with_complex_schema(
286
+ self,
287
+ client: TestClient,
288
+ memory_repo: MemoryAssemblySpecificationRepository,
289
+ ) -> None:
290
+ """Test getting specification with complex JSON schema."""
291
+ complex_spec = AssemblySpecification(
292
+ assembly_specification_id="complex-spec-123",
293
+ name="Complex Meeting Minutes",
294
+ applicability="Detailed meeting transcripts with metadata",
295
+ jsonschema={
296
+ "type": "object",
297
+ "properties": {
298
+ "metadata": {
299
+ "type": "object",
300
+ "properties": {
301
+ "date": {"type": "string", "format": "date"},
302
+ "duration": {"type": "integer"},
303
+ },
304
+ },
305
+ "attendees": {
306
+ "type": "array",
307
+ "items": {
308
+ "type": "object",
309
+ "properties": {
310
+ "name": {"type": "string"},
311
+ "role": {"type": "string"},
312
+ },
313
+ },
314
+ },
315
+ "agenda": {
316
+ "type": "array",
317
+ "items": {"type": "string"},
318
+ },
319
+ },
320
+ "required": ["metadata", "attendees"],
321
+ },
322
+ knowledge_service_queries={
323
+ "/properties/metadata/properties/date": "date-query",
324
+ "/properties/attendees": "attendees-query",
325
+ "/properties/agenda": "agenda-query",
326
+ },
327
+ )
328
+
329
+ await memory_repo.save(complex_spec)
330
+
331
+ response = client.get(
332
+ f"/assembly_specifications/{complex_spec.assembly_specification_id}"
333
+ )
334
+
335
+ assert response.status_code == 200
336
+ data = response.json()
337
+
338
+ # Verify complex schema is preserved
339
+ assert data["jsonschema"]["properties"]["metadata"]["properties"]
340
+ assert data["jsonschema"]["required"] == ["metadata", "attendees"]
341
+ assert len(data["knowledge_service_queries"]) == 3
342
+
343
+ def test_get_assembly_specification_invalid_id_format(
344
+ self,
345
+ client: TestClient,
346
+ memory_repo: MemoryAssemblySpecificationRepository,
347
+ ) -> None:
348
+ """Test getting specification with various ID formats."""
349
+ # Test with empty string (should be handled by FastAPI routing)
350
+ response = client.get("/assembly_specifications/")
351
+ assert response.status_code == 200 # This hits the list endpoint
352
+
353
+ # Test with special characters
354
+ response = client.get("/assembly_specifications/test@spec#123")
355
+ assert response.status_code == 404 # Not found is expected
356
+
357
+ async def test_get_assembly_specification_different_statuses(
358
+ self,
359
+ client: TestClient,
360
+ memory_repo: MemoryAssemblySpecificationRepository,
361
+ ) -> None:
362
+ """Test getting specifications with different status values."""
363
+ for status in AssemblySpecificationStatus:
364
+ spec = AssemblySpecification(
365
+ assembly_specification_id=f"spec-{status.value}",
366
+ name=f"Spec {status.value}",
367
+ applicability="Test applicability",
368
+ jsonschema={"type": "object", "properties": {}},
369
+ status=status,
370
+ )
371
+ await memory_repo.save(spec)
372
+
373
+ response = client.get(
374
+ f"/assembly_specifications/{spec.assembly_specification_id}"
375
+ )
376
+ assert response.status_code == 200
377
+ data = response.json()
378
+ assert data["status"] == status.value
379
+
380
+
381
+ class TestCreateAssemblySpecification:
382
+ """Test the POST / endpoint for creating assembly specifications."""
383
+
384
+ def test_create_assembly_specification_success(
385
+ self,
386
+ client: TestClient,
387
+ memory_repo: MemoryAssemblySpecificationRepository,
388
+ ) -> None:
389
+ """Test successful creation of an assembly specification."""
390
+ request_data = {
391
+ "name": "Meeting Minutes Template",
392
+ "applicability": "Online video meeting transcripts",
393
+ "jsonschema": {
394
+ "type": "object",
395
+ "properties": {
396
+ "attendees": {
397
+ "type": "array",
398
+ "items": {"type": "string"},
399
+ },
400
+ "summary": {"type": "string"},
401
+ "action_items": {
402
+ "type": "array",
403
+ "items": {"type": "string"},
404
+ },
405
+ },
406
+ },
407
+ "knowledge_service_queries": {
408
+ "/properties/attendees": "attendee-extractor-query",
409
+ "/properties/summary": "summary-extractor-query",
410
+ },
411
+ "version": "1.0.0",
412
+ }
413
+
414
+ response = client.post("/assembly_specifications/", json=request_data)
415
+
416
+ assert response.status_code == 200
417
+ data = response.json()
418
+
419
+ # Verify response structure
420
+ assert "assembly_specification_id" in data
421
+ assert data["name"] == request_data["name"]
422
+ assert data["applicability"] == request_data["applicability"]
423
+ assert data["jsonschema"] == request_data["jsonschema"]
424
+ assert (
425
+ data["knowledge_service_queries"]
426
+ == request_data["knowledge_service_queries"]
427
+ )
428
+ assert data["version"] == request_data["version"]
429
+ assert data["status"] == "draft" # Default status
430
+ assert "created_at" in data
431
+ assert "updated_at" in data
432
+
433
+ # Verify the specification was saved to repository
434
+ spec_id = data["assembly_specification_id"]
435
+ assert spec_id is not None
436
+ assert spec_id != ""
437
+
438
+ async def test_create_assembly_specification_persisted(
439
+ self,
440
+ client: TestClient,
441
+ memory_repo: MemoryAssemblySpecificationRepository,
442
+ ) -> None:
443
+ """Test that created specification is persisted in repository."""
444
+ request_data = {
445
+ "name": "Project Status Report",
446
+ "applicability": "Weekly project status documents",
447
+ "jsonschema": {
448
+ "type": "object",
449
+ "properties": {
450
+ "project_name": {"type": "string"},
451
+ "status": {"type": "string"},
452
+ "milestones": {
453
+ "type": "array",
454
+ "items": {"type": "string"},
455
+ },
456
+ },
457
+ },
458
+ "knowledge_service_queries": {
459
+ "/properties/project_name": "project-name-query",
460
+ "/properties/status": "status-query",
461
+ },
462
+ }
463
+
464
+ response = client.post("/assembly_specifications/", json=request_data)
465
+ assert response.status_code == 200
466
+
467
+ spec_id = response.json()["assembly_specification_id"]
468
+
469
+ # Verify specification was saved by retrieving it
470
+ saved_spec = await memory_repo.get(spec_id)
471
+ assert saved_spec is not None
472
+ assert saved_spec.name == request_data["name"]
473
+ assert saved_spec.applicability == request_data["applicability"]
474
+ assert saved_spec.jsonschema == request_data["jsonschema"]
475
+ assert (
476
+ saved_spec.knowledge_service_queries
477
+ == request_data["knowledge_service_queries"]
478
+ )
479
+
480
+ def test_create_assembly_specification_minimal_fields(
481
+ self,
482
+ client: TestClient,
483
+ memory_repo: MemoryAssemblySpecificationRepository,
484
+ ) -> None:
485
+ """Test creation with only required fields."""
486
+ request_data = {
487
+ "name": "Minimal Spec",
488
+ "applicability": "Test applicability",
489
+ "jsonschema": {"type": "object", "properties": {}},
490
+ }
491
+
492
+ response = client.post("/assembly_specifications/", json=request_data)
493
+
494
+ assert response.status_code == 200
495
+ data = response.json()
496
+
497
+ assert data["name"] == request_data["name"]
498
+ assert data["applicability"] == request_data["applicability"]
499
+ assert data["jsonschema"] == request_data["jsonschema"]
500
+ assert data["knowledge_service_queries"] == {}
501
+ assert data["version"] == "0.1.0" # Default version
502
+ assert data["status"] == "draft" # Default status
503
+
504
+ def test_create_assembly_specification_validation_errors(
505
+ self,
506
+ client: TestClient,
507
+ memory_repo: MemoryAssemblySpecificationRepository,
508
+ ) -> None:
509
+ """Test validation error handling."""
510
+ # Test empty name
511
+ request_data = {
512
+ "name": "",
513
+ "applicability": "Test applicability",
514
+ "jsonschema": {"type": "object", "properties": {}},
515
+ }
516
+
517
+ response = client.post("/assembly_specifications/", json=request_data)
518
+ assert response.status_code == 422
519
+
520
+ # Test empty applicability
521
+ request_data = {
522
+ "name": "Test Spec",
523
+ "applicability": "",
524
+ "jsonschema": {"type": "object", "properties": {}},
525
+ }
526
+
527
+ response = client.post("/assembly_specifications/", json=request_data)
528
+ assert response.status_code == 422
529
+
530
+ # Test invalid JSON schema
531
+ request_data = {
532
+ "name": "Test Spec",
533
+ "applicability": "Test applicability",
534
+ "jsonschema": {
535
+ "invalid": "schema"
536
+ }, # Invalid JSON schema: contains an unrecognized keyword
537
+ }
538
+
539
+ response = client.post("/assembly_specifications/", json=request_data)
540
+ assert response.status_code == 422
541
+
542
+ def test_create_assembly_specification_missing_required_fields(
543
+ self,
544
+ client: TestClient,
545
+ memory_repo: MemoryAssemblySpecificationRepository,
546
+ ) -> None:
547
+ """Test handling of missing required fields."""
548
+ # Missing name
549
+ request_data = {
550
+ "applicability": "Test applicability",
551
+ "jsonschema": {"type": "object", "properties": {}},
552
+ }
553
+
554
+ response = client.post("/assembly_specifications/", json=request_data)
555
+ assert response.status_code == 422
556
+
557
+ # Missing applicability
558
+ request_data = {
559
+ "name": "Test Spec",
560
+ "jsonschema": {"type": "object", "properties": {}},
561
+ }
562
+
563
+ response = client.post("/assembly_specifications/", json=request_data)
564
+ assert response.status_code == 422
565
+
566
+ # Missing jsonschema
567
+ request_data = {
568
+ "name": "Test Spec",
569
+ "applicability": "Test applicability",
570
+ }
571
+
572
+ response = client.post("/assembly_specifications/", json=request_data)
573
+ assert response.status_code == 422
574
+
575
+ def test_create_assembly_specification_invalid_json_pointers(
576
+ self,
577
+ client: TestClient,
578
+ memory_repo: MemoryAssemblySpecificationRepository,
579
+ ) -> None:
580
+ """Test validation of JSON pointer paths in knowledge queries."""
581
+ # Invalid JSON pointer (doesn't exist in schema)
582
+ request_data = {
583
+ "name": "Test Spec",
584
+ "applicability": "Test applicability",
585
+ "jsonschema": {
586
+ "type": "object",
587
+ "properties": {"summary": {"type": "string"}},
588
+ },
589
+ "knowledge_service_queries": {
590
+ "/properties/nonexistent": "some-query-id", # Path not found
591
+ },
592
+ }
593
+
594
+ response = client.post("/assembly_specifications/", json=request_data)
595
+ assert response.status_code == 422
596
+
597
+ # Invalid JSON pointer format
598
+ request_data = {
599
+ "name": "Test Spec",
600
+ "applicability": "Test applicability",
601
+ "jsonschema": {
602
+ "type": "object",
603
+ "properties": {"summary": {"type": "string"}},
604
+ },
605
+ "knowledge_service_queries": {
606
+ "invalid-pointer": "some-query-id", # Invalid format
607
+ },
608
+ }
609
+
610
+ response = client.post("/assembly_specifications/", json=request_data)
611
+ assert response.status_code == 422
612
+
613
+ def test_create_assembly_specification_complex_schema(
614
+ self,
615
+ client: TestClient,
616
+ memory_repo: MemoryAssemblySpecificationRepository,
617
+ ) -> None:
618
+ """Test creation with complex JSON schema and query mappings."""
619
+ request_data = {
620
+ "name": "Complex Meeting Minutes",
621
+ "applicability": "Detailed enterprise meeting transcripts",
622
+ "jsonschema": {
623
+ "type": "object",
624
+ "properties": {
625
+ "metadata": {
626
+ "type": "object",
627
+ "properties": {
628
+ "meeting_date": {
629
+ "type": "string",
630
+ "format": "date",
631
+ },
632
+ "duration_minutes": {"type": "integer"},
633
+ "location": {"type": "string"},
634
+ },
635
+ "required": ["meeting_date"],
636
+ },
637
+ "attendees": {
638
+ "type": "array",
639
+ "items": {
640
+ "type": "object",
641
+ "properties": {
642
+ "name": {"type": "string"},
643
+ "role": {"type": "string"},
644
+ "department": {"type": "string"},
645
+ },
646
+ "required": ["name"],
647
+ },
648
+ },
649
+ "agenda_items": {
650
+ "type": "array",
651
+ "items": {"type": "string"},
652
+ },
653
+ "decisions": {
654
+ "type": "array",
655
+ "items": {
656
+ "type": "object",
657
+ "properties": {
658
+ "description": {"type": "string"},
659
+ "owner": {"type": "string"},
660
+ "due_date": {
661
+ "type": "string",
662
+ "format": "date",
663
+ },
664
+ },
665
+ },
666
+ },
667
+ },
668
+ "required": ["metadata", "attendees"],
669
+ },
670
+ "knowledge_service_queries": {
671
+ "/properties/metadata/properties/meeting_date": ("date-extractor"),
672
+ "/properties/metadata/properties/location": ("location-extractor"),
673
+ "/properties/attendees": "attendee-extractor",
674
+ "/properties/agenda_items": "agenda-extractor",
675
+ "/properties/decisions": "decision-extractor",
676
+ },
677
+ "version": "2.1.0",
678
+ }
679
+
680
+ response = client.post("/assembly_specifications/", json=request_data)
681
+
682
+ assert response.status_code == 200
683
+ data = response.json()
684
+
685
+ # Verify complex schema is preserved
686
+ assert data["jsonschema"]["properties"]["metadata"]["properties"]
687
+ assert data["jsonschema"]["required"] == ["metadata", "attendees"]
688
+ assert len(data["knowledge_service_queries"]) == 5
689
+ assert data["version"] == "2.1.0"
690
+
691
+ def test_post_and_get_integration(
692
+ self,
693
+ client: TestClient,
694
+ memory_repo: MemoryAssemblySpecificationRepository,
695
+ ) -> None:
696
+ """Test that POST and GET endpoints work together."""
697
+ # Create a specification via POST
698
+ request_data = {
699
+ "name": "Integration Test Specification",
700
+ "applicability": "Test integration between endpoints",
701
+ "jsonschema": {
702
+ "type": "object",
703
+ "properties": {
704
+ "title": {"type": "string"},
705
+ "content": {"type": "string"},
706
+ },
707
+ },
708
+ "knowledge_service_queries": {
709
+ "/properties/title": "title-query",
710
+ "/properties/content": "content-query",
711
+ },
712
+ }
713
+
714
+ post_response = client.post("/assembly_specifications/", json=request_data)
715
+ assert post_response.status_code == 200
716
+ created_spec = post_response.json()
717
+
718
+ # Verify the specification appears in GET list response
719
+ list_response = client.get("/assembly_specifications/")
720
+ assert list_response.status_code == 200
721
+ list_data = list_response.json()
722
+
723
+ # Should find our created specification in the list
724
+ assert list_data["total"] == 1
725
+ assert len(list_data["items"]) == 1
726
+
727
+ returned_spec = list_data["items"][0]
728
+ assert (
729
+ returned_spec["assembly_specification_id"]
730
+ == created_spec["assembly_specification_id"]
731
+ )
732
+ assert returned_spec["name"] == request_data["name"]
733
+
734
+ # Verify the specification can be retrieved by ID
735
+ spec_id = created_spec["assembly_specification_id"]
736
+ get_response = client.get(f"/assembly_specifications/{spec_id}")
737
+ assert get_response.status_code == 200
738
+ retrieved_spec = get_response.json()
739
+
740
+ assert (
741
+ retrieved_spec["assembly_specification_id"]
742
+ == created_spec["assembly_specification_id"]
743
+ )
744
+ assert retrieved_spec["name"] == request_data["name"]
745
+ assert retrieved_spec["applicability"] == request_data["applicability"]
746
+ assert (
747
+ retrieved_spec["knowledge_service_queries"]
748
+ == request_data["knowledge_service_queries"]
749
+ )