fix(embed,mutation): SPYRJA fold — report input on every path; an instrument that cannot certify what it did not run

The heid bug-hunt (hulda, with heid's second voice) on 377e652 found eight
issues. Every fixed one has a red-first test.

embed.js:
- H1: the report-input guard covered only the batch path. A lone changed
  answer went out as a native POST, whose 303 navigation took the report's
  typed text with it. Unsaved report input now routes even a lone answer
  in place. With nothing of ours to send, the submit block says so.
- H7 / V1: a bare <select>, and a range or color input with no value
  attribute, read as typed-into by their default attributes, so every clean
  batch refused its reload with a false message. Report controls are now
  measured against how they stood when the Booth mounted. A control added
  later falls back to its defaults, counting a select's first option as its
  default.
- H2 / V2: a contenteditable region counts as report input.

scripts/mutation_check.py:
- H5: any non-zero exit counted as proof, including a collection error
  where the test never ran. Only pytest's "tests failed" (1) proves now.
- H6: the test run has a timeout (300 s). A hang reports "timed out" and
  the source is still restored.
- H3: source is read and restored as bytes, so a CRLF file comes back
  byte-exact.
- H4: one run per tree, enforced by a lock. The in-flight marker lives with
  the tree it guards.
- H8: anchors are counted with overlaps. The check is `matches()`, not
  str.count.

Tool controls +5 (tests/test_mutation_check.py). u3_submit_all +3 rows.
This commit is contained in:
vh
2026-09-28 17:49:13 -07:00
parent 377e652670
commit 213071b6ce
6 changed files with 295 additions and 23 deletions
+26
View File
@@ -148,3 +148,29 @@ old = '''
if (!failed.length && !changed && !host) { location.reload(); return; }'''
new = '''
if (!failed.length && !changed) { location.reload(); return; }'''
[[mutation]]
label = "a lone answer ignores report input and takes the browser's navigation (SPYRJA H1)"
file = "booth/static/embed.js"
test = "tests/test_embed_browser.py::test_a_single_answer_does_not_navigate_over_text_typed_into_the_report"
old = '''
var others = forms.some(function (f) { return f !== form && dirty(f); }) || hostDirty(forms);'''
new = '''
var others = forms.some(function (f) { return f !== form && dirty(f); });'''
[[mutation]]
label = "report controls are measured against their defaults, not the mount (a bare select holds every reload)"
file = "booth/static/embed.js"
test = "tests/test_embed_browser.py::test_untouched_report_controls_do_not_hold_the_reload"
old = '''
for (var i = 0; i < els.length; i++) els[i].__bkHost = hostState(els[i]);'''
new = ''''''
[[mutation]]
label = "a contenteditable region is not counted as report input"
file = "booth/static/embed.js"
test = "tests/test_embed_browser.py::test_a_clean_batch_does_not_reload_over_an_edited_contenteditable"
old = '''
var HOST = "input, textarea, select, [contenteditable]:not([contenteditable='false'])";'''
new = '''
var HOST = "input, textarea, select";'''