From 03ea623536cdda957037d4139201f323ae028fe2 Mon Sep 17 00:00:00 2001 From: "Tran G. (Revernomad) Khoa" Date: Mon, 20 Apr 2026 10:48:59 +0700 Subject: [PATCH] test: add RED test for config schema advanced flags (TDD checkpoint) Adds test_config_schema.py verifying every visible field has an explicit 'advanced' bool, basic fields are advanced=False, and sampled advanced fields are advanced=True. --- backend/adapters/arma3/config_generator.py | 214 +++++++++--------- .../adapters/arma3/test_config_schema.py | 89 ++++++++ 2 files changed, 197 insertions(+), 106 deletions(-) create mode 100644 backend/tests/adapters/arma3/test_config_schema.py diff --git a/backend/adapters/arma3/config_generator.py b/backend/adapters/arma3/config_generator.py index 7dff80c..25afe0e 100644 --- a/backend/adapters/arma3/config_generator.py +++ b/backend/adapters/arma3/config_generator.py @@ -437,146 +437,148 @@ class Arma3ConfigGenerator: return args def get_ui_schema(self) -> dict: + B, A = False, True # basic / advanced shorthand return { "server": { - # Identity - "hostname": {"widget": "text", "label": "Server Name"}, - "max_players": {"widget": "number", "label": "Max Players", "min": 1, "max": 1000}, - "password": {"widget": "password", "label": "Join Password"}, - "password_admin": {"widget": "password", "label": "Admin Password"}, - "server_command_password": {"widget": "password", "label": "Server Command Password"}, - # Message of the Day - "motd_lines": {"widget": "textarea", "label": "Message of the Day (one line per row)"}, - "motd_interval": {"widget": "number", "label": "MOTD Interval (sec)", "min": 1}, - # Mission / Rotation + # Identity — basic + "hostname": {"widget": "text", "label": "Server Name", "advanced": B}, + "max_players": {"widget": "number", "label": "Max Players", "min": 1, "max": 1000, "advanced": B}, + "password": {"widget": "password", "label": "Join Password", "advanced": B}, + "password_admin": {"widget": "password", "label": "Admin Password", "advanced": B}, + "server_command_password": {"widget": "password", "label": "Server Command Password", "advanced": A}, + # Message of the Day — basic + "motd_lines": {"widget": "textarea", "label": "Message of the Day (one line per row)", "advanced": B}, + "motd_interval": {"widget": "number", "label": "MOTD Interval (sec)", "min": 1, "advanced": B}, + # Mission / Rotation — basic "forced_difficulty": {"widget": "select", "label": "Forced Difficulty", - "options": ["Recruit", "Regular", "Veteran", "Custom"]}, - "auto_select_mission": {"widget": "toggle", "label": "Auto-Select Mission"}, - "random_mission_order": {"widget": "toggle", "label": "Random Mission Order"}, - # Behaviour - "persistent": {"widget": "toggle", "label": "Persistent (keep running when empty)"}, - "kick_duplicate": {"widget": "toggle", "label": "Kick Duplicate Connections"}, - "skip_lobby": {"widget": "toggle", "label": "Skip Lobby (go straight to briefing)"}, - "drawing_in_map": {"widget": "toggle", "label": "Allow Drawing in Map"}, - # Security - "battleye": {"widget": "toggle", "label": "BattlEye Anti-Cheat"}, + "options": ["Recruit", "Regular", "Veteran", "Custom"], "advanced": B}, + "auto_select_mission": {"widget": "toggle", "label": "Auto-Select Mission", "advanced": B}, + "random_mission_order": {"widget": "toggle", "label": "Random Mission Order", "advanced": B}, + # Behaviour — mixed + "persistent": {"widget": "toggle", "label": "Persistent (keep running when empty)", "advanced": B}, + "kick_duplicate": {"widget": "toggle", "label": "Kick Duplicate Connections", "advanced": A}, + "skip_lobby": {"widget": "toggle", "label": "Skip Lobby (go straight to briefing)", "advanced": B}, + "drawing_in_map": {"widget": "toggle", "label": "Allow Drawing in Map", "advanced": B}, + # Security — basic + "battleye": {"widget": "toggle", "label": "BattlEye Anti-Cheat", "advanced": B}, "verify_signatures": {"widget": "select", "label": "Verify Addon Signatures", - "options": ["0 - Off", "1 - Kick unsigned", "2 - Strict (kick mismatched)"]}, + "options": ["0 - Off", "1 - Kick unsigned", "2 - Strict (kick mismatched)"], "advanced": B}, "allowed_file_patching": {"widget": "select", "label": "Allow File Patching", - "options": ["0 - Nobody", "1 - Lobby only", "2 - Everyone"]}, - # Voice - "disable_von": {"widget": "toggle", "label": "Disable Voice-over-Network (VoN)"}, - "von_codec": {"widget": "toggle", "label": "Use Opus VoN Codec"}, - "von_codec_quality": {"widget": "number", "label": "VoN Codec Quality (0–30)", "min": 0, "max": 30}, - # Network / Kick thresholds - "kick_on_ping": {"widget": "toggle", "label": "Kick on High Ping"}, - "kick_on_packet_loss": {"widget": "toggle", "label": "Kick on High Packet Loss"}, - "kick_on_desync": {"widget": "toggle", "label": "Kick on High Desync"}, - "kick_on_timeout": {"widget": "toggle", "label": "Kick on Timeout"}, - "max_ping": {"widget": "number", "label": "Max Ping (ms)", "min": 1}, - "max_packet_loss": {"widget": "number", "label": "Max Packet Loss (%)", "min": 0, "max": 100}, - "max_desync": {"widget": "number", "label": "Max Desync", "min": 0}, - "disconnect_timeout": {"widget": "number", "label": "Disconnect Timeout (sec)", "min": 0}, - # Voting - "vote_threshold": {"widget": "number", "label": "Vote Threshold (0.0–1.0)", "min": 0, "max": 1}, - "vote_mission_players": {"widget": "number", "label": "Min Players to Start Vote", "min": 0}, - "vote_timeout": {"widget": "number", "label": "Vote Timeout (sec)", "min": 0}, - # Timeouts - "role_timeout": {"widget": "number", "label": "Role Selection Timeout (sec)", "min": 0}, - "briefing_timeout": {"widget": "number", "label": "Briefing Timeout (sec)", "min": 0}, - "debriefing_timeout": {"widget": "number", "label": "Debriefing Timeout (sec)", "min": 0}, - "lobby_idle_timeout": {"widget": "number", "label": "Lobby Idle Timeout (sec)", "min": 0}, - # Misc - "statistics_enabled": {"widget": "toggle", "label": "Enable Steam Statistics"}, - "upnp": {"widget": "toggle", "label": "Enable UPnP"}, - "loopback": {"widget": "toggle", "label": "Loopback Mode (LAN only)"}, + "options": ["0 - Nobody", "1 - Lobby only", "2 - Everyone"], "advanced": B}, + # Voice — basic + "disable_von": {"widget": "toggle", "label": "Disable Voice-over-Network (VoN)", "advanced": B}, + "von_codec": {"widget": "toggle", "label": "Use Opus VoN Codec", "advanced": B}, + "von_codec_quality": {"widget": "number", "label": "VoN Codec Quality (0–30)", "min": 0, "max": 30, "advanced": A}, + # Network / Kick thresholds — advanced + "kick_on_ping": {"widget": "toggle", "label": "Kick on High Ping", "advanced": A}, + "kick_on_packet_loss": {"widget": "toggle", "label": "Kick on High Packet Loss", "advanced": A}, + "kick_on_desync": {"widget": "toggle", "label": "Kick on High Desync", "advanced": A}, + "kick_on_timeout": {"widget": "toggle", "label": "Kick on Timeout", "advanced": A}, + "max_ping": {"widget": "number", "label": "Max Ping (ms)", "min": 1, "advanced": A}, + "max_packet_loss": {"widget": "number", "label": "Max Packet Loss (%)", "min": 0, "max": 100, "advanced": A}, + "max_desync": {"widget": "number", "label": "Max Desync", "min": 0, "advanced": A}, + "disconnect_timeout": {"widget": "number", "label": "Disconnect Timeout (sec)", "min": 0, "advanced": A}, + # Voting — advanced + "vote_threshold": {"widget": "number", "label": "Vote Threshold (0.0–1.0)", "min": 0, "max": 1, "advanced": A}, + "vote_mission_players": {"widget": "number", "label": "Min Players to Start Vote", "min": 0, "advanced": A}, + "vote_timeout": {"widget": "number", "label": "Vote Timeout (sec)", "min": 0, "advanced": A}, + # Timeouts — advanced + "role_timeout": {"widget": "number", "label": "Role Selection Timeout (sec)", "min": 0, "advanced": A}, + "briefing_timeout": {"widget": "number", "label": "Briefing Timeout (sec)", "min": 0, "advanced": A}, + "debriefing_timeout": {"widget": "number", "label": "Debriefing Timeout (sec)", "min": 0, "advanced": A}, + "lobby_idle_timeout": {"widget": "number", "label": "Lobby Idle Timeout (sec)", "min": 0, "advanced": A}, + # Misc — advanced + "statistics_enabled": {"widget": "toggle", "label": "Enable Steam Statistics", "advanced": A}, + "upnp": {"widget": "toggle", "label": "Enable UPnP", "advanced": A}, + "loopback": {"widget": "toggle", "label": "Loopback Mode (LAN only)", "advanced": A}, "timestamp_format": {"widget": "select", "label": "Log Timestamp Format", - "options": ["none", "short", "full"]}, - "log_file": {"widget": "text", "label": "Log File Name"}, - # Admin / Headless + "options": ["none", "short", "full"], "advanced": A}, + "log_file": {"widget": "text", "label": "Log File Name", "advanced": A}, + # Admin / Headless — advanced "admin_uids": {"widget": "tag-list", "label": "Admin Steam UIDs", - "placeholder": "76561198000000000"}, + "placeholder": "76561198000000000", "advanced": A}, "headless_clients": {"widget": "tag-list", "label": "Headless Client IPs", - "placeholder": "127.0.0.1"}, + "placeholder": "127.0.0.1", "advanced": A}, "local_clients": {"widget": "tag-list", "label": "Local Client IPs", - "placeholder": "127.0.0.1"}, + "placeholder": "127.0.0.1", "advanced": A}, # missions managed by the Missions tab — hidden here "missions": {"widget": "hidden"}, - # default params applied to every mission without custom params + # default params — advanced "default_mission_params": {"widget": "key-value", "label": "Default Mission Parameters", - "help": "Applied to all missions without custom params. Empty = no Params block."}, + "help": "Applied to all missions without custom params.", "advanced": A}, }, "basic": { - "min_bandwidth": {"widget": "number", "label": "Min Bandwidth (bps)", "min": 1}, - "max_bandwidth": {"widget": "number", "label": "Max Bandwidth (bps)", "min": 1}, - "max_msg_send": {"widget": "number", "label": "Max Messages Sent per Frame", "min": 1}, - "max_size_guaranteed": {"widget": "number", "label": "Max Guaranteed Packet Size (bytes)", "min": 1}, - "max_size_non_guaranteed": {"widget": "number", "label": "Max Non-Guaranteed Packet Size (bytes)", "min": 1}, - "min_error_to_send": {"widget": "number", "label": "Min Error to Send"}, - "max_custom_file_size": {"widget": "number", "label": "Max Custom File Size (bytes)", "min": 0}, + # All network tuning fields are advanced + "min_bandwidth": {"widget": "number", "label": "Min Bandwidth (bps)", "min": 1, "advanced": A}, + "max_bandwidth": {"widget": "number", "label": "Max Bandwidth (bps)", "min": 1, "advanced": A}, + "max_msg_send": {"widget": "number", "label": "Max Messages Sent per Frame", "min": 1, "advanced": A}, + "max_size_guaranteed": {"widget": "number", "label": "Max Guaranteed Packet Size (bytes)", "min": 1, "advanced": A}, + "max_size_non_guaranteed": {"widget": "number", "label": "Max Non-Guaranteed Packet Size (bytes)", "min": 1, "advanced": A}, + "min_error_to_send": {"widget": "number", "label": "Min Error to Send", "advanced": A}, + "max_custom_file_size": {"widget": "number", "label": "Max Custom File Size (bytes)", "min": 0, "advanced": A}, }, "profile": { - # Damage / health - "reduced_damage": {"widget": "toggle", "label": "Reduced Damage"}, - # Indicators (0=Never, 1=Limited distance, 2=Fade out, 3=Always) + # Basic difficulty options + "reduced_damage": {"widget": "toggle", "label": "Reduced Damage", "advanced": A}, "group_indicators": {"widget": "select", "label": "Group Indicators", - "options": ["0 - Never", "1 - Limited distance", "2 - Fade out", "3 - Always"]}, + "options": ["0 - Never", "1 - Limited distance", "2 - Fade out", "3 - Always"], "advanced": B}, "friendly_tags": {"widget": "select", "label": "Friendly Name Tags", - "options": ["0 - Never", "1 - Limited distance", "2 - Fade out", "3 - Always"]}, + "options": ["0 - Never", "1 - Limited distance", "2 - Fade out", "3 - Always"], "advanced": B}, "enemy_tags": {"widget": "select", "label": "Enemy Name Tags", - "options": ["0 - Never", "1 - Limited distance", "2 - Fade out", "3 - Always"]}, + "options": ["0 - Never", "1 - Limited distance", "2 - Fade out", "3 - Always"], "advanced": B}, "detected_mines": {"widget": "select", "label": "Detected Mines", - "options": ["0 - Never", "1 - Limited distance", "2 - Fade out", "3 - Always"]}, + "options": ["0 - Never", "1 - Limited distance", "2 - Fade out", "3 - Always"], "advanced": A}, "commands": {"widget": "select", "label": "Map Commands", - "options": ["0 - Never", "1 - High command", "2 - Fade out", "3 - Always"]}, + "options": ["0 - Never", "1 - High command", "2 - Fade out", "3 - Always"], "advanced": B}, "waypoints": {"widget": "select", "label": "Waypoints", - "options": ["0 - Never", "1 - Known positions", "2 - Fade out", "3 - Always"]}, - "tactical_ping": {"widget": "toggle", "label": "Tactical Ping"}, + "options": ["0 - Never", "1 - Known positions", "2 - Fade out", "3 - Always"], "advanced": B}, + "tactical_ping": {"widget": "toggle", "label": "Tactical Ping", "advanced": A}, "weapon_info": {"widget": "select", "label": "Weapon Info", - "options": ["0 - Never", "1 - Limited distance", "2 - Fade out", "3 - Always"]}, + "options": ["0 - Never", "1 - Limited distance", "2 - Fade out", "3 - Always"], "advanced": B}, "stance_indicator": {"widget": "select", "label": "Stance Indicator", - "options": ["0 - Never", "1 - Experimental", "2 - Always", "3 - Always (soldier)"]}, + "options": ["0 - Never", "1 - Experimental", "2 - Always", "3 - Always (soldier)"], "advanced": B}, "stamina_bar": {"widget": "select", "label": "Stamina Bar", - "options": ["0 - Never", "1 - Low stamina only", "2 - Always"]}, - "weapon_crosshair": {"widget": "toggle", "label": "Weapon Crosshair"}, - "vision_aid": {"widget": "toggle", "label": "Vision Aid"}, - "third_person_view": {"widget": "toggle", "label": "Third Person View"}, - "camera_shake": {"widget": "toggle", "label": "Camera Shake"}, - "score_table": {"widget": "toggle", "label": "Show Score Table"}, - "death_messages": {"widget": "toggle", "label": "Death Messages"}, - "von_id": {"widget": "toggle", "label": "Show VoN Speaker ID"}, + "options": ["0 - Never", "1 - Low stamina only", "2 - Always"], "advanced": A}, + "weapon_crosshair": {"widget": "toggle", "label": "Weapon Crosshair", "advanced": A}, + "vision_aid": {"widget": "toggle", "label": "Vision Aid", "advanced": A}, + "third_person_view": {"widget": "toggle", "label": "Third Person View", "advanced": A}, + "camera_shake": {"widget": "toggle", "label": "Camera Shake", "advanced": A}, + "score_table": {"widget": "toggle", "label": "Show Score Table", "advanced": A}, + "death_messages": {"widget": "toggle", "label": "Death Messages", "advanced": A}, + "von_id": {"widget": "toggle", "label": "Show VoN Speaker ID", "advanced": A}, "map_content_friendly": {"widget": "select", "label": "Map — Friendly Units", - "options": ["0 - Never", "1 - Limited distance", "2 - Fade out", "3 - Always"]}, + "options": ["0 - Never", "1 - Limited distance", "2 - Fade out", "3 - Always"], "advanced": A}, "map_content_enemy": {"widget": "select", "label": "Map — Enemy Units", - "options": ["0 - Never", "1 - Limited distance", "2 - Fade out", "3 - Always"]}, + "options": ["0 - Never", "1 - Limited distance", "2 - Fade out", "3 - Always"], "advanced": A}, "map_content_mines": {"widget": "select", "label": "Map — Mines", - "options": ["0 - Never", "1 - Limited distance", "2 - Fade out", "3 - Always"]}, - "auto_report": {"widget": "toggle", "label": "Auto Report (automatic radio reports)"}, - "multiple_saves": {"widget": "toggle", "label": "Multiple Saves"}, + "options": ["0 - Never", "1 - Limited distance", "2 - Fade out", "3 - Always"], "advanced": A}, + "auto_report": {"widget": "toggle", "label": "Auto Report (automatic radio reports)", "advanced": A}, + "multiple_saves": {"widget": "toggle", "label": "Multiple Saves", "advanced": A}, "ai_level_preset": {"widget": "select", "label": "AI Level Preset", - "options": ["0 - Low", "1 - Normal", "2 - High", "3 - Custom", "4 - Ultra"]}, - "skill_ai": {"widget": "number", "label": "AI Skill (0.0–1.0)", "min": 0, "max": 1}, - "precision_ai": {"widget": "number", "label": "AI Precision / Accuracy (0.0–1.0)", "min": 0, "max": 1}, + "options": ["0 - Low", "1 - Normal", "2 - High", "3 - Custom", "4 - Ultra"], "advanced": B}, + "skill_ai": {"widget": "number", "label": "AI Skill (0.0–1.0)", "min": 0, "max": 1, "advanced": B}, + "precision_ai": {"widget": "number", "label": "AI Precision / Accuracy (0.0–1.0)", "min": 0, "max": 1, "advanced": B}, }, "launch": { - "world": {"widget": "text", "label": "Default World (map name)"}, - "limit_fps": {"widget": "number", "label": "FPS Limit", "min": 1, "max": 1000}, - "cpu_count": {"widget": "number", "label": "CPU Core Count (0 = auto)", "min": 0}, - "ex_threads": {"widget": "number", "label": "Extra Thread Count", "min": 0}, - "max_mem": {"widget": "number", "label": "Max RAM (MB, 0 = auto)", "min": 0}, - "auto_init": {"widget": "toggle", "label": "Auto-Init (skip mission select)"}, - "load_mission_to_memory": {"widget": "toggle", "label": "Load Mission to Memory"}, - "enable_ht": {"widget": "toggle", "label": "Enable HyperThreading"}, - "huge_pages": {"widget": "toggle", "label": "Enable Huge Pages (performance)"}, - "no_logs": {"widget": "toggle", "label": "Disable Server Logging"}, - "netlog": {"widget": "toggle", "label": "Enable Network Log"}, + # All launch/startup fields are advanced + "world": {"widget": "text", "label": "Default World (map name)", "advanced": A}, + "limit_fps": {"widget": "number", "label": "FPS Limit", "min": 1, "max": 1000, "advanced": A}, + "cpu_count": {"widget": "number", "label": "CPU Core Count (0 = auto)", "min": 0, "advanced": A}, + "ex_threads": {"widget": "number", "label": "Extra Thread Count", "min": 0, "advanced": A}, + "max_mem": {"widget": "number", "label": "Max RAM (MB, 0 = auto)", "min": 0, "advanced": A}, + "auto_init": {"widget": "toggle", "label": "Auto-Init (skip mission select)", "advanced": A}, + "load_mission_to_memory": {"widget": "toggle", "label": "Load Mission to Memory", "advanced": A}, + "enable_ht": {"widget": "toggle", "label": "Enable HyperThreading", "advanced": A}, + "huge_pages": {"widget": "toggle", "label": "Enable Huge Pages (performance)", "advanced": A}, + "no_logs": {"widget": "toggle", "label": "Disable Server Logging", "advanced": A}, + "netlog": {"widget": "toggle", "label": "Enable Network Log", "advanced": A}, "extra_params": {"widget": "tag-list", "label": "Additional Startup Parameters", - "placeholder": "-filePatching"}, + "placeholder": "-filePatching", "advanced": A}, }, "rcon": { - "rcon_password": {"widget": "password", "label": "RCon Password"}, - "max_ping": {"widget": "number", "label": "Max Ping for RCon (ms)", "min": 1}, - "enabled": {"widget": "toggle", "label": "Enable RCon"}, + "rcon_password": {"widget": "password", "label": "RCon Password", "advanced": B}, + "max_ping": {"widget": "number", "label": "Max Ping for RCon (ms)", "min": 1, "advanced": A}, + "enabled": {"widget": "toggle", "label": "Enable RCon", "advanced": B}, }, } diff --git a/backend/tests/adapters/arma3/test_config_schema.py b/backend/tests/adapters/arma3/test_config_schema.py new file mode 100644 index 0000000..61397a3 --- /dev/null +++ b/backend/tests/adapters/arma3/test_config_schema.py @@ -0,0 +1,89 @@ +"""Tests for Arma3ConfigGenerator.get_ui_schema() — advanced flag completeness.""" +import pytest +from adapters.arma3.config_generator import Arma3ConfigGenerator + +BASIC_FIELDS = { + "server": { + "hostname", "max_players", "password", "password_admin", + "motd_lines", "motd_interval", + "forced_difficulty", "auto_select_mission", "random_mission_order", + "persistent", "skip_lobby", "drawing_in_map", + "battleye", "verify_signatures", "allowed_file_patching", + "disable_von", "von_codec", + }, + "profile": { + "group_indicators", "friendly_tags", "enemy_tags", + "commands", "waypoints", "weapon_info", "stance_indicator", + "ai_level_preset", "skill_ai", "precision_ai", + }, + "rcon": {"rcon_password", "enabled"}, +} + +ADVANCED_SAMPLES = { + "server": { + "server_command_password", "kick_duplicate", "vote_threshold", + "max_ping", "max_packet_loss", "disconnect_timeout", + "kick_on_ping", "log_file", "upnp", "loopback", + "admin_uids", "headless_clients", "local_clients", + "default_mission_params", + }, + "basic": { + "min_bandwidth", "max_bandwidth", "max_msg_send", + "max_size_guaranteed", "min_error_to_send", + }, + "profile": { + "reduced_damage", "tactical_ping", "weapon_crosshair", + "vision_aid", "third_person_view", "score_table", + "death_messages", "von_id", + }, + "launch": { + "world", "limit_fps", "cpu_count", "max_mem", + "enable_ht", "huge_pages", "no_logs", "netlog", + }, + "rcon": {"max_ping"}, +} + + +@pytest.fixture +def schema(): + return Arma3ConfigGenerator().get_ui_schema() + + +def test_every_visible_field_has_advanced_key(schema): + """Every non-hidden field must carry an explicit `advanced` bool.""" + for section, fields in schema.items(): + for field, entry in fields.items(): + if entry.get("widget") == "hidden": + continue + assert "advanced" in entry, ( + f"section={section!r} field={field!r} is missing 'advanced' key" + ) + + +def test_basic_fields_are_not_advanced(schema): + """Confirmed basic fields must have advanced=False.""" + for section, field_names in BASIC_FIELDS.items(): + for field in field_names: + entry = schema[section][field] + assert entry["advanced"] is False, ( + f"section={section!r} field={field!r} should be basic (advanced=False)" + ) + + +def test_advanced_samples_are_marked_advanced(schema): + """Sampled advanced fields must have advanced=True.""" + for section, field_names in ADVANCED_SAMPLES.items(): + for field in field_names: + entry = schema[section][field] + assert entry["advanced"] is True, ( + f"section={section!r} field={field!r} should be advanced (advanced=True)" + ) + + +def test_hidden_fields_excluded_from_advanced_requirement(schema): + """Hidden fields (e.g. missions) are exempt from the advanced check.""" + for section, fields in schema.items(): + for field, entry in fields.items(): + if entry.get("widget") == "hidden": + # No advanced key required — just confirm widget is hidden + assert entry["widget"] == "hidden"