Inspectora · Test run · run 2026-11-07
Inspectora Test run, as shipped in the download
305 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. expiryFrom: months added, month-end CLAMPED, 0 = never expires == PASS 1 Aug + 12m (got '2027-08-01', want '2027-08-01') PASS 31 Jan + 1m clamps to Feb 28 (2026 is not a leap year) (got '2026-02-28', want '2026-02-28') PASS 29 Feb + 12m clamps to Feb 28 (got '2025-02-28', want '2025-02-28') PASS 31 Mar + 1m clamps to Apr 30 (got '2026-04-30', want '2026-04-30') PASS 31 Dec + 1m crosses the year (got '2027-01-31', want '2027-01-31') PASS 1 Jan + 36m (got '2029-01-01', want '2029-01-01') PASS 0 months = never expires (got NULL, want NULL) PASS negative months = never expires (got NULL, want NULL) PASS a nonsense date yields nothing (got NULL, want NULL) PASS 30 Jan and 31 Jan + 1m land on the SAME day, not three days apart == 2. state: the expiry day itself is still valid == PASS expires in 90 days, warn 30 -> valid (got 'valid', want 'valid') PASS expires in 10 days, warn 30 -> expiring (got 'expiring', want 'expiring') PASS expires TODAY -> expiring, never expired (got 'expiring', want 'expiring') PASS expired yesterday -> expired (got 'expired', want 'expired') PASS exactly at the warning edge -> expiring (got 'expiring', want 'expiring') PASS one day past the edge -> valid (got 'valid', want 'valid') PASS no expiry date at all -> valid forever (got 'valid', want 'valid') PASS warn 0 means only today is amber (got 'expiring', want 'expiring') PASS a cell with no record at all (got 'none', want 'none') == 2b. a failed check holds nothing (F2b, 2026-09-24) == PASS the engine knows three results (got array ( 0 => 'pass', 1 => 'pass_remarks', 2 => 'fail', ), want array ( 0 => 'pass', 1 => 'pass_remarks', 2 => 'fail', )) PASS a fail is FAILED even with an expiry still ahead of it (got 'failed', want 'failed') PASS a fail with no expiry is FAILED, never "valid forever" (got 'failed', want 'failed') PASS a pass with remarks is read by its dates like any pass (got 'valid', want 'valid') PASS a record with no result at all (older rows) is a pass (got 'expired', want 'expired') PASS isFail reads the result and nothing else PASS a failed cell says so (got 'failed', want 'failed') PASS failed is the worst state, read first (got 'failed', want 'failed') PASS a per-row tally counts failed cells in its own key (got array ( 'valid' => 1, 'expiring' => 0, 'expired' => 0, 'missing' => 0, 'failed' => 1, ), want array ( 'valid' => 1, 'expiring' => 0, 'expired' => 0, 'missing' => 0, 'failed' => 1, )) == 3. daysUntil == PASS 10 days ahead (got 10, want 10) PASS today is 0 (got 0, want 0) PASS yesterday is -1 (got -1, want -1) PASS across a month boundary (got 31, want 31) PASS null in, null out (got NULL, want NULL) == 4. phrase: never invents a number it was not given == PASS missing (got 'never recorded', want 'never recorded') PASS overdue (got '84d overdue', want '84d overdue') PASS today (got 'expires today', want 'expires today') PASS soon (got 'in 12d', want 'in 12d') PASS valid with no date (got 'valid', want 'valid') == 5. applies: blank scope = everyone, otherwise the tag must match == PASS a blank scope applies to everyone PASS a scoped requirement applies to its own group PASS and not to another group PASS matching ignores case and stray spaces PASS a subject with no group is outside a scoped requirement == 6. dates == PASS isDate accepts a real date PASS isDate rejects 30 February PASS isDate rejects a non-date PASS isDate rejects a partial date PASS addDays forward (got '2026-09-01', want '2026-09-01') PASS addDays backward (got '2026-08-22', want '2026-08-22') == 7. tableFor: the only interpolated identifier is whitelisted == PASS subject maps to this product's table (got 'assets', want 'assets') PASS an unknown role is refused, not guessed PASS ROLES is exactly the six the engine knows PASS Domain names a table for every role == 8. the shared engine is byte-identical to the canonical copy == PASS the shipped engine matches the canonical one byte for byte PASS the engine file ships with the product PASS the two products ship the SAME code apart from the four that may differ == 9. Pdf: produces a valid, non-trivial document == PASS starts with the PDF header PASS ends with the EOF marker PASS is not a stub == 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 CURRENT version (old caches purged on activate) PASS offline fallback page ships PASS the layout links the manifest + registers the SW PASS the layout carries a theme toggle and loads the kit == platform shape == PASS index.php migrates at boot — the ONLY upgrade path for an existing install PASS the twelve domain routes are registered PASS route segments come from Domain, so index.php is one file for both products PASS the platform routes are registered (api, mcp, 2fa, security, backup, import, healthz, openapi) PASS 2FA intercepts login + login is rate-limited PASS exactly ONE place inserts a record, and it is the engine PASS every door reaches the engine: grid cell, round attendance, REST, MCP, (Trainora 3.2) a confirmed certificate sent in, (Inspectora 3.2) the checklist inspection and (Trainora 3.3) a spreadsheet import PASS no controller recomputes an expiry date of its own PASS no controller hardcodes a physical table name — they all ask Domain PASS the write path returns refusals rather than throwing (every door must explain itself) PASS every MCP read tool declares readOnly (the core hides writes from a read key by THIS flag) PASS the MCP endpoint hands the key scope to the core PASS a read-scoped key is refused at the door, once — not per route PASS mintKey persists the scope and the key form offers it PASS the certificate hand-off emits certora's three header names, and nothing generates a certificate here PASS settings is two forms writing one row, each declaring its block PASS counterparty mail is opt-in and swallows its own failures PASS absolute URLs are built once (no doubled origin) PASS dark palette + zero hardcoded white backgrounds PASS the app layer only re-tints the accent; the kit owns the rest PASS API.md ships and documents the vendor-neutral connect recipe PASS /backup.sqlite is carved out of the blanket .sqlite block in BOTH the dev router and .htaccess PASS every cron script the docs and screens name ships (reminders.php) PASS the reminder runner is CLI-only, calls the one shared pass, and the web server denies cron/ PASS BACKUP: both backup downloads are administrators-only, and only administrators are shown them PASS WEBHOOKS: the events offered for subscription are exactly the events the code fires (record.created) PASS DASHBOARD: a completed getting-started step draws its tick (the kit draws the ✓ into <i>) == every function the routes name actually exists == PASS every route handler is defined (83 routes checked) PASS security page: the backup card says webhook URLs and every credential are left out of the JSON (ruling 1, 2026-09-25) PASS api roles: a viewer's key cannot write a record (403); a member's key gets past the gate to the data checks == 3.2: the checklist inspection with photos, the report of one inspection, QR labels, certificates, assigned schedules == PASS 3.2: steps_parsed PASS 3.2: result_needs_every_answer PASS 3.2: one_failed_step_fails_it PASS 3.2: no_checklist_uses_the_posted_result PASS 3.2: incomplete_writes_nothing PASS 3.2: answers_kept_in_order_and_fail_recorded PASS 3.2: a_pass_starts_the_clock PASS 3.2: engine_refusals_still_apply PASS 3.2: schedule_row_marked_done_once PASS 3.2: photo_filed_as_evidence_by_its_bytes PASS 3.2: photo_size_limit PASS 3.2: report_pdf_of_one_inspection PASS 3.2: link_opens_that_inspection_only PASS 3.2: a_new_link_stops_the_old PASS 3.2: withdrawn_link_opens_nothing PASS 3.2: share_mail_names_the_result_and_link PASS 3.2: certificate_rows PASS 3.2: labels_active_assets_by_group PASS 3.2: only_members_and_admins_can_be_assigned PASS 3.2: my_inspections_lists_what_is_left PASS 3.2: a_held_schedule_leaves_the_list PASS 3.2: unassign PASS 3.2: assignee_from_the_create_form PASS 3.2: routes_registered_only_for_inspectora PASS 3.2: writes_are_gated PASS 3.2: schema_is_gated PASS 3.2: a_send_is_claimed_once_released_on_refusal_and_a_dead_claim_taken_over PASS 3.2: a viewer cannot open the inspection form; a member sees every step PASS 3.2: posting with a step unanswered records nothing and says why PASS 3.2: answered in full, one failed step records a fail with both answers PASS 3.2: the read-only link opens that report with no login, kept out of search engines PASS 3.2: a wrong link answers 404 and names nothing PASS 3.2: a viewer cannot make or withdraw a link PASS 3.2: a viewer can print the report of one inspection PASS 3.2: a failed inspection has no certificate PASS 3.2: the QR label sheet lists the assets PASS 3.2: "My inspections" shows the member their assigned schedule with an Inspect button PASS 3.2: deleting an inspection takes its checklist answers with it, and its link opens nothing PASS 3.2: a viewer cannot assign a schedule PASS 3.3: the phone form carries a one-off send token and keeps its answers on the phone as they are filled in PASS 3.3: the same send twice (its answer lost the first time) is recorded once, and the second is told so PASS 3.3: a refused send gives its token back, so the corrected form with the same token is recorded PASS 3.3: the record page clears the phone's kept copy of that send only, and only when it says which == runtime == PASS migrate() creates the six engine tables under this domain's names PASS install() ALONE produces the current schema PASS migrate() creates the settings row PASS migrate() twice changes nothing PASS HAPPY: a completion is recorded and its expiry computed PASS REFUSAL: a future date is refused PASS REFUSAL: a nonsense date is refused PASS REFUSAL: a requirement scoped to another group is refused PASS REFUSAL: an archived subject is refused PASS REFUSAL: unknown ids are refused, not created PASS REFUSAL: a refused write leaves the table untouched PASS PERMISSION: viewer may read and may not write PASS PERMISSION: member may write records PASS PERMISSION: member may not delete PASS PERMISSION: admin may do everything PASS PERMISSION: an unknown role gets the most restrictive one PASS IDEMPOTENCY: marking attendance twice writes one record PASS the state a round stores is the word the screens compare against PASS IDEMPOTENCY: the same person cannot be booked twice PASS IDEMPOTENCY: a reminder ledger row is claimed before the send PASS IDEMPOTENCY: a failed send releases its slot PASS a renewal supersedes without deleting the old record PASS the current record is the latest achieved_on PASS coverage counts only cells that APPLY PASS the attention list is ordered worst-first, stably PASS FAILED: a failed record makes the cell failed; an earlier-dated pass does not clear it, a later pass does PASS FAILED: a fail stores no expiry, and the API never calls it "never expires" PASS FAILED: a fail comes off the in-date count (today and in the history) and heads the attention list PASS FAILED: a result the engine does not know is refused and writes nothing PASS FAILED: a pass with remarks is a pass, with its expiry PASS FAILED: the agent tool takes a result only where the product records one; otherwise it is unchanged PASS EXPORT: the matrix CSV header and cells are as documented PASS EXPORT: the records CSV header is as documented PASS HAND-OFF: exactly name,course,email — certora's importer contract PASS HAND-OFF: an expired holder is not offered a certificate PASS a fresh install writes to nobody PASS switched on with no SMTP still sends nothing PASS RUNNER: cron/reminders.php --dry-run names who is due and sends and writes nothing PASS RUNNER: cron/reminders.php writes to each person due AND sends the owner digest, over real SMTP PASS RUNNER: a real pass records when it ran and what it did PASS RUNNER: a second pass the same day attempts no mail at all PASS RUNNER: a digest the mail server refuses is retried next run PASS RUNNER: switched-on reminders that never ran are flagged, a fresh run is not PASS EVIDENCE: the bytes decide the type, not the name PASS every write path leaves an audit row PASS no audit row carries a secret PASS the calendar feed carries no email addresses PASS GDPR: names go, the compliance history stays PASS GDPR: refused while a seat is still booked PASS backup + restore parses its own file PASS BACKUP: the permission the downloads ask for is held by an administrator and by no viewer or member 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 == every backup and restore route is administrator-only == PASS BACKUP: a signed-in viewer gets 403 on GET /backup.json PASS BACKUP: a signed-in viewer gets 403 on GET /backup.sqlite PASS BACKUP: a signed-in viewer gets 403 on GET /restore PASS BACKUP: a signed-in viewer gets 403 on POST /restore PASS BACKUP: a signed-in viewer gets 403 on POST /restore/commit PASS BACKUP: a signed-in viewer gets 403 on POST /api-settings/backup-token PASS BACKUP: a signed-in viewer without the token gets nothing from GET /backup/scheduled PASS BACKUP: a signed-in member gets 403 on GET /backup.json PASS BACKUP: a signed-in member gets 403 on GET /backup.sqlite PASS BACKUP: a signed-in member gets 403 on GET /restore PASS BACKUP: a signed-in member gets 403 on POST /restore PASS BACKUP: a signed-in member gets 403 on POST /restore/commit PASS BACKUP: a signed-in member gets 403 on POST /api-settings/backup-token PASS BACKUP: a signed-in member without the token gets nothing from GET /backup/scheduled PASS BACKUP: an admin still downloads the JSON backup == the dashboard speaks this product's language == PASS WORDS: the main button reads "Open the due board", never "Open the what is …" PASS WORDS: the first-run step asks for the inspections already done, not what people hold PASS WORDS: the certificates box describes evidence on the record, with no Certora hand-off PASS WORDS: the assets register is not drawn with a person icon PASS UI: the sidebar brand cannot be shrunk to nothing (flex-shrink:0) == fix wave: the webhook buttons work == PASS webhooks: Add creates the endpoint (no 404) PASS webhooks: Pause switches it off (no 404) PASS webhooks: pressing it again switches it back on PASS webhooks: the page offers Pause and Delete for the endpoint, at those same addresses PASS webhooks: Delete removes it (no 404) == F2b: a failed check, through the screens == PASS FAILED: the grid form records a fail, and the cell comes back failed (got 'failed', want 'failed') PASS FAILED: the stored record says fail and carries no next-due date PASS FAILED: the grid paints the cell failed, offers a "Failed" filter and legend, and the form asks for the result PASS FAILED: a round sheet asks for each row's result, and a fail on it is recorded as a fail PASS FAILED: the dashboard counts failed checks with the expired ones PASS FAILED: the REST door hands the posted result to the same reader as the other doors -- F2b: the demo ribbon's wording and size; the JSON backup's secrets -- PASS demo ribbon: the read-only report link (Inspectora 3.2) does not tell its visitor they are signed in as the admin PASS demo ribbon: Inspectora has no public page, so a page served without sign-in keeps the admin wording too PASS demo ribbon: an administrator's page keeps the admin wording PASS demo ribbon: below 600px it starts as the small "Own it" pill; a tap opens it and the choice holds for the session PASS demo ribbon: registered only behind the .demo-mode marker, and non-HTML output passes through untouched PASS backup: the JSON download carries none of the 12 credentials this schema holds (settings.oidc_client_secret, settings.backup_token_hash, settings.smtp_pass, users.password_hash, users.totp_secret, users.totp_recovery, records.share_token_hash, api_keys.token_hash, webhooks.secret, backup_state.s3_secret, invites.token_hash, feed_tokens.token_hash) ———————————————————————————————————————————————————— PASS 3.3 import dry run: the_upload_reaches_the_dry_run PASS 3.3 import dry run: the_dry_run_writes_nothing PASS 3.3 import dry run: the_dry_run_counts_only_what_will_import PASS 3.3 import dry run: the_dry_run_names_why PASS 3.3 import dry run: the_import_brings_in_exactly_what_the_dry_run_said == 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) SKIP updates: the state row (this suite keeps no open database in its own process; update_e2e.sh installs, migrates and updates for real) 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 product 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 product 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 product, 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 1 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 (25 tables dumped) ALL 305 TESTS PASSED (0 failed)