mirror of
https://github.com/screentinker/screentinker.git
synced 2026-08-13 13:53:12 -06:00
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
88 lines
5 KiB
JavaScript
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');
|
|
});
|