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)