Waiverly · Test run · run 2026-11-07
Waiverly Test run, as shipped in the download
399 checks passed, 0 failed, when the suite that ships in the zip was run on 2026-11-07 with php tests/run.php. Run it yourself after you download.
[DB: SQLite]
== 1. Canonical text + content hashing ==
PASS CRLF and LF bodies hash identically (got 'a441ee0b36b56efefa2d4a0ed1d3cbeb332ed58e3de7448c9939a93c42b260e0', want 'a441ee0b36b56efefa2d4a0ed1d3cbeb332ed58e3de7448c9939a93c42b260e0')
PASS leading/trailing whitespace is canonicalized away (got 'a441ee0b36b56efefa2d4a0ed1d3cbeb332ed58e3de7448c9939a93c42b260e0', want 'a441ee0b36b56efefa2d4a0ed1d3cbeb332ed58e3de7448c9939a93c42b260e0')
PASS changing one word changes the hash
PASS changing the title changes the hash
PASS adding an acknowledgment changes the hash
PASS hash is 64 hex chars
== 2. Template versioning (immutable snapshots) ==
PASS creating a template publishes version 1
PASS metadata-only edit does NOT bump the version
PASS metadata edit still saved (minor_age) (got 16, want 16)
PASS current version unchanged (got 1, want 1)
PASS text edit publishes version 2
PASS version 1 snapshot is untouched (immutable) (got 'Original text.', want 'Original text.')
PASS version 1 hash still verifies against its own text (got '1898463c2e568c6836bc855d81618633c4a9d351631b6d5e1cbc8dd83f6ba523', want '1898463c2e568c6836bc855d81618633c4a9d351631b6d5e1cbc8dd83f6ba523')
== 3. Age math (minor gate) ==
PASS day before 18th birthday -> 17 (got 17, want 17)
PASS on the 18th birthday -> 18 (got 18, want 18)
PASS day after -> still 18 (got 18, want 18)
PASS Feb-29 dob, non-leap Feb 28 -> not yet 18 (got 17, want 17)
PASS Feb-29 dob, non-leap Mar 1 -> 18 (got 18, want 18)
PASS Feb-29 dob, leap-year Feb 29 -> exactly N (got 20, want 20)
PASS born today -> 0 (got 0, want 0)
== 4. Expiry month math (month-end clamping) ==
PASS Jan 31 + 1mo -> Feb 28 (non-leap) (got '2026-02-28', want '2026-02-28')
PASS Jan 31 + 1mo -> Feb 29 (leap) (got '2024-02-29', want '2024-02-29')
PASS Aug 31 + 1mo -> Sep 30 (got '2026-09-30', want '2026-09-30')
PASS +12mo -> same date next year (got '2027-07-04', want '2027-07-04')
PASS Dec 15 + 2mo rolls the year (got '2027-02-15', want '2027-02-15')
PASS Jan 31 + 13mo -> Feb 28 next year (got '2027-02-28', want '2027-02-28')
PASS +24mo from Feb 29 clamps (got '2026-02-28', want '2026-02-28')
== 5. Signing: validation + record creation ==
PASS adult signs successfully
PASS verify code is 10 chars (got 10, want 10)
PASS code avoids 0/O/1/I lookalikes
PASS adult is not flagged minor (got 0, want 0)
PASS expiry = signed date + 6 months (template setting) (got '2027-05-07', want '2027-05-07')
PASS record binds to the CURRENT version (v2) (got 2, want 2)
PASS fresh record passes integrity
PASS status of a fresh record (got 'valid', want 'valid')
PASS rejects empty name — “Enter your full name.”
PASS rejects bad email — “That email address does not look right.”
PASS rejects missing DOB — “Enter your date of birth.”
PASS rejects impossible DOB — “Enter your date of birth.”
PASS C2R: refuses an array where signer_name belongs — “Please fill in each field with a single value.”
PASS C2R: refuses an array where signer_email belongs — “Please fill in each field with a single value.”
PASS C2R: refuses an array where signer_dob belongs — “Please fill in each field with a single value.”
PASS C2R: refuses an array where the signature belongs — “Please fill in each field with a single value.”
PASS C2R: refuses an array where guardian_name belongs — “Please fill in each field with a single value.”
PASS C2R: not one of those refusals stored a waiver named "Array"
PASS C2R: `acks` is still allowed to be a list (the one legitimate array field)
PASS rejects future DOB — “Check the date of birth.”
PASS rejects unticked acknowledgment — “Please tick every acknowledgment box.”
PASS rejects missing signature — “Please sign in the signature box.”
PASS rejects non-PNG payload — “Please sign in the signature box.”
PASS oversize-fixture really exceeds the cap
PASS rejects oversize signature — “Please sign in the signature box.”
PASS rejects inactive template — “This waiver is not accepting signatures.”
PASS minor WITHOUT guardian is rejected — “A parent or guardian must complete this waiver for a minor.”
PASS minor WITH guardian signs (flagged minor) (got 1, want 1)
PASS guardian name stored (got 'Casey Guardian', want 'Casey Guardian')
PASS guardian signature stored
PASS kiosk source recorded (got 'kiosk', want 'kiosk')
PASS adult submission blanks stray guardian fields (got '', want '')
== 6. Derived status: valid / expired / superseded ==
PASS valid while before expiry (got 'valid', want 'valid')
PASS expired the day AFTER expires_on (got 'expired', want 'expired')
PASS still valid ON expires_on itself (got 'valid', want 'valid')
PASS never-expires ('' expiry) stays valid decades on (got 'valid', want 'valid')
PASS old version + require_resign -> superseded (got 'superseded', want 'superseded')
PASS old version + require_resign OFF -> stays valid (got 'valid', want 'valid')
PASS superseded beats expired (precedence) (got 'superseded', want 'superseded')
PASS publishing v3 supersedes the v2 record
PASS superseded record still shows the exact v2 text it signed
PASS superseded record still passes integrity
== 7. Tamper evidence ==
PASS editing the signer name in the DB flags TAMPERED
PASS restoring the original value verifies again
PASS swapping the signature image flags TAMPERED
PASS original signature verifies again
== 8. Verify-code lookup ==
PASS byCode finds the record
PASS byCode is case/format tolerant (lowercase + dashes)
PASS unknown code -> null
PASS all codes unique (got 4, want 4)
== 9. CSV export formula-injection guard ==
PASS neutralizes =formula signer name (got '\'=cmd|\'/c calc\'!A1', want '\'=cmd|\'/c calc\'!A1')
PASS neutralizes +formula (got '\'+SUM(A1)', want '\'+SUM(A1)')
PASS neutralizes @formula (got '\'@A1', want '\'@A1')
PASS neutralizes leading-minus text (not numeric) (got '\'-1+1@cmd', want '\'-1+1@cmd')
PASS neutralizes leading TAB (got '\' =1', want '\' =1')
PASS leaves plain negative number untouched (got '-45', want '-45')
PASS leaves plain decimal untouched (got '-45.00', want '-45.00')
PASS leaves ordinary name untouched (got 'Alex Sample', want 'Alex Sample')
PASS leaves date string untouched (got '2026-07-04', want '2026-07-04')
PASS leaves empty untouched (got '', want '')
== 10. Regression: validity window covered by record hash + input length caps ==
PASS extending expires_on in the DB flags TAMPERED
PASS restoring expires_on verifies again
PASS backdating signed_on in the DB flags TAMPERED
PASS restoring signed_on verifies again
PASS rejects over-long phone (would 500 on MySQL strict) — “That phone number is too long.”
PASS rejects over-long email — “That email address does not look right.”
PASS rejects over-long guardian relation — “The guardian relationship is too long.”
PASS normal-length fields still accepted after caps
== PWA ==
PASS manifest.json is valid JSON
PASS manifest has required fields
PASS manifest URLs are RELATIVE (subpath-install safe)
PASS manifest icons exist on disk (192 + 512 + apple-touch)
PASS sw.js never caches non-GET
PASS sw.js never caches page navigations (auth HTML can not enter cache storage)
PASS sw.js cache name carries the version (old caches purged on activate)
PASS offline fallback page ships
PASS every app layout links the manifest + registers the SW
== Own It 2.0 ==
PASS v2: tables
PASS v2: totp_cols
PASS v2: smtp_cols
PASS v2: mint
PASS v2: revoke
PASS v2: totp
PASS v2: recovery
PASS v2: delivery
PASS v2: rate
PASS v2: backup_tables
PASS v2: no_signature_image
PASS v2: no_signer_ip
PASS v2: has_record_hash
PASS v2: routes registered
PASS v2: NO signing endpoint (a waiver must be signed by a person, in a browser)
PASS v2: integrity check reuses the product core (Waiver::integrityOk)
PASS v2: signing fires waiver.signed + notifies, never fatally
PASS v2: 2FA intercepts login + rate limit
PASS v2: zero clip() dependency
PASS v2: no ternary key re-reads (PLAN item 10)
PASS v2: dark palette; signature pad pinned light in BOTH themes
PASS v2: theme.js on the interactive layouts (print.php deliberately excluded)
PASS v3: API.md ships + sw cache and healthz/ping/openapi all report the CURRENT version (4.1.3)
PASS v2: cli-server base guard present
-- Own It 3.0 --
PASS v3: v3_tables
PASS v3: v3_columns
PASS v3: upgrade_never_loosens
PASS v3: settings_singleton_ensured
PASS v3: upgrade_from_2x
PASS v3: migrate_called_at_boot
PASS v3: roles
PASS v3: last_admin_guard
PASS v3: waiver_signed
PASS v3: signed_text_is_pinned
PASS v3: becomes_superseded
PASS v3: integrity_still_ok
PASS v3: mcp_tools
PASS v3: mcp_is_read_only
PASS v3: mcp_arg_guard
PASS v3: mcp_returns_signed_snapshot
PASS v3: mcp_never_returns_signature
PASS v3: mcp_get_is_405
PASS v3: expiry_data_shared
PASS v3: pdf_renders_pinned_text
PASS v3: gdpr_blocked_while_valid
PASS v3: gdpr_allowed_once_expired
PASS v3: gdpr_keeps_the_evidence_skeleton
PASS v3: ical_token
PASS v3: ical_feed_shape
PASS v3: branding_content_decides
PASS v3: restore_refuses_other_product
PASS v3: audit_redacts
PASS v3: routes registered, and LITERALS come before {id} patterns
PASS v3: the PDF renders the PINNED snapshot, never the current template
PASS v3: GDPR refuses to erase a waiver that can still be relied on
PASS v3: the signature image is never exposed by the API or the audit trail
PASS v3: expiry is computed in ONE place and reused by the list, the feed and the stats tool
PASS v3: re-signing never edits the original waiver
PASS v3: core Mcp is byte-identical to the shared core
PASS v3: core AuditLog is byte-identical to the shared core
PASS v3: core Pdf is byte-identical to the shared core
PASS v3: core Ical is byte-identical to the shared core
PASS v3: core BackupRestore is byte-identical to the shared core
PASS v3: core Branding is byte-identical to the shared core
PASS v3: core RolesKit is byte-identical to the shared core
PASS v3: core Gdpr is byte-identical to the shared core
PASS v3: core Oidc is byte-identical to the shared core
PASS v3/C2: the viewer seat is READ-ONLY in the map — it holds nothing that changes the record
PASS v3/C2: the legal text and the register are role-gated, not merely signed-in
PASS v3/C2: the backup routes hand over the whole register — settings.edit, not requireLogin
PASS v3/C2: no route that predates 3.0 is left on a bare requireLogin except the user's own account
PASS v3/C2: the sidebar hides Settings from a role the route refuses
PASS v3/C2: /backup.sqlite is an app ROUTE and the dev router does not 403 it
PASS v3/C2: bin/demo.php can actually run — migrate() calls AuditLog::ensure()
PASS nav: every sidebar link is highlighted by the page it opens
PASS nav: no page highlights a sidebar item that does not exist
PASS ip: a direct visitor's forged headers are ignored
PASS ip: behind a trusted proxy the forwarded visitor is used
PASS ip: CF-Connecting-IP wins over the X-Forwarded-For chain
PASS ip: the chain is read right-to-left, past trusted hops
PASS ip: a Cloudflare edge is not a trusted proxy by default
PASS ip: CIDR maths (v4 boundaries + v6, no cross-family match)
== UI kit: returning-signer check (kiosk hero endpoint) ==
PASS returning: exact email+DOB finds the record, case-insensitively
PASS returning: reports the engine-computed expiry (got '2027-11-07', want '2027-11-07')
PASS returning: answer carries NO name and NO code (nothing to enumerate)
PASS returning: right email + wrong DOB stays silent
PASS returning: unknown email stays silent
PASS returning: malformed email / DOB are refused before the query
PASS returning: past the expiry date the engine says expired
PASS returning: a text bump flips the answer to superseded (must re-sign)
PASS returning: /w/{token}/check + /k/{token}/check routed, CSRF-checked, minimal JSON
== v3.1: the signer gets their own copy ==
PASS signer copy: carries the title, the dates and the reference code
PASS signer copy: links the PUBLIC verify page, code included
PASS signer copy: carries NO signature image, record hash, IP or internal note
PASS signer copy: subject names the waiver and the business
PASS signer copy: no base URL configured → the email simply omits the link
PASS signer copy: OFF by default — an operator who never asked sends nothing (got 'Signer copies are switched off in Settings.', want 'Signer copies are switched off in Settings.')
PASS signer copy: demo installs never reach a credential (got 'Demo mode — no mail leaves this server.', want 'Demo mode — no mail leaves this server.')
PASS signer copy: unconfigured SMTP refuses in plain words (got 'SMTP is not configured.', want 'SMTP is not configured.')
PASS signer copy: a signer who gave no address is not an error, just a no-send (got 'This signer did not give an email address.', want 'This signer did not give an email address.')
PASS signer copy: the automatic send does not repeat itself (got 'A copy was already sent for this waiver.', want 'A copy was already sent for this waiver.')
PASS signer copy: an operator CAN send it again on purpose (got '', want '')
PASS signer copy: the manual route is permission-gated and CSRF-checked
PASS signer copy: signing calls it, and a mail failure cannot break the signature
PASS signer copy: the SMTP password is write-only — a blank post keeps the stored secret
PASS signer copy: a refused SMTP connection returns a reason instead of a fatal error
PASS signer copy: the failure is written on the row, not marked as sent
PASS signer copy: the failed send leaves the signed record untouched
PASS API.md: names the real signing paths (/w/{token}, /k/{token}), not a /sign route that 404s
== F2: walk-in kit — QR poster per template, prefilled share links ==
PASS poster: GET /templates/{id}/poster is routed to template_poster, behind template.view
PASS poster: the QR carries the Settings public address when one is set (got 'https://waivers.example.com/w/b9491c392f7ea6489475452ff76e2714', want 'https://waivers.example.com/w/b9491c392f7ea6489475452ff76e2714')
PASS poster: renders the share link as a QR (qr.js) plus the address in print
PASS poster: the kiosk token is never on it (the kiosk stays on the desk tablet)
PASS poster: every template row links to its poster
PASS prefill: ?name=&email= on the share link fills both fields, escaped
PASS prefill: the kiosk ignores it (the next person must not inherit a name)
PASS signer copy: the signing route itself also catches anything the copy throws, so the owner notice still sends
PASS scope: api_keys gains a scope column defaulting to full — an upgrade never loosens
PASS scope: requireKey reads the key scope and refuses a write on a read-only key
PASS scope: mintKey persists the chosen scope, and anything but read is full access
PASS scope: /mcp hands the key scope to the core, which refuses write tools per tool
PASS scope: the key form lets an administrator mint a read-only key
PASS license: every font ships the SIL OFL text for its own family beside it
PASS license: LICENSE.txt is present and carries the IF WE STOP (CONTINUITY) clause
PASS forms: every POST form on a screen reaches a registered route (39 checked)
PASS demo ribbon: on the public pages it says what the page is, not "signed in as the admin"
PASS demo ribbon: below 600px it starts as the small pill unless the visitor opened it
PASS backups: every non-admin role gets 403 on every backup, restore and backup-token route
PASS backups: an administrator still gets the JSON backup (got 'GATE 200', want 'GATE 200')
PASS backup JSON: an administrator gets a backup, with every credential column planted (16)
PASS backup JSON: the download carries none of them
PASS backup JSON: the scheduled backup carries none of them
PASS backup JSON: the download and the scheduled writer read the one redaction list
PASS security page: the backup card says every credential is redacted, not only the SMTP password
== 3.2: waiver questions, show-if rules, marketing consent, sealed ==
PASS questions: five parsed with their types, required flags and options
PASS questions: an "if:" names the earlier question, stored by its key
PASS questions: a one-option choice, an unknown type and an "if:" on nothing are refused
PASS questions: the edit screen gets back the same list
PASS content hash: a version without questions hashes exactly as before 3.2
PASS template: the questions and the consent wording are part of the version snapshot
PASS template: saving the same questions publishes nothing; changing them publishes a new version
PASS signing: a required question left empty is refused
PASS signing: a required follow-up is refused only when it is shown
PASS signing: yes/no, choice and date answers are checked
PASS record: the shown answers are stored with their questions; a hidden answer is never stored
PASS record: new records are sealed in format 2 and verify
PASS record: the consent box is unticked unless the signer ticks it
PASS record: a template with no consent box records no consent, whatever is posted
PASS tamper: an edited answer shows TAMPERED
PASS tamper: a withdrawn consent edited in the database shows TAMPERED
PASS tamper: relabeling a format-2 record as format 1 does not make it verify
PASS tamper: restored, it verifies again
PASS format 1: a record signed before 3.2 (no answers) still verifies with the old formula
PASS outputs: the CSV has answers and consent columns; the PDF prints the answers and the consent
PASS outputs: the API one-record read carries the answers; every payload carries the consent
PASS signing page: questions render by type with their show-if rule; the consent box is unticked; questions.js applies the rule
PASS template form: the questions box and the consent wording are on the edit screen
== 3.2 pass B: check-in against a valid waiver, mailing-list export, photo ==
PASS check: a valid record wins over an expired one for the same person
PASS check: only expired → expired; nobody → none
PASS check: a tampered record is never counted as valid
PASS check: GET /api/waivers/check is routed before /api/waivers/{code}, behind a key, and answers no signature or code
PASS arrivals: a valid waiver checks in; an expired one is refused and told to sign again
PASS arrivals: the front desk (viewer and member roles) may check in; the page and route exist
PASS mailing list: only signers who ticked the box, once per address, in Mailchimp's headings
PASS photo: only a JPEG data URL of sane size is taken
PASS photo: a template that requires one refuses a signing without it
PASS photo: taken, stored and sealed in the record
PASS photo: a swapped photo shows TAMPERED
PASS photo: off by default; the template form offers it; the sign page asks with the camera; anonymizing erases it and the answers
== 3.2 overflow: sign for a child after your own waiver; one kiosk for every waiver ==
PASS guardian link: an adult who has just signed is remembered in this session, and fills the guardian in
PASS guardian link: it lasts 15 minutes, never longer
PASS guardian link: "sign for a child" opens the form with the guardian's name, email and phone filled in
PASS guardian link: the child's waiver is linked to the adult's, once, and an adult's waiver is never linked
PASS guardian link: the adult's "all set" page offers "Sign for a child now"
PASS guardian link: a fresh form forgets it, so the next guest at a kiosk never sees the last one's details
PASS guardian link: the child's record links to the adult's waiver and the adult's lists the children
PASS guardian link: signing links the child when the form came from the adult, and remembers an adult
PASS kiosk hub: no link until one is made
PASS kiosk hub: the link lists every active waiver, each opening its kiosk form with the way back
PASS kiosk hub: the form and the reset both come back to the hub
PASS kiosk hub: a made-up hub is ignored (back to the waiver's own kiosk)
PASS kiosk hub: wrong links are rate-limited; making or replacing the link needs template.edit and CSRF; the token leaves the shareable backup
== Text messages: the signing link by text, through the owner's own SMS gateway ==
PASS texts: off by default — no gateway, nothing stored, nothing sent, and the owner is told where to turn it on
PASS texts: a gateway address that is not https never counts as on
PASS texts: numbers are normalized; something that cannot be a number is refused
PASS gateway: JSON by default, fields to/from/text, the key as a Bearer token
PASS gateway: Twilio-style — To/From/Body, form-encoded, key as HTTP Basic
PASS gateway: "not sent" leaves the key out; no sender leaves the from field out
PASS consent: no text without the tick that they gave this number for the link; a reminder needs a visit date
PASS consent: the tick is stored with who recorded it; a link without the reminder tick has remind_consent 0
PASS text: names the business and the waiver, links to sign (marked as texted) and to stop
PASS text: the token in the text is kept only as its SHA-256; no column holds it, and it finds its request
PASS once: the same text again is refused by its key before the gateway is called
PASS reminder: no reminder without its own consent tick, even when the visit is tomorrow (the tick is in the query)
PASS reminder: with consent, one reminder the day before the visit, and only one however often the run repeats
PASS window: 09:00–20:00 in the business time zone by default; an overnight window works too
PASS window: a due reminder waits outside the window, then goes inside it
PASS signed: through the texted link, or with the same number on the form, closes the request: no reminder
PASS signed: a texted link only closes its own waiver's request, once
PASS stop: opening the link only asks (a link preview stops nobody); the page names the business and never shows the whole number
PASS stop: the button stops that number for good: no reminder, and no new link from any team member
PASS failure: a refused or unreachable gateway is written on the text's row and shown, never thrown
PASS failure: no reminder follows a link text that did not go
PASS page: the form asks for the consent ticks (the link one required) and lists each text with its status and error
PASS sign link: a texted link keeps its marker through the form; another waiver's page ignores it
PASS sign link: signing through it closes the request (sign_submit marks it, link signings only)
PASS settings: saving needs settings.edit and CSRF; https only; the key is write-only; sending needs waiver.resend
PASS privacy: the gateway key and the token hashes leave the shareable backup; erasing a signer erases the texted request that led to the waiver
PASS privacy: anonymizing the waiver blanks that request's name and number
PASS demo: the public demo never sends a text; the cron run sits behind the scheduled-backup token
PASS template: a reminder run stops at the first refusal (one attempt, the next one untouched)
PASS template: a refused reminder is never tried twice on one day; the others go
PASS template: asked directly, a reminder refused today is still not claimed again today (the claim itself refuses)
PASS template: a reminder refused on an earlier day is claimed again, once, and goes
PASS template: the daily cap stops the reminders for the day
PASS template: consent is read again at the moment of sending (withdrawn mid-run, the next text does not go)
PASS template: both consents are conditions of the query that picks reminders, as well as being read again at send
PASS template: the audit names the number only by its last four digits
————————————————————————————————————————————————————
== 4.0: the Update button is wired ==
PASS updates: the release public key is pinned
PASS updates: Database::migrate() creates the updater table (in code, not in a comment)
PASS updates: the updater table holds exactly one state row
PASS updates: index.php clears stale compiled code before anything else loads
PASS updates: index.php finishes or rolls back an update before any controller loads, with this app's canary
PASS updates: route get /updates is updates_page()
PASS updates: route post /updates/key is updates_key_save()
PASS updates: route post /updates/check is updates_check()
PASS updates: route post /updates/apply is updates_apply()
PASS updates: updates_page() opens with the restore page's admin gate
PASS updates: updates_key_save() opens with the restore page's admin gate
PASS updates: updates_check() opens with the restore page's admin gate
PASS updates: updates_apply() opens with the restore page's admin gate
PASS updates: every POST checks the CSRF token
PASS updates: the page is told why updates are off here (Updater::whyOff, F5 A1 pass)
PASS updates: the menu links to the page
PASS updates: bin/update.php refuses the web and finishes in a fresh process
PASS updates: where updates are off (Cloud, the demo, a missing extension) the page says why and offers no button
PASS updates: with a key and a newer signed release the page offers "Back up and update"
== 4.0 A2: sample data, backups, health ==
PASS A2: the owner's-install checks ran to the end
PASS A2: an owner's own install (no Cloud config, no demo marker)
PASS A2: a fresh install's sample tables are all empty, so the installer's sample data can go in
PASS A2: switched off: an install without "Start with sample data" gets none
PASS A2: first request: the marker is used once and the sample business is there
PASS A2: every one of the owner's settings is theirs, not the sample's
PASS A2: the sample logins cannot sign in: no password matches, and switched off where the product can
PASS A2: the owner's own login is untouched
PASS A2: the load is in the audit log
PASS A2: a second marker on a Waiverly with records seeds nothing and changes nothing
PASS A2: real use: one of the owner's own rows among the sample
PASS A2: remove: the owner's own row stays, and a sample row it points at is kept
PASS A2: remove: the report says what went, and what was kept because the owner's records use it
PASS A2: remove: the rest of the sample is gone
PASS A2: remove: every kept sample row keeps all its own parts
PASS A2: remove: no row is left pointing at a row that was removed
PASS A2: remove: the sample logins are gone but the ones kept rows use, the owner stays, and nothing counts as sample any more
PASS A2: remove: the sample's files are gone from disk (0 written; one a row that stays names stays with it)
PASS A2: remove: the owner's own uploaded file is untouched
PASS A2: "Load demo data" on a Waiverly with records refuses and deletes nothing
PASS A2: backup: written, sent to the bucket (a signed PUT), and read back
PASS A2: backup: the bucket secret is not in the backup it protects
PASS A2: backup: under the standard keep count (14) every backup is kept
PASS A2: switched off: with automatic backups off, a backup is never due however old the last one
PASS A2: the backup lines for Backup & restore: last backup fine, offsite fine, read-back fine
PASS A2: an offsite endpoint must be https for an owner (plain http only in tests)
PASS A2: all 25 owner's-install checks reported
PASS A2: Database::migrate() creates the sample-data and backup tables (in code, not in a comment)
PASS A2: index.php loads the three cores and the wiring
PASS A2: the first request seeds the installer's sample, and an owner's install backs itself up when due
PASS A2: route post /sample-data/remove is sample_data_remove()
PASS A2: route post /restore/backups is backups_save()
PASS A2: route post /restore/backup-now is backup_now()
PASS A2: route get /health is health_page()
PASS A2: sample_data_remove() opens with the restore page's admin gate
PASS A2: backups_save() opens with the restore page's admin gate
PASS A2: backup_now() opens with the restore page's admin gate
PASS A2: health_page() opens with the restore page's admin gate
PASS A2: every POST checks the CSRF token
PASS A2: off an owner's install (Cloud, the demo) the backup settings and "Back up now" refuse in words
PASS A2: the installer offers sample data, ticked, and only leaves the first-run marker (with every table's row count)
PASS A2: the first run is strictly once (a marker that cannot be removed seeds nothing), and treats the installer's rows as its defaults only while nothing has changed
PASS A2: the scheduled-backup URL runs the same backup (offsite copy and read-back included)
PASS A2: "Load demo data" outside the public demo goes through SampleData (only into an empty Waiverly, removable)
PASS A2: the menu links to Health
PASS A2: the sample-data banner shows only to an admin, only while sample data is there, with its Remove button, and stays put (nested: the UI kit toasts a bare .main > .alert away)
PASS A2: Backup & restore shows the automatic backups, "Back up now" and the bucket settings (none on Cloud)
SKIP A2: the Health page render (this suite loads no view helpers in its own process; the owner's-install check and update_e2e.sh run the real app)
== sec3 D4: Export selected (CSV) writes what a signer typed as text, never a formula ==
PASS sec3 D4 (setup): four waivers signed through the public form's path (=1+1, @SUM(1), -2+3, a plain name)
PASS sec3 D4: the export answers a CSV with the four selected waivers
PASS sec3 D4: a signer named =1+1 reads '=1+1 (and the address =1+1@… '=1+1@…)
PASS sec3 D4: a signer named @SUM(1) reads '@SUM(1)
PASS sec3 D4: a signer named -2+3 reads '-2+3, and the address +4@… reads '+4@…
PASS sec3 D4: no cell of the file starts with = + - @ (the formula characters)
PASS erase planting · sqlite: the subject is created and a marker is planted in all 17 personal columns
PASS erase planting · sqlite: the app's own erase runs through its route and reports no failure
PASS erase planting · sqlite: no planted marker survives anywhere (22 tables dumped), outside the 1 column(s) kept by ruling (possible legal records)
PASS erase planting · sqlite: the erase page (/waivers/1) says what an erase keeps: "Kept: the signer's phone number on the text stop list, if th…"
PASS erase planting · sqlite: MANUAL.md says the same: "Kept: the signer's phone number on the text stop list, if th…"
ALL 399 TESTS PASSED (0 failed)