diff --git a/.gitignore b/.gitignore index e76c980..8d51b66 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,8 @@ deck_files/ csv_files/ !config/card_lists/*.json !config/deck.json +!test_exclude_cards.txt +!test_include_exclude_config.json RELEASE_NOTES.md *.bkp .github/*.md \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 233722e..6b14a2d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ This format follows Keep a Changelog principles and aims for Semantic Versioning ## [Unreleased] ### Added +- Comprehensive structured logging for include/exclude operations with event tracking - Include/exclude card lists feature with `ALLOW_MUST_HAVES=true` environment variable flag - Phase 1 exclude-only implementation: filter cards from deck building pool before construction - Web UI "Advanced Options" section with exclude cards textarea and file upload (.txt) @@ -29,6 +30,10 @@ This format follows Keep a Changelog principles and aims for Semantic Versioning - Engine integration with include injection after lands, before creatures/spells with ordering tests - Exclude re-entry prevention ensuring blocked cards cannot re-enter via downstream heuristics - Web UI enhancement with two-column layout, chips/tag UI, and real-time validation +- EDH format compliance checking for include/exclude cards against commander color identity + +### Changed +- **Test organization**: Moved all test files from project root to centralized `code/tests/` directory for better structure - **CLI enhancement: Enhanced help text with type indicators** - All CLI arguments now show expected value types (PATH, NAME, INT, BOOL) and organized into logical groups - **CLI enhancement: Ideal count arguments** - New CLI flags for deck composition: `--ramp-count`, `--land-count`, `--basic-land-count`, `--creature-count`, `--removal-count`, `--wipe-count`, `--card-advantage-count`, `--protection-count` - **CLI enhancement: Theme tag name support** - Theme selection by name instead of index: `--primary-tag`, `--secondary-tag`, `--tertiary-tag` as alternatives to numeric choices diff --git a/RELEASE_NOTES_TEMPLATE.md b/RELEASE_NOTES_TEMPLATE.md index 3fe70fb..5b3af95 100644 --- a/RELEASE_NOTES_TEMPLATE.md +++ b/RELEASE_NOTES_TEMPLATE.md @@ -1,7 +1,8 @@ # MTG Python Deckbuilder ${VERSION} ## Highlights -- **Include/Exclude Cards Feature Complete**: Full implementation with enhanced web UI, intelligent fuzzy matching, and performance optimization. Users can now specify must-include and must-exclude cards with comprehensive card knowledge base and excellent performance. +- **Quality & Observability Complete**: Comprehensive structured logging system with event tracking for include/exclude operations providing detailed diagnostics and operational insights. +- **Include/Exclude Cards Feature Complete**: Full implementation with enhanced web UI, intelligent fuzzy matching, color identity validation, and performance optimization. Users can now specify must-include and must-exclude cards with comprehensive EDH format compliance. - **Enhanced CLI with Type Safety**: Comprehensive CLI enhancement with type indicators, ideal count arguments, and theme tag name support making headless operation more user-friendly and discoverable. - **Theme Tag Name Selection**: Intelligent theme selection by name instead of index numbers, automatically resolving to correct choices accounting for selection ordering. - **Enhanced Fuzzy Matching**: Advanced algorithm with 300+ Commander-legal card knowledge base, popular/iconic card prioritization, and dark theme confirmation modal for optimal user experience. @@ -11,6 +12,10 @@ - **Dual Architecture Support**: Seamless functionality across both web interface (staging system) and CLI (direct build) with proper include injection timing. ## What's new +- **Quality & Observability** + - Structured logging with event types: EXCLUDE_FILTER, INCLUDE_EXCLUDE_CONFLICT, STRICT_MODE_SUCCESS/FAILURE, INCLUDE_COLOR_VIOLATION + - Comprehensive diagnostics for include/exclude operations with performance metrics and validation results + - Enhanced error tracking and operational visibility for debugging and monitoring - **Enhanced CLI Experience** - Type-safe help text with value indicators (PATH, NAME, INT, BOOL) and organized argument groups - Ideal count CLI arguments: `--ramp-count`, `--land-count`, `--creature-count`, etc. for deck composition control @@ -28,7 +33,7 @@ - Enhanced fuzzy matching algorithm with 300+ Commander-legal card knowledge base - Popular cards (184) and iconic cards (102) prioritization for improved matching accuracy - Dark theme confirmation modal with card preview and top 3 alternatives for <90% confidence matches - - Color identity validation ensuring included cards match commander colors + - **EDH color identity validation**: Automatic checking of included cards against commander color identity with clear illegal status feedback - File upload support (.txt) with deduplication and user feedback - JSON export/import preserving all include/exclude configuration via permalink system - **Web Interface Enhancement** diff --git a/code/deck_builder/builder.py b/code/deck_builder/builder.py index f1efcf3..e4859c7 100644 --- a/code/deck_builder/builder.py +++ b/code/deck_builder/builder.py @@ -1364,9 +1364,16 @@ class DeckBuilder( # 5. Color identity validation for includes if processed_includes and hasattr(self, 'color_identity') and self.color_identity: - # This would need commander color identity checking logic - # For now, accept all includes (color validation can be added later) - pass + validated_includes = [] + for card_name in processed_includes: + if self._validate_card_color_identity(card_name): + validated_includes.append(card_name) + else: + diagnostics.ignored_color_identity.append(card_name) + # M5: Structured logging for color identity violations + logger.warning(f"INCLUDE_COLOR_VIOLATION: card={card_name} commander_colors={self.color_identity}") + self.output_func(f"Card '{card_name}' has invalid color identity for commander (ignored)") + processed_includes = validated_includes # 6. Handle exclude conflicts (exclude overrides include) final_includes = [] @@ -1433,6 +1440,64 @@ class DeckBuilder( # M5: Structured logging for strict mode success logger.info("STRICT_MODE_SUCCESS: all_includes_satisfied=true") + def _validate_card_color_identity(self, card_name: str) -> bool: + """ + Check if a card's color identity is legal for this commander. + + Args: + card_name: Name of the card to validate + + Returns: + True if card is legal for commander's color identity, False otherwise + """ + if not hasattr(self, 'color_identity') or not self.color_identity: + # No commander color identity set, allow all cards + return True + + # Get card data from our dataframes + if hasattr(self, '_full_cards_df') and self._full_cards_df is not None: + # Handle both possible column names + name_col = 'name' if 'name' in self._full_cards_df.columns else 'Name' + card_matches = self._full_cards_df[self._full_cards_df[name_col].str.lower() == card_name.lower()] + if not card_matches.empty: + card_row = card_matches.iloc[0] + card_color_identity = card_row.get('colorIdentity', '') + + # Parse card's color identity + if isinstance(card_color_identity, str) and card_color_identity.strip(): + # Handle "Colorless" as empty color identity + if card_color_identity.lower() == 'colorless': + card_colors = [] + elif ',' in card_color_identity: + # Handle format like "R, U" or "W, U, B" + card_colors = [c.strip() for c in card_color_identity.split(',') if c.strip()] + elif card_color_identity.startswith('[') and card_color_identity.endswith(']'): + # Handle format like "['W']" or "['U','R']" + import ast + try: + card_colors = ast.literal_eval(card_color_identity) + except Exception: + # Fallback parsing + card_colors = [c.strip().strip("'\"") for c in card_color_identity.strip('[]').split(',') if c.strip()] + else: + # Handle simple format like "W" or single color + card_colors = [card_color_identity.strip()] + elif isinstance(card_color_identity, list): + card_colors = card_color_identity + else: + # No color identity or colorless + card_colors = [] + + # Check if card's colors are subset of commander's colors + commander_colors = set(self.color_identity) + card_colors_set = set(c.upper() for c in card_colors if c) + + return card_colors_set.issubset(commander_colors) + + # If we can't find the card or determine its color identity, assume it's illegal + # (This is safer for validation purposes) + return False + # --------------------------- # Card Library Management # --------------------------- diff --git a/fuzzy_test.html b/code/tests/fuzzy_test.html similarity index 100% rename from fuzzy_test.html rename to code/tests/fuzzy_test.html diff --git a/test_cli_ideal_counts.py b/code/tests/test_cli_ideal_counts.py similarity index 100% rename from test_cli_ideal_counts.py rename to code/tests/test_cli_ideal_counts.py diff --git a/test_comprehensive_exclude.py b/code/tests/test_comprehensive_exclude.py similarity index 100% rename from test_comprehensive_exclude.py rename to code/tests/test_comprehensive_exclude.py diff --git a/test_constants_refactor.py b/code/tests/test_constants_refactor.py similarity index 100% rename from test_constants_refactor.py rename to code/tests/test_constants_refactor.py diff --git a/test_direct_exclude.py b/code/tests/test_direct_exclude.py similarity index 100% rename from test_direct_exclude.py rename to code/tests/test_direct_exclude.py diff --git a/code/tests/test_exclude_cards.txt b/code/tests/test_exclude_cards.txt new file mode 100644 index 0000000..3af1222 --- /dev/null +++ b/code/tests/test_exclude_cards.txt @@ -0,0 +1,5 @@ +Sol Ring +Rhystic Study +Smothering Tithe +Lightning Bolt +Counterspell diff --git a/test_exclude_filtering.py b/code/tests/test_exclude_filtering.py similarity index 100% rename from test_exclude_filtering.py rename to code/tests/test_exclude_filtering.py diff --git a/test_exclude_integration.py b/code/tests/test_exclude_integration.py similarity index 100% rename from test_exclude_integration.py rename to code/tests/test_exclude_integration.py diff --git a/test_final_fuzzy.py b/code/tests/test_final_fuzzy.py similarity index 100% rename from test_final_fuzzy.py rename to code/tests/test_final_fuzzy.py diff --git a/test_fuzzy_logic.py b/code/tests/test_fuzzy_logic.py similarity index 100% rename from test_fuzzy_logic.py rename to code/tests/test_fuzzy_logic.py diff --git a/test_fuzzy_modal.py b/code/tests/test_fuzzy_modal.py similarity index 100% rename from test_fuzzy_modal.py rename to code/tests/test_fuzzy_modal.py diff --git a/test_improved_fuzzy.py b/code/tests/test_improved_fuzzy.py similarity index 100% rename from test_improved_fuzzy.py rename to code/tests/test_improved_fuzzy.py diff --git a/code/tests/test_include_exclude_config.json b/code/tests/test_include_exclude_config.json new file mode 100644 index 0000000..028f0bd --- /dev/null +++ b/code/tests/test_include_exclude_config.json @@ -0,0 +1,19 @@ +{ + "commander": "Alania, Divergent Storm", + "primary_tag": "Spellslinger", + "secondary_tag": "Otter Kindred", + "bracket_level": 3, + "include_cards": [ + "Sol Ring", + "Lightning Bolt", + "Counterspell" + ], + "exclude_cards": [ + "Mana Crypt", + "Brainstorm", + "Force of Will" + ], + "enforcement_mode": "warn", + "allow_illegal": false, + "fuzzy_matching": true +} diff --git a/test_include_exclude_performance.py b/code/tests/test_include_exclude_performance.py similarity index 100% rename from test_include_exclude_performance.py rename to code/tests/test_include_exclude_performance.py diff --git a/test_json_reexport.py b/code/tests/test_json_reexport.py similarity index 100% rename from test_json_reexport.py rename to code/tests/test_json_reexport.py diff --git a/test_lightning_direct.py b/code/tests/test_lightning_direct.py similarity index 100% rename from test_lightning_direct.py rename to code/tests/test_lightning_direct.py diff --git a/test_structured_logging.py b/code/tests/test_m5_logging.py similarity index 100% rename from test_structured_logging.py rename to code/tests/test_m5_logging.py diff --git a/test_specific_matches.py b/code/tests/test_specific_matches.py similarity index 100% rename from test_specific_matches.py rename to code/tests/test_specific_matches.py diff --git a/code/tests/test_structured_logging.py b/code/tests/test_structured_logging.py new file mode 100644 index 0000000..ab4145c --- /dev/null +++ b/code/tests/test_structured_logging.py @@ -0,0 +1,152 @@ +#!/usr/bin/env python3 +""" +Test M5 Quality & Observability features. +Verify structured logging events for include/exclude decisions. +""" + +import sys +import os +import logging +import io +sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'code')) + +from deck_builder.builder import DeckBuilder + + +def test_m5_structured_logging(): + """Test that M5 structured logging events are emitted correctly.""" + + # Capture log output + log_capture = io.StringIO() + handler = logging.StreamHandler(log_capture) + handler.setLevel(logging.INFO) + formatter = logging.Formatter('%(levelname)s:%(name)s:%(message)s') + handler.setFormatter(formatter) + + # Get the deck builder logger + from deck_builder import builder + logger = logging.getLogger(builder.__name__) + logger.addHandler(handler) + logger.setLevel(logging.INFO) + + print("๐Ÿ” Testing M5 Structured Logging...") + + try: + # Create a mock builder instance + builder_obj = DeckBuilder() + + # Mock the required functions to avoid prompts + from unittest.mock import Mock + builder_obj.input_func = Mock(return_value="") + builder_obj.output_func = Mock() + + # Set up test attributes + builder_obj.commander_name = "Alesha, Who Smiles at Death" + builder_obj.include_cards = ["Sol Ring", "Lightning Bolt", "Chaos Warp"] + builder_obj.exclude_cards = ["Mana Crypt", "Force of Will"] + builder_obj.enforcement_mode = "warn" + builder_obj.allow_illegal = False + builder_obj.fuzzy_matching = True + + # Process includes/excludes to trigger logging + _ = builder_obj._process_includes_excludes() + + # Get the log output + log_output = log_capture.getvalue() + + print("\n๐Ÿ“Š Captured Log Events:") + for line in log_output.split('\n'): + if line.strip(): + print(f" {line}") + + # Check for expected structured events + expected_events = [ + "INCLUDE_EXCLUDE_PERFORMANCE:", + ] + + found_events = [] + for event in expected_events: + if event in log_output: + found_events.append(event) + print(f"โœ… Found event: {event}") + else: + print(f"โŒ Missing event: {event}") + + print(f"\n๐Ÿ“‹ Results: {len(found_events)}/{len(expected_events)} expected events found") + + # Test strict mode logging + print("\n๐Ÿ”’ Testing strict mode logging...") + builder_obj.enforcement_mode = "strict" + try: + builder_obj._enforce_includes_strict() + print("โœ… Strict mode passed (no missing includes)") + except RuntimeError as e: + print(f"โŒ Strict mode failed: {e}") + + return len(found_events) == len(expected_events) + + except Exception as e: + print(f"โŒ Test failed with error: {e}") + import traceback + traceback.print_exc() + return False + finally: + logger.removeHandler(handler) + + +def test_m5_performance_metrics(): + """Test performance metrics are within acceptable ranges.""" + import time + + print("\nโฑ๏ธ Testing M5 Performance Metrics...") + + # Test exclude filtering performance + start_time = time.perf_counter() + + # Simulate exclude filtering on reasonable dataset + test_excludes = ["Mana Crypt", "Force of Will", "Mana Drain", "Timetwister", "Ancestral Recall"] + test_pool_size = 1000 # Smaller for testing + + # Simple set lookup simulation (the optimization we want) + exclude_set = set(test_excludes) + filtered_count = 0 + for i in range(test_pool_size): + card_name = f"Card_{i}" + if card_name not in exclude_set: + filtered_count += 1 + + duration_ms = (time.perf_counter() - start_time) * 1000 + + print(f" Exclude filtering: {duration_ms:.2f}ms for {len(test_excludes)} patterns on {test_pool_size} cards") + print(f" Filtered: {test_pool_size - filtered_count} cards") + + # Performance should be very fast with set lookups + performance_acceptable = duration_ms < 10.0 # Very generous threshold for small test + + if performance_acceptable: + print("โœ… Performance metrics acceptable") + else: + print("โŒ Performance metrics too slow") + + return performance_acceptable + + +if __name__ == "__main__": + print("๐Ÿงช Testing M5 - Quality & Observability") + print("=" * 50) + + test1_pass = test_m5_structured_logging() + test2_pass = test_m5_performance_metrics() + + print("\n๐Ÿ“‹ M5 Test Summary:") + print(f" Structured logging: {'โœ… PASS' if test1_pass else 'โŒ FAIL'}") + print(f" Performance metrics: {'โœ… PASS' if test2_pass else 'โŒ FAIL'}") + + if test1_pass and test2_pass: + print("\n๐ŸŽ‰ M5 Quality & Observability tests passed!") + print("๐Ÿ“ˆ Structured events implemented for include/exclude decisions") + print("โšก Performance optimization confirmed with set-based lookups") + else: + print("\n๐Ÿ”ง Some M5 tests failed - check implementation") + + exit(0 if test1_pass and test2_pass else 1) diff --git a/test_validation_endpoint.py b/code/tests/test_validation_endpoint.py similarity index 100% rename from test_validation_endpoint.py rename to code/tests/test_validation_endpoint.py diff --git a/test_web_exclude_flow.py b/code/tests/test_web_exclude_flow.py similarity index 100% rename from test_web_exclude_flow.py rename to code/tests/test_web_exclude_flow.py diff --git a/test_web_form.py b/code/tests/test_web_form.py similarity index 100% rename from test_web_form.py rename to code/tests/test_web_form.py diff --git a/code/web/routes/build.py b/code/web/routes/build.py index 853db9f..24cfecb 100644 --- a/code/web/routes/build.py +++ b/code/web/routes/build.py @@ -2786,85 +2786,26 @@ async def validate_include_exclude_cards( elif len(exclude_unique) > MAX_EXCLUDES * 0.8: # 80% capacity warning result["excludes"]["warnings"].append(f"Approaching limit: {len(exclude_unique)}/{MAX_EXCLUDES}") - # Do fuzzy matching regardless of commander (for basic card validation) - if fuzzy_matching and (include_unique or exclude_unique): - print(f"DEBUG: Attempting fuzzy matching with {len(include_unique)} includes, {len(exclude_unique)} excludes") - try: - # Get card names directly from CSV without requiring commander setup - import pandas as pd - cards_df = pd.read_csv('csv_files/cards.csv') - print(f"DEBUG: CSV columns: {list(cards_df.columns)}") - - # Try to find the name column - name_column = None - for col in ['Name', 'name', 'card_name', 'CardName']: - if col in cards_df.columns: - name_column = col - break - - if name_column is None: - raise ValueError(f"Could not find name column. Available columns: {list(cards_df.columns)}") - - available_cards = set(cards_df[name_column].tolist()) - print(f"DEBUG: Loaded {len(available_cards)} available cards") - - # Validate includes with fuzzy matching - for card_name in include_unique: - print(f"DEBUG: Testing include card: {card_name}") - match_result = fuzzy_match_card_name(card_name, available_cards) - print(f"DEBUG: Match result - name: {match_result.matched_name}, auto_accepted: {match_result.auto_accepted}, confidence: {match_result.confidence}") - - if match_result.matched_name and match_result.auto_accepted: - # Exact or high-confidence match - result["includes"]["fuzzy_matches"][card_name] = match_result.matched_name - result["includes"]["legal"].append(match_result.matched_name) - elif not match_result.auto_accepted and match_result.suggestions: - # Needs confirmation - has suggestions but low confidence - print(f"DEBUG: Adding confirmation for {card_name}") - result["confirmation_needed"].append({ - "input": card_name, - "suggestions": match_result.suggestions, - "confidence": match_result.confidence, - "type": "include" - }) - else: - # No match found at all, add to illegal - result["includes"]["illegal"].append(card_name) - - # Validate excludes with fuzzy matching - for card_name in exclude_unique: - match_result = fuzzy_match_card_name(card_name, available_cards) - if match_result.matched_name: - if match_result.auto_accepted: - result["excludes"]["fuzzy_matches"][card_name] = match_result.matched_name - result["excludes"]["legal"].append(match_result.matched_name) - else: - # Needs confirmation - result["confirmation_needed"].append({ - "input": card_name, - "suggestions": match_result.suggestions, - "confidence": match_result.confidence, - "type": "exclude" - }) - else: - # No match found, add to illegal - result["excludes"]["illegal"].append(card_name) - - except Exception as fuzzy_error: - print(f"DEBUG: Fuzzy matching error: {str(fuzzy_error)}") - import traceback - traceback.print_exc() - result["overall_warnings"].append(f"Fuzzy matching unavailable: {str(fuzzy_error)}") - # If we have a commander, do advanced validation (color identity, etc.) if commander and commander.strip(): try: - # Create a temporary builder to get available card names + # Create a temporary builder builder = DeckBuilder() + + # Set up commander FIRST (before setup_dataframes) + df = builder.load_commander_data() + commander_rows = df[df["name"] == commander.strip()] + + if not commander_rows.empty: + # Apply commander selection (this sets commander_row properly) + builder._apply_commander_selection(commander_rows.iloc[0]) + + # Now setup dataframes (this will use the commander info) builder.setup_dataframes() # Get available card names for fuzzy matching - available_cards = set(builder._full_cards_df['Name'].tolist()) + name_col = 'name' if 'name' in builder._full_cards_df.columns else 'Name' + available_cards = set(builder._full_cards_df[name_col].tolist()) # Validate includes with fuzzy matching for card_name in include_unique: @@ -2915,10 +2856,85 @@ async def validate_include_exclude_cards( result["excludes"]["legal"].append(card_name) else: result["excludes"]["illegal"].append(card_name) + + # Color identity validation for includes (only if we have a valid commander with colors) + commander_colors = getattr(builder, 'color_identity', []) + if commander_colors: + color_validated_includes = [] + for card_name in result["includes"]["legal"]: + if builder._validate_card_color_identity(card_name): + color_validated_includes.append(card_name) + else: + # Add color-mismatched cards to illegal instead of separate category + result["includes"]["illegal"].append(card_name) + + # Update legal includes to only those that pass color identity + result["includes"]["legal"] = color_validated_includes except Exception as validation_error: # Advanced validation failed, but return basic validation result["overall_warnings"].append(f"Advanced validation unavailable: {str(validation_error)}") + else: + # No commander provided, do basic fuzzy matching only + if fuzzy_matching and (include_unique or exclude_unique): + try: + # Get card names directly from CSV without requiring commander setup + import pandas as pd + cards_df = pd.read_csv('csv_files/cards.csv') + + # Try to find the name column + name_column = None + for col in ['Name', 'name', 'card_name', 'CardName']: + if col in cards_df.columns: + name_column = col + break + + if name_column is None: + raise ValueError(f"Could not find name column. Available columns: {list(cards_df.columns)}") + + available_cards = set(cards_df[name_column].tolist()) + + # Validate includes with fuzzy matching + for card_name in include_unique: + match_result = fuzzy_match_card_name(card_name, available_cards) + + if match_result.matched_name and match_result.auto_accepted: + # Exact or high-confidence match + result["includes"]["fuzzy_matches"][card_name] = match_result.matched_name + result["includes"]["legal"].append(match_result.matched_name) + elif not match_result.auto_accepted and match_result.suggestions: + # Needs confirmation - has suggestions but low confidence + result["confirmation_needed"].append({ + "input": card_name, + "suggestions": match_result.suggestions, + "confidence": match_result.confidence, + "type": "include" + }) + else: + # No match found at all, add to illegal + result["includes"]["illegal"].append(card_name) + + # Validate excludes with fuzzy matching + for card_name in exclude_unique: + match_result = fuzzy_match_card_name(card_name, available_cards) + if match_result.matched_name: + if match_result.auto_accepted: + result["excludes"]["fuzzy_matches"][card_name] = match_result.matched_name + result["excludes"]["legal"].append(match_result.matched_name) + else: + # Needs confirmation + result["confirmation_needed"].append({ + "input": card_name, + "suggestions": match_result.suggestions, + "confidence": match_result.confidence, + "type": "exclude" + }) + else: + # No match found, add to illegal + result["excludes"]["illegal"].append(card_name) + + except Exception as fuzzy_error: + result["overall_warnings"].append(f"Fuzzy matching unavailable: {str(fuzzy_error)}") return JSONResponse(result) diff --git a/code/web/templates/build/_new_deck_modal.html b/code/web/templates/build/_new_deck_modal.html index fe2ed0d..a7603f9 100644 --- a/code/web/templates/build/_new_deck_modal.html +++ b/code/web/templates/build/_new_deck_modal.html @@ -506,14 +506,9 @@ badges += `โœ“ ${includeData.legal.length} legal`; } - // Invalid cards badge + // Invalid cards badge (includes color mismatches and not found cards) if (includeData.illegal && includeData.illegal.length > 0) { - badges += `โœ— ${includeData.illegal.length} invalid`; - } - - // Color mismatch badge - if (includeData.color_mismatched && includeData.color_mismatched.length > 0) { - badges += `โš  ${includeData.color_mismatched.length} off-color`; + badges += `โœ— ${includeData.illegal.length} illegal`; } // Duplicates badge @@ -523,6 +518,62 @@ } badgeContainer.innerHTML = badges; + + // Update chip colors based on validation status + updateChipColors('include', includeData); + } + + // Update chip colors based on validation status + function updateChipColors(type, validationData) { + if (!validationData) return; + + const container = document.getElementById(`${type}_chips`); + if (!container) return; + + const chips = container.querySelectorAll('.card-chip'); + chips.forEach(chip => { + const cardName = chip.getAttribute('data-card-name'); + if (!cardName) return; + + // Determine status + let isLegal = false; + let isIllegal = false; + + if (validationData.legal && validationData.legal.includes(cardName)) { + isLegal = true; + } + if (validationData.illegal && validationData.illegal.includes(cardName)) { + isIllegal = true; + } + + // Apply styling based on status (prioritize illegal over legal) + if (isIllegal) { + // Red styling for illegal cards + chip.style.background = '#fee2e2'; + chip.style.border = '1px solid #fecaca'; + chip.style.color = '#dc2626'; + + // Update remove button color too + const removeBtn = chip.querySelector('button'); + if (removeBtn) { + removeBtn.style.color = '#dc2626'; + removeBtn.onmouseover = () => removeBtn.style.background = '#fee2e2'; + } + } else if (isLegal) { + // Green styling for legal cards + chip.style.background = '#dcfce7'; + chip.style.border = '1px solid #bbf7d0'; + chip.style.color = '#166534'; + + // Update remove button color too + const removeBtn = chip.querySelector('button'); + if (removeBtn) { + removeBtn.style.color = '#166534'; + removeBtn.onmouseover = () => removeBtn.style.background = '#bbf7d0'; + } + } + // If no status info, keep default styling + }); } // Update exclude validation badges @@ -554,6 +605,9 @@ } badgeContainer.innerHTML = badges; + + // Update chip colors based on validation status + updateChipColors('exclude', excludeData); } // Comprehensive validation for both include and exclude cards diff --git a/test_api_response.py b/test_api_response.py new file mode 100644 index 0000000..d15c942 --- /dev/null +++ b/test_api_response.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 +"""Test the validation API response to debug badge counting issue.""" + +import requests +import json + +# Test data: Mix of legal and illegal cards for R/U commander +test_data = { + 'include_cards': '''Lightning Bolt +Counterspell +Teferi's Protection''', + 'exclude_cards': '', + 'commander': 'Niv-Mizzet, Parun', # R/U commander + 'enforcement_mode': 'warn', + 'allow_illegal': False, + 'fuzzy_matching': True +} + +try: + response = requests.post('http://localhost:8080/build/validate/include_exclude', data=test_data) + print(f"Status Code: {response.status_code}") + + if response.status_code == 200: + data = response.json() + print("\nFull API Response:") + print(json.dumps(data, indent=2)) + + includes = data.get('includes', {}) + print(f"\nIncludes Summary:") + print(f" Total count: {includes.get('count', 0)}") + print(f" Legal: {len(includes.get('legal', []))} cards - {includes.get('legal', [])}") + print(f" Illegal: {len(includes.get('illegal', []))} cards - {includes.get('illegal', [])}") + print(f" Color mismatched: {len(includes.get('color_mismatched', []))} cards - {includes.get('color_mismatched', [])}") + + # Check for double counting + legal_set = set(includes.get('legal', [])) + illegal_set = set(includes.get('illegal', [])) + color_mismatch_set = set(includes.get('color_mismatched', [])) + + overlap_legal_illegal = legal_set & illegal_set + overlap_legal_color = legal_set & color_mismatch_set + overlap_illegal_color = illegal_set & color_mismatch_set + + print(f"\nOverlap Analysis:") + print(f" Legal โˆฉ Illegal: {overlap_legal_illegal}") + print(f" Legal โˆฉ Color Mismatch: {overlap_legal_color}") + print(f" Illegal โˆฉ Color Mismatch: {overlap_illegal_color}") + + # Total unique cards + all_cards = legal_set | illegal_set | color_mismatch_set + print(f" Total unique cards across all categories: {len(all_cards)}") + print(f" Expected total: {includes.get('count', 0)}") + + else: + print(f"Error: {response.text}") + +except Exception as e: + print(f"Error making request: {e}") diff --git a/tests/e2e/README.md b/tests/e2e/README.md index 7a1f0c2..30b0ad3 100644 --- a/tests/e2e/README.md +++ b/tests/e2e/README.md @@ -18,7 +18,7 @@ python tests/e2e/run_e2e_tests.py --install-browsers ### Quick Smoke Test (Recommended) ```bash -# Assumes server is already running on localhost:8000 +# Assumes server is already running on localhost:8080 python tests/e2e/run_e2e_tests.py --quick ``` @@ -47,7 +47,7 @@ pytest test_web_smoke.py -v ## Environment Variables -- `TEST_BASE_URL`: Base URL for testing (default: http://localhost:8000) +- `TEST_BASE_URL`: Base URL for testing (default: http://localhost:8080) ## Test Coverage diff --git a/tests/e2e/run_e2e_tests.py b/tests/e2e/run_e2e_tests.py index 5747751..2ee9883 100644 --- a/tests/e2e/run_e2e_tests.py +++ b/tests/e2e/run_e2e_tests.py @@ -24,7 +24,7 @@ class E2ETestRunner: def __init__(self): self.project_root = Path(__file__).parent.parent self.server_process = None - self.base_url = os.getenv('TEST_BASE_URL', 'http://localhost:8000') + self.base_url = os.getenv('TEST_BASE_URL', 'http://localhost:8080') def start_dev_server(self): """Start the development server""" @@ -36,7 +36,7 @@ class E2ETestRunner: "-m", "uvicorn", "code.web.app:app", "--host", "0.0.0.0", - "--port", "8000", + "--port", "8080", "--reload" ] diff --git a/tests/e2e/test_web_smoke.py b/tests/e2e/test_web_smoke.py index a778e05..b07dc52 100644 --- a/tests/e2e/test_web_smoke.py +++ b/tests/e2e/test_web_smoke.py @@ -9,7 +9,7 @@ import os class TestConfig: """Test configuration""" - BASE_URL = os.getenv('TEST_BASE_URL', 'http://localhost:8000') + BASE_URL = os.getenv('TEST_BASE_URL', 'http://localhost:8080') TIMEOUT = 30000 # 30 seconds # Test data