support-bot: add tests for Grok batch dedup and initial response gating

7 new tests covering the duplicate Grok reply fix:
- batch dedup: only last customer message per group triggers API call
- batch dedup: multi-group batches handled independently
- batch dedup: non-customer messages filtered from batch
- initial response gating: per-message responses suppressed during activateGrok
- gating clears: per-message responses resume after activation completes

Update implementation plan test catalog (122 → 129 tests).
This commit is contained in:
Narasimha-sc
2026-04-16 10:55:28 +00:00
parent 1f1777cbe1
commit 6d69cf9a7c
2 changed files with 137 additions and 4 deletions
+128
View File
@@ -700,6 +700,73 @@ describe("Grok Conversation", () => {
await bot.onGrokNewChatItems(grokEvt)
expect(grokApi.calls.length).toBe(0)
})
test("batch: multiple customer messages in one event → only last triggers Grok API call", async () => {
chat.groups.set(GROK_LOCAL_GROUP_ID, makeGroupInfo(GROK_LOCAL_GROUP_ID))
addCustomerMessageToHistory("First question", GROK_LOCAL_GROUP_ID)
addCustomerMessageToHistory("Second question", GROK_LOCAL_GROUP_ID)
const ci1 = makeChatItem({dir: "groupRcv", text: "First question", memberId: CUSTOMER_ID})
const ci2 = makeChatItem({dir: "groupRcv", text: "Second question", memberId: CUSTOMER_ID})
const evt = {
type: "newChatItems" as const,
user: makeUser(GROK_USER_ID),
chatItems: [
{chatInfo: {type: "group", groupInfo: makeGroupInfo(GROK_LOCAL_GROUP_ID)}, chatItem: ci1},
{chatInfo: {type: "group", groupInfo: makeGroupInfo(GROK_LOCAL_GROUP_ID)}, chatItem: ci2},
],
}
await bot.onGrokNewChatItems(evt)
expect(grokApi.calls.length).toBe(1)
expect(grokApi.calls[0].message).toBe("Second question")
})
test("batch: messages from different groups → each group gets one response", async () => {
const GROK_GROUP_A = 201
const GROK_GROUP_B = 202
chat.groups.set(GROK_GROUP_A, makeGroupInfo(GROK_GROUP_A))
chat.groups.set(GROK_GROUP_B, makeGroupInfo(GROK_GROUP_B))
addCustomerMessageToHistory("Question A", GROK_GROUP_A)
addCustomerMessageToHistory("Question B", GROK_GROUP_B)
const ciA = makeChatItem({dir: "groupRcv", text: "Question A", memberId: CUSTOMER_ID})
const ciB = makeChatItem({dir: "groupRcv", text: "Question B", memberId: CUSTOMER_ID})
const evt = {
type: "newChatItems" as const,
user: makeUser(GROK_USER_ID),
chatItems: [
{chatInfo: {type: "group", groupInfo: makeGroupInfo(GROK_GROUP_A)}, chatItem: ciA},
{chatInfo: {type: "group", groupInfo: makeGroupInfo(GROK_GROUP_B)}, chatItem: ciB},
],
}
await bot.onGrokNewChatItems(evt)
expect(grokApi.calls.length).toBe(2)
})
test("batch: non-customer messages filtered, only customer messages trigger response", async () => {
chat.groups.set(GROK_LOCAL_GROUP_ID, makeGroupInfo(GROK_LOCAL_GROUP_ID))
addCustomerMessageToHistory("Customer question", GROK_LOCAL_GROUP_ID)
const custCi = makeChatItem({dir: "groupRcv", text: "Customer question", memberId: CUSTOMER_ID})
const teamCi = makeChatItem({dir: "groupRcv", text: "Team reply", memberId: "not-customer", memberContactId: TEAM_MEMBER_1_ID})
const evt = {
type: "newChatItems" as const,
user: makeUser(GROK_USER_ID),
chatItems: [
{chatInfo: {type: "group", groupInfo: makeGroupInfo(GROK_LOCAL_GROUP_ID)}, chatItem: custCi},
{chatInfo: {type: "group", groupInfo: makeGroupInfo(GROK_LOCAL_GROUP_ID)}, chatItem: teamCi},
],
}
await bot.onGrokNewChatItems(evt)
expect(grokApi.calls.length).toBe(1)
expect(grokApi.calls[0].message).toBe("Customer question")
})
})
describe("/team Activation", () => {
@@ -1424,6 +1491,67 @@ describe("Grok Join Flow", () => {
expect(chat.joined).toContain(GROK_LOCAL_GROUP_ID)
expectSentToGroup(CUSTOMER_GROUP_ID, "now chatting with Grok")
})
test("per-message responses suppressed during activateGrok initial response", async () => {
await reachQueue()
addBotMessage("The team can see your message")
// Customer's message visible in Grok's view (activateGrok reads it for initial response)
addCustomerMessageToHistory("Hello, I need help", GROK_LOCAL_GROUP_ID)
chat.groups.set(GROK_LOCAL_GROUP_ID, makeGroupInfo(GROK_LOCAL_GROUP_ID))
// Start /grok activation (fireAndForget)
const botPromise = bot.onNewChatItems(customerMessage("/grok"))
// Wait for apiAddMember to complete
await new Promise(r => setTimeout(r, 10))
// Simulate Grok invitation → sets grokGroupMap/reverseGrokMap
const memberId = `member-${GROK_CONTACT_ID}`
await bot.onGrokGroupInvitation({
type: "receivedGroupInvitation",
user: makeUser(GROK_USER_ID),
groupInfo: {...makeGroupInfo(GROK_LOCAL_GROUP_ID), membership: {memberId}},
contact: {contactId: 99},
fromMemberRole: GroupMemberRole.Admin,
memberRole: GroupMemberRole.Member,
})
// grokInitialResponsePending is set, reverseGrokMap is set.
// Simulate per-message event (as if message backlog arrived for Grok profile)
await bot.onGrokNewChatItems(grokViewCustomerMessage("Hello, I need help"))
// Gating: per-message handler must NOT have called Grok API
expect(grokApi.calls.length).toBe(0)
// Now complete the join → activateGrok sends initial combined response
await bot.onGrokMemberConnected({
type: "connectedToGroupMember",
user: makeUser(GROK_USER_ID),
groupInfo: makeGroupInfo(GROK_LOCAL_GROUP_ID),
member: {memberId: "bot-in-grok-view", groupMemberId: 9999, memberContactId: undefined},
})
await botPromise
await bot.flush()
// Only 1 Grok API call: the initial combined response from activateGrok
expect(grokApi.calls.length).toBe(1)
expect(grokApi.calls[0].message).toContain("Hello, I need help")
})
test("per-message responses resume after activateGrok completes", async () => {
await reachGrok()
await bot.flush()
const callsAfterActivation = grokApi.calls.length
// Send a new customer message via Grok's view — should be processed normally
addCustomerMessageToHistory("Follow-up question", GROK_LOCAL_GROUP_ID)
await bot.onGrokNewChatItems(grokViewCustomerMessage("Follow-up question"))
expect(grokApi.calls.length).toBe(callsAfterActivation + 1)
expect(grokApi.calls[grokApi.calls.length - 1].message).toBe("Follow-up question")
})
})
describe("Grok No-History Fallback", () => {
@@ -965,7 +965,7 @@ async function reachTeam(groupId?) // reachTeamPending → add team membe
Called as: `const p = simulateGrokJoinSuccess(); await bot.onNewChatItems(...); await p;`
### 20.4 Test Catalog (122 tests, 27 suites)
### 20.4 Test Catalog (129 tests, 27 suites)
#### 1. Welcome & First Message (4 tests)
- first message → queue reply + card created with /join command
@@ -980,13 +980,16 @@ Called as: `const p = simulateGrokJoinSuccess(); await bot.onNewChatItems(...);
- /grok when grokContactId is null → grokUnavailableMessage
- /grok as first message + Grok join fails → queue message sent as fallback
#### 3. Grok Conversation (6 tests)
#### 3. Grok Conversation (10 tests)
- Grok per-message: reads history, calls API, sends response
- customer non-text → no Grok API call
- Grok API error → grokErrorMessage sent
- Grok ignores bot commands from customer
- Grok ignores non-customer messages
- Grok ignores own messages (groupSnd)
- batch: multiple customer messages in one event → only last triggers Grok API call
- batch: messages from different groups → each group gets one response
- batch: non-customer messages filtered, only customer messages trigger response
#### 4. /team Activation (4 tests)
- /team from QUEUE → ALL team members added, teamAddedMessage sent
@@ -1079,10 +1082,12 @@ Called as: `const p = simulateGrokJoinSuccess(); await bot.onNewChatItems(...);
- own messages (groupSnd) → ignored
- non-business group messages → ignored
#### 19. Grok Join Flow (3 tests)
#### 19. Grok Join Flow (5 tests)
- receivedGroupInvitation → apiJoinGroup called (full async flow)
- unmatched Grok invitation → buffered (not joined until activateGrok drains)
- buffered invitation drained after pendingGrokJoins set → apiJoinGroup called
- per-message responses suppressed during activateGrok initial response (grokInitialResponsePending gate)
- per-message responses resume after activateGrok completes
#### 20. Grok No-History Fallback (1 test)
- Grok joins but sees no customer messages → grokNoHistoryMessage
@@ -1152,7 +1157,7 @@ Called as: `const p = simulateGrokJoinSuccess(); await bot.onNewChatItems(...);
### 20.6 Test Coverage Notes
**Covered vs plan catalog:**
- §20.4 items 1-13, 15, 17-27 fully covered (122 tests across 27 suites)
- §20.4 items 1-13, 15, 17-27 fully covered (129 tests across 27 suites)
- §20.4 item 14 (Weekend Detection) — not unit-tested; `isWeekend` depends on `Intl.DateTimeFormat(new Date())`, would need clock mocking
- §20.4 item 16 (Profile Mutex) — not unit-tested; mutex serialization is verified implicitly through all other tests (MockChatApi tracks activeUserId)
- §20.4 item 19 (Startup & State Persistence) — not unit-tested; tests `index.ts` startup which requires native ChatApi. Integration test only. This includes `deleteInviteLink` (profileMutex + `apiSetActiveUser` before `apiDeleteGroupLink`), the conditional `apiUpdateGroupProfile` (compare `fullGroupPreferences` before calling), and the best-effort `apiCreateGroupLink` (catch + log on SMP relay failure) — all are in startup code and cannot be covered by MockChatApi-based tests.