diff --git a/src/tests/ofc24/.gitlab-ci.yml b/src/tests/ofc24/.gitlab-ci.yml
index 6d52579649bc6dc00ab498fef09c92ceed8f939a..6dc32a181de0978151971a02f1d7d6c67125558d 100644
--- a/src/tests/ofc24/.gitlab-ci.yml
+++ b/src/tests/ofc24/.gitlab-ci.yml
@@ -45,16 +45,55 @@ end2end_test ofc24:
   #  - build ofc24
   before_script:
     - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY
+    - docker rm -f na-t1 na-t2 na-r1 na-r2
+    - docker network rm na-br
+
   script:
     # Download Docker image to run the test
     - docker pull "${CI_REGISTRY_IMAGE}/${TEST_NAME}:latest"
+    - docker pull asgamb1/oc23bgp.img:latest
+    - docker pull asgamb1/flexscale-node.img:latest
 
     # Check MicroK8s is ready
     - microk8s status --wait-ready
     - kubectl get pods --all-namespaces
 
     # Deploy Optical Device Node Agents
-    - ./src/tests/${TEST_NAME}/deploy-node-agents.sh
+    - >
+      docker network create -d bridge --subnet=172.254.253.0/24 --gateway=172.254.253.254 \
+      --ip-range=172.254.253.0/24 na-br
+    - >
+      docker run -dit --init --name na-t1 --network=na-br --ip 172.254.253.101 \
+      --volume "$PWD/src/tests/${TEST_NAME}/node-agents-config/startNetconfAgent-tp.sh:/confd/examples.confd/OC23/startNetconfAgent.sh" \
+      --volume "$PWD/src/tests/${TEST_NAME}/node-agents-config/platform_t1.xml:/confd/examples.confd/OC23/platform.xml" \
+      asgamb1/oc23bgp.img:latest /confd/examples.confd/OC23/startNetconfAgent.sh
+    - >
+      docker run -dit --init --name na-t2 --network=na-br --ip 172.254.253.102 \
+      --volume "$PWD/src/tests/${TEST_NAME}/node-agents-config/startNetconfAgent-tp.sh:/confd/examples.confd/OC23/startNetconfAgent.sh" \
+      --volume "$PWD/src/tests/${TEST_NAME}/node-agents-config/platform_t2.xml:/confd/examples.confd/OC23/platform.xml" \
+      asgamb1/oc23bgp.img:latest /confd/examples.confd/OC23/startNetconfAgent.sh
+    - >
+      docker run -dit --init --name na-r1 --network=na-br --ip 172.254.253.201 \
+      --volume "$PWD/src/tests/${TEST_NAME}/node-agents-config/startNetconfAgent-mg-on.sh:/confd/examples.confd/OC23/startNetconfAgent.sh" \
+      --volume "$PWD/src/tests/${TEST_NAME}/node-agents-config/platform_r1.xml:/confd/examples.confd/OC23/platform.xml" \
+      asgamb1/flexscale-node.img:latest /confd/examples.confd/OC23/startNetconfAgent.sh
+    - >
+      docker run -dit --init --name na-r2 --network=na-br --ip 172.254.253.202 \
+      --volume "$PWD/src/tests/${TEST_NAME}/node-agents-config/startNetconfAgent-mg-on.sh:/confd/examples.confd/OC23/startNetconfAgent.sh" \
+      --volume "$PWD/src/tests/${TEST_NAME}/node-agents-config/platform_r2.xml:/confd/examples.confd/OC23/platform.xml" \
+      asgamb1/flexscale-node.img:latest /confd/examples.confd/OC23/startNetconfAgent.sh
+
+
+    # Wait for initialization of Optical Device Node Agents
+    - sleep 3
+    - docker ps -a
+    - while ! docker logs na-t1 2>&1 | grep -q '*** ConfD OpenConfig NETCONF agent ***'; do sleep 1; done
+    - while ! docker logs na-t2 2>&1 | grep -q '*** ConfD OpenConfig NETCONF agent ***'; do sleep 1; done
+    - while ! docker logs na-r1 2>&1 | grep -q '*** ConfD OpenConfig NETCONF agent ***'; do sleep 1; done
+    - while ! docker logs na-r2 2>&1 | grep -q '*** ConfD OpenConfig NETCONF agent ***'; do sleep 1; done
+    - sleep 3
+    - docker ps -a
+
 
     # Configure TeraFlowSDN deployment
     # Uncomment if DEBUG log level is needed for the components
@@ -89,6 +128,7 @@ end2end_test ofc24:
       --volume "$PWD/tfs_runtime_env_vars.sh:/var/teraflow/tfs_runtime_env_vars.sh"
       --volume "$PWD/src/tests/${TEST_NAME}:/opt/results"
       $CI_REGISTRY_IMAGE/${TEST_NAME}:latest
+
   after_script:
     # Dump TeraFlowSDN component logs
     - source src/tests/${TEST_NAME}/deploy_specs.sh
diff --git a/src/tests/ofc24/Dockerfile b/src/tests/ofc24/Dockerfile
index df42fe3382e9aeb4f7c753d2086dc9d920362321..db1d1d9ef277659657b46d64392e6ce519eec5f7 100644
--- a/src/tests/ofc24/Dockerfile
+++ b/src/tests/ofc24/Dockerfile
@@ -92,10 +92,12 @@ RUN tee ./run_tests.sh <<EOF
 #!/bin/bash
 source /var/teraflow/tfs_runtime_env_vars.sh
 export PYTHONPATH=/var/teraflow
-pytest --verbose --log-level=INFO /var/teraflow/tests/ofc24/tests/test_functional_bootstrap.py      --junitxml=/opt/results/report_bootstrap.xml
-pytest --verbose --log-level=INFO /var/teraflow/tests/ofc24/tests/test_functional_create_service.py --junitxml=/opt/results/report_create_service.xml
-pytest --verbose --log-level=INFO /var/teraflow/tests/ofc24/tests/test_functional_delete_service.py --junitxml=/opt/results/report_delete_service.xml
-pytest --verbose --log-level=INFO /var/teraflow/tests/ofc24/tests/test_functional_cleanup.py        --junitxml=/opt/results/report_cleanup.xml
+pytest --verbose --log-level=INFO /var/teraflow/tests/ofc24/tests/test_functional_bootstrap.py             --junitxml=/opt/results/report_bootstrap.xml
+pytest --verbose --log-level=INFO /var/teraflow/tests/ofc24/tests/test_functional_create_service_unidir.py --junitxml=/opt/results/report_create_service_unidir.xml
+pytest --verbose --log-level=INFO /var/teraflow/tests/ofc24/tests/test_functional_delete_service_unidir.py --junitxml=/opt/results/report_delete_service_unidir.xml
+pytest --verbose --log-level=INFO /var/teraflow/tests/ofc24/tests/test_functional_create_service_bidir.py  --junitxml=/opt/results/report_create_service_bidir.xml
+pytest --verbose --log-level=INFO /var/teraflow/tests/ofc24/tests/test_functional_delete_service_bidir.py  --junitxml=/opt/results/report_delete_service_bidir.xml
+pytest --verbose --log-level=INFO /var/teraflow/tests/ofc24/tests/test_functional_cleanup.py               --junitxml=/opt/results/report_cleanup.xml
 EOF
 RUN chmod ug+x ./run_tests.sh
 
diff --git a/src/tests/ofc24/tests/test_functional_bootstrap.py b/src/tests/ofc24/tests/test_functional_bootstrap.py
index bc648d16de57c8c287c7d601f994cb81ed45bc04..e6562f8a2f95f3d1f504b2bbc5dd6dc3b4c47077 100644
--- a/src/tests/ofc24/tests/test_functional_bootstrap.py
+++ b/src/tests/ofc24/tests/test_functional_bootstrap.py
@@ -24,7 +24,7 @@ from tests.Fixtures import context_client, device_client # pylint: disable=unuse
 LOGGER = logging.getLogger(__name__)
 LOGGER.setLevel(logging.DEBUG)
 
-DESCRIPTOR_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'descriptors_topology.json')
+DESCRIPTOR_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'descriptors', 'topology.json')
 ADMIN_CONTEXT_ID = ContextId(**json_context_id(DEFAULT_CONTEXT_NAME))
 
 def test_scenario_bootstrap(
diff --git a/src/tests/ofc24/tests/test_functional_cleanup.py b/src/tests/ofc24/tests/test_functional_cleanup.py
index 5f1ce23f13051759e0e688a42c7118eaff8d3c72..281b0969d708d07be8bdda0aca83b8976d6fa1dd 100644
--- a/src/tests/ofc24/tests/test_functional_cleanup.py
+++ b/src/tests/ofc24/tests/test_functional_cleanup.py
@@ -24,7 +24,7 @@ from tests.Fixtures import context_client, device_client    # pylint: disable=un
 LOGGER = logging.getLogger(__name__)
 LOGGER.setLevel(logging.DEBUG)
 
-DESCRIPTOR_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'descriptors_topology.json')
+DESCRIPTOR_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'descriptors', 'topology.json')
 ADMIN_CONTEXT_ID = ContextId(**json_context_id(DEFAULT_CONTEXT_NAME))
 
 def test_scenario_cleanup(
diff --git a/src/tests/ofc24/tests/test_functional_create_service.py b/src/tests/ofc24/tests/test_functional_create_service_bidir.py
similarity index 61%
rename from src/tests/ofc24/tests/test_functional_create_service.py
rename to src/tests/ofc24/tests/test_functional_create_service_bidir.py
index 74c74483eb82325afec2cae833ebd460566da153..19c05425b607688fd6d14d3aea7b58b1883ca7c5 100644
--- a/src/tests/ofc24/tests/test_functional_create_service.py
+++ b/src/tests/ofc24/tests/test_functional_create_service_bidir.py
@@ -12,15 +12,13 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-import logging, os, random
+import logging, os
 from common.Constants import DEFAULT_CONTEXT_NAME
-from common.proto.context_pb2 import ContextId, Empty, ServiceTypeEnum
-from common.proto.kpi_sample_types_pb2 import KpiSampleType
+from common.proto.context_pb2 import ContextId, ServiceTypeEnum
 from common.tools.descriptor.Loader import DescriptorLoader
 from common.tools.grpc.Tools import grpc_message_to_json_string
 from common.tools.object_factory.Context import json_context_id
 from context.client.ContextClient import ContextClient
-from monitoring.client.MonitoringClient import MonitoringClient
 from tests.Fixtures import context_client, monitoring_client                    # pylint: disable=unused-import
 from tests.tools.mock_osm.MockOSM import MockOSM
 from .Fixtures import osm_wim                                                   # pylint: disable=unused-import
@@ -29,10 +27,10 @@ from .Objects import WIM_SERVICE_CONNECTION_POINTS, WIM_SERVICE_TYPE
 LOGGER = logging.getLogger(__name__)
 LOGGER.setLevel(logging.DEBUG)
 
-DESCRIPTOR_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'descriptors_emulated.json')
+DESCRIPTOR_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'descriptors', 'service-bidir.json')
 ADMIN_CONTEXT_ID = ContextId(**json_context_id(DEFAULT_CONTEXT_NAME))
 
-def test_service_creation(context_client : ContextClient, osm_wim : MockOSM): # pylint: disable=redefined-outer-name
+def test_service_creation_bidir(context_client : ContextClient, osm_wim : MockOSM): # pylint: disable=redefined-outer-name
     # Load descriptors and validate the base scenario
     descriptor_loader = DescriptorLoader(descriptors_file=DESCRIPTOR_FILE, context_client=context_client)
     descriptor_loader.validate()
@@ -68,35 +66,3 @@ def test_service_creation(context_client : ContextClient, osm_wim : MockOSM): #
         else:
             str_service = grpc_message_to_json_string(service)
             raise Exception('Unexpected ServiceType: {:s}'.format(str_service))
-
-
-def test_scenario_kpi_values_created(
-    monitoring_client: MonitoringClient,    # pylint: disable=redefined-outer-name
-) -> None:
-    """
-    This test validates that KPI values have been inserted into the monitoring database.
-    We short k KPI descriptors to test.
-    """
-    response = monitoring_client.GetKpiDescriptorList(Empty())
-    kpi_descriptors = random.choices(response.kpi_descriptor_list, k=2)
-
-    for kpi_descriptor in kpi_descriptors:
-        MSG = 'KPI(kpi_uuid={:s}, device_uuid={:s}, endpoint_uuid={:s}, service_uuid={:s}, kpi_sample_type={:s})...'
-        LOGGER.info(MSG.format(
-            str(kpi_descriptor.kpi_id.kpi_id.uuid), str(kpi_descriptor.device_id.device_uuid.uuid),
-            str(kpi_descriptor.endpoint_id.endpoint_uuid.uuid), str(kpi_descriptor.service_id.service_uuid.uuid),
-            str(KpiSampleType.Name(kpi_descriptor.kpi_sample_type))))
-        response = monitoring_client.GetInstantKpi(kpi_descriptor.kpi_id)
-        kpi_uuid = response.kpi_id.kpi_id.uuid
-        assert kpi_uuid == kpi_descriptor.kpi_id.kpi_id.uuid
-        kpi_value_type = response.kpi_value.WhichOneof('value')
-        if kpi_value_type is None:
-            MSG = '  KPI({:s}): No instant value found'
-            LOGGER.warning(MSG.format(str(kpi_uuid)))
-        else:
-            kpi_timestamp = response.timestamp.timestamp
-            assert kpi_timestamp > 0
-            assert kpi_value_type == 'floatVal'
-            kpi_value = getattr(response.kpi_value, kpi_value_type)
-            MSG = '  KPI({:s}): timestamp={:s} value_type={:s} value={:s}'
-            LOGGER.info(MSG.format(str(kpi_uuid), str(kpi_timestamp), str(kpi_value_type), str(kpi_value)))
diff --git a/src/tests/ofc24/tests/test_functional_create_service_unidir.py b/src/tests/ofc24/tests/test_functional_create_service_unidir.py
new file mode 100644
index 0000000000000000000000000000000000000000..35d9664f72e13d3a4cf31e16e727c727ed4e8c2b
--- /dev/null
+++ b/src/tests/ofc24/tests/test_functional_create_service_unidir.py
@@ -0,0 +1,68 @@
+# Copyright 2022-2023 ETSI TeraFlowSDN - TFS OSG (https://tfs.etsi.org/)
+#
+# 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 logging, os
+from common.Constants import DEFAULT_CONTEXT_NAME
+from common.proto.context_pb2 import ContextId, ServiceTypeEnum
+from common.tools.descriptor.Loader import DescriptorLoader
+from common.tools.grpc.Tools import grpc_message_to_json_string
+from common.tools.object_factory.Context import json_context_id
+from context.client.ContextClient import ContextClient
+from tests.Fixtures import context_client, monitoring_client                    # pylint: disable=unused-import
+from tests.tools.mock_osm.MockOSM import MockOSM
+from .Fixtures import osm_wim                                                   # pylint: disable=unused-import
+from .Objects import WIM_SERVICE_CONNECTION_POINTS, WIM_SERVICE_TYPE
+
+LOGGER = logging.getLogger(__name__)
+LOGGER.setLevel(logging.DEBUG)
+
+DESCRIPTOR_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'descriptors', 'service-bidir.json')
+ADMIN_CONTEXT_ID = ContextId(**json_context_id(DEFAULT_CONTEXT_NAME))
+
+def test_service_creation_unidir(context_client : ContextClient, osm_wim : MockOSM): # pylint: disable=redefined-outer-name
+    # Load descriptors and validate the base scenario
+    descriptor_loader = DescriptorLoader(descriptors_file=DESCRIPTOR_FILE, context_client=context_client)
+    descriptor_loader.validate()
+
+    # Verify the scenario has no services/slices
+    response = context_client.GetContext(ADMIN_CONTEXT_ID)
+    assert len(response.service_ids) == 0
+    assert len(response.slice_ids) == 0
+
+    # Create Connectivity Service
+    service_uuid = osm_wim.create_connectivity_service(WIM_SERVICE_TYPE, WIM_SERVICE_CONNECTION_POINTS)
+    osm_wim.get_connectivity_service_status(service_uuid)
+
+    # Ensure slices and services are created
+    response = context_client.ListSlices(ADMIN_CONTEXT_ID)
+    LOGGER.info('Slices[{:d}] = {:s}'.format(len(response.slices), grpc_message_to_json_string(response)))
+    assert len(response.slices) == 1 # OSM slice
+
+    response = context_client.ListServices(ADMIN_CONTEXT_ID)
+    LOGGER.info('Services[{:d}] = {:s}'.format(len(response.services), grpc_message_to_json_string(response)))
+    assert len(response.services) == 2 # 1xL3NM + 1xTAPI
+
+    for service in response.services:
+        service_id = service.service_id
+        response = context_client.ListConnections(service_id)
+        LOGGER.info('  ServiceId[{:s}] => Connections[{:d}] = {:s}'.format(
+            grpc_message_to_json_string(service_id), len(response.connections), grpc_message_to_json_string(response)))
+
+        if service.service_type == ServiceTypeEnum.SERVICETYPE_L3NM:
+            assert len(response.connections) == 1 # 1 connection per service
+        elif service.service_type == ServiceTypeEnum.SERVICETYPE_TAPI_CONNECTIVITY_SERVICE:
+            assert len(response.connections) == 1 # 1 connection per service
+        else:
+            str_service = grpc_message_to_json_string(service)
+            raise Exception('Unexpected ServiceType: {:s}'.format(str_service))
diff --git a/src/tests/ofc24/tests/test_functional_delete_service.py b/src/tests/ofc24/tests/test_functional_delete_service_bidir.py
similarity index 95%
rename from src/tests/ofc24/tests/test_functional_delete_service.py
rename to src/tests/ofc24/tests/test_functional_delete_service_bidir.py
index daff29064f07e8117a4503fc243c7acac9f88bc6..64a6161f62877225959e6af501a7b9bf10e0dc7a 100644
--- a/src/tests/ofc24/tests/test_functional_delete_service.py
+++ b/src/tests/ofc24/tests/test_functional_delete_service_bidir.py
@@ -26,10 +26,10 @@ from .Fixtures import osm_wim               # pylint: disable=unused-import
 LOGGER = logging.getLogger(__name__)
 LOGGER.setLevel(logging.DEBUG)
 
-DESCRIPTOR_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'descriptors_emulated.json')
+DESCRIPTOR_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'descriptors', 'service-bidir.json')
 ADMIN_CONTEXT_ID = ContextId(**json_context_id(DEFAULT_CONTEXT_NAME))
 
-def test_service_removal(context_client : ContextClient, osm_wim : MockOSM): # pylint: disable=redefined-outer-name
+def test_service_removal_bidir(context_client : ContextClient, osm_wim : MockOSM): # pylint: disable=redefined-outer-name
     # Ensure slices and services are created
     response = context_client.ListSlices(ADMIN_CONTEXT_ID)
     LOGGER.info('Slices[{:d}] = {:s}'.format(len(response.slices), grpc_message_to_json_string(response)))
diff --git a/src/tests/ofc24/tests/test_functional_delete_service_unidir.py b/src/tests/ofc24/tests/test_functional_delete_service_unidir.py
new file mode 100644
index 0000000000000000000000000000000000000000..ebdb602559039353cc787addf58412e6edafc4f8
--- /dev/null
+++ b/src/tests/ofc24/tests/test_functional_delete_service_unidir.py
@@ -0,0 +1,74 @@
+# Copyright 2022-2023 ETSI TeraFlowSDN - TFS OSG (https://tfs.etsi.org/)
+#
+# 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 logging, os
+from common.Constants import DEFAULT_CONTEXT_NAME
+from common.proto.context_pb2 import ContextId, ServiceTypeEnum
+from common.tools.descriptor.Loader import DescriptorLoader
+from common.tools.grpc.Tools import grpc_message_to_json_string
+from common.tools.object_factory.Context import json_context_id
+from context.client.ContextClient import ContextClient
+from tests.Fixtures import context_client   # pylint: disable=unused-import
+from tests.tools.mock_osm.MockOSM import MockOSM
+from .Fixtures import osm_wim               # pylint: disable=unused-import
+
+LOGGER = logging.getLogger(__name__)
+LOGGER.setLevel(logging.DEBUG)
+
+DESCRIPTOR_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'descriptors', 'service-unidir.json')
+ADMIN_CONTEXT_ID = ContextId(**json_context_id(DEFAULT_CONTEXT_NAME))
+
+def test_service_removal_unidir(context_client : ContextClient, osm_wim : MockOSM): # pylint: disable=redefined-outer-name
+    # Ensure slices and services are created
+    response = context_client.ListSlices(ADMIN_CONTEXT_ID)
+    LOGGER.info('Slices[{:d}] = {:s}'.format(len(response.slices), grpc_message_to_json_string(response)))
+    assert len(response.slices) == 1 # OSM slice
+
+    response = context_client.ListServices(ADMIN_CONTEXT_ID)
+    LOGGER.info('Services[{:d}] = {:s}'.format(len(response.services), grpc_message_to_json_string(response)))
+    assert len(response.services) == 2 # 1xL3NM + 1xTAPI
+
+    service_uuids = set()
+    for service in response.services:
+        service_id = service.service_id
+        response = context_client.ListConnections(service_id)
+        LOGGER.info('  ServiceId[{:s}] => Connections[{:d}] = {:s}'.format(
+            grpc_message_to_json_string(service_id), len(response.connections), grpc_message_to_json_string(response)))
+
+        if service.service_type == ServiceTypeEnum.SERVICETYPE_L3NM:
+            assert len(response.connections) == 1 # 1 connection per service
+            service_uuid = service_id.service_uuid.uuid
+            service_uuids.add(service_uuid)
+            osm_wim.conn_info[service_uuid] = {}
+        elif service.service_type == ServiceTypeEnum.SERVICETYPE_TAPI_CONNECTIVITY_SERVICE:
+            assert len(response.connections) == 1 # 1 connection per service
+        else:
+            str_service = grpc_message_to_json_string(service)
+            raise Exception('Unexpected ServiceType: {:s}'.format(str_service))
+
+    # Identify service to delete
+    assert len(service_uuids) == 1  # assume a single L3NM service has been created
+    service_uuid = set(service_uuids).pop()
+
+    # Delete Connectivity Service
+    osm_wim.delete_connectivity_service(service_uuid)
+
+    # Verify the scenario has no services/slices
+    response = context_client.GetContext(ADMIN_CONTEXT_ID)
+    assert len(response.service_ids) == 0
+    assert len(response.slice_ids) == 0
+
+    # Load descriptors and validate the base scenario
+    descriptor_loader = DescriptorLoader(descriptors_file=DESCRIPTOR_FILE, context_client=context_client)
+    descriptor_loader.validate()