Ownware
Home›Inspectora›Test run
Inspectora · Test run · run 2026-09-25

Inspectora Test run, as shipped in the download

186 checks passed, 0 failed, when the suite that ships in the zip was run on 2026-09-25 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 and MCP
  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 (60 routes checked)

== 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  licence: every font ships the SIL OFL text for its own family beside it
  PASS  licence: 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: 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 10 credentials this schema holds (settings.oidc_client_secret, settings.backup_token_hash, settings.smtp_pass, users.password_hash, users.totp_secret, users.totp_recovery, api_keys.token_hash, webhooks.secret, invites.token_hash, feed_tokens.token_hash)

————————————————————————————————————————————————————
ALL 186 TESTS PASSED (0 failed)

← Back to Inspectora · Manual · Quickstart · API

Affiliate program
Recommend tools people own — earn 35% on every sale. 90-day tracking, instant delivery, payouts by Lemon Squeezy.
Become an affiliate →