screentinker/server/test/device-command-gating.test.js
ScreenTinker 0082191f9b Show only the controls a display can actually honour
Every device control was offered to every display. A browser tab was shown
"Reboot device", a Tizen TV was shown screen power, a player with no
framebuffer read was shown a live view that stayed black. They all looked
like working buttons and did nothing — the "reports success and changes
nothing" shape that keeps costing people days.

Players now declare what they can do at registration, because only the
player knows at runtime: an Android panel gains real screenshots when
accessibility is switched on and loses Tier-2 when device owner is revoked.
The dashboard hides what is not supported rather than disabling it, and the
Info tab lists the capability set so a missing control is explainable.

The declaration is three-state and the middle state is load bearing: NULL
means "has never told us anything" and falls back to a per-platform
baseline, because several hundred displays in the field will not update
before this deploys and blanking their controls would be a far worse bug.
An empty array means "I genuinely can do nothing" and is honoured.

Hiding a button is not enforcement, so unsupported commands are also
refused server-side — the socket is reachable directly and a stale tab
still renders the old controls. Group sends report skipped devices
separately from sent ones; counting an unreachable member as "sent" is how
an operator walks away believing the whole group rebooted.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uaeo9MvzKoyXuN6ZsbhtkL
2026-08-05 14:24:52 -05:00

88 lines
5 KiB
JavaScript

'use strict';
// Hiding a button is not enforcement, and the dashboard is not the only way to send a command.
// The socket is reachable directly, a group send fans out to a mixed-platform fleet, and an
// operator with a tab open from before the panel declared anything still has the old controls on
// screen. In every one of those paths a command the player cannot honour used to be DELIVERED and
// silently dropped — the "reports success and changes nothing" shape again, one layer down.
//
// So the refusal lives on the server and names the capability, and the group route reports the
// skipped devices separately from the ones it actually reached. A group toast that counts an
// unreachable web player as "sent" is how an operator walks away believing the whole group
// rebooted.
const { test } = require('node:test');
const assert = require('node:assert/strict');
const caps = require('../lib/player-capabilities');
test('a browser tab is refused reboot, and the refusal says which capability was missing', () => {
const web = { android_version: 'Web/Chrome' };
const verdict = caps.commandAllowed(web, 'reboot');
assert.equal(verdict.ok, false);
assert.equal(verdict.capability, 'system.reboot', 'the operator has to be told WHY, not just "no"');
});
test('the legacy fleet is not locked out of the commands it has always accepted', () => {
// The failure mode that would be worse than the bug: several hundred Android displays declare
// nothing, and a refusal keyed off "declared nothing => supports nothing" bricks every control
// in the product at once.
const legacy = { client_type: 'apk', android_version: '9' };
for (const cmd of ['reboot', 'launch', 'refresh', 'update', 'screen_on', 'screen_off', 'set_volume']) {
assert.equal(caps.commandAllowed(legacy, cmd).ok, true, `${cmd} must still reach a legacy Android panel`);
}
});
test('a command with no capability requirement is never refused', () => {
// set_debug is diagnostics. Gating it would take away the tool you reach for precisely when a
// panel is misreporting what it can do.
assert.equal(caps.capabilityForCommand('set_debug'), null);
assert.equal(caps.commandAllowed({ android_version: 'Web/Chrome' }, 'set_debug').ok, true);
});
test('an unrecognised command type is passed through, not silently swallowed', () => {
// New player features ship before the server learns their names. Refusing by default would make
// every such command fail with a confusing "unsupported" instead of reaching the panel.
assert.equal(caps.capabilityForCommand('some_future_command'), null);
assert.equal(caps.commandAllowed({ client_type: 'apk' }, 'some_future_command').ok, true);
});
test('shutdown and reboot share one privilege, so a panel cannot be half-refused', () => {
// They are the same "device power lifecycle" authority. Splitting them produced a UI with
// Shutdown present and Reboot missing on the same display, which reads as a broken dashboard.
assert.equal(caps.capabilityForCommand('shutdown'), caps.capabilityForCommand('reboot'));
});
test('the per-window dim is NOT the backlight — conflating them hides a working slider', () => {
// set_brightness is the player's own overlay (Android Tier 0, no device owner);
// set_system_brightness writes the real backlight and needs settings-write. Mapping both to
// system.brightness — which is deliberately absent from every baseline because it is
// conditional — would have removed the overlay slider from the entire undeclared Android fleet.
const legacy = { client_type: 'apk', android_version: '11' };
assert.equal(caps.commandAllowed(legacy, 'set_brightness').ok, true, 'overlay dim has always worked here');
assert.equal(caps.commandAllowed(legacy, 'set_system_brightness').ok, false, 'backlight is conditional');
assert.notEqual(caps.capabilityForCommand('set_brightness'), caps.capabilityForCommand('set_system_brightness'));
});
test('every command in the map points at a capability that actually exists', () => {
// A typo here does not fail loudly: supports() returns false for an unknown name, so the command
// is refused for EVERY device on every platform, forever.
for (const [cmd, cap] of Object.entries(caps.COMMAND_CAPABILITY)) {
if (cap === null) continue;
assert.ok(caps.CAP_SET.has(cap), `${cmd} maps to unknown capability ${cap}`);
}
});
test('a device row that failed to load refuses everything rather than guessing', () => {
// A missing row would otherwise fall through platformFamily() to the web baseline and cheerfully
// authorise commands against a device that does not exist.
assert.equal(caps.commandAllowed(null, 'reboot').ok, false);
assert.equal(caps.commandAllowed(undefined, 'set_volume').ok, false);
});
test('a player declaring nothing at all is refused every gated command', () => {
const mute = { client_type: 'apk', capabilities: '[]' };
assert.equal(caps.commandAllowed(mute, 'reboot').ok, false);
assert.equal(caps.commandAllowed(mute, 'set_volume').ok, false);
assert.equal(caps.commandAllowed(mute, 'set_debug').ok, true, 'ungated commands still pass');
});