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,258 @@
1
+ """Tests for App domain model."""
2
+
3
+ import pytest
4
+ from pydantic import ValidationError
5
+
6
+ from julee.docs.sphinx_hcd.domain.models.app import App, AppType
7
+
8
+
9
+ class TestAppType:
10
+ """Test AppType enum."""
11
+
12
+ def test_app_type_values(self) -> None:
13
+ """Test AppType enum values."""
14
+ assert AppType.STAFF.value == "staff"
15
+ assert AppType.EXTERNAL.value == "external"
16
+ assert AppType.MEMBER_TOOL.value == "member-tool"
17
+ assert AppType.UNKNOWN.value == "unknown"
18
+
19
+ def test_from_string_valid(self) -> None:
20
+ """Test from_string with valid values."""
21
+ assert AppType.from_string("staff") == AppType.STAFF
22
+ assert AppType.from_string("external") == AppType.EXTERNAL
23
+ assert AppType.from_string("member-tool") == AppType.MEMBER_TOOL
24
+
25
+ def test_from_string_case_insensitive(self) -> None:
26
+ """Test from_string is case-insensitive."""
27
+ assert AppType.from_string("STAFF") == AppType.STAFF
28
+ assert AppType.from_string("Staff") == AppType.STAFF
29
+
30
+ def test_from_string_unknown(self) -> None:
31
+ """Test from_string returns UNKNOWN for invalid values."""
32
+ assert AppType.from_string("invalid") == AppType.UNKNOWN
33
+ assert AppType.from_string("") == AppType.UNKNOWN
34
+
35
+
36
+ class TestAppCreation:
37
+ """Test App model creation and validation."""
38
+
39
+ def test_create_app_with_required_fields(self) -> None:
40
+ """Test creating an app with minimum required fields."""
41
+ app = App(
42
+ slug="staff-portal",
43
+ name="Staff Portal",
44
+ )
45
+
46
+ assert app.slug == "staff-portal"
47
+ assert app.name == "Staff Portal"
48
+ assert app.app_type == AppType.UNKNOWN
49
+ assert app.status is None
50
+ assert app.description == ""
51
+ assert app.accelerators == []
52
+
53
+ def test_create_app_with_all_fields(self) -> None:
54
+ """Test creating an app with all fields."""
55
+ app = App(
56
+ slug="staff-portal",
57
+ name="Staff Portal",
58
+ app_type=AppType.STAFF,
59
+ status="live",
60
+ description="Portal for staff members",
61
+ accelerators=["user-auth", "doc-upload"],
62
+ manifest_path="/path/to/app.yaml",
63
+ )
64
+
65
+ assert app.slug == "staff-portal"
66
+ assert app.name == "Staff Portal"
67
+ assert app.app_type == AppType.STAFF
68
+ assert app.status == "live"
69
+ assert app.description == "Portal for staff members"
70
+ assert app.accelerators == ["user-auth", "doc-upload"]
71
+ assert app.manifest_path == "/path/to/app.yaml"
72
+
73
+ def test_name_normalized_computed_automatically(self) -> None:
74
+ """Test that name_normalized is computed from name."""
75
+ app = App(
76
+ slug="staff-portal",
77
+ name="Staff Portal",
78
+ )
79
+
80
+ assert app.name_normalized == "staff portal"
81
+
82
+ def test_empty_slug_raises_error(self) -> None:
83
+ """Test that empty slug raises validation error."""
84
+ with pytest.raises(ValidationError, match="slug cannot be empty"):
85
+ App(
86
+ slug="",
87
+ name="Test App",
88
+ )
89
+
90
+ def test_empty_name_raises_error(self) -> None:
91
+ """Test that empty name raises validation error."""
92
+ with pytest.raises(ValidationError, match="name cannot be empty"):
93
+ App(
94
+ slug="test-app",
95
+ name="",
96
+ )
97
+
98
+ def test_whitespace_only_slug_raises_error(self) -> None:
99
+ """Test that whitespace-only slug raises validation error."""
100
+ with pytest.raises(ValidationError, match="slug cannot be empty"):
101
+ App(
102
+ slug=" ",
103
+ name="Test App",
104
+ )
105
+
106
+
107
+ class TestAppFromManifest:
108
+ """Test App.from_manifest factory method."""
109
+
110
+ def test_from_manifest_creates_app(self) -> None:
111
+ """Test creating an app from manifest data."""
112
+ manifest = {
113
+ "name": "Staff Portal",
114
+ "type": "staff",
115
+ "status": "live",
116
+ "description": "Portal for staff members",
117
+ "accelerators": ["user-auth"],
118
+ }
119
+
120
+ app = App.from_manifest(
121
+ slug="staff-portal",
122
+ manifest=manifest,
123
+ manifest_path="/apps/staff-portal/app.yaml",
124
+ )
125
+
126
+ assert app.slug == "staff-portal"
127
+ assert app.name == "Staff Portal"
128
+ assert app.app_type == AppType.STAFF
129
+ assert app.status == "live"
130
+ assert app.description == "Portal for staff members"
131
+ assert app.accelerators == ["user-auth"]
132
+ assert app.manifest_path == "/apps/staff-portal/app.yaml"
133
+
134
+ def test_from_manifest_default_name(self) -> None:
135
+ """Test default name from slug when not in manifest."""
136
+ manifest = {}
137
+
138
+ app = App.from_manifest(
139
+ slug="staff-portal",
140
+ manifest=manifest,
141
+ manifest_path="/apps/staff-portal/app.yaml",
142
+ )
143
+
144
+ assert app.name == "Staff Portal"
145
+
146
+ def test_from_manifest_default_type(self) -> None:
147
+ """Test default type when not in manifest."""
148
+ manifest = {"name": "Test App"}
149
+
150
+ app = App.from_manifest(
151
+ slug="test-app",
152
+ manifest=manifest,
153
+ manifest_path="/apps/test-app/app.yaml",
154
+ )
155
+
156
+ assert app.app_type == AppType.UNKNOWN
157
+
158
+ def test_from_manifest_strips_description(self) -> None:
159
+ """Test that description whitespace is stripped."""
160
+ manifest = {
161
+ "name": "Test App",
162
+ "description": " Some description \n",
163
+ }
164
+
165
+ app = App.from_manifest(
166
+ slug="test-app",
167
+ manifest=manifest,
168
+ manifest_path="/apps/test-app/app.yaml",
169
+ )
170
+
171
+ assert app.description == "Some description"
172
+
173
+
174
+ class TestAppMatching:
175
+ """Test App matching methods."""
176
+
177
+ @pytest.fixture
178
+ def sample_app(self) -> App:
179
+ """Create a sample app for testing."""
180
+ return App(
181
+ slug="staff-portal",
182
+ name="Staff Portal",
183
+ app_type=AppType.STAFF,
184
+ )
185
+
186
+ def test_matches_type_with_enum(self, sample_app: App) -> None:
187
+ """Test type matching with AppType enum."""
188
+ assert sample_app.matches_type(AppType.STAFF) is True
189
+ assert sample_app.matches_type(AppType.EXTERNAL) is False
190
+
191
+ def test_matches_type_with_string(self, sample_app: App) -> None:
192
+ """Test type matching with string."""
193
+ assert sample_app.matches_type("staff") is True
194
+ assert sample_app.matches_type("external") is False
195
+
196
+ def test_matches_name_exact(self, sample_app: App) -> None:
197
+ """Test name matching with exact name."""
198
+ assert sample_app.matches_name("Staff Portal") is True
199
+
200
+ def test_matches_name_case_insensitive(self, sample_app: App) -> None:
201
+ """Test name matching is case-insensitive."""
202
+ assert sample_app.matches_name("staff portal") is True
203
+ assert sample_app.matches_name("STAFF PORTAL") is True
204
+
205
+ def test_matches_name_no_match(self, sample_app: App) -> None:
206
+ """Test name matching returns False for non-match."""
207
+ assert sample_app.matches_name("External App") is False
208
+
209
+
210
+ class TestAppTypeLabel:
211
+ """Test App type_label property."""
212
+
213
+ def test_type_label_staff(self) -> None:
214
+ """Test type label for staff app."""
215
+ app = App(slug="test", name="Test", app_type=AppType.STAFF)
216
+ assert app.type_label == "Staff Application"
217
+
218
+ def test_type_label_external(self) -> None:
219
+ """Test type label for external app."""
220
+ app = App(slug="test", name="Test", app_type=AppType.EXTERNAL)
221
+ assert app.type_label == "External Application"
222
+
223
+ def test_type_label_member_tool(self) -> None:
224
+ """Test type label for member tool."""
225
+ app = App(slug="test", name="Test", app_type=AppType.MEMBER_TOOL)
226
+ assert app.type_label == "Member Tool"
227
+
228
+ def test_type_label_unknown(self) -> None:
229
+ """Test type label for unknown type."""
230
+ app = App(slug="test", name="Test", app_type=AppType.UNKNOWN)
231
+ assert app.type_label == "Unknown"
232
+
233
+
234
+ class TestAppSerialization:
235
+ """Test App serialization."""
236
+
237
+ def test_app_to_dict(self) -> None:
238
+ """Test app can be serialized to dict."""
239
+ app = App(
240
+ slug="test-app",
241
+ name="Test App",
242
+ app_type=AppType.STAFF,
243
+ )
244
+
245
+ data = app.model_dump()
246
+ assert data["slug"] == "test-app"
247
+ assert data["name"] == "Test App"
248
+ assert data["app_type"] == AppType.STAFF
249
+
250
+ def test_app_to_json(self) -> None:
251
+ """Test app can be serialized to JSON."""
252
+ app = App(
253
+ slug="test-app",
254
+ name="Test App",
255
+ )
256
+
257
+ json_str = app.model_dump_json()
258
+ assert '"slug":"test-app"' in json_str
@@ -0,0 +1,231 @@
1
+ """Tests for CodeInfo domain models."""
2
+
3
+ import pytest
4
+ from pydantic import ValidationError
5
+
6
+ from julee.docs.sphinx_hcd.domain.models.code_info import (
7
+ BoundedContextInfo,
8
+ ClassInfo,
9
+ )
10
+
11
+
12
+ class TestClassInfo:
13
+ """Test ClassInfo model."""
14
+
15
+ def test_create_with_name_only(self) -> None:
16
+ """Test creating with just name."""
17
+ info = ClassInfo(name="Document")
18
+ assert info.name == "Document"
19
+ assert info.docstring == ""
20
+ assert info.file == ""
21
+
22
+ def test_create_with_all_fields(self) -> None:
23
+ """Test creating with all fields."""
24
+ info = ClassInfo(
25
+ name="Document",
26
+ docstring="A document entity.",
27
+ file="document.py",
28
+ )
29
+ assert info.name == "Document"
30
+ assert info.docstring == "A document entity."
31
+ assert info.file == "document.py"
32
+
33
+ def test_empty_name_raises_error(self) -> None:
34
+ """Test that empty name raises validation error."""
35
+ with pytest.raises(ValidationError, match="name cannot be empty"):
36
+ ClassInfo(name="")
37
+
38
+ def test_whitespace_name_raises_error(self) -> None:
39
+ """Test that whitespace-only name raises validation error."""
40
+ with pytest.raises(ValidationError, match="name cannot be empty"):
41
+ ClassInfo(name=" ")
42
+
43
+ def test_name_stripped(self) -> None:
44
+ """Test that name is stripped of whitespace."""
45
+ info = ClassInfo(name=" Document ")
46
+ assert info.name == "Document"
47
+
48
+
49
+ class TestBoundedContextInfoCreation:
50
+ """Test BoundedContextInfo model creation and validation."""
51
+
52
+ def test_create_minimal(self) -> None:
53
+ """Test creating with minimum fields."""
54
+ info = BoundedContextInfo(slug="vocabulary")
55
+ assert info.slug == "vocabulary"
56
+ assert info.entities == []
57
+ assert info.use_cases == []
58
+ assert info.repository_protocols == []
59
+ assert info.service_protocols == []
60
+ assert info.has_infrastructure is False
61
+ assert info.code_dir == ""
62
+ assert info.objective is None
63
+ assert info.docstring is None
64
+
65
+ def test_create_complete(self) -> None:
66
+ """Test creating with all fields."""
67
+ entities = [
68
+ ClassInfo(
69
+ name="Vocabulary", docstring="A vocabulary entity", file="vocabulary.py"
70
+ ),
71
+ ClassInfo(name="Term", docstring="A term in a vocabulary", file="term.py"),
72
+ ]
73
+ use_cases = [
74
+ ClassInfo(
75
+ name="CreateVocabulary",
76
+ docstring="Create a vocabulary",
77
+ file="create.py",
78
+ ),
79
+ ]
80
+ repo_protocols = [
81
+ ClassInfo(
82
+ name="VocabularyRepository",
83
+ docstring="Repository protocol",
84
+ file="vocabulary.py",
85
+ ),
86
+ ]
87
+
88
+ info = BoundedContextInfo(
89
+ slug="vocabulary",
90
+ entities=entities,
91
+ use_cases=use_cases,
92
+ repository_protocols=repo_protocols,
93
+ service_protocols=[],
94
+ has_infrastructure=True,
95
+ code_dir="vocabulary",
96
+ objective="Manage vocabulary catalogs",
97
+ docstring="Full module documentation here.",
98
+ )
99
+
100
+ assert info.slug == "vocabulary"
101
+ assert len(info.entities) == 2
102
+ assert len(info.use_cases) == 1
103
+ assert len(info.repository_protocols) == 1
104
+ assert info.has_infrastructure is True
105
+ assert info.objective == "Manage vocabulary catalogs"
106
+
107
+ def test_empty_slug_raises_error(self) -> None:
108
+ """Test that empty slug raises validation error."""
109
+ with pytest.raises(ValidationError, match="slug cannot be empty"):
110
+ BoundedContextInfo(slug="")
111
+
112
+
113
+ class TestBoundedContextInfoProperties:
114
+ """Test BoundedContextInfo properties."""
115
+
116
+ @pytest.fixture
117
+ def sample_context(self) -> BoundedContextInfo:
118
+ """Create a sample bounded context for testing."""
119
+ return BoundedContextInfo(
120
+ slug="vocabulary",
121
+ entities=[
122
+ ClassInfo(name="Vocabulary", file="vocabulary.py"),
123
+ ClassInfo(name="Term", file="term.py"),
124
+ ],
125
+ use_cases=[
126
+ ClassInfo(name="CreateVocabulary", file="create.py"),
127
+ ],
128
+ repository_protocols=[
129
+ ClassInfo(name="VocabularyRepository", file="vocabulary.py"),
130
+ ],
131
+ service_protocols=[
132
+ ClassInfo(name="NotificationService", file="notification.py"),
133
+ ],
134
+ has_infrastructure=True,
135
+ )
136
+
137
+ def test_entity_count(self, sample_context: BoundedContextInfo) -> None:
138
+ """Test entity_count property."""
139
+ assert sample_context.entity_count == 2
140
+
141
+ def test_use_case_count(self, sample_context: BoundedContextInfo) -> None:
142
+ """Test use_case_count property."""
143
+ assert sample_context.use_case_count == 1
144
+
145
+ def test_protocol_count(self, sample_context: BoundedContextInfo) -> None:
146
+ """Test protocol_count property."""
147
+ assert sample_context.protocol_count == 2 # 1 repo + 1 service
148
+
149
+ def test_has_entities(self, sample_context: BoundedContextInfo) -> None:
150
+ """Test has_entities property."""
151
+ assert sample_context.has_entities is True
152
+
153
+ def test_has_entities_empty(self) -> None:
154
+ """Test has_entities with empty context."""
155
+ info = BoundedContextInfo(slug="test")
156
+ assert info.has_entities is False
157
+
158
+ def test_has_use_cases(self, sample_context: BoundedContextInfo) -> None:
159
+ """Test has_use_cases property."""
160
+ assert sample_context.has_use_cases is True
161
+
162
+ def test_has_use_cases_empty(self) -> None:
163
+ """Test has_use_cases with empty context."""
164
+ info = BoundedContextInfo(slug="test")
165
+ assert info.has_use_cases is False
166
+
167
+ def test_has_protocols(self, sample_context: BoundedContextInfo) -> None:
168
+ """Test has_protocols property."""
169
+ assert sample_context.has_protocols is True
170
+
171
+ def test_has_protocols_empty(self) -> None:
172
+ """Test has_protocols with empty context."""
173
+ info = BoundedContextInfo(slug="test")
174
+ assert info.has_protocols is False
175
+
176
+ def test_get_entity_names(self, sample_context: BoundedContextInfo) -> None:
177
+ """Test get_entity_names method."""
178
+ names = sample_context.get_entity_names()
179
+ assert names == ["Vocabulary", "Term"]
180
+
181
+ def test_get_use_case_names(self, sample_context: BoundedContextInfo) -> None:
182
+ """Test get_use_case_names method."""
183
+ names = sample_context.get_use_case_names()
184
+ assert names == ["CreateVocabulary"]
185
+
186
+ def test_summary(self, sample_context: BoundedContextInfo) -> None:
187
+ """Test summary method."""
188
+ summary = sample_context.summary()
189
+ assert "2 entities" in summary
190
+ assert "1 use cases" in summary
191
+ assert "1 repository protocols" in summary
192
+ assert "1 service protocols" in summary
193
+
194
+ def test_summary_empty(self) -> None:
195
+ """Test summary with empty context."""
196
+ info = BoundedContextInfo(slug="test")
197
+ assert info.summary() == "empty"
198
+
199
+ def test_summary_partial(self) -> None:
200
+ """Test summary with partial data."""
201
+ info = BoundedContextInfo(
202
+ slug="test",
203
+ entities=[ClassInfo(name="Entity", file="e.py")],
204
+ )
205
+ summary = info.summary()
206
+ assert summary == "1 entities"
207
+
208
+
209
+ class TestBoundedContextInfoSerialization:
210
+ """Test BoundedContextInfo serialization."""
211
+
212
+ def test_to_dict(self) -> None:
213
+ """Test serialization to dict."""
214
+ info = BoundedContextInfo(
215
+ slug="test",
216
+ entities=[ClassInfo(name="Entity", file="e.py")],
217
+ objective="Test objective",
218
+ )
219
+
220
+ data = info.model_dump()
221
+ assert data["slug"] == "test"
222
+ assert len(data["entities"]) == 1
223
+ assert data["entities"][0]["name"] == "Entity"
224
+ assert data["objective"] == "Test objective"
225
+
226
+ def test_to_json(self) -> None:
227
+ """Test serialization to JSON."""
228
+ info = BoundedContextInfo(slug="test", objective="Test")
229
+ json_str = info.model_dump_json()
230
+ assert '"slug":"test"' in json_str
231
+ assert '"objective":"Test"' in json_str
@@ -0,0 +1,163 @@
1
+ """Tests for Epic domain model."""
2
+
3
+ import pytest
4
+ from pydantic import ValidationError
5
+
6
+ from julee.docs.sphinx_hcd.domain.models.epic import Epic
7
+
8
+
9
+ class TestEpicCreation:
10
+ """Test Epic model creation and validation."""
11
+
12
+ def test_create_epic_minimal(self) -> None:
13
+ """Test creating an epic with minimum fields."""
14
+ epic = Epic(slug="vocabulary-management")
15
+ assert epic.slug == "vocabulary-management"
16
+ assert epic.description == ""
17
+ assert epic.story_refs == []
18
+ assert epic.docname == ""
19
+
20
+ def test_create_epic_complete(self) -> None:
21
+ """Test creating an epic with all fields."""
22
+ epic = Epic(
23
+ slug="vocabulary-management",
24
+ description="Manage terminology and vocabulary catalogs",
25
+ story_refs=["Upload Document", "Review Vocabulary", "Publish Catalog"],
26
+ docname="epics/vocabulary-management",
27
+ )
28
+
29
+ assert epic.slug == "vocabulary-management"
30
+ assert epic.description == "Manage terminology and vocabulary catalogs"
31
+ assert len(epic.story_refs) == 3
32
+ assert epic.docname == "epics/vocabulary-management"
33
+
34
+ def test_empty_slug_raises_error(self) -> None:
35
+ """Test that empty slug raises validation error."""
36
+ with pytest.raises(ValidationError, match="slug cannot be empty"):
37
+ Epic(slug="")
38
+
39
+ def test_whitespace_slug_raises_error(self) -> None:
40
+ """Test that whitespace-only slug raises validation error."""
41
+ with pytest.raises(ValidationError, match="slug cannot be empty"):
42
+ Epic(slug=" ")
43
+
44
+ def test_slug_stripped(self) -> None:
45
+ """Test that slug is stripped of whitespace."""
46
+ epic = Epic(slug=" vocabulary-management ")
47
+ assert epic.slug == "vocabulary-management"
48
+
49
+
50
+ class TestEpicStoryOperations:
51
+ """Test Epic story reference operations."""
52
+
53
+ @pytest.fixture
54
+ def sample_epic(self) -> Epic:
55
+ """Create a sample epic for testing."""
56
+ return Epic(
57
+ slug="vocabulary-management",
58
+ description="Manage terminology",
59
+ story_refs=["Upload Document", "Review Vocabulary"],
60
+ docname="epics/vocabulary-management",
61
+ )
62
+
63
+ def test_add_story(self) -> None:
64
+ """Test adding a story to an epic."""
65
+ epic = Epic(slug="test-epic")
66
+ assert epic.story_count == 0
67
+
68
+ epic.add_story("New Story")
69
+ assert epic.story_count == 1
70
+ assert "New Story" in epic.story_refs
71
+
72
+ def test_has_story_exact_match(self, sample_epic: Epic) -> None:
73
+ """Test has_story with exact match."""
74
+ assert sample_epic.has_story("Upload Document") is True
75
+ assert sample_epic.has_story("Review Vocabulary") is True
76
+
77
+ def test_has_story_case_insensitive(self, sample_epic: Epic) -> None:
78
+ """Test has_story is case-insensitive."""
79
+ assert sample_epic.has_story("upload document") is True
80
+ assert sample_epic.has_story("UPLOAD DOCUMENT") is True
81
+ assert sample_epic.has_story("Upload document") is True
82
+
83
+ def test_has_story_no_match(self, sample_epic: Epic) -> None:
84
+ """Test has_story returns False for non-match."""
85
+ assert sample_epic.has_story("Unknown Story") is False
86
+
87
+ def test_get_story_refs_normalized(self, sample_epic: Epic) -> None:
88
+ """Test getting normalized story references."""
89
+ normalized = sample_epic.get_story_refs_normalized()
90
+ assert normalized == ["upload document", "review vocabulary"]
91
+
92
+
93
+ class TestEpicProperties:
94
+ """Test Epic properties."""
95
+
96
+ def test_display_title(self) -> None:
97
+ """Test display_title property."""
98
+ epic = Epic(slug="vocabulary-management")
99
+ assert epic.display_title == "Vocabulary Management"
100
+
101
+ def test_display_title_multiple_words(self) -> None:
102
+ """Test display_title with multiple hyphens."""
103
+ epic = Epic(slug="credential-creation-workflow")
104
+ assert epic.display_title == "Credential Creation Workflow"
105
+
106
+ def test_story_count_empty(self) -> None:
107
+ """Test story_count with no stories."""
108
+ epic = Epic(slug="test")
109
+ assert epic.story_count == 0
110
+
111
+ def test_story_count_with_stories(self) -> None:
112
+ """Test story_count with stories."""
113
+ epic = Epic(slug="test", story_refs=["Story 1", "Story 2", "Story 3"])
114
+ assert epic.story_count == 3
115
+
116
+ def test_has_stories_empty(self) -> None:
117
+ """Test has_stories with no stories."""
118
+ epic = Epic(slug="test")
119
+ assert epic.has_stories is False
120
+
121
+ def test_has_stories_with_stories(self) -> None:
122
+ """Test has_stories with stories."""
123
+ epic = Epic(slug="test", story_refs=["Story 1"])
124
+ assert epic.has_stories is True
125
+
126
+
127
+ class TestEpicSerialization:
128
+ """Test Epic serialization."""
129
+
130
+ def test_epic_to_dict(self) -> None:
131
+ """Test epic can be serialized to dict."""
132
+ epic = Epic(
133
+ slug="test",
134
+ description="Test description",
135
+ story_refs=["Story 1"],
136
+ docname="test/doc",
137
+ )
138
+
139
+ data = epic.model_dump()
140
+ assert data["slug"] == "test"
141
+ assert data["description"] == "Test description"
142
+ assert data["story_refs"] == ["Story 1"]
143
+ assert data["docname"] == "test/doc"
144
+
145
+ def test_epic_to_json(self) -> None:
146
+ """Test epic can be serialized to JSON."""
147
+ epic = Epic(slug="test", description="Test")
148
+ json_str = epic.model_dump_json()
149
+ assert '"slug":"test"' in json_str
150
+ assert '"description":"Test"' in json_str
151
+
152
+ def test_epic_from_dict(self) -> None:
153
+ """Test epic can be deserialized from dict."""
154
+ data = {
155
+ "slug": "test",
156
+ "description": "Test description",
157
+ "story_refs": ["Story 1", "Story 2"],
158
+ "docname": "test/doc",
159
+ }
160
+ epic = Epic.model_validate(data)
161
+ assert epic.slug == "test"
162
+ assert epic.description == "Test description"
163
+ assert len(epic.story_refs) == 2