Files
booth/tests/mutations/u3_submit_all.toml
T
vh 377e652670 fix(inplace,embed): input set back mid-flight, report inputs, ambiguous anchors
Four items owed after S5b, reported by design-dev during the anti-slop run:

- carry() measured a sent-then-changed form against its OLD DEFAULTS. An
  answer set back mid-flight to the value the page first showed read as
  untouched, and the swap put the just-saved value over it. A form sent and
  then changed is now measured against its sent snapshot (sentSet.snapOf).
- The embed's clean-batch reload saw only our own forms. A report's own
  inputs lost whatever the operator had typed into them. Unsaved text in
  any control we don't own now holds the reload, and the page says so.
- Two r2b.toml rows ("D3 a stored theme...", "D3 forced light...") matched
  twice, so they proved only by where the first match fell. Both are
  re-anchored, and scripts/mutation_check.py now refuses any anchor that
  matches more than once. A new tool control covers that.
- The r2_flow contract's C3 steps 2 and 4 now say what S5b superseded. U3
  gains the report-input rule.

Mutation rows: u3_submit_all +1, r2_submit_all +1. Four rows were
re-anchored onto the moved lines.
2026-09-28 16:52:42 -07:00

151 lines
5.6 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; }'''