main has been red since 4f7b4e3 for a reason visible nowhere in its diff.
The player-parity baselines describe what an UN-UPDATED display can do, so
they are judged against the SHIPPED source — `git show <latest tag>:…` —
rather than the working tree. actions/checkout defaults to a shallow clone
with no tags, so that lookup found nothing and the suite fell back to the
working tree. In a tree where today's QA had just fixed the players'
set_volume payload bug, the biconditional then demanded that
BASELINE.web/tizen gain audio.volume — for displays that cannot possibly
have the fix yet. Green locally, red in CI, and the failing assertion names
a baseline rather than the checkout that caused it.
So fetch the tags in the test job, and make the biconditionals SKIP when
there are none instead of asserting against the wrong source. That fallback
was never a slightly-early assertion; it was an inverted one. A skipped
assertion announces itself, a wrong one does not.
Verified both ways: with tags 15/15 pass, in a tagless shallow clone 13
pass + 2 skip + 0 fail (previously 1 fail).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014skWYXJUWhF73EvNPgB2AS
screen_off blanks a fielded panel for real (owner/admin FORCE_LOCK, else the
accessibility lock); screen_on is a logged no-op. One capability renders both
dashboard buttons, so withholding the pair to hide the dead ON button also takes
blank-at-night — the half that gets scheduled — away from every panel that has
not updated. Panels that have updated declare for themselves.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uaeo9MvzKoyXuN6ZsbhtkL
Two tests disagreed after the QA merges, and both were right about their own
half — which is what made the disagreement worth resolving rather than silencing.
A baseline describes what an UN-UPDATED display can do. The baselines were
justified against `git show v1.9.28:<source>` and then asserted against the
working tree, so the moment a player's payload bug was fixed the biconditional
demanded a baseline change for displays that cannot possibly have the fix yet. A
baseline entry moves when a fix SHIPS. It now reads the newest release tag, and
falls back to the tree when tags are unavailable (a shallow CI clone), because a
missing tag is a worse reason to fail a build than a slightly-early assertion.
While fixing it the helper threw a ReferenceError — the require was missing — and
its own broad catch swallowed it and quietly compared against the working tree
anyway. The catch now rethrows ReferenceError and TypeError. A fallback that
hides a programming error is the same failure shape as everything else this QA
pass found.
The BrightSign assertion encoded the older, more generous baseline: reboot needs
the BrightScript host bridge, and an undeclared unit is precisely the one we
cannot know has it. What that test is really pinning is that the row still
classifies as brightsign rather than decaying to `web` — so it now asserts that,
plus the video playback that is genuinely safe to assume.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uaeo9MvzKoyXuN6ZsbhtkL
The parity doc and the capability model had drifted from the players in both
directions, and nothing failed when they did. Auditing all four players against
their shipped sources turned up three controls a customer can press today that
change nothing, and a set of baselines that were partly too generous and partly
too stingy.
The three dead controls:
- The volume slider works on Android only. The dashboard sends set_volume as
{ level: 0..1 }; the web player reads payload.value and Tizen reads
payload.value ?? payload.volume, so on both the number is undefined and the
handler quietly declines. Three complete, working volume implementations
that cannot be driven. The fix is one line in each player and belongs to
those files; audio.volume is out of the web and brightsign baselines until
it lands, held there by a biconditional test that fails the moment a player
starts reading `level`.
- Every #161 Tier-2 command was refused for the entire fleet. lock_now,
power_menu, status_bar, block_uninstall and unblock_uninstall were gated on
system.device_owner, which no player declares and no baseline grants, so
supports() was false everywhere -- including on the device-owner panels the
feature was built for. The dashboard still drew the buttons because it also
gates on device.tier === 2. Fixed here: those five now accept
system.device_owner OR system.kiosk, which PlayerCapabilities.kt declares
under `if (isOwner)` and nothing else, and which no non-Android player
declares. Android should declare system.device_owner and retire the
stand-in.
- enable_system_capture required the capability it creates. It raises the
MediaProjection consent dialog -- the way a panel GAINS capture -- and was
gated on remote.screenshot, so the only panel that needs it was the one
panel that could not be sent it. Now ungated. The dashboard still hides the
button behind the same check; that half is a frontend change.
The baselines describe what an un-updated fielded display can do, and since
v1.9.29 is the first build in which any player declares anything, that means
v1.9.28. Every entry is now justified against `git show v1.9.28:<source>`:
- android loses display.power (v1.9.28 answers screen_on with a logged no-op,
so the ON half is dead on every fielded panel and one capability renders
both buttons) and system.reboot (owner-only; off-owner it paints an
accessibility power dialog over the signage). Scheduled reboots now skip
undeclared Android panels rather than logging a reboot that never happened,
which is the reason that gate exists.
- tizen gains display.power: v1.9.28 implements both halves with no signing
and no panel API, so withholding it hid a working control.
- brightsign loses audio.volume, display.power, system.reboot,
system.restart_player and offline.cache. All need a host bridge the unit is
not known to have, and restart_player without one is the page reload that
darkened a panel on 2026-07-28.
Also found, not fixed here because the files belong to others:
st-bridge.js computeCapabilities() is dead code -- nothing calls BS.capabilities()
-- and its 199 lines of passing tests constrain nothing a BrightSign actually
declares; the two disagree on six capabilities and the bridge is right about
most of them. BrightSign's "Force update" button is dead. PlayerCapabilities.kt
under-declares display.brightness.
The new test reads the player sources rather than the table: a dead-button rule
(every gated command has a branch somewhere), an unreachable-capability rule
(which would have caught system.device_owner), and biconditionals so a fix in a
player fails the test until the baseline follows. Claims that need hardware --
CEC reaching a display, a widget being allowed a service worker, SyncManager
holding frame lock -- are marked unverifiable in the document instead of
asserted.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uaeo9MvzKoyXuN6ZsbhtkL