From f31e6f8dbcef1d42dc7541785a51f47fd80d7d2c Mon Sep 17 00:00:00 2001 From: Paul Lizer Date: Tue, 6 Oct 2026 14:06:53 -0400 Subject: [PATCH] Hide always-on tabular preflight switches from V2 Admin Settings The V2 admin fallback scan drew enable_tabular_search_shared_preflight under Shared Conversation File Approvals (word-stem match on 'shared') with no description, and drew enable_tabular_analyze_durable_preflight and enable_tabular_hierarchical_analysis under Other capabilities. All three are reset to True by normalize_tabular_parity_durable_preflight_defaults() on every settings read, so the switches reverted after saving; SIMPLECHAT_DISABLE_TABULAR_PARITY_DURABLE_PREFLIGHT remains the only off switch. Suppress all three in SUPPRESSED_CAPABILITY_KEYS with written reasons, require every enable_* key in TABULAR_PARITY_DURABLE_PREFLIGHT_ACTIVE_DEFAULTS to stay suppressed, and add Workspaces to the groups that must receive no guessed switches. Version 0.261.260. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../single_app/admin_settings_fields.py | 32 +++++ application/single_app/config.py | 2 +- .../TABULAR_ANALYZE_SEARCH_PARITY_ROLLOUT.md | 2 + .../features/V2_ADMIN_CHAT_SETTINGS.md | 8 +- .../V2_ADMIN_TABULAR_PREFLIGHT_SWITCH_FIX.md | 105 ++++++++++++++++ docs/explanation/release_notes.md | 10 ++ .../test_v2_admin_capability_placement.py | 115 +++++++++++++++++- 7 files changed, 266 insertions(+), 8 deletions(-) create mode 100644 docs/explanation/fixes/V2_ADMIN_TABULAR_PREFLIGHT_SWITCH_FIX.md diff --git a/application/single_app/admin_settings_fields.py b/application/single_app/admin_settings_fields.py index 374c9632f..59e2876ef 100644 --- a/application/single_app/admin_settings_fields.py +++ b/application/single_app/admin_settings_fields.py @@ -6328,6 +6328,38 @@ "Staged rollout flag gated behind enable_mixed_source_chat_search, with no " "control in the server-rendered admin form." ), + # The three tabular durable-preflight switches below are always on by design. + # normalize_tabular_parity_durable_preflight_defaults() in functions_settings.py + # resets each one to True on every settings read and persists the correction, and + # _apply_tabular_parity_env_kill_switch() forces them off only while the + # SIMPLECHAT_DISABLE_TABULAR_PARITY_DURABLE_PREFLIGHT environment variable is set. + # A switch would therefore revert in either direction. The word "shared" in the + # first key also filed it under Shared Conversation File Approvals, where it had + # nothing to do with the cards around it. + "enable_tabular_search_shared_preflight": ( + "Always on. Routes an exhaustive row-by-row request against a CSV or XLSX file, " + "made from a regular chat message (Search), through the shared tabular planner, " + "so it can run as a durable background job over every row rather than a " + "bounded foreground answer that covers only the first few. get_settings() " + "resets it to True on every read, so a switch would revert; the " + "SIMPLECHAT_DISABLE_TABULAR_PARITY_DURABLE_PREFLIGHT environment variable is " + "the only way to turn it off." + ), + "enable_tabular_analyze_durable_preflight": ( + "Always on. The Analyze counterpart of enable_tabular_search_shared_preflight: " + "an Analyze request against a single CSV or XLSX file goes through the same " + "shared planner and can run as a durable background job. get_settings() resets " + "it to True on every read; the SIMPLECHAT_DISABLE_TABULAR_PARITY_DURABLE_PREFLIGHT " + "environment variable is the only way to turn it off." + ), + "enable_tabular_hierarchical_analysis": ( + "Always on. Lets the durable tabular job take exhaustive per-row and per-line " + "requests, including narrative ones that ask for written answers rather than a " + "CSV, JSON or XML export; without it they fall back to a bounded foreground " + "answer. get_settings() resets it to True on every read; the " + "SIMPLECHAT_DISABLE_TABULAR_PARITY_DURABLE_PREFLIGHT environment variable is the " + "only way to turn it off." + ), } diff --git a/application/single_app/config.py b/application/single_app/config.py index 670ab4808..3a890675e 100644 --- a/application/single_app/config.py +++ b/application/single_app/config.py @@ -101,7 +101,7 @@ EXECUTOR_TYPE = 'thread' EXECUTOR_MAX_WORKERS = 30 SESSION_TYPE = 'filesystem' -VERSION = "0.261.259" +VERSION = "0.261.260" IS_DEVELOPMENT = is_development_env_enabled() # Opt-out for deployments where App Service Easy Auth is active but the platform diff --git a/docs/explanation/features/TABULAR_ANALYZE_SEARCH_PARITY_ROLLOUT.md b/docs/explanation/features/TABULAR_ANALYZE_SEARCH_PARITY_ROLLOUT.md index 99cc87839..aa0c87608 100644 --- a/docs/explanation/features/TABULAR_ANALYZE_SEARCH_PARITY_ROLLOUT.md +++ b/docs/explanation/features/TABULAR_ANALYZE_SEARCH_PARITY_ROLLOUT.md @@ -39,6 +39,8 @@ All Phase 8 rollout controls are backend-only and are removed by `sanitize_setti > **Update (0.250.186):** `tabular_request_planner_mode` now defaults to `active` and both `enable_tabular_search_shared_preflight` and `enable_tabular_analyze_durable_preflight` default to `True`. Bounded foreground synthesis for exhaustive row-by-row tabular requests is no longer the default behavior. There is intentionally no admin UI toggle for this; use the `SIMPLECHAT_DISABLE_TABULAR_PARITY_DURABLE_PREFLIGHT` environment variable for emergency rollback (see below). +> **Update (0.261.260):** V2 Admin Settings had been drawing switches for these flags through its `enable_*` fallback scan, and those switches reverted on the next settings read. They are now listed in `SUPPRESSED_CAPABILITY_KEYS`, so neither admin interface offers a toggle. See [V2 Admin Tabular Preflight Switch Fix](../fixes/V2_ADMIN_TABULAR_PREFLIGHT_SWITCH_FIX.md). + ## Usage Instructions How to enable/configure: diff --git a/docs/explanation/features/V2_ADMIN_CHAT_SETTINGS.md b/docs/explanation/features/V2_ADMIN_CHAT_SETTINGS.md index 348747a0b..85fc9b153 100644 --- a/docs/explanation/features/V2_ADMIN_CHAT_SETTINGS.md +++ b/docs/explanation/features/V2_ADMIN_CHAT_SETTINGS.md @@ -145,9 +145,15 @@ claims there is something to edit. | `enable_enhanced_citations_mount` | No control in either interface; forced off unless Enhanced Citations is on | | `enable_mixed_source_chat_search` | Staged rollout flag with no administrator control | | `enable_mixed_source_conversation_continuity` | Staged rollout flag gated behind the above | +| `enable_tabular_search_shared_preflight` | Always on; `get_settings()` resets it to `True` on every read, and `SIMPLECHAT_DISABLE_TABULAR_PARITY_DURABLE_PREFLIGHT` is the only off switch | +| `enable_tabular_analyze_durable_preflight` | Always on, forced the same way | +| `enable_tabular_hierarchical_analysis` | Always on, forced the same way | The list is served on the settings GET as `suppressed_capabilities` and honoured -by `buildCapabilityIndex`. +by `buildCapabilityIndex`. Every `enable_*` key in +`TABULAR_PARITY_DURABLE_PREFLIGHT_ACTIVE_DEFAULTS` must be suppressed; see +[V2 Admin Tabular Preflight Switch Fix](../fixes/V2_ADMIN_TABULAR_PREFLIGHT_SWITCH_FIX.md) +(0.261.260). ### Enhanced Citations storage test diff --git a/docs/explanation/fixes/V2_ADMIN_TABULAR_PREFLIGHT_SWITCH_FIX.md b/docs/explanation/fixes/V2_ADMIN_TABULAR_PREFLIGHT_SWITCH_FIX.md new file mode 100644 index 000000000..3bf9a59e0 --- /dev/null +++ b/docs/explanation/fixes/V2_ADMIN_TABULAR_PREFLIGHT_SWITCH_FIX.md @@ -0,0 +1,105 @@ +# V2 Admin Tabular Preflight Switch Fix + +## Issue + +V2 Admin Settings showed a switch called **Tabular search shared preflight** +(`enable_tabular_search_shared_preflight`) under Workspaces › Files & Sharing › +Shared Conversation File Approvals. It had no description and nothing to do with file +approvals. Changing it had no lasting effect: a switch turned off came back on after the +next page load. + +Two related flags, `enable_tabular_analyze_durable_preflight` and +`enable_tabular_hierarchical_analysis`, had the same problem. They appeared as raw keys +under **Other capabilities**. + +## What these flags do + +All three control how SimpleChat handles an exhaustive request against a CSV or XLSX +file, such as "for every row, answer these questions" or "give me a CSV with one line per +row". + +| Flag | Effect while on | +| --- | --- | +| `enable_tabular_search_shared_preflight` | A regular chat message (Search) goes through the shared tabular planner. The planner can run the request as a durable background job over every row. | +| `enable_tabular_analyze_durable_preflight` | The Analyze path does the same for a request against a single tabular file. | +| `enable_tabular_hierarchical_analysis` | The durable job accepts exhaustive per-row and per-line requests, including ones that ask for written answers rather than a file export. | + +With any of them off, those requests fall back to the older foreground path. That path +answers the first rows that fit in one model turn and then truncates, or keeps retrying +without finishing. This was the customer-reported failure the tabular parity work fixed. +The only reason to turn them off is an incident rollback, which the environment variable +below handles, so they are always on. + +## Root cause + +The V2 admin page draws a switch for every `enable_*` boolean in the settings document +that `admin_settings_fields.py` does not declare. It places each one under the navigation +section whose id shares the most words with the key (`buildCapabilityIndex` in +`AdminSettingsPage.tsx`). The only word `enable_tabular_search_shared_preflight` shares +with any section id is "shared", in `shared-conversation-file-approvals-section`. That is +where it was drawn. + +None of the three flags can be edited: + +- `normalize_tabular_parity_durable_preflight_defaults()` in `functions_settings.py` sets + each one back to `True` on every `get_settings()` read and saves the correction. It + exists because older deployments stored these flags as `False` before their defaults + were raised. See [Tabular Parity Stale Settings Migration Fix](TABULAR_PARITY_STALE_SETTINGS_MIGRATION_FIX.md). +- `_apply_tabular_parity_env_kill_switch()` forces them to `False` while the + `SIMPLECHAT_DISABLE_TABULAR_PARITY_DURABLE_PREFLIGHT` environment variable is set. This is + the supported emergency rollback. + +A switch therefore reverted in either direction. The classic admin page has never had a +control for these flags, and the rollout documentation says there is intentionally no +admin toggle. + +## Fixed in version: **0.261.260** + +The application version is maintained in `application/single_app/config.py`. + +## Technical details + +### Files modified + +| File | Change | +| --- | --- | +| `application/single_app/admin_settings_fields.py` | Added the three flags to `SUPPRESSED_CAPABILITY_KEYS`, each with the reason it is not an administrator setting. | +| `functional_tests/test_v2_admin_capability_placement.py` | Expects the three suppressions, adds a check that covers every forced flag, and adds Workspaces to the groups that must receive no guessed switches. | +| `application/single_app/config.py` | Version bump. | + +### Code changes + +- The settings GET already sends `SUPPRESSED_CAPABILITY_KEYS` as `suppressed_capabilities`, + and `buildCapabilityIndex` skips those keys. Listing the three flags removes their switches + and needs no frontend change. +- Defaults and runtime behavior are unchanged. The flags stay `True`, and the environment + variable still turns all of them off. + +### Impact + +- Shared Conversation File Approvals now shows only its own setting. +- **Other capabilities** no longer lists the two sibling flags. +- Administrators can no longer flip a switch that appears to save but has no lasting effect. + +## Validation + +`functional_tests/test_v2_admin_capability_placement.py` passes 8/8. To confirm the guards +catch the regression, the suppression for `enable_tabular_search_shared_preflight` was +removed in memory, and three checks then failed: + +- `test_described_groups_receive_no_guessed_capabilities` reported the key filed under + Workspaces › Files & Sharing › `shared-conversation-file-approvals-section`. Workspaces + is now one of the groups that must receive no guessed switches. +- `test_forced_tabular_parity_flags_are_suppressed` reads + `TABULAR_PARITY_DURABLE_PREFLIGHT_ACTIVE_DEFAULTS` from `functions_settings.py` and + requires every `enable_*` key in it to be suppressed with a reason. A flag added to that + map later is covered without editing the test. +- `test_non_editable_capabilities_are_suppressed_not_declared` reported the missing + suppression. + +Before: an unlabelled switch under Shared Conversation File Approvals and two raw keys +under Other capabilities, all of which reverted after saving. After: no switch for any of +the three, and all three stay on. + +Related: [V2 Admin Chat Settings](../features/V2_ADMIN_CHAT_SETTINGS.md), +[Tabular Analyze Search Parity Rollout](../features/TABULAR_ANALYZE_SEARCH_PARITY_ROLLOUT.md). diff --git a/docs/explanation/release_notes.md b/docs/explanation/release_notes.md index ebc32df8c..7ffc28675 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](https://github.com/microsoft/simplechat/tree/main/docs/explanation/features) and [Fixes by Version](https://github.com/microsoft/simplechat/tree/main/docs/explanation/fixes). +### **(v0.261.260)** + +#### Bug Fixes + +* **Always-On Tabular Switches Removed From V2 Admin Settings** + * V2 Admin Settings no longer shows an unlabelled **Tabular search shared preflight** switch under Workspaces › Files & Sharing › Shared Conversation File Approvals. It landed there only because its key contains the word "shared". It also no longer shows `enable_tabular_analyze_durable_preflight` or `enable_tabular_hierarchical_analysis` under Other capabilities. + * All three flags send exhaustive row-by-row requests against CSV and XLSX files to a durable background job that covers every row, instead of a foreground answer that truncates. They are always on: settings loading resets them to on every time, so the switches reverted after every save. Behavior is unchanged, and the `SIMPLECHAT_DISABLE_TABULAR_PARITY_DURABLE_PREFLIGHT` environment variable is still the emergency off switch. + * The capability placement test now fails if any flag that settings loading resets is drawn as a switch, or if a guessed switch lands in the Workspaces group. + * (Ref: `admin_settings_fields.SUPPRESSED_CAPABILITY_KEYS`, `TABULAR_PARITY_DURABLE_PREFLIGHT_ACTIVE_DEFAULTS`, `test_v2_admin_capability_placement.py`, [V2 Admin Tabular Preflight Switch Fix](fixes/V2_ADMIN_TABULAR_PREFLIGHT_SWITCH_FIX.md)) + ### **(v0.261.259)** #### Bug Fixes diff --git a/functional_tests/test_v2_admin_capability_placement.py b/functional_tests/test_v2_admin_capability_placement.py index 8c45da801..8cf6ff8e1 100644 --- a/functional_tests/test_v2_admin_capability_placement.py +++ b/functional_tests/test_v2_admin_capability_placement.py @@ -2,7 +2,7 @@ #!/usr/bin/env python3 """ Functional test pinning where the V2 admin surface files each capability toggle. -Version: 0.261.122 +Version: 0.261.260 Implemented in: 0.261.047 Settings that ``admin_settings_fields.py`` does not describe are still shown in the @@ -24,17 +24,22 @@ - ``enable_text_plugin`` matched "text" in ``home-page-text-section`` and appeared under Appearance > Branding. -Declaring a field is what takes a key out of that scan. This test holds three +Declaring a field is what takes a key out of that scan. This test holds four invariants so the misfiling cannot come back: - 1. The Appearance, Chat and Security groups are fully described by the schema, so - they must receive *no* guessed rows at all. A new undeclared key that lands in - any of them fails here, and the fix is to declare it in its real section. + 1. The Appearance, Chat, Security, Agents & Actions and Workspaces groups are fully + described by the schema, so they must receive *no* guessed rows at all. A new + undeclared key that lands in any of them fails here, and the fix is to declare + it in its real section. 2. The keys that were moved stay declared where they were moved to. 3. Keys that are not editable settings at all stay suppressed rather than declared. ``enable_tabular_processing_plugin`` is the clearest case: it is derived from ``enable_enhanced_citations`` and rewritten by ``get_settings`` on every read, so a switch would appear to save and then revert. + 4. Every ``enable_*`` flag that ``get_settings`` forces back to its default on + every read stays suppressed. Those flags are listed in + ``TABULAR_PARITY_DURABLE_PREFLIGHT_ACTIVE_DEFAULTS``, and new ones added there + are covered automatically. Security was described later and had misfilings of its own. The clearest was ``enable_app_maintenance`` and ``enable_startup_app_maintenance``, which matched @@ -45,8 +50,19 @@ "storage" in ``data-management-storage-section`` and appeared under Backup & Recovery, while ``enable_key_vault_secret_expiration_reminders`` matched nothing at all and fell into "Other capabilities". + +``enable_tabular_search_shared_preflight`` matched "shared" in +``shared-conversation-file-approvals-section`` and appeared under Workspaces > Files +& Sharing as an unlabelled switch. It is not an administrator setting at all: +``normalize_tabular_parity_durable_preflight_defaults`` resets it to True on every +settings read, and the ``SIMPLECHAT_DISABLE_TABULAR_PARITY_DURABLE_PREFLIGHT`` +environment variable is the only way to turn it off. A switch for it, or for +``enable_tabular_analyze_durable_preflight`` or ``enable_tabular_hierarchical_analysis`` +(which are forced the same way and were filed under "Other capabilities"), saved and +then reverted, so all three are suppressed. """ +import ast import re import sys from pathlib import Path @@ -68,7 +84,13 @@ # Groups whose sections are described by the schema in full. A guessed row landing # in one of these is a key that was filed by word stems into a group that has a # real home for everything it owns, which means it is in the wrong place. -FULLY_DESCRIBED_GROUP_IDS = (APPEARANCE_GROUP_ID, "chat", "security", "agents-actions") +FULLY_DESCRIBED_GROUP_IDS = ( + APPEARANCE_GROUP_ID, + "chat", + "security", + "agents-actions", + "workspaces", +) # Where each relocated toggle now lives, and the V1 pane it is mirrored from. The # pane is checked too, because a schema field with no server-rendered counterpart @@ -120,8 +142,17 @@ "enable_enhanced_citations_mount", "enable_mixed_source_chat_search", "enable_mixed_source_conversation_continuity", + # Forced to True by get_settings() on every read; the environment kill switch + # is the only way to turn them off. + "enable_tabular_search_shared_preflight", + "enable_tabular_analyze_durable_preflight", + "enable_tabular_hierarchical_analysis", ) +# The map in functions_settings.py of flags get_settings() resets to their active +# default on every read. Every enable_* key in it must be suppressed. +FORCED_TABULAR_PARITY_DEFAULTS = "TABULAR_PARITY_DURABLE_PREFLIGHT_ACTIVE_DEFAULTS" + # Relocations with no server-rendered counterpart to check against. Both are # documented in ``V2_ONLY_FIELDS``, which is what the section assertion below reads # instead of a pane. @@ -162,6 +193,30 @@ def read_capability_keys(): return keys +def read_forced_tabular_parity_capability_keys(): + """Return the ``enable_*`` keys ``get_settings`` forces back to their default. + + ``normalize_tabular_parity_durable_preflight_defaults`` resets every key in + ``TABULAR_PARITY_DURABLE_PREFLIGHT_ACTIVE_DEFAULTS`` on each settings read, so none + of them can be changed from the admin page. The map is read out of the source + for the same reason ``read_capability_keys`` reads the defaults that way. + """ + tree = ast.parse(SETTINGS_MODULE.read_text(encoding="utf-8")) + for node in tree.body: + if ( + isinstance(node, ast.Assign) + and len(node.targets) == 1 + and isinstance(node.targets[0], ast.Name) + and node.targets[0].id == FORCED_TABULAR_PARITY_DEFAULTS + ): + forced_defaults = ast.literal_eval(node.value) + return sorted(key for key in forced_defaults if key.startswith("enable_")) + raise AssertionError( + f"{FORCED_TABULAR_PARITY_DEFAULTS} was not found in {SETTINGS_MODULE.name}; " + "the extraction likely broke." + ) + + def build_sections(): """Return every navigation section with the tokens the renderer matches on.""" sections = [] @@ -338,6 +393,53 @@ def test_suppressed_capabilities_are_real_settings_keys(): return True +def test_forced_tabular_parity_flags_are_suppressed(): + """A flag ``get_settings`` resets on every read cannot be an admin switch.""" + print("\nTesting that forced tabular parity flags are suppressed...") + + assert_app_version_at_least("0.261.260") + + forced_keys = read_forced_tabular_parity_capability_keys() + assert forced_keys, ( + f"No enable_* keys were found in {FORCED_TABULAR_PARITY_DEFAULTS}; " + "the extraction likely broke." + ) + + suppressed = fields_module.SUPPRESSED_CAPABILITY_KEYS + # A read-only mirror reports a value without offering to change it, so it is + # allowed; only a declaration that would save the value is not. + editable = { + field["key"] + for _section_id, field in fields_module.iter_fields() + if field.get("key") and not field.get("readonly") + } + + problems = [] + for key in forced_keys: + if key not in suppressed: + problems.append( + f"{key}: not suppressed, so the fallback scan draws a switch that " + "reverts on the next settings read" + ) + elif not str(suppressed[key] or "").strip(): + problems.append(f"{key}: suppressed with no reason recorded") + if key in editable: + problems.append( + f"{key}: declared as an editable field, but get_settings() " + "overwrites it on every read" + ) + + assert not problems, ( + "These flags are reset to their active default by " + "normalize_tabular_parity_durable_preflight_defaults() on every settings " + "read. Suppress each one, with its reason, in " + "admin_settings_fields.SUPPRESSED_CAPABILITY_KEYS:\n " + "\n ".join(problems) + ) + + print(f" All {len(forced_keys)} forced tabular parity flag(s) are suppressed.") + return True + + def test_relocated_capabilities_are_declared_where_they_belong(): """Undeclaring one of these silently returns it to the group it was guessed into.""" print("\nTesting the relocated capability declarations...") @@ -436,6 +538,7 @@ def test_relocated_capabilities_exist_in_their_v1_panes(): test_described_groups_receive_no_guessed_capabilities, test_non_editable_capabilities_are_suppressed_not_declared, test_suppressed_capabilities_are_real_settings_keys, + test_forced_tabular_parity_flags_are_suppressed, test_relocated_capabilities_are_declared_where_they_belong, test_v2_only_relocations_are_documented, test_relocated_capabilities_exist_in_their_v1_panes,