diff --git a/src/quant_strategy_plugins/ai_audit.py b/src/quant_strategy_plugins/ai_audit.py index 0320c6d..fe79792 100644 --- a/src/quant_strategy_plugins/ai_audit.py +++ b/src/quant_strategy_plugins/ai_audit.py @@ -118,7 +118,7 @@ def _env_bool(*names: str, default: bool = False) -> bool: def _sanitize_user_input(value: Any, *, max_length: int = SANITIZE_MAX_FIELD_LENGTH) -> str: """Strip control characters and truncate free-text fields before LLM submission.""" - text = str(value or "").strip() + text = str(value if value is not None else "").strip() # Remove C0/C1 control chars except common whitespace (tab, newline) text = re.sub(r"[\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\x9f]", "", text) return text[:max_length] diff --git a/tests/test_ai_audit.py b/tests/test_ai_audit.py index c663c7f..305f202 100644 --- a/tests/test_ai_audit.py +++ b/tests/test_ai_audit.py @@ -499,3 +499,34 @@ def test_advisory_provider_mismatch_still_rejected(monkeypatch): )) with pytest.raises(AiAuditError, match="ai_gateway_provider_mismatch"): _llm_via_gateway("synthetic prompt", "test-model", "openai", 1.0) + + +@pytest.mark.parametrize("value,expected", [ + (False, "False"), (True, "True"), (0, "0"), (0.0, "0.0"), + (None, ""), ("", ""), (" text\x00\x7f ", "text"), +]) +def test_input_sanitizer_preserves_known_falsy_values(value, expected): + assert ai_audit._sanitize_user_input(value) == expected + + +def test_input_sanitizer_still_truncates_after_cleaning(): + assert ai_audit._sanitize_user_input(" a\x00bcdef ", max_length=3) == "abc" + + +@pytest.mark.parametrize("builder,boolean_fields", [ + (ai_audit._build_crisis_audit_messages, ( + "would_trade_if_enabled", "price_scanner_active", "bubble_fragility_active", "kill_switch_active", + )), + (ai_audit._build_taco_audit_messages, ( + "manual_review_required", "rebound_context_active", "event_context_active", + "price_stress_scan_active", "price_crisis_guard_active", + )), +]) +def test_original_prompts_distinguish_false_zero_and_missing(builder, boolean_fields): + source = dict.fromkeys(boolean_fields, False) + source["data_freshness"] = 0 + user = json.loads(builder(source)[1]["content"]) + assert all(user[field] == "False" for field in boolean_fields) + assert user["data_freshness"] == "0" + assert user["as_of"] == "" + assert source == {**dict.fromkeys(boolean_fields, False), "data_freshness": 0}