From f857fa1f0dfa634da0fdd89b26d0443d8de86c9c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 14 Sep 2025 19:22:33 -0500 Subject: [PATCH 01/10] [dashboard] Fix archive handler incorrectly deleting build folders instead of archiving them --- esphome/dashboard/web_server.py | 6 +- tests/dashboard/test_web_server.py | 128 +++++++++++++++++++++++++++-- 2 files changed, 122 insertions(+), 12 deletions(-) diff --git a/esphome/dashboard/web_server.py b/esphome/dashboard/web_server.py index 294a180794..e4c0b5b84a 100644 --- a/esphome/dashboard/web_server.py +++ b/esphome/dashboard/web_server.py @@ -1039,11 +1039,11 @@ class ArchiveRequestHandler(BaseHandler): storage_json = StorageJSON.load(storage_path) if storage_json is not None: - # Delete build folder (if exists) + # Move build folder to archive (if exists) name = storage_json.name build_folder = os.path.join(settings.config_dir, name) - if build_folder is not None: - shutil.rmtree(build_folder, os.path.join(archive_path, name)) + if os.path.exists(build_folder): + shutil.move(build_folder, os.path.join(archive_path, name)) class UnArchiveRequestHandler(BaseHandler): diff --git a/tests/dashboard/test_web_server.py b/tests/dashboard/test_web_server.py index e206090ac0..ea14309997 100644 --- a/tests/dashboard/test_web_server.py +++ b/tests/dashboard/test_web_server.py @@ -589,7 +589,7 @@ async def test_archive_request_handler_post( mock_ext_storage_path: MagicMock, tmp_path: Path, ) -> None: - """Test ArchiveRequestHandler.post method.""" + """Test ArchiveRequestHandler.post method without storage_json.""" # Set up temp directories config_dir = Path(get_fixture_path("conf")) @@ -599,14 +599,18 @@ async def test_archive_request_handler_post( test_config = config_dir / "test_archive.yaml" test_config.write_text("esphome:\n name: test_archive\n") - # Archive the configuration - response = await dashboard.fetch( - "/archive", - method="POST", - body="configuration=test_archive.yaml", - headers={"Content-Type": "application/x-www-form-urlencoded"}, - ) - assert response.code == 200 + # Mock storage_json to return None (no storage) + with patch("esphome.dashboard.web_server.StorageJSON.load") as mock_load: + mock_load.return_value = None + + # Archive the configuration + response = await dashboard.fetch( + "/archive", + method="POST", + body="configuration=test_archive.yaml", + headers={"Content-Type": "application/x-www-form-urlencoded"}, + ) + assert response.code == 200 # Verify file was moved to archive assert not test_config.exists() @@ -616,6 +620,112 @@ async def test_archive_request_handler_post( ).read_text() == "esphome:\n name: test_archive\n" +@pytest.mark.asyncio +async def test_archive_handler_with_build_folder( + dashboard: DashboardTestHelper, + mock_archive_storage_path: MagicMock, + mock_ext_storage_path: MagicMock, + mock_dashboard_settings: MagicMock, + tmp_path: Path, +) -> None: + """Test ArchiveRequestHandler.post with storage_json and build folder.""" + # Set up temp directories + config_dir = tmp_path / "config" + config_dir.mkdir() + archive_dir = tmp_path / "archive" + archive_dir.mkdir() + + # Create a test configuration file + configuration = "test_device.yaml" + test_config = config_dir / configuration + test_config.write_text("esphome:\n name: test_device\n") + + # Create build folder with content + build_folder = config_dir / "test_device" + build_folder.mkdir() + (build_folder / "firmware.bin").write_text("binary content") + (build_folder / ".pioenvs").mkdir() + + # Mock settings to use our temp directory + mock_dashboard_settings.config_dir = str(config_dir) + mock_dashboard_settings.rel_path.return_value = str(test_config) + mock_archive_storage_path.return_value = str(archive_dir) + + # Mock storage_json with device name + with patch("esphome.dashboard.web_server.StorageJSON.load") as mock_load: + mock_storage = MagicMock() + mock_storage.name = "test_device" + mock_load.return_value = mock_storage + + # Archive the configuration + response = await dashboard.fetch( + "/archive", + method="POST", + body=f"configuration={configuration}", + headers={"Content-Type": "application/x-www-form-urlencoded"}, + ) + assert response.code == 200 + + # Verify config file was moved to archive + assert not test_config.exists() + assert (archive_dir / configuration).exists() + + # Verify build folder was moved to archive + assert not build_folder.exists() + assert (archive_dir / "test_device").exists() + assert (archive_dir / "test_device" / "firmware.bin").exists() + + +@pytest.mark.asyncio +async def test_archive_handler_no_build_folder( + dashboard: DashboardTestHelper, + mock_archive_storage_path: MagicMock, + mock_ext_storage_path: MagicMock, + mock_dashboard_settings: MagicMock, + tmp_path: Path, +) -> None: + """Test ArchiveRequestHandler.post with storage_json but no build folder.""" + # Set up temp directories + config_dir = tmp_path / "config" + config_dir.mkdir() + archive_dir = tmp_path / "archive" + archive_dir.mkdir() + + # Create a test configuration file + configuration = "test_device.yaml" + test_config = config_dir / configuration + test_config.write_text("esphome:\n name: test_device\n") + + # Note: No build folder created + + # Mock settings to use our temp directory + mock_dashboard_settings.config_dir = str(config_dir) + mock_dashboard_settings.rel_path.return_value = str(test_config) + mock_archive_storage_path.return_value = str(archive_dir) + + # Mock storage_json with device name + with patch("esphome.dashboard.web_server.StorageJSON.load") as mock_load: + mock_storage = MagicMock() + mock_storage.name = "test_device" + mock_load.return_value = mock_storage + + # Archive the configuration (should not fail even without build folder) + response = await dashboard.fetch( + "/archive", + method="POST", + body=f"configuration={configuration}", + headers={"Content-Type": "application/x-www-form-urlencoded"}, + ) + assert response.code == 200 + + # Verify config file was moved to archive + assert not test_config.exists() + assert (archive_dir / configuration).exists() + + # Verify no build folder in archive (since it didn't exist) + assert not (archive_dir / "test_device").exists() + + @pytest.mark.skipif(os.name == "nt", reason="Unix sockets are not supported on Windows") @pytest.mark.usefixtures("mock_trash_storage_path", "mock_archive_storage_path") def test_start_web_server_with_unix_socket(tmp_path: Path) -> None: From fa00e07e1032163e037bd0d4ab1f04cf3b50adc6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 15 Sep 2025 17:19:28 -0500 Subject: [PATCH 02/10] fix --- esphome/dashboard/web_server.py | 9 ++++----- tests/dashboard/test_web_server.py | 18 +++++++++++------- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/esphome/dashboard/web_server.py b/esphome/dashboard/web_server.py index e4c0b5b84a..ef6ec061cd 100644 --- a/esphome/dashboard/web_server.py +++ b/esphome/dashboard/web_server.py @@ -1038,12 +1038,11 @@ class ArchiveRequestHandler(BaseHandler): shutil.move(config_file, os.path.join(archive_path, configuration)) storage_json = StorageJSON.load(storage_path) - if storage_json is not None: - # Move build folder to archive (if exists) - name = storage_json.name - build_folder = os.path.join(settings.config_dir, name) + if storage_json is not None and storage_json.build_path: + # Delete build folder (if exists) + build_folder = storage_json.build_path if os.path.exists(build_folder): - shutil.move(build_folder, os.path.join(archive_path, name)) + shutil.rmtree(build_folder, ignore_errors=True) class UnArchiveRequestHandler(BaseHandler): diff --git a/tests/dashboard/test_web_server.py b/tests/dashboard/test_web_server.py index ea14309997..6bb9a04f6c 100644 --- a/tests/dashboard/test_web_server.py +++ b/tests/dashboard/test_web_server.py @@ -634,14 +634,16 @@ async def test_archive_handler_with_build_folder( config_dir.mkdir() archive_dir = tmp_path / "archive" archive_dir.mkdir() + build_dir = tmp_path / "build" + build_dir.mkdir() # Create a test configuration file configuration = "test_device.yaml" test_config = config_dir / configuration test_config.write_text("esphome:\n name: test_device\n") - # Create build folder with content - build_folder = config_dir / "test_device" + # Create build folder with content (in proper location) + build_folder = build_dir / "test_device" build_folder.mkdir() (build_folder / "firmware.bin").write_text("binary content") (build_folder / ".pioenvs").mkdir() @@ -651,10 +653,11 @@ async def test_archive_handler_with_build_folder( mock_dashboard_settings.rel_path.return_value = str(test_config) mock_archive_storage_path.return_value = str(archive_dir) - # Mock storage_json with device name + # Mock storage_json with device name and build_path with patch("esphome.dashboard.web_server.StorageJSON.load") as mock_load: mock_storage = MagicMock() mock_storage.name = "test_device" + mock_storage.build_path = str(build_folder) mock_load.return_value = mock_storage # Archive the configuration @@ -670,10 +673,10 @@ async def test_archive_handler_with_build_folder( assert not test_config.exists() assert (archive_dir / configuration).exists() - # Verify build folder was moved to archive + # Verify build folder was deleted (not archived) assert not build_folder.exists() - assert (archive_dir / "test_device").exists() - assert (archive_dir / "test_device" / "firmware.bin").exists() + # Build folder should NOT be in archive + assert not (archive_dir / "test_device").exists() @pytest.mark.asyncio @@ -703,10 +706,11 @@ async def test_archive_handler_no_build_folder( mock_dashboard_settings.rel_path.return_value = str(test_config) mock_archive_storage_path.return_value = str(archive_dir) - # Mock storage_json with device name + # Mock storage_json with device name but no build_path with patch("esphome.dashboard.web_server.StorageJSON.load") as mock_load: mock_storage = MagicMock() mock_storage.name = "test_device" + mock_storage.build_path = None mock_load.return_value = mock_storage # Archive the configuration (should not fail even without build folder) From 47d24edd0eb1324b91f6b383b5a27697647239e0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 15 Sep 2025 17:23:01 -0500 Subject: [PATCH 03/10] cleanup --- tests/dashboard/test_web_server.py | 72 ++++++++++++++---------------- 1 file changed, 34 insertions(+), 38 deletions(-) diff --git a/tests/dashboard/test_web_server.py b/tests/dashboard/test_web_server.py index 6bb9a04f6c..6b5b2e7e6a 100644 --- a/tests/dashboard/test_web_server.py +++ b/tests/dashboard/test_web_server.py @@ -599,18 +599,14 @@ async def test_archive_request_handler_post( test_config = config_dir / "test_archive.yaml" test_config.write_text("esphome:\n name: test_archive\n") - # Mock storage_json to return None (no storage) - with patch("esphome.dashboard.web_server.StorageJSON.load") as mock_load: - mock_load.return_value = None - - # Archive the configuration - response = await dashboard.fetch( - "/archive", - method="POST", - body="configuration=test_archive.yaml", - headers={"Content-Type": "application/x-www-form-urlencoded"}, - ) - assert response.code == 200 + # Archive the configuration + response = await dashboard.fetch( + "/archive", + method="POST", + body="configuration=test_archive.yaml", + headers={"Content-Type": "application/x-www-form-urlencoded"}, + ) + assert response.code == 200 # Verify file was moved to archive assert not test_config.exists() @@ -626,6 +622,7 @@ async def test_archive_handler_with_build_folder( mock_archive_storage_path: MagicMock, mock_ext_storage_path: MagicMock, mock_dashboard_settings: MagicMock, + mock_storage_json: MagicMock, tmp_path: Path, ) -> None: """Test ArchiveRequestHandler.post with storage_json and build folder.""" @@ -654,20 +651,19 @@ async def test_archive_handler_with_build_folder( mock_archive_storage_path.return_value = str(archive_dir) # Mock storage_json with device name and build_path - with patch("esphome.dashboard.web_server.StorageJSON.load") as mock_load: - mock_storage = MagicMock() - mock_storage.name = "test_device" - mock_storage.build_path = str(build_folder) - mock_load.return_value = mock_storage + mock_storage = MagicMock() + mock_storage.name = "test_device" + mock_storage.build_path = str(build_folder) + mock_storage_json.load.return_value = mock_storage - # Archive the configuration - response = await dashboard.fetch( - "/archive", - method="POST", - body=f"configuration={configuration}", - headers={"Content-Type": "application/x-www-form-urlencoded"}, - ) - assert response.code == 200 + # Archive the configuration + response = await dashboard.fetch( + "/archive", + method="POST", + body=f"configuration={configuration}", + headers={"Content-Type": "application/x-www-form-urlencoded"}, + ) + assert response.code == 200 # Verify config file was moved to archive assert not test_config.exists() @@ -685,6 +681,7 @@ async def test_archive_handler_no_build_folder( mock_archive_storage_path: MagicMock, mock_ext_storage_path: MagicMock, mock_dashboard_settings: MagicMock, + mock_storage_json: MagicMock, tmp_path: Path, ) -> None: """Test ArchiveRequestHandler.post with storage_json but no build folder.""" @@ -707,20 +704,19 @@ async def test_archive_handler_no_build_folder( mock_archive_storage_path.return_value = str(archive_dir) # Mock storage_json with device name but no build_path - with patch("esphome.dashboard.web_server.StorageJSON.load") as mock_load: - mock_storage = MagicMock() - mock_storage.name = "test_device" - mock_storage.build_path = None - mock_load.return_value = mock_storage + mock_storage = MagicMock() + mock_storage.name = "test_device" + mock_storage.build_path = None + mock_storage_json.load.return_value = mock_storage - # Archive the configuration (should not fail even without build folder) - response = await dashboard.fetch( - "/archive", - method="POST", - body=f"configuration={configuration}", - headers={"Content-Type": "application/x-www-form-urlencoded"}, - ) - assert response.code == 200 + # Archive the configuration (should not fail even without build folder) + response = await dashboard.fetch( + "/archive", + method="POST", + body=f"configuration={configuration}", + headers={"Content-Type": "application/x-www-form-urlencoded"}, + ) + assert response.code == 200 # Verify config file was moved to archive assert not test_config.exists() From f7bfbb619d5eb9acd746259e1de33b421c0fa935 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 15 Sep 2025 17:24:45 -0500 Subject: [PATCH 04/10] cleanup --- tests/dashboard/test_web_server.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/tests/dashboard/test_web_server.py b/tests/dashboard/test_web_server.py index 6b5b2e7e6a..1ca7478fe8 100644 --- a/tests/dashboard/test_web_server.py +++ b/tests/dashboard/test_web_server.py @@ -590,16 +590,12 @@ async def test_archive_request_handler_post( tmp_path: Path, ) -> None: """Test ArchiveRequestHandler.post method without storage_json.""" - - # Set up temp directories config_dir = Path(get_fixture_path("conf")) archive_dir = tmp_path / "archive" - # Create a test configuration file test_config = config_dir / "test_archive.yaml" test_config.write_text("esphome:\n name: test_archive\n") - # Archive the configuration response = await dashboard.fetch( "/archive", method="POST", @@ -608,7 +604,6 @@ async def test_archive_request_handler_post( ) assert response.code == 200 - # Verify file was moved to archive assert not test_config.exists() assert (archive_dir / "test_archive.yaml").exists() assert ( From 55684d079e692c42a737db39557a474a2a0ee760 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 15 Sep 2025 17:24:58 -0500 Subject: [PATCH 05/10] cleanup --- tests/dashboard/test_web_server.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/tests/dashboard/test_web_server.py b/tests/dashboard/test_web_server.py index 1ca7478fe8..d3f5ba8edb 100644 --- a/tests/dashboard/test_web_server.py +++ b/tests/dashboard/test_web_server.py @@ -621,7 +621,6 @@ async def test_archive_handler_with_build_folder( tmp_path: Path, ) -> None: """Test ArchiveRequestHandler.post with storage_json and build folder.""" - # Set up temp directories config_dir = tmp_path / "config" config_dir.mkdir() archive_dir = tmp_path / "archive" @@ -629,29 +628,24 @@ async def test_archive_handler_with_build_folder( build_dir = tmp_path / "build" build_dir.mkdir() - # Create a test configuration file configuration = "test_device.yaml" test_config = config_dir / configuration test_config.write_text("esphome:\n name: test_device\n") - # Create build folder with content (in proper location) build_folder = build_dir / "test_device" build_folder.mkdir() (build_folder / "firmware.bin").write_text("binary content") (build_folder / ".pioenvs").mkdir() - # Mock settings to use our temp directory mock_dashboard_settings.config_dir = str(config_dir) mock_dashboard_settings.rel_path.return_value = str(test_config) mock_archive_storage_path.return_value = str(archive_dir) - # Mock storage_json with device name and build_path mock_storage = MagicMock() mock_storage.name = "test_device" mock_storage.build_path = str(build_folder) mock_storage_json.load.return_value = mock_storage - # Archive the configuration response = await dashboard.fetch( "/archive", method="POST", @@ -660,13 +654,10 @@ async def test_archive_handler_with_build_folder( ) assert response.code == 200 - # Verify config file was moved to archive assert not test_config.exists() assert (archive_dir / configuration).exists() - # Verify build folder was deleted (not archived) assert not build_folder.exists() - # Build folder should NOT be in archive assert not (archive_dir / "test_device").exists() From 62b713a04cb7ec2ffc14f95dc3dee11e061f59f2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 15 Sep 2025 17:25:23 -0500 Subject: [PATCH 06/10] cleanup --- tests/dashboard/test_web_server.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/tests/dashboard/test_web_server.py b/tests/dashboard/test_web_server.py index d3f5ba8edb..f434647cec 100644 --- a/tests/dashboard/test_web_server.py +++ b/tests/dashboard/test_web_server.py @@ -671,31 +671,24 @@ async def test_archive_handler_no_build_folder( tmp_path: Path, ) -> None: """Test ArchiveRequestHandler.post with storage_json but no build folder.""" - # Set up temp directories config_dir = tmp_path / "config" config_dir.mkdir() archive_dir = tmp_path / "archive" archive_dir.mkdir() - # Create a test configuration file configuration = "test_device.yaml" test_config = config_dir / configuration test_config.write_text("esphome:\n name: test_device\n") - # Note: No build folder created - - # Mock settings to use our temp directory mock_dashboard_settings.config_dir = str(config_dir) mock_dashboard_settings.rel_path.return_value = str(test_config) mock_archive_storage_path.return_value = str(archive_dir) - # Mock storage_json with device name but no build_path mock_storage = MagicMock() mock_storage.name = "test_device" mock_storage.build_path = None mock_storage_json.load.return_value = mock_storage - # Archive the configuration (should not fail even without build folder) response = await dashboard.fetch( "/archive", method="POST", @@ -704,11 +697,8 @@ async def test_archive_handler_no_build_folder( ) assert response.code == 200 - # Verify config file was moved to archive assert not test_config.exists() assert (archive_dir / configuration).exists() - - # Verify no build folder in archive (since it didn't exist) assert not (archive_dir / "test_device").exists() From 601c7929131a44daaede2c9c11c1b9113790fd8c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 15 Sep 2025 17:25:56 -0500 Subject: [PATCH 07/10] cleanup --- esphome/dashboard/web_server.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/esphome/dashboard/web_server.py b/esphome/dashboard/web_server.py index ef6ec061cd..e6c5fd3d84 100644 --- a/esphome/dashboard/web_server.py +++ b/esphome/dashboard/web_server.py @@ -1040,9 +1040,7 @@ class ArchiveRequestHandler(BaseHandler): storage_json = StorageJSON.load(storage_path) if storage_json is not None and storage_json.build_path: # Delete build folder (if exists) - build_folder = storage_json.build_path - if os.path.exists(build_folder): - shutil.rmtree(build_folder, ignore_errors=True) + shutil.rmtree(storage_json.build_path, ignore_errors=True) class UnArchiveRequestHandler(BaseHandler): From 50f22a362ffb67dd032a45209829a51ba8f86ee0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 15 Sep 2025 17:28:00 -0500 Subject: [PATCH 08/10] cleanup --- tests/dashboard/test_web_server.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/dashboard/test_web_server.py b/tests/dashboard/test_web_server.py index f434647cec..1938617f20 100644 --- a/tests/dashboard/test_web_server.py +++ b/tests/dashboard/test_web_server.py @@ -590,12 +590,16 @@ async def test_archive_request_handler_post( tmp_path: Path, ) -> None: """Test ArchiveRequestHandler.post method without storage_json.""" + + # Set up temp directories config_dir = Path(get_fixture_path("conf")) archive_dir = tmp_path / "archive" + # Create a test configuration file test_config = config_dir / "test_archive.yaml" test_config.write_text("esphome:\n name: test_archive\n") + # Archive the configuration response = await dashboard.fetch( "/archive", method="POST", @@ -604,6 +608,7 @@ async def test_archive_request_handler_post( ) assert response.code == 200 + # Verify file was moved to archive assert not test_config.exists() assert (archive_dir / "test_archive.yaml").exists() assert ( From f3c156ca578a3771e27426c5ad9731602a7cdd61 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 15 Sep 2025 17:29:51 -0500 Subject: [PATCH 09/10] add more coverage to make sure we are more careful about deletes --- tests/unit_tests/core/test_config.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/unit_tests/core/test_config.py b/tests/unit_tests/core/test_config.py index e520db9e33..4c543bff9c 100644 --- a/tests/unit_tests/core/test_config.py +++ b/tests/unit_tests/core/test_config.py @@ -384,6 +384,8 @@ def test_preload_core_config_basic(setup_core: Path) -> None: assert platform == "esp32" assert KEY_CORE in CORE.data assert CONF_BUILD_PATH in config[CONF_ESPHOME] + # Verify default build path is "build/" + assert config[CONF_ESPHOME][CONF_BUILD_PATH].endswith("build/test_device") def test_preload_core_config_with_build_path(setup_core: Path) -> None: @@ -418,6 +420,8 @@ def test_preload_core_config_env_build_path(setup_core: Path) -> None: assert CONF_BUILD_PATH in config[CONF_ESPHOME] assert "test_device" in config[CONF_ESPHOME][CONF_BUILD_PATH] + # Verify it uses the env var path with device name appended + assert config[CONF_ESPHOME][CONF_BUILD_PATH].endswith("/env/build/test_device") assert platform == "rp2040" From f91a6979b4a02f15d7ef3e1601f27030208bba36 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 15 Sep 2025 17:37:35 -0500 Subject: [PATCH 10/10] add more coverage to make sure we are more careful about deletes --- tests/unit_tests/core/test_config.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tests/unit_tests/core/test_config.py b/tests/unit_tests/core/test_config.py index 4c543bff9c..7d3b90794b 100644 --- a/tests/unit_tests/core/test_config.py +++ b/tests/unit_tests/core/test_config.py @@ -385,7 +385,8 @@ def test_preload_core_config_basic(setup_core: Path) -> None: assert KEY_CORE in CORE.data assert CONF_BUILD_PATH in config[CONF_ESPHOME] # Verify default build path is "build/" - assert config[CONF_ESPHOME][CONF_BUILD_PATH].endswith("build/test_device") + build_path = config[CONF_ESPHOME][CONF_BUILD_PATH] + assert build_path.endswith(os.path.join("build", "test_device")) def test_preload_core_config_with_build_path(setup_core: Path) -> None: @@ -421,7 +422,11 @@ def test_preload_core_config_env_build_path(setup_core: Path) -> None: assert CONF_BUILD_PATH in config[CONF_ESPHOME] assert "test_device" in config[CONF_ESPHOME][CONF_BUILD_PATH] # Verify it uses the env var path with device name appended - assert config[CONF_ESPHOME][CONF_BUILD_PATH].endswith("/env/build/test_device") + build_path = config[CONF_ESPHOME][CONF_BUILD_PATH] + expected_path = os.path.join("/env/build", "test_device") + assert build_path == expected_path or build_path == expected_path.replace( + "/", os.sep + ) assert platform == "rp2040"