diff --git a/meta/lib/oeqa/selftest/cases/devtool.py b/meta/lib/oeqa/selftest/cases/devtool.py
index 3b24ce3914..e9b8f38449 100644
--- a/meta/lib/oeqa/selftest/cases/devtool.py
+++ b/meta/lib/oeqa/selftest/cases/devtool.py
@@ -2762,13 +2762,15 @@ class DevtoolIdeSdkTests(DevtoolBase):
         if self.logger.isEnabledFor(logging.DEBUG):
             self._cmd_logger = self.logger
 
-    def _write_bb_config(self, recipe_names):
+    def _write_bb_config(self, recipe_names, extra_packages=None):
         """Helper to write the bitbake local.conf file"""
+        image_install = 'gdbserver ' + ' '.join([r + '-ptest' for r in recipe_names])
+        if extra_packages:
+            image_install += ' ' + ' '.join(extra_packages)
         conf_lines = [
             'IMAGE_CLASSES += "image-combined-dbg"',
             'IMAGE_GEN_DEBUGFS = "1"',
-            'IMAGE_INSTALL:append = " gdbserver %s"' % ' '.join(
-                [r + '-ptest' for r in recipe_names]),
+            'IMAGE_INSTALL:append = " %s"' % image_install,
             'DISTRO_FEATURES:append = " ptest"',
             # Static UIDs/GIDs are required so that files installed via
             # "install -o ${BPN}" in do_install embed the same UID that gets
@@ -3217,7 +3219,9 @@ class DevtoolIdeSdkTests(DevtoolBase):
 
     def _verify_service_running(self, qemu, service_name):
         """Helper to verify a service is running in Qemu"""
-        status, output = qemu.run("pgrep %s" % service_name)
+        # Use anchored regex (^name$) instead of pgrep -x because the target
+        # may have busybox pgrep which does not support the -x flag.
+        status, output = qemu.run("pgrep '^%s$'" % service_name)
         self.assertEqual(status, 0, msg="%s service not running: %s" %
                          (service_name, output))
         self.assertTrue(output.strip().isdigit(),
@@ -3908,6 +3912,472 @@ class DevtoolIdeSdkTests(DevtoolBase):
         runCmdEnv('meson setup %s' % tempdir_meson, cwd=cpp_example_src, output_log=self._cmd_logger)
         runCmdEnv('meson compile', cwd=tempdir_meson, output_log=self._cmd_logger)
 
+    def _verify_launch_json_lldb(self, tempdir):
+        """Verify the launch.json file contains valid CodeLLDB (type: lldb) configurations."""
+        launch_json_path = os.path.join(tempdir, '.vscode', 'launch.json')
+        self.assertTrue(os.path.exists(launch_json_path), "launch.json file should exist")
+
+        with open(launch_json_path) as launch_j:
+            launch_d = json.load(launch_j)
+
+        self.assertIn("configurations", launch_d)
+        configurations = launch_d["configurations"]
+        self.assertGreater(len(configurations), 0,
+                           "Should have at least one debug configuration")
+
+        for config in configurations:
+            config_name = config.get("name", "Unknown")
+            # CodeLLDB configs use "type": "lldb", not "type": "cppdbg"
+            self.assertEqual(config["type"], "lldb",
+                             f"Configuration '{config_name}' should use lldb type (CodeLLDB)")
+            self.assertNotIn("MIMode", config,
+                             f"Configuration '{config_name}' should not have MIMode (CodeLLDB)")
+            self.assertNotIn("miDebuggerPath", config,
+                             f"Configuration '{config_name}' should not have miDebuggerPath")
+            self.assertEqual(config["request"], "launch",
+                             f"Configuration '{config_name}' should be launch type")
+            self.assertEqual(config["cwd"], "/tmp",
+                             f"Configuration '{config_name}' cwd should be /tmp (writable on target)")
+
+            # Verify initCommands contain the platform connect sequence
+            init_commands = config.get("initCommands", [])
+            self.assertTrue(any("platform select remote-linux" in cmd
+                                for cmd in init_commands),
+                            f"Configuration '{config_name}' should select remote-linux platform")
+            self.assertTrue(any("platform connect" in cmd for cmd in init_commands),
+                            f"Configuration '{config_name}' should connect to remote platform")
+
+            # Verify targetCreateCommands creates the target with --remote-file so
+            # LLDB uses the host debug binary for symbols but executes the pre-deployed
+            # binary on the target (avoiding the module-cache upload path).
+            target_create_commands = config.get("targetCreateCommands", [])
+            self.assertTrue(len(target_create_commands) > 0,
+                            f"Configuration '{config_name}' should have targetCreateCommands")
+            create_cmd = target_create_commands[0]
+            self.assertIn("--remote-file", create_cmd,
+                          f"Configuration '{config_name}' targetCreateCommands should use "
+                          "--remote-file to specify the remote binary path")
+            self.assertIn("/image/", create_cmd,
+                          f"Configuration '{config_name}' targetCreateCommands should reference "
+                          "the host debug binary in the image directory")
+
+            # Verify preLaunchTask referencing the lldb-server start task
+            task = config.get("preLaunchTask", "")
+            self.assertTrue(task,
+                            f"Configuration '{config_name}' preLaunchTask should not be empty")
+
+    def _find_source_break_line(self, tempdir, source_file, marker):
+        """Find the 1-based line number of `marker` in tempdir/source_file
+
+        Used to set a source-level (file:line) breakpoint at a stable,
+        self-documenting location instead of hard-coding a line number that
+        would silently go stale whenever the example source changes.
+        """
+        source_path = os.path.join(tempdir, source_file)
+        with open(source_path) as f:
+            for lineno, line in enumerate(f, start=1):
+                if marker in line:
+                    return lineno
+        self.fail("Could not find marker %r in %s" % (marker, source_path))
+
+    def _lldb_debug_cpp_example_batch_commands(self, tempdir):
+        """Get a list of lldb --batch '-o' commands to debug the cpp-example-lib example
+
+        Mirrors _gdb_debug_cpp_example: sets source-level (file:line)
+        breakpoints in the executable (cpp-example.cpp), the library's own
+        .cpp file (cpp-example-lib.cpp) and a header-only inline function
+        (cpp-example-lib.hpp), then inspects a variable at each stop. This
+        exercises breakpoint resolution against all three distinct debug-info
+        sources (executable, library, header-only), instead of only the
+        single call-site breakpoint in the executable used previously.
+
+        The breakpoints are deliberately set by source file:line (rather than
+        by function/symbol name, e.g. "b main") because that is what exercises
+        CodeLLDB/LLDB's sourceMap reverse-lookup: translating a source file
+        path back into debug-info space to resolve the breakpoint location.
+        A symbol breakpoint resolves directly from the symbol table and would
+        not catch a broken/ambiguous sourceMap (see the "fix source map for
+        lldb" change), so it is not a sufficient regression test on its own.
+
+        Line numbers are (re-)discovered via markers rather than hard-coded,
+        so the same commands keep working after a caller shifts lines with
+        _shift_cpp_example_lines_and_recompile() and recompiles.
+        """
+        exe_break_line = self._find_source_break_line(
+            tempdir, 'cpp-example.cpp', 'cpp_example.print_json();')
+        lib_break_line = self._find_source_break_line(
+            tempdir, 'cpp-example-lib.cpp',
+            'std::cout << json_object_to_json_string_ext(jobj, flag) << std::endl;')
+        hpp_break_line = self._find_source_break_line(
+            tempdir, 'cpp-example-lib.hpp', 'int scaled = n * 7;')
+
+        return [
+            "-o", "breakpoint set --file cpp-example.cpp --line %d" % exe_break_line,
+            "-o", "breakpoint set --file cpp-example-lib.cpp --line %d" % lib_break_line,
+            "-o", "breakpoint set --file cpp-example-lib.hpp --line %d" % hpp_break_line,
+            # Report the resolved location count right away: sourceMap
+            # ambiguity/underflow (the meson/ninja relative-path bug) leaves a
+            # breakpoint at "locations = 0 (pending)" even though the
+            # breakpoint gutter/status can otherwise look "verified".
+            "-o", "breakpoint list",
+            "-o", "run",
+            # Stop 1: the executable's own breakpoint, right before the call
+            # into the library.
+            "-o", "p cpp_example.get_string()",
+            "-o", "continue",
+            # Stop 2: inside the library's own .cpp file, by file:line. This
+            # is the breakpoint that would fail to resolve (or resolve to a
+            # stale line) if the freshly rebuilt library debug info was not
+            # preferred over a stale rootfs-dbg copy. Inspect a plain local
+            # variable (rather than the CppExample::test_string static class
+            # member, as the GDB test does) because LLDB cannot reliably
+            # evaluate an expression that needs to trigger the lazy
+            # initialization guard of a C++17 inline static std::string over
+            # a remote gdb-remote connection ("Couldn't look up symbols").
+            "-o", "p flag",
+            "-o", "continue",
+            # Stop 3: inside an inline function defined directly in the
+            # header (cpp-example-lib.hpp).
+            "-o", "p n",
+            "-o", "continue",
+            "-o", "exit",
+        ]
+
+    def _lldb_debug_cpp_example_check(self, output, magic_string):
+        """Check the output of an lldb --batch session run with the commands
+        from _lldb_debug_cpp_example_batch_commands()"""
+        self.assertNotIn("(pending)", output,
+                         "breakpoints should resolve to a location instead of staying "
+                         "pending (sourceMap/prefix-map path mismatch): %s" % output)
+        # LLDB emits this when the MD5 checksum embedded in the DWARF line
+        # table (recorded by the compiler at compile time) doesn't match the
+        # file currently on disk. This should never happen for a freshly
+        # (re)compiled and (re)deployed example: it would mean the debugger
+        # is displaying/attributing source lines that don't actually
+        # correspond to the binary being debugged. Treat it as a hard
+        # failure instead of silently tolerating it, so a regression here
+        # (e.g. a sourceMap entry getting clobbered, causing LLDB to resolve
+        # a source file against the wrong, stale location) doesn't go
+        # unnoticed.
+        self.assertNotIn("source file checksum mismatch", output,
+                         "debug info should match the current source files exactly "
+                         "(sourceMap resolving to a stale copy of the file?): %s" % output)
+        # exe (by file:line), library (by file:line) and header (by
+        # file:line) breakpoints should all have been hit: 1 + 1 + 1 = 3
+        self.assertEqual(output.count("stop reason = breakpoint"), 3,
+                         "expected 3 breakpoint hits (executable once, library "
+                         "once, header inline function once): %s" % output)
+        # a local variable should be inspectable at the library breakpoint,
+        # proving the library's own debug info is usable (JSON_C_TO_STRING_SPACED
+        # | JSON_C_TO_STRING_PRETTY == 3). LLDB doesn't always print a "$N ="
+        # value slot (e.g. "(const int) 3" for a compile-time constant vs.
+        # "(int) $0 = 3"/"(int) 3" for an ordinary variable), so anchor the
+        # check on the "p flag" command itself rather than assuming a "="
+        # appears in its output.
+        self.assertRegex(output, r"\(lldb\) p flag\r?\n[^\n]*\b3\b",
+                        "should be able to inspect flag at the library breakpoint: %s" % output)
+        # the magic string should be visible in the program's own output,
+        # proving the freshly (re)compiled example actually ran
+        self.assertIn(magic_string, output,
+                     "should be able to see the magic string printed by the example")
+        # the header-only inline function was hit once, called with n == 6
+        self.assertRegex(output, r"\(lldb\) p n\r?\n[^\n]*\b6\b",
+                        "should be able to inspect n == 6 at the header breakpoint: %s" % output)
+        self.assertIn("exited with status = 0", output,
+                     "the example should run to completion and exit normally: %s" % output)
+
+    def _shift_cpp_example_lines_and_recompile(self, tempdir, compile_cmd, install_deploy_cmd,
+                                               magic_string_new, line_shift=3):
+        """Change the magic string and insert line_shift extra lines before the
+        statements the LLDB file:line breakpoints target, then recompile and redeploy.
+
+        Mirrors the code-change step of _devtool_ide_sdk_qemu (used by the GDB
+        tests): shifts the executable's, the library's and the header's source
+        lines so that a subsequent debugging pass' file:line breakpoints only
+        resolve correctly if they are based on the freshly rebuilt debug info,
+        rather than a stale/cached line-to-address mapping (or, for the
+        library, a stale rootfs-dbg copy instead of the freshly redeployed
+        image folder).
+        """
+        extra_lines = "".join(
+            "    // extra line %d inserted by the test to shift subsequent line numbers\n" % i
+            for i in range(line_shift))
+
+        cpp_example_lib_hpp = os.path.join(tempdir, 'cpp-example-lib.hpp')
+        with open(cpp_example_lib_hpp, 'r') as file:
+            cpp_code = file.read()
+        cpp_code = cpp_code.replace(DevtoolIdeSdkTests.MAGIC_STRING_ORIG, magic_string_new)
+        cpp_code = cpp_code.replace(
+            "    inline static int scale_number(int n)",
+            extra_lines + "    inline static int scale_number(int n)")
+        with open(cpp_example_lib_hpp, 'w') as file:
+            file.write(cpp_code)
+
+        cpp_example_cpp = os.path.join(tempdir, 'cpp-example.cpp')
+        with open(cpp_example_cpp, 'r') as file:
+            cpp_code = file.read()
+        cpp_code = cpp_code.replace(
+            "    cpp_example.print_json();",
+            extra_lines + "    cpp_example.print_json();")
+        with open(cpp_example_cpp, 'w') as file:
+            file.write(cpp_code)
+
+        cpp_example_lib_cpp = os.path.join(tempdir, 'cpp-example-lib.cpp')
+        with open(cpp_example_lib_cpp, 'r') as file:
+            cpp_code = file.read()
+        cpp_code = cpp_code.replace(
+            "    std::cout << json_object_to_json_string_ext(jobj, flag) << std::endl;",
+            extra_lines + "    std::cout << json_object_to_json_string_ext(jobj, flag) << std::endl;")
+        with open(cpp_example_lib_cpp, 'w') as file:
+            file.write(cpp_code)
+
+        runCmd(compile_cmd, cwd=tempdir, output_log=self._cmd_logger)
+        runCmd(install_deploy_cmd, cwd=tempdir, output_log=self._cmd_logger)
+
+    def _lldb_cross_debugging_multi(self, tempdir, recipe_name, compile_cmd, lldb_session_func):
+        """Verify LLDB remote debugging in Qemu, before and after a code change.
+
+        Mirrors _devtool_ide_sdk_qemu (the GDB equivalent): debugs the example
+        once, then edits the source (magic string + line shift on the
+        executable, library and header), recompiles, redeploys, and debugs a
+        second time to prove the breakpoints resolve against the freshly
+        rebuilt debug info rather than a stale/cached line-to-address mapping,
+        closing the coverage gap between the GDB and LLDB test paths.
+
+        lldb_session_func is called once per pass with the magic string
+        expected for that pass; it is expected to run an lldb --batch session
+        and check its output (e.g. a closure around _lldb_server_debugging_once
+        or _lldb_none_debugging_multi).
+        """
+        recipe_id, _ = self._get_recipe_ids(recipe_name)
+        install_deploy_cmd = os.path.join(
+            self._workspace_scripts_dir(recipe_name), 'install_and_deploy_' + recipe_id)
+        self.assertExists(install_deploy_cmd, '%s script not found' % install_deploy_cmd)
+        runCmd(install_deploy_cmd, output_log=self._cmd_logger)
+
+        # First pass: debug the freshly deployed, unmodified example
+        lldb_session_func(DevtoolIdeSdkTests.MAGIC_STRING_ORIG)
+
+        # Change the magic string, shift breakpoint lines, recompile and redeploy
+        magic_string_new = "Magic: 987654321"
+        self._shift_cpp_example_lines_and_recompile(
+            tempdir, compile_cmd, install_deploy_cmd, magic_string_new)
+
+        # Second pass: debug again, breakpoints now resolved at shifted line numbers
+        lldb_session_func(magic_string_new)
+
+    def _lldb_server_debugging_once(self, tempdir, qemu, magic_string):
+        """Verify lldb-server (platform mode) + lldb batch debugging works end-to-end.
+
+        Reads the preLaunchTask SSH command from tasks.json to start lldb-server
+        on the target, then runs lldb --batch to perform a debugging session
+        covering the executable, the library and a header-only inline function
+        (see _lldb_debug_cpp_example_batch_commands), and checks that the
+        expected magic string and variable values are visible.
+        """
+        sshargs = '-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no'
+
+        with open(os.path.join(tempdir, '.vscode', 'launch.json')) as f:
+            launch_d = json.load(f)
+        with open(os.path.join(tempdir, '.vscode', 'tasks.json')) as f:
+            tasks_d = json.load(f)
+
+        # Find the first *_once or *_multi config
+        lldb_config = next(
+            (c for c in launch_d["configurations"]
+             if "_once" in c["name"] or "_multi" in c["name"]), None)
+        self.assertIsNotNone(lldb_config, "Should have at least one lldb debug configuration")
+
+        prelaunch_task_name = lldb_config["preLaunchTask"]
+        prelaunch_task = next(
+            (t for t in tasks_d["tasks"] if t["label"] == prelaunch_task_name), None)
+        self.assertIsNotNone(prelaunch_task,
+                             "preLaunchTask '%s' not found in tasks.json" % prelaunch_task_name)
+
+        # Extract the SSH command and start lldb-server on the target
+        task_command = prelaunch_task["command"]
+        task_args = prelaunch_task["args"]
+        self.assertEqual(task_command, "ssh",
+                         "preLaunchTask should use ssh to start lldb-server")
+        ssh_cmd = [task_command] + task_args
+        if ssh_cmd[-1].startswith('"') and ssh_cmd[-1].endswith('"'):
+            # The tasks.json arg is formatted for a shell double-quoted context
+            # (e.g. bash running "ssh ... \"...\$((...))...\"").  Strip the
+            # surrounding quotes and undo the \$ → $ escaping that would
+            # normally be done by bash, so the arg works when passed directly
+            # to SSH via subprocess without an intermediate shell.
+            ssh_cmd[-1] = ssh_cmd[-1][1:-1].replace('\\$', '$')
+
+        # Extract connection details from initCommands
+        init_commands = lldb_config["initCommands"]
+        connect_cmd = next((c for c in init_commands if "platform connect" in c), None)
+        self.assertIsNotNone(connect_cmd, "initCommands should contain a platform connect command")
+
+        # Find lldb binary from lldb-native sysroot
+        lldb_native_sysroot = get_bb_var('RECIPE_SYSROOT_NATIVE', 'lldb-native')
+        lldb_binary = os.path.join(lldb_native_sysroot, 'usr', 'bin', 'lldb')
+        self.assertExists(lldb_binary, "lldb binary should exist in lldb-native sysroot")
+
+        with RunCmdBackground(ssh_cmd, output_log=self._cmd_logger):
+            time.sleep(1)
+
+            # Verify lldb-server is running on the target
+            r = runCmd('ssh %s root@%s ps' % (sshargs, qemu.ip),
+                       output_log=self._cmd_logger)
+            self.assertIn("lldb-server", r.output,
+                          "lldb-server should be running on target")
+
+            # Run lldb --batch: connect to platform, create target with remote-file,
+            # set a source-level breakpoint, and run.
+            # targetCreateCommands replaces the "program" field; each entry is
+            # passed as a separate -o command in batch mode.
+            target_create_commands = lldb_config.get("targetCreateCommands", [])
+            source_map = lldb_config.get("sourceMap", {})
+
+            lldb_batch = [lldb_binary, "--batch"]
+            for cmd in init_commands:
+                lldb_batch += ["-o", cmd]
+            for cmd in target_create_commands:
+                lldb_batch += ["-o", cmd]
+            if source_map:
+                # "settings set target.source-map" replaces the *entire*
+                # mapping list rather than appending to it. Issuing one
+                # "-o settings set target.source-map ..." per entry (as done
+                # previously) silently discards all but the last mapping, so
+                # LLDB ends up resolving source files (and verifying their
+                # DWARF MD5 checksum) against the wrong location, e.g. a
+                # stale rootfs-dbg copy of a devtool-modified recipe's own
+                # sources instead of the freshly edited workspace srctree.
+                # All pairs must therefore be set together in a single
+                # command, exactly like CodeLLDB itself does.
+                source_map_args = []
+                for k, v in source_map.items():
+                    v_resolved = v.replace("${workspaceFolder}", tempdir)
+                    source_map_args += [k, v_resolved]
+                lldb_batch += ["-o", "settings set target.source-map %s" % " ".join(source_map_args)]
+            lldb_batch += self._lldb_debug_cpp_example_batch_commands(tempdir)
+            r = runCmd(lldb_batch, output_log=self._cmd_logger)
+            self.assertEqual(r.status, 0, "lldb batch session failed: %s" % r.output)
+            self._lldb_debug_cpp_example_check(r.output, magic_string)
+
+    @OETestTag("runqemu")
+    def test_devtool_ide_sdk_code_cmake_clang(self):
+        """Verify a cmake recipe built with clang works with ide=code (CodeLLDB debugging).
+
+        This test uses the cmake-example-clang recipe which is a cmake-example variant
+        built with clang. It installs a separate binary (cmake-example-clang) so all four
+        recipe variants (cmake/meson x gcc/clang) can be installed in the same image
+        without conflicts. It is configured to use lldb-server for debugging instead of
+        gdbserver. The test flow is similar to test_devtool_ide_sdk_code_cmake but with
+        additional checks related to lldb:
+        - devtool ide-sdk selects lldb-native / lldb-server instead of gdb-cross
+        - launch.json uses "type": "lldb" (CodeLLDB) instead of "type": "cppdbg"
+        - extensions.json recommends vadimcn.vscode-lldb
+        - A basic lldb --batch remote debugging session succeeds against the
+          lldb-server platform running on the Qemu target
+        """
+        recipe_name = "cmake-example-clang"
+        build_file = "CMakeLists.txt"
+        testimage = "oe-selftest-image"
+
+        self._check_workspace()
+        self._write_bb_config([recipe_name], extra_packages=['lldb-server'])
+
+        self._check_runqemu_prerequisites()
+        bitbake(testimage)
+        with runqemu(testimage, runqemuparams="nographic") as qemu:
+            tempdir = self._devtool_ide_sdk_recipe(recipe_name, build_file, testimage)
+            bitbake_sdk_cmd = 'devtool ide-sdk %s %s -t root@%s -c --ide=code' % (
+                recipe_name, testimage, qemu.ip)
+            runCmd(bitbake_sdk_cmd, output_log=self._cmd_logger)
+
+            # Verify the cmake preset still works (build system unchanged)
+            compile_cmd = self._verify_cmake_preset(tempdir)
+
+            # Verify the install && deploy-target task script exists
+            self._verify_install_script_code(tempdir, recipe_name)
+
+            # Verify extensions.json recommends CodeLLDB instead of / alongside cpptools
+            with open(os.path.join(tempdir, '.vscode', 'extensions.json')) as ext_j:
+                ext_d = json.load(ext_j)
+            recommendations = ext_d.get('recommendations', [])
+            self.assertIn('vadimcn.vscode-lldb', recommendations,
+                          'vadimcn.vscode-lldb should be recommended for clang recipes')
+
+            # Verify launch.json uses CodeLLDB format
+            self._verify_launch_json_lldb(tempdir)
+
+            # Verify deployment and lldb batch remote debugging work end-to-end,
+            # before and after a code change/recompile/redeploy cycle (see
+            # _lldb_cross_debugging_multi)
+            self._lldb_cross_debugging_multi(
+                tempdir, recipe_name, compile_cmd,
+                lambda magic_string: self._lldb_server_debugging_once(
+                    tempdir, qemu, magic_string))
+
+    @OETestTag("runqemu")
+    def test_devtool_ide_sdk_code_meson_clang(self):
+        """Verify a meson recipe built with clang works with ide=code (CodeLLDB debugging).
+
+        This is the meson/ninja counterpart of test_devtool_ide_sdk_code_cmake_clang.
+        It matters as its own test (rather than being covered by the cmake/clang
+        test alone) because meson/ninja invoke the compiler with source paths
+        relative to the build directory, unlike cmake (with the Ninja or
+        Makefiles generators used here), which normally passes absolute source
+        paths. That relative-path compilation is what previously caused
+        -fdebug-prefix-map/-ffile-prefix-map underflow (DW_AT_name climbing
+        above DW_AT_comp_dir with excess dot-dot components) for devtool
+        workspaces, breaking source-level breakpoint resolution in CodeLLDB.
+        The cmake/clang test alone would not catch that regression.
+
+        This test uses the meson-example-clang recipe which is a meson-example
+        variant built with clang. It installs a separate binary
+        (mesonex-clang) so all four recipe variants (cmake/meson x gcc/clang)
+        can be installed in the same image without conflicts.
+        """
+        recipe_name = "meson-example-clang"
+        build_file = "meson.build"
+        testimage = "oe-selftest-image"
+
+        self._check_workspace()
+        self._write_bb_config([recipe_name], extra_packages=['lldb-server'])
+
+        # Build image with debug settings (lldb-server for clang) before starting QEMU
+        self._check_runqemu_prerequisites()
+        tempdir = self._devtool_ide_sdk_recipe(recipe_name, build_file, testimage)
+        runCmd('devtool ide-sdk %s %s -c --ide=code' % (recipe_name, testimage),
+               output_log=self._cmd_logger)
+
+        with runqemu(testimage, runqemuparams="nographic") as qemu:
+            # Re-run with actual QEMU IP; image is already built
+            bitbake_sdk_cmd = 'devtool ide-sdk %s %s -t root@%s -c --skip-bitbake --ide=code' % (
+                recipe_name, testimage, qemu.ip)
+            runCmd(bitbake_sdk_cmd, output_log=self._cmd_logger)
+
+            # Verify the meson build system still works (unchanged by clang/lldb support)
+            compile_cmd = self._verify_meson_build(tempdir, recipe_name)
+
+            # Verify the install && deploy-target task script exists
+            self._verify_install_script_code(tempdir, recipe_name)
+
+            # Verify extensions.json recommends CodeLLDB instead of / alongside cpptools
+            with open(os.path.join(tempdir, '.vscode', 'extensions.json')) as ext_j:
+                ext_d = json.load(ext_j)
+            recommendations = ext_d.get('recommendations', [])
+            self.assertIn('vadimcn.vscode-lldb', recommendations,
+                          'vadimcn.vscode-lldb should be recommended for clang recipes')
+
+            # Verify launch.json uses CodeLLDB format
+            self._verify_launch_json_lldb(tempdir)
+
+            # Verify deployment and lldb batch remote debugging work end-to-end,
+            # before and after a code change/recompile/redeploy cycle (see
+            # _lldb_cross_debugging_multi)
+            self._lldb_cross_debugging_multi(
+                tempdir, recipe_name, compile_cmd,
+                lambda magic_string: self._lldb_server_debugging_once(
+                    tempdir, qemu, magic_string))
+
     def test_devtool_ide_sdk_plugins(self):
         """Test that devtool ide-sdk can use plugins from other layers."""
 
