Files
Aperant/tests/test_thinking_level_validation.py
T
Andy 390ba6a588 fix(fast-mode): use setting_sources instead of env var for CLI fast mode (#1771)
* fix(fast-mode): use setting_sources instead of env var for CLI fast mode

The Claude Code CLI reads fastMode from user settings (~/.claude/settings.json),
not from environment variables. The previous CLAUDE_CODE_FAST_MODE env var approach
was non-functional. This fix writes fastMode=true to user settings before spawning
the CLI and enables the "user" setting source so the CLI reads it.

Key changes:
- Fast mode now writes to ~/.claude/settings.json with atomic file writes
- Extracted shared fast mode helpers into core/fast_mode.py (DRY)
- Moved fast mode toggle from global settings to per-task configuration
- Added opus-4.5 model option and adaptive thinking badges
- Sanitize legacy thinking levels (ultrathink→high, none→low) at all layers
- Shared LEGACY_THINKING_MAP, PHASE_KEYS, sanitizeThinkingLevel in frontend
- Moved diagnostic test script to scripts/ to prevent pytest collection
- SDK requirement bumped to >=0.1.33 for Opus 4.6 adaptive thinking

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: persist fastMode toggle state in task edit and creation dialogs

When users toggled fast mode OFF, the change didn't persist because the code used
a conditional that only set fastMode when true. This meant the old fastMode: true
value was preserved during metadata merge. Now fastMode is always set explicitly,
matching the pattern used by requireReviewBeforeCoding.

Fixed in both:
- TaskEditDialog.tsx line 251
- TaskCreationWizard.tsx line 459

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: address PR review findings for fast mode implementation

- Fix fastMode toggle not persisting when disabled in TaskEditDialog
- Replace manual atomic write with write_json_atomic from core/file_utils
- Rename _ensure_fast_mode_in_user_settings to public (no underscore)
- Replace hardcoded validLevels with VALID_THINKING_LEVELS constant

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix github issues

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 23:34:16 +01:00

127 lines
5.1 KiB
Python

"""
Tests for thinking level validation in phase_config module.
Ensures that invalid thinking levels are caught with proper warnings
and default to 'medium' as expected.
"""
import logging
import sys
from pathlib import Path
# Add auto-claude to path
sys.path.insert(0, str(Path(__file__).parent.parent / "apps" / "backend"))
from phase_config import THINKING_BUDGET_MAP, get_thinking_budget, sanitize_thinking_level
class TestThinkingLevelValidation:
"""Test thinking level validation and error handling."""
def test_valid_thinking_levels(self):
"""Test that all valid thinking levels return correct budgets."""
valid_levels = ["low", "medium", "high"]
for level in valid_levels:
budget = get_thinking_budget(level)
expected = THINKING_BUDGET_MAP[level]
assert budget == expected, f"Expected {expected} for {level}, got {budget}"
def test_invalid_level_logs_warning(self, caplog):
"""Test that invalid thinking level logs a warning."""
with caplog.at_level(logging.WARNING):
budget = get_thinking_budget("invalid_level")
# Should default to medium
assert budget == THINKING_BUDGET_MAP["medium"]
# Should have logged a warning
assert len(caplog.records) == 1
assert "Invalid thinking_level 'invalid_level'" in caplog.text
assert "Valid values:" in caplog.text
assert "Defaulting to 'medium'" in caplog.text
def test_invalid_level_shows_valid_options(self, caplog):
"""Test that warning message includes all valid options."""
with caplog.at_level(logging.WARNING):
get_thinking_budget("bad_value")
# Check all valid levels are mentioned
for level in ["low", "medium", "high"]:
assert level in caplog.text
def test_empty_string_level(self, caplog):
"""Test that empty string is treated as invalid."""
with caplog.at_level(logging.WARNING):
budget = get_thinking_budget("")
assert budget == THINKING_BUDGET_MAP["medium"]
assert "Invalid thinking_level" in caplog.text
def test_case_sensitive(self, caplog):
"""Test that thinking level is case-sensitive."""
with caplog.at_level(logging.WARNING):
# "MEDIUM" should be invalid (not "medium")
budget = get_thinking_budget("MEDIUM")
assert budget == THINKING_BUDGET_MAP["medium"]
assert "Invalid thinking_level 'MEDIUM'" in caplog.text
def test_multiple_invalid_calls(self, caplog):
"""Test that each invalid call produces a warning."""
invalid_levels = ["bad1", "bad2", "bad3"]
with caplog.at_level(logging.WARNING):
for level in invalid_levels:
get_thinking_budget(level)
# Should have 3 warnings
assert len(caplog.records) == 3
def test_budget_values_match_expected(self):
"""Test that budget values match documented amounts."""
assert get_thinking_budget("low") == 1024
assert get_thinking_budget("medium") == 4096
assert get_thinking_budget("high") == 16384
def test_removed_none_treated_as_invalid(self, caplog):
"""Test that removed 'none' level is treated as invalid and defaults to medium."""
with caplog.at_level(logging.WARNING):
budget = get_thinking_budget("none")
assert budget == THINKING_BUDGET_MAP["medium"]
assert "Invalid thinking_level 'none'" in caplog.text
def test_removed_ultrathink_treated_as_invalid(self, caplog):
"""Test that removed 'ultrathink' level is treated as invalid and defaults to medium."""
with caplog.at_level(logging.WARNING):
budget = get_thinking_budget("ultrathink")
assert budget == THINKING_BUDGET_MAP["medium"]
assert "Invalid thinking_level 'ultrathink'" in caplog.text
class TestSanitizeThinkingLevel:
"""Test sanitize_thinking_level for CLI argparse validation."""
def test_valid_levels_pass_through(self):
"""Test that valid thinking levels are returned unchanged."""
assert sanitize_thinking_level("low") == "low"
assert sanitize_thinking_level("medium") == "medium"
assert sanitize_thinking_level("high") == "high"
def test_ultrathink_maps_to_high(self):
"""Test that legacy 'ultrathink' is mapped to 'high'."""
assert sanitize_thinking_level("ultrathink") == "high"
def test_none_maps_to_low(self):
"""Test that legacy 'none' is mapped to 'low'."""
assert sanitize_thinking_level("none") == "low"
def test_unknown_value_defaults_to_medium(self):
"""Test that completely unknown values default to 'medium'."""
assert sanitize_thinking_level("garbage") == "medium"
assert sanitize_thinking_level("") == "medium"
assert sanitize_thinking_level("ULTRA") == "medium"
def test_case_sensitive(self):
"""Test that sanitize_thinking_level is case-sensitive."""
assert sanitize_thinking_level("HIGH") == "medium"
assert sanitize_thinking_level("Medium") == "medium"