"""
Integration tests for Phase 3: Dual-Tier Pitting Assessment
Tests the end-to-end integration of:
- Tier 1: PREN/CPT empirical assessment (always available)
- Tier 2: E_pit vs E_mix electrochemical assessment (requires DO, NRL materials)
Validates:
1. Tier 1 works without DO (all materials)
2. Tier 2 activates with DO (NRL materials: HY80, HY100, SS316)
3. Tier 2 gracefully degrades to Tier 1 on errors
4. Output structure matches documentation
Author: Claude Code
Date: 2025-10-19
Codex Session: 0199ff66-c28e-7cf0-86b4-1f7b3abe09ba
"""
import pytest
from tools.mechanistic.localized_corrosion import calculate_localized_corrosion
# Test 1: Tier 1 only (no DO provided)
def test_tier1_only_316L():
"""Test Tier 1 PREN/CPT assessment without dissolved oxygen."""
result = calculate_localized_corrosion(
material="316L",
temperature_C=60.0,
Cl_mg_L=500.0,
pH=7.0,
)
# Tier 1 fields must be present
assert "pitting" in result
assert "CPT_C" in result["pitting"]
assert "PREN" in result["pitting"]
assert "Cl_threshold_mg_L" in result["pitting"]
assert "susceptibility" in result["pitting"]
assert "margin_C" in result["pitting"]
assert "interpretation" in result["pitting"]
# Tier 2 fields must be None (no DO)
assert result["pitting"]["E_pit_VSCE"] is None
assert result["pitting"]["E_mix_VSCE"] is None
assert result["pitting"]["electrochemical_margin_V"] is None
assert result["pitting"]["electrochemical_risk"] is None
assert result["pitting"]["electrochemical_interpretation"] is None
# Validate Tier 1 values
assert result["pitting"]["PREN"] > 20 # 316L has PREN ≈ 24
assert result["pitting"]["CPT_C"] > 0 # Should have positive CPT
assert result["pitting"]["susceptibility"] in ["low", "moderate", "high", "critical"]
# Test 2: Tier 1 + Tier 2 (SS316 with DO, seawater conditions)
def test_tier1_tier2_SS316_seawater():
"""Test dual-tier assessment for SS316 in seawater with dissolved oxygen."""
result = calculate_localized_corrosion(
material="SS316",
temperature_C=25.0,
Cl_mg_L=19000.0, # Seawater chloride
pH=8.0,
dissolved_oxygen_mg_L=8.0, # Aerated seawater
)
# Tier 1 fields must be present
assert "pitting" in result
assert result["pitting"]["CPT_C"] is not None
assert result["pitting"]["PREN"] is not None
# Tier 2 fields must be populated (SS316 + DO)
assert result["pitting"]["E_pit_VSCE"] is not None
assert result["pitting"]["E_mix_VSCE"] is not None
assert result["pitting"]["electrochemical_margin_V"] is not None
assert result["pitting"]["electrochemical_risk"] is not None
assert result["pitting"]["electrochemical_interpretation"] is not None
# Validate Tier 2 values (E_pit should be higher than E_mix for SS316 at seawater)
E_pit = result["pitting"]["E_pit_VSCE"]
E_mix = result["pitting"]["E_mix_VSCE"]
dE = result["pitting"]["electrochemical_margin_V"]
assert E_pit > 0.35 # SS316 E_pit in seawater typically 0.35-0.5 V_SCE (NRL kinetics)
assert E_mix > 0.0 # Aerated seawater E_mix typically 0.3-0.6 V_SCE
assert dE == pytest.approx(E_mix - E_pit, abs=0.001)
# Note: In seawater (19g/L Cl⁻), E_mix may exceed E_pit for SS316, indicating pitting risk
# The electrochemical assessment correctly identifies this as high/critical risk
assert result["pitting"]["electrochemical_risk"] in ["low", "moderate", "high", "critical"]
# Codex improvement: Check tier disagreement detection
assert "tier_disagreement" in result
# SS316 seawater: Both Tier 1 and Tier 2 may indicate high/critical risk
if result["tier_disagreement"]["detected"]:
assert result["tier_disagreement"]["tier1_assessment"] is not None
assert result["tier_disagreement"]["tier2_assessment"] is not None
assert "explanation" in result["tier_disagreement"]
# Test 3: Tier 2 behavior for HY80 at seawater
def test_tier2_graceful_degradation_HY80():
"""Test Tier 2 behavior for HY80 (may compute or degrade gracefully)."""
result = calculate_localized_corrosion(
material="HY80",
temperature_C=25.0,
Cl_mg_L=19000.0,
pH=8.0,
dissolved_oxygen_mg_L=8.0,
)
# Tier 1 must still work
assert "pitting" in result
assert result["pitting"]["CPT_C"] is not None
assert result["pitting"]["PREN"] is not None
assert result["pitting"]["susceptibility"] in ["low", "moderate", "high", "critical"]
# Tier 2 may compute a value or gracefully degrade to None
# (depends on NRL kinetics - HY80 may have valid or negative activation energies)
E_pit = result["pitting"]["E_pit_VSCE"]
if E_pit is not None:
# Tier 2 computed successfully - validate structure
assert result["pitting"]["E_mix_VSCE"] is not None
assert result["pitting"]["electrochemical_margin_V"] is not None
assert result["pitting"]["electrochemical_risk"] in ["low", "moderate", "high", "critical"]
assert result["pitting"]["electrochemical_interpretation"] is not None
else:
# Tier 2 degraded gracefully - validate fallback
assert result["pitting"]["E_mix_VSCE"] is None
assert result["pitting"]["electrochemical_margin_V"] is None
assert result["pitting"]["electrochemical_risk"] is None
assert result["pitting"]["electrochemical_interpretation"] is not None
assert "unavailable" in result["pitting"]["electrochemical_interpretation"].lower()
# Overall result must still be valid
assert "overall_risk" in result
assert "recommendations" in result
# Test 4: Tier 1 only for non-NRL materials (even with DO)
def test_tier1_only_non_nrl_material():
"""Test that non-NRL materials (e.g., 2205 duplex) only get Tier 1."""
result = calculate_localized_corrosion(
material="2205", # Duplex, not in NRL database
temperature_C=40.0,
Cl_mg_L=1000.0,
pH=7.5,
dissolved_oxygen_mg_L=6.0, # DO provided, but material not in NRL database
)
# Tier 1 must be present
assert result["pitting"]["CPT_C"] is not None
assert result["pitting"]["PREN"] is not None
assert result["pitting"]["PREN"] > 30 # Duplex 2205 has PREN ≈ 35
# Tier 2 must be None (not an NRL material)
assert result["pitting"]["E_pit_VSCE"] is None
assert result["pitting"]["E_mix_VSCE"] is None
assert result["pitting"]["electrochemical_margin_V"] is None
# Test 5: Tier 2 with low DO (anaerobic conditions)
def test_tier2_low_DO_anaerobic():
"""Test Tier 2 with low dissolved oxygen (anaerobic conditions)."""
result = calculate_localized_corrosion(
material="SS316",
temperature_C=35.0,
Cl_mg_L=500.0,
pH=7.2,
dissolved_oxygen_mg_L=0.5, # Anaerobic (low DO)
)
# Both Tier 1 and Tier 2 should be present
assert result["pitting"]["CPT_C"] is not None
assert result["pitting"]["E_pit_VSCE"] is not None
assert result["pitting"]["E_mix_VSCE"] is not None
# E_mix should be lower than aerated seawater (0.6-0.7 V_SCE)
E_mix = result["pitting"]["E_mix_VSCE"]
assert E_mix < 0.6 # Low DO → moderately low E_mix (but not as low as fully anaerobic)
# Test 6: Output structure validation
def test_output_structure():
"""Validate complete output structure matches documentation."""
result = calculate_localized_corrosion(
material="SS316",
temperature_C=25.0,
Cl_mg_L=19000.0,
pH=8.0,
dissolved_oxygen_mg_L=8.0,
)
# Top-level keys
assert "pitting" in result
assert "crevice" in result
assert "material" in result
assert "temperature_C" in result
assert "Cl_mg_L" in result
assert "pH" in result
assert "overall_risk" in result
assert "recommendations" in result
# Pitting Tier 1 keys
pitting = result["pitting"]
tier1_keys = ["CPT_C", "PREN", "Cl_threshold_mg_L", "susceptibility", "margin_C", "interpretation"]
for key in tier1_keys:
assert key in pitting
# Pitting Tier 2 keys (when present)
tier2_keys = [
"E_pit_VSCE",
"E_mix_VSCE",
"electrochemical_margin_V",
"electrochemical_risk",
"electrochemical_interpretation",
]
for key in tier2_keys:
assert key in pitting
# Crevice keys
crevice = result["crevice"]
crevice_keys = ["CCT_C", "IR_drop_V", "acidification_factor", "susceptibility", "margin_C", "interpretation"]
for key in crevice_keys:
assert key in crevice
# Recommendations must be list
assert isinstance(result["recommendations"], list)
# Test 7: Tier 1 vs Tier 2 risk comparison (CPT vs E_pit)
def test_tier1_tier2_risk_comparison():
"""Test case where Tier 1 and Tier 2 give different risk assessments."""
# SS316 at T > CPT (critical by Tier 1), but E_pit >> E_mix (low by Tier 2)
result = calculate_localized_corrosion(
material="SS316",
temperature_C=30.0, # Slightly above CPT (~10°C for SS316)
Cl_mg_L=200.0, # Moderate chloride
pH=7.5,
dissolved_oxygen_mg_L=8.0, # Aerated
)
tier1_susceptibility = result["pitting"]["susceptibility"]
tier2_risk = result["pitting"]["electrochemical_risk"]
# Tier 1 may say "critical" or "high" (T > CPT)
# Tier 2 may say "low" (E_mix << E_pit due to low Cl)
# Both are valid, user must interpret based on context
assert tier1_susceptibility in ["low", "moderate", "high", "critical"]
assert tier2_risk in ["low", "moderate", "high", "critical"]
# At least one tier should provide valid assessment
assert tier1_susceptibility is not None or tier2_risk is not None
# Test 8: Material alias mapping (Codex improvement)
def test_material_alias_316L():
"""Test that 316L alias maps to SS316 for Tier 2."""
result = calculate_localized_corrosion(
material="316L", # Alias, should map to SS316
temperature_C=25.0,
Cl_mg_L=19000.0,
pH=8.0,
dissolved_oxygen_mg_L=8.0,
)
# Tier 2 should be available (316L → SS316)
assert result["pitting"]["E_pit_VSCE"] is not None
assert result["pitting"]["E_mix_VSCE"] is not None
assert result["pitting"]["electrochemical_risk"] is not None
# Should not have "unavailable" message
assert "unavailable" not in result["pitting"]["electrochemical_interpretation"].lower()
# Test 9: RedoxState warning propagation (Codex recommendation)
def test_redox_warning_propagation():
"""Test that RedoxState warnings are surfaced in Tier 2 interpretation."""
# Use very low DO to trigger anaerobic warning (if threshold met)
result = calculate_localized_corrosion(
material="SS316",
temperature_C=35.0,
Cl_mg_L=500.0,
pH=7.2,
dissolved_oxygen_mg_L=0.01, # Very low DO (anaerobic)
)
# Tier 2 should be calculated
assert result["pitting"]["E_pit_VSCE"] is not None
assert result["pitting"]["E_mix_VSCE"] is not None
assert result["pitting"]["electrochemical_interpretation"] is not None
# If RedoxState generates warning, it should appear in interpretation
# Note: May not trigger if threshold is lower than 0.01 mg/L
# This test validates the *propagation mechanism*, not the threshold
interpretation = result["pitting"]["electrochemical_interpretation"]
# Test passes if either:
# 1. Warning present (ideal), or
# 2. No warning but mechanism is wired (interpretation is string, not None)
assert isinstance(interpretation, str)
# If warning present, validate format
if "[RedoxState:" in interpretation:
assert interpretation.endswith("]") # Proper formatting
if __name__ == "__main__":
# Run tests with verbose output
pytest.main([__file__, "-v", "-s"])