anvil-dev-framework 0.1.7 → 0.1.9
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 +71 -22
- package/VERSION +1 -1
- package/docs/ANV-263-hook-logging-investigation.md +116 -0
- package/docs/command-reference.md +398 -17
- package/docs/session-workflow.md +62 -9
- package/docs/system-architecture.md +584 -0
- package/global/api/__pycache__/ralph_api.cpython-314.pyc +0 -0
- package/global/api/openapi.yaml +357 -0
- package/global/api/ralph_api.py +528 -0
- package/global/commands/anvil-settings.md +47 -19
- package/global/commands/audit.md +163 -0
- package/global/commands/checklist.md +180 -0
- package/global/commands/coderabbit-fix.md +282 -0
- package/global/commands/efficiency.md +356 -0
- package/global/commands/evidence.md +117 -33
- package/global/commands/hud.md +24 -0
- package/global/commands/insights.md +101 -3
- package/global/commands/orient.md +22 -21
- package/global/commands/patterns.md +115 -0
- package/global/commands/ralph.md +47 -1
- package/global/commands/token-budget.md +214 -0
- package/global/commands/weekly-review.md +21 -1
- package/global/config/notifications.yaml.template +50 -0
- package/global/hooks/ralph_stop.sh +33 -1
- package/global/hooks/statusline.sh +67 -2
- package/global/lib/__pycache__/coderabbit_metrics.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/command_tracker.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/context_optimizer.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/git_utils.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/issue_models.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/linear_provider.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/optimization_applier.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/ralph_state.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/ralph_webhooks.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/state_manager.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/token_analyzer.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/token_metrics.cpython-314.pyc +0 -0
- package/global/lib/coderabbit_metrics.py +647 -0
- package/global/lib/command_tracker.py +147 -0
- package/global/lib/context_optimizer.py +323 -0
- package/global/lib/linear_provider.py +210 -16
- package/global/lib/log_rotation.py +287 -0
- package/global/lib/optimization_applier.py +582 -0
- package/global/lib/ralph_events.py +398 -0
- package/global/lib/ralph_notifier.py +366 -0
- package/global/lib/ralph_state.py +264 -24
- package/global/lib/ralph_webhooks.py +470 -0
- package/global/lib/state_manager.py +121 -0
- package/global/lib/token_analyzer.py +1383 -0
- package/global/lib/token_metrics.py +919 -0
- package/global/tests/__pycache__/test_command_tracker.cpython-314-pytest-9.0.2.pyc +0 -0
- package/global/tests/__pycache__/test_context_optimizer.cpython-314-pytest-9.0.2.pyc +0 -0
- package/global/tests/__pycache__/test_doc_coverage.cpython-314-pytest-9.0.2.pyc +0 -0
- package/global/tests/__pycache__/test_git_utils.cpython-314-pytest-9.0.2.pyc +0 -0
- package/global/tests/__pycache__/test_issue_models.cpython-314-pytest-9.0.2.pyc +0 -0
- package/global/tests/__pycache__/test_linear_filtering.cpython-314-pytest-9.0.2.pyc +0 -0
- package/global/tests/__pycache__/test_linear_provider.cpython-314-pytest-9.0.2.pyc +0 -0
- package/global/tests/__pycache__/test_local_provider.cpython-314-pytest-9.0.2.pyc +0 -0
- package/global/tests/__pycache__/test_optimization_applier.cpython-314-pytest-9.0.2.pyc +0 -0
- package/global/tests/__pycache__/test_token_analyzer.cpython-314-pytest-9.0.2.pyc +0 -0
- package/global/tests/__pycache__/test_token_analyzer_phase6.cpython-314-pytest-9.0.2.pyc +0 -0
- package/global/tests/__pycache__/test_token_metrics.cpython-314-pytest-9.0.2.pyc +0 -0
- package/global/tests/test_command_tracker.py +172 -0
- package/global/tests/test_context_optimizer.py +321 -0
- package/global/tests/test_linear_filtering.py +319 -0
- package/global/tests/test_linear_provider.py +40 -1
- package/global/tests/test_optimization_applier.py +508 -0
- package/global/tests/test_token_analyzer.py +735 -0
- package/global/tests/test_token_analyzer_phase6.py +537 -0
- package/global/tests/test_token_metrics.py +829 -0
- package/global/tools/README.md +153 -0
- package/global/tools/__pycache__/anvil-hud.cpython-314.pyc +0 -0
- package/global/tools/__pycache__/orient_linear.cpython-314.pyc +0 -0
- package/global/tools/__pycache__/ralph-watchcpython-314.pyc +0 -0
- package/global/tools/anvil-hud.py +86 -1
- package/global/tools/anvil-memory/src/__tests__/ccs/context-monitor.test.ts +472 -0
- package/global/tools/anvil-memory/src/__tests__/ccs/fixtures.ts +405 -0
- package/global/tools/anvil-memory/src/__tests__/ccs/index.ts +36 -0
- package/global/tools/anvil-memory/src/__tests__/ccs/prompt-generator.test.ts +653 -0
- package/global/tools/anvil-memory/src/__tests__/ccs/ralph-stop.test.ts +727 -0
- package/global/tools/anvil-memory/src/__tests__/ccs/test-utils.ts +340 -0
- package/global/tools/anvil-memory/src/__tests__/commands.test.ts +218 -0
- package/global/tools/anvil-memory/src/commands/context.ts +322 -0
- package/global/tools/anvil-memory/src/db.ts +108 -0
- package/global/tools/anvil-memory/src/index.ts +2 -8
- package/global/tools/orient_linear.py +159 -0
- package/global/tools/ralph-watch +423 -0
- package/package.json +2 -1
- package/project/.anvil-project.yaml.template +93 -0
- package/project/CLAUDE.md.template +343 -0
- package/project/agents/README.md +119 -0
- package/project/agents/cross-layer-debugger.md +217 -0
- package/project/agents/security-code-reviewer.md +162 -0
- package/project/constitution.md.template +235 -0
- package/project/coordination.md +103 -0
- package/project/docs/background-tasks.md +258 -0
- package/project/docs/skills-frontmatter.md +243 -0
- package/project/examples/README.md +106 -0
- package/project/examples/api-route-template.ts +171 -0
- package/project/examples/component-template.tsx +110 -0
- package/project/examples/hook-template.ts +152 -0
- package/project/examples/service-template.ts +207 -0
- package/project/examples/test-template.test.tsx +249 -0
- package/project/hooks/README.md +491 -0
- package/project/hooks/__pycache__/notification.cpython-314.pyc +0 -0
- package/project/hooks/__pycache__/post_tool_use.cpython-314.pyc +0 -0
- package/project/hooks/__pycache__/pre_tool_use.cpython-314.pyc +0 -0
- package/project/hooks/__pycache__/session_start.cpython-314.pyc +0 -0
- package/project/hooks/__pycache__/stop.cpython-314.pyc +0 -0
- package/project/hooks/notification.py +183 -0
- package/project/hooks/permission_request.py +438 -0
- package/project/hooks/post_tool_use.py +397 -0
- package/project/hooks/pre_compact.py +126 -0
- package/project/hooks/pre_tool_use.py +454 -0
- package/project/hooks/session_start.py +656 -0
- package/project/hooks/stop.py +356 -0
- package/project/hooks/subagent_start.py +223 -0
- package/project/hooks/subagent_stop.py +215 -0
- package/project/hooks/user_prompt_submit.py +110 -0
- package/project/hooks/utils/llm/anth.py +114 -0
- package/project/hooks/utils/llm/oai.py +114 -0
- package/project/hooks/utils/tts/elevenlabs_tts.py +63 -0
- package/project/hooks/utils/tts/mlx_audio_tts.py +86 -0
- package/project/hooks/utils/tts/openai_tts.py +92 -0
- package/project/hooks/utils/tts/pyttsx3_tts.py +75 -0
- package/project/linear.yaml.template +23 -0
- package/project/product.md.template +238 -0
- package/project/retros/README.md +126 -0
- package/project/rules/README.md +90 -0
- package/project/rules/debugging.md +139 -0
- package/project/rules/security-review.md +115 -0
- package/project/settings.yaml.template +185 -0
- package/project/specs/SPEC-ANV-72-hud-kanban.md +525 -0
- package/project/templates/api-python/CLAUDE.md +547 -0
- package/project/templates/generic/CLAUDE.md +260 -0
- package/project/templates/saas/CLAUDE.md +478 -0
- package/project/tests/README.md +140 -0
- package/project/tests/__pycache__/test_transcript_parser.cpython-314-pytest-9.0.2.pyc +0 -0
- package/project/tests/fixtures/sample-transcript.jsonl +21 -0
- package/project/tests/test-hooks.sh +259 -0
- package/project/tests/test-lib.sh +248 -0
- package/project/tests/test-statusline.sh +165 -0
- package/project/tests/test_transcript_parser.py +323 -0
|
@@ -0,0 +1,508 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tests for Optimization Applier Service (Phase 6).
|
|
3
|
+
|
|
4
|
+
Tests the safe application of token efficiency optimizations with
|
|
5
|
+
backup, rollback, and tracking capabilities.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
from datetime import datetime, timezone
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from unittest.mock import Mock, patch, MagicMock
|
|
12
|
+
import tempfile
|
|
13
|
+
import shutil
|
|
14
|
+
|
|
15
|
+
# Import the module under test
|
|
16
|
+
import sys
|
|
17
|
+
sys.path.insert(0, str(Path(__file__).parent.parent / "lib"))
|
|
18
|
+
|
|
19
|
+
from optimization_applier import (
|
|
20
|
+
OptimizationApplier,
|
|
21
|
+
OptimizationType,
|
|
22
|
+
OptimizationResult,
|
|
23
|
+
AppliedOptimization,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class TestOptimizationType:
|
|
28
|
+
"""Tests for OptimizationType enum."""
|
|
29
|
+
|
|
30
|
+
def test_optimization_types_exist(self):
|
|
31
|
+
"""Test that all optimization types are defined."""
|
|
32
|
+
assert OptimizationType.REMOVE_UNUSED_PATTERN.value == "remove_unused_pattern"
|
|
33
|
+
assert OptimizationType.DEFER_LOADING.value == "defer_loading"
|
|
34
|
+
assert OptimizationType.CONSOLIDATE_COMMANDS.value == "consolidate_commands"
|
|
35
|
+
assert OptimizationType.REDUCE_CONTEXT.value == "reduce_context"
|
|
36
|
+
assert OptimizationType.PRUNE_RARELY_USED.value == "prune_rarely_used"
|
|
37
|
+
|
|
38
|
+
def test_optimization_type_from_string(self):
|
|
39
|
+
"""Test creating OptimizationType from string."""
|
|
40
|
+
opt_type = OptimizationType("defer_loading")
|
|
41
|
+
assert opt_type == OptimizationType.DEFER_LOADING
|
|
42
|
+
|
|
43
|
+
def test_invalid_optimization_type(self):
|
|
44
|
+
"""Test that invalid type raises ValueError."""
|
|
45
|
+
with pytest.raises(ValueError):
|
|
46
|
+
OptimizationType("invalid_type")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class TestOptimizationResult:
|
|
50
|
+
"""Tests for OptimizationResult dataclass."""
|
|
51
|
+
|
|
52
|
+
def test_result_defaults(self):
|
|
53
|
+
"""Test default values for OptimizationResult."""
|
|
54
|
+
result = OptimizationResult(success=False)
|
|
55
|
+
|
|
56
|
+
assert result.success is False
|
|
57
|
+
assert result.optimization_id is None
|
|
58
|
+
assert result.recommendation_id is None
|
|
59
|
+
assert result.optimization_type is None
|
|
60
|
+
assert result.files_modified == []
|
|
61
|
+
assert result.backup_paths == []
|
|
62
|
+
assert result.tokens_before == 0
|
|
63
|
+
assert result.tokens_after == 0
|
|
64
|
+
assert result.savings == 0
|
|
65
|
+
assert result.error_message is None
|
|
66
|
+
assert result.commit_hash is None
|
|
67
|
+
assert result.applied_at is not None
|
|
68
|
+
|
|
69
|
+
def test_result_with_values(self):
|
|
70
|
+
"""Test OptimizationResult with custom values."""
|
|
71
|
+
result = OptimizationResult(
|
|
72
|
+
success=True,
|
|
73
|
+
optimization_id=1,
|
|
74
|
+
recommendation_id=100,
|
|
75
|
+
optimization_type=OptimizationType.DEFER_LOADING,
|
|
76
|
+
files_modified=["file1.md", "file2.md"],
|
|
77
|
+
tokens_before=1000,
|
|
78
|
+
tokens_after=700,
|
|
79
|
+
savings=300
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
assert result.success is True
|
|
83
|
+
assert result.optimization_id == 1
|
|
84
|
+
assert result.savings == 300
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class TestAppliedOptimization:
|
|
88
|
+
"""Tests for AppliedOptimization dataclass."""
|
|
89
|
+
|
|
90
|
+
def test_applied_optimization_creation(self):
|
|
91
|
+
"""Test creating AppliedOptimization record."""
|
|
92
|
+
now = datetime.now(timezone.utc)
|
|
93
|
+
applied = AppliedOptimization(
|
|
94
|
+
id=1,
|
|
95
|
+
recommendation_id=100,
|
|
96
|
+
optimization_type="defer_loading",
|
|
97
|
+
description="Defer patterns command",
|
|
98
|
+
files_modified=["CLAUDE.md"],
|
|
99
|
+
tokens_saved=500,
|
|
100
|
+
applied_at=now,
|
|
101
|
+
commit_hash="abc123"
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
assert applied.id == 1
|
|
105
|
+
assert applied.recommendation_id == 100
|
|
106
|
+
assert applied.tokens_saved == 500
|
|
107
|
+
assert applied.reverted is False
|
|
108
|
+
assert applied.reverted_at is None
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class TestOptimizationApplier:
|
|
112
|
+
"""Tests for OptimizationApplier class."""
|
|
113
|
+
|
|
114
|
+
@pytest.fixture
|
|
115
|
+
def temp_project(self):
|
|
116
|
+
"""Create a temporary project directory for testing."""
|
|
117
|
+
temp_dir = tempfile.mkdtemp()
|
|
118
|
+
yield Path(temp_dir)
|
|
119
|
+
shutil.rmtree(temp_dir)
|
|
120
|
+
|
|
121
|
+
@pytest.fixture
|
|
122
|
+
def applier(self, temp_project):
|
|
123
|
+
"""Create an OptimizationApplier with temp project."""
|
|
124
|
+
return OptimizationApplier(project_root=temp_project, auto_commit=False)
|
|
125
|
+
|
|
126
|
+
def test_init_creates_backup_dir(self, temp_project):
|
|
127
|
+
"""Test that initialization creates backup directory."""
|
|
128
|
+
applier = OptimizationApplier(project_root=temp_project)
|
|
129
|
+
|
|
130
|
+
backup_dir = temp_project / ".claude" / "backups" / "optimizations"
|
|
131
|
+
assert backup_dir.exists()
|
|
132
|
+
|
|
133
|
+
def test_init_default_values(self, applier):
|
|
134
|
+
"""Test default initialization values."""
|
|
135
|
+
assert applier.auto_commit is False
|
|
136
|
+
assert applier._applied_optimizations == []
|
|
137
|
+
assert applier._next_id == 1
|
|
138
|
+
|
|
139
|
+
def test_apply_recommendation_invalid_type(self, applier):
|
|
140
|
+
"""Test applying recommendation with invalid type."""
|
|
141
|
+
result = applier.apply_recommendation(
|
|
142
|
+
recommendation_id=1,
|
|
143
|
+
recommendation_type="invalid_type",
|
|
144
|
+
description="Test",
|
|
145
|
+
target_files=[],
|
|
146
|
+
changes={},
|
|
147
|
+
estimated_savings=100
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
assert result.success is False
|
|
151
|
+
assert "Unknown optimization type" in result.error_message
|
|
152
|
+
|
|
153
|
+
def test_apply_recommendation_creates_backup(self, applier, temp_project):
|
|
154
|
+
"""Test that applying creates backup of target files."""
|
|
155
|
+
# Create a test file
|
|
156
|
+
test_file = temp_project / "test.md"
|
|
157
|
+
test_file.write_text("original content")
|
|
158
|
+
|
|
159
|
+
result = applier.apply_recommendation(
|
|
160
|
+
recommendation_id=1,
|
|
161
|
+
recommendation_type="remove_unused_pattern",
|
|
162
|
+
description="Remove test pattern",
|
|
163
|
+
target_files=["test.md"],
|
|
164
|
+
changes={"pattern": "original"},
|
|
165
|
+
estimated_savings=50
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
assert result.success is True
|
|
169
|
+
assert len(result.backup_paths) == 1
|
|
170
|
+
assert Path(result.backup_paths[0]).exists()
|
|
171
|
+
|
|
172
|
+
def test_apply_recommendation_modifies_file(self, applier, temp_project):
|
|
173
|
+
"""Test that applying modifies target file."""
|
|
174
|
+
# Create a test file
|
|
175
|
+
test_file = temp_project / "test.md"
|
|
176
|
+
test_file.write_text("This is the pattern to remove. Keep this.")
|
|
177
|
+
|
|
178
|
+
result = applier.apply_recommendation(
|
|
179
|
+
recommendation_id=1,
|
|
180
|
+
recommendation_type="remove_unused_pattern",
|
|
181
|
+
description="Remove pattern",
|
|
182
|
+
target_files=["test.md"],
|
|
183
|
+
changes={"pattern": "pattern to remove. "},
|
|
184
|
+
estimated_savings=50
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
assert result.success is True
|
|
188
|
+
assert test_file.read_text() == "This is the Keep this."
|
|
189
|
+
|
|
190
|
+
def test_apply_recommendation_tracks_optimization(self, applier, temp_project):
|
|
191
|
+
"""Test that applied optimization is tracked."""
|
|
192
|
+
test_file = temp_project / "test.md"
|
|
193
|
+
test_file.write_text("test content")
|
|
194
|
+
|
|
195
|
+
applier.apply_recommendation(
|
|
196
|
+
recommendation_id=1,
|
|
197
|
+
recommendation_type="remove_unused_pattern",
|
|
198
|
+
description="Test optimization",
|
|
199
|
+
target_files=["test.md"],
|
|
200
|
+
changes={"pattern": "test"},
|
|
201
|
+
estimated_savings=50
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
optimizations = applier.get_applied_optimizations()
|
|
205
|
+
assert len(optimizations) == 1
|
|
206
|
+
assert optimizations[0].description == "Test optimization"
|
|
207
|
+
|
|
208
|
+
def test_get_applied_optimizations_excludes_reverted(self, applier, temp_project):
|
|
209
|
+
"""Test that reverted optimizations are excluded by default."""
|
|
210
|
+
test_file = temp_project / "test.md"
|
|
211
|
+
test_file.write_text("content")
|
|
212
|
+
|
|
213
|
+
# Apply and then mark as reverted
|
|
214
|
+
applier.apply_recommendation(
|
|
215
|
+
recommendation_id=1,
|
|
216
|
+
recommendation_type="remove_unused_pattern",
|
|
217
|
+
description="Test",
|
|
218
|
+
target_files=["test.md"],
|
|
219
|
+
changes={"pattern": ""},
|
|
220
|
+
estimated_savings=50
|
|
221
|
+
)
|
|
222
|
+
applier._applied_optimizations[0].reverted = True
|
|
223
|
+
|
|
224
|
+
optimizations = applier.get_applied_optimizations(include_reverted=False)
|
|
225
|
+
assert len(optimizations) == 0
|
|
226
|
+
|
|
227
|
+
optimizations_all = applier.get_applied_optimizations(include_reverted=True)
|
|
228
|
+
assert len(optimizations_all) == 1
|
|
229
|
+
|
|
230
|
+
def test_get_total_savings(self, applier, temp_project):
|
|
231
|
+
"""Test calculating total token savings."""
|
|
232
|
+
test_file = temp_project / "test.md"
|
|
233
|
+
test_file.write_text("content")
|
|
234
|
+
|
|
235
|
+
applier.apply_recommendation(
|
|
236
|
+
recommendation_id=1,
|
|
237
|
+
recommendation_type="remove_unused_pattern",
|
|
238
|
+
description="Test 1",
|
|
239
|
+
target_files=["test.md"],
|
|
240
|
+
changes={"pattern": ""},
|
|
241
|
+
estimated_savings=100
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
test_file.write_text("more content")
|
|
245
|
+
applier.apply_recommendation(
|
|
246
|
+
recommendation_id=2,
|
|
247
|
+
recommendation_type="remove_unused_pattern",
|
|
248
|
+
description="Test 2",
|
|
249
|
+
target_files=["test.md"],
|
|
250
|
+
changes={"pattern": ""},
|
|
251
|
+
estimated_savings=200
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
savings = applier.get_total_savings()
|
|
255
|
+
assert savings["optimizations_count"] == 2
|
|
256
|
+
assert savings["total_tokens_saved"] == 300
|
|
257
|
+
assert savings["reverted_count"] == 0
|
|
258
|
+
|
|
259
|
+
def test_generate_impact_report(self, applier, temp_project):
|
|
260
|
+
"""Test generating impact report."""
|
|
261
|
+
test_file = temp_project / "test.md"
|
|
262
|
+
test_file.write_text("content")
|
|
263
|
+
|
|
264
|
+
applier.apply_recommendation(
|
|
265
|
+
recommendation_id=1,
|
|
266
|
+
recommendation_type="remove_unused_pattern",
|
|
267
|
+
description="Test optimization for report",
|
|
268
|
+
target_files=["test.md"],
|
|
269
|
+
changes={"pattern": ""},
|
|
270
|
+
estimated_savings=150
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
report = applier.generate_impact_report()
|
|
274
|
+
|
|
275
|
+
assert "Optimization Impact Report" in report
|
|
276
|
+
assert "Active Optimizations" in report
|
|
277
|
+
assert "1" in report # optimization ID
|
|
278
|
+
assert "Test optimization for report" in report or "Test optimization" in report
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
class TestRemovePatternOptimization:
|
|
282
|
+
"""Tests for the remove_unused_pattern optimization type."""
|
|
283
|
+
|
|
284
|
+
@pytest.fixture
|
|
285
|
+
def temp_project(self):
|
|
286
|
+
temp_dir = tempfile.mkdtemp()
|
|
287
|
+
yield Path(temp_dir)
|
|
288
|
+
shutil.rmtree(temp_dir)
|
|
289
|
+
|
|
290
|
+
@pytest.fixture
|
|
291
|
+
def applier(self, temp_project):
|
|
292
|
+
return OptimizationApplier(project_root=temp_project, auto_commit=False)
|
|
293
|
+
|
|
294
|
+
def test_remove_pattern_single_occurrence(self, applier, temp_project):
|
|
295
|
+
"""Test removing a pattern that occurs once."""
|
|
296
|
+
test_file = temp_project / "test.md"
|
|
297
|
+
test_file.write_text("Before REMOVE_ME After")
|
|
298
|
+
|
|
299
|
+
result = applier.apply_recommendation(
|
|
300
|
+
recommendation_id=1,
|
|
301
|
+
recommendation_type="remove_unused_pattern",
|
|
302
|
+
description="Remove pattern",
|
|
303
|
+
target_files=["test.md"],
|
|
304
|
+
changes={"pattern": "REMOVE_ME "},
|
|
305
|
+
estimated_savings=20
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
assert result.success is True
|
|
309
|
+
assert test_file.read_text() == "Before After"
|
|
310
|
+
|
|
311
|
+
def test_remove_pattern_multiple_occurrences(self, applier, temp_project):
|
|
312
|
+
"""Test removing a pattern with multiple occurrences."""
|
|
313
|
+
test_file = temp_project / "test.md"
|
|
314
|
+
test_file.write_text("A REMOVE B REMOVE C")
|
|
315
|
+
|
|
316
|
+
result = applier.apply_recommendation(
|
|
317
|
+
recommendation_id=1,
|
|
318
|
+
recommendation_type="remove_unused_pattern",
|
|
319
|
+
description="Remove pattern",
|
|
320
|
+
target_files=["test.md"],
|
|
321
|
+
changes={"pattern": "REMOVE "},
|
|
322
|
+
estimated_savings=20
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
assert result.success is True
|
|
326
|
+
assert test_file.read_text() == "A B C"
|
|
327
|
+
|
|
328
|
+
def test_remove_pattern_not_found(self, applier, temp_project):
|
|
329
|
+
"""Test when pattern is not found in file."""
|
|
330
|
+
test_file = temp_project / "test.md"
|
|
331
|
+
test_file.write_text("Some content without pattern")
|
|
332
|
+
|
|
333
|
+
result = applier.apply_recommendation(
|
|
334
|
+
recommendation_id=1,
|
|
335
|
+
recommendation_type="remove_unused_pattern",
|
|
336
|
+
description="Remove nonexistent",
|
|
337
|
+
target_files=["test.md"],
|
|
338
|
+
changes={"pattern": "NONEXISTENT"},
|
|
339
|
+
estimated_savings=20
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
assert result.success is True
|
|
343
|
+
assert result.files_modified == [] # File not modified
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
class TestReduceContextOptimization:
|
|
347
|
+
"""Tests for the reduce_context optimization type."""
|
|
348
|
+
|
|
349
|
+
@pytest.fixture
|
|
350
|
+
def temp_project(self):
|
|
351
|
+
temp_dir = tempfile.mkdtemp()
|
|
352
|
+
yield Path(temp_dir)
|
|
353
|
+
shutil.rmtree(temp_dir)
|
|
354
|
+
|
|
355
|
+
@pytest.fixture
|
|
356
|
+
def applier(self, temp_project):
|
|
357
|
+
return OptimizationApplier(project_root=temp_project, auto_commit=False)
|
|
358
|
+
|
|
359
|
+
def test_reduce_context_removes_section(self, applier, temp_project):
|
|
360
|
+
"""Test removing a markdown section."""
|
|
361
|
+
test_file = temp_project / "test.md"
|
|
362
|
+
test_file.write_text("""# Main
|
|
363
|
+
|
|
364
|
+
## Keep This
|
|
365
|
+
Content to keep.
|
|
366
|
+
|
|
367
|
+
## Remove This
|
|
368
|
+
Content to remove.
|
|
369
|
+
|
|
370
|
+
## Also Keep
|
|
371
|
+
More content.
|
|
372
|
+
""")
|
|
373
|
+
|
|
374
|
+
result = applier.apply_recommendation(
|
|
375
|
+
recommendation_id=1,
|
|
376
|
+
recommendation_type="reduce_context",
|
|
377
|
+
description="Remove section",
|
|
378
|
+
target_files=["test.md"],
|
|
379
|
+
changes={"sections": ["Remove This"]},
|
|
380
|
+
estimated_savings=100
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
assert result.success is True
|
|
384
|
+
content = test_file.read_text()
|
|
385
|
+
assert "Keep This" in content
|
|
386
|
+
assert "Remove This" not in content
|
|
387
|
+
assert "Also Keep" in content
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
class TestRollback:
|
|
391
|
+
"""Tests for optimization rollback functionality."""
|
|
392
|
+
|
|
393
|
+
@pytest.fixture
|
|
394
|
+
def temp_project(self):
|
|
395
|
+
temp_dir = tempfile.mkdtemp()
|
|
396
|
+
yield Path(temp_dir)
|
|
397
|
+
shutil.rmtree(temp_dir)
|
|
398
|
+
|
|
399
|
+
@pytest.fixture
|
|
400
|
+
def applier(self, temp_project):
|
|
401
|
+
return OptimizationApplier(project_root=temp_project, auto_commit=False)
|
|
402
|
+
|
|
403
|
+
def test_rollback_nonexistent_optimization(self, applier):
|
|
404
|
+
"""Test rolling back optimization that doesn't exist."""
|
|
405
|
+
success = applier.rollback_optimization(999)
|
|
406
|
+
assert success is False
|
|
407
|
+
|
|
408
|
+
def test_rollback_already_reverted(self, applier, temp_project):
|
|
409
|
+
"""Test rolling back already-reverted optimization."""
|
|
410
|
+
test_file = temp_project / "test.md"
|
|
411
|
+
test_file.write_text("content")
|
|
412
|
+
|
|
413
|
+
applier.apply_recommendation(
|
|
414
|
+
recommendation_id=1,
|
|
415
|
+
recommendation_type="remove_unused_pattern",
|
|
416
|
+
description="Test",
|
|
417
|
+
target_files=["test.md"],
|
|
418
|
+
changes={"pattern": ""},
|
|
419
|
+
estimated_savings=50
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
# Manually mark as reverted
|
|
423
|
+
applier._applied_optimizations[0].reverted = True
|
|
424
|
+
|
|
425
|
+
success = applier.rollback_optimization(1)
|
|
426
|
+
assert success is False
|
|
427
|
+
|
|
428
|
+
def test_rollback_without_commit_hash_marks_reverted(self, applier, temp_project):
|
|
429
|
+
"""Test that rollback without commit hash marks optimization as reverted."""
|
|
430
|
+
test_file = temp_project / "test.md"
|
|
431
|
+
test_file.write_text("content")
|
|
432
|
+
|
|
433
|
+
applier.apply_recommendation(
|
|
434
|
+
recommendation_id=1,
|
|
435
|
+
recommendation_type="remove_unused_pattern",
|
|
436
|
+
description="Test",
|
|
437
|
+
target_files=["test.md"],
|
|
438
|
+
changes={"pattern": ""},
|
|
439
|
+
estimated_savings=50
|
|
440
|
+
)
|
|
441
|
+
|
|
442
|
+
# Verify initial state
|
|
443
|
+
opt = applier._applied_optimizations[0]
|
|
444
|
+
assert opt.reverted is False
|
|
445
|
+
assert opt.commit_hash is None # No commit hash since auto_commit=False
|
|
446
|
+
|
|
447
|
+
# Rollback without commit_hash still marks as reverted
|
|
448
|
+
success = applier.rollback_optimization(1)
|
|
449
|
+
|
|
450
|
+
# The implementation marks as reverted and returns True
|
|
451
|
+
# even without a commit hash (no git revert needed)
|
|
452
|
+
assert success is True
|
|
453
|
+
assert opt.reverted is True
|
|
454
|
+
assert opt.reverted_at is not None
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
class TestBackupRestore:
|
|
458
|
+
"""Tests for backup and restore functionality."""
|
|
459
|
+
|
|
460
|
+
@pytest.fixture
|
|
461
|
+
def temp_project(self):
|
|
462
|
+
temp_dir = tempfile.mkdtemp()
|
|
463
|
+
yield Path(temp_dir)
|
|
464
|
+
shutil.rmtree(temp_dir)
|
|
465
|
+
|
|
466
|
+
@pytest.fixture
|
|
467
|
+
def applier(self, temp_project):
|
|
468
|
+
return OptimizationApplier(project_root=temp_project, auto_commit=False)
|
|
469
|
+
|
|
470
|
+
def test_create_backup(self, applier, temp_project):
|
|
471
|
+
"""Test creating a backup of a file."""
|
|
472
|
+
test_file = temp_project / "test.md"
|
|
473
|
+
test_file.write_text("original content")
|
|
474
|
+
|
|
475
|
+
backup_path = applier._create_backup(test_file)
|
|
476
|
+
|
|
477
|
+
assert backup_path is not None
|
|
478
|
+
assert backup_path.exists()
|
|
479
|
+
assert backup_path.read_text() == "original content"
|
|
480
|
+
|
|
481
|
+
def test_create_backup_nonexistent_file(self, applier, temp_project):
|
|
482
|
+
"""Test creating backup of nonexistent file."""
|
|
483
|
+
test_file = temp_project / "nonexistent.md"
|
|
484
|
+
|
|
485
|
+
backup_path = applier._create_backup(test_file)
|
|
486
|
+
|
|
487
|
+
assert backup_path is None
|
|
488
|
+
|
|
489
|
+
def test_backup_preserves_content(self, applier, temp_project):
|
|
490
|
+
"""Test that backup preserves original content after modification."""
|
|
491
|
+
test_file = temp_project / "test.md"
|
|
492
|
+
test_file.write_text("original content with PATTERN")
|
|
493
|
+
|
|
494
|
+
result = applier.apply_recommendation(
|
|
495
|
+
recommendation_id=1,
|
|
496
|
+
recommendation_type="remove_unused_pattern",
|
|
497
|
+
description="Remove pattern",
|
|
498
|
+
target_files=["test.md"],
|
|
499
|
+
changes={"pattern": "PATTERN"},
|
|
500
|
+
estimated_savings=20
|
|
501
|
+
)
|
|
502
|
+
|
|
503
|
+
# Check file was modified
|
|
504
|
+
assert test_file.read_text() == "original content with "
|
|
505
|
+
|
|
506
|
+
# Check backup has original content
|
|
507
|
+
backup_path = Path(result.backup_paths[0])
|
|
508
|
+
assert backup_path.read_text() == "original content with PATTERN"
|