From 4546ce786ab95a41417b05416b469bdeb460ed76 Mon Sep 17 00:00:00 2001 From: Paul Lizer Date: Wed, 19 Aug 2026 14:16:42 -0400 Subject: [PATCH 1/2] Enforce the Admin Settings form field contract in CI Admin Settings submits a single form and the backend reads every value by field name, so the set of name attributes is the real contract between the template and route_frontend_admin_settings.py. Renaming or dropping one silently stops that setting from saving: no error is raised, the control still renders, and the value simply never arrives. Every stage of the information architecture rework has been verified against a field-name fingerprint, but that check lived in a scratch script and only ran when someone remembered to run it. Stage D moves 96 cards between tabs, which is exactly when an accidental rename is most likely and least visible. The check is now a functional test with the baseline committed alongside it, so it runs in CI on every commit and protects anyone editing these templates later, not just this migration. Adding settings is unaffected: new field names are reported, not failed. Removing or renaming one requires regenerating the baseline in the same commit via --update-baseline, which turns a silent loss into a reviewed decision. The test also rejects duplicate field names outside the five known radio and checkbox groups. That is the property that stops a mirrored control from submitting its value twice, which the Access and Roles roster in stage E depends on. Both assertions were verified to fail correctly: renaming a field is reported by name, and introducing a duplicate is reported with its count. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- application/single_app/config.py | 2 +- docs/explanation/release_notes.md | 10 + .../test_admin_settings_field_contract.py | 169 +++++++ .../admin_settings_field_baseline.json | 463 ++++++++++++++++++ 4 files changed, 643 insertions(+), 1 deletion(-) create mode 100644 functional_tests/test_admin_settings_field_contract.py create mode 100644 functional_tests/test_support/admin_settings_field_baseline.json diff --git a/application/single_app/config.py b/application/single_app/config.py index 8dd84f2b2..e342eef4a 100644 --- a/application/single_app/config.py +++ b/application/single_app/config.py @@ -96,7 +96,7 @@ EXECUTOR_TYPE = 'thread' EXECUTOR_MAX_WORKERS = 30 SESSION_TYPE = 'filesystem' -VERSION = "0.260.008" +VERSION = "0.260.009" IS_DEVELOPMENT = is_development_env_enabled() SESSION_COOKIE_SAMESITE = os.getenv('SESSION_COOKIE_SAMESITE', 'Lax') diff --git a/docs/explanation/release_notes.md b/docs/explanation/release_notes.md index 5bfcd0b91..a1d4772bc 100644 --- a/docs/explanation/release_notes.md +++ b/docs/explanation/release_notes.md @@ -2,6 +2,16 @@ For feature-focused and fix-focused drill-downs by version, see [Features by Version](/explanation/features/) and [Fixes by Version](/explanation/fixes/). +### **(v0.260.009)** + +#### New Features + +* **Admin Settings Form Field Contract Is Now Enforced** + * Admin Settings submits one form and the backend reads every value by field name, so the set of `name` attributes is the real contract between the template and the settings backend. Renaming or dropping one silently stops that setting from saving, with no error anywhere. + * A new test pins every field name against a committed baseline, and fails the build if one disappears. Adding settings is unaffected; removing one now requires regenerating the baseline in the same commit, which makes it a visible, reviewed decision. + * The same test rejects duplicate field names, which is what prevents a mirrored control from submitting a value twice. + * (Ref: `test_admin_settings_field_contract.py`, `admin_settings_field_baseline.json`) + ### **(v0.260.008)** #### New Features diff --git a/functional_tests/test_admin_settings_field_contract.py b/functional_tests/test_admin_settings_field_contract.py new file mode 100644 index 000000000..9f450f545 --- /dev/null +++ b/functional_tests/test_admin_settings_field_contract.py @@ -0,0 +1,169 @@ +#!/usr/bin/env python3 +# test_admin_settings_field_contract.py +""" +Functional test pinning the Admin Settings form field contract. +Version: 0.260.009 +Implemented in: 0.260.009 + +Admin Settings submits a single form, and the backend reads every value by +field name. That makes the set of `name` attributes the real contract between +the template and `route_frontend_admin_settings.py`. Moving a card between tabs +does not change it; renaming or dropping a `name` silently stops a setting from +saving, with no error anywhere. + +The information architecture rework moves most cards between tabs, so this test +holds that contract still: + + 1. Every field name recorded in the baseline still exists. + 2. No field name is duplicated except the known radio and checkbox groups, + which is what stops a mirrored control from submitting a value twice. + +New field names are allowed and reported, because adding settings is ordinary +work. Removing or renaming one requires regenerating the baseline in the same +commit: + + python functional_tests/test_admin_settings_field_contract.py --update-baseline + +That makes losing a setting a visible, reviewed decision rather than an +accident. +""" + +import json +import re +import sys +from collections import Counter +from pathlib import Path + +from test_support.templates import read_admin_settings_template +from test_support.versioning import assert_app_version_at_least + + +BASELINE_PATH = ( + Path(__file__).resolve().parent + / "test_support" + / "admin_settings_field_baseline.json" +) + +FIELD_RE = re.compile(r'\sname="([^"]+)"') + +# Jinja-templated names cannot be compared literally. +TEMPLATED = re.compile(r"\{\{|\{%") + + +def collect_field_names(markup): + """Return a Counter of literal form field names in the composed template.""" + return Counter( + name for name in FIELD_RE.findall(markup) if not TEMPLATED.search(name) + ) + + +def load_baseline(): + """Read the committed field-name contract.""" + assert BASELINE_PATH.is_file(), ( + f"Missing baseline at {BASELINE_PATH}. Generate it with " + "--update-baseline." + ) + return json.loads(BASELINE_PATH.read_text(encoding="utf-8")) + + +def test_no_admin_settings_field_is_lost(): + """A dropped or renamed field name silently stops a setting from saving.""" + print("Testing Admin Settings field name contract...") + + assert_app_version_at_least("0.260.009") + baseline = load_baseline() + current = collect_field_names(read_admin_settings_template()) + + required = set(baseline["field_names"]) + present = set(current) + + missing = sorted(required - present) + assert not missing, ( + "These form fields disappeared from Admin Settings, so the settings " + "they carry can no longer be saved. If a removal is intended, " + "regenerate the baseline in the same commit with --update-baseline:\n " + + "\n ".join(missing) + ) + + added = sorted(present - required) + if added: + print(f" {len(added)} new field(s) since the baseline: {added[:5]}") + + print(f"All {len(required)} baseline field(s) still present.") + + +def test_no_unexpected_duplicate_field_names(): + """Two controls sharing a name submit the value twice.""" + print("Testing Admin Settings duplicate field names...") + + baseline = load_baseline() + current = collect_field_names(read_admin_settings_template()) + + allowed = baseline["allowed_duplicates"] + unexpected = { + name: count + for name, count in current.items() + if count > 1 and name not in allowed + } + + assert not unexpected, ( + "These field names appear more than once, so the form would submit " + "each of them multiple times. Mirrored controls must omit the name " + "attribute rather than repeat it:\n " + + "\n ".join(f"{name} x{count}" for name, count in sorted(unexpected.items())) + ) + + # A group that stops being a group is also a contract change worth noticing. + shrunk = sorted( + name for name, count in allowed.items() if current.get(name, 0) < count + ) + if shrunk: + print(f" note: known group(s) now smaller than baseline: {shrunk}") + + print(f"No unexpected duplicates; {len(allowed)} known group(s) intact.") + + +def update_baseline(): + """Rewrite the baseline from the current template, deliberately.""" + current = collect_field_names(read_admin_settings_template()) + payload = { + "_comment": ( + "Contract for Admin Settings form field names. Regenerate with " + "test_admin_settings_field_contract.py --update-baseline only when " + "a field is intentionally removed or renamed." + ), + "field_names": sorted(current), + "allowed_duplicates": { + name: count for name, count in sorted(current.items()) if count > 1 + }, + } + BASELINE_PATH.write_text( + json.dumps(payload, indent=2) + "\n", encoding="utf-8" + ) + print(f"Baseline written: {len(payload['field_names'])} field(s), " + f"{len(payload['allowed_duplicates'])} known group(s)") + + +if __name__ == "__main__": + if "--update-baseline" in sys.argv: + update_baseline() + sys.exit(0) + + tests = [ + test_no_admin_settings_field_is_lost, + test_no_unexpected_duplicate_field_names, + ] + + results = [] + for test in tests: + try: + test() + results.append(True) + except Exception as exc: + print(f"FAILED {test.__name__}: {exc}") + import traceback + traceback.print_exc() + results.append(False) + + print(f"\nResults: {sum(results)}/{len(results)} passed") + sys.exit(0 if all(results) else 1) diff --git a/functional_tests/test_support/admin_settings_field_baseline.json b/functional_tests/test_support/admin_settings_field_baseline.json new file mode 100644 index 000000000..3c9fe0019 --- /dev/null +++ b/functional_tests/test_support/admin_settings_field_baseline.json @@ -0,0 +1,463 @@ +{ + "_comment": "Contract for Admin Settings form field names. Regenerate with test_admin_settings_field_contract.py --update-baseline only when a field is intentionally removed or renamed.", + "field_names": [ + "access_denied_message", + "agent_templates_allow_user_submission", + "agent_templates_require_approval", + "agents_page_disclaimer_markdown", + "agents_page_hero_color_mode", + "agents_page_hero_primary_color", + "agents_page_hero_secondary_color", + "agents_page_promoted_popular_agents_json", + "agents_page_promoted_popular_order", + "agents_page_promoted_popular_tag_enabled", + "agents_page_promoted_popular_tag_label", + "agents_page_show_instructions_in_details", + "agents_page_subtitle", + "agents_page_title", + "ai_notice_frequency", + "ai_notice_message", + "allow_group_agents", + "allow_group_custom_endpoints", + "allow_group_plugins", + "allow_group_workflows", + "allow_group_workspace_file_downloads", + "allow_personal_workspace_file_downloads", + "allow_public_workspace_file_downloads", + "allow_user_agents", + "allow_user_custom_endpoints", + "allow_user_plugins", + "allow_user_workflows", + "app_title", + "audio_files_storage_account_url", + "azure_ai_search_authentication_type", + "azure_ai_search_endpoint", + "azure_ai_search_key", + "azure_apim_ai_search_endpoint", + "azure_apim_ai_search_subscription_key", + "azure_apim_content_safety_endpoint", + "azure_apim_content_safety_subscription_key", + "azure_apim_document_intelligence_endpoint", + "azure_apim_document_intelligence_subscription_key", + "azure_apim_embedding_api_version", + "azure_apim_embedding_deployment", + "azure_apim_embedding_endpoint", + "azure_apim_embedding_subscription_key", + "azure_apim_gpt_api_version", + "azure_apim_gpt_deployment", + "azure_apim_gpt_endpoint", + "azure_apim_gpt_subscription_key", + "azure_apim_image_gen_api_version", + "azure_apim_image_gen_deployment", + "azure_apim_image_gen_endpoint", + "azure_apim_image_gen_subscription_key", + "azure_content_understanding_analyzer_id", + "azure_content_understanding_api_version", + "azure_content_understanding_authentication_type", + "azure_content_understanding_endpoint", + "azure_content_understanding_image_analyzer_id", + "azure_content_understanding_key", + "azure_document_intelligence_authentication_type", + "azure_document_intelligence_endpoint", + "azure_document_intelligence_key", + "azure_openai_embedding_api_version", + "azure_openai_embedding_authentication_type", + "azure_openai_embedding_endpoint", + "azure_openai_embedding_key", + "azure_openai_embedding_resource_group", + "azure_openai_embedding_subscription_id", + "azure_openai_gpt_api_version", + "azure_openai_gpt_authentication_type", + "azure_openai_gpt_endpoint", + "azure_openai_gpt_key", + "azure_openai_gpt_resource_group", + "azure_openai_gpt_subscription_id", + "azure_openai_image_gen_api_version", + "azure_openai_image_gen_authentication_type", + "azure_openai_image_gen_endpoint", + "azure_openai_image_gen_key", + "azure_openai_image_gen_resource_group", + "azure_openai_image_gen_subscription_id", + "chunk_size_csv", + "chunk_size_doc", + "chunk_size_docm", + "chunk_size_docx", + "chunk_size_excel", + "chunk_size_html", + "chunk_size_json", + "chunk_size_log", + "chunk_size_md", + "chunk_size_pdf", + "chunk_size_pptx", + "chunk_size_transcript", + "chunk_size_txt", + "chunk_size_xml", + "chunk_size_yaml", + "chunk_size_yml", + "classification_banner_color", + "classification_banner_enabled", + "classification_banner_text", + "classification_banner_text_color", + "content_safety_authentication_type", + "content_safety_endpoint", + "content_safety_include_trigger_information", + "content_safety_key", + "content_safety_violation_message", + "control_center_auto_refresh_enabled", + "control_center_auto_refresh_time", + "control_center_auto_refresh_timezone", + "conversation_cache_ttl_seconds", + "conversation_history_limit", + "cosmos_throughput_account_name", + "cosmos_throughput_auto_scale_down_enabled", + "cosmos_throughput_auto_scale_up_enabled", + "cosmos_throughput_autoscale_enabled", + "cosmos_throughput_container_policies_json", + "cosmos_throughput_convert_manual_to_autoscale_enabled", + "cosmos_throughput_database_name", + "cosmos_throughput_enforce_container_defaults", + "cosmos_throughput_ignore_max_limit", + "cosmos_throughput_ignore_min_limit", + "cosmos_throughput_max_ru", + "cosmos_throughput_metrics_window_minutes", + "cosmos_throughput_min_ru", + "cosmos_throughput_resource_group", + "cosmos_throughput_scale_down_cooldown_minutes", + "cosmos_throughput_scale_down_step_ru", + "cosmos_throughput_scale_down_threshold_percent", + "cosmos_throughput_scale_up_cooldown_minutes", + "cosmos_throughput_scale_up_step_ru", + "cosmos_throughput_scale_up_threshold_percent", + "cosmos_throughput_subscription_id", + "custom_pages_force_menu", + "custom_pages_menu_name", + "custom_pages_restart_acknowledged", + "data_management_migration_groups_mode_choice", + "data_management_migration_mode", + "data_management_migration_public_workspaces_mode_choice", + "data_management_migration_users_mode_choice", + "debug_timer_unit", + "debug_timer_value", + "deep_research_enable_ledger_artifact", + "deep_research_enable_query_planning", + "deep_research_max_search_queries_per_turn", + "deep_research_max_user_urls_per_turn", + "default_model_selection_json", + "default_retention_conversation_group", + "default_retention_conversation_personal", + "default_retention_conversation_public", + "default_retention_document_group", + "default_retention_document_personal", + "default_retention_document_public", + "default_system_prompt", + "disable_group_creation", + "document_access_index_backfill_batch_size", + "document_access_index_cache_ttl_seconds", + "document_access_index_repair_batch_size", + "document_action_analyze_chat_max_documents", + "document_action_analyze_enabled", + "document_action_analyze_workflow_max_documents", + "document_action_comparison_chat_max_documents", + "document_action_comparison_enabled", + "document_action_comparison_workflow_max_documents", + "document_classification_categories_json", + "document_intelligence_auto_sample_pages", + "document_intelligence_pdf_image_extraction_mode", + "embedding_model_json", + "enable_agent_template_gallery", + "enable_ai_notice", + "enable_ai_search_apim", + "enable_appinsights_global_logging", + "enable_audio_file_support", + "enable_chat_completion_audio_cues", + "enable_chat_file_uploads", + "enable_chunk_size_override", + "enable_content_safety", + "enable_content_safety_apim", + "enable_conversation_archiving", + "enable_conversation_cache", + "enable_conversation_contents_drawer", + "enable_custom_pages", + "enable_dark_mode_default", + "enable_debug_logging", + "enable_debug_logging_timer", + "enable_deep_source_review", + "enable_default_embedding_model_plugin", + "enable_desktop_notifications", + "enable_document_access_index_cache", + "enable_document_access_index_reads", + "enable_document_access_index_shadow_validation", + "enable_document_access_index_write_through", + "enable_document_classification", + "enable_document_intelligence_apim", + "enable_document_intelligence_formula_extraction", + "enable_embedding_apim", + "enable_enhanced_citations", + "enable_enhanced_extraction", + "enable_external_healthcheck", + "enable_external_links", + "enable_extract_meta_data", + "enable_fact_memory_plugin", + "enable_file_processing_logs", + "enable_file_processing_logs_timer", + "enable_file_sharing", + "enable_file_sync", + "enable_file_sync_group", + "enable_file_sync_personal", + "enable_file_sync_public", + "enable_front_door", + "enable_gpt_apim", + "enable_group_workspaces", + "enable_http_plugin", + "enable_idle_timeout", + "enable_image_gen_apim", + "enable_image_generation", + "enable_inbound_mcp_rate_limits", + "enable_inbound_mcp_server", + "enable_key_vault_secret_expiration_reminders", + "enable_key_vault_secret_storage", + "enable_landing_page_editor", + "enable_left_nav_default", + "enable_math_plugin", + "enable_mcp_destination_governance", + "enable_multi_model_endpoints", + "enable_multimodal_vision", + "enable_no_auth_external_healthcheck", + "enable_office_embedded_image_analysis", + "enable_public_workspaces", + "enable_redis_cache", + "enable_retention_policy_group", + "enable_retention_policy_personal", + "enable_retention_policy_public", + "enable_semantic_kernel", + "enable_source_review", + "enable_speech_to_text_input", + "enable_startup_document_access_index_backfill", + "enable_support_latest_feature_documentation_links", + "enable_support_latest_features", + "enable_support_menu", + "enable_support_send_feedback", + "enable_swagger", + "enable_tabular_durable_run_confirmation", + "enable_terms_of_use", + "enable_text_plugin", + "enable_text_to_speech", + "enable_thoughts", + "enable_time_plugin", + "enable_url_access", + "enable_user_agreement", + "enable_user_agreement_daily", + "enable_user_feedback", + "enable_user_workspace", + "enable_video_file_support", + "enable_wait_plugin", + "enable_web_search", + "enable_web_search_user_notice", + "enforce_workspace_scope_lock", + "external_links_force_menu", + "external_links_json", + "external_links_menu_name", + "favicon_file", + "file_download_allowed_group_ids", + "file_download_allowed_public_workspace_ids", + "file_sync_allow_recursive_sources", + "file_sync_allowed_group_ids", + "file_sync_allowed_public_workspace_ids", + "file_sync_group_admin_only", + "file_sync_max_concurrent_runs", + "file_sync_max_files_per_run", + "file_sync_max_gb_per_run", + "file_sync_max_sources_per_scope", + "file_sync_min_schedule_interval_minutes", + "file_sync_personal_admin_only", + "file_sync_personal_require_app_role", + "file_sync_public_admin_only", + "file_sync_visible_source_types", + "file_timer_unit", + "file_timer_value", + "front_door_url", + "governance_global_actions_usage", + "governance_global_agents_usage", + "governance_global_endpoints", + "governance_group_actions", + "governance_group_agents", + "governance_group_endpoints", + "governance_user_actions", + "governance_user_agents", + "governance_user_endpoints", + "gpt_model_json", + "group_workflow_allowed_group_ids", + "hide_app_title", + "idle_timeout_minutes", + "idle_warning_message", + "idle_warning_minutes", + "image_gen_model_json", + "inbound_mcp_allow_all_source_ids", + "inbound_mcp_allow_external_tenants", + "inbound_mcp_allowed_client_app_entries_json", + "inbound_mcp_allowed_source_entries_json", + "inbound_mcp_allowed_tenant_entries_json", + "inbound_mcp_max_request_bytes", + "inbound_mcp_rate_limit_read_per_window", + "inbound_mcp_rate_limit_search_per_window", + "inbound_mcp_rate_limit_window_seconds", + "inbound_mcp_rate_limit_write_per_window", + "inbound_mcp_required_app_role", + "inbound_mcp_required_scope", + "inbound_mcp_required_user_role", + "inbound_mcp_source_header", + "key_vault_identity", + "key_vault_name", + "key_vault_secret_expiration_admin_roles", + "key_vault_secret_expiration_default_contact_email", + "key_vault_secret_expiration_default_lead_days", + "key_vault_secret_expiration_emit_contact_email_in_telemetry", + "key_vault_secret_expiration_require_expiration", + "key_vault_secret_expiration_scan_interval_seconds", + "landing_page_alignment", + "landing_page_logo_scale_percent", + "landing_page_text", + "latest_features_enable_enhanced_citations_proxy", + "latest_features_enable_redis_cache_proxy", + "latest_features_enable_thoughts_proxy", + "latest_features_office_docs_authentication_type_proxy", + "latest_features_office_docs_storage_account_blob_endpoint_proxy", + "latest_features_office_docs_storage_account_url_proxy", + "latest_features_redis_auth_type_proxy", + "latest_features_redis_key_proxy", + "latest_features_redis_url_proxy", + "latest_features_tabular_preview_max_blob_size_mb_proxy", + "logo_dark_file", + "logo_file", + "max_file_size_mb", + "max_rounds_per_agent", + "mcp_block_unsafe_destinations", + "merge_global_semantic_kernel_with_workspace", + "metadata_extraction_model", + "metadata_extraction_model_selection_json", + "model_endpoint_identity_header_enabled", + "model_endpoint_identity_header_name", + "model_endpoint_identity_header_value_type", + "model_endpoints_json", + "multimodal_vision_model", + "office_docs_authentication_type", + "office_docs_storage_account_blob_endpoint", + "office_docs_storage_account_url", + "office_embedded_image_max_per_document", + "office_embedded_image_min_pixels", + "orchestration_type", + "per_user_semantic_kernel", + "public_workspace_display_name", + "redis_auth_type", + "redis_key", + "redis_url", + "release_notifications_email", + "release_notifications_name", + "release_notifications_org", + "release_notifications_registered", + "release_notifications_registered_at", + "release_notifications_updated_at", + "require_group_assignment_for_file_downloads", + "require_group_assignment_for_file_sync", + "require_group_assignment_for_group_workflows", + "require_member_of_chat_file_upload_user", + "require_member_of_control_center_admin", + "require_member_of_control_center_dashboard_reader", + "require_member_of_create_group", + "require_member_of_create_public_workspace", + "require_member_of_deep_research_user", + "require_member_of_feedback_admin", + "require_member_of_safety_violation_admin", + "require_member_of_url_access_user", + "require_member_of_workflow_user", + "require_owner_for_group_agent_management", + "require_public_workspace_assignment_for_file_downloads", + "require_public_workspace_assignment_for_file_sync", + "require_shared_conversation_file_approval", + "retention_policy_execution_hour", + "send_feedback_bug_details", + "send_feedback_bug_email", + "send_feedback_bug_name", + "send_feedback_bug_org", + "send_feedback_feature_details", + "send_feedback_feature_email", + "send_feedback_feature_name", + "send_feedback_feature_org", + "show_logo", + "source_review_allow_internal_hosts", + "source_review_allow_js_rendering", + "source_review_audit_logging", + "source_review_default_mode", + "source_review_enable_llm_planning", + "source_review_js_load_more_clicks", + "source_review_max_bytes_per_page_mb", + "source_review_max_depth", + "source_review_max_pages_per_turn", + "source_review_max_redirects", + "source_review_max_seed_pages_per_turn", + "source_review_respect_robots_txt", + "source_review_timeout_seconds", + "speech_service_authentication_type", + "speech_service_endpoint", + "speech_service_key", + "speech_service_locale", + "speech_service_location", + "speech_service_resource_group", + "speech_service_resource_id", + "speech_service_resource_name", + "speech_service_subscription_id", + "support_feedback_recipient_email", + "support_menu_name", + "tabular_durable_run_confirmation_threshold_batches", + "tabular_durable_run_confirmation_threshold_rows", + "tabular_generated_output_chunk_model_deployment", + "tabular_generated_output_chunk_model_mode", + "tabular_preview_max_blob_size_mb", + "terms_of_use_accept_button_text", + "terms_of_use_decline_button_text", + "terms_of_use_decline_redirect_url", + "terms_of_use_frequency", + "terms_of_use_message", + "terms_of_use_title", + "url_access_allowed_domains", + "url_access_blocked_domains", + "url_access_max_chat_urls_per_turn", + "url_access_max_workflow_urls_per_run", + "user_agreement_apply_chat", + "user_agreement_apply_group", + "user_agreement_apply_personal", + "user_agreement_apply_public", + "user_agreement_text", + "video_files_storage_account_url", + "video_index_timeout", + "video_indexer_account_id", + "video_indexer_account_name", + "video_indexer_arm_api_version", + "video_indexer_endpoint", + "video_indexer_location", + "video_indexer_resource_group", + "video_indexer_subscription_id", + "web_search_consent_accepted", + "web_search_foundry_agent_id", + "web_search_foundry_api_version", + "web_search_foundry_auth_type", + "web_search_foundry_authority", + "web_search_foundry_client_id", + "web_search_foundry_client_secret", + "web_search_foundry_cloud", + "web_search_foundry_endpoint", + "web_search_foundry_managed_identity_client_id", + "web_search_foundry_managed_identity_type", + "web_search_foundry_tenant_id", + "web_search_user_notice_text", + "workflow_max_auto_invoke_attempts", + "workflow_max_tasks" + ], + "allowed_duplicates": { + "data_management_migration_groups_mode_choice": 3, + "data_management_migration_mode": 3, + "data_management_migration_public_workspaces_mode_choice": 3, + "data_management_migration_users_mode_choice": 3, + "file_sync_visible_source_types": 3 + } +} From ae46771003d0ca44e35dc6be5c9385b9320f2593 Mon Sep 17 00:00:00 2001 From: Paul Lizer Date: Wed, 19 Aug 2026 14:17:27 -0400 Subject: [PATCH 2/2] Document Admin Settings IA rework status and handoff The rework spans several pull requests into a staging branch, and the context needed to continue it safely has been living in conversation history and in scratch scripts rather than in the repository. Records what has shipped, how to run each guardrail, the regression baseline of 33 pre-existing failures, the three structural complications found while splitting the template, the locked decisions, and the CI branch filters that must be reverted before the staging branch merges into Development. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../ADMIN_SETTINGS_IA_REWORK_STATUS.md | 154 ++++++++++++++++++ 1 file changed, 154 insertions(+) create mode 100644 docs/explanation/features/ADMIN_SETTINGS_IA_REWORK_STATUS.md diff --git a/docs/explanation/features/ADMIN_SETTINGS_IA_REWORK_STATUS.md b/docs/explanation/features/ADMIN_SETTINGS_IA_REWORK_STATUS.md new file mode 100644 index 000000000..8addedc3e --- /dev/null +++ b/docs/explanation/features/ADMIN_SETTINGS_IA_REWORK_STATUS.md @@ -0,0 +1,154 @@ +# Admin Settings IA rework — handoff + +Everything a new conversation needs to continue this work. The session-local +scratch scripts do **not** survive, so the parts that mattered were moved into +the repository. + +--- + +## Where things stand + +Staging branch `feature/admin-settings-ia` on `microsoft/simplechat`, cut from +`Development`. Nothing reaches `Development` until the whole rework is tested +and merged in one final reviewed PR. + +``` +Development ─── feature/admin-settings-ia ──┬── #1297 stage 1 MERGED + ├── #1304 stage A+B MERGED + ├── stage C next + ├── stage D + └── stage E + └──► final PR ──► Development +``` + +Current version: **0.260.009**. Fingerprint: **462 field names / 110 card ids**. + +### Shipped + +| Stage | Content | +|---|---| +| 1 | Template split into 18 per-tab partials under `templates/admin/_panes/`; Latest Features moved last and General made the landing tab; Global Identities heading; File Sync sidebar submenu; `sectionMap` pruned 72 → 6; classification banner preview fixed; CodeQL XSS sink fixed | +| A | 12 cross-tab links converted from tab-coupled `switchTab` to `data-admin-link` card targets; two were already broken; `switchTab` removed | +| B | `data-requires` dependency announcements with inline mirror and link; File Sync and Permissions wired | +| — | Form field contract enforced in CI | + +--- + +## Guardrails — these now live in the repo + +The single most important rule: **never rename a `name=` attribute.** Admin +Settings posts one form and the backend reads by field name, so a rename +silently stops a setting from saving with no error anywhere. + +| Check | How to run | +|---|---| +| **Field contract** | `python -m pytest functional_tests/test_admin_settings_field_contract.py` | +| Regenerate baseline *(only for a deliberate removal)* | `python functional_tests/test_admin_settings_field_contract.py --update-baseline` | +| Composition contract | `python -m pytest functional_tests/test_admin_settings_template_composition.py` | +| Card links | `python -m pytest functional_tests/test_admin_card_links.py` | +| Dependencies | `python -m pytest functional_tests/test_admin_settings_dependencies.py` | +| Sidebar parity | `python -m pytest functional_tests/test_admin_settings_sidebar_card_parity.py` | +| XSS | `python scripts/check_xss_sinks.py --full-file ` | + +**Regression baseline:** the 75 functional test files that reference +`admin_settings.html` sit at **33 pre-existing failures**. That number must not +change. To reproduce the list: + +``` +cd functional_tests +python -m pytest $(grep -rl admin_settings.html test_*.py) -q -p no:randomly +``` + +Exclude `test_enhanced_pii_analysis_standalone.py`, which needs live Azure +credentials. + +### Reading templates in tests + +`admin_settings.html` is only a shell now. Tests must compose it: + +```python +from test_support.templates import read_admin_settings_template +markup = read_admin_settings_template() +``` + +`test_admin_settings_template_composition.py` fails any test that reads the +template uncomposed while referencing a partial-backed card or field. + +--- + +## Next: Stage C + +Add the group navigation level **against the current 18 tabs**, so grouping is +proven before any card moves. + +1. Group row in the top-tab strip; collapsible group headers in + `_sidebar_nav.html` with persisted state. +2. Three-level sidebar search (group → tab → card). +3. Legacy hash redirect map inside `showAdminTab`. + +A server-side nav map is worth introducing here, since both navs need to render +the same group structure. Note it is **not** needed for link resolution — +`admin_card_links.js` resolves card → tab from the DOM via +`closest('.tab-pane')`, which is why links already survive any IA change. + +## Then Stage D — the risky one + +Re-home cards into the 14 groups, **one group per commit**, running the field +contract test on each. Three complications found while splitting: + +- **I1** Modals are interleaved *between* cards, not collected at the end. Each + modal moves with the tab that owns its trigger. +- **I2** Three cards are nested inside other cards and must move with their + parents: `conversation-contents-drawer-section` (inside chat file uploads), + `content-understanding-section` and `office-embedded-image-section` (inside + document intelligence). All three land in the same target tab as their parent. +- **I4** The Data Management migration workflow is ~985 lines of non-card markup + forming one unit. Split it across Backup/Migrate/Restore/Jobs **last and on + its own**. + +## Then Stage E + +- Split `system-settings-section`: `max_file_size_mb` → Workspaces; + `conversation_history_limit` and `default_system_prompt` → Chat; idle timeout + fields → Security; `access_denied_message` → Security. **No renames.** +- Access and Roles roster mirroring the 10 `require_member_of_*` toggles, built + on the Stage B proxy handling. Mirrors carry **no `name` attribute** — the + field contract test enforces this. + +--- + +## Decisions already locked + +| | Decision | +|---|---| +| D1 | Split `system-settings-section` across Chat / Security / Workspaces | +| D2 | Workflow becomes its own group | +| D3 | Mirror role toggles rather than move them | +| D4 | Legacy hash redirect map | +| D5 | Per-tab partials | +| Q2 | File Sync warns rather than blocks; hard-disable elsewhere | +| — | Land each stage as its own PR into the staging branch | + +## Target structure — 14 groups + +`Appearance · Chat · AI Models · Agents & Actions · Workspaces · Workflow · +Knowledge · Security · Governance · Data Lifecycle · Backup & Recovery · Scale · +Operations · Help` + +Full card-by-card placement is in the PR description of #1304 and in the +stage 2 plan. Key rationale: grouping follows *capability weight*, not card +titles — Governance has 196 KB of backing JS so it cannot be a Security +sub-tab; Video and audio are extraction pipelines, not chat features; Retention +is data lifecycle, not a workspace toggle. + +--- + +## Before the final merge to Development + +Revert the CI branch filters added in #1297. Six workflows list +`feature/admin-settings-ia` in their `pull_request` branch filters so staged +PRs are gated; `Development` should end up with its original configuration. + +Files: `codeql.yml`, `xss-sink-check.yml`, `broken-access-control-check.yml`, +`malicious-pr-security-review.yml`, `python-syntax-check.yml`, +`swagger-route-check.yml`.