fix(composer): coerce remaining raw int() payload values, harden JS file/download handling

CodeRabbit flagged several payload-derived int() conversions in composer.py
that could raise ValueError instead of clamping like every other coerced
value in the module (_as_rgb_filter and its callers -- fillR/G/B, outR/G/B,
bgR/G/B, trackR/G/B -- plus min_width, lineSpacing, barWidth/Height,
start/endAngle, borderRadius, pip*, sparkline bar*, marquee gap/scrollSpeed).
Route them all through _safe_int for consistency with the rest of the module
and to avoid the generic 422 a raw ValueError produces.

Also:
- FileReader.onerror was unset in importDesign(), so a failed file read
  produced no status message.
- URL.revokeObjectURL() ran synchronously right after link.click() in both
  exportDesign() and generateZip(); deferred via setTimeout(..., 0) so the
  download reliably starts before the object URL is revoked.
- _module_level_code() (test helper) filtered ast.ImportFrom but not
  ast.Import, so a bare `import os` payload wouldn't be caught by the
  helper itself, even though downstream assertions still caught it.

Added regression tests for the fillR/G/B injection + clamping path, which
had no coverage (existing tests only covered the r/g/b _rgb_expr path).

Skipped as not worth the churn (CodeRabbit nitpicks, both "Trivial/Low value"):
- test_composer_empty_block.py's branch regex not matching digit-containing
  type names -- no such type exists today.
- test_composer_path_containment.py's `C.composer_bp.name and Flask(...)`
  truthiness guard -- cosmetic, blueprint name is never falsy.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XKc832xpVEx3C3W5BVqQ5Z
This commit is contained in:
Claude
2026-09-12 17:35:16 +00:00
parent f12d11334a
commit 6c23994b2f
3 changed files with 50 additions and 23 deletions
@@ -572,7 +572,7 @@ function composerApp() {
link.download = (this.metadata.id || 'composer-design') + '.composer.json';
link.href = url;
link.click();
URL.revokeObjectURL(url);
setTimeout(() => URL.revokeObjectURL(url), 0);
},
importDesign() {
@@ -583,6 +583,9 @@ function composerApp() {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onerror = () => {
this._setStatus('Failed to read file', 'error');
};
reader.onload = (ev) => {
try {
const data = JSON.parse(ev.target.result);
@@ -1326,7 +1329,7 @@ function composerApp() {
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url; a.download = `${this.metadata.id}.zip`; a.click();
URL.revokeObjectURL(url);
setTimeout(() => URL.revokeObjectURL(url), 0);
this.generateStatus = 'done';
this._setStatus('Plugin ZIP downloaded', 'success');
setTimeout(() => { if (this.generateStatus === 'done') this.generateStatus = 'idle'; }, 4000);