julee 0.1.5__py3-none-any.whl → 0.1.6__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 (105) hide show
  1. julee/docs/sphinx_hcd/__init__.py +146 -13
  2. julee/docs/sphinx_hcd/domain/__init__.py +5 -0
  3. julee/docs/sphinx_hcd/domain/models/__init__.py +32 -0
  4. julee/docs/sphinx_hcd/domain/models/accelerator.py +152 -0
  5. julee/docs/sphinx_hcd/domain/models/app.py +151 -0
  6. julee/docs/sphinx_hcd/domain/models/code_info.py +121 -0
  7. julee/docs/sphinx_hcd/domain/models/epic.py +79 -0
  8. julee/docs/sphinx_hcd/domain/models/integration.py +230 -0
  9. julee/docs/sphinx_hcd/domain/models/journey.py +222 -0
  10. julee/docs/sphinx_hcd/domain/models/persona.py +106 -0
  11. julee/docs/sphinx_hcd/domain/models/story.py +128 -0
  12. julee/docs/sphinx_hcd/domain/repositories/__init__.py +25 -0
  13. julee/docs/sphinx_hcd/domain/repositories/accelerator.py +98 -0
  14. julee/docs/sphinx_hcd/domain/repositories/app.py +57 -0
  15. julee/docs/sphinx_hcd/domain/repositories/base.py +89 -0
  16. julee/docs/sphinx_hcd/domain/repositories/code_info.py +69 -0
  17. julee/docs/sphinx_hcd/domain/repositories/epic.py +62 -0
  18. julee/docs/sphinx_hcd/domain/repositories/integration.py +79 -0
  19. julee/docs/sphinx_hcd/domain/repositories/journey.py +106 -0
  20. julee/docs/sphinx_hcd/domain/repositories/story.py +68 -0
  21. julee/docs/sphinx_hcd/domain/use_cases/__init__.py +64 -0
  22. julee/docs/sphinx_hcd/domain/use_cases/derive_personas.py +166 -0
  23. julee/docs/sphinx_hcd/domain/use_cases/resolve_accelerator_references.py +236 -0
  24. julee/docs/sphinx_hcd/domain/use_cases/resolve_app_references.py +144 -0
  25. julee/docs/sphinx_hcd/domain/use_cases/resolve_story_references.py +121 -0
  26. julee/docs/sphinx_hcd/parsers/__init__.py +48 -0
  27. julee/docs/sphinx_hcd/parsers/ast.py +150 -0
  28. julee/docs/sphinx_hcd/parsers/gherkin.py +155 -0
  29. julee/docs/sphinx_hcd/parsers/yaml.py +184 -0
  30. julee/docs/sphinx_hcd/repositories/__init__.py +4 -0
  31. julee/docs/sphinx_hcd/repositories/memory/__init__.py +25 -0
  32. julee/docs/sphinx_hcd/repositories/memory/accelerator.py +86 -0
  33. julee/docs/sphinx_hcd/repositories/memory/app.py +45 -0
  34. julee/docs/sphinx_hcd/repositories/memory/base.py +106 -0
  35. julee/docs/sphinx_hcd/repositories/memory/code_info.py +59 -0
  36. julee/docs/sphinx_hcd/repositories/memory/epic.py +54 -0
  37. julee/docs/sphinx_hcd/repositories/memory/integration.py +70 -0
  38. julee/docs/sphinx_hcd/repositories/memory/journey.py +96 -0
  39. julee/docs/sphinx_hcd/repositories/memory/story.py +63 -0
  40. julee/docs/sphinx_hcd/sphinx/__init__.py +28 -0
  41. julee/docs/sphinx_hcd/sphinx/adapters.py +116 -0
  42. julee/docs/sphinx_hcd/sphinx/context.py +163 -0
  43. julee/docs/sphinx_hcd/sphinx/directives/__init__.py +160 -0
  44. julee/docs/sphinx_hcd/sphinx/directives/accelerator.py +576 -0
  45. julee/docs/sphinx_hcd/sphinx/directives/app.py +349 -0
  46. julee/docs/sphinx_hcd/sphinx/directives/base.py +211 -0
  47. julee/docs/sphinx_hcd/sphinx/directives/epic.py +434 -0
  48. julee/docs/sphinx_hcd/sphinx/directives/integration.py +220 -0
  49. julee/docs/sphinx_hcd/sphinx/directives/journey.py +642 -0
  50. julee/docs/sphinx_hcd/sphinx/directives/persona.py +345 -0
  51. julee/docs/sphinx_hcd/sphinx/directives/story.py +575 -0
  52. julee/docs/sphinx_hcd/sphinx/event_handlers/__init__.py +16 -0
  53. julee/docs/sphinx_hcd/sphinx/event_handlers/builder_inited.py +31 -0
  54. julee/docs/sphinx_hcd/sphinx/event_handlers/doctree_read.py +27 -0
  55. julee/docs/sphinx_hcd/sphinx/event_handlers/doctree_resolved.py +43 -0
  56. julee/docs/sphinx_hcd/sphinx/event_handlers/env_purge_doc.py +42 -0
  57. julee/docs/sphinx_hcd/sphinx/initialization.py +139 -0
  58. julee/docs/sphinx_hcd/tests/__init__.py +9 -0
  59. julee/docs/sphinx_hcd/tests/conftest.py +6 -0
  60. julee/docs/sphinx_hcd/tests/domain/__init__.py +1 -0
  61. julee/docs/sphinx_hcd/tests/domain/models/__init__.py +1 -0
  62. julee/docs/sphinx_hcd/tests/domain/models/test_accelerator.py +266 -0
  63. julee/docs/sphinx_hcd/tests/domain/models/test_app.py +258 -0
  64. julee/docs/sphinx_hcd/tests/domain/models/test_code_info.py +231 -0
  65. julee/docs/sphinx_hcd/tests/domain/models/test_epic.py +163 -0
  66. julee/docs/sphinx_hcd/tests/domain/models/test_integration.py +327 -0
  67. julee/docs/sphinx_hcd/tests/domain/models/test_journey.py +249 -0
  68. julee/docs/sphinx_hcd/tests/domain/models/test_persona.py +172 -0
  69. julee/docs/sphinx_hcd/tests/domain/models/test_story.py +216 -0
  70. julee/docs/sphinx_hcd/tests/domain/use_cases/__init__.py +1 -0
  71. julee/docs/sphinx_hcd/tests/domain/use_cases/test_derive_personas.py +314 -0
  72. julee/docs/sphinx_hcd/tests/domain/use_cases/test_resolve_accelerator_references.py +476 -0
  73. julee/docs/sphinx_hcd/tests/domain/use_cases/test_resolve_app_references.py +265 -0
  74. julee/docs/sphinx_hcd/tests/domain/use_cases/test_resolve_story_references.py +229 -0
  75. julee/docs/sphinx_hcd/tests/integration/__init__.py +1 -0
  76. julee/docs/sphinx_hcd/tests/parsers/__init__.py +1 -0
  77. julee/docs/sphinx_hcd/tests/parsers/test_ast.py +298 -0
  78. julee/docs/sphinx_hcd/tests/parsers/test_gherkin.py +282 -0
  79. julee/docs/sphinx_hcd/tests/parsers/test_yaml.py +496 -0
  80. julee/docs/sphinx_hcd/tests/repositories/__init__.py +1 -0
  81. julee/docs/sphinx_hcd/tests/repositories/test_accelerator.py +298 -0
  82. julee/docs/sphinx_hcd/tests/repositories/test_app.py +218 -0
  83. julee/docs/sphinx_hcd/tests/repositories/test_base.py +151 -0
  84. julee/docs/sphinx_hcd/tests/repositories/test_code_info.py +253 -0
  85. julee/docs/sphinx_hcd/tests/repositories/test_epic.py +237 -0
  86. julee/docs/sphinx_hcd/tests/repositories/test_integration.py +268 -0
  87. julee/docs/sphinx_hcd/tests/repositories/test_journey.py +294 -0
  88. julee/docs/sphinx_hcd/tests/repositories/test_story.py +236 -0
  89. julee/docs/sphinx_hcd/tests/sphinx/__init__.py +1 -0
  90. julee/docs/sphinx_hcd/tests/sphinx/directives/__init__.py +1 -0
  91. julee/docs/sphinx_hcd/tests/sphinx/directives/test_base.py +160 -0
  92. julee/docs/sphinx_hcd/tests/sphinx/test_adapters.py +176 -0
  93. julee/docs/sphinx_hcd/tests/sphinx/test_context.py +257 -0
  94. {julee-0.1.5.dist-info → julee-0.1.6.dist-info}/METADATA +2 -1
  95. {julee-0.1.5.dist-info → julee-0.1.6.dist-info}/RECORD +98 -13
  96. julee/docs/sphinx_hcd/accelerators.py +0 -1175
  97. julee/docs/sphinx_hcd/apps.py +0 -518
  98. julee/docs/sphinx_hcd/epics.py +0 -453
  99. julee/docs/sphinx_hcd/integrations.py +0 -310
  100. julee/docs/sphinx_hcd/journeys.py +0 -797
  101. julee/docs/sphinx_hcd/personas.py +0 -457
  102. julee/docs/sphinx_hcd/stories.py +0 -960
  103. {julee-0.1.5.dist-info → julee-0.1.6.dist-info}/WHEEL +0 -0
  104. {julee-0.1.5.dist-info → julee-0.1.6.dist-info}/licenses/LICENSE +0 -0
  105. {julee-0.1.5.dist-info → julee-0.1.6.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,327 @@
1
+ """Tests for Integration domain model."""
2
+
3
+ import pytest
4
+ from pydantic import ValidationError
5
+
6
+ from julee.docs.sphinx_hcd.domain.models.integration import (
7
+ Direction,
8
+ ExternalDependency,
9
+ Integration,
10
+ )
11
+
12
+
13
+ class TestDirection:
14
+ """Test Direction enum."""
15
+
16
+ def test_direction_values(self) -> None:
17
+ """Test Direction enum values."""
18
+ assert Direction.INBOUND.value == "inbound"
19
+ assert Direction.OUTBOUND.value == "outbound"
20
+ assert Direction.BIDIRECTIONAL.value == "bidirectional"
21
+
22
+ def test_from_string_valid(self) -> None:
23
+ """Test from_string with valid values."""
24
+ assert Direction.from_string("inbound") == Direction.INBOUND
25
+ assert Direction.from_string("outbound") == Direction.OUTBOUND
26
+ assert Direction.from_string("bidirectional") == Direction.BIDIRECTIONAL
27
+
28
+ def test_from_string_case_insensitive(self) -> None:
29
+ """Test from_string is case-insensitive."""
30
+ assert Direction.from_string("INBOUND") == Direction.INBOUND
31
+ assert Direction.from_string("Outbound") == Direction.OUTBOUND
32
+
33
+ def test_from_string_unknown(self) -> None:
34
+ """Test from_string defaults to BIDIRECTIONAL for invalid values."""
35
+ assert Direction.from_string("invalid") == Direction.BIDIRECTIONAL
36
+ assert Direction.from_string("") == Direction.BIDIRECTIONAL
37
+
38
+ def test_direction_labels(self) -> None:
39
+ """Test direction label property."""
40
+ assert Direction.INBOUND.label == "Inbound (data source)"
41
+ assert Direction.OUTBOUND.label == "Outbound (data sink)"
42
+ assert Direction.BIDIRECTIONAL.label == "Bidirectional"
43
+
44
+
45
+ class TestExternalDependency:
46
+ """Test ExternalDependency model."""
47
+
48
+ def test_create_with_name_only(self) -> None:
49
+ """Test creating with just name."""
50
+ dep = ExternalDependency(name="External API")
51
+ assert dep.name == "External API"
52
+ assert dep.url is None
53
+ assert dep.description == ""
54
+
55
+ def test_create_with_all_fields(self) -> None:
56
+ """Test creating with all fields."""
57
+ dep = ExternalDependency(
58
+ name="External API",
59
+ url="https://api.example.com",
60
+ description="Third party API",
61
+ )
62
+ assert dep.name == "External API"
63
+ assert dep.url == "https://api.example.com"
64
+ assert dep.description == "Third party API"
65
+
66
+ def test_empty_name_raises_error(self) -> None:
67
+ """Test that empty name raises validation error."""
68
+ with pytest.raises(ValidationError, match="name cannot be empty"):
69
+ ExternalDependency(name="")
70
+
71
+ def test_from_dict_complete(self) -> None:
72
+ """Test from_dict with complete data."""
73
+ data = {
74
+ "name": "External API",
75
+ "url": "https://api.example.com",
76
+ "description": "Third party API",
77
+ }
78
+ dep = ExternalDependency.from_dict(data)
79
+ assert dep.name == "External API"
80
+ assert dep.url == "https://api.example.com"
81
+
82
+ def test_from_dict_minimal(self) -> None:
83
+ """Test from_dict with minimal data."""
84
+ data = {"name": "Simple API"}
85
+ dep = ExternalDependency.from_dict(data)
86
+ assert dep.name == "Simple API"
87
+ assert dep.url is None
88
+
89
+
90
+ class TestIntegrationCreation:
91
+ """Test Integration model creation and validation."""
92
+
93
+ def test_create_with_required_fields(self) -> None:
94
+ """Test creating with minimum required fields."""
95
+ integration = Integration(
96
+ slug="data-sync",
97
+ module="data_sync",
98
+ name="Data Sync",
99
+ )
100
+
101
+ assert integration.slug == "data-sync"
102
+ assert integration.module == "data_sync"
103
+ assert integration.name == "Data Sync"
104
+ assert integration.direction == Direction.BIDIRECTIONAL
105
+ assert integration.depends_on == []
106
+
107
+ def test_create_with_all_fields(self) -> None:
108
+ """Test creating with all fields."""
109
+ deps = [ExternalDependency(name="AWS S3", url="https://aws.amazon.com/s3")]
110
+ integration = Integration(
111
+ slug="data-sync",
112
+ module="data_sync",
113
+ name="Data Sync",
114
+ description="Synchronizes data with external systems",
115
+ direction=Direction.OUTBOUND,
116
+ depends_on=deps,
117
+ manifest_path="/path/to/integration.yaml",
118
+ )
119
+
120
+ assert integration.slug == "data-sync"
121
+ assert integration.direction == Direction.OUTBOUND
122
+ assert len(integration.depends_on) == 1
123
+ assert integration.depends_on[0].name == "AWS S3"
124
+
125
+ def test_name_normalized_computed(self) -> None:
126
+ """Test that name_normalized is computed."""
127
+ integration = Integration(
128
+ slug="data-sync",
129
+ module="data_sync",
130
+ name="Data Sync Service",
131
+ )
132
+ assert integration.name_normalized == "data sync service"
133
+
134
+ def test_empty_slug_raises_error(self) -> None:
135
+ """Test that empty slug raises validation error."""
136
+ with pytest.raises(ValidationError, match="slug cannot be empty"):
137
+ Integration(slug="", module="test", name="Test")
138
+
139
+ def test_empty_module_raises_error(self) -> None:
140
+ """Test that empty module raises validation error."""
141
+ with pytest.raises(ValidationError, match="module cannot be empty"):
142
+ Integration(slug="test", module="", name="Test")
143
+
144
+ def test_empty_name_raises_error(self) -> None:
145
+ """Test that empty name raises validation error."""
146
+ with pytest.raises(ValidationError, match="name cannot be empty"):
147
+ Integration(slug="test", module="test", name="")
148
+
149
+
150
+ class TestIntegrationFromManifest:
151
+ """Test Integration.from_manifest factory method."""
152
+
153
+ def test_from_manifest_complete(self) -> None:
154
+ """Test creating from complete manifest."""
155
+ manifest = {
156
+ "slug": "pilot-data",
157
+ "name": "Pilot Data Collection",
158
+ "description": "Collects pilot data",
159
+ "direction": "inbound",
160
+ "depends_on": [
161
+ {"name": "Pilot API", "url": "https://pilot.example.com"},
162
+ {"name": "Data Lake"},
163
+ ],
164
+ }
165
+
166
+ integration = Integration.from_manifest(
167
+ module_name="pilot_data_collection",
168
+ manifest=manifest,
169
+ manifest_path="/integrations/pilot_data_collection/integration.yaml",
170
+ )
171
+
172
+ assert integration.slug == "pilot-data"
173
+ assert integration.module == "pilot_data_collection"
174
+ assert integration.name == "Pilot Data Collection"
175
+ assert integration.direction == Direction.INBOUND
176
+ assert len(integration.depends_on) == 2
177
+ assert integration.depends_on[0].name == "Pilot API"
178
+ assert integration.depends_on[0].url == "https://pilot.example.com"
179
+
180
+ def test_from_manifest_default_slug(self) -> None:
181
+ """Test default slug from module name."""
182
+ manifest = {"name": "Test Integration"}
183
+
184
+ integration = Integration.from_manifest(
185
+ module_name="my_integration",
186
+ manifest=manifest,
187
+ manifest_path="/path/to/integration.yaml",
188
+ )
189
+
190
+ assert integration.slug == "my-integration"
191
+
192
+ def test_from_manifest_default_name(self) -> None:
193
+ """Test default name from slug."""
194
+ manifest = {}
195
+
196
+ integration = Integration.from_manifest(
197
+ module_name="data_sync",
198
+ manifest=manifest,
199
+ manifest_path="/path/to/integration.yaml",
200
+ )
201
+
202
+ assert integration.name == "Data Sync"
203
+
204
+ def test_from_manifest_default_direction(self) -> None:
205
+ """Test default direction is bidirectional."""
206
+ manifest = {"name": "Test"}
207
+
208
+ integration = Integration.from_manifest(
209
+ module_name="test",
210
+ manifest=manifest,
211
+ manifest_path="/path/to/integration.yaml",
212
+ )
213
+
214
+ assert integration.direction == Direction.BIDIRECTIONAL
215
+
216
+ def test_from_manifest_string_dependency(self) -> None:
217
+ """Test parsing simple string dependencies."""
218
+ manifest = {
219
+ "name": "Test",
220
+ "depends_on": ["Simple Dep"],
221
+ }
222
+
223
+ integration = Integration.from_manifest(
224
+ module_name="test",
225
+ manifest=manifest,
226
+ manifest_path="/path/to/integration.yaml",
227
+ )
228
+
229
+ assert len(integration.depends_on) == 1
230
+ assert integration.depends_on[0].name == "Simple Dep"
231
+
232
+
233
+ class TestIntegrationMatching:
234
+ """Test Integration matching methods."""
235
+
236
+ @pytest.fixture
237
+ def sample_integration(self) -> Integration:
238
+ """Create a sample integration for testing."""
239
+ return Integration(
240
+ slug="data-sync",
241
+ module="data_sync",
242
+ name="Data Sync Service",
243
+ direction=Direction.OUTBOUND,
244
+ depends_on=[
245
+ ExternalDependency(name="AWS S3"),
246
+ ExternalDependency(name="External API"),
247
+ ],
248
+ )
249
+
250
+ def test_matches_direction_with_enum(self, sample_integration: Integration) -> None:
251
+ """Test direction matching with enum."""
252
+ assert sample_integration.matches_direction(Direction.OUTBOUND) is True
253
+ assert sample_integration.matches_direction(Direction.INBOUND) is False
254
+
255
+ def test_matches_direction_with_string(
256
+ self, sample_integration: Integration
257
+ ) -> None:
258
+ """Test direction matching with string."""
259
+ assert sample_integration.matches_direction("outbound") is True
260
+ assert sample_integration.matches_direction("inbound") is False
261
+
262
+ def test_matches_name_exact(self, sample_integration: Integration) -> None:
263
+ """Test name matching with exact name."""
264
+ assert sample_integration.matches_name("Data Sync Service") is True
265
+
266
+ def test_matches_name_case_insensitive(
267
+ self, sample_integration: Integration
268
+ ) -> None:
269
+ """Test name matching is case-insensitive."""
270
+ assert sample_integration.matches_name("data sync service") is True
271
+
272
+ def test_has_dependency(self, sample_integration: Integration) -> None:
273
+ """Test checking for dependency."""
274
+ assert sample_integration.has_dependency("AWS S3") is True
275
+ assert sample_integration.has_dependency("aws s3") is True
276
+ assert sample_integration.has_dependency("Unknown") is False
277
+
278
+
279
+ class TestIntegrationProperties:
280
+ """Test Integration properties."""
281
+
282
+ def test_direction_label(self) -> None:
283
+ """Test direction_label property."""
284
+ integration = Integration(
285
+ slug="test",
286
+ module="test",
287
+ name="Test",
288
+ direction=Direction.INBOUND,
289
+ )
290
+ assert integration.direction_label == "Inbound (data source)"
291
+
292
+ def test_module_path(self) -> None:
293
+ """Test module_path property."""
294
+ integration = Integration(
295
+ slug="test",
296
+ module="my_module",
297
+ name="Test",
298
+ )
299
+ assert integration.module_path == "integrations.my_module"
300
+
301
+
302
+ class TestIntegrationSerialization:
303
+ """Test Integration serialization."""
304
+
305
+ def test_integration_to_dict(self) -> None:
306
+ """Test integration can be serialized to dict."""
307
+ integration = Integration(
308
+ slug="test",
309
+ module="test",
310
+ name="Test",
311
+ direction=Direction.INBOUND,
312
+ )
313
+
314
+ data = integration.model_dump()
315
+ assert data["slug"] == "test"
316
+ assert data["direction"] == Direction.INBOUND
317
+
318
+ def test_integration_to_json(self) -> None:
319
+ """Test integration can be serialized to JSON."""
320
+ integration = Integration(
321
+ slug="test",
322
+ module="test",
323
+ name="Test",
324
+ )
325
+
326
+ json_str = integration.model_dump_json()
327
+ assert '"slug":"test"' in json_str
@@ -0,0 +1,249 @@
1
+ """Tests for Journey domain model."""
2
+
3
+ import pytest
4
+ from pydantic import ValidationError
5
+
6
+ from julee.docs.sphinx_hcd.domain.models.journey import (
7
+ Journey,
8
+ JourneyStep,
9
+ StepType,
10
+ )
11
+
12
+
13
+ class TestStepType:
14
+ """Test StepType enum."""
15
+
16
+ def test_step_type_values(self) -> None:
17
+ """Test StepType enum values."""
18
+ assert StepType.STORY.value == "story"
19
+ assert StepType.EPIC.value == "epic"
20
+ assert StepType.PHASE.value == "phase"
21
+
22
+ def test_from_string_valid(self) -> None:
23
+ """Test from_string with valid values."""
24
+ assert StepType.from_string("story") == StepType.STORY
25
+ assert StepType.from_string("epic") == StepType.EPIC
26
+ assert StepType.from_string("phase") == StepType.PHASE
27
+
28
+ def test_from_string_case_insensitive(self) -> None:
29
+ """Test from_string is case-insensitive."""
30
+ assert StepType.from_string("STORY") == StepType.STORY
31
+ assert StepType.from_string("Epic") == StepType.EPIC
32
+
33
+ def test_from_string_invalid(self) -> None:
34
+ """Test from_string raises for invalid values."""
35
+ with pytest.raises(ValueError, match="Invalid step type"):
36
+ StepType.from_string("invalid")
37
+
38
+
39
+ class TestJourneyStep:
40
+ """Test JourneyStep model."""
41
+
42
+ def test_create_story_step(self) -> None:
43
+ """Test creating a story step."""
44
+ step = JourneyStep(step_type=StepType.STORY, ref="Upload Document")
45
+ assert step.step_type == StepType.STORY
46
+ assert step.ref == "Upload Document"
47
+ assert step.is_story is True
48
+ assert step.is_epic is False
49
+ assert step.is_phase is False
50
+
51
+ def test_create_epic_step(self) -> None:
52
+ """Test creating an epic step."""
53
+ step = JourneyStep(step_type=StepType.EPIC, ref="vocabulary-management")
54
+ assert step.step_type == StepType.EPIC
55
+ assert step.ref == "vocabulary-management"
56
+ assert step.is_epic is True
57
+
58
+ def test_create_phase_step(self) -> None:
59
+ """Test creating a phase step with description."""
60
+ step = JourneyStep(
61
+ step_type=StepType.PHASE,
62
+ ref="Upload Sources",
63
+ description="Add reference materials to the knowledge base.",
64
+ )
65
+ assert step.step_type == StepType.PHASE
66
+ assert step.ref == "Upload Sources"
67
+ assert step.description == "Add reference materials to the knowledge base."
68
+ assert step.is_phase is True
69
+
70
+ def test_empty_ref_raises_error(self) -> None:
71
+ """Test that empty ref raises validation error."""
72
+ with pytest.raises(ValidationError, match="ref cannot be empty"):
73
+ JourneyStep(step_type=StepType.STORY, ref="")
74
+
75
+ def test_story_factory(self) -> None:
76
+ """Test story factory method."""
77
+ step = JourneyStep.story("Upload Document")
78
+ assert step.step_type == StepType.STORY
79
+ assert step.ref == "Upload Document"
80
+
81
+ def test_epic_factory(self) -> None:
82
+ """Test epic factory method."""
83
+ step = JourneyStep.epic("vocabulary-management")
84
+ assert step.step_type == StepType.EPIC
85
+ assert step.ref == "vocabulary-management"
86
+
87
+ def test_phase_factory(self) -> None:
88
+ """Test phase factory method."""
89
+ step = JourneyStep.phase("Upload Sources", "Add materials.")
90
+ assert step.step_type == StepType.PHASE
91
+ assert step.ref == "Upload Sources"
92
+ assert step.description == "Add materials."
93
+
94
+ def test_phase_factory_without_description(self) -> None:
95
+ """Test phase factory without description."""
96
+ step = JourneyStep.phase("Upload Sources")
97
+ assert step.description == ""
98
+
99
+
100
+ class TestJourneyCreation:
101
+ """Test Journey model creation and validation."""
102
+
103
+ def test_create_journey_minimal(self) -> None:
104
+ """Test creating a journey with minimum fields."""
105
+ journey = Journey(slug="build-vocabulary")
106
+ assert journey.slug == "build-vocabulary"
107
+ assert journey.persona == ""
108
+ assert journey.steps == []
109
+ assert journey.depends_on == []
110
+
111
+ def test_create_journey_complete(self) -> None:
112
+ """Test creating a journey with all fields."""
113
+ steps = [
114
+ JourneyStep.story("Upload Document"),
115
+ JourneyStep.epic("vocabulary-management"),
116
+ ]
117
+ journey = Journey(
118
+ slug="build-vocabulary",
119
+ persona="Knowledge Curator",
120
+ intent="Ensure consistent terminology across programs",
121
+ outcome="Semantic interoperability enabling compliance mapping",
122
+ goal="Create a Sustainable Vocabulary Catalog",
123
+ depends_on=["operate-pipelines", "setup-system"],
124
+ steps=steps,
125
+ preconditions=["Source materials available", "SME accessible"],
126
+ postconditions=["SVC published and versioned"],
127
+ docname="journeys/build-vocabulary",
128
+ )
129
+
130
+ assert journey.slug == "build-vocabulary"
131
+ assert journey.persona == "Knowledge Curator"
132
+ assert journey.persona_normalized == "knowledge curator"
133
+ assert journey.intent == "Ensure consistent terminology across programs"
134
+ assert len(journey.steps) == 2
135
+ assert len(journey.depends_on) == 2
136
+ assert journey.docname == "journeys/build-vocabulary"
137
+
138
+ def test_persona_normalized_computed(self) -> None:
139
+ """Test that persona_normalized is computed."""
140
+ journey = Journey(slug="test", persona="Knowledge Curator")
141
+ assert journey.persona_normalized == "knowledge curator"
142
+
143
+ def test_empty_slug_raises_error(self) -> None:
144
+ """Test that empty slug raises validation error."""
145
+ with pytest.raises(ValidationError, match="slug cannot be empty"):
146
+ Journey(slug="")
147
+
148
+
149
+ class TestJourneyMatching:
150
+ """Test Journey matching methods."""
151
+
152
+ @pytest.fixture
153
+ def sample_journey(self) -> Journey:
154
+ """Create a sample journey for testing."""
155
+ return Journey(
156
+ slug="build-vocabulary",
157
+ persona="Knowledge Curator",
158
+ depends_on=["operate-pipelines", "setup-system"],
159
+ steps=[
160
+ JourneyStep.story("Upload Document"),
161
+ JourneyStep.epic("vocabulary-management"),
162
+ JourneyStep.story("Review Vocabulary"),
163
+ ],
164
+ )
165
+
166
+ def test_matches_persona_exact(self, sample_journey: Journey) -> None:
167
+ """Test persona matching with exact name."""
168
+ assert sample_journey.matches_persona("Knowledge Curator") is True
169
+
170
+ def test_matches_persona_case_insensitive(self, sample_journey: Journey) -> None:
171
+ """Test persona matching is case-insensitive."""
172
+ assert sample_journey.matches_persona("knowledge curator") is True
173
+ assert sample_journey.matches_persona("KNOWLEDGE CURATOR") is True
174
+
175
+ def test_matches_persona_no_match(self, sample_journey: Journey) -> None:
176
+ """Test persona matching returns False for non-match."""
177
+ assert sample_journey.matches_persona("Analyst") is False
178
+
179
+ def test_has_dependency(self, sample_journey: Journey) -> None:
180
+ """Test dependency checking."""
181
+ assert sample_journey.has_dependency("operate-pipelines") is True
182
+ assert sample_journey.has_dependency("setup-system") is True
183
+ assert sample_journey.has_dependency("unknown") is False
184
+
185
+ def test_get_story_refs(self, sample_journey: Journey) -> None:
186
+ """Test getting story references."""
187
+ refs = sample_journey.get_story_refs()
188
+ assert refs == ["Upload Document", "Review Vocabulary"]
189
+
190
+ def test_get_epic_refs(self, sample_journey: Journey) -> None:
191
+ """Test getting epic references."""
192
+ refs = sample_journey.get_epic_refs()
193
+ assert refs == ["vocabulary-management"]
194
+
195
+
196
+ class TestJourneySteps:
197
+ """Test Journey step operations."""
198
+
199
+ def test_add_step(self) -> None:
200
+ """Test adding a step."""
201
+ journey = Journey(slug="test")
202
+ assert journey.step_count == 0
203
+
204
+ journey.add_step(JourneyStep.story("Test Story"))
205
+ assert journey.step_count == 1
206
+ assert journey.has_steps is True
207
+
208
+ def test_has_steps_empty(self) -> None:
209
+ """Test has_steps with empty journey."""
210
+ journey = Journey(slug="test")
211
+ assert journey.has_steps is False
212
+
213
+
214
+ class TestJourneyProperties:
215
+ """Test Journey properties."""
216
+
217
+ def test_display_title(self) -> None:
218
+ """Test display_title property."""
219
+ journey = Journey(slug="build-vocabulary")
220
+ assert journey.display_title == "Build Vocabulary"
221
+
222
+ def test_display_title_multiple_words(self) -> None:
223
+ """Test display_title with multiple hyphens."""
224
+ journey = Journey(slug="operate-data-pipelines")
225
+ assert journey.display_title == "Operate Data Pipelines"
226
+
227
+
228
+ class TestJourneySerialization:
229
+ """Test Journey serialization."""
230
+
231
+ def test_journey_to_dict(self) -> None:
232
+ """Test journey can be serialized to dict."""
233
+ journey = Journey(
234
+ slug="test",
235
+ persona="User",
236
+ steps=[JourneyStep.story("Test Story")],
237
+ )
238
+
239
+ data = journey.model_dump()
240
+ assert data["slug"] == "test"
241
+ assert data["persona"] == "User"
242
+ assert len(data["steps"]) == 1
243
+ assert data["steps"][0]["step_type"] == StepType.STORY
244
+
245
+ def test_journey_to_json(self) -> None:
246
+ """Test journey can be serialized to JSON."""
247
+ journey = Journey(slug="test", persona="User")
248
+ json_str = journey.model_dump_json()
249
+ assert '"slug":"test"' in json_str