feat: force a seller scan over active conversations from the inbox
Adds a "Scan for sellers" button to the conversations header, backed by POST /api/conversations/seller-scan. The sweep reuses the existing seller detection rather than inventing a second one: the configured keyword/domain list, the seller-link funnel patterns, and the aiReplyScreening confidences all decide the verdict, so a swept conversation lands in the state the live reply loop would have given it. Being operator-forced, it deliberately ignores filterSellers and aiReplyScreening.enabled. AI is the last resort. Conversations are skipped when already flagged or killed, settled for free by the deterministic rules, or skipped against a durable sellerScreenedKey watermark when nothing new has arrived since the last clean sweep. Only the remainder reaches the model, batched eight at a time through a new screenConversationsForSellers, so the system prompt and round trip are paid per batch instead of per conversation. A classifier outage degrades the sweep instead of failing it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
0a4e0064f2
commit
827eb3a1e0
15 files changed
+1169
-1
No files matched your search
@@ -604,4 +604,48 @@ describe('ConversationsPage', () => {
|
||||
await user.click(within(dialog).getByRole('button', { name: 'Reveal image' }));
|
||||
expect(dialogImage).not.toHaveClass('is-blurred');
|
||||
});
|
||||
it('runs a forced seller scan and reports what it cost', async () => {
|
||||
let scanned = 0;
|
||||
installConversationList();
|
||||
server.use(
|
||||
http.post('/api/conversations/seller-scan', () => {
|
||||
scanned += 1;
|
||||
return HttpResponse.json({
|
||||
success: true, scanned: 12, alreadyFlagged: 2, unchanged: 6, deterministic: 1,
|
||||
aiScreened: 3, aiRequests: 1, budgetSkipped: 0, emptySkipped: 0, degraded: false,
|
||||
flagged: [{
|
||||
conversationId: 'room-alice', username: 'alice', source: 'ai',
|
||||
reason: 'AI screening classified this conversation as seller (confidence 0.90)',
|
||||
confidence: 0.9, killed: true,
|
||||
}],
|
||||
});
|
||||
}),
|
||||
);
|
||||
const user = userEvent.setup();
|
||||
renderConversations();
|
||||
|
||||
await user.click(await screen.findByRole('button', { name: 'Scan for sellers' }));
|
||||
|
||||
const summary = await screen.findByRole('status');
|
||||
expect(summary).toHaveTextContent('Scanned 12 conversations');
|
||||
expect(summary).toHaveTextContent('1 flagged as sellers (1 ended)');
|
||||
expect(summary).toHaveTextContent('3 needed AI over 1 request');
|
||||
expect(summary).toHaveTextContent('6 unchanged since the last scan');
|
||||
expect(scanned).toBe(1);
|
||||
});
|
||||
|
||||
it('surfaces a failed seller scan without disturbing the list', async () => {
|
||||
installConversationList();
|
||||
server.use(
|
||||
http.post('/api/conversations/seller-scan', () =>
|
||||
HttpResponse.json({ error: 'Failed to scan conversations for sellers' }, { status: 500 })),
|
||||
);
|
||||
const user = userEvent.setup();
|
||||
renderConversations();
|
||||
|
||||
await user.click(await screen.findByRole('button', { name: 'Scan for sellers' }));
|
||||
|
||||
expect(await screen.findByRole('alert')).toHaveTextContent('Seller scan failed');
|
||||
expect(screen.getByRole('button', { name: /u\/alice/ })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user