julee 0.1.5__py3-none-any.whl → 0.1.7__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 (108) hide show
  1. julee/__init__.py +1 -1
  2. julee/contrib/polling/apps/worker/pipelines.py +3 -1
  3. julee/contrib/polling/tests/unit/apps/worker/test_pipelines.py +3 -0
  4. julee/docs/sphinx_hcd/__init__.py +146 -13
  5. julee/docs/sphinx_hcd/domain/__init__.py +5 -0
  6. julee/docs/sphinx_hcd/domain/models/__init__.py +32 -0
  7. julee/docs/sphinx_hcd/domain/models/accelerator.py +152 -0
  8. julee/docs/sphinx_hcd/domain/models/app.py +151 -0
  9. julee/docs/sphinx_hcd/domain/models/code_info.py +121 -0
  10. julee/docs/sphinx_hcd/domain/models/epic.py +79 -0
  11. julee/docs/sphinx_hcd/domain/models/integration.py +230 -0
  12. julee/docs/sphinx_hcd/domain/models/journey.py +222 -0
  13. julee/docs/sphinx_hcd/domain/models/persona.py +106 -0
  14. julee/docs/sphinx_hcd/domain/models/story.py +128 -0
  15. julee/docs/sphinx_hcd/domain/repositories/__init__.py +25 -0
  16. julee/docs/sphinx_hcd/domain/repositories/accelerator.py +98 -0
  17. julee/docs/sphinx_hcd/domain/repositories/app.py +57 -0
  18. julee/docs/sphinx_hcd/domain/repositories/base.py +89 -0
  19. julee/docs/sphinx_hcd/domain/repositories/code_info.py +69 -0
  20. julee/docs/sphinx_hcd/domain/repositories/epic.py +62 -0
  21. julee/docs/sphinx_hcd/domain/repositories/integration.py +79 -0
  22. julee/docs/sphinx_hcd/domain/repositories/journey.py +106 -0
  23. julee/docs/sphinx_hcd/domain/repositories/story.py +68 -0
  24. julee/docs/sphinx_hcd/domain/use_cases/__init__.py +64 -0
  25. julee/docs/sphinx_hcd/domain/use_cases/derive_personas.py +166 -0
  26. julee/docs/sphinx_hcd/domain/use_cases/resolve_accelerator_references.py +236 -0
  27. julee/docs/sphinx_hcd/domain/use_cases/resolve_app_references.py +144 -0
  28. julee/docs/sphinx_hcd/domain/use_cases/resolve_story_references.py +121 -0
  29. julee/docs/sphinx_hcd/parsers/__init__.py +48 -0
  30. julee/docs/sphinx_hcd/parsers/ast.py +150 -0
  31. julee/docs/sphinx_hcd/parsers/gherkin.py +155 -0
  32. julee/docs/sphinx_hcd/parsers/yaml.py +184 -0
  33. julee/docs/sphinx_hcd/repositories/__init__.py +4 -0
  34. julee/docs/sphinx_hcd/repositories/memory/__init__.py +25 -0
  35. julee/docs/sphinx_hcd/repositories/memory/accelerator.py +86 -0
  36. julee/docs/sphinx_hcd/repositories/memory/app.py +45 -0
  37. julee/docs/sphinx_hcd/repositories/memory/base.py +106 -0
  38. julee/docs/sphinx_hcd/repositories/memory/code_info.py +59 -0
  39. julee/docs/sphinx_hcd/repositories/memory/epic.py +54 -0
  40. julee/docs/sphinx_hcd/repositories/memory/integration.py +70 -0
  41. julee/docs/sphinx_hcd/repositories/memory/journey.py +96 -0
  42. julee/docs/sphinx_hcd/repositories/memory/story.py +63 -0
  43. julee/docs/sphinx_hcd/sphinx/__init__.py +28 -0
  44. julee/docs/sphinx_hcd/sphinx/adapters.py +116 -0
  45. julee/docs/sphinx_hcd/sphinx/context.py +163 -0
  46. julee/docs/sphinx_hcd/sphinx/directives/__init__.py +160 -0
  47. julee/docs/sphinx_hcd/sphinx/directives/accelerator.py +576 -0
  48. julee/docs/sphinx_hcd/sphinx/directives/app.py +349 -0
  49. julee/docs/sphinx_hcd/sphinx/directives/base.py +211 -0
  50. julee/docs/sphinx_hcd/sphinx/directives/epic.py +434 -0
  51. julee/docs/sphinx_hcd/sphinx/directives/integration.py +220 -0
  52. julee/docs/sphinx_hcd/sphinx/directives/journey.py +642 -0
  53. julee/docs/sphinx_hcd/sphinx/directives/persona.py +345 -0
  54. julee/docs/sphinx_hcd/sphinx/directives/story.py +575 -0
  55. julee/docs/sphinx_hcd/sphinx/event_handlers/__init__.py +16 -0
  56. julee/docs/sphinx_hcd/sphinx/event_handlers/builder_inited.py +31 -0
  57. julee/docs/sphinx_hcd/sphinx/event_handlers/doctree_read.py +27 -0
  58. julee/docs/sphinx_hcd/sphinx/event_handlers/doctree_resolved.py +43 -0
  59. julee/docs/sphinx_hcd/sphinx/event_handlers/env_purge_doc.py +42 -0
  60. julee/docs/sphinx_hcd/sphinx/initialization.py +139 -0
  61. julee/docs/sphinx_hcd/tests/__init__.py +9 -0
  62. julee/docs/sphinx_hcd/tests/conftest.py +6 -0
  63. julee/docs/sphinx_hcd/tests/domain/__init__.py +1 -0
  64. julee/docs/sphinx_hcd/tests/domain/models/__init__.py +1 -0
  65. julee/docs/sphinx_hcd/tests/domain/models/test_accelerator.py +266 -0
  66. julee/docs/sphinx_hcd/tests/domain/models/test_app.py +258 -0
  67. julee/docs/sphinx_hcd/tests/domain/models/test_code_info.py +231 -0
  68. julee/docs/sphinx_hcd/tests/domain/models/test_epic.py +163 -0
  69. julee/docs/sphinx_hcd/tests/domain/models/test_integration.py +327 -0
  70. julee/docs/sphinx_hcd/tests/domain/models/test_journey.py +249 -0
  71. julee/docs/sphinx_hcd/tests/domain/models/test_persona.py +172 -0
  72. julee/docs/sphinx_hcd/tests/domain/models/test_story.py +216 -0
  73. julee/docs/sphinx_hcd/tests/domain/use_cases/__init__.py +1 -0
  74. julee/docs/sphinx_hcd/tests/domain/use_cases/test_derive_personas.py +314 -0
  75. julee/docs/sphinx_hcd/tests/domain/use_cases/test_resolve_accelerator_references.py +476 -0
  76. julee/docs/sphinx_hcd/tests/domain/use_cases/test_resolve_app_references.py +265 -0
  77. julee/docs/sphinx_hcd/tests/domain/use_cases/test_resolve_story_references.py +229 -0
  78. julee/docs/sphinx_hcd/tests/integration/__init__.py +1 -0
  79. julee/docs/sphinx_hcd/tests/parsers/__init__.py +1 -0
  80. julee/docs/sphinx_hcd/tests/parsers/test_ast.py +298 -0
  81. julee/docs/sphinx_hcd/tests/parsers/test_gherkin.py +282 -0
  82. julee/docs/sphinx_hcd/tests/parsers/test_yaml.py +496 -0
  83. julee/docs/sphinx_hcd/tests/repositories/__init__.py +1 -0
  84. julee/docs/sphinx_hcd/tests/repositories/test_accelerator.py +298 -0
  85. julee/docs/sphinx_hcd/tests/repositories/test_app.py +218 -0
  86. julee/docs/sphinx_hcd/tests/repositories/test_base.py +151 -0
  87. julee/docs/sphinx_hcd/tests/repositories/test_code_info.py +253 -0
  88. julee/docs/sphinx_hcd/tests/repositories/test_epic.py +237 -0
  89. julee/docs/sphinx_hcd/tests/repositories/test_integration.py +268 -0
  90. julee/docs/sphinx_hcd/tests/repositories/test_journey.py +294 -0
  91. julee/docs/sphinx_hcd/tests/repositories/test_story.py +236 -0
  92. julee/docs/sphinx_hcd/tests/sphinx/__init__.py +1 -0
  93. julee/docs/sphinx_hcd/tests/sphinx/directives/__init__.py +1 -0
  94. julee/docs/sphinx_hcd/tests/sphinx/directives/test_base.py +160 -0
  95. julee/docs/sphinx_hcd/tests/sphinx/test_adapters.py +176 -0
  96. julee/docs/sphinx_hcd/tests/sphinx/test_context.py +257 -0
  97. {julee-0.1.5.dist-info → julee-0.1.7.dist-info}/METADATA +2 -1
  98. {julee-0.1.5.dist-info → julee-0.1.7.dist-info}/RECORD +101 -16
  99. julee/docs/sphinx_hcd/accelerators.py +0 -1175
  100. julee/docs/sphinx_hcd/apps.py +0 -518
  101. julee/docs/sphinx_hcd/epics.py +0 -453
  102. julee/docs/sphinx_hcd/integrations.py +0 -310
  103. julee/docs/sphinx_hcd/journeys.py +0 -797
  104. julee/docs/sphinx_hcd/personas.py +0 -457
  105. julee/docs/sphinx_hcd/stories.py +0 -960
  106. {julee-0.1.5.dist-info → julee-0.1.7.dist-info}/WHEEL +0 -0
  107. {julee-0.1.5.dist-info → julee-0.1.7.dist-info}/licenses/LICENSE +0 -0
  108. {julee-0.1.5.dist-info → julee-0.1.7.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,253 @@
1
+ """Tests for MemoryCodeInfoRepository."""
2
+
3
+ import pytest
4
+ import pytest_asyncio
5
+
6
+ from julee.docs.sphinx_hcd.domain.models.code_info import (
7
+ BoundedContextInfo,
8
+ ClassInfo,
9
+ )
10
+ from julee.docs.sphinx_hcd.repositories.memory.code_info import (
11
+ MemoryCodeInfoRepository,
12
+ )
13
+
14
+
15
+ def create_class_info(name: str, file: str = "test.py") -> ClassInfo:
16
+ """Helper to create ClassInfo."""
17
+ return ClassInfo(name=name, docstring=f"{name} class", file=file)
18
+
19
+
20
+ def create_context_info(
21
+ slug: str = "test-context",
22
+ entities: list[ClassInfo] | None = None,
23
+ use_cases: list[ClassInfo] | None = None,
24
+ repository_protocols: list[ClassInfo] | None = None,
25
+ service_protocols: list[ClassInfo] | None = None,
26
+ has_infrastructure: bool = False,
27
+ code_dir: str = "",
28
+ ) -> BoundedContextInfo:
29
+ """Helper to create test context info."""
30
+ return BoundedContextInfo(
31
+ slug=slug,
32
+ entities=entities or [],
33
+ use_cases=use_cases or [],
34
+ repository_protocols=repository_protocols or [],
35
+ service_protocols=service_protocols or [],
36
+ has_infrastructure=has_infrastructure,
37
+ code_dir=code_dir or slug,
38
+ )
39
+
40
+
41
+ class TestMemoryCodeInfoRepositoryBasicOperations:
42
+ """Test basic CRUD operations."""
43
+
44
+ @pytest.fixture
45
+ def repo(self) -> MemoryCodeInfoRepository:
46
+ """Create a fresh repository."""
47
+ return MemoryCodeInfoRepository()
48
+
49
+ @pytest.mark.asyncio
50
+ async def test_save_and_get(self, repo: MemoryCodeInfoRepository) -> None:
51
+ """Test saving and retrieving context info."""
52
+ info = create_context_info(slug="vocabulary")
53
+ await repo.save(info)
54
+
55
+ retrieved = await repo.get("vocabulary")
56
+ assert retrieved is not None
57
+ assert retrieved.slug == "vocabulary"
58
+
59
+ @pytest.mark.asyncio
60
+ async def test_get_nonexistent(self, repo: MemoryCodeInfoRepository) -> None:
61
+ """Test getting nonexistent context info returns None."""
62
+ result = await repo.get("nonexistent")
63
+ assert result is None
64
+
65
+ @pytest.mark.asyncio
66
+ async def test_list_all(self, repo: MemoryCodeInfoRepository) -> None:
67
+ """Test listing all context infos."""
68
+ await repo.save(create_context_info(slug="context-1"))
69
+ await repo.save(create_context_info(slug="context-2"))
70
+ await repo.save(create_context_info(slug="context-3"))
71
+
72
+ all_infos = await repo.list_all()
73
+ assert len(all_infos) == 3
74
+ slugs = {i.slug for i in all_infos}
75
+ assert slugs == {"context-1", "context-2", "context-3"}
76
+
77
+ @pytest.mark.asyncio
78
+ async def test_delete(self, repo: MemoryCodeInfoRepository) -> None:
79
+ """Test deleting context info."""
80
+ await repo.save(create_context_info(slug="to-delete"))
81
+ assert await repo.get("to-delete") is not None
82
+
83
+ result = await repo.delete("to-delete")
84
+ assert result is True
85
+ assert await repo.get("to-delete") is None
86
+
87
+ @pytest.mark.asyncio
88
+ async def test_delete_nonexistent(self, repo: MemoryCodeInfoRepository) -> None:
89
+ """Test deleting nonexistent context info."""
90
+ result = await repo.delete("nonexistent")
91
+ assert result is False
92
+
93
+ @pytest.mark.asyncio
94
+ async def test_clear(self, repo: MemoryCodeInfoRepository) -> None:
95
+ """Test clearing all context infos."""
96
+ await repo.save(create_context_info(slug="context-1"))
97
+ await repo.save(create_context_info(slug="context-2"))
98
+ assert len(await repo.list_all()) == 2
99
+
100
+ await repo.clear()
101
+ assert len(await repo.list_all()) == 0
102
+
103
+
104
+ class TestMemoryCodeInfoRepositoryQueries:
105
+ """Test code info-specific query methods."""
106
+
107
+ @pytest.fixture
108
+ def repo(self) -> MemoryCodeInfoRepository:
109
+ """Create a repository."""
110
+ return MemoryCodeInfoRepository()
111
+
112
+ @pytest_asyncio.fixture
113
+ async def populated_repo(
114
+ self, repo: MemoryCodeInfoRepository
115
+ ) -> MemoryCodeInfoRepository:
116
+ """Create a repository with sample context infos."""
117
+ contexts = [
118
+ create_context_info(
119
+ slug="vocabulary",
120
+ entities=[
121
+ create_class_info("Vocabulary", "vocabulary.py"),
122
+ create_class_info("Term", "term.py"),
123
+ ],
124
+ use_cases=[
125
+ create_class_info("CreateVocabulary", "create.py"),
126
+ create_class_info("PublishVocabulary", "publish.py"),
127
+ ],
128
+ repository_protocols=[
129
+ create_class_info("VocabularyRepository", "vocabulary.py"),
130
+ ],
131
+ has_infrastructure=True,
132
+ code_dir="vocabulary",
133
+ ),
134
+ create_context_info(
135
+ slug="traceability",
136
+ entities=[
137
+ create_class_info("TraceLink", "trace_link.py"),
138
+ ],
139
+ use_cases=[
140
+ create_class_info("CreateTraceLink", "create.py"),
141
+ ],
142
+ has_infrastructure=True,
143
+ code_dir="traceability",
144
+ ),
145
+ create_context_info(
146
+ slug="conformity",
147
+ entities=[
148
+ create_class_info("Assessment", "assessment.py"),
149
+ ],
150
+ # No use cases
151
+ has_infrastructure=False,
152
+ code_dir="conformity",
153
+ ),
154
+ create_context_info(
155
+ slug="empty-context",
156
+ # No entities, no use cases
157
+ has_infrastructure=False,
158
+ code_dir="empty_context",
159
+ ),
160
+ ]
161
+ for ctx in contexts:
162
+ await repo.save(ctx)
163
+ return repo
164
+
165
+ @pytest.mark.asyncio
166
+ async def test_get_by_code_dir(
167
+ self, populated_repo: MemoryCodeInfoRepository
168
+ ) -> None:
169
+ """Test getting context info by code directory."""
170
+ info = await populated_repo.get_by_code_dir("vocabulary")
171
+ assert info is not None
172
+ assert info.slug == "vocabulary"
173
+
174
+ @pytest.mark.asyncio
175
+ async def test_get_by_code_dir_different_name(
176
+ self, populated_repo: MemoryCodeInfoRepository
177
+ ) -> None:
178
+ """Test getting context info where code_dir differs from slug."""
179
+ info = await populated_repo.get_by_code_dir("empty_context")
180
+ assert info is not None
181
+ assert info.slug == "empty-context"
182
+
183
+ @pytest.mark.asyncio
184
+ async def test_get_by_code_dir_not_found(
185
+ self, populated_repo: MemoryCodeInfoRepository
186
+ ) -> None:
187
+ """Test getting context info for unknown code directory."""
188
+ info = await populated_repo.get_by_code_dir("unknown")
189
+ assert info is None
190
+
191
+ @pytest.mark.asyncio
192
+ async def test_get_with_entities(
193
+ self, populated_repo: MemoryCodeInfoRepository
194
+ ) -> None:
195
+ """Test getting contexts with entities."""
196
+ contexts = await populated_repo.get_with_entities()
197
+ assert len(contexts) == 3
198
+ slugs = {c.slug for c in contexts}
199
+ assert slugs == {"vocabulary", "traceability", "conformity"}
200
+
201
+ @pytest.mark.asyncio
202
+ async def test_get_with_use_cases(
203
+ self, populated_repo: MemoryCodeInfoRepository
204
+ ) -> None:
205
+ """Test getting contexts with use cases."""
206
+ contexts = await populated_repo.get_with_use_cases()
207
+ assert len(contexts) == 2
208
+ slugs = {c.slug for c in contexts}
209
+ assert slugs == {"vocabulary", "traceability"}
210
+
211
+ @pytest.mark.asyncio
212
+ async def test_get_with_infrastructure(
213
+ self, populated_repo: MemoryCodeInfoRepository
214
+ ) -> None:
215
+ """Test getting contexts with infrastructure."""
216
+ contexts = await populated_repo.get_with_infrastructure()
217
+ assert len(contexts) == 2
218
+ slugs = {c.slug for c in contexts}
219
+ assert slugs == {"vocabulary", "traceability"}
220
+
221
+ @pytest.mark.asyncio
222
+ async def test_get_all_entity_names(
223
+ self, populated_repo: MemoryCodeInfoRepository
224
+ ) -> None:
225
+ """Test getting all unique entity names."""
226
+ names = await populated_repo.get_all_entity_names()
227
+ expected = {"Vocabulary", "Term", "TraceLink", "Assessment"}
228
+ assert names == expected
229
+
230
+ @pytest.mark.asyncio
231
+ async def test_get_all_entity_names_empty_repo(
232
+ self, repo: MemoryCodeInfoRepository
233
+ ) -> None:
234
+ """Test getting entity names from empty repository."""
235
+ names = await repo.get_all_entity_names()
236
+ assert names == set()
237
+
238
+ @pytest.mark.asyncio
239
+ async def test_get_all_use_case_names(
240
+ self, populated_repo: MemoryCodeInfoRepository
241
+ ) -> None:
242
+ """Test getting all unique use case names."""
243
+ names = await populated_repo.get_all_use_case_names()
244
+ expected = {"CreateVocabulary", "PublishVocabulary", "CreateTraceLink"}
245
+ assert names == expected
246
+
247
+ @pytest.mark.asyncio
248
+ async def test_get_all_use_case_names_empty_repo(
249
+ self, repo: MemoryCodeInfoRepository
250
+ ) -> None:
251
+ """Test getting use case names from empty repository."""
252
+ names = await repo.get_all_use_case_names()
253
+ assert names == set()
@@ -0,0 +1,237 @@
1
+ """Tests for MemoryEpicRepository."""
2
+
3
+ import pytest
4
+ import pytest_asyncio
5
+
6
+ from julee.docs.sphinx_hcd.domain.models.epic import Epic
7
+ from julee.docs.sphinx_hcd.repositories.memory.epic import MemoryEpicRepository
8
+
9
+
10
+ def create_epic(
11
+ slug: str = "test-epic",
12
+ description: str = "Test description",
13
+ docname: str = "epics/test",
14
+ story_refs: list[str] | None = None,
15
+ ) -> Epic:
16
+ """Helper to create test epics."""
17
+ return Epic(
18
+ slug=slug,
19
+ description=description,
20
+ docname=docname,
21
+ story_refs=story_refs or [],
22
+ )
23
+
24
+
25
+ class TestMemoryEpicRepositoryBasicOperations:
26
+ """Test basic CRUD operations."""
27
+
28
+ @pytest.fixture
29
+ def repo(self) -> MemoryEpicRepository:
30
+ """Create a fresh repository."""
31
+ return MemoryEpicRepository()
32
+
33
+ @pytest.mark.asyncio
34
+ async def test_save_and_get(self, repo: MemoryEpicRepository) -> None:
35
+ """Test saving and retrieving an epic."""
36
+ epic = create_epic(slug="vocabulary-management")
37
+ await repo.save(epic)
38
+
39
+ retrieved = await repo.get("vocabulary-management")
40
+ assert retrieved is not None
41
+ assert retrieved.slug == "vocabulary-management"
42
+
43
+ @pytest.mark.asyncio
44
+ async def test_get_nonexistent(self, repo: MemoryEpicRepository) -> None:
45
+ """Test getting a nonexistent epic returns None."""
46
+ result = await repo.get("nonexistent")
47
+ assert result is None
48
+
49
+ @pytest.mark.asyncio
50
+ async def test_list_all(self, repo: MemoryEpicRepository) -> None:
51
+ """Test listing all epics."""
52
+ await repo.save(create_epic(slug="epic-1"))
53
+ await repo.save(create_epic(slug="epic-2"))
54
+ await repo.save(create_epic(slug="epic-3"))
55
+
56
+ all_epics = await repo.list_all()
57
+ assert len(all_epics) == 3
58
+ slugs = {e.slug for e in all_epics}
59
+ assert slugs == {"epic-1", "epic-2", "epic-3"}
60
+
61
+ @pytest.mark.asyncio
62
+ async def test_delete(self, repo: MemoryEpicRepository) -> None:
63
+ """Test deleting an epic."""
64
+ await repo.save(create_epic(slug="to-delete"))
65
+ assert await repo.get("to-delete") is not None
66
+
67
+ result = await repo.delete("to-delete")
68
+ assert result is True
69
+ assert await repo.get("to-delete") is None
70
+
71
+ @pytest.mark.asyncio
72
+ async def test_delete_nonexistent(self, repo: MemoryEpicRepository) -> None:
73
+ """Test deleting a nonexistent epic."""
74
+ result = await repo.delete("nonexistent")
75
+ assert result is False
76
+
77
+ @pytest.mark.asyncio
78
+ async def test_clear(self, repo: MemoryEpicRepository) -> None:
79
+ """Test clearing all epics."""
80
+ await repo.save(create_epic(slug="epic-1"))
81
+ await repo.save(create_epic(slug="epic-2"))
82
+ assert len(await repo.list_all()) == 2
83
+
84
+ await repo.clear()
85
+ assert len(await repo.list_all()) == 0
86
+
87
+
88
+ class TestMemoryEpicRepositoryQueries:
89
+ """Test epic-specific query methods."""
90
+
91
+ @pytest.fixture
92
+ def repo(self) -> MemoryEpicRepository:
93
+ """Create a repository."""
94
+ return MemoryEpicRepository()
95
+
96
+ @pytest_asyncio.fixture
97
+ async def populated_repo(self, repo: MemoryEpicRepository) -> MemoryEpicRepository:
98
+ """Create a repository with sample epics."""
99
+ epics = [
100
+ create_epic(
101
+ slug="vocabulary-management",
102
+ description="Manage vocabulary catalogs",
103
+ docname="epics/vocabulary",
104
+ story_refs=["Upload Document", "Review Vocabulary", "Publish Catalog"],
105
+ ),
106
+ create_epic(
107
+ slug="credential-creation",
108
+ description="Create credentials",
109
+ docname="epics/credentials",
110
+ story_refs=["Create Credential", "Assign Credential"],
111
+ ),
112
+ create_epic(
113
+ slug="pipeline-operations",
114
+ description="Operate data pipelines",
115
+ docname="epics/pipelines",
116
+ story_refs=["Configure Pipeline", "Run Pipeline"],
117
+ ),
118
+ create_epic(
119
+ slug="analytics",
120
+ description="Analytics features",
121
+ docname="epics/vocabulary", # Same docname as vocabulary-management
122
+ story_refs=["Review Vocabulary", "Generate Report"],
123
+ ),
124
+ create_epic(
125
+ slug="empty-epic",
126
+ description="Epic with no stories",
127
+ docname="epics/empty",
128
+ ),
129
+ ]
130
+ for epic in epics:
131
+ await repo.save(epic)
132
+ return repo
133
+
134
+ @pytest.mark.asyncio
135
+ async def test_get_by_docname(self, populated_repo: MemoryEpicRepository) -> None:
136
+ """Test getting epics by document name."""
137
+ epics = await populated_repo.get_by_docname("epics/vocabulary")
138
+ assert len(epics) == 2
139
+ slugs = {e.slug for e in epics}
140
+ assert slugs == {"vocabulary-management", "analytics"}
141
+
142
+ @pytest.mark.asyncio
143
+ async def test_get_by_docname_single(
144
+ self, populated_repo: MemoryEpicRepository
145
+ ) -> None:
146
+ """Test getting epics for document with one epic."""
147
+ epics = await populated_repo.get_by_docname("epics/credentials")
148
+ assert len(epics) == 1
149
+ assert epics[0].slug == "credential-creation"
150
+
151
+ @pytest.mark.asyncio
152
+ async def test_get_by_docname_no_results(
153
+ self, populated_repo: MemoryEpicRepository
154
+ ) -> None:
155
+ """Test getting epics for unknown document."""
156
+ epics = await populated_repo.get_by_docname("unknown/document")
157
+ assert len(epics) == 0
158
+
159
+ @pytest.mark.asyncio
160
+ async def test_clear_by_docname(self, populated_repo: MemoryEpicRepository) -> None:
161
+ """Test clearing epics by document name."""
162
+ count = await populated_repo.clear_by_docname("epics/vocabulary")
163
+ assert count == 2
164
+ assert await populated_repo.get("vocabulary-management") is None
165
+ assert await populated_repo.get("analytics") is None
166
+ # Other epics should remain
167
+ assert len(await populated_repo.list_all()) == 3
168
+
169
+ @pytest.mark.asyncio
170
+ async def test_clear_by_docname_none_found(
171
+ self, populated_repo: MemoryEpicRepository
172
+ ) -> None:
173
+ """Test clearing non-existent document returns 0."""
174
+ count = await populated_repo.clear_by_docname("unknown/document")
175
+ assert count == 0
176
+
177
+ @pytest.mark.asyncio
178
+ async def test_get_with_story_ref(
179
+ self, populated_repo: MemoryEpicRepository
180
+ ) -> None:
181
+ """Test getting epics with a story reference."""
182
+ epics = await populated_repo.get_with_story_ref("Upload Document")
183
+ assert len(epics) == 1
184
+ assert epics[0].slug == "vocabulary-management"
185
+
186
+ @pytest.mark.asyncio
187
+ async def test_get_with_story_ref_multiple(
188
+ self, populated_repo: MemoryEpicRepository
189
+ ) -> None:
190
+ """Test getting epics with a story in multiple epics."""
191
+ epics = await populated_repo.get_with_story_ref("Review Vocabulary")
192
+ assert len(epics) == 2
193
+ slugs = {e.slug for e in epics}
194
+ assert slugs == {"vocabulary-management", "analytics"}
195
+
196
+ @pytest.mark.asyncio
197
+ async def test_get_with_story_ref_case_insensitive(
198
+ self, populated_repo: MemoryEpicRepository
199
+ ) -> None:
200
+ """Test story ref matching is case-insensitive."""
201
+ epics = await populated_repo.get_with_story_ref("upload document")
202
+ assert len(epics) == 1
203
+ assert epics[0].slug == "vocabulary-management"
204
+
205
+ @pytest.mark.asyncio
206
+ async def test_get_with_story_ref_no_results(
207
+ self, populated_repo: MemoryEpicRepository
208
+ ) -> None:
209
+ """Test getting epics with nonexistent story."""
210
+ epics = await populated_repo.get_with_story_ref("Unknown Story")
211
+ assert len(epics) == 0
212
+
213
+ @pytest.mark.asyncio
214
+ async def test_get_all_story_refs(
215
+ self, populated_repo: MemoryEpicRepository
216
+ ) -> None:
217
+ """Test getting all unique story references."""
218
+ refs = await populated_repo.get_all_story_refs()
219
+ expected = {
220
+ "upload document",
221
+ "review vocabulary",
222
+ "publish catalog",
223
+ "create credential",
224
+ "assign credential",
225
+ "configure pipeline",
226
+ "run pipeline",
227
+ "generate report",
228
+ }
229
+ assert refs == expected
230
+
231
+ @pytest.mark.asyncio
232
+ async def test_get_all_story_refs_empty_repo(
233
+ self, repo: MemoryEpicRepository
234
+ ) -> None:
235
+ """Test getting story refs from empty repository."""
236
+ refs = await repo.get_all_story_refs()
237
+ assert refs == set()