Home›Cyresora›Test run
Cyresora · Test run · run 2026-11-07

Cyresora Test run, as shipped in the download

409 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. Schema install + migrate (SQLite in-memory) ==
  PASS  schemaExists() after install
  PASS  table users exists
  PASS  table settings exists
  PASS  table assets exists
  PASS  table vulnerabilities exists
  PASS  table incidents exists
  PASS  table notifications exists
  PASS  table contacts exists
  PASS  table evidence exists
  PASS  table api_keys exists
  PASS  table webhooks exists
  PASS  table invites exists
  PASS  table audit_log exists
  PASS  install() + migrate() are idempotent
  PASS  api_keys.scope exists and defaults to full

== 2. The instruments, and when they start applying ==
  PASS  CRA Article 14 applies from 11 September 2026
  PASS  NIS2 transposition deadline
  PASS  five regimes are modeled (GDPR and DORA since 1.1.0)
  PASS  CRA vulnerability track cites Article 14
  PASS  NIS2 cites Article 23
  PASS  eight distinct anchors exist: the CRA and NIS2 four, and GDPR's and DORA's own (1.1.0)
  PASS  ...and they are the ones the instruments name
  PASS  a CRA VULNERABILITY final report is 14 days...
  PASS  ...counted from the corrective measure being available
  PASS  a CRA SEVERE INCIDENT final report is 1 month...
  PASS  ...counted from the submission of the notification
  PASS  the two CRA tracks therefore differ in BOTH duration and anchor
  PASS  NIS2 early warning is 24h from awareness
  PASS  NIS2 notification is 72h from awareness
  PASS  NIS2 final report is 1 month from the SUBMISSION
  PASS  NIS2 has an on-request intermediate report
  PASS  ...and a post-handling final report

== 3. 24 and 72 hours are ABSOLUTE durations, computed on UTC ==
  PASS  24 hours
  PASS  72 hours
  PASS  across a month boundary
  PASS  across a year boundary
  PASS  DST cannot touch a 24-hour deadline (EU clocks go back 2026-10-25)
  PASS  ...nor a 72-hour one
  PASS  hoursBetween is signed
  PASS  ...and negative going backwards

== 4. `+1 month` from 31 January — the bug that makes a deadline LATER than the law ==
  PASS  PHP overflows 31 Jan + 1 month (this is the hazard)
  PASS  Clock clamps it to the last day of February
  PASS  ...and to the 29th in a leap year
  PASS  31 March + 1 month is 30 April
  PASS  31 May + 1 month is 30 June
  PASS  a date that exists in both months is untouched
  PASS  December rolls the year
  PASS  30 January is fine, 28 February
  PASS  one month across a DST change keeps the local wall-clock time
  PASS  ...which is 00:00 local on both sides of the change
  PASS  14 days is a calendar span in the business timezone

== 5. There is no working-day carve-out, and the register says so ==
  PASS  Friday 18:00 + 24h lands on Saturday
  PASS  ...and the register flags it as a weekend deadline
  PASS  Friday + 72h lands on Monday and is not flagged
  PASS  a Sunday is a weekend
  PASS  a Wednesday is not
  PASS  the weekend test is applied in the BUSINESS timezone, not UTC

== 6. The ladder: states, and the deadline that has not started ==
  PASS  the CRA vulnerability ladder has three stages
  PASS  early warning is due 24h after awareness
  PASS  ...and is still running at hour 10
  PASS  notification is due 72h after awareness
  PASS  THE FINAL REPORT HAS NO DUE DATE YET
  PASS  ...because its state is pending, not overdue
  PASS  ...and the register names what starts it
  PASS  once a measure is available, the 14 days can be computed
  PASS  ...and the clock is running
  PASS  at hour 30 the 24-hour early warning is overdue
  PASS  ...by six hours, as a negative number
  PASS  the 72-hour notification is still open
  PASS  four hours out, the early warning is due_soon
  PASS  filed before the deadline is submitted
  PASS  filed after it is submitted_late — recorded, not quietly forgiven

== 7. The one-month clocks run from the submission instant ==
  PASS  the notification was submitted at hour 40
  PASS  so the final report is due one month from THAT, not from the 72-hour deadline
  PASS  ...which is a different, EARLIER date than anchoring to the deadline would give
  PASS  ...and the difference is exactly the 32 hours they filed early

== 8. NIS2 Article 23(4)(c) only exists when the authority asks ==
  PASS  unrequested, the intermediate report has no state to act on
  PASS  ...and no due date
  PASS  once requested, it is on the board
  PASS  the post-handling final report waits for the handling to complete
  PASS  worst() surfaces the overdue stage
  PASS  worst() prefers due_soon over open
  PASS  everything filed reads as submitted
  PASS  tones are the kit's own vocabulary
  PASS  a late filing is shown as bad, not as done
  PASS  humanLeft reads in hours under two days
  PASS  ...and in days and hours beyond that
  PASS  ...and says "ago" once it is blown
  PASS  ...and copes with nothing to say

== 9. The register, through the real schema ==
  PASS  an incident can run two regimes at once
  PASS  ...and gets a ladder for each
  PASS  an unknown regime is dropped rather than crashing
  PASS  a recorded submission anchors the one-month clock through the database too
  PASS  ...and the OTHER regime on the same incident is unaffected by it
  PASS  filing the same stage twice is refused by the database
  PASS  ...but the same stage under a DIFFERENT regime is fine

== 10. What the register refuses to record ==
  PASS  an incident needs a title
  PASS  ...and a moment of awareness
  PASS  a DATE with no time is refused, because a 24-hour deadline cannot be computed from one
  PASS  an incident with no regime has no clock, so it is refused
  PASS  an unknown regime is refused
  PASS  a good incident passes
  PASS  a local time is converted to UTC for storage
  PASS  ...and back again for the form
  PASS  a UTC install is a no-op

== 11. Roles — working an incident is not declaring that a report went out ==
  PASS  viewer may NOT incident.edit
  PASS  viewer may NOT notify.submit
  PASS  member may incident.edit
  PASS  member may vuln.edit
  PASS  member may evidence.attach
  PASS  member may NOT notify.submit
  PASS  member may NOT settings.edit
  PASS  responder may notify.submit
  PASS  responder may contact.edit
  PASS  responder may NOT settings.edit
  PASS  admin may notify.submit
  PASS  admin may settings.edit

== 12. The documents an authority is handed ==
  PASS  a formula is neutralized
  PASS  a plain negative number stays a number
  PASS  ordinary text is untouched
  PASS  the incident report is a real PDF
  PASS  ...whose streams inflate to readable text
  PASS  ...with the key/value block populated (Pdf::kv takes PAIRS, not an associative array)
  PASS  ...naming the incident
  PASS  ...carrying both regime ladders
  PASS  ...stating that the times are UTC
  PASS  ...and warning that the clocks do not pause at a weekend
  PASS  ...and that NIS2 is a Directive whose deadlines are national

== 13. Source-level regressions ==
  PASS  every route is registered through App::get/App::post (112 routes)
  PASS  no route is registered by a bare function call
  PASS  every routed handler exists
  PASS  every static and Pdf method called exists
  PASS  no handler reaches for a non-existent App::param()
  PASS  Clock never reads the system time (comments stripped before scanning)
  PASS  the calendar-month clamp is present and commented
  PASS  the application dates are pinned, not computed
  PASS  no controller accepts a due date as input
  PASS  every ladder is built by Clock::ladder()
  PASS  Api::requireKey selects the key scope
  PASS  ...and refuses a write on a read-only key at the door
  PASS  Api::mintKey persists the chosen scope
  PASS  the MCP endpoint hands the scope to the core
  PASS  all six MCP tools are declared
  PASS  1.1.0 adds three more, all read-only: draft_report, list_risks, list_documents
  PASS  ...and every one declares a handler the core can call
  PASS  no tool uses a key the core does not read
  PASS  every read-only MCP tool declares readOnly => true
  PASS  the write tools are role-gated
  PASS  a created resource returns 201 through Api::json
  PASS  uploads are sniffed by magic bytes
  PASS  the stored filename is minted
  PASS  the serve path is containment-checked AND re-sniffed
  PASS  nosniff is set on served evidence
  PASS  backup redaction covers password_hash
  PASS  backup redaction covers totp_secret
  PASS  backup redaction covers totp_recovery
  PASS  backup redaction covers token_hash
  PASS  backup redaction covers secret
  PASS  backup redaction covers smtp_pass
  PASS  backup redaction covers backup_token_hash
  PASS  backup redaction covers oidc_client_secret
  PASS  one redaction list, used by both backup doors
  PASS  no view loads anything from an external host
  PASS  no view pulls a font or script from a CDN
  PASS  permanent info alerts are nested so the kit cannot toast them away: contacts.php
  PASS  permanent info alerts are nested so the kit cannot toast them away: dashboard.php
  PASS  permanent info alerts are nested so the kit cannot toast them away: incident_draft.php
  PASS  permanent info alerts are nested so the kit cannot toast them away: layout.php
  PASS  the service worker cache is versioned for this release
  PASS  ...and never caches a page navigation
  PASS  the manifest names this product
  PASS  the layout loads the kit and groups the nav
  PASS  the kit itself is untouched
  PASS  the session cookie is named for THIS product
  PASS  no build-environment trace: jay...
  PASS  no build-environment trace: aero...
  PASS  no build-environment trace: eir...
  PASS  no build-environment trace: clean...
  PASS  no build-environment trace: mission...
  PASS  no trace of a spine product: aiac...
  PASS  no trace of a spine product: compl...
  PASS  no trace of a spine product: compl...

== 14. The demo board is alive, and shows every state at once ==
  PASS  1.1.0 demo: five risks across the Article 21(2) areas, and three documents with five versions kept, as real PDFs
  PASS  1.1.0 demo: the incident response plan is past its review, and the continuity plan has a version waiting to take effect
  PASS  five incidents (the fifth, since 1.1.0, a personal-data breach on the GDPR clock)
  PASS  ...its GDPR clock runs from the later breach awareness, 48 hours left
  PASS  four products and systems
  PASS  a notification rota with a deputy
  PASS  the board shows an OVERDUE deadline
  PASS  ...a clock still running
  PASS  ...something already submitted
  PASS  ...something submitted LATE
  PASS  ...and a deadline that has NOT STARTED because no corrective measure exists yet
  PASS  the dashboard counts at least one overdue
  PASS  ...and at least one exploited vulnerability with no fix
  PASS  no seeded incident is in the future
  PASS  no seeded notification is in the future
  PASS  no seeded notification precedes its own incident
  PASS  reset clears the register but not the login
  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

== F2. The warning setting, and deadline alerts to the rota ==
  PASS  warn hours: a 24 h setting marks an early warning 20 h out as due soon
  PASS  warn hours: the default 6 h leaves it running
  PASS  warn hours: the saved setting reaches the board (24 h → due soon, 6 h → running) — it was saved and never read
  PASS  alert points: parsed descending, de-duplicated, nonsense dropped
  PASS  alert points: empty stays empty
  PASS  alert rung: 13 h out reaches no point
  PASS  alert rung: 11 h out is the 12 h point
  PASS  alert rung: 3 h out is the TIGHTEST point reached (4), not every point
  PASS  alert rung: under an hour is the 1 h point
  PASS  alert rung: just overdue is point 0
  PASS  alert rung: overdue for days alerts no more (switching alerts on does not mail old history)
  PASS  alert rung: a submitted stage never alerts
  PASS  alert rung: a stage whose clock has not started never alerts
  PASS  alert recipients: the regime's notifier and their deputy, nobody else
  PASS  alerts: switched off (the default), nothing is sent
  PASS  alerts: on, a clock 3 h from its deadline fires ONE alert at the 4 h point, to 2 recipients
  PASS  alerts: with no mail server the alert is recorded as no-smtp, never as sent
  PASS  alerts: a second run (cron after a page load) sends nothing — one latch
  PASS  alerts: the Clocks screen is told alerts are on but cannot be delivered
  PASS  alerts: a deadline that has just passed alerts once more (point 0), through the owner's SMTP
  PASS  alerts: a failed send keeps its reason for the Clocks screen
  PASS  alerts: a stage overdue for weeks does not suddenly alert
  PASS  alerts: the email names the stage, the incident, the deadline and says Cyresora files nothing
  PASS  alerts: the email carries no credential or SMTP detail
  PASS  alerts: deadline.alert is a webhook event an endpoint can subscribe to
  PASS  alerts: a cron route runs them, guarded by the backup token
  PASS  alerts: the Clocks screen runs the watcher and shows its health
  PASS  webhooks: notification.recorded fires from the REST route and the agent tool too, after the write
  PASS  webhooks: incident.opened fires from the agent tool too (open_incident), after the write
  PASS  settings: the mail-server copy no longer claims invitations are emailed
  PASS  dashboard: a zero "Due within hours" tile no longer says "act now"
  PASS  backup guard: the harness made one signed-in account per role (viewer,member,responder,admin)
  PASS  backup guard: GET /backup.json — 403 for every non-admin role, open to admin
  PASS  backup guard: GET /backup.sqlite — 403 for every non-admin role, open to admin
  PASS  backup guard: GET /restore — 403 for every non-admin role, open to admin
  PASS  backup guard: POST /restore/dry-run — 403 for every non-admin role, open to admin
  PASS  backup guard: POST /restore/commit — 403 for every non-admin role, open to admin
  PASS  backup guard: POST /restore/token — 403 for every non-admin role, open to admin
  PASS  backup guard: GET /backup/scheduled without its token refuses every signed-in role, admin included
  PASS  backup guard: GET /alerts/run without its token refuses every signed-in role, admin included
  PASS  webhooks: "Add" reaches its route and saves the endpoint (was a 404)
  PASS  webhooks: "Pause" reaches its route and flips the endpoint
  PASS  webhooks: "Delete" reaches its route and removes the endpoint
  PASS  demo card: no page here is public, so every page keeps "signed in as the admin"
  PASS  demo card: below 600px it starts as the small "Own it" pill; a tap opens it and the choice holds for the session
  PASS  demo card: boot() hooks it only behind the .demo-mode marker, and a page with no </body> passes through untouched
  PASS  backup redaction: a canary sits in every credential column of the schema (api_keys.token_hash, backup_state.s3_secret, invites.token_hash, settings.smtp_pass, settings.backup_token_hash, settings.oidc_client_secret, updater_state.license_api_key, users.password_hash, users.totp_secret, users.totp_recovery, webhooks.secret)
  PASS  backup redaction: each arrives as [REDACTED] and no canary is anywhere in the JSON backup

== 1.1.0 · GDPR Article 33 and DORA on the same incident, each with its own anchors ==
  PASS  GDPR and DORA are modeled, with the dates they apply from
  PASS  GDPR: one stage, the notification to the supervisory authority, 72 h after awareness of the breach
  PASS  GDPR: a later awareness of the personal data breach moves only the GDPR clock
  PASS  DORA: not classified as major yet — the initial notification has no invented date
  PASS  DORA: classified at hour 2 — 4 h from the classification
  PASS  DORA: classified at hour 22 — capped at 24 h from awareness (Article 5(1))
  PASS  DORA: classified at hour 30 — 4 h from the classification (Article 5(2))
  PASS  DORA: the intermediate report runs 72 h from the SUBMITTED initial notification
  PASS  ...and the final report waits for the intermediate one
  PASS  DORA: the final report is one calendar month after the intermediate report
  PASS  DORA: the note says the weekend extension of Article 5(4) is not applied
  PASS  the form's local times are stored as UTC (Berlin, summer time)
  PASS  one incident, three regimes, three ladders
  PASS  ...GDPR from the breach awareness, DORA from the classification, NIS2 from the incident awareness
  PASS  a recorded DORA initial notification starts the 72-hour intermediate clock through the database
  PASS  cleared: GDPR falls back to the incident awareness; the DORA initial notification stays submitted
  PASS  the API record names both new anchors and carries the GDPR and DORA ladders
  PASS  the form refuses a classification before awareness
  PASS  ...and a date without a time
  PASS  the authority pre-filled per regime family

== 1.1.0 · what to send, drafted: per-stage drafts from the incident record ==
  PASS  NIS2 early warning draft: the two things Article 23(4)(a) asks, answered from the record
  PASS  NIS2 notification draft: updates the early warning, severity, and names the indicators of compromise as a gap
  PASS  GDPR draft: the four points of Article 33(3), with the counts and consequences marked to complete
  PASS  ...and once the 72 hours have passed, it asks for the reasons for the delay (Article 33(1))
  PASS  CRA vulnerability final report draft: the vulnerability, the malicious-actor gap, and the remedy from the register
  PASS  CRA early warning draft: product with its version, and the Member States as a gap
  PASS  DORA draft: says the template of Implementing Regulation (EU) 2025/302 decides, and asks for the classification time
  PASS  a field the record holds is filled, not marked: adding the root cause removes that gap
  PASS  no draft for a regime the incident does not run, or a stage that does not exist
  PASS  router setup: an admin, a viewer, a responder and a member; an incident on NIS2 and GDPR
  PASS  the incident page links a draft for each stage still to file
  PASS  a viewer reads the draft, read-only, with no way to record it as sent
  PASS  a responder gets the draft in the recording form, the authority pre-filled
  PASS  an unknown stage is a 404
  PASS  recording from the draft keeps the text as sent, as the record of the notification
  PASS  ...after which the draft page says it is already recorded, and offers no second record
  PASS  API: a read key gets the GDPR draft with its gaps listed
  PASS  API: a regime the incident does not run is a 422

== 1.1.0 · the risk register ==
  PASS  score is likelihood × impact on the 1-5 scales; out of scale is 0
  PASS  bands: 15+ high, 8+ medium, 1+ low
  PASS  ten Article 21(2) areas, (a) to (j)
  PASS  a risk needs a name
  PASS  ...and both scales in range
  PASS  ...a known area
  PASS  ...and a review date that is a date
  PASS  summary: the grid counts open risks only; areas with none are listed; closed risks raise no review or lateness
  PASS  a viewer reads the register
  PASS  ...with no way to add or edit
  PASS  a viewer cannot add a risk
  PASS  a member adds a risk; it is audited
  PASS  an out-of-scale likelihood is refused in the form, nothing stored
  PASS  an edit records the score and status change in the audit trail
  PASS  the register shows the score, the residual, and the areas with none recorded
  PASS  the CSV carries the register and defuses a formula
  PASS  API /api/risks: highest score first, with the uncovered areas
  PASS  removing a risk is audited

== 1.1.0 · the document hub: versions kept, the file decides its type, reviews dated ==
  PASS  the bytes decide: PDF, Word, OpenDocument, Markdown
  PASS  ...and refuse an executable named .pdf, a spreadsheet ZIP, and binary named .txt
  PASS  versions are stored with the SHA-256 of the file kept
  PASS  a second version 2.0, a date that is not a date, and a fake PDF are refused
  PASS  the version in force is the latest effective one; review 12 months after it: within 30 days
  PASS  ...on an earlier day, the earlier version was in force and 2.0 was upcoming
  PASS  ...review overdue once the date has passed; none when the interval is 0
  PASS  a document whose only version is in the future has nothing in force
  PASS  a viewer sees the hub, the version in force, and no upload form
  PASS  the document page lists the version with its approver and hash
  PASS  the version downloads byte for byte
  PASS  an upload without a file says so
  PASS  a member edits the details; audited
  PASS  a member cannot remove a document and its history
  PASS  API /api/documents: current version with its hash and the review state
  PASS  an admin removes it: rows and files gone, audited
  PASS  the document checks leave nothing behind in data/uploads

== 1.1.0 · assets from Microsoft Entra ID (Azure AD) and other directory exports ==
  PASS  dry run on an Entra "Download devices" CSV: 2 to add, a nameless row and a repeated deviceId skipped, nothing written
  PASS  the import lands each device as an internal system: OS and model, version, owner, the device id kept, scope unset
  PASS  next month's export updates the same rows (no duplicates) and never touches the scope you marked
  PASS  a missing name column is named, with the columns the file does have
  PASS  another export, semicolon-separated, imports through the column names you type
  PASS  router: the import page offers the Entra ID export
  PASS  router: no file, a clear refusal
  PASS  router: a viewer cannot import
  PASS  the router checks leave no data/imports folder behind
  PASS  the demo reset removes the stored demo documents
  PASS  import dry run: a row with an owner email that is not an address is counted as skipped, not ready, and nothing is written (ready 1, skipped 1)

== 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)
  PASS  updates: the updater table holds exactly one state row
  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 Cyresora 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 (5 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 Cyresora 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 Cyresora, 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 4 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 (25 tables dumped), outside the 1 column(s) kept by ruling (possible legal records)
  PASS  erase planting · sqlite: the alert log keeps the colleague who was also told
  PASS  erase planting · sqlite: a responder cannot erase: HTTP 403, nothing changed
  PASS  erase planting · sqlite: a responder cannot open the Data requests page: HTTP 403
  PASS  erase planting · sqlite: an admin is refused while the person is on the alert rota for an open incident, and nothing changes
  PASS  erase planting · sqlite: an admin opens the Data requests page and the person is listed
  PASS  erase planting · sqlite: the erase page (/gdpr) says what an erase keeps: "Kept: the audit trail's lines that this person was added to …"
  PASS  erase planting · sqlite: MANUAL.md says the same: "Kept: the audit trail's lines that this person was added to …"

====================================================
ALL 409 TESTS PASSED (0 failed)

← Back to Cyresora · Manual · Quickstart · API