Sfoglia il codice sorgente

tests: test metadata manager features.txt and standard artifacts collection

Shiv Tyagi 1 mese fa
parent
commit
a23e63a4a1

+ 12 - 0
tests/metadata_manager/fixtures/manifest_sample.json

@@ -13,6 +13,18 @@
       "mav-type": "QUADROTOR",
       "image_size": 100
     },
+    {
+      "vehicletype": "Copter",
+      "platform": "CubeOrange",
+      "git-sha": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
+      "url": "https://firmware.ardupilot.org/Copter/stable/CubeOrange/arducopter.apj",
+      "mav-firmware-version-type": "OFFICIAL",
+      "mav-firmware-version": "4.6.3",
+      "format": "apj",
+      "latest": 0,
+      "mav-type": "QUADROTOR",
+      "image_size": 100
+    },
     {
       "vehicletype": "Copter",
       "platform": "CubeOrange-heli",

+ 161 - 0
tests/metadata_manager/test_features_txt_client.py

@@ -0,0 +1,161 @@
+import requests
+from unittest.mock import Mock, patch
+
+import dill
+
+from metadata_manager.firmware_server.features import (
+    FeaturesTxtClient,
+    parse_features_txt,
+)
+
+
+class TestParseFeaturesTxt:
+    def test_parses_enabled_and_disabled_features(self):
+        text = "\n".join([
+            "HAL_LOGGING_ENABLED",
+            "!HAL_PROFILER_ENABLED",
+            "# comment",
+            "",
+            "AP_FENCE_ENABLED",
+        ])
+        assert parse_features_txt(text) == {
+            "HAL_LOGGING_ENABLED": 1,
+            "HAL_PROFILER_ENABLED": 0,
+            "AP_FENCE_ENABLED": 1,
+        }
+
+
+class TestFeaturesTxtClient:
+    def test_get_defaults_fetches_and_caches_with_etag(self):
+        client = FeaturesTxtClient(caching_enabled=True)
+        client._redis_client = Mock()
+        client._redis_client.get.return_value = None
+        response = Mock(
+            status_code=200,
+            text="HAL_LOGGING_ENABLED\n!HAL_PROFILER_ENABLED",
+            headers={"ETag": '"abc"'},
+        )
+        response.raise_for_status = Mock()
+
+        with patch(
+            "metadata_manager.firmware_server.features.requests.get",
+            return_value=response,
+        ) as mock_get:
+            result = client.get_defaults("https://example.com/features.txt")
+
+        assert result == {
+            "HAL_LOGGING_ENABLED": 1,
+            "HAL_PROFILER_ENABLED": 0,
+        }
+        mock_get.assert_called_once()
+        client._redis_client.set.assert_called_once()
+        stored = dill.loads(client._redis_client.set.call_args.kwargs["value"])
+        assert stored == {
+            "etag": '"abc"',
+            "defaults": result,
+        }
+
+    def test_get_defaults_skips_cache_without_etag(self):
+        client = FeaturesTxtClient(caching_enabled=True)
+        client._redis_client = Mock()
+        client._redis_client.get.return_value = None
+        response = Mock(status_code=200, text="HAL_LOGGING_ENABLED", headers={})
+        response.raise_for_status = Mock()
+
+        with patch(
+            "metadata_manager.firmware_server.features.requests.get",
+            return_value=response,
+        ):
+            result = client.get_defaults("https://example.com/features.txt")
+
+        assert result == {"HAL_LOGGING_ENABLED": 1}
+        client._redis_client.set.assert_not_called()
+
+    def test_get_defaults_revalidates_cached_entry_with_if_none_match(self):
+        client = FeaturesTxtClient(caching_enabled=True)
+        client._redis_client = Mock()
+        cached = {
+            "etag": '"abc"',
+            "defaults": {"HAL_LOGGING_ENABLED": 1},
+        }
+        client._redis_client.get.return_value = dill.dumps(cached)
+        response = Mock(status_code=304, headers={})
+        response.raise_for_status = Mock()
+
+        with patch(
+            "metadata_manager.firmware_server.features.requests.get",
+            return_value=response,
+        ) as mock_get:
+            result = client.get_defaults("https://example.com/features.txt")
+
+        assert result == cached["defaults"]
+        mock_get.assert_called_once_with(
+            "https://example.com/features.txt",
+            headers={
+                "User-Agent": "CustomBuild/1.0",
+                "Accept-Encoding": "identity",
+                "If-None-Match": '"abc"',
+            },
+            timeout=30,
+        )
+        client._redis_client.set.assert_not_called()
+
+    def test_get_defaults_updates_cache_when_etag_changes(self):
+        client = FeaturesTxtClient(caching_enabled=True)
+        client._redis_client = Mock()
+        cached = {
+            "etag": '"old"',
+            "defaults": {"OLD_FEATURE": 1},
+        }
+        client._redis_client.get.return_value = dill.dumps(cached)
+        response = Mock(
+            status_code=200,
+            text="NEW_FEATURE\n!OLD_FEATURE",
+            headers={"ETag": '"new"'},
+        )
+        response.raise_for_status = Mock()
+
+        with patch(
+            "metadata_manager.firmware_server.features.requests.get",
+            return_value=response,
+        ) as mock_get:
+            result = client.get_defaults("https://example.com/features.txt")
+
+        assert result == {"NEW_FEATURE": 1, "OLD_FEATURE": 0}
+        mock_get.assert_called_once_with(
+            "https://example.com/features.txt",
+            headers={
+                "User-Agent": "CustomBuild/1.0",
+                "Accept-Encoding": "identity",
+                "If-None-Match": '"old"',
+            },
+            timeout=30,
+        )
+        stored = dill.loads(client._redis_client.set.call_args.kwargs["value"])
+        assert stored["etag"] == '"new"'
+        assert stored["defaults"] == result
+
+    def test_get_defaults_returns_none_on_fetch_failure(self):
+        client = FeaturesTxtClient(caching_enabled=False)
+
+        with patch(
+            "metadata_manager.firmware_server.features.requests.get",
+            side_effect=requests.RequestException("network error"),
+        ):
+            result = client.get_defaults("https://example.com/features.txt")
+
+        assert result is None
+
+    def test_get_defaults_does_not_cache_failure_when_caching_enabled(self):
+        client = FeaturesTxtClient(caching_enabled=True)
+        client._redis_client = Mock()
+        client._redis_client.get.return_value = None
+
+        with patch(
+            "metadata_manager.firmware_server.features.requests.get",
+            side_effect=requests.RequestException("network error"),
+        ):
+            result = client.get_defaults("https://example.com/missing/features.txt")
+
+        assert result is None
+        client._redis_client.set.assert_not_called()

+ 109 - 3
tests/metadata_manager/test_firmware_server.py

@@ -3,8 +3,9 @@ import lzma
 from pathlib import Path
 from unittest.mock import Mock, patch
 
-from metadata_manager import ManifestClient, ManifestIndex
+from metadata_manager import ManifestClient, ManifestIndex, ManifestJSON
 from metadata_manager.firmware_server.client import _CacheMeta
+from metadata_manager.firmware_server.index import latest_features_txt_url
 
 
 FIXTURES_DIR = Path(__file__).parent / "fixtures"
@@ -34,10 +35,115 @@ class TestManifestIndex:
         assert len(latest) == 1
         assert latest[0].commit_reference.startswith("eeee")
         assert latest[0].version_number == "NA"
-        assert latest[0].ap_build_artifacts_url == (
-            "https://firmware.ardupilot.org/Copter/latest"
+
+    def test_indexes_board_artifacts_for_vehicle_version_board(self):
+        index = ManifestIndex.build(SAMPLE_MANIFEST)
+
+        copter_artifacts = index.get_board_artifacts(
+            "copter", "stable", "4.6.3", "CubeOrange"
+        )
+        assert len(copter_artifacts) == 1
+        assert copter_artifacts[0].name == "arducopter.apj"
+        assert copter_artifacts[0].format == "apj"
+        assert copter_artifacts[0].url.endswith("/Copter/stable-4.6.3/CubeOrange/arducopter.apj")
+
+        heli_artifacts = index.get_board_artifacts(
+            "heli", "stable", "4.6.3", "CubeOrange"
+        )
+        assert len(heli_artifacts) == 1
+        assert heli_artifacts[0].name == "arducopter-heli.apj"
+
+        latest_artifacts = index.get_board_artifacts(
+            "copter", "latest", "NA", "CubeOrange"
+        )
+        assert len(latest_artifacts) == 1
+        assert latest_artifacts[0].url.endswith("/Copter/latest/CubeOrange/arducopter.apj")
+
+    def test_board_artifacts_dedupe_generic_stable_alias(self):
+        index = ManifestIndex.build(SAMPLE_MANIFEST)
+
+        copter_artifacts = index.get_board_artifacts(
+            "copter", "stable", "4.6.3", "CubeOrange"
+        )
+        assert len(copter_artifacts) == 1
+        assert copter_artifacts[0].url.endswith("/Copter/stable-4.6.3/CubeOrange/arducopter.apj")
+
+        heli_artifacts = index.get_board_artifacts(
+            "heli", "stable", "4.6.3", "CubeOrange"
+        )
+        assert len(heli_artifacts) == 1
+        assert heli_artifacts[0].name == "arducopter-heli.apj"
+
+        latest_artifacts = index.get_board_artifacts(
+            "copter", "latest", "NA", "CubeOrange"
+        )
+        assert len(latest_artifacts) == 1
+        assert latest_artifacts[0].url.endswith("/Copter/latest/CubeOrange/arducopter.apj")
+
+    def test_board_artifacts_missing_returns_empty_list(self):
+        index = ManifestIndex.build(SAMPLE_MANIFEST)
+
+        assert index.get_board_artifacts("copter", "stable", "4.6.3", "UnknownBoard") == []
+
+    def test_get_features_txt_url_from_manifest_artifact(self):
+        index = ManifestIndex.build(SAMPLE_MANIFEST)
+
+        url = index.get_features_txt_url("copter", "stable", "4.6.3", "CubeOrange")
+        assert url == (
+            "https://firmware.ardupilot.org/Copter/stable-4.6.3/"
+            "CubeOrange/features.txt"
+        )
+
+        heli_url = index.get_features_txt_url("heli", "stable", "4.6.3", "CubeOrange")
+        assert heli_url == (
+            "https://firmware.ardupilot.org/Copter/stable-4.6.3/"
+            "CubeOrange-heli/features.txt"
         )
 
+    def test_get_features_txt_url_missing_board_returns_none(self):
+        index = ManifestIndex.build(SAMPLE_MANIFEST)
+
+        assert index.get_features_txt_url(
+            "copter", "stable", "4.6.3", "UnknownBoard"
+        ) is None
+
+
+class TestLatestFeaturesTxtUrl:
+    def test_copter_board(self):
+        assert latest_features_txt_url("copter", "CubeOrange") == (
+            "https://firmware.ardupilot.org/Copter/latest/CubeOrange/features.txt"
+        )
+
+    def test_heli_board(self):
+        assert latest_features_txt_url("heli", "CubeOrange") == (
+            "https://firmware.ardupilot.org/Copter/latest/CubeOrange-heli/features.txt"
+        )
+
+
+class TestManifestJSONFeaturesUrl:
+    def test_tag_release_uses_latest_url(self):
+        manifest_json = ManifestJSON(url="https://example.com/manifest.json", cache_dir="/tmp")
+        assert manifest_json.get_features_txt_url(
+            "copter", "tag", "my-feature", "CubeOrange"
+        ) == latest_features_txt_url("copter", "CubeOrange")
+
+    def test_stable_release_uses_manifest_index(self):
+        manifest_json = ManifestJSON(url="https://example.com/manifest.json", cache_dir="/tmp")
+        manifest_json._index = ManifestIndex.build(SAMPLE_MANIFEST)
+
+        assert manifest_json.get_features_txt_url(
+            "copter", "stable", "4.6.3", "CubeOrange"
+        ) == (
+            "https://firmware.ardupilot.org/Copter/stable-4.6.3/"
+            "CubeOrange/features.txt"
+        )
+
+    def test_unavailable_manifest_returns_none_for_stable(self):
+        manifest_json = ManifestJSON(url="https://example.com/manifest.json", cache_dir="/tmp")
+        assert manifest_json.get_features_txt_url(
+            "copter", "stable", "4.6.3", "CubeOrange"
+        ) is None
+
 
 class TestManifestClientCache:
     def test_uses_cache_on_304(self, tmp_path):

+ 0 - 1
tests/metadata_manager/test_versions_manager.py

@@ -71,7 +71,6 @@ class TestVersionsManagerDedup:
             commit_ref=commit_ref,
             release_type=release_type,
             version_number=version_number,
-            ap_build_artifacts_url=None,
         )
 
     def test_stable_wins_when_beta_is_seen_first(self, versions_manager):

+ 2 - 0
tests/web/conftest.py

@@ -247,6 +247,8 @@ def app_with_mocked_dependencies(
         # Create mock AP source metadata fetcher
         mock_ap_src_fetcher = Mock()
         app.state.ap_src_metadata_fetcher = mock_ap_src_fetcher
+        app.state.manifest_json = Mock()
+        app.state.features_txt_client = Mock()
 
         # Don't start background tasks in test mode
         # versions_manager.start()

+ 0 - 2
tests/web/test_builds_service.py

@@ -53,14 +53,12 @@ def make_version_info(
     commit_ref="refs/tags/Copter-4.5.0",
     release_type="stable",
     version_number="4.5.0",
-    ap_build_artifacts_url=None,
 ):
     return VersionInfo(
         remote_info=RemoteInfo(name=remote_name, url=remote_url),
         commit_ref=commit_ref,
         release_type=release_type,
         version_number=version_number,
-        ap_build_artifacts_url=ap_build_artifacts_url,
     )
 
 

+ 60 - 0
tests/web/test_vehicles_api.py

@@ -9,6 +9,7 @@ from web.schemas import (
     VehicleBase,
     VersionOut,
     BoardOut,
+    StandardArtifactOut,
     FeatureOut,
     CategoryBase,
     FeatureDefault,
@@ -59,6 +60,15 @@ class TestVehiclesAPI:
             version_id=version_id,
         )
 
+    @staticmethod
+    def dummy_standard_artifact():
+        return StandardArtifactOut(
+            name="arducopter.apj",
+            url="https://firmware.ardupilot.org/Copter/stable-4.6.3/CubeOrange/arducopter.apj",
+            format="apj",
+            size=100,
+        )
+
     @staticmethod
     def dummy_feature(
         vehicle_id="copter",
@@ -442,6 +452,56 @@ class TestVehiclesAPI:
             response = method("/api/v1/vehicles/copter/versions/v1/boards/b1")
             assert response.status_code == status.HTTP_405_METHOD_NOT_ALLOWED
 
+    # GET /vehicles/{vehicle_id}/versions/{version_id}/boards/{board_id}/standard_artifacts
+
+    _STANDARD_ARTIFACTS_URL = (
+        "/api/v1/vehicles/copter/versions/copter-4.5.0-stable/"
+        "boards/MatekH743/standard_artifacts"
+    )
+
+    def test_list_board_standard_artifacts_returns_200(self, client):
+        mock_vehicles_service = Mock()
+        mock_vehicles_service.get_board_standard_artifacts.return_value = [
+            self.dummy_standard_artifact()
+        ]
+        with self.override_vehicles_service(client, mock_vehicles_service):
+            response = client.get(self._STANDARD_ARTIFACTS_URL)
+
+        assert response.status_code == status.HTTP_200_OK
+
+    def test_list_board_standard_artifacts_returns_404_when_not_found(self, client):
+        mock_vehicles_service = Mock()
+        mock_vehicles_service.get_board_standard_artifacts.return_value = None
+        with self.override_vehicles_service(client, mock_vehicles_service):
+            response = client.get(self._STANDARD_ARTIFACTS_URL)
+
+        assert response.status_code == status.HTTP_404_NOT_FOUND
+
+    def test_list_board_standard_artifacts_response_schema(self, client):
+        mock_vehicles_service = Mock()
+        mock_vehicles_service.get_board_standard_artifacts.return_value = [
+            self.dummy_standard_artifact()
+        ]
+        with self.override_vehicles_service(client, mock_vehicles_service):
+            response = client.get(self._STANDARD_ARTIFACTS_URL)
+
+        data = response.json()
+        assert data[0]["name"] == "arducopter.apj"
+        assert data[0]["format"] == "apj"
+        assert "url" in data[0]
+
+    def test_list_board_standard_artifacts_service_called_with_correct_ids(self, client):
+        mock_vehicles_service = Mock()
+        mock_vehicles_service.get_board_standard_artifacts.return_value = [
+            self.dummy_standard_artifact()
+        ]
+        with self.override_vehicles_service(client, mock_vehicles_service):
+            client.get(self._STANDARD_ARTIFACTS_URL)
+
+        mock_vehicles_service.get_board_standard_artifacts.assert_called_once_with(
+            "copter", "copter-4.5.0-stable", "MatekH743"
+        )
+
     # GET /vehicles/{vehicle_id}/versions/{version_id}/boards/{board_id}/features
 
     _FEATURES_URL = "/api/v1/vehicles/copter/versions/copter-4.5.0-stable/boards/MatekH743/features"

+ 208 - 79
tests/web/test_vehicles_service.py

@@ -5,6 +5,7 @@ import pytest
 from unittest.mock import Mock
 
 from metadata_manager import (
+    BoardArtifact,
     RemoteInfo,
     Vehicle,
     VersionInfo,
@@ -13,11 +14,34 @@ from web.services.vehicles import VehiclesService
 
 
 @pytest.fixture
-def service(mock_vehicles_manager, mock_versions_manager, mock_ap_src_metadata_fetcher, mock_git_repo):
+def mock_manifest_json():
+    mock = Mock()
+    mock.get_features_txt_url.return_value = None
+    return mock
+
+
+@pytest.fixture
+def mock_features_txt_client():
+    mock = Mock()
+    mock.get_defaults.return_value = None
+    return mock
+
+
+@pytest.fixture
+def service(
+    mock_vehicles_manager,
+    mock_versions_manager,
+    mock_ap_src_metadata_fetcher,
+    mock_manifest_json,
+    mock_features_txt_client,
+    mock_git_repo,
+):
     return VehiclesService(
         vehicle_manager=mock_vehicles_manager,
         versions_manager=mock_versions_manager,
         ap_src_metadata_fetcher=mock_ap_src_metadata_fetcher,
+        manifest_json=mock_manifest_json,
+        features_txt_client=mock_features_txt_client,
         repo=mock_git_repo,
     )
 
@@ -154,7 +178,6 @@ class TestVehiclesService:
                 commit_ref="refs/heads/master",
                 release_type="latest",
                 version_number="NA",
-                ap_build_artifacts_url=None,
             ),
         ]
         versions = service.get_versions("copter")
@@ -169,21 +192,18 @@ class TestVehiclesService:
                 commit_ref="refs/heads/master",
                 release_type="latest",
                 version_number="NA",
-                ap_build_artifacts_url=None,
             ),
             VersionInfo(
                 remote_info=RemoteInfo(name="ardupilot", url="https://github.com/ArduPilot/ardupilot.git"),
                 commit_ref="refs/tags/Copter-4.5.0",
                 release_type="stable",
                 version_number="4.5.0",
-                ap_build_artifacts_url=None,
             ),
             VersionInfo(
                 remote_info=RemoteInfo(name="ardupilot", url="https://github.com/ArduPilot/ardupilot.git"),
                 commit_ref="refs/tags/Copter-4.6.0-beta",
                 release_type="beta",
                 version_number="4.6.0",
-                ap_build_artifacts_url=None,
             ),
         ]
         versions = service.get_versions("copter")
@@ -198,21 +218,18 @@ class TestVehiclesService:
                 commit_ref="refs/tags/Copter-4.5.0",
                 release_type="stable",
                 version_number="4.5.0",
-                ap_build_artifacts_url=None,
             ),
             VersionInfo(
                 remote_info=RemoteInfo(name="ardupilot", url="https://github.com/ArduPilot/ardupilot.git"),
                 commit_ref="refs/heads/master",
                 release_type="latest",
                 version_number="NA",
-                ap_build_artifacts_url=None,
             ),
             VersionInfo(
                 remote_info=RemoteInfo(name="ardupilot", url="https://github.com/ArduPilot/ardupilot.git"),
                 commit_ref="refs/tags/Copter-4.6.0-beta",
                 release_type="beta",
                 version_number="4.6.0",
-                ap_build_artifacts_url=None,
             ),
         ]
         versions = service.get_versions("copter")
@@ -241,21 +258,18 @@ class TestVehiclesService:
                 commit_ref="refs/tags/Copter-4.5.0",
                 release_type="stable",
                 version_number="4.5.0",
-                ap_build_artifacts_url=None,
             ),
             VersionInfo(
                 remote_info=RemoteInfo(name="ardupilot", url="https://github.com/ArduPilot/ardupilot.git"),
                 commit_ref="refs/tags/Copter-4.6.0-beta",
                 release_type="beta",
                 version_number="4.6.0",
-                ap_build_artifacts_url=None,
             ),
             VersionInfo(
                 remote_info=RemoteInfo(name="ardupilot", url="https://github.com/ArduPilot/ardupilot.git"),
                 commit_ref="refs/heads/master",
                 release_type="latest",
                 version_number="NA",
-                ap_build_artifacts_url=None,
             ),
         ]
         versions = service.get_versions("copter", type_filter="stable")
@@ -273,14 +287,12 @@ class TestVehiclesService:
                 commit_ref="refs/tags/Copter-4.5.0",
                 release_type="stable",
                 version_number="4.5.0",
-                ap_build_artifacts_url=None,
             ),
             VersionInfo(
                 remote_info=RemoteInfo(name="ardupilot", url="https://github.com/ArduPilot/ardupilot.git"),
                 commit_ref="refs/tags/Copter-4.6.0-beta",
                 release_type="beta",
                 version_number="4.6.0",
-                ap_build_artifacts_url=None,
             ),
         ]
         versions = service.get_versions("copter", type_filter="latest")
@@ -297,21 +309,18 @@ class TestVehiclesService:
                 commit_ref="refs/tags/Copter-4.5.0",
                 release_type="stable",
                 version_number="4.5.0",
-                ap_build_artifacts_url=None,
             ),
             VersionInfo(
                 remote_info=RemoteInfo(name="ardupilot", url="https://github.com/ArduPilot/ardupilot.git"),
                 commit_ref="refs/tags/Copter-4.6.0-beta",
                 release_type="beta",
                 version_number="4.6.0",
-                ap_build_artifacts_url=None,
             ),
             VersionInfo(
                 remote_info=RemoteInfo(name="ardupilot", url="https://github.com/ArduPilot/ardupilot.git"),
                 commit_ref="refs/heads/master",
                 release_type="latest",
                 version_number="NA",
-                ap_build_artifacts_url=None,
             ),
         ]
         versions = service.get_versions("copter")
@@ -328,21 +337,18 @@ class TestVehiclesService:
                 commit_ref="refs/tags/Copter-4.4.0",
                 release_type="stable",
                 version_number="4.4.0",
-                ap_build_artifacts_url=None,
             ),
             VersionInfo(
                 remote_info=RemoteInfo(name="ardupilot", url="https://github.com/ArduPilot/ardupilot.git"),
                 commit_ref="refs/tags/Copter-4.5.0",
                 release_type="stable",
                 version_number="4.5.0",
-                ap_build_artifacts_url=None,
             ),
             VersionInfo(
                 remote_info=RemoteInfo(name="ardupilot", url="https://github.com/ArduPilot/ardupilot.git"),
                 commit_ref="refs/heads/master",
                 release_type="latest",
                 version_number="NA",
-                ap_build_artifacts_url=None,
             ),
         ]
         versions = service.get_versions("copter", type_filter="stable")
@@ -360,7 +366,6 @@ class TestVehiclesService:
                 commit_ref="refs/heads/master",
                 release_type="latest",
                 version_number="NA",
-                ap_build_artifacts_url=None,
             ),
         ]
         versions = service.get_versions("copter")
@@ -377,7 +382,6 @@ class TestVehiclesService:
                 commit_ref="refs/tags/Copter-4.5.0",
                 release_type="stable",
                 version_number="4.5.0",
-                ap_build_artifacts_url=None,
             ),
         ]
         versions = service.get_versions("copter")
@@ -393,7 +397,6 @@ class TestVehiclesService:
             commit_ref="refs/tags/Copter-4.5.0",
             release_type="stable",
             version_number="4.5.0",
-            ap_build_artifacts_url=None,
         )
         mock_versions_manager.get_versions_for_vehicle.return_value = [version_info]
 
@@ -410,7 +413,6 @@ class TestVehiclesService:
                 commit_ref="refs/tags/Copter-4.5.0",
                 release_type="stable",
                 version_number="4.5.0",
-                ap_build_artifacts_url=None,
             ),
         ]
 
@@ -433,21 +435,18 @@ class TestVehiclesService:
             commit_ref="refs/tags/Copter-4.5.0",
             release_type="stable",
             version_number="4.5.0",
-            ap_build_artifacts_url=None,
         )
         beta_info = VersionInfo(
             remote_info=RemoteInfo(name="ardupilot", url="https://github.com/ArduPilot/ardupilot.git"),
             commit_ref="refs/tags/Copter-4.6.0-beta",
             release_type="beta",
             version_number="4.6.0",
-            ap_build_artifacts_url=None,
         )
         latest_info = VersionInfo(
             remote_info=RemoteInfo(name="ardupilot", url="https://github.com/ArduPilot/ardupilot.git"),
             commit_ref="refs/heads/master",
             release_type="latest",
             version_number="NA",
-            ap_build_artifacts_url=None,
         )
         mock_versions_manager.get_versions_for_vehicle.return_value = [
             stable_info, beta_info, latest_info,
@@ -491,7 +490,6 @@ class TestVehiclesService:
             commit_ref="refs/tags/Copter-4.5.0",
             release_type="stable",
             version_number="4.5.0",
-            ap_build_artifacts_url=None,
         )
         mock_versions_manager.get_version_info.return_value = version_info
         mock_ap_src_metadata_fetcher.get_boards.return_value = []
@@ -509,7 +507,6 @@ class TestVehiclesService:
             commit_ref="refs/tags/Copter-4.5.0",
             release_type="stable",
             version_number="4.5.0",
-            ap_build_artifacts_url=None,
         )
         mock_versions_manager.get_version_info.return_value = version_info
         mock_ap_src_metadata_fetcher.get_boards.return_value = ["CubeRed"]
@@ -529,7 +526,6 @@ class TestVehiclesService:
             commit_ref="refs/tags/Copter-4.5.0",
             release_type="stable",
             version_number="4.5.0",
-            ap_build_artifacts_url=None,
         )
         mock_versions_manager.get_version_info.return_value = version_info
         mock_ap_src_metadata_fetcher.get_boards.return_value = [
@@ -550,7 +546,6 @@ class TestVehiclesService:
             commit_ref="refs/tags/Copter-4.5.0",
             release_type="stable",
             version_number="4.5.0",
-            ap_build_artifacts_url=None,
         )
         mock_versions_manager.get_version_info.return_value = version_info
         mock_ap_src_metadata_fetcher.get_boards.return_value = ["CubeRed"]
@@ -569,7 +564,6 @@ class TestVehiclesService:
             commit_ref="refs/tags/Copter-4.5.0",
             release_type="stable",
             version_number="4.5.0",
-            ap_build_artifacts_url=None,
         )
         mock_versions_manager.get_version_info.return_value = version_info
         mock_ap_src_metadata_fetcher.get_boards.return_value = []
@@ -591,7 +585,6 @@ class TestVehiclesService:
             commit_ref="refs/tags/Copter-4.5.0",
             release_type="stable",
             version_number="4.5.0",
-            ap_build_artifacts_url=None,
         )
         mock_versions_manager.get_version_info.return_value = version_info
         mock_ap_src_metadata_fetcher.get_boards.return_value = ["CubeRed", "CubeOrange"]
@@ -611,7 +604,6 @@ class TestVehiclesService:
             commit_ref="refs/tags/Copter-4.5.0",
             release_type="stable",
             version_number="4.5.0",
-            ap_build_artifacts_url=None,
         )
         mock_versions_manager.get_version_info.return_value = version_info
         mock_ap_src_metadata_fetcher.get_boards.return_value = ["CubeRed", "CubeOrange"]
@@ -629,7 +621,6 @@ class TestVehiclesService:
             commit_ref="refs/tags/Copter-4.5.0",
             release_type="stable",
             version_number="4.5.0",
-            ap_build_artifacts_url=None,
         )
         mock_versions_manager.get_version_info.return_value = version_info
         mock_ap_src_metadata_fetcher.get_boards.return_value = [
@@ -662,7 +653,6 @@ class TestVehiclesService:
             commit_ref="refs/tags/Copter-4.5.0",
             release_type="stable",
             version_number="4.5.0",
-            ap_build_artifacts_url=None,
         )
         mock_versions_manager.get_version_info.return_value = version_info
         mock_ap_src_metadata_fetcher.get_build_options_at_commit.return_value = []
@@ -680,7 +670,6 @@ class TestVehiclesService:
             commit_ref="refs/tags/Copter-4.5.0",
             release_type="stable",
             version_number="4.5.0",
-            ap_build_artifacts_url=None,
         )
         opt = Mock()
         opt.label = "HAL_LOGGING_ENABLED"
@@ -691,7 +680,6 @@ class TestVehiclesService:
         opt.dependency = None
         mock_versions_manager.get_version_info.return_value = version_info
         mock_ap_src_metadata_fetcher.get_build_options_at_commit.return_value = [opt]
-        mock_ap_src_metadata_fetcher.get_board_defaults_from_fw_server.return_value = None
 
         result = service.get_features("copter", version_info.version_id, "CubeRed")
 
@@ -708,7 +696,6 @@ class TestVehiclesService:
             commit_ref="refs/tags/Copter-4.5.0",
             release_type="stable",
             version_number="4.5.0",
-            ap_build_artifacts_url=None,
         )
         opt_logging = Mock()
         opt_logging.label, opt_logging.define, opt_logging.category = "HAL_LOGGING_ENABLED", "HAL_LOGGING_ENABLED", "Logging"
@@ -721,7 +708,6 @@ class TestVehiclesService:
         opt_sensors.description, opt_sensors.default, opt_sensors.dependency = "", 1, None
         mock_versions_manager.get_version_info.return_value = version_info
         mock_ap_src_metadata_fetcher.get_build_options_at_commit.return_value = [opt_logging, opt_ekf, opt_sensors]
-        mock_ap_src_metadata_fetcher.get_board_defaults_from_fw_server.return_value = None
 
         result = service.get_features("copter", version_info.version_id, "CubeRed")
 
@@ -736,7 +722,6 @@ class TestVehiclesService:
             commit_ref="refs/tags/Copter-4.5.0",
             release_type="stable",
             version_number="4.5.0",
-            ap_build_artifacts_url=None,
         )
         opt_z = Mock()
         opt_z.label, opt_z.define, opt_z.category = "FEATURE_Z", "DEFINE_Z", "Sensors"
@@ -749,22 +734,21 @@ class TestVehiclesService:
         opt_m.description, opt_m.default, opt_m.dependency = "", 1, None
         mock_versions_manager.get_version_info.return_value = version_info
         mock_ap_src_metadata_fetcher.get_build_options_at_commit.return_value = [opt_z, opt_a, opt_m]
-        mock_ap_src_metadata_fetcher.get_board_defaults_from_fw_server.return_value = None
 
         result = service.get_features("copter", version_info.version_id, "CubeRed")
 
         assert [f.category.name for f in result] == ["EKF", "Logging", "Sensors"]
 
-    def test_get_features_uses_fallback_defaults_when_no_artifacts_url(
-        self, service, mock_versions_manager, mock_ap_src_metadata_fetcher
+    def test_get_features_uses_fallback_defaults_when_no_features_url(
+        self, service, mock_versions_manager, mock_ap_src_metadata_fetcher,
+        mock_manifest_json, mock_features_txt_client,
     ):
-        """Test that build-options-py defaults are used when ap_build_artifacts_url is None."""
+        """Test that build-options-py defaults are used when no features.txt URL is available."""
         version_info = VersionInfo(
             remote_info=RemoteInfo(name="ardupilot", url="https://github.com/ArduPilot/ardupilot.git"),
             commit_ref="refs/tags/Copter-4.5.0",
             release_type="stable",
             version_number="4.5.0",
-            ap_build_artifacts_url=None,
         )
         opt_on = Mock()
         opt_on.label, opt_on.define, opt_on.category = "FEATURE_ON", "DEFINE_ON", "Cat"
@@ -774,6 +758,7 @@ class TestVehiclesService:
         opt_off.description, opt_off.default, opt_off.dependency = "", 0, None
         mock_versions_manager.get_version_info.return_value = version_info
         mock_ap_src_metadata_fetcher.get_build_options_at_commit.return_value = [opt_on, opt_off]
+        mock_manifest_json.get_features_txt_url.return_value = None
 
         result = service.get_features("copter", version_info.version_id, "CubeRed")
 
@@ -782,10 +767,11 @@ class TestVehiclesService:
         assert by_id["FEATURE_ON"].source == "build-options-py"
         assert by_id["FEATURE_OFF"].enabled is False
         assert by_id["FEATURE_OFF"].source == "build-options-py"
-        mock_ap_src_metadata_fetcher.get_board_defaults_from_fw_server.assert_not_called()
+        mock_features_txt_client.get_defaults.assert_not_called()
 
     def test_get_features_uses_firmware_server_defaults_when_available(
-        self, service, mock_versions_manager, mock_ap_src_metadata_fetcher
+        self, service, mock_versions_manager, mock_ap_src_metadata_fetcher,
+        mock_manifest_json, mock_features_txt_client,
     ):
         """Test that firmware-server defaults override build-options-py when present."""
         version_info = VersionInfo(
@@ -793,7 +779,6 @@ class TestVehiclesService:
             commit_ref="refs/tags/Copter-4.5.0",
             release_type="stable",
             version_number="4.5.0",
-            ap_build_artifacts_url="https://firmware.ardupilot.org/Copter/stable-4.5.0",
         )
         opt_a = Mock()
         opt_a.label, opt_a.define, opt_a.category = "FEATURE_A", "DEFINE_A", "Cat"
@@ -803,8 +788,11 @@ class TestVehiclesService:
         opt_b.description, opt_b.default, opt_b.dependency = "", 1, None
         mock_versions_manager.get_version_info.return_value = version_info
         mock_ap_src_metadata_fetcher.get_build_options_at_commit.return_value = [opt_a, opt_b]
-        # firmware server says DEFINE_A is disabled, DEFINE_B is enabled
-        mock_ap_src_metadata_fetcher.get_board_defaults_from_fw_server.return_value = {
+        features_url = (
+            "https://firmware.ardupilot.org/Copter/stable-4.5.0/CubeRed/features.txt"
+        )
+        mock_manifest_json.get_features_txt_url.return_value = features_url
+        mock_features_txt_client.get_defaults.return_value = {
             "DEFINE_A": 0,
             "DEFINE_B": 1,
         }
@@ -816,9 +804,11 @@ class TestVehiclesService:
         assert by_id["FEATURE_A"].source == "firmware-server"
         assert by_id["FEATURE_B"].enabled is True
         assert by_id["FEATURE_B"].source == "firmware-server"
+        mock_features_txt_client.get_defaults.assert_called_once_with(features_url)
 
     def test_get_features_falls_back_to_defaults_when_firmware_server_returns_none(
-        self, service, mock_versions_manager, mock_ap_src_metadata_fetcher
+        self, service, mock_versions_manager, mock_ap_src_metadata_fetcher,
+        mock_manifest_json, mock_features_txt_client,
     ):
         """Test that build-options-py fallback is used when firmware server fetch fails."""
         version_info = VersionInfo(
@@ -826,14 +816,16 @@ class TestVehiclesService:
             commit_ref="refs/tags/Copter-4.5.0",
             release_type="stable",
             version_number="4.5.0",
-            ap_build_artifacts_url="https://firmware.ardupilot.org/Copter/stable-4.5.0",
         )
         opt = Mock()
         opt.label, opt.define, opt.category = "FEATURE_A", "DEFINE_A", "Cat"
         opt.description, opt.default, opt.dependency = "", 1, None
         mock_versions_manager.get_version_info.return_value = version_info
         mock_ap_src_metadata_fetcher.get_build_options_at_commit.return_value = [opt]
-        mock_ap_src_metadata_fetcher.get_board_defaults_from_fw_server.return_value = None
+        mock_manifest_json.get_features_txt_url.return_value = (
+            "https://firmware.ardupilot.org/Copter/stable-4.5.0/CubeRed/features.txt"
+        )
+        mock_features_txt_client.get_defaults.return_value = None
 
         result = service.get_features("copter", version_info.version_id, "CubeRed")
 
@@ -841,7 +833,8 @@ class TestVehiclesService:
         assert result[0].default.source == "build-options-py"
 
     def test_get_features_firmware_server_overrides_only_known_defines(
-        self, service, mock_versions_manager, mock_ap_src_metadata_fetcher
+        self, service, mock_versions_manager, mock_ap_src_metadata_fetcher,
+        mock_manifest_json, mock_features_txt_client,
     ):
         """Test that a define absent from firmware-server data falls back to build-options-py."""
         version_info = VersionInfo(
@@ -849,7 +842,6 @@ class TestVehiclesService:
             commit_ref="refs/tags/Copter-4.5.0",
             release_type="stable",
             version_number="4.5.0",
-            ap_build_artifacts_url="https://firmware.ardupilot.org/Copter/stable-4.5.0",
         )
         opt_known = Mock()
         opt_known.label, opt_known.define, opt_known.category = "FEATURE_KNOWN", "DEFINE_KNOWN", "Cat"
@@ -859,8 +851,10 @@ class TestVehiclesService:
         opt_unknown.description, opt_unknown.default, opt_unknown.dependency = "", 1, None
         mock_versions_manager.get_version_info.return_value = version_info
         mock_ap_src_metadata_fetcher.get_build_options_at_commit.return_value = [opt_known, opt_unknown]
-        # firmware server only knows about DEFINE_KNOWN
-        mock_ap_src_metadata_fetcher.get_board_defaults_from_fw_server.return_value = {
+        mock_manifest_json.get_features_txt_url.return_value = (
+            "https://firmware.ardupilot.org/Copter/stable-4.5.0/CubeRed/features.txt"
+        )
+        mock_features_txt_client.get_defaults.return_value = {
             "DEFINE_KNOWN": 1,
         }
 
@@ -872,6 +866,65 @@ class TestVehiclesService:
         assert by_id["FEATURE_UNKNOWN"].enabled is True
         assert by_id["FEATURE_UNKNOWN"].source == "build-options-py"
 
+    def test_get_features_tag_version_uses_latest_url(
+        self, service, mock_versions_manager, mock_ap_src_metadata_fetcher,
+        mock_manifest_json, mock_features_txt_client,
+    ):
+        """Fork tag builds use hardcoded latest features.txt URL."""
+        version_info = VersionInfo(
+            remote_info=RemoteInfo(name="shiv-tyagi", url="https://github.com/shiv-tyagi/ardupilot.git"),
+            commit_ref="refs/tags/custom-build/my-feature",
+            release_type="tag",
+            version_number="my-feature",
+        )
+        opt = Mock()
+        opt.label, opt.define, opt.category = "FEATURE_A", "DEFINE_A", "Cat"
+        opt.description, opt.default, opt.dependency = "", 0, None
+        mock_versions_manager.get_version_info.return_value = version_info
+        mock_ap_src_metadata_fetcher.get_build_options_at_commit.return_value = [opt]
+        latest_url = (
+            "https://firmware.ardupilot.org/Copter/latest/CubeOrange/features.txt"
+        )
+        mock_features_txt_client.get_defaults.return_value = {"DEFINE_A": 1}
+
+        result = service.get_features("copter", version_info.version_id, "CubeOrange")
+
+        mock_manifest_json.get_features_txt_url.assert_not_called()
+        mock_features_txt_client.get_defaults.assert_called_once_with(latest_url)
+        assert result[0].default.enabled is True
+        assert result[0].default.source == "firmware-server"
+
+    def test_get_features_fork_remote_uses_latest_url(
+        self, service, mock_versions_manager, mock_ap_src_metadata_fetcher,
+        mock_manifest_json, mock_features_txt_client,
+    ):
+        """Non-official fork remotes use latest ardupilot features.txt for any release."""
+        version_info = VersionInfo(
+            remote_info=RemoteInfo(
+                name="test-remote-1",
+                url="https://github.com/test/ardupilot.git",
+            ),
+            commit_ref="refs/heads/master",
+            release_type="latest",
+            version_number="4.6.0",
+        )
+        opt = Mock()
+        opt.label, opt.define, opt.category = "FEATURE_A", "DEFINE_A", "Cat"
+        opt.description, opt.default, opt.dependency = "", 1, None
+        mock_versions_manager.get_version_info.return_value = version_info
+        mock_ap_src_metadata_fetcher.get_build_options_at_commit.return_value = [opt]
+        latest_url = (
+            "https://firmware.ardupilot.org/Copter/latest/CubeOrange/features.txt"
+        )
+        mock_features_txt_client.get_defaults.return_value = {"DEFINE_A": 0}
+
+        result = service.get_features("copter", version_info.version_id, "CubeOrange")
+
+        mock_manifest_json.get_features_txt_url.assert_not_called()
+        mock_features_txt_client.get_defaults.assert_called_once_with(latest_url)
+        assert result[0].default.enabled is False
+        assert result[0].default.source == "firmware-server"
+
     def test_get_features_dependency_none(
         self, service, mock_versions_manager, mock_ap_src_metadata_fetcher
     ):
@@ -881,14 +934,12 @@ class TestVehiclesService:
             commit_ref="refs/tags/Copter-4.5.0",
             release_type="stable",
             version_number="4.5.0",
-            ap_build_artifacts_url=None,
         )
         opt = Mock()
         opt.label, opt.define, opt.category = "FEATURE_A", "DEFINE_A", "Cat"
         opt.description, opt.default, opt.dependency = "", 1, None
         mock_versions_manager.get_version_info.return_value = version_info
         mock_ap_src_metadata_fetcher.get_build_options_at_commit.return_value = [opt]
-        mock_ap_src_metadata_fetcher.get_board_defaults_from_fw_server.return_value = None
 
         result = service.get_features("copter", version_info.version_id, "CubeRed")
 
@@ -903,14 +954,12 @@ class TestVehiclesService:
             commit_ref="refs/tags/Copter-4.5.0",
             release_type="stable",
             version_number="4.5.0",
-            ap_build_artifacts_url=None,
         )
         opt = Mock()
         opt.label, opt.define, opt.category = "FEATURE_A", "DEFINE_A", "Cat"
         opt.description, opt.default, opt.dependency = "", 1, "DEP_ONE"
         mock_versions_manager.get_version_info.return_value = version_info
         mock_ap_src_metadata_fetcher.get_build_options_at_commit.return_value = [opt]
-        mock_ap_src_metadata_fetcher.get_board_defaults_from_fw_server.return_value = None
 
         result = service.get_features("copter", version_info.version_id, "CubeRed")
 
@@ -925,14 +974,12 @@ class TestVehiclesService:
             commit_ref="refs/tags/Copter-4.5.0",
             release_type="stable",
             version_number="4.5.0",
-            ap_build_artifacts_url=None,
         )
         opt = Mock()
         opt.label, opt.define, opt.category = "FEATURE_A", "DEFINE_A", "Cat"
         opt.description, opt.default, opt.dependency = "", 1, "DEP_ONE,DEP_TWO,DEP_THREE"
         mock_versions_manager.get_version_info.return_value = version_info
         mock_ap_src_metadata_fetcher.get_build_options_at_commit.return_value = [opt]
-        mock_ap_src_metadata_fetcher.get_board_defaults_from_fw_server.return_value = None
 
         result = service.get_features("copter", version_info.version_id, "CubeRed")
 
@@ -947,14 +994,12 @@ class TestVehiclesService:
             commit_ref="refs/tags/Copter-4.5.0",
             release_type="stable",
             version_number="4.5.0",
-            ap_build_artifacts_url=None,
         )
         opt = Mock()
         opt.label, opt.define, opt.category = "FEATURE_A", "DEFINE_A", "Cat"
         opt.description, opt.default, opt.dependency = "", 1, "DEP_ONE , DEP_TWO , DEP_THREE"
         mock_versions_manager.get_version_info.return_value = version_info
         mock_ap_src_metadata_fetcher.get_build_options_at_commit.return_value = [opt]
-        mock_ap_src_metadata_fetcher.get_board_defaults_from_fw_server.return_value = None
 
         result = service.get_features("copter", version_info.version_id, "CubeRed")
 
@@ -969,14 +1014,12 @@ class TestVehiclesService:
             commit_ref="refs/tags/Copter-4.5.0",
             release_type="stable",
             version_number="4.5.0",
-            ap_build_artifacts_url=None,
         )
         opt = Mock()
         opt.label, opt.define, opt.category = "FEATURE_A", "DEFINE_A", "Cat"
         opt.description, opt.default, opt.dependency = "", 1, None
         mock_versions_manager.get_version_info.return_value = version_info
         mock_ap_src_metadata_fetcher.get_build_options_at_commit.return_value = [opt]
-        mock_ap_src_metadata_fetcher.get_board_defaults_from_fw_server.return_value = None
 
         result = service.get_features("copter", version_info.version_id, "CubeRed")
 
@@ -993,7 +1036,6 @@ class TestVehiclesService:
             commit_ref="refs/tags/Copter-4.5.0",
             release_type="stable",
             version_number="4.5.0",
-            ap_build_artifacts_url=None,
         )
         opt_logging = Mock()
         opt_logging.label, opt_logging.define, opt_logging.category = "HAL_LOGGING_ENABLED", "HAL_LOGGING_ENABLED", "Logging"
@@ -1006,7 +1048,6 @@ class TestVehiclesService:
         opt_sensors.description, opt_sensors.default, opt_sensors.dependency = "", 1, None
         mock_versions_manager.get_version_info.return_value = version_info
         mock_ap_src_metadata_fetcher.get_build_options_at_commit.return_value = [opt_logging, opt_ekf, opt_sensors]
-        mock_ap_src_metadata_fetcher.get_board_defaults_from_fw_server.return_value = None
 
         result = service.get_features("copter", version_info.version_id, "CubeRed", category_id="Logging")
 
@@ -1023,7 +1064,6 @@ class TestVehiclesService:
             commit_ref="refs/tags/Copter-4.5.0",
             release_type="stable",
             version_number="4.5.0",
-            ap_build_artifacts_url=None,
         )
         opt_logging = Mock()
         opt_logging.label, opt_logging.define, opt_logging.category = "HAL_LOGGING_ENABLED", "HAL_LOGGING_ENABLED", "Logging"
@@ -1033,7 +1073,6 @@ class TestVehiclesService:
         opt_ekf.description, opt_ekf.default, opt_ekf.dependency = "", 1, None
         mock_versions_manager.get_version_info.return_value = version_info
         mock_ap_src_metadata_fetcher.get_build_options_at_commit.return_value = [opt_logging, opt_ekf]
-        mock_ap_src_metadata_fetcher.get_board_defaults_from_fw_server.return_value = None
 
         result = service.get_features("copter", version_info.version_id, "CubeRed", category_id="Sensors")
 
@@ -1048,7 +1087,6 @@ class TestVehiclesService:
             commit_ref="refs/tags/Copter-4.5.0",
             release_type="stable",
             version_number="4.5.0",
-            ap_build_artifacts_url=None,
         )
         opt_a = Mock()
         opt_a.label, opt_a.define, opt_a.category = "FEATURE_A", "DEFINE_A", "Logging"
@@ -1058,7 +1096,6 @@ class TestVehiclesService:
         opt_b.description, opt_b.default, opt_b.dependency = "", 1, None
         mock_versions_manager.get_version_info.return_value = version_info
         mock_ap_src_metadata_fetcher.get_build_options_at_commit.return_value = [opt_a, opt_b]
-        mock_ap_src_metadata_fetcher.get_board_defaults_from_fw_server.return_value = None
 
         result = service.get_features("copter", version_info.version_id, "CubeRed", category_id="NonExistent")
 
@@ -1075,14 +1112,12 @@ class TestVehiclesService:
             commit_ref="refs/tags/Copter-4.5.0",
             release_type="stable",
             version_number="4.5.0",
-            ap_build_artifacts_url=None,
         )
         opt = Mock()
         opt.label, opt.define, opt.category = "HAL_LOGGING_ENABLED", "HAL_LOGGING_ENABLED", "Logging"
         opt.description, opt.default, opt.dependency = "", 1, None
         mock_versions_manager.get_version_info.return_value = version_info
         mock_ap_src_metadata_fetcher.get_build_options_at_commit.return_value = [opt]
-        mock_ap_src_metadata_fetcher.get_board_defaults_from_fw_server.return_value = None
 
         result = service.get_feature("copter", version_info.version_id, "CubeRed", "HAL_LOGGING_ENABLED")
 
@@ -1099,14 +1134,12 @@ class TestVehiclesService:
             commit_ref="refs/tags/Copter-4.5.0",
             release_type="stable",
             version_number="4.5.0",
-            ap_build_artifacts_url=None,
         )
         opt = Mock()
         opt.label, opt.define, opt.category = "HAL_LOGGING_ENABLED", "HAL_LOGGING_ENABLED", "Logging"
         opt.description, opt.default, opt.dependency = "", 1, None
         mock_versions_manager.get_version_info.return_value = version_info
         mock_ap_src_metadata_fetcher.get_build_options_at_commit.return_value = [opt]
-        mock_ap_src_metadata_fetcher.get_board_defaults_from_fw_server.return_value = None
 
         result = service.get_feature("copter", version_info.version_id, "CubeRed", "NONEXISTENT_FEATURE")
 
@@ -1121,7 +1154,6 @@ class TestVehiclesService:
             commit_ref="refs/tags/Copter-4.5.0",
             release_type="stable",
             version_number="4.5.0",
-            ap_build_artifacts_url=None,
         )
         opt_a = Mock()
         opt_a.label, opt_a.define, opt_a.category = "FEATURE_A", "DEFINE_A", "Cat"
@@ -1134,10 +1166,107 @@ class TestVehiclesService:
         opt_c.description, opt_c.default, opt_c.dependency = "", 1, None
         mock_versions_manager.get_version_info.return_value = version_info
         mock_ap_src_metadata_fetcher.get_build_options_at_commit.return_value = [opt_a, opt_b, opt_c]
-        mock_ap_src_metadata_fetcher.get_board_defaults_from_fw_server.return_value = None
 
         result = service.get_feature("copter", version_info.version_id, "CubeRed", "FEATURE_B")
 
         assert result is not None
         assert result.id == "FEATURE_B"
         assert result.default.enabled is False
+
+
+class TestGetBoardStandardArtifacts:
+    def test_board_not_found_returns_none(
+        self, service, mock_versions_manager, mock_ap_src_metadata_fetcher,
+        mock_manifest_json,
+    ):
+        remote_info = Mock()
+        remote_info.name = "ardupilot"
+        mock_versions_manager.get_version_info.return_value = Mock(
+            version_id="v1",
+            commit_ref="abc123",
+            remote_info=remote_info,
+        )
+        mock_ap_src_metadata_fetcher.get_boards.return_value = []
+
+        result = service.get_board_standard_artifacts("copter", "v1", "CubeOrange")
+
+        assert result is None
+        mock_manifest_json.get_board_artifacts.assert_not_called()
+
+    def test_success(
+        self, service, mock_versions_manager, mock_ap_src_metadata_fetcher,
+        mock_manifest_json,
+    ):
+        remote_info = Mock()
+        remote_info.name = "ardupilot"
+        version_info = Mock(
+            version_id="v1",
+            commit_ref="abc123",
+            release_type="stable",
+            version_number="4.6.3",
+            remote_info=remote_info,
+        )
+        mock_versions_manager.get_version_info.return_value = version_info
+        mock_ap_src_metadata_fetcher.get_boards.return_value = ["CubeOrange"]
+        mock_manifest_json.get_board_artifacts.return_value = [
+            BoardArtifact(
+                name="arducopter.apj",
+                url="https://firmware.ardupilot.org/Copter/stable-4.6.3/CubeOrange/arducopter.apj",
+                format="apj",
+                size=100,
+            )
+        ]
+
+        result = service.get_board_standard_artifacts("copter", "v1", "CubeOrange")
+
+        assert len(result) == 1
+        assert result[0].name == "arducopter.apj"
+        assert result[0].format == "apj"
+        mock_manifest_json.get_board_artifacts.assert_called_once_with(
+            vehicle_id="copter",
+            release_type="stable",
+            version_number="4.6.3",
+            board_id="CubeOrange",
+        )
+
+    def test_not_in_manifest_returns_none(
+        self, service, mock_versions_manager, mock_ap_src_metadata_fetcher,
+        mock_manifest_json,
+    ):
+        remote_info = Mock()
+        remote_info.name = "ardupilot"
+        version_info = Mock(
+            version_id="v1",
+            commit_ref="abc123",
+            release_type="stable",
+            version_number="4.6.3",
+            remote_info=remote_info,
+        )
+        mock_versions_manager.get_version_info.return_value = version_info
+        mock_ap_src_metadata_fetcher.get_boards.return_value = ["CubeOrange"]
+        mock_manifest_json.get_board_artifacts.return_value = []
+
+        result = service.get_board_standard_artifacts("copter", "v1", "CubeOrange")
+
+        assert result is None
+
+    def test_fork_remote_returns_none_without_manifest_lookup(
+        self, service, mock_versions_manager, mock_ap_src_metadata_fetcher,
+        mock_manifest_json,
+    ):
+        remote_info = Mock()
+        remote_info.name = "shiv-tyagi"
+        version_info = Mock(
+            version_id="v1",
+            commit_ref="abc123",
+            release_type="tag",
+            version_number="my-feature",
+            remote_info=remote_info,
+        )
+        mock_versions_manager.get_version_info.return_value = version_info
+        mock_ap_src_metadata_fetcher.get_boards.return_value = ["CubeOrange"]
+
+        result = service.get_board_standard_artifacts("copter", "v1", "CubeOrange")
+
+        assert result is None
+        mock_manifest_json.get_board_artifacts.assert_not_called()