// Runs without Slack or a real API key: a tiny local server stands in for Influship. import assert from 'node:assert/strict'; import { createServer } from 'node:http'; import { after, before, test } from 'node:test'; const creator = (n: number) => ({ creator: { id: `00000000-0000-4000-8000-00000000000${n % 10}`, name: `Creator ${n}`, avatar_url: null, bio: null }, location_unverified: null, match: { score: 0.9 - n * 0.01, confidence: 0.8, low_confidence: false, reasons: [`Reason for creator ${n} & symbols`], evidence: [ { text: `Races trail ultras (${n})`, provenance: 'post_evidence', evidence_quote: 'I took on a 100 miler last weekend', fact_id: null, source_post_id: 'p1' }, ], }, primary_profile: { id: 'p', platform: 'instagram', username: `creator${n}`, url: `https://www.instagram.com/creator${n}/`, followers: 18_326 + n, engagement_rate: 5.15, is_verified: false, data_updated_at: null }, relevant_profile: null, }); const all = Array.from({ length: 12 }, (_, n) => creator(n + 1)); let server: ReturnType; let bot: typeof import('../src/influship.js'); let blocks: typeof import('../src/blocks.js'); before(async () => { server = createServer((req, res) => { const url = new URL(req.url ?? '/', 'http://localhost'); res.setHeader('content-type', 'application/json'); if (req.method === 'POST' && url.pathname === '/v1/search') { res.setHeader('x-credits-charged', '65'); res.end(JSON.stringify({ data: all, search_id: 'search-1', total: all.length, has_more: false, next_cursor: null, quality: { mode: 'reranked', reason: null } })); } else if (url.pathname === '/v1/search/search-1') { const offset = Number(url.searchParams.get('cursor') ?? 0); const limit = Number(url.searchParams.get('limit')); const data = all.slice(offset, offset + limit); const more = offset + limit < all.length; res.end(JSON.stringify({ data, has_more: more, next_cursor: more ? String(offset + limit) : null })); } else if (url.pathname === '/v1/creators/lookalike') { res.setHeader('x-credits-charged', '7.5'); res.end(JSON.stringify({ data: all.slice(0, 5).map((c) => ({ creator: c.creator, primary_profile: c.primary_profile, similarity: { score: 0.81, shared_traits: ['trail running', 'gear reviews', 'ultras', 'extra'] } })), has_more: false, next_cursor: null })); } else { res.statusCode = 404; res.end(JSON.stringify({ error: { code: 'not_found', message: url.pathname } })); } }); await new Promise((resolve) => server.listen(0, resolve)); const address = server.address(); process.env.INFLUSHIP_BASE_URL = `http://localhost:${typeof address === 'object' && address ? address.port : 0}`; process.env.INFLUSHIP_API_KEY = 'test'; bot = await import('../src/influship.js'); blocks = await import('../src/blocks.js'); }); after(() => server.close()); function assertValidMessage(message: { blocks: unknown[] }) { assert.ok(message.blocks.length <= 50, 'Slack allows at most 50 blocks'); for (const block of message.blocks as Array>) { if (block.type === 'section') assert.ok(block.text.text.length <= 3000, 'section text over 3000 chars'); if (block.type === 'header') assert.ok(block.text.text.length <= 150, 'header over 150 chars'); const buttons = [block.accessory, ...(block.elements ?? [])].filter((e) => e?.type === 'button'); for (const button of buttons) assert.ok(button.value.length <= 2000, 'button value over 2000 chars'); } } test('parses a follower range out of the brief', () => { assert.deepEqual(bot.parseCommand('trail runners who review shoes, 10k-100k followers'), { query: 'trail runners who review shoes', followers: { min: 10_000, max: 100_000 }, }); assert.deepEqual(bot.parseCommand('home cooks 50k to 1.5m followers').followers, { min: 50_000, max: 1_500_000 }); assert.deepEqual(bot.parseCommand('vegan bakers in Leeds'), { query: 'vegan bakers in Leeds' }); }); test('search shows the first page and records the real cost', async () => { const page = await bot.searchCreators('trail runners who review shoes'); assert.equal(page.creators.length, 5); assert.equal(page.credits, 65); assert.equal(page.nextCursor, '5'); const message = blocks.resultsMessage(page, 'U123'); assertValidMessage(message); const text = JSON.stringify(message.blocks); assert.match(text, /Showing 1–5 of 12/); assert.match(text, /<with> & symbols/); assert.match(text, /I took on a 100 miler/); assert.match(text, /next_page/); }); test('Next 5 reads the stored search and stops on the last page', async () => { const second = await bot.readPage('search-1', '5'); assert.equal(second.creators.length, 5); const third = await bot.readPage('search-1', '10'); assert.equal(third.creators.length, 2); assert.equal(third.nextCursor, null); const message = blocks.resultsMessage({ ...third, query: 'q', total: 12, offset: 10, credits: 0, unranked: false }); assertValidMessage(message); assert.doesNotMatch(JSON.stringify(message.blocks), /next_page/); assert.match(JSON.stringify(message.blocks), /Showing 11–12 of 12/); }); test('Similar posts lookalikes with shared traits', async () => { const { creators, credits } = await bot.findSimilar(all[0].creator.id); assert.equal(credits, 7.5); const message = blocks.similarMessage('Creator 1', creators, credits); assertValidMessage(message); assert.match(JSON.stringify(message.blocks), />trail running/); assert.doesNotMatch(JSON.stringify(message.blocks), /gear reviews/); }); test('the daily cap blocks spend past the limit', () => { assert.equal(bot.canSpend('T1', 400), true); bot.recordSpend('T1', 450); assert.equal(bot.canSpend('T1', 65), false); assert.equal(bot.canSpend('T2', 65), true); });