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.
Files changed (143) hide show
  1. package/README.md +71 -22
  2. package/VERSION +1 -1
  3. package/docs/ANV-263-hook-logging-investigation.md +116 -0
  4. package/docs/command-reference.md +398 -17
  5. package/docs/session-workflow.md +62 -9
  6. package/docs/system-architecture.md +584 -0
  7. package/global/api/__pycache__/ralph_api.cpython-314.pyc +0 -0
  8. package/global/api/openapi.yaml +357 -0
  9. package/global/api/ralph_api.py +528 -0
  10. package/global/commands/anvil-settings.md +47 -19
  11. package/global/commands/audit.md +163 -0
  12. package/global/commands/checklist.md +180 -0
  13. package/global/commands/coderabbit-fix.md +282 -0
  14. package/global/commands/efficiency.md +356 -0
  15. package/global/commands/evidence.md +117 -33
  16. package/global/commands/hud.md +24 -0
  17. package/global/commands/insights.md +101 -3
  18. package/global/commands/orient.md +22 -21
  19. package/global/commands/patterns.md +115 -0
  20. package/global/commands/ralph.md +47 -1
  21. package/global/commands/token-budget.md +214 -0
  22. package/global/commands/weekly-review.md +21 -1
  23. package/global/config/notifications.yaml.template +50 -0
  24. package/global/hooks/ralph_stop.sh +33 -1
  25. package/global/hooks/statusline.sh +67 -2
  26. package/global/lib/__pycache__/coderabbit_metrics.cpython-314.pyc +0 -0
  27. package/global/lib/__pycache__/command_tracker.cpython-314.pyc +0 -0
  28. package/global/lib/__pycache__/context_optimizer.cpython-314.pyc +0 -0
  29. package/global/lib/__pycache__/git_utils.cpython-314.pyc +0 -0
  30. package/global/lib/__pycache__/issue_models.cpython-314.pyc +0 -0
  31. package/global/lib/__pycache__/linear_provider.cpython-314.pyc +0 -0
  32. package/global/lib/__pycache__/optimization_applier.cpython-314.pyc +0 -0
  33. package/global/lib/__pycache__/ralph_state.cpython-314.pyc +0 -0
  34. package/global/lib/__pycache__/ralph_webhooks.cpython-314.pyc +0 -0
  35. package/global/lib/__pycache__/state_manager.cpython-314.pyc +0 -0
  36. package/global/lib/__pycache__/token_analyzer.cpython-314.pyc +0 -0
  37. package/global/lib/__pycache__/token_metrics.cpython-314.pyc +0 -0
  38. package/global/lib/coderabbit_metrics.py +647 -0
  39. package/global/lib/command_tracker.py +147 -0
  40. package/global/lib/context_optimizer.py +323 -0
  41. package/global/lib/linear_provider.py +210 -16
  42. package/global/lib/log_rotation.py +287 -0
  43. package/global/lib/optimization_applier.py +582 -0
  44. package/global/lib/ralph_events.py +398 -0
  45. package/global/lib/ralph_notifier.py +366 -0
  46. package/global/lib/ralph_state.py +264 -24
  47. package/global/lib/ralph_webhooks.py +470 -0
  48. package/global/lib/state_manager.py +121 -0
  49. package/global/lib/token_analyzer.py +1383 -0
  50. package/global/lib/token_metrics.py +919 -0
  51. package/global/tests/__pycache__/test_command_tracker.cpython-314-pytest-9.0.2.pyc +0 -0
  52. package/global/tests/__pycache__/test_context_optimizer.cpython-314-pytest-9.0.2.pyc +0 -0
  53. package/global/tests/__pycache__/test_doc_coverage.cpython-314-pytest-9.0.2.pyc +0 -0
  54. package/global/tests/__pycache__/test_git_utils.cpython-314-pytest-9.0.2.pyc +0 -0
  55. package/global/tests/__pycache__/test_issue_models.cpython-314-pytest-9.0.2.pyc +0 -0
  56. package/global/tests/__pycache__/test_linear_filtering.cpython-314-pytest-9.0.2.pyc +0 -0
  57. package/global/tests/__pycache__/test_linear_provider.cpython-314-pytest-9.0.2.pyc +0 -0
  58. package/global/tests/__pycache__/test_local_provider.cpython-314-pytest-9.0.2.pyc +0 -0
  59. package/global/tests/__pycache__/test_optimization_applier.cpython-314-pytest-9.0.2.pyc +0 -0
  60. package/global/tests/__pycache__/test_token_analyzer.cpython-314-pytest-9.0.2.pyc +0 -0
  61. package/global/tests/__pycache__/test_token_analyzer_phase6.cpython-314-pytest-9.0.2.pyc +0 -0
  62. package/global/tests/__pycache__/test_token_metrics.cpython-314-pytest-9.0.2.pyc +0 -0
  63. package/global/tests/test_command_tracker.py +172 -0
  64. package/global/tests/test_context_optimizer.py +321 -0
  65. package/global/tests/test_linear_filtering.py +319 -0
  66. package/global/tests/test_linear_provider.py +40 -1
  67. package/global/tests/test_optimization_applier.py +508 -0
  68. package/global/tests/test_token_analyzer.py +735 -0
  69. package/global/tests/test_token_analyzer_phase6.py +537 -0
  70. package/global/tests/test_token_metrics.py +829 -0
  71. package/global/tools/README.md +153 -0
  72. package/global/tools/__pycache__/anvil-hud.cpython-314.pyc +0 -0
  73. package/global/tools/__pycache__/orient_linear.cpython-314.pyc +0 -0
  74. package/global/tools/__pycache__/ralph-watchcpython-314.pyc +0 -0
  75. package/global/tools/anvil-hud.py +86 -1
  76. package/global/tools/anvil-memory/src/__tests__/ccs/context-monitor.test.ts +472 -0
  77. package/global/tools/anvil-memory/src/__tests__/ccs/fixtures.ts +405 -0
  78. package/global/tools/anvil-memory/src/__tests__/ccs/index.ts +36 -0
  79. package/global/tools/anvil-memory/src/__tests__/ccs/prompt-generator.test.ts +653 -0
  80. package/global/tools/anvil-memory/src/__tests__/ccs/ralph-stop.test.ts +727 -0
  81. package/global/tools/anvil-memory/src/__tests__/ccs/test-utils.ts +340 -0
  82. package/global/tools/anvil-memory/src/__tests__/commands.test.ts +218 -0
  83. package/global/tools/anvil-memory/src/commands/context.ts +322 -0
  84. package/global/tools/anvil-memory/src/db.ts +108 -0
  85. package/global/tools/anvil-memory/src/index.ts +2 -8
  86. package/global/tools/orient_linear.py +159 -0
  87. package/global/tools/ralph-watch +423 -0
  88. package/package.json +2 -1
  89. package/project/.anvil-project.yaml.template +93 -0
  90. package/project/CLAUDE.md.template +343 -0
  91. package/project/agents/README.md +119 -0
  92. package/project/agents/cross-layer-debugger.md +217 -0
  93. package/project/agents/security-code-reviewer.md +162 -0
  94. package/project/constitution.md.template +235 -0
  95. package/project/coordination.md +103 -0
  96. package/project/docs/background-tasks.md +258 -0
  97. package/project/docs/skills-frontmatter.md +243 -0
  98. package/project/examples/README.md +106 -0
  99. package/project/examples/api-route-template.ts +171 -0
  100. package/project/examples/component-template.tsx +110 -0
  101. package/project/examples/hook-template.ts +152 -0
  102. package/project/examples/service-template.ts +207 -0
  103. package/project/examples/test-template.test.tsx +249 -0
  104. package/project/hooks/README.md +491 -0
  105. package/project/hooks/__pycache__/notification.cpython-314.pyc +0 -0
  106. package/project/hooks/__pycache__/post_tool_use.cpython-314.pyc +0 -0
  107. package/project/hooks/__pycache__/pre_tool_use.cpython-314.pyc +0 -0
  108. package/project/hooks/__pycache__/session_start.cpython-314.pyc +0 -0
  109. package/project/hooks/__pycache__/stop.cpython-314.pyc +0 -0
  110. package/project/hooks/notification.py +183 -0
  111. package/project/hooks/permission_request.py +438 -0
  112. package/project/hooks/post_tool_use.py +397 -0
  113. package/project/hooks/pre_compact.py +126 -0
  114. package/project/hooks/pre_tool_use.py +454 -0
  115. package/project/hooks/session_start.py +656 -0
  116. package/project/hooks/stop.py +356 -0
  117. package/project/hooks/subagent_start.py +223 -0
  118. package/project/hooks/subagent_stop.py +215 -0
  119. package/project/hooks/user_prompt_submit.py +110 -0
  120. package/project/hooks/utils/llm/anth.py +114 -0
  121. package/project/hooks/utils/llm/oai.py +114 -0
  122. package/project/hooks/utils/tts/elevenlabs_tts.py +63 -0
  123. package/project/hooks/utils/tts/mlx_audio_tts.py +86 -0
  124. package/project/hooks/utils/tts/openai_tts.py +92 -0
  125. package/project/hooks/utils/tts/pyttsx3_tts.py +75 -0
  126. package/project/linear.yaml.template +23 -0
  127. package/project/product.md.template +238 -0
  128. package/project/retros/README.md +126 -0
  129. package/project/rules/README.md +90 -0
  130. package/project/rules/debugging.md +139 -0
  131. package/project/rules/security-review.md +115 -0
  132. package/project/settings.yaml.template +185 -0
  133. package/project/specs/SPEC-ANV-72-hud-kanban.md +525 -0
  134. package/project/templates/api-python/CLAUDE.md +547 -0
  135. package/project/templates/generic/CLAUDE.md +260 -0
  136. package/project/templates/saas/CLAUDE.md +478 -0
  137. package/project/tests/README.md +140 -0
  138. package/project/tests/__pycache__/test_transcript_parser.cpython-314-pytest-9.0.2.pyc +0 -0
  139. package/project/tests/fixtures/sample-transcript.jsonl +21 -0
  140. package/project/tests/test-hooks.sh +259 -0
  141. package/project/tests/test-lib.sh +248 -0
  142. package/project/tests/test-statusline.sh +165 -0
  143. 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"