anvil-dev-framework 0.1.6

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 (190) hide show
  1. package/README.md +719 -0
  2. package/VERSION +1 -0
  3. package/docs/ANVIL-REPO-IMPLEMENTATION-PLAN.md +441 -0
  4. package/docs/FIRST-SKILL-TUTORIAL.md +408 -0
  5. package/docs/INSTALLATION-RETRO-NOTES.md +458 -0
  6. package/docs/INSTALLATION.md +984 -0
  7. package/docs/anvil-hud.md +469 -0
  8. package/docs/anvil-init.md +255 -0
  9. package/docs/anvil-state.md +210 -0
  10. package/docs/boris-cherny-ralph-wiggum-insights.md +608 -0
  11. package/docs/command-reference.md +2022 -0
  12. package/docs/hooks-tts.md +368 -0
  13. package/docs/implementation-guide.md +810 -0
  14. package/docs/linear-github-integration.md +247 -0
  15. package/docs/local-issues.md +677 -0
  16. package/docs/patterns/README.md +419 -0
  17. package/docs/planning-responsibilities.md +139 -0
  18. package/docs/session-workflow.md +573 -0
  19. package/docs/simplification-plan-template.md +297 -0
  20. package/docs/simplification-principles.md +129 -0
  21. package/docs/specifications/CCS-RALPH-INTEGRATION-DESIGN.md +633 -0
  22. package/docs/specifications/CCS-RESEARCH-REPORT.md +169 -0
  23. package/docs/specifications/PLAN-ANV-verification-ralph-wiggum.md +403 -0
  24. package/docs/specifications/PLAN-parallel-tracks-anvil-memory-ccs.md +494 -0
  25. package/docs/specifications/SPEC-ANV-VRW/component-01-verify.md +208 -0
  26. package/docs/specifications/SPEC-ANV-VRW/component-02-stop-gate.md +226 -0
  27. package/docs/specifications/SPEC-ANV-VRW/component-03-posttooluse.md +209 -0
  28. package/docs/specifications/SPEC-ANV-VRW/component-04-ralph-wiggum.md +604 -0
  29. package/docs/specifications/SPEC-ANV-VRW/component-05-atomic-actions.md +311 -0
  30. package/docs/specifications/SPEC-ANV-VRW/component-06-verify-subagent.md +264 -0
  31. package/docs/specifications/SPEC-ANV-VRW/component-07-claude-md.md +363 -0
  32. package/docs/specifications/SPEC-ANV-VRW/index.md +182 -0
  33. package/docs/specifications/SPEC-ANV-anvil-memory.md +573 -0
  34. package/docs/specifications/SPEC-ANV-context-checkpoints.md +781 -0
  35. package/docs/specifications/SPEC-ANV-verification-ralph-wiggum.md +789 -0
  36. package/docs/sync.md +122 -0
  37. package/global/CLAUDE.md +140 -0
  38. package/global/agents/verify-app.md +164 -0
  39. package/global/commands/anvil-settings.md +527 -0
  40. package/global/commands/anvil-sync.md +121 -0
  41. package/global/commands/change.md +197 -0
  42. package/global/commands/clarify.md +252 -0
  43. package/global/commands/cleanup.md +292 -0
  44. package/global/commands/commit-push-pr.md +207 -0
  45. package/global/commands/decay-review.md +127 -0
  46. package/global/commands/discover.md +158 -0
  47. package/global/commands/doc-coverage.md +122 -0
  48. package/global/commands/evidence.md +307 -0
  49. package/global/commands/explore.md +121 -0
  50. package/global/commands/force-exit.md +135 -0
  51. package/global/commands/handoff.md +191 -0
  52. package/global/commands/healthcheck.md +302 -0
  53. package/global/commands/hud.md +84 -0
  54. package/global/commands/insights.md +319 -0
  55. package/global/commands/linear-setup.md +184 -0
  56. package/global/commands/lint-fix.md +198 -0
  57. package/global/commands/orient.md +510 -0
  58. package/global/commands/plan.md +228 -0
  59. package/global/commands/ralph.md +346 -0
  60. package/global/commands/ready.md +182 -0
  61. package/global/commands/release.md +305 -0
  62. package/global/commands/retro.md +96 -0
  63. package/global/commands/shard.md +166 -0
  64. package/global/commands/spec.md +227 -0
  65. package/global/commands/sprint.md +184 -0
  66. package/global/commands/tasks.md +228 -0
  67. package/global/commands/test-and-commit.md +151 -0
  68. package/global/commands/validate.md +132 -0
  69. package/global/commands/verify.md +251 -0
  70. package/global/commands/weekly-review.md +156 -0
  71. package/global/hooks/__pycache__/ralph_context_monitor.cpython-314.pyc +0 -0
  72. package/global/hooks/__pycache__/statusline_agent_sync.cpython-314.pyc +0 -0
  73. package/global/hooks/anvil_memory_observe.ts +322 -0
  74. package/global/hooks/anvil_memory_session.ts +166 -0
  75. package/global/hooks/anvil_memory_stop.ts +187 -0
  76. package/global/hooks/parse_transcript.py +116 -0
  77. package/global/hooks/post_merge_cleanup.sh +132 -0
  78. package/global/hooks/post_tool_format.sh +215 -0
  79. package/global/hooks/ralph_context_monitor.py +240 -0
  80. package/global/hooks/ralph_stop.sh +502 -0
  81. package/global/hooks/statusline.sh +1110 -0
  82. package/global/hooks/statusline_agent_sync.py +224 -0
  83. package/global/hooks/stop_gate.sh +250 -0
  84. package/global/lib/.claude/anvil-state.json +21 -0
  85. package/global/lib/__pycache__/agent_registry.cpython-314.pyc +0 -0
  86. package/global/lib/__pycache__/claim_service.cpython-314.pyc +0 -0
  87. package/global/lib/__pycache__/coderabbit_service.cpython-314.pyc +0 -0
  88. package/global/lib/__pycache__/config_service.cpython-314.pyc +0 -0
  89. package/global/lib/__pycache__/coordination_service.cpython-314.pyc +0 -0
  90. package/global/lib/__pycache__/doc_coverage_service.cpython-314.pyc +0 -0
  91. package/global/lib/__pycache__/gate_logger.cpython-314.pyc +0 -0
  92. package/global/lib/__pycache__/github_service.cpython-314.pyc +0 -0
  93. package/global/lib/__pycache__/hygiene_service.cpython-314.pyc +0 -0
  94. package/global/lib/__pycache__/issue_models.cpython-314.pyc +0 -0
  95. package/global/lib/__pycache__/issue_provider.cpython-314.pyc +0 -0
  96. package/global/lib/__pycache__/linear_data_service.cpython-314.pyc +0 -0
  97. package/global/lib/__pycache__/linear_provider.cpython-314.pyc +0 -0
  98. package/global/lib/__pycache__/local_provider.cpython-314.pyc +0 -0
  99. package/global/lib/__pycache__/quality_service.cpython-314.pyc +0 -0
  100. package/global/lib/__pycache__/ralph_state.cpython-314.pyc +0 -0
  101. package/global/lib/__pycache__/state_manager.cpython-314.pyc +0 -0
  102. package/global/lib/__pycache__/transcript_parser.cpython-314.pyc +0 -0
  103. package/global/lib/__pycache__/verification_runner.cpython-314.pyc +0 -0
  104. package/global/lib/__pycache__/verify_iteration.cpython-314.pyc +0 -0
  105. package/global/lib/__pycache__/verify_subagent.cpython-314.pyc +0 -0
  106. package/global/lib/agent_registry.py +995 -0
  107. package/global/lib/anvil-state.sh +435 -0
  108. package/global/lib/claim_service.py +515 -0
  109. package/global/lib/coderabbit_service.py +314 -0
  110. package/global/lib/config_service.py +423 -0
  111. package/global/lib/coordination_service.py +331 -0
  112. package/global/lib/doc_coverage_service.py +1305 -0
  113. package/global/lib/gate_logger.py +316 -0
  114. package/global/lib/github_service.py +310 -0
  115. package/global/lib/handoff_generator.py +775 -0
  116. package/global/lib/hygiene_service.py +712 -0
  117. package/global/lib/issue_models.py +257 -0
  118. package/global/lib/issue_provider.py +339 -0
  119. package/global/lib/linear_data_service.py +210 -0
  120. package/global/lib/linear_provider.py +987 -0
  121. package/global/lib/linear_provider.py.backup +671 -0
  122. package/global/lib/local_provider.py +486 -0
  123. package/global/lib/orient_fast.py +457 -0
  124. package/global/lib/quality_service.py +470 -0
  125. package/global/lib/ralph_prompt_generator.py +563 -0
  126. package/global/lib/ralph_state.py +1202 -0
  127. package/global/lib/state_manager.py +417 -0
  128. package/global/lib/transcript_parser.py +597 -0
  129. package/global/lib/verification_runner.py +557 -0
  130. package/global/lib/verify_iteration.py +490 -0
  131. package/global/lib/verify_subagent.py +250 -0
  132. package/global/skills/README.md +155 -0
  133. package/global/skills/quality-gates/SKILL.md +252 -0
  134. package/global/skills/skill-template/SKILL.md +109 -0
  135. package/global/skills/testing-strategies/SKILL.md +337 -0
  136. package/global/templates/CHANGE-template.md +105 -0
  137. package/global/templates/HANDOFF-template.md +63 -0
  138. package/global/templates/PLAN-template.md +111 -0
  139. package/global/templates/SPEC-template.md +93 -0
  140. package/global/templates/ralph/PROMPT.md.template +89 -0
  141. package/global/templates/ralph/fix_plan.md.template +31 -0
  142. package/global/templates/ralph/progress.txt.template +23 -0
  143. package/global/tests/__pycache__/test_doc_coverage.cpython-314.pyc +0 -0
  144. package/global/tests/test_doc_coverage.py +520 -0
  145. package/global/tests/test_issue_models.py +299 -0
  146. package/global/tests/test_local_provider.py +323 -0
  147. package/global/tools/README.md +178 -0
  148. package/global/tools/__pycache__/anvil-hud.cpython-314.pyc +0 -0
  149. package/global/tools/anvil-hud.py +3622 -0
  150. package/global/tools/anvil-hud.py.bak +3318 -0
  151. package/global/tools/anvil-issue.py +432 -0
  152. package/global/tools/anvil-memory/CLAUDE.md +49 -0
  153. package/global/tools/anvil-memory/README.md +42 -0
  154. package/global/tools/anvil-memory/bun.lock +25 -0
  155. package/global/tools/anvil-memory/bunfig.toml +9 -0
  156. package/global/tools/anvil-memory/package.json +23 -0
  157. package/global/tools/anvil-memory/src/__tests__/ccs/context-monitor.test.ts +535 -0
  158. package/global/tools/anvil-memory/src/__tests__/ccs/edge-cases.test.ts +645 -0
  159. package/global/tools/anvil-memory/src/__tests__/ccs/fixtures.ts +363 -0
  160. package/global/tools/anvil-memory/src/__tests__/ccs/index.ts +8 -0
  161. package/global/tools/anvil-memory/src/__tests__/ccs/integration.test.ts +417 -0
  162. package/global/tools/anvil-memory/src/__tests__/ccs/prompt-generator.test.ts +571 -0
  163. package/global/tools/anvil-memory/src/__tests__/ccs/ralph-stop.test.ts +440 -0
  164. package/global/tools/anvil-memory/src/__tests__/ccs/test-utils.ts +252 -0
  165. package/global/tools/anvil-memory/src/__tests__/commands.test.ts +657 -0
  166. package/global/tools/anvil-memory/src/__tests__/db.test.ts +641 -0
  167. package/global/tools/anvil-memory/src/__tests__/hooks.test.ts +272 -0
  168. package/global/tools/anvil-memory/src/__tests__/performance.test.ts +427 -0
  169. package/global/tools/anvil-memory/src/__tests__/test-utils.ts +113 -0
  170. package/global/tools/anvil-memory/src/commands/checkpoint.ts +197 -0
  171. package/global/tools/anvil-memory/src/commands/get.ts +115 -0
  172. package/global/tools/anvil-memory/src/commands/init.ts +94 -0
  173. package/global/tools/anvil-memory/src/commands/observe.ts +163 -0
  174. package/global/tools/anvil-memory/src/commands/search.ts +112 -0
  175. package/global/tools/anvil-memory/src/db.ts +638 -0
  176. package/global/tools/anvil-memory/src/index.ts +205 -0
  177. package/global/tools/anvil-memory/src/types.ts +122 -0
  178. package/global/tools/anvil-memory/tsconfig.json +29 -0
  179. package/global/tools/ralph-loop.sh +359 -0
  180. package/package.json +45 -0
  181. package/scripts/anvil +822 -0
  182. package/scripts/extract_patterns.py +222 -0
  183. package/scripts/init-project.sh +541 -0
  184. package/scripts/install.sh +229 -0
  185. package/scripts/postinstall.js +41 -0
  186. package/scripts/rollback.sh +188 -0
  187. package/scripts/sync.sh +623 -0
  188. package/scripts/test-statusline.sh +248 -0
  189. package/scripts/update_claude_md.py +224 -0
  190. package/scripts/verify.sh +255 -0
@@ -0,0 +1,299 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Unit tests for issue_models.py
4
+
5
+ Run with: python3 -m pytest global/tests/test_issue_models.py -v
6
+ Or: python3 global/tests/test_issue_models.py
7
+ """
8
+
9
+ import sys
10
+ from pathlib import Path
11
+ import json
12
+
13
+ # Add parent directory to path for imports
14
+ sys.path.insert(0, str(Path(__file__).parent.parent / "lib"))
15
+
16
+ from issue_models import Issue, IssueStatus, Priority
17
+
18
+
19
+ class TestIssueStatus:
20
+ """Tests for IssueStatus enum."""
21
+
22
+ def test_from_linear_state_todo(self):
23
+ assert IssueStatus.from_linear_state("Todo") == IssueStatus.TODO
24
+ assert IssueStatus.from_linear_state("todo") == IssueStatus.TODO
25
+
26
+ def test_from_linear_state_in_progress(self):
27
+ assert IssueStatus.from_linear_state("In Progress") == IssueStatus.IN_PROGRESS
28
+ assert IssueStatus.from_linear_state("in progress") == IssueStatus.IN_PROGRESS
29
+
30
+ def test_from_linear_state_done(self):
31
+ assert IssueStatus.from_linear_state("Done") == IssueStatus.DONE
32
+ assert IssueStatus.from_linear_state("done") == IssueStatus.DONE
33
+
34
+ def test_from_linear_state_cancelled(self):
35
+ assert IssueStatus.from_linear_state("Canceled") == IssueStatus.CANCELLED
36
+ assert IssueStatus.from_linear_state("Cancelled") == IssueStatus.CANCELLED
37
+ assert IssueStatus.from_linear_state("Duplicate") == IssueStatus.CANCELLED
38
+
39
+ def test_from_linear_state_unknown_defaults_to_backlog(self):
40
+ assert IssueStatus.from_linear_state("Unknown State") == IssueStatus.BACKLOG
41
+
42
+ def test_to_display(self):
43
+ assert IssueStatus.TODO.to_display() == "Todo"
44
+ assert IssueStatus.IN_PROGRESS.to_display() == "In Progress"
45
+ assert IssueStatus.IN_REVIEW.to_display() == "In Review"
46
+
47
+
48
+ class TestPriority:
49
+ """Tests for Priority enum."""
50
+
51
+ def test_from_linear_priority(self):
52
+ assert Priority.from_linear_priority(0) == Priority.URGENT
53
+ assert Priority.from_linear_priority(1) == Priority.HIGH
54
+ assert Priority.from_linear_priority(2) == Priority.MEDIUM
55
+ assert Priority.from_linear_priority(3) == Priority.LOW
56
+ assert Priority.from_linear_priority(4) == Priority.NONE
57
+
58
+ def test_from_linear_priority_invalid_defaults_to_none(self):
59
+ assert Priority.from_linear_priority(99) == Priority.NONE
60
+ assert Priority.from_linear_priority(-1) == Priority.NONE
61
+
62
+ def test_to_display(self):
63
+ assert Priority.URGENT.to_display() == "P0"
64
+ assert Priority.HIGH.to_display() == "P1"
65
+ assert Priority.MEDIUM.to_display() == "P2"
66
+ assert Priority.LOW.to_display() == "P3"
67
+ assert Priority.NONE.to_display() == "—"
68
+
69
+
70
+ class TestIssue:
71
+ """Tests for Issue dataclass."""
72
+
73
+ def test_create_minimal_issue(self):
74
+ issue = Issue(
75
+ id="test-001",
76
+ identifier="TEST-001",
77
+ title="Test issue"
78
+ )
79
+ assert issue.id == "test-001"
80
+ assert issue.identifier == "TEST-001"
81
+ assert issue.title == "Test issue"
82
+ assert issue.status == IssueStatus.TODO
83
+ assert issue.priority == Priority.MEDIUM
84
+ assert issue.provider == "local"
85
+
86
+ def test_create_full_issue(self):
87
+ issue = Issue(
88
+ id="test-002",
89
+ identifier="TEST-002",
90
+ title="Full test issue",
91
+ description="Detailed description",
92
+ status=IssueStatus.IN_PROGRESS,
93
+ priority=Priority.HIGH,
94
+ parent_id="parent-001",
95
+ labels=["bug", "urgent"],
96
+ assigned_agent="agent-xyz",
97
+ estimate="2h",
98
+ provider="linear",
99
+ external_id="linear-uuid-123"
100
+ )
101
+ assert issue.description == "Detailed description"
102
+ assert issue.status == IssueStatus.IN_PROGRESS
103
+ assert issue.priority == Priority.HIGH
104
+ assert issue.parent_id == "parent-001"
105
+ assert issue.labels == ["bug", "urgent"]
106
+ assert issue.assigned_agent == "agent-xyz"
107
+ assert issue.estimate == "2h"
108
+ assert issue.provider == "linear"
109
+ assert issue.external_id == "linear-uuid-123"
110
+
111
+ def test_to_dict(self):
112
+ issue = Issue(
113
+ id="test-003",
114
+ identifier="TEST-003",
115
+ title="Dict test"
116
+ )
117
+ data = issue.to_dict()
118
+ assert data["id"] == "test-003"
119
+ assert data["identifier"] == "TEST-003"
120
+ assert data["title"] == "Dict test"
121
+ assert data["status"] == "todo"
122
+ assert data["priority"] == 2
123
+ assert isinstance(data["created_at"], str)
124
+ assert isinstance(data["updated_at"], str)
125
+
126
+ def test_to_json(self):
127
+ issue = Issue(
128
+ id="test-004",
129
+ identifier="TEST-004",
130
+ title="JSON test"
131
+ )
132
+ json_str = issue.to_json()
133
+ parsed = json.loads(json_str)
134
+ assert parsed["identifier"] == "TEST-004"
135
+
136
+ def test_from_dict(self):
137
+ data = {
138
+ "id": "test-005",
139
+ "identifier": "TEST-005",
140
+ "title": "From dict test",
141
+ "status": "in_progress",
142
+ "priority": 1,
143
+ "labels": ["feature"]
144
+ }
145
+ issue = Issue.from_dict(data)
146
+ assert issue.id == "test-005"
147
+ assert issue.identifier == "TEST-005"
148
+ assert issue.status == IssueStatus.IN_PROGRESS
149
+ assert issue.priority == Priority.HIGH
150
+ assert issue.labels == ["feature"]
151
+
152
+ def test_from_json(self):
153
+ json_str = '{"id": "test-006", "identifier": "TEST-006", "title": "From JSON"}'
154
+ issue = Issue.from_json(json_str)
155
+ assert issue.identifier == "TEST-006"
156
+ assert issue.title == "From JSON"
157
+
158
+ def test_roundtrip_serialization(self):
159
+ original = Issue(
160
+ id="test-007",
161
+ identifier="TEST-007",
162
+ title="Roundtrip test",
163
+ description="Test description",
164
+ status=IssueStatus.IN_REVIEW,
165
+ priority=Priority.URGENT,
166
+ labels=["test", "roundtrip"]
167
+ )
168
+ json_str = original.to_json()
169
+ restored = Issue.from_json(json_str)
170
+ assert restored.id == original.id
171
+ assert restored.identifier == original.identifier
172
+ assert restored.title == original.title
173
+ assert restored.description == original.description
174
+ assert restored.status == original.status
175
+ assert restored.priority == original.priority
176
+ assert restored.labels == original.labels
177
+
178
+ def test_from_linear(self):
179
+ linear_data = {
180
+ "id": "linear-uuid-123",
181
+ "identifier": "ANV-72",
182
+ "title": "HUD Kanban",
183
+ "description": "Implement Kanban board",
184
+ "state": {"name": "In Progress"},
185
+ "priority": 1,
186
+ "labels": {"nodes": [{"name": "feature"}, {"name": "hud"}]},
187
+ "createdAt": "2026-01-03T00:00:00Z",
188
+ "updatedAt": "2026-01-03T01:00:00Z"
189
+ }
190
+ issue = Issue.from_linear(linear_data)
191
+ assert issue.id == "linear-uuid-123"
192
+ assert issue.identifier == "ANV-72"
193
+ assert issue.title == "HUD Kanban"
194
+ assert issue.status == IssueStatus.IN_PROGRESS
195
+ assert issue.priority == Priority.HIGH
196
+ assert issue.labels == ["feature", "hud"]
197
+ assert issue.provider == "linear"
198
+ assert issue.external_id == "linear-uuid-123"
199
+
200
+ def test_is_ready(self):
201
+ todo_issue = Issue(id="1", identifier="T-1", title="Todo", status=IssueStatus.TODO)
202
+ wip_issue = Issue(id="2", identifier="T-2", title="WIP", status=IssueStatus.IN_PROGRESS)
203
+ assert todo_issue.is_ready() is True
204
+ assert wip_issue.is_ready() is False
205
+
206
+ def test_is_active(self):
207
+ todo = Issue(id="1", identifier="T-1", title="Todo", status=IssueStatus.TODO)
208
+ wip = Issue(id="2", identifier="T-2", title="WIP", status=IssueStatus.IN_PROGRESS)
209
+ review = Issue(id="3", identifier="T-3", title="Review", status=IssueStatus.IN_REVIEW)
210
+ done = Issue(id="4", identifier="T-4", title="Done", status=IssueStatus.DONE)
211
+ assert todo.is_active() is False
212
+ assert wip.is_active() is True
213
+ assert review.is_active() is True
214
+ assert done.is_active() is False
215
+
216
+ def test_is_complete(self):
217
+ wip = Issue(id="1", identifier="T-1", title="WIP", status=IssueStatus.IN_PROGRESS)
218
+ done = Issue(id="2", identifier="T-2", title="Done", status=IssueStatus.DONE)
219
+ cancelled = Issue(id="3", identifier="T-3", title="Cancelled", status=IssueStatus.CANCELLED)
220
+ assert wip.is_complete() is False
221
+ assert done.is_complete() is True
222
+ assert cancelled.is_complete() is True
223
+
224
+ def test_claim_and_unclaim(self):
225
+ issue = Issue(id="1", identifier="T-1", title="Test")
226
+ assert issue.assigned_agent is None
227
+
228
+ issue.claim("agent-123")
229
+ assert issue.assigned_agent == "agent-123"
230
+
231
+ issue.unclaim()
232
+ assert issue.assigned_agent is None
233
+
234
+ def test_transition_to(self):
235
+ issue = Issue(id="1", identifier="T-1", title="Test", status=IssueStatus.TODO)
236
+
237
+ issue.transition_to(IssueStatus.IN_PROGRESS)
238
+ assert issue.status == IssueStatus.IN_PROGRESS
239
+ assert issue.completed_at is None
240
+
241
+ issue.transition_to(IssueStatus.DONE)
242
+ assert issue.status == IssueStatus.DONE
243
+ assert issue.completed_at is not None
244
+
245
+ # Reopen clears completed_at
246
+ issue.transition_to(IssueStatus.TODO)
247
+ assert issue.status == IssueStatus.TODO
248
+ assert issue.completed_at is None
249
+
250
+ def test_str_representation(self):
251
+ issue = Issue(
252
+ id="1",
253
+ identifier="TEST-001",
254
+ title="Sample issue",
255
+ status=IssueStatus.IN_PROGRESS
256
+ )
257
+ assert "TEST-001" in str(issue)
258
+ assert "Sample issue" in str(issue)
259
+ assert "In Progress" in str(issue)
260
+
261
+ def test_str_with_agent(self):
262
+ issue = Issue(
263
+ id="1",
264
+ identifier="TEST-001",
265
+ title="Sample",
266
+ assigned_agent="agent-xyz"
267
+ )
268
+ assert "[agent-xyz]" in str(issue)
269
+
270
+
271
+ def run_tests():
272
+ """Run all tests manually if pytest not available."""
273
+ test_classes = [TestIssueStatus, TestPriority, TestIssue]
274
+ passed = 0
275
+ failed = 0
276
+
277
+ for test_class in test_classes:
278
+ instance = test_class()
279
+ for method_name in dir(instance):
280
+ if method_name.startswith("test_"):
281
+ try:
282
+ getattr(instance, method_name)()
283
+ print(f" ✓ {test_class.__name__}.{method_name}")
284
+ passed += 1
285
+ except AssertionError as e:
286
+ print(f" ✗ {test_class.__name__}.{method_name}: {e}")
287
+ failed += 1
288
+ except Exception as e:
289
+ print(f" ✗ {test_class.__name__}.{method_name}: {type(e).__name__}: {e}")
290
+ failed += 1
291
+
292
+ print(f"\nResults: {passed} passed, {failed} failed")
293
+ return failed == 0
294
+
295
+
296
+ if __name__ == "__main__":
297
+ print("Running issue_models tests...\n")
298
+ success = run_tests()
299
+ sys.exit(0 if success else 1)
@@ -0,0 +1,323 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Integration tests for LocalJsonProvider.
4
+
5
+ Run with: python3 -m pytest global/tests/test_local_provider.py -v
6
+ Or: python3 global/tests/test_local_provider.py
7
+ """
8
+
9
+ import sys
10
+ import tempfile
11
+ import shutil
12
+ from pathlib import Path
13
+
14
+ # Add parent directory to path for imports
15
+ sys.path.insert(0, str(Path(__file__).parent.parent / "lib"))
16
+
17
+ from issue_models import Issue, IssueStatus, Priority
18
+ from local_provider import LocalJsonProvider
19
+
20
+
21
+ class TestLocalJsonProvider:
22
+ """Integration tests for LocalJsonProvider."""
23
+
24
+ def setup_method(self):
25
+ """Create temporary storage directory for each test."""
26
+ self.temp_dir = Path(tempfile.mkdtemp())
27
+ self.provider = LocalJsonProvider(
28
+ storage_path=self.temp_dir,
29
+ prefix="TEST"
30
+ )
31
+
32
+ def teardown_method(self):
33
+ """Clean up temporary directory after each test."""
34
+ shutil.rmtree(self.temp_dir, ignore_errors=True)
35
+
36
+ def test_provider_name(self):
37
+ assert self.provider.name == "local"
38
+
39
+ def test_provider_available(self):
40
+ assert self.provider.is_available is True
41
+
42
+ def test_create_issue(self):
43
+ issue = self.provider.create_issue(
44
+ title="Test issue",
45
+ description="Test description",
46
+ priority=Priority.HIGH
47
+ )
48
+ assert issue.identifier == "TEST-001"
49
+ assert issue.title == "Test issue"
50
+ assert issue.description == "Test description"
51
+ assert issue.priority == Priority.HIGH
52
+ assert issue.status == IssueStatus.TODO
53
+ assert issue.provider == "local"
54
+
55
+ def test_create_multiple_issues(self):
56
+ issue1 = self.provider.create_issue(title="First")
57
+ issue2 = self.provider.create_issue(title="Second")
58
+ issue3 = self.provider.create_issue(title="Third")
59
+
60
+ assert issue1.identifier == "TEST-001"
61
+ assert issue2.identifier == "TEST-002"
62
+ assert issue3.identifier == "TEST-003"
63
+
64
+ def test_list_issues(self):
65
+ self.provider.create_issue(title="Issue 1")
66
+ self.provider.create_issue(title="Issue 2")
67
+ self.provider.create_issue(title="Issue 3")
68
+
69
+ issues = self.provider.list_issues()
70
+ assert len(issues) == 3
71
+
72
+ def test_list_issues_by_status(self):
73
+ self.provider.create_issue(title="Todo 1", status=IssueStatus.TODO)
74
+ self.provider.create_issue(title="Todo 2", status=IssueStatus.TODO)
75
+ self.provider.create_issue(title="In Progress", status=IssueStatus.IN_PROGRESS)
76
+
77
+ todo_issues = self.provider.list_issues(status=IssueStatus.TODO)
78
+ wip_issues = self.provider.list_issues(status=IssueStatus.IN_PROGRESS)
79
+
80
+ assert len(todo_issues) == 2
81
+ assert len(wip_issues) == 1
82
+
83
+ def test_list_issues_with_limit(self):
84
+ for i in range(10):
85
+ self.provider.create_issue(title=f"Issue {i}")
86
+
87
+ issues = self.provider.list_issues(limit=5)
88
+ assert len(issues) == 5
89
+
90
+ def test_get_issue(self):
91
+ created = self.provider.create_issue(title="Find me")
92
+ found = self.provider.get_issue(created.identifier)
93
+
94
+ assert found is not None
95
+ assert found.identifier == created.identifier
96
+ assert found.title == "Find me"
97
+
98
+ def test_get_issue_case_insensitive(self):
99
+ created = self.provider.create_issue(title="Case test")
100
+ found = self.provider.get_issue(created.identifier.lower())
101
+
102
+ assert found is not None
103
+ assert found.identifier == created.identifier
104
+
105
+ def test_get_issue_not_found(self):
106
+ found = self.provider.get_issue("NONEXISTENT-999")
107
+ assert found is None
108
+
109
+ def test_get_ready_issues(self):
110
+ self.provider.create_issue(title="Ready 1", priority=Priority.LOW)
111
+ self.provider.create_issue(title="Ready 2", priority=Priority.HIGH)
112
+ self.provider.create_issue(title="Not ready", status=IssueStatus.IN_PROGRESS)
113
+
114
+ ready = self.provider.get_ready_issues()
115
+ assert len(ready) == 2
116
+ # Should be sorted by priority (HIGH first)
117
+ assert ready[0].priority == Priority.HIGH
118
+
119
+ def test_get_in_progress_issues(self):
120
+ self.provider.create_issue(title="Todo", status=IssueStatus.TODO)
121
+ self.provider.create_issue(title="WIP", status=IssueStatus.IN_PROGRESS)
122
+ self.provider.create_issue(title="Review", status=IssueStatus.IN_REVIEW)
123
+
124
+ active = self.provider.get_in_progress_issues()
125
+ assert len(active) == 2
126
+
127
+ def test_update_issue_title(self):
128
+ issue = self.provider.create_issue(title="Original title")
129
+ updated = self.provider.update_issue(issue.identifier, title="New title")
130
+
131
+ assert updated.title == "New title"
132
+
133
+ # Verify persistence
134
+ fetched = self.provider.get_issue(issue.identifier)
135
+ assert fetched.title == "New title"
136
+
137
+ def test_update_issue_status(self):
138
+ issue = self.provider.create_issue(title="Status test")
139
+ assert issue.status == IssueStatus.TODO
140
+
141
+ updated = self.provider.update_issue(
142
+ issue.identifier,
143
+ status=IssueStatus.IN_PROGRESS
144
+ )
145
+ assert updated.status == IssueStatus.IN_PROGRESS
146
+
147
+ def test_update_issue_priority(self):
148
+ issue = self.provider.create_issue(title="Priority test")
149
+ updated = self.provider.update_issue(
150
+ issue.identifier,
151
+ priority=Priority.URGENT
152
+ )
153
+ assert updated.priority == Priority.URGENT
154
+
155
+ def test_update_issue_labels(self):
156
+ issue = self.provider.create_issue(title="Labels test")
157
+ updated = self.provider.update_issue(
158
+ issue.identifier,
159
+ labels=["bug", "urgent"]
160
+ )
161
+ assert updated.labels == ["bug", "urgent"]
162
+
163
+ def test_update_issue_not_found(self):
164
+ try:
165
+ self.provider.update_issue("NONEXISTENT-999", title="New")
166
+ assert False, "Should have raised KeyError"
167
+ except KeyError:
168
+ pass
169
+
170
+ def test_delete_issue_soft(self):
171
+ issue = self.provider.create_issue(title="Delete me")
172
+ result = self.provider.delete_issue(issue.identifier)
173
+
174
+ assert result is True
175
+ fetched = self.provider.get_issue(issue.identifier)
176
+ assert fetched.status == IssueStatus.CANCELLED
177
+
178
+ def test_delete_issue_not_found(self):
179
+ result = self.provider.delete_issue("NONEXISTENT-999")
180
+ assert result is False
181
+
182
+ def test_hard_delete_issue(self):
183
+ issue = self.provider.create_issue(title="Hard delete me")
184
+ result = self.provider.hard_delete_issue(issue.identifier)
185
+
186
+ assert result is True
187
+ fetched = self.provider.get_issue(issue.identifier)
188
+ assert fetched is None
189
+
190
+ def test_assign_to_agent(self):
191
+ issue = self.provider.create_issue(title="Assign test")
192
+ updated = self.provider.assign_to_agent(issue.identifier, "agent-123")
193
+
194
+ assert updated.assigned_agent == "agent-123"
195
+
196
+ # Verify persistence
197
+ fetched = self.provider.get_issue(issue.identifier)
198
+ assert fetched.assigned_agent == "agent-123"
199
+
200
+ def test_unassign_agent(self):
201
+ issue = self.provider.create_issue(title="Unassign test")
202
+ self.provider.assign_to_agent(issue.identifier, "agent-123")
203
+ updated = self.provider.unassign_agent(issue.identifier)
204
+
205
+ assert updated.assigned_agent is None
206
+
207
+ def test_get_agent_issues(self):
208
+ self.provider.create_issue(title="Issue 1")
209
+ issue2 = self.provider.create_issue(title="Issue 2")
210
+ issue3 = self.provider.create_issue(title="Issue 3")
211
+
212
+ self.provider.assign_to_agent(issue2.identifier, "agent-xyz")
213
+ self.provider.assign_to_agent(issue3.identifier, "agent-xyz")
214
+
215
+ agent_issues = self.provider.get_agent_issues("agent-xyz")
216
+ assert len(agent_issues) == 2
217
+
218
+ def test_import_issues(self):
219
+ issues = [
220
+ Issue(id="imp-1", identifier="IMP-001", title="Imported 1"),
221
+ Issue(id="imp-2", identifier="IMP-002", title="Imported 2"),
222
+ ]
223
+ count = self.provider.import_issues(issues)
224
+
225
+ assert count == 2
226
+ assert self.provider.get_issue("IMP-001") is not None
227
+ assert self.provider.get_issue("IMP-002") is not None
228
+
229
+ def test_export_issues(self):
230
+ self.provider.create_issue(title="Export 1")
231
+ self.provider.create_issue(title="Export 2")
232
+
233
+ exported = self.provider.export_issues()
234
+ assert len(exported) == 2
235
+ assert all(isinstance(e, dict) for e in exported)
236
+ assert exported[0]["title"] in ["Export 1", "Export 2"]
237
+
238
+ def test_get_statistics(self):
239
+ self.provider.create_issue(title="T1", status=IssueStatus.TODO, priority=Priority.HIGH)
240
+ self.provider.create_issue(title="T2", status=IssueStatus.TODO, priority=Priority.LOW)
241
+ self.provider.create_issue(title="T3", status=IssueStatus.IN_PROGRESS)
242
+ issue = self.provider.create_issue(title="T4", status=IssueStatus.DONE)
243
+ self.provider.assign_to_agent(issue.identifier, "agent-1")
244
+
245
+ stats = self.provider.get_statistics()
246
+
247
+ assert stats["total"] == 4
248
+ assert stats["by_status"]["todo"] == 2
249
+ assert stats["by_status"]["in_progress"] == 1
250
+ assert stats["by_status"]["done"] == 1
251
+ assert stats["assigned"] == 1
252
+ assert stats["unassigned"] == 3
253
+
254
+ def test_persistence_across_instances(self):
255
+ # Create issue with first instance
256
+ self.provider.create_issue(title="Persistent issue")
257
+
258
+ # Create new provider instance pointing to same storage
259
+ new_provider = LocalJsonProvider(
260
+ storage_path=self.temp_dir,
261
+ prefix="TEST"
262
+ )
263
+
264
+ issues = new_provider.list_issues()
265
+ assert len(issues) == 1
266
+ assert issues[0].title == "Persistent issue"
267
+
268
+ def test_issue_with_parent(self):
269
+ parent = self.provider.create_issue(title="Parent issue")
270
+ child = self.provider.create_issue(
271
+ title="Child issue",
272
+ parent_id=parent.identifier
273
+ )
274
+
275
+ assert child.parent_id == parent.identifier
276
+
277
+ # Verify persistence
278
+ fetched = self.provider.get_issue(child.identifier)
279
+ assert fetched.parent_id == parent.identifier
280
+
281
+ def test_priority_sorting(self):
282
+ self.provider.create_issue(title="Low", priority=Priority.LOW)
283
+ self.provider.create_issue(title="Urgent", priority=Priority.URGENT)
284
+ self.provider.create_issue(title="Medium", priority=Priority.MEDIUM)
285
+
286
+ issues = self.provider.list_issues()
287
+
288
+ # Should be sorted: Urgent (0), Medium (2), Low (3)
289
+ assert issues[0].priority == Priority.URGENT
290
+ assert issues[1].priority == Priority.MEDIUM
291
+ assert issues[2].priority == Priority.LOW
292
+
293
+
294
+ def run_tests():
295
+ """Run all tests manually if pytest not available."""
296
+ test_instance = TestLocalJsonProvider()
297
+ passed = 0
298
+ failed = 0
299
+
300
+ for method_name in sorted(dir(test_instance)):
301
+ if method_name.startswith("test_"):
302
+ test_instance.setup_method()
303
+ try:
304
+ getattr(test_instance, method_name)()
305
+ print(f" ✓ {method_name}")
306
+ passed += 1
307
+ except AssertionError as e:
308
+ print(f" ✗ {method_name}: {e}")
309
+ failed += 1
310
+ except Exception as e:
311
+ print(f" ✗ {method_name}: {type(e).__name__}: {e}")
312
+ failed += 1
313
+ finally:
314
+ test_instance.teardown_method()
315
+
316
+ print(f"\nResults: {passed} passed, {failed} failed")
317
+ return failed == 0
318
+
319
+
320
+ if __name__ == "__main__":
321
+ print("Running LocalJsonProvider tests...\n")
322
+ success = run_tests()
323
+ sys.exit(0 if success else 1)