Add TC_BRBINFO_4_1 to CI (#35940)

* Use async sleep instead of blocking one

* Keep AppServer wrapper class in chip.testing.tasks

* Fix typos

* Arguments for CI workflow

* Require dut_fsa_stdin_pipe options in CI run

* Start ICD server on CI run

* Read all attributes defined in test plan

* Wrap ICD server with IcdAppServerSubprocess

* Move app testing wrappers to chip.testing.apps

* Add TODO for fixing imports in unit test

* Restyled by isort

* Remove test from exception list

---------

Co-authored-by: Restyled.io <commits@restyled.io>
diff --git a/src/python_testing/TC_BRBINFO_4_1.py b/src/python_testing/TC_BRBINFO_4_1.py
index 32dd541..f0c194a 100644
--- a/src/python_testing/TC_BRBINFO_4_1.py
+++ b/src/python_testing/TC_BRBINFO_4_1.py
@@ -15,22 +15,44 @@
 #    limitations under the License.
 #
 
+# See https://github.com/project-chip/connectedhomeip/blob/master/docs/testing/python.md#defining-the-ci-test-arguments
+# for details about the block below.
+#
+# === BEGIN CI TEST ARGUMENTS ===
+# test-runner-runs:
+#   run1:
+#     app: examples/fabric-admin/scripts/fabric-sync-app.py
+#     app-args: --app-admin=${FABRIC_ADMIN_APP} --app-bridge=${FABRIC_BRIDGE_APP} --stdin-pipe=dut-fsa-stdin --discriminator=1234
+#     app-ready-pattern: "Successfully opened pairing window on the device"
+#     script-args: >
+#       --PICS src/app/tests/suites/certification/ci-pics-values
+#       --storage-path admin_storage.json
+#       --commissioning-method on-network
+#       --discriminator 1234
+#       --passcode 20202021
+#       --string-arg th_icd_server_app_path:${LIT_ICD_APP} dut_fsa_stdin_pipe:dut-fsa-stdin
+#       --trace-to json:${TRACE_TEST_JSON}.json
+#       --trace-to perfetto:${TRACE_TEST_PERFETTO}.perfetto
+#     factoryreset: true
+#     quiet: true
+# === END CI TEST ARGUMENTS ===
+
 # This test requires a TH_ICD_SERVER application. Please specify with --string-arg th_icd_server_app_path:<path_to_app>
 # TH_ICD_SERVER must support following arguments: --secured-device-port --discriminator --passcode --KVS
 # E.g: python3 src/python_testing/TC_BRBINFO_4_1.py --commissioning-method on-network --qr-code MT:-24J042C00KA0648G00 \
 #      --string-arg th_icd_server_app_path:out/linux-x64-lit-icd/lit-icd-app
 
+import asyncio
 import logging
 import os
 import queue
-import signal
-import subprocess
-import time
-import uuid
+import random
+import tempfile
 
 import chip.clusters as Clusters
 from chip import ChipDeviceCtrl
 from chip.interaction_model import InteractionModelError, Status
+from chip.testing.apps import IcdAppServerSubprocess
 from matter_testing_support import MatterBaseTest, SimpleEventCallback, TestStep, async_test_body, default_matter_test_main
 from mobly import asserts
 
@@ -40,13 +62,6 @@
 
 class TC_BRBINFO_4_1(MatterBaseTest):
 
-    #
-    # Class Helper functions
-    #
-
-    async def _read_attribute_expect_success(self, endpoint, cluster, attribute, node_id):
-        return await self.read_single_attribute_check_success(endpoint=endpoint, cluster=cluster, attribute=attribute, node_id=node_id)
-
     # This test has some manual steps and also multiple sleeps >= 30 seconds. Test typically runs under 3 mins,
     # so 6 minutes is more than enough.
     @property
@@ -58,7 +73,7 @@
         return "[TC_BRBINFO_4_1] Verification of KeepActive Command [DUT-Server]"
 
     def steps_TC_BRBINFO_4_1(self) -> list[TestStep]:
-        steps = [
+        return [
             TestStep("0",  "DUT commissioned and preconditions", is_commissioning=True),
             TestStep("1", "TH reads from the ICD the A_IDLE_MODE_DURATION, A_ACTIVE_MODE_DURATION, and ACTIVE_MODE_THRESHOLD attributes"),
             TestStep("2", "Setting up subscribe to ActiveChange event"),
@@ -77,16 +92,16 @@
             TestStep("15", "Send KeepActive command with shortest TimeoutMs value while TH_ICD is prevented from sending check-ins"),
             TestStep("16", "Wait 15 seconds then send second KeepActive command with double the TimeoutMs value of the previous step"),
             TestStep("17", "TH allows TH_ICD to resume sending check-ins after timeout from step 15 expired but before second timeout from step 16 still valid"),
-            TestStep("18", "Wait for TH_ICD to check into TH, then confirm we have received new event from DUT")]
-        return steps
+            TestStep("18", "Wait for TH_ICD to check into TH, then confirm we have received new event from DUT"),
+        ]
 
-    def _ask_for_vendor_commissioniong_ux_operation(self, discriminator, setupPinCode, setupManualCode, setupQRCode):
+    def _ask_for_vendor_commissioning_ux_operation(self, discriminator, setupPinCode, setupManualCode, setupQRCode):
         self.wait_for_user_input(
             prompt_msg=f"Using the DUT vendor's provided interface, commission the ICD device using the following parameters:\n"
             f"- discriminator: {discriminator}\n"
             f"- setupPinCode: {setupPinCode}\n"
             f"- setupQRCode: {setupQRCode}\n"
-            f"- setupManualcode: {setupManualCode}\n"
+            f"- setupManualCode: {setupManualCode}\n"
             f"If using FabricSync Admin test app, you may type:\n"
             f">>> pairing onnetwork 111 {setupPinCode} --icd-registration true")
 
@@ -117,81 +132,88 @@
 
     @async_test_body
     async def setup_class(self):
+        super().setup_class()
+
         # These steps are not explicitly, but they help identify the dynamically added endpoint
         # The second part of this process happens on _get_dynamic_endpoint()
-        root_part_list = await self.read_single_attribute_check_success(cluster=Clusters.Descriptor, attribute=Clusters.Descriptor.Attributes.PartsList, endpoint=_ROOT_ENDPOINT_ID)
+        root_part_list = await self.read_single_attribute_check_success(
+            cluster=Clusters.Descriptor,
+            attribute=Clusters.Descriptor.Attributes.PartsList,
+            endpoint=_ROOT_ENDPOINT_ID)
         self.set_of_dut_endpoints_before_adding_device = set(root_part_list)
 
-        super().setup_class()
         self._active_change_event_subscription = None
-        self.app_process = None
-        self.app_process_paused = False
-        app = self.user_params.get("th_icd_server_app_path", None)
-        if not app:
+        self.th_icd_server = None
+        self.storage = None
+
+        th_icd_server_app = self.user_params.get("th_icd_server_app_path", None)
+        if not th_icd_server_app:
             asserts.fail('This test requires a TH_ICD_SERVER app. Specify app path with --string-arg th_icd_server_app_path:<path_to_app>')
+        if not os.path.exists(th_icd_server_app):
+            asserts.fail(f'The path {th_icd_server_app} does not exist')
 
-        self.kvs = f'kvs_{str(uuid.uuid4())}'
-        discriminator = 3850
-        passcode = 20202021
-        cmd = [app]
-        cmd.extend(['--secured-device-port', str(5543)])
-        cmd.extend(['--discriminator', str(discriminator)])
-        cmd.extend(['--passcode', str(passcode)])
-        cmd.extend(['--KVS', self.kvs])
+        # Create a temporary storage directory for keeping KVS files.
+        self.storage = tempfile.TemporaryDirectory(prefix=self.__class__.__name__)
+        logging.info("Temporary storage directory: %s", self.storage.name)
 
-        logging.info("Starting ICD Server App")
-        self.app_process = subprocess.Popen(cmd)
-        logging.info("ICD started")
-        time.sleep(3)
+        if self.is_pics_sdk_ci_only:
+            # Get the named pipe path for the DUT_FSA app input from the user params.
+            dut_fsa_stdin_pipe = self.user_params.get("dut_fsa_stdin_pipe")
+            if not dut_fsa_stdin_pipe:
+                asserts.fail("CI setup requires --string-arg dut_fsa_stdin_pipe:<path_to_pipe>")
+            self.dut_fsa_stdin = open(dut_fsa_stdin_pipe, "w")
+
+        self.th_icd_server_port = 5543
+        self.th_icd_server_discriminator = random.randint(0, 4095)
+        self.th_icd_server_passcode = 20202021
+
+        # Start the TH_ICD_SERVER app.
+        self.th_icd_server = IcdAppServerSubprocess(
+            th_icd_server_app,
+            storage_dir=self.storage.name,
+            port=self.th_icd_server_port,
+            discriminator=self.th_icd_server_discriminator,
+            passcode=self.th_icd_server_passcode)
+        self.th_icd_server.start(
+            expected_output="Server initialization complete",
+            timeout=30)
 
         logging.info("Commissioning of ICD to fabric one (TH)")
         self.icd_nodeid = 1111
 
         self.default_controller.EnableICDRegistration(self.default_controller.GenerateICDRegistrationParameters())
-        await self.default_controller.CommissionOnNetwork(nodeId=self.icd_nodeid, setupPinCode=passcode, filterType=ChipDeviceCtrl.DiscoveryFilterType.LONG_DISCRIMINATOR, filter=discriminator)
+        await self.default_controller.CommissionOnNetwork(
+            nodeId=self.icd_nodeid,
+            setupPinCode=self.th_icd_server_passcode,
+            filterType=ChipDeviceCtrl.DiscoveryFilterType.LONG_DISCRIMINATOR,
+            filter=self.th_icd_server_discriminator)
 
         logging.info("Commissioning of ICD to fabric two (DUT)")
         params = await self.openCommissioningWindow(dev_ctrl=self.default_controller, node_id=self.icd_nodeid)
 
-        self._ask_for_vendor_commissioniong_ux_operation(params.randomDiscriminator, params.commissioningParameters.setupPinCode,
-                                                         params.commissioningParameters.setupManualCode, params.commissioningParameters.setupQRCode)
+        if not self.is_pics_sdk_ci_only:
+            self._ask_for_vendor_commissioning_ux_operation(
+                params.randomDiscriminator,
+                params.commissioningParameters.setupPinCode,
+                params.commissioningParameters.setupManualCode,
+                params.commissioningParameters.setupQRCode)
+        else:
+            self.dut_fsa_stdin.write(
+                f"pairing onnetwork 2 {params.commissioningParameters.setupPinCode} --icd-registration true\n")
+            self.dut_fsa_stdin.flush()
+            # Wait for the commissioning to complete.
+            await asyncio.sleep(5)
 
     def teardown_class(self):
         if self._active_change_event_subscription is not None:
             self._active_change_event_subscription.Shutdown()
             self._active_change_event_subscription = None
-
-        # In case the th_icd_server_app_path does not exist, then we failed the test
-        # and there is nothing to remove
-        if self.app_process is not None:
-            self.resume_th_icd_server(check_state=False)
-            logging.warning("Stopping app with SIGTERM")
-            self.app_process.send_signal(signal.SIGTERM.value)
-            self.app_process.wait()
-
-            if os.path.exists(self.kvs):
-                os.remove(self.kvs)
-
+        if self.th_icd_server is not None:
+            self.th_icd_server.terminate()
+        if self.storage is not None:
+            self.storage.cleanup()
         super().teardown_class()
 
-    def pause_th_icd_server(self, check_state):
-        if check_state:
-            asserts.assert_false(self.app_process_paused, "ICD TH Server unexpectedly is already paused")
-        if self.app_process_paused:
-            return
-        # stops (halts) the ICD server process by sending a SIGTOP signal
-        self.app_process.send_signal(signal.SIGSTOP.value)
-        self.app_process_paused = True
-
-    def resume_th_icd_server(self, check_state):
-        if check_state:
-            asserts.assert_true(self.app_process_paused, "ICD TH Server unexpectedly is already running")
-        if not self.app_process_paused:
-            return
-        # resumes (continues) the ICD server process by sending a SIGCONT signal
-        self.app_process.send_signal(signal.SIGCONT.value)
-        self.app_process_paused = False
-
     #
     # BRBINFO 4.1 Test Body
     #
@@ -210,34 +232,42 @@
         self.step("0")
         logging.info("Ensuring DUT is commissioned to TH")
 
-        # Confirms commissioning of DUT on TH as it reads its fature map
-        await self._read_attribute_expect_success(
-            _ROOT_ENDPOINT_ID,
-            basic_info_cluster,
-            basic_info_attributes.FeatureMap,
-            self.dut_node_id
+        # Confirms commissioning of DUT on TH as it reads its feature map
+        await self.read_single_attribute_check_success(
+            endpoint=_ROOT_ENDPOINT_ID,
+            cluster=basic_info_cluster,
+            attribute=basic_info_attributes.FeatureMap,
+            node_id=self.dut_node_id,
         )
 
         logging.info("Ensuring ICD is commissioned to TH")
 
         self.step("1")
 
-        idle_mode_duration_s = await self._read_attribute_expect_success(
-            _ROOT_ENDPOINT_ID,
-            icdm_cluster,
-            icdm_attributes.IdleModeDuration,
-            self.icd_nodeid
+        idle_mode_duration_s = await self.read_single_attribute_check_success(
+            endpoint=_ROOT_ENDPOINT_ID,
+            cluster=icdm_cluster,
+            attribute=icdm_attributes.IdleModeDuration,
+            node_id=self.icd_nodeid,
         )
         logging.info(f"IdleModeDurationS: {idle_mode_duration_s}")
 
-        active_mode_duration_ms = await self._read_attribute_expect_success(
-            _ROOT_ENDPOINT_ID,
-            icdm_cluster,
-            icdm_attributes.ActiveModeDuration,
-            self.icd_nodeid
+        active_mode_duration_ms = await self.read_single_attribute_check_success(
+            endpoint=_ROOT_ENDPOINT_ID,
+            cluster=icdm_cluster,
+            attribute=icdm_attributes.ActiveModeDuration,
+            node_id=self.icd_nodeid,
         )
         logging.info(f"ActiveModeDurationMs: {active_mode_duration_ms}")
 
+        active_mode_threshold_ms = await self.read_single_attribute_check_success(
+            endpoint=_ROOT_ENDPOINT_ID,
+            cluster=icdm_cluster,
+            attribute=icdm_attributes.ActiveModeThreshold,
+            node_id=self.icd_nodeid,
+        )
+        logging.info(f"ActiveModeThresholdMs: {active_mode_threshold_ms}")
+
         self.step("2")
         event = brb_info_cluster.Events.ActiveChanged
         self.q = queue.Queue()
@@ -292,7 +322,7 @@
         asserts.assert_equal(self.q.qsize(), 0, "Unexpected event received from DUT")
 
         self.step("9")
-        self.pause_th_icd_server(check_state=True)
+        self.th_icd_server.pause()
         # sends 3x keep active commands
         stay_active_duration_ms = 2000
         keep_active_timeout_ms = 60000
@@ -304,7 +334,7 @@
         await self._send_keep_active_command(stay_active_duration_ms, keep_active_timeout_ms, dynamic_endpoint_id)
 
         self.step("10")
-        self.resume_th_icd_server(check_state=True)
+        self.th_icd_server.resume()
         await self.default_controller.WaitForActive(self.icd_nodeid, timeoutSeconds=wait_for_icd_checkin_timeout_s, stayActiveDurationMs=5000)
         promised_active_duration_ms = await self._wait_for_active_changed_event(timeout_s=wait_for_dut_event_subscription_s)
         asserts.assert_equal(self.q.qsize(), 0, "More than one event received from DUT")
@@ -314,14 +344,14 @@
         asserts.assert_equal(self.q.qsize(), 0, "More than one event received from DUT")
 
         self.step("12")
-        self.pause_th_icd_server(check_state=True)
+        self.th_icd_server.pause()
         stay_active_duration_ms = 2000
         keep_active_timeout_ms = 30000
         await self._send_keep_active_command(stay_active_duration_ms, keep_active_timeout_ms, dynamic_endpoint_id)
 
         self.step("13")
-        time.sleep(30)
-        self.resume_th_icd_server(check_state=True)
+        await asyncio.sleep(30)
+        self.th_icd_server.resume()
 
         self.step("14")
         await self.default_controller.WaitForActive(self.icd_nodeid, timeoutSeconds=wait_for_icd_checkin_timeout_s, stayActiveDurationMs=5000)
@@ -329,20 +359,20 @@
         asserts.assert_equal(self.q.qsize(), 0, "Unexpected event received from DUT")
 
         self.step("15")
-        self.pause_th_icd_server(check_state=True)
+        self.th_icd_server.pause()
         stay_active_duration_ms = 2000
         keep_active_timeout_ms = 30000
         await self._send_keep_active_command(stay_active_duration_ms, keep_active_timeout_ms, dynamic_endpoint_id)
 
         self.step("16")
-        time.sleep(15)
+        await asyncio.sleep(15)
         stay_active_duration_ms = 2000
         keep_active_timeout_ms = 60000
         await self._send_keep_active_command(stay_active_duration_ms, keep_active_timeout_ms, dynamic_endpoint_id)
 
         self.step("17")
-        time.sleep(15)
-        self.resume_th_icd_server(check_state=True)
+        await asyncio.sleep(15)
+        self.th_icd_server.resume()
 
         self.step("18")
         await self.default_controller.WaitForActive(self.icd_nodeid, timeoutSeconds=wait_for_icd_checkin_timeout_s, stayActiveDurationMs=5000)
diff --git a/src/python_testing/TC_CCTRL_2_2.py b/src/python_testing/TC_CCTRL_2_2.py
index 4b6f800..ea2918f 100644
--- a/src/python_testing/TC_CCTRL_2_2.py
+++ b/src/python_testing/TC_CCTRL_2_2.py
@@ -49,10 +49,10 @@
 import chip.clusters as Clusters
 from chip import ChipDeviceCtrl
 from chip.interaction_model import InteractionModelError, Status
+from chip.testing.apps import AppServerSubprocess
 from matter_testing_support import (MatterBaseTest, TestStep, async_test_body, default_matter_test_main, has_cluster,
                                     run_if_endpoint_matches)
 from mobly import asserts
-from TC_MCORE_FS_1_1 import AppServer
 
 
 class TC_CCTRL_2_2(MatterBaseTest):
@@ -79,13 +79,15 @@
         self.th_server_passcode = 20202021
 
         # Start the TH_SERVER app.
-        self.th_server = AppServer(
+        self.th_server = AppServerSubprocess(
             th_server_app,
             storage_dir=self.storage.name,
             port=self.th_server_port,
             discriminator=self.th_server_discriminator,
             passcode=self.th_server_passcode)
-        self.th_server.start()
+        self.th_server.start(
+            expected_output="Server initialization complete",
+            timeout=30)
 
         logging.info("Commissioning from separate fabric")
 
diff --git a/src/python_testing/TC_CCTRL_2_3.py b/src/python_testing/TC_CCTRL_2_3.py
index 15f7304..c5ccaa8 100644
--- a/src/python_testing/TC_CCTRL_2_3.py
+++ b/src/python_testing/TC_CCTRL_2_3.py
@@ -49,10 +49,10 @@
 import chip.clusters as Clusters
 from chip import ChipDeviceCtrl
 from chip.interaction_model import InteractionModelError, Status
+from chip.testing.apps import AppServerSubprocess
 from matter_testing_support import (MatterBaseTest, TestStep, async_test_body, default_matter_test_main, has_cluster,
                                     run_if_endpoint_matches)
 from mobly import asserts
-from TC_MCORE_FS_1_1 import AppServer
 
 
 class TC_CCTRL_2_3(MatterBaseTest):
@@ -79,13 +79,15 @@
         self.th_server_passcode = 20202021
 
         # Start the TH_SERVER app.
-        self.th_server = AppServer(
+        self.th_server = AppServerSubprocess(
             th_server_app,
             storage_dir=self.storage.name,
             port=self.th_server_port,
             discriminator=self.th_server_discriminator,
             passcode=self.th_server_passcode)
-        self.th_server.start()
+        self.th_server.start(
+            expected_output="Server initialization complete",
+            timeout=30)
 
         logging.info("Commissioning from separate fabric")
 
diff --git a/src/python_testing/TC_ECOINFO_2_1.py b/src/python_testing/TC_ECOINFO_2_1.py
index f3f22bb..cd966e4 100644
--- a/src/python_testing/TC_ECOINFO_2_1.py
+++ b/src/python_testing/TC_ECOINFO_2_1.py
@@ -46,10 +46,10 @@
 import chip.clusters as Clusters
 from chip.clusters.Types import NullValue
 from chip.interaction_model import Status
+from chip.testing.apps import AppServerSubprocess
 from chip.tlv import uint
 from matter_testing_support import MatterBaseTest, TestStep, async_test_body, default_matter_test_main, type_matches
 from mobly import asserts
-from TC_MCORE_FS_1_1 import AppServer
 
 
 class TC_ECOINFO_2_1(MatterBaseTest):
@@ -95,13 +95,15 @@
         self.th_server_passcode = 20202021
 
         # Start the server app.
-        self.th_server = AppServer(
+        self.th_server = AppServerSubprocess(
             th_server_app,
             storage_dir=self.storage.name,
             port=self.th_server_port,
             discriminator=self.th_server_discriminator,
             passcode=self.th_server_passcode)
-        self.th_server.start()
+        self.th_server.start(
+            expected_output="Server initialization complete",
+            timeout=30)
 
         # Add some server to the DUT_FSA's Aggregator/Bridge.
         self.dut_fsa_stdin.write(f"pairing onnetwork 2 {self.th_server_passcode}\n")
diff --git a/src/python_testing/TC_ECOINFO_2_2.py b/src/python_testing/TC_ECOINFO_2_2.py
index 96fa2cd..41d7fc0 100644
--- a/src/python_testing/TC_ECOINFO_2_2.py
+++ b/src/python_testing/TC_ECOINFO_2_2.py
@@ -45,9 +45,9 @@
 
 import chip.clusters as Clusters
 from chip.interaction_model import Status
+from chip.testing.apps import AppServerSubprocess
 from matter_testing_support import MatterBaseTest, TestStep, async_test_body, default_matter_test_main
 from mobly import asserts
-from TC_MCORE_FS_1_1 import AppServer
 
 _DEVICE_TYPE_AGGREGGATOR = 0x000E
 
@@ -94,13 +94,15 @@
         self.th_server_passcode = 20202021
 
         # Start the server app.
-        self.th_server = AppServer(
+        self.th_server = AppServerSubprocess(
             th_server_app,
             storage_dir=self.storage.name,
             port=self.th_server_port,
             discriminator=self.th_server_discriminator,
             passcode=self.th_server_passcode)
-        self.th_server.start()
+        self.th_server.start(
+            expected_output="Server initialization complete",
+            timeout=30)
 
     def steps_TC_ECOINFO_2_2(self) -> list[TestStep]:
         return [
diff --git a/src/python_testing/TC_MCORE_FS_1_1.py b/src/python_testing/TC_MCORE_FS_1_1.py
index 8e43d61..c30c1ec 100755
--- a/src/python_testing/TC_MCORE_FS_1_1.py
+++ b/src/python_testing/TC_MCORE_FS_1_1.py
@@ -47,31 +47,11 @@
 
 import chip.clusters as Clusters
 from chip import ChipDeviceCtrl
-from chip.testing.tasks import Subprocess
+from chip.testing.apps import AppServerSubprocess
 from matter_testing_support import MatterBaseTest, TestStep, async_test_body, default_matter_test_main
 from mobly import asserts
 
 
-class AppServer(Subprocess):
-    """Wrapper class for starting an application server in a subprocess."""
-
-    # Prefix for log messages from the application server.
-    PREFIX = b"[SERVER]"
-
-    def __init__(self, app: str, storage_dir: str, discriminator: int, passcode: int, port: int = 5540):
-        storage_kvs_dir = tempfile.mkstemp(dir=storage_dir, prefix="kvs-app-")[1]
-        # Start the server application with dedicated KVS storage.
-        super().__init__(app, "--KVS", storage_kvs_dir,
-                         '--secured-device-port', str(port),
-                         "--discriminator", str(discriminator),
-                         "--passcode", str(passcode),
-                         output_cb=lambda line, is_stderr: self.PREFIX + line)
-
-    def start(self):
-        # Start process and block until it prints the expected output.
-        super().start(expected_output="Server initialization complete")
-
-
 class TC_MCORE_FS_1_1(MatterBaseTest):
 
     @async_test_body
@@ -96,13 +76,15 @@
         self.th_server_passcode = 20202021
 
         # Start the TH_SERVER app.
-        self.th_server = AppServer(
+        self.th_server = AppServerSubprocess(
             th_server_app,
             storage_dir=self.storage.name,
             port=self.th_server_port,
             discriminator=self.th_server_discriminator,
             passcode=self.th_server_passcode)
-        self.th_server.start()
+        self.th_server.start(
+            expected_output="Server initialization complete",
+            timeout=30)
 
         logging.info("Commissioning from separate fabric")
         # Create a second controller on a new fabric to communicate to the server
diff --git a/src/python_testing/TC_MCORE_FS_1_2.py b/src/python_testing/TC_MCORE_FS_1_2.py
index 6cd1c85..f7e8887 100644
--- a/src/python_testing/TC_MCORE_FS_1_2.py
+++ b/src/python_testing/TC_MCORE_FS_1_2.py
@@ -50,10 +50,10 @@
 
 import chip.clusters as Clusters
 from chip import ChipDeviceCtrl
+from chip.testing.apps import AppServerSubprocess
 from ecdsa.curves import NIST256p
 from matter_testing_support import MatterBaseTest, TestStep, async_test_body, default_matter_test_main, type_matches
 from mobly import asserts
-from TC_MCORE_FS_1_1 import AppServer
 from TC_SC_3_6 import AttributeChangeAccumulator
 
 # Length of `w0s` and `w1s` elements
@@ -97,9 +97,11 @@
         self.storage = tempfile.TemporaryDirectory(prefix=self.__class__.__name__)
         logging.info("Temporary storage directory: %s", self.storage.name)
 
-        # Get the named pipe path for the DUT_FSA app input from the user params.
-        dut_fsa_stdin_pipe = self.user_params.get("dut_fsa_stdin_pipe", None)
-        if dut_fsa_stdin_pipe is not None:
+        if self.is_pics_sdk_ci_only:
+            # Get the named pipe path for the DUT_FSA app input from the user params.
+            dut_fsa_stdin_pipe = self.user_params.get("dut_fsa_stdin_pipe")
+            if not dut_fsa_stdin_pipe:
+                asserts.fail("CI setup requires --string-arg dut_fsa_stdin_pipe:<path_to_pipe>")
             self.dut_fsa_stdin = open(dut_fsa_stdin_pipe, "w")
 
         self.th_server_port = th_server_port
@@ -111,13 +113,15 @@
             passcode=20202021)
 
         # Start the TH_SERVER app.
-        self.th_server = AppServer(
+        self.th_server = AppServerSubprocess(
             th_server_app,
             storage_dir=self.storage.name,
             port=self.th_server_port,
             discriminator=self.th_server_setup_params.discriminator,
             passcode=self.th_server_setup_params.passcode)
-        self.th_server.start()
+        self.th_server.start(
+            expected_output="Server initialization complete",
+            timeout=30)
 
     def teardown_class(self):
         if self._partslist_subscription is not None:
@@ -135,7 +139,7 @@
             f"- discriminator: {setup_params.discriminator}\n"
             f"- setupPinCode: {setup_params.passcode}\n"
             f"- setupQRCode: {setup_params.setup_qr_code}\n"
-            f"- setupManualcode: {setup_params.manual_code}\n"
+            f"- setupManualCode: {setup_params.manual_code}\n"
             f"If using FabricSync Admin test app, you may type:\n"
             f">>> pairing onnetwork 111 {setup_params.passcode}")
 
diff --git a/src/python_testing/TC_MCORE_FS_1_3.py b/src/python_testing/TC_MCORE_FS_1_3.py
index 49dc893..7dcca36 100644
--- a/src/python_testing/TC_MCORE_FS_1_3.py
+++ b/src/python_testing/TC_MCORE_FS_1_3.py
@@ -50,9 +50,9 @@
 import chip.clusters as Clusters
 from chip import ChipDeviceCtrl
 from chip.interaction_model import Status
+from chip.testing.apps import AppServerSubprocess
 from matter_testing_support import MatterBaseTest, TestStep, async_test_body, default_matter_test_main, type_matches
 from mobly import asserts
-from TC_MCORE_FS_1_1 import AppServer
 
 
 class TC_MCORE_FS_1_3(MatterBaseTest):
@@ -84,13 +84,15 @@
         self.th_server_passcode = 20202021
 
         # Start the TH_SERVER_NO_UID app.
-        self.th_server = AppServer(
+        self.th_server = AppServerSubprocess(
             th_server_app,
             storage_dir=self.storage.name,
             port=self.th_server_port,
             discriminator=self.th_server_discriminator,
             passcode=self.th_server_passcode)
-        self.th_server.start()
+        self.th_server.start(
+            expected_output="Server initialization complete",
+            timeout=30)
 
     def teardown_class(self):
         if self.th_server is not None:
diff --git a/src/python_testing/TC_MCORE_FS_1_4.py b/src/python_testing/TC_MCORE_FS_1_4.py
index c365b4e..90d1960 100644
--- a/src/python_testing/TC_MCORE_FS_1_4.py
+++ b/src/python_testing/TC_MCORE_FS_1_4.py
@@ -49,10 +49,10 @@
 import chip.clusters as Clusters
 from chip import ChipDeviceCtrl
 from chip.interaction_model import Status
+from chip.testing.apps import AppServerSubprocess
 from chip.testing.tasks import Subprocess
 from matter_testing_support import MatterBaseTest, TestStep, async_test_body, default_matter_test_main, type_matches
 from mobly import asserts
-from TC_MCORE_FS_1_1 import AppServer
 
 
 class FabricSyncApp(Subprocess):
@@ -160,9 +160,11 @@
             vendor_id=0xFFF1)
         self.th_fsa_controller.start()
 
-        # Get the named pipe path for the DUT_FSA app input from the user params.
-        dut_fsa_stdin_pipe = self.user_params.get("dut_fsa_stdin_pipe", None)
-        if dut_fsa_stdin_pipe is not None:
+        if self.is_pics_sdk_ci_only:
+            # Get the named pipe path for the DUT_FSA app input from the user params.
+            dut_fsa_stdin_pipe = self.user_params.get("dut_fsa_stdin_pipe")
+            if not dut_fsa_stdin_pipe:
+                asserts.fail("CI setup requires --string-arg dut_fsa_stdin_pipe:<path_to_pipe>")
             self.dut_fsa_stdin = open(dut_fsa_stdin_pipe, "w")
 
         self.th_server_port = 5544
@@ -170,13 +172,15 @@
         self.th_server_passcode = 20202022
 
         # Start the TH_SERVER_NO_UID app.
-        self.th_server = AppServer(
+        self.th_server = AppServerSubprocess(
             th_server_app,
             storage_dir=self.storage.name,
             port=self.th_server_port,
             discriminator=self.th_server_discriminator,
             passcode=self.th_server_passcode)
-        self.th_server.start()
+        self.th_server.start(
+            expected_output="Server initialization complete",
+            timeout=30)
 
     def teardown_class(self):
         if self.th_fsa_controller is not None:
diff --git a/src/python_testing/TC_MCORE_FS_1_5.py b/src/python_testing/TC_MCORE_FS_1_5.py
index d4f408a..9b7e32b 100755
--- a/src/python_testing/TC_MCORE_FS_1_5.py
+++ b/src/python_testing/TC_MCORE_FS_1_5.py
@@ -50,10 +50,10 @@
 
 import chip.clusters as Clusters
 from chip import ChipDeviceCtrl
+from chip.testing.apps import AppServerSubprocess
 from ecdsa.curves import NIST256p
 from matter_testing_support import MatterBaseTest, TestStep, async_test_body, default_matter_test_main, type_matches
 from mobly import asserts
-from TC_MCORE_FS_1_1 import AppServer
 from TC_SC_3_6 import AttributeChangeAccumulator
 
 # Length of `w0s` and `w1s` elements
@@ -98,9 +98,11 @@
         self.storage = tempfile.TemporaryDirectory(prefix=self.__class__.__name__)
         logging.info("Temporary storage directory: %s", self.storage.name)
 
-        # Get the named pipe path for the DUT_FSA app input from the user params.
-        dut_fsa_stdin_pipe = self.user_params.get("dut_fsa_stdin_pipe", None)
-        if dut_fsa_stdin_pipe is not None:
+        if self.is_pics_sdk_ci_only:
+            # Get the named pipe path for the DUT_FSA app input from the user params.
+            dut_fsa_stdin_pipe = self.user_params.get("dut_fsa_stdin_pipe")
+            if not dut_fsa_stdin_pipe:
+                asserts.fail("CI setup requires --string-arg dut_fsa_stdin_pipe:<path_to_pipe>")
             self.dut_fsa_stdin = open(dut_fsa_stdin_pipe, "w")
 
         self.th_server_port = th_server_port
@@ -112,13 +114,15 @@
             passcode=20202021)
 
         # Start the TH_SERVER app.
-        self.th_server = AppServer(
+        self.th_server = AppServerSubprocess(
             th_server_app,
             storage_dir=self.storage.name,
             port=self.th_server_port,
             discriminator=self.th_server_setup_params.discriminator,
             passcode=self.th_server_setup_params.passcode)
-        self.th_server.start()
+        self.th_server.start(
+            expected_output="Server initialization complete",
+            timeout=30)
 
     def teardown_class(self):
         if self._partslist_subscription is not None:
@@ -139,7 +143,7 @@
             f"- discriminator: {setup_params.discriminator}\n"
             f"- setupPinCode: {setup_params.passcode}\n"
             f"- setupQRCode: {setup_params.setup_qr_code}\n"
-            f"- setupManualcode: {setup_params.manual_code}\n"
+            f"- setupManualCode: {setup_params.manual_code}\n"
             f"If using FabricSync Admin test app, you may type:\n"
             f">>> pairing onnetwork 111 {setup_params.passcode}")
 
diff --git a/src/python_testing/execute_python_tests.py b/src/python_testing/execute_python_tests.py
index f316d49..4e67821 100644
--- a/src/python_testing/execute_python_tests.py
+++ b/src/python_testing/execute_python_tests.py
@@ -66,7 +66,6 @@
         "TC_TMP_2_1.py",  # src/python_testing/test_testing/test_TC_TMP_2_1.py is the Unit test of this test
         "TC_OCC_3_1.py",  # There are CI issues for the test cases that implements manually controlling sensor device for the occupancy state ON/OFF change
         "TC_OCC_3_2.py",  # There are CI issues for the test cases that implements manually controlling sensor device for the occupancy state ON/OFF change
-        "TC_BRBINFO_4_1.py",  # This test requires a TH_ICD_SERVER application, hence not ready run with CI
         "TestCommissioningTimeSync.py",  # Code/Test not being used or not shared code for any other tests
         "TestConformanceSupport.py",  # Unit test - does not run against an app
         "TestChoiceConformanceSupport.py",  # Unit test - does not run against an app
diff --git a/src/python_testing/matter_testing_infrastructure/BUILD.gn b/src/python_testing/matter_testing_infrastructure/BUILD.gn
index c8d54fb..41bbcef 100644
--- a/src/python_testing/matter_testing_infrastructure/BUILD.gn
+++ b/src/python_testing/matter_testing_infrastructure/BUILD.gn
@@ -30,6 +30,7 @@
 
   sources = [
     "chip/testing/__init__.py",
+    "chip/testing/apps.py",
     "chip/testing/metadata.py",
     "chip/testing/tasks.py",
   ]
diff --git a/src/python_testing/matter_testing_infrastructure/chip/testing/apps.py b/src/python_testing/matter_testing_infrastructure/chip/testing/apps.py
new file mode 100644
index 0000000..af56efc
--- /dev/null
+++ b/src/python_testing/matter_testing_infrastructure/chip/testing/apps.py
@@ -0,0 +1,69 @@
+# Copyright (c) 2024 Project CHIP Authors
+#
+# Licensed 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.
+
+import os
+import signal
+import tempfile
+
+from .tasks import Subprocess
+
+
+class AppServerSubprocess(Subprocess):
+    """Wrapper class for starting an application server in a subprocess."""
+
+    # Prefix for log messages from the application server.
+    PREFIX = b"[SERVER]"
+
+    def __init__(self, app: str, storage_dir: str, discriminator: int,
+                 passcode: int, port: int = 5540):
+        self.kvs_fd, kvs_path = tempfile.mkstemp(dir=storage_dir, prefix="kvs-app-")
+        # Start the server application with dedicated KVS storage.
+        super().__init__(app, "--KVS", kvs_path,
+                         '--secured-device-port', str(port),
+                         "--discriminator", str(discriminator),
+                         "--passcode", str(passcode),
+                         output_cb=lambda line, is_stderr: self.PREFIX + line)
+
+    def __del__(self):
+        # Do not leak KVS file descriptor.
+        os.close(self.kvs_fd)
+
+
+class IcdAppServerSubprocess(AppServerSubprocess):
+    """Wrapper class for starting an ICD application server in a subprocess."""
+
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        self.paused = False
+
+    def pause(self, check_state: bool = True):
+        if check_state and self.paused:
+            raise ValueError("ICD TH Server unexpectedly is already paused")
+        if not self.paused:
+            # Stop (halt) the ICD server process by sending a SIGTOP signal.
+            self.p.send_signal(signal.SIGSTOP)
+            self.paused = True
+
+    def resume(self, check_state: bool = True):
+        if check_state and not self.paused:
+            raise ValueError("ICD TH Server unexpectedly is already running")
+        if self.paused:
+            # Resume (continue) the ICD server process by sending a SIGCONT signal.
+            self.p.send_signal(signal.SIGCONT)
+            self.paused = False
+
+    def terminate(self):
+        # Make sure the ICD server process is not paused before terminating it.
+        self.resume(check_state=False)
+        super().terminate()
diff --git a/src/python_testing/matter_testing_infrastructure/chip/testing/test_tasks.py b/src/python_testing/matter_testing_infrastructure/chip/testing/test_tasks.py
index 5e91a89..051d571 100644
--- a/src/python_testing/matter_testing_infrastructure/chip/testing/test_tasks.py
+++ b/src/python_testing/matter_testing_infrastructure/chip/testing/test_tasks.py
@@ -14,6 +14,10 @@
 
 import unittest
 
+# TODO: Allow to use relative imports or imports from chip.testing package. Then,
+#       rename "tasks" module to "subprocess", because it would be more descriptive.
+#       Unfortunately, current way of importing clashes with the subprocess module
+#       from the Python standard library.
 from tasks import Subprocess