Show only the bundle's name in the sensor configuration row

The row carried the origin, the calibration selection and the SDK's verdict, which a 30 px
line cannot explain; it is now the descriptor's display_name, else the folder name, and the
sensor dialog states the rest under the selector — name and folder, profile, shape, root,
verdict, calibration and every finding — with a suffix only where two rows would otherwise
read alike. Verified with the studio suite, the catalog tests in shared/tests, ui_check and
the lint gate.
This commit is contained in:
ykcheungab
2026-10-05 17:01:42 +08:00
parent 92b2f2fcd7
commit d11441cd79
7 changed files with 457 additions and 135 deletions
@@ -7,8 +7,9 @@ descriptor bundle, a legacy ``sensor.json`` bundle, or an entry that stays liste
with the SDK's own reason.
The two functions below stay the picker's compatibility surface. The picker itself uses the
catalog object directly, so it can show each entry's name, origin and calibration selection,
with the identity and the SDK's verdict in the tooltip.
catalog object directly, so it can show each entry's name — with the origin only where two rows
would otherwise read alike — and keep the identity and the SDK's verdict in the details surface
below the selector.
"""
from __future__ import annotations
@@ -29,19 +30,23 @@ def list_sensor_types() -> list[str]:
def list_configs_for_type(sensor_type: str) -> list[tuple[str, str]]:
"""Return ``(display_name, config_key)`` entries for one family.
"""Return ``(row_name, config_key)`` entries for one family.
The key is the configuration file path the app already passes around (``sensor.json``, or
the bundle folder for an entry that has none), so a selection keeps its meaning. When no
bundle exists at all, a single default entry named after the type is offered — for
preferences and a future Run — exactly as before.
the bundle folder for an entry that has none), so a selection keeps its meaning. The name is
the row the picker shows: the bundle's own name, with the origin appended only where two
entries offered together would read alike (see
:func:`~orisys_app_shared.sensors.bundle_catalog.row_labels`). When no bundle exists at all, a
single default entry named after the type is offered — for preferences and a future Run —
exactly as before.
"""
name = str(sensor_type or "").strip()
if not name:
return []
entries = scan_sensor_catalog().entries_for_family(name)
catalog = scan_sensor_catalog()
entries = catalog.entries_for_family(name)
if entries:
return [(entry.label(), entry.key) for entry in entries]
return [(catalog.row_label(entry), entry.key) for entry in entries]
if name == FAMILY_UNIDENTIFIED:
return []
return [(name, name)]
+107 -31
View File
@@ -10,14 +10,17 @@ selected, and two bundles sharing one ``profile_id``.
The tree is built here, in the test's own temporary directory: a committed fixture would be a
second copy of the bundle format that could drift from the real one without anyone noticing.
The labels are asserted here too, because they are the catalog's whole operator-facing
contract: what a configuration is, where it came from and what it will give — never a
``profile_id``, an ``ok``/``degraded``/``failed`` verdict, or the word ``legacy``.
The rows are asserted here too, because they are the catalog's whole operator-facing contract: a
row is the bundle's name — the descriptor's ``display_name``, else the folder name — and never a
``profile_id``, an ``ok``/``degraded``/``failed`` verdict, the origin word, the calibration
phrasing or the word ``legacy``. A suffix is added in exactly one case, two visible entries that
would otherwise read alike, and the suffix is asserted to be the one that tells them apart.
"""
from __future__ import annotations
import json
import re
from pathlib import Path
from typing import Any
@@ -86,6 +89,11 @@ def _write(path: Path, payload: object) -> Path:
return path
def _WORD_RE(word: str) -> re.Pattern[str]:
"""``word`` matched whole: a folder may legitimately contain its letters (``broken-manifest``)."""
return re.compile(rf"\b{re.escape(word)}\b")
@pytest.fixture
def trees(tmp_path):
"""One shipped root and one user root covering every shape the catalog must handle."""
@@ -256,31 +264,77 @@ def test_an_entry_the_sdk_rejects_is_never_reported_as_startable(trees):
assert (entry.start_blocked_reason is None) == expected, entry.ref
def test_the_label_states_what_it_is_where_it_came_from_and_what_it_gives(trees):
entry = trees.by_ref("user/mini-lab")
def test_the_row_is_the_bundles_name_and_nothing_else(trees):
"""The picker row stopped carrying the origin, the calibration and the verdict.
A legacy bundle with no collision is named by the SDK's built-in table for its folder; that
name, and nothing after it, is the row. What moved out of the row is asserted where it has
to be readable instead: the dialog's details surface, in ``apps/studio/tests``.
"""
entry = trees.by_ref("shipped/box")
assert entry is not None
assert entry.label() == "Mini Lab · User · No calibration selected — raw force"
assert trees.row_label(entry) == "Box"
assert trees.row_label(entry) == entry.name
def test_a_startable_label_carries_no_identity_and_no_severity_word(trees):
"""What an operator chooses between is products and outcomes, not internal names."""
def test_no_row_carries_identity_origin_calibration_or_a_severity_word(trees):
"""What an operator chooses between is products, not internal names or severities.
Checked two ways: through the shape of the row — the name, or the name with a collision
suffix and nothing else, so nothing can have been appended — and as whole words, because a
folder called ``broken-manifest`` legitimately contains the letters of ``ok``.
"""
for entry in trees.entries:
if not entry.valid:
continue
label = entry.label()
assert entry.profile_id not in label, label
for word in ("ok", "degraded", "failed", "legacy"):
assert word not in label, f"{word!r} reached the row: {label}"
row = trees.row_label(entry)
assert row == entry.name or row.startswith(f"{entry.name} ("), row
if entry.profile_id:
assert entry.profile_id not in row, row
for word in ("ok", "degraded", "failed", "legacy", "Shipped", "Calibration"):
assert _WORD_RE(word).search(row) is None, f"{word!r} reached the row: {row}"
def test_no_calibration_selected_reads_as_raw_force_not_as_a_defect(trees):
entry = trees.by_ref("user/mini-lab")
assert entry is not None
# The SDK reports the absent calibration as a warning, which is the state the row used to
# read as "degraded" — a severity word that never said "raw force" or "not selected".
assert entry.state == "degraded"
assert "No calibration selected — raw force" in entry.label()
assert entry.selected_calibration == ""
def test_two_roots_offering_one_name_are_told_apart_by_origin(tmp_path):
"""``shipped/box`` beside ``sensors-user/box``: two entries, two tellable rows."""
shipped = tmp_path / "shipped"
user = tmp_path / "user"
for root in (shipped, user):
_write(root / "box" / "sensor.json", _config(stitcher=True))
catalog = scan_sensor_catalog(shipped_root=shipped, user_root=user)
first = catalog.by_ref("shipped/box")
second = catalog.by_ref("user/box")
assert first is not None and second is not None
assert first.name == second.name == "Box", "the premise of a collision"
assert catalog.row_label(first) == "Box (shipped)"
assert catalog.row_label(second) == "Box (user)"
# Neither is shadowed: both are offered by the family the picker lists them under.
offered = [entry.ref for entry in catalog.entries_for_family("box")]
assert offered == ["shipped/box", "user/box"]
def test_one_root_declaring_one_name_twice_is_told_apart_by_folder(trees):
"""The origin cannot separate two entries of one root, so the folder does.
``mini-lab`` and ``mini-cal`` both declare ``Mini Lab``: one name, one root, two bundles. A
row that reads the same as another row in the same list would be a silent collapse, so each
carries the folder — the identity that actually differs.
"""
mini_lab = trees.row_label(trees.by_ref("user/mini-lab"))
mini_cal = trees.row_label(trees.by_ref("user/mini-cal"))
assert mini_lab == "Mini Lab (mini-lab)"
assert mini_cal == "Mini Lab (mini-cal)"
assert mini_lab != mini_cal
def test_a_name_two_entries_share_is_suffixed_without_touching_the_other_families(trees):
"""Only the entries that collide carry a suffix; the picker is not made noisy for them."""
seen = {entry.key: trees.row_label(entry) for entry in trees.entries}
assert seen[trees.by_ref("user/missing-assets").key] == "Finger R01 (map missing)"
assert seen[trees.by_ref("shipped/box").key] == "Box"
# Every row of one family group is unique, which is the invariant the suffix exists for.
for family in {entry.family for entry in trees.entries}:
rows = [trees.row_label(entry) for entry in trees.entries_for_family(family)]
assert len(rows) == len(set(rows)), (family, rows)
def test_a_selected_calibration_reads_as_a_selection(trees):
@@ -289,24 +343,44 @@ def test_a_selected_calibration_reads_as_a_selection(trees):
assert entry.state == "ok", entry.reason
assert entry.selected_calibration == "force_calibration.json"
# Named, and said as something selected — never as a property of the bundle ...
assert entry.label() == "Mini Lab · User · Calibration: force_calibration.json"
assert entry.calibration_selection == "force_calibration.json"
# ... and the "ok" the SDK calls that is not a word an operator needs to read.
assert "ok" not in entry.label()
assert "ok" not in trees.row_label(entry)
def test_an_unstartable_label_says_cannot_start_with_the_sdks_own_reason(trees):
def test_no_calibration_selected_reads_as_raw_force_not_as_a_defect(trees):
entry = trees.by_ref("user/mini-lab")
assert entry is not None
# The SDK reports the absent calibration as a warning, which the row used to read as
# "degraded" — a severity word that never said "raw force" or "not selected".
assert entry.state == "degraded"
assert entry.selected_calibration == ""
assert entry.calibration_selection == "No calibration selected — raw force"
def test_an_unstartable_entry_keeps_its_name_and_its_reason(trees):
"""The row is the name; the reason the entry cannot start is still the SDK's own."""
entry = trees.by_ref("user/missing-assets")
assert entry is not None and not entry.valid
assert entry.label() == f"Finger R01 (map missing) · Cannot start: {entry.reason}"
assert "unroll_map" in entry.label()
assert trees.row_label(entry) == "Finger R01 (map missing)"
assert entry.start_blocked_reason == entry.reason
assert "unroll_map" in entry.start_blocked_reason
# The reason quotes the errors it was built from, so the sentence forms are the ones a
# finding that is *not* already in it still needs — here, the calibration warning.
assert entry.warning_sentences()
assert entry.error_sentences() == ()
def test_a_duplicate_label_names_the_twin(trees):
def test_a_duplicate_keeps_its_name_and_names_its_twin(trees):
first = trees.by_ref("user/dup-a")
second = trees.by_ref("user/dup-b")
assert first is not None and second is not None
assert first.label() == "Mini Lab · User · Also provided by user/dup-b"
assert second.label() == "Mini Lab · User · Also provided by user/dup-a"
assert first.duplicates == ("user/dup-b",)
assert second.duplicates == ("user/dup-a",)
assert trees.row_label(first) == "Mini Lab (dup-a)"
assert trees.row_label(second) == "Mini Lab (dup-b)"
assert "user/dup-b" in first.start_blocked_reason
assert "user/dup-a" in second.start_blocked_reason
def test_the_tooltip_keeps_the_identity_the_shape_the_state_and_the_findings(trees):
@@ -315,10 +389,12 @@ def test_the_tooltip_keeps_the_identity_the_shape_the_state_and_the_findings(tre
assert entry is not None
tooltip = entry.tooltip()
assert entry.profile_id in tooltip
assert entry.folder in tooltip, "the row may be a display name, so the folder must be here"
assert str(entry.bundle_path) in tooltip
assert "descriptor bundle" in tooltip
assert f"SDK validation: {entry.state}" in tooltip
assert "processing can start" in tooltip, "the verdict word is left unexplained"
assert "calibration: none selected — raw force" in tooltip
assert f"calibration selection: {entry.calibration_selection}" in tooltip
# A finding is a sentence: the code is dropped, the capability it names is not.
assert "calibration.absent" not in tooltip
assert "calibrated force is unavailable; raw force is available" in tooltip