This is an automated email from the ASF dual-hosted git repository.

rusackas pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git


The following commit(s) were added to refs/heads/master by this push:
     new 7b65d222050 chore(tags): TAGGING_SYSTEM to True by default (#39888)
7b65d222050 is described below

commit 7b65d222050104bf3df183ddbdb59bc0b0cab917
Author: Sam Firke <[email protected]>
AuthorDate: Tue Sep 8 19:27:37 2026 -0400

    chore(tags): TAGGING_SYSTEM to True by default (#39888)
    
    Co-authored-by: rusackas <[email protected]>
---
 UPDATING.md                                        | 28 +++++++++++++++++++
 docs/static/feature-flags.json                     | 12 ++++-----
 superset/app.py                                    | 13 +++++----
 superset/config.py                                 |  6 ++---
 superset/tags/models.py                            | 31 ++++++++++++++++++++++
 tests/integration_tests/base_tests.py              | 19 +++++++++++++
 tests/integration_tests/charts/api_tests.py        |  8 +++++-
 tests/integration_tests/charts/commands_tests.py   | 14 +++++++---
 .../integration_tests/dashboards/commands_tests.py |  3 +++
 tests/integration_tests/fixtures/tags.py           | 21 ++++++++++-----
 tests/integration_tests/tags/api_tests.py          |  8 +++++-
 .../commands/importers/v1/assets_test.py           |  3 +++
 tests/unit_tests/initialization_test.py            |  9 ++-----
 tests/unit_tests/tags/commands/create_test.py      | 19 +++++++++----
 tests/unit_tests/tags/commands/update_test.py      | 10 +++++--
 15 files changed, 162 insertions(+), 42 deletions(-)

diff --git a/UPDATING.md b/UPDATING.md
index 786f4add82b..389d6b90370 100644
--- a/UPDATING.md
+++ b/UPDATING.md
@@ -24,6 +24,34 @@ assists people when migrating to a new version.
 
 ## Next
 
+### Tagging is on by default
+
+`TAGGING_SYSTEM` now ships **on**. The Tags menu entry, the tag columns and
+filters on the chart, dashboard and saved-query lists, and the Tags field in 
the
+chart and dashboard property modals are all visible without configuration, and
+tags are included in asset export and import.
+
+**What operators should expect:**
+
+- **Implicit tags accrue.** Saving a chart, dashboard, dataset or saved query,
+  and favoriting an asset, write rows to `tag` and `tagged_object` 
(`type:chart`,
+  `editor:<user id>`, `favorited_by:<user id>`). These have always been created
+  when the flag was on; they are simply no longer opt-in.
+- **Exports gain a `tags` key and a `tags.yaml` file.** Chart and dashboard
+  export bundles carry custom tags. Importers on 6.0 and later understand both;
+  older importers skip the unrecognized `tags.yaml` file but reject chart and
+  dashboard YAML that contains a `tags` key, so strip that key before importing
+  a bundle into Superset 5.x or earlier.
+- **The flag is honored at write time.** The tagging SQLA event listeners are
+  always attached at startup; the ones that create tags check `TAGGING_SYSTEM`
+  when they fire, so the flag, including a runtime override through
+  `GET_FEATURE_FLAGS_FUNC` or `IS_FEATURE_ENABLED_FUNC`, takes effect without a
+  restart. The cleanup listeners run regardless of the flag, so deleting an
+  asset never leaves orphaned `tagged_object` rows behind.
+
+Set `FEATURE_FLAGS = {"TAGGING_SYSTEM": False}` to restore the previous
+behavior. Existing tag rows are left untouched.
+
 ### Global Async Queries re-platformed onto the Global Task Framework 
(breaking)
 
 Global Async Queries (GAQ) no longer runs on its own bespoke async-events
diff --git a/docs/static/feature-flags.json b/docs/static/feature-flags.json
index a34590d95ea..7a679395464 100644
--- a/docs/static/feature-flags.json
+++ b/docs/static/feature-flags.json
@@ -98,12 +98,6 @@
         "default": false,
         "lifecycle": "development",
         "description": "Enable Table V2 time comparison feature"
-      },
-      {
-        "name": "TAGGING_SYSTEM",
-        "default": false,
-        "lifecycle": "development",
-        "description": "Enables the tagging system for organizing assets"
       }
     ],
     "testing": [
@@ -240,6 +234,12 @@
         "description": "Allow users to enable SSH tunneling when creating a DB 
connection. DB engine must support SSH Tunnels.",
         "docs": 
"https://superset.apache.org/docs/configuration/setup-ssh-tunneling";
       },
+      {
+        "name": "TAGGING_SYSTEM",
+        "default": true,
+        "lifecycle": "testing",
+        "description": "Enables the tagging system for organizing assets"
+      },
       {
         "name": "USE_ANALOGOUS_COLORS",
         "default": false,
diff --git a/superset/app.py b/superset/app.py
index fb7ba012b7a..2ce5bbf0484 100644
--- a/superset/app.py
+++ b/superset/app.py
@@ -184,9 +184,6 @@ class SupersetApp(Flask):
         app startup.
         """
         try:
-            # Import here to avoid circular import issues
-            from superset.extensions import feature_flag_manager
-
             # Check if database is up-to-date with migrations
             if not self._is_database_up_to_date():
                 logger.info("Pending database migrations: run 'superset db 
upgrade'")
@@ -194,11 +191,13 @@ class SupersetApp(Flask):
 
             logger.info("Syncing configuration to database...")
 
-            # Register SQLA event listeners for tagging system
-            if feature_flag_manager.is_feature_enabled("TAGGING_SYSTEM"):
-                from superset.tags.core import register_sqla_event_listeners
+            # Register SQLA event listeners for the tagging system. The
+            # listeners that create tags check TAGGING_SYSTEM when they fire,
+            # and the cleanup listeners must run regardless of the flag so a
+            # deleted object never leaves orphaned `tagged_object` rows behind.
+            from superset.tags.core import register_sqla_event_listeners
 
-                register_sqla_event_listeners()
+            register_sqla_event_listeners()
 
             # Seed system themes from configuration
             from superset.commands.theme.seed import SeedSystemThemesCommand
diff --git a/superset/config.py b/superset/config.py
index 3a32ae85b81..eb20815eb56 100644
--- a/superset/config.py
+++ b/superset/config.py
@@ -740,9 +740,6 @@ DEFAULT_FEATURE_FLAGS: dict[str, bool] = {
     # Enable Table V2 time comparison feature
     # @lifecycle: development
     "TABLE_V2_TIME_COMPARISON_ENABLED": False,
-    # Enables the tagging system for organizing assets
-    # @lifecycle: development
-    "TAGGING_SYSTEM": False,
     # Enables the version history panel on Explore and Dashboard pages.
     # History only accrues while ``ENABLE_VERSIONING_CAPTURE`` is also on;
     # with capture off the panel renders empty or stale history, so the two
@@ -826,6 +823,9 @@ DEFAULT_FEATURE_FLAGS: dict[str, bool] = {
     # @lifecycle: testing
     # @docs: https://superset.apache.org/docs/configuration/setup-ssh-tunneling
     "SSH_TUNNELING": False,
+    # Enables the tagging system for organizing assets
+    # @lifecycle: testing
+    "TAGGING_SYSTEM": True,
     # Enable AWS IAM authentication for database connections (Aurora, 
Redshift).
     # Allows cross-account role assumption via STS AssumeRole.
     # Security note: When enabled, ensure Superset's IAM role has restricted
diff --git a/superset/tags/models.py b/superset/tags/models.py
index c60e660b23d..68425d9d917 100644
--- a/superset/tags/models.py
+++ b/superset/tags/models.py
@@ -170,6 +170,28 @@ def get_object_type(class_name: str) -> ObjectType:
         ) from ex
 
 
+def tagging_enabled() -> bool:
+    """
+    Whether the tagging system is enabled.
+
+    The SQLA event listeners below are attached unconditionally at app startup
+    (see ``superset.app.SupersetApp.sync_config_to_db``). Listeners that
+    *create* tags check the flag when they fire, so the flag, including a
+    runtime override, is honored on the write path the same way it is on the
+    UI, export and import paths. The ``after_delete`` listeners deliberately
+    skip this check: they only remove ``tagged_object`` rows, and skipping that
+    cleanup would orphan rows pointing at a deleted object whose id may later
+    be reused.
+    """
+    # Resolved through the manager on every call rather than bound at import
+    # time, so that patching the manager (as the tests do) takes effect.
+    from superset.extensions import (  # pylint: 
disable=import-outside-toplevel
+        feature_flag_manager,
+    )
+
+    return feature_flag_manager.is_feature_enabled("TAGGING_SYSTEM")
+
+
 class ObjectUpdater:
     object_type: str = "default"
 
@@ -235,6 +257,9 @@ class ObjectUpdater:
         connection: Connection,
         target: Dashboard | FavStar | Slice | Query | SqlaTable,
     ) -> None:
+        if not tagging_enabled():
+            return
+
         with Session(bind=connection) as session:  # pylint: 
disable=disallowed-name
             # add `editor:` tags
             cls._add_editors(session, target)
@@ -253,6 +278,9 @@ class ObjectUpdater:
         connection: Connection,
         target: Dashboard | FavStar | Slice | Query | SqlaTable,
     ) -> None:
+        if not tagging_enabled():
+            return
+
         with Session(bind=connection) as session:  # pylint: 
disable=disallowed-name
             # Fetch current editor tags
             existing_tags = (
@@ -345,6 +373,9 @@ class FavStarUpdater:
     def after_insert(
         cls, _mapper: Mapper, connection: Connection, target: FavStar
     ) -> None:
+        if not tagging_enabled():
+            return
+
         with Session(bind=connection) as session:  # pylint: 
disable=disallowed-name
             name = f"favorited_by:{target.user_id}"
             tag = get_tag(name, session, TagType.favorited_by)
diff --git a/tests/integration_tests/base_tests.py 
b/tests/integration_tests/base_tests.py
index aaf82ff1449..8bef7da4dff 100644
--- a/tests/integration_tests/base_tests.py
+++ b/tests/integration_tests/base_tests.py
@@ -45,6 +45,7 @@ from superset.models.slice import Slice
 from superset.sql.parse import CTASMethod
 from superset.subjects.models import Subject
 from superset.subjects.types import SubjectType
+from superset.tags.models import Tag, TaggedObject
 from superset.utils import json
 from superset.utils.core import get_example_default_schema, shortid
 from superset.utils.database import get_example_database
@@ -271,10 +272,28 @@ class SupersetTestCase(TestCase):
                 db.session.delete(temp_role)
             if login:
                 self.logout()
+            self._release_tag_references(temp_user.id)
             db.session.delete(temp_user)
             db.session.commit()
             g.user = previous_g_user
 
+    @staticmethod
+    def _release_tag_references(user_id: int) -> None:
+        """
+        Drop `ab_user` references held by rows the tagging system wrote.
+
+        Anything a user saves while `TAGGING_SYSTEM` is on stamps the audit
+        columns of the `tag` and `tagged_object` rows it creates, and those are
+        foreign keys. Deleting the user without clearing them fails with a
+        foreign key violation on backends that enforce them.
+        """
+        for model in (Tag, TaggedObject):
+            for column in ("created_by_fk", "changed_by_fk"):
+                db.session.query(model).filter(
+                    getattr(model, column) == user_id
+                ).update({column: None}, synchronize_session=False)
+        db.session.commit()
+
     @staticmethod
     def create_user(
         username: str,
diff --git a/tests/integration_tests/charts/api_tests.py 
b/tests/integration_tests/charts/api_tests.py
index 71a0aefaa3d..048d5aa75ee 100644
--- a/tests/integration_tests/charts/api_tests.py
+++ b/tests/integration_tests/charts/api_tests.py
@@ -1103,7 +1103,6 @@ class TestChartApi(ApiEditorsTestCaseMixin, 
InsertChartMixin, SupersetTestCase):
             "viewers": [],
             "params": None,
             "slice_name": "title",
-            "tags": [],
             "viz_type": None,
             "query_context": None,
             "is_managed_externally": False,
@@ -1113,6 +1112,13 @@ class TestChartApi(ApiEditorsTestCaseMixin, 
InsertChartMixin, SupersetTestCase):
         assert "id" in data["result"]
         assert "thumbnail_url" in data["result"]
         assert "url" in data["result"]
+        # implicit tags created by the tagging system's SQLA event listeners
+        tags = data["result"].pop("tags")
+        assert len(tags) == 2
+        assert {(tag["name"], tag["type"]) for tag in tags} == {
+            ("type:chart", TagType.type.value),
+            (f"editor:{admin.id}", TagType.editor.value),
+        }
         for key, value in data["result"].items():
             # We can't assert timestamp values or id/urls
             if key not in (
diff --git a/tests/integration_tests/charts/commands_tests.py 
b/tests/integration_tests/charts/commands_tests.py
index 7a5e76b156d..40e7fbc32ea 100644
--- a/tests/integration_tests/charts/commands_tests.py
+++ b/tests/integration_tests/charts/commands_tests.py
@@ -83,6 +83,7 @@ class TestExportChartsCommand(SupersetTestCase):
             f"charts/Energy_Sankey_{example_chart.id}.yaml",
             f"datasets/examples/energy_usage_{example_chart.table.id}.yaml",
             "databases/examples.yaml",
+            "tags.yaml",
         ]
         assert expected == list(contents.keys())
 
@@ -109,6 +110,7 @@ class TestExportChartsCommand(SupersetTestCase):
             "uuid": str(example_chart.uuid),
             "version": "1.0.0",
             "query_context": None,
+            "tags": [],
         }
 
     @patch("superset.utils.core.g")
@@ -161,6 +163,7 @@ class TestExportChartsCommand(SupersetTestCase):
             "uuid",
             "version",
             "dataset_uuid",
+            "tags",
         ]
 
     @patch("superset.security.manager.g")
@@ -548,6 +551,14 @@ class TestChartsUpdateCommand(SupersetTestCase):
         chart = db.session.query(Slice).filter_by(slice_name="Energy 
Sankey").one()
         pk = chart.id
         admin = security_manager.find_user(username="admin")
+
+        # gamma has no access to the energy datasource and cannot edit the 
chart.
+        # Bind the patched `g` to a real user before the setup commit below:
+        # that commit fires the tagging listeners, whose audit columns resolve
+        # through `g.user.id` and cannot be bound as a mock.
+        gamma = security_manager.find_user(username="gamma")
+        mock_core_g.user = mock_sm_g.user = mock_update_g.user = gamma
+
         chart.editors = subjects_from_users([admin])
         db.session.commit()
 
@@ -555,9 +566,6 @@ class TestChartsUpdateCommand(SupersetTestCase):
         # own raise_for_access gate is what denies the request.
         mock_find_by_id.return_value = chart
 
-        # gamma has no access to the energy datasource and cannot edit the 
chart
-        gamma = security_manager.find_user(username="gamma")
-        mock_core_g.user = mock_sm_g.user = mock_update_g.user = gamma
         json_obj = {
             "query_context_generation": True,
             "query_context": json.dumps({"foo": "bar"}),
diff --git a/tests/integration_tests/dashboards/commands_tests.py 
b/tests/integration_tests/dashboards/commands_tests.py
index 2ed70e1e4c9..17503039034 100644
--- a/tests/integration_tests/dashboards/commands_tests.py
+++ b/tests/integration_tests/dashboards/commands_tests.py
@@ -80,6 +80,7 @@ class TestExportDashboardsCommand(SupersetTestCase):
             "metadata.yaml",
             f"dashboards/World_Banks_Data_{example_dashboard.id}.yaml",
             "databases/examples.yaml",
+            "tags.yaml",
         }
         for chart in example_dashboard.slices:
             chart_slug = secure_filename(chart.slice_name)
@@ -238,6 +239,7 @@ class TestExportDashboardsCommand(SupersetTestCase):
             "metadata": {"mock_key": "mock_value"},
             "version": "1.0.0",
             "theme_uuid": None,
+            "tags": [],
         }
 
     # @pytest.mark.usefixtures("load_covid_dashboard")
@@ -343,6 +345,7 @@ class TestExportDashboardsCommand(SupersetTestCase):
             "position",
             "metadata",
             "version",
+            "tags",
         }
 
     @pytest.mark.usefixtures("load_world_bank_dashboard_with_slices")
diff --git a/tests/integration_tests/fixtures/tags.py 
b/tests/integration_tests/fixtures/tags.py
index 8fb42701509..2158cef5456 100644
--- a/tests/integration_tests/fixtures/tags.py
+++ b/tests/integration_tests/fixtures/tags.py
@@ -18,20 +18,27 @@
 import pytest
 
 from superset import db
-from superset.tags.core import clear_sqla_event_listeners, 
register_sqla_event_listeners
+from superset.tags.core import register_sqla_event_listeners
 from superset.tags.models import Tag
 from tests.integration_tests.test_app import app
 
 
 @pytest.fixture
 def with_tagging_system_feature():
-    is_enabled = app.config["DEFAULT_FEATURE_FLAGS"]["TAGGING_SYSTEM"]
-    if not is_enabled:
-        app.config["DEFAULT_FEATURE_FLAGS"]["TAGGING_SYSTEM"] = True
-        register_sqla_event_listeners()
+    was_enabled = app.config["DEFAULT_FEATURE_FLAGS"]["TAGGING_SYSTEM"]
+    app.config["DEFAULT_FEATURE_FLAGS"]["TAGGING_SYSTEM"] = True
+    # Idempotent: SQLAlchemy keys listeners by (target, event, fn), so
+    # re-registering on top of the app-startup registration is a no-op. Calling
+    # it keeps these tests independent of whether startup registration ran (it
+    # is skipped when the metadata DB is behind on migrations).
+    register_sqla_event_listeners()
+    try:
         yield
-        app.config["DEFAULT_FEATURE_FLAGS"]["TAGGING_SYSTEM"] = False
-        clear_sqla_event_listeners()
+    finally:
+        # Only the flag is restored. The listeners are shared app-level state
+        # that startup registered unconditionally, and they check the flag when
+        # they fire, so unregistering here would break every later test.
+        app.config["DEFAULT_FEATURE_FLAGS"]["TAGGING_SYSTEM"] = was_enabled
 
 
 @pytest.fixture
diff --git a/tests/integration_tests/tags/api_tests.py 
b/tests/integration_tests/tags/api_tests.py
index 15d0ec2ec0b..d19f16d2926 100644
--- a/tests/integration_tests/tags/api_tests.py
+++ b/tests/integration_tests/tags/api_tests.py
@@ -697,7 +697,13 @@ class TestTagApi(InsertChartMixin, SupersetTestCase):
     @pytest.mark.usefixtures("create_tags")
     def test_add_tag_not_found(self):
         self.login(ADMIN_USERNAME)
-        uri = "api/v1/tag/123/favorites/"  # noqa: F541
+
+        # Pick an id that is genuinely free: the tagging system mints implicit
+        # tags as assets are saved, so no fixed id stays unused.
+        existing_ids = [tag_id for (tag_id,) in db.session.query(Tag.id).all()]
+        non_existent_id = max(existing_ids, default=0) + 1
+
+        uri = f"api/v1/tag/{non_existent_id}/favorites/"
         rv = self.client.post(uri, follow_redirects=True)
 
         assert rv.status_code == 404
diff --git a/tests/unit_tests/commands/importers/v1/assets_test.py 
b/tests/unit_tests/commands/importers/v1/assets_test.py
index 307525d1a1a..8da939dff67 100644
--- a/tests/unit_tests/commands/importers/v1/assets_test.py
+++ b/tests/unit_tests/commands/importers/v1/assets_test.py
@@ -285,6 +285,9 @@ def test_import_threads_overwrite_flag(mocker: 
MockerFixture, session: Session)
     mocker.patch.object(assets_module, "find_chart_uuids", return_value=[])
     mocker.patch.object(assets_module, "update_id_refs", side_effect=lambda c, 
*_: c)
     mocker.patch.object(assets_module, "migrate_dashboard")
+    # tag import isn't under test here; mock it out so this test doesn't
+    # depend on the TAGGING_SYSTEM feature flag default or a real tag table
+    mocker.patch.object(assets_module, "import_tag")
     mocker.patch("superset.db.session.execute")
 
     configs = {
diff --git a/tests/unit_tests/initialization_test.py 
b/tests/unit_tests/initialization_test.py
index fea1d86b32f..c995d11f6a6 100644
--- a/tests/unit_tests/initialization_test.py
+++ b/tests/unit_tests/initialization_test.py
@@ -78,20 +78,17 @@ class TestSupersetApp:
             "Pending database migrations: run 'superset db upgrade'"
         )
 
-    @patch("superset.extensions.feature_flag_manager")
     @patch("superset.app.logger")
     @patch("superset.commands.theme.seed.SeedSystemThemesCommand")
     def test_sync_config_to_db_initializes_when_tables_exist(
         self,
         mock_seed_themes_command,
         mock_logger,
-        mock_feature_flag_manager,
     ):
         """Test that features are initialized when database is up-to-date."""
         # Setup
         app = SupersetApp(__name__)
         app.config = {"SQLALCHEMY_DATABASE_URI": 
"postgresql://user:pass@host:5432/db"}
-        mock_feature_flag_manager.is_feature_enabled.return_value = True
         mock_seed_themes = MagicMock()
         mock_seed_themes_command.return_value = mock_seed_themes
 
@@ -105,10 +102,8 @@ class TestSupersetApp:
             # Execute
             app.sync_config_to_db()
 
-        # Assert
-        mock_feature_flag_manager.is_feature_enabled.assert_called_with(
-            "TAGGING_SYSTEM"
-        )
+        # Assert: tagging listeners are registered regardless of the flag; the
+        # write listeners check TAGGING_SYSTEM themselves when they fire
         mock_register_listeners.assert_called_once()
         # Should seed themes
         mock_seed_themes_command.assert_called_once()
diff --git a/tests/unit_tests/tags/commands/create_test.py 
b/tests/unit_tests/tags/commands/create_test.py
index 292e343f3fb..72509498043 100644
--- a/tests/unit_tests/tags/commands/create_test.py
+++ b/tests/unit_tests/tags/commands/create_test.py
@@ -72,7 +72,7 @@ def test_create_command_success(session_with_data: Session, 
mocker: MockerFixtur
     from superset.models.dashboard import Dashboard
     from superset.models.slice import Slice
     from superset.models.sql_lab import SavedQuery
-    from superset.tags.models import ObjectType, TaggedObject
+    from superset.tags.models import ObjectType, Tag, TaggedObject
 
     # Define a list of objects to tag
     query = db.session.query(SavedQuery).first()
@@ -95,11 +95,17 @@ def test_create_command_success(session_with_data: Session, 
mocker: MockerFixtur
         data={"name": "test_tag", "objects_to_tag": objects_to_tag}
     ).run()
 
-    assert len(db.session.query(TaggedObject).all()) == len(objects_to_tag)
+    # Scope to the tag under test: with TAGGING_SYSTEM on, saving the fixture's
+    # assets also mints implicit `type:` tags, so a global count is unstable.
+    tag = db.session.query(Tag).filter_by(name="test_tag").one()
+    assert len(db.session.query(TaggedObject).filter_by(tag_id=tag.id).all()) 
== len(
+        objects_to_tag
+    )
     for object_type, object_id in objects_to_tag:
         assert (
             db.session.query(TaggedObject)
             .filter(
+                TaggedObject.tag_id == tag.id,
                 TaggedObject.object_type == object_type,
                 TaggedObject.object_id == object_id,
             )
@@ -184,7 +190,7 @@ def test_create_command_success_clear(
     from superset.models.dashboard import Dashboard
     from superset.models.slice import Slice
     from superset.models.sql_lab import SavedQuery
-    from superset.tags.models import ObjectType, TaggedObject
+    from superset.tags.models import ObjectType, Tag, TaggedObject
 
     # Define a list of objects to tag
     query = db.session.query(SavedQuery).first()
@@ -206,10 +212,13 @@ def test_create_command_success_clear(
     CreateCustomTagWithRelationshipsCommand(
         data={"name": "test_tag", "objects_to_tag": objects_to_tag}
     ).run()
-    assert len(db.session.query(TaggedObject).all()) == len(objects_to_tag)
+    tag = db.session.query(Tag).filter_by(name="test_tag").one()
+    assert len(db.session.query(TaggedObject).filter_by(tag_id=tag.id).all()) 
== len(
+        objects_to_tag
+    )
 
     CreateCustomTagWithRelationshipsCommand(
         data={"name": "test_tag", "objects_to_tag": []}
     ).run()
 
-    assert len(db.session.query(TaggedObject).all()) == 0
+    assert len(db.session.query(TaggedObject).filter_by(tag_id=tag.id).all()) 
== 0
diff --git a/tests/unit_tests/tags/commands/update_test.py 
b/tests/unit_tests/tags/commands/update_test.py
index 0263968dd9f..de425ddfdf5 100644
--- a/tests/unit_tests/tags/commands/update_test.py
+++ b/tests/unit_tests/tags/commands/update_test.py
@@ -107,7 +107,11 @@ def test_update_command_success(session_with_data: 
Session, mocker: MockerFixtur
     updated_tag = TagDAO.find_by_name("new_name")
     assert updated_tag is not None
     assert updated_tag.description == "new_description"
-    assert len(db.session.query(TaggedObject).all()) == len(objects_to_tag)
+    # Scope to the tag under test: with TAGGING_SYSTEM on, saving the fixture's
+    # assets also mints implicit `type:` tags, so a global count is unstable.
+    assert len(
+        db.session.query(TaggedObject).filter_by(tag_id=updated_tag.id).all()
+    ) == len(objects_to_tag)
 
 
 def test_update_command_success_duplicates(
@@ -156,7 +160,9 @@ def test_update_command_success_duplicates(
     updated_tag = TagDAO.find_by_name("new_name")
     assert updated_tag is not None
     assert updated_tag.description == "new_description"
-    assert len(db.session.query(TaggedObject).all()) == len(objects_to_tag)
+    assert len(
+        db.session.query(TaggedObject).filter_by(tag_id=updated_tag.id).all()
+    ) == len(objects_to_tag)
     assert changed_model.objects[0].object_id == chart.id
 
 

Reply via email to