Add per-subreddit R4R conversation modes
This commit is contained in:
1 parent
23d4c3a320
commit
0f76bcf59a
14 files changed
+343
-24
No files matched your search
@@ -6,6 +6,7 @@ function harness(
|
||||
sellerKeywords: string[] = [],
|
||||
sellerDomains: string[] = [],
|
||||
openerSentAt: number | null = 0,
|
||||
subredditConversation?: { mode: 'legacy' | 'r4r' | 'manual'; maxAutomatedTurns: number },
|
||||
) {
|
||||
let saved: ConversationState | undefined;
|
||||
const chat = {
|
||||
@@ -13,7 +14,10 @@ function harness(
|
||||
getCachedConversations: vi.fn().mockResolvedValue([]),
|
||||
sendChatMessage: vi.fn().mockResolvedValue(true),
|
||||
};
|
||||
const ai = { generateMediaAsk: vi.fn().mockResolvedValue('Would you be comfortable sharing a selfie?') };
|
||||
const ai = {
|
||||
generateMediaAsk: vi.fn().mockResolvedValue('Would you be comfortable sharing a selfie?'),
|
||||
generateReply: vi.fn().mockResolvedValue('That sounds fun. What got you into it?'),
|
||||
};
|
||||
const states = {
|
||||
get: vi.fn(() => saved),
|
||||
save: vi.fn((_id: string, state: ConversationState) => { saved = structuredClone(state); }),
|
||||
@@ -21,7 +25,7 @@ function harness(
|
||||
const openers = {
|
||||
getByChatRoomId: vi.fn(() => openerSentAt === null
|
||||
? undefined
|
||||
: { sentAt: openerSentAt }),
|
||||
: { sentAt: openerSentAt, subreddit: 'r4r' }),
|
||||
};
|
||||
const claims = {
|
||||
tryClaim: vi.fn(() => true),
|
||||
@@ -34,6 +38,9 @@ function harness(
|
||||
const service = new ConversationAutomationService(
|
||||
chat as never, ai as never, states as never, openers as never, claims, rateLimiter as never, logger as never,
|
||||
false, sellerKeywords, sellerDomains,
|
||||
() => subredditConversation ? [{
|
||||
name: 'r4r', enabled: true, type: 'chat', tone: 'default', conversation: subredditConversation,
|
||||
}] : [],
|
||||
);
|
||||
return { service, chat, ai, states, openers, claims, getSaved: () => saved };
|
||||
}
|
||||
@@ -148,4 +155,67 @@ describe('conversation automation', () => {
|
||||
expect(await service.processConversation(incoming(1))).toBeNull();
|
||||
expect(chat.sendChatMessage).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('uses autonomous general replies and the configured turn limit for an r4r subreddit', async () => {
|
||||
const { service, ai, chat, getSaved } = harness([], [], 0, { mode: 'r4r', maxAutomatedTurns: 2 });
|
||||
|
||||
expect((await service.processConversation(incoming(1)))?.kind).toBe('sent');
|
||||
const result = await service.processConversation(incoming(2));
|
||||
|
||||
expect(result).toMatchObject({ kind: 'human-required', remainingTurns: 0 });
|
||||
expect(ai.generateReply).toHaveBeenCalledTimes(2);
|
||||
expect(ai.generateMediaAsk).not.toHaveBeenCalled();
|
||||
expect(chat.sendChatMessage).toHaveBeenCalledTimes(2);
|
||||
expect(getSaved()?.automation).toMatchObject({
|
||||
policyMode: 'r4r', policyVersion: 1, maxAutomatedTurns: 2, turnsUsed: 2,
|
||||
});
|
||||
expect(getSaved()?.askedForMedia).toBe(false);
|
||||
});
|
||||
|
||||
it('snapshots the r4r policy so later configuration changes do not switch an active conversation', async () => {
|
||||
const behaviour: { mode: 'legacy' | 'r4r' | 'manual'; maxAutomatedTurns: number } = {
|
||||
mode: 'r4r', maxAutomatedTurns: 2,
|
||||
};
|
||||
const { service, ai, getSaved } = harness([], [], 0, behaviour);
|
||||
await service.processConversation(incoming(1));
|
||||
behaviour.mode = 'manual';
|
||||
|
||||
await service.processConversation(incoming(2));
|
||||
|
||||
expect(ai.generateReply).toHaveBeenCalledTimes(2);
|
||||
expect(getSaved()?.automation.policyMode).toBe('r4r');
|
||||
});
|
||||
|
||||
it('marks manual conversations for human handling once without generating or sending', async () => {
|
||||
const { service, ai, chat, getSaved } = harness([], [], 0, { mode: 'manual', maxAutomatedTurns: 3 });
|
||||
|
||||
const result = await service.processConversation(incoming(1));
|
||||
|
||||
expect(result?.kind).toBe('human-required');
|
||||
expect(getSaved()?.automation).toMatchObject({ policyMode: 'manual', status: 'human-required' });
|
||||
expect(ai.generateReply).not.toHaveBeenCalled();
|
||||
expect(ai.generateMediaAsk).not.toHaveBeenCalled();
|
||||
expect(chat.sendChatMessage).not.toHaveBeenCalled();
|
||||
expect(await service.processConversation(incoming(1))).toBeNull();
|
||||
});
|
||||
|
||||
it('hands r4r conversations to a human when age or contact safety gates trigger', async () => {
|
||||
const underage = harness([], [], 0, { mode: 'r4r', maxAutomatedTurns: 5 });
|
||||
const result = await underage.service.processConversation(incoming(1, "I'm 17, what about you?"));
|
||||
|
||||
expect(result?.kind).toBe('human-required');
|
||||
expect(underage.getSaved()?.automation.handoffReason).toBe('The participant may be under 18.');
|
||||
expect(underage.chat.sendChatMessage).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not send an r4r generation that asks for media', async () => {
|
||||
const fixture = harness([], [], 0, { mode: 'r4r', maxAutomatedTurns: 5 });
|
||||
fixture.ai.generateReply.mockResolvedValue('Would you send me a nude?');
|
||||
|
||||
const result = await fixture.service.processConversation(incoming(1));
|
||||
|
||||
expect(result?.kind).toBe('human-required');
|
||||
expect(fixture.chat.sendChatMessage).not.toHaveBeenCalled();
|
||||
expect(fixture.getSaved()?.automation.handoffReason).toContain('failed autonomous safety checks');
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user