You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sling.apache.org by ro...@apache.org on 2026/02/20 15:40:27 UTC

(sling-org-apache-sling-installer-core) branch bugfix/SLING-13123 created (now 40b2d29)

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

royteeuwen pushed a change to branch bugfix/SLING-13123
in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-installer-core.git


      at 40b2d29  SLING-13123: Fix for when a bundle switches to a fragment to not restart it

This branch includes the following new commits:

     new 40b2d29  SLING-13123: Fix for when a bundle switches to a fragment to not restart it

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



(sling-org-apache-sling-installer-core) 01/01: SLING-13123: Fix for when a bundle switches to a fragment to not restart it

Posted by ro...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

royteeuwen pushed a commit to branch bugfix/SLING-13123
in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-installer-core.git

commit 40b2d291de94b9924075cc283cd59a80c204c1d9
Author: Roy Teeuwen <ro...@apache.org>
AuthorDate: Fri Feb 20 16:40:13 2026 +0100

    SLING-13123: Fix for when a bundle switches to a fragment to not restart it
---
 .../core/impl/tasks/RestartActiveBundlesTask.java  |  14 +-
 .../impl/tasks/RestartActiveBundlesTaskTest.java   | 524 +++++++++++++++++++++
 2 files changed, 537 insertions(+), 1 deletion(-)

diff --git a/src/main/java/org/apache/sling/installer/core/impl/tasks/RestartActiveBundlesTask.java b/src/main/java/org/apache/sling/installer/core/impl/tasks/RestartActiveBundlesTask.java
index 4efd143..da7a482 100644
--- a/src/main/java/org/apache/sling/installer/core/impl/tasks/RestartActiveBundlesTask.java
+++ b/src/main/java/org/apache/sling/installer/core/impl/tasks/RestartActiveBundlesTask.java
@@ -49,6 +49,15 @@ public class RestartActiveBundlesTask extends AbstractInstallTask {
         if (ids == null) {
             ids = new HashSet<Long>();
         }
+        final Set<Long> fragmentIds = new HashSet<Long>();
+        for (Long id : ids) {
+            Bundle bundle = support.getBundleContext().getBundle(id);
+            if (bundle != null && BundleUtil.getFragmentHostHeader(bundle) != null) {
+                getLogger().debug("Id {} switched from bundle to a fragment, removing from restart task", id);
+                fragmentIds.add(id);
+            }
+        }
+        ids.removeAll(fragmentIds);
         for (final Bundle bundle : support.getBundleContext().getBundles()) {
             if (bundle.getBundleId() > 0
                     && BundleUtil.getFragmentHostHeader(bundle) == null
@@ -68,7 +77,10 @@ public class RestartActiveBundlesTask extends AbstractInstallTask {
             final Set<Long> remove = new HashSet<Long>();
             for (final Long id : ids) {
                 final Bundle bundle = this.getBundleContext().getBundle(id);
-                if (bundle != null
+                if (bundle != null && BundleUtil.getFragmentHostHeader(bundle) != null) {
+                    getLogger().debug("Bundle {} is a fragment, skipping restart", bundle);
+                    remove.add(id);
+                } else if (bundle != null
                         && bundle.getState() != Bundle.ACTIVE
                         && bundle.getState() != Bundle.STARTING
                         && bundle.getState() != Bundle.STOPPING
diff --git a/src/test/java/org/apache/sling/installer/core/impl/tasks/RestartActiveBundlesTaskTest.java b/src/test/java/org/apache/sling/installer/core/impl/tasks/RestartActiveBundlesTaskTest.java
new file mode 100644
index 0000000..6676099
--- /dev/null
+++ b/src/test/java/org/apache/sling/installer/core/impl/tasks/RestartActiveBundlesTaskTest.java
@@ -0,0 +1,524 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.installer.core.impl.tasks;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Dictionary;
+import java.util.HashSet;
+import java.util.Hashtable;
+import java.util.List;
+import java.util.Set;
+
+import org.apache.sling.installer.api.tasks.InstallationContext;
+import org.apache.sling.installer.core.impl.EntityResourceList;
+import org.apache.sling.installer.core.impl.MockBundleContext;
+import org.apache.sling.installer.core.impl.MockBundleResource;
+import org.apache.sling.installer.core.impl.PersistentResourceList;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleException;
+import org.osgi.framework.Constants;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@RunWith(MockitoJUnitRunner.class)
+public class RestartActiveBundlesTaskTest {
+
+    private static final String ATTR_BUNDLES = "bundles";
+
+    @Mock
+    private InstallationContext installationContext;
+
+    // -------------------------------------------------------------------------
+    // Helper: build a task + its EntityResourceList using a given bundle list
+    // -------------------------------------------------------------------------
+
+    private static final String ENTITY_ID =
+            PersistentResourceList.RESTART_ACTIVE_BUNDLES_TYPE + ':' + PersistentResourceList.RESTART_ACTIVE_BUNDLES_ID;
+
+    private RestartActiveBundlesTask createTask(List<Bundle> bundles) throws IOException {
+        MockBundleContext ctx = new MockBundleContext(bundles);
+        TaskSupport support = new TaskSupport(ctx);
+
+        MockBundleResource resource = new MockBundleResource("dummy", "1.0");
+        EntityResourceList erl = new EntityResourceList(ENTITY_ID, new MockInstallationListener());
+        erl.addOrUpdate(resource.getRegisteredResourceImpl());
+
+        return new RestartActiveBundlesTask(erl, support);
+    }
+
+    /** Convenience overload for tests that supply a pre-built erl. */
+    private RestartActiveBundlesTask createTask(EntityResourceList erl, List<Bundle> bundles) {
+        MockBundleContext ctx = new MockBundleContext(bundles);
+        TaskSupport support = new TaskSupport(ctx);
+        return new RestartActiveBundlesTask(erl, support);
+    }
+
+    // -------------------------------------------------------------------------
+    // Helper: build a non-fragment, non-system-bundle mock
+    // -------------------------------------------------------------------------
+
+    private Bundle mockBundle(long id, int state) {
+        Bundle b = mock(Bundle.class);
+        when(b.getBundleId()).thenReturn(id);
+        when(b.getState()).thenReturn(state);
+        Dictionary<String, String> headers = new Hashtable<>();
+        when(b.getHeaders("")).thenReturn(headers);
+        return b;
+    }
+
+    /** Mock a fragment bundle (has Fragment-Host header). */
+    private Bundle mockFragmentBundle(long id, int state) {
+        Bundle b = mockBundle(id, state);
+        Dictionary<String, String> headers = new Hashtable<>();
+        headers.put(Constants.FRAGMENT_HOST, "some.host.bundle");
+        when(b.getHeaders("")).thenReturn(headers);
+        return b;
+    }
+
+    // =========================================================================
+    // Constructor tests
+    // =========================================================================
+
+    @Test
+    public void testConstructor_CollectsActiveBundles() throws IOException {
+        Bundle active1 = mockBundle(1L, Bundle.ACTIVE);
+        Bundle active2 = mockBundle(2L, Bundle.ACTIVE);
+
+        List<Bundle> bundles = new ArrayList<>();
+        bundles.add(active1);
+        bundles.add(active2);
+
+        MockBundleResource resource = new MockBundleResource("dummy", "1.0");
+        EntityResourceList erl = new EntityResourceList(ENTITY_ID, new MockInstallationListener());
+        erl.addOrUpdate(resource.getRegisteredResourceImpl());
+
+        MockBundleContext ctx = new MockBundleContext(bundles);
+        TaskSupport support = new TaskSupport(ctx);
+        new RestartActiveBundlesTask(erl, support);
+
+        @SuppressWarnings("unchecked")
+        Set<Long> ids = (Set<Long>) erl.getActiveResource().getAttribute(ATTR_BUNDLES);
+        assertNotNull("Bundle id set should be stored as attribute", ids);
+        assertTrue("Bundle 1 should be in set", ids.contains(1L));
+        assertTrue("Bundle 2 should be in set", ids.contains(2L));
+    }
+
+    @Test
+    public void testConstructor_SkipsSystemBundle() throws IOException {
+        // Bundle with id == 0 is the system bundle and must be skipped
+        Bundle systemBundle = mockBundle(0L, Bundle.ACTIVE);
+
+        List<Bundle> bundles = new ArrayList<>();
+        bundles.add(systemBundle);
+
+        MockBundleResource resource = new MockBundleResource("dummy", "1.0");
+        EntityResourceList erl = new EntityResourceList(ENTITY_ID, new MockInstallationListener());
+        erl.addOrUpdate(resource.getRegisteredResourceImpl());
+
+        MockBundleContext ctx = new MockBundleContext(bundles);
+        TaskSupport support = new TaskSupport(ctx);
+        new RestartActiveBundlesTask(erl, support);
+
+        @SuppressWarnings("unchecked")
+        Set<Long> ids = (Set<Long>) erl.getActiveResource().getAttribute(ATTR_BUNDLES);
+        assertNotNull(ids);
+        assertTrue("System bundle (id 0) must not be in set", ids.isEmpty());
+    }
+
+    @Test
+    public void testConstructor_SkipsFragmentBundles() throws IOException {
+        // Fragment bundles must not be added to the restart set
+        Bundle fragment = mockFragmentBundle(3L, Bundle.ACTIVE);
+
+        List<Bundle> bundles = new ArrayList<>();
+        bundles.add(fragment);
+
+        MockBundleResource resource = new MockBundleResource("dummy", "1.0");
+        EntityResourceList erl = new EntityResourceList(ENTITY_ID, new MockInstallationListener());
+        erl.addOrUpdate(resource.getRegisteredResourceImpl());
+
+        MockBundleContext ctx = new MockBundleContext(bundles);
+        TaskSupport support = new TaskSupport(ctx);
+        new RestartActiveBundlesTask(erl, support);
+
+        @SuppressWarnings("unchecked")
+        Set<Long> ids = (Set<Long>) erl.getActiveResource().getAttribute(ATTR_BUNDLES);
+        assertNotNull(ids);
+        assertTrue("Fragment bundle must not be in set", ids.isEmpty());
+    }
+
+    @Test
+    public void testConstructor_SkipsNonActiveBundles() throws IOException {
+        Bundle resolved = mockBundle(4L, Bundle.RESOLVED);
+        Bundle installed = mockBundle(5L, Bundle.INSTALLED);
+
+        List<Bundle> bundles = new ArrayList<>();
+        bundles.add(resolved);
+        bundles.add(installed);
+
+        MockBundleResource resource = new MockBundleResource("dummy", "1.0");
+        EntityResourceList erl = new EntityResourceList(ENTITY_ID, new MockInstallationListener());
+        erl.addOrUpdate(resource.getRegisteredResourceImpl());
+
+        MockBundleContext ctx = new MockBundleContext(bundles);
+        TaskSupport support = new TaskSupport(ctx);
+        new RestartActiveBundlesTask(erl, support);
+
+        @SuppressWarnings("unchecked")
+        Set<Long> ids = (Set<Long>) erl.getActiveResource().getAttribute(ATTR_BUNDLES);
+        assertNotNull(ids);
+        assertTrue("Non-active bundles must not be in set", ids.isEmpty());
+    }
+
+    @Test
+    public void testConstructor_RemovesMultipleFragmentBundlesFromExistingIds_NoConcurrentModificationException()
+            throws IOException {
+        Bundle fragment1 = mockFragmentBundle(10L, Bundle.RESOLVED);
+        Bundle fragment2 = mockFragmentBundle(11L, Bundle.RESOLVED);
+        Bundle fragment3 = mockFragmentBundle(12L, Bundle.RESOLVED);
+
+        List<Bundle> bundles = new ArrayList<>();
+        bundles.add(fragment1);
+        bundles.add(fragment2);
+        bundles.add(fragment3);
+
+        MockBundleResource resource = new MockBundleResource("dummy", "1.0");
+        EntityResourceList erl = new EntityResourceList(ENTITY_ID, new MockInstallationListener());
+        erl.addOrUpdate(resource.getRegisteredResourceImpl());
+
+        // Pre-populate all three fragment ids so the constructor must remove them all
+        Set<Long> existingIds = new HashSet<>();
+        existingIds.add(10L);
+        existingIds.add(11L);
+        existingIds.add(12L);
+        erl.getActiveResource().setAttribute(ATTR_BUNDLES, existingIds);
+
+        MockBundleContext ctx = new MockBundleContext(bundles);
+        TaskSupport support = new TaskSupport(ctx);
+
+        new RestartActiveBundlesTask(erl, support);
+
+        @SuppressWarnings("unchecked")
+        Set<Long> ids = (Set<Long>) erl.getActiveResource().getAttribute(ATTR_BUNDLES);
+        assertNotNull(ids);
+        assertTrue("Fragment 10 must be removed", !ids.contains(10L));
+        assertTrue("Fragment 11 must be removed", !ids.contains(11L));
+        assertTrue("Fragment 12 must be removed", !ids.contains(12L));
+    }
+
+    // =========================================================================
+    // execute() tests
+    // =========================================================================
+
+    @Test
+    public void testExecute_StartsResolvedBundle() throws Exception {
+        Bundle bundle = mockBundle(1L, Bundle.RESOLVED);
+
+        List<Bundle> bundles = new ArrayList<>();
+        bundles.add(bundle);
+
+        MockBundleResource resource = new MockBundleResource("dummy", "1.0");
+        EntityResourceList erl = new EntityResourceList(ENTITY_ID, new MockInstallationListener());
+        erl.addOrUpdate(resource.getRegisteredResourceImpl());
+
+        // Pre-seed ids with bundle id 1
+        Set<Long> preIds = new HashSet<>();
+        preIds.add(1L);
+        erl.getActiveResource().setAttribute(ATTR_BUNDLES, preIds);
+
+        MockBundleContext ctx = new MockBundleContext(bundles);
+        TaskSupport support = new TaskSupport(ctx);
+        RestartActiveBundlesTask task = new RestartActiveBundlesTask(erl, support);
+
+        task.execute(installationContext);
+
+        verify(bundle).start();
+    }
+
+    @Test
+    public void testExecute_DoesNotStartActiveBundle() throws Exception {
+        Bundle bundle = mockBundle(1L, Bundle.ACTIVE);
+
+        List<Bundle> bundles = new ArrayList<>();
+        bundles.add(bundle);
+
+        MockBundleResource resource = new MockBundleResource("dummy", "1.0");
+        EntityResourceList erl = new EntityResourceList(ENTITY_ID, new MockInstallationListener());
+        erl.addOrUpdate(resource.getRegisteredResourceImpl());
+
+        // Seed the ids – the bundle is already ACTIVE so execute must not call start()
+        Set<Long> preIds = new HashSet<>();
+        preIds.add(1L);
+        erl.getActiveResource().setAttribute(ATTR_BUNDLES, preIds);
+
+        MockBundleContext ctx = new MockBundleContext(bundles);
+        TaskSupport support = new TaskSupport(ctx);
+        // Constructor will also add bundle 1 because it is ACTIVE, but that is expected
+        RestartActiveBundlesTask task = new RestartActiveBundlesTask(erl, support);
+
+        task.execute(installationContext);
+
+        verify(bundle, never()).start();
+    }
+
+    @Test
+    public void testExecute_DoesNotStartStartingBundle() throws Exception {
+        Bundle bundle = mockBundle(2L, Bundle.STARTING);
+
+        List<Bundle> bundles = new ArrayList<>();
+        bundles.add(bundle);
+
+        MockBundleResource resource = new MockBundleResource("dummy", "1.0");
+        EntityResourceList erl = new EntityResourceList(ENTITY_ID, new MockInstallationListener());
+        erl.addOrUpdate(resource.getRegisteredResourceImpl());
+
+        Set<Long> preIds = new HashSet<>();
+        preIds.add(2L);
+        erl.getActiveResource().setAttribute(ATTR_BUNDLES, preIds);
+
+        MockBundleContext ctx = new MockBundleContext(bundles);
+        TaskSupport support = new TaskSupport(ctx);
+        RestartActiveBundlesTask task = new RestartActiveBundlesTask(erl, support);
+
+        task.execute(installationContext);
+
+        verify(bundle, never()).start();
+    }
+
+    @Test
+    public void testExecute_DoesNotStartStoppingBundle() throws Exception {
+        Bundle bundle = mockBundle(3L, Bundle.STOPPING);
+
+        List<Bundle> bundles = new ArrayList<>();
+        bundles.add(bundle);
+
+        MockBundleResource resource = new MockBundleResource("dummy", "1.0");
+        EntityResourceList erl = new EntityResourceList(ENTITY_ID, new MockInstallationListener());
+        erl.addOrUpdate(resource.getRegisteredResourceImpl());
+
+        Set<Long> preIds = new HashSet<>();
+        preIds.add(3L);
+        erl.getActiveResource().setAttribute(ATTR_BUNDLES, preIds);
+
+        MockBundleContext ctx = new MockBundleContext(bundles);
+        TaskSupport support = new TaskSupport(ctx);
+        RestartActiveBundlesTask task = new RestartActiveBundlesTask(erl, support);
+
+        task.execute(installationContext);
+
+        verify(bundle, never()).start();
+    }
+
+    @Test
+    public void testExecute_SkipsBundleNotFoundInContext() throws Exception {
+        // Bundle id 99 is in the set but getBundleContext().getBundle(99) returns null
+        List<Bundle> bundles = new ArrayList<>(); // empty context
+
+        MockBundleResource resource = new MockBundleResource("dummy", "1.0");
+        EntityResourceList erl = new EntityResourceList(ENTITY_ID, new MockInstallationListener());
+        erl.addOrUpdate(resource.getRegisteredResourceImpl());
+
+        MockBundleContext ctx = new MockBundleContext(bundles);
+        TaskSupport support = new TaskSupport(ctx);
+        // Construct first so the constructor runs with an empty context (no NPE)
+        RestartActiveBundlesTask task = new RestartActiveBundlesTask(erl, support);
+
+        // Inject the unknown id AFTER construction so execute() encounters it
+        @SuppressWarnings("unchecked")
+        Set<Long> ids = (Set<Long>) erl.getActiveResource().getAttribute(ATTR_BUNDLES);
+        ids.add(99L);
+
+        // Should not throw even when bundle is null
+        task.execute(installationContext);
+
+        assertTrue("Unknown bundle id should be removed from set after execute", !ids.contains(99L));
+    }
+
+    @Test
+    public void testExecute_HandlesNullAttributeGracefully() throws Exception {
+        List<Bundle> bundles = new ArrayList<>();
+
+        MockBundleResource resource = new MockBundleResource("dummy", "1.0");
+        EntityResourceList erl = new EntityResourceList(ENTITY_ID, new MockInstallationListener());
+        erl.addOrUpdate(resource.getRegisteredResourceImpl());
+
+        // Do NOT set the ATTR_BUNDLES attribute – it remains null
+        MockBundleContext ctx = new MockBundleContext(bundles);
+        TaskSupport support = new TaskSupport(ctx);
+        RestartActiveBundlesTask task = new RestartActiveBundlesTask(erl, support);
+
+        // Manually clear the attribute set by the constructor so execute sees null
+        erl.getActiveResource().setAttribute(ATTR_BUNDLES, null);
+
+        // Should complete without NPE
+        task.execute(installationContext);
+    }
+
+    @Test
+    public void testExecute_HandlesBundleException() throws Exception {
+        Bundle bundle = mockBundle(5L, Bundle.RESOLVED);
+        doThrow(new BundleException("start failed")).when(bundle).start();
+
+        List<Bundle> bundles = new ArrayList<>();
+        bundles.add(bundle);
+
+        MockBundleResource resource = new MockBundleResource("dummy", "1.0");
+        EntityResourceList erl = new EntityResourceList(ENTITY_ID, new MockInstallationListener());
+        erl.addOrUpdate(resource.getRegisteredResourceImpl());
+
+        Set<Long> preIds = new HashSet<>();
+        preIds.add(5L);
+        erl.getActiveResource().setAttribute(ATTR_BUNDLES, preIds);
+
+        MockBundleContext ctx = new MockBundleContext(bundles);
+        TaskSupport support = new TaskSupport(ctx);
+        RestartActiveBundlesTask task = new RestartActiveBundlesTask(erl, support);
+
+        // Should NOT throw; BundleException is caught and logged
+        task.execute(installationContext);
+
+        verify(bundle).start();
+        // id remains in the set because start failed
+        @SuppressWarnings("unchecked")
+        Set<Long> ids = (Set<Long>) erl.getActiveResource().getAttribute(ATTR_BUNDLES);
+        assertTrue("Id should remain in set when start throws BundleException", ids.contains(5L));
+    }
+
+    @Test
+    public void testExecute_HandlesIllegalStateException() throws Exception {
+        Bundle bundle = mockBundle(6L, Bundle.RESOLVED);
+        doThrow(new IllegalStateException("bundle uninstalled")).when(bundle).start();
+
+        List<Bundle> bundles = new ArrayList<>();
+        bundles.add(bundle);
+
+        MockBundleResource resource = new MockBundleResource("dummy", "1.0");
+        EntityResourceList erl = new EntityResourceList(ENTITY_ID, new MockInstallationListener());
+        erl.addOrUpdate(resource.getRegisteredResourceImpl());
+
+        Set<Long> preIds = new HashSet<>();
+        preIds.add(6L);
+        erl.getActiveResource().setAttribute(ATTR_BUNDLES, preIds);
+
+        MockBundleContext ctx = new MockBundleContext(bundles);
+        TaskSupport support = new TaskSupport(ctx);
+        RestartActiveBundlesTask task = new RestartActiveBundlesTask(erl, support);
+
+        // Should NOT throw; IllegalStateException is caught and the id is removed
+        task.execute(installationContext);
+
+        verify(bundle).start();
+        @SuppressWarnings("unchecked")
+        Set<Long> ids = (Set<Long>) erl.getActiveResource().getAttribute(ATTR_BUNDLES);
+        assertTrue("Id should be removed from set when start throws IllegalStateException", !ids.contains(6L));
+    }
+
+    @Test
+    public void testExecute_RemovesIdAfterSuccessfulStart() throws Exception {
+        Bundle bundle = mockBundle(7L, Bundle.RESOLVED);
+
+        List<Bundle> bundles = new ArrayList<>();
+        bundles.add(bundle);
+
+        MockBundleResource resource = new MockBundleResource("dummy", "1.0");
+        EntityResourceList erl = new EntityResourceList(ENTITY_ID, new MockInstallationListener());
+        erl.addOrUpdate(resource.getRegisteredResourceImpl());
+
+        Set<Long> preIds = new HashSet<>();
+        preIds.add(7L);
+        erl.getActiveResource().setAttribute(ATTR_BUNDLES, preIds);
+
+        MockBundleContext ctx = new MockBundleContext(bundles);
+        TaskSupport support = new TaskSupport(ctx);
+        RestartActiveBundlesTask task = new RestartActiveBundlesTask(erl, support);
+
+        task.execute(installationContext);
+
+        @SuppressWarnings("unchecked")
+        Set<Long> ids = (Set<Long>) erl.getActiveResource().getAttribute(ATTR_BUNDLES);
+        assertTrue("Successfully started bundle's id should be removed from set", !ids.contains(7L));
+    }
+
+    /**
+     * Regression test for the execute() fragment-guard fix.
+     * <p>
+     * If a bundle id that is stored in the restart set has become a fragment by
+     * the time {@code execute()} runs (e.g. persisted from a previous cycle),
+     * the old code would call {@code bundle.start()} on it, producing the
+     * "Fragment bundles can not be started" error in the logs.
+     * The fix detects fragments early and removes them silently without ever
+     * calling {@code start()}.
+     */
+    @Test
+    public void testExecute_SkipsAndRemovesFragmentBundle() throws Exception {
+        Bundle fragment = mockFragmentBundle(20L, Bundle.RESOLVED);
+
+        List<Bundle> bundles = new ArrayList<>();
+        bundles.add(fragment);
+
+        MockBundleResource resource = new MockBundleResource("dummy", "1.0");
+        EntityResourceList erl = new EntityResourceList(ENTITY_ID, new MockInstallationListener());
+        erl.addOrUpdate(resource.getRegisteredResourceImpl());
+
+        MockBundleContext ctx = new MockBundleContext(bundles);
+        TaskSupport support = new TaskSupport(ctx);
+        RestartActiveBundlesTask task = new RestartActiveBundlesTask(erl, support);
+
+        // Inject the fragment id directly so execute() encounters it even if the
+        // constructor already cleaned it out (simulates a persisted stale entry)
+        @SuppressWarnings("unchecked")
+        Set<Long> ids = (Set<Long>) erl.getActiveResource().getAttribute(ATTR_BUNDLES);
+        ids.add(20L);
+
+        task.execute(installationContext);
+
+        // start() must never be called on a fragment
+        verify(fragment, never()).start();
+        // The id must be cleaned up from the set
+        assertTrue("Fragment id must be removed from set after execute", !ids.contains(20L));
+    }
+
+    @Test
+    public void testExecute_EmptyBundleSet_NoStartCalled() throws Exception {
+        List<Bundle> bundles = new ArrayList<>();
+
+        MockBundleResource resource = new MockBundleResource("dummy", "1.0");
+        EntityResourceList erl = new EntityResourceList(ENTITY_ID, new MockInstallationListener());
+        erl.addOrUpdate(resource.getRegisteredResourceImpl());
+
+        // Empty set – constructor finds no active bundles in an empty context
+        MockBundleContext ctx = new MockBundleContext(bundles);
+        TaskSupport support = new TaskSupport(ctx);
+        RestartActiveBundlesTask task = new RestartActiveBundlesTask(erl, support);
+
+        // Should complete without any start() calls
+        task.execute(installationContext);
+    }
+}