From 265761733e0ba4ec5983ffcea55764e46ef3bc0b Mon Sep 17 00:00:00 2001 From: Anthony Ettinger Date: Tue, 28 Jul 2026 03:17:31 +0000 Subject: [PATCH] fix(leads): honour scan_prospects on the discovery path too MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The setting was threaded into the runner's stage-3 research call and nowhere else, so the discovery top-up — the path that sees every newly discovered domain — kept queueing a scan per candidate no matter what the campaign asked for. A campaign with scanning off still scanned everything it found, which is exactly the behaviour the setting exists to prevent. Observable in the audits table: a tick on a scan_prospects=false campaign produced fresh audits of artstation.com, adobe.com and every other domain discovery turned up. The guard asserts every researchProspect call in the runner passes skipScan, and derives it from the campaign rather than hardcoding it. Source-level, because the alternative is standing up the runner with a live database and a scan worker to watch for a call that should not happen. Reverting the fix fails it. Co-Authored-By: Claude Opus 5 (1M context) --- lib/outreach/runner.ts | 5 +++++ tests/runner-scan-gate.test.ts | 41 ++++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 tests/runner-scan-gate.test.ts diff --git a/lib/outreach/runner.ts b/lib/outreach/runner.ts index a6773ed4..d2fc6c7a 100644 --- a/lib/outreach/runner.ts +++ b/lib/outreach/runner.ts @@ -256,6 +256,11 @@ export async function runEmailCampaignTick(campaign: CampaignRow): Promise { + const source = readFileSync(RUNNER, "utf8"); + + // Every researchProspect({...}) call in the runner. + const calls = [...source.matchAll(/researchProspect\(\{([\s\S]*?)\}\)/g)].map((m) => m[1]); + + it("researches prospects in more than one place", () => { + // If this drops to one, the test below stops proving anything. + expect(calls.length).toBeGreaterThanOrEqual(2); + }); + + it("passes skipScan at every research call site", () => { + const missing = calls.filter((body) => !body.includes("skipScan")); + expect( + missing, + `researchProspect call(s) without skipScan — a campaign with scanning off would still scan:\n${missing.join("\n---\n")}`, + ).toHaveLength(0); + }); + + it("derives skipScan from the campaign setting rather than hardcoding it", () => { + for (const body of calls) { + expect(body).toMatch(/skipScan:\s*!campaign\.scan_prospects/); + } + }); +});