Fleetora · Test run · run 2026-09-25
Fleetora Test run, as shipped in the download
373 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. dates: a date is a real calendar date or it is nothing ==
PASS '2026-02-28' is a date (got '2026-02-28', want '2026-02-28')
PASS '2026-02-29' is not (2026 is not a leap year) (got NULL, want NULL)
PASS '2024-02-29' is (2024 is) (got '2024-02-29', want '2024-02-29')
PASS '0000-00-00' is refused (got NULL, want NULL)
PASS '2026-13-01' is refused (got NULL, want NULL)
PASS junk is refused (got NULL, want NULL)
PASS a datetime is truncated to its date (got '2026-06-15', want '2026-06-15')
PASS daysUntil forward (got 10, want 10)
PASS daysUntil backward (got -10, want -10)
PASS daysUntil today (got 0, want 0)
PASS daysUntil across a month end (got 29, want 29)
PASS daysUntil on a bad date is null (got NULL, want NULL)
PASS addDays crosses a year end (got '2027-01-09', want '2027-01-09')
PASS addDays backwards (got '2025-12-26', want '2025-12-26')
PASS addDays on a bad date is null (got NULL, want NULL)
== 2. expiryState: the four bands, and the fifth answer that is not a band ==
PASS yesterday is expired (got 'expired', want 'expired')
PASS today is critical, not expired (got 'critical', want 'critical')
PASS inside 7 days is critical (got 'critical', want 'critical')
PASS the 8th day is only soon (got 'soon', want 'soon')
PASS inside 30 days is soon (got 'soon', want 'soon')
PASS the 31st day is ok (got 'ok', want 'ok')
PASS no date recorded is "none", never "ok" (got 'none', want 'none')
PASS the day count comes back with the state (got 10, want 10)
PASS a 60-day warning window moves the boundary (got 'soon', want 'soon')
PASS a 0-day critical window still flags the day itself (got 'critical', want 'critical')
== 3. ⭐ serviceDue: whichever comes FIRST — the rule the product is sold on ==
PASS A: due date is computed from the date interval (got '2027-01-01', want '2027-01-01')
PASS A: due odometer is computed from the distance interval (got 50000, want 50000)
PASS A: days until (got 200, want 200)
PASS A: distance until (got 400, want 400)
PASS A: the DISTANCE leg is the one that bites (got 'distance', want 'distance')
PASS A: state is soon (got 'soon', want 'soon')
PASS B: the DATE leg is the one that bites (got 'date', want 'date')
PASS B: days until (got 10, want 10)
PASS B: distance until (got 9000, want 9000)
PASS B: state is soon (got 'soon', want 'soon')
PASS B2: two legs in the same band are not ranked against each other (got 'both', want 'both')
PASS B2: and the state is that shared band (got 'ok', want 'ok')
PASS C: overdue (got 'overdue', want 'overdue')
PASS C: both legs are past, so due_by says both (got 'both', want 'both')
PASS C: the date overrun is reported as a negative (got -135, want -135)
PASS C: the distance overrun is reported as a negative (got -8600, want -8600)
PASS D: state is unknown, never a guess (got 'unknown', want 'unknown')
PASS D: no due date is invented (got NULL, want NULL)
PASS D: no day count is invented (got NULL, want NULL)
PASS D: no distance-remaining is invented (got NULL, want NULL)
PASS D: but the due odometer IS known, because it is arithmetic (got 20000, want 20000)
PASS E: a missing reading is not hidden behind a healthy date leg (got 'unknown', want 'unknown')
PASS E: the date leg is still reported (got 351, want 351)
PASS F: no interval of either kind is "none" (got 'none', want 'none')
PASS F: due_by is null when nothing is tracked (got NULL, want NULL)
PASS G: due exactly today is critical (got 'critical', want 'critical')
PASS G: zero days until (got 0, want 0)
PASS H: 100 miles to go is critical (got 'critical', want 'critical')
PASS H: due_by is distance (got 'distance', want 'distance')
PASS H2: 300 miles to go is only soon (got 'soon', want 'soon')
PASS I: a 5,000 warning window makes 3,000 to go "soon" (got 'soon', want 'soon')
PASS J: after completion the date clock is full (got 365, want 365)
PASS J: after completion the distance clock is full (got 10000, want 10000)
PASS J: and it is no longer due (got 'ok', want 'ok')
== 4. urgencyRank: the order the whole product is sorted by ==
PASS expired and overdue rank together at the top
PASS the bands descend: overdue < critical < soon
PASS UNKNOWN sorts after everything actionable but BEFORE ok — it is a gap to close, not a pass
PASS not-tracked sorts last of the known states
PASS an unrecognised state sorts last of all rather than first
== 5. readingOk: an odometer does not run backwards ==
PASS a higher reading is accepted
PASS the same reading is accepted
PASS a lower reading is refused
PASS the refusal names the last reading, so the operator can see the typo
PASS the first reading on a vehicle is always allowed
PASS zero is a legitimate first reading
PASS a negative reading is refused
PASS an absurd reading is refused
PASS every refusal carries a sentence, never a bare false
== 6. dayPhrase: three different truths never collapse into one ==
PASS today (got 'today', want 'today')
PASS tomorrow (got 'tomorrow', want 'tomorrow')
PASS yesterday (got 'yesterday', want 'yesterday')
PASS forward (got 'in 12 days', want 'in 12 days')
PASS backward (got '12 days ago', want '12 days ago')
PASS unknown says WHY it is unknown (got 'no reading on file', want 'no reading on file')
PASS not tracked is not the same as unknown (got 'not tracked', want 'not tracked')
PASS a distance-due row says so rather than "no date" (got 'by distance', want 'by distance')
== 7. defects: the flow is a matrix, and the screen reads the same matrix ==
PASS open can be triaged, fixed or dismissed
PASS a fixed defect can only be reopened — the record of the fix stands
PASS a dismissed defect can only be reopened
PASS a status cannot transition to itself
PASS an unknown status has no moves at all
PASS the dropdown is generated from the SAME matrix the writer enforces
PASS severity is ordered worst-first, and immobilise is the worst
== 8. distance: formatted in the vehicle's OWN unit, never converted ==
PASS miles (got '49,600 mi', want '49,600 mi')
PASS kilometres (got '49,600 km', want '49,600 km')
PASS an unknown unit falls back to mi rather than inventing one (got '10 mi', want '10 mi')
PASS no conversion factor exists anywhere in the engine
== 9. csvCell: numeric-aware formula-injection guard ==
PASS a leading = is neutralised (got '\'=1+1', want '\'=1+1')
PASS a leading + is neutralised (got '\'+cmd', want '\'+cmd')
PASS a leading @ is neutralised (got '\'@SUM', want '\'@SUM')
PASS a leading tab is neutralised (got '\' =1', want '\' =1')
PASS a NEGATIVE NUMBER is left alone — it is data, not a formula (got '-1500', want '-1500')
PASS a negative decimal is left alone (got '-15.00', want '-15.00')
PASS a hyphenated word is still neutralised (got '\'-cmd', want '\'-cmd')
PASS ordinary text is untouched (got 'FL-1042', want 'FL-1042')
== 10. csrf_valid: the empty-token bypass (hash_equals('','') is TRUE) ==
PASS an unset session token can never match
PASS an unset session token rejects a supplied one
PASS a real token matches itself
PASS a real token rejects a wrong one
== 11. Pdf: produces a valid, non-trivial document ==
PASS starts with the PDF magic
PASS ends with the EOF marker
PASS is not a stub
PASS page placeholders are resolved, never printed literally
== 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: notify_defaults_off
PASS v2: warning_windows_seeded
PASS v2: fleet_tables
PASS v2: reg_is_unique
PASS v2: mint
PASS v2: revoke
PASS v2: totp
PASS v2: recovery
PASS v2: delivery
PASS v2: rate
PASS v2: csv_dry
PASS v2: backup_tables
PASS v2: upgrade_from_1x
PASS v2: serializer_matches_core
PASS v2: index.php calls Database::migrate() at boot — the ONLY upgrade path for an existing install
PASS v2: routes registered (api, 2fa, security, backup, import, healthz)
PASS v2: 2FA intercepts login + login is rate-limited
PASS v2: the API reuses the tested core (never re-derives a state or a guard)
PASS v2: no ternary key re-reads (PLAN item 10)
PASS v2: dark palette + zero hardcoded white backgrounds
PASS v2: the kit ships and the layout loads it + has a toggle
PASS v3: API.md ships + sw cache is v1.0.4
PASS v2: cli-server base guard present (PLAN item 9)
PASS v3: v3_tables
PASS v3: v3_columns
PASS v3: upgrade_restores_v3_tables
PASS v3: update_on_a_missing_settings_row_silently_does_nothing
PASS v3: migrate_ensures_the_settings_singleton
PASS v3: due_by_distance_when_distance_bites_first
PASS v3: due_by_date_when_date_bites_first
PASS v3: overdue_on_both_legs_is_overdue
PASS v3: distance_service_with_no_reading_is_unknown_never_guessed
PASS v3: no_interval_is_not_tracked_not_overdue
PASS v3: there_is_exactly_ONE_service_completion_writer
PASS v3: completing_a_service_resets_BOTH_clocks
PASS v3: a_completed_service_is_no_longer_due
PASS v3: a_reading_may_go_up
PASS v3: a_reading_may_not_go_down
PASS v3: a_first_reading_is_always_allowed
PASS v3: a_negative_reading_is_refused
PASS v3: every_door_uses_the_same_reading_guard
PASS v3: refused_reading_wrote_nothing
PASS v3: expiry_bands
PASS v3: a_zero_date_is_not_a_date
PASS v3: hero_merges_documents_and_services
PASS v3: hero_is_ranked_worst_first
PASS v3: hero_puts_unknown_after_actionable_but_before_ok
PASS v3: hero_counts_are_a_count_of_the_hero_rows
PASS v3: every_state_key_is_present_even_at_zero
PASS v3: expired_and_overdue_are_counted_separately
PASS v3: mcp_expiring_matches_the_engine
PASS v3: mcp_record_matches_the_engine
PASS v3: mcp_refuses_an_unknown_registration
PASS v3: every_read_tool_is_annotated_readOnly
PASS v3: write_tools_are_not_annotated_readOnly
PASS v3: a_read_only_key_still_sees_the_read_tools
PASS v3: mcp_write_hits_the_same_refusal
PASS v3: mcp_write_actually_writes
PASS v3: unknown_role_gets_the_most_restrictive
PASS v3: empty_role_is_treated_as_legacy_admin
PASS v3: a_driver_may_report_and_log
PASS v3: a_driver_may_not_edit_the_fleet
PASS v3: a_member_may_not_dispose
PASS v3: only_an_admin_owns_the_doc_types
PASS v3: a_viewer_writes_nothing
PASS v3: last_admin_cannot_be_demoted
PASS v3: a_driver_sees_report_and_log_but_the_register_stays_read_only
PASS v3: the_role_ladder_has_one_definition
PASS v3: every_role_survives_being_set
PASS v3: a_normalize_without_the_product_list_would_lose_the_driver
PASS v3: no_caller_normalizes_without_the_product_list
PASS v3: the_team_screen_offers_every_rung
PASS v3: defect_flow_allows_the_real_moves
PASS v3: defect_flow_refuses_a_jump_that_skips_reality
PASS v3: the_screen_offers_exactly_what_the_writer_accepts
PASS v3: mail_is_off_until_the_operator_arms_it
PASS v3: a_disarmed_notify_sends_nothing_and_says_so
PASS v3: the_same_notice_is_never_sent_twice
PASS v3: a_failed_notice_is_still_retryable
PASS v3: a_bad_address_is_refused_before_a_connection_is_opened
PASS v3: driver_mail_carries_no_cost_and_no_other_driver
PASS v3: the_installer_requires_only_modules_that_exist
PASS v3: the_installer_migrates_before_it_writes_a_row
PASS v3: the_installer_keeps_what_the_operator_typed
PASS v3: the_installer_seeds_neutral_document_types
PASS v3: healthz_answers_before_the_installer_redirect
PASS v3: a_dry_run_writes_nothing
PASS v3: the_report_and_the_import_agree_exactly
PASS v3: the_report_names_every_reason_a_row_is_skipped
PASS v3: the_import_wrote_exactly_what_it_promised
PASS v3: vehicle_disposed_is_declared
PASS v3: vehicle_disposed_actually_fires
PASS v3: disposal_closes_the_open_assignment
PASS v3: a_disposed_vehicle_leaves_the_hero_list
PASS v3: a_disposed_vehicle_is_still_in_the_full_list
PASS v3: fleet_pdf_engine_renders
PASS v3: feed_token_verifies
PASS v3: revoking_the_feed_locks_it
PASS v3: feed_renders_a_calendar
PASS v3: feed_omits_the_undateable_rather_than_guessing
PASS v3: brand_css_brands_the_light_theme
PASS v3: brand_css_brands_the_dark_theme_too
PASS v3: brand_dark_accent_is_legible_on_the_dark_canvas
PASS v3: unbranded_install_emits_no_brand_css
PASS v3: scheduled_backup_writes_a_dated_file
PASS v3: scheduled_backup_redacts_credential_material
PASS v3: scheduled_backup_prunes_to_the_retention_count
PASS v3: a_vehicle_row_holds_no_personal_data
PASS v3: the_personal_data_is_where_the_product_says_it_is
PASS v3: audit_records_the_writes
PASS v3: audit_detail_carries_no_secret
PASS v3: db_files_cleaned
PASS v3: the layout INJECTS the brand CSS (a helper nothing calls brands nothing)
PASS v3: the branding form exists and posts to the branding route
PASS v3: the logo is rendered, not merely stored
PASS v3: the calendar feed and the scheduled backup are reachable (helpers with a route)
PASS v3: the feed can be REVOKED from the screen, not only created
PASS v3: GET /mcp answers the transport hint BEFORE the key check
PASS v3: ONE completion writer — every door goes through _fl_service_complete_apply()
PASS v3: the hero endpoint does NOT own a second copy of the completion rule
PASS v3: the PDFs read Fleet/Registry rather than doing their own date arithmetic
PASS v3: no view posts to a route that does not exist
PASS v3: no POST route shares a path with a real directory
PASS v3: every route has a function to answer it
PASS v3: every function the controllers CALL actually exists
PASS v3: every view the controllers render actually exists on disk
PASS v3: the deploy kit is filled and ships inside app/ (where package.sh will find it)
PASS v3: the nine cores are byte-identical to the reference copy (or skipped when unavailable)
PASS v3: README documents the release and API.md gains the MCP section
PASS v3: the audit viewer reads the column the core actually writes
PASS v3: MCP domain refusals THROW McpRefusal, never return an error array
PASS v3: UI mutations + api-settings are role-gated, not merely logged in
PASS v3: a screen that renders a credential is gated on READ, not only on write
PASS v3: every sidebar link a role can SEE is a page that role can OPEN
PASS v3: no ungated write route beyond the by-design baseline (router-derived)
PASS nav: every sidebar link is highlighted by the page it opens
PASS nav: no page highlights a sidebar item that does not exist
== client_ip ==
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 ==
PASS kit: layout loads the kit css+js, groups the nav (>=3 groups), keeps the theme toggle and a who-line
PASS kit: no theme.js; style.css is an app layer with a dark accent; pre-kit scratch file removed
PASS kit: App::asset() is versioned by mtime and the service worker revalidates code
PASS kit: no font or script is fetched from another host (fonts bundled)
PASS kit: every structured empty state uses an icon the kit actually masks
PASS kit: no shipped asset is dead weight — every js/css file is actually loaded
PASS kit: every table uses the kit's WRAPPER contract, not class="tbl" on the table itself
PASS kit: no view leans on a CSS class the app layer never defines
PASS hero: the dashboard IS the ranked list, computed in the controller from the engine
PASS hero: the one-click action posts CSRF as a HEADER, because the body is JSON
PASS hero: the row is re-rendered from the SERVER answer, not from what the button hoped
PASS hero: a 422 asking for a fact is asked, never filled in with a plausible number
PASS dashboard: the first-run checklist is computed from the DATABASE, never a stored flag
PASS record: the vehicle page shows documents, servicing, defects, readings and who holds it
PASS honesty: the product says out loud what it cannot know
PASS country-neutrality: no country's compliance regime is hard-coded anywhere in the product
PASS country-neutrality: document types are ROWS the operator owns, with a screen to own them
== API-key scope ==
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: every MCP tool declares whether it reads or writes, so a read-only key can see something
PASS scope: the key form lets an administrator mint a read-only key
PASS scope: the key-create route passes the chosen scope through to mintKey
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
== fix wave: backups are admin-only ==
PASS backup: a viewer is refused the JSON backup (403, no tables)
PASS backup: a viewer is refused the database file (403, no SQLite bytes)
PASS backup: the Security page shows a viewer no backup buttons
PASS backup: a driver is refused the JSON backup (403, no tables)
PASS backup: a driver is refused the database file (403, no SQLite bytes)
PASS backup: the Security page shows a driver no backup buttons
PASS backup: a member is refused the JSON backup (403, no tables)
PASS backup: a member is refused the database file (403, no SQLite bytes)
PASS backup: the Security page shows a member no backup buttons
PASS backup: an admin still downloads the JSON backup
PASS backup: an admin still downloads the database file
PASS backup: the Security page still offers an admin both downloads
== fix wave: expiry and service-due reminders actually go out ==
PASS reminders: a fresh install writes to nobody
PASS reminders: switched on without smtp sends nothing and says so
PASS reminders: dry run names who would be written to and sends nothing
PASS reminders: each driver and the office get one message
PASS reminders: a driver hears only about their own vehicle
PASS reminders: a service notice says the service is overdue
PASS reminders: the office digest lists every item in window and the unknowns
PASS reminders: the same day run twice writes to nobody twice
PASS reminders: crossing into expired tells the driver once more
PASS reminders: a renewed document leaves the run
PASS reminders: with the driver switch off no driver is written to
PASS reminders: every reference fits the 60 character column
PASS reminders: the run is audited
PASS reminders: both saved settings are read by the sender (the defect: saved, read by nothing)
PASS reminders: cron/expiry.php exists and calls the one run
PASS reminders: cron/expiry.php refuses anything but the command line
PASS reminders: the web server refuses /cron/ (router and .htaccess)
PASS reminders: the button route exists and is admin-only
PASS reminders: an admin presses the button and is told what happened (here: mail is off)
PASS reminders: Settings shows the admin the schedule line and the button
PASS reminders: a viewer is refused the send-now button (403)
PASS reminders: a driver is refused the send-now button (403)
PASS reminders: a member is refused the send-now button (403)
== fix wave: every REST write checks the key holder's role ==
PASS api: a viewer's key is refused POST /api/vehicles (403, needs fleet.write)
PASS api: a driver's key is refused POST /api/vehicles (403, needs fleet.write)
PASS api: a member's key passes the role check on POST /api/vehicles
PASS api: a admin's key passes the role check on POST /api/vehicles
PASS api: a viewer's key is refused POST /api/vehicles/999/mileage (403, needs mileage.log)
PASS api: a driver's key passes the role check on POST /api/vehicles/999/mileage
PASS api: a member's key passes the role check on POST /api/vehicles/999/mileage
PASS api: a viewer's key is refused POST /api/vehicles/999/defects (403, needs defect.report)
PASS api: a driver's key passes the role check on POST /api/vehicles/999/defects
PASS api: a admin's key passes the role check on POST /api/vehicles/999/defects
PASS api: every /api/* write route checks the key holder's role (no key-only writes)
== fix wave: 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 /restore/backup-token
PASS backup: a signed-in viewer without the token gets nothing from GET /backup/scheduled
PASS backup: a signed-in driver gets 403 on GET /backup.json
PASS backup: a signed-in driver gets 403 on GET /backup.sqlite
PASS backup: a signed-in driver gets 403 on GET /restore
PASS backup: a signed-in driver gets 403 on POST /restore
PASS backup: a signed-in driver gets 403 on POST /restore/commit
PASS backup: a signed-in driver gets 403 on POST /restore/backup-token
PASS backup: a signed-in driver 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 /restore/backup-token
PASS backup: a signed-in member without the token gets nothing from GET /backup/scheduled
PASS backup: an admin still opens the restore page
== fix wave: the SMTP password is never written into the page ==
PASS smtp: a typed password is saved
PASS smtp: the settings page does not contain the stored password
PASS smtp: saving with the password field left blank keeps the stored password
== fix wave: one server name on every door ==
PASS mcp: the spec and the probe both name the server "fleetora"
PASS mcp: the probe answers with the same name
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: the demo ribbon's wording and size; the JSON backup's secrets --
PASS demo ribbon: Fleetora 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.feed_token_hash, settings.smtp_pass, users.password_hash, users.totp_secret, users.totp_recovery, api_keys.token_hash, webhooks.secret, invites.token_hash)
————————————————————————————————————————————————————
ALL 373 TESTS PASSED (0 failed)