diff --git a/scripts/check-site.mjs b/scripts/check-site.mjs index 0e301463..700aff4b 100644 --- a/scripts/check-site.mjs +++ b/scripts/check-site.mjs @@ -174,6 +174,29 @@ for (const text of requiredWorkerCopy) { if (!worker.includes(text)) failures.push(`Missing worker agent behavior: ${text}`); } +// The current-product readiness verdict must key off the intake path only. +// The Website Appraisal depends on the D1 email_signups table behind +// /api/signups; the retired Agent Desk's AI binding and agent tables are +// reported as legacy state, never as the current product's readiness. A +// regression that lets the legacy machinery gate /health — or that drops the +// intake table from the check — would make the machine-readable truth lie. +const requiredHealthCopy = [ + "surface: APPRAISAL_SURFACE", + "signupsTable", + "checks.db && checks.signupsTable", + "email_signups", + "agentRunsTable", + "usageLimitsTable" +]; + +for (const text of requiredHealthCopy) { + if (!worker.includes(text)) failures.push(`Missing worker health truth: ${text}`); +} + +if (worker.includes("checks.ai && checks.db")) { + failures.push("Worker /health verdict must not gate on the retired Agent Desk AI binding"); +} + for (const text of requiredPublicArtifacts) { // llms.txt and offer.md are mirrors of the same offer contract. A fact must // appear in BOTH (case-insensitively, since one file may head it while the diff --git a/scripts/test-agent-worker.mjs b/scripts/test-agent-worker.mjs index 47bff504..e7fde73f 100644 --- a/scripts/test-agent-worker.mjs +++ b/scripts/test-agent-worker.mjs @@ -1016,6 +1016,77 @@ test("signup handler accepts a bare-domain website with a test email and stores assert.ok(insert, "signup handler must persist through the existing email_signups path"); assert.equal(insert.values[0], "audit-check+test@example.com"); assert.equal(insert.values[7], "https://example.com"); + // The current appraisal intake must label its rows with the current offer, + // never the retired self-serve Agent Desk surface name. + assert.equal(insert.values[1], "website-appraisal", "current intake signups must carry the current-offer source label"); + assert.notEqual(insert.values[1], "agent-self-serve", "current intake signups must not carry the retired Agent Desk source label"); +}); + +test("worker /health names the current Website Appraisal surface, not the retired Agent Desk", async () => { + class HealthStatement extends FakeStatement { + async all() { + this.db.calls.push({ method: "all", sql: this.sql, values: this.values }); + return { results: [{ name: "email_signups" }, { name: "agent_runs" }, { name: "agent_usage_limits" }] }; + } + } + + class HealthDB extends FakeDB { + prepare(sql) { + return new HealthStatement(this, sql); + } + } + + const res = await worker.fetch(new Request("https://tinystudio.io/health"), { DB: new HealthDB(), AI: {} }); + assert.equal(res.status, 200); + const body = await res.json(); + assert.equal(body.service, "tinystudio-io-public"); + assert.equal(body.surface, "website-appraisal", "health surface must name the current offer"); + assert.notEqual(body.surface, "agent-desk", "health surface must not name the retired Agent Desk"); + assert.equal(body.ok, true); +}); + +test("worker /health verdict keys off the current intake path, not the retired Agent Desk machinery", async () => { + // The current product depends on the D1 email_signups table behind + // /api/signups. The retired Agent Desk's AI binding and agent tables must + // not gate the current product's readiness verdict: a green /health while + // the signup path is broken would be a false positive, and a red /health + // when the appraisal intake is healthy would be a false alarm. The env + // deliberately carries no AI binding — the current product has no model + // dependency. + class HealthStatement extends FakeStatement { + async all() { + this.db.calls.push({ method: "all", sql: this.sql, values: this.values }); + return { results: [{ name: "email_signups" }] }; + } + } + + class HealthDB extends FakeDB { + prepare(sql) { + return new HealthStatement(this, sql); + } + } + + const res = await worker.fetch(new Request("https://tinystudio.io/health"), { DB: new HealthDB() }); + assert.equal(res.status, 200); + const body = await res.json(); + assert.equal(body.ok, true, "the current product is ready even without the retired Agent Desk machinery"); + assert.equal(body.checks.signupsTable, true, "the intake table is present"); + assert.equal(body.checks.ai, false, "the retired AI binding is absent"); + assert.equal(body.checks.agentRunsTable, false, "the retired agent_runs table is absent"); + assert.equal(body.checks.usageLimitsTable, false, "the retired usage-limits table is absent"); +}); + +test("legacy /api/agent-audit still labels its rows with the retired self-serve source", async () => { + const db = new FakeDB(); + const ai = new FakeAI(VALID_AGENT_OUTPUT); + const res = await worker.fetch(agentRequest(validBody()), { DB: db, AI: ai }); + assert.equal(res.status, 200); + const signupInsert = db.calls.find((call) => call.sql.includes("INSERT INTO email_signups")); + assert.ok(signupInsert, "agent audit must persist through the existing email_signups path"); + assert.equal(signupInsert.values[1], "agent-self-serve", "the legacy surface keeps its own source label"); + const runInsert = db.calls.find((call) => call.sql.includes("INSERT INTO agent_runs")); + assert.ok(runInsert, "agent audit must record the legacy run"); + assert.equal(runInsert.values[2], "agent-self-serve", "the legacy run keeps its own source label"); }); test("worker serves the same-origin font promotion script (render-blocking fix b8f6046e942a)", async () => { diff --git a/src/worker.js b/src/worker.js index 0475c3f8..06159d1b 100644 --- a/src/worker.js +++ b/src/worker.js @@ -82,6 +82,12 @@ const MAX_FIELD_LENGTH = 1800; const MAX_REQUEST_BYTES = 24000; const SOFT_AGENT_RUNS_PER_EMAIL_PER_DAY = 5; const MAX_AGENT_RUNS_PER_IP_PER_DAY = 20; +// The current product's public intake (homepage and /audit) posts to +// /api/signups. Its rows and the public /health surface must be labeled with +// the current offer — The Website Appraisal — never the retired self-serve +// Agent Desk, which keeps its own "agent-self-serve" labels on the legacy +// /api/agent-audit path. +const APPRAISAL_SURFACE = "website-appraisal"; const CURRENCY_AMOUNT_PATTERN = String.raw`(?:(?:₹|\$|€|£|inr|usd|us\$|aud|cad|sgd|gbp|eur|rs\.?|rupees?)\s*\d[\d,.]*(?:\s*(?:k|lakh|lakhs|l|cr))?|\d[\d,.]*\s*(?:inr|usd|aud|cad|sgd|gbp|eur|rupees?))`; const METRIC_VALUE_PATTERN = String.raw`(?:${CURRENCY_AMOUNT_PATTERN}|\b\d[\d,.]*\b)`; const WEEKLY_METRIC_LABELS = [ @@ -359,7 +365,7 @@ async function signupResponse(request, env, url) { return jsonResponse({ ok: false, error: "invalid_email" }, { status: 400 }); } - await saveEmailSignup(request, env, url, email, "agent-self-serve", website); + await saveEmailSignup(request, env, url, email, APPRAISAL_SURFACE, website); if (wantsHtmlRedirect(request)) { return htmlRedirect(url, "saved"); @@ -1206,9 +1212,16 @@ async function agentAuditResponse(request, env, url) { } async function healthResponse(env) { + // The current product (The Website Appraisal) depends on exactly one + // backend: the D1 `email_signups` table behind /api/signups. The retired + // self-serve Agent Desk's machinery (the AI binding and its own agent_runs / + // agent_usage_limits tables) is reported as legacy state, never as the + // current product's readiness: /health must not go red when the appraisal + // intake is healthy, nor green when the signup path is broken. const checks = { - ai: Boolean(env.AI), db: Boolean(env.DB), + signupsTable: false, + ai: Boolean(env.AI), agentRunsTable: false, usageLimitsTable: false }; @@ -1216,9 +1229,10 @@ async function healthResponse(env) { if (env.DB) { try { const tableResult = await env.DB.prepare( - "SELECT name FROM sqlite_master WHERE type = 'table' AND name IN ('agent_runs', 'agent_usage_limits')" + "SELECT name FROM sqlite_master WHERE type = 'table' AND name IN ('email_signups', 'agent_runs', 'agent_usage_limits')" ).all(); const tables = new Set((tableResult.results || []).map((row) => row.name)); + checks.signupsTable = tables.has("email_signups"); checks.agentRunsTable = tables.has("agent_runs"); checks.usageLimitsTable = tables.has("agent_usage_limits"); } catch (error) { @@ -1226,14 +1240,14 @@ async function healthResponse(env) { } } - const ok = checks.ai && checks.db && checks.agentRunsTable && checks.usageLimitsTable; + // The current-product readiness verdict keys off the intake path only. + const ok = checks.db && checks.signupsTable; return jsonResponse( { ok, service: "tinystudio-io-public", - surface: "agent-desk", - ai: checks.ai ? "configured" : "missing", + surface: APPRAISAL_SURFACE, db: checks.db ? "configured" : "missing", checks, routes: ["tinystudio.io", "www.tinystudio.io", "app.tinystudio.io", "api.tinystudio.io"]