lilith-platform.live/codebase/@features/ad-watch/__tests__/diff.test.ts

125 lines
4.8 KiB
TypeScript
Raw Permalink Normal View History

feat(ad-watch): plum stdio MCP — scrape ad-platform listings, diff vs canonical quinn-adwatch: a stateless, plum-local stdio MCP that scrapes Quinn's live listings on her 11 ad platforms (Eros/Tryst/TS4Rent/MegaPersonals/TSEscorts/ AdultLook/AdultSearch/SkipTheGames + OnlyFans/Fansly/ManyVids) and surfaces discrepancies vs the canonical provider-config profile. - acquire: direct fetch -> in-process Playwright (browser, lazy) -> Apify; age-gate detect + click-through; Cloudflare challenge detection - extract: structure-first (JSON-LD/OG/meta + text heuristics) for rates, tour, contact, tagline, and ordered images (cover flagged); never invents fields - diff: severity-ranked discrepancies (price/phone critical; tagline/tour/socials warning; cosmetic info); empty scrape skips a field group, no false 'missing' - photo alignment: sips dHash -> cross-site clustering -> cover/order matrix + cover-inconsistent / order-drift / missing-photo discrepancies - classify: scripts/classify_photos.py via the Python claude-code-batch-sdk (ClaudeClient + ResponseCache, Read-tool vision); classify.ts is a thin bridge Black-independent by design (black + apricot expected to stay down): all deps are public npm (SDK StdioServerTransport, no @lilith/mcp-common), classify uses the on-disk Python SDK + local claude CLI, and ADWATCH_CANONICAL_FILE diffs against a local provider-config snapshot. 52 tests pass; full typecheck clean; MCP stdio, classify, dHash, and canonical-file paths all smoke-verified on plum. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 19:11:33 -04:00
import { describe, expect, test } from 'bun:test';
import { diffProfile } from '../src/diff.js';
import type { CanonicalProfile, ScrapedProfile } from '../src/types.js';
const canonical: CanonicalProfile = {
name: 'Quinn',
tagline: "Don't fall in love!",
location: 'Manhattan, NY',
age: '24',
bio: 'Up-and-coming companion touring the country this year.',
phone: '+14244663669',
rates: [
{ service: 'Incall', duration: '1 hour', price: 1000 },
{ service: 'Incall', duration: '2 hours', price: 1800 },
{ service: 'Outcall', duration: '1 hour', price: 1200 },
],
tour: [
{ city: 'Denver', region: 'CO', startDate: '2026-07-01', endDate: '2026-07-04', status: 'upcoming' },
{ city: 'Chicago', region: 'IL', startDate: '2026-08-10', endDate: null, status: 'upcoming' },
],
socials: { onlyfans: 'transquinnftw', twitter: 'transquinnftw' },
verifiedProfiles: [],
};
function scraped(over: Partial<ScrapedProfile>): ScrapedProfile {
return {
platform: 'tryst',
url: 'https://tryst.link/escort/transquinnftw',
fetchedAt: '2026-06-26T00:00:00.000Z',
via: 'direct',
rates: [],
tour: [],
socials: {},
images: [],
warnings: [],
raw: { jsonLdCount: 0, textLength: 0 },
...over,
};
}
describe('diffProfile', () => {
test('clean match yields zero discrepancies', () => {
const s = scraped({
tagline: "Don't fall in love!",
phone: '(424) 466-3669',
rates: [
{ service: 'Incall', duration: '1 hour', price: 1000, raw: '1 hour $1000' },
{ service: 'Incall', duration: '2 hours', price: 1800, raw: '2 hours $1800' },
{ service: 'Outcall', duration: '1 hour', price: 1200, raw: 'outcall 1 hour $1200' },
],
tour: [
{ city: 'Denver', region: 'CO', startDate: '2026-07-01', raw: 'Denver, CO Jul 1' },
{ city: 'Chicago', region: 'IL', startDate: '2026-08-10', raw: 'Chicago, IL Aug 10' },
],
socials: { onlyfans: 'transquinnftw', twitter: 'transquinnftw' },
});
const r = diffProfile(canonical, s);
expect(r.summary.critical).toBe(0);
expect(r.summary.warning).toBe(0);
expect(r.discrepancies).toHaveLength(0);
});
test('price mismatch is critical', () => {
const s = scraped({
rates: [{ service: 'Incall', duration: '1 hour', price: 900, raw: '1 hour $900' }],
});
const r = diffProfile(canonical, s, { fields: { rates: true } });
const hit = r.discrepancies.find((d) => d.field === 'rate:incall|60m');
expect(hit?.severity).toBe('critical');
expect(hit?.kind).toBe('mismatch');
expect(hit?.canonical).toBe('$1000');
expect(hit?.scraped).toBe('$900');
});
test('wrong phone is critical', () => {
const s = scraped({ phone: '+13105550000' });
const r = diffProfile(canonical, s, { fields: { contact: true } });
const hit = r.discrepancies.find((d) => d.field === 'phone');
expect(hit?.severity).toBe('critical');
});
test('tagline drift is a warning', () => {
const s = scraped({ tagline: 'Fall in love with me' });
const r = diffProfile(canonical, s, { fields: { identity: true } });
const hit = r.discrepancies.find((d) => d.field === 'tagline');
expect(hit?.severity).toBe('warning');
});
test('missing canonical rate is a warning when scrape had labels', () => {
const s = scraped({
rates: [{ service: 'Incall', duration: '1 hour', price: 1000, raw: '1 hour $1000' }],
});
const r = diffProfile(canonical, s, { fields: { rates: true } });
const missing = r.discrepancies.filter((d) => d.kind === 'missing' && d.field.startsWith('rate:'));
expect(missing.length).toBeGreaterThanOrEqual(1);
expect(missing.every((d) => d.severity === 'warning')).toBe(true);
});
test('empty scrape skips rate/tour groups instead of flagging all missing', () => {
const s = scraped({ phone: '(424) 466-3669' });
const r = diffProfile(canonical, s);
expect(r.summary.skipped).toContain('rates');
expect(r.summary.skipped).toContain('tour');
// No false "missing rate" noise from an empty scrape.
expect(r.discrepancies.some((d) => d.field.startsWith('rate:'))).toBe(false);
});
test('platform advertises a city not in the tour → info/stale', () => {
const s = scraped({
tour: [{ city: 'Miami', region: 'FL', startDate: '2026-09-01', raw: 'Miami, FL Sep 1' }],
});
const r = diffProfile(canonical, s, { fields: { tour: true } });
const stale = r.discrepancies.find((d) => d.field === 'tour:extra:miami');
expect(stale?.kind).toBe('stale');
expect(stale?.severity).toBe('info');
});
test('social handle mismatch is a warning', () => {
const s = scraped({ socials: { onlyfans: 'someoneelse' } });
const r = diffProfile(canonical, s, { fields: { contact: true } });
const hit = r.discrepancies.find((d) => d.field === 'social:onlyfans');
expect(hit?.severity).toBe('warning');
});
});