From 1d5a1f307bf2919ea1da7110d6bbd1c6c933d578 Mon Sep 17 00:00:00 2001 From: Nish <257724087+nish3451@users.noreply.github.com> Date: Wed, 19 Aug 2026 13:24:19 +0530 Subject: [PATCH] feat: repeatable production walk of the anonymous one-page check entry path Rebased onto main (2c396c2). Only conflict was the single-line `check` script in package.json, resolved as a union: main's `test:access-events`, `test:access` and `test:indexnow` are all kept, and this branch's `test:public-check-walk` is inserted at the position it held on the branch. `check` now runs 34 suites and every named script resolves. README additions auto-merged. --- README.md | 16 ++ package.json | 4 +- scripts/run-public-check-walk.mjs | 251 +++++++++++++++++++++++++ scripts/run-public-check-walk.test.mjs | 194 +++++++++++++++++++ 4 files changed, 464 insertions(+), 1 deletion(-) create mode 100644 scripts/run-public-check-walk.mjs create mode 100644 scripts/run-public-check-walk.test.mjs diff --git a/README.md b/README.md index 5ea264b..ab94e3a 100644 --- a/README.md +++ b/README.md @@ -89,6 +89,22 @@ no-ranking promise the README makes, that `/llms.txt`, `/sitemap.xml`, `/robots. npm run audit:live-promise ``` +For the item's verify half — a fresh production walk of `POST /api/public-check` +on a known test URL (the product's own deterministic `/fixture/rendered-page` by +default) that records the proof fields verbatim (rendered evidence, guarded +false positives, actionable findings when present, issue counts, engine +version), verifies the access-conversion copy (next step into the private beta, +no-ranking boundary), and verifies zero unsupported result claims: + +```bash +npm run audit:public-check-walk +``` + +The walk is quota-aware: one anonymous check consumes part of the running +network's per-day budget, so a 429 reports as `needs-quota` (exit 3) instead of +failing. The same assertions are locked offline by `test:public-check-walk`, +which runs inside `npm run check`. + For the repeatable live walk of the private-beta funnel (the backlog item "Live-surface walk of the private-beta funnel"): a real-browser (Playwright Chromium) walk of the funnel stops home → `/demo` → `/packages`, with the diff --git a/package.json b/package.json index 4336b0c..349d169 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "cf:dry-run": "npm run build && node scripts/wrangler-dry-run.mjs", "ops:audit-owned": "node scripts/run-live-audit-batch.mjs", "audit:live-promise": "node scripts/live-promise-spot-check.mjs", + "audit:public-check-walk": "node scripts/run-public-check-walk.mjs", "test:billing": "node server/dodo-payment-smoke-test.js", "test:billing-route": "node --test worker/routes/billing.test.mjs", "test:product-truth": "node server/product-truth-smoke-test.js", @@ -42,12 +43,13 @@ "test:large-crawl": "node server/large-rendered-crawl-smoke-test.js", "test:promise-audit": "node --test shared/promise-audit.test.mjs", "test:live-promise-spot-check": "node --test scripts/live-promise-spot-check.test.mjs", + "test:public-check-walk": "node --test scripts/run-public-check-walk.test.mjs", "test:app-contract": "node --test src/app-contract.test.mjs", "test:canary-dry-run": "node --test scripts/wrangler-dry-run.test.mjs", "test:indexnow": "node --test scripts/submit-indexnow.test.mjs", "submit:indexnow": "node scripts/submit-indexnow.mjs", "test:check-inventory": "node --test scripts/check-chain-inventory.test.mjs", - "check": "npm run test:billing && npm run test:billing-route && npm run test:product-truth && npm run test:audit && npm run test:large-crawl-security && npm run test:report-retention-security && npm run test:local-developer-api-security && npm run test:worker-dispatch && npm run test:worker-email && npm run test:public-pages && npm run test:public-check && npm run test:audit-engine && npm run test:account && npm run test:ai-answer-readiness && npm run test:growth-opportunities && npm run test:repair-queue && npm run test:repair-proof-receipt && npm run test:repair-implementation-pack && npm run test:repair-agent && npm run test:developer-api && npm run test:remediation-brief && npm run test:audit-batch-runner && npm run test:webhooks && npm run test:access-events && npm run test:access && npm run test:app-contract && npm run test:promise-audit && npm run test:live-promise-spot-check && npm run test:funnel-walk && npm run test:large-crawl && npm run test:canary-dry-run && npm run test:indexnow && npm run test:check-inventory && npm run build", + "check": "npm run test:billing && npm run test:billing-route && npm run test:product-truth && npm run test:audit && npm run test:large-crawl-security && npm run test:report-retention-security && npm run test:local-developer-api-security && npm run test:worker-dispatch && npm run test:worker-email && npm run test:public-pages && npm run test:public-check && npm run test:audit-engine && npm run test:account && npm run test:ai-answer-readiness && npm run test:growth-opportunities && npm run test:repair-queue && npm run test:repair-proof-receipt && npm run test:repair-implementation-pack && npm run test:repair-agent && npm run test:developer-api && npm run test:remediation-brief && npm run test:audit-batch-runner && npm run test:webhooks && npm run test:access-events && npm run test:access && npm run test:app-contract && npm run test:promise-audit && npm run test:live-promise-spot-check && npm run test:public-check-walk && npm run test:funnel-walk && npm run test:large-crawl && npm run test:canary-dry-run && npm run test:indexnow && npm run test:check-inventory && npm run build", "audit:funnel-walk": "node scripts/run-private-beta-funnel-walk.mjs", "test:funnel-walk": "node --test scripts/run-private-beta-funnel-walk.test.mjs" }, diff --git a/scripts/run-public-check-walk.mjs b/scripts/run-public-check-walk.mjs new file mode 100644 index 0000000..438438c --- /dev/null +++ b/scripts/run-public-check-walk.mjs @@ -0,0 +1,251 @@ +import { pathToFileURL } from "node:url"; + +// Repeatable production walk of the anonymous one-page check, the +// proof-to-repair entry path (backlog item "Turn the public sample into a +// truthful, searchable proof-to-repair entry path", verify half). +// +// The item's verify acceptance is: "fresh production walk on a known test URL +// records proof fields, access conversion, and no unsupported result claims". +// This script does exactly that against the deployed Worker: +// +// 1. POSTs a known test URL to POST /api/public-check. The default is the +// product's own deterministic fixture (/fixture/rendered-page), which the +// engine test proves renders substantial content and yields one guarded +// false positive plus one actionable finding - so a green walk exercises +// every accept field: rendered evidence, guarded false positive, findings +// when present, next step, and the no-ranking boundary. +// 2. Records the proof fields verbatim (measured facts, issue counts, guard +// and finding titles, engine version, scan time). +// 3. Verifies the access-conversion copy: nextStep hands off into the +// private beta and boundary keeps the no-ranking promise. +// 4. Verifies no unsupported result claims: the payload carries no +// affirmative ranking/traffic/indexing/revenue/AI-citation guarantee. +// +// The walk is opt-in and quota-aware: it consumes one anonymous check from the +// running network's per-day budget, and a 429 is reported as `needs-quota` +// (exit 3), not as a product failure - mirroring the fleet journal's +// "skipped-needs-quota" state. The offline regression lock for the same +// assertions lives in scripts/run-public-check-walk.test.mjs, which is part of +// `npm run check` (CI stays offline-only): +// +// npm run audit:public-check-walk +// SEOFIXKIT_WALK_URL=https://example.com/ npm run audit:public-check-walk + +const DEFAULT_BASE_URL = process.env.SEOFIXKIT_BASE_URL || "https://seofixkit.com"; +const DEFAULT_WALK_URL = `${DEFAULT_BASE_URL}/fixture/rendered-page`; +const DEFAULT_TIMEOUT_MS = Number(process.env.SEOFIXKIT_WALK_TIMEOUT_MS || 120000); + +export const WALK_STATUS = { + PASS: "pass", + NEEDS_QUOTA: "needs-quota", + FAIL: "fail" +}; + +// Exit codes: 0 pass, 1 walk failed (proof fields / conversion / claims broke), +// 3 needs-quota (retry later), 2 unexpected error. +export const WALK_EXIT_CODES = { pass: 0, fail: 1, error: 2, "needs-quota": 3 }; + +// NOTE: the direct-run block lives at the END of this module, after every +// const declaration. A top-level `await main()` before `const NEGATIONS` etc. +// would hit the temporal dead zone because module evaluation suspends at the +// await and the walked payload would be validated before those consts exist. + +async function main() { + const baseUrl = DEFAULT_BASE_URL; + const walkUrl = process.env.SEOFIXKIT_WALK_URL || DEFAULT_WALK_URL; + const result = await runPublicCheckWalk({ baseUrl, walkUrl }); + console.log(JSON.stringify(result, null, 2)); + process.exitCode = WALK_EXIT_CODES[result.status]; + if (result.status === WALK_STATUS.NEEDS_QUOTA) { + console.error("Walk skipped: anonymous-check daily budget exhausted from this network (needs-quota). Retry tomorrow; the offline regression lock still covers the assertions."); + } +} + +export async function runPublicCheckWalk({ + baseUrl = DEFAULT_BASE_URL, + walkUrl = `${baseUrl}/fixture/rendered-page`, + fetcher = fetch, + timeoutMs = DEFAULT_TIMEOUT_MS +}) { + const start = Date.now(); + const apiUrl = `${baseUrl}/api/public-check`; + let response; + try { + response = await fetchWithTimeout( + apiUrl, + { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ url: walkUrl }) + }, + timeoutMs, + fetcher + ); + } catch (error) { + return { + status: WALK_STATUS.FAIL, + apiUrl, + walkUrl, + checkedAt: new Date().toISOString(), + reason: `Could not reach the walk endpoint: ${String(error?.message || error)}` + }; + } + + const raw = await response.text().catch(() => ""); + let payload = null; + try { + payload = JSON.parse(raw); + } catch { + payload = null; + } + + if (response.status === 429) { + return { + status: WALK_STATUS.NEEDS_QUOTA, + apiUrl, + walkUrl, + checkedAt: new Date().toISOString(), + error: payload?.error || "Rate limited (429) by the anonymous-check quota.", + resetAt: payload?.resetAt || null + }; + } + + if (!response.ok) { + return { + status: WALK_STATUS.FAIL, + apiUrl, + walkUrl, + checkedAt: new Date().toISOString(), + httpStatus: response.status, + error: payload?.error || `Walk endpoint returned HTTP ${response.status}.`, + raw: raw.slice(0, 400) + }; + } + + const failures = validateWalkPayload(payload); + const record = failures.length === 0 ? walkRecord(payload) : null; + return { + status: failures.length === 0 ? WALK_STATUS.PASS : WALK_STATUS.FAIL, + apiUrl, + walkUrl, + checkedAt: new Date().toISOString(), + durationMs: Date.now() - start, + ...(record ? { proof: record } : {}), + ...(failures.length > 0 ? { failures } : {}) + }; +} + +// The item's verify assertions, kept in one place so the live walk and the +// offline regression lock cannot drift. Every field checked here is a field +// buildPublicCheckResponse emits from real engine output. +export function validateWalkPayload(payload) { + const failures = []; + if (!payload || typeof payload !== "object") return ["response is not a JSON object"]; + if (payload.ok !== true) failures.push("response does not report ok:true"); + if (payload.mode !== "one-page-check") failures.push("response mode is not one-page-check"); + if (typeof payload.checkedUrl !== "string" || payload.checkedUrl.length === 0) { + failures.push("missing checkedUrl"); + } + if (typeof payload.engineVersion !== "string" || payload.engineVersion.length === 0) { + failures.push("missing engineVersion"); + } + const measured = payload.measured; + if (!measured || typeof measured !== "object") { + failures.push("missing measured proof fields"); + } else { + for (const key of ["staticWordCount", "renderedWordCount", "renderedH1", "renderedTitle", "renderedInternalLinkCount"]) { + if (!(key in measured)) failures.push(`missing measured.${key}`); + } + if (typeof measured.staticWordCount !== "number" || typeof measured.renderedWordCount !== "number") { + failures.push("measured word counts are not numbers"); + } + } + const issues = payload.issues; + if (!issues || typeof issues !== "object") { + failures.push("missing issues counts"); + } else { + for (const key of ["critical", "warnings", "notices", "guardedFalsePositives"]) { + if (typeof issues[key] !== "number") failures.push(`issues.${key} is not a number`); + } + } + if (!Array.isArray(payload.guards)) failures.push("guards is not an array"); + if (!Array.isArray(payload.findings)) failures.push("findings is not an array"); + if (typeof payload.nextStep !== "string" || !payload.nextStep.includes("private beta")) { + failures.push("nextStep does not hand off into the private beta"); + } + if (typeof payload.boundary !== "string" || !payload.boundary.includes("does not guarantee rankings")) { + failures.push("boundary does not keep the no-ranking promise"); + } + const unsupported = findUnsupportedClaims(payload); + if (unsupported.length > 0) { + failures.push(`unsupported result claims present: ${unsupported.join("; ")}`); + } + return failures; +} + +// Affirmative result claims that must never appear: rankings, traffic, +// indexing, revenue, AI citations, or live answer-engine visibility +// guarantees/promises. The truthful boundary copy negates them ("does not +// guarantee rankings"), so negated phrasings are stripped before scanning. +const UNSUPPORTED_CLAIM = /\b(guarantees?|promises?|will\s+ensure)\s+(rankings?|traffic|indexing|revenue|AI citations?|answer[- ]engine visibility)/i; +const NEGATIONS = [ + /does not guarantee/gi, + /do not guarantee/gi, + /never guarantees?/gi, + /no ranking promise/gi, + /no ranking or traffic guarantee/gi, + /not promise/gi, + /no ranking, traffic, indexing, revenue, AI citations/gi +]; + +export function findUnsupportedClaims(payload) { + const text = JSON.stringify(payload); + let remaining = text; + for (const negation of NEGATIONS) remaining = remaining.replace(negation, " "); + const matches = remaining.match(new RegExp(UNSUPPORTED_CLAIM.source, "gi")) || []; + return [...new Set(matches.map((match) => match.trim()))]; +} + +// The recorded proof record: exactly the fields the page renders and the +// verify acceptance names (rendered evidence, guarded false positives, +// findings, next step, boundary). +export function walkRecord(payload) { + return { + checkedUrl: payload.checkedUrl, + finalUrl: payload.finalUrl || payload.checkedUrl, + scannedAt: payload.scannedAt || "", + engineVersion: payload.engineVersion, + measured: payload.measured, + issues: payload.issues, + guards: (payload.guards || []).map((guard) => ({ severity: guard.severity, title: guard.title })), + findings: (payload.findings || []).map((finding) => ({ severity: finding.severity, title: finding.title })), + nextStep: payload.nextStep, + boundary: payload.boundary + }; +} + +async function fetchWithTimeout(url, options, timeoutMs, fetcher = fetch) { + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), timeoutMs); + try { + return await fetcher(url, { ...options, signal: controller.signal }); + } catch (error) { + if (error?.name === "AbortError") { + throw new Error(`Request timed out after ${timeoutMs}ms.`); + } + throw error; + } finally { + clearTimeout(timer); + } +} + +function isDirectRun() { + return import.meta.url === pathToFileURL(process.argv[1] || "").href; +} + +// Direct-run block at the end of the module: by the time this executes every +// const above is initialized, so the walk can validate payloads without +// hitting the temporal dead zone. +if (isDirectRun()) { + await main(); +} diff --git a/scripts/run-public-check-walk.test.mjs b/scripts/run-public-check-walk.test.mjs new file mode 100644 index 0000000..40ab75c --- /dev/null +++ b/scripts/run-public-check-walk.test.mjs @@ -0,0 +1,194 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { buildPublicCheckResponse } from "../worker/routes/public-check.js"; +import { + WALK_STATUS, + findUnsupportedClaims, + runPublicCheckWalk, + validateWalkPayload, + walkRecord +} from "./run-public-check-walk.mjs"; + +const origin = "https://seofixkit.com"; + +// A minimal report in the exact shape the shared engine produces, reused to +// build the walk payload through buildPublicCheckResponse - the same mapping +// the deployed Worker uses - so the walk test cannot drift from the API. +function makeEngineShapedReport() { + return { + url: `${origin}/fixture/rendered-page`, + scannedAt: "2026-08-11T00:00:00.000Z", + durationMs: 1234, + pages: [ + { + url: `${origin}/fixture/rendered-page`, + finalUrl: `${origin}/fixture/rendered-page`, + static: { wordCount: 3 }, + rendered: { + finalUrl: `${origin}/fixture/rendered-page`, + wordCount: 277, + title: "Rendered fixture page", + h1s: ["Rendered fixture page with real content"], + internalLinks: [{ href: "/fixture/rendered-page" }, { href: "/fixture/robots.txt" }] + } + } + ], + summary: { critical: 1, warnings: 1, notices: 0, guardedFalsePositives: 1, totalFindings: 3 }, + findings: [ + { + type: "guard", + severity: "good", + title: "False positive guarded on fixture: rendered content is not thin", + why: "The static HTML looks thin, but the browser render shows substantial content.", + evidence: "277 rendered words found.", + fix: "No thin-content fix is needed for this page based on rendered text." + }, + { + type: "issue", + severity: "critical", + title: "Canonical conflicts with noindex on fixture", + why: "A page should not consolidate signals while telling engines not to index it.", + evidence: "Canonical: https://seofixkit.com/fixture/rendered-page; robots meta: noindex.", + fix: "If the page should rank, remove noindex." + }, + { + type: "issue", + severity: "warning", + title: "Long title on fixture", + fix: "Shorten the title." + } + ] + }; +} + +function jsonResponse(value, status = 200) { + return new Response(JSON.stringify(value), { + status, + headers: { "content-type": "application/json; charset=utf-8" } + }); +} + +function passingPayload() { + return buildPublicCheckResponse(makeEngineShapedReport()); +} + +test("walk passes against the exact payload shape the API returns for the fixture", async () => { + const payload = passingPayload(); + assert.deepEqual(validateWalkPayload(payload), []); + const fetcher = async () => jsonResponse(payload); + const result = await runPublicCheckWalk({ baseUrl: origin, fetcher }); + assert.equal(result.status, WALK_STATUS.PASS); + assert.equal(result.proof.checkedUrl, `${origin}/fixture/rendered-page`); + assert.equal(result.proof.measured.renderedWordCount, 277); + assert.equal(result.proof.measured.renderedH1, "Rendered fixture page with real content"); + assert.equal(result.proof.issues.guardedFalsePositives, 1); + assert.equal(result.proof.guards.length, 1); + assert.equal(result.proof.guards[0].severity, "good"); + assert.equal(result.proof.findings.length, 2); + assert.ok(result.proof.nextStep.includes("private beta"), "access conversion hands into the private beta"); + assert.ok(result.proof.boundary.includes("does not guarantee rankings"), "boundary keeps the no-ranking promise"); +}); + +test("walk fails when proof fields are missing", async () => { + const payload = passingPayload(); + delete payload.measured; + delete payload.issues; + delete payload.engineVersion; + const failures = validateWalkPayload(payload); + assert.ok(failures.includes("missing measured proof fields")); + assert.ok(failures.includes("missing issues counts")); + assert.ok(failures.includes("missing engineVersion")); + const result = await runPublicCheckWalk({ + baseUrl: origin, + fetcher: async () => jsonResponse(payload) + }); + assert.equal(result.status, WALK_STATUS.FAIL); + assert.ok(result.failures.length > 0); +}); + +test("walk fails when measured word counts are not real numbers", async () => { + const payload = passingPayload(); + payload.measured.staticWordCount = "not a number"; + const failures = validateWalkPayload(payload); + assert.ok(failures.includes("measured word counts are not numbers")); +}); + +test("walk fails when the next step or boundary stops handing into private access", async () => { + const noHandoff = passingPayload(); + noHandoff.nextStep = "Nothing more to do."; + const handoffFailures = validateWalkPayload(noHandoff); + assert.ok(handoffFailures.some((failure) => failure.includes("nextStep"))); + + const noBoundary = passingPayload(); + noBoundary.boundary = "This check will rank your page."; + const boundaryFailures = validateWalkPayload(noBoundary); + assert.ok(boundaryFailures.some((failure) => failure.includes("boundary"))); +}); + +test("walk fails on unsupported result claims but not on truthful negated copy", () => { + const payload = passingPayload(); + assert.deepEqual(findUnsupportedClaims(payload), [], "truthful boundary copy must not be flagged"); + + const claimed = JSON.parse(JSON.stringify(payload)); + claimed.boundary = "This check guarantees rankings and traffic."; + assert.deepEqual(findUnsupportedClaims(claimed), ["guarantees rankings"]); + + const promised = JSON.parse(JSON.stringify(payload)); + promised.nextStep = "It promises indexing for your site."; + assert.deepEqual(findUnsupportedClaims(promised), ["promises indexing"]); + + const aiCitation = JSON.parse(JSON.stringify(payload)); + aiCitation.boundary = "We guarantee AI citations within a week."; + assert.deepEqual(findUnsupportedClaims(aiCitation), ["guarantee AI citations"]); +}); + +test("walk reports needs-quota on 429 with the reset window", async () => { + const result = await runPublicCheckWalk({ + baseUrl: origin, + fetcher: async () => + jsonResponse( + { error: "Daily one-page check limit reached from this network. Try again tomorrow.", resetAt: "2026-08-12T00:00:00.000Z" }, + 429 + ) + }); + assert.equal(result.status, WALK_STATUS.NEEDS_QUOTA); + assert.equal(result.resetAt, "2026-08-12T00:00:00.000Z"); +}); + +test("walk fails when the endpoint is unreachable", async () => { + const result = await runPublicCheckWalk({ + baseUrl: origin, + fetcher: async () => { + throw new Error("network down"); + } + }); + assert.equal(result.status, WALK_STATUS.FAIL); + assert.match(result.reason, /network down/); +}); + +test("walk fails on a non-200 non-429 response", async () => { + const result = await runPublicCheckWalk({ + baseUrl: origin, + fetcher: async () => jsonResponse({ error: "Check storage is not configured." }, 503) + }); + assert.equal(result.status, WALK_STATUS.FAIL); + assert.equal(result.httpStatus, 503); +}); + +test("walkRecord keeps only the fields the page renders and the verify acceptance names", () => { + const record = walkRecord(passingPayload()); + assert.deepEqual(Object.keys(record).sort(), [ + "boundary", + "checkedUrl", + "engineVersion", + "finalUrl", + "findings", + "guards", + "issues", + "measured", + "nextStep", + "scannedAt" + ]); + assert.equal(record.guards[0].why, undefined, "guard details stay out of the compact record"); + assert.equal(record.findings[0].evidence, undefined, "finding details stay out of the compact record"); +});