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.
- package/README.md +719 -0
- package/VERSION +1 -0
- package/docs/ANVIL-REPO-IMPLEMENTATION-PLAN.md +441 -0
- package/docs/FIRST-SKILL-TUTORIAL.md +408 -0
- package/docs/INSTALLATION-RETRO-NOTES.md +458 -0
- package/docs/INSTALLATION.md +984 -0
- package/docs/anvil-hud.md +469 -0
- package/docs/anvil-init.md +255 -0
- package/docs/anvil-state.md +210 -0
- package/docs/boris-cherny-ralph-wiggum-insights.md +608 -0
- package/docs/command-reference.md +2022 -0
- package/docs/hooks-tts.md +368 -0
- package/docs/implementation-guide.md +810 -0
- package/docs/linear-github-integration.md +247 -0
- package/docs/local-issues.md +677 -0
- package/docs/patterns/README.md +419 -0
- package/docs/planning-responsibilities.md +139 -0
- package/docs/session-workflow.md +573 -0
- package/docs/simplification-plan-template.md +297 -0
- package/docs/simplification-principles.md +129 -0
- package/docs/specifications/CCS-RALPH-INTEGRATION-DESIGN.md +633 -0
- package/docs/specifications/CCS-RESEARCH-REPORT.md +169 -0
- package/docs/specifications/PLAN-ANV-verification-ralph-wiggum.md +403 -0
- package/docs/specifications/PLAN-parallel-tracks-anvil-memory-ccs.md +494 -0
- package/docs/specifications/SPEC-ANV-VRW/component-01-verify.md +208 -0
- package/docs/specifications/SPEC-ANV-VRW/component-02-stop-gate.md +226 -0
- package/docs/specifications/SPEC-ANV-VRW/component-03-posttooluse.md +209 -0
- package/docs/specifications/SPEC-ANV-VRW/component-04-ralph-wiggum.md +604 -0
- package/docs/specifications/SPEC-ANV-VRW/component-05-atomic-actions.md +311 -0
- package/docs/specifications/SPEC-ANV-VRW/component-06-verify-subagent.md +264 -0
- package/docs/specifications/SPEC-ANV-VRW/component-07-claude-md.md +363 -0
- package/docs/specifications/SPEC-ANV-VRW/index.md +182 -0
- package/docs/specifications/SPEC-ANV-anvil-memory.md +573 -0
- package/docs/specifications/SPEC-ANV-context-checkpoints.md +781 -0
- package/docs/specifications/SPEC-ANV-verification-ralph-wiggum.md +789 -0
- package/docs/sync.md +122 -0
- package/global/CLAUDE.md +140 -0
- package/global/agents/verify-app.md +164 -0
- package/global/commands/anvil-settings.md +527 -0
- package/global/commands/anvil-sync.md +121 -0
- package/global/commands/change.md +197 -0
- package/global/commands/clarify.md +252 -0
- package/global/commands/cleanup.md +292 -0
- package/global/commands/commit-push-pr.md +207 -0
- package/global/commands/decay-review.md +127 -0
- package/global/commands/discover.md +158 -0
- package/global/commands/doc-coverage.md +122 -0
- package/global/commands/evidence.md +307 -0
- package/global/commands/explore.md +121 -0
- package/global/commands/force-exit.md +135 -0
- package/global/commands/handoff.md +191 -0
- package/global/commands/healthcheck.md +302 -0
- package/global/commands/hud.md +84 -0
- package/global/commands/insights.md +319 -0
- package/global/commands/linear-setup.md +184 -0
- package/global/commands/lint-fix.md +198 -0
- package/global/commands/orient.md +510 -0
- package/global/commands/plan.md +228 -0
- package/global/commands/ralph.md +346 -0
- package/global/commands/ready.md +182 -0
- package/global/commands/release.md +305 -0
- package/global/commands/retro.md +96 -0
- package/global/commands/shard.md +166 -0
- package/global/commands/spec.md +227 -0
- package/global/commands/sprint.md +184 -0
- package/global/commands/tasks.md +228 -0
- package/global/commands/test-and-commit.md +151 -0
- package/global/commands/validate.md +132 -0
- package/global/commands/verify.md +251 -0
- package/global/commands/weekly-review.md +156 -0
- package/global/hooks/__pycache__/ralph_context_monitor.cpython-314.pyc +0 -0
- package/global/hooks/__pycache__/statusline_agent_sync.cpython-314.pyc +0 -0
- package/global/hooks/anvil_memory_observe.ts +322 -0
- package/global/hooks/anvil_memory_session.ts +166 -0
- package/global/hooks/anvil_memory_stop.ts +187 -0
- package/global/hooks/parse_transcript.py +116 -0
- package/global/hooks/post_merge_cleanup.sh +132 -0
- package/global/hooks/post_tool_format.sh +215 -0
- package/global/hooks/ralph_context_monitor.py +240 -0
- package/global/hooks/ralph_stop.sh +502 -0
- package/global/hooks/statusline.sh +1110 -0
- package/global/hooks/statusline_agent_sync.py +224 -0
- package/global/hooks/stop_gate.sh +250 -0
- package/global/lib/.claude/anvil-state.json +21 -0
- package/global/lib/__pycache__/agent_registry.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/claim_service.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/coderabbit_service.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/config_service.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/coordination_service.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/doc_coverage_service.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/gate_logger.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/github_service.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/hygiene_service.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/issue_models.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/issue_provider.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/linear_data_service.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/linear_provider.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/local_provider.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/quality_service.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/ralph_state.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/state_manager.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/transcript_parser.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/verification_runner.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/verify_iteration.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/verify_subagent.cpython-314.pyc +0 -0
- package/global/lib/agent_registry.py +995 -0
- package/global/lib/anvil-state.sh +435 -0
- package/global/lib/claim_service.py +515 -0
- package/global/lib/coderabbit_service.py +314 -0
- package/global/lib/config_service.py +423 -0
- package/global/lib/coordination_service.py +331 -0
- package/global/lib/doc_coverage_service.py +1305 -0
- package/global/lib/gate_logger.py +316 -0
- package/global/lib/github_service.py +310 -0
- package/global/lib/handoff_generator.py +775 -0
- package/global/lib/hygiene_service.py +712 -0
- package/global/lib/issue_models.py +257 -0
- package/global/lib/issue_provider.py +339 -0
- package/global/lib/linear_data_service.py +210 -0
- package/global/lib/linear_provider.py +987 -0
- package/global/lib/linear_provider.py.backup +671 -0
- package/global/lib/local_provider.py +486 -0
- package/global/lib/orient_fast.py +457 -0
- package/global/lib/quality_service.py +470 -0
- package/global/lib/ralph_prompt_generator.py +563 -0
- package/global/lib/ralph_state.py +1202 -0
- package/global/lib/state_manager.py +417 -0
- package/global/lib/transcript_parser.py +597 -0
- package/global/lib/verification_runner.py +557 -0
- package/global/lib/verify_iteration.py +490 -0
- package/global/lib/verify_subagent.py +250 -0
- package/global/skills/README.md +155 -0
- package/global/skills/quality-gates/SKILL.md +252 -0
- package/global/skills/skill-template/SKILL.md +109 -0
- package/global/skills/testing-strategies/SKILL.md +337 -0
- package/global/templates/CHANGE-template.md +105 -0
- package/global/templates/HANDOFF-template.md +63 -0
- package/global/templates/PLAN-template.md +111 -0
- package/global/templates/SPEC-template.md +93 -0
- package/global/templates/ralph/PROMPT.md.template +89 -0
- package/global/templates/ralph/fix_plan.md.template +31 -0
- package/global/templates/ralph/progress.txt.template +23 -0
- package/global/tests/__pycache__/test_doc_coverage.cpython-314.pyc +0 -0
- package/global/tests/test_doc_coverage.py +520 -0
- package/global/tests/test_issue_models.py +299 -0
- package/global/tests/test_local_provider.py +323 -0
- package/global/tools/README.md +178 -0
- package/global/tools/__pycache__/anvil-hud.cpython-314.pyc +0 -0
- package/global/tools/anvil-hud.py +3622 -0
- package/global/tools/anvil-hud.py.bak +3318 -0
- package/global/tools/anvil-issue.py +432 -0
- package/global/tools/anvil-memory/CLAUDE.md +49 -0
- package/global/tools/anvil-memory/README.md +42 -0
- package/global/tools/anvil-memory/bun.lock +25 -0
- package/global/tools/anvil-memory/bunfig.toml +9 -0
- package/global/tools/anvil-memory/package.json +23 -0
- package/global/tools/anvil-memory/src/__tests__/ccs/context-monitor.test.ts +535 -0
- package/global/tools/anvil-memory/src/__tests__/ccs/edge-cases.test.ts +645 -0
- package/global/tools/anvil-memory/src/__tests__/ccs/fixtures.ts +363 -0
- package/global/tools/anvil-memory/src/__tests__/ccs/index.ts +8 -0
- package/global/tools/anvil-memory/src/__tests__/ccs/integration.test.ts +417 -0
- package/global/tools/anvil-memory/src/__tests__/ccs/prompt-generator.test.ts +571 -0
- package/global/tools/anvil-memory/src/__tests__/ccs/ralph-stop.test.ts +440 -0
- package/global/tools/anvil-memory/src/__tests__/ccs/test-utils.ts +252 -0
- package/global/tools/anvil-memory/src/__tests__/commands.test.ts +657 -0
- package/global/tools/anvil-memory/src/__tests__/db.test.ts +641 -0
- package/global/tools/anvil-memory/src/__tests__/hooks.test.ts +272 -0
- package/global/tools/anvil-memory/src/__tests__/performance.test.ts +427 -0
- package/global/tools/anvil-memory/src/__tests__/test-utils.ts +113 -0
- package/global/tools/anvil-memory/src/commands/checkpoint.ts +197 -0
- package/global/tools/anvil-memory/src/commands/get.ts +115 -0
- package/global/tools/anvil-memory/src/commands/init.ts +94 -0
- package/global/tools/anvil-memory/src/commands/observe.ts +163 -0
- package/global/tools/anvil-memory/src/commands/search.ts +112 -0
- package/global/tools/anvil-memory/src/db.ts +638 -0
- package/global/tools/anvil-memory/src/index.ts +205 -0
- package/global/tools/anvil-memory/src/types.ts +122 -0
- package/global/tools/anvil-memory/tsconfig.json +29 -0
- package/global/tools/ralph-loop.sh +359 -0
- package/package.json +45 -0
- package/scripts/anvil +822 -0
- package/scripts/extract_patterns.py +222 -0
- package/scripts/init-project.sh +541 -0
- package/scripts/install.sh +229 -0
- package/scripts/postinstall.js +41 -0
- package/scripts/rollback.sh +188 -0
- package/scripts/sync.sh +623 -0
- package/scripts/test-statusline.sh +248 -0
- package/scripts/update_claude_md.py +224 -0
- 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)
|