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
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Unit tests for command_tracker.py (ANV-293)
|
|
4
|
+
|
|
5
|
+
Run with: python3 -m pytest global/tests/test_command_tracker.py -v
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import sys
|
|
9
|
+
import os
|
|
10
|
+
import tempfile
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
# Add parent directory to path for imports
|
|
14
|
+
sys.path.insert(0, str(Path(__file__).parent.parent / "lib"))
|
|
15
|
+
|
|
16
|
+
from token_metrics import TokenMetrics
|
|
17
|
+
from command_tracker import (
|
|
18
|
+
track_command_load,
|
|
19
|
+
get_command_token_estimate,
|
|
20
|
+
_find_command_content,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class TestCommandTracker:
|
|
25
|
+
"""Tests for command tracking functionality."""
|
|
26
|
+
|
|
27
|
+
def setup_method(self):
|
|
28
|
+
"""Create a fresh metrics instance with temp database."""
|
|
29
|
+
self.temp_dir = tempfile.mkdtemp()
|
|
30
|
+
self.db_path = Path(self.temp_dir) / "test_metrics.db"
|
|
31
|
+
|
|
32
|
+
# Create a temp command file
|
|
33
|
+
self.cmd_dir = Path(self.temp_dir) / ".claude" / "commands"
|
|
34
|
+
self.cmd_dir.mkdir(parents=True)
|
|
35
|
+
self.test_command = self.cmd_dir / "test-cmd.md"
|
|
36
|
+
self.test_command.write_text("# Test Command\n\nThis is a test command for tracking.")
|
|
37
|
+
|
|
38
|
+
# Change to temp dir so command finding works
|
|
39
|
+
self.original_cwd = os.getcwd()
|
|
40
|
+
os.chdir(self.temp_dir)
|
|
41
|
+
|
|
42
|
+
# Initialize metrics with temp database
|
|
43
|
+
self.metrics = TokenMetrics(db_path=self.db_path)
|
|
44
|
+
self.metrics.start_session()
|
|
45
|
+
|
|
46
|
+
# Patch get_metrics to return our test instance
|
|
47
|
+
import command_tracker
|
|
48
|
+
self._original_get_metrics = command_tracker.get_metrics
|
|
49
|
+
command_tracker.get_metrics = lambda: self.metrics
|
|
50
|
+
|
|
51
|
+
def teardown_method(self):
|
|
52
|
+
"""Clean up temp files and restore state."""
|
|
53
|
+
self.metrics.end_session()
|
|
54
|
+
os.chdir(self.original_cwd)
|
|
55
|
+
|
|
56
|
+
# Restore get_metrics
|
|
57
|
+
import command_tracker
|
|
58
|
+
command_tracker.get_metrics = self._original_get_metrics
|
|
59
|
+
|
|
60
|
+
# Clean up temp files
|
|
61
|
+
if self.db_path.exists():
|
|
62
|
+
self.db_path.unlink()
|
|
63
|
+
if self.test_command.exists():
|
|
64
|
+
self.test_command.unlink()
|
|
65
|
+
self.cmd_dir.rmdir()
|
|
66
|
+
(Path(self.temp_dir) / ".claude").rmdir()
|
|
67
|
+
os.rmdir(self.temp_dir)
|
|
68
|
+
|
|
69
|
+
def test_track_command_load_with_source_path(self):
|
|
70
|
+
"""Should track command load when source path is provided."""
|
|
71
|
+
tokens = track_command_load("test-cmd", source_path=str(self.test_command))
|
|
72
|
+
|
|
73
|
+
assert tokens is not None
|
|
74
|
+
assert tokens > 0
|
|
75
|
+
|
|
76
|
+
# Verify it was recorded in the database
|
|
77
|
+
summary = self.metrics.get_session_summary()
|
|
78
|
+
assert summary.total_tokens >= tokens
|
|
79
|
+
assert "command" in summary.component_breakdown
|
|
80
|
+
|
|
81
|
+
def test_track_command_load_with_content(self):
|
|
82
|
+
"""Should track command load when content is provided directly."""
|
|
83
|
+
content = "# Direct Content\n\nThis is test content."
|
|
84
|
+
tokens = track_command_load("direct-cmd", content=content)
|
|
85
|
+
|
|
86
|
+
assert tokens is not None
|
|
87
|
+
assert tokens > 0
|
|
88
|
+
|
|
89
|
+
summary = self.metrics.get_session_summary()
|
|
90
|
+
assert summary.total_tokens >= tokens
|
|
91
|
+
|
|
92
|
+
def test_track_command_load_finds_file_automatically(self):
|
|
93
|
+
"""Should find command file in known locations."""
|
|
94
|
+
tokens = track_command_load("test-cmd")
|
|
95
|
+
|
|
96
|
+
assert tokens is not None
|
|
97
|
+
assert tokens > 0
|
|
98
|
+
|
|
99
|
+
def test_track_command_load_nonexistent_command(self):
|
|
100
|
+
"""Should handle nonexistent commands gracefully."""
|
|
101
|
+
tokens = track_command_load("nonexistent-cmd")
|
|
102
|
+
|
|
103
|
+
# Should return 0 or None when content can't be found
|
|
104
|
+
assert tokens is None or tokens == 0
|
|
105
|
+
|
|
106
|
+
def test_find_command_content(self):
|
|
107
|
+
"""Should find command content from known locations."""
|
|
108
|
+
content = _find_command_content("test-cmd")
|
|
109
|
+
assert content is not None
|
|
110
|
+
assert "Test Command" in content
|
|
111
|
+
|
|
112
|
+
def test_find_command_content_not_found(self):
|
|
113
|
+
"""Should return None for commands that don't exist."""
|
|
114
|
+
content = _find_command_content("nonexistent-cmd")
|
|
115
|
+
assert content is None
|
|
116
|
+
|
|
117
|
+
def test_get_command_token_estimate(self):
|
|
118
|
+
"""Should estimate tokens without recording."""
|
|
119
|
+
estimate = get_command_token_estimate("test-cmd")
|
|
120
|
+
|
|
121
|
+
assert estimate is not None
|
|
122
|
+
assert estimate > 0
|
|
123
|
+
|
|
124
|
+
# Verify nothing was recorded (no change to session)
|
|
125
|
+
summary = self.metrics.get_session_summary()
|
|
126
|
+
assert summary.total_tokens == 0
|
|
127
|
+
|
|
128
|
+
def test_track_command_multiple_loads(self):
|
|
129
|
+
"""Should track multiple command loads."""
|
|
130
|
+
tokens1 = track_command_load("test-cmd")
|
|
131
|
+
tokens2 = track_command_load("test-cmd")
|
|
132
|
+
|
|
133
|
+
assert tokens1 is not None
|
|
134
|
+
assert tokens2 is not None
|
|
135
|
+
|
|
136
|
+
summary = self.metrics.get_session_summary()
|
|
137
|
+
assert summary.total_tokens >= tokens1 + tokens2
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class TestCommandTrackerNoSession:
|
|
141
|
+
"""Tests for command tracking without active session."""
|
|
142
|
+
|
|
143
|
+
def setup_method(self):
|
|
144
|
+
"""Create metrics instance without starting session."""
|
|
145
|
+
self.temp_dir = tempfile.mkdtemp()
|
|
146
|
+
self.db_path = Path(self.temp_dir) / "test_metrics.db"
|
|
147
|
+
|
|
148
|
+
self.metrics = TokenMetrics(db_path=self.db_path)
|
|
149
|
+
# Note: NOT starting session
|
|
150
|
+
|
|
151
|
+
import command_tracker
|
|
152
|
+
self._original_get_metrics = command_tracker.get_metrics
|
|
153
|
+
command_tracker.get_metrics = lambda: self.metrics
|
|
154
|
+
|
|
155
|
+
def teardown_method(self):
|
|
156
|
+
"""Clean up."""
|
|
157
|
+
import command_tracker
|
|
158
|
+
command_tracker.get_metrics = self._original_get_metrics
|
|
159
|
+
|
|
160
|
+
if self.db_path.exists():
|
|
161
|
+
self.db_path.unlink()
|
|
162
|
+
os.rmdir(self.temp_dir)
|
|
163
|
+
|
|
164
|
+
def test_track_without_session_returns_none(self):
|
|
165
|
+
"""Should return None when no session is active."""
|
|
166
|
+
tokens = track_command_load("test-cmd", content="Test content")
|
|
167
|
+
assert tokens is None
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
if __name__ == "__main__":
|
|
171
|
+
import pytest
|
|
172
|
+
pytest.main([__file__, "-v"])
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tests for Context Optimizer Service (Phase 5).
|
|
3
|
+
|
|
4
|
+
Tests the trigger keyword detection and context loading optimization.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
from datetime import datetime, timezone
|
|
9
|
+
|
|
10
|
+
# Import the module under test
|
|
11
|
+
import sys
|
|
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 context_optimizer import (
|
|
18
|
+
ContextOptimizer,
|
|
19
|
+
TriggerRule,
|
|
20
|
+
LoadSuggestion,
|
|
21
|
+
DEFAULT_TRIGGER_RULES,
|
|
22
|
+
get_optimizer,
|
|
23
|
+
detect_triggers,
|
|
24
|
+
format_trigger_table,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class TestTriggerRule:
|
|
29
|
+
"""Tests for TriggerRule dataclass."""
|
|
30
|
+
|
|
31
|
+
def test_trigger_rule_creation(self):
|
|
32
|
+
"""Test creating a trigger rule."""
|
|
33
|
+
rule = TriggerRule(
|
|
34
|
+
command="/test",
|
|
35
|
+
keywords=["test", "testing"],
|
|
36
|
+
description="Test command",
|
|
37
|
+
estimated_tokens=100,
|
|
38
|
+
priority=1
|
|
39
|
+
)
|
|
40
|
+
assert rule.command == "/test"
|
|
41
|
+
assert rule.keywords == ["test", "testing"]
|
|
42
|
+
assert rule.estimated_tokens == 100
|
|
43
|
+
assert rule.priority == 1
|
|
44
|
+
|
|
45
|
+
def test_trigger_rule_default_priority(self):
|
|
46
|
+
"""Test default priority is 2."""
|
|
47
|
+
rule = TriggerRule(
|
|
48
|
+
command="/cmd",
|
|
49
|
+
keywords=["kw"],
|
|
50
|
+
description="desc",
|
|
51
|
+
estimated_tokens=50
|
|
52
|
+
)
|
|
53
|
+
assert rule.priority == 2
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class TestLoadSuggestion:
|
|
57
|
+
"""Tests for LoadSuggestion dataclass."""
|
|
58
|
+
|
|
59
|
+
def test_load_suggestion_creation(self):
|
|
60
|
+
"""Test creating a load suggestion."""
|
|
61
|
+
suggestion = LoadSuggestion(
|
|
62
|
+
command="/patterns",
|
|
63
|
+
reason="Anti-patterns and code patterns",
|
|
64
|
+
estimated_tokens=1500,
|
|
65
|
+
priority=1,
|
|
66
|
+
keywords_matched=["anti-pattern", "avoid"]
|
|
67
|
+
)
|
|
68
|
+
assert suggestion.command == "/patterns"
|
|
69
|
+
assert len(suggestion.keywords_matched) == 2
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class TestContextOptimizer:
|
|
73
|
+
"""Tests for ContextOptimizer class."""
|
|
74
|
+
|
|
75
|
+
def test_init_with_default_rules(self):
|
|
76
|
+
"""Test initialization with default rules."""
|
|
77
|
+
optimizer = ContextOptimizer()
|
|
78
|
+
assert len(optimizer.rules) == len(DEFAULT_TRIGGER_RULES)
|
|
79
|
+
|
|
80
|
+
def test_init_with_custom_rules(self):
|
|
81
|
+
"""Test initialization with custom rules."""
|
|
82
|
+
custom_rules = [
|
|
83
|
+
TriggerRule(
|
|
84
|
+
command="/custom",
|
|
85
|
+
keywords=["custom"],
|
|
86
|
+
description="Custom command",
|
|
87
|
+
estimated_tokens=100
|
|
88
|
+
)
|
|
89
|
+
]
|
|
90
|
+
optimizer = ContextOptimizer(custom_rules=custom_rules)
|
|
91
|
+
assert len(optimizer.rules) == 1
|
|
92
|
+
|
|
93
|
+
def test_detect_triggers_finds_patterns(self):
|
|
94
|
+
"""Test detecting triggers for patterns command."""
|
|
95
|
+
optimizer = ContextOptimizer()
|
|
96
|
+
suggestions = optimizer.detect_triggers("I need help avoiding anti-patterns")
|
|
97
|
+
|
|
98
|
+
# Should find /patterns command
|
|
99
|
+
commands = [s.command for s in suggestions]
|
|
100
|
+
assert "/patterns" in commands
|
|
101
|
+
|
|
102
|
+
def test_detect_triggers_finds_checklist(self):
|
|
103
|
+
"""Test detecting triggers for checklist command."""
|
|
104
|
+
optimizer = ContextOptimizer()
|
|
105
|
+
suggestions = optimizer.detect_triggers("I need the quality checklist before PR")
|
|
106
|
+
|
|
107
|
+
commands = [s.command for s in suggestions]
|
|
108
|
+
assert "/checklist" in commands
|
|
109
|
+
|
|
110
|
+
def test_detect_triggers_finds_audit(self):
|
|
111
|
+
"""Test detecting triggers for audit command."""
|
|
112
|
+
optimizer = ContextOptimizer()
|
|
113
|
+
suggestions = optimizer.detect_triggers("Show me my token usage")
|
|
114
|
+
|
|
115
|
+
commands = [s.command for s in suggestions]
|
|
116
|
+
assert "/audit" in commands
|
|
117
|
+
|
|
118
|
+
def test_detect_triggers_case_insensitive(self):
|
|
119
|
+
"""Test that trigger detection is case insensitive."""
|
|
120
|
+
optimizer = ContextOptimizer()
|
|
121
|
+
|
|
122
|
+
lower_result = optimizer.detect_triggers("avoid bad practice")
|
|
123
|
+
upper_result = optimizer.detect_triggers("AVOID BAD PRACTICE")
|
|
124
|
+
|
|
125
|
+
assert len(lower_result) == len(upper_result)
|
|
126
|
+
|
|
127
|
+
def test_detect_triggers_excludes_loaded(self):
|
|
128
|
+
"""Test that already-loaded commands are excluded by default."""
|
|
129
|
+
optimizer = ContextOptimizer()
|
|
130
|
+
optimizer.mark_loaded("/patterns")
|
|
131
|
+
|
|
132
|
+
suggestions = optimizer.detect_triggers("help with anti-patterns")
|
|
133
|
+
commands = [s.command for s in suggestions]
|
|
134
|
+
|
|
135
|
+
assert "/patterns" not in commands
|
|
136
|
+
|
|
137
|
+
def test_detect_triggers_includes_loaded_when_requested(self):
|
|
138
|
+
"""Test including loaded commands when explicitly requested."""
|
|
139
|
+
optimizer = ContextOptimizer()
|
|
140
|
+
optimizer.mark_loaded("/patterns")
|
|
141
|
+
|
|
142
|
+
suggestions = optimizer.detect_triggers(
|
|
143
|
+
"help with anti-patterns",
|
|
144
|
+
include_loaded=True
|
|
145
|
+
)
|
|
146
|
+
commands = [s.command for s in suggestions]
|
|
147
|
+
|
|
148
|
+
assert "/patterns" in commands
|
|
149
|
+
|
|
150
|
+
def test_mark_loaded_normalizes_command(self):
|
|
151
|
+
"""Test that mark_loaded normalizes command names."""
|
|
152
|
+
optimizer = ContextOptimizer()
|
|
153
|
+
|
|
154
|
+
optimizer.mark_loaded("patterns") # Without /
|
|
155
|
+
assert optimizer.is_loaded("/patterns")
|
|
156
|
+
|
|
157
|
+
optimizer.mark_loaded("/checklist") # With /
|
|
158
|
+
assert optimizer.is_loaded("checklist")
|
|
159
|
+
|
|
160
|
+
def test_get_loaded_commands(self):
|
|
161
|
+
"""Test getting list of loaded commands."""
|
|
162
|
+
optimizer = ContextOptimizer()
|
|
163
|
+
optimizer.mark_loaded("/patterns")
|
|
164
|
+
optimizer.mark_loaded("/checklist")
|
|
165
|
+
|
|
166
|
+
loaded = optimizer.get_loaded_commands()
|
|
167
|
+
assert len(loaded) == 2
|
|
168
|
+
assert "/patterns" in loaded
|
|
169
|
+
assert "/checklist" in loaded
|
|
170
|
+
|
|
171
|
+
def test_get_estimated_savings(self):
|
|
172
|
+
"""Test calculating estimated token savings."""
|
|
173
|
+
optimizer = ContextOptimizer()
|
|
174
|
+
|
|
175
|
+
# Initially, nothing is loaded
|
|
176
|
+
savings = optimizer.get_estimated_savings()
|
|
177
|
+
assert savings["commands_loaded"] == 0
|
|
178
|
+
assert savings["deferred_tokens"] > 0
|
|
179
|
+
assert savings["savings_percent"] == 100.0
|
|
180
|
+
|
|
181
|
+
# Load one command
|
|
182
|
+
optimizer.mark_loaded("/patterns")
|
|
183
|
+
savings = optimizer.get_estimated_savings()
|
|
184
|
+
assert savings["commands_loaded"] == 1
|
|
185
|
+
assert savings["loaded_tokens"] > 0
|
|
186
|
+
|
|
187
|
+
def test_format_suggestions_empty(self):
|
|
188
|
+
"""Test formatting empty suggestions returns empty string."""
|
|
189
|
+
optimizer = ContextOptimizer()
|
|
190
|
+
result = optimizer.format_suggestions([])
|
|
191
|
+
assert result == ""
|
|
192
|
+
|
|
193
|
+
def test_format_suggestions_with_items(self):
|
|
194
|
+
"""Test formatting suggestions produces markdown."""
|
|
195
|
+
optimizer = ContextOptimizer()
|
|
196
|
+
suggestions = [
|
|
197
|
+
LoadSuggestion(
|
|
198
|
+
command="/test",
|
|
199
|
+
reason="Test reason",
|
|
200
|
+
estimated_tokens=100,
|
|
201
|
+
priority=1,
|
|
202
|
+
keywords_matched=["test"]
|
|
203
|
+
)
|
|
204
|
+
]
|
|
205
|
+
result = optimizer.format_suggestions(suggestions)
|
|
206
|
+
|
|
207
|
+
assert "/test" in result
|
|
208
|
+
assert "Test reason" in result
|
|
209
|
+
assert "100 tokens" in result
|
|
210
|
+
|
|
211
|
+
def test_format_suggestions_respects_max(self):
|
|
212
|
+
"""Test that format_suggestions respects max_suggestions."""
|
|
213
|
+
optimizer = ContextOptimizer()
|
|
214
|
+
suggestions = optimizer.detect_triggers(
|
|
215
|
+
"anti-pattern avoid mistake checklist quality validate token budget"
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
# Format with max of 2
|
|
219
|
+
result = optimizer.format_suggestions(suggestions, max_suggestions=2)
|
|
220
|
+
|
|
221
|
+
# Should mention "and X more" if there are more
|
|
222
|
+
if len(suggestions) > 2:
|
|
223
|
+
assert "more" in result
|
|
224
|
+
|
|
225
|
+
def test_add_rule(self):
|
|
226
|
+
"""Test adding a custom rule."""
|
|
227
|
+
optimizer = ContextOptimizer()
|
|
228
|
+
initial_count = len(optimizer.rules)
|
|
229
|
+
|
|
230
|
+
new_rule = TriggerRule(
|
|
231
|
+
command="/newcmd",
|
|
232
|
+
keywords=["newkeyword"],
|
|
233
|
+
description="New command",
|
|
234
|
+
estimated_tokens=200
|
|
235
|
+
)
|
|
236
|
+
optimizer.add_rule(new_rule)
|
|
237
|
+
|
|
238
|
+
assert len(optimizer.rules) == initial_count + 1
|
|
239
|
+
|
|
240
|
+
def test_reset_session(self):
|
|
241
|
+
"""Test resetting session state."""
|
|
242
|
+
optimizer = ContextOptimizer()
|
|
243
|
+
optimizer.mark_loaded("/patterns")
|
|
244
|
+
optimizer.mark_loaded("/checklist")
|
|
245
|
+
|
|
246
|
+
assert len(optimizer.get_loaded_commands()) == 2
|
|
247
|
+
|
|
248
|
+
optimizer.reset_session()
|
|
249
|
+
|
|
250
|
+
assert len(optimizer.get_loaded_commands()) == 0
|
|
251
|
+
|
|
252
|
+
def test_suggestions_sorted_by_priority(self):
|
|
253
|
+
"""Test that suggestions are sorted by priority."""
|
|
254
|
+
optimizer = ContextOptimizer()
|
|
255
|
+
suggestions = optimizer.detect_triggers(
|
|
256
|
+
"ralph autonomous migration anti-pattern avoid"
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
# Should be sorted by priority (1, 2, 3)
|
|
260
|
+
priorities = [s.priority for s in suggestions]
|
|
261
|
+
assert priorities == sorted(priorities)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
class TestGlobalFunctions:
|
|
265
|
+
"""Tests for module-level convenience functions."""
|
|
266
|
+
|
|
267
|
+
def test_get_optimizer_returns_singleton(self):
|
|
268
|
+
"""Test that get_optimizer returns a singleton."""
|
|
269
|
+
opt1 = get_optimizer()
|
|
270
|
+
opt2 = get_optimizer()
|
|
271
|
+
assert opt1 is opt2
|
|
272
|
+
|
|
273
|
+
def test_detect_triggers_convenience(self):
|
|
274
|
+
"""Test the convenience detect_triggers function."""
|
|
275
|
+
suggestions = detect_triggers("help with anti-patterns")
|
|
276
|
+
assert len(suggestions) > 0
|
|
277
|
+
|
|
278
|
+
def test_format_trigger_table(self):
|
|
279
|
+
"""Test formatting trigger table for CLAUDE.md."""
|
|
280
|
+
table = format_trigger_table()
|
|
281
|
+
|
|
282
|
+
assert "| Trigger | Command | Description |" in table
|
|
283
|
+
assert "/patterns" in table
|
|
284
|
+
assert "/checklist" in table
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
class TestDefaultTriggerRules:
|
|
288
|
+
"""Tests for the default trigger rules configuration."""
|
|
289
|
+
|
|
290
|
+
def test_default_rules_exist(self):
|
|
291
|
+
"""Test that default rules are defined."""
|
|
292
|
+
assert len(DEFAULT_TRIGGER_RULES) > 0
|
|
293
|
+
|
|
294
|
+
def test_patterns_rule_exists(self):
|
|
295
|
+
"""Test that /patterns rule exists with correct keywords."""
|
|
296
|
+
patterns_rule = next(
|
|
297
|
+
(r for r in DEFAULT_TRIGGER_RULES if r.command == "/patterns"),
|
|
298
|
+
None
|
|
299
|
+
)
|
|
300
|
+
assert patterns_rule is not None
|
|
301
|
+
assert "anti-pattern" in patterns_rule.keywords
|
|
302
|
+
assert patterns_rule.priority == 1
|
|
303
|
+
|
|
304
|
+
def test_checklist_rule_exists(self):
|
|
305
|
+
"""Test that /checklist rule exists with correct keywords."""
|
|
306
|
+
checklist_rule = next(
|
|
307
|
+
(r for r in DEFAULT_TRIGGER_RULES if r.command == "/checklist"),
|
|
308
|
+
None
|
|
309
|
+
)
|
|
310
|
+
assert checklist_rule is not None
|
|
311
|
+
assert "checklist" in checklist_rule.keywords
|
|
312
|
+
assert "quality" in checklist_rule.keywords
|
|
313
|
+
|
|
314
|
+
def test_all_rules_have_required_fields(self):
|
|
315
|
+
"""Test that all default rules have required fields."""
|
|
316
|
+
for rule in DEFAULT_TRIGGER_RULES:
|
|
317
|
+
assert rule.command.startswith("/")
|
|
318
|
+
assert len(rule.keywords) > 0
|
|
319
|
+
assert rule.description
|
|
320
|
+
assert rule.estimated_tokens > 0
|
|
321
|
+
assert rule.priority in [1, 2, 3]
|