Fleetora · Test run · run 2026-11-07
Fleetora Test run, as shipped in the download
485 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. 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 unrecognized 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 immobilize 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 neutralized (got '\'=1+1', want '\'=1+1')
PASS a leading + is neutralized (got '\'+cmd', want '\'+cmd')
PASS a leading @ is neutralized (got '\'@SUM', want '\'@SUM')
PASS a leading tab is neutralized (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 neutralized (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 v4.1.3
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 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
== 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 11 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, backup_state.s3_secret, invites.token_hash)
== 1.1 · the paper behind each expiry, the fuel log, the walk-round, imported readings ==
PASS 1.1: reading_rule
PASS 1.1: fuel_amount_parsed
PASS 1.1: economy_in_the_drivers_words
PASS 1.1: full_to_full_stretches
PASS 1.1: fill_up_logs_the_reading
PASS 1.1: a_lower_odometer_refuses_the_fill
PASS 1.1: fill_up_refusals
PASS 1.1: fill_up_without_odometer
PASS 1.1: document_file_filed_by_its_bytes
PASS 1.1: document_delete_takes_its_files
PASS 1.1: walk_items_from_settings_or_default
PASS 1.1: walk_key_off_until_on
PASS 1.1: walk_needs_every_answer_and_a_note_per_defect
PASS 1.1: each_defect_becomes_a_reported_fault
PASS 1.1: walk_odometer_through_the_one_rule
PASS 1.1: a_lower_walk_odometer_is_kept_not_logged
PASS 1.1: walk_link_rate_limited
PASS 1.1: telematics_parsers
PASS 1.1: telematics_plan_decides_each_row
PASS 1.1: telematics_apply_writes_only_what_the_plan_logged
PASS 1.1: telematics_same_file_twice_writes_nothing
PASS 1.1: telematics_csv_columns_found_by_name
PASS 1.1: walk_same_send_recorded_once
PASS 1.1 door: a driver logs a fill-up and its odometer becomes the reading; a viewer cannot
PASS 1.1 door: fuel costs are for the office: a driver is refused the fuel page, a member sees it
PASS 1.1 door: a driver sees the vehicle's fuel without its costs
PASS 1.1 door: the walk-round link is a 404 while it is off
PASS 1.1 door: switched on, the cab link opens the check with no login, kept out of search engines
PASS 1.1 door: a check with a defect from the cab link is received and the defect is reported
PASS 1.2 door: both walk-round forms carry a one-off send reference and keep their answers on the phone
PASS 1.2 door: the cab link sent twice (the first answer lost) is one check and one defect; the second page says so and clears the phone's copy
PASS 1.2 door: the signed-in check sent twice is recorded once, and the vehicle page it lands on clears that send only
PASS 1.1 door: the trap field keeps nothing
PASS 1.1 door: a member cannot switch the cab link on (settings.write)
PASS 1.1 door: a new key closes every old link
PASS 1.1 door: the walk-round list, and the QR sheet carrying each vehicle's full link with the current key
PASS 1.1 api: a viewer's key cannot push readings
PASS 1.1 door: only someone who edits the fleet imports readings
————————————————————————————————————————————————————
== 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 Fleetora 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 Fleetora 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 Fleetora, 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 5 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 (27 tables dumped), outside the 3 column(s) kept by ruling (possible legal records)
PASS erase planting · sqlite: a member (fleet manager seat) cannot erase: HTTP 403, nothing changed
PASS erase planting · sqlite: a member cannot open the Data requests page: HTTP 403
PASS erase planting · sqlite: an admin is refused while the driver still holds a vehicle, and nothing changes
PASS erase planting · sqlite: an admin opens the Data requests page and the driver is listed
PASS erase planting · sqlite: the erase page (/gdpr) says what an erase keeps: "Kept: the driver's name on the walk-round checks they did an…"
PASS erase planting · sqlite: MANUAL.md says the same: "Kept: the driver's name on the walk-round checks they did an…"
ALL 485 TESTS PASSED (0 failed)