Integrate local business workflow and SaaS redesign

This commit is contained in:
2026-06-12 21:08:35 +02:00
parent f00c5a3193
commit 21c7e4c9a4
88 changed files with 2683 additions and 849 deletions

View File

@@ -0,0 +1,23 @@
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import { join } from "node:path";
import test from "node:test";
const campaignFormDialogPath = join(
process.cwd(),
"components",
"campaigns",
"campaign-form-dialog.tsx",
);
test("CampaignFormDialog presents campaigns as lead discovery only", async () => {
const source = await readFile(campaignFormDialogPath, "utf8");
assert.match(source, /Max\. neue Leads/);
assert.doesNotMatch(source, /Max\. Audits/);
assert.match(
source,
/maxAuditsPerRun:\s*campaign\.maxAuditsPerRun\s*\?\?\s*campaignFormDefaults\.maxAuditsPerRun/,
);
assert.match(source, /maxAuditsPerRun:\s*values\.maxAuditsPerRun/);
});

View File

@@ -29,6 +29,8 @@ test("campaign board renders campaigns as responsive cards", async () => {
assert.match(source, /openEditDialog\(campaign\)/);
assert.match(source, /toggleCampaign\(campaign\)/);
assert.match(source, /runCampaign\(campaign\)/);
assert.match(source, /Lead-Limit:\s*\{campaign\.maxNewLeadsPerRun\}/);
assert.doesNotMatch(source, /Limits:\s*L/);
});
test("campaign board surfaces recent run logs", async () => {

View File

@@ -15,6 +15,8 @@ test("settings metadata rejects secret-like keys", () => {
"OPENROUTER_API_KEY",
"smtp.password",
"googlePlacesToken",
"LOCAL_BUSINESS_DATA_API_KEY",
"rapidapi.token",
"provider_secret",
"convex credential",
];

View File

@@ -211,6 +211,7 @@ test("duplicate detection uses placeId and websiteDomain", () => {
const existingLeads = [
{
googlePlaceId: "dup-1",
sourceBusinessId: "business-dup-1",
websiteDomain: "other.de",
email: "blocked@example.de",
},
@@ -233,6 +234,31 @@ test("duplicate detection uses placeId and websiteDomain", () => {
googlePrimaryType: null,
googleMapsUrl: null,
sourceProvider: "google_places",
sourceBusinessId: "business-other",
sourceFetchedAt: 0,
},
existingLeads,
),
true,
);
assert.equal(
isDuplicateCandidate(
{
placeId: "none",
businessName: "Test",
address: "A",
websiteUrl: null,
websiteDomain: null,
phone: null,
rating: null,
userRatingCount: null,
businessStatus: null,
googleTypes: [],
googlePrimaryType: null,
googleMapsUrl: null,
sourceProvider: "local_business_data",
sourceBusinessId: "business-dup-1",
sourceFetchedAt: 0,
},
existingLeads,
@@ -439,9 +465,10 @@ test("probable duplicates are detected by normalized company+address or normaliz
);
});
test("blacklist matches include google_place_id, domain, company and phone", () => {
test("blacklist matches include source ids, domain, company and phone", () => {
const candidate = {
placeId: "place-blacklisted",
sourceBusinessId: "business-blacklisted",
businessName: "Muster GmbH",
address: "A",
websiteUrl: "https://www.Blocked.de",
@@ -461,6 +488,7 @@ test("blacklist matches include google_place_id, domain, company and phone", ()
assert.deepEqual(getBlacklistLookupValues(candidate), [
{ type: "google_place_id", normalizedValue: "place-blacklisted" },
{ type: "source_business_id", normalizedValue: "business-blacklisted" },
{ type: "domain", normalizedValue: "blocked.de" },
{ type: "company", normalizedValue: "muster gmbh" },
{ type: "phone", normalizedValue: "4930555123" },
@@ -477,6 +505,11 @@ test("blacklist matches include google_place_id, domain, company and phone", ()
value: "place-blacklisted",
normalizedValue: "place-blacklisted",
},
{
type: "source_business_id",
value: "business-blacklisted",
normalizedValue: "business-blacklisted",
},
{ type: "domain", value: "blocked.de", normalizedValue: "blocked.de" },
{ type: "company", value: "Muster GmbH", normalizedValue: "muster gmbh" },
{ type: "phone", value: "+49 30 555 123", normalizedValue: "4930555123" },
@@ -498,7 +531,15 @@ test("blacklist matches include google_place_id, domain, company and phone", ()
const matchTypes = matches.map((match) => match.type).sort();
assert.deepEqual(
matchTypes,
["company", "domain", "google_place_id", "phone", "phone", "email"].sort(),
[
"company",
"domain",
"google_place_id",
"source_business_id",
"phone",
"phone",
"email",
].sort(),
);
});
@@ -522,6 +563,7 @@ test("company normalization for blacklist lookup uses text normalization", () =>
assert.deepEqual(getBlacklistLookupValues(candidate), [
{ type: "google_place_id", normalizedValue: "place-company-spaces" },
{ type: "source_business_id", normalizedValue: "place-company-spaces" },
{ type: "company", normalizedValue: "muster gmbh" },
{ type: "phone", normalizedValue: "4930555123" },
{ type: "phone", normalizedValue: "+49 30 555 123" },

View File

@@ -0,0 +1,152 @@
import assert from "node:assert/strict";
import test from "node:test";
import {
LOCAL_BUSINESS_DATA_HOST,
buildLocalBusinessSearchUrl,
getLocalBusinessSearchSpec,
normalizeLocalBusinessSearchResponse,
} from "../lib/lead-discovery-local-business";
test("Local Business Data search URL uses RapidAPI host-compatible query params", () => {
const url = new URL(
buildLocalBusinessSearchUrl({
query: "Anwalt in 10115 Deutschland",
limit: 9999,
}),
);
assert.equal(url.origin, `https://${LOCAL_BUSINESS_DATA_HOST}`);
assert.equal(url.pathname, "/search");
assert.equal(url.searchParams.get("query"), "Anwalt in 10115 Deutschland");
assert.equal(url.searchParams.get("limit"), "500");
assert.equal(url.searchParams.get("language"), "de");
assert.equal(url.searchParams.get("region"), "de");
assert.equal(url.searchParams.get("extract_emails_and_contacts"), "true");
});
test("Local Business Data campaign spec builds the SaaS lead-discovery query from niche and PLZ", () => {
const spec = getLocalBusinessSearchSpec({
categoryMode: "custom",
category: "Anderes",
customSearchTerm: "Webdesigner fuer Restaurants",
postalCode: "79098",
maxNewLeads: 8,
});
assert.equal(spec.query, "Webdesigner fuer Restaurants in 79098 Deutschland");
assert.equal(spec.limit, 8);
assert.match(spec.url, /extract_emails_and_contacts=true/);
});
test("Local Business Data response normalizes direct business emails into lead candidates", () => {
const candidates = normalizeLocalBusinessSearchResponse(
{
status: "OK",
request_id: "req-123",
data: [
{
business_id: "biz-1",
place_id: "place-1",
name: "Kanzlei Beispiel",
full_address: "Musterstrasse 1, 10115 Berlin",
website: "https://www.beispiel-kanzlei.de/kontakt",
phone_number: "+49 30 123456",
rating: 4.7,
review_count: 31,
business_status: "OPERATIONAL",
types: ["lawyer"],
google_maps_url: "https://maps.google.com/?cid=biz-1",
emails: ["Herr.Bewerber@beispiel-kanzlei.de", "Info@Beispiel-Kanzlei.de"],
},
],
},
1717480000000,
);
assert.equal(candidates.length, 1);
assert.equal(candidates[0]?.sourceProvider, "local_business_data");
assert.equal(candidates[0]?.sourceBusinessId, "biz-1");
assert.equal(candidates[0]?.placeId, "place-1");
assert.equal(candidates[0]?.businessName, "Kanzlei Beispiel");
assert.equal(candidates[0]?.websiteDomain, "beispiel-kanzlei.de");
assert.equal(candidates[0]?.contactEmails?.length, 2);
assert.equal(candidates[0]?.contactEmails?.[1]?.email, "info@beispiel-kanzlei.de");
assert.equal(candidates[0]?.contactEmails?.[1]?.emailSource, "local_business_data");
});
test("Local Business Data response reads extracted emails from emails_and_contacts", () => {
const candidates = normalizeLocalBusinessSearchResponse(
{
status: "OK",
request_id: "req-nested-email",
data: [
{
business_id: "biz-nested",
name: "PORZIG Immobilien GmbH",
full_address: "Silberstrasse 18, 08451 Crimmitschau",
website: "https://porzig.info",
phone_number: "+49 3762 759775",
emails_and_contacts: {
emails: [
"info@porzig.info",
"Info@Porzig.Info",
"makler@porzig.info",
],
},
},
],
},
1717480000003,
);
assert.equal(candidates.length, 1);
assert.equal(candidates[0]?.contactEmails?.length, 2);
assert.equal(candidates[0]?.email, "info@porzig.info");
assert.equal(candidates[0]?.emailSource, "local_business_data");
assert.deepEqual(
candidates[0]?.contactEmails?.map((entry) => entry.email),
["info@porzig.info", "makler@porzig.info"],
);
});
test("Local Business Data response accepts object-wrapped business arrays", () => {
const candidates = normalizeLocalBusinessSearchResponse(
{
status: "OK",
request_id: "req-456",
data: {
businesses: [
{
business_id: "biz-2",
name: "Malerbetrieb Beispiel",
address: "Hauptstrasse 2, 79098 Freiburg",
site: "https://maler.example",
phone: "+49 761 123",
email: "kontakt@maler.example",
},
],
},
},
1717480000001,
);
assert.equal(candidates.length, 1);
assert.equal(candidates[0]?.placeId, "biz-2");
assert.equal(candidates[0]?.email, "kontakt@maler.example");
});
test("Local Business Data response throws provider error messages", () => {
assert.throws(
() =>
normalizeLocalBusinessSearchResponse(
{
status: "ERROR",
request_id: "req-error",
error: { message: "Missing query", code: 400 },
},
1717480000002,
),
/Local Business Data API error 400: Missing query/,
);
});

View File

@@ -61,24 +61,49 @@ test("persistDiscoveredLeads does not schedule website enrichment jobs directly"
);
});
test("processCampaignRun schedules website enrichment after lead persistence", () => {
test("persistDiscoveredLeads backfills missing emails on duplicate re-runs only", () => {
const source = extractExportSource("persistDiscoveredLeads");
assert.match(source, /getUsableContactEmail\(candidate\)/);
assert.match(source, /duplicateLeadForEmailBackfill/);
assert.match(source, /ctx\.db\.patch\(\s*duplicateLeadForEmailBackfill\._id/);
assert.match(source, /normalizedEmail:\s*usableEmail\.email/);
assert.match(source, /email:\s*usableEmail\.email/);
assert.match(source, /contactStatus\s*=\s*"new"/);
assert.match(source, /contactStatus\s*!==\s*"do_not_contact"/);
assert.match(source, /blacklistStatus\s*!==\s*"blocked"/);
assert.match(source, /duplicateByEmailRows\.length\s*===\s*0/);
assert.doesNotMatch(source, /internal\.websiteEnrichment\.queueLeadEnrichment/);
assert.doesNotMatch(source, /internal\.pageSpeed\.queueLeadPageSpeedAudit/);
});
test("processCampaignRun uses Local Business Data and does not schedule website enrichment", () => {
const source = extractExportSource("processCampaignRun");
const persistIndex = source.indexOf(
"internal.leadDiscovery.persistDiscoveredLeads",
);
const queueCall = source.indexOf("internal.websiteEnrichment.queueLeadEnrichment");
const eventMessageIndex = source.indexOf("Website-Kontaktanreicherung geplant.");
assert.notEqual(persistIndex, -1, "processCampaignRun should persist discovered leads");
assert.notEqual(queueCall, -1, "processCampaignRun should schedule website enrichment");
assert.notEqual(eventMessageIndex, -1, "processCampaignRun should append enrichment schedule events");
assert.ok(
persistIndex < queueCall,
"processCampaignRun should schedule enrichment after persistence succeeds",
assert.equal(
queueCall,
-1,
"Campaign discovery must not schedule website enrichment in the SaaS flow",
);
assert.ok(
queueCall < eventMessageIndex,
"processCampaignRun should append enrichment event after scheduling",
assert.equal(
source.includes("GOOGLE_GEOCODING_API_KEY") || source.includes("GOOGLE_PLACES_API_KEY"),
false,
"Campaign discovery should no longer require Google lead-discovery keys",
);
assert.match(
source,
/LOCAL_BUSINESS_DATA_API_KEY/,
"Campaign discovery should read the Local Business Data API key",
);
assert.match(
source,
/local_business_data/,
"Campaign discovery should record Local Business Data as the source",
);
});

View File

@@ -80,25 +80,34 @@ test("LeadsReviewTable uses compact card summaries with modal review details", a
"Location should use overflow-safe text classes in compact card.",
);
const emailSpanMatch = source.match(
/<span className="([^"]+)">\s*\{lead\.email \|\| "Keine E-Mail"\}\s*<\/span>/,
const emailAnchorMatch = source.match(
/<a className="([^"]+)" href=\{emailHref\}>\s*\{lead\.email\}\s*<\/a>/,
);
assert.ok(
emailSpanMatch !== null &&
emailAnchorMatch !== null &&
/(?:^|\s)(break-all|max-w-full|min-w-0)(?:\s|$)/.test(
emailSpanMatch[1],
emailAnchorMatch[1],
),
"Lead email should use overflow-safe text classes in compact card.",
"Lead email should use an overflow-safe mailto link in compact card.",
);
assert.match(source, /<span className="[^"]*">\s*Keine E-Mail\s*<\/span>/);
const phoneSpanMatch = source.match(
/<span className="([^"]+)">\s*\{lead\.phone\}\s*<\/span>/,
const phoneAnchorMatch = source.match(
/<a className="([^"]+)" href=\{phoneHref\}>\s*\{lead\.phone\}\s*<\/a>/,
);
assert.ok(
phoneSpanMatch !== null &&
/(?:^|\s)(break-all|max-w-full|min-w-0)(?:\s|$)/.test(phoneSpanMatch[1]),
"Lead phone should use overflow-safe text classes in compact card.",
phoneAnchorMatch !== null &&
/(?:^|\s)(break-all|max-w-full|min-w-0)(?:\s|$)/.test(phoneAnchorMatch[1]),
"Lead phone should use an overflow-safe tel link in compact card.",
);
assert.match(source, /toEmailHref/);
assert.match(source, /mailto:\$\{normalizedEmail\}/);
assert.match(source, /toPhoneHref/);
assert.match(source, /tel:\$\{dialablePhone\}/);
assert.match(source, /toWebsiteHref/);
assert.match(source, /href=\{websiteHref\}/);
assert.match(source, /target="_blank"/);
assert.match(source, /rel="noreferrer"/);
assert.match(source, /Kontaktstatus/);
assert.match(source, /Review-E-Mail/);
@@ -122,3 +131,15 @@ test("LeadsReviewTable exposes count filters and live status feedback", async ()
assert.match(source, /role="status"/);
assert.match(source, /role="alert"/);
});
test("LeadsReviewTable exposes explicit manual audit start action", async () => {
const source = await readFile(leadsReviewPath, "utf8");
assert.match(source, /api\.pageSpeed\.requestLeadAudit/);
assert.match(source, /api\.pageSpeed\.getLeadAuditStartStates/);
assert.match(source, /Audit starten/);
assert.match(source, /auditStartDisabledReason/);
assert.match(source, /Website fehlt|Keine Website/);
assert.match(source, /Audit l(?:ä|ae)uft|Audit läuft/);
assert.doesNotMatch(source, /api\.websiteEnrichment\.queueLeadEnrichment/);
});

View File

@@ -10,7 +10,7 @@ test("integration readiness covers all MVP providers", () => {
assert.deepEqual(
integrationReadinessDefinitions.map((definition) => definition.id),
[
"google",
"local_business_data",
"pagespeed",
"openrouter",
"screenshotone",
@@ -24,24 +24,21 @@ test("integration readiness covers all MVP providers", () => {
test("integration readiness reports missing configuration without leaking values", () => {
const rows = getIntegrationReadiness({
GOOGLE_GEOCODING_API_KEY: "secret-google",
GOOGLE_PLACES_API_KEY: "secret-places",
LOCAL_BUSINESS_DATA_API_KEY: "secret-local-business",
PAGESPEED_API_KEY: "",
});
const google = rows.find((row) => row.id === "google");
const localBusinessData = rows.find((row) => row.id === "local_business_data");
const pageSpeed = rows.find((row) => row.id === "pagespeed");
assert.equal(google?.status, "configured");
assert.equal(localBusinessData?.status, "configured");
assert.equal(pageSpeed?.status, "missing");
assert.equal(JSON.stringify(rows).includes("secret-google"), false);
assert.equal(JSON.stringify(rows).includes("secret-places"), false);
assert.equal(JSON.stringify(rows).includes("secret-local-business"), false);
});
test("integration readiness treats ScreenshotOne as required and Jina as optional", () => {
const rows = getIntegrationReadiness({
GOOGLE_GEOCODING_API_KEY: "secret-google",
GOOGLE_PLACES_API_KEY: "secret-places",
LOCAL_BUSINESS_DATA_API_KEY: "secret-local-business",
PAGESPEED_API_KEY: "secret-pagespeed",
PAGESPEED_TIMEOUT_MS: "60000",
OPENROUTER_API_KEY: "secret-openrouter",

View File

@@ -16,7 +16,7 @@ test("settings page surfaces integration status instead of a placeholder", () =>
assert.match(pageSource, /OperationsReadiness/);
for (const label of [
"Google",
"Local Business Data",
"PageSpeed",
"OpenRouter",
"ScreenshotOne",

View File

@@ -112,13 +112,14 @@ test("OutreachReviewWorkspace separates audit publication from email approval",
assert.match(source, /Audit veröffentlichen/);
assert.match(source, /Änderungen speichern/);
assert.match(source, /E-Mail freigeben und senden/);
assert.match(source, /E-Mail freigeben/);
assert.match(source, /Final senden/);
assert.match(source, /useAction/);
assert.match(source, /outreachSendAction[\s\S]*sendApprovedEmail/);
const auditPublishIndex = source.indexOf("Audit veröffentlichen");
const auditSaveIndex = source.indexOf("Änderungen speichern");
const emailApprovalIndex = source.indexOf("E-Mail freigeben und senden");
const emailApprovalIndex = source.indexOf("E-Mail freigeben");
assert.ok(auditPublishIndex >= 0);
assert.ok(auditSaveIndex >= 0);

View File

@@ -84,6 +84,8 @@ test("pageSpeed module exports mutation contracts", () => {
assert.equal(existsSync(pageSpeedPath), true, "pageSpeed.ts should be present");
const exports = getExportedConstNames(sourceFile);
const required = [
"getLeadAuditStartStates",
"requestLeadAudit",
"queueLeadPageSpeedAudit",
"startPageSpeedAuditRun",
"persistPageSpeedResult",
@@ -110,12 +112,40 @@ test("pageSpeed module uses internalMutation for queue/start/persist/finish", ()
}
});
test("requestLeadAudit is a public authenticated mutation that queues PageSpeed only after user intent", () => {
const source = extractExportSource("requestLeadAudit");
assert.equal(
hasPattern(pageSpeedSource, /export const requestLeadAudit = mutation\s*\(/),
true,
"requestLeadAudit should be a public mutation for UI-triggered audit starts.",
);
assert.match(source, /requireOperator\(ctx\)/);
assert.match(source, /queueLeadPageSpeedAuditForLead/);
assert.match(source, /triggeredBy:\s*"manual"/);
assert.match(source, /Audit-Start wurde manuell angefordert\./);
});
test("getLeadAuditStartStates exposes active audit run status for lead review buttons", () => {
const source = extractExportSource("getLeadAuditStartStates");
assert.equal(
hasPattern(pageSpeedSource, /export const getLeadAuditStartStates = query\s*\(/),
true,
"getLeadAuditStartStates should be a public query.",
);
assert.match(source, /requireOperator\(ctx\)/);
assert.match(source, /leadIds:\s*v\.array\(v\.id\("leads"\)\)/);
assert.match(pageSpeedSource, /by_type_and_status_and_leadId/);
assert.match(source, /canStart/);
});
test("queueLeadPageSpeedAudit dedupes per lead and schedules pagespeed action", () => {
const queueSource = extractExportSource("queueLeadPageSpeedAudit");
const queueSource = pageSpeedSource;
assert.equal(
hasPattern(
queueSource,
/withIndex\(\s*"by_type_and_status_and_leadId"[\s\S]*?eq\("type",\s*"audit"\)[\s\S]*?eq\("status",\s*"pending"\)[\s\S]*?eq\("leadId",\s*args\.leadId\)/,
/withIndex\(\s*"by_type_and_status_and_leadId"[\s\S]*?eq\("type",\s*"audit"\)[\s\S]*?eq\("status",\s*"pending"\)[\s\S]*?eq\("leadId",\s*(?:args\.)?leadId\)/,
),
true,
"Queue should dedupe pending audit runs by type+status+leadId.",
@@ -123,7 +153,7 @@ test("queueLeadPageSpeedAudit dedupes per lead and schedules pagespeed action",
assert.equal(
hasPattern(
queueSource,
/withIndex\(\s*"by_type_and_status_and_leadId"[\s\S]*?eq\("type",\s*"audit"\)[\s\S]*?eq\("status",\s*"running"\)[\s\S]*?eq\("leadId",\s*args\.leadId\)/,
/withIndex\(\s*"by_type_and_status_and_leadId"[\s\S]*?eq\("type",\s*"audit"\)[\s\S]*?eq\("status",\s*"running"\)[\s\S]*?eq\("leadId",\s*(?:args\.)?leadId\)/,
),
true,
"Queue should dedupe running audit runs by type+status+leadId.",

View File

@@ -151,7 +151,7 @@ const usageReadQueries = [
test("usage domain constants declare supported providers and operations", () => {
assertHas(
/USAGE_EVENT_PROVIDERS\s*=\s*\[[\s\S]*"openrouter"[\s\S]*"screenshotone"[\s\S]*"jina"[\s\S]*"pagespeed"[\s\S]*"google_places"[\s\S]*\]\s*as const/,
/USAGE_EVENT_PROVIDERS\s*=\s*\[[\s\S]*"openrouter"[\s\S]*"screenshotone"[\s\S]*"jina"[\s\S]*"pagespeed"[\s\S]*"google_places"[\s\S]*"local_business_data"[\s\S]*\]\s*as const/,
domainSource,
"Domain should declare usage providers for all managed external services.",
);

View File

@@ -336,8 +336,8 @@ test("browserless website enrichment persists crawl evidence without screenshots
);
assert.equal(
hasPattern(fallbackSource, /internal\.pageSpeed\.queueLeadPageSpeedAudit/),
true,
"Browserless enrichment should keep the downstream PageSpeed handoff.",
false,
"Browserless enrichment must not automatically queue PageSpeed or audit runs.",
);
});
@@ -760,7 +760,7 @@ test("website enrichment guards long browser work before Convex action runtime a
);
});
test("processLeadEnrichment schedules PageSpeed audit jobs after successful enrichment", () => {
test("processLeadEnrichment does not schedule PageSpeed audit jobs after enrichment", () => {
const processBody = extractExportSource(actionSource, "processLeadEnrichment");
const persistIndex = processBody.indexOf(
"internal.websiteEnrichment.persistLeadEnrichmentResult",
@@ -769,49 +769,16 @@ test("processLeadEnrichment schedules PageSpeed audit jobs after successful enri
"internal.pageSpeed.queueLeadPageSpeedAudit",
persistIndex,
);
const finishIndex = processBody.indexOf(
"internal.websiteEnrichment.finishLeadEnrichmentRun",
persistIndex,
);
assert.notEqual(queueIndex, -1, "processLeadEnrichment should queue PageSpeed audits");
assert.notEqual(persistIndex, -1, "processLeadEnrichment should persist website enrichment result");
assert.notEqual(finishIndex, -1, "processLeadEnrichment should finish enrichment run");
assert.equal(
hasPattern(
processBody,
/runMutation\(\s*internal\.pageSpeed\.queueLeadPageSpeedAudit[\s\S]*leadId:\s*started\.lead\._id[\s\S]*parentRunId:\s*runId[\s\S]*\)/,
),
true,
"Queue call should pass lead ID and parent run ID",
);
assert.equal(queueIndex > persistIndex, true, "PageSpeed queueing should happen after persistence");
assert.equal(queueIndex < finishIndex, true, "PageSpeed queueing should happen before success finish");
});
test("processLeadEnrichment records warning on PageSpeed queue failure and continues", () => {
const processBody = extractExportSource(actionSource, "processLeadEnrichment");
assert.equal(
hasPattern(
processBody,
/try\s*\{[\s\S]*internal\.pageSpeed\.queueLeadPageSpeedAudit[\s\S]*\}\s*catch\s*\([^)]*\)\s*\{[\s\S]*internal\.runs\.appendEventInternal[\s\S]*level:\s*"warning"/,
),
true,
"Queueing PageSpeed should be wrapped in warning-safe try/catch",
);
assert.equal(
hasPattern(
processBody,
/PageSpeed-Analyse konnte nicht in die Warteschlange gesetzt werden\./,
),
true,
"Warning event should describe queue failure",
queueIndex,
-1,
"Website enrichment must not automatically queue PageSpeed or audit runs",
);
});
test("processLeadEnrichment regression: queue PageSpeed on invalid URL failure when started lead exists", () => {
test("processLeadEnrichment regression: invalid URL failure does not queue PageSpeed", () => {
const processBody = extractExportSource(actionSource, "processLeadEnrichment");
const invalidUrlStart = processBody.indexOf("if (!rootUrl)");
assert.notEqual(invalidUrlStart, -1, "Invalid URL guard should exist");
@@ -823,27 +790,16 @@ test("processLeadEnrichment regression: queue PageSpeed on invalid URL failure w
"Invalid URL branch should return null",
);
const queueCallInInvalidUrl = processBody.indexOf(
"internal.pageSpeed.queueLeadPageSpeedAudit",
invalidUrlStart,
);
assert.equal(
queueCallInInvalidUrl > invalidUrlStart && queueCallInInvalidUrl < invalidUrlReturnNull,
true,
"Invalid URL failure path should queue PageSpeed before returning.",
);
const invalidUrlBranch = processBody.slice(invalidUrlStart, invalidUrlReturnNull);
assert.equal(
hasPattern(
invalidUrlBranch,
/leadId:\s*started\.lead\._id[\s\S]*?parentRunId:\s*runId/,
processBody.slice(invalidUrlStart, invalidUrlReturnNull).includes(
"internal.pageSpeed.queueLeadPageSpeedAudit",
),
true,
"Invalid URL queue payload should use started.lead._id and parentRunId runId.",
false,
"Invalid URL branch should not queue PageSpeed automatically.",
);
});
test("processLeadEnrichment regression: queue PageSpeed in fatal catch path with started lead", () => {
test("processLeadEnrichment regression: fatal catch path does not queue PageSpeed", () => {
const processBody = extractExportSource(actionSource, "processLeadEnrichment");
const outerCatchStart = processBody.lastIndexOf("catch (error)");
assert.notEqual(outerCatchStart, -1, "Outer catch block should exist");
@@ -858,24 +814,11 @@ test("processLeadEnrichment regression: queue PageSpeed in fatal catch path with
"Outer catch should return null on unrecoverable errors.",
);
const queueCallInCatch = processBody.indexOf(
"internal.pageSpeed.queueLeadPageSpeedAudit",
outerCatchStart,
);
assert.equal(
queueCallInCatch > outerCatchStart &&
queueCallInCatch > startedGuard &&
queueCallInCatch < catchReturnNull,
true,
"Fatal catch path should queue PageSpeed before returning, while started lead exists.",
);
const catchBlock = processBody.slice(outerCatchStart, catchReturnNull);
assert.equal(
hasPattern(
catchBlock,
/leadId:\s*started\.lead\._id[\s\S]*?parentRunId:\s*runId/,
processBody.slice(outerCatchStart, catchReturnNull).includes(
"internal.pageSpeed.queueLeadPageSpeedAudit",
),
true,
"Catch-path PageSpeed queue payload should use started.lead._id and parentRunId runId.",
false,
"Fatal catch path should not queue PageSpeed automatically.",
);
});