diff --git a/docs/_data/firmware_controls.json b/docs/_data/firmware_controls.json index d471756c7..a18e0abb1 100644 --- a/docs/_data/firmware_controls.json +++ b/docs/_data/firmware_controls.json @@ -1,5 +1,101 @@ { "familyTag": "v1.17.1.7-halo-keymind-cascade-dev-2d03e098", + "partitionMigrations": { + "Ebyte_EoRa-S3_Repeater": "ebyte-eora-s3-repeater", + "Ebyte_EoRa-S3_room_server": "ebyte-eora-s3-room-server", + "Generic_E22_sx1262_repeater": "generic-e22-sx1262-repeater", + "Generic_E22_sx1268_repeater": "generic-e22-sx1268-repeater", + "Heltec_E213_repeater": "heltec-e213-repeater", + "Heltec_E213_room_server": "heltec-e213-room-server", + "Heltec_E290_repeater": "heltec-e290-repeater", + "Heltec_E290_room_server": "heltec-e290-room-server", + "Heltec_T190_repeater_": "heltec-t190-repeater", + "Heltec_T190_room_server_": "heltec-t190-room-server", + "Heltec_WSL3_repeater": "heltec-wsl3-repeater", + "Heltec_WSL3_room_server": "heltec-wsl3-room-server", + "Heltec_WSL3_sensor": "heltec-wsl3-sensor", + "Heltec_Wireless_Paper_repeater": "heltec-wireless-paper-repeater", + "Heltec_Wireless_Paper_room_server": "heltec-wireless-paper-room-server", + "Heltec_Wireless_Tracker_repeater": "heltec-wireless-tracker-repeater", + "Heltec_Wireless_Tracker_room_server": "heltec-wireless-tracker-room-server", + "Heltec_ct62_repeater": "heltec-ct62-repeater", + "Heltec_ct62_sensor": "heltec-ct62-sensor", + "Heltec_v2_repeater": "heltec-v2-repeater", + "Heltec_v2_room_server": "heltec-v2-room-server", + "Heltec_v3_repeater": "heltec-v3-repeater", + "Heltec_v3_room_server": "heltec-v3-room-server", + "Heltec_v3_sensor": "heltec-v3-sensor", + "LilyGo_T3S3_sx1262_repeater": "t3s3-sx1262-repeater", + "LilyGo_T3S3_sx1262_room_server": "t3s3-sx1262-room-server", + "LilyGo_T3S3_sx1276_repeater": "t3s3-sx1276-repeater", + "LilyGo_T3S3_sx1276_room_server": "t3s3-sx1276-room-server", + "LilyGo_TBeam_1W_repeater": "lilygo-tbeam-1w-repeater", + "LilyGo_TBeam_1W_room_server": "lilygo-tbeam-1w-room-server", + "LilyGo_TDeck_repeater": "lilygo-tdeck-repeater", + "LilyGo_TETH_Elite_sx1262_repeater": "lilygo-teth-elite-sx1262-repeater", + "LilyGo_TETH_Elite_sx1262_room_server": "lilygo-teth-elite-sx1262-room-server", + "MKE_s3_repeater": "mke-s3-repeater", + "MKE_s3_room_server": "mke-s3-room-server", + "MKE_s3_sensor": "mke-s3-sensor", + "RAK_3112_repeater": "rak-3112-repeater", + "RAK_3112_room_server": "rak-3112-room-server", + "RAK_3112_sensor": "rak-3112-sensor", + "Station_G2_logging_repeater": "station-g2-logging-repeater", + "Station_G2_repeater": "station-g2-repeater", + "Station_G2_room_server": "station-g2-room-server", + "Station_G3_ESP32_logging_repeater": "station-g3-esp32-logging-repeater", + "Station_G3_ESP32_repeater": "station-g3-esp32-repeater", + "Station_G3_ESP32_room_server": "station-g3-esp32-room-server", + "T_Beam_S3_Supreme_SX1262_repeater": "t-beam-s3-supreme-sx1262-repeater", + "T_Beam_S3_Supreme_SX1262_room_server": "t-beam-s3-supreme-sx1262-room-server", + "Tenstar_C3_sx1262_repeater": "tenstar-c3-sx1262-repeater", + "Tenstar_C3_sx1268_repeater": "tenstar-c3-sx1268-repeater", + "ThinkNode_M2_Repeater": "thinknode-m2-repeater", + "ThinkNode_M2_room_server": "thinknode-m2-room-server", + "ThinkNode_M5_Repeater": "thinknode-m5-repeater", + "ThinkNode_M5_room_server": "thinknode-m5-room-server", + "ThinkNode_M7_repeater": "thinknode-m7-repeater", + "ThinkNode_M7_room_server": "thinknode-m7-room-server", + "ThinkNode_M9_repeater_": "thinknode-m9-repeater", + "ThinkNode_M9_room_server_": "thinknode-m9-room-server", + "Xiao_C3_repeater": "xiao-c3-repeater", + "Xiao_C3_room_server": "xiao-c3-room-server", + "Xiao_S3_WIO_repeater": "xiao-s3-wio", + "Xiao_S3_WIO_room_server": "xiao-s3-wio-room-server", + "Xiao_S3_WIO_sensor": "xiao-s3-wio-sensor", + "Xiao_S3_repeater": "xiao-s3-repeater", + "Xiao_S3_room_server": "xiao-s3-room-server", + "Xiao_S3_sensor": "xiao-s3-sensor", + "heltec_rc32_repeater": "heltec-rc32-repeater", + "heltec_rc32_room_server": "heltec-rc32-room-server", + "heltec_rc32_sensor": "heltec-rc32-sensor", + "heltec_rc32_without_display_repeater": "heltec-rc32-no-display-repeater", + "heltec_rc32_without_display_room_server": "heltec-rc32-no-display-room-server", + "heltec_rc32_without_display_sensor": "heltec-rc32-no-display-sensor", + "heltec_tracker_v2_repeater": "heltec-tracker-v2-repeater", + "heltec_tracker_v2_room_server": "heltec-tracker-v2-room-server", + "heltec_tracker_v2_sensor": "heltec-tracker-v2-sensor", + "heltec_v4_expansionkit_repeater": "heltec-v4-expansionkit-repeater", + "heltec_v4_r8_repeater": "heltec-v4-r8-repeater", + "heltec_v4_r8_room_server": "heltec-v4-r8-room-server", + "heltec_v4_r8_sensor": "heltec-v4-r8-sensor", + "heltec_v4_r8_tft_repeater": "heltec-v4-r8-tft-repeater", + "heltec_v4_r8_tft_room_server": "heltec-v4-r8-tft-room-server", + "heltec_v4_r8_tft_sensor": "heltec-v4-r8-tft-sensor", + "heltec_v4_repeater": "heltec-v4", + "heltec_v4_room_server": "heltec-v4-room-server", + "heltec_v4_sensor": "heltec-v4-sensor", + "heltec_v4_tft_repeater": "heltec-v4-tft-repeater", + "heltec_v4_tft_room_server": "heltec-v4-tft-room-server", + "heltec_v4_tft_sensor": "heltec-v4-tft-sensor", + "meshnology_w12_repeater": "meshnology-w12-repeater", + "meshnology_w12_room_server": "meshnology-w12-room-server", + "meshnology_w12_sensor": "meshnology-w12-sensor", + "nibble_screen_connect_repeater_": "nibble-screen-repeater", + "nibble_screen_connect_room_server_": "nibble-screen-room-server", + "nibble_zero_connect_repeater_": "nibble-zero-repeater", + "nibble_zero_connect_room_server_": "nibble-zero-room-server" + }, "profiles": { "Ebyte_EoRa-S3_Repeater": { "display": true, diff --git a/docs/_javascript/firmware_picker.js b/docs/_javascript/firmware_picker.js index a3376ee2b..c2a3492e6 100644 --- a/docs/_javascript/firmware_picker.js +++ b/docs/_javascript/firmware_picker.js @@ -779,6 +779,10 @@ controlData.profiles && controlData.profiles[profile.target]) { profile.controls = controlData.profiles[profile.target]; applyReleaseRuntimeCapabilities(profile, profile.controls); + profile.migrationPackage = migrationPackageForProfile( + profile, controlData.partitionMigrations + ); + profile.migrationAsset = migrationAssetForProfile(profile, rows); if (isFullCompanion(profile) && profile.controls.mqtt && !Array.isArray(profile.controls.loggingModes)) { profile.loggingModes = ["none", "usb", "wifi", "both"]; @@ -1060,6 +1064,46 @@ return url !== asset.releaseUrl && isSafeGithubUrl(url) ? url : ""; } + function migrationPackageForProfile(profile, lookup) { + if (!isExpandedEsp32Infrastructure(profile) || !lookup) return ""; + const target = profile.target.replace(/-full-(?:logging|usb-wifi)$/i, ""); + // The packages name canonical physical board/role identities. Observer + // Full profiles use that same board/role's recipe; the package README + // still governs the installed OTA target/layout. Preserve every hardware + // variant (including logging hardware and trailing underscores). + const canonical = target.replace(/_observer_mqtt(?=_?$)/i, ""); + const key = Object.prototype.hasOwnProperty.call(lookup, target) + ? target : canonical; + const packageName = Object.prototype.hasOwnProperty.call(lookup, key) + ? lookup[key] : ""; + return typeof packageName === "string" && /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(packageName) + ? packageName : ""; + } + + function migrationAssetForProfile(profile, rows) { + if (!profile.migrationPackage) return null; + const releaseUrl = migrationReleaseUrl(profile, profile.files[0]); + if (!releaseUrl) return null; + const name = profile.migrationPackage + "-" + profile.releaseFamily + "-migration.zip"; + const downloadUrl = releaseUrl.replace("/releases/tag/", "/releases/download/") + + "/" + encodeURIComponent(name); + const matches = rows.filter(function (row) { + return row.name === name && row.releaseTag === "utility-" + profile.releaseFamily && + row.releaseUrl === releaseUrl && row.url === downloadUrl; + }); + // Only offer a download actually published for this release and exact + // board/role. Missing, unsafe, or ambiguous assets retain the fallback. + return matches.length === 1 ? matches[0] : null; + } + + function migrationLink(profile, asset) { + const fallback = migrationReleaseUrl(profile, asset); + if (!fallback) return null; + return profile.migrationAsset + ? { url: profile.migrationAsset.url, label: "Download exact partition-expansion ZIP" } + : { url: fallback, label: "Check partition-expansion availability" }; + } + function installSteps(profile, kind) { const common = [ "Verify that the hardware name and every displayed variant match the physical board.", @@ -1111,7 +1155,7 @@ const extra = []; if (isExpandedEsp32Infrastructure(profile) && kind === "bin") { extra.push( - "If the node still has a smaller layout, first flash the matching merged image over USB or, if listed, use the exact board/role migration ZIP from the utility release and follow its README. Never send this app-only image across a partition change." + "If the node still has a smaller layout, first flash the matching merged image over USB or use the linked exact board/role migration ZIP, when available. Follow its README to check the installed OTA target and source layout before starting. Never send this app-only image across a partition change." ); } if (profile.variant.includes("w25q16")) { @@ -1449,10 +1493,10 @@ const release = createElement("a", "Open release notes"); release.href = asset.releaseUrl; actions.appendChild(release); - const migrationUrl = migrationReleaseUrl(profile, asset); - if (migrationUrl) { - const migration = createElement("a", "Check exact ESP32 migration ZIP"); - migration.href = migrationUrl; + const migrationAction = migrationLink(profile, asset); + if (migrationAction) { + const migration = createElement("a", migrationAction.label); + migration.href = migrationAction.url; actions.appendChild(migration); } card.appendChild(actions); @@ -1918,6 +1962,9 @@ renderRuntimeDirections: renderRuntimeDirections, installSteps: installSteps, migrationReleaseUrl: migrationReleaseUrl, + migrationPackageForProfile: migrationPackageForProfile, + migrationAssetForProfile: migrationAssetForProfile, + migrationLink: migrationLink, humanizeHardware: humanizeHardware, humanizeVariant: humanizeVariant, labelFor: labelFor, diff --git a/docs/firmware_picker.md b/docs/firmware_picker.md index 87a5714bf..af5577b8f 100644 --- a/docs/firmware_picker.md +++ b/docs/firmware_picker.md @@ -17,7 +17,7 @@ The picker reads public release metadata from GitHub. It does not upload device information. Hardware names, target names, and download links come directly from the published firmware assets. -
Loading release information...
@@ -29,9 +29,11 @@ from the published firmware assets.
Upgrading an existing ESP32 infrastructure node from a smaller partition
- layout? The selected Full result links to the utility release. If an
- exact board and role migration ZIP is listed, follow its included README
- for a supported Wi-Fi or LoRa route. USB installation uses the matching
+ layout? The selected Full result links directly to its exact board and
+ role partition-expansion ZIP when published. Follow its included README
+ to check the installed OTA target and source layout for a supported Wi-Fi
+ or LoRa route. If no unique matching ZIP is available, the picker links
+ to the utility release to check availability. USB installation uses the matching
merged image instead. An app-only .bin cannot change the partition table.
If the installed firmware supports it, run get storage.layout
to see the live flash size and OTA slots. Firmware version alone does not
@@ -481,6 +483,14 @@ python3 scripts/generate_picker_controls.py \
Refresh the `data-controls-url` version query when publishing the generated
metadata so browsers fetch the new release's controls.
+The generated `partitionMigrations` lookup comes from `BOARDS` in
+`scripts/package_esp32_partition_migration.py`, the same exact board/role table
+used to build the migration packages. Expanded ESP32 Full results use this
+lookup (including canonical board/role recipes for Observer profiles), then
+require one matching published ZIP in the same release family's utility page.
+Hardware variants and roles remain distinct. Unlisted targets, missing or
+ambiguous assets, and stale metadata never produce a guessed download link.
+
For the downloadable version, save the release family's public GitHub release
objects as a JSON array, then package the same picker UI and controls:
diff --git a/mkdocs.yml b/mkdocs.yml
index 891894dc7..d89934d14 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -26,7 +26,7 @@ extra_css:
extra_javascript:
- https://unpkg.com/leaflet@1.9.4/dist/leaflet.js
- - _javascript/firmware_picker.js?v=20260927-3
+ - _javascript/firmware_picker.js?v=20260927-4
- _javascript/telemetry_decoder.js
- _javascript/management_decoder.js
- _javascript/filter_tool.js
diff --git a/scripts/generate_picker_controls.py b/scripts/generate_picker_controls.py
index 4b881a880..337cea85a 100644
--- a/scripts/generate_picker_controls.py
+++ b/scripts/generate_picker_controls.py
@@ -9,9 +9,22 @@ import json
import re
from pathlib import Path
+from package_esp32_partition_migration import BOARDS
+
ROOT = Path(__file__).resolve().parents[1]
+def partition_migrations():
+ """Use the packager's exact board/role lookup, not guessed ZIP slugs."""
+ result = {}
+ for package, spec in BOARDS.items():
+ target = spec['target']
+ if target in result:
+ raise ValueError('Ambiguous partition migration target: ' + target)
+ result[target] = package
+ return result
+
+
def capacity_note(reductions, defines):
notes = []
for reduction in reductions:
@@ -141,7 +154,8 @@ def generate(stage, config):
raise ValueError('Conflicting target metadata: ' + target)
profiles[target] = controls
family = next(g['tag'] for g in plan['groups'] if g['key'] == 'companion')
- return {'familyTag': family, 'source': plan['source'], 'profiles': profiles}
+ return {'familyTag': family, 'source': plan['source'], 'profiles': profiles,
+ 'partitionMigrations': partition_migrations()}
if __name__ == '__main__':
diff --git a/test/test_firmware_picker.js b/test/test_firmware_picker.js
index 47354eee7..a541a3820 100644
--- a/test/test_firmware_picker.js
+++ b/test/test_firmware_picker.js
@@ -1134,6 +1134,110 @@ assert.strictEqual(commands(combinedDirections).filter(command => command === 's
assert.strictEqual(commands(combinedDirections).filter(command => command === 'set bridge.enabled on').length, 1);
console.log('current release metadata and capacity directions tests passed');
+// Partition expansion uses the packager's lookup and published utility assets,
+// not hardware labels or guessed slugs (several historical names differ).
+const migrationFamily = currentControls.familyTag;
+const utilityTag = 'utility-' + migrationFamily;
+const utilityUrl = 'https://github.com/mikecarper/MeshCore/releases/tag/' + utilityTag;
+function migrationZip(packageName, tag = migrationFamily) {
+ const name = packageName + '-' + tag + '-migration.zip';
+ return {...asset(name), browser_download_url:
+ 'https://github.com/mikecarper/MeshCore/releases/download/utility-' + tag + '/' + name};
+}
+const migrationAssets = Object.values(currentControls.partitionMigrations).map(name => migrationZip(name));
+function migrationCatalog(assets = migrationAssets, metadata = currentControls, utilityOverrides = {}) {
+ return picker.buildCatalog([
+ release(migrationFamily, '2026-09-13T00:00:00Z', currentAssets),
+ release(utilityTag, '2026-09-13T00:00:01Z', assets, utilityOverrides),
+ ], metadata);
+}
+const withMigrations = migrationCatalog();
+assert.strictEqual(withMigrations.rows.length, currentCatalog.rows.length,
+ 'Migration ZIPs must not become firmware/Serial DFU choices');
+const g2Target = 'Station_G2_repeater_observer_mqtt-full-usb-wifi';
+function g2MigrationLink(catalog) {
+ const profile = catalog.profiles.find(item => item.target === g2Target);
+ return picker.migrationLink(profile, profile.files[0]);
+}
+const directG2Link = {
+ url: migrationZip('station-g2-repeater').browser_download_url,
+ label: 'Download exact partition-expansion ZIP',
+};
+const fallbackMigrationLink = {
+ url: utilityUrl, label: 'Check partition-expansion availability',
+};
+assert.deepStrictEqual(g2MigrationLink(withMigrations), directG2Link);
+const g2PickerUrl = 'https://mikecarper.github.io/MeshCore/firmware_picker/' +
+ '?chipFamily=esp32&hardwareFamily=Station_G2&hardware=Station_G2&role=repeater&mode=espnow&install=bin&chipAuto=1';
+const g2Selection = picker.selectionFromUrl(g2PickerUrl, withMigrations.profiles);
+assert.deepStrictEqual(g2Selection.unavailable, []);
+const g2Selected = withMigrations.profiles.filter(profile =>
+ picker.profileMatchesFacets(profile, g2Selection.filters));
+assert.deepStrictEqual(g2Selected.map(profile => profile.target), [g2Target]);
+assert.deepStrictEqual(picker.migrationLink(g2Selected[0], g2Selected[0].files[0]), directG2Link);
+
+// Exercise every row in the packager lookup, even recipes whose canonical
+// image lives only inside its ZIP rather than the ordinary firmware matrix.
+for (const [target, packageName] of Object.entries(currentControls.partitionMigrations)) {
+ for (const suffix of ['-full-logging', '-full-usb-wifi']) {
+ const fullTarget = target + suffix;
+ const profile = Object.assign(picker.parseTargetProfile(fullTarget), {
+ releaseFamily: migrationFamily, controls: {platform: 'ESP32_PLATFORM'},
+ files: [{releaseUrl: 'https://github.com/mikecarper/MeshCore/releases/tag/' + migrationFamily}],
+ });
+ profile.migrationPackage = picker.migrationPackageForProfile(profile, currentControls.partitionMigrations);
+ profile.migrationAsset = picker.migrationAssetForProfile(profile, migrationAssets.map(item => ({
+ name: item.name, url: item.browser_download_url, releaseTag: utilityTag, releaseUrl: utilityUrl,
+ })));
+ assert.strictEqual(profile.migrationPackage, packageName, fullTarget);
+ assert.strictEqual(picker.migrationLink(profile, profile.files[0]).url,
+ migrationZip(packageName).browser_download_url, fullTarget);
+ }
+}
+const currentMigrationProfiles = withMigrations.profiles.filter(profile => profile.migrationPackage);
+assert(currentMigrationProfiles.length > 0);
+for (const profile of currentMigrationProfiles) {
+ assert(profile.migrationAsset, profile.target);
+ assert.strictEqual(picker.migrationLink(profile, profile.files[0]).url,
+ migrationZip(profile.migrationPackage).browser_download_url, profile.target);
+}
+assert.strictEqual(picker.migrationLink(ordinaryEsp32, ordinaryEsp32.files[0]), null);
+assert.strictEqual(picker.migrationLink(ikokaNormal, ikokaNormal.files[0]), null);
+const unmappedVariant = withMigrations.profiles.find(profile =>
+ profile.target === 'Station_G3_ESP32_r2_repeater_observer_mqtt-full-usb-wifi');
+assert(unmappedVariant);
+assert.deepStrictEqual(picker.migrationLink(unmappedVariant, unmappedVariant.files[0]), fallbackMigrationLink,
+ 'An unlisted board revision must not borrow another board recipe');
+
+const noG2Assets = migrationAssets.filter(item => item.name !== migrationZip('station-g2-repeater').name);
+assert.deepStrictEqual(g2MigrationLink(migrationCatalog(noG2Assets)), fallbackMigrationLink,
+ 'Room-server and logging-hardware packages must not stand in for G2 repeater');
+assert.deepStrictEqual(g2MigrationLink(migrationCatalog([
+ ...noG2Assets, migrationZip('station-g2-repeater', family),
+])), fallbackMigrationLink, 'A ZIP for another release is not a match');
+assert.deepStrictEqual(g2MigrationLink(migrationCatalog(migrationAssets, currentControls,
+ {tag_name: 'utility-' + family})), fallbackMigrationLink, 'Ignore other release families');
+assert.deepStrictEqual(g2MigrationLink(migrationCatalog(migrationAssets, currentControls,
+ {tag_name: 'logging-' + migrationFamily})), fallbackMigrationLink, 'Require the utility page');
+assert.deepStrictEqual(g2MigrationLink(migrationCatalog([
+ ...migrationAssets, migrationZip('station-g2-repeater'),
+])), fallbackMigrationLink, 'Duplicate matches must fail closed');
+for (const badUrl of ['javascript:alert(1)', 'https://example.com/download.zip',
+ migrationZip('station-g2-repeater').browser_download_url.replace('/mikecarper/', '/someone-else/'),
+ migrationZip('station-g2-room-server').browser_download_url]) {
+ assert.deepStrictEqual(g2MigrationLink(migrationCatalog([
+ ...noG2Assets, {...migrationZip('station-g2-repeater'), browser_download_url: badUrl},
+ ])), fallbackMigrationLink, badUrl);
+}
+assert.deepStrictEqual(g2MigrationLink(migrationCatalog(migrationAssets, currentControls,
+ {html_url: utilityUrl.replace('/mikecarper/', '/someone-else/')})), fallbackMigrationLink);
+assert.deepStrictEqual(g2MigrationLink(migrationCatalog(migrationAssets,
+ {...currentControls, partitionMigrations: undefined})), fallbackMigrationLink,
+ 'Older controls without a lookup retain a clearly labeled fallback');
+assert.strictEqual(g2MigrationLink(migrationCatalog(migrationAssets,
+ {...currentControls, familyTag: family})), null, 'Stale controls must not supply a migration');
+console.log('exact partition-expansion links passed for all ' + migrationAssets.length + ' lookup recipes');
+
assert.strictEqual(nrf.chipFamily, 'nrf52');
assert.strictEqual(mqttCompanion.chipFamily, 'esp32');
assert.deepStrictEqual(picker.facetValues(controlled.profiles, {chipFamily: 'nrf52'}, 'hardwareFamily'), ['RAK_4631']);
diff --git a/test/test_picker_controls_generator.py b/test/test_picker_controls_generator.py
index 00ff22c0f..03950dfde 100644
--- a/test/test_picker_controls_generator.py
+++ b/test/test_picker_controls_generator.py
@@ -1,16 +1,29 @@
import importlib.util
import json
+import sys
import tempfile
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
+sys.path.insert(0, str(ROOT / 'scripts'))
SPEC = importlib.util.spec_from_file_location('picker_controls', ROOT / 'scripts/generate_picker_controls.py')
GENERATOR = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(GENERATOR)
class PickerControlsTests(unittest.TestCase):
+ def test_partition_migrations_follow_the_complete_packager_lookup(self):
+ expected = {spec['target']: package
+ for package, spec in GENERATOR.BOARDS.items()}
+ self.assertEqual(len(expected), len(GENERATOR.BOARDS))
+ self.assertEqual(GENERATOR.partition_migrations(), expected)
+ checked_in = json.loads((ROOT / 'docs/_data/firmware_controls.json').read_text())
+ self.assertEqual(checked_in['partitionMigrations'], expected)
+ self.assertEqual(expected['Station_G2_repeater'], 'station-g2-repeater')
+ self.assertEqual(expected['Station_G2_logging_repeater'], 'station-g2-logging-repeater')
+ self.assertEqual(expected['nibble_zero_connect_room_server_'], 'nibble-zero-room-server')
+
def test_runtime_metadata_uses_verified_image_capabilities(self):
source = 'a' * 40
manifest = dict(source_commit=source,
@@ -51,6 +64,7 @@ class PickerControlsTests(unittest.TestCase):
(stage / 'companion/TARGET-MANIFEST.json').write_text(json.dumps([manifest]))
config = [('env:sample', [('build_flags', list(flags))])]
result = GENERATOR.generate(stage, config)
+ self.assertEqual(result['partitionMigrations'], GENERATOR.partition_migrations())
return result['profiles']['sample_companion_radio_full']
def test_compiled_capacity_overrides_unreduced_config_and_binds_repair_source(self):