Deliora · Test run · run 2026-11-05
Deliora Test run, as shipped in the download
1,831 checks passed, 0 failed, when the suite that ships in the zip was run on 2026-11-05 with php tests/run.php. Run it yourself after you download.
== 0. launch-mode flags ship OFF on a fresh install ==
PASS fresh install: demos_live defaults to 0 (got 0, want 0)
PASS fresh install: sales_live defaults to 0 (got 0, want 0)
== 1. helpers ==
PASS e() escapes for view context (got '<b>"x"</b>', want '<b>"x"</b>')
PASS clip caps to width (got 190, want 190)
PASS clip passes short values (got 'abc', want 'abc')
PASS clamp_int caps range (got 2000000000, want 2000000000)
PASS clamp_int floor (got -2000000000, want -2000000000)
PASS clamp_int non-numeric -> 0 (got 0, want 0)
PASS fmt_money USD (got '$49.00', want '$49.00')
PASS fmt_money unknown code suffixes (got '49.00 SEK', want '49.00 SEK')
PASS now_utc format
== 2. safe markdown renderer (escape-first) ==
PASS heading rendered as h2
PASS subheading rendered as h3
PASS bold rendered
PASS list rendered
PASS paragraph rendered
PASS script tag escaped
PASS img/onerror escaped
PASS raw HTML never passes through bold
PASS heading content escaped
PASS list content escaped
PASS quote attribute breakout escaped
PASS table rendered with thead
PASS table body cell inline-rendered
PASS separator row not rendered as cells
PASS blockquote rendered
PASS ordered list rendered
PASS hr rendered
PASS code span rendered
PASS italic rendered
PASS table cell XSS escaped
PASS code span in table escaped
PASS blockquote XSS escaped
PASS ordered-list XSS escaped
PASS code span shields markdown and html
PASS bold not applied inside code
PASS legacy constructs unchanged
PASS line breaks: a wrapped paragraph line is a space, not a <br> (got '<p>reaching for when they type the phrase.</p>', want '<p>reaching for when they type the phrase.</p>')
PASS line breaks: two trailing spaces or a backslash still break the line (got '<p>one<br>two<br>three</p>', want '<p>one<br>two<br>three</p>')
PASS line breaks: a wrapped bullet continues its item instead of leaving the list (got '<ul><li>first item that wraps here</li><li>second</li></ul>
<ol><li>one that wraps</li></ol>', want '<ul><li>first item that wraps here</li><li>second</li></ul>
<ol><li>one that wraps</li></ol>')
PASS line breaks: blockquote lines stay one per line (a quote, then its source) (got '<blockquote><p>the quote<br>— the source</p></blockquote>', want '<blockquote><p>the quote<br>— the source</p></blockquote>')
PASS line breaks: an indented block after a blank line is code, escaped, one line each (got '<p>Headers:</p>
<pre><code>X-Event: a.b
Sig: <hex></code></pre>
<p>After.</p>', want '<p>Headers:</p>
<pre><code>X-Event: a.b
Sig: <hex></code></pre>
<p>After.</p>')
PASS line breaks: an indented line inside a paragraph is only a continuation (got '<p>a paragraph continued</p>', want '<p>a paragraph continued</p>')
== 3. Payments demo contract (crypto module boundary) ==
PASS demo fixture parses
PASS type normalized (got 'paid', want 'paid')
PASS order_ref kept (got 'ord_123', want 'ord_123')
PASS buyer email lowercased (got 'buyer@x.com', want 'buyer@x.com')
PASS currency uppercased (got 'USD', want 'USD')
PASS amount_cents int (got 4900, want 4900)
PASS items mapped (got array (
0 =>
array (
'sku' => 'invoicer-pro',
'tier' => 'single',
),
), want array (
0 =>
array (
'sku' => 'invoicer-pro',
'tier' => 'single',
),
))
PASS refunded event parses without items (got 'refunded', want 'refunded')
PASS unknown type -> other (got 'other', want 'other')
PASS garbage body -> null
PASS missing order_ref -> null
PASS bad email -> null
PASS paid with no items -> null
PASS provider mismatch -> null (demo body to lemonsqueezy endpoint)
PASS LS: forged signature -> null
PASS Stripe: bogus/stale signature -> null
PASS unknown provider -> null
PASS LS: valid signature -> normalized paid event
PASS LS: tampered signature -> null
PASS LS: wrong secret -> null
PASS LS: UUID identifier preferred over numeric data.id as order_ref
PASS LS: empty identifier falls back to data.id (refund keeps matching old rows)
PASS LS O13: a PAID order with no identifier never takes the sequential data.id as its ref
PASS LS O13: that derived ref is deterministic, so an LS resend stays idempotent
PASS Stripe: valid signature -> normalized paid event
PASS Stripe: stale timestamp -> null
PASS Stripe: tampered payload -> null
PASS module HAS real HMAC verification (not a stub)
== 4. Licensing (real HMAC, DB-bound tamper-evidence) ==
PASS sign() returns a real OWX key (not DEMO), fits 80 chars
PASS sign -> insert -> verify returns the row grant
PASS verify with WRONG secret -> null
PASS verify detects a tampered DB grant -> null
PASS verify unissued real-format key -> null
PASS verify demo-format key (never issued) -> null
PASS verify garbage -> null
PASS verify with no secret -> null
PASS 200 keys for identical claims are unique (random nonce) (got 200, want 200)
PASS module HAS real HMAC signing (not a stub)
== 5. FULL webhook → fulfillment loop ==
PASS W16-6: the two download fixtures are shutdown-guarded, not only cleaned inline
PASS W16-6: data/files carries no orphaned test_*.zip from an earlier aborted run
PASS paid webhook -> 200 (got 200, want 200)
PASS one item fulfilled (got 1, want 1)
PASS customer upserted
PASS order created paid
PASS order amount stored (got 4900, want 4900)
PASS order provider stored (got 'demo', want 'demo')
PASS one order item (got 1, want 1)
PASS license issued (real signed key that verifies)
PASS license active (got 'active', want 'active')
PASS activation limit from settings (got 3, want 3)
PASS download token issued (48 hex)
PASS download max_uses from settings (got 5, want 5)
PASS download expiry ≈ 72h from settings
PASS fulfillment email in outbox (demo mode, not sent)
PASS email points at the Lemon Squeezy receipt for the license, and never carries a second key
PASS email contains download link
PASS DUPLICATE order_ref -> 200 (got 200, want 200)
PASS duplicate flagged (got true, want true)
PASS duplicate: NO double fulfillment (orders/items/licenses/downloads/outbox all unchanged) (got array (
0 => 1,
1 => 1,
2 => 1,
3 => 1,
4 => 1,
), want array (
0 => 1,
1 => 1,
2 => 1,
3 => 1,
4 => 1,
))
PASS multi-item order fulfills both (got 2, want 2)
PASS two licenses on multi order (got 2, want 2)
PASS two downloads on multi order (got 2, want 2)
PASS unknown sku -> 200 (got 200, want 200)
PASS unknown sku -> 0 fulfilled (got 0, want 0)
PASS unknown sku order still recorded
PASS pipeline: non-demo provider with stub -> 400 (fail closed) (got 400, want 400)
PASS pipeline: demo webhook rejected while real provider active (got 400, want 400)
PASS rejected webhook wrote NOTHING
PASS refund -> 200 (got 200, want 200)
PASS order marked refunded (got 'refunded', want 'refunded')
PASS ALL its licenses revoked (got 2, want 2)
PASS refund again idempotent -> 200 (got 200, want 200)
PASS refund of unknown ref -> 200 no-op
== 6. download tokens ==
PASS refunded order download -> 410 (got 410, want 410)
PASS valid token -> 200 (got 200, want 200)
PASS resolved path is inside data/files
PASS attachment filename derived from slug+tier+version
PASS used_count incremented (got 1, want 1)
PASS used_count capped at max_uses (atomic) (got 5, want 5)
PASS exhausted token -> 410 (got 410, want 410)
PASS expired token -> 410 (got 410, want 410)
PASS expired token consumed no use (got 0, want 0)
PASS garbage token -> 404 (got 404, want 404)
PASS well-formed unknown token -> 404 (got 404, want 404)
PASS edition without a file -> 404 (got 404, want 404)
PASS path-traversal download_file -> 404 (basename + realpath guard) (got 404, want 404)
== 7. license API ==
PASS check valid
PASS check wrong sku (got 'sku_mismatch', want 'sku_mismatch')
PASS check garbage key (got 'invalid_key', want 'invalid_key')
PASS check non-demo-format key (got 'invalid_key', want 'invalid_key')
PASS activate consumes a slot
PASS re-activate SAME domain idempotent (no slot consumed)
PASS normalized variants map to the same domain (still no slot)
PASS limit reached exactly
PASS over limit rejected
PASS same-domain still fine at limit (idempotent)
PASS domain normalization (got 'foo-bar.com', want 'foo-bar.com')
PASS invalid domain -> empty (got '', want '')
PASS activate with invalid domain (got 'invalid_domain', want 'invalid_domain')
PASS admin activation reset frees the slot (got 1, want 1)
PASS check on revoked (got 'revoked', want 'revoked')
PASS activate on revoked (got 'revoked', want 'revoked')
PASS reissue creates a NEW key
PASS old key revoked by reissue (got 'revoked', want 'revoked')
PASS old key now fails check (got 'revoked', want 'revoked')
PASS new key valid with a fresh activation budget
PASS Licensing::verify returns claims from the DB row
== 8. CSV formula-injection guard (numeric-aware) ==
PASS =-formula neutralized (got '\'=1+2', want '\'=1+2')
PASS @-formula neutralized (got '\'@SUM(A1)', want '\'@SUM(A1)')
PASS +cmd neutralized (got '\'+cmd', want '\'+cmd')
PASS +1 numeric -> KEPT (got '+1', want '+1')
PASS -text neutralized (got '\'-abc', want '\'-abc')
PASS negative number KEPT (got '-45.00', want '-45.00')
PASS leading TAB neutralized (got '\' X', want '\' X')
PASS leading CR neutralized + quoted
PASS plain value untouched (got 'Ava T', want 'Ava T')
PASS comma value quoted (got '"a,b"', want '"a,b"')
PASS quote value escaped (got '"say ""hi"""', want '"say ""hi"""')
PASS export header row (got 'name,amount', want 'name,amount')
PASS CRLF endings
PASS formula neutralized in real export
PASS negative amount preserved in export
PASS empty export -> empty string (got '', want '')
== 9. clip/clamp on write paths (MySQL strict-mode defense) ==
PASS customer email clipped to 190 (got 190, want 190)
PASS customer name clipped to 190 (got 190, want 190)
PASS webhook order_ref clipped to 190 (got 190, want 190)
PASS webhook currency clipped to 8 (got 8, want 8)
PASS webhook sku clipped to 80 (got 80, want 80)
PASS webhook amount clamped inside INT (got 2000000000, want 2000000000)
PASS 100KB description survives clip(200000) intact
== 10. buyer-portal magic links ==
PASS unknown email -> true (no enumeration)
PASS …and no token row created (got 0, want 0)
PASS buyer with orders -> true
PASS portal token created (48 hex, 1h expiry)
PASS magic link is in the outbox email
PASS consume returns the customer id
PASS token is single-use
PASS expired magic link rejected
PASS garbage magic link rejected
PASS invalid email -> true, silent
== 11. per-IP rate limiter ==
PASS 61st hit from same IP+bucket blocked
PASS different IP unaffected
PASS different bucket unaffected
== 12. Mailer message building (no socket) ==
PASS From header present
PASS To header present
PASS Subject header present
PASS MIME text/plain utf-8
PASS dot-stuffing applied
PASS CRLF stripped from To (header injection)
PASS CRLF stripped from Subject (header injection)
PASS encodeHeader UTF-8 -> RFC 2047 (got '=?UTF-8?B?Q2Fmw6k=?=', want '=?UTF-8?B?Q2Fmw6k=?=')
PASS send refuses when unconfigured (no socket)
PASS send refuses invalid recipient before any socket
PASS sendMail in demo/provider-demo mode -> outbox only, never a socket
== 13. auth + csrf ==
PASS stored hash verifies
PASS wrong password rejected
PASS empty stored csrf token can never match (bypass killed)
PASS non-matching token rejected by hash_equals
PASS matching token accepted
== 14. security hardening regressions ==
PASS csrf_check rejects empty stored token (hash_equals empty-token bypass)
PASS installer rejects empty install token (_it bypass)
PASS settings_save blocks writes in public demo mode
PASS settings view never echoes smtp_pass value
PASS settings view never echoes ls_webhook_secret value
PASS settings view never echoes stripe_webhook_secret value
PASS webhook handler NEVER echoes the raw body
PASS webhook exceptions leak no details (generic 500)
PASS download response sets nosniff + attachment
PASS image serve sets nosniff + whitelisted mime
PASS demo session cookie is None + Secure + Partitioned (CHIPS)
PASS buyer session cookie is HttpOnly + Lax + Secure-on-HTTPS
PASS session cookie named deliora
PASS dev router denies config.php / data / .sqlite
PASS .htaccess denies config.php / sqlite / demo marker
PASS .htaccess denies data/ (the deliverables live there)
PASS no error_log of credentials anywhere in mail/domain layer
PASS secrets never in Deliora logs: raw body never persisted
PASS MEDIUMTEXT used for big columns on MySQL
PASS license column avoids MySQL reserved word `key`
PASS portable UTC timestamps (no SQLite datetime() in domain/controllers)
PASS front controller catches boot errors (no stack traces)
PASS SaaS boot shim present verbatim
PASS license API rate-limited per IP
PASS portal + demo-checkout rate-limited
PASS licenseGate compares sku with hash_equals (constant time)
PASS installer generates license_secret + demo provider default
PASS CSV exports go through the guarded toCsv
== 15. schema migration v1.0 -> v1.1 ==
PASS install() left the v1.1 landing columns present
PASS install() left settings site_url + demo_base present
PASS comparisons + demo_clicks tables exist
PASS legacy shape reproduced (columns gone)
PASS migrate() restores landing columns
PASS migrate() restores settings columns + new tables
PASS old->new migration keeps every product row (got 2, want 2)
PASS old->new migration keeps row data intact (got 'Invoicer Pro', want 'Invoicer Pro')
PASS migrate() is idempotent (re-runs are no-ops, no throw)
== 16. Storefront helpers ==
PASS break-even ceil: 12900/6200 -> 3 (got 3, want 3)
PASS break-even ceil: 9900/900 -> 11 (got 11, want 11)
PASS break-even exact: 7900/7900 -> 1 (got 1, want 1)
PASS break-even guard: monthly 0 -> 0 (got 0, want 0)
PASS featuresFrom parses valid list
PASS featuresFrom garbage -> [] (got array (
), want array (
))
PASS featuresFrom wrong shape -> [] (got array (
), want array (
))
PASS featuresFrom skips titleless items (got 1, want 1)
PASS faqFrom parses valid list
PASS faqFrom garbage -> [] (got array (
), want array (
))
PASS validateFeaturesJson: empty + valid -> null
PASS validateFeaturesJson: broken JSON -> friendly error
PASS validateFeaturesJson: missing title -> item-numbered error
PASS validateFaqJson: missing answer -> friendly error
PASS sentence leaves an existing full stop alone (got 'Self-hosted invoicing.', want 'Self-hosted invoicing.')
PASS sentence adds one when missing (got 'Self-hosted invoicing.', want 'Self-hosted invoicing.')
PASS sentence respects a question mark (got 'Ready?', want 'Ready?')
PASS sentence respects an ellipsis (got 'Own it once…', want 'Own it once…')
PASS sentence leaves empty empty (got '', want '')
PASS sentence trims before deciding (got 'Tagline.', want 'Tagline.')
PASS the alternatives page never double-punctuates a tagline
PASS priceTextCurrency euro sign (got 'EUR', want 'EUR')
PASS priceTextCurrency EUR word (got 'EUR', want 'EUR')
PASS priceTextCurrency dollars (got 'USD', want 'USD')
PASS priceTextCurrency pound sign (got 'GBP', want 'GBP')
PASS priceTextCurrency GBP word (got 'GBP', want 'GBP')
PASS priceTextCurrency real Papertrail text (got 'GBP', want 'GBP')
PASS priceTextCurrency A$ is AUD (got 'AUD', want 'AUD')
PASS priceTextCurrency C$ is CAD (got 'CAD', want 'CAD')
PASS priceTextCurrency quote-gated falls back (got 'USD', want 'USD')
PASS priceTextCurrency empty falls back (got 'USD', want 'USD')
PASS a GBP price renders with a pound sign, not a dollar sign
PASS detector and renderer agree on the currency set
PASS vs-calc.js SYM covers every currency the detector returns
PASS siteBase trims trailing slash (got 'https://ownware.example', want 'https://ownware.example')
PASS canonical builds off site_url (got 'https://ownware.example/p/foo', want 'https://ownware.example/p/foo')
PASS demoTarget: product demo_url wins (got 'https://direct.example/x', want 'https://direct.example/x')
PASS demoTarget: template substitutes {slug} (got 'https://foo.demo.ownware.example', want 'https://foo.demo.ownware.example')
PASS demoTarget: non-http scheme rejected
PASS demoTarget: nothing configured -> null
PASS demo_click_sources table exists (M12 migration)
PASS demo clicks by source: bar=2, page=2 (unknown via counts as page), legacy total=4 — the total stays comparable
PASS no via row other than page/bar can exist
PASS demo click upsert: ONE row, count incremented to 2
PASS demo click: second product gets its own row (got 2, want 2)
== 17. landing page render (XSS + SEO + JSON-LD) ==
PASS landing renders (non-empty, has storefront chrome)
PASS NO raw <script>alert survives ANY injected field
PASS product name escaped in h1
PASS feature title + blurb escaped
PASS FAQ rendered as details/summary (no JS accordion)
PASS pain + limitations escaped through md_render
PASS thanks: no false email promise when SMTP unset
PASS thanks: promises the email only when SMTP configured
PASS claim: offers the email-verified recovery route
PASS claim: posts to itself with a CSRF field
PASS claim: promises no key on the page itself (email-verified only)
PASS claim: points the buyer at spam before support
PASS claim route is registered GET+POST
PASS exactly one h1 (sane heading hierarchy) (got 1, want 1)
PASS canonical link off site_url
PASS meta description + og:title + twitter card present
PASS demo CTA present (demo_base template active)
PASS vs cross-link ABSENT with no comparison rows
PASS JSON-LD block present
PASS JSON-LD decodes to a schema.org @graph
PASS graph Product node: matching Offer (49.00 USD, InStock) + raw product name
PASS FREE product JSON-LD: SoftwareApplication with $0 offer, never a bare Product node
PASS offerMerchantFields: no shippingDetails and no hasMerchantReturnPolicy on any Offer (08 #18)
PASS graph BreadcrumbList: Home › product, 2 positioned items ending at /p/xss-probe
PASS graph FAQPage: built from the REAL FAQ (question present, Answer typed), no raw <script> leaked
PASS faq source: the shared Q&A file returns categories
PASS /faq renders the Q&A (its canonical home)
PASS the help widget no longer inlines the generic answers into a page
PASS comparisons schema carries the per-competitor substance columns
PASS alternativesGrouped exposes the substance fields to the templates
PASS comparisons carries the source_dead flag
PASS UPGRADE PATH: migrate() re-adds the comparison-facts columns on an existing install
PASS UPGRADE PATH: alternativesGrouped() runs after the re-migration
PASS /faq is reachable from the site chrome, not just the widget (no orphan pages)
PASS /faq.json serves the same categories the widget renders
PASS home page now carries canonical + og:title + meta description (was missing entirely)
PASS home JSON-LD: Organization + WebSite graph, WebSite publisher-linked to the Organization
== 18. vs page render (cited rows ONLY) ==
PASS vs page renders with priced competitor (escaped) + price text
PASS vs page REFUSES the row with empty source_url
PASS vs page skips inactive rows
PASS quote-gated row renders qualitatively (name, no calculator entry)
PASS visible citation footnote: date + source link
PASS break-even line rendered with the right month count (ceil(4900/6200)=1), singular
PASS break-even never renders the "1 months" grammar defect
PASS calculator markup: data-own-cents from the edition
PASS calculator option carries the cited monthly cents
PASS calculator script included
PASS vs page links back to the landing page
PASS landing now cross-links the vs page (rows exist)
PASS vs page with zero renderable rows -> 404 page
PASS quote-gated-only vs page: NO calculator markup at all
== 18b. /alternatives — competitor-keyed inversion of /vs ==
PASS competitorSlug "When I Work" -> when-i-work (got 'when-i-work', want 'when-i-work')
PASS competitorSlug "Bill.com" -> bill-com (got 'bill-com', want 'bill-com')
PASS competitorSlug collapses/trims junk (got 'zoho-expense', want 'zoho-expense')
PASS competitorSlug all-punct -> "" (got '', want '')
PASS alternativesGrouped includes cited priced + quote-gated competitors
PASS alternativesGrouped enforces the honesty invariant (drops empty-source + inactive)
PASS alternativeBySlug(priced) -> product + break-even ceil(4900/6200)=1
PASS alternativeBySlug(quote-gated) -> no break-even (null monthly)
PASS alternativeBySlug(missing) -> null
PASS alternatives page ESCAPES the competitor name (no raw script)
PASS alternatives page renders break-even + the cited source link, singular
PASS alternatives page never renders the "1 months" grammar defect
PASS alternatives page cross-links the product + its /vs page
PASS alternatives page for an uncited competitor -> 404 (never surfaces)
PASS alternatives index lists cited, hides uncited
== 18c. /self-hosted — category & use-case landing pages ==
PASS selfhostedPages: 28 pages (>= the 14 shipped 2026-08), each fully formed (slug/label/h1/intro/slugs)
PASS selfhostedPages h1s carry the category keyword phrase
PASS productsBySlugs: active products only, with min price from editions
PASS productsBySlugs preserves caller order + drops unknowns
PASS selfhostedBySlug(unknown) -> null
PASS selfhostedPagesFor maps a product to its landing pages (pure map, no DB)
PASS /self-hosted hub renders with h1 + alternatives cross-link
PASS /self-hosted hub carries canonical + ItemList JSON-LD
PASS category page with no live products -> 404 (honesty: never an empty landing page)
PASS sitemap includes the /self-hosted hub + all 28 landing pages
== 18d. price-recheck — honest freshness (dates only, never prices) ==
PASS normalize folds tags/entities/unicode-dashes/whitespace/case
PASS quoteFragments splits the " ... " citation separator
PASS quoteStillPresent needs EVERY fragment (normalized match)
PASS recheck dry-run: 2 cited rows, 1 confirmed, 1 stale, dates untouched
PASS recheck --apply: confirmed row date bumped, stale row untouched, report stored
PASS recheck fetch-failure: rows counted, nothing flagged stale, no date moves (blocked vendor is not evidence)
PASS recheck reports a cited vendor that now answers on a different host
PASS recheck reports NO move when every citation lands where it pointed
PASS a moved citation is reported, never rewritten (the stored URL is untouched)
PASS OW3-TAIL: an inline "<" in a script no longer swallows the rest of the page
PASS OW3-TAIL: script and style content is not itself searchable (a price in a script is not on the page)
PASS OW3-TAIL: a comment is not searchable either (a withdrawn price left in the markup is not published)
PASS OW3-TAIL: unreadable() discriminates — well-formed page no, swallowed page yes, tiny page no
PASS OW3-TAIL: a page the matcher cannot read through is reported unreadable, NOT stale, and no date moves
PASS OW3-TAIL: finding the quote still confirms, even on a page that is otherwise unreadable
PASS B7b checks skipped (dist zip build - bin/quote-audit.php is a store-only tool)
== 19. sitemap + robots ==
PASS sitemap is valid XML (simplexml parses)
PASS sitemap contains home + landing + vs URLs and EXCLUDES the private /portal (GSC 2026-07-29)
PASS sitemap entries carry lastmod
PASS sitemap includes the /alternatives hub + a cited competitor page
PASS inactive product excluded from sitemap
PASS vs URL only for products WITH citable rows (+ /vs/cloud, only where this store hosts something)
PASS robots.txt allows all + sitemap line
== 20. savings-calculator JS (source-level) ==
PASS vs-calc.js exists, is vanilla JS (no PHP, no frameworks)
PASS vs-calc.js writes via textContent ONLY (no innerHTML injection surface)
PASS vs-calc.js braces/parens balanced
PASS node not available in this container - syntax proven by balanced-brace check above (node --check run in CI/dev)
== 21. admin guards: csrf + validation ==
PASS admin_landing_save requires login + csrf_check
PASS admin_comparison_create requires login + csrf_check
PASS admin_comparison_update requires login + csrf_check
PASS admin_comparison_delete requires login + csrf_check
PASS settings save validates site_url + demo_base schemes
PASS comparison input: EMPTY source_url rejected with friendly error
PASS comparison input: non-http source rejected
PASS comparison input: valid row passes, blank cents -> NULL
PASS comparison input: clip to column widths (190/300/500)
PASS comparison input: cents clamped inside INT (got 2000000000, want 2000000000)
PASS landing input: broken features JSON -> friendly error
PASS landing input: FAQ missing answer -> friendly error
PASS landing input: bad demo URL -> friendly error
PASS landing input: valid payload passes; 150KB pain_md survives clip(200000)
== 22. bundles schema migration (additive + idempotent) ==
PASS install() left bundles + bundle_items + order_items.bundle_id present
PASS pre-bundles shape reproduced
PASS migrate() restores bundles + bundle_items + order_items.bundle_id
PASS bundles migration keeps every order_items row (got 3, want 3)
PASS bundles migrate() is idempotent (re-runs are no-ops, no throw)
== 23. suite fulfillment: 1 checkout -> N licenses + N downloads + 1 order ==
PASS suite SKU namespace parses (got 'toolkit', want 'toolkit')
PASS non-suite SKUs stay out of the namespace
PASS components = each product's PRIMARY (cheapest) edition, in sort order
PASS suite webhook -> 200 (got 200, want 200)
PASS suite expands: 2 components fulfilled (got 2, want 2)
PASS ONE order, paid, at the SUITE price
PASS 2 order items, each tagged with the bundle (suite badge source)
PASS suite items carry the PRODUCT sku (not the suite sku)
PASS 2 licenses issued under the one order (got 2, want 2)
PASS each suite license is a real signed key that verifies
PASS suite licenses work with the license API per PRODUCT sku
PASS 2 download tokens issued under the one order (got 2, want 2)
PASS ONE outbox email lists EVERY included product
PASS …and never a second license key, for any product in the suite
PASS …and each product's download link
PASS …and names the suite
PASS DUPLICATE suite webhook -> 200 + duplicate flag
PASS duplicate suite: identical state (orders/items/licenses/downloads/outbox unchanged) (got array (
0 => 4,
1 => 5,
2 => 6,
3 => 7,
4 => 6,
), want array (
0 => 4,
1 => 5,
2 => 6,
3 => 7,
4 => 6,
))
PASS suite refund -> 200 (got 200, want 200)
PASS suite order marked refunded (got 'refunded', want 'refunded')
PASS refund revokes ALL N suite licenses (got 2, want 2)
PASS unknown suite -> 200, order recorded, 0 fulfilled
PASS mixed order fulfills suite components + the plain item (3 licenses)
PASS mixed order: plain item stays untagged, suite items tagged
== 24. suite savings math is computed LIVE (never hardcoded) ==
PASS suiteTotals: sum of members' single prices (4900+5900) (got 10800, want 10800)
PASS suiteTotals: savings vs the 8900 suite price (got 1900, want 1900)
PASS suiteTotals: member count (got 2, want 2)
PASS …and the suite math CHANGES on the next read (data layer)
PASS suite page renders the LIVE figures ($128.00 separately, save $39.00)
PASS price restored -> page recomputes ($108.00 separately, save $19.00)
== 24b. cross-sell band: best suite per member product, live math, ONE band ==
PASS bestSuiteFor: member product -> its active suite with LIVE totals attached
PASS bestSuiteFor: product in NO active bundle -> null (no band data)
PASS member landing renders the band: BETTER TOGETHER eyebrow + names + live figures
PASS band is link-only: "See the suite" -> /s/{slug}; NO buy CTA inside the band
PASS …and the band math CHANGES at the data layer (sum 12800, save 3900 — never stored)
PASS …and the rendered band recomputes on the next request ($128.00 separately — save $39.00)
PASS product in TWO active suites -> the bigger absolute saving wins (2900 > 1900)
PASS multi-suite member renders a band for EVERY topic suite it belongs to, best saving FIRST (contract changed 2026-08-30: one band hid the Front-Desk Suite from half its members)
PASS deactivated bundle drops out of the ranking (band falls back to the active suite)
== 25. suite landing page (XSS + JSON-LD + structure) ==
PASS suite page renders (storefront chrome)
PASS NO raw <script>alert survives ANY injected bundle field
PASS suite name escaped in h1
PASS exactly one h1 on the suite page (got 1, want 1)
PASS per-product cards link to their /p/ pages
PASS combined honest-limitations note carries the members' limitations (escaped)
PASS suite FAQ present (details/summary, no JS)
PASS Buy CTA points at the suite checkout path
PASS suite JSON-LD decodes to schema.org Product
PASS suite JSON-LD Offer price = the BUNDLE price
PASS suite JSON-LD url is the canonical /s/ page
PASS deactivated suite -> 404 page (nothing leaks)
== 26. storefront suites strip + sitemap ==
PASS home renders the Suites strip with the suite (escaped) + live savings
PASS strip links to the /s/ landing page
PASS sitemap includes the /s/ suite pages
PASS sitemap is still valid XML with suites
PASS deactivated suite drops out of the sitemap
PASS deactivated suite drops off the home strip too
== 27. bundles admin: input clip/clamp + csrf + immutable slug ==
PASS bundle input: valid payload passes
PASS bundle slug sanitized to SKU-safe form (got 'my-suite', want 'my-suite')
PASS bundle input clipped to column widths (190/255/190/300/8)
PASS bundle price clamped inside INT (got 2000000000, want 2000000000)
PASS bundle items: only REAL product ids survive; sort clamped
PASS bundle input: no real products -> friendly error; missing slug -> error
PASS admin_bundle_create requires login + csrf_check
PASS admin_bundle_update requires login + csrf_check
PASS admin_bundle_delete requires login + csrf_check
PASS suite slug is IMMUTABLE on update (input without slug; UPDATE never touches slug)
PASS sold suites are protected from deletion (deactivate instead)
== 28. ownware.io storefront seed (dev tree only - excluded from dist) ==
PASS ownware-seed.php absent (dist zip build) - seed suite runs in the dev tree
== OW4-2 RIBBON (2026-09-14): the demo ribbon sells both ladders ==
PASS RIBBON checks skipped (dist zip build - store_instance.php absent)
PASS SAAS KIT LICENSE checks skipped (dist zip build - store_instance.php absent)
PASS KIT CATALOG checks skipped (dist zip build - store_instance.php absent)
PASS RELEASE GUARD checks skipped (dist zip build - store_instance.php absent)
== FIXLCP (2026-09-13): the banner AVIF, the route, the markup, the gate's warm-up ==
PASS FIXLCP checks skipped (dist zip build - store_instance.php absent)
== T6 unit 3 (2026-08-31): the answer to github.com, on the 11 gap pages + 14 hubs ==
PASS repo-answer coverage skipped (dist zip build - store_instance.php absent)
PASS llms.txt alternative-to coverage skipped (dist zip build)
== C7D (2026-09-07): derived prompt-map FAQs, 24 products ==
PASS C7D FAQ checks skipped (dist zip build - store_instance.php absent)
== 29. presentation: theme accents + shipped gallery ==
PASS install() left theme_color + images_json present
PASS pre-presentation shape reproduced
PASS migrate() restores theme_color + images_json (additive, data kept)
PASS presentation migrate() is idempotent (re-runs are no-ops, no throw)
PASS themeColor: valid hex normalized lowercase (got '#21b5cb', want '#21b5cb')
PASS themeColor: 3-hex / no-# / css-injection / attr-breakout / null all rejected
PASS themeStyle: invalid input -> empty string (no style attribute emitted) (got '', want '')
PASS themeStyle: exactly four derived accent vars, ALL locked to 6-hex values
PASS themeBlend midpoint math (got '#808080', want '#808080')
PASS storeImages: ONLY the existing whitelisted image survives (missing / traversal / .php dropped)
PASS storeImages: garbage JSON -> empty manifest (got array (
'banner' => NULL,
'shots' =>
array (
),
), want array (
'banner' => NULL,
'shots' =>
array (
),
))
PASS shotPath maps the manifest path onto the nosniff route (got 'shot/zz-test-probe/shot-01.jpg', want 'shot/zz-test-probe/shot-01.jpg')
PASS shotPath stamps a real image with its mtime — SKIP (dist zip: a store-instance fact)
PASS landing scopes the VALIDATED accent inline
PASS gallery renders ONLY the existing file via /shot/ (missing + traversal entries absent)
PASS demo button hidden when demo_url empty and no demo_base template
PASS INVALID stored theme_color never reaches the page (no accent vars, no attribute breakout)
PASS demo button reappears via the demo_base template (tracked /demo/ redirect)
PASS store_asset_serve: nosniff + realpath jail + whitelisted mime (source level)
== 30. launch-mode flags: coming-soon gating + notify waitlist ==
PASS pre-flags shape reproduced
PASS migrate() restores demos_live + sales_live + notify_signups
PASS flags migrate() is idempotent (re-runs are no-ops, no throw)
== notify double opt-in ==
PASS signup mints a 48-hex confirm token
PASS row starts UNconfirmed
PASS confirm by token succeeds
PASS row is now confirmed
PASS re-click is idempotent (still true, timestamp unchanged)
PASS garbage token rejected
PASS duplicate signup keeps the ORIGINAL token (no token churn)
PASS pre-opt-in row (empty token) gets a token minted on re-signup
PASS confirm route is registered
PASS welcome mail carries the confirm link
== deal-code redemption ==
PASS gate: /redeem is OFF while no codes exist
PASS normalize: lowercase/spaces/junk collapse to canonical code
PASS gate: codes seeded switches /redeem ON
PASS unknown code refused
PASS invalid email refused
PASS unfulfillable sku refused as config (code NOT burned)
PASS valid redeem succeeds via normalized input (lowercase in, RD- ref out)
PASS redeem created a paid $0 appsumo order
PASS redeem issued exactly one license + one download
PASS code row marked redeemed with buyer email + order id
PASS same code + same email re-surfaces the SAME order (idempotent, no new fulfillment)
PASS same code + DIFFERENT email refused
PASS redeem routes registered
PASS follow-up: fresh redemption (today) is NOT yet eligible
PASS follow-up: 8-day-old redemption IS eligible exactly once
PASS follow-up: refunded order is excluded (never ask a refunder for a review)
PASS follow-up: marked rows never re-send
PASS follow-up: script ships + sends exactly one mail, no drip
PASS gate: back OFF after cleanup (buyer stores never expose /redeem by accident)
== Own It 2.0 ==
PASS v2: tables + columns migrated
PASS v2: mintKey stores only the hash; revocation kills lookup
PASS v2: TOTP verify + drift + recovery consume-once
PASS v2: outbound webhook delivery logged with failure detail
PASS v2: routes registered (owner api, 2fa, security, backup, import, healthz)
PASS v2: login intercepted (2FA + rate limit) and fulfillment fires owner events
PASS v2: admin dark mode scoped away from the storefront + theme assets ship
PASS restored flag columns default OFF (got array (
0 => 0,
1 => 0,
), want array (
0 => 0,
1 => 0,
))
PASS home OFF: "Demo — coming soon" chips + ZERO internal /demo/ hrefs; grid never sells (no Codester links, cards funnel through "View details")
PASS home OFF (dist zip build): no external Try-demo links leak into a buyer store
PASS vendra landing checks skipped (dist zip build - ownware-seed.php absent)
PASS landing OFF: demo chip present, NO /demo/ href anywhere
PASS landing OFF: hero shows "Available soon — get notified", no Buy/own-it CTAs
PASS landing OFF: notify form carries csrf + honeypot + the product slug
PASS cross-sell band under sales_live=0: still renders, links ONLY the (gated) suite page
PASS vs page OFF: comparisons + citations + calculator STILL render
PASS vs page OFF: Buy CTAs replaced by get-notified, demo link replaced by chip
PASS suite page OFF: live savings math kept, /buy-suite/ gone, notify form with suite slug
PASS /demo/{slug} with demos_live=0 -> 404 page, NO redirect
PASS …and NO click row was recorded (got 7, want 7)
PASS /buy/{id} with sales_live=0 -> 404 page (no provider hand-off)
PASS /checkout/demo/{id} gated -> 404 (fake fulfillment unreachable from public UI)
PASS /buy-suite/{id} + /checkout/suite/{id} gated -> 404
PASS ALL six checkout entry handlers call the sales gate FIRST (source level)
PASS webhook endpoint is NOT gated by the launch flags (existing buyers/refunds)
PASS notifySignup stores a validated row (lowercased email)
PASS duplicate signup is a silent success and stays ONE row (idempotent)
PASS same email may follow a DIFFERENT product / the whole store
PASS invalid email rejected, nothing stored
PASS product_slug sanitized to SKU alphabet + clipped to 80
PASS notify endpoint: csrf + per-IP rate limit + honeypot drop (source level)
PASS notify CSV export goes through the guarded _csv_out/toCsv path
PASS notify export cells stay formula-guarded (numeric-aware)
PASS flags ON + wired edition: demo link + Buy button + own-it-once hero restored, notify UI gone
PASS home ON: live Try-demo links return, chips gone
PASS suite ON: Buy-the-suite CTA back, notify form gone
PASS payment webhook fulfills normally under both flags OFF
PASS /license/check unaffected by the flags
PASS buyer re-download still serves under both flags OFF (got 200, want 200)
PASS buyer portal still lists the order, key, and download link under both flags OFF
== 31. storefront narrative, title dedupe, favicon, editorial skin scope ==
PASS home <title> is the full SEO title, store name once (no "Store · Store")
PASS non-home pages keep "Page · Store" titles
PASS product pages keep their SEO title (name — tagline)
PASS assets/favicon.png ships (non-empty PNG)
PASS storefront layout links rel=icon + apple-touch-icon
PASS admin layout links the favicon too (source level)
PASS home narrative: hero → #idea → #compare → #pillars → #complete → #catalog → grid, in order
PASS rent-vs-own comparison renders (both columns + the illustrative range, no invented stats)
PASS three self-hosted pillars render
PASS storefront layout links store.css + carries body.storefront
PASS Signal CSS carries the approved token set (light-first + dark) and the legacy aliases read it
PASS editorial CSS: self-hosted fonts (no hotlink), mono accents, tag.soon chip, reduced-motion
PASS no glass surfaces outside the flagship block: backdrop blur ONLY on the sticky header; no feTurbulence; the glow radial is bounded
PASS the flagship exemption is one marked block, present once, and the only place feTurbulence appears
PASS no admin or data-grid selectors in store.css
PASS home hero is the Signal hero (sig-hero + Machine SVG + rack), no glass class anywhere
PASS the rack degrades on a buyer store (few/no shots): the section still renders without error
PASS Signal brand: wordmark link in the header, brand block in the footer columns, no blue dot
PASS admin stylesheet + admin layout stay skin-free (no backdrop-filter, store.css not linked)
PASS portal + admin views carry no glass class and no backdrop-filter
PASS storefront views carry no glass class either (editorial system replaced it)
== 32. guides engine: migrate, CRUD guards, published-only, XSS, links, JSON-LD, sitemap, nav ==
PASS install() left the guides table present
PASS legacy shape reproduced (guides table gone)
PASS migrate() restores the guides table with the full column set
PASS guides migration is idempotent (re-runs are no-ops, no throw)
PASS guide input: slug sanitized, title→190, description→300, body→200000, published=1
PASS guide input: unchecked publish box stays a draft (0) (got 0, want 0)
PASS admin_guide_create requires login + csrf_check
PASS admin_guide_update requires login + csrf_check
PASS admin_guide_delete requires login + csrf_check
PASS guides index: published guide listed (escaped), draft invisible
PASS guides index: canonical + storefront chrome
PASS draft guide page -> 404 view, nothing leaks
PASS guide body renders escape-first: NO script/onerror survives any field
PASS markdown structure renders: # -> h2, ## -> h3, bold, list
PASS exactly one h1 on a guide page (got 1, want 1)
PASS internal /p link renders as an anchor; external https link carries rel=noopener
PASS javascript: scheme is REFUSED — stays literal escaped text, never an href
PASS JSON-LD Article decodes with headline + canonical url + dates
PASS related band: ACTIVE linked product gets a pill; inactive linked product does not
PASS guide page ends with the store CTA block
PASS storefront nav + footer both link the guides index
PASS sitemap lists /guides + the published guide, never the draft
PASS sitemap is still valid XML with guides
PASS unpublishing 404s the page AND removes it from the sitemap
PASS with zero published guides the /guides index drops out of the sitemap too
== PWA ==
PASS pwaManifest() returns the required fields
PASS manifest name follows store_name setting (falls back to Deliora)
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
PASS sw.js cache versioned + purges old caches
PASS offline fallback ships
PASS both layouts (admin + store) link the manifest + register the SW
PASS the /manifest.json ROUTE is registered
PASS bin/demo.php loads StoreApi + OutWebhook (Deliora.php calls both)
PASS bin/flush-outbox.php loads StoreApi + OutWebhook (Deliora.php calls both)
PASS bin/redeem-codes.php loads StoreApi + OutWebhook (Deliora.php calls both)
PASS bin/review-followup.php loads StoreApi + OutWebhook (Deliora.php calls both)
== v3 ==
PASS v3: dropped v3 tables restored by migrate()
PASS v3: role/active/branding/token/oidc columns present
PASS v3: settings singleton ensured with EXPLICIT id=1
PASS v3: index.php migrates at boot
PASS v3: reference copy absent (buyer machine) — byte-identity skipped
PASS W16-8: the dev tree runs the REAL byte-identity branch, never the buyer-zip skip
PASS v3: healthz/ping/openapi all read App::VERSION (one source)
PASS v3: sw cache names the code version
PASS v3: README has New in 3.0 + API.md has the MCP section
PASS v3: backup redaction widened + shared (export uses DELIORA_BACKUP_REDACT, scheduled too)
PASS v3: GET /mcp transport-hint route registered
PASS v3: requireKey checks u.active (deactivated users keys die)
PASS v3: deploy kit filled (deliora/acme/8090, no placeholders)
PASS v3: kit healthcheck is the json_decode probe with $$ escape
PASS v3: mint core refuses unknown sku by name
PASS mint rule: an admin key may mint codes
PASS mint rule: a support key may not mint codes
PASS mint rule: a key row with no role is a pre-roles admin key, as the MCP door always read it
PASS POST /api/redeem-codes checks the mint rule before it mints
PASS the MCP mint tool uses the same rule
PASS v3: invite token verifies + expiry honored
PASS v3: last-admin guard math
PASS nav: every sidebar link is highlighted by the page it opens
PASS nav: no page highlights a sidebar item that does not exist
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)
PASS urls: a cron never derives a base path from its own script name
PASS urls: a cron-built URL keeps the host and path separate
PASS urls: a cron uses the canonical site_url when one is configured
PASS urls: cron-built download links are absolute and correct
PASS urls: a malformed site_url is ignored rather than emitted
PASS urls: an ordinary web request is unaffected
== UI kit v1.0 (2026-08-19): admin on the kit, resend delivery, dashboard ==
PASS resend: fixture order fulfilled
PASS resend: a paid order re-sends its delivery email (outbox +1) with a fresh download link and no second license key
PASS resend: an expired download link is re-minted (a usable token exists again)
PASS resend: a refunded order is refused with a sentence
PASS resend: unknown order is refused
PASS routes: POST admin/orders/{id}/resend registered → admin_order_resend (csrf + flash + redirect)
PASS kit: admin layout loads kit css+js + admin.css, grouped nav (≥4 groups), theme toggle, who-line; storefront layout untouched (style.css + store.css, no kit)
PASS kit: admin.css is an app layer with light + dark accent; no theme.js; versioned assets; revalidating worker; bundled fonts only
PASS dashboard: first-run checklist from the database (editions / files / provider / mail / orders), needs-action for unfulfilled orders, 30-day revenue sparkline
PASS order record: rail (customer, emails, portal), license cards with reissue/revoke/free-seat, usable/expired download rows, resend button
PASS licenses list: table with search + status counts; products/customers lists link into records
== v3.1: promo pass-through ==
PASS promo: the LS checkout URL carries the provider discount code (never repriced on-box)
PASS promo: an invalid code is IGNORED, never emitted
PASS promo: demo checkout stays untouched (no provider = no code)
PASS promo: settings save validates the code alphabet and the product page shows the banner only when set
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: the key form lets an administrator mint a read-only key
== nexura unlisted (operator ruling 2026-08-24) ==
Warning: file_get_contents(tests/../bin/ownware-store.php): Failed to open stream: No such file or directory in tests/run.php on line 2664
Warning: file_get_contents(tests/../bin/ownware-seed.php): Failed to open stream: No such file or directory in tests/run.php on line 2665
Warning: file_get_contents(tests/../tests/store_instance.php): Failed to open stream: No such file or directory in tests/run.php on line 2666
PASS nexura: in the curation deactivation list (deploy runs curation after every seed) — SKIP (dist zip: a store-instance fact)
PASS nexura: NOT in any suite seed (was Compliance Suite; suite ships 6 tools now)
PASS nexura: test instance mirrors curation (nexura-only) before active-state invariants — SKIP (dist zip: a store-instance fact)
== /pricing: the page AI agents were guessing at (2026-08-24) ==
PASS pricing: route + controller exist and every figure is read from the DB (no typed prices)
PASS pricing: every guessed 404 spelling now lands — /plans, /plan, /prices, /plans-pricing, /pricing-plans
PASS pricing: per-product guesses land on the product page, unknown slugs on /pricing
PASS pricing: unwired products say "Available soon", never a stale price (same rule as llms.txt)
PASS pricing: in the sitemap and named in llms.txt Reference
== citable Observatory + guide capture (2026-08-23) ==
PASS observatory.csv: route registered and the exporter neutralizes leading =+-@ (external vendor text)
PASS observatory page: quote-gated means the price TEXT says so, never a missing normalized figure (the 7shifts CAD trap)
PASS observatory page: copy-paste citation + CSV and JSON download links
PASS sitemap: observatory entry carries lastmod = newest verified date, not an undated URL
PASS llms.txt: names the CSV download beside the JSON API
PASS guide capture: form posts to /notify with guide-<slug> tag, honeypot intact, csrf present
PASS notify handler: guide-<slug> errors bounce back to the guide, published-only
== conversion pass P2-P4 (2026-08-23) ==
PASS trust block: refund, tests-in-box and continuity lines at the buy button, source line stack-aware (the one product not written in PHP gets its own line)
PASS ladder line: Cloud names this product at its own price from cloudOffers(), with the 12-month license fact
PASS listedon partial: shared, honors a caller skip list, and reads it from legal.local
PASS post-Codester rule: this deployment excludes Codester on product pages — SKIP (dist zip: a store-instance fact)
PASS listedon partial: no directory name is hard-coded in the shipped view
PASS listedon callers: both includes are guarded (the partial is not in a buyer zip)
PASS suites hero: no suite slug is hard-coded in the shipped view
PASS suites hero: a store naming both gets hero + alternative
PASS suites hero: a store naming only the first gets a hero and no alternative
PASS suites hero: a buyer store naming none gets no hero — every suite falls to the list
PASS suites hero: THIS deployment still names its two suites — SKIP (dist zip: a store-instance fact)
PASS /security: no deployment fact is hard-coded in the shipped view
PASS /security: all three deployment facts read from legal.local
PASS /security: each fallback tells the operator which key to set, and claims nothing
PASS /security: THIS deployment still states all three — SKIP (dist zip: a store-instance fact)
PASS source-layout details: read-before-you-buy tree, skipped for the one product not written in PHP
== undeliverable editions (2026-08-30: a paid product whose edition had no file) ==
PASS an edition with no file at all is reported
PASS an edition naming a file that is not on disk is reported too — the same 404 to a buyer
PASS an edition whose file is present is NOT reported
PASS an INACTIVE product is not reported — nobody can buy it
PASS the setup checklist is green only when EVERY sellable edition can be delivered
PASS the dashboard warning is fed by the same check as the checklist
== /suites, /contact, /p/<x>-suite, bare /<slug>: what the first buyer's AI agent 404'd on (2026-08-30) ==
PASS routes registered: /suites, /bundles, /contact, /products, /apps
PASS bare /{slug} is the LAST GET route registered — every explicit route above it still wins
PASS aliases are 301s (permanent addresses, not temporary detours)
PASS /suites is in the sitemap and named in llms.txt Reference
PASS /suites view carries no typed price or saving — every figure comes from suiteTotals()
PASS /support carries the #contact anchor /contact redirects to
PASS /privacy, /terms, /refunds redirect into /legal at anchors the page really has
PASS /openapi.json redirects to the spec route that exists (/api/openapi.json)
PASS the four aliases are registered before the bare-slug catch-all
PASS bare /<product-slug> resolves to its /p/ page (got '/p/gw-probe', want '/p/gw-probe')
PASS bare /<suite-slug> resolves to its /s/ page (got '/s/gw-field', want '/s/gw-field')
PASS bare /<suite>-suite resolves to the suite (the agent guessed /p/field-service-suite) (got '/s/gw-field', want '/s/gw-field')
PASS suite_slug_for strips a -suite suffix and a suite- prefix (got 'gw-field', want 'gw-field')
PASS an unknown bare slug resolves to nothing (ordinary 404, no guessing)
PASS a slug with path characters never resolves (no open redirect)
PASS uppercase is tolerated (agents and humans type it)
PASS /suites lists the probe suite with its /s/ link and its member product
PASS /suites shows the live saving for the probe suite ($59 parts vs $49 suite = $10)
PASS an INACTIVE product no longer resolves from its bare slug
PASS an INACTIVE suite no longer resolves (neither bare nor -suite form)
== suites: every topic suite on the product page, and the suite page answers what the buyer asked (2026-08-30) ==
PASS suitesFor returns BOTH topic suites of a dual-membership product (got 2, want 2)
PASS suitesFor orders by live saving, best first ($59-$39 beats $59-$49) (got 'gs-beta', want 'gs-beta')
PASS the product page names BOTH suites (was: only bestSuiteFor, the second silently dropped)
PASS the suite page states what runs it (PHP 8, MySQL/SQLite) — the buyer question it did not answer
PASS the suite page states data ownership and source (never phones home, full source)
PASS the suite page carries the two FAQ answers the first buyer needed (already own one / start with one)
PASS suite page: a per-member demo link renders only through the same demosLive gate as the product page
== typed prices that drifted from the store (found by the growth wave, 2026-08-30) ==
Warning: file_get_contents(tests/../bin/ownware-seed.php): Failed to open stream: No such file or directory in tests/run.php on line 2960
PASS the Ownware OS guide no longer sells the OS at $499 or $999 (it is $1,299) nor claims 37 apps
PASS the whole-catalog bundle copy carries no typed "$499 + $249 = $699" arithmetic
PASS llms.txt derives the Observatory vendor count from the DB (it read "~74" while the dataset held 110)
PASS QuotaPath is cited with its platform fee ($525/month governs for five users or fewer), not $105 for three seats — SKIP (dist zip: a store-instance fact)
PASS Qminder is cited at its month-to-month price ($869) with the annual figure beside it, not the annual-equivalent as monthly — SKIP (dist zip: a store-instance fact)
== typed catalog counts (the OS guide's 37/38 were not alone — sweep 2026-08-30) ==
Warning: file_get_contents(tests/../bin/ownware-seed.php): Failed to open stream: No such file or directory in tests/run.php on line 2981
PASS the OS guide no longer claims one of the apps is free (Tokora is $69) nor counts 38
PASS the 3.1 release notes no longer say "across all 37"
PASS ruling 5: the affiliates page is absent from this build and /affiliates answers 404
PASS /go/cloud: a store that is not ownware.io answers 404 (no hop, no script)
PASS /go/cloud: the hop page is absent from this build and the route answers 404 even as the flagship
PASS robots.txt keeps the referral hop out of search (Disallow: /go/)
== short product names in buttons and running text (2026-08-30) ==
PASS shortName cuts at the em-dash (got 'Fixora', want 'Fixora')
PASS shortName cuts at the en-dash (got 'Examplia', want 'Examplia')
PASS shortName cuts at a colon (got 'Approva', want 'Approva')
PASS shortName leaves a plain name alone (got 'Menura', want 'Menura')
PASS alternatives page: the CTA and running text use the short name, the product list keeps the full one
== L1.3 + L5.20 (2026-08-31): gap pages carry the buyer's phrase; llms Optional; press traction block; derived press desc ==
Warning: file_get_contents(tests/../bin/ownware-seed.php): Failed to open stream: No such file or directory in tests/run.php on line 3124
Warning: file_get_contents(tests/../bin/ownware-seed-w3.php): Failed to open stream: No such file or directory in tests/run.php on line 3124
PASS gap page opens with the buyer's phrase: self-hosted invoicing software — one time payment — SKIP (dist zip: a store-instance fact)
PASS gap page opens with the buyer's phrase: self-hosted CMMS maintenance software you buy once — SKIP (dist zip: a store-instance fact)
PASS gap page opens with the buyer's phrase: self-hosted fixed asset depreciation register — SKIP (dist zip: a store-instance fact)
PASS gap page opens with the buyer's phrase: self-hosted purchase order approval system — SKIP (dist zip: a store-instance fact)
PASS gap page opens with the buyer's phrase: Self-hosted review request automation — SKIP (dist zip: a store-instance fact)
PASS gap page opens with the buyer's phrase: self-hosted visitor sign-in system for reception — SKIP (dist zip: a store-instance fact)
PASS gap page opens with the buyer's phrase: self-hosted helpdesk request tracker, small business — SKIP (dist zip: a store-instance fact)
PASS llms.txt has an Optional section holding the guides, after Reference
PASS press: SEO description is derived (no typed "37" or "$34–$129")
PASS press checks skipped (dist zip build - views/press.php is ownware.io's own, not shipped)
== L1.4 (2026-08-31): agent-readiness surfaces — /status /changelog /security /docs /for-agents /llms-full.txt ==
PASS route /status → status_page registered
PASS route /changelog → changelog_page registered
PASS route /security → security_page_public registered
PASS route /docs → docs_page registered
PASS route /for-agents → for_agents_page registered
PASS /status renders the live checks and the catalog counts
PASS /changelog lists current versions read from editions
PASS /security names the report address and the merchant-of-record fact
PASS /docs and /for-agents share one surface list (llms.txt, openapi, observatory present on both)
PASS /for-agents documents the guessed addresses and how to verify
PASS llms-full.txt carries one section per active product with its URL and price
PASS view status.php carries no typed dollar figure
PASS view changelog.php carries no typed dollar figure
PASS view docs.php carries no typed dollar figure
PASS view for_agents.php carries no typed dollar figure
PASS view repo_answer.php carries no typed dollar figure
PASS the six agent pages are in the sitemap and llms.txt Reference names all six surfaces
== F19 SHIP (2026-09-13): the commerce door is advertised only where it is routed ==
PASS flag off: llms.txt advertises no /agent/v1 address
PASS flag off: the agent card carries no commerce endpoint
PASS flag off: the card note does not name a door this install has not opened
PASS flag off: the card still parses and still carries its open surfaces
PASS the llms.txt door line exists and is gated on DELIORA_AGENT_DOOR
PASS the card endpoint and the buying note are gated on the same constant
PASS the card drops a gated key rather than printing a null (array_filter on endpoints and buying)
PASS the fourteen routes are still inside the flag, not beside it
== Release package 2 (2026-09-02): /badge + /your-ai-can-vet-us ==
PASS /badge renders through the public layout with its canonical
PASS /your-ai-can-vet-us renders through the public layout with its canonical
PASS badge asset runs-on-ownware.svg exists and is well-formed SVG — SKIP (dist zip: a store-instance fact)
PASS badge asset runs-on-ownware.svg carries no script, no external reference, no comment — SKIP (dist zip: a store-instance fact)
PASS /badge shows runs-on-ownware.svg and its embed code linking to https://ownware.io — SKIP (dist zip: a store-instance fact)
PASS badge asset runs-on-ownware-dark.svg exists and is well-formed SVG — SKIP (dist zip: a store-instance fact)
PASS badge asset runs-on-ownware-dark.svg carries no script, no external reference, no comment — SKIP (dist zip: a store-instance fact)
PASS /badge shows runs-on-ownware-dark.svg and its embed code linking to https://ownware.io — SKIP (dist zip: a store-instance fact)
PASS /badge says what the mark does NOT claim (no certification, license independent) — SKIP (dist zip: a store-instance fact)
PASS /your-ai-can-vet-us carries the receipt '/connect-your-ai'
PASS /your-ai-can-vet-us carries the receipt '/legal'
PASS /your-ai-can-vet-us carries the receipt '/support'
PASS /your-ai-can-vet-us carries the receipt '/.well-known/security.txt'
PASS /your-ai-can-vet-us carries the receipt '/contact'
PASS /your-ai-can-vet-us carries the receipt '/suites'
PASS /your-ai-can-vet-us carries the receipt '/api/openapi.json'
PASS /your-ai-can-vet-us carries the receipt '/for-agents'
PASS /your-ai-can-vet-us carries the receipt 'POST /mcp'
PASS /your-ai-can-vet-us carries the receipt 'read-only'
PASS /your-ai-can-vet-us: the MCP receipts name a REAL demo host from the database
PASS view vet_us.php hard-codes no absolute host — every URL comes from $B() or $demoHost
PASS /your-ai-can-vet-us counts are read from the DB (tool count = active products minus the flagship, the one definition since the site survey)
PASS /your-ai-can-vet-us: the flagship row renders for the product legal.local names
PASS /your-ai-can-vet-us: with no demo host the MCP rows are skipped, not printed empty
PASS /your-ai-can-vet-us: with no flagship product the sealed-mode row is skipped
PASS /your-ai-can-vet-us: the generic receipts still render for a store with neither
PASS /your-ai-can-vet-us holds the customer story as an HTML comment placeholder only — no link, no slug
Warning: file_get_contents(tests/../views/badge.php): Failed to open stream: No such file or directory in tests/run.php on line 3282
PASS view badge.php carries no typed dollar figure
PASS view vet_us.php carries no typed dollar figure
PASS agent_surfaces() lists your-ai-can-vet-us, so /docs, /for-agents and llms.txt all carry it
PASS llms.txt points at /your-ai-can-vet-us
PASS /for-agents points at /your-ai-can-vet-us
PASS agent_surfaces(): one bundle reads "The bundle, its members", never "The 1 bundles"
PASS agent_surfaces() reads the suite count from the DB (no typed "twelve")
PASS ruling 5: a store that is not ownware.io lists neither our badge nor our press kit in its sitemap
PASS sitemap carries /badge and /your-ai-can-vet-us
PASS portal offers the badge once, only to buyers with purchases, with no obligation
Warning: file_get_contents(tests/../views/badge.php): Failed to open stream: No such file or directory in tests/run.php on line 3298
PASS the badge and vet pages use no class the other brand's stylesheet defines (fingerprint firewall)
== Release package 3 (2026-09-02): guide seed set 9 + footer/press addendum ==
PASS release package 3 guide checks skipped (dist zip build - store_instance.php absent)
PASS footer Help column carries "Your AI can vet us" on every public page
PASS press badge-link check skipped (dist zip build - views/press.php is ownware.io's own)
== Package 4 phase B, batch 1 (2026-09-02): back-catalogue retrofit ==
PASS retrofit batch-1 checks skipped (dist zip build - store_instance.php absent)
== Package 4 phase B, batch 2 (2026-09-02): back-catalogue retrofit ==
PASS retrofit batch-2 checks skipped (dist zip build - store_instance.php absent)
== Package 4 phase B, batch 3 (2026-09-02): back-catalogue retrofit ==
PASS retrofit batch-3 checks skipped (dist zip build - store_instance.php absent)
== Package 4 phase B, batch 4 (2026-09-02): back-catalogue retrofit ==
PASS retrofit batch-4 checks skipped (dist zip build - store_instance.php absent)
== Package 4 phase B, batch 5 (2026-09-02): back-catalogue retrofit ==
PASS retrofit batch-5 checks skipped (dist zip build - store_instance.php absent)
== Package 4 phase B, batch 6 (2026-09-02): back-catalogue retrofit ==
PASS retrofit batch-6 checks skipped (dist zip build - store_instance.php absent)
== Package 4 phase B, batch 7 (2026-09-02): back-catalogue retrofit ==
PASS retrofit batch-7 checks skipped (dist zip build - store_instance.php absent)
== Package 4 phase B, batches 8+9 (2026-09-02): back-catalogue retrofit, final ==
PASS retrofit batch-8/9 checks skipped (dist zip build - store_instance.php absent)
== Package 5 (2026-09-02): the flagship product page ==
PASS flagship page checks skipped (dist zip build - store_instance.php absent)
== The customer story (operator GO 2026-09-02): published under the reshaped slug ==
PASS customer-story checks skipped (dist zip build - store_instance.php absent)
== Doorman close-out (2026-09-03): the agent pack ==
PASS agent-pack checks skipped (dist zip build - store_instance.php absent)
PASS WS-4 pack 1 skipped (dist zip build - store_instance.php absent)
PASS WS-14 guards skipped (dist zip build - store_instance.php absent)
== TAIL GUIDES: every money figure in the rent-vs-own guides, asserted against the database ==
PASS tail-guide figure checks skipped (dist zip build - store_instance.php absent)
PASS FIX-F3 freshness checks skipped (dist zip build - store_instance.php absent)
PASS OW3B-SHIP cloud checks skipped (dist zip build - store_instance.php absent)
PASS OW4-11 GUIDECTA checks skipped (dist zip build - store_instance.php absent)
PASS C8P measured-page checks skipped (dist zip build - store_instance.php absent)
PASS LLMS feed checks skipped (dist zip build - store_instance.php absent)
PASS FAQ2 checks skipped (dist zip build - store_instance.php absent)
PASS DEMAND guide-set checks skipped (dist zip build - store_instance.php absent)
PASS ORGANIC guide-set checks skipped (dist zip build - store_instance.php absent)
PASS LEAVING guide-set checks skipped (dist zip build - store_instance.php absent)
PASS REMINDERS guide checks skipped (dist zip build - store_instance.php absent)
PASS F3 store-copy checks skipped (dist zip build - store_instance.php absent)
== M8 + M9 (2026-09-15): the register code and the demo capture ==
== Unit 19 (2026-09-15): the German landing, § 16 HinSchG ==
PASS German landing checks skipped (dist zip build - ownware.io's own page)
PASS M8/M9 checks skipped (dist zip build — src/Register.php absent)
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
PASS reach: the home catalog is an OfferCatalog (an ItemList) of the priced products
PASS reach: every home Product CARRIES its Offer with price + currency + availability (the GSC critical)
PASS reach: the home Offer carries seller + priceValidUntil like the product page, and (08 #18, 2026-09-26) no US-only shipping/return fields
PASS reach: a free product is a SoftwareApplication with a $0 offer, never a Product without one
PASS reach: no aggregateRating and no review is ever invented (2 buyers, no ratings to mark up)
PASS reach: no Product node anywhere in the home graph lacks offers
PASS reach: suite Offer carries sku, name, seller, priceValidUntil (C5 §2: 13 suites, one code path)
PASS reach: /faq FAQPage from the help_qa sections, incomplete pairs skipped
PASS reach: /guides ItemList lists the published guides by url + name, in order
PASS reach: /suites ItemList carries each suite as the Product+Offer its page carries
PASS reach: /pricing ItemList carries both license Offers per wired product and the suites, and skips "Available soon"
PASS reach: a page that brings no richer markup still gets a WebPage node from its own head
PASS reach: no WebPage node without a canonical
PASS reach: a product Offer carries validFrom from the edition date (GSC warning ×25)
PASS reach: no validFrom is asserted when no date is on the row
PASS reach: validFromDate accepts a datetime and refuses garbage
PASS reach: the product hero image carries fetchpriority="high" and banner dimensions
PASS reach: the layout preloads the two above-the-fold fonts and falls back to a WebPage graph
PASS reach: the router sends a slashed GET to the slashless address with a 301 and keeps the query string
== email list: exit token, welcome once, audience, buyer invite, broadcasts ==
PASS list: migration added the row columns
PASS list: migration added broadcasts + broadcast_sends + the buyer_invite_live flag (default 0)
PASS list: signup records its source and mints a 48-hex exit token distinct from the confirm token
PASS list: an unconfirmed row is not in any audience
PASS list: the welcome mail goes ONCE across two confirm clicks (welcomed_at)
PASS list: the welcome mail carries THIS row's one-click unsubscribe URL and no unfilled placeholder
PASS list: a confirmed row is in the audience; store audience sees it, guides audience does not
PASS list: one address on two tags is ONE recipient
PASS list: garbage exit tokens are refused
PASS list: one click leaves EVERY list of that address
PASS list: signing up again after leaving re-opens the row as UNconfirmed (the new click is the consent)
PASS list: List-Unsubscribe headers are emitted and a CRLF in a value cannot inject a header
PASS list: the buyer opt-in token is stateless and tamper-evident
PASS list: the click creates ONE confirmed buyer row and the buyers audience sees it
PASS list: the delivery mail carries the invite ONLY when buyer_invite_live = 1 (default 0 = no invite)
PASS list: the form partial states the purpose, the ceiling, the confirm step and the exit; posts source + honeypot
PASS list: the home page footer carries the form with source=footer
PASS list: mail template confirm fills every placeholder and names no other brand
PASS list: mail template welcome fills every placeholder and names no other brand
PASS list: mail template footer fills every placeholder and names no other brand
PASS list: mail template buyer_invite fills every placeholder and names no other brand
PASS list: broadcast.php refuses without the operator's approval line and needs BOTH --send and --go; caps a run at the provider's daily free limit — SKIP (dist zip: a store-instance fact)
PASS list: routes for the exit and the buyer yes exist
== 18b2. /catalogue — every tool by the job it does (F51 D-8 carry-over, 2026-09-27) ==
PASS /catalogue renders with an h1 and the breadcrumb
PASS /catalogue lists every active product once
PASS /catalogue has at least one group and every jump chip points at a group on the page
PASS /catalogue (this store): at least two groups, each reachable from a jump chip — SKIP (dist zip: a store-instance fact)
PASS /catalogue carries canonical + BreadcrumbList JSON-LD
PASS /catalogue names no product by a raw slug in the visible headings
== SC. Store conversion (F51) ==
PASS SC1: Storefront::proof() reads assets/store/{slug}/proof.json from an overridable dir
PASS SC1: no proof.json -> null (a missing proof is never a number)
PASS SC1: proof.json read: 140 passed (got 140, want 140)
PASS SC1: a proof with failures is refused (null) — the strip never shows a red suite as proof
PASS SC1: a corrupt proof.json is refused (null), never a warning on the page
PASS SC1: the product hero carries the proof strip
PASS SC1: strip: "140 tests" links to the real run log /p/{slug}/tests
PASS SC1: strip: the manual links to the manual rendered on the store
PASS SC1: strip: "API + MCP" links to the rendered API.md
PASS SC1: strip: the version is the edition's (v1.0.0) and links to the quickstart
PASS SC1: strip: "Source included" links to the license answer on the page
PASS SC1: strip: the demo item links the demo and says it resets itself
PASS SC1: proof-first: the strip sits ABOVE the price box
PASS SC1: without proof.json the strip is absent — no typed fallback number
PASS SC1: a product with no shipped document at all (no tree) says so in its strip — the pre-release line, never a number
PASS SC1: the route /p/{slug}/{doc} exists and the controller product_doc() is defined
PASS SC1: /p/{slug}/manual renders the manual through the escape-first md renderer
PASS SC1: the manual page names the product and links back to it
PASS SC1: /p/{slug}/tests renders the run log verbatim-escaped with its run date
PASS SC1: the manual page does not claim to be in the download; the test-run page still says it ships
PASS SC1: an unknown doc name is the 404 page
PASS SC1: a product without a shipped manual is the 404 page, never an empty page
PASS SC1: bin/ownware-proof-collect.php exists, runs each product suite itself and refuses to write a proof for a failing or unparseable run — SKIP (dist zip: a store-instance fact)
PASS SC3: fixture: sc-invoicer sits in an active suite
PASS SC3: fixture: the paid order exists
PASS SC3: mail: subject says start here
PASS SC3: mail: the download link is first
PASS SC3: mail: the QUICKSTART travels inline
PASS SC3: mail: the manual and Connect-your-AI links
PASS SC3: a suite-upgrade code is minted at fulfillment (one per order)
PASS SC3: the code is worth exactly the paid amount (suite arithmetic, no typed money) (got 4900, want 4900)
PASS SC3: the code targets the best suite for the product (got 4, want 4)
PASS SC3: the code lives 30 days
PASS SC3: mail: the suite bridge names the suite, the code, the computed difference and the expiry
PASS SC3: mail: the difference is suite price minus what was paid
PASS SC3: mail: portal link + the license-key ruling line survive
PASS SC3: minting twice returns the same code (idempotent)
PASS SC3: a product in no suite mints no code and its mail carries no bridge
PASS SC3: a suite order mints no upgrade code
PASS SC2: /thanks: the quickstart renders inline under the download
PASS SC2: /thanks: manual + Connect-your-AI links
PASS SC2: /thanks: the suite bridge with the computed difference and the code
PASS SC2: /thanks: "the other N" is counted from the catalog
PASS SC2: /thanks/{unknown numeric ref} is the recording state, not a 404 (the webhook can arrive after the redirect)
PASS SC2: the recording state refreshes itself and offers /claim as the fallback
PASS SC2: the recording state is noindex
PASS SC2: a non-numeric unknown ref is still the 404 page
PASS SC3: routes /upgrade GET+POST exist
PASS SC3: /upgrade renders a code form
PASS SC3: lookup: a live code reports its suite, amount and the difference
PASS SC3: lookup: an unknown code is null
PASS SC3: request: marks the code requested and mails the owner the exact figure
PASS SC3: request: a second request on the same code is refused
PASS SC3: request: the wrong email is refused
PASS SC3: an expired code reads expired and is refused
PASS SC4: a guide that mentions a product links its demo, tagged guide/cta/{guide-slug}
PASS SC4: the demo link promises sample data and no sign-up
PASS SC4: the guide ENDS with the demo: the closer band carries the demo of the product it teaches
PASS SC4: gated — with demos_live = 0 a guide links no /demo/
PASS SC5: Deliora::numbersPayload() parses the form field: calc:<slugs> keeps only real calculator rows
PASS SC5: numbersPayload(): vs:<slug> for a product with priced rows; garbage -> []
PASS SC5: the numbers block is computed from the cited rows: rent/month, 3 years, own once, break-even
PASS SC5: the vs block lists every priced competitor of the product with its source
PASS SC5: notifySignup() stores the payload on the row
PASS SC5: the confirmation mail carries the numbers when a payload is stored — and only then
PASS SC5: the calculator carries the "email me these numbers" form with source=calculator and a numbers field
PASS SC5: calculator.js fills the numbers field from the ticked rows
PASS SC5: the rent-vs-own table carries the form with numbers=vs:{slug}
PASS SC5: the controller accepts the two new sources
PASS SC6: the price block carries the rent-vs-own anchor from the top cited row
PASS SC6: the anchor sits inside the price box
PASS SC6: the buy button answers the license question: Single license · one site
PASS SC6: overlay flag ships OFF
PASS SC6: with the flag off there is no lemon.js and no overlay class
PASS SC6: flag on + LS: the buy button is an overlay button on the LS URL and loads lemon.js
PASS SC6: flag on: the click still counts (beacon to /buy/{id}?ov=1) and Checkout.Success lands on /thanks/{order id}
PASS SC6: buy_target(): the beacon answers 204 and never redirects; a plain click is the 302 to the provider
PASS SC6: flag back off: no overlay markup
PASS SC6: the admin settings form saves the flag
PASS magnet: with no code set, Deliora::magnet() is null
PASS magnet: with no code set, the store pitch is empty — the form promises nothing
PASS magnet: with no code set, the welcome mail block is empty
PASS magnet: the welcome mail mentions no discount and leaves no dangling placeholder
PASS magnet: the subscribe form advertises no discount while no code exists
PASS magnet: a code with 0% is not an offer
PASS magnet: a percent with no code is not an offer
PASS magnet: an out-of-range percent is not an offer
PASS magnet: a real code + percent produces the offer
PASS magnet: the ruled offer is 10% off a FIRST purchase
PASS magnet: the mail block carries the real code, never a placeholder
PASS magnet: the mail block states the one-use-per-order terms honestly
PASS magnet: the code is delivered by the POST-CONFIRMATION welcome mail
PASS magnet: the welcome mail still carries its one-click unsubscribe
PASS magnet: the CONFIRM-LINK mail carries no code — the discount is earned by confirming
PASS magnet: with a code set the subscribe form states the offer plainly
PASS magnet: the form never prints the code itself — it is earned by confirming
PASS magnet: notifyWelcome() is what passes the block, so the code rides the confirmed-only mail
PASS magnet: the settings form exposes the code and the admin save validates it
PASS magnet: no LS API client exists on the box, so the code is never machine-created here
PASS magnet: the suite leaves the offer OFF, so nothing ships switched on by a test
== agent door ==
PASS agent: carts + cart_items exist after migrate()
PASS agent: cart_items refuses a duplicate (cart_id, edition_id) line
PASS agent: migrate() is idempotent for the cart tables
PASS agent: AgentDoor class exists (src/AgentDoor.php)
PASS agent search: two families match "agentic" (name beats tagline) (got array (
0 => 'agentic-app',
1 => 'other-tool',
), want array (
0 => 'agentic-app',
1 => 'other-tool',
))
PASS agent search: a family is one result with its option values in display order
PASS agent search: family price = lowest SELLABLE variant (single 49.00; extended unwired) (got 49.0, want 49.0)
PASS agent search: family in stock while any variant is; currency and tagline carried
PASS agent search: nothing matches → [] (got array (
), want array (
))
PASS agent search: max_price filter on the family price (got array (
), want array (
))
PASS agent search: min_price filter keeps the dearer family only (got array (
0 => 'agentic-app',
), want array (
0 => 'agentic-app',
))
PASS agent search: limit is honored (got 1, want 1)
PASS agent search: price_asc puts the cheaper family first (got array (
0 => 'other-tool',
1 => 'agentic-app',
), want array (
0 => 'other-tool',
1 => 'agentic-app',
))
PASS agent search: price_desc puts the dearer family first (got array (
0 => 'agentic-app',
1 => 'other-tool',
), want array (
0 => 'agentic-app',
1 => 'other-tool',
))
PASS agent search: empty query lists the catalog (families, by name)
PASS agent details: id resolves case-insensitively to the family
PASS agent details: every variant listed, cheapest first (got array (
0 => 'agentic-app:single',
1 => 'agentic-app:extended',
), want array (
0 => 'agentic-app:single',
1 => 'agentic-app:extended',
))
PASS agent details: a variant carries its option value, its family, its own price and stock
PASS agent details: a variant title names the product and the license
PASS agent details: long description is the product copy
PASS agent details: a variant id returns that variant
PASS agent details: unknown id → null (got NULL, want NULL)
PASS agent details: unknown tier → null (got NULL, want NULL)
PASS agent details: an inactive product is unknown (got NULL, want NULL)
PASS agent details: cited comparisons become specs; an uncited row never reaches the agent
PASS agent details: no image → image_url null
PASS agent stock: family price follows the lowest sellable variant when the single goes unwired (got 99.0, want 99.0)
PASS agent stock: an LS variant id with a store name is sellable (URL built, not stored)
PASS agent stock: sales_live = 0 → nothing is in stock, prices still shown
PASS agent category: a catalog slug maps to its home-page section — SKIP (dist zip: a store-instance fact)
PASS agent category: an unlisted slug has none (got NULL, want NULL)
PASS agent cart: a fresh session has an empty cart (got array (
'items' =>
array (
),
'currency' => 'USD',
), want array (
'items' =>
array (
),
'currency' => 'USD',
))
PASS agent cart: adding twice merges into one line whose quantity is the sum
PASS agent cart: the line carries the variant id, title, price, option value and family
PASS agent cart: another session sees nothing (got array (
), want array (
))
PASS agent cart: update sets the quantity (got 1, want 1)
PASS agent cart: updating a line the cart does not hold leaves the cart as it is (got array (
0 => 'agentic-app:single',
), want array (
0 => 'agentic-app:single',
))
PASS agent cart: removing a line the cart does not hold leaves the cart as it is (got array (
0 => 'agentic-app:single',
), want array (
0 => 'agentic-app:single',
))
PASS agent cart: a second variant is a second line, in insertion order (got array (
0 => 'agentic-app:single',
1 => 'other-tool:single',
), want array (
0 => 'agentic-app:single',
1 => 'other-tool:single',
))
PASS agent cart: remove takes the line out (got array (
0 => 'other-tool:single',
), want array (
0 => 'other-tool:single',
))
PASS agent cart: remove the last line → empty (got array (
), want array (
))
PASS agent cart: a family id is refused (422 family) — the cart takes a variant
PASS agent cart: an unsellable variant is refused (409 unavailable) and the detail names ids only
PASS agent cart: an unknown id is refused (404 not_found)
PASS agent cart: quantity 0 is refused (422 quantity)
PASS agent cart: quantity above 99 is refused (422 quantity)
PASS agent cart: nothing was written by the refused calls (got array (
), want array (
))
PASS agent cart: the buyer the host resolved is recorded on the cart (got 42, want 42)
PASS agent cart: with sales_live = 0 nothing can be added (unavailable, no siblings in stock)
PASS agent prefs: no customer → a guest profile (got array (
'user_id' => 'guest',
'display_name' => 'Guest',
), want array (
'user_id' => 'guest',
'display_name' => 'Guest',
))
PASS agent prefs: an id that names nobody → a guest profile (got array (
'user_id' => 'guest',
'display_name' => 'Guest',
), want array (
'user_id' => 'guest',
'display_name' => 'Guest',
))
PASS agent prefs: a customer → their id and name (got array (
'user_id' => 'cust:6',
'display_name' => 'Agent Buyer',
), want array (
'user_id' => 'cust:6',
'display_name' => 'Agent Buyer',
))
PASS agent prefs: a customer without a name is "Customer", never their email (got array (
'user_id' => 'cust:7',
'display_name' => 'Customer',
), want array (
'user_id' => 'cust:7',
'display_name' => 'Customer',
))
PASS agent orders: a guest is asked to sign in (401 sign_in_required), not shown an empty list
PASS agent order: a guest is asked to sign in (401 sign_in_required)
PASS agent orders: the customer's own orders, newest first (got array (
0 => 'AG-ORDER-3',
1 => 'AG-ORDER-2',
2 => 'AG-ORDER-1',
), want array (
0 => 'AG-ORDER-3',
1 => 'AG-ORDER-2',
2 => 'AG-ORDER-1',
))
PASS agent orders: limit is honored (got 2, want 2)
PASS agent orders: paid → delivered (a download was issued), refunded → refunded (got array (
0 => 'refunded',
1 => 'delivered',
2 => 'delivered',
), want array (
0 => 'refunded',
1 => 'delivered',
2 => 'delivered',
))
PASS agent orders: placed_at is ISO 8601 in UTC
PASS agent orders: an item is the variant the order holds
PASS agent orders: total, currency, instant delivery, no tracking link
PASS agent orders: someone with no orders gets an empty list (got array (
), want array (
))
PASS agent order: one order resolves case-insensitively
PASS agent order: another customer's order is unknown (got NULL, want NULL)
PASS agent order: an unknown ref is null (got NULL, want NULL)
PASS agent account: a guest has none (got NULL, want NULL)
PASS agent account: active licenses and the products owned (got array (
'licences_active' => 1,
'products_owned' =>
array (
0 => 'agentic-app',
),
), want array (
'licences_active' => 1,
'products_owned' =>
array (
0 => 'agentic-app',
),
))
PASS agent account: a customer with nothing active (got array (
'licences_active' => 0,
'products_owned' =>
array (
),
), want array (
'licences_active' => 0,
'products_owned' =>
array (
),
))
PASS agent policies: "refund" leads with the refunds section of /legal, as plain text
PASS agent policies: license tiers are passages from the one place the terms are written
PASS agent policies: nothing matches → [] (got array (
), want array (
))
PASS agent policies: an empty query returns the standing corpus — three licenses and three legal sections, titled
PASS agent policies: a published guide is a passage, clipped
PASS agent policies: an unpublished guide never appears
PASS agent disclosure: a facts box per product — one row per license with price and sites
PASS agent disclosure: a license not on sale says so in its note
PASS agent disclosure: no comparisons → no sources, no footnotes
PASS agent disclosure: a cited comparison is a row with the read date and its source; an uncited one is not
PASS agent disclosure: unknown → null (got NULL, want NULL)
PASS agent handoff: an empty cart has nowhere to go (got array (
), want array (
))
PASS agent handoff: one hosted checkout per line, in cart order, labeled by product and license — the very URL the Buy button uses (LS custom data included) (got array (
0 =>
array (
'url' => 'https://example.lemonsqueezy.com/buy/abc?checkout%5Bcustom%5D%5Bsku%5D=agentic-app&checkout%5Bcustom%5D%5Btier%5D=single',
'label' => 'Pay for Agentic App — Single license',
),
1 =>
array (
'url' => 'https://example.lemonsqueezy.com/buy/def?checkout%5Bcustom%5D%5Bsku%5D=other-tool&checkout%5Bcustom%5D%5Btier%5D=single',
'label' => 'Pay for Other Tool — Single license',
),
), want array (
0 =>
array (
'url' => 'https://example.lemonsqueezy.com/buy/abc?checkout%5Bcustom%5D%5Bsku%5D=agentic-app&checkout%5Bcustom%5D%5Btier%5D=single',
'label' => 'Pay for Agentic App — Single license',
),
1 =>
array (
'url' => 'https://example.lemonsqueezy.com/buy/def?checkout%5Bcustom%5D%5Bsku%5D=other-tool&checkout%5Bcustom%5D%5Btier%5D=single',
'label' => 'Pay for Other Tool — Single license',
),
))
PASS agent handoff: the hosted URL is https and carries the sku the webhook fulfills by
PASS agent handoff: a line whose edition lost its checkout is left out rather than linked nowhere (got 1, want 1)
PASS agent handoff: an LS variant id + store name builds the hosted URL (never stored, always the Buy button's)
PASS agent handoff: the demo provider hands off to the store's own absolute checkout route (every edition is sellable there)
PASS agent door: controllers/agent.php exists
PASS agent door: index.php requires the controller
PASS agent door: index.php loads src/AgentDoor.php on every request (the suite loads it itself, a server does not)
PASS agent door: route GET /agent/v1/health is registered
PASS agent door: route GET /agent/v1/products is registered
PASS agent door: route GET /agent/v1/products/{pid} is registered
PASS agent door: route GET /agent/v1/cart is registered
PASS agent door: route POST /agent/v1/cart/add is registered
PASS agent door: route POST /agent/v1/cart/update is registered
PASS agent door: route POST /agent/v1/cart/remove is registered
PASS agent door: route GET /agent/v1/checkout-handoff is registered
PASS agent door: route GET /agent/v1/preferences is registered
PASS agent door: route GET /agent/v1/account is registered
PASS agent door: route GET /agent/v1/orders is registered
PASS agent door: route GET /agent/v1/orders/{oid} is registered
PASS agent door: route GET /agent/v1/policies is registered
PASS agent door: route GET /agent/v1/disclosure/{pid} is registered
PASS agent door: fourteen handlers are defined
PASS agent door: agent_health authenticates with the store's API key before anything else
PASS agent door: agent_products authenticates with the store's API key before anything else
PASS agent door: agent_product authenticates with the store's API key before anything else
PASS agent door: agent_cart authenticates with the store's API key before anything else
PASS agent door: agent_cart_add authenticates with the store's API key before anything else
PASS agent door: agent_cart_update authenticates with the store's API key before anything else
PASS agent door: agent_cart_remove authenticates with the store's API key before anything else
PASS agent door: agent_checkout_handoff authenticates with the store's API key before anything else
PASS agent door: agent_preferences authenticates with the store's API key before anything else
PASS agent door: agent_account authenticates with the store's API key before anything else
PASS agent door: agent_orders authenticates with the store's API key before anything else
PASS agent door: agent_order authenticates with the store's API key before anything else
PASS agent door: agent_policies authenticates with the store's API key before anything else
PASS agent door: agent_disclosure authenticates with the store's API key before anything else
PASS agent door: no handler asks requireKey for a scope of none (read-only keys must be refused on writes)
PASS agent door: no handler reads a user or customer id from the query or the body
PASS agent door: the session header is a 64-hex digest, validated before use
PASS agent door: the customer header is a positive integer, validated before use
PASS agent door: handlers never call csrf_check (the key is the credential, like /api/*)
PASS agent door: door errors map to their status code (guarded), never to a 500
PASS agent door: the search limit is clamped to the contract ceiling of 25
== OW4-9 CONNPREP: MCP tool annotations meet directory review ==
PASS CONNPREP: tools/list returns every tool (got 3, want 3)
PASS CONNPREP: EVERY tool carries a title (directory review rejects a missing one)
PASS CONNPREP: a read tool is annotated readOnlyHint (got true, want true)
PASS CONNPREP: a WRITE tool is annotated destructiveHint (it carried no annotation at all before) (got true, want true)
PASS CONNPREP: a write tool is never mislabeled read-only
PASS CONNPREP: a spec-supplied title wins over the derived one (got 'A hand-written title', want 'A hand-written title')
PASS CONNPREP: a derived title is the tool name made readable (got 'List things', want 'List things')
PASS CONNPREP: every shipped tool name is 64 characters or fewer ()
PASS CONNPREP: the store ships no catch-all api_request tool with a method parameter
== CLOUDMETERS: hosted in the door, intent on the list ==
PASS hosted: SKIPPED — no readable plane offering in this checkout
PASS hosted: a Cloud-card signup lands unconfirmed, tagged cloud, against the app slug
PASS hosted: unconfirmed is not mailable — the click is the consent, not the POST
PASS hosted: confirming makes it mailable for that app only
PASS hosted: the forward endpoint fails closed without a secret, uses hash_equals and is rate limited
PASS ATTRIB: campaignFromRequest reads the three UTMs and lower-cases them
PASS ATTRIB: a request with no UTMs is organic, not a campaign named empty
PASS ATTRIB: a UTM outside [A-Za-z0-9._-]{1,64} is dropped, not sanitized into something else
PASS ATTRIB: the campaign key is source|medium|campaign, and organic is the empty string
PASS ATTRIB: withCampaign respects an existing query string and leaves organic links alone
PASS ATTRIB: the campaign rides the SAME checkout[custom] channel the sku rides
PASS ATTRIB: an absent UTM sends NO key at all (an empty key would be a campaign named "")
PASS ATTRIB: organic checkout URLs are byte-identical to what they were before this unit
PASS ATTRIB: suiteCheckoutUrl now carries the signed-checkout guard its sibling always had
PASS ATTRIB: the campaign comes home off the webhook, lower-cased, junk keys ignored
PASS ATTRIB: a webhook with no campaign yields organic, never a partial key
PASS ATTRIB: orders carries the campaign column, defaulted so every existing row reads organic
PASS ATTRIB: both buy routes pass the request campaign into the checkout builder
PASS ATTRIB: store_product.php hangs the campaign on every buy link it renders
PASS ATTRIB: the flagship view — SKIP (dist zip: a view package.sh holds back)
PASS ATTRIB: suite_page.php hangs the campaign on every buy link it renders
Warning: file_get_contents(tests/../bin/export-attribution.php): Failed to open stream: No such file or directory in tests/run.php on line 4143
PASS ATTRIB: the attribution export selects no personal column (an order id is not a person)
PASS M6: the three beacon kinds resolve to 204
PASS M6: any other kind resolves to 404, so the log cannot be seeded with invented stage names
PASS M6: the kinds the store publishes are exactly the stages the funnel defines
PASS M6: the beacon writes nothing — no DB, no session, no cookie in its own body
PASS M6: the route is registered
PASS M6: the handler now answers the bare 'close' string lemon.js forwards on dismissal
PASS M6: a close carries how long the overlay was open, so the <10s split is answerable
PASS M6: opening beacons too, and the edition id is taken from the buy URL rather than re-derived
PASS M6: Checkout.Success still redirects to /thanks — the existing behavior is not traded away
PASS M6: the whole instrument sits inside the checkoutOverlay() block — dormant while the flag is 0
PASS M7: every path in the trust set is a real route on this store
PASS M6: the beacon URL the overlay builds matches the route the router compiles
PASS N9: the continuity clause is found in LICENSE.txt and is not empty
PASS N9: the quoted clause carries the successor binding the other copies dropped
PASS N9: it carries the license's own MIT wording, not legal.php's paraphrase
PASS N9: it is BYTE-FOR-BYTE the license — every sentence appears in LICENSE.txt itself
PASS N9: a license with no continuity heading yields null, never a guess
PASS D8: a product not on Cloud gets no sentence rather than an invented one
PASS D8: a product with no price gets no sentence
PASS D8: the year is twelve times the LIVE monthly, computed here and not typed
PASS D8: the sentence states the trade in the license's favor, not the subscription's
PASS D8: no hardcoded Cloud multiple or money figure in the new methods
PASS N8: a product with no collected artifact renders no peek at all
PASS N8: a collected peek carries a real tree and names the file it excerpts
PASS N8: the peek lists the tree as the ZIP ships it — no packager-deleted leftovers
PASS N8: the excerpt is never one of the shared cores
PASS N8: the page prints the rule it used to choose the file, rather than asking for trust
PASS TRUSTBLOCK: all three blocks render on the product page
PASS TRUSTBLOCK: the clause and the ladder sit AT the buy area — after the trust list, before the gallery
PASS N8: the peek renders BELOW the demo CTA, so it cannot eat the agent-first 2KB
PASS TRUSTBLOCK: no JavaScript — details/summary, the same pattern the FAQ uses
PASS TRUSTBLOCK: every collected value is escaped before it reaches the page
PASS TRUSTBLOCK: the continuity clause block stays OFF the buy area (ruling 2026-09-15)
== OW4-20 CATMCP: the public unkeyed catalog MCP ==
PASS CATMCP: exactly the seven tools (M1's five, plus compare and guides) (search_catalogue product quickstart checkout_link hosted_offer compare guides)
PASS CATMCP: NOT ONE tool is a write tool ()
PASS CATMCP: the spec declares scope=read (got 'read', want 'read')
PASS CATMCP: a write tool added to this door is NOT LISTED (scope=read refuses it)
PASS CATMCP: …and CALLING it is refused, not merely hidden
PASS CATMCP: all seven list (got 7, want 7)
PASS CATMCP: every listed tool carries a title AND readOnlyHint
PASS CATMCP: every tool name is 64 chars or fewer
PASS CATMCP: no handler selects from an orders/customers/licence/key table ()
PASS CATMCP: the door reads only products/editions
PASS CATMCP: the keyed /mcp door still requires a key (this unit added a door, it did not open one)
PASS CATMCP: the public door is registered on its own path, not on /mcp
PASS CATMCP: the POST route is rate limited through the existing limiter
PASS CATMCP: the limiter uses its own bucket, so it cannot exhaust another surface
PASS CATMCP: a GET learns the method WITHOUT being rate limited first (B13 lesson, comments stripped)
PASS CATMCP: the comment stripper is doing real work here
PASS CATMCP: GET answers 405 (got 405, want 405)
PASS CATMCP/GET: an Accept naming text/html is treated as a browser
PASS CATMCP/GET: an explicit JSON or SSE Accept keeps the byte-identical JSON error
PASS CATMCP/GET: a generic or empty Accept gets the page (link scanners, curious humans)
PASS CATMCP/GET: the page says what the door is
PASS CATMCP/GET: the page tells a human where to point an agent
PASS CATMCP/GET: the page links /connect-your-ai
PASS CATMCP/GET: the page is noindex — it is an endpoint, not a marketing page
PASS CATMCP/GET: every tool in the spec appears on the page ()
PASS CATMCP/GET: the count in the prose is computed, not typed
PASS CATMCP/GET: the tool name is escaped with e() before printing
PASS CATMCP/GET: the tool description is escaped with e() before printing
PASS CATMCP/GET: the POST handler is untouched — it still rate-limits then hands to Mcp::handle
PASS CATMCP/GET: nothing about HTML leaked into the POST path
PASS CATMCP/GET: the GET route still answers 405 and still sends Allow: POST
PASS CATMCP: search_catalogue returns products for a real word
PASS CATMCP: product returns the record for a real slug — SKIP (dist zip: a store-instance fact)
PASS CATMCP: product refuses an unknown slug with a readable error
PASS CATMCP: quickstart says so rather than returning an empty string
PASS CATMCP: hosted_offer lists the hosted products
PASS CATMCP: hosted_offer answers honestly for a product Cloud does not host
== VENDORRISK (2026-09-20): /if-we-disappear — the five questions, both shapes ==
PASS /if-we-disappear carries 'any legal successor'
PASS /if-we-disappear carries 'MIT license'
PASS /if-we-disappear carries 'no license server'
PASS /if-we-disappear carries 'escrow'
PASS /if-we-disappear carries 'mysqldump'
PASS /if-we-disappear carries 'What support covers'
PASS /if-we-disappear carries '/legal#continuity'
PASS /if-we-disappear carries 'does not phone home'
PASS vendor_risk neutral shape: no dated re-check line is invented
PASS vendor_risk neutral shape: the export sentence is not inherited
PASS vendor_risk neutral shape: the universal rows stand
PASS view vendor_risk.php hard-codes no absolute host — URLs come from $B() or App::url
PASS index.php routes /if-we-disappear
PASS index.php routes the guessed /vendor-risk to the alias
PASS index.php routes the guessed /continuity to the alias
PASS llms.txt advertises /if-we-disappear (C-T1)
== W3-U2: conditional requests on the public pages (Cache-Control + Last-Modified + ETag + 304) ==
PASS U2: a listing plan carries Cache-Control public max-age=600, an ETag and a Last-Modified
PASS U2: a content-page plan carries Cache-Control public max-age=1800 (the ruled 300-3600 range)
PASS U2: If-None-Match with the page's own ETag answers 304 (and a different ETag answers 200)
PASS U2: If-Modified-Since after the page date answers 304, before it answers 200
PASS U2: a page with NO stored date sends no Last-Modified — the clock is never invented
PASS U2: public_validators() adds the validators and leaves Cache-Control to the route (crawler files)
PASS BUILD: a build newer than the page date moves Last-Modified to the build, so a refresh holding the old date gets 200
PASS BUILD: a refresh already holding the build's date still gets the cheap 304
PASS BUILD: a page date newer than the build keeps its own date
PASS BUILD: a new build is a new ETag; no build (0) leaves the ETag exactly as before
PASS BUILD: no stamp in this tree, so app_build_ts() is 0 and nothing here moves
PASS EDGE: every public Cache-Control tells the shared (Cloudflare) cache to re-ask us every time (s-maxage=0), so the origin log sees every visit
PASS BUILD: deploy-store.sh writes .build-stamp after the sync and before the image is rebuilt
PASS U2: /guides renders in full, then answers a matching If-None-Match with an EMPTY body
PASS U2: /llms.txt answers a matching If-None-Match with no page content, its crawler_cache(3600) untouched
PASS U2: all fourteen named public routes answer conditional requests (got array (
), want array (
))
PASS agencies: views/agencies.php is ownware.io's own and absent from this build (dist zip) - skipped
PASS U2: nothing outside the public-page allow-list is cacheable (no checkout/cart/account/portal/licence route) (got array (
), want array (
))
PASS U2: the named checkout/portal/licence/admin handlers carry no public cache header at all (got array (
), want array (
))
PASS U2: every crawler_cache() second is exactly what it was before this wave (26 calls) (got array (
0 =>
array (
60 => 1,
300 => 2,
1800 => 3,
3600 => 17,
86400 => 2,
604800 => 1,
),
1 => 26,
), want array (
0 =>
array (
60 => 1,
300 => 2,
1800 => 3,
3600 => 17,
86400 => 2,
604800 => 1,
),
1 => 26,
))
== W3-U3: honest schema on the /alternatives pages (Product + Offer + dated WebPage + a real FAQ) ==
PASS U3: the catalog offers an /alternatives page whose product carries a stored FAQ (fixture found) — SKIP (dist zip: a store-instance fact)
PASS U3: the alternatives page's JSON-LD parses and carries BreadcrumbList + Product + an Offer — SKIP (dist zip: a store-instance fact)
PASS U3: the Product node is the SAME node as on /p/{slug} — one @id, not two products — SKIP (dist zip: a store-instance fact)
PASS U3: dateModified on the page node equals the newest STORED price-check date behind the page (got '', want '')
PASS U3: the rival is named as text only — the page's about Thing and its description, never our Product — SKIP (dist zip: a store-instance fact)
PASS U3: FAQPage is present, and every marked-up question is VISIBLE on the page — SKIP (dist zip: a store-instance fact)
PASS U3: with no visible Q/A the page carries NO FAQPage (the markup never invents a question) — SKIP (dist zip: a store-instance fact)
PASS U3: no AggregateRating and no Review anywhere on an alternatives page — we have none to show
== W3-U4: the /vs comparison pages — Product + Offer + the visible as-of stamp ==
PASS U4: the catalog has a /vs page whose cited rows carry a verification date (fixture found)
PASS U4: the /vs JSON-LD parses with Product (the same @id as /p/{slug}) + an Offer + BreadcrumbList
PASS U4: the VISIBLE as-of stamp, the stored verification date and dateModified are one value (got array (
0 => '2026-09-01',
1 => '2026-09-01',
), want array (
0 => '2026-09-01',
1 => '2026-09-01',
))
PASS U4: the as-of anchor id exists and points at the Sources list it is citing
PASS U4: the compared product is named as text (the about Thing), never as our Product or Offer
== W3-U5: the /vs index — the 50 comparison pages finally have one ==
PASS U5: /vs answers through the bare-slug route (200, not the 404 page) and lists every comparison page
PASS U5: the index's ItemList holds one item per comparison page, each a real /vs/{slug} URL (got array (
0 => 1,
1 => 1,
2 => 1,
), want array (
0 => 1,
1 => 1,
2 => 1,
))
PASS U5: /vs is canonical to itself, carries a dated WebPage, and is in the sitemap
PASS U5: every /vs/{slug} page and the store footer link the index
PASS U5: a bare slug that is neither product, suite nor the index still 404s
PASS U5: vs_index() is one of the conditional public routes (U2's allow-list member)
== W3-U6: internal links — the related blocks, the guides' products, the orphan, the link ceiling ==
PASS U6: every /alternatives and every /vs page gets 3-5 related links, none rival-to-rival, none empty (got array (
0 => true,
1 => true,
2 => 0,
3 => 0,
), want array (
0 => true,
1 => true,
2 => 0,
3 => 0,
))
PASS U6: the block is RENDERED on an alternatives page and on a /vs page, with its links present — SKIP (dist zip: a store-instance fact)
PASS U6: the related links are DERIVED, not random — the same page twice gives the same block
PASS U6: a guide links every product its body names, as a real /p/{slug} anchor
PASS U6: no content page (product, guide, alternatives, vs — worst case of each) carries more than 150 anchors (got array (
), want array (
))
PASS U6: the footer links all five indexes — /vs, /alternatives, /guides, /self-hosted, /suites
PASS U6: /docs — the store's one orphan — is now linked from every page's footer
== W3-U7: page weight — inline script out of the page, every image dimensioned, /guides under 80 KB ==
PASS U7: the home page's inline JavaScript is under 8 KB (was 8,673 B in six blocks)
PASS U7: the two home scripts are deferred files under assets/js/, plain and fetch-free, and the page loads them — SKIP (dist zip: a store-instance fact)
PASS U7: the behavior the scripts drive is still on the page — the filter, the FREE toggle, the chips, the carousel — SKIP (dist zip: a store-instance fact)
PASS U7: every <img> on the home page carries width and height, read from the file itself — SKIP (dist zip: a store-instance fact)
PASS U7: every home image below the fold is lazy-loaded
PASS U7: the dimensions are READ, never typed — a missing file yields no attribute at all — SKIP (dist zip: a store-instance fact)
PASS U7: /guides is under 80 KB of HTML and still links every published guide
PASS U7: /guides leads its title with the category set W2 measured it missing
PASS U7: home HTML stays under its measured ceiling of 240 KB (the 61 KB OfferCatalog is 26% of it)
== W3-U8: sitemap lastmod = each page's own newest dated fact, never the build date ==
PASS U8: every sampled page's <lastmod> is exactly the date its own Last-Modified is built from — SKIP (dist zip: a store-instance fact)
PASS U8: no <lastmod> in the sitemap is in the future or malformed, and the undated pages carry none — SKIP (dist zip: a store-instance fact)
PASS U8: the Last-Modified the route sends decodes back to the very date the sitemap published (got array (
), want array (
))
PASS U8: the four-date clustering W1 measured is gone — the sitemap now carries many distinct dates — SKIP (dist zip: a store-instance fact)
== W3-U8b: the two buyer-question pages join the sitemap ==
PASS U8b: /if-we-disappear and /what-self-hosting-costs are in the sitemap, each with its own checked date
== W3-U9: llms.txt — every guide, comparison and alternatives page, each with its date ==
PASS U9: the four new sections exist, with one line per page — counts read from the database, not typed (got array (
0 => 1,
1 => 1,
2 => 1,
3 => 1,
), want array (
0 => 1,
1 => 1,
2 => 1,
3 => 1,
))
PASS U9: every guide line carries its updated date and every comparison line its as-of date
PASS U9: every alternatives line names the Ownware product that replaces the rival
PASS U9: every page URL in the three new inventories is published in the sitemap (got array (
), want array (
))
PASS U9: /vs — absent from llms.txt entirely before this wave — is advertised, and the route keeps crawler_cache(3600)
PASS U9: the new lines are US English and name no revenue
PASS U9: /llms.txt stays under 96 KB and carries a line for every guide, comparison and alternative page
== FIX WAVE 2026-09-24: the cookieless visitor count ==
PASS HIT: POST /hit is routed to visitor_hit
PASS HIT: the endpoint answers with an empty body (204) and stores nothing
PASS HIT: every store page loads the first-party signal.js that sends the ping
PASS HIT: the ping skips automated browsers, sends only the path and the referring host, and omits credentials
PASS HIT: the privacy page says the store counts its own page views, with no identifier and no cookie
== W3-U2b: Content-Signal as a response header on every public page ==
PASS U2b: one helper emits the robots line as a header, with the three ruled values verbatim
PASS U2b: every public response passes through it — the crawler files via crawler_cache(), the pages via public_cache_apply() — and nothing else emits it
== W3-U3b: meta descriptions cut at a sentence boundary, never mid-word ==
PASS U3b: a description that fits is untouched; a long one is cut at the sentence end; a hand-written one is exempt
PASS U3b: with no sentence end in range it cuts at a WORD boundary inside the limit, marked, and a decimal point is not a sentence end
PASS U3b: on every template — home, product, vs, alternatives, guide, and the six indexes — no description runs past 155 characters — SKIP (dist zip: a store-instance fact)
PASS U3b: and none of them ends mid-sentence — every one closes on a sentence or a marked cut — SKIP (dist zip: a store-instance fact)
== W3-U6b: links into the robots-disallowed paths are nofollowed; /demo/ answers noindex ==
PASS U6b: the filter marks internal /demo/ and /buy/ anchors, merges an existing rel, is idempotent, and never touches an external /buy/ URL
PASS U6b: on the rendered home, product and comparison pages, every /demo/ and /buy/ anchor is nofollowed (got array (
0 => true,
1 => 0,
), want array (
0 => true,
1 => 0,
))
PASS U6b: /demo/{slug} answers X-Robots-Tag: noindex, and robots.txt still disallows both paths
PASS U6b: the flagship template is the single documented exception — named in the layout, and its own buy anchors left as the existing assertions match them
PASS U6b: the checkout exception is honored — buy_redirect() sets no response header of ours
== W3FU-FU2: the two vendor-risk pages leave the ADMIN layout ==
PASS FU2: both pages render in the STORE layout — 0 /admin links, 0 "Sign out", exactly one notify form, the store header and footer present (got array (
), want array (
))
PASS FU2: every content byte the views carry is still on the page
PASS FU2: Last-Modified, the ETag and the sitemap all come from the page's own checked date (got array (
0 => false,
1 => '',
2 => '',
), want array (
0 => false,
1 => '',
2 => '',
))
PASS FU2: both answer a matching If-None-Match with no page content, keeping crawler_cache(3600)
PASS FU2: neither page pulls in views/layout.php any more — the navlien redeclare that aborted this suite cannot happen
== W3FU-FU1: the gate cannot print PASSED on a fatal ==
PASS FU1: the fatal handler inspects error_get_last() for the four fatal types and exits 2
PASS FU1: it is registered BEFORE the summary handler, so a fatal can never reach the PASSED line
== W3FU-FU3: the eighteen public pages U2's list did not name ==
PASS FU3: all eighteen handlers are now cacheable — none was before this wave (got 18, want 18)
PASS FU3: every one of the eighteen calls public_cache() in its own body (got array (
), want array (
))
PASS FU3: the dated pages send public max-age + the page's own Last-Modified + an ETag, and answer a match with an empty 304 — SKIP (dist zip: a store-instance fact)
PASS FU3: the nine static pages are cacheable for an hour and claim neither a date nor an ETag — SKIP (dist zip: a store-instance fact)
PASS FU3: a plan with no seed carries no ETag, so no request can be answered 304 by accident
PASS FU3: /privacy/{slug} — the printed update date is what the validator carries (got '2026-08-03', want '2026-08-03')
PASS FU3: /manifest.json is cacheable for a day, and the two exiting routes are wired at source
== W3FU-FU4: the directory strip's rel values, from the vendor reads ==
PASS FU4 strip checks skipped (dist zip build - store_instance.php absent)
== W3FU-FU5: the measured retitles (twenty until 2026-09-26; eighteen since O1 lifted p:complia and p:safora to the copy file) ==
PASS FU5: all eighteen measured pages render a title, and the map holds exactly eighteen keys (got array (
0 => 18,
1 => 18,
2 => 0,
), want array (
0 => 18,
1 => 18,
2 => 0,
))
PASS FU5: every one of the eighteen <title> elements is 60 characters or fewer (got array (
), want array (
))
PASS FU5: every one of them is US English (got array (
), want array (
))
PASS FU5: the category word on the one remaining "category first" row (tokora; complia and safora lifted to the copy file 2026-09-26) is the label of the /self-hosted page that lists the product — SKIP (dist zip: a store-instance fact)
PASS FU5: the counts and the date inside the index titles are READ from the page's own data, never typed
PASS FU5: a page outside the twenty keeps the title it had — the blast radius is the measured set — SKIP (dist zip: a store-instance fact)
PASS FU5: the price record and the report carry their dates in the graph — Dataset dateModified and a Report with datePublished
== W3c stateless CSRF ==
PASS W3c: the stateless token has the ruled shape s0.<bucket>.<64 hex>
PASS W3c: it verifies at the instant it was minted
PASS W3c: it verifies across its whole 12 h bucket, first second to last
PASS W3c: it still verifies through the NEXT bucket — a rendered form lives 12 to 24 hours
PASS W3c: at the 24 h mark it is refused — two buckets on is out of the window
PASS W3c: another install's secret cannot verify it, and an EMPTY secret verifies nothing
PASS W3c: one altered hex digit is refused
PASS W3c: the empty string, a bare prefix and a malformed bucket are all refused
PASS W3c: a SESSION token is never accepted by the stateless check
PASS W3c: csrf_token() mints a session token while a session is active — unchanged for every other form
PASS W3c: only /notify can take the stateless branch, and only for an s0. token
== HONESTCOSTS (2026-09-20): /what-self-hosting-costs — the buyer's half of the price ==
PASS /what-self-hosting-costs carries 'you are the sysadmin'
PASS /what-self-hosting-costs carries 'mysqldump'
PASS /what-self-hosting-costs carries 'When NOT to buy from us'
PASS /what-self-hosting-costs carries 'rather lose that sale'
PASS /what-self-hosting-costs carries '/if-we-disappear'
PASS /what-self-hosting-costs carries 'What support covers'
PASS /what-self-hosting-costs carries '/calculator'
PASS /what-self-hosting-costs carries 'no queue workers'
PASS /what-self-hosting-costs types NO money figure
PASS honest_costs neutral shape: no dated line invented
PASS honest_costs neutral shape: the universal rows stand
PASS view honest_costs.php hard-codes no absolute host
PASS index.php routes /what-self-hosting-costs
PASS index.php routes the two guessed cost addresses
PASS llms.txt advertises /what-self-hosting-costs (C-T1)
== AGENT ACCESS (2026-09-24): markdown pages, the catalog MCP's card and its two new tools ==
PASS AGENT ACCESS store-instance checks skipped (dist zip build - store_instance.php absent)
== AGENT U2: markdown on the public content pages -- the branch, the handlers, the builders ==
PASS AGENT U2: ten addresses answer markdown, each named with its handler (got 10, want 10)
PASS AGENT U2: every listed handler asks wanted(), answers before public_cache(), and returns straight after send() (got array (
), want array (
))
PASS AGENT U2: each listed address is routed to the handler the list names (got array (
), want array (
))
PASS AGENT U2: guide_show answers a draft 404 BEFORE the markdown branch (a draft is a 404 in both formats)
PASS AGENT U2: wanted() is true only when markdown is named and wins on q
PASS AGENT U2: send() under CLI prints exactly the body and records what a server would send
PASS AGENT U2: off the CLI, send() still sends every header from headers() and exits
PASS AGENT U2: datedRows() keeps cited AND dated rows only, in order (got array (
0 => 'Dated Rival',
1 => 'Gone Rival',
), want array (
0 => 'Dated Rival',
1 => 'Gone Rival',
))
PASS AGENT U2: /vs markdown drops the undated and the uncited row and keeps the dated ones
PASS AGENT U2: a withdrawn source is said to be withdrawn and its address is never printed
PASS AGENT U2: a pipe inside a vendor price cannot split the table cell
PASS AGENT U2: money keeps its cents ($49.50) and drops only a round .00
PASS AGENT U2: a guide is its title, description, address and written date, then the stored body as-is
PASS AGENT U2: llms.txt lists every markdown address from the one list, and no longer says "any address"
PASS AGENT U2: the catalog markdown points an agent at every other markdown address
== AGENT U3: a markdown reply is never cacheable; the HTML twin's cache headers are untouched ==
PASS AGENT U3: every markdown reply says Cache-Control: private, no-store (the net under the zone's bypass rule)
PASS AGENT U3: /guides as markdown carries private, no-store and computes no public cache plan (no ETag, no Last-Modified)
PASS AGENT U3: /guides as HTML keeps exactly its public plan: max-age=600, an ETag, a Last-Modified, and no markdown reply
PASS AGENT U3: off the CLI, send() adds the Content-Signal the HTML twin carries, from the one helper, inside the headers_sent() guard
== AGENT U4: the catalog MCP's server card at /.well-known/mcp/server-card.json ==
PASS AGENT U4: the card's tool names are the live tools/list names, in order (got array (
0 => 'search_catalogue',
1 => 'product',
2 => 'quickstart',
3 => 'checkout_link',
4 => 'hosted_offer',
5 => 'compare',
6 => 'guides',
), want array (
0 => 'search_catalogue',
1 => 'product',
2 => 'quickstart',
3 => 'checkout_link',
4 => 'hosted_offer',
5 => 'compare',
6 => 'guides',
))
PASS AGENT U4: the card's version is serverInfo.version (got '1.1.0', want '1.1.0')
PASS AGENT U4: serverInfo, capabilities and protocol are the server's own initialize answer
PASS AGENT U4: the description is the server's own instructions, with the product count read from the DB
PASS AGENT U4: every tool carries its title, description and read-only annotation, as tools/list gives them
PASS AGENT U4: transport streamable-http at this store's own /catalogue/mcp, no authentication, docs at the door
PASS AGENT U4: the route is registered beside the door, and its pattern matches the address and nothing near it
PASS AGENT U4: the route answers JSON, cacheable like the other discovery files, readable cross-origin
PASS AGENT U4: the served body parses and is the card
PASS AGENT U4: the surface table lists the door and its card (so /docs and /for-agents carry them)
PASS AGENT U4: llms.txt advertises the catalog MCP and its card (C-T1)
PASS AGENT U4: /.well-known/agent.json lists the catalog MCP and its card beside the keyed doors
PASS AGENT U4: the agent card names the markdown addresses from the one list instead of "any address"
== AGENT U5: the catalog MCP's compare and guides tools (1.1.0) ==
PASS AGENT U5: compare and guides are listed, each with a title and readOnlyHint
PASS AGENT U5: the server says 1.1.0, and the card says what the server says (got array (
0 => '1.1.0',
1 => '1.1.0',
), want array (
0 => '1.1.0',
1 => '1.1.0',
))
PASS AGENT U5: one version literal, read by the spec and by the GET stub
PASS AGENT U5: compare returns every cited, dated row the /vs page shows (sc-invoicer: 1)
PASS AGENT U5: every compare row carries its read date, and a source address unless the page is withdrawn
PASS AGENT U5: compare on a product with no cited rows returns an empty list and the reason (uikit-pro)
PASS AGENT U5: compare refuses an unknown slug with a readable error
PASS AGENT U5: guides lists the published guides, and only those (1)
PASS AGENT U5: guides honors its limit and still reports the total
PASS AGENT U5: guides filtered by a word ('sc') returns matches only, and at least the guide it came from
PASS AGENT U5: guides by slug returns that guide in full, its body as stored
PASS AGENT U5: an unpublished guide returns nothing -- no title, no body -- by slug or by search
PASS AGENT U5: compare reads the /vs rows through comparisonsFor and datedRows; guides reads only through the published-guide helpers
PASS AGENT U5 store-instance checks skipped (dist zip build - store_instance.php absent)
== SEO JOB 2 (2026-09-26, research 08 §11 #1 #5 #6 #12 #13 #17 #18) ==
PASS J2 #6: robots.txt disallows /buy-suite/ beside /buy/ and /demo/
PASS J2 #1: the four shipped documents of a product with a tree are in the sitemap, dated with the product
PASS J2 #1: a product without a shipped document lists no document URL (sc-booking has no tree)
PASS J2 #12: /support is in the sitemap
PASS J2 #12: /changelog carries a lastmod = the newest shipped edition or release note (pageDate changelog)
PASS J2 #12: the state report carries the price record's checked date (the date its route answers with)
PASS J2 #12 (U8 kept): pages with no stored dated fact stay undated — /faq, /legal, /security, /docs, /status
PASS J2 #1: the API page title is product name + page type (got 'SC Invoicer REST API and webhooks: reference', want 'SC Invoicer REST API and webhooks: reference')
PASS J2 #1: the manual page title is product name + page type (got 'SC Invoicer manual: setup, settings and daily use', want 'SC Invoicer manual: setup, settings and daily use')
PASS J2 #1: the quickstart page title (got 'SC Invoicer quickstart: install in minutes', want 'SC Invoicer quickstart: install in minutes')
PASS J2 #1: the test-run page title carries the count from proof.json, never a typed one (got 'SC Invoicer test run: 140 checks, as shipped', want 'SC Invoicer test run: 140 checks, as shipped')
PASS J2 #1: the API description names the product and the agent line; the test-run description carries 140 passed / 0 failed
PASS J2 #1: the four titles differ from each other and from the product page's
PASS J2 copy rule: none of guaranteed/must/should/compliant/eligible/required/obliged in a document title or description (got 0, want 0)
PASS J2 #17: a document page ships BreadcrumbList Home › product › API ending at /p/sc-invoicer/api, plus the Organization node
PASS J2 #13: a ##-only document renders h2/h3 (top level normalized), a # inside a fence is not a heading
PASS J2 #13: a #-topped document renders exactly as before (h2 / h3 / h4)
PASS J2 #13 (07 #8): on a guide written with ## sections the first heading after the h1 is an h2, not an h3
PASS J2 #5: guide Article author = Organization @id, never a Person; image set; publisher carries logo + @id
PASS og-home: the share image URL is stamped with the file's time (?v=<mtime>), so the CDN never serves an old card
PASS J2 #5: the guide page ships the Organization node the Article's @id points at (same graph)
PASS J2 #17: /s/{slug} ships BreadcrumbList Home › Suites › suite ending at /s/j2-suite, plus the Organization node, beside the Product node
PASS J2 #18: the product page's JSON-LD carries no shippingDetails and no hasMerchantReturnPolicy
== SPEED follow-up (2026-09-26, O1 08:4x): every store image the home page serves is a WebP variant ==
PASS SPEED: no image under assets/store/ on the home page is served as the original JPEG (all are -400/-800 WebP) (got array (
), want array (
))
PASS SPEED (store): the flagship block on the home page serves its 800 px WebP shot and the variant file exists — SKIP (dist zip: a store-instance fact)
== SEO COPY WIRE-IN (2026-09-26): 5.1-c's titles/descriptions from views/seo.local.csv; /vs descriptions built at render time ==
PASS COPY: a row is found by PATH whatever the host, from a full URL or a bare path
PASS COPY: a page with no row gets null
PASS COPY: the product page <title> is the copy row (got 'Wire Probe — a title from the copy file', want 'Wire Probe — a title from the copy file')
PASS COPY: the product page description is the copy row, verbatim (got 'A description from the copy file for the product page.', want 'A description from the copy file for the product page.')
PASS COPY: og:title and og:description carry the same copy
PASS COPY: a product with no row keeps its own title and description
PASS COPY: an ORGANIC measured title stays on top of the file; the file still supplies the description
PASS COPY: a controller-built description (desc_dynamic) is never replaced by the file
PASS COPY: the guide <title> is the copy row (got 'Live guide: a shorter title from the copy file', want 'Live guide: a shorter title from the copy file')
PASS COPY: the guide h1 and the Article headline keep the STORED title (the file changes the tag, not the guide)
PASS COPY (store): the /self-hosted page <title> is the copy row — SKIP (dist zip: a store-instance fact)
PASS COPY (store): the /self-hosted description is the copy row — SKIP (dist zip: a store-instance fact)
PASS COPY /vs: the description is BUILT from the row the page prints — rival, its first priced clause, the read date, our price, then the file's tail (got 'RivalCo Starter $12 per user/month billed annually, read 2026-09-20. Wire Probe: $99.00 once, self-hosted, full source, no per-user fee.', want 'RivalCo Starter $12 per user/month billed annually, read 2026-09-20. Wire Probe: $99.00 once, self-hosted, full source, no per-user fee.')
PASS COPY /vs: the price clause, the read date and our price in the description are printed on the page itself
PASS COPY /vs: ≤ 155 characters
PASS COPY /vs: the <title> is the copy row (got 'Wire Probe vs RivalCo: one-time wiring', want 'Wire Probe vs RivalCo: one-time wiring')
PASS COPY /vs: a price refresh in the data moves the description with the page — the file's example price never shows
PASS COPY /vs: an over-long price text is cut at word boundaries to keep the description ≤ 155
PASS COPY /vs: without a copy row the tail is the plain one
PASS COPY (store): views/seo.local.csv parses — 230 rows, every title ≤ 60, every description ≤ 155, no duplicate url/title/description, only /p /vs /self-hosted /guides paths (and the two indexes), no banned word, every product /vs row carries an "once, " tail — SKIP (dist zip: a store-instance fact)
PASS COPY (store): tools/package.sh holds back views/seo.local.csv beside legal.local.php — SKIP (dist zip: a store-instance fact)
PASS ST setup: a paid order through the webhook issued a license for the test product
== ST-1. license-gated update check (POST /license/update) ==
PASS same version: valid, no update, no download link
PASS newer version on the edition: update_available with a /d/ link and its expiry
PASS a second check hands back the same usable link (no link minted per poll)
PASS once the order's links are used up, a fresh link is minted
PASS the minted link resolves to the edition's file
PASS "v"-prefixed versions compare as versions (v1.0.9 < 1.1.0)
PASS an installed version newer than the store's is not an update
PASS no version sent: the latest version and a download link, update_available false
PASS a domain the license is not activated on is refused (got 'not_activated', want 'not_activated')
PASS the same call from an activated domain answers
PASS wrong sku -> invalid_key, as an unknown key (4.0 A4, F5: the endpoint is no oracle) (got 'invalid_key', want 'invalid_key')
PASS forged key -> invalid_key (got 'invalid_key', want 'invalid_key')
== ST-1a. one key (4.0 A4, the operator's ruling D-A4-1: the store's key and the Lemon Squeezy key both work) ==
PASS A4: the Lemon Squeezy key recorded for a license answers for its edition, as the store's own key does
PASS A4 (F5): a key for another product, either kind, answers exactly as an unknown key (got array (
0 => 'invalid_key',
1 => 'invalid_key',
2 => 'invalid_key',
), want array (
0 => 'invalid_key',
1 => 'invalid_key',
2 => 'invalid_key',
))
PASS A4: an empty key never matches a license with no Lemon Squeezy key recorded
PASS A4 (F5): a refunded order's keys, both kinds, answer as unknown
PASS A4: one Lemon Squeezy key on a two-product order answers for each product in it
PASS A4: that key for a product outside the order still answers as an unknown key (got 'invalid_key', want 'invalid_key')
== ST-1b. signed manifests on /license/update (the 4.0 in-app updater) ==
PASS an edition with no stored manifest: the link as before, manifest and signature null
PASS release day: a manifest signed offline by a release key, for exactly this file, is accepted
PASS …signed by another key: refused
PASS …for another sku or version: refused
PASS …describing another file: refused
PASS …no release keys configured: refused
PASS the stored manifest and signature are handed to the updater with the link, byte for byte
PASS no update, so no link: no manifest either
PASS a refused license carries no manifest
PASS an edition with no file: still valid, says no_download, gives no link
== ST-1c. the handoff on the order page and the receipt (4.0 A4 Task 3) ==
PASS A4: a film is read from legal.local only when its address is https, and only for its product
PASS A4: a release without the Update button (no signed manifest) and no film: neither line, on the page or in the receipt
PASS A4: a signed 4.0 release: the order page and the receipt say where the key goes and that the app never checks by itself
PASS A4: a published film: its title, minutes and https link on the order page, and "Watch it done" with the link in the receipt
PASS A4: a resent receipt keeps the Updates line
PASS A4: a film whose address is not https is left out of the page
== ST-1d. Lemon Squeezy test-mode orders are never sales (4.0 A4) ==
PASS A4: test orders are off by default (a new column, 0)
PASS A4: a signed Lemon Squeezy TEST-mode purchase is answered and ignored: no order, no license
PASS A4: a real (live-mode) purchase is recorded and fulfilled as before
PASS A4: a test-mode refund cannot touch a real order
PASS A4: an unsigned test-mode payload is still refused (the guard sits after the signature check)
PASS A4: with test orders switched on (while an owner sets a store up), a test-mode purchase fulfills
PASS A4: Settings offers the switch, and saving the form writes it
== ST-1e. Lemon Squeezy's own license key is recorded (4.0 A4, D-A4-1; the real event of 2026-09-29) ==
PASS A4: a paid Lemon Squeezy order keeps Lemon Squeezy's order number (the license event names the order by it)
PASS A4: license_key_created records the key on the order's license, in capitals
PASS A4: the recorded key answers for its product in either case, and the store's own key still does
PASS A4: a key that arrives before its order waits, and the order takes it when it is fulfilled
PASS A4: a key Lemon Squeezy disabled stops working here too
PASS A4: a test-mode key event changes nothing
PASS A4: the order page says the Lemon Squeezy key works too only once that key is recorded
PASS A4: the receipt names both keys and prints neither
PASS A4 (F5 R1 a): a stranded key is healed by the next event for it, with nothing left waiting
PASS A4 (F5 R1 c): a key parked while the order was still being written is applied once it commits, with nothing left waiting
PASS A4 (F5 R1 b): the order fulfilled between the key's look-up and its parking still takes the key, with nothing left waiting
PASS A4 (F5 A-2): a resent receipt says "either works" only when the Lemon Squeezy key is recorded; else it names the order-page key alone
== ST-2. self-serve deactivation (POST /license/deactivate) ==
PASS two domains activated: one slot left of three (got 1, want 1)
PASS deactivating shop.example: valid, the slot comes back, 2 of 3 self-serve deactivations left
PASS its activation row is gone and the deactivation is logged as self-serve
PASS the same domain again -> not_activated (a slot is never given back twice) (got 'not_activated', want 'not_activated')
PASS a domain it never held -> not_activated (got 'not_activated', want 'not_activated')
PASS garbage domain -> invalid_domain (got 'invalid_domain', want 'invalid_domain')
PASS re-activating after a move takes a slot again
PASS cap of 1 already used this 30 days -> deactivation_limit, and b.example stays activated
PASS a deactivation older than 30 days no longer counts
PASS cap 0 -> self-service off (deactivation_off) (got 'deactivation_off', want 'deactivation_off')
PASS activations_used never goes below zero
== ST-3. offline license documents (POST /license/offline, GET /license/public-key) ==
PASS a domain that is not activated gets no document (got 'not_activated', want 'not_activated')
PASS an activated domain gets a signed document; the public key is the one /license/public-key serves
PASS the document verifies with the public key alone and names the key, sku, tier and domain
PASS it expires after the store's offline days (30 by default)
PASS a changed byte fails verification
PASS past its expiry it fails verification
PASS the key pair is stable across calls (derived, not stored)
PASS another install's secret gives another key pair
PASS no license_secret (a demo install): no public key, reason offline_unavailable
PASS offline days 0 -> offline_off (got 'offline_off', want 'offline_off')
PASS a revoked license gets no update (answered as an unknown key, 4.0 A4), no deactivation and no document
PASS index.php routes the four license endpoints to handlers that exist
PASS the license endpoints share /license/check's per-IP rate limit
== ST-4. discount codes ==
PASS create: a bad code is refused (got false, want false)
PASS create: SPRING25 for all products
PASS create: the same code in another case is a duplicate (got 'That code already exists.', want 'That code already exists.')
PASS create: an unknown product is refused (got 'Unknown product.', want 'Unknown product.')
PASS create: a malformed date is refused (got 'Dates are YYYY-MM-DD.', want 'Dates are YYYY-MM-DD.')
PASS create: an end before the start is refused (got 'The end date is before the start date.', want 'The end date is before the start date.')
PASS create: APPONLY for the test product only
PASS create: a code with a window in the past and one in the future
PASS find: any case finds the stored code, and the stored spelling is what is used
PASS find: a product-only code applies to its product, not another, not a suite
PASS find: an all-products code applies to a suite (product 0)
PASS find: outside its dates a code does not apply
PASS find: a code turned off does not apply
PASS apply (Lemon Squeezy): checkout[discount_code] and checkout[custom][code] carry it
PASS apply (Stripe): prefilled_promo_code, the field Stripe documents (got 'https://buy.stripe.com/x?prefilled_promo_code=SPRING25', want 'https://buy.stripe.com/x?prefilled_promo_code=SPRING25')
PASS apply (Stripe): a code Stripe would refuse (not letters and digits) is left off (got 'https://buy.stripe.com/x', want 'https://buy.stripe.com/x')
PASS apply: a URL that already names a discount is untouched (got 'https://st.lemonsqueezy.com/buy/abc?checkout[discount_code]=X', want 'https://st.lemonsqueezy.com/buy/abc?checkout[discount_code]=X')
PASS apply: the built-in demo checkout is untouched (got '/checkout/demo/1', want '/checkout/demo/1')
PASS checkout without ?code= carries the store-wide promo code, as before
PASS checkout with ?code=apponly carries APPONLY instead, and counts one checkout
PASS a code that does not apply falls back to the store-wide code and counts nothing
PASS store-wide promo on Stripe is now prefilled_promo_code (it was checkout[discount_code], which Stripe ignores) (got 'https://buy.stripe.com/x?prefilled_promo_code=STOREWIDE', want 'https://buy.stripe.com/x?prefilled_promo_code=STOREWIDE')
PASS buy links carry ?code= from the page (and drop a malformed ref) (got '/buy/7?utm_source=mail&code=SPRING25', want '/buy/7?utm_source=mail&code=SPRING25')
PASS buy links are unchanged on a page with neither (got '/buy/7', want '/buy/7')
PASS the paid order records the code the store filled in and the discount the provider reported
PASS a malformed code or a negative discount from the webhook is not stored
PASS the codes page counts that paid order and the discount given, and links to ?code=
PASS a product-only code links to that product page
PASS Lemon Squeezy webhook: the code and ref come back from custom data, the discount from discount_total
PASS Stripe webhook: the affiliate comes back as client_reference_id aff-CODE, the discount from total_details; no code name
== ST-5. download log ==
PASS network: IPv4 keeps the /24 (got '203.0.113.0/24', want '203.0.113.0/24')
PASS network: IPv6 keeps the /48 (got '2001:db8:abcd::/48', want '2001:db8:abcd::/48')
PASS network: anything else is empty (got '', want '')
PASS a served download is recorded with its network and browser, never the full address
PASS used up and expired attempts are recorded as such (got array (
0 => 'served',
1 => 'served',
2 => 'used_up',
3 => 'expired',
), want array (
0 => 'served',
1 => 'served',
2 => 'used_up',
3 => 'expired',
))
PASS an unknown token is not recorded (there is no order to attach it to) (got 5, want 5)
PASS order summary: 2 served from 2 networks, 2 refused (got array (
'served' => 2,
'refused' => 2,
'networks' => 2,
), want array (
'served' => 2,
'refused' => 2,
'networks' => 2,
))
PASS the log searches by buyer email
PASS a customer's subject-access export carries their download log
PASS the GDPR export and anonymize handlers call the download log
PASS with the log off nothing is recorded (got 5, want 5)
== ST-6. card-testing guard ==
PASS 3 buyers (one of them twice) under the threshold of 4: no flag, no email
PASS an order over the cheap limit does not count
PASS the 4th buyer flags every cheap order in the burst (5 orders), and nothing else (got array (
0 => 'st-c1',
1 => 'st-c2',
2 => 'st-c3',
3 => 'st-c3b',
4 => 'st-c4',
), want array (
0 => 'st-c1',
1 => 'st-c2',
2 => 'st-c3',
3 => 'st-c3b',
4 => 'st-c4',
))
PASS the owner gets one email for the burst (got 1, want 1)
PASS a 5th order joins the burst: flagged, and no second email
PASS record, never determine: every flagged order is still paid and its license still active
PASS clearing a flag records who and when, and leaves the order alone
PASS threshold 0 turns the guard off
== ST-7. affiliates ==
PASS program off: no attribution at all
PASS create: a bad code is refused (got false, want false)
PASS create: a bad email is refused (got 'That email address is not valid.', want 'That email address is not valid.')
PASS create: over 100% is refused (got 'The commission is a percentage from 0 to 100.', want 'The commission is a percentage from 0 to 100.')
PASS create: ann at 20%, self at 12.5%, the default when empty
PASS create: codes are unique in any case (got 'That code is taken.', want 'That code is taken.')
PASS commission rates are stored in basis points (got array (
'ann' => 2000,
'bob' => 1250,
'cat' => 2000,
), want array (
'ann' => 2000,
'bob' => 1250,
'cat' => 2000,
))
PASS a ?ref= visit counts one click and remembers the code
PASS the same visitor with the same code is not a second click (got 1, want 1)
PASS later pages: the cookie still attributes the visit (got 'ann', want 'ann')
PASS ?ref= on the page wins over the cookie (got 'bob', want 'bob')
PASS apply (Lemon Squeezy): checkout[custom][ref] (got 'https://st.lemonsqueezy.com/buy/x?checkout%5Bcustom%5D%5Bref%5D=ann', want 'https://st.lemonsqueezy.com/buy/x?checkout%5Bcustom%5D%5Bref%5D=ann')
PASS apply (Stripe): client_reference_id=aff-CODE (got 'https://buy.stripe.com/x?a=1&client_reference_id=aff-ann', want 'https://buy.stripe.com/x?a=1&client_reference_id=aff-ann')
PASS apply (Stripe): a link that already has a client_reference_id is left alone (got 'https://buy.stripe.com/x?client_reference_id=z', want 'https://buy.stripe.com/x?client_reference_id=z')
PASS apply: the demo checkout is left alone (got '/checkout/demo/1', want '/checkout/demo/1')
PASS a checkout from a visit that came by ann's link carries ref=ann
PASS a paid order with ref=ann records 20% as held, due after the 30-day hold, and the order names the affiliate
PASS a duplicate webhook records no second commission (got 1, want 1)
PASS the affiliate buying through their own link: recorded as rejected (self-referral), with the rounded-down amount
PASS an unknown or switched-off affiliate earns nothing (got 0, want 0)
PASS a refund during the hold reverses the commission (got 'reversed', want 'reversed')
PASS past the hold the commissions are payable, and the payout CSV has one line for ann: 2 commissions, 26.00 USD
PASS the CSV guards against formulas (the name "=Ann" is written as text)
PASS mark paid: only approved commissions in the list are paid (a held one slipped in is not) (got 2, want 2)
PASS paid commissions carry a paid date
PASS a refund after payout leaves the commission paid, with a note (getting it back is the seller's call)
PASS stats: ann has 4 commissions, 26.00 paid and 1.00 held
PASS the stats page token finds the affiliate; anything else finds nothing
== ST-8. the new pages render ==
PASS Selling tools renders its flags, download log and settings with no PHP warning
PASS Discount codes renders the codes with their links, with no PHP warning
PASS Affiliates renders affiliates, commissions and the self-referral note, with no PHP warning, and escapes the name
PASS an affiliate's own page shows their link and totals, nobody else's orders or emails
PASS order page: an open card-testing flag shows, and says nothing was refunded
PASS order page: a cleared flag does not show
PASS order page: the download log line (served, networks, refused) links to the log for this order
PASS order page: the discount code and the discount the provider reported
PASS order page: the affiliate and the commission
PASS /admin/selling: support may read it (flags and download log), with no Clear button and no settings form
PASS /admin/selling: an admin gets the page and its settings form
PASS admin_discounts (GET): support is refused, an admin gets in
PASS admin_affiliates (GET): support is refused, an admin gets in
PASS POST /admin/selling/flags/{id}/clear: support is refused and the flag stays open (W7 8a)
PASS POST /admin/selling/flags/{id}/clear: an admin clears it, recorded against the admin
PASS the other 7 POSTs of the selling tools refuse support
PASS the payout CSV (GET) refuses support
PASS every admin POST routed to controllers/selling.php (8) checks the CSRF token and the signed-in user
PASS every admin handler there (12) requires a signed-in user
PASS index.php captures ?ref= before routing and routes the 13 new pages
PASS anonymizing the customer blanks the network and browser, and keeps the attempts
PASS ownware.io keeps the download log off (O1, 2026-09-26): bin/ownware-store.php, run on every store deploy, sets it to 0 and keeps no rows — SKIP (dist zip: a store-instance fact)
PASS the product default stays on for a buyer's install (a new settings row reads download_log 1) (got 1, want 1)
== 3.2.1: the shipped views and assets name no store ==
PASS no shipped view, script or stylesheet names a store (STRICT_SOURCE) (got array (
), want array (
))
PASS the store-only list above is the packager's own (every name is removed by tools/package.sh) — SKIP (dist zip: a store-instance fact)
PASS views/legal.local.php carries the facts the shipped views now read (cloud, badge, OS suite, footer, FAQ) — SKIP (dist zip: a store-instance fact)
== 3.2.1: the shareable backup leaves out license keys, tokens, session keys and redeem codes ==
PASS backup: every licenses.license_key is redacted (39 rows)
PASS backup: every carts.session_key is redacted (1 rows)
PASS backup: every downloads.token is redacted (41 rows)
PASS backup: every affiliates.token is redacted (3 rows)
PASS backup: every redeem_codes.code is redacted (1 rows)
PASS backup: the same column names blank the public discount and affiliate codes too (the core matches by column name)
PASS backup: the shared core is untouched — the list is Deliora's own (DELIORA_BACKUP_REDACT), applied by BackupExport and BackupRestore alike
PASS restore onto a working install: every license key, token, session key and code (discount and affiliate ones included) keeps its live value
PASS docs/MANUAL.md says where keys and codes come back from on a rebuild (the SQLite file or mysqldump) — SKIP (dist zip: a store-instance fact)
== 3.2.1: the product import's dry run agrees with the import ==
PASS dry run: 1 ready, 3 skipped (existing slug, a slug repeated in the file, a row without a name) (got array (
0 => 1,
1 => 3,
), want array (
0 => 1,
1 => 3,
))
PASS dry run writes nothing (got 11, want 11)
PASS the import takes exactly what the dry run said (got array (
0 => 1,
1 => 3,
), want array (
0 => 1,
1 => 3,
))
PASS and skips the same lines for the same reasons
PASS the imported row is an inactive draft, the existing product untouched
PASS the upload handler dry-runs through the import's own handler, not run()'s dry-run mode
== SITE SURVEY PHASE 0 (2026-09-27): no sideways scroll, no word cut in half, one address for prices ==
PASS phase 0: a code block and a doc table scroll inside their own box, an inline code chip wraps — the page never scrolls sideways
PASS phase 0: excerpt() cuts on a word boundary and marks the cut; short text and one long word are handled
PASS phase 0: no view shortens visible text with clip() (the DB column guard); /self-hosted, search, /vs, alternatives and the catalog use excerpt()
PASS phase 0: /prices (and /plans, /plan, /plans-pricing, /pricing-plans) answer 301 to /pricing
== D-15 REDESIGN behind a switch: home, product, catalog, pricing ==
PASS switch off: the home renders today's layout (store.css, no site.css)
PASS switch on: the home renders the redesign (site.css, body class "site", the hero, one h1)
PASS switch on: the redesigned pages load neither style.css nor store.css (speed; the partials' utilities live in site.css), and request the two fonts before any stylesheet
PASS help widget (operator 19:59): redesigned pages carry help_widget_v2 (quick actions, search, /faq.json), never the old one; the license question is in the shared list
PASS the help launcher is the green Q&A bubble the thank-you page names (round, the site green, labeled Q&A)
PASS the two designs never share an ETag (a browser cannot revalidate one into the other)
PASS hero deck (operator 18:01): one card per app with a dashboard shot, the first loaded at once and never lazy, the rest deferred until they come near (no shots on this store: no deck rendered)
PASS switch on: the real-dashboards rack carries every active app that has a dashboard shot (operator 17:33), the flagship outside it (no shots on this store: no rack rendered)
PASS switch on: the home shows every active suite, whole-catalogue bundles included (operator 17:35 "shouldnt all suites be there?")
PASS switch on: the home links every active app on the shelf
PASS product (sc-invoicer), switch on: the redesign renders (site.css, the sticky buy card #pricing, #licence, #zip, one h1)
PASS product (sc-invoicer): every wired edition buys through its own /buy/{id} link, and the license choice names each one
PASS product (sc-invoicer): the buy box says updates are included, and Runs on names Docker (4.0 A4 Task 4)
PASS product (sc-invoicer): the one-line install names this product and the store's own address, and only where the store serves /install.sh (4.0 A4)
PASS product (sc-invoicer), launch mode: no /buy/ link anywhere, and the notify card is there instead
PASS product (sc-invoicer), switch off: today's page, and a different ETag from the redesign
PASS A4: every product this store sells ships a Dockerfile, as its page says
PASS catalog, switch on: one card per active app (the flagship outside the groups), each linking its page
PASS pricing, switch on: every active app has a row with its live Single price (or "Available soon" when unwired)
PASS the checkout-overlay script has one copy, the partial, and both product templates include it
PASS every public validator carries the design (one helper, not per route), and every cache path marks a preview private
PASS switch on: a page with no redesigned template (/legal) renders its own body in the new chrome, restyled by site-legacy.css (never store.css); switch off: unchanged
PASS guides, switch on: the redesigned index lists every published guide once, in the new chrome without the old styles
PASS guide, switch on: the article renders its body, its Article JSON-LD and a side panel of the apps it mentions
PASS suites, switch on: the redesigned index links every active suite, in the new chrome without the old styles
PASS suite, switch on: the redesigned page shows every member app, the buy card (#buy) and the suite FAQ, prices read live
PASS comparisons, switch on: the redesigned index has one card per comparison page, without the old styles
PASS comparison (sc-invoicer), switch on: every cited row has its numbered source, and the calculator's ids are all there
PASS alternatives, switch on: the savings wall has one card per cited rival, without the old styles
PASS alternative (rentco), switch on: the page cites its source (#src-1) and renders without the old styles
PASS by kind of business, switch on: the redesigned index has one card per hub with a live product, without the old styles
PASS no unprefixed class in the redesign is styled on its own by style.css (got array (
), want array (
))
PASS product (sc-invoicer): the demo sits under the name and price, before the screenshots, in its own color
PASS home rack: every card has a button to its page, and a demo button only while demos are live
PASS every product gallery shot is a real screen: no flagged defect, no near-blank capture (got array (
), want array (
))
PASS the switch exists only where the store has chosen a design: ?design=v2 does nothing here (no design chosen, as in a buyer's copy)
PASS ST cleanup: the test zip it stored in data/files is gone
== 2026-09-26 09:2x: O1's ruling on research 08 #7 (two ORGANIC keys lifted) + O2's admin nav links ==
PASS RULING: p:complia and p:safora are no longer measured; the other eight product/suite/alt/guide keys and p:tokora stay
PASS RULING (store): the copy file now titles /p/complia and /p/safora — both name what the product records, carry "self-hosted", never "compliance", and fit 60 — SKIP (dist zip: a store-instance fact)
PASS RULING (store): with the key lifted, seoCopyApply lets the file title /p/complia and /p/safora; /p/approva keeps its ORGANIC title — SKIP (dist zip: a store-instance fact)
PASS NAV (O2 3.2.0): the admin layout links Selling tools, Discount codes and Affiliates in the Sell group, after Downloads
PASS NAV (O2 3.2.0): the three links render only when controllers/selling.php exists, and the flags badge only when CardTestGuard is loaded — a tree without 3.2.0 selling tools shows no dead link
PASS NAV (audit 8b): Discount codes and Affiliates are inside if ($isAdm) — after Selling tools, before the Catalog group — and Selling tools is outside it
PASS NAV (O2 3.2.0): the three icons exist in ui_icons.php (security, tag, team) and the links carry no data-key shortcut that could clash
== JOB 3 (2026-09-26): research 08 #9 #10 #20 + 07 §4 #4 #6 #10 ==
PASS J3 #9: guidesMentioning with cap 0 returns EVERY guide whose body links the product (7 more than before), cap 5 still returns 5 (got array (
0 => 7,
1 => 5,
), want array (
0 => 7,
1 => 5,
))
PASS J3 #9: the product page links all 7 mentioning guides — five in full and the rest in the "Also covered in" row (got array (
0 => 7,
1 => 1,
), want array (
0 => 7,
1 => 1,
))
PASS J3 #9 (store): /for-agents links /badge — SKIP (dist zip: a store-instance fact)
PASS J3 #10: relatedLinks(product) carries the product's own /vs page, kind "vs", right after its category pages
PASS J3 #10: on the /vs page itself the block never links the page it is on
PASS J3 #10: the rendered /vs page carries the related block without a self-link, and 3–5 links (U6 kept)
PASS J3 #20: a document page carries og:image = Storefront::coverUrl(product)
PASS J3 07#4: the store nav carries the CSS-only Menu control before its links, and Prices + Guides are the kept links
PASS J3 07#4: at ≤760 px the nav no longer scrolls sideways, links are 44 px tall, non-kept links hide until the Menu checkbox is checked
PASS J3 07#4: the rendered home carries the Menu control once and the two kept links
PASS J3 07#6: the flagship grid on the classic home carries its grid class and store.css stacks it below 640 px
PASS J3 07#10: with ours + one rival (two cards) no "Show all" control is rendered
PASS J3 07#10: with ours + two rivals the checkbox precedes .pricetbl and the "Show all 3 plans" label follows the cards
PASS J3 07#10: store.css hides the cards after the second at ≤390 px until the control is checked, and .tiercard .t is at least 12 px
== COPY-RISK (2026-09-26): research 10 findings applied at source; pinned old sentences never render again ==
PASS COPY-RISK checks skipped (dist zip build - store_instance.php absent)
PASS COPY-RISK F-86 meta check skipped (dist zip build - store_instance.php absent)
== 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 /admin/updates is updates_page()
PASS updates: route post /admin/updates/key is updates_key_save()
PASS updates: route post /admin/updates/check is updates_check()
PASS updates: route post /admin/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"
PASS updates: the ownware.io store never registers the Update routes (views/legal.local.php marks it)
PASS updates: bin/update.php refuses to run on the ownware.io store
== 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 Deliora 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 (8 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 Deliora 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: under the standard keep count (14) every backup is 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 /admin/sample-data/remove is sample_data_remove()
PASS A2: route post /admin/restore/backups is backups_save()
PASS A2: route post /admin/restore/backup-now is backup_now()
PASS A2: route get /admin/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 Deliora, 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 8 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 (48 tables dumped)
== 4.1.2: Stripe fulfils only what was paid, and a refund takes it back (S-1, S-2) ==
PASS S-1: a completed checkout whose payment is still unpaid fulfils nothing (HTTP 200)
PASS S-1: ... and when that payment fails, still nothing (HTTP 200)
PASS S-1: a delayed payment that settles (async_payment_succeeded) is fulfilled once (HTTP 200)
PASS S-1: ... a resend of it fulfils nothing twice
PASS S-1: a completed checkout that is paid is fulfilled as before (HTTP 200)
PASS S-1: a completed checkout with no payment required (a full discount the seller made) is fulfilled
PASS S-1: a completed checkout without payment_status (older Stripe API versions) is fulfilled as before
PASS S-2: the paid order keeps its payment_intent
PASS S-2: a Stripe refund marks the order refunded (HTTP 200, {"ok":true,"refunded":true})
PASS S-2: ... and revokes its licenses
PASS S-2: ... and no other order changed
PASS S-2: a refund for a payment the store never fulfilled changes nothing (HTTP 200)
————————————————————————————————————————————————————
ALL 1831 TESTS PASSED (0 failed)
← Back to Deliora · Manual · Quickstart · API