Gate alpha release on geocaching checks and retry transient Pages downloads

This commit is contained in:
liu weikai committed 2026-10-01 01:31:20 +08:00
1 parent 6f73515335
commit f4ee4e9add
5 files changed
+136 -8

No files matched your search

+6 -1
View File
@@ -442,7 +442,9 @@ jobs:
node-version: "22"
- name: Verify locale packs
run: python tools/validate_locale_packs.py --pack-root packs
run: |
python tools/validate_locale_packs.py --pack-root packs
python tests/test_prepare_pages_site.py
- name: Generate Pages release metadata and package repository
env:
@@ -547,6 +549,9 @@ jobs:
needs:
- boundary
- format
- geocaching_web
- reticulum_gateway
- geocaching
- build
- p4_contract
- build_c6
+22 -5
View File
@@ -25,6 +25,8 @@ runtime:
ui_thread: "arduino_loop"
radio_thread: "existing_esp_mesh_runtime"
gps_thread: "existing_esp_gps_runtime"
geocaching_owner: "existing_storage_owner"
note: "Geocaching advances bounded storage/network slices in the existing storage maintenance task; UI calls enqueue work or read mutex-protected snapshots. Runtime shells remain ESP-specific."
isr_policy: "defer_to_owner"
lock_policy: "separate_i2c_radio_audio_and_filesystem_owners"
@@ -86,11 +88,13 @@ capability_bindings:
lora:
board_provider: "wio_tracker_l2.radio"
platform_driver: "SX1262_on_dedicated_SPI"
protocol_consumers: ["core_mesh", "core_chat"]
protocol_consumers: ["core_mesh.meshtastic", "core_mesh.meshcore", "core_chat.reticulum"]
gps:
board_provider: "wio_tracker_l2.gps"
platform_driver: "L76K_UART"
protocol_consumers: ["core_gps"]
protocol_consumers: ["core_gps.nmea", "core_gps.location_service", "core_gps.time_authority"]
ble:
protocol_consumers: ["core_phone.meshtastic", "core_phone.meshcore"]
storage:
board_provider: "wio_tracker_l2.sdmmc"
platform_driver: "ArduinoSdmmcBlockDevice"
@@ -101,10 +105,12 @@ capability_bindings:
runtime_owner: "single_platform_playback_owner"
protocols:
meshtastic: {enabled: true, runtime_owner: "shared_protocol_runtime"}
meshcore: {enabled: true, runtime_owner: "shared_protocol_runtime"}
meshtastic: {enabled: true, runtime_owner: "shared_protocol_runtime", radio_profile_source: "core_mesh", ble_phone_core: "core_phone"}
meshcore: {enabled: true, runtime_owner: "shared_protocol_runtime", radio_profile_source: "core_mesh", ble_phone_core: "core_phone"}
reticulum: {enabled: true, runtime_owner: "existing_reticulum_runtime"}
selection: "one_active_product_protocol"
selection: "one_active_chat_protocol"
reticulum_ip_service: "available_independently_of_active_chat_protocol"
note: "Geocaching uses a background IP-only Reticulum service alongside Meshtastic or MeshCore; when Reticulum/RNode chat is active it reuses that backend. Chat/contact protocol partitions remain separate."
storage:
backend: "existing_SdFat_contract_over_1bit_SDMMC"
@@ -112,6 +118,7 @@ storage:
messages: "existing_protocol_partitioned_stores_and_ledgers"
node_store: "existing_protocol_partitioned_stores"
map_storage: "existing_map_runtime"
geocaching: "SD_indexed_journal_and_signed_GPX_details"
race_policy:
app_to_ui: "snapshot"
@@ -126,4 +133,14 @@ build:
entrypoint: "platformio.ini"
app: "apps/esp32_lvgl"
platformio_envs: ["wio_tracker_l2"]
enabled_modules:
- "core_chat"
- "core_device"
- "core_gps"
- "core_phone"
- "core_mesh"
- "core_geocaching"
- "ui_presentation"
- "core_sys"
- "ui_shared"
hardware_validation: "pending"
+6 -2
View File
@@ -72,8 +72,12 @@ def open_with_retries(
for attempt in range(1, attempts + 1):
try:
return urllib.request.urlopen(request, timeout=timeout)
except urllib.error.HTTPError:
raise
except urllib.error.HTTPError as exc:
if exc.code not in (408, 429, 500, 502, 503, 504) or attempt >= attempts:
raise
last_error = exc
exc.close()
time.sleep(attempt)
except (urllib.error.URLError, TimeoutError, http.client.RemoteDisconnected) as exc:
last_error = exc
if attempt >= attempts:
+88
View File
@@ -0,0 +1,88 @@
"""Pages downloads retry transient HTTP errors without hiding permanent failures."""
import importlib.util
import io
from pathlib import Path
import sys
import unittest
from unittest.mock import call, patch
import urllib.error
import urllib.request
scripts = Path(__file__).resolve().parents[1] / "scripts"
spec = importlib.util.spec_from_file_location("prepare_pages_site", scripts / "prepare_pages_site.py")
pages = importlib.util.module_from_spec(spec)
sys.modules[spec.name] = pages
sys.path.insert(0, str(scripts))
try:
spec.loader.exec_module(pages)
finally:
sys.path.pop(0)
class PagesDownloadRetryTests(unittest.TestCase):
def setUp(self):
self.request = urllib.request.Request("https://example.invalid/asset.bin")
def http_error(self, status):
return urllib.error.HTTPError(
self.request.full_url, status, "HTTP failure", {}, io.BytesIO(b"error")
)
def test_transient_http_failure_then_success(self):
for status in (408, 429, 500, 502, 503, 504):
with self.subTest(status=status):
error = self.http_error(status)
response = object()
with patch.object(pages.urllib.request, "urlopen", side_effect=[error, response]) as opened, \
patch.object(pages.time, "sleep") as slept:
self.assertIs(response, pages.open_with_retries(self.request, timeout=7))
self.assertEqual([call(self.request, timeout=7)] * 2, opened.call_args_list)
slept.assert_called_once_with(1)
self.assertTrue(error.fp.closed)
def test_transient_http_retries_exhausted(self):
errors = [self.http_error(500) for _ in range(3)]
with patch.object(pages.urllib.request, "urlopen", side_effect=errors) as opened, \
patch.object(pages.time, "sleep") as slept:
with self.assertRaises(urllib.error.HTTPError) as caught:
pages.open_with_retries(self.request)
self.assertIs(errors[-1], caught.exception)
self.assertEqual(3, opened.call_count)
self.assertEqual([call(1), call(2)], slept.call_args_list)
self.assertTrue(all(error.fp.closed for error in errors[:-1]))
errors[-1].close()
def test_permanent_http_errors_fail_immediately(self):
for status in (400, 401, 403, 404, 501, 505):
with self.subTest(status=status):
error = self.http_error(status)
with patch.object(pages.urllib.request, "urlopen", side_effect=error) as opened, \
patch.object(pages.time, "sleep") as slept:
with self.assertRaises(urllib.error.HTTPError) as caught:
pages.open_with_retries(self.request)
self.assertIs(error, caught.exception)
opened.assert_called_once_with(self.request, timeout=30)
slept.assert_not_called()
error.close()
def test_single_attempt_has_no_retry(self):
error = self.http_error(503)
with patch.object(pages.urllib.request, "urlopen", side_effect=error) as opened, \
patch.object(pages.time, "sleep") as slept:
with self.assertRaises(urllib.error.HTTPError):
pages.open_with_retries(self.request, attempts=1)
self.assertEqual(1, opened.call_count)
slept.assert_not_called()
error.close()
def test_network_errors_keep_existing_retry_behavior(self):
response = object()
with patch.object(pages.urllib.request, "urlopen", side_effect=[urllib.error.URLError("reset"), response]) as opened, \
patch.object(pages.time, "sleep") as slept:
self.assertIs(response, pages.open_with_retries(self.request))
self.assertEqual(2, opened.call_count)
slept.assert_called_once_with(1)
if __name__ == "__main__":
unittest.main()
@@ -6,6 +6,20 @@ without turning the architecture baseline PR into a behavior-changing rewrite.
## Current Known Categories
### Reticulum and Geocaching compatibility composition
The ESP Geocaching runtime binds its presentation source through
`modules/ui_shared/include/ui/screens/geocaching/geocaching_page_shell.h`.
This binding belongs in the composition root in a later extraction. The
storage-owner worker publishes snapshots and does not mutate LVGL objects.
The ESP LXMF/Reticulum compatibility adapters recognize the
`trailmate.geocache` destination and sign Geocaching records with the existing
Reticulum identity. This protocol-facing glue should move behind a shared
protocol/identity port when the adapters are thinned. It does not change board
drivers or radio-driver policy. Geocaching and Reticulum Settings also extend
the historical `core_sys` platform/UI contract layout described below.
### core_sys still contains platform UI contracts
`modules/core_sys/include/platform/ui/*` currently exposes platform-facing UI