Complia · Test run · run 2026-11-07
Complia Test run, as shipped in the download
440 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.
== 1. Schema install (SQLite in-memory) ==
PASS schemaExists() after install
PASS table users exists
PASS table settings exists
PASS table complaints exists
PASS table complaint_log exists
PASS install() is idempotent (re-run does not throw)
== 2. Settings ==
PASS org_name read through App::set
PASS ack_window_days read
PASS timezone read
PASS App::today() uses business timezone
== 3. s.164A(3) acknowledgment-deadline math ==
PASS default 29-day window (day-1-inclusive safe)
PASS strict statutory reading satisfied: received Mon 2026-08-03 -> due 2026-09-01, never 09-02
PASS explicit 30-day window still available
PASS month rollover
PASS year rollover
PASS leap year Feb
PASS non-leap Feb
PASS custom window 14 days
PASS custom window 60 days
PASS window clamped: 0 -> 1 day
PASS window clamped: 9999 -> 365
== 4. daysRemaining (signed) ==
PASS 10 days left
PASS due today = 0
PASS overdue by 5
PASS across month
PASS across year
== 5. Status workflow transitions ==
PASS received -> acknowledged
PASS received -> investigating
PASS received -> resolved
PASS received -> rejected
PASS acknowledged -> investigating
PASS acknowledged -> resolved
PASS investigating -> resolved
PASS investigating -> rejected
PASS NO acknowledged -> received (backwards)
PASS NO investigating -> acknowledged (backwards)
PASS NO resolved -> anything (terminal)
PASS NO rejected -> anything (terminal)
PASS NO received -> received (no-op)
PASS NO transition to bogus status
== 6. Derived display status (ack_overdue precedence) ==
PASS received before due -> received
PASS received past due -> ack_overdue
PASS received due today -> received (not overdue)
PASS acknowledged past due -> acknowledged
PASS investigating past due -> investigating
PASS resolved past due -> resolved
PASS rejected past due -> rejected
PASS received + ack timestamp -> received (duty met)
PASS bogus stored status -> received
== 7. Public intake -> token -> lookup flow ==
PASS create returns id
PASS token is 40 hex chars
PASS token hex only
PASS byToken finds complaint
PASS email lowercased + trimmed
PASS status starts received
PASS ack_due_at = today + 30 (business tz)
PASS reference stored
PASS submitted log entry written
PASS byToken miss -> null
PASS byToken rejects malformed token
PASS byToken rejects empty token
PASS byToken rejects SQLi-ish token
== 8. Intake validation ==
PASS valid input accepted
PASS empty name rejected
PASS overlong name rejected
PASS bad email rejected
PASS empty subject rejected
PASS empty details rejected
== 9. Anti-abuse: per-IP rate limit ==
PASS 10 submissions/hour from one IP -> limited
PASS different IP not limited
PASS empty IP never limited (proxied installs)
== 10. clip/clamp overflow safety (MySQL strict-mode defense) ==
PASS clip long string to 190
PASS clip short string untouched
PASS clip multibyte-safe
PASS clamp_int huge -> 2e9
PASS clamp_int -huge -> -2e9
PASS clamp_int non-numeric -> 0
PASS clamp_int custom bounds
PASS oversized subject clipped to 255
PASS oversized reference clipped to 100
PASS oversized ip clipped to 45
PASS 100KB details stored intact (big text column)
== 11. Investigation log (append-only) ==
PASS two notes appended
PASS order preserved (append-only)
PASS no UPDATE/DELETE of complaint_log anywhere in controllers
== 12. Status change semantics (ack timestamp + outcome) ==
PASS acknowledged_at recorded
PASS isAcked true after acknowledgment
PASS derived status acknowledged even past due
PASS outcome stored
PASS outcome_date stored
PASS terminal: no transitions from resolved
== 13. Auth (real users table) ==
PASS correct password accepted
PASS wrong password rejected
PASS unknown user rejected
PASS email case-insensitive
PASS inactive user rejected
== 14. Audit CSV export (formula guard, numeric-aware) ==
PASS =SUM formula neutralized
PASS @ formula neutralized
PASS +cmd neutralized
PASS -text neutralized
PASS leading TAB neutralized
PASS negative number NOT corrupted
PASS negative decimal NOT corrupted
PASS positive numeric "+1" kept
PASS plain text untouched
PASS comma cell RFC-4180 quoted
PASS quote cell escaped
PASS formula + comma: guarded AND quoted
PASS CSV header
PASS CSV row count = complaints + history + header
PASS CRLF line endings
PASS complaint rows tagged
PASS history rows present
PASS complaint row cell count
PASS formula name neutralized in real export
PASS formula subject neutralized in real export
== 15. Security hardening regressions (source-level) ==
PASS csrf_check rejects empty stored token (hash_equals empty-token bypass)
PASS installer rejects empty install token (_it bypass)
PASS demo session cookie is None + Secure + Partitioned (CHIPS)
PASS buyer session cookie is HttpOnly + Lax + Secure-on-HTTPS
PASS portable UTC timestamps (no SQLite datetime() in app SQL)
PASS clip + clamp_int helpers exist
PASS settings save clips org fields + clamps ack window
PASS complaint create clips every VARCHAR-bound field
PASS details/notes are MEDIUMTEXT on MySQL (64KB TEXT cap avoided)
PASS public POST handlers csrf_check (intake + track)
PASS honeypot field checked in intake handler
PASS per-IP rate limit enforced in intake handler
PASS status route enforces transition matrix
PASS closing a complaint requires an outcome (in the shared rule)
PASS v3: exactly ONE status-advance rule, used by UI + API + MCP
PASS token lookups are exact parameterized matches
PASS CSV export requires the export permission (not merely login)
PASS public status page 404s on miss
PASS all output escaped: views use e() on complaint fields
PASS .htaccess blocks config/data/src/bin/tests/controllers
== 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/v3: tables
PASS v2/v3: totp_cols
PASS v2/v3: smtp_cols
PASS v2/v3: mint
PASS v2/v3: revoke
PASS v2/v3: totp
PASS v2/v3: recovery
PASS v2/v3: delivery
PASS v2/v3: no_phantom_events
PASS v2/v3: rate
PASS v2/v3: ack_due
PASS v2/v3: days_remaining_signed
PASS v2/v3: derive_status
PASS v2/v3: overdue_not_a_column
PASS v2/v3: transitions
PASS v2/v3: create_validates
PASS v2/v3: create_sets_clock
PASS v2/v3: complaint_serializer
PASS v2/v3: serializer_nocompute
PASS v2/v3: csv_dry
PASS v2/v3: csv_backlog_clock
PASS v2/v3: backup_tables
PASS v2/v3: v3_tables
PASS v2/v3: audit_redacts
PASS v2/v3: gdpr_map_columns_exist
PASS v2/v3: letter_fields_are_data_only
PASS v2: routes registered (api, 2fa, security, backup, import, healthz)
PASS v2: migrate() runs on every boot (1.x had none — existing installs need it)
PASS v2: 2FA intercepts login + login is rate-limited
PASS v2: notifications + webhooks fire on the BROWSER path too, never fatally
PASS v2/v3: the product reuses its tested core (no re-derived guards)
PASS v2: the API never computes its own due date or overdue flag
PASS v2/v3: closing needs an outcome (shared rule), and ?status filters the DERIVED status
PASS v2: UK-reframed marketing/public copy left untouched
PASS v2: dark palette + no hardcoded white backgrounds outside @media print
PASS v2: theme.js + qr.js ship and layout loads theme.js + a toggle
PASS v2: shared modules are copy-identical to the shared core (never forked per app)
PASS v3: API.md ships + sw cache is v4.1.3 (App::VERSION)
PASS v3: no call to a function this product does not define
PASS C2: no sensitive handler is login-only — all are role-gated
PASS C2: both backup doors call the ONE shared redaction list
PASS C2: Api::requireKey refuses a deactivated user (u.active)
PASS C2: every App:: method the controllers call actually exists
PASS C2: Api::base() does not double an already-absolute base
PASS C2: the subject-access export takes the same standing as anonymize
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: acknowledging from the register ==
PASS register ack: the transition is accepted
PASS register ack: the stored status moved
PASS register ack: the statutory clock stopped (acknowledged_at is set)
PASS register ack: the derived status is no longer overdue-able
PASS register ack: it appended to the append-only log, it did not overwrite it
PASS register ack: an unacknowledged complaint past its deadline reads as overdue first
PASS register ack: a late acknowledgment is still recorded (lateness is evidence, not a lock)
PASS register ack: a resolved complaint refuses the transition, in the engine's own words
PASS register ack: the refused complaint was not touched
PASS register ack: acknowledging can never CLOSE a complaint (closing needs a written outcome)
PASS register ack: POST /complaints/{id}/ack is registered
PASS register ack: the endpoint requires complaint.advance and checks CSRF
PASS register ack: it delegates to the ONE shared transition rule — no second copy of the workflow
PASS register ack: an unknown complaint 404s and a refused transition answers 409
PASS register ack: it can only ever ask for "acknowledged"
PASS kit: layout loads kit css+js, groups the nav with icons + g-keys, carries the theme toggle and a who-line
PASS kit: the overdue count rides the sidebar on every page
PASS kit: no theme.js; app css is an app layer with a dark accent; the pre-kit file is gone
PASS kit: the printed register still forces a light palette (this print-out is the evidence pack)
PASS kit: App::asset() versions by mtime and the service worker revalidates code
PASS kit: nothing is fetched from another host
PASS dashboard: the first-run checklist is computed from the database, not the session
PASS dashboard: KPI sparkline, needs-your-action and the stage strip are wired
PASS hero: the register carries a clock in every row, tabs with counts, row links and the inline acknowledge
PASS record: the complaint page has the kit rail, the big clock and the append-only log
PASS public: the complaint form and the status page are on the kit public frame
PASS copy: no other product's vocabulary survives in the views
== 21. v3.1 — the complainant's receipt (P1) ==
PASS receipt: the subject names the organization and nothing else
PASS receipt: the body carries received date, the response deadline, their own reference and the tracking link
PASS receipt: it says why it is so short, so the silence reads as design rather than a bug
PASS receipt: sending is due when the operator AND the complainant both said yes
PASS receipt REFUSES to echo the complaint back: no subject line, no details, in body OR subject
PASS receipt carries no token beyond the one already in the complainant's own link, and no secret
PASS receipt refuses: operator switch off means nothing sends, even for a complainant who asked
PASS receipt refuses: complainant did not tick the box — the operator switch cannot override that
PASS receipt refuses: an unusable email address is never mailed
PASS receipt refuses: an install with no organization name still produces a sane subject
PASS receipt idempotency: a complaint already receipted is never due again
PASS receipt idempotency: the row is STAMPED before the mail is sent, and the UPDATE re-checks the stamp
PASS receipt: a mail failure can never lose the complaint — the send is wrapped and swallowed
PASS receipt: one emitter — the automatic path and the resend button both call _cp_send_receipt
PASS receipt permission: resending needs complaint.note + CSRF; a viewer cannot
PASS opt-in ticked is stored as 1
PASS opt-in untouched is stored as 0 — silence is never consent
PASS the public form only offers the box when the operator has switched receipts on
== 22. v3.1 — the complainant's evidence (P3) ==
PASS evidence: ONE storage implementation serves both doors — staff and complainant
PASS evidence: the file's own BYTES decide its type, in the one place that stores it
PASS evidence: stored under a RANDOM name, never the name the uploader chose
PASS evidence: capped at 10 MB and refused at zero bytes
PASS evidence: the public form is multipart, or the browser would send filenames and no files
PASS evidence: PHP's parallel-array upload shape is normalized, so evidence[] actually stores
PASS evidence: the record page finally RENDERS attachments — the 3.0 route had no screen at all
PASS evidence refuses: public uploads are OFF until the operator opens that door
PASS evidence refuses: at most 3 files from the public form, whatever the browser posts
PASS evidence refuses: a rejected file NEVER fails the submission — the complaint still lands
PASS evidence refuses: an attachment is never public — serving needs a login and a permission
PASS evidence permission: a viewer can neither attach nor delete
PASS evidence permission: delete is owner-gated — your own uploads, or admin
PASS evidence permission: only an ADMIN may remove what the complainant supplied
PASS evidence: deleting removes the ROW AND THE FILE — a delete that leaves the bytes is the defect
PASS evidence: deleting a file that is already gone still removes the row (a second click is safe)
== 23. v3.1 — anonymizing now reaches the evidence ==
PASS erasure: the map now names the identifying columns on attachments
PASS erasure: the FILES are unlinked, and stored_file is blanked so nothing points at them
PASS erasure: the files go BEFORE the column pass — a half-done erasure must not leave the bytes
PASS erasure: the count is reported to the operator and recorded in the log and the audit trail
PASS erasure: still refuses while the complaint is open — that rule is untouched
PASS the tracking link in the receipt actually opens the case — GET /track?t= looks it up
PASS a wrong or absent tracking code on that link is a 404, never a listing
PASS a refused file is REPORTED to the complainant, never silently dropped
== 24. v3.1 — truth on the surfaces ==
PASS README changelog carries 3.1.1 and both features
PASS README no longer claims Complia never emails the complainant
PASS every empty-state icon a view asks for is one the kit actually draws
PASS no .alert stacks multiple element children straight into the kit's flex row
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
== F2. Uploads: the pages promise what the server accepts, and an over-size form never loses a complaint ==
PASS ini sizes: 2M, 512K, 1G and "unlimited"
PASS upload limits: per file never above 10 MB and never above what PHP takes
PASS upload limits: labels read as the page shows them
PASS an over-size form (PHP dropped every field) is recognized, not mistaken for an expired session
PASS ...and an ordinary form is not
PASS a file PHP refused as too large is kept (and so refused out loud); an empty slot is not
PASS ...and the refusal names the size limit instead of "choose a file first"
PASS the public form states the server's real limit and checks sizes before sending
PASS the public submit handles an over-size form BEFORE the CSRF check (which would 419 and lose the complaint)
PASS the Docker image accepts 10 MB files and a three-file form
PASS receipt: a send the mail server refuses is reported as not sent
PASS receipt: ...the register does not say "emailed", it says NOT sent
PASS receipt: ...and the claim is released, so it can be resent once mail works
PASS settings: saving the Organization form keeps the SMTP host, the recipient and every switch
PASS settings: the Email form still saves its own fields, unticked boxes included
PASS reminder steps: parsed descending, de-duplicated, nonsense dropped
PASS reminder step: 10 days left reaches none
PASS reminder step: 5 days left is the 7-day step
PASS reminder step: 2 days left is the TIGHTEST step reached (3), not every step
PASS reminder step: the day after the deadline is the overdue step (0)
PASS reminder step: a week overdue sends nothing more
PASS owners: only active members and admins can own a complaint
PASS reminders: off (the default), nothing is sent
PASS reminders: on, the complaint 3 days out gets the 3-day step, to its owner and the notification address
PASS reminders: the complaint due yesterday gets one overdue reminder
PASS reminders: long-overdue and acknowledged complaints get nothing
PASS reminders: with no mail server the step is recorded as no-smtp, never as sent
PASS reminders: the attempt is written to the complaint's own log, failure included
PASS reminders: a second run (cron after a page load) sends nothing — one latch
PASS reminders: undelivered steps are listed for the dashboard
PASS reminders: a cron route runs them, guarded by the backup token
PASS owner: a route names the owner, gated like a note and CSRF-checked
PASS dashboard: the reminder steps ride the page load
PASS the demo checklist promises a register, not a "defensible" one
PASS backup guard: the harness made one signed-in account per role (viewer,member,admin)
PASS backup guard: GET /backup.json — 403 for every non-admin role, open to admin
PASS backup guard: GET /backup.sqlite — 403 for every non-admin role, open to admin
PASS backup guard: GET /restore — 403 for every non-admin role, open to admin
PASS backup guard: POST /restore/dry-run — 403 for every non-admin role, open to admin
PASS backup guard: POST /restore/commit — 403 for every non-admin role, open to admin
PASS backup guard: POST /restore/token — 403 for every non-admin role, open to admin
PASS backup guard: GET /backup/scheduled without its token refuses every signed-in role, admin included
PASS backup guard: GET /reminders/run without its token refuses every signed-in role, admin included
PASS webhooks: "Add" reaches its route and saves the endpoint (was a 404)
PASS webhooks: "Pause" reaches its route and flips the endpoint
PASS webhooks: "Delete" reaches its route and removes the endpoint
PASS demo card: the public pages say what the page is, not "signed in as the admin"
PASS demo card: the admin pages keep "signed in as the admin"
PASS demo card: below 600px it starts as the small "Own it" pill; a tap opens it and the choice holds for the session
PASS demo card: boot() hooks it only behind the .demo-mode marker, and a page with no </body> passes through untouched
PASS backup redaction: a canary sits in every credential column of the schema (api_keys.token_hash, backup_state.s3_secret, feed_tokens.token_hash, invites.token_hash, settings.smtp_pass, settings.privara_api_key, settings.backup_token_hash, settings.oidc_client_secret, updater_state.license_api_key, users.password_hash, users.totp_secret, users.totp_recovery, webhooks.secret)
PASS backup redaction: each arrives as [REDACTED] and no canary is anywhere in the JSON backup
== 3.2.0: leadership report, procedure, related records ==
PASS report: received / on time / late / not yet overdue / not yet in time, in the period
PASS report: on-time rate counts only decided ones (2 of 4 = 50%); medians of 2, 31, 1 days and 20, 4 days
PASS report: outcomes, open now (any period) and overdue now, open by owner
PASS report: month by month
PASS report: an empty period has no rate and no medians, never a zero that reads as a result
PASS report page: renders the numbers for the chosen period
PASS report CSV: a row per month and a total
PASS report: a viewer can read it (a read-only seat is the board's)
PASS report period: a bad custom range falls back to the last 12 months
PASS procedure: a PDF is stored; a file that is not a document is refused whatever its name
PASS procedure: the version in effect today is current, not a future one
PASS procedure: review state overdue / soon / ok / none
PASS procedure: the public link serves the current public version, and the complaint form links to it
PASS procedure: unlinked, nothing is public and the form shows no link
PASS procedure: an overdue review shows on the statutory clock page
PASS procedure: a viewer cannot upload (403)
PASS related: a DSAR link is added and logged; a javascript: address is refused
PASS Privara: a reference is read through the detail route (type, status, deadline)
PASS Privara: an older Privara (reference 404 on the detail route) is found in the list
PASS Privara: a refused key and an unknown reference are said plainly, never shown as a status
PASS Privara: "Check in Privara" on the complaint stores what was read and when
PASS related: the complaint page shows the link, the Privara answer and the check button
PASS related: a viewer cannot add, check or remove (403)
PASS Privara settings: an empty key field keeps the saved key
PASS Privara settings: "Forget the saved key" clears it
PASS related: removing a link is logged
PASS backup: the Privara API key is a credential column, so the JSON backup redacts it by pattern
PASS the procedure checks leave no file behind in data/uploads
PASS api roles: POST /api/complaints refuses every role without the permission (403) and admits the rest
PASS api roles: POST /api/complaints/{id}/status refuses every role without the permission (403) and admits the rest
== customer voice: the team's chat ==
PASS chat payload: Slack, Teams and Google Chat read "text"; Discord reads "content"
PASS chat: a complaint from the public form posts one line with its number and acknowledgment date, and nothing the complainant wrote
PASS chat: nothing when the new-complaint notice is switched off
PASS chat: an acknowledgment reminder posts its line too, by number and date only
PASS Settings: "Send a test line" reports what the chat answered, and is audited
PASS ...and a chat that refuses is reported as refused (HTTP 500)
PASS the shareable JSON backup leaves the chat address out
PASS import dry run: an impossible date (2026-02-30) or a date in the wrong format is counted as skipped, not ready, and nothing is written (ready 1, skipped 2)
————————————————————————————————————————————————————
== 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 Complia 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 (1 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 Complia 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: the owner's "keep" count is honored (2 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 Complia, 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)
PASS erase planting · sqlite: the subject is created and a marker is planted in all 10 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 (23 tables dumped), outside the 3 column(s) kept by ruling (possible legal records)
PASS erase planting · sqlite: the file attached to the complaint is deleted from disk
PASS erase planting · sqlite: the erase page (/complaints/1) says what an erase keeps: "Kept: the complaint's subject, details and outcome and the h…"
PASS erase planting · sqlite: MANUAL.md says the same: "Kept: the complaint's subject, details and outcome and the h…"
ALL 440 TESTS PASSED (0 failed)
← Back to Complia · Manual · Quickstart · API