import assert from "node:assert/strict";
import { mkdtemp, readFile, readdir, writeFile } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { LinkedInToolsAdapter } from "../src/adapters/linkedinTools.js";
import { DirectoryDiscoveryAdapter } from "../src/adapters/directoryDiscovery.js";
import { ownerEvidenceToolPolicy } from "../src/adapters/ownerEvidenceTools.js";
import { OwnerRabbitHoleAdapter } from "../src/adapters/ownerRabbitHole.js";
import { classifyMapsFailure } from "../src/adapters/mapsDiscovery.js";
import { leadsCsv, leadExportFilename, leadsJson } from "../src/export.js";
import { FlowWatchdog } from "../src/flowWatchdog.js";
import { HermesAgent } from "../src/hermes.js";
import { MiniMaxClient } from "../src/minimax.js";
import { OWNER_FIRST_STRATEGY_VERSION, buildOwnerFirstDiscoveryPlan, buildOwnerFirstQueryVariants } from "../src/ownerFirstDiscovery.js";
import { repairPlaybook } from "../src/repairPlaybook.js";
import { Storage } from "../src/storage.js";

function makeStorage(memory = { skills: [], avoid: [] }) {
  const runs = new Map();
  const appended = { skills: [], avoid: [] };
  return {
    rawDir: "/tmp",
    appended,
    async getOperationalMemory() {
      return JSON.parse(JSON.stringify(memory));
    },
    async saveRun(run) {
      runs.set(run.id, JSON.parse(JSON.stringify(run)));
    },
    async getRun(id) {
      return JSON.parse(JSON.stringify(runs.get(id)));
    },
    async listRecoverableRuns() {
      return [...runs.values()]
        .filter((run) => ["queued", "running", "interrupted"].includes(run.status))
        .map((run) => JSON.parse(JSON.stringify(run)));
    },
    async appendSkill(skill) {
      appended.skills.push(skill);
    },
    async appendAvoid(entry) {
      appended.avoid.push(entry);
    }
  };
}

async function waitFor(agent) {
  while (agent.activeRunCount()) {
    await new Promise((resolve) => setTimeout(resolve, 5));
  }
}

function makeRows(prefix, count = 5) {
  return Array.from({ length: count }, (_, index) => ({
    name: `${prefix} ${index}`,
    address: `${index} Main St`,
    phone: "555-111-2222",
    website: `https://${prefix.toLowerCase().replace(/\s+/g, "-")}-${index}.example`
  }));
}

async function testDefaultRunUsesBusinessContactWorkflow() {
  const storage = makeStorage();
  const agent = new HermesAgent({ projectName: "eval", rootDir: process.cwd(), storage });
  agent.googleMaps = {
    async discover() {
      return {
        rawPath: "/tmp/business-contact.csv",
        rows: [{
          name: "Acme Dental",
          address: "1 Main St, New York, NY",
          phone: "212-555-0100",
          website: "https://acme.example",
          sourceUrl: "https://maps.example/acme"
        }]
      };
    }
  };
  agent.scrapling = {
    async enrich() {
      return {
        ok: true,
        emails: ["hello@acme.example"],
        phones: ["212-555-0100"],
        links: ["https://acme.example/contact", "https://instagram.com/acmedental"],
        socialLinks: ["https://instagram.com/acmedental"],
        text: "Acme Dental provides dental services in New York. Contact hello@acme.example."
      };
    }
  };
  agent.minimax = {
    async qualifyLead({ criteria }) {
      assert.match(criteria, /public business contact information/i);
      assert.doesNotMatch(criteria, /Identify a real person owner/i);
      return { qualified: true, score: 92, reason: "Business contact evidence is present.", missing: [] };
    },
    async proposeSearchStrategy() { return { queries: ["dentists New York public business contact"], reason: "business contact default" }; },
    async proposeRunReview() { return { summary: "ok", skills: [], avoid: [] }; }
  };

  const run = await agent.startRun({ niche: "dentists", cities: "New York, NY", target: 1, minScore: 50, maxQueriesPerCity: 1, maxPlannedLocations: 1 });
  await waitFor(agent);
  const finalRun = await storage.getRun(run.id);
  assert.equal(finalRun.input.source, "lead-scraper");
  assert.equal(finalRun.input.strategyVersion, "business-contact-v2");
  assert.equal(finalRun.workflow.name, "Business Contact Lead Machine");
  assert.equal(finalRun.qualifiedLeads.length, 1);
  assert.equal(finalRun.qualifiedLeads[0].contactTarget, "business");
  assert.equal(finalRun.qualifiedLeads[0].email, "hello@acme.example");
  assert.equal(finalRun.qualifiedLeads[0].campaignReady, true);
  assert.equal(Boolean(finalRun.qualifiedLeads[0].ownerEmail), false);
  assert.equal(finalRun.steps.some((step) => /owner-contact|owner-direct|owner-associated/i.test(step.message || "")), false);
}

function installOwnerSuccess(agent) {
  agent.ownerRabbitHole = {
    async findOwnerContact({ lead }) {
      return {
        businessEmail: "seed@example.com",
        businessPhone: "555-111-2222",
        ownerName: "Jane Morgan",
        ownerTitle: "Owner",
        ownerEmail: "owner@example.com",
        ownerPhone: "+1 555 222 3333",
        ownerEmails: ["owner@example.com"],
        ownerPhones: ["+1 555 222 3333"],
        ownerLinkedIn: "https://www.linkedin.com/in/mock-owner",
        ownerSocialProfiles: ["https://www.linkedin.com/in/mock-owner"],
        ownerEvidenceUrl: "https://www.linkedin.com/in/mock-owner",
        ownerContactSource: "test-owner-profile",
        ownerContactEvidence: [
          { type: "email", value: "owner@example.com", url: "https://www.linkedin.com/in/mock-owner", source: "test-owner-profile" },
          { type: "phone", value: "+1 555 222 3333", url: "https://www.linkedin.com/in/mock-owner", source: "test-owner-profile" }
        ],
        rabbitHoleStatus: "owner-contact-found",
        rabbitHoleStrategy: { searchQueries: ["mock owner linkedin"], strategyReason: "test fixture" }
      };
    }
  };
}

async function testMemoryReuse() {
  const storage = makeStorage({
    skills: [{ title: "Use emergency repair", when: "empty results", do: 'query "emergency roof repair {city}"', avoid: "generic only" }],
    avoid: [{ stage: "discovery", failure: "duplicate results", context: { query: "roofers in Miami, FL, USA" }, avoid: "Do not repeat exact query." }]
  });
  const agent = new HermesAgent({ projectName: "eval", rootDir: process.cwd(), storage });
  installOwnerSuccess(agent);
  const calls = [];
  agent.googleMaps = {
    async discover({ query }) {
      calls.push(query);
      return { rawPath: `/tmp/${calls.length}.csv`, rows: makeRows("Memory Roof") };
    }
  };
  agent.scrapling = { async enrich() { return { ok: true, emails: [], phones: [], text: "" }; } };
  agent.minimax = {
    async qualifyLead() { return { qualified: true, score: 100, reason: "ok", missing: [] }; },
    async proposeSearchStrategy() { return { queries: ["commercial roof repair Miami FL USA"], reason: "planner should not outrank learned memory" }; },
    async proposeRunReview() { return { summary: "ok", skills: [], avoid: [] }; }
  };

  const run = await agent.startRun({ source: "multi-source-owner-evidence", niche: "roofers", cities: "Miami, FL", target: 1, minScore: 50 });
  await waitFor(agent);
  const finalRun = await storage.getRun(run.id);
  assert.match(calls[0], /emergency roof repair/i);
  assert.equal(calls.includes("roofers in Miami, FL, USA"), false);
  assert.equal(finalRun.agentDecisions.some((decision) => decision.type === "load_memory"), true);
}

async function testMemoryDoesNotSearchScraperLogs() {
  const storage = makeStorage({
    skills: [
      {
        title: "Extend scraper timeout when Google-Maps-Scrapper logs Arrived at all available",
        when: "discovery failure shows Timed out after 45000ms",
        do: "Retry same query with timeout=90000 after Arrived at all available and Page.wait_for_selector timeout.",
        avoid: "Do not search the scraper log text."
      }
    ],
    avoid: [
      {
        stage: "discovery",
        failure: "Google-Maps-Scrapper failed: Traceback Page.wait_for_selector Timeout 30000ms exceeded. Arrived at all available.",
        context: { query: "Arrived at all available USA" },
        avoid: "Never search scraper status text."
      }
    ]
  });
  const agent = new HermesAgent({ projectName: "eval", rootDir: process.cwd(), storage });
  installOwnerSuccess(agent);
  const calls = [];
  agent.googleMaps = {
    async discover({ query }) {
      calls.push(query);
      return { rawPath: `/tmp/log-filter-${calls.length}.csv`, rows: makeRows("Log Filter Roof") };
    }
  };
  agent.scrapling = { async enrich() { return { ok: true, emails: [], phones: [], text: "" }; } };
  agent.minimax = {
    async proposeSearchStrategy() {
      return { queries: ["emergency roof repair Miami FL USA"], reason: "valid planned query" };
    },
    async qualifyLead() { return { qualified: true, score: 100, reason: "ok", missing: [] }; },
    async proposeRunReview() { return { summary: "ok", skills: [], avoid: [] }; }
  };

  const run = await agent.startRun({ source: "multi-source-owner-evidence", niche: "roofers", cities: "Miami, FL", target: 1, minScore: 50 });
  await waitFor(agent);
  assert.equal(calls.some((query) => /arrived at all available|timed out|traceback|wait_for_selector/i.test(query)), false);
  assert.equal(calls[0], "emergency roof repair Miami FL USA");
  const finalRun = await storage.getRun(run.id);
  assert.equal(finalRun.status, "complete");
}

async function testDirectoryOfficialSiteFallbackUnblocksSeedDiscovery() {
  const storage = makeStorage();
  const agent = new HermesAgent({ projectName: "eval", rootDir: process.cwd(), storage });
  let mapsCalls = 0;
  agent.googleMaps = {
    available() { return true; },
    async discover() {
      mapsCalls += 1;
      throw new Error("Google-Maps-Scrapper failed: Page.goto: net::ERR_CONNECTION_CLOSED at https://www.google.com/maps/");
    }
  };
  agent.directoryDiscovery = {
    async searchDirectoryOfficialSite() {
      return {
        seeds: [
          {
            name: "Fairbanks Pipe Pros",
            category: "plumbers",
            address: "100 Main St, Fairbanks, AK, USA",
            city: "Fairbanks, AK, USA",
            country: "United States",
            phone: "907-555-0101",
            website: "https://fairbankspipepros.example",
            source: "bbb",
            sourceUrl: "https://www.bbb.org/search"
          },
          {
            name: "Aurora Drain Service",
            category: "plumbers",
            address: "200 Main St, Fairbanks, AK, USA",
            city: "Fairbanks, AK, USA",
            country: "United States",
            phone: "907-555-0202",
            website: "https://auroradrain.example",
            source: "angi",
            sourceUrl: "https://www.angi.com/companylist"
          },
          {
            name: "Canada Pipe Shop",
            category: "plumbers",
            address: "Whitehorse, Canada",
            city: "Whitehorse, Canada",
            country: "Canada",
            phone: "867-555-0101",
            website: "https://canadapipe.example",
            source: "directory",
            sourceUrl: "https://example.test"
          }
        ],
        sourceBreakdown: [{ source: "bbb", ok: true, count: 2 }]
      };
    }
  };
  agent.scrapling = {
    async enrich() {
      return { ok: true, emails: ["office@example.com"], phones: ["907-555-0101"], text: "Call our office for plumbing help." };
    }
  };
  agent.ownerRabbitHole = {
    async findOwnerContact({ lead }) {
      return {
        businessEmail: "office@example.com",
        businessPhone: lead.phone,
        ownerName: "",
        ownerTitle: "",
        ownerEmail: "",
        ownerPhone: "",
        ownerContactEvidence: [],
        ownerEvidence: [],
        rabbitHoleStatus: "owner-contact-exhausted"
      };
    }
  };
  agent.minimax = {
    async proposeSearchStrategy() { return { queries: [], reason: "use fixed templates" }; },
    async proposeRetry() { return { variable: "query", value: "plumbers Fairbanks AK official website", reason: "retry" }; },
    async qualifyLead() { return { qualified: true, score: 95, reason: "business is relevant", missing: [] }; },
    async proposeRunReview() { return { summary: "directory fallback", skills: [], avoid: [] }; }
  };

  const run = await agent.startRun({ source: "multi-source-owner-evidence", niche: "plumbers", cities: "Fairbanks, AK", target: 1, minScore: 50, maxQueriesPerCity: 4 });
  await waitFor(agent);
  const finalRun = await storage.getRun(run.id);
  assert.equal(mapsCalls >= 2, true);
  assert.equal(finalRun.counters.directoryFallbacks >= 1, true);
  assert.equal(finalRun.discoveredLeads.length >= 2, true);
  assert.equal(finalRun.discoveredLeads.some((lead) => /canada/i.test(`${lead.address} ${lead.city} ${lead.country}`)), false);
  assert.equal(finalRun.counters.ownerRabbitHoles >= 2, true);
  assert.equal(finalRun.counters.ownerContactsFound, 0);
  assert.equal(finalRun.qualifiedLeads.length, 0);
  assert.equal(finalRun.rejectedLeads.some((lead) => lead.reason && /owner/i.test(lead.ownerHuntDecision || lead.reason)), true);
}

async function testOwnerAssociatedFallbackQualifiesNamedOwnerWithBusinessContact() {
  const storage = makeStorage();
  const agent = new HermesAgent({ projectName: "eval", rootDir: process.cwd(), storage });
  agent.googleMaps = {
    async discover() {
      return {
        rawPath: "/tmp/owner-associated.csv",
        rows: [{
          name: "Plumber Kings",
          address: "123 Main St, Tucson, AZ, USA",
          phone: "520-777-0546",
          website: "https://theplumberkings.com/"
        }]
      };
    }
  };
  agent.scrapling = {
    async enrich() {
      return {
        ok: true,
        mode: "simple",
        text: "Plumber Kings serves Tucson. Chris DeVore is the owner. Call 520-777-0546.",
        emails: [],
        phones: ["520-777-0546"],
        links: [],
        socialLinks: []
      };
    }
  };
  agent.ownerRabbitHole = {
    async findOwnerContact() {
      return {
        businessPhone: "520-777-0546",
        ownerName: "Chris DeVore",
        ownerTitle: "Owner",
        ownerEvidenceUrl: "https://theplumberkings.com/about",
        ownerContactEvidence: [],
        ownerEmail: "",
        ownerPhone: "",
        ownerEmails: [],
        ownerPhones: [],
        rabbitHoleStatus: "owner-found-no-profile",
        ownerEvidence: [{ url: "https://theplumberkings.com/about", ownerCandidates: ["Chris DeVore"], textSample: "Chris DeVore is the owner." }]
      };
    }
  };
  agent.minimax = {
    async proposeSearchStrategy() { return { queries: [], reason: "fixed" }; },
    async qualifyLead() { return { qualified: false, score: 30, reason: "no owner reach channel", missing: ["owner phone"] }; },
    async proposeRunReview() { return { summary: "associated", skills: [], avoid: [] }; }
  };

  const run = await agent.startRun({ source: "multi-source-owner-evidence", niche: "plumbers", cities: "Tucson, AZ", target: 1, minScore: 60 });
  await waitFor(agent);
  const finalRun = await storage.getRun(run.id);
  assert.equal(finalRun.input.strategyVersion, OWNER_FIRST_STRATEGY_VERSION);
  assert.equal(finalRun.qualifiedLeads.length, 1);
  assert.equal(finalRun.qualifiedLeads[0].contactConfidence, "owner-associated");
  assert.equal(finalRun.qualifiedLeads[0].quality.ownerAssociatedFallback, true);
  assert.equal(finalRun.qualifiedLeads[0].ownerEvidenceGraph.strategyVersion, OWNER_FIRST_STRATEGY_VERSION);
  assert.equal(finalRun.qualifiedLeads[0].ownerEvidenceGraph.ownerIdentityProven, true);
  assert.equal(finalRun.qualifiedLeads[0].ownerEvidenceGraph.ownerAssociatedContactProven, true);
  assert.equal(finalRun.scaleAudit.ownerDirectContacts, 0);
  assert.equal(finalRun.scaleAudit.ownerAssociatedContacts, 1);
  assert.equal(finalRun.scaleAudit.targetReached, true);
  assert.equal(finalRun.counters.ownerContactsFound, 0);
}

async function testOwnerAssociatedFallbackRejectsBadNameEmailAndPhone() {
  const storage = makeStorage();
  const agent = new HermesAgent({ projectName: "eval", rootDir: process.cwd(), storage });
  agent.googleMaps = {
    async discover() {
      return {
        rawPath: "/tmp/owner-associated-bad.csv",
        rows: [{
          name: "Rad Plumbing",
          address: "123 Main St, Mesa, AZ, USA",
          phone: "2026-05-14",
          website: "https://radplumbingaz.com/"
        }]
      };
    }
  };
  agent.scrapling = {
    async enrich() {
      return {
        ok: true,
        mode: "simple",
        text: "Rad Plumbing. Message from the team. Contact 2026-05-14 or zooinfo@zoo.org.",
        emails: ["zooinfo@zoo.org"],
        phones: ["2026-05-14"],
        links: [],
        socialLinks: []
      };
    }
  };
  agent.ownerRabbitHole = {
    async findOwnerContact() {
      return {
        businessEmail: "zooinfo@zoo.org",
        businessPhone: "2026-05-14",
        ownerName: "Message From The",
        ownerEvidenceUrl: "https://radplumbingaz.com/about",
        ownerContactEvidence: [],
        ownerEmail: "",
        ownerPhone: "",
        ownerEmails: [],
        ownerPhones: [],
        rabbitHoleStatus: "owner-found-no-profile",
        ownerEvidence: [{ url: "https://radplumbingaz.com/about", ownerCandidates: ["Message From The"], textSample: "Message from the team." }]
      };
    }
  };
  agent.minimax = {
    async proposeSearchStrategy() { return { queries: [], reason: "fixed" }; },
    async proposeRetry() { return { variable: "query", value: "", reason: "stop" }; },
    async qualifyLead() { return { qualified: false, score: 30, reason: "bad associated contact", missing: ["owner contact"] }; },
    async proposeRunReview() { return { summary: "bad associated", skills: [], avoid: [] }; }
  };

  const run = await agent.startRun({ source: "multi-source-owner-evidence", niche: "plumbers", cities: "Mesa, AZ", target: 1, minScore: 60, maxQueriesPerCity: 1, maxPlannedLocations: 1 });
  await waitFor(agent);
  const finalRun = await storage.getRun(run.id);
  assert.equal(finalRun.qualifiedLeads.length, 0);
  assert.equal(finalRun.rejectedLeads.some((lead) => lead.name === "Rad Plumbing"), true);
  const rejected = finalRun.rejectedLeads.find((lead) => lead.name === "Rad Plumbing");
  assert.equal(rejected.ownerEvidenceGraph.strategyVersion, OWNER_FIRST_STRATEGY_VERSION);
  assert.equal(rejected.ownerEvidenceGraph.ownerIdentityProven, false);
}

async function testOwnerAssociatedFallbackDropsGenericEmailWhenPhoneIsClean() {
  const storage = makeStorage();
  const agent = new HermesAgent({ projectName: "eval", rootDir: process.cwd(), storage });
  agent.googleMaps = {
    async discover() {
      return {
        rawPath: "/tmp/owner-associated-clean-phone.csv",
        rows: [{
          name: "Summit Roof Care",
          address: "20 Oak St, Birmingham, AL, USA",
          phone: "205-555-0198",
          website: "https://summitroofcare.com/"
        }]
      };
    }
  };
  agent.scrapling = {
    async enrich() {
      return {
        ok: true,
        mode: "simple",
        text: "Summit Roof Care is owned by Rachel Carter. Call 205-555-0198.",
        emails: ["summitroofcare@gmail.com"],
        phones: ["205-555-0198"],
        links: [],
        socialLinks: []
      };
    }
  };
  agent.ownerRabbitHole = {
    async findOwnerContact() {
      return {
        businessEmail: "summitroofcare@gmail.com",
        businessPhone: "205-555-0198",
        ownerName: "Rachel Carter",
        ownerTitle: "Owner",
        ownerEvidenceUrl: "https://summitroofcare.com/about",
        ownerContactEvidence: [],
        ownerEmail: "",
        ownerPhone: "",
        ownerEmails: [],
        ownerPhones: [],
        rabbitHoleStatus: "owner-found-no-profile",
        ownerEvidence: [{ url: "https://summitroofcare.com/about", ownerCandidates: ["Rachel Carter"], textSample: "Rachel Carter owns Summit Roof Care in Birmingham." }]
      };
    }
  };
  agent.minimax = {
    async proposeSearchStrategy() { return { queries: [], reason: "fixed" }; },
    async qualifyLead() { return { qualified: false, score: 30, reason: "no owner reach channel", missing: ["owner phone"] }; },
    async proposeRunReview() { return { summary: "associated", skills: [], avoid: [] }; }
  };

  const run = await agent.startRun({ source: "multi-source-owner-evidence", niche: "roofers", cities: "Birmingham, AL", target: 1, minScore: 60, maxQueriesPerCity: 1, maxPlannedLocations: 1 });
  await waitFor(agent);
  const finalRun = await storage.getRun(run.id);
  assert.equal(finalRun.input.strategyVersion, OWNER_FIRST_STRATEGY_VERSION);
  assert.equal(finalRun.qualifiedLeads.length, 1);
  assert.equal(finalRun.qualifiedLeads[0].contactConfidence, "owner-associated");
  assert.equal(finalRun.qualifiedLeads[0].businessEmail, "");
  assert.equal(finalRun.qualifiedLeads[0].businessPhone, "205-555-0198");
  assert.equal(finalRun.qualifiedLeads[0].ownerEvidenceGraph.qualityRedFlags.length, 0);
}

async function testOwnerAssociatedFallbackRejectsWeakV4OwnerNameAndSuspiciousEmail() {
  const storage = makeStorage();
  const agent = new HermesAgent({ projectName: "eval", rootDir: process.cwd(), storage });
  agent.googleMaps = {
    async discover() {
      return {
        rawPath: "/tmp/owner-associated-v4-bad.csv",
        rows: [{
          name: "Rocket Roofing",
          address: "1 Roof St, Birmingham, AL, USA",
          phone: "",
          website: "https://rocketroofing.example/"
        }]
      };
    }
  };
  agent.scrapling = {
    async enrich() {
      return {
        ok: true,
        mode: "simple",
        text: "Rocket Roofing hidden contact forum@xnxx.com.",
        emails: ["forum@xnxx.com"],
        phones: [],
        links: [],
        socialLinks: []
      };
    }
  };
  agent.ownerRabbitHole = {
    async findOwnerContact() {
      return {
        businessEmail: "forum@xnxx.com",
        businessPhone: "",
        ownerName: "Meet Rachel Protecting What",
        ownerTitle: "Owner",
        ownerEvidenceUrl: "https://www.bbb.org/us/al/birmingham/profile/roofing/rocket-roofing",
        ownerContactEvidence: [],
        ownerEmail: "",
        ownerPhone: "",
        ownerEmails: [],
        ownerPhones: [],
        rabbitHoleStatus: "owner-found-no-profile",
        ownerEvidence: [{ url: "https://www.bbb.org/us/al/birmingham/profile/roofing/rocket-roofing", ownerCandidates: ["Meet Rachel Protecting What"], textSample: "Rocket Roofing profile." }]
      };
    }
  };
  agent.minimax = {
    async proposeSearchStrategy() { return { queries: [], reason: "fixed" }; },
    async proposeRetry() { return { variable: "query", value: "", reason: "stop" }; },
    async qualifyLead() { return { qualified: true, score: 90, reason: "looks good", missing: [] }; },
    async proposeRunReview() { return { summary: "bad", skills: [], avoid: [] }; }
  };

  const run = await agent.startRun({ source: "multi-source-owner-evidence", niche: "roofers", cities: "Birmingham, AL", target: 1, minScore: 60, maxQueriesPerCity: 1, maxPlannedLocations: 1 });
  await waitFor(agent);
  const finalRun = await storage.getRun(run.id);
  assert.equal(finalRun.qualifiedLeads.length, 0);
  const rejected = finalRun.rejectedLeads.find((lead) => lead.name === "Rocket Roofing");
  assert.ok(rejected);
  assert.equal(rejected.ownerEvidenceGraph.ownerAssociatedContactProven, false);
  assert.equal(rejected.ownerEvidenceGraph.qualityRedFlags.includes("strong owner person name"), true);
}

async function testOwnerAssociatedFallbackRejectsWeakBusinessMatch() {
  const storage = makeStorage();
  const agent = new HermesAgent({ projectName: "eval", rootDir: process.cwd(), storage });
  agent.googleMaps = {
    async discover() {
      return {
        rawPath: "/tmp/owner-associated-weak-business.csv",
        rows: [{
          name: "Rocket Roofing",
          address: "1 Roof St, Birmingham, AL, USA",
          phone: "205-555-0134",
          website: "https://rocketroofing.example/"
        }]
      };
    }
  };
  agent.scrapling = {
    async enrich() {
      return {
        ok: true,
        mode: "simple",
        text: "Rocket Roofing contact 205-555-0134.",
        emails: [],
        phones: ["205-555-0134"],
        links: [],
        socialLinks: []
      };
    }
  };
  agent.ownerRabbitHole = {
    async findOwnerContact() {
      return {
        businessEmail: "",
        businessPhone: "205-555-0134",
        ownerName: "Rachel Carter",
        ownerTitle: "Owner",
        ownerEvidenceUrl: "https://www.linkedin.com/in/rachel-carter",
        ownerContactEvidence: [],
        ownerEmail: "",
        ownerPhone: "",
        ownerEmails: [],
        ownerPhones: [],
        rabbitHoleStatus: "owner-found-no-profile",
        ownerEvidence: [{ url: "https://www.linkedin.com/in/rachel-carter", ownerCandidates: ["Rachel Carter"], textSample: "Rachel Carter owns Blue Sky Cafe." }]
      };
    }
  };
  agent.minimax = {
    async proposeSearchStrategy() { return { queries: [], reason: "fixed" }; },
    async proposeRetry() { return { variable: "query", value: "", reason: "stop" }; },
    async qualifyLead() { return { qualified: true, score: 90, reason: "looks good", missing: [] }; },
    async proposeRunReview() { return { summary: "weak match", skills: [], avoid: [] }; }
  };

  const run = await agent.startRun({ source: "multi-source-owner-evidence", niche: "roofers", cities: "Birmingham, AL", target: 1, minScore: 60, maxQueriesPerCity: 1, maxPlannedLocations: 1 });
  await waitFor(agent);
  const finalRun = await storage.getRun(run.id);
  assert.equal(finalRun.qualifiedLeads.length, 0);
  const rejected = finalRun.rejectedLeads.find((lead) => lead.name === "Rocket Roofing");
  assert.ok(rejected);
  assert.equal(rejected.ownerEvidenceGraph.qualityRedFlags.includes("owner-to-business evidence match"), true);
}

async function testDirectoryDiscoveryUsesScraplingForProtectedSources() {
  let fetchCalled = false;
  const adapter = new DirectoryDiscoveryAdapter({
    fetcher: async () => {
      fetchCalled = true;
      return { ok: false, status: 403, async text() { return ""; } };
    },
    scrapling: {
      available() { return true; },
      async enrich({ mode }) {
        assert.equal(mode, "stealth");
        return {
          ok: true,
          title: "Protected directory",
          text: "Solar Bright Energy Phoenix AZ solar installer 602-555-0199",
          links: ["https://solarbright.example"]
        };
      }
    }
  });
  const result = await adapter.searchDirectoryOfficialSite({ city: "Phoenix, AZ, USA", niche: "solar panel", limit: 2 });
  assert.equal(fetchCalled, false);
  assert.equal(result.seeds.some((seed) => seed.name === "Solar Bright Energy Phoenix AZ solar installer 602-555-0199" || /solar bright energy/i.test(seed.name)), true);
  assert.equal(result.seeds.some((seed) => seed.website === "https://solarbright.example/"), true);
}

async function testDirectoryBusinessPhoneDoesNotBecomeOwnerDirectContact() {
  const previousFetch = global.fetch;
  global.fetch = async () => ({
    ok: true,
    async text() {
      return "";
    }
  });
  try {
    const scrapling = {
      available() { return true; },
      async enrich({ url }) {
        return {
          ok: true,
          url,
          title: "AL Coronado Plumbing BBB profile",
          text: "AL Coronado Plumbing BBB profile. Ben Wood owner. Reach phone 520-323-7848.",
          emails: [],
          phones: ["520-323-7848"],
          links: [],
          socialLinks: []
        };
      }
    };
    const adapter = new OwnerRabbitHoleAdapter({
      scrapling,
      linkedinTools: { enrichProfile: scrapling.enrich },
      minimax: {
        configured() { return true; },
        async proposeOwnerRabbitHoleStrategy() {
          return {
            pagePaths: [],
            searchQueries: [],
            profileUrlHints: ["https://www.bbb.org/us/az/tucson/profile/plumber/al-coronado-plumbing-1286-20001930"],
            ownerNameHints: ["Ben Wood"],
            strategyReason: "test directory gate"
          };
        },
        async proposeOwnerAgentSwarm() {
          return { agents: [] };
        }
      }
    });
    const result = await adapter.findOwnerContact({
      lead: {
        name: "Al Coronado Plumbing",
        city: "Tucson, AZ, USA",
        category: "plumber",
        website: "https://alcoronadoplumbing.com/",
        ownerAgentSwarmSize: 1
      },
      initialEnrichment: { ok: true, text: "Al Coronado Plumbing.", links: [], socialLinks: [] },
      run: { input: { ownerAgentSwarmSize: 1 } }
    });
    assert.equal(result.ownerPhone, "");
    assert.equal(result.ownerContactEvidence.length, 0);
    assert.notEqual(result.rabbitHoleStatus, "owner-contact-found");
  } finally {
    global.fetch = previousFetch;
  }
}

async function testMapsFailureClassification() {
  assert.equal(classifyMapsFailure("Error: write EPIPE errno -32"), "scraper_unhealthy");
  assert.equal(classifyMapsFailure("Page.goto: net::ERR_CONNECTION_CLOSED at https://www.google.com/maps/"), "scraper_unhealthy");
  assert.equal(classifyMapsFailure("Timed out after 45000ms"), "render_timeout");
  assert.equal(classifyMapsFailure("Google Maps discovery returned no rows."), "empty_results");
}

async function testInitialStrategyPlansBeforeTemplates() {
  const storage = makeStorage();
  const agent = new HermesAgent({ projectName: "eval", rootDir: process.cwd(), storage });
  installOwnerSuccess(agent);
  const calls = [];
  agent.googleMaps = {
    async discover({ query }) {
      calls.push(query);
      return { rawPath: `/tmp/planned-${calls.length}.csv`, rows: makeRows("Planned Roof") };
    }
  };
  agent.scrapling = { async enrich() { return { ok: true, emails: [], phones: [], text: "" }; } };
  let initialTimeoutMs = 0;
  agent.minimax = {
    async proposeSearchStrategy({ timeoutMs }) {
      initialTimeoutMs = timeoutMs;
      return { queries: ["commercial roof leak repair Miami FL USA"], reason: "commercial leak intent matches the niche and city" };
    },
    async qualifyLead() { return { qualified: true, score: 100, reason: "ok", missing: [] }; },
    async proposeRunReview() { return { summary: "planned", skills: [], avoid: [] }; }
  };

  const run = await agent.startRun({ source: "multi-source-owner-evidence", niche: "roofers", cities: "Miami, FL", target: 1, minScore: 50 });
  await waitFor(agent);
  const finalRun = await storage.getRun(run.id);
  assert.equal(finalRun.status, "complete");
  assert.equal(calls[0], "commercial roof leak repair Miami FL USA");
  assert.equal(initialTimeoutMs, Math.max(3000, Number(process.env.HERMES_MINIMAX_INITIAL_SEARCH_TIMEOUT_MS || 45000)));
  assert.equal(finalRun.agentDecisions.some((decision) => decision.type === "initial_strategy"), true);
  assert.equal(finalRun.counters.initialStrategies, 1);
}

async function testLegacyOwnerSourceTermsNeverReachMapsSearch() {
  const storage = makeStorage({
    skills: [{
      title: "Old unrelated query",
      when: "search",
      do: "Use query \"plumbers in Florida\" when Google Maps stalls."
    }],
    avoid: []
  });
  const agent = new HermesAgent({ projectName: "eval", rootDir: process.cwd(), storage });
  installOwnerSuccess(agent);
  const calls = [];
  agent.googleMaps = {
    async discover({ query }) {
      calls.push(query);
      return { rawPath: `/tmp/legacy-query-filter-${calls.length}.csv`, rows: makeRows("Legacy Filter Salon") };
    }
  };
  agent.scrapling = { async enrich() { return { ok: true, emails: [], phones: [], text: "" }; } };
  agent.minimax = {
    async proposeSearchStrategy() {
      return {
        queries: [
          "google_maps github-owners Austin, TX, USA",
          "hair salon owner Austin TX contact email site:google.com/maps",
          "hair salons Austin TX phone website"
        ],
        reason: "test rejects legacy source query"
      };
    },
    async qualifyLead() { return { qualified: true, score: 100, reason: "ok", missing: [] }; },
    async proposeRunReview() { return { summary: "ok", skills: [], avoid: [] }; }
  };

  const run = await agent.startRun({ source: "multi-source-owner-evidence", niche: "salons", cities: "Austin, TX", target: 1, minScore: 50 });
  await waitFor(agent);
  const finalRun = await storage.getRun(run.id);
  assert.equal(finalRun.status, "complete");
  assert.equal(calls.some((query) => /github-owners|github owners|google_maps|plumbers|florida|email|site:/i.test(query)), false);
  assert.equal(calls.some((query) => /salon owner|owner stylist|independent stylist|owner esthetician|our story|founder|ribbon cutting/i.test(query)), true);
}

async function testPlannerProviderFallbackIsRecorded() {
  const storage = makeStorage();
  const agent = new HermesAgent({ projectName: "eval", rootDir: process.cwd(), storage });
  installOwnerSuccess(agent);
  const calls = [];
  agent.googleMaps = {
    async discover({ query }) {
      calls.push(query);
      return { rawPath: `/tmp/provider-fallback-${calls.length}.csv`, rows: makeRows("Fallback Roof") };
    }
  };
  agent.scrapling = { async enrich() { return { ok: true, emails: [], phones: [], text: "" }; } };
  agent.minimax = {
    model: "mock-model",
    async proposeSearchStrategy() {
      return {
        providerFallback: true,
        providerError: "planner unavailable",
        providerMeta: { provider: "minimax", model: "mock-model", durationMs: 25, timeoutMs: 1000 },
        queries: ["fallback roof repair Miami FL USA"],
        reason: "local fallback query"
      };
    },
    async qualifyLead() { return { qualified: true, score: 100, reason: "ok", missing: [] }; },
    async proposeRunReview() { return { summary: "fallback recorded", skills: [], avoid: [] }; }
  };

  const run = await agent.startRun({ source: "multi-source-owner-evidence", niche: "roofers", cities: "Miami, FL", target: 1, minScore: 50 });
  await waitFor(agent);
  const finalRun = await storage.getRun(run.id);
  assert.equal(finalRun.status, "complete");
  assert.equal(calls[0], "fallback roof repair Miami FL USA");
  assert.equal(finalRun.counters.providerFallbacks, 1);
  assert.equal(finalRun.counters.llmCalls, 1);
  assert.equal(finalRun.counters.llmFallbacks, 1);
  assert.equal(finalRun.scaleAudit.llmCalls, 1);
  assert.equal(finalRun.failures.some((failure) => failure.stage === "provider" && failure.context.stage === "initial_strategy"), true);
  assert.equal(finalRun.agentDecisions.some((decision) => decision.type === "provider_fallback"), true);
}

async function testAdaptiveSearch() {
  const storage = makeStorage();
  const agent = new HermesAgent({ projectName: "eval", rootDir: process.cwd(), storage });
  installOwnerSuccess(agent);
  const calls = [];
  agent.googleMaps = {
    async discover({ query }) {
      calls.push(query);
      return { rawPath: `/tmp/${calls.length}.csv`, rows: [] };
    }
  };
  agent.minimax = {
    async proposeRetry() { return { variable: "query", value: "", reason: "empty" }; },
    async proposeSearchStrategy() { return { queries: ["emergency roof repair Miami FL USA"], reason: "broaden" }; },
    async proposeRunReview() { return { summary: "empty", skills: [], avoid: [] }; }
  };

  const run = await agent.startRun({ source: "multi-source-owner-evidence", niche: "roofers", cities: "Miami, FL", target: 10, maxQueriesPerCity: 3 });
  await waitFor(agent);
  const finalRun = await storage.getRun(run.id);
  assert.equal(calls.includes("emergency roof repair Miami FL USA"), true);
  assert.equal(finalRun.agentDecisions.some((decision) => decision.type === "adapt_search"), true);
}

async function testQueryDiversityRejectsRepeatedKeywords() {
  const storage = makeStorage();
  const agent = new HermesAgent({ projectName: "eval", rootDir: process.cwd(), storage });
  installOwnerSuccess(agent);
  agent.finalRecoveryEnabled = false;
  const calls = [];
  agent.googleMaps = {
    async discover({ query }) {
      calls.push(query);
      return { rawPath: `/tmp/repeated-${calls.length}.csv`, rows: [] };
    }
  };
  agent.minimax = {
    async proposeRetry() { return { variable: "query", value: "roofing contractors Miami FL USA", reason: "duplicate retry" }; },
    async proposeSearchStrategy() {
      return {
        queries: [
          "roofers Miami FL USA",
          "roofers in Miami FL USA",
          "roofing contractors Miami FL USA",
          "emergency roof repair Miami FL USA"
        ],
        reason: "mix duplicate and distinct search intents"
      };
    },
    async proposeRunReview() { return { summary: "duplicates rejected", skills: [], avoid: [] }; }
  };

  const run = await agent.startRun({ source: "multi-source-owner-evidence", niche: "roofers", cities: "Miami, FL", target: 10, maxQueriesPerCity: 4, maxPlannedLocations: 1 });
  await waitFor(agent);
  const finalRun = await storage.getRun(run.id);
  assert.equal(calls.includes("roofers Miami FL USA"), true);
  assert.equal(calls.includes("emergency roof repair Miami FL USA"), true);
  assert.equal(calls.includes("roofers in Miami FL USA"), false);
  assert.equal(calls.includes("roofing contractors Miami FL USA"), false);
  assert.equal(finalRun.counters.queryDuplicates >= 2, true);
  assert.equal(finalRun.agentDecisions.some((decision) => decision.type === "query_diversity"), true);
}

async function testEnrichmentFailureRecovery() {
  const storage = makeStorage();
  const agent = new HermesAgent({ projectName: "eval", rootDir: process.cwd(), storage });
  installOwnerSuccess(agent);
  let enrichCalls = 0;
  agent.googleMaps = {
    async discover() {
      return { rawPath: "/tmp/recover.csv", rows: makeRows("Recover Roof") };
    }
  };
  agent.scrapling = {
    async enrich() {
      enrichCalls += 1;
      if (enrichCalls === 1) throw new Error("fetch failed");
      return { ok: true, emails: [], phones: [], text: "" };
    }
  };
  agent.minimax = {
    async qualifyLead() { return { qualified: true, score: 100, reason: "ok", missing: [] }; },
    async proposeRunReview() { return { summary: "recovered", skills: [], avoid: [] }; }
  };

  const run = await agent.startRun({ source: "multi-source-owner-evidence", niche: "roofers", cities: "Florida", target: 3, minScore: 50 });
  await waitFor(agent);
  const finalRun = await storage.getRun(run.id);
  assert.equal(finalRun.status, "complete");
  assert.equal(finalRun.counters.enrichmentFailures > 0, true);
}

async function testDynamicEnrichmentRepairBeforeRejecting() {
  const storage = makeStorage();
  const agent = new HermesAgent({ projectName: "eval", rootDir: process.cwd(), storage });
  const enrichmentModes = [];
  let qualifyCalls = 0;
  agent.googleMaps = {
    async discover() {
      return { rawPath: "/tmp/deep-enrich.csv", rows: makeRows("Deep Enrich Roof", 1) };
    }
  };
  agent.scrapling = {
    async enrich({ mode }) {
      enrichmentModes.push(mode);
      if (mode === "dynamic") {
        return { ok: true, mode: "dynamic", emails: ["seed@deep.example"], phones: [], text: "licensed roofing contractor emergency repair commercial roof leak insurance claims owner Jane Deep".repeat(20) };
      }
      return { ok: true, mode: "simple", emails: [], phones: [], text: "thin" };
    }
  };
  agent.ownerRabbitHole = {
    async findOwnerContact({ initialEnrichment }) {
      if (initialEnrichment.mode === "dynamic") {
        return {
          ownerName: "Jane Deep",
          ownerTitle: "Owner",
          ownerEmail: "jane@deep.example",
          ownerPhone: "",
          ownerEmails: ["jane@deep.example"],
          ownerPhones: [],
          ownerLinkedIn: "https://www.linkedin.com/in/jane-deep",
          ownerSocialProfiles: ["https://www.linkedin.com/in/jane-deep"],
          ownerContactSource: "test-dynamic-owner-profile",
          ownerContactEvidence: [
            { type: "email", value: "jane@deep.example", url: "https://www.linkedin.com/in/jane-deep", source: "test-dynamic-owner-profile" }
          ],
          rabbitHoleStatus: "owner-contact-found",
          rabbitHoleStrategy: { searchQueries: ["Jane Deep owner"], strategyReason: "test dynamic repair" }
        };
      }
      return { ownerSocialProfiles: [], ownerEmails: [], ownerPhones: [], rabbitHoleStatus: "owner-not-found" };
    }
  };
  agent.minimax = {
    async proposeSearchStrategy() { return { queries: ["roofers Miami FL USA"], reason: "initial" }; },
    async qualifyLead({ lead }) {
      qualifyCalls += 1;
      if (lead.enrichmentMode === "dynamic" && lead.ownerEmail) {
        return { qualified: true, score: 100, reason: "dynamic evidence found owner contact and niche match", missing: [] };
      }
      return { qualified: false, score: 40, reason: "weak owner evidence", missing: ["owner contact"] };
    },
    async proposeRunReview() { return { summary: "deep enrichment repaired", skills: [], avoid: [] }; }
  };

  const run = await agent.startRun({ source: "multi-source-owner-evidence", niche: "roofers", cities: "Miami, FL", target: 1, minScore: 80 });
  await waitFor(agent);
  const finalRun = await storage.getRun(run.id);
  assert.equal(finalRun.status, "complete");
  assert.deepEqual(enrichmentModes, ["simple", "dynamic"]);
  assert.equal(qualifyCalls, 2);
  assert.equal(finalRun.counters.deepEnrichments, 1);
  assert.equal(finalRun.counters.ownerRabbitHoles, 2);
  assert.equal(finalRun.counters.ownerContactsFound, 1);
  assert.equal(finalRun.scaleAudit.ownerHunts, 2);
  assert.equal(finalRun.scaleAudit.ownerContacts, 1);
  assert.equal(finalRun.agentDecisions.some((decision) => decision.type === "repair_enrichment"), true);
  assert.equal(finalRun.qualifiedLeads[0].enrichmentMode, "dynamic");
}

async function testLocationExpansion() {
  const storage = makeStorage();
  const agent = new HermesAgent({ projectName: "eval", rootDir: process.cwd(), storage });
  installOwnerSuccess(agent);
  agent.googleMaps = {
    async discover() {
      return { rawPath: "/tmp/empty.csv", rows: [] };
    }
  };
  agent.minimax = {
    async proposeRetry() { return { variable: "query", value: "", reason: "none" }; },
    async proposeSearchStrategy() { return { queries: ["roof repair Miami FL USA"], reason: "adapt" }; },
    async proposeLocationStrategy({ context }) { return { locations: context.untriedLocationHints?.slice(0, 2) || [], reason: "expand" }; },
    async proposeRunReview() { return { summary: "expanded", skills: [], avoid: [] }; }
  };

  const run = await agent.startRun({ source: "multi-source-owner-evidence", niche: "roofers", cities: "Miami, FL", target: 100, maxQueriesPerCity: 2, maxPlannedLocations: 4 });
  await waitFor(agent);
  const finalRun = await storage.getRun(run.id);
  assert.equal(finalRun.input.cities.length > 1, true);
  assert.equal(finalRun.agentDecisions.some((decision) => decision.type === "expand_locations"), true);
}

async function testPortfolioDefaultsUseBroadLocationCapacity() {
  const storage = makeStorage();
  const agent = new HermesAgent({ projectName: "eval", rootDir: process.cwd(), storage });
  agent.execute = async () => {};

  const defaultRun = await agent.startRun({ source: "multi-source-owner-evidence", niche: "salons", cities: "Austin, TX\nRound Rock, TX", target: 50 });
  const explicitRun = await agent.startRun({ source: "multi-source-owner-evidence", niche: "salons", cities: "Austin, TX", target: 50, maxPlannedLocations: 12 });
  await waitFor(agent);

  assert.equal(defaultRun.input.maxPlannedLocations, 100);
  assert.equal(explicitRun.input.maxPlannedLocations, 12);
}

async function testAllUsaInputExpandsNationwidePlan() {
  const storage = makeStorage();
  const agent = new HermesAgent({ projectName: "eval", rootDir: process.cwd(), storage });
  installOwnerSuccess(agent);
  const calls = [];
  agent.googleMaps = {
    available() { return true; },
    async discover({ query }) {
      calls.push(query);
      return { rawPath: `/tmp/all-usa-${calls.length}.csv`, rows: makeRows("National Gym") };
    }
  };
  agent.scrapling = {
    available() { return true; },
    async enrich() { return { ok: true, emails: [], phones: [], text: "" }; }
  };
  agent.minimax = {
    async qualifyLead() { return { qualified: true, score: 100, reason: "ok", missing: [] }; },
    async proposeRunReview() { return { summary: "all usa", skills: [], avoid: [] }; }
  };

  const run = await agent.startRun({ source: "multi-source-owner-evidence", niche: "gyms", allUsa: "on", target: 1, minScore: 50 });
  await waitFor(agent);
  const finalRun = await storage.getRun(run.id);
  assert.equal(finalRun.status, "complete");
  assert.equal(finalRun.input.locationPlan.requested.length >= 50, true);
  assert.equal(finalRun.input.cities.some((location) => /california, USA/i.test(location)), true);
  assert.equal(calls.length > 0, true);
}

async function testStagnationPivotsInsteadOfRepeating() {
  const storage = makeStorage();
  const agent = new HermesAgent({ projectName: "eval", rootDir: process.cwd(), storage });
  installOwnerSuccess(agent);
  const calls = [];
  let strategyCalls = 0;
  agent.googleMaps = {
    async discover({ query }) {
      calls.push(query);
      return { rawPath: `/tmp/stagnant-${calls.length}.csv`, rows: [] };
    }
  };
  agent.minimax = {
    async proposeRetry() { return { variable: "query", value: "", reason: "empty" }; },
    async proposeSearchStrategy() {
      strategyCalls += 1;
      return { queries: [`licensed roof repair backup ${strategyCalls} Miami FL USA`], reason: "try another term" };
    },
    async proposeLocationStrategy() { return { locations: ["Orlando, FL"], reason: "pivot to another Florida market" }; },
    async proposeRunReview() { return { summary: "stagnated", skills: [], avoid: [] }; }
  };

  const run = await agent.startRun({ source: "multi-source-owner-evidence", niche: "roofers", cities: "Miami, FL", target: 50, maxQueriesPerCity: 6, maxPlannedLocations: 2 });
  await waitFor(agent);
  const finalRun = await storage.getRun(run.id);
  const miamiAttempts = finalRun.search.attemptedQueries.filter((key) => key.startsWith("miami, fl, usa::"));
  assert.equal(finalRun.status, "partial");
  assert.equal(miamiAttempts.length, 2);
  assert.equal(miamiAttempts.length < finalRun.input.maxQueriesPerCity, true);
  assert.equal(finalRun.agentDecisions.some((decision) => decision.type === "stagnation_detected"), true);
  assert.equal(finalRun.agentDecisions.some((decision) => decision.type === "pivot_strategy"), true);
  assert.equal(finalRun.input.cities.includes("Orlando, FL, USA"), true);
  assert.equal(finalRun.counters.stagnationPivots > 0, true);
}

async function testFinalRecoveryBeforePartial() {
  const storage = makeStorage();
  const agent = new HermesAgent({ projectName: "eval", rootDir: process.cwd(), storage });
  installOwnerSuccess(agent);
  const calls = [];
  agent.googleMaps = {
    async discover({ query }) {
      calls.push(query);
      if (/orlando/i.test(query)) return { rawPath: "/tmp/orlando.csv", rows: makeRows("Orlando Rescue", 1) };
      return { rawPath: `/tmp/empty-${calls.length}.csv`, rows: [] };
    }
  };
  agent.scrapling = { async enrich() { return { ok: true, emails: [], phones: [], text: "" }; } };
  agent.minimax = {
    async proposeRetry() { return { variable: "query", value: "", reason: "empty" }; },
    async proposeSearchStrategy({ context }) { return { queries: [`roofers ${context.city} USA`], reason: "search current city" }; },
    async proposeLocationStrategy() { return { locations: ["Orlando, FL"], reason: "last recovery location" }; },
    async qualifyLead() { return { qualified: true, score: 100, reason: "ok", missing: [] }; },
    async proposeRunReview() { return { summary: "recovered before partial", skills: [], avoid: [] }; }
  };

  const run = await agent.startRun({ source: "multi-source-owner-evidence", niche: "roofers", cities: "Miami, FL", target: 1, minScore: 50, maxQueriesPerCity: 4, maxPlannedLocations: 1 });
  await waitFor(agent);
  const finalRun = await storage.getRun(run.id);
  assert.equal(finalRun.status, "complete");
  assert.equal(finalRun.input.cities.includes("Orlando, FL, USA"), true);
  assert.equal(finalRun.agentDecisions.some((decision) => decision.type === "final_recovery"), true);
  assert.equal(finalRun.counters.finalRecoveryPasses, 1);
  assert.equal(finalRun.finalRecovery.searchedLocations, 1);
}

async function testFinalRecoverySearchesWhileOwnerBacklogRuns() {
  const storage = makeStorage();
  const agent = new HermesAgent({ projectName: "eval", rootDir: process.cwd(), storage });
  const calls = [];
  let releaseOwner = null;
  agent.googleMaps = {
    async discover({ query }) {
      calls.push(query);
      if (/orlando/i.test(query) && releaseOwner) releaseOwner();
      return {
        rawPath: `/tmp/recovery-backlog-${calls.length}.csv`,
        rows: /orlando/i.test(query)
          ? makeRows("Recovery Backlog Orlando", 3)
          : makeRows("Recovery Backlog Miami", 1)
      };
    }
  };
  agent.scrapling = { async enrich() { return { ok: true, emails: [], phones: [], text: "" }; } };
  agent.ownerRabbitHole = {
    async findOwnerContact({ lead }) {
      await new Promise((resolve) => {
        releaseOwner = resolve;
        setTimeout(resolve, 15000);
      });
      return {
        ownerName: "Jane Morgan",
        ownerTitle: "Owner",
        ownerEmail: "owner@example.com",
        ownerEmails: ["owner@example.com"],
        ownerPhones: [],
        ownerLinkedIn: "https://www.linkedin.com/in/mock-owner",
        ownerSocialProfiles: ["https://www.linkedin.com/in/mock-owner"],
        ownerEvidenceUrl: "https://www.linkedin.com/in/mock-owner",
        ownerContactSource: "test-owner-profile",
        ownerContactEvidence: [
          { type: "email", value: "owner@example.com", url: "https://www.linkedin.com/in/mock-owner", source: "test-owner-profile" }
        ],
        rabbitHoleStatus: "owner-contact-found",
        rabbitHoleStrategy: { searchQueries: ["mock owner linkedin"], strategyReason: "test fixture" }
      };
    }
  };
  agent.minimax = {
    async proposeRetry() { return { variable: "query", value: "", reason: "none" }; },
    async proposeSearchStrategy({ context }) { return { queries: [`roofers ${context.city} USA`], reason: "search current city" }; },
    async proposeLocationStrategy() { return { locations: ["Orlando, FL"], reason: "recovery while owner backlog is active" }; },
    async qualifyLead() { return { qualified: true, score: 100, reason: "ok", missing: [] }; },
    async proposeRunReview() { return { summary: "recovery while owner backlog ran", skills: [], avoid: [] }; }
  };

  const run = await agent.startRun({ source: "multi-source-owner-evidence", niche: "roofers", cities: "Miami, FL", target: 3, minScore: 50, maxQueriesPerCity: 4, maxPlannedLocations: 1, leadWorkerConcurrency: 1 });
  await waitFor(agent);
  const finalRun = await storage.getRun(run.id);
  const orlandoSearchIndex = finalRun.steps.findIndex((step) => step.phase === "maps_discovery" && /orlando/i.test(step.message || ""));
  const firstQualifiedIndex = finalRun.steps.findIndex((step) => step.phase === "evolve" && /Qualified/i.test(step.message || ""));
  assert.equal(finalRun.status, "complete");
  assert.equal(finalRun.counters.finalRecoveryPasses, 1);
  assert.equal(finalRun.input.cities.includes("Orlando, FL, USA"), true);
  assert.equal(finalRun.agentDecisions.some((decision) => decision.type === "final_recovery"), true);
  assert.equal(orlandoSearchIndex >= 0, true);
  assert.equal(firstQualifiedIndex === -1 || orlandoSearchIndex < firstQualifiedIndex, true);
}

async function testDiscoveryContinuesAfterOwnerDrainWhenTargetUnmet() {
  const storage = makeStorage();
  const agent = new HermesAgent({ projectName: "eval", rootDir: process.cwd(), storage });
  const calls = [];
  let locationStrategyCalls = 0;
  agent.googleMaps = {
    async discover({ query }) {
      calls.push(query);
      const label = /tampa/i.test(query) ? "Tampa Rescue" : /orlando/i.test(query) ? "Orlando No Owner" : "Miami No Owner";
      return { rawPath: `/tmp/post-drain-${calls.length}.csv`, rows: makeRows(label, 1) };
    }
  };
  agent.scrapling = { async enrich() { return { ok: true, emails: [], phones: [], text: "" }; } };
  agent.ownerRabbitHole = {
    async findOwnerContact({ lead }) {
      if (/tampa/i.test(lead.city || "")) {
        return {
          ownerName: "Jane Morgan",
          ownerTitle: "Owner",
          ownerEmail: "owner@example.com",
          ownerEmails: ["owner@example.com"],
          ownerPhones: [],
          ownerLinkedIn: "https://www.linkedin.com/in/mock-owner",
          ownerSocialProfiles: ["https://www.linkedin.com/in/mock-owner"],
          ownerEvidenceUrl: "https://www.linkedin.com/in/mock-owner",
          ownerContactSource: "test-owner-profile",
          ownerContactEvidence: [
            { type: "email", value: "owner@example.com", url: "https://www.linkedin.com/in/mock-owner", source: "test-owner-profile" }
          ],
          rabbitHoleStatus: "owner-contact-found"
        };
      }
      return {
        ownerName: "",
        ownerEmails: [],
        ownerPhones: [],
        ownerSocialProfiles: [],
        ownerContactEvidence: [],
        rabbitHoleStatus: "owner-contact-exhausted"
      };
    }
  };
  agent.minimax = {
    async proposeRetry() { return { variable: "query", value: "", reason: "none" }; },
    async proposeSearchStrategy({ context }) { return { queries: [`roofers ${context.city} USA`], reason: "search current city" }; },
    async proposeLocationStrategy() {
      locationStrategyCalls += 1;
      return {
        locations: [locationStrategyCalls === 1 ? "Orlando, FL" : "Tampa, FL"],
        reason: "continue discovery after owner backlog drain"
      };
    },
    async qualifyLead() { return { qualified: true, score: 100, reason: "ok", missing: [] }; },
    async proposeRunReview() { return { summary: "continued after owner drain", skills: [], avoid: [] }; }
  };

  const run = await agent.startRun({ source: "multi-source-owner-evidence", niche: "roofers", cities: "Miami, FL", target: 1, minScore: 50, maxQueriesPerCity: 4, maxPlannedLocations: 1, leadWorkerConcurrency: 1 });
  await waitFor(agent);
  const finalRun = await storage.getRun(run.id);
  assert.equal(finalRun.status, "complete");
  assert.equal(finalRun.counters.finalRecoveryPasses, 1);
  assert.equal(finalRun.input.cities.includes("Orlando, FL, USA"), true);
  assert.equal(finalRun.input.cities.includes("Tampa, FL, USA"), true);
  assert.equal(calls.some((query) => /tampa/i.test(query)), true);
  assert.equal(finalRun.qualifiedLeads.some((lead) => /tampa/i.test(lead.city || "")), true);
}

async function testSelfReviewWritesMemory() {
  const storage = makeStorage();
  const agent = new HermesAgent({ projectName: "eval", rootDir: process.cwd(), storage });
  installOwnerSuccess(agent);
  agent.googleMaps = {
    async discover() {
      return { rawPath: "/tmp/review.csv", rows: makeRows("Review Roof") };
    }
  };
  agent.scrapling = { async enrich() { return { ok: true, emails: [], phones: [], text: "" }; } };
  agent.minimax = {
    async qualifyLead() { return { qualified: true, score: 100, reason: "ok", missing: [] }; },
    async proposeRunReview() {
      return {
        summary: "good run",
        skills: [{ title: "Review skill", when: "similar run", do: "reuse productive query", avoid: "generic restart" }],
        avoid: [{ stage: "review", failure: "none", avoid: "do not ignore learned query" }]
      };
    }
  };

  const run = await agent.startRun({ source: "multi-source-owner-evidence", niche: "roofers", cities: "Miami, FL", target: 1, minScore: 50 });
  await waitFor(agent);
  const finalRun = await storage.getRun(run.id);
  assert.equal(finalRun.agentDecisions.some((decision) => decision.type === "self_review"), true);
  assert.equal(storage.appended.skills.length, 1);
  assert.equal(storage.appended.avoid.length, 1);
}

async function testResumeSavedRun() {
  const storage = makeStorage();
  const agent = new HermesAgent({ projectName: "eval", rootDir: process.cwd(), storage });
  installOwnerSuccess(agent);
  let phase = "empty";
  const calls = [];
  agent.googleMaps = {
    async discover({ query }) {
      calls.push(query);
      if (phase === "empty") return { rawPath: "/tmp/empty.csv", rows: [] };
      return { rawPath: "/tmp/resume.csv", rows: makeRows("Resume Roof") };
    }
  };
  agent.scrapling = { async enrich() { return { ok: true, emails: [], phones: [], text: "" }; } };
  agent.minimax = {
    async qualifyLead() { return { qualified: true, score: 100, reason: "ok", missing: [] }; },
    async proposeSkill() { return { title: "Resume skill", when: "resumed run", do: "continue from checkpoint", avoid: "restart" }; },
    async proposeRetry({ context }) { return { variable: "query", value: context.untriedQueryHints?.[0] || "", reason: "continue" }; },
    async proposeSearchStrategy() { return { queries: [], reason: "none" }; },
    async proposeLocationStrategy() { return { locations: [], reason: "none" }; },
    async proposeRunReview() { return { summary: "review", skills: [], avoid: [] }; }
  };

  const run = await agent.startRun({ source: "multi-source-owner-evidence", niche: "roofers", cities: "Miami, FL", target: 1, minScore: 50, maxQueriesPerCity: 1 });
  await waitFor(agent);
  let finalRun = await storage.getRun(run.id);
  assert.equal(finalRun.status, "partial");
  const attemptedBefore = finalRun.search.attemptedQueries.length;

  phase = "leads";
  await agent.resumeRun(run.id);
  await waitFor(agent);
  finalRun = await storage.getRun(run.id);
  assert.equal(finalRun.status, "complete");
  assert.equal(finalRun.agentDecisions.some((decision) => decision.type === "resume_run"), true);
  assert.equal(finalRun.search.attemptedQueries.length > attemptedBefore, true);
}

async function testRecoverInterruptedRun() {
  const storage = makeStorage();
  const interrupted = {
    id: "interrupted-run",
    agent: "Hermes eval",
    status: "running",
    input: {
      niche: "roofers",
      countryScope: "United States",
      cities: ["Miami, FL, USA"],
      target: 1,
      minScore: 50,
      maxQueriesPerCity: 4,
      maxPlannedLocations: 1,
      criteria: "Has a reachable website, email or valid phone and is located in the United States."
    },
    createdAt: new Date().toISOString(),
    updatedAt: new Date().toISOString(),
    steps: [],
    failures: [],
    skillUpdates: [],
    agentDecisions: [],
    selfReviews: [],
    memory: { skills: [], avoid: [] },
    discoveredLeads: [],
    qualifiedLeads: [],
    counters: { discovered: 0, enriched: 0, qualified: 0, retries: 0, searchAttempts: 0 },
    rejectedLeads: [],
    search: { attemptedQueries: [], exhaustedCities: [] }
  };
  await storage.saveRun(interrupted);
  const agent = new HermesAgent({ projectName: "eval", rootDir: process.cwd(), storage });
  installOwnerSuccess(agent);
  agent.googleMaps = {
    async discover() {
      return { rawPath: "/tmp/recovered.csv", rows: makeRows("Recovered Roof") };
    }
  };
  agent.scrapling = { async enrich() { return { ok: true, emails: [], phones: [], text: "" }; } };
  agent.minimax = {
    async qualifyLead() { return { qualified: true, score: 100, reason: "ok", missing: [] }; },
    async proposeSkill() { return { title: "Recovered skill", when: "recovered", do: "continue", avoid: "restart" }; },
    async proposeRunReview() { return { summary: "recovered", skills: [], avoid: [] }; }
  };

  const recovered = await agent.recoverInterruptedRuns();
  await waitFor(agent);
  const finalRun = await storage.getRun(interrupted.id);
  assert.deepEqual(recovered, [interrupted.id]);
  assert.equal(finalRun.status, "complete");
  assert.equal(finalRun.agentDecisions.some((decision) => decision.type === "recover_run"), true);
}

async function testStartRunKillsPreviousRecoverableRun() {
  const storage = makeStorage();
  const agent = new HermesAgent({ projectName: "eval", rootDir: process.cwd(), storage });
  agent.execute = async () => {};

  const first = await agent.startRun({ source: "multi-source-owner-evidence", niche: "restaurants", cities: "Austin, TX", target: 1 });
  const second = await agent.startRun({ source: "multi-source-owner-evidence", niche: "dentists", cities: "Austin, TX", target: 1 });
  await waitFor(agent);

  const oldRun = await storage.getRun(first.id);
  const newRun = await storage.getRun(second.id);
  assert.equal(oldRun.status, "killed");
  assert.equal(oldRun.steps.some((step) => /newer query/i.test(step.message)), true);
  assert.equal(newRun.status, "queued");
  assert.equal(newRun.input.niche, "dentists");
}

async function testRecoverInterruptedRunsOnlyLatestQuery() {
  const storage = makeStorage();
  const older = {
    id: "older-running-run",
    agent: "Hermes eval",
    status: "running",
    input: {
      niche: "restaurants",
      countryScope: "United States",
      cities: ["Austin, TX, USA"],
      target: 1,
      minScore: 60,
      maxQueriesPerCity: 4,
      maxPlannedLocations: 1,
      criteria: "owner contact"
    },
    createdAt: "2026-06-25T10:00:00.000Z",
    updatedAt: "2026-06-25T10:00:00.000Z",
    steps: [],
    failures: [],
    skillUpdates: [],
    agentDecisions: [],
    selfReviews: [],
    memory: { skills: [], avoid: [] },
    discoveredLeads: [],
    qualifiedLeads: [],
    counters: { discovered: 0, enriched: 0, qualified: 0, retries: 0, searchAttempts: 0 },
    rejectedLeads: [],
    search: { attemptedQueries: [], exhaustedCities: [] }
  };
  const latest = {
    ...older,
    id: "latest-running-run",
    input: { ...older.input, niche: "dentists" },
    createdAt: "2026-06-25T10:05:00.000Z",
    updatedAt: "2026-06-25T10:05:00.000Z"
  };
  await storage.saveRun(older);
  await storage.saveRun(latest);
  const agent = new HermesAgent({ projectName: "eval", rootDir: process.cwd(), storage });
  agent.execute = async (id) => {
    const run = await storage.getRun(id);
    run.status = "complete";
    await storage.saveRun(run);
  };

  const recovered = await agent.recoverInterruptedRuns();
  await waitFor(agent);
  const olderRun = await storage.getRun(older.id);
  const latestRun = await storage.getRun(latest.id);
  assert.deepEqual(recovered, [latest.id]);
  assert.equal(olderRun.status, "killed");
  assert.equal(olderRun.steps.some((step) => /newer query/i.test(step.message)), true);
  assert.equal(latestRun.status, "complete");
}

async function testMarkInactiveRunsInterrupted() {
  const storage = makeStorage();
  const stale = {
    id: "stale-running-run",
    agent: "Hermes eval",
    status: "running",
    input: {
      niche: "salons",
      countryScope: "United States",
      cities: ["Austin, TX, USA"],
      target: 5,
      minScore: 60,
      maxQueriesPerCity: 4,
      maxPlannedLocations: 1,
      criteria: "owner contact"
    },
    createdAt: new Date().toISOString(),
    updatedAt: new Date().toISOString(),
    steps: [],
    failures: [],
    skillUpdates: [],
    agentDecisions: [],
    selfReviews: [],
    memory: { skills: [], avoid: [] },
    discoveredLeads: makeRows("Stale Salon", 3),
    qualifiedLeads: [],
    counters: { discovered: 3, ownerRabbitHoles: 2, ownerContactsFound: 0, qualified: 0 },
    rejectedLeads: [],
    search: { attemptedQueries: ["Austin, TX, USA|salons Austin"], exhaustedCities: [] }
  };
  await storage.saveRun(stale);
  const agent = new HermesAgent({ projectName: "eval", rootDir: process.cwd(), storage });

  const interrupted = await agent.markInterruptedRuns();
  const marked = await storage.getRun(stale.id);
  assert.deepEqual(interrupted, [stale.id]);
  assert.equal(marked.status, "interrupted");
  assert.equal(marked.interruptions.length, 1);
  assert.equal(marked.scaleAudit.discovered, 3);
  assert.equal(marked.scaleAudit.ownerHunts, 2);

  const recoverable = await storage.listRecoverableRuns();
  assert.deepEqual(recoverable.map((run) => run.id), [stale.id]);
}

async function testKilledRunCannotBeResurrectedByStalePersist() {
  const storage = makeStorage();
  const agent = new HermesAgent({ projectName: "eval", rootDir: process.cwd(), storage });
  const run = {
    id: "killed-stale-save",
    agent: "Hermes eval",
    status: "running",
    input: {
      source: "google-maps",
      niche: "salons",
      countryScope: "United States",
      cities: ["Austin, TX, USA"],
      target: 1,
      minScore: 60,
      maxQueriesPerCity: 4,
      maxPlannedLocations: 1,
      criteria: "owner contact"
    },
    createdAt: new Date().toISOString(),
    updatedAt: new Date().toISOString(),
    steps: [],
    failures: [],
    skillUpdates: [],
    agentDecisions: [],
    selfReviews: [],
    memory: { skills: [], avoid: [] },
    discoveredLeads: [],
    qualifiedLeads: [],
    counters: { discovered: 0, qualified: 0, searchAttempts: 0 },
    rejectedLeads: [],
    search: { attemptedQueries: [], exhaustedCities: [] },
    flowWatchdog: { enabled: false }
  };
  await storage.saveRun({ ...run, status: "killed" });
  agent.active.set(run.id, { cancelled: true, killed: true });

  await agent.persist(run, "maps_discovery", "Stale worker tried to save after kill.");
  const saved = await storage.getRun(run.id);
  assert.equal(saved.status, "killed");
  assert.equal(saved.steps.at(-1).message, "Stale worker tried to save after kill.");
}

async function testProviderFallback() {
  const originalKey = process.env.MINIMAX_API_KEY;
  const originalBase = process.env.MINIMAX_BASE_URL;
  process.env.MINIMAX_API_KEY = "sk-test";
  process.env.MINIMAX_BASE_URL = "http://127.0.0.1:9";
  const client = new MiniMaxClient();
  const result = await client.proposeSearchStrategy({ context: { niche: "roofers", city: "Miami, FL, USA" } });
  if (originalKey === undefined) delete process.env.MINIMAX_API_KEY; else process.env.MINIMAX_API_KEY = originalKey;
  if (originalBase === undefined) delete process.env.MINIMAX_BASE_URL; else process.env.MINIMAX_BASE_URL = originalBase;
  assert.equal(result.providerFallback, true);
  assert.equal(Array.isArray(result.queries), true);
  assert.equal(result.queries.length > 0, true);
  assert.equal(Boolean(result.providerMeta), true);
}

async function testMiniMaxTimeoutFallback() {
  const originalKey = process.env.MINIMAX_API_KEY;
  const originalBase = process.env.MINIMAX_BASE_URL;
  const originalTimeout = process.env.HERMES_MINIMAX_TIMEOUT_MS;
  const originalFetch = global.fetch;
  process.env.MINIMAX_API_KEY = "sk-test";
  process.env.MINIMAX_BASE_URL = "https://minimax.invalid";
  process.env.HERMES_MINIMAX_TIMEOUT_MS = "5";
  global.fetch = async (_url, options = {}) => new Promise((_resolve, reject) => {
    options.signal?.addEventListener("abort", () => {
      const error = new Error("aborted");
      error.name = "AbortError";
      reject(error);
    });
  });
  try {
    const client = new MiniMaxClient();
    const result = await client.proposeSearchStrategy({ context: { niche: "roofers", city: "Miami, FL, USA" } });
    assert.equal(result.providerFallback, true);
    assert.equal(result.providerMeta.timedOut, true);
    assert.match(result.providerError, /timed out/i);
  } finally {
    if (originalKey === undefined) delete process.env.MINIMAX_API_KEY; else process.env.MINIMAX_API_KEY = originalKey;
    if (originalBase === undefined) delete process.env.MINIMAX_BASE_URL; else process.env.MINIMAX_BASE_URL = originalBase;
    if (originalTimeout === undefined) delete process.env.HERMES_MINIMAX_TIMEOUT_MS; else process.env.HERMES_MINIMAX_TIMEOUT_MS = originalTimeout;
    global.fetch = originalFetch;
  }
}

async function testMiniMaxSchedulerLimitsConcurrency() {
  const originalKey = process.env.MINIMAX_API_KEY;
  const originalConcurrency = process.env.HERMES_MINIMAX_MAX_CONCURRENCY;
  const originalRpm = process.env.HERMES_MINIMAX_RPM_LIMIT;
  const originalRetries = process.env.HERMES_MINIMAX_RETRIES;
  const originalFetch = global.fetch;
  process.env.MINIMAX_API_KEY = "sk-test";
  process.env.HERMES_MINIMAX_MAX_CONCURRENCY = "1";
  process.env.HERMES_MINIMAX_RPM_LIMIT = "120";
  process.env.HERMES_MINIMAX_RETRIES = "0";
  let active = 0;
  let maxActive = 0;
  global.fetch = async () => {
    active += 1;
    maxActive = Math.max(maxActive, active);
    await new Promise((resolve) => setTimeout(resolve, 20));
    active -= 1;
    return {
      ok: true,
      async json() {
        return { choices: [{ message: { content: "{\"ok\":true}" } }] };
      }
    };
  };
  try {
    const client = new MiniMaxClient();
    const results = await Promise.all([
      client.chatJson({ system: "Return JSON", user: "{}", fallback: {} }),
      client.chatJson({ system: "Return JSON", user: "{}", fallback: {} }),
      client.chatJson({ system: "Return JSON", user: "{}", fallback: {} })
    ]);
    assert.equal(results.every((result) => result.ok === true), true);
    assert.equal(maxActive, 1);
    assert.equal(client.metrics().maxConcurrency, 1);
    assert.equal(client.metrics().totalStarted, 3);
  } finally {
    if (originalKey === undefined) delete process.env.MINIMAX_API_KEY; else process.env.MINIMAX_API_KEY = originalKey;
    if (originalConcurrency === undefined) delete process.env.HERMES_MINIMAX_MAX_CONCURRENCY; else process.env.HERMES_MINIMAX_MAX_CONCURRENCY = originalConcurrency;
    if (originalRpm === undefined) delete process.env.HERMES_MINIMAX_RPM_LIMIT; else process.env.HERMES_MINIMAX_RPM_LIMIT = originalRpm;
    if (originalRetries === undefined) delete process.env.HERMES_MINIMAX_RETRIES; else process.env.HERMES_MINIMAX_RETRIES = originalRetries;
    global.fetch = originalFetch;
  }
}

async function testMiniMaxFallbackQueriesStayOnNiche() {
  const originalKey = process.env.MINIMAX_API_KEY;
  delete process.env.MINIMAX_API_KEY;
  try {
    const client = new MiniMaxClient();
    const result = await client.proposeSearchStrategy({ context: { niche: "salons", city: "Austin, TX, USA" } });
    assert.equal(result.queries.some((query) => /contractors/i.test(query)), false);
    assert.equal(result.queries.some((query) => /owner|founder|family|ribbon|opened|story|independent/i.test(query)), true);
    assert.equal(result.queries.some((query) => /salon|stylist|beauty|nail|spa/i.test(query)), true);
    assert.equal(result.queries.every((query) => /Austin/i.test(query)), true);
    assert.equal(result.queries.some((query) => /\bnear\b|best|email phone/i.test(query)), false);
  } finally {
    if (originalKey === undefined) delete process.env.MINIMAX_API_KEY;
    else process.env.MINIMAX_API_KEY = originalKey;
  }
}

async function testOwnerFirstDiscoveryBuildsDiverseRestaurantAngles() {
  const plan = buildOwnerFirstDiscoveryPlan({ niche: "restaurant", location: "Austin, TX, USA", maxAngles: 24 });
  const queries = buildOwnerFirstQueryVariants("restaurant", "Austin, TX, USA", { maxQueries: 24, includeInventoryFallback: false });
  assert.equal(plan.version, OWNER_FIRST_STRATEGY_VERSION);
  assert.equal(plan.niche, "restaurant");
  assert.equal(plan.angles.length >= 20, true);
  assert.equal(plan.angles.every((angle) => angle.strategyVersion === OWNER_FIRST_STRATEGY_VERSION), true);
  assert.equal(plan.sourceFamilies.includes("about team page"), true);
  assert.equal(plan.sourceFamilies.includes("booking vendor bio"), true);
  assert.equal(queries.length >= 20, true);
  assert.equal(queries.some((query) => /chef-owner|chef owner/i.test(query)), true);
  assert.equal(queries.some((query) => /food truck|brick|pop-up|supper club/i.test(query)), true);
  assert.equal(queries.some((query) => /ribbon cutting|grand opening|under new ownership/i.test(query)), true);
  assert.equal(queries.some((query) => /chamber|permit|license|local food press|community/i.test(query)), true);
  assert.equal(queries.some((query) => /about.*team|founder story|booking.*bio|professional directory/i.test(query)), true);
  assert.equal(queries.some((query) => /^restaurant Austin|restaurant in Austin|restaurant near Austin|best restaurant Austin/i.test(query)), false);
  assert.equal(new Set(plan.angles.map((angle) => angle.signature)).size, plan.angles.length);
}

async function testOwnerFirstDiscoveryV4PrioritizesOwnerEvidenceSources() {
  const plan = buildOwnerFirstDiscoveryPlan({ niche: "roofers", location: "Birmingham, AL, USA", maxAngles: 24 });
  const queries = buildOwnerFirstQueryVariants("roofers", "Birmingham, AL, USA", { maxQueries: 24, includeInventoryFallback: false });
  assert.equal(plan.version, OWNER_FIRST_STRATEGY_VERSION);
  assert.equal(plan.sourceFamilies.includes("BBB principal profile"), true);
  assert.equal(plan.sourceFamilies.includes("contractor license board"), true);
  assert.equal(plan.angles.some((angle) => angle.mode === "bbb-principal"), true);
  assert.equal(plan.angles.some((angle) => angle.mode === "contractor-license-board"), true);
  assert.equal(queries.some((query) => /BBB.*principal|principal.*BBB/i.test(query)), true);
  assert.equal(queries.some((query) => /contractor license.*qualifying party|qualifying party.*contractor license/i.test(query)), true);
}

async function testPreflightBlocksMissingTool() {
  const storage = makeStorage();
  const agent = new HermesAgent({ projectName: "eval", rootDir: process.cwd(), storage });
  agent.autoRepairTools = false;
  let discoverCalled = false;
  agent.googleMaps = {
    available() { return false; },
    async discover() {
      discoverCalled = true;
      return { rawPath: "/tmp/nope.csv", rows: [] };
    }
  };
  agent.scrapling = {
    available() { return true; },
    async enrich() { return { ok: true, emails: [], phones: [], text: "" }; }
  };
  agent.minimax = { async proposeRunReview() { return { summary: "preflight", skills: [], avoid: [] }; } };

  const run = await agent.startRun({ source: "multi-source-owner-evidence", niche: "roofers", cities: "Miami, FL", target: 1 });
  await waitFor(agent);
  const finalRun = await storage.getRun(run.id);
  assert.equal(finalRun.status, "failed");
  assert.equal(finalRun.preflight.ok, false);
  assert.equal(finalRun.agentDecisions.some((decision) => decision.type === "preflight"), true);
  assert.equal(discoverCalled, false);
}

async function testPreflightRepairsMissingTool() {
  const storage = makeStorage();
  const agent = new HermesAgent({ projectName: "eval", rootDir: process.cwd(), storage });
  installOwnerSuccess(agent);
  let mapsAvailable = false;
  let repairCalled = false;
  let discoverCalled = false;
  agent.googleMaps = {
    available() { return mapsAvailable; },
    async discover() {
      discoverCalled = true;
      return { rawPath: "/tmp/repaired.csv", rows: makeRows("Repaired Roof") };
    }
  };
  agent.scrapling = {
    available() { return true; },
    async enrich() { return { ok: true, emails: [], phones: [], text: "" }; }
  };
  agent.repairTools = async () => {
    repairCalled = true;
    mapsAvailable = true;
    return {
      type: "tool_bootstrap",
      ok: true,
      command: "bash scripts/bootstrap_tools.sh",
      code: 0,
      output: "installed",
      message: "mock repair completed"
    };
  };
  agent.minimax = {
    async qualifyLead() { return { qualified: true, score: 100, reason: "ok", missing: [] }; },
    async proposeRunReview() { return { summary: "preflight repaired", skills: [], avoid: [] }; }
  };

  const run = await agent.startRun({ source: "multi-source-owner-evidence", niche: "roofers", cities: "Miami, FL", target: 1 });
  await waitFor(agent);
  const finalRun = await storage.getRun(run.id);
  assert.equal(finalRun.status, "complete");
  assert.equal(repairCalled, true);
  assert.equal(discoverCalled, true);
  assert.equal(finalRun.preflight.ok, true);
  assert.equal(finalRun.counters.toolRepairs, 1);
  assert.equal(finalRun.agentDecisions.some((decision) => decision.type === "repair_tools"), true);
  assert.equal(finalRun.preflight.repairs.some((repair) => repair.ok && repair.type === "tool_bootstrap"), true);
}

async function testPreflightPassesBeforeDiscovery() {
  const storage = makeStorage();
  const agent = new HermesAgent({ projectName: "eval", rootDir: process.cwd(), storage });
  installOwnerSuccess(agent);
  let discoverCalled = false;
  agent.googleMaps = {
    available() { return true; },
    async discover() {
      discoverCalled = true;
      return { rawPath: "/tmp/preflight.csv", rows: makeRows("Preflight Roof") };
    }
  };
  agent.scrapling = {
    available() { return true; },
    async enrich() { return { ok: true, emails: [], phones: [], text: "" }; }
  };
  agent.minimax = {
    async qualifyLead() { return { qualified: true, score: 100, reason: "ok", missing: [] }; },
    async proposeRunReview() { return { summary: "preflight", skills: [], avoid: [] }; }
  };

  const run = await agent.startRun({ source: "multi-source-owner-evidence", niche: "roofers", cities: "Miami, FL", target: 1 });
  await waitFor(agent);
  const finalRun = await storage.getRun(run.id);
  assert.equal(finalRun.status, "complete");
  assert.equal(finalRun.preflight.ok, true);
  assert.equal(discoverCalled, true);
}

async function testFlowWatchdogRecordsRepairTrainingMetadata() {
  const watchdog = new FlowWatchdog({
    minimax: {
      configured() { return true; },
      async chatJson({ user }) {
        const payload = JSON.parse(user);
        assert.equal(payload.repairPlaybook.codingTools.some((tool) => tool.id === "syntax_check"), true);
        assert.equal(payload.repairPlaybook.codingTools.some((tool) => tool.id === "dashboard_syntax_check"), true);
        assert.equal(payload.repairPlaybook.plugins.some((plugin) => plugin.id === "flow_watchdog"), true);
        assert.equal(payload.repairPlaybook.diagnosticRunbooks.some((runbook) => runbook.id === "provider_timeout_drill"), true);
        assert.equal(payload.repairPlaybook.patchTemplates.some((template) => template.id === "provider_timeout_patch"), true);
        assert.equal(payload.repairPlaybook.executionPolicy.reviewRequiredTools.includes("code_patch_plan"), true);
        assert.equal(payload.repairPlaybook.patchIdeas.length > 0, true);
        return {
          action: "Run syntax check, then create a bounded patch plan if it fails.",
          reason: "The playbook says coding repairs must be validated before resume.",
          variable: "code_patch_plan",
          tool: "syntax_check",
          runbook: "provider_timeout_drill",
          patchTemplate: "provider_timeout_patch",
          validationCommand: "npm run check",
          patchIdea: "Patch the smallest file that caused the syntax failure."
        };
      }
    }
  });
  const run = {
    status: "running",
    updatedAt: "2026-06-23T00:00:00.000Z",
    input: { niche: "salons" },
    steps: [{ phase: "classify", message: "recorded failure: timeout" }],
    failures: [],
    agentDecisions: [],
    counters: {}
  };

  const repair = await watchdog.inspect(run, { phase: "classify", message: "recorded failure: timeout" });
  assert.equal(repair.tool, "syntax_check");
  assert.equal(repair.variable, "code_patch_plan");
  assert.equal(repair.runbook, "provider_timeout_drill");
  assert.equal(repair.patchTemplate, "provider_timeout_patch");
  assert.equal(repair.validationCommand, "npm run check");
  assert.equal(run.flowWatchdog.repairs[0].patchIdea.includes("smallest file"), true);
  assert.equal(run.agentDecisions[0].tool, "syntax_check");
  assert.equal(run.agentDecisions[0].runbook, "provider_timeout_drill");
  assert.equal(run.agentDecisions[0].patchTemplate, "provider_timeout_patch");
  assert.equal(run.agentDecisions[0].patchIdea.includes("syntax failure"), true);
}

async function testFlowWatchdogProviderTimeoutFallbackIsConcrete() {
  const watchdog = new FlowWatchdog({
    minimax: {
      configured() { return false; }
    }
  });
  const run = {
    status: "running",
    updatedAt: "2026-06-23T00:00:00.000Z",
    input: { niche: "salons" },
    steps: [{ phase: "classify", message: "recorded failure: MiniMax request timed out after 90000ms" }],
    failures: [],
    agentDecisions: [],
    counters: {}
  };

  const repair = await watchdog.inspect(run, { phase: "classify", message: "recorded failure: MiniMax request timed out after 90000ms" });
  assert.equal(repair.variable, "provider_repair");
  assert.equal(repair.tool, "node_probe");
  assert.equal(repair.runbook, "provider_timeout_drill");
  assert.equal(repair.patchTemplate, "provider_timeout_patch");
  assert.equal(repair.validationCommand, "npm run check && npm test");
  assert.equal(run.flowWatchdog.alerts[0].code, "provider_repair_needed");
  assert.equal(run.agentDecisions[0].variable, "provider_repair");
}

async function testRepairPlaybookTrainsSafeCodingRepairs() {
  const toolIds = new Set(repairPlaybook.codingTools.map((tool) => tool.id));
  const runbookIds = new Set(repairPlaybook.diagnosticRunbooks.map((runbook) => runbook.id));
  const patchTemplateIds = new Set(repairPlaybook.patchTemplates.map((template) => template.id));

  for (const tool of repairPlaybook.codingTools) {
    assert.equal(Boolean(tool.id), true);
    assert.equal(Boolean(tool.command), true);
    assert.equal(Boolean(tool.useFor), true);
  }
  for (const toolId of repairPlaybook.executionPolicy.autoRunnableTools) {
    assert.equal(toolIds.has(toolId), true);
    assert.equal(repairPlaybook.codingTools.find((tool) => tool.id === toolId).safeToRunAutomatically, true);
  }
  for (const toolId of repairPlaybook.executionPolicy.reviewRequiredTools) {
    assert.equal(toolIds.has(toolId), true);
    assert.equal(repairPlaybook.codingTools.find((tool) => tool.id === toolId).safeToRunAutomatically, false);
  }

  assert.equal(toolIds.has("run_state_audit"), true);
  assert.equal(toolIds.has("playbook_consistency_probe"), true);
  assert.equal(toolIds.has("owner_evidence_audit"), true);
  assert.equal(runbookIds.has("run_lifecycle_drill"), true);
  assert.equal(runbookIds.has("playbook_consistency_drill"), true);
  assert.equal(patchTemplateIds.has("lifecycle_stale_worker_patch"), true);
  assert.equal(patchTemplateIds.has("export_field_patch"), true);

  assert.equal(repairPlaybook.executionPolicy.forbiddenAutomaticActions.includes("weaken owner-contact qualification"), true);
  assert.equal(repairPlaybook.executionPolicy.forbiddenAutomaticActions.includes("invent owner evidence"), true);
  assert.equal(repairPlaybook.guardrails.some((guardrail) => /unknown tool, runbook, variable, or patch ids/i.test(guardrail)), true);
  assert.equal(repairPlaybook.guardrails.some((guardrail) => /Never print secret values/i.test(guardrail)), true);
}

async function testLeadCsvExport() {
  const run = {
    input: { niche: "salons" },
    updatedAt: "2026-06-20T10:00:00.000Z",
    qualifiedLeads: [
      {
        name: 'Open Hair "Miami" Salon',
        category: "salons",
        city: "Miami, FL, USA",
        address: "1 Main St, Suite 2",
        rating: "4.8",
        reviews: "120",
        ownerName: "Olivia Parker",
        ownerTitle: "Owner",
        ownerEmail: "olivia@example.com",
        ownerPhone: "+1 786-442-9863",
        ownerEmails: ["olivia@example.com"],
        ownerPhones: ["+1 786-442-9863"],
        ownerLinkedIn: "https://www.linkedin.com/in/olivia-owner",
        ownerSocialProfiles: ["https://www.linkedin.com/in/olivia-owner"],
        ownerEvidenceUrl: "https://www.linkedin.com/in/olivia-owner",
        ownerContactSource: "owner-public-profile-or-article",
        ownerContactEvidence: [
          { type: "email", value: "olivia@example.com", url: "https://www.linkedin.com/in/olivia-owner", source: "owner-profile-fetch" },
          { type: "phone", value: "+1 786-442-9863", url: "https://www.linkedin.com/in/olivia-owner", source: "owner-profile-fetch" }
        ],
        rabbitHoleStatus: "owner-contact-found",
        rabbitHoleStrategy: { searchQueries: ["Open Hair Miami owner LinkedIn"] },
        businessPhone: "+1 786-000-0000",
        businessEmail: "openhairmiami@gmail.com",
        website: "https://example.com",
        quality: { score: 100, reason: "Matches niche, USA location, and reachable contact." }
      }
    ]
  };

  const csv = leadsCsv(run);
  assert.match(csv, /^Business Name,Category,Location,Website,Public Business Email,Phone Number,Contact Form URL,Social Links,Source\/Evidence URL,Validation Status,Contact Confidence Score,Campaign Ready,/);
  assert.match(csv, /"Open Hair ""Miami"" Salon"/);
  assert.match(csv, /openhairmiami@gmail\.com/);
  assert.match(csv, /business-direct/);
  assert.match(csv, /Campaign Ready/);
  assert.match(csv, /,100,/);
  assert.equal(leadExportFilename(run), "google-maps-salons-2026-06-20-qualified-leads.csv");
}

async function testBusinessContactAppearsInPrimaryCsvColumns() {
  const run = {
    input: { source: "lead-scraper", niche: "gyms" },
    updatedAt: "2026-06-24T10:00:00.000Z",
    qualifiedLeads: [
      {
        name: "Anchor Gym",
        category: "gym",
        city: "Orange Beach AL USA",
        businessPhone: "251-978-5071",
        businessEmail: "djs@example.com",
        contactForms: ["https://example.com/contact"],
        socialLinks: ["https://instagram.com/anchorgym"],
        sourceUrl: "https://maps.example/anchor-gym",
        website: "https://example.com",
        approved: true,
        quality: {
          score: 72,
          reason: "Owner reach fallback uses the listed contact."
        }
      }
    ]
  };

  const [header, row] = leadsCsv(run, { approvedOnly: true }).trim().split("\n");
  const headers = header.split(",");
  const cells = row.split(",");

  assert.equal(cells[headers.indexOf("Public Business Email")], "djs@example.com");
  assert.equal(cells[headers.indexOf("Phone Number")], "251-978-5071");
  assert.equal(cells[headers.indexOf("Contact Confidence Score")], "business-direct");
  assert.equal(cells[headers.indexOf("Campaign Ready")], "Yes");
  assert.equal(cells[headers.indexOf("Source/Evidence URL")], "https://maps.example/anchor-gym");
}

async function testLeadExportFiltersHiddenAndUnapproved() {
  const run = {
    input: { niche: "gyms" },
    qualifiedLeads: [
      { name: "Approved Gym", email: "a@example.com", approved: true, quality: { score: 100 } },
      { name: "Unapproved Gym", email: "b@example.com", approved: false, quality: { score: 100 } },
      { name: "Hidden Gym", email: "c@example.com", excluded: true, quality: { score: 100 } }
    ]
  };

  const csv = leadsCsv(run, { approvedOnly: true });
  assert.match(csv, /Approved Gym/);
  assert.doesNotMatch(csv, /Unapproved Gym/);
  assert.doesNotMatch(csv, /Hidden Gym/);
  const json = leadsJson(run, { approvedOnly: true });
  assert.deepEqual(json.map((lead) => lead.name), ["Approved Gym"]);
}

async function testStorageSerializesConcurrentRunSaves() {
  const tmp = await mkdtemp(path.join(os.tmpdir(), "hermes-storage-"));
  const storage = new Storage(tmp);
  await storage.init();
  const run = { id: "concurrent-save", status: "running", steps: [], input: {}, qualifiedLeads: [], counters: {} };

  await Promise.all(Array.from({ length: 20 }, async (_, index) => {
    run.steps.push({ phase: "test", message: `step ${index}`, at: new Date().toISOString() });
    run.counters.saved = index + 1;
    await storage.saveRun(run);
  }));

  const saved = await storage.getRun(run.id);
  assert.equal(saved.steps.length, 20);
  assert.equal(saved.counters.saved, 20);
}

async function testStorageRepairsCorruptOperationalMemory() {
  const tmp = await mkdtemp(path.join(os.tmpdir(), "hermes-storage-memory-"));
  const storage = new Storage(tmp);
  await storage.init();
  const avoidPath = path.join(tmp, "avoid", "hermes-avoid.json");
  await writeFile(avoidPath, '[{"bad":true}]trailing-corruption');

  await storage.appendAvoid({ stage: "owner", failure: "timeout", avoid: "Do not crash on corrupt memory." });

  const repaired = JSON.parse(await readFile(avoidPath, "utf8"));
  const backups = (await readdir(path.join(tmp, "avoid"))).filter((file) => file.startsWith("hermes-avoid.json.corrupt."));
  assert.equal(Array.isArray(repaired), true);
  assert.equal(repaired[0].failure, "timeout");
  assert.equal(backups.length, 1);
}

async function testLeadWorkerConcurrency() {
  const storage = makeStorage();
  const agent = new HermesAgent({ projectName: "eval", rootDir: process.cwd(), storage });
  let active = 0;
  let maxActive = 0;
  agent.scrapling = {
    async enrich() {
      active += 1;
      maxActive = Math.max(maxActive, active);
      await new Promise((resolve) => setTimeout(resolve, 15));
      active -= 1;
      return { ok: true, mode: "simple", emails: [], phones: [], text: "owner proof ".repeat(100) };
    }
  };
  installOwnerSuccess(agent);
  agent.minimax = {
    async qualifyLead() { return { qualified: true, score: 100, reason: "ok", missing: [] }; },
    async proposeRunReview() { return { summary: "ok", skills: [], avoid: [] }; }
  };
  const run = {
    id: "lead-worker-test",
    input: {
      source: "google-maps",
      niche: "salons",
      target: 6,
      minScore: 50,
      criteria: "owner contact",
      leadWorkerConcurrency: 3,
      ownerAgentSwarmSize: 5
    },
    status: "running",
    steps: [],
    failures: [],
    skillUpdates: [],
    agentDecisions: [],
    workflow: null,
    flowWatchdog: { enabled: false },
    qualifiedLeads: [],
    rejectedLeads: [],
    counters: { enriched: 0, qualified: 0, rejected: 0 }
  };

  const stats = await agent.enrichAndQualify(run, makeRows("Worker Salon", 6));
  assert.equal(stats.processed, 6);
  assert.equal(run.qualifiedLeads.length, 6);
  assert.equal(maxActive, 3);
  assert.equal(run.scaleAudit.qualified, 6);
  assert.equal(run.scaleAudit.targetReached, true);
  assert.equal(run.scaleAudit.portfolioStopReason, "target_reached");
  assert.equal(run.scaleAudit.ownerHunts, 6);
  assert.equal(run.scaleAudit.ownerContacts, 6);
  assert.equal(run.scaleAudit.ownerContactYieldPercent, 100);
}

async function testOwnerRabbitHoleTimeoutExhaustsLead() {
  const storage = makeStorage();
  const agent = new HermesAgent({ projectName: "eval", rootDir: process.cwd(), storage });
  agent.ownerHuntTimeoutMs = 20;
  agent.scrapling = {
    async enrich() {
      return { ok: true, mode: "simple", emails: [], phones: [], text: "timeout owner proof" };
    }
  };
  agent.ownerRabbitHole = {
    async findOwnerContact() {
      return new Promise(() => {});
    }
  };
  agent.minimax = {
    async qualifyLead() { return { qualified: true, score: 100, reason: "would qualify if owner contact existed", missing: [] }; }
  };
  const run = {
    id: "owner-timeout-test",
    input: {
      source: "google-maps",
      niche: "salons",
      target: 1,
      minScore: 50,
      criteria: "owner contact",
      leadWorkerConcurrency: 1,
      ownerAgentSwarmSize: 5
    },
    status: "running",
    steps: [],
    failures: [],
    skillUpdates: [],
    agentDecisions: [],
    flowWatchdog: { enabled: false },
    qualifiedLeads: [],
    rejectedLeads: [],
    counters: { enriched: 0, qualified: 0, rejected: 0 }
  };

  const stats = await agent.enrichAndQualify(run, makeRows("Timeout Salon", 1));
  assert.equal(stats.processed, 1);
  assert.equal(stats.rejected, 1);
  assert.equal(run.qualifiedLeads.length, 0);
  assert.equal(run.rejectedLeads[0].ownerHuntStatus, "owner-contact-exhausted");
  assert.equal(run.counters.ownerHuntTimeouts, 1);
  assert.equal(run.counters.ownerRabbitHoles, 1);
  assert.equal(run.counters.ownerAgentsTimedOut, 5);
  assert.equal(run.rejectedLeads[0].ownerAgentSwarm.summary.timedOut, 1);
  assert.equal(run.steps.some((step) => step.phase === "quality_gate"), false);
  assert.equal(run.agentDecisions.some((decision) => decision.type === "repair_enrichment"), false);
}

async function testScaleAuditProvesBacklogAndPlanExhaustion() {
  const storage = makeStorage();
  const agent = new HermesAgent({ projectName: "eval", rootDir: process.cwd(), storage });
  agent.scrapling = {
    async enrich() {
      return { ok: true, mode: "simple", emails: [], phones: [], text: "no owner contact proof" };
    }
  };
  agent.ownerRabbitHole = {
    async findOwnerContact() {
      return {
        rabbitHoleStatus: "owner-contact-exhausted",
        ownerSocialProfiles: [],
        ownerEmails: [],
        ownerPhones: []
      };
    }
  };
  const discoveredLeads = makeRows("Exhausted Salon", 1);
  const run = {
    id: "scale-cycle-closed-test",
    input: {
      source: "google-maps",
      niche: "salons",
      target: 2,
      minScore: 50,
      criteria: "owner contact",
      cities: ["Austin, TX, USA"],
      maxPlannedLocations: 1,
      leadWorkerConcurrency: 1,
      ownerAgentSwarmSize: 5
    },
    status: "partial",
    steps: [],
    failures: [],
    skillUpdates: [],
    agentDecisions: [],
    flowWatchdog: { enabled: false },
    discoveredLeads,
    qualifiedLeads: [],
    rejectedLeads: [],
    counters: { discovered: 1, enriched: 0, qualified: 0, rejected: 0 },
    search: { attemptedQueries: ["austin::nail salon Austin TX USA"], exhaustedCities: ["Austin, TX, USA"] }
  };

  const stats = await agent.enrichAndQualify(run, discoveredLeads);
  assert.equal(stats.processed, 1);
  assert.equal(run.scaleAudit.targetReached, false);
  assert.equal(run.scaleAudit.pendingDiscovered, 0);
  assert.equal(run.scaleAudit.ownerBacklogDrained, true);
  assert.equal(run.scaleAudit.locationPlanSearched, true);
  assert.equal(run.scaleAudit.locationCapacityExhausted, true);
  assert.equal(run.scaleAudit.portfolioCycleClosed, true);
  assert.equal(run.scaleAudit.portfolioStopReason, "plan_exhausted");
}

async function testDiscoveryContinuesWhileOwnerBacklogRuns() {
  const storage = makeStorage();
  const agent = new HermesAgent({ projectName: "eval", rootDir: process.cwd(), storage });
  let discoveries = 0;
  let ownerFinished = false;
  let releaseOwner = null;
  let secondDiscoverBeforeOwnerFinished = false;
  agent.googleMaps = {
    async discover() {
      discoveries += 1;
      if (discoveries === 2) {
        secondDiscoverBeforeOwnerFinished = !ownerFinished;
        if (releaseOwner) releaseOwner();
      }
      return { rawPath: `/tmp/parallel-${discoveries}.csv`, rows: makeRows(`Parallel Roof ${discoveries}`) };
    }
  };
  agent.scrapling = { async enrich() { return { ok: true, emails: [], phones: [], text: "" }; } };
  agent.ownerRabbitHole = {
    async findOwnerContact({ lead }) {
      await new Promise((resolve) => {
        releaseOwner = resolve;
        setTimeout(resolve, 1000);
      });
      ownerFinished = true;
      return {
        ownerName: "Jane Morgan",
        ownerTitle: "Owner",
        ownerEmail: "owner@example.com",
        ownerPhone: "",
        ownerEmails: ["owner@example.com"],
        ownerPhones: [],
        ownerLinkedIn: "https://www.linkedin.com/in/mock-owner",
        ownerSocialProfiles: ["https://www.linkedin.com/in/mock-owner"],
        ownerEvidenceUrl: "https://www.linkedin.com/in/mock-owner",
        ownerContactSource: "test-owner-profile",
        ownerContactEvidence: [
          { type: "email", value: "owner@example.com", url: "https://www.linkedin.com/in/mock-owner", source: "test-owner-profile" }
        ],
        rabbitHoleStatus: "owner-contact-found",
        rabbitHoleStrategy: { searchQueries: ["mock owner linkedin"], strategyReason: "test fixture" }
      };
    }
  };
  agent.minimax = {
    async proposeSearchStrategy() { return { queries: ["parallel roof repair Miami FL USA"], reason: "parallel discovery" }; },
    async qualifyLead() { return { qualified: true, score: 100, reason: "ok", missing: [] }; },
    async proposeRunReview() { return { summary: "parallel", skills: [], avoid: [] }; }
  };

  const run = await agent.startRun({ source: "multi-source-owner-evidence", niche: "roofers", cities: "Miami, FL", target: 1, minScore: 50, maxQueriesPerCity: 2, leadWorkerConcurrency: 1 });
  await waitFor(agent);
  const finalRun = await storage.getRun(run.id);
  assert.equal(finalRun.status, "complete");
  assert.equal(discoveries >= 2, true);
  assert.equal(secondDiscoverBeforeOwnerFinished, true);
  assert.equal(finalRun.qualifiedLeads.length, 1);
}

async function testLocationExpansionContinuesWhileOwnerBacklogRuns() {
  const storage = makeStorage();
  const agent = new HermesAgent({ projectName: "eval", rootDir: process.cwd(), storage });
  const discoveries = [];
  let ownerFinished = false;
  let releaseOwner = null;
  agent.googleMaps = {
    async discover({ query }) {
      discoveries.push(query);
      if (/orlando/i.test(query)) {
        if (releaseOwner) releaseOwner();
      }
      return { rawPath: `/tmp/location-backlog-${discoveries.length}.csv`, rows: makeRows(`Backlog Roof ${discoveries.length}`, 2) };
    }
  };
  agent.scrapling = { async enrich() { return { ok: true, emails: [], phones: [], text: "" }; } };
  agent.ownerRabbitHole = {
    async findOwnerContact({ lead }) {
      await new Promise((resolve) => {
        releaseOwner = resolve;
        setTimeout(resolve, 15000);
      });
      ownerFinished = true;
      return {
        ownerName: "Jane Morgan",
        ownerTitle: "Owner",
        ownerEmail: "owner@example.com",
        ownerEmails: ["owner@example.com"],
        ownerPhones: [],
        ownerLinkedIn: "https://www.linkedin.com/in/mock-owner",
        ownerSocialProfiles: ["https://www.linkedin.com/in/mock-owner"],
        ownerEvidenceUrl: "https://www.linkedin.com/in/mock-owner",
        ownerContactSource: "test-owner-profile",
        ownerContactEvidence: [
          { type: "email", value: "owner@example.com", url: "https://www.linkedin.com/in/mock-owner", source: "test-owner-profile" }
        ],
        rabbitHoleStatus: "owner-contact-found",
        rabbitHoleStrategy: { searchQueries: ["mock owner linkedin"], strategyReason: "test fixture" }
      };
    }
  };
  agent.minimax = {
    async proposeSearchStrategy() { return { queries: ["parallel roof repair Miami FL USA"], reason: "parallel discovery" }; },
    async proposeLocationStrategy() { return { locations: ["Orlando, FL, USA"], reason: "owner backlog should not block discovery expansion" }; },
    async qualifyLead() { return { qualified: true, score: 100, reason: "ok", missing: [] }; },
    async proposeRunReview() { return { summary: "expanded while owner backlog ran", skills: [], avoid: [] }; }
  };

  const run = await agent.startRun({ source: "multi-source-owner-evidence", niche: "roofers", cities: "Miami, FL", target: 1, minScore: 50, maxQueriesPerCity: 4, maxPlannedLocations: 2, leadWorkerConcurrency: 1 });
  await waitFor(agent);
  const finalRun = await storage.getRun(run.id);
  const ownerStartIndex = finalRun.steps.findIndex((step) => /Rabbit-hole owner search/i.test(step.message || ""));
  const orlandoSearchIndex = finalRun.steps.findIndex((step) => step.phase === "maps_discovery" && /orlando/i.test(step.message || ""));
  const qualifiedIndex = finalRun.steps.findIndex((step) => step.phase === "evolve" && /Qualified/i.test(step.message || ""));
  assert.equal(finalRun.status, "complete");
  assert.equal(finalRun.input.cities.includes("Orlando, FL, USA"), true);
  assert.equal(discoveries.some((query) => /orlando/i.test(query)), true);
  assert.equal(ownerStartIndex >= 0, true);
  assert.equal(orlandoSearchIndex > ownerStartIndex, true);
  assert.equal(qualifiedIndex === -1 || orlandoSearchIndex < qualifiedIndex, true);
}

async function testPendingDiscoveredLeadsResumeOnce() {
  const storage = makeStorage();
  const agent = new HermesAgent({ projectName: "eval", rootDir: process.cwd(), storage });
  installOwnerSuccess(agent);
  agent.scrapling = {
    async enrich() {
      return { ok: true, mode: "simple", emails: [], phones: [], text: "pending owner proof" };
    }
  };
  agent.minimax = {
    async qualifyLead() { return { qualified: true, score: 100, reason: "ok", missing: [] }; },
    async proposeRunReview() { return { summary: "ok", skills: [], avoid: [] }; }
  };
  const run = {
    id: "pending-discovered-test",
    input: {
      source: "google-maps",
      niche: "salons",
      target: 5,
      minScore: 50,
      criteria: "owner contact",
      leadWorkerConcurrency: 2,
      ownerAgentSwarmSize: 5
    },
    status: "running",
    steps: [],
    failures: [],
    skillUpdates: [],
    agentDecisions: [],
    workflow: null,
    flowWatchdog: { enabled: false },
    discoveredLeads: makeRows("Pending Salon", 3),
    qualifiedLeads: [],
    rejectedLeads: [],
    counters: { discovered: 3, enriched: 0, qualified: 0, rejected: 0 },
    search: { attemptedQueries: [], exhaustedCities: [] }
  };

  const first = await agent.processPendingDiscoveredLeads(run);
  assert.equal(first.processed, 3);
  assert.equal(run.qualifiedLeads.length, 3);
  assert.equal(run.processedLeadKeys.length, 3);
  assert.equal(run.scaleAudit.pendingDiscovered, 0);

  const second = await agent.processPendingDiscoveredLeads(run);
  assert.equal(second.processed, 0);
  assert.equal(run.qualifiedLeads.length, 3);
}

async function testPendingDiscoveredLeadsPrioritizeLocalOwnerContactYield() {
  const storage = makeStorage();
  const agent = new HermesAgent({ projectName: "eval", rootDir: process.cwd(), storage });
  const order = [];
  agent.scrapling = {
    async enrich() {
      return { ok: true, mode: "simple", emails: [], phones: [], text: "local owner proof" };
    }
  };
  agent.ownerRabbitHole = {
    async findOwnerContact({ lead }) {
      order.push(lead.name);
      return {
        ownerName: "Jane Morgan",
        ownerTitle: "Owner",
        ownerEmail: "owner@example.com",
        ownerEmails: ["owner@example.com"],
        ownerPhones: [],
        ownerLinkedIn: "https://www.linkedin.com/in/local-owner",
        ownerSocialProfiles: ["https://www.linkedin.com/in/local-owner"],
        ownerEvidenceUrl: "https://www.linkedin.com/in/local-owner",
        ownerContactSource: "test-owner-profile",
        ownerContactEvidence: [
          { type: "email", value: "owner@example.com", url: "https://www.linkedin.com/in/local-owner", source: "test-owner-profile" }
        ],
        rabbitHoleStatus: "owner-contact-found",
        rabbitHoleStrategy: { searchQueries: ["local owner linkedin"], strategyReason: "test ordering" }
      };
    }
  };
  agent.minimax = {
    async qualifyLead() { return { qualified: true, score: 100, reason: "ok", missing: [] }; },
    async proposeRunReview() { return { summary: "ok", skills: [], avoid: [] }; }
  };
  const run = {
    id: "pending-priority-test",
    input: {
      source: "google-maps",
      niche: "nail salon",
      target: 3,
      minScore: 50,
      criteria: "owner contact",
      leadWorkerConcurrency: 1,
      ownerAgentSwarmSize: 5
    },
    status: "running",
    steps: [],
    failures: [],
    skillUpdates: [],
    agentDecisions: [],
    workflow: null,
    flowWatchdog: { enabled: false },
    discoveredLeads: [
      {
        name: "Lash and Company Austin/Bee Cave",
        address: "12701 Hill Country Blvd O110, Bee Cave, TX",
        phone: "+1 512-300-0080",
        website: "https://lashandcompany.com/"
      },
      {
        name: "Lv Nail Lounge (10% Off Mon-Thurs)",
        address: "6501 S Congress Ave Ste 2-207, Austin, TX",
        phone: "+1 512-383-8702",
        website: "https://lvnailloungeaustin.com/"
      },
      {
        name: "Amor Nails & Spa",
        address: "5510 S I-35 Frontage Rd Ste 130, Austin, TX",
        phone: "+1 512-326-8383",
        website: ""
      }
    ],
    qualifiedLeads: [],
    rejectedLeads: [],
    counters: { discovered: 3, enriched: 0, qualified: 0, rejected: 0 },
    search: { attemptedQueries: [], exhaustedCities: [] }
  };

  const stats = await agent.processPendingDiscoveredLeads(run);
  assert.equal(stats.processed, 3);
  assert.deepEqual(order, [
    "Lv Nail Lounge (10% Off Mon-Thurs)",
    "Lash and Company Austin/Bee Cave",
    "Amor Nails & Spa"
  ]);
}

async function testRejectedDiscoveredLeadsAreSkippedOnResume() {
  const storage = makeStorage();
  const agent = new HermesAgent({ projectName: "eval", rootDir: process.cwd(), storage });
  let ownerHunts = 0;
  agent.scrapling = {
    async enrich() {
      return { ok: true, mode: "simple", emails: [], phones: [], text: "should not be used" };
    }
  };
  agent.ownerRabbitHole = {
    async findOwnerContact() {
      ownerHunts += 1;
      return {};
    }
  };
  agent.minimax = {
    async qualifyLead() { return { qualified: false, score: 0, reason: "should not score", missing: [] }; },
    async proposeRunReview() { return { summary: "ok", skills: [], avoid: [] }; }
  };
  const rejected = makeRows("Rejected Pending Salon", 1)[0];
  const run = {
    id: "pending-rejected-skip-test",
    input: {
      source: "google-maps",
      niche: "salons",
      target: 5,
      minScore: 50,
      criteria: "owner contact",
      leadWorkerConcurrency: 2,
      ownerAgentSwarmSize: 5
    },
    status: "running",
    steps: [],
    failures: [],
    skillUpdates: [],
    agentDecisions: [],
    workflow: null,
    flowWatchdog: { enabled: false },
    discoveredLeads: [rejected],
    qualifiedLeads: [],
    rejectedLeads: [{ ...rejected, score: 10, requiredScore: 50, qualified: false, reason: "previous rejection" }],
    processedLeadKeys: ["stale-partial-key"],
    counters: { discovered: 1, enriched: 0, qualified: 0, rejected: 1 },
    search: { attemptedQueries: [], exhaustedCities: [] }
  };

  const stats = await agent.processPendingDiscoveredLeads(run);
  assert.equal(stats.processed, 0);
  assert.equal(ownerHunts, 0);
  assert.equal(run.processedLeadKeys.includes(rejected.website), true);
}

async function testOwnerAgentMetricsRollIntoScaleAudit() {
  const storage = makeStorage();
  const agent = new HermesAgent({ projectName: "eval", rootDir: process.cwd(), storage });
  agent.scrapling = {
    async enrich() {
      return { ok: true, mode: "simple", emails: [], phones: [], text: "metric owner proof" };
    }
  };
  agent.ownerRabbitHole = {
    async findOwnerContact({ lead }) {
      return {
        ownerName: "Jane Morgan",
        ownerTitle: "Owner",
        ownerEmail: "metrics@example.net",
        ownerEmails: ["metrics@example.net"],
        ownerPhones: [],
        ownerSocialProfiles: ["https://www.linkedin.com/in/metrics-owner"],
        ownerEvidenceUrl: "https://www.linkedin.com/in/metrics-owner",
        ownerContactSource: "test-owner-profile",
        ownerContactEvidence: [
          { type: "email", value: "metrics@example.net", url: "https://www.linkedin.com/in/metrics-owner", source: "test-owner-profile" }
        ],
        ownerAgentSwarm: {
          deployedTotal: 6,
          summary: {
            searchHits: 18,
            pagesFetched: 7,
            ownerContactPromotions: 1
          }
        },
        rabbitHoleStatus: "owner-contact-found",
        rabbitHoleStrategy: { searchQueries: ["metric owner linkedin"], strategyReason: "test metrics" }
      };
    }
  };
  agent.minimax = {
    async qualifyLead() { return { qualified: true, score: 100, reason: "ok", missing: [] }; },
    async proposeRunReview() { return { summary: "ok", skills: [], avoid: [] }; }
  };
  const run = {
    id: "owner-agent-metrics-test",
    input: {
      source: "google-maps",
      niche: "salons",
      target: 1,
      minScore: 50,
      criteria: "owner contact",
      leadWorkerConcurrency: 1,
      ownerAgentSwarmSize: 5
    },
    status: "running",
    steps: [],
    failures: [],
    skillUpdates: [],
    agentDecisions: [],
    workflow: null,
    flowWatchdog: { enabled: false },
    qualifiedLeads: [],
    rejectedLeads: [],
    counters: { enriched: 0, qualified: 0, rejected: 0 }
  };

  const stats = await agent.enrichAndQualify(run, makeRows("Metrics Salon", 1));
  assert.equal(stats.qualified, 1);
  assert.equal(run.counters.ownerAgentsDeployed, 6);
  assert.equal(run.counters.ownerAgentSearchHits, 18);
  assert.equal(run.counters.ownerAgentPagesFetched, 7);
  assert.equal(run.counters.ownerAgentContactPromotions, 1);
  assert.equal(run.scaleAudit.ownerAgentsDeployed, 6);
  assert.equal(run.scaleAudit.ownerAgentsPerHunt, 6);
}

async function testOwnerContactWithoutEvidenceIsRejected() {
  const storage = makeStorage();
  const agent = new HermesAgent({ projectName: "eval", rootDir: process.cwd(), storage });
  agent.scrapling = {
    async enrich() {
      return { ok: true, mode: "simple", emails: [], phones: [], text: "owner contact proof missing" };
    }
  };
  agent.ownerRabbitHole = {
    async findOwnerContact() {
      return {
        ownerName: "No Proof Owner",
        ownerTitle: "Owner",
        ownerEmail: "noproof@example.net",
        ownerPhone: "",
        ownerEmails: ["noproof@example.net"],
        ownerPhones: [],
        ownerLinkedIn: "https://www.linkedin.com/in/no-proof-owner",
        ownerSocialProfiles: ["https://www.linkedin.com/in/no-proof-owner"],
        ownerContactSource: "unverified-test-profile",
        rabbitHoleStatus: "owner-contact-found",
        rabbitHoleStrategy: { searchQueries: ["No Proof Owner LinkedIn"], strategyReason: "test missing proof" }
      };
    }
  };
  agent.minimax = {
    async qualifyLead() { return { qualified: true, score: 100, reason: "model accepted but proof is absent", missing: [] }; },
    async proposeRunReview() { return { summary: "ok", skills: [], avoid: [] }; }
  };
  const run = {
    id: "owner-contact-proof-required-test",
    input: {
      source: "google-maps",
      niche: "salons",
      target: 1,
      minScore: 50,
      criteria: "owner contact",
      leadWorkerConcurrency: 1,
      ownerAgentSwarmSize: 5
    },
    status: "running",
    steps: [],
    failures: [],
    skillUpdates: [],
    agentDecisions: [],
    workflow: null,
    flowWatchdog: { enabled: false },
    qualifiedLeads: [],
    rejectedLeads: [],
    counters: { enriched: 0, qualified: 0, rejected: 0 }
  };

  const stats = await agent.enrichAndQualify(run, makeRows("No Proof Salon", 1));
  assert.equal(stats.rejected, 1);
  assert.equal(run.qualifiedLeads.length, 0);
  assert.equal(run.counters.ownerContactsFound, 0);
  assert.equal(run.scaleAudit.ownerHunts, 1);
  assert.equal(run.scaleAudit.ownerContacts, 0);
  assert.equal(Boolean(run.rejectedLeads[0].leadKey), true);
  assert.equal(run.rejectedLeads[0].ownerName, "No Proof Owner");
  assert.equal(run.rejectedLeads[0].ownerEmail, "noproof@example.net");
  assert.equal(run.rejectedLeads[0].rabbitHoleStatus, "owner-contact-found");
  assert.equal(run.rejectedLeads[0].ownerSocialProfiles[0], "https://www.linkedin.com/in/no-proof-owner");
  assert.equal(run.failures.some((failure) => /without owner-level contact evidence/i.test(failure.message)), true);
}

async function testOwnerSwarmEscalatesPromisingLead() {
  const storage = makeStorage();
  const agent = new HermesAgent({ projectName: "eval", rootDir: process.cwd(), storage });
  let observedSwarmSize = 0;
  agent.scrapling = {
    async enrich() {
      return {
        ok: true,
        mode: "simple",
        emails: [],
        phones: [],
        links: ["https://example.com/team", "https://www.linkedin.com/company/example"],
        socialLinks: ["https://www.linkedin.com/in/example-owner"],
        text: "Meet Jane Escalate, founder and owner of Example Salon.".repeat(20)
      };
    }
  };
  agent.ownerRabbitHole = {
    async findOwnerContact({ lead }) {
      observedSwarmSize = lead.ownerAgentSwarmSize;
      return {
        ownerName: "Jane Escalate",
        ownerTitle: "Owner",
        ownerEmail: "jane@escalate.example",
        ownerPhone: "",
        ownerEmails: ["jane@escalate.example"],
        ownerPhones: [],
        ownerLinkedIn: "https://www.linkedin.com/in/example-owner",
        ownerSocialProfiles: ["https://www.linkedin.com/in/example-owner"],
        ownerContactSource: "test-owner-profile",
        ownerContactEvidence: [
          { type: "email", value: "jane@escalate.example", url: "https://www.linkedin.com/in/example-owner", source: "test-owner-profile" }
        ],
        rabbitHoleStatus: "owner-contact-found",
        rabbitHoleStrategy: { searchQueries: ["Jane Escalate owner LinkedIn"], strategyReason: "test escalation" }
      };
    }
  };
  agent.minimax = {
    async qualifyLead() { return { qualified: true, score: 100, reason: "ok", missing: [] }; },
    async proposeRunReview() { return { summary: "ok", skills: [], avoid: [] }; }
  };
  const run = {
    id: "owner-swarm-escalation-test",
    input: {
      source: "google-maps",
      niche: "salons",
      target: 1,
      minScore: 50,
      criteria: "owner contact",
      leadWorkerConcurrency: 1,
      ownerAgentSwarmSize: 5
    },
    status: "running",
    steps: [],
    failures: [],
    skillUpdates: [],
    agentDecisions: [],
    workflow: null,
    flowWatchdog: { enabled: false },
    qualifiedLeads: [],
    rejectedLeads: [],
    counters: { enriched: 0, qualified: 0, rejected: 0 }
  };

  await agent.enrichAndQualify(run, makeRows("Escalate Salon", 1));
  assert.equal(observedSwarmSize, 14);
  assert.equal(run.qualifiedLeads.length, 1);
}

async function testOwnerSwarmDoesNotEscalateMapsPhoneOnlyLead() {
  const storage = makeStorage();
  const agent = new HermesAgent({ projectName: "eval", rootDir: process.cwd(), storage });
  let observedLead = null;
  agent.scrapling = {
    async enrich() {
      return {
        ok: true,
        mode: "simple",
        emails: [],
        phones: [],
        links: [],
        socialLinks: [],
        text: "Basic appointment information with no owner, team, profile, or personal email signal."
      };
    }
  };
  agent.ownerRabbitHole = {
    async findOwnerContact({ lead }) {
      observedLead = lead;
      return {
        ownerName: "Maya Chen",
        ownerTitle: "Owner",
        ownerEmail: "base-owner@example.net",
        ownerPhone: "",
        ownerEmails: ["base-owner@example.net"],
        ownerPhones: [],
        ownerSocialProfiles: ["https://www.linkedin.com/in/base-owner"],
        ownerContactSource: "test-owner-profile",
        ownerContactEvidence: [
          { type: "email", value: "base-owner@example.net", url: "https://www.linkedin.com/in/base-owner", source: "test-owner-profile" }
        ],
        rabbitHoleStatus: "owner-contact-found",
        rabbitHoleStrategy: { searchQueries: ["base owner LinkedIn"], strategyReason: "test base sizing" }
      };
    }
  };
  agent.minimax = {
    async qualifyLead() { return { qualified: true, score: 100, reason: "ok", missing: [] }; },
    async proposeRunReview() { return { summary: "ok", skills: [], avoid: [] }; }
  };
  const run = {
    id: "owner-swarm-base-size-test",
    input: {
      source: "google-maps",
      niche: "salons",
      target: 1,
      minScore: 50,
      criteria: "owner contact",
      leadWorkerConcurrency: 1,
      ownerAgentSwarmSize: 5
    },
    status: "running",
    steps: [],
    failures: [],
    skillUpdates: [],
    agentDecisions: [],
    workflow: null,
    flowWatchdog: { enabled: false },
    qualifiedLeads: [],
    rejectedLeads: [],
    counters: { enriched: 0, qualified: 0, rejected: 0 }
  };

  await agent.enrichAndQualify(run, makeRows("Base Salon", 1));
  assert.equal(observedLead.ownerAgentSwarmSize, 5);
  assert.equal(observedLead.ownerAgentEscalated, false);
  assert.equal(observedLead.ownerAgentPotentialScore < 3, true);
  assert.equal(run.qualifiedLeads.length, 1);
}

async function testMiniMaxPlansOwnerSwarmAgents() {
  const previousFetch = global.fetch;
  global.fetch = async () => ({
    ok: true,
    async text() {
      return '<a class="result__a" href="https://example.com/interview">Interview</a><a class="result__snippet">Jane Planned is the owner of Planned Salon.</a>';
    }
  });
  try {
    const strategyTimeouts = [];
    const swarmTimeouts = [];
    const pageTimeouts = [];
    const scrapling = {
      available() { return true; },
      async enrich({ url, timeout }) {
        pageTimeouts.push(timeout);
        return {
          ok: true,
          url,
          title: "Owner interview",
          text: "Jane Planned is the owner of Planned Salon. Email jane@planned.example or call +1 555 555 0199.",
          emails: ["jane@planned.example"],
          phones: ["+1 555 555 0199"],
          links: ["https://www.linkedin.com/in/jane-planned"],
          socialLinks: ["https://www.linkedin.com/in/jane-planned"]
        };
      }
    };
    const adapter = new OwnerRabbitHoleAdapter({
      scrapling,
      linkedinTools: { enrichProfile: scrapling.enrich },
      minimax: {
        configured() { return true; },
        async proposeOwnerRabbitHoleStrategy({ timeoutMs }) {
          strategyTimeouts.push(timeoutMs);
          return {
            pagePaths: ["/about"],
            searchQueries: ['"Planned Salon" owner'],
            profileUrlHints: [],
            ownerNameHints: [],
            strategyReason: "test strategy"
          };
        },
        async proposeOwnerAgentSwarm({ timeoutMs }) {
          swarmTimeouts.push(timeoutMs);
          return {
            agents: [{
              role: "planned-linkedin-owner",
              hypothesis: "AI planned exact owner LinkedIn search",
              queries: ['site:linkedin.com/in "Jane Planned" "Planned Salon"'],
              priority: 99
            }]
          };
        }
      }
    });
    const result = await adapter.findOwnerContact({
      lead: {
        name: "Planned Salon",
        city: "Austin, TX, USA",
        category: "salon",
        website: "https://example.com",
        ownerAgentSwarmSize: 3
      },
      initialEnrichment: { ok: true, text: "Jane Planned, owner of Planned Salon.", links: [], socialLinks: [] },
      run: { input: { ownerAgentSwarmSize: 3, ownerHuntTimeoutMs: 90000 }, counters: {} }
    });
    const trace = result.ownerAgentSwarm.traces.find((entry) => entry.role === "planned-linkedin-owner");
    assert.equal(Boolean(trace), true);
    assert.equal(trace.planner, "minimax");
    assert.equal(result.ownerAgentSwarm.summary.searchHits > 0, true);
    assert.equal(result.ownerAgentSwarm.summary.pagesFetched > 0, true);
    assert.equal(result.ownerAgentSwarm.summary.byPhase["identity-swarm"].agents > 0, true);
    assert.equal(result.ownerAgentSwarm.deployedTotal, result.ownerAgentSwarm.traces.length);
    assert.equal(result.ownerName, "Jane Planned");
    assert.equal(result.ownerEmail, "jane@planned.example");
    assert.equal(strategyTimeouts.every((timeout) => timeout > 0 && timeout <= 12000), true);
    assert.equal(swarmTimeouts.every((timeout) => timeout > 0 && timeout <= 12000), true);
    assert.equal(pageTimeouts.includes(8), true);
  } finally {
    global.fetch = previousFetch;
  }
}

async function testOwnerDiscoveryPagesAreBudgetedBeforeSwarm() {
  const previousFetch = global.fetch;
  global.fetch = async () => ({
    ok: true,
    async text() {
      return "";
    }
  });
  try {
    const businessPageFetches = [];
    const scrapling = {
      available() { return true; },
      async enrich({ url }) {
        if (/budgeted\.example/i.test(url)) businessPageFetches.push(url);
        return {
          ok: true,
          url,
          title: "Budgeted Salon",
          text: "Budgeted Salon Austin booking page.",
          emails: [],
          phones: [],
          links: [],
          socialLinks: []
        };
      }
    };
    const adapter = new OwnerRabbitHoleAdapter({
      scrapling,
      linkedinTools: { enrichProfile: scrapling.enrich },
      minimax: { configured() { return false; } }
    });

    const result = await adapter.findOwnerContact({
      lead: {
        name: "Budgeted Salon",
        city: "Austin, TX, USA",
        category: "salon",
        website: "https://budgeted.example",
        ownerAgentSwarmSize: 1
      },
      initialEnrichment: {
        ok: true,
        text: "Budgeted Salon Austin.",
        emails: [],
        phones: [],
        links: [],
        socialLinks: []
      },
      run: { input: { ownerAgentSwarmSize: 1, ownerHuntTimeoutMs: 90000 }, counters: {} }
    });

    assert.equal(businessPageFetches.length <= 4, true);
    assert.equal(result.rabbitHoleStatus, "owner-not-found");
    assert.equal(result.ownerEmail, "");
  } finally {
    global.fetch = previousFetch;
  }
}

async function testOwnerSwarmSkipsMiniMaxWhenTimeoutCircuitOpen() {
  const previousFetch = global.fetch;
  global.fetch = async () => ({
    ok: true,
    async text() {
      return "";
    }
  });
  let minimaxCalls = 0;
  try {
    const scrapling = {
      available() { return true; },
      async enrich({ url }) {
        return {
          ok: true,
          url,
          title: "About Circuit Salon",
          text: "Circuit Salon is a local salon in Austin.",
          emails: [],
          phones: [],
          links: [],
          socialLinks: []
        };
      }
    };
    const adapter = new OwnerRabbitHoleAdapter({
      scrapling,
      linkedinTools: { enrichProfile: scrapling.enrich },
      minimax: {
        configured() { return true; },
        async proposeOwnerRabbitHoleStrategy() {
          minimaxCalls += 1;
          return { pagePaths: [], searchQueries: [], profileUrlHints: [], ownerNameHints: [] };
        },
        async proposeOwnerAgentSwarm() {
          minimaxCalls += 1;
          return { agents: [] };
        },
        async chatJson() {
          minimaxCalls += 1;
          return { owners: [] };
        }
      }
    });
    const result = await adapter.findOwnerContact({
      lead: {
        name: "Circuit Salon",
        city: "Austin, TX, USA",
        category: "salon",
        website: "https://example.com",
        ownerAgentSwarmSize: 2
      },
      initialEnrichment: { ok: true, text: "Circuit Salon in Austin.", links: [], socialLinks: [] },
      run: { input: { ownerAgentSwarmSize: 2 }, counters: { llmTimeouts: 5, llmConsecutiveTimeoutFallbacks: 5 } }
    });
    assert.equal(minimaxCalls, 0);
    assert.equal(result.ownerAgentSwarm.deployedTotal > 0, true);
    assert.equal(result.ownerAgentSwarm.traces.every((trace) => trace.planner !== "minimax"), true);
  } finally {
    global.fetch = previousFetch;
  }
}

async function testOwnerSwarmSkipsMiniMaxWhenTimeoutRateIsHigh() {
  const previousFetch = global.fetch;
  global.fetch = async () => ({
    ok: true,
    async text() {
      return "";
    }
  });
  let minimaxCalls = 0;
  try {
    const scrapling = {
      available() { return true; },
      async enrich({ url }) {
        return {
          ok: true,
          url,
          title: "Rate Circuit Salon",
          text: "Rate Circuit Salon is a local salon in Austin.",
          emails: [],
          phones: [],
          links: [],
          socialLinks: []
        };
      }
    };
    const adapter = new OwnerRabbitHoleAdapter({
      scrapling,
      linkedinTools: { enrichProfile: scrapling.enrich },
      minimax: {
        configured() { return true; },
        async proposeOwnerRabbitHoleStrategy() {
          minimaxCalls += 1;
          return { pagePaths: [], searchQueries: [], profileUrlHints: [], ownerNameHints: [] };
        },
        async proposeOwnerAgentSwarm() {
          minimaxCalls += 1;
          return { agents: [] };
        },
        async chatJson() {
          minimaxCalls += 1;
          return { owners: [] };
        }
      }
    });
    const result = await adapter.findOwnerContact({
      lead: {
        name: "Rate Circuit Salon",
        city: "Austin, TX, USA",
        category: "salon",
        website: "https://rate-circuit.example",
        ownerAgentSwarmSize: 20
      },
      initialEnrichment: { ok: true, text: "Rate Circuit Salon in Austin.", links: [], socialLinks: [] },
      run: { input: { ownerAgentSwarmSize: 20, ownerHuntTimeoutMs: 90000 }, counters: { llmCalls: 10, llmTimeouts: 5, llmConsecutiveTimeoutFallbacks: 0 } }
    });
    assert.equal(minimaxCalls, 0);
    assert.equal(result.ownerAgentSwarm.deployedTotal > 0, true);
    assert.equal(result.ownerAgentSwarm.deployedTotal <= 16, true);
    assert.equal(result.ownerAgentSwarm.requested, 8);
    assert.equal(result.ownerAgentSwarm.traces.every((trace) => trace.planner !== "minimax"), true);
  } finally {
    global.fetch = previousFetch;
  }
}

async function testOwnerSwarmSkipsMiniMaxWhenOwnerBudgetIsShort() {
  const previousFetch = global.fetch;
  global.fetch = async () => ({
    ok: true,
    async text() {
      return '<a class="result__a" href="https://example.com/short-owner">Short owner</a><a class="result__snippet">Short Budget is the owner of Short Budget Salon.</a>';
    }
  });
  let minimaxCalls = 0;
  try {
    const scrapling = {
      available() { return true; },
      async enrich({ url }) {
        return {
          ok: true,
          url,
          title: "Short Budget profile",
          text: "Short Budget is the owner of Short Budget Salon.",
          emails: [],
          phones: [],
          links: [],
          socialLinks: []
        };
      }
    };
    const adapter = new OwnerRabbitHoleAdapter({
      scrapling,
      linkedinTools: { enrichProfile: scrapling.enrich },
      minimax: {
        configured() { return true; },
        async proposeOwnerRabbitHoleStrategy() {
          minimaxCalls += 1;
          return { pagePaths: [], searchQueries: [], profileUrlHints: [], ownerNameHints: [] };
        },
        async proposeOwnerAgentSwarm() {
          minimaxCalls += 1;
          return { agents: [] };
        },
        async chatJson() {
          minimaxCalls += 1;
          return { owners: [] };
        }
      }
    });
    const run = { input: { ownerAgentSwarmSize: 2, ownerHuntTimeoutMs: 20000 }, counters: {} };
    const result = await adapter.findOwnerContact({
      lead: {
        name: "Short Budget Salon",
        city: "Austin, TX, USA",
        category: "salon",
        website: "https://example.com",
        ownerAgentSwarmSize: 2
      },
      initialEnrichment: { ok: true, text: "Short Budget Salon in Austin.", links: [], socialLinks: [] },
      run
    });
    assert.equal(minimaxCalls, 0);
    assert.equal(run.counters.ownerAgentsStarted > 0, true);
    assert.equal(result.ownerAgentSwarm.deployedTotal > 0, true);
    assert.equal(result.ownerAgentSwarm.traces.every((trace) => trace.planner !== "minimax"), true);
  } finally {
    global.fetch = previousFetch;
  }
}

async function testOwnerEvidenceToolPolicySkipsTimeoutHeavyToolOnly() {
  const run = {
    ownerEvidenceToolAttempts: [
      {
        mode: "website",
        toolAttempts: [
          { tool: "laramies/theHarvester", status: "failed", timeout: true },
          { tool: "s0md3v/Photon", status: "used", timeout: false }
        ]
      },
      {
        mode: "website",
        toolAttempts: [
          { tool: "laramies/theHarvester", status: "failed", timeout: true },
          { tool: "s0md3v/Photon", status: "failed", timeout: false }
        ]
      },
      {
        mode: "website",
        toolAttempts: [
          { tool: "laramies/theHarvester", status: "failed", timeout: true },
          { tool: "s0md3v/Photon", status: "used", timeout: false }
        ]
      },
      {
        mode: "profiles",
        toolAttempts: [
          { tool: "soxoj/maigret", status: "used", timeout: false }
        ]
      }
    ]
  };

  const websitePolicy = ownerEvidenceToolPolicy(run, { mode: "website" });
  const profilePolicy = ownerEvidenceToolPolicy(run, { mode: "profiles" });
  assert.equal(websitePolicy.action, "skip-timeout-heavy-tools");
  assert.equal(websitePolicy.disabledTools.includes("laramies/theHarvester"), true);
  assert.equal(websitePolicy.disabledTools.includes("s0md3v/Photon"), false);
  assert.match(websitePolicy.reasons["laramies/theHarvester"], /public-profile\/Scrapling paths/);
  assert.equal(profilePolicy.disabledTools.length, 0);
}

async function testDirectOwnerContactSearchPromotesOwnerEmail() {
  const previousFetch = global.fetch;
  global.fetch = async () => ({
    ok: true,
    async text() {
      return '<a class="result__a" href="https://example.com/jane-contact">Jane contact</a><a class="result__snippet">Jane Direct owner of Direct Salon email jane@direct.example</a>';
    }
  });
  try {
    const scrapling = {
      available() { return true; },
      async enrich({ url }) {
        return {
          ok: true,
          url,
          title: "Jane Direct contact",
          text: "Jane Direct is the owner of Direct Salon. Contact Jane Direct at jane@direct.example.",
          emails: ["jane@direct.example"],
          phones: [],
          links: [],
          socialLinks: []
        };
      }
    };
    const adapter = new OwnerRabbitHoleAdapter({
      scrapling,
      linkedinTools: { enrichProfile: scrapling.enrich },
      minimax: {
        configured() { return true; },
        async proposeOwnerRabbitHoleStrategy() {
          return {
            pagePaths: ["/about"],
            searchQueries: ['"Direct Salon" owner'],
            profileUrlHints: [],
            ownerNameHints: [],
            strategyReason: "test direct owner contact"
          };
        },
        async proposeOwnerAgentSwarm({ phase }) {
          return {
            agents: [{
              role: phase === "profile-contact-swarm" ? "direct-owner-contact" : "owner-identity",
              hypothesis: "Find Jane Direct owner contact",
              queries: ['"Jane Direct" "Direct Salon" email'],
              priority: 100
            }]
          };
        }
      }
    });
    const result = await adapter.findOwnerContact({
      lead: {
        name: "Direct Salon",
        city: "Austin, TX, USA",
        category: "salon",
        website: "https://example.com",
        ownerAgentSwarmSize: 2
      },
      initialEnrichment: { ok: true, text: "Jane Direct is the owner of Direct Salon.", links: [], socialLinks: [] },
      run: { input: { ownerAgentSwarmSize: 2 } }
    });
    assert.equal(result.ownerName, "Jane Direct");
    assert.equal(result.ownerEmail, "jane@direct.example");
    assert.equal(result.businessEmail, "");
    assert.equal(result.ownerContactSource, "owner-public-profile-or-article");
    assert.equal(result.ownerContactEvidence.some((entry) => entry.value === "jane@direct.example" && entry.url === "https://example.com/jane-contact"), true);
    assert.equal(result.ownerAgentSwarm.traces.some((trace) => trace.ownerContactPromoted), true);
  } finally {
    global.fetch = previousFetch;
  }
}

async function testFoundedByOwnerPhonePromotesFormattedPhone() {
  const previousFetch = global.fetch;
  global.fetch = async () => ({
    ok: true,
    async text() {
      return "";
    }
  });
  try {
    const scrapling = {
      available() { return true; },
      async enrich({ url }) {
        return {
          ok: true,
          url,
          title: "About Format Salon",
          text: "Founded by Jane Format, Format Salon serves Austin clients. Contact Jane Format directly at (512) 555-0199 for consultations.",
          emails: [],
          phones: ["512-555-0199"],
          links: [],
          socialLinks: []
        };
      }
    };
    const adapter = new OwnerRabbitHoleAdapter({
      scrapling,
      linkedinTools: { enrichProfile: scrapling.enrich },
      minimax: { configured() { return false; } }
    });
    const result = await adapter.findOwnerContact({
      lead: {
        name: "Format Salon",
        city: "Austin, TX, USA",
        category: "salon",
        website: "https://example.com",
        ownerAgentSwarmSize: 1
      },
      initialEnrichment: {
        ok: true,
        text: "Founded by Jane Format, Format Salon serves Austin clients. Contact Jane Format directly at (512) 555-0199.",
        phones: ["512-555-0199"],
        links: [],
        socialLinks: []
      },
      run: { input: { ownerAgentSwarmSize: 1 } }
    });
    assert.equal(result.ownerName, "Jane Format");
    assert.equal(result.ownerPhone, "512-555-0199");
    assert.equal(result.businessPhone, "");
    assert.equal(result.ownerContactEvidence.some((entry) => entry.value === "512-555-0199"), true);
  } finally {
    global.fetch = previousFetch;
  }
}

async function testOwnerHintEmailLocalPartPromotesOwnerEmail() {
  const previousFetch = global.fetch;
  global.fetch = async () => ({
    ok: true,
    async text() {
      return "";
    }
  });
  try {
    const scrapling = {
      available() { return true; },
      async enrich({ url }) {
        return {
          ok: true,
          url,
          title: "LV Nail Lounge",
          text: "LV Nail Lounge in Austin, TX. PHONE NUMBER 512-383-8702 SEND MESSAGE ivypham2012@yahoo.com.",
          emails: ["ivypham2012@yahoo.com"],
          phones: ["512-383-8702"],
          links: [],
          socialLinks: []
        };
      }
    };
    const adapter = new OwnerRabbitHoleAdapter({
      scrapling,
      linkedinTools: { enrichProfile: scrapling.enrich },
      minimax: {
        configured() { return true; },
        async proposeOwnerRabbitHoleStrategy() {
          return {
            pagePaths: ["/about"],
            searchQueries: ['"LV Nail Lounge" "Ivy Pham"', '"Ivy Pham" nail salon Austin'],
            profileUrlHints: [],
            ownerNameHints: ["Ivy Pham"],
            strategyReason: "test owner hint from evidence"
          };
        },
        async proposeOwnerAgentSwarm() {
          return { agents: [] };
        }
      }
    });
    const result = await adapter.findOwnerContact({
      lead: {
        name: "LV Nail Lounge",
        city: "Austin, TX, USA",
        category: "nail salon",
        website: "https://lvnailloungeaustin.com",
        ownerAgentSwarmSize: 1
      },
      initialEnrichment: {
        ok: true,
        text: "LV Nail Lounge in Austin, TX. PHONE NUMBER 512-383-8702 SEND MESSAGE ivypham2012@yahoo.com.",
        emails: ["ivypham2012@yahoo.com"],
        phones: ["512-383-8702"],
        links: [],
        socialLinks: []
      },
      run: { input: { ownerAgentSwarmSize: 1 } }
    });
    assert.equal(result.ownerName, "Ivy Pham");
    assert.equal(result.ownerEmail, "ivypham2012@yahoo.com");
    assert.equal(result.businessEmail, "");
    assert.equal(result.ownerContactEvidence.some((entry) => entry.source === "owner-name-hint-business-page"), true);
  } finally {
    global.fetch = previousFetch;
  }
}

async function testOwnerHintDoesNotPromoteGenericBusinessEmail() {
  const previousFetch = global.fetch;
  global.fetch = async () => ({
    ok: true,
    async text() {
      return "";
    }
  });
  try {
    const scrapling = {
      available() { return true; },
      async enrich({ url }) {
        return {
          ok: true,
          url,
          title: "Generic Salon",
          text: "Generic Salon. Contact info@genericsalon.com for appointments.",
          emails: ["info@genericsalon.com"],
          phones: [],
          links: [],
          socialLinks: []
        };
      }
    };
    const adapter = new OwnerRabbitHoleAdapter({
      scrapling,
      linkedinTools: { enrichProfile: scrapling.enrich },
      minimax: {
        configured() { return true; },
        async proposeOwnerRabbitHoleStrategy() {
          return {
            pagePaths: ["/about"],
            searchQueries: ['"Generic Salon" "Jane Generic"'],
            profileUrlHints: [],
            ownerNameHints: ["Jane Generic"],
            strategyReason: "test generic email"
          };
        },
        async proposeOwnerAgentSwarm() {
          return { agents: [] };
        }
      }
    });
    const result = await adapter.findOwnerContact({
      lead: {
        name: "Generic Salon",
        city: "Austin, TX, USA",
        category: "salon",
        website: "https://generic.example.net",
        ownerAgentSwarmSize: 1
      },
      initialEnrichment: { ok: true, text: "Generic Salon. Contact info@genericsalon.com.", emails: ["info@genericsalon.com"], links: [], socialLinks: [] },
      run: { input: { ownerAgentSwarmSize: 1 } }
    });
    assert.equal(result.ownerEmail, "");
    assert.equal(result.businessEmail, "info@genericsalon.com");
    assert.notEqual(result.rabbitHoleStatus, "owner-contact-found");
  } finally {
    global.fetch = previousFetch;
  }
}

async function testBusinessNameHintDoesNotPromoteOwnerContact() {
  const previousFetch = global.fetch;
  global.fetch = async () => ({
    ok: true,
    async text() {
      return "";
    }
  });
  try {
    const scrapling = {
      available() { return true; },
      async enrich({ url }) {
        return {
          ok: true,
          url,
          title: "Milano Nail Spa",
          text: "Milano Nail Spa in Austin. Email milanonailspaatx@gmail.com for appointments.",
          emails: ["milanonailspaatx@gmail.com"],
          phones: [],
          links: [
            "https://milanonailspaatx.com/_nuxt/entry.css",
            "https://milanonailspaatx.com/_payload.json?abc"
          ],
          socialLinks: []
        };
      }
    };
    const adapter = new OwnerRabbitHoleAdapter({
      scrapling,
      linkedinTools: { enrichProfile: scrapling.enrich },
      minimax: {
        configured() { return true; },
        async proposeOwnerRabbitHoleStrategy() {
          return {
            pagePaths: ["/about"],
            searchQueries: ['"Milano Nail Spa" Austin owner'],
            profileUrlHints: ["https://milanonailspaatx.com/_payload.json?abc"],
            ownerNameHints: ["Milano Nail Spa"],
            strategyReason: "test business name hint"
          };
        },
        async proposeOwnerAgentSwarm() {
          return { agents: [] };
        }
      }
    });
    const result = await adapter.findOwnerContact({
      lead: {
        name: "MILANO NAIL SPA AUSTIN | Best Nail Salon in Austin Texas",
        city: "Austin, TX, USA",
        category: "nail salon",
        website: "https://milanonailspaatx.com",
        ownerAgentSwarmSize: 1
      },
      initialEnrichment: {
        ok: true,
        text: "Milano Nail Spa in Austin. Email milanonailspaatx@gmail.com for appointments.",
        emails: ["milanonailspaatx@gmail.com"],
        links: ["https://milanonailspaatx.com/_nuxt/entry.css", "https://milanonailspaatx.com/_payload.json?abc"],
        socialLinks: []
      },
      run: { input: { ownerAgentSwarmSize: 1 } }
    });
    assert.equal(result.ownerName, "");
    assert.equal(result.ownerEmail, "");
    assert.equal(result.businessEmail, "milanonailspaatx@gmail.com");
    assert.equal(result.ownerSocialProfiles.some((url) => /_nuxt|_payload/.test(url)), false);
  } finally {
    global.fetch = previousFetch;
  }
}

async function testBogusOwnerPhraseDoesNotCertifyBusinessPhone() {
  const previousFetch = global.fetch;
  global.fetch = async () => ({
    ok: true,
    async text() {
      return "";
    }
  });
  try {
    const scrapling = {
      available() { return true; },
      async enrich({ url }) {
        return {
          ok: true,
          url,
          title: "About Phrase Salon",
          text: "OUR VALUE STATEMENTS Take pride in service. Owner Stylist. Phrase Salon Austin. Call 512-433-6762 for appointments.",
          emails: [],
          phones: ["512-433-6762"],
          links: ["https://www.instagram.com/phrasesalon"],
          socialLinks: ["https://www.instagram.com/phrasesalon"]
        };
      }
    };
    const adapter = new OwnerRabbitHoleAdapter({
      scrapling,
      linkedinTools: { enrichProfile: scrapling.enrich },
      minimax: { configured() { return false; } }
    });
    const result = await adapter.findOwnerContact({
      lead: {
        name: "Phrase Salon",
        city: "Austin, TX, USA",
        category: "salon",
        website: "https://example.com",
        ownerAgentSwarmSize: 1
      },
      initialEnrichment: { ok: true, text: "Phrase Salon. Call 512-433-6762.", phones: ["512-433-6762"], links: [], socialLinks: [] },
      run: { input: { ownerAgentSwarmSize: 1 } }
    });
    assert.equal(result.ownerName, "");
    assert.equal(result.ownerPhone, "");
    assert.equal(result.businessPhone, "512-433-6762");
    assert.notEqual(result.rabbitHoleStatus, "owner-contact-found");
  } finally {
    global.fetch = previousFetch;
  }
}

async function testTravelMarketingPhraseDoesNotBecomeOwnerName() {
  const previousFetch = global.fetch;
  global.fetch = async () => ({
    ok: true,
    async text() {
      return "";
    }
  });
  try {
    const scrapling = {
      available() { return true; },
      async enrich({ url }) {
        return {
          ok: true,
          url,
          title: "Vietnam tour article",
          text: "Premium Vietnam Tour Operator guide. Highland Nails Austin booking page.",
          emails: [],
          phones: [],
          links: [],
          socialLinks: []
        };
      }
    };
    const adapter = new OwnerRabbitHoleAdapter({
      scrapling,
      linkedinTools: { enrichProfile: scrapling.enrich },
      minimax: { configured() { return false; } }
    });
    const result = await adapter.findOwnerContact({
      lead: {
        name: "Highland Nails & Spa | Best Nail Salon in ATX",
        city: "Austin, TX, USA",
        category: "nail salon",
        website: "https://highlandnailsatx.com",
        ownerAgentSwarmSize: 1
      },
      initialEnrichment: {
        ok: true,
        text: "Premium Vietnam Tour Operator guide. Highland Nails Austin booking page.",
        links: [],
        socialLinks: []
      },
      run: { input: { ownerAgentSwarmSize: 1 } }
    });
    assert.equal(result.ownerName, "");
    assert.notEqual(result.rabbitHoleStatus, "owner-profile-found-no-contact");
  } finally {
    global.fetch = previousFetch;
  }
}

async function testBingFallbackSearchFeedsOwnerSwarm() {
  const previousFetch = global.fetch;
  const calls = [];
  let searchPageFetches = 0;
  global.fetch = async (url) => {
    calls.push(String(url));
    if (String(url).includes("duckduckgo.com")) {
      throw new Error("duckduckgo unavailable");
    }
    return {
      ok: true,
      async text() {
        return '<li class="b_algo"><h2><a href="https://www.bing.com/ck/a?u=a1aHR0cHM6Ly9leGFtcGxlLmNvbS9qYW5lLWJpbmc=">Jane Bing contact</a></h2><p>Jane Bing is the owner of Bing Salon. Email jane@bing.example.</p></li>';
      }
    };
  };
  try {
    const scrapling = {
      available() { return true; },
      async enrich({ url }) {
        if (String(url).includes("jane-bing")) searchPageFetches += 1;
        return {
          ok: true,
          url,
          title: "Jane Bing contact",
          text: "Jane Bing is the owner of Bing Salon. Contact Jane Bing at jane@bing.example.",
          emails: ["jane@bing.example"],
          phones: [],
          links: [],
          socialLinks: []
        };
      }
    };
    const adapter = new OwnerRabbitHoleAdapter({
      scrapling,
      linkedinTools: { enrichProfile: scrapling.enrich },
      minimax: {
        configured() { return false; }
      }
    });
    const result = await adapter.findOwnerContact({
      lead: {
        name: "Bing Salon",
        city: "Austin, TX, USA",
        category: "salon",
        website: "https://example.com",
        ownerAgentSwarmSize: 2
      },
      initialEnrichment: { ok: true, text: "Jane Bing is the owner of Bing Salon.", links: [], socialLinks: [] },
      run: { input: { ownerAgentSwarmSize: 2 } }
    });
    assert.equal(calls.some((url) => url.includes("www.bing.com/search")), true);
    assert.equal(result.ownerName, "Jane Bing");
    assert.equal(result.ownerEmail, "jane@bing.example");
    assert.equal(result.ownerAgentSwarm.summary.searchHits > 0, true);
    assert.equal(result.ownerAgentSwarm.summary.pagesFetched > 0, true);
    assert.equal(searchPageFetches <= 2, true);
  } finally {
    global.fetch = previousFetch;
  }
}

async function testRelevantSearchSnippetSeedsOwnerIdentity() {
  const previousFetch = global.fetch;
  const calls = [];
  global.fetch = async (url) => {
    calls.push(String(url));
    return {
      ok: true,
      async text() {
        return '<a class="result__a" href="https://local.example.com/lv-nail-lounge-owner">LV Nail Lounge owner</a><a class="result__snippet">Ivy Pham owner of LV Nail Lounge in Austin shares her salon story.</a>';
      }
    };
  };
  try {
    const scrapling = {
      available() { return true; },
      async enrich({ url }) {
        return {
          ok: true,
          url,
          title: "LV Nail Lounge profile",
          text: "LV Nail Lounge Austin profile. Book appointments online.",
          emails: [],
          phones: [],
          links: [],
          socialLinks: []
        };
      }
    };
    const adapter = new OwnerRabbitHoleAdapter({
      scrapling,
      linkedinTools: { enrichProfile: scrapling.enrich },
      minimax: {
        configured() { return true; },
        async proposeOwnerRabbitHoleStrategy() {
          return {
            pagePaths: [],
            searchQueries: ['"LV Nail Lounge" owner Austin'],
            profileUrlHints: [],
            ownerNameHints: [],
            strategyReason: "test search snippet owner identity"
          };
        },
        async proposeOwnerAgentSwarm() {
          return { agents: [] };
        }
      }
    });
    const result = await adapter.findOwnerContact({
      lead: {
        name: "LV Nail Lounge",
        city: "Austin, TX, USA",
        category: "nail salon",
        website: "https://lvnailloungeaustin.com",
        ownerAgentSwarmSize: 1
      },
      initialEnrichment: { ok: true, text: "LV Nail Lounge Austin.", links: [], socialLinks: [] },
      run: { input: { ownerAgentSwarmSize: 1 } }
    });
    assert.equal(calls.some((url) => url.includes("duckduckgo.com")), true);
    assert.equal(result.ownerName, "Ivy Pham");
    assert.equal(result.ownerEmail, "");
    assert.equal(result.ownerAgentSwarm.traces.some((trace) => trace.ownerCandidates.includes("Ivy Pham")), true);
    assert.equal(result.rabbitHoleStatus, "owner-found-no-profile");
  } finally {
    global.fetch = previousFetch;
  }
}

async function testNoisyMapsBusinessNameUsesCanonicalOwnerSearch() {
  const previousFetch = global.fetch;
  const calls = [];
  global.fetch = async (url) => {
    calls.push(String(url));
    return {
      ok: true,
      async text() {
        return '<a class="result__a" href="https://local.example.com/nail-art-house-owner">Nail Art House owner</a><a class="result__snippet">Ivy Pham owner of Nail Art House in Austin shares her salon story.</a>';
      }
    };
  };
  try {
    const scrapling = {
      available() { return true; },
      async enrich({ url }) {
        return {
          ok: true,
          url,
          title: "Nail Art House profile",
          text: "Nail Art House Austin profile. Book appointments online.",
          emails: [],
          phones: [],
          links: [],
          socialLinks: []
        };
      }
    };
    const adapter = new OwnerRabbitHoleAdapter({
      scrapling,
      linkedinTools: { enrichProfile: scrapling.enrich },
      minimax: { configured() { return false; } }
    });
    const result = await adapter.findOwnerContact({
      lead: {
        name: "Nail Art House | Central Austin",
        city: "Austin, TX, USA",
        category: "nail salon",
        website: "https://nailarthouseatx.com",
        ownerAgentSwarmSize: 1
      },
      initialEnrichment: { ok: true, text: "Nail Art House Austin.", links: [], socialLinks: [] },
      run: { input: { ownerAgentSwarmSize: 1 } }
    });
    assert.equal(calls.some((url) => decodeURIComponent(url).includes('"Nail Art House" owner')), true);
    assert.equal(calls.some((url) => decodeURIComponent(url).includes('"Nail Art House | Central Austin" owner')), false);
    assert.equal(result.ownerName, "Ivy Pham");
    assert.equal(result.ownerEmail, "");
  } finally {
    global.fetch = previousFetch;
  }
}

async function testIdentitySwarmFetchesNonProfileOwnerPages() {
  const previousFetch = global.fetch;
  global.fetch = async () => ({
    ok: true,
    async text() {
      return '<a class="result__a" href="https://local.example.com/nail-art-house-about">Nail Art House about</a><a class="result__snippet">Official Nail Art House information and Austin studio details.</a>';
    }
  });
  try {
    let fetchedOwnerPage = false;
    const scrapling = {
      available() { return true; },
      async enrich({ url }) {
        if (String(url).includes("nail-art-house-about")) {
          fetchedOwnerPage = true;
          return {
            ok: true,
            url,
            title: "Nail Art House owner profile",
            text: "Nail Art House Austin studio. Jane Registry is the owner of Nail Art House.",
            emails: [],
            phones: [],
            links: [],
            socialLinks: []
          };
        }
        return {
          ok: true,
          url,
          title: "Nail Art House",
          text: "Nail Art House Austin studio.",
          emails: [],
          phones: [],
          links: [],
          socialLinks: []
        };
      }
    };
    const adapter = new OwnerRabbitHoleAdapter({
      scrapling,
      linkedinTools: { enrichProfile: scrapling.enrich },
      minimax: { configured() { return false; } }
    });
    const result = await adapter.findOwnerContact({
      lead: {
        name: "Nail Art House | Central Austin",
        city: "Austin, TX, USA",
        category: "nail salon",
        website: "https://nailarthouseatx.com",
        ownerAgentSwarmSize: 1
      },
      initialEnrichment: { ok: true, text: "Nail Art House Austin.", links: [], socialLinks: [] },
      run: { input: { ownerAgentSwarmSize: 1 } }
    });
    assert.equal(fetchedOwnerPage, true);
    assert.equal(result.ownerName, "Jane Registry");
    assert.equal(result.ownerAgentSwarm.summary.pagesFetched > 0, true);
  } finally {
    global.fetch = previousFetch;
  }
}

async function testFallbackEmailPivotFindsOwnerThenPromotesBusinessEmail() {
  const previousFetch = global.fetch;
  const calls = [];
  global.fetch = async (url) => {
    calls.push(String(url));
    const text = decodeURIComponent(String(url));
    return {
      ok: true,
      async text() {
        if (/ivypham2012@yahoo\.com|ivypham2012/i.test(text)) {
          return '<a class="result__a" href="https://local.example.com/lv-nail-lounge-owner">Ivy Pham LV Nail Lounge</a><a class="result__snippet">Ivy Pham owner of LV Nail Lounge in Austin shares her salon story.</a>';
        }
        return "";
      }
    };
  };
  try {
    const scrapling = {
      available() { return true; },
      async enrich({ url }) {
        return {
          ok: true,
          url,
          title: "LV Nail Lounge profile",
          text: "LV Nail Lounge Austin profile. Book appointments online.",
          emails: [],
          phones: [],
          links: [],
          socialLinks: []
        };
      }
    };
    const adapter = new OwnerRabbitHoleAdapter({
      scrapling,
      linkedinTools: { enrichProfile: scrapling.enrich },
      minimax: { configured() { return false; } }
    });
    const result = await adapter.findOwnerContact({
      lead: {
        name: "LV Nail Lounge",
        city: "Austin, TX, USA",
        category: "nail salon",
        website: "https://lvnailloungeaustin.com",
        ownerAgentSwarmSize: 12
      },
      initialEnrichment: {
        ok: true,
        text: "LV Nail Lounge in Austin, TX. SEND MESSAGE ivypham2012@yahoo.com.",
        emails: ["ivypham2012@yahoo.com"],
        phones: [],
        links: [],
        socialLinks: []
      },
      run: { input: { ownerAgentSwarmSize: 12 } }
    });
    assert.equal(calls.some((url) => decodeURIComponent(url).includes("ivypham2012@yahoo.com")), true);
    assert.equal(result.ownerName, "Ivy Pham");
    assert.equal(result.ownerEmail, "ivypham2012@yahoo.com");
    assert.equal(result.businessEmail, "");
    assert.equal(result.ownerContactEvidence.some((entry) => entry.source === "known-owner-business-page"), true);
  } finally {
    global.fetch = previousFetch;
  }
}

async function testSeedEmailProbeFindsOwnerBeforeFullSwarm() {
  const previousFetch = global.fetch;
  const calls = [];
  let identitySwarmOwner = "";
  global.fetch = async (url) => {
    calls.push(String(url));
    const text = decodeURIComponent(String(url));
    return {
      ok: true,
      async text() {
        if (/ivypham2012@yahoo\.com|ivypham2012/i.test(text)) {
          return '<a class="result__a" href="https://local.example.com/lv-nail-lounge-owner">Ivy Pham LV Nail Lounge</a><a class="result__snippet">Ivy Pham owner of LV Nail Lounge in Austin shares her salon story.</a>';
        }
        return "";
      }
    };
  };
  try {
    const scrapling = {
      available() { return true; },
      async enrich({ url }) {
        return {
          ok: true,
          url,
          title: "LV Nail Lounge profile",
          text: "LV Nail Lounge Austin profile. Book appointments online.",
          emails: [],
          phones: [],
          links: [],
          socialLinks: []
        };
      }
    };
    const adapter = new OwnerRabbitHoleAdapter({
      scrapling,
      linkedinTools: { enrichProfile: scrapling.enrich },
      minimax: {
        configured() { return true; },
        async proposeOwnerRabbitHoleStrategy() {
          return {
            pagePaths: [],
            searchQueries: ['"LV Nail Lounge" owner Austin'],
            profileUrlHints: [],
            ownerNameHints: [],
            strategyReason: "test seed email focused probe"
          };
        },
        async proposeOwnerAgentSwarm({ phase, owner }) {
          if (phase === "identity-swarm") identitySwarmOwner = owner?.name || "";
          return { agents: [] };
        }
      }
    });
    const result = await adapter.findOwnerContact({
      lead: {
        name: "LV Nail Lounge",
        city: "Austin, TX, USA",
        category: "nail salon",
        website: "https://lvnailloungeaustin.com",
        ownerAgentSwarmSize: 1
      },
      initialEnrichment: {
        ok: true,
        text: "LV Nail Lounge in Austin, TX. SEND MESSAGE ivypham2012@yahoo.com.",
        emails: ["ivypham2012@yahoo.com"],
        phones: [],
        links: [],
        socialLinks: []
      },
      run: { input: { ownerAgentSwarmSize: 1 } }
    });
    assert.equal(calls.some((url) => decodeURIComponent(url).includes("ivypham2012@yahoo.com")), true);
    assert.equal(identitySwarmOwner, "Ivy Pham");
    assert.equal(result.ownerName, "Ivy Pham");
    assert.equal(result.ownerEmail, "ivypham2012@yahoo.com");
    assert.equal(result.ownerAgentSwarm.summary.byPhase["seed-email-identity-probe"].agents, 1);
    assert.equal(result.ownerContactEvidence.some((entry) => entry.source === "known-owner-business-page"), true);
  } finally {
    global.fetch = previousFetch;
  }
}

async function testGenericBusinessEmailDoesNotBecomeSearchPivot() {
  const previousFetch = global.fetch;
  const calls = [];
  global.fetch = async (url) => {
    calls.push(String(url));
    return {
      ok: true,
      async text() {
        return "";
      }
    };
  };
  try {
    const scrapling = {
      available() { return true; },
      async enrich({ url }) {
        return {
          ok: true,
          url,
          title: "Generic Salon",
          text: "Generic Salon Austin booking page.",
          emails: [],
          phones: [],
          links: [],
          socialLinks: []
        };
      }
    };
    const adapter = new OwnerRabbitHoleAdapter({
      scrapling,
      linkedinTools: { enrichProfile: scrapling.enrich },
      minimax: { configured() { return false; } }
    });
    const result = await adapter.findOwnerContact({
      lead: {
        name: "Generic Salon",
        city: "Austin, TX, USA",
        category: "salon",
        website: "https://generic.example",
        ownerAgentSwarmSize: 1
      },
      initialEnrichment: {
        ok: true,
        text: "Generic Salon. Contact info@generic.example or editorhp2019@gmail.com for appointments.",
        emails: ["info@generic.example", "editorhp2019@gmail.com"],
        links: [],
        socialLinks: []
      },
      run: { input: { ownerAgentSwarmSize: 1 } }
    });
    assert.equal(calls.some((url) => /info%40generic|info@generic|editorhp2019/i.test(url)), false);
    assert.equal(result.ownerName, "");
    assert.equal(result.ownerEmail, "");
    assert.equal(result.businessEmail, "info@generic.example");
  } finally {
    global.fetch = previousFetch;
  }
}

async function testOwnerProfilesRejectSingleTokenOwnerNoise() {
  const previousFetch = global.fetch;
  global.fetch = async () => ({
    ok: true,
    async text() {
      return "";
    }
  });
  try {
    const scrapling = {
      available() { return true; },
      async enrich({ url }) {
        return {
          ok: true,
          url,
          title: String(url).includes("facebook.com") ? "Facebook search" : "Generic profile",
          text: "",
          emails: [],
          phones: [],
          links: [],
          socialLinks: []
        };
      }
    };
    const adapter = new OwnerRabbitHoleAdapter({
      scrapling,
      linkedinTools: { enrichProfile: scrapling.enrich },
      minimax: {
        configured() { return true; },
        async proposeOwnerRabbitHoleStrategy() {
          return {
            pagePaths: [],
            searchQueries: [],
            profileUrlHints: [
              "https://www.facebook.com/search/top?q=Ivy%20Pham%20LV%20Nail%20Lounge",
              "https://www.youtube.com/@Pham",
              "https://www.thespruce.com/types-of-ivy-6979655",
              "https://www.britannica.com/plant/ivy-plant"
            ],
            ownerNameHints: ["Ivy Pham"],
            strategyReason: "test profile URL filtering"
          };
        },
        async proposeOwnerAgentSwarm() {
          return { agents: [] };
        }
      }
    });
    const result = await adapter.findOwnerContact({
      lead: {
        name: "LV Nail Lounge",
        city: "Austin, TX, USA",
        category: "nail salon",
        website: "https://lvnailloungeaustin.com",
        ownerAgentSwarmSize: 1
      },
      initialEnrichment: {
        ok: true,
        text: "LV Nail Lounge in Austin, TX. SEND MESSAGE ivypham2012@yahoo.com.",
        emails: ["ivypham2012@yahoo.com"],
        phones: [],
        links: [],
        socialLinks: []
      },
      run: { input: { ownerAgentSwarmSize: 1 } }
    });
    assert.equal(result.ownerName, "Ivy Pham");
    assert.equal(result.ownerSocialProfiles.includes("https://www.facebook.com/search/top?q=Ivy%20Pham%20LV%20Nail%20Lounge"), true);
    assert.equal(result.ownerSocialProfiles.some((url) => /youtube\.com\/@Pham|types-of-ivy|ivy-plant/i.test(url)), false);
  } finally {
    global.fetch = previousFetch;
  }
}

async function testSearchSnippetOwnerPromotesMatchingBusinessEmailAfterIdentity() {
  const previousFetch = global.fetch;
  global.fetch = async () => ({
    ok: true,
    async text() {
      return '<a class="result__a" href="https://local.example.com/lv-nail-lounge-owner">LV Nail Lounge owner</a><a class="result__snippet">Ivy Pham owner of LV Nail Lounge in Austin shares her salon story.</a>';
    }
  });
  try {
    const scrapling = {
      available() { return true; },
      async enrich({ url }) {
        return {
          ok: true,
          url,
          title: "LV Nail Lounge profile",
          text: "LV Nail Lounge Austin profile. Book appointments online.",
          emails: [],
          phones: [],
          links: [],
          socialLinks: []
        };
      }
    };
    const adapter = new OwnerRabbitHoleAdapter({
      scrapling,
      linkedinTools: { enrichProfile: scrapling.enrich },
      minimax: {
        configured() { return true; },
        async proposeOwnerRabbitHoleStrategy() {
          return {
            pagePaths: [],
            searchQueries: ['"LV Nail Lounge" owner Austin'],
            profileUrlHints: [],
            ownerNameHints: [],
            strategyReason: "test search snippet owner identity then business email"
          };
        },
        async proposeOwnerAgentSwarm() {
          return { agents: [] };
        }
      }
    });
    const result = await adapter.findOwnerContact({
      lead: {
        name: "LV Nail Lounge",
        city: "Austin, TX, USA",
        category: "nail salon",
        website: "https://lvnailloungeaustin.com",
        ownerAgentSwarmSize: 1
      },
      initialEnrichment: {
        ok: true,
        text: "LV Nail Lounge in Austin, TX. PHONE NUMBER 512-383-8702 SEND MESSAGE ivypham2012@yahoo.com.",
        emails: ["ivypham2012@yahoo.com"],
        phones: ["512-383-8702"],
        links: [],
        socialLinks: []
      },
      run: { input: { ownerAgentSwarmSize: 1 } }
    });
    assert.equal(result.ownerName, "Ivy Pham");
    assert.equal(result.ownerEmail, "ivypham2012@yahoo.com");
    assert.equal(result.businessEmail, "");
    assert.equal(result.ownerContactEvidence.some((entry) => entry.source === "known-owner-business-page"), true);
  } finally {
    global.fetch = previousFetch;
  }
}

async function testIrrelevantLinkedinResultDoesNotBecomeOwnerProfile() {
  const previousFetch = global.fetch;
  global.fetch = async (url) => {
    if (String(url).includes("duckduckgo.com")) throw new Error("duckduckgo unavailable");
    return {
      ok: true,
      async text() {
        return '<li class="b_algo"><h2><a href="https://www.linkedin.com/in/sitpune">SIT Pune investors</a></h2><p>Investors Kunal Shah CRED and unrelated startup details.</p></li>';
      }
    };
  };
  try {
    const scrapling = {
      available() { return true; },
      async enrich({ url }) {
        if (String(url).includes("sitpune")) {
          return {
            ok: true,
            url,
            title: "SIT Pune investors",
            text: "Investors Kunal Shah CRED and unrelated startup details.",
            emails: [],
            phones: [],
            links: [],
            socialLinks: []
          };
        }
        return {
          ok: true,
          url,
          title: "Sit Still Kids Mesa",
          text: "Sit Still Kids Mesa is a children salon. Book appointments online.",
          emails: [],
          phones: [],
          links: [],
          socialLinks: ["https://www.instagram.com/sitstillkidsmesa"]
        };
      }
    };
    const adapter = new OwnerRabbitHoleAdapter({
      scrapling,
      linkedinTools: { enrichProfile: scrapling.enrich },
      minimax: { configured() { return false; } }
    });
    const result = await adapter.findOwnerContact({
      lead: {
        name: "Sit Still Kids - Mesa",
        city: "Austin, TX, USA",
        category: "children salon",
        website: "https://mesa.example.com",
        ownerAgentSwarmSize: 2
      },
      initialEnrichment: { ok: true, text: "Sit Still Kids Mesa is a children salon.", links: [], socialLinks: ["https://www.instagram.com/sitstillkidsmesa"] },
      run: { input: { ownerAgentSwarmSize: 2 } }
    });
    assert.equal(result.ownerName, "");
    assert.equal(result.ownerSocialProfiles.includes("https://www.linkedin.com/in/sitpune"), false);
    assert.notEqual(result.rabbitHoleStatus, "owner-profile-found-no-contact");
  } finally {
    global.fetch = previousFetch;
  }
}

async function testSingleTokenBusinessNameRejectsIrrelevantOwnerCandidate() {
  const previousFetch = global.fetch;
  global.fetch = async (url) => {
    if (String(url).includes("duckduckgo.com")) throw new Error("duckduckgo unavailable");
    return {
      ok: true,
      async text() {
        return '<li class="b_algo"><h2><a href="https://www.netflix.com/tudum/articles/legends-release-date-cast-news">Legends Global release</a></h2><p>Legends Global executives and streaming release news.</p></li>';
      }
    };
  };
  try {
    const scrapling = {
      available() { return true; },
      async enrich({ url }) {
        if (String(url).includes("netflix.com")) {
          return {
            ok: true,
            url,
            title: "Legends Global release",
            text: "Legends Global is a streaming title. Executive Producer Alex Series discusses the launch.",
            emails: [],
            phones: [],
            links: [],
            socialLinks: []
          };
        }
        return {
          ok: true,
          url,
          title: "Legends Salon",
          text: "Legends Salon is a Round Rock salon with stylists and online booking.",
          emails: [],
          phones: [],
          links: [],
          socialLinks: ["https://www.instagram.com/legendssalonroundrock"]
        };
      }
    };
    const adapter = new OwnerRabbitHoleAdapter({
      scrapling,
      linkedinTools: { enrichProfile: scrapling.enrich },
      minimax: { configured() { return false; } }
    });
    const result = await adapter.findOwnerContact({
      lead: {
        name: "Legends Salon",
        city: "Austin, TX, USA",
        category: "salon",
        website: "https://legendssalonroundrock.example",
        ownerAgentSwarmSize: 2
      },
      initialEnrichment: { ok: true, text: "Legends Salon is a Round Rock salon.", links: [], socialLinks: ["https://www.instagram.com/legendssalonroundrock"] },
      run: { input: { ownerAgentSwarmSize: 2 } }
    });
    assert.equal(result.ownerName, "");
    assert.equal(result.ownerSocialProfiles.some((url) => String(url).includes("netflix.com")), false);
  } finally {
    global.fetch = previousFetch;
  }
}

async function testIrrelevantSearchSnippetDoesNotSeedOwnerIdentity() {
  const previousFetch = global.fetch;
  global.fetch = async () => ({
    ok: true,
    async text() {
      return '<a class="result__a" href="https://www.netflix.com/tudum/articles/legends-global-executive">Legends Global executive</a><a class="result__snippet">Daniel Levy is CEO of Legends Global and discusses media rights.</a>';
    }
  });
  try {
    const scrapling = {
      available() { return true; },
      async enrich({ url }) {
        if (String(url).includes("legendssalon.example")) {
          return {
            ok: true,
            url,
            title: "Legends Salon",
            text: "Legends Salon Mesa services and booking information.",
            emails: [],
            phones: [],
            links: [],
            socialLinks: []
          };
        }
        return {
          ok: true,
          url,
          title: "Legends Global executive",
          text: "Daniel Levy is CEO of Legends Global and discusses media rights.",
          emails: [],
          phones: [],
          links: [],
          socialLinks: []
        };
      }
    };
    const adapter = new OwnerRabbitHoleAdapter({
      scrapling,
      linkedinTools: { enrichProfile: scrapling.enrich },
      minimax: {
        configured() { return true; },
        async proposeOwnerRabbitHoleStrategy() {
          return {
            pagePaths: [],
            searchQueries: ['"Legends Salon" owner Mesa'],
            profileUrlHints: [],
            ownerNameHints: [],
            strategyReason: "test irrelevant search snippet rejection"
          };
        },
        async proposeOwnerAgentSwarm() {
          return { agents: [] };
        }
      }
    });
    const result = await adapter.findOwnerContact({
      lead: {
        name: "Legends Salon",
        city: "Mesa, AZ, USA",
        category: "salon",
        website: "https://legendssalon.example",
        ownerAgentSwarmSize: 1
      },
      initialEnrichment: { ok: true, text: "Legends Salon Mesa.", links: [], socialLinks: [] },
      run: { input: { ownerAgentSwarmSize: 1 } }
    });
    assert.equal(result.ownerName, "");
    assert.equal(result.ownerEvidence.some((entry) => entry.source.endsWith(":search-snippet")), false);
  } finally {
    global.fetch = previousFetch;
  }
}

async function testNetflixDoesNotMatchXDotComSocialEvidence() {
  const previousFetch = global.fetch;
  global.fetch = async () => ({
    ok: true,
    async text() {
      return '<a class="result__a" href="https://www.netflix.com/tudum/articles/legends-global-executive">Legends Global executive</a><a class="result__snippet">Daniel Levy is CEO of Legends Global and discusses media rights.</a>';
    }
  });
  try {
    const scrapling = {
      available() { return true; },
      async enrich({ url }) {
        return {
          ok: true,
          url,
          title: "Boundary Salon",
          text: "Boundary Salon Austin booking page.",
          emails: [],
          phones: [],
          links: [],
          socialLinks: []
        };
      }
    };
    const adapter = new OwnerRabbitHoleAdapter({
      scrapling,
      linkedinTools: { enrichProfile: scrapling.enrich },
      minimax: {
        configured() { return true; },
        async proposeOwnerRabbitHoleStrategy() {
          return {
            pagePaths: [],
            searchQueries: ['"Boundary Salon" owner Austin'],
            profileUrlHints: [],
            ownerNameHints: [],
            strategyReason: "test x.com domain boundary"
          };
        },
        async proposeOwnerAgentSwarm() {
          return { agents: [] };
        }
      }
    });
    const result = await adapter.findOwnerContact({
      lead: {
        name: "Boundary Salon",
        city: "Austin, TX, USA",
        category: "salon",
        website: "https://boundary.example",
        ownerAgentSwarmSize: 1
      },
      initialEnrichment: { ok: true, text: "Boundary Salon Austin.", links: [], socialLinks: [] },
      run: { input: { ownerAgentSwarmSize: 1 } }
    });
    const allSocial = result.ownerEvidence.flatMap((entry) => entry.socialLinks || []);
    assert.equal(allSocial.some((url) => url.includes("netflix.com")), false);
    assert.equal(result.ownerSocialProfiles.some((url) => url.includes("netflix.com")), false);
  } finally {
    global.fetch = previousFetch;
  }
}

async function testLinkedinScraperBridgeMergesProfileContacts() {
  const tmp = await mkdtemp(path.join(os.tmpdir(), "hermes-linkedin-bridge-"));
  const bridgePath = path.join(tmp, "mock_linkedin_bridge.py");
  await writeFile(bridgePath, [
    "import json, sys",
    "json.loads(sys.stdin.read() or '{}')",
    "print(json.dumps({'ok': True, 'emails': ['owner@linkedin.example'], 'phones': ['+1 555 555 0101'], 'text': 'LinkedIn profile contact', 'profile': {'name': 'Owner LinkedIn'}}))"
  ].join("\n"));

  const previous = {
    script: process.env.HERMES_LINKEDIN_SCRAPER_SCRIPT,
    python: process.env.HERMES_LINKEDIN_PYTHON,
    session: process.env.HERMES_LINKEDIN_SESSION_JSON,
    cookie: process.env.HERMES_LINKEDIN_LI_AT
  };
  process.env.HERMES_LINKEDIN_SCRAPER_SCRIPT = bridgePath;
  process.env.HERMES_LINKEDIN_PYTHON = "python3";
  process.env.HERMES_LINKEDIN_SESSION_JSON = path.join(tmp, "session.json");
  delete process.env.HERMES_LINKEDIN_LI_AT;
  try {
    const adapter = new LinkedInToolsAdapter({
      rootDir: process.cwd(),
      scrapling: {
        available() { return true; },
        async enrich() {
          return { ok: true, emails: [], phones: [], links: [], text: "public profile text" };
        }
      }
    });
    const result = await adapter.enrichProfile({ url: "https://www.linkedin.com/in/owner" });
    const attempt = result.toolAttempts.find((entry) => entry.tool === "joeyism/linkedin_scraper");
    assert.equal(attempt.status, "used");
    assert.equal(result.emails.includes("owner@linkedin.example"), true);
    assert.equal(result.phones.includes("+1 555 555 0101"), true);
  } finally {
    restoreEnv("HERMES_LINKEDIN_SCRAPER_SCRIPT", previous.script);
    restoreEnv("HERMES_LINKEDIN_PYTHON", previous.python);
    restoreEnv("HERMES_LINKEDIN_SESSION_JSON", previous.session);
    restoreEnv("HERMES_LINKEDIN_LI_AT", previous.cookie);
  }
}

async function testLinkedinJobToolBridgesRecordAttempts() {
  const tmp = await mkdtemp(path.join(os.tmpdir(), "hermes-linkedin-jobs-"));
  const jobSpyPath = path.join(tmp, "mock_jobspy.py");
  const pyJobsPath = path.join(tmp, "mock_py_jobs.py");
  const mockScript = [
    "import json, sys",
    "json.loads(sys.stdin.read() or '{}')",
    "print(json.dumps({'ok': True, 'jobs': [{'title': 'Owner role', 'company': 'Bridge Salon'}], 'emails': [], 'phones': [], 'text': 'job corroboration'}))"
  ].join("\n");
  await writeFile(jobSpyPath, mockScript);
  await writeFile(pyJobsPath, mockScript);

  const previous = {
    python: process.env.HERMES_LINKEDIN_PYTHON,
    jobSpy: process.env.HERMES_JOBSPY_LINKEDIN_SCRIPT,
    pyJobs: process.env.HERMES_PY_LINKEDIN_JOBS_SCRIPT,
    enablePyJobs: process.env.HERMES_ENABLE_PY_LINKEDIN_JOBS,
    session: process.env.HERMES_LINKEDIN_SESSION_JSON,
    cookie: process.env.HERMES_LINKEDIN_LI_AT
  };
  process.env.HERMES_LINKEDIN_PYTHON = "python3";
  process.env.HERMES_JOBSPY_LINKEDIN_SCRIPT = jobSpyPath;
  process.env.HERMES_PY_LINKEDIN_JOBS_SCRIPT = pyJobsPath;
  process.env.HERMES_ENABLE_PY_LINKEDIN_JOBS = "1";
  delete process.env.HERMES_LINKEDIN_SESSION_JSON;
  delete process.env.HERMES_LINKEDIN_LI_AT;
  try {
    const adapter = new LinkedInToolsAdapter({
      rootDir: process.cwd(),
      scrapling: {
        available() { return true; },
        async enrich() {
          return { ok: true, emails: [], phones: [], links: [], text: "public profile text" };
        }
      }
    });
    const result = await adapter.enrichProfile({
      url: "https://www.linkedin.com/in/owner",
      lead: { name: "Bridge Salon", city: "Austin, TX, USA" },
      owner: { name: "Owner Bridge" }
    });
    const jobSpy = result.toolAttempts.find((entry) => entry.tool === "speedyapply/JobSpy");
    const pyJobs = result.toolAttempts.find((entry) => entry.tool === "spinlud/py-linkedin-jobs-scraper");
    assert.equal(jobSpy.status, "used");
    assert.equal(pyJobs.status, "used");
    assert.equal(result.linkedinJobEvidence.jobSpy.length, 1);
    assert.equal(result.linkedinJobEvidence.pyLinkedinJobs.length, 1);
  } finally {
    restoreEnv("HERMES_LINKEDIN_PYTHON", previous.python);
    restoreEnv("HERMES_JOBSPY_LINKEDIN_SCRIPT", previous.jobSpy);
    restoreEnv("HERMES_PY_LINKEDIN_JOBS_SCRIPT", previous.pyJobs);
    restoreEnv("HERMES_ENABLE_PY_LINKEDIN_JOBS", previous.enablePyJobs);
    restoreEnv("HERMES_LINKEDIN_SESSION_JSON", previous.session);
    restoreEnv("HERMES_LINKEDIN_LI_AT", previous.cookie);
  }
}

function restoreEnv(key, value) {
  if (value === undefined) delete process.env[key];
  else process.env[key] = value;
}

const tests = [
  testDefaultRunUsesBusinessContactWorkflow,
  testMemoryReuse,
  testMemoryDoesNotSearchScraperLogs,
  testMapsFailureClassification,
  testDirectoryOfficialSiteFallbackUnblocksSeedDiscovery,
  testOwnerAssociatedFallbackQualifiesNamedOwnerWithBusinessContact,
  testOwnerAssociatedFallbackRejectsBadNameEmailAndPhone,
  testOwnerAssociatedFallbackDropsGenericEmailWhenPhoneIsClean,
  testOwnerAssociatedFallbackRejectsWeakV4OwnerNameAndSuspiciousEmail,
  testOwnerAssociatedFallbackRejectsWeakBusinessMatch,
  testDirectoryDiscoveryUsesScraplingForProtectedSources,
  testDirectoryBusinessPhoneDoesNotBecomeOwnerDirectContact,
  testInitialStrategyPlansBeforeTemplates,
  testLegacyOwnerSourceTermsNeverReachMapsSearch,
  testPlannerProviderFallbackIsRecorded,
  testAdaptiveSearch,
  testQueryDiversityRejectsRepeatedKeywords,
  testEnrichmentFailureRecovery,
  testDynamicEnrichmentRepairBeforeRejecting,
  testLocationExpansion,
  testPortfolioDefaultsUseBroadLocationCapacity,
  testAllUsaInputExpandsNationwidePlan,
  testStagnationPivotsInsteadOfRepeating,
  testFinalRecoveryBeforePartial,
  testFinalRecoverySearchesWhileOwnerBacklogRuns,
  testDiscoveryContinuesAfterOwnerDrainWhenTargetUnmet,
  testSelfReviewWritesMemory,
  testResumeSavedRun,
  testRecoverInterruptedRun,
  testStartRunKillsPreviousRecoverableRun,
  testRecoverInterruptedRunsOnlyLatestQuery,
  testMarkInactiveRunsInterrupted,
  testKilledRunCannotBeResurrectedByStalePersist,
  testProviderFallback,
  testMiniMaxTimeoutFallback,
  testMiniMaxSchedulerLimitsConcurrency,
  testMiniMaxFallbackQueriesStayOnNiche,
  testOwnerFirstDiscoveryBuildsDiverseRestaurantAngles,
  testOwnerFirstDiscoveryV4PrioritizesOwnerEvidenceSources,
  testPreflightBlocksMissingTool,
  testPreflightRepairsMissingTool,
  testPreflightPassesBeforeDiscovery,
  testFlowWatchdogRecordsRepairTrainingMetadata,
  testFlowWatchdogProviderTimeoutFallbackIsConcrete,
  testRepairPlaybookTrainsSafeCodingRepairs,
  testLeadWorkerConcurrency,
  testOwnerRabbitHoleTimeoutExhaustsLead,
  testScaleAuditProvesBacklogAndPlanExhaustion,
  testDiscoveryContinuesWhileOwnerBacklogRuns,
  testLocationExpansionContinuesWhileOwnerBacklogRuns,
  testPendingDiscoveredLeadsResumeOnce,
  testPendingDiscoveredLeadsPrioritizeLocalOwnerContactYield,
  testRejectedDiscoveredLeadsAreSkippedOnResume,
  testOwnerAgentMetricsRollIntoScaleAudit,
  testOwnerContactWithoutEvidenceIsRejected,
  testOwnerSwarmEscalatesPromisingLead,
  testOwnerSwarmDoesNotEscalateMapsPhoneOnlyLead,
  testMiniMaxPlansOwnerSwarmAgents,
  testOwnerDiscoveryPagesAreBudgetedBeforeSwarm,
  testOwnerSwarmSkipsMiniMaxWhenTimeoutCircuitOpen,
  testOwnerSwarmSkipsMiniMaxWhenTimeoutRateIsHigh,
  testOwnerSwarmSkipsMiniMaxWhenOwnerBudgetIsShort,
  testOwnerEvidenceToolPolicySkipsTimeoutHeavyToolOnly,
  testDirectOwnerContactSearchPromotesOwnerEmail,
  testFoundedByOwnerPhonePromotesFormattedPhone,
  testOwnerHintEmailLocalPartPromotesOwnerEmail,
  testOwnerHintDoesNotPromoteGenericBusinessEmail,
  testBusinessNameHintDoesNotPromoteOwnerContact,
  testBogusOwnerPhraseDoesNotCertifyBusinessPhone,
  testTravelMarketingPhraseDoesNotBecomeOwnerName,
  testBingFallbackSearchFeedsOwnerSwarm,
  testRelevantSearchSnippetSeedsOwnerIdentity,
  testNoisyMapsBusinessNameUsesCanonicalOwnerSearch,
  testIdentitySwarmFetchesNonProfileOwnerPages,
  testFallbackEmailPivotFindsOwnerThenPromotesBusinessEmail,
  testSeedEmailProbeFindsOwnerBeforeFullSwarm,
  testGenericBusinessEmailDoesNotBecomeSearchPivot,
  testOwnerProfilesRejectSingleTokenOwnerNoise,
  testSearchSnippetOwnerPromotesMatchingBusinessEmailAfterIdentity,
  testIrrelevantLinkedinResultDoesNotBecomeOwnerProfile,
  testSingleTokenBusinessNameRejectsIrrelevantOwnerCandidate,
  testIrrelevantSearchSnippetDoesNotSeedOwnerIdentity,
  testNetflixDoesNotMatchXDotComSocialEvidence,
  testLinkedinScraperBridgeMergesProfileContacts,
  testLinkedinJobToolBridgesRecordAttempts,
  testLeadCsvExport,
  testBusinessContactAppearsInPrimaryCsvColumns,
  testLeadExportFiltersHiddenAndUnapproved,
  testStorageSerializesConcurrentRunSaves,
  testStorageRepairsCorruptOperationalMemory
];

for (const test of tests) {
  await test();
  console.log(`ok ${test.name}`);
}
