Skip to content

Ai kill switch - #1140

Open
jsegunosidefox wants to merge 14 commits into
mainfrom
AI-KillSwitch
Open

Ai kill switch#1140
jsegunosidefox wants to merge 14 commits into
mainfrom
AI-KillSwitch

Conversation

@jsegunosidefox

@jsegunosidefox jsegunosidefox commented Mar 5, 2026

Copy link
Copy Markdown
Collaborator

Delete this block after confirming that you will do the following:

  • If asked to make changes, I will re-request reviews.
  • If conversations are added, I will resolve them before merging.
  • I will delete my branch after merging, or set to delete upon merge.
  • I will update any Bugzilla bugs or TestRail test cases if needed.

Relevant Links

Bugzilla: _
TestRail: _

Description of Code / Doc Changes

Leave a bullet-pointed list of changes you made.

Process Changes Required

Mark the relevant boxes, delete irrelevant lines.

  • Adds a dependency (rerun pipenv install)
  • Modifies a git hook (rerun ./devsetup.sh)
  • Changes the BasePage
  • Changes or creates a BOM/POM (name the object model): _
  • Changes CI flow
  • Changes scheduled Beta or DevEdition
  • Changes Git hooks or Github settings
  • Changes L10n harness

Screenshots or Explanations

If you need to explain your code, do it here.

Comments or Future Work

Do we need to start another PR soon to address something you saw while working on this?

Workflow Checklist

  • Reviewers have been requested.
  • Code has been linted and formatted.
  • If this is an unblocker, a message has been posted to #dte-automation in Slack.

Thank you!

@ben-c-at-moz ben-c-at-moz left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Excellent first PR for StarFox tests. I know our system has some idiosyncrasies. Just a few things to look into at first, and if you want to merge a recently-pulled main into this branch and make sure your tests exist in manifests/key.yaml. You can use python scripts/addtests.py for that if you like.

Comment thread conftest.py Outdated
# Nightly might not have this element, that's okay
import time
time.sleep(1) # Give the page a moment to settle
pass

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do need this check to run when we're executing against Beta -- can we refactor this so that we simply ignore the check in Nightly instead of having a fallback? (The fallback invalidates the version check for Beta as well)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We still need to refactor this so that we don't invalidate version testing for Beta -- but the current code on main is pretty different than what was changed here, I believe.

Comment thread ai_prefs_dump.html Outdated

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need this file in the repository?

# Simulate keyboard interaction - in real test, use pynput
# For now, we verify the element is accessible
logging.info("AI Controls toggle is accessible")
except Exception as e:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we don't have to do explicit pytest.fail() calls--the failure message in the assert call will log as an error. If you want that additional info, you could add it to the failure message in the assert. This isn't a dealbreaker, and if you really want this kind of messaging you can keep it as is.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please separate this into three test files, one per test case in TestRail.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please split this into one file per test case

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please break this out into one test file per test case

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please break this out into one test file per test case

@ben-c-at-moz ben-c-at-moz left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Progress, but we need to resolve some code issues and some convention issues, and one versioning issue:

Versioning

  • We need to rebase this branch on a current main

Convention

  • Every test file needs to have a test_case fixture that returns the TestRail case number as a string
  • If there are multiple "concerns" covered by a single test case, they can be separate test functions, but those test functions need to be in the same test file
  • The suite_id fixture in the suite conftest.py file should correspond to the suite number of the tests in TestRail, in the form ("Snnn", "Suite Description"), where nis an integer. If the tests are in multiple suites, those tests should be in different subdirectories of./tests`.
  • Currently, we do not support subfolders of suites. We could implement this in future, but for now, all of these new tests should live directly in tests/ai_controls (unless you need multiple suites, as mentioned above. If this is the case, they would be in tests/ai_controls_a11y, tests/ai_controls_functional, etc.)

Code

  • In general, we prefer WebDriverWait().until() over assert. Many functions on BasePage implement until(), like expect(), element_visible(), url_contains(), etc.
  • In some cases, an assert or other testing code is in the try branch of a try: except Exception: block, which will prevent the tests from ever failing.
  • The code changes in the main conftest.py file should be a separate PR. We currently do not run these tests against "true" Nightly, and aren't going to run into the concern addressed.

Let me know if you need any help addressing any of these points. Thanks for working on this!

Comment thread manifests/key.yaml
result: pass
splits:
- functional2
ai_controls:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We may want a separate split called "ai_controls"...Not exactly sure on this one, will have to think about it.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need test_case fixture

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

need test_case fixture

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

need test_case fixture

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

need test_case fixture

try:
state = about_prefs.get_ai_translations_state()
logging.info(f"Translations feature state: {state}")
except Exception as e:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test unlikely to fail as all exceptions caught on the main testing code.

try:
toggle_state = about_prefs.get_ai_killswitch_state()
logging.info(f"Killswitch state under policy: {toggle_state}")
except Exception as e:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test unlikely to fail as all exceptions to testing code caught.

C3341237 - AI controls labels localized
"""
about_prefs.navigate_to_ai_controls()
about_prefs.verify_ai_controls_page_loaded()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a stub?

chatbot = about_prefs.get_element("ai-control-sidebar-chatbot-select")
assert chatbot is not None, "Chatbot provider control should exist"
logging.info("Chatbot feature is localized")
except Exception as e:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test unlikely to fail as main testing code exceptions always caught.

translations = about_prefs.get_element("ai-control-translations-select")
assert translations is not None, "Translations control should exist"
logging.info("Translations feature is localized")
except Exception as e:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test unlikely to fail as all exceptions on testing code caught.

@github-actions

Copy link
Copy Markdown
Contributor

Code Review

Overview

This PR adds an ai_controls test suite (25 tests) for the Firefox AI kill switch feature, along with new POM methods in AboutPrefs and element selectors in the JSON manifest.


Critical Issues

1. Manifest keys don't match file names (will break the skip system)

All files in tests/ai_controls/ follow the test_c{id}_<name>.py naming pattern (e.g., test_c3276002_ai_features_keyboard_accessible.py), but manifests/key.yaml registers them under short names (e.g., test_ai_features_keyboard_accessible). The manifest skip system uses the filename without .py as the key — see other suites like address_bar_and_search/ where each manifest key matches its filename exactly. Run python addtests.py after deciding on the final file names to regenerate correct entries.

2. test_c3276002_ai_features_keyboard_accessible.py contains 4 test functions

Per the project convention (CLAUDE.md): "Each test file should contain one test function." The three keyboard navigation tests need to be in their own files to be independently registered and skippable in the manifest.

3. test_ai_killswitch.py is unregistered and has a duplicate TestRail ID

This file is missing from key.yaml entirely, and it uses TestRail case 3341325 — the same ID as test_c3341325_provider_selection_reflects.py. Remove or properly integrate it.


POM Issues

4. Bare except Exception: pass in set_ai_blocking

Silently swallowing all exceptions hides real failures. If find_in_settings("AI") fails because the page hasn't loaded, the test will continue in a broken state with no signal. At minimum, log the exception.

5. from time import sleep inside the method body + fixed-duration sleep

Imports belong at the top of the file. Also, sleep(0.5) is a fixed-duration wait — an anti-pattern in Selenium. Use self.expect() or WebDriverWait with an explicit condition instead.

6. verify_ai_controls_page_loaded doesn't actually verify

element_exists() return values are discarded. If an element is missing, nothing is raised. The method silently passes even if the page is empty.


Test Logic Issues

7. Invalid state value in test_c3310314_features_remain_after_unblock.py line 20

set_ai_translations("available") passes "available" but the method's docstring specifies valid values as "enabled", "removed", or "blocked".

8. Most tests are empty stubs marked as pass in the manifest

  • Policy tests (test_c3277712 through test_c3298825, test_c3279955) just navigate and log — no assertions, no policy setup.
  • The high-contrast test doesn't enable high contrast mode.
  • Keyboard accessibility tests don't use the keyboard (no send_keys(Tab) etc.) — they just check element existence.
  • Localization tests don't inspect any label strings.

Tests that are stubs should be marked disabled or unstable in the manifest, not pass.


Minor

  • conftest.py: suite_id returns ("TODO", "AI Controls") — the TestRail suite number must be filled in before merge.
  • PR description, Bugzilla link, and workflow checklist are unfilled.

Comment thread tests/ai_controls/conftest.py Outdated
@pytest.fixture()
def suite_id():
# TODO: Replace with the actual TestRail suite number, e.g. ("12345", "AI Controls")
return ("TODO", "AI Controls")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The TestRail suite number is still a "TODO" placeholder. This needs to be filled in with the real suite ID before merge — the driver fixture depends on suite_id for test reporting.

Comment thread modules/page_object_prefs.py
Comment on lines +353 to +354
provider
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two issues here:

  1. from time import sleep should be at the top of the file with other imports, not inside a method body.
  2. sleep(0.5) is a fixed-duration wait — an anti-pattern in Selenium. The backend change you're waiting for should be detectable via a DOM condition. Use self.expect() with a lambda that checks the resulting pref or element state instead.

Comment thread tests/ai_controls/test_c3310314_features_remain_after_unblock.py
logging.info("Block all button may use moz-toggle element")


def test_keyboard_navigation_to_translations_setting(about_prefs: AboutPrefs):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file contains 4 test functions, violating the project convention of one test per file (CLAUDE.md). The three keyboard navigation functions below (test_keyboard_navigation_to_translations_setting, test_keyboard_navigation_to_link_preview_setting, test_keyboard_navigation_to_smart_tab_groups_setting) need to be moved to their own files so they can be independently registered in key.yaml and skipped per-platform.

Comment thread tests/ai_controls/test_ai_killswitch.py Outdated

@pytest.fixture()
def test_case():
return "3341325"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file uses TestRail case "3341325" which is already claimed by test_c3341325_provider_selection_reflects.py. Duplicate test_case IDs cause reporting collisions. Additionally, this file has no entry in key.yaml. It should either be removed (the scaffold intent is covered by the other file) or given a unique ID and registered in the manifest.

@github-actions

Copy link
Copy Markdown
Contributor

Review: AI Kill Switch Tests

Overview: Adds a new ai_controls test suite (26 tests) covering the AI killswitch feature in about:preferences#ai, plus new POM methods and component JSON entries for the moz-toggle/moz-select custom elements.


Critical Issues

1. Duplicate TestRail IDs
Four test files all return the same test_case ID "3276002":

  • test_c3276002_ai_features_keyboard_accessible.py
  • test_c3276002_keyboard_nav_translations.py
  • test_c3276002_keyboard_nav_link_preview.py
  • test_c3276002_keyboard_nav_smart_tab_groups.py

Each file must return its own unique TestRail case ID. If these are genuinely subtests of the same case, they should share a test file with subtest functions (see key.yaml subtest format).

2. suite_id is a TODO placeholder
conftest.py returns ("TODO", "AI Controls"). This will break TestRail reporting for the entire suite and must be resolved before merging.


Tests That Are Pure Stubs (No Assertions)

Several tests navigate to the page and log a message but perform no actual verification. These will always pass regardless of browser behavior:

  • test_c3277712_disable_all_policies — logs killswitch state, no assert
  • test_c3277713_disable_chatbot_policies — logs translations state, no assert
  • test_c3277714_disable_smart_tab_policies — just navigates
  • test_c3277716_disable_link_preview_policies — just navigates
  • test_c3298825_disable_translations_policies — just navigates
  • test_c3276003_ai_settings_visible_high_contrast — no contrast mode activated
  • test_c3276004_ai_settings_link_shortcut — no shortcut navigation performed
  • test_c3341237_labels_localized — no string comparisons

Tests that claim to verify something but don't fully deliver:

  • test_c3341331_extensions_api_disabled — sets killswitch but doesn't check extensions.ml.enabled
  • test_c3343878_models_deleted_when_blocked — sets killswitch but doesn't verify model deletion
  • test_c3340562_cancel_blocking — simulates cancel by toggling state; doesn't interact with any cancel dialog/button

Keyboard Tests Don't Test Keyboard Navigation

Tests named keyboard_nav_* only call element_visible(). Keyboard accessibility implies actually sending TAB/ENTER keys and verifying focus. Consider using ActionChains or webdriver.Keys if this is a genuine keyboard navigation test, or rename the tests to reflect they only check visibility.


POM: set_ai_blocking silently suppresses find_in_settings errors

The bare except Exception around find_in_settings("AI") will hide real failures (wrong context, stale element, etc.). If find_in_settings is truly optional here, the catch should be narrowed to a specific exception type.


Minor

  • Several tests call navigate_to_ai_controls() (which already calls verify_ai_controls_page_loaded()) and then call verify_ai_controls_page_loaded() again — redundant double-check.
  • test_c3341325_provider_selection_reflects: calls set_ai_chatbot_provider("ChatGPT") but never asserts get_ai_chatbot_provider() == "ChatGPT".
  • PR description leaves bullets/links as template placeholders (_).

Comment thread tests/ai_controls/conftest.py Outdated
@pytest.fixture()
def suite_id():
# TODO: Replace with the actual TestRail suite number, e.g. ("12345", "AI Controls")
return ("TODO", "AI Controls")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The suite_id is a placeholder — this must be replaced with the real TestRail suite ID before merging. A "TODO" value here will break TestRail result reporting for every test in this suite.


@pytest.fixture()
def test_case():
return "3276002"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicate TestRail ID: test_c3276002_ai_features_keyboard_accessible.py, test_c3276002_keyboard_nav_link_preview.py, and test_c3276002_keyboard_nav_smart_tab_groups.py all return the same "3276002". Each test file needs a unique case ID, or these should be collapsed into one file as subtests.

Comment thread modules/page_object_prefs.py Outdated
Comment on lines +334 to +338

Arguments:
provider: Provider name (e.g., "ChatGPT", "Claude", "Copilot")
"""
select_elem = self.get_element("ai-control-sidebar-chatbot-select")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bare except Exception silently swallows all failures. If find_in_settings genuinely may not apply here, narrow this to the specific exception it can raise (e.g. TimeoutException) so real errors like wrong context or a stale element aren't hidden.

Comment on lines +20 to +26
about_prefs.navigate_to_ai_controls()
about_prefs.verify_ai_controls_page_loaded()

toggle_state = about_prefs.get_ai_killswitch_state()
logging.info(f"Killswitch state under policy: {toggle_state}")
# Note: When DisableAIEnhancements policy is active, killswitch should be forced on.
# Full validation requires enterprise policy configuration in the browser profile.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test has no assertions — it will always pass. The note acknowledges the limitation but the test shouldn't be merged in this state. Either: configure a profile with the DisableAIEnhancements enterprise policy and assert get_ai_killswitch_state() is True, or mark it disabled in key.yaml until the infrastructure is ready. The same applies to test_c3277713, test_c3277714, test_c3277716, and test_c3298825.

Comment on lines +22 to +24
about_prefs.element_exists("ai-control-sidebar-chatbot-select")
about_prefs.set_ai_chatbot_provider("ChatGPT")
logging.info("Chatbot provider set via AI controls page")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing assertion after setting the provider. set_ai_chatbot_provider silently no-ops if the provider name doesn't match any option. Add a verification step:

Suggested change
about_prefs.element_exists("ai-control-sidebar-chatbot-select")
about_prefs.set_ai_chatbot_provider("ChatGPT")
logging.info("Chatbot provider set via AI controls page")
about_prefs.set_ai_chatbot_provider("ChatGPT")
assert about_prefs.get_ai_chatbot_provider() == "ChatGPT", (
"Chatbot provider should reflect 'ChatGPT' after selection"
)

Comment thread modules/page_object_prefs.py Outdated
# Use the find-in-settings box to reveal the control if needed
self.find_in_settings("AI")
except TimeoutException as e:
logging.warning(f"find_in_settings('AI') raised: {e}")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Silently swallowing TimeoutException here is risky. If the AI Controls pane isn't actually visible, the next get_element("ai-controls-toggle") call will also time out with a much less descriptive error. Either remove the try/except (let the navigation handle visibility), or re-raise after logging if the element still can't be found.

Comment thread modules/page_object_prefs.py Outdated
)

if block != current_state:
# Set the pressed property directly (clicking doesn't work on moz-toggle)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Setting element.pressed via JS bypasses all real UI interactions. The comment says clicking doesn't work on moz-toggle, but test C3276002 ("AI features keyboard accessible") is specifically meant to verify keyboard accessibility. The POM helper using JS is fine for state setup, but the keyboard accessibility test should be testing actual keyboard interaction (Tab + Space/Enter), not delegating to this method.

Comment thread modules/page_object_prefs.py Outdated
self.driver.execute_script(
"arguments[0].dispatchEvent(new Event('change', { bubbles: true }));",
select_elem
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

set_ai_translations doesn't verify the state was actually applied (no expect() call), unlike set_ai_blocking. Consider adding:

Suggested change
)
self.expect(lambda _: self.get_ai_translations_state() == state)
return self

select.dispatchEvent(new Event('change', { bubbles: true }));
""",
select_elem,
provider

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

set_ai_chatbot_provider silently does nothing if provider doesn't match any option (the loop just falls through, then the change event fires with the old value). This will make test_c3341325 pass even if "ChatGPT" isn't in the dropdown. Consider adding a return value from JS to detect a no-match and raising an error.

Comment thread modules/page_object_prefs.py Outdated

def verify_ai_controls_page_loaded(self) -> BasePage:
"""
Verify that the AI Controls page has loaded successfully by checking for required elements.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

verify_ai_controls_page_loaded only checks three elements but the suite has five AI control elements (also ai-control-link-preview-select and ai-control-smart-tab-groups-select). Either check all, or rename the method to reflect the partial scope (e.g., verify_ai_controls_core_elements_visible).

Comment thread tests/ai_controls/conftest.py Outdated
@@ -0,0 +1,21 @@
import pytest
from selenium.webdriver import Firefox

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Firefox is imported but only used as a type annotation on the driver parameter — this import isn't strictly needed since driver is injected by the fixture system. Remove or replace with from __future__ import annotations if purely for type hints.

@pytest.fixture()
def policies_list():
return {
"AIControls": {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

navigate_to_ai_controls() defaults to verify=True, which calls element_visible("ai-control-translations-select") and element_visible("ai-control-sidebar-chatbot-select"). With this policy active, those controls are hidden — so verification will time out before the test assertions are even reached. Use navigate_to_ai_controls(verify=False) here (and in the other policy-hiding tests: test_c3277713, test_c3277714, test_c3277716, test_c3298825).

Comment thread conftest.py Outdated
options.binary_location = fx_executable

# Write enterprise policies.json if any policies were requested
policy_file: Path | None = None

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

policy_file is initialised inside the try block. If an exception is raised before this line (e.g., from the Options() or argument-setter calls above), the finally block at line 586 will hit a NameError on policy_file. Move policy_file = None to just before the try: to guarantee it's always bound.

Suggested change
policy_file: Path | None = None
policy_file: Path | None = None



def test_block_all_ai_features_option_persists(about_prefs: AboutPrefs):
"""

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the kill switch hides individual feature controls in the UI, set_ai_translations will call get_element("ai-control-translations-select") on a hidden element and time out. Confirm whether individual controls remain accessible (just disabled) when the kill switch is on, or whether this call needs to bypass the element lookup (e.g., set the pref directly via JS).

Comment thread modules/page_object_prefs.py Outdated
Comment on lines +452 to +457
def navigate_to_ai_controls(self) -> BasePage:
"""
Navigate to the AI Controls preference page.
"""
self.driver.get("about:preferences#ai")
def navigate_to_ai_controls(self, verify: bool = True) -> "AboutPrefs":

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are two definitions of navigate_to_ai_controls back-to-back — this looks like a bad merge. The first definition (no verify param, returns nothing) is dead code since Python silently uses the second one. Delete lines 452–456.

Suggested change
def navigate_to_ai_controls(self) -> BasePage:
"""
Navigate to the AI Controls preference page.
"""
self.driver.get("about:preferences#ai")
def navigate_to_ai_controls(self, verify: bool = True) -> "AboutPrefs":
def navigate_to_ai_controls(self, verify: bool = True) -> "AboutPrefs":

Comment thread modules/page_object_prefs.py Outdated
Comment on lines +393 to +398
"""
try:
# Use the find-in-settings box to reveal the control if needed
self.find_in_settings("AI")
except Exception as e:
logging.warning(f"find_in_settings('AI') raised: {e}")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The "docstring" is actually commented-out code wrapped in a triple-quoted string — this method has no real docstring and the dead code block will confuse future readers. Replace with a proper docstring and keep the commented logic as a regular # comment if needed.

Suggested change
"""
try:
# Use the find-in-settings box to reveal the control if needed
self.find_in_settings("AI")
except Exception as e:
logging.warning(f"find_in_settings('AI') raised: {e}")
"""
Set the sidebar chatbot provider by matching option label.
Arguments:
provider: The display label of the provider (e.g. "ChatGPT").
"""

Comment thread conftest.py
unpack_archive(os.path.join("profiles", f"{use_profile}.zip"), profile_path)
options.profile = profile_path

options.set_preference("app.update.disabledForTesting", False)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line was previously commented out and is now being added unconditionally. Setting app.update.disabledForTesting to False (i.e. not disabling updates) means Firefox may try to auto-update during test runs, which can interfere with tests. Was this intentional? If the intent is to disable updates during testing the value should be True; if it's truly not needed, remove this line rather than setting it to False.

Set the AI Translations feature state.

Arguments:
state: "available" or "blocked"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docstring for set_ai_translations lists "available" or "blocked" as valid values, but get_ai_translations_state returns "enabled", "removed", or "blocked". Additionally, test_c3310314 calls set_ai_translations("enabled"). The valid state strings are inconsistent — please align the docstrings and usages to a single set of accepted values.

return {}


def test_enable_ai_features_when_policy_changed(about_prefs: AboutPrefs):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test is named "enable when policy changed" but the policies_list fixture returns {} — the same as the default. The test only verifies baseline behavior (killswitch is off with no policy); it doesn't simulate a policy being changed or removed. Consider either renaming the test to reflect what it actually checks, or adding a companion scenario that starts with a blocking policy and then overrides it.

@github-actions

Copy link
Copy Markdown
Contributor

Review: AI Kill Switch (#1140)

Overview

Adds an ai_controls test suite covering the AI kill switch and per-feature AI controls in about:preferences#ai. Introduces a policies_list fixture that writes distribution/policies.json before Firefox launches (and cleans it up in finally), enabling enterprise-policy tests without a separate profile. Good structural approach overall.

Bugs / Must-Fix

  • Duplicate method definition (page_object_prefs.py lines 452–464): Two back-to-back definitions of navigate_to_ai_controls — clearly a bad merge. Python silently picks the second; the first is dead code. (Inline comment posted.)

  • set_ai_chatbot_provider has no real docstring (page_object_prefs.py ~line 393): The triple-quoted string is commented-out code, not a docstring. (Inline comment posted.)

  • app.update.disabledForTesting = False (conftest.py line 538): This was previously commented out. Setting it to False explicitly opts Firefox into update checks during tests, which can cause interference. If updates should be suppressed, the value should be True. (Inline comment posted.)

  • Inconsistent state strings (set_ai_translations / get_ai_translations_state): The setter docstring lists "available" | "blocked", the getter docstring lists "enabled" | "removed" | "blocked", and test_c3310314 passes "enabled". These need to be reconciled. (Inline comment posted.)

Minor Issues

  • test_c3279955 doesn't test a policy change: The policies_list fixture returns {} (same as the default), so the test only verifies baseline behavior. Name or intent needs to be corrected. (Inline comment posted.)

  • Stub tests marked pass in manifest: Several localization tests (c3341234, c3341235, c3341236, c3341237) only check element existence, not localization. If that is the accepted scope for now, a comment in the test noting the limitation would help.

  • PR description is blank: The template sections (description of changes, links, screenshots) are unfilled.

What Works Well

  • The policies_list fixture + finally cleanup is a clean way to inject enterprise policies without a dedicated profile.
  • UnboundLocalError catch in the finally block is simpler than the previous locals()/globals() check.
  • Genuinely incomplete tests are correctly disabled in the manifest rather than left as false-passing stubs.

@github-actions

Copy link
Copy Markdown
Contributor

Review Notes

Overall: Good start on the AI Controls test suite. Several issues need addressing before this is ready to merge.

Blocking issues

  1. conftest.py changes belong in a separate PR. The policies_list fixture, driver teardown rewrite, and the app.update.disabledForTesting change affect every test suite in the repo. They should be reviewed and landed independently (as previously requested by @ben-c-at-moz).

  2. test_c3340562_cancel_blocking does not test cancellation of a dialog — it just toggles the pref on and off. If no confirmation dialog exists in the UI yet, the test should be marked disabled in the manifest until it can be properly implemented.

  3. test_c3279955_enable_when_policy_changed only asserts the default state with no policy. It never applies a policy and then removes it, so it doesn't cover the scenario described in the TestRail case.

  4. set_ai_translations / get_ai_translations_state value mismatch. The setter docstring lists valid inputs as "available" or "blocked", but test_c3310314 calls it with "enabled" (which is a value only the getter returns). Pick one consistent set of values and document it clearly.

Code quality issues

  1. get_ai_killswitch_state() returns element.pressed via JS with no type guarantee. If the property is missing, JS returns undefined → Python gets None. Many tests then do assert … is False which will produce a confusing failure instead of a useful error. Consider adding a guard or explicit cast.

  2. set_ai_chatbot_provider iterates over select.options on a moz-select custom element. Standard HTMLSelectElement.options may not exist on that element, causing a silent JS TypeError and always returning false.

  3. Tests marked pass in the manifest with TODO bodies (e.g. test_c3341235, test_c3341236): these only assert element presence, not localization. They should be disabled until the actual localization assertion is implemented.

  4. suite_id format — per project convention the suite number should use the S prefix (e.g. ("S71443", "AI Controls")).

Comment thread modules/page_object_prefs.py Outdated
return self.driver.execute_script(
"return arguments[0].pressed;",
toggle
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JS returns undefined (→ Python None) if the element lacks a pressed property, making assert result is False pass incorrectly or fail with a confusing message. Consider adding a cast:

Suggested change
)
return bool(self.driver.execute_script(
"return arguments[0].pressed;",
toggle
))

Set the AI Translations feature state.

Arguments:
state: "available" or "blocked"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Docstring says valid states are "available" or "blocked", but get_ai_translations_state() can return "enabled" and test_c3310314 calls this method with "enabled". Reconcile the valid value sets across setter, getter, and all callers.

Comment thread modules/page_object_prefs.py Outdated
"""
const select = arguments[0];
const provider = arguments[1];
for (let option of select.options) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

select.options is a property of HTMLSelectElement, not moz-select (a custom element). This will likely be undefined, causing for...of to throw a TypeError and always return false. You may need to query the shadow DOM for moz-option children instead, e.g. select.querySelectorAll('moz-option').

Comment thread conftest.py
unpack_archive(os.path.join("profiles", f"{use_profile}.zip"), profile_path)
options.profile = profile_path

options.set_preference("app.update.disabledForTesting", False)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line was previously commented out; unconditionally setting it to False enables auto-updates during all tests across every suite — not just AI Controls. Please confirm this is intentional and, if so, move this change to a dedicated conftest.py PR with an explanation.

Comment thread tests/ai_controls/conftest.py Outdated

@pytest.fixture()
def suite_id():
return ("71443", "AI Controls")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Per project convention, the TestRail suite number should carry the S prefix.

Suggested change
return ("71443", "AI Controls")
return ("S71443", "AI Controls")

return {}


def test_enable_ai_features_when_policy_changed(about_prefs: AboutPrefs):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test only verifies the default state when no policy is present. The TestRail case ("Enable AI features when policy changed") requires launching with a restricting policy active, then simulating removal/change of that policy and confirming features become re-enabled. The current implementation doesn't cover that scenario at all.


initial_state = about_prefs.get_ai_killswitch_state()

# Block AI, then restore to initial state (simulating a cancel action)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't test cancellation — it enables blocking then re-disables it programmatically. The test case title ("Cancel blocking from the Block AI Enhancements prompt") implies a confirmation dialog that the user cancels. If that dialog doesn't exist yet, mark this disabled in the manifest rather than shipping a test that asserts the wrong thing.

"""
about_prefs.navigate_to_ai_controls()

about_prefs.set_ai_translations("enabled")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

set_ai_translations docstring says valid values are "available" or "blocked""enabled" is only a value the getter returns. Calling the setter with "enabled" may silently write an invalid pref value and the subsequent expect could pass for the wrong reason (element already in that state). Use "available" here to match the setter's documented API.

C3341235 - Translations feature localized
"""
about_prefs.navigate_to_ai_controls()
about_prefs.element_exists("ai-control-translations-select")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This only checks that the element exists — it doesn't verify anything about localization. The TODO in the next line describes what's actually needed. This test is registered as result: pass in the manifest; it should be disabled until the document.l10n.formatValue(...) assertion is implemented.

Comment thread conftest.py

def _get_version(driver: Firefox):
driver.get(ABOUT_FIREFOX)
version_el = driver.find_element(By.ID, "version")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential flakiness: find_element is called immediately after driver.get() with no wait for the element to be present. The about:firefox page may not have finished rendering #version by the time this runs. Suggest using WebDriverWait here like _fx_up_to_date does:

Suggested change
version_el = driver.find_element(By.ID, "version")
WebDriverWait(driver, 10).until(
EC.visibility_of_element_located((By.ID, "version"))
)
version_el = driver.find_element(By.ID, "version")

Comment thread conftest.py
"""Collect executables, but just the version number for Fx"""
if not os.environ.get("CI"):
return None
return collect_executables.main("-n")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The previous guard if not os.environ.get("CI"): return None was removed, which means collect_executables.main("-n") now runs in all environments including local dev. If that script fails or is slow outside CI, every local test run will be affected. Was this intentional?

Comment thread conftest.py
@@ -415,14 +421,8 @@ def pytest_sessionfinish(session):
raise OSError("Could not find TestRail credentials")

tr_session = tri.testrail_init()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The if tr_session is None: return guard was removed. If tri.testrail_init() returns None, calling tri.collect_changes(tr_session, report) here will raise an AttributeError or TypeError instead of failing gracefully. The guard should be kept.

Comment thread conftest.py
tri.mark_results(tr_session, passes)
else:
logging.warning("No test results found.")
tri.mark_results(tr_session, passes)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tri.mark_results is now called unconditionally even when passes is empty/falsy. Previously there was a guard:

if passes:
    tri.mark_results(tr_session, passes)
else:
    logging.warning("No test results found.")

If mark_results doesn't handle an empty collection gracefully, this could cause a silent failure or error.

Comment thread conftest.py
logging.info(f"Removed enterprise policies file {policy_file}")
except OSError:
pass
if not hard_quit and ("driver" in locals() or "driver" in globals()) and driver:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The hard_quit path previously called driver.service.stop() before returning — that cleanup is now gone entirely. With this change, hard_quit=True only skips driver.quit(), but the service process is never stopped, which may leave a dangling geckodriver process.


if block != current_state:
self.driver.execute_script(
"Services.prefs.setStringPref('browser.ai.control.default', arguments[0]);",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Services.prefs is only available in chrome context. Other AboutPrefs methods that interact with chrome-privileged APIs use @BasePage.context_chrome (e.g. remove_search_engine at line 168). This call will fail in content context. Consider adding the decorator — or wrapping the execute_script call in with self.driver.context(self.driver.CONTEXT_CHROME):.

# moz-toggle uses the 'pressed' property; cast to bool for safety
# in case the property is missing (JS returns undefined → Python None)
return bool(self.driver.execute_script(
"return arguments[0].pressed;",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same chrome-context issue: driver.execute_script("return arguments[0].pressed;", toggle) on a moz-toggle custom element — accessing .pressed as a JS property on a shadow-DOM component typically works in content context, but the get_element() call that precedes it may fail if the element requires chrome context to locate. Worth verifying this works correctly in a content-context environment.

@github-actions

Copy link
Copy Markdown
Contributor

Review: AI Kill Switch

Overview

Adds a new tests/ai_controls/ suite (18 test files) covering the Firefox AI kill switch and enterprise policy controls, along with a new policies_list fixture that writes distribution/policies.json to enable enterprise policy testing.

conftest.py — Notable Regressions

Several previously-guarded code paths were removed without clear justification:

  • Removed if not driver: guards in screenshot_content and log_content: these were defensive guards for driver-less teardown paths. Removing them adds crash surface.
  • build_version no longer short-circuits locally: the if not os.environ.get("CI"): return None guard was dropped, so collect_executables.main("-n") now runs in every environment. See inline comment.
  • tr_session is None check removed: tri.testrail_init() can return None; calling tri.collect_changes on it will raise rather than fail gracefully. See inline comment.
  • hard_quit no longer stops the service: the old driver.service.stop() call was removed, leaving geckodriver potentially running when hard_quit=True. See inline comment.

New policies_list Fixture

The approach of writing distribution/policies.json to the Firefox install directory is clean and the PermissionError → pytest.skip handling is appropriate. On macOS, confirm the Resources/distribution path resolves correctly for all install layouts (custom vs. standard).

New POM Methods (page_object_prefs.py)

  • set_ai_blocking and related methods use Services.prefs via execute_script, which requires chrome context. Other methods that need chrome access use @BasePage.context_chrome. These new methods appear to be missing that decorator — see inline comment.
  • The _get_version helper has a potential race condition (no wait before find_element) — see inline comment.

Stub Tests

Many test files are correctly marked disabled in key.yaml and contain # TODO bodies. That's a reasonable approach for shipping placeholders, but the PR description doesn't list which tests are stubs vs. functional — worth noting in the description for reviewers.

Checklist

  • The PR description checklist is largely unfilled (no Bugzilla/TestRail links, linting not confirmed, BOM/POM name not filled in).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants