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

Onboardora Test run, as shipped in the download

151 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.

[DB: SQLite]

== due dates are derived, never stored ==
  PASS  date: day 0 is the start date itself (got '2026-06-01', want '2026-06-01')
  PASS  date: a positive offset moves forwards (got '2026-07-01', want '2026-07-01')
  PASS  date: a negative offset moves backwards (got '2026-05-25', want '2026-05-25')
  PASS  date: it crosses a month end correctly (got '2026-02-01', want '2026-02-01')
  PASS  date: and a leap day (got '2028-02-29', want '2028-02-29')
  PASS  date: and a year end (got '2027-01-04', want '2027-01-04')
  PASS  between: forwards is positive (got 14, want 14)
  PASS  between: backwards is negative (got -14, want -14)
  PASS  between: the same day is zero (got 0, want 0)
  PASS  label: the first morning reads as day 1 (got 'day 1', want 'day 1')
  PASS  label: an offset of four reads as day 5 (got 'day 5', want 'day 5')
  PASS  label: before they arrive, it counts down (got '7 days before', want '7 days before')
  PASS  label: and the singular is right (got '1 day before', want '1 day before')
  PASS  phase: negative is before they arrive (got 'before', want 'before')
  PASS  phase: zero is day one (got 'day-one', want 'day-one')
  PASS  phase: the first week ends at day 7 (got 'week-one', want 'week-one')
  PASS  phase: the first month ends at day 30 (got 'month-one', want 'month-one')
  PASS  phase: and everything after is the ninety (got 'ninety', want 'ninety')
  PASS  clean: an offset out of range is clamped, not accepted (got 365, want 365)
  PASS  clean: and on the other side (got -60, want -60)
  PASS  clean: nonsense reads as day one, not as a crash (got 0, want 0)

== a starter's plan is a snapshot of the checklist ==
  PASS  happy: a checklist stores its tasks as a PATTERN (starter_id NULL) (got 5, want 5)
  PASS  happy: adding a starter COPIES the tasks on to them (got 5, want 5)
  PASS  happy: and the pattern is untouched (got 5, want 5)
  PASS  immutable: rewriting the checklist does NOT change a plan already in flight (got 5, want 5)
  PASS  immutable: and the live plan still has its own task titles (got 1, want 1)
  PASS  immutable: while the NEXT starter gets the new one (got 1, want 1)

== applying a checklist is additive, not duplicating ==
  PASS  idempotent: re-applying the same checklist adds only what is missing (got 0, want 0)
  PASS  idempotent: so the plan is unchanged (got 5, want 5)
  PASS  happy: a SECOND checklist adds to the plan rather than replacing it (got 1, want 1)
  PASS  happy: so the plan now carries both (got 6, want 6)

== the plan reads its own dates ==
  PASS  happy: the plan knows how many tasks it has (got 6, want 6)
  PASS  happy: nothing is done yet (got 0, want 0)
  PASS  happy: it is 10 days before they start (got -10, want -10)
  PASS  happy: so the status derives to planned (got 'planned', want 'planned')
  PASS  happy: a task carries a due date derived from the start date
  PASS  happy: start + (-7) is right (got '2026-06-18', want '2026-06-18')
  PASS  happy: it is not late — it is not due for another three days
  PASS  happy: and the timeline knows which band it belongs in (got 'before', want 'before')
  PASS  derived: moving the start date moves every due date with it, with nothing stored to migrate (got '2026-06-28', want '2026-06-28')
  PASS  derived: there is NO due_date or is_late column to go stale

== late is derived too ==
  PASS  happy: five days in, the status derives to active (got 'active', want 'active')
  PASS  happy: everything due before today and not done is late (got 3, want 3)
  PASS  happy: and the plan says so (got 3, want 3)
  PASS  happy: the chase list finds them across everybody
  PASS  happy: worst first — the longest outstanding is at the top
  PASS  refusal: a task that is not due yet is not on the chase list

== ticking a task ==
  PASS  happy: ticking a task reports that it changed
  PASS  happy: it records when, and who
  PASS  happy: the plan counts it (got 1, want 1)
  PASS  happy: and it stops being late
  PASS  idempotent: ticking it again still reports success (two people, one checklist)
  PASS  idempotent: and does NOT move the time it was done (got '2026-09-25 00:19:50', want '2026-09-25 00:19:50')
  PASS  idempotent: nor the name of who did it first (got 'Marcus', want 'Marcus')
  PASS  idempotent: and the count does not double (got 1, want 1)
  PASS  happy: un-ticking works
  PASS  happy: and clears both the time and the name
  PASS  refusal: a task id that does not exist is refused, not created
  PASS  refusal: a TEMPLATE task cannot be ticked — a pattern is not somebody's work

== reassigning a task (the hero interaction) ==
  PASS  happy: reassigning reports that it changed
  PASS  happy: and names both ends, so the audit line means something later (got 'IT -> HR', want 'IT -> HR')
  PASS  happy: the row really moved (got 'hr', want 'hr')
  PASS  idempotent: reassigning to where it already is changes nothing and says so
  PASS  idempotent: and still reports where it is (got 'HR', want 'HR')
  PASS  happy: it can go to a named person
  PASS  happy: with their address kept for the reminder (got 'ros@test.co', want 'ros@test.co')
  PASS  refusal: a named owner with no name is refused — "who has this?" must stay answerable
  PASS  refusal: a malformed address is refused
  PASS  refusal: an owner kind the product does not have falls back to the manager, never to nothing
  PASS  refusal: a task that does not exist is refused
  PASS  refusal: a TEMPLATE task cannot be reassigned — a pattern has a default, not an owner
  PASS  permission: moving to a non-named owner clears the stale name and address behind it

== who a reminder goes to ==
  PASS  owner: a manager task goes to the manager (got 'marcus@test.co', want 'marcus@test.co')
  PASS  owner: an HR task goes to the HR address in settings (got 'hr@test.co', want 'hr@test.co')
  PASS  owner: an IT task goes to the IT address (got 'it@test.co', want 'it@test.co')
  PASS  owner: a named task goes to the address on the task (got 'ros@test.co', want 'ros@test.co')
  PASS  refusal: a buddy has nowhere to go — the product does not invent one (got '', want '')
  PASS  refusal: a malformed stored address is not used (got '', want '')
  PASS  owner: a task owned by THE STARTER goes to their personal address — their work mailbox does not exist until IT has done its own task (got 'ade.personal@example.com', want 'ade.personal@example.com')
  PASS  owner: with no personal address it falls back to the work one (got 'ade@test.co', want 'ade@test.co')

== status is derived, except the one decision a person made ==
  PASS  happy: every task done means complete — not "after 90 days" (got 'complete', want 'complete')
  PASS  happy: somebody who finished early IS finished (got 'complete', want 'complete')
  PASS  happy: cancelled is the ONE status a person decides, so it wins over the derivation (got 'cancelled', want 'cancelled')
  PASS  happy: a starter with no tasks at all is not "complete" by accident (got 'active', want 'active')

== adding a starter refuses what it should ==
  PASS  refusal: a blank name is refused
  PASS  refusal: a start date that is not a date is refused
  PASS  refusal: an impossible date is refused
  PASS  refusal: a malformed email is refused
  PASS  refusal: a malformed MANAGER email is refused too
  PASS  refusal: a checklist that does not exist is refused rather than silently ignored
  PASS  refusal: an ARRAY where a name belongs is refused, never cast to the string "Array"
  PASS  happy: a starter with NO checklist is allowed — most people write one afterwards
  PASS  refusal: a checklist with no name is refused

== documents and equipment ==
  PASS  happy: marking a document returned works
  PASS  idempotent: marking it again reports success
  PASS  idempotent: and does not move the time it came back (got '2026-09-25 00:19:52', want '2026-09-25 00:19:52')
  PASS  refusal: a document that does not exist is refused
  PASS  refusal: something can only come back if it went out
  PASS  happy: issuing it works
  PASS  happy: and now it can be returned
  PASS  happy: both facts are kept separately — issued AND returned
  PASS  happy: un-issuing also clears the return — it cannot have come back if it never went

== the board is one query everybody reads ==
  PASS  happy: the board has every non-cancelled starter
  PASS  happy: with counts and a percentage on each row
  PASS  happy: a cancelled starter is off the board
  PASS  happy: and their tasks are off the chase list too

== guards the runtime cannot prove ==
  PASS  guard: every App:: method the controllers call actually exists
  PASS  guard: every routed handler is defined
  PASS  guard: every POST route that mutates is CSRF-checked and permission-gated
  PASS  guard: the audit trail records only REAL changes, never a no-op tick
  PASS  guard: every read-only MCP tool declares readOnly, so a read key can actually see it
  PASS  guard: the MCP endpoint applies the key scope, and GET /mcp answers 405 before the key check
  PASS  guard: there is NO tool or endpoint that CREATES a starter
  PASS  guard: no serializer leaks the address somebody applied with
  PASS  guard: and a backup redacts it, along with every credential
  PASS  guard: outbound mail is gated on demo mode BEFORE any credential is read
  PASS  guard: mail needs BOTH the master switch and the event flag, and both default to 0
  PASS  guard: a mail failure can never break the thing that triggered it
  PASS  guard: every mail attempt, refused or not, writes an outbox row
  PASS  guard: a task is reminded ONCE — the stamp is written only on a successful send
  PASS  guard: the reminder cron refuses to answer over HTTP
  PASS  guard: the cron sends through the SAME function the app's button uses — no second message
  PASS  guard: the cron loads BOTH controllers, so it cannot fatal on its first real pass
  PASS  guard: overlapping cron runs are refused by an atomic mkdir, not a lock file
  PASS  guard: erasing somebody is refused while their onboarding is still running
  PASS  guard: no standing statement is written as a non-error .main > .alert — the kit turns those into a five-second toast and the message evaporates
  PASS  guard: no page reaches an external host — fonts and scripts are bundled
  PASS  guard: the API key scope is enforced at the door, once, not per route
  PASS  guard: api_keys.scope is added by migrate() with DEFAULT full (an upgrade changes nothing)
  PASS  guard: the one write on the REST surface also consults the ROLE, not just the key
  PASS  guard: a viewer sees the board and changes nothing; a member works the list
  PASS  webhooks: every door that ticks a task (browser, REST, agent) fires through the one helper
  PASS  webhooks: the helper really delivers task.completed and task.reopened (logged per endpoint)
  PASS  dashboard: a zero change vs the quarter before carries no arrow
  PASS  guard: the shipped version string is one value, everywhere it is claimed
  PASS  guard: no OTHER product's name survives anywhere in the tree
  PASS  guard: the shared core modules were not forked for this product
  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
  PASS  demo ribbon: every path renders the card with the admin wording (no public page)
  PASS  demo ribbon: below 600px it starts as the small pill; a tap opens it and the choice holds for the session
  PASS  demo ribbon: a response with no </body> (CSV, JSON, PDF) passes through untouched
  PASS  forms: every POST form on a screen reaches a registered route (37 checked)
  PASS  webhooks: ticking tasks in bulk sends task.completed for each task that moved, and for no other
  PASS  webhooks: the bulk handler goes through the helper that fires
  PASS  backups: every non-admin role gets 403 on every backup, restore and backup-token route
  PASS  backup JSON: an administrator gets a backup, with every credential column planted (10)
  PASS  backup JSON: the download carries none of them
  PASS  backup JSON: the scheduled backup carries none of them
  PASS  backup JSON: the download and the scheduled writer read the one redaction list
  PASS  security page: the backup card says every credential is redacted, not only the SMTP password

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

← Back to Onboardora · 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 →