diff --git a/scripts/check_app_discovery_live.sh b/scripts/check_app_discovery_live.sh index 8f765a0..b5e6e3e 100644 --- a/scripts/check_app_discovery_live.sh +++ b/scripts/check_app_discovery_live.sh @@ -61,7 +61,8 @@ grep -Fq 'data-position="hero"' "$tmp/dotchi.html" || { echo 'FAIL Dotchi store grep -Fq 'const FEAT_T={en:{0:"Choose A or B on a question."' "$tmp/dotchi.html" || { echo 'FAIL Dotchi translated feature claims still stale' >&2; exit 1; } # Read-only release gate for the recently corrected Debate Me SEO landing. fetch '/support/debate-me/' 'debate-me.html' -grep -Fq 'AI Debate Practice & Argument Skills' "$tmp/debate-me.html" || grep -Fq 'AI Debate Practice & Argument Skills' "$tmp/debate-me.html" || { echo 'FAIL Debate Me product title is not live' >&2; exit 1; } +# #78 retitled the page from GSC evidence: "AI Debate Practice & Feedback". +grep -Fq 'AI Debate Practice & Feedback' "$tmp/debate-me.html" || grep -Fq 'AI Debate Practice & Feedback' "$tmp/debate-me.html" || { echo 'FAIL Debate Me product title is not live' >&2; exit 1; } grep -Fq 'data-position="hero"' "$tmp/debate-me.html" || { echo 'FAIL Debate Me first-screen Play CTA not live' >&2; exit 1; } grep -Fq 'data-position="details"' "$tmp/debate-me.html" || { echo 'FAIL Debate Me details Play CTA missing' >&2; exit 1; } grep -Fq 'support_debate_me_hero' "$tmp/debate-me.html" || { echo 'FAIL Debate Me Play attribution missing' >&2; exit 1; } @@ -111,5 +112,6 @@ if grep -Eiq 'Letters to future self|Time capsule feature|FutureMe Premium.*[0-9 echo 'FAIL FutureMe support page contains outdated feature or pricing claims' >&2; exit 1 fi fetch '/robots.txt' 'robots.txt' -grep -Fq 'Allow: /icons/apps/' "$tmp/robots.txt" || { echo 'FAIL Google image crawl allow not live' >&2; exit 1; } +# #53 broadened the allow to /icons/ (covers /icons/apps/). +grep -Eq '^Allow: /icons/(apps/)?$' "$tmp/robots.txt" || { echo 'FAIL Google image crawl allow not live' >&2; exit 1; } echo 'PASS: catalog, guides, priority search landings, store CTAs, icon crawl policy and sitemap current (HTTP 200)' diff --git a/scripts/check_growth_live.py b/scripts/check_growth_live.py index 2e29903..d8b82ac 100644 --- a/scripts/check_growth_live.py +++ b/scripts/check_growth_live.py @@ -83,7 +83,10 @@ def check(path, key, valid): check("/apps", "store_click_tracking", "catalog_store_click" in catalog and "download_click" in catalog) check("/apps", "detail_click_tracking", "catalog_app_detail_click" in catalog) check("/apps", "finalized_search_analytics", - "input.addEventListener('input', render)" in catalog and + # #77: the input handler re-renders and clears the dedupe key, but never reports per keystroke. + "input.addEventListener('input', () => {" in catalog and + "if (!clean(input.value)) lastReportedSearch = '';" in catalog and + "input.addEventListener('input', reportSearch)" not in catalog and "input.addEventListener('blur', reportSearch)" in catalog and "if (event.key === 'Enter') reportSearch()" in catalog and "query === lastReportedSearch" in catalog and diff --git a/scripts/publish_app_discovery.sh b/scripts/publish_app_discovery.sh index 58d9e3b..7b0bfa8 100644 --- a/scripts/publish_app_discovery.sh +++ b/scripts/publish_app_discovery.sh @@ -15,7 +15,7 @@ root="${APP_SITE_DEPLOY_ROOT:-/var/www/html}" if [[ ! "$host" =~ ^[a-zA-Z0-9_.@-]+$ || ! "$root" =~ ^/var/www/[a-zA-Z0-9_./-]+$ ]]; then echo 'Invalid APP_SITE_DEPLOY_HOST or APP_SITE_DEPLOY_ROOT' >&2; exit 2 fi -files=(index.html apps.html sitemap.xml robots.txt guides/quick-games/index.html guides/daily-habits/index.html guides/decision-helpers/index.html guides/ai-tools/index.html support/brain-burst/index.html support/snaplingo/index.html support/face-reaction-battle/index.html support/twotaku/index.html support/debate-me/index.html support/idea-market/index.html support/blur/index.html support/instant-judge/index.html support/roast-me/index.html support/future-me/index.html support/tappitsu-battle/index.html) +files=(index.html apps.html sitemap.xml robots.txt guides/quick-games/index.html guides/daily-habits/index.html guides/decision-helpers/index.html guides/ai-tools/index.html support/brain-burst/index.html support/snaplingo/index.html support/face-reaction-battle/index.html support/twotaku/index.html support/debate-me/index.html support/idea-market/index.html support/blur/index.html support/instant-judge/index.html support/roast-me/index.html support/future-me/index.html support/tappitsu-battle/index.html support/taletime/index.html support/dog-breeds-encyclopedia/index.html) for file in "${files[@]}"; do [[ -s "$file" ]] || { echo "Missing source file: $file" >&2; exit 2; } done diff --git a/scripts/test_growth_live.py b/scripts/test_growth_live.py index 365ed4a..9126688 100644 --- a/scripts/test_growth_live.py +++ b/scripts/test_growth_live.py @@ -26,7 +26,8 @@ def valid_pages(): pages["/apps"] = ( '' + cards + " catalog_store_click download_click catalog_app_detail_click " + - "input.addEventListener('input', render) input.addEventListener('blur', reportSearch) " + + "input.addEventListener('input', () => { if (!clean(input.value)) lastReportedSearch = ''; render(); }); " + + "input.addEventListener('blur', reportSearch) " + "if (event.key === 'Enter') reportSearch() query === lastReportedSearch " + "lastReportedSearch = query " + "const app = slug(card), content = clean((card.textContent || '') + ' ' + app; app.replace(/-/g, ' ')" + @@ -217,7 +218,7 @@ def test_store_schema_package_mismatch_fails(self): def test_first_keystroke_analytics_rejected(self): data = valid_pages() data["/apps"] = data["/apps"].replace( - "input.addEventListener('input', render)", + "input.addEventListener('input', () => { if (!clean(input.value)) lastReportedSearch = ''; render(); }); ", "input.addEventListener('input', reportSearch)") self.assertFalse(inspect(data)["passed"]) diff --git a/tests/catalog-growth.test.mjs b/tests/catalog-growth.test.mjs index 911f10d..623a914 100644 --- a/tests/catalog-growth.test.mjs +++ b/tests/catalog-growth.test.mjs @@ -26,7 +26,7 @@ test('filters work client-side without tracking raw search text',()=>{ const source=scripts.find(s=>s.includes('catalog_store_click')); assert.ok(source); new Script(source); function el(extra={}) {return {events:{},hidden:false,...extra,addEventListener(name,fn){this.events[name]=fn;},setAttribute(key,value){this[key]=value;}}} - const input=el({value:''}),count=el({textContent:''}),empty=el(); + const input=el({value:''}),count=el({textContent:''}),empty=el(),reset=el({focus(){}}); const buttons=['all','quick-games','habits','decisions','ai'].map(goal=>el({dataset:{goal}})); const cards=[ el({textContent:'Blur: Guess the Image Guess the photo',querySelector:()=>({getAttribute:()=> 'icons/apps/blur.png'})}), @@ -34,7 +34,7 @@ test('filters work client-side without tracking raw search text',()=>{ el({textContent:'Food Roulette pick a restaurant',querySelector:()=>({getAttribute:()=> 'icons/apps/food-roulette.png'})}), ]; const events=[]; - runInNewContext(source,{document:{getElementById(id){return {'find-app':input,'finder-count':count,'finder-empty':empty}[id];},querySelectorAll(q){return q==='.grid .app-card'?cards:buttons;}},window:{gtag(...args){events.push(args);}}}); + runInNewContext(source,{document:{referrer:'',getElementById(id){return {'find-app':input,'finder-count':count,'finder-empty':empty,'finder-reset':reset}[id];},querySelectorAll(q){return q==='.grid .app-card'?cards:q==='[data-detail-app]'?[]:buttons;}},URLSearchParams,URL,window:{location:{search:'',href:'https://appdevelopsk.com/apps',origin:'https://appdevelopsk.com'},history:{state:null,replaceState(){}},gtag(...args){events.push(args);}}}); assert.equal(count.textContent,'3 apps shown'); buttons[1].events.click(); assert.equal(count.textContent,'1 apps shown'); @@ -155,5 +155,5 @@ test('home features public Dotchi instead of an unavailable app, with 17 languag assert.equal(typeof translations[lang].twotaku,'string',lang+' must localize Dotchi caption'); assert.ok(translations[lang].twotaku.length>=12,lang+' missing meaningful caption'); } - assert.match(home,/gtag\('event', 'app_view'/,'homepage app-card clicks remain measurable'); + assert.match(home,/gtag\('event', 'app_detail_click', \{[^}]*placement: 'homepage_card'/,'homepage app-card clicks remain measurable'); }); diff --git a/tests/debate-me-support-growth.test.mjs b/tests/debate-me-support-growth.test.mjs index 92f710e..df15ebc 100644 --- a/tests/debate-me-support-growth.test.mjs +++ b/tests/debate-me-support-growth.test.mjs @@ -11,7 +11,7 @@ function dictionary(name, after) { return runInNewContext('('+match[1]+')'); } test('first paint shows real AI debate and never a food spinner', () => { - assert.match(html, /Debate Me: AI Debate Practice for Android/); + assert.match(html, /<title>Debate Me – AI Debate Practice &(amp;)? Feedback/); assert.match(html, /<h1>Debate Me<\/h1>/); assert.match(html, /<div class="hero-emoji">🗣️<\/div>/); assert.match(html, /<div class="hero-cat">Education<\/div>/); diff --git a/tests/dotchi-support-growth.test.mjs b/tests/dotchi-support-growth.test.mjs index 30d216e..e6ef3f4 100644 --- a/tests/dotchi-support-growth.test.mjs +++ b/tests/dotchi-support-growth.test.mjs @@ -49,5 +49,6 @@ test('Dotchi support publication is part of the non-destructive VPS release gate assert.match(release,/support\/twotaku\/index\.html/); assert.match(live,/support\/twotaku/); assert.match(live,/FAIL Dotchi anime-only support content still live/); - assert.match(sitemap,/https:\/\/appdevelopsk\.com\/support\/twotaku\/<\/loc><lastmod>2026-10-27/); + const dotchiLastmod=sitemap.match(/https:\/\/appdevelopsk\.com\/support\/twotaku\/<\/loc><lastmod>(\d{4}-\d{2}-\d{2})/)?.[1]; + assert.ok(dotchiLastmod&&dotchiLastmod>='2026-10-27','Dotchi sitemap lastmod must not regress before 2026-10-27'); }); diff --git a/tests/search-landing-growth.test.mjs b/tests/search-landing-growth.test.mjs index 6597dc3..753e035 100644 --- a/tests/search-landing-growth.test.mjs +++ b/tests/search-landing-growth.test.mjs @@ -51,7 +51,7 @@ test('unavailable Face Reaction Battle stays unavailable and offers distinct pub test('public catalog icons are crawlable and explicit VPS release is non-destructive',()=>{ const robots=read('robots.txt'); - assert.match(robots,/Allow: \/icons\/apps\//); + assert.match(robots,/Allow: \/icons\/(apps\/)?$/m); const pub=read('scripts/publish_app_discovery.sh'); assert.match(pub,/--dry-run/); assert.match(pub,/support\/brain-burst\/index\.html/); @@ -65,7 +65,7 @@ test('public catalog icons are crawlable and explicit VPS release is non-destruc assert.doesNotMatch(pub,/^rsync[^\n]*--delete/m); const live=read('scripts/check_app_discovery_live.sh'); assert.match(live,/data-position="hero"/); - assert.match(live,/Allow: \/icons\/apps\//); + assert.ok(live.includes("Allow: /icons/(apps/)?")); }); test('daily live acquisition check is read-only, scheduled and fails visibly on stale VPS',()=>{