Lint the commits being pushed and scope overlay failures per sensor

The pre-push gate linted only the dirty tree, so the commits about to be pushed went
unchecked; and every failed overlay projection was sticky, which cost a finger result its
overlay over one bad frame while box results were retried nowhere. gate.ps1 now takes
-LintBase (default origin/main, exit 2 on an unknown ref), the projector declares whether
its own failures are permanent, and serve_stored_view decides before it sets the view.
Verified with gate.ps1 (PASS: 569 studio + 485 shared tests, 632 ui_check checks) and the
two round-2 red samples kept in scratch.
This commit is contained in:
ykcheungab
2026-10-18 21:17:03 +08:00
parent 0998009ec3
commit c37efd7a44
9 changed files with 648 additions and 260 deletions
+12 -5
View File
@@ -11,15 +11,22 @@ generated pixels, and a synthetic 896² clip sent the chain into minutes of work
| | |
|---|---|
| Sensor | `box` (shipped bundle) |
| Source | `dist/OrisysStudio/outputs/recordings/20260930T015028Z_session0002_40cb8e7441434feab48bb646afdba011`, recorded 2026-09-30, `session_id` 2 |
| Source | [`.scratch/result-visible/recording/20260930T015028Z_session0002_40cb8e7441434feab48bb646afdba011/`](../../../../.scratch/result-visible/recording/20260930T015028Z_session0002_40cb8e7441434feab48bb646afdba011/) — recorded 2026-09-30, `session_id` 2. The directory name is the recording's own identity, so it matches the `dist/` path below. |
| Length | 33 frames, frame ids **20…52**, 1.92 s of signal time |
| Files | `raw_0001.mp4`, `frames.csv`, `forces.csv`, `events.csv`, `session.json` (3.8 MB) |
| Not included | `preroll_0001.mp4` (3.5 MB, optional for every reader: `RecordingReader.preroll_segment()` answers `None` when the file is absent) |
`dist/` is build output and is not tracked, so the copy under it disappeared on
2026-10-14 when the studio was repackaged. That took 11 tests to `ERROR` and 4 to
`failed` without anyone noticing — see
[`docs/test-repair-plan.zh.md`](../../../docs/test-repair-plan.zh.md) §1 and §3 #1–#2.
`dist/` is build output and is not tracked, so the copy under `dist/OrisysStudio/outputs/recordings/` disappeared on 2026-10-14 when the studio was repackaged. That took 11 tests to `ERROR` and 4 to `failed` without anyone noticing — see [`docs/test-repair-plan.zh.md`](../../../docs/test-repair-plan.zh.md) §1 and §3 #1–#2.
The `dist/` original is **gone**, so the files here cannot be compared against it byte for byte. They are byte-identical to the copy preserved under `.scratch/result-visible/` (the source named above), which is the strongest provenance available. The hashes are recorded here so a future copy can be checked against this one instead of argued about:
| File | SHA256 |
|---|---|
| `raw_0001.mp4` | `907867d0fe82966dbaacf97a8bb0a8d5647de25458ad5fd4640535d83d417207` |
| `frames.csv` | `fab25e6780908aef8f7616504b43cf34062ab7ca3ba317b6ff4c0631969e0601` |
| `forces.csv` | `972d3c951ff0be847a6a224b53339401f115cd3728039f1acb24a33bf215ecb1` |
| `events.csv` | `6fe7121b1c621ebed472ee42376babfd76a32a364ab9f11660da077fb365a946` |
| `session.json` | `f3c9afc4502180d2a88cd260cb70f1f4313e203f2c3c8441704de6ac53a1db4c` |
The frame ids are deliberately not 0…32: a test that used the frame *index* would pass on
a 0-based recording, and real recordings start wherever the device's counter was.
@@ -0,0 +1,147 @@
"""How long an overlay-base failure lasts: one frame, or the whole session.
R2 of ``docs/test-repair-rework.zh.md``. The second round made every failed projection
sticky, so one bad finger frame cost that result its overlay view for the session — a
behaviour change beyond the box case it was written for. The scope is now the
projector's own declaration (``overlay_base.OverlayBaseProjector``), never a guess made
from a ``None`` or from an exception's text:
* a **finger** unroll that loses a frame is transient — the next frame is tried and the
session is *not* marked unavailable;
* a **box** stitch that cannot rebuild the lenslet grid on this footage is permanent —
the session records it and stops asking, so scrubbing does not rebuild the same
failure on every cursor move;
* a projector that cannot be **opened** at all is permanent, because no frame can work.
No SDK and no recording are involved: the projector is given a stand-in ``project``, and
the session is handed that projector directly, so these run on any machine.
"""
from __future__ import annotations
from pathlib import Path
from types import SimpleNamespace
import numpy as np
import pytest
from orisys_app_shared.analysis.model import Provenance
from orisys_app_shared.analysis.overlay_base import (
OverlayBaseProjector,
projection_failures_are_permanent,
)
from orisys_studio.replay_analysis import AnalysisSession
FRAME = np.zeros((6, 6, 3), dtype=np.uint8)
BASE = np.full((6, 6, 3), 7, dtype=np.uint8)
class _Reader:
"""The smallest reader ``AnalysisSession`` accepts, with no result behind it."""
complete = True
key = "overlay-scope"
directory = Path(".")
frame_ids = np.array([1, 2], dtype=np.int64)
times = np.array([0.0, 0.1])
keyframes: dict = {}
provenance = Provenance()
def mask(self):
return None
def frame(self, frame_id):
return SimpleNamespace(depth=np.zeros((6, 6), dtype=np.float32))
def grid(self, grid_id):
raise AssertionError("no grid in this stub")
def _session(projector) -> AnalysisSession:
"""A session whose projector is supplied directly (its provenance is not used)."""
session = AnalysisSession(_Reader())
session._overlay = projector
return session
def test_one_bad_finger_frame_does_not_cost_the_session_its_overlay():
"""A finger unroll that fails on one frame is retried on the next."""
answers = [None, BASE]
projector = OverlayBaseProjector(
sensor_type="finger",
project=lambda image: answers.pop(0),
projection_failures_are_permanent=False,
)
session = _session(projector)
assert session.overlay_base(FRAME, frame_id=1) is None
assert session.overlay_unavailable is False, "one frame made the whole result unavailable"
base = session.overlay_base(FRAME, frame_id=2)
assert base is not None, "the second frame was never projected"
assert np.array_equal(base, BASE)
assert session.overlay_unavailable is False
def test_a_finger_projection_that_raises_is_also_only_that_frame():
"""The exception text is not consulted: a raising finger projection is transient."""
answers = [RuntimeError("unroll miss"), BASE]
def project(image):
answer = answers.pop(0)
if isinstance(answer, Exception):
raise answer
return answer
session = _session(
OverlayBaseProjector(
sensor_type="finger",
project=project,
projection_failures_are_permanent=False,
)
)
assert session.overlay_base(FRAME, frame_id=1) is None
assert session.overlay_unavailable is False
assert session.overlay_base(FRAME, frame_id=2) is not None
def test_a_box_projection_that_fails_is_permanent_and_is_not_retried():
"""A box stitch that fails will fail again; the session stops asking."""
calls: list[int] = []
def project(image):
calls.append(1)
return None
session = _session(OverlayBaseProjector(sensor_type="box", project=project))
assert session.overlay_base(FRAME, frame_id=1) is None
assert session.overlay_unavailable is True
assert session.overlay_base(FRAME, frame_id=2) is None
assert len(calls) == 1, "a permanent failure was retried on the next frame"
def test_a_projector_that_cannot_open_is_permanent(monkeypatch):
"""No projector at all is the other permanent case: no frame could ever work."""
calls: list[int] = []
@classmethod
def _cannot_open(cls, provenance):
calls.append(1)
return None
monkeypatch.setattr(OverlayBaseProjector, "open_from_provenance", _cannot_open)
session = AnalysisSession(_Reader())
assert session.overlay_base(FRAME, frame_id=1) is None
assert session.overlay_unavailable is True
assert session.overlay_base(FRAME, frame_id=2) is None
assert len(calls) == 1, "opening was retried after it had already failed"
@pytest.mark.parametrize(
("sensor_type", "permanent"),
(("finger", False), ("box", True)),
)
def test_the_sensor_decides_which_kind_of_failure_it_has(sensor_type, permanent):
"""The policy lives with the sensor's opener, not in each caller's judgement."""
assert projection_failures_are_permanent(sensor_type) is permanent
+58
View File
@@ -772,3 +772,61 @@ def test_a_bare_video_with_a_declined_prompt_promises_no_pass(
)
assert t(RAW_NOTE_KEY) not in window.arrow_panel.note_label.text()
assert plan_image(captured[-1]).note_key == "panels.replay_analysis_failed"
def test_adopting_a_box_result_never_flashes_the_overlay_refusal(
window, stored_recording, monkeypatch
):
"""R3: pick the drawable view once, draw once, and never show the refusal on the way.
The box result has no stored unroll map and its stitch rebuild fails on this footage,
so the overlay base cannot be built. The pre-rework order — set the overlay view,
render, discover the failure, switch, render again — put
``panels.arrow_capture_unavailable`` on screen while adopting the result, and drew the
same frame twice. The decision is now made before the view is set, so the panel is set
once and rendered once, and the refusal is still what the operator gets when they ask
for the overlay themselves.
"""
from orisys_app_shared.i18n import t
controller = window.replay_ctl
refusal = t("panels.arrow_capture_unavailable")
seen: list[str] = []
renders: list[int] = []
inside = {"serve": False}
original_render = controller.render_frame
def render_spy():
if inside["serve"]:
renders.append(1)
original_render()
seen.append(window.arrow_panel.image_label.text())
else:
original_render()
original_serve = controller.serve_stored_view
def serve_spy():
inside["serve"] = True
try:
return original_serve()
finally:
inside["serve"] = False
monkeypatch.setattr(controller, "render_frame", render_spy)
monkeypatch.setattr(controller, "serve_stored_view", serve_spy)
_open(window, stored_recording, monkeypatch)
assert inside["serve"] is False, "adoption never reached the view it serves"
assert len(renders) == 1, f"adoption rendered the frame {len(renders)} times, not once"
assert refusal not in seen, "the overlay refusal was shown while adopting the result"
assert controller._image_panel.view_mode() == ARROW_PURE, (
"a result whose overlay cannot be drawn was adopted on the overlay view"
)
# The overlay is still on offer, and choosing it still says why it cannot be drawn.
controller._image_panel.set_view_mode(ARROW_STITCHED)
controller.render_frame()
assert window.arrow_panel.image_label.text() == refusal