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.
177 lines
6.8 KiB
TOML
177 lines
6.8 KiB
TOML
# U3 amendment, 2026-09-27: one submit saves every ask on a verbatim report.
|
|
# The operator answered three asks top to bottom, pressed the last button, and
|
|
# the 303 reload wiped the first two (`auk-audition`, 15:02:23). Contract:
|
|
# docs/contracts/u3_declared_embed_seam.contract.md, "Submitting several asks at
|
|
# once" and INV-8. Every row is a change tests/test_embed_browser.py claims to
|
|
# forbid. The flight-window rows came from the heid bug-hunt panel on the first
|
|
# cut, where every guard of that window survived its mutation.
|
|
|
|
unit = "u3 submit all"
|
|
|
|
[[mutation]]
|
|
label = "only the pressed form is sent (the reported defect)"
|
|
file = "booth/static/embed.js"
|
|
test = "tests/test_embed_browser.py::test_one_submit_saves_every_answered_ask_on_the_page[a3]"
|
|
old = '''
|
|
var batch = forms.filter(dirty);'''
|
|
new = '''
|
|
var batch = [form];'''
|
|
|
|
[[mutation]]
|
|
label = "the pressed form is sent whether or not it is dirty (a blank one 400s)"
|
|
file = "booth/static/embed.js"
|
|
test = "tests/test_embed_browser.py::test_a_blank_ask_is_skipped_never_refused"
|
|
old = '''
|
|
var batch = forms.filter(dirty);'''
|
|
new = '''
|
|
var batch = forms.filter(function (f) { return f === form || dirty(f); });'''
|
|
|
|
[[mutation]]
|
|
label = "every form is sent, touched or not (re-dates an answer nobody gave)"
|
|
file = "booth/static/embed.js"
|
|
test = "tests/test_embed_browser.py::test_an_ask_nobody_touched_is_not_re_sent"
|
|
old = '''
|
|
var batch = forms.filter(dirty);'''
|
|
new = '''
|
|
var batch = forms;'''
|
|
|
|
[[mutation]]
|
|
label = "a lone dirty form is intercepted instead of left to the browser"
|
|
file = "booth/static/embed.js"
|
|
test = "tests/test_embed_browser.py::test_one_changed_ask_still_submits_as_a_plain_form"
|
|
old = '''
|
|
if (!others) return; // the browser's own POST and 303'''
|
|
new = ''''''
|
|
|
|
[[mutation]]
|
|
label = "the forms are sent in reverse document order"
|
|
file = "booth/static/embed.js"
|
|
test = "tests/test_embed_browser.py::test_the_asks_are_sent_in_document_order"
|
|
old = '''
|
|
var batch = forms.filter(dirty);'''
|
|
new = '''
|
|
var batch = forms.filter(dirty).reverse();'''
|
|
|
|
[[mutation]]
|
|
label = "a refusal stops the forms after it"
|
|
file = "booth/static/embed.js"
|
|
test = "tests/test_embed_browser.py::test_a_refused_ask_costs_only_itself_and_clears_nothing"
|
|
old = '''
|
|
return send(f, bodies[n]).then(function (why) {'''
|
|
new = '''
|
|
if (failed.length) return;
|
|
return send(f, bodies[n]).then(function (why) {'''
|
|
|
|
[[mutation]]
|
|
label = "a refusal reloads the page and clears what was entered"
|
|
file = "booth/static/embed.js"
|
|
test = "tests/test_embed_browser.py::test_a_refused_ask_costs_only_itself_and_clears_nothing"
|
|
old = '''
|
|
if (!failed.length && !changed && !host) { location.reload(); return; }'''
|
|
new = '''
|
|
location.reload(); return;'''
|
|
|
|
[[mutation]]
|
|
label = "only dirtiness blocks the reload (a refused form set back to its first value reloads over the failure)"
|
|
file = "booth/static/embed.js"
|
|
test = "tests/test_embed_browser.py::test_a_refusal_blocks_the_reload_even_when_nothing_reads_dirty"
|
|
old = '''
|
|
if (!failed.length && !changed && !host) { location.reload(); return; }'''
|
|
new = '''
|
|
if (!changed && !host) { location.reload(); return; }'''
|
|
|
|
[[mutation]]
|
|
label = "a refusal is never said"
|
|
file = "booth/static/embed.js"
|
|
test = "tests/test_embed_browser.py::test_a_refused_ask_costs_only_itself_and_clears_nothing"
|
|
old = '''
|
|
st.hidden = false;'''
|
|
new = ''''''
|
|
|
|
[[mutation]]
|
|
label = "an author's own form is taken over (no ownership check at the entry)"
|
|
file = "booth/static/embed.js"
|
|
test = "tests/test_embed_browser.py::test_an_authors_own_form_is_never_taken_over"
|
|
old = '''
|
|
if (forms.indexOf(form) < 0) return;'''
|
|
new = ''''''
|
|
|
|
[[mutation]]
|
|
label = "an author's form wearing our id prefix counts as ours (no mounted-root check)"
|
|
file = "booth/static/embed.js"
|
|
test = "tests/test_embed_browser.py::test_an_authors_own_form_is_never_taken_over"
|
|
old = '''
|
|
if (ours[j].contains(all[i])) { out.push(all[i]); break; }'''
|
|
new = '''
|
|
out.push(all[i]); break;'''
|
|
|
|
[[mutation]]
|
|
label = "a press during the flight falls through to the browser (a native POST races the batch)"
|
|
file = "booth/static/embed.js"
|
|
test = "tests/test_embed_browser.py::test_a_press_inside_the_flight_never_fires_a_native_post"
|
|
old = '''
|
|
if (sending) { ev.preventDefault(); return; }'''
|
|
new = ''''''
|
|
|
|
[[mutation]]
|
|
label = "a change made during the flight is reloaded away"
|
|
file = "booth/static/embed.js"
|
|
test = "tests/test_embed_browser.py::test_input_made_during_the_flight_is_kept_and_saved_on_the_next_press"
|
|
old = '''
|
|
var changed = forms.some(dirty);'''
|
|
new = '''
|
|
var changed = false;'''
|
|
|
|
[[mutation]]
|
|
label = "a saved form keeps its old baseline, so a retry re-sends (re-dates) it"
|
|
file = "booth/static/embed.js"
|
|
test = "tests/test_embed_browser.py::test_a_retry_after_a_refusal_sends_only_what_did_not_save"
|
|
old = '''
|
|
if (why === null) f.__bkSaved = bodies[n].toString();
|
|
else failed.push'''
|
|
new = '''
|
|
if (why !== null) failed.push'''
|
|
|
|
[[mutation]]
|
|
label = "the empty status line shows under a host `p{display:block}`"
|
|
file = "booth/static/embed.js"
|
|
test = "tests/test_embed_browser.py::test_the_empty_status_line_stays_hidden_under_host_css"
|
|
old = '''
|
|
".bk-ask-status[hidden]{display:none}",'''
|
|
new = ''''''
|
|
|
|
[[mutation]]
|
|
label = "the reload ignores the report's own inputs (text typed into the host page is lost)"
|
|
file = "booth/static/embed.js"
|
|
test = "tests/test_embed_browser.py::test_a_clean_batch_does_not_reload_over_text_typed_into_the_report"
|
|
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";'''
|