Serve a per-node-type manifest alongside the shared one #82

Merged
Claude merged 3 commits from feat/per-type-manifest into main 2026-09-21 07:51:06 +00:00
Collaborator

CCS-UHA-24. Firmware side (CCS-UF-20) moves a node onto /firmware/manifest/0x<type>.json instead of downloading the whole shared manifest, because the fleet's entry count now exceeds the node's 2048-byte buffer.

Routes the new path in updater/file_server.py: a bare object with the same four members already generated per entry, no images wrapper, matched with a strict 0x + two lower-case hex digits pattern (anything else 404s). A node type with nothing cached gets a genuine 404. The shared /firmware/manifest.json keeps serving unchanged, for firmware that still asks for it. Guarded at 512 bytes, matching the node's smaller buffer for this path (firmware_manifest.h).

Tested: new cases cover the per-type file, the 404 for an unpublished type, the bare-object body, lower-case-only matching, a malformed type 404ing, the body staying under 512 bytes, and the shared manifest still working. Full suite and ruff pass.

CCS-UHA-24. Firmware side (CCS-UF-20) moves a node onto `/firmware/manifest/0x<type>.json` instead of downloading the whole shared manifest, because the fleet's entry count now exceeds the node's 2048-byte buffer. Routes the new path in `updater/file_server.py`: a bare object with the same four members already generated per entry, no `images` wrapper, matched with a strict `0x` + two lower-case hex digits pattern (anything else 404s). A node type with nothing cached gets a genuine 404. The shared `/firmware/manifest.json` keeps serving unchanged, for firmware that still asks for it. Guarded at 512 bytes, matching the node's smaller buffer for this path (`firmware_manifest.h`). Tested: new cases cover the per-type file, the 404 for an unpublished type, the bare-object body, lower-case-only matching, a malformed type 404ing, the body staying under 512 bytes, and the shared manifest still working. Full suite and `ruff` pass.
Serve a per-node-type manifest alongside the shared one
All checks were successful
Lint, type check and test / hassfest (pull_request) Successful in 8s
Lint, type check and test / quality (pull_request) Successful in 1m43s
Lint, type check and test / release (pull_request) Has been skipped
d2bae986f4
Claude left a comment

Implementation checks out against every requirement: lower-case-only regex (fullmatch, no IGNORECASE), bare 4-member body, 512-byte guard distinct from the shared 2048, genuine 404 for an uncached type and for a malformed segment, shared /firmware/manifest.json route untouched, traversal defence stays structural (looked up via the advertised dict, never joined to the filesystem). Diff is minimal, manifest_body/offers untouched. Tabs, British spelling, comments carry rationale rather than restating code. Full suite (1177 passed) and ruff check/format --check are clean on this branch.

Two minor test gaps, neither blocking:

  1. tests/test_node_file_server.py::test_the_per_type_manifest_path_matches_lower_case_only — the upper_digit case (/firmware/manifest/0x1A.json) doesn't actually exercise digit-level case folding. Node type 0x1A is never cached in the test, so it 404s via "type not found" regardless of whether the regex were case-sensitive on hex digits — a regression to [0-9a-fA-F] would slip through this assertion unnoticed. Only the upper_prefix case (0X12, where 0x12/bathroom is cached) is a genuine regression test. Worth caching an image with a hex-letter type and asserting 0x1a.json is 200 while 0x1A.json is 404, or dropping the misleading half.

  2. type_manifest_body's over-512-bytes warning branch (updater/file_server.py:119-125) is the one uncovered line in the file — no test exercises it, unlike the shared manifest's equivalent (test_a_manifest_a_node_would_refuse_is_warned_about).

Implementation checks out against every requirement: lower-case-only regex (`fullmatch`, no `IGNORECASE`), bare 4-member body, 512-byte guard distinct from the shared 2048, genuine 404 for an uncached type and for a malformed segment, shared `/firmware/manifest.json` route untouched, traversal defence stays structural (looked up via the `advertised` dict, never joined to the filesystem). Diff is minimal, `manifest_body`/`offers` untouched. Tabs, British spelling, comments carry rationale rather than restating code. Full suite (1177 passed) and `ruff check`/`format --check` are clean on this branch. Two minor test gaps, neither blocking: 1. `tests/test_node_file_server.py::test_the_per_type_manifest_path_matches_lower_case_only` — the `upper_digit` case (`/firmware/manifest/0x1A.json`) doesn't actually exercise digit-level case folding. Node type `0x1A` is never cached in the test, so it 404s via "type not found" regardless of whether the regex were case-sensitive on hex digits — a regression to `[0-9a-fA-F]` would slip through this assertion unnoticed. Only the `upper_prefix` case (`0X12`, where `0x12`/bathroom *is* cached) is a genuine regression test. Worth caching an image with a hex-letter type and asserting `0x1a.json` is 200 while `0x1A.json` is 404, or dropping the misleading half. 2. `type_manifest_body`'s over-512-bytes warning branch (`updater/file_server.py:119-125`) is the one uncovered line in the file — no test exercises it, unlike the shared manifest's equivalent (`test_a_manifest_a_node_would_refuse_is_warned_about`).
Make the manifest tests exercise the case guard and the size guard
Some checks failed
Lint, type check and test / hassfest (pull_request) Successful in 8s
Lint, type check and test / quality (pull_request) Failing after 1m24s
Lint, type check and test / release (pull_request) Has been skipped
1328ea06df
Claude left a comment

Round 2: both round-1 gaps are properly closed.

  1. Case test now caches node_underfloor (0xA0) and requests 0xA0.json, so it's a real regression test — verified by mutating the regex to re.IGNORECASE, which makes it fail as expected.
  2. The new test_a_type_manifest_a_node_would_refuse_is_warned_about exercises the >512 branch in type_manifest_body directly — verified by removing the warning branch, which makes it fail. Building CachedImage by hand is consistent with the existing image() helper pattern in tests/test_firmware_versions.py, and is the right call here since the real HTTP path can never reach 512 bytes (image paths are capped at 63 by MAX_IMAGE_PATH, well under what's needed).

Full suite (1178 passed, 3 skipped) and ruff check/ruff format --check both clean. updater/file_server.py is at 100% coverage. No further issues — clean.

Round 2: both round-1 gaps are properly closed. 1. Case test now caches `node_underfloor` (0xA0) and requests `0xA0.json`, so it's a real regression test — verified by mutating the regex to `re.IGNORECASE`, which makes it fail as expected. 2. The new `test_a_type_manifest_a_node_would_refuse_is_warned_about` exercises the >512 branch in `type_manifest_body` directly — verified by removing the warning branch, which makes it fail. Building `CachedImage` by hand is consistent with the existing `image()` helper pattern in `tests/test_firmware_versions.py`, and is the right call here since the real HTTP path can never reach 512 bytes (image paths are capped at 63 by `MAX_IMAGE_PATH`, well under what's needed). Full suite (1178 passed, 3 skipped) and `ruff check`/`ruff format --check` both clean. `updater/file_server.py` is at 100% coverage. No further issues — clean.
Re-run CI after an unrelated flake
All checks were successful
Lint, type check and test / hassfest (pull_request) Successful in 17s
Lint, type check and test / quality (pull_request) Successful in 1m57s
Lint, type check and test / release (pull_request) Has been skipped
ca9b2dfce9
Claude merged commit 4748216ab5 into main 2026-09-21 07:51:06 +00:00
Claude deleted branch feat/per-type-manifest 2026-09-21 07:51:06 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
rob/CampervanHomeAssistant!82
No description provided.