fix(bot-mode): an empty member seat surfaces an error instead of swallowing the group send
Ported from the pre-TSX PR onto the current module layout. main's
group-chat-view.tsx already restores the composer draft when
sendToGroupChat returns null (feat(desktop): retain Bot group drafts by
room, b42d8279ed), so the draft-loss half of the original fix is
FIXED_ON_MAIN and not re-applied here. What remained: sendToGroupChat in
group-rounds.ts still folded "no text" and "no members" into one silent
`return null`, so a fully typed message into a room whose roster had not
hydrated (or a legacy room record without member descriptors) was
rejected with no thread, no log entry and no error.
The guards are split: empty content stays silent, an empty member seat
raises host.notify with a Bot Mode i18n string (en/ja/zh/zh-hant). The
wording no longer promises a retry will help (review: a legacy room with
no member descriptors never recovers by retrying) and points at the two
real remedies.
The original source-regex .mjs tests are dropped per review; the
contract is pinned by a behavioural vitest in group-rounds.test.ts that
drives sendToGroupChat through the scripted room harness: empty members
→ null + one error toast + no log entry; blank text → null, no toast.
This commit is contained in:
@@ -514,6 +514,19 @@ describe('threads', () => {
|
||||
expect(alphaMessages.some(message => message.content.includes('BETA_TOPIC'))).toBe(false)
|
||||
expect(betaMessages.some(message => message.content.includes('ALPHA_TOPIC'))).toBe(false)
|
||||
})
|
||||
|
||||
it('surfaces an empty member seat instead of swallowing the send; empty text stays silent', async () => {
|
||||
const room = await loadRoom()
|
||||
const notify = room.gateway.host.notify as ReturnType<typeof vi.fn>
|
||||
|
||||
expect(room.rounds.sendToGroupChat('Seating', [], 'anyone there?')).toBeNull()
|
||||
expect(log(room, 'Seating')).toHaveLength(0)
|
||||
expect(notify).toHaveBeenCalledTimes(1)
|
||||
expect(notify.mock.calls[0][0]).toMatchObject({ kind: 'error', message: expect.stringContaining('Seating') })
|
||||
|
||||
expect(room.rounds.sendToGroupChat('Seating', MEMBERS, ' ')).toBeNull()
|
||||
expect(notify).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('turn prompt', () => {
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { botFriendlyNames, botHandle, mentionNameForms } from './data'
|
||||
/**
|
||||
* Room-level coordination: who speaks, in what order, for how long — the
|
||||
* @mention parse, the round-robin driver, the #93129 member holds, the stop
|
||||
* path, and the user send that starts it all.
|
||||
*/
|
||||
import { host } from '@hermes/plugin-sdk'
|
||||
|
||||
import { botFriendlyNames, botHandle, mentionNameForms } from './data'
|
||||
import { recordGroupActivity } from './group-activity'
|
||||
import {
|
||||
$groupChats,
|
||||
@@ -28,6 +30,7 @@ import {
|
||||
import { runGroupContinuationMembers, runGroupRoundMember } from './group-round-members'
|
||||
import { rejectGroupSlashCommand } from './group-slash'
|
||||
import { GROUP_TURN_HARD_CAP_MS, harvestStrandedGroupReply } from './group-turns'
|
||||
import { botsText } from './i18n'
|
||||
import { requestForBot } from './routing'
|
||||
import type { Attachment, GroupMember, GroupMessage } from './types'
|
||||
|
||||
@@ -685,7 +688,20 @@ export function sendToGroupChat(
|
||||
|
||||
const attached = Array.isArray(images) ? images.filter((img: Attachment) => img && img.data) : []
|
||||
|
||||
if ((!trimmed && !attached.length) || !members.length) {
|
||||
if (!trimmed && !attached.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
// An empty member seat (roster hydration race, meta clobber, legacy room
|
||||
// record without member descriptors) used to swallow the send: a fully
|
||||
// typed message vanished with no thread and no error. Surface it — the
|
||||
// caller keeps the draft, so nothing is lost.
|
||||
if (!members.length) {
|
||||
host.notify({
|
||||
kind: 'error',
|
||||
message: botsText().group.noMembersToSend(group)
|
||||
})
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
@@ -191,6 +191,7 @@ type BotsMessages = {
|
||||
heldMembersStatus: (members: string) => string
|
||||
holdReleaseHint: string
|
||||
needsYourInput: string
|
||||
noMembersToSend: (group: string) => string
|
||||
pictureGenerationFailed: string
|
||||
nameTaken: (name: string) => string
|
||||
memberCount: (count: number) => string
|
||||
@@ -424,6 +425,7 @@ const en: BotsMessages = {
|
||||
heldMembersStatus: members => `Paused: ${members}`,
|
||||
holdReleaseHint: 'Mention a paused bot or send @all resume to release them.',
|
||||
needsYourInput: 'A bot in this group chat needs your input',
|
||||
noMembersToSend: group => `${group} has no members to send to — add a bot, or reopen the room if members are still loading.`,
|
||||
pictureGenerationFailed: 'Group picture generation failed',
|
||||
nameTaken: name => `A group named “${name}” already exists.`,
|
||||
memberCount: count => `${count} bots`,
|
||||
@@ -650,6 +652,7 @@ const ja: BotsMessages = {
|
||||
heldMembersStatus: members => `一時停止中: ${members}`,
|
||||
holdReleaseHint: '一時停止中のボットにメンションするか、@all resume を送信して再開します。',
|
||||
needsYourInput: 'このグループチャットのボットが入力を待っています',
|
||||
noMembersToSend: group => `${group} に送信先のメンバーがいません。ボットを追加するか、メンバーの読み込み中であればルームを開き直してください。`,
|
||||
pictureGenerationFailed: 'グループ画像の生成に失敗しました',
|
||||
nameTaken: name => `「${name}」という名前のグループはすでに存在します。`,
|
||||
memberCount: count => `ボット${count}体`,
|
||||
@@ -871,6 +874,7 @@ const zh: BotsMessages = {
|
||||
heldMembersStatus: members => `已暂停:${members}`,
|
||||
holdReleaseHint: '提及已暂停的机器人,或发送 @all resume 以恢复它们。',
|
||||
needsYourInput: '此群聊中有机器人需要你输入',
|
||||
noMembersToSend: group => `${group} 没有可发送的成员——请添加机器人,如果成员仍在加载,请重新打开该群聊。`,
|
||||
pictureGenerationFailed: '群组图片生成失败',
|
||||
nameTaken: name => `已存在名为“${name}”的群聊。`,
|
||||
memberCount: count => `${count} 个机器人`,
|
||||
@@ -1092,6 +1096,7 @@ const zhHant: BotsMessages = {
|
||||
heldMembersStatus: members => `已暫停:${members}`,
|
||||
holdReleaseHint: '提及已暫停的機器人,或傳送 @all resume 以恢復它們。',
|
||||
needsYourInput: '此群組聊天中有機器人需要您的輸入',
|
||||
noMembersToSend: group => `${group} 沒有可傳送的成員——請新增機器人,若成員仍在載入中,請重新開啟該群組聊天。`,
|
||||
pictureGenerationFailed: '群組圖片產生失敗',
|
||||
nameTaken: name => `已存在名為「${name}」的群組聊天。`,
|
||||
memberCount: count => `${count} 個機器人`,
|
||||
|
||||
Reference in New Issue
Block a user