allow_insecure_mqtt (CWE-319, requested): a password with TLS disabled crosses
the network in cleartext. Refused now rather than merely warned about -- but
refused, not forbidden, because unencrypted MQTT on a trusted LAN is a normal
deliberate setup. allow_insecure_mqtt is the explicit acknowledgement, defaults
false, and is coerced like the other booleans so the string "false" cannot
switch the guard off.
starlark.py:796 -- the supported service runs Flask threaded, so two start
requests could each see running=False, each launch an editor, and the second
state write replace the first PID, orphaning a process that holds the display
down with nothing recording it. The check-launch-write sequence now takes a
module-level lock.
starlark.py:848 -- if the state write failed the route returned success with an
editor running and no PID recorded: status and stop both reported no session
while the display stayed down until the timeout expired. It now terminates the
process group and returns an error.
starlark.py:890 -- SIGKILL gives the script's EXIT trap no chance to run, so
nothing hands the display back, yet the response said "the display is
restarting". After an escalation the display is now restarted explicitly, and a
failure to do so returns an error naming the manual step instead of a success.
pixlet_config_editor.sh:184 -- find_pixlet supports Darwin but macOS ships no
timeout(1); GNU coreutils installs it as gtimeout. Resolved up front so the
failure lands before the display is stopped rather than after.
pixlet_config_editor.sh:154 -- wildcard, loopback and an explicit interface
address are three cases, not two. Collapsing the last two printed a URL saying
"localhost" whenever PIXLET_EDITOR_HOST named a LAN address.
tools.html:1254 -- escHtml does not encode single quotes, and the app id was
interpolated into an inline onclick="startPixletEditor('...')", so a directory
containing an apostrophe could break out of the JS string and run script. The
handler binds with addEventListener and reads the id from dataset, where it is
only ever parsed as an HTML attribute.
Tests: test_mqtt_bridge_config_endpoint.py grows to 23 cases covering the opt-in
in both directions. The tools DOM suite gains three guards asserting the edit
buttons carry no inline onclick and pass the id via dataset -- those need jsdom
and did not run here, so CI verifies them.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Web-interface JS tests
Covers web_interface/static/v3/js/plugins/list_filter.js (the shared
search/filter/sort controller) and the plugin-manager grids that use it:
Installed Plugins, the Plugin Store, and Starlark Apps.
There is no JS toolchain in this repo, so these are plain node scripts with no
test framework. Each prints ok/FAIL lines and exits non-zero on failure.
Running
cd test/js
npm install # jsdom, for the DOM suites only
node run_all.js
The unit suites need nothing but node. The DOM suites additionally need a running web interface, because they test against the real server-rendered HTML and the real API rather than fixtures:
# in another shell, from the repo root
EMULATOR=true python3 web_interface/app.py # http://localhost:5000
# or point the suites at a device
BASE=http://10.0.10.169:5000 node run_all.js
run_all.js skips the DOM suites (rather than failing) when jsdom is missing or
nothing is listening, so it stays useful in a bare checkout.
The suites
| Suite | Needs a server | Covers |
|---|---|---|
unit/test_list_filter.js |
no | ListFilter search/filter/sort/count/sticky, and the installed-plugins config extracted verbatim from plugins_manager.js so the test can't drift from it |
unit/test_render_cards.js |
no | renderInstalledCards markup, both empty states, and HTML-escaping of hostile plugin metadata |
dom/test_installed_dom.js |
yes | The toolbar in a real DOM: pill/search/sort interaction, the HTMX partial re-swap, and a getComputedStyle check that .filter-pill[data-active] really matches the emitted markup |
dom/test_store_dom.js |
yes | Store pagination, per-page, category, tri-state Installed button, and persistence across a re-boot, against the live registry |
dom/test_no_double_fetch.js |
yes | Loads the whole plugins_manager.js and counts requests: typing in the store search must filter the cached list, not refetch /api/v3/plugins/store/list |
dom/test_tools_sections.js |
yes | The Tools tab's MQTT bridge and Pixlet editor sections: form prefill, the write-only password (blank means unchanged), the running-session banner and countdown, and that the editor link points at the host you loaded the page from |
Point the DOM suites at a rig with a full plugin set when it matters — a dev box with two plugins installed will pass while exercising very little.
Notes for whoever changes this next
- The suites read the shipped files off disk and, for the DOM ones, the partial from the running server. They do not keep their own copy of the markup, so renaming an element id will fail them loudly rather than silently pass.
unit/test_list_filter.jsevals a slice ofplugins_manager.jslocated by the textfunction installedSortName(plugin). If that function is renamed, fix the slice markers rather than pasting a copy of the config into the test.- A few assertions exist specifically to stop earlier bugs coming back:
trailing spaces surviving the search debounce; a multi-word query that spans
two adjacent search fields (field order in the haystack is load-bearing);
window.installedPluginsstaying at full length while the grid is filtered. - Watch for assertions that can pass vacuously. Several here deliberately guard against it — e.g. counting only non-skeleton cards, and asserting a search phrase matches something before comparing two results.
The old-vs-new differential suites used to verify that the store and Starlark migrations were behaviour-preserving are not included: they compared against the pre-refactor implementation, which now only exists in git history. See PR #540 if that comparison ever needs redoing.