feat: 精确会话自动化规则

This commit is contained in:
Wxw-Gu
2026-09-21 11:06:59 +08:00
parent 95384a4a92
commit 069d836dea
25 changed files with 1710 additions and 122 deletions
@@ -0,0 +1,118 @@
import { fireEvent, render, screen } from '@testing-library/react'
import { describe, expect, it } from 'vitest'
import { RuleEditorPanel } from '../../src/renderer/src/features/automation/RuleEditorPanel'
import {
createDefaultDailyReportRule,
type AutomationRule,
type AutomationRuleDraft
} from '../../src/shared/automation'
/**
* 「回复前等待」是**规则自己的一项执行参数**,配置位置就是
* 「编辑自动化 → 3 · 触发后执行(命中条件后,TraceMemo 会按下面的顺序依次执行)」。
*
* 这一层要锁的是**接线**:初值必须来自规则本身、保存必须带回去。
* 因为 `draftFromRule` 是逐字段搬运的,漏一个字段就会被 `normalizeRuleDraft`
* 的默认值悄悄覆盖(把用户设成 0 的等待重置回 2 秒),这种 bug 只有真渲染才发现。
*/
const GROUPS = [{ id: '12345678@chatroom', name: '测试群' }]
function renderEditor(
rule: AutomationRule,
onSave: (draft: AutomationRuleDraft) => void = () => {}
): void {
render(
<RuleEditorPanel
mode="edit"
rule={rule}
groups={GROUPS}
saving={false}
onCancel={() => {}}
onSave={onSave}
/>
)
}
const delayField = (): HTMLInputElement =>
screen.getByLabelText('回复前等待秒数') as HTMLInputElement
describe('RuleEditorPanel · 回复前等待', () => {
it('它就在「3 · 触发后执行」里,和动作顺序在同一段', () => {
renderEditor(createDefaultDailyReportRule(0))
expect(screen.getByText('命中条件后,TraceMemo 会按下面的顺序依次执行。')).toBeInTheDocument()
expect(delayField()).toBeInTheDocument()
expect(screen.getByText('回复前等待(秒)')).toBeInTheDocument()
})
it('初值来自规则本身,不是硬编码的默认值', () => {
const rule = createDefaultDailyReportRule(0)
rule.replyDelaySeconds = 7
renderEditor(rule)
expect(delayField()).toHaveValue(7)
})
it('保存时把改动带回去', () => {
const saved: AutomationRuleDraft[] = []
renderEditor(createDefaultDailyReportRule(0), (draft) => saved.push(draft))
fireEvent.change(delayField(), { target: { value: '0' } })
fireEvent.click(screen.getByRole('button', { name: '保存' }))
expect(saved).toHaveLength(1)
expect(saved[0].replyDelaySeconds).toBe(0)
})
it('用户设成 0(立刻回复)时,保存不会把它顶回默认 2 秒', () => {
const saved: AutomationRuleDraft[] = []
const rule = createDefaultDailyReportRule(0)
rule.replyDelaySeconds = 0
renderEditor(rule, (draft) => saved.push(draft))
// 什么都不改,直接保存:0 必须原样保留(而不是被默认的 2 顶掉)。
fireEvent.click(screen.getByRole('button', { name: '保存' }))
expect(saved[0].replyDelaySeconds).toBe(0)
})
})
/**
* 「所有群聊」的边界提示。
*
* 真实能力是:同一时刻多个会话同时来消息时可能漏掉少量触发。
* 这条限制**只挂在「所有群聊」这个上下文里** —— 首页不挂、指定群聊时不挂,
* 而且只讲用户能理解的结果,不解释表事件 / 会话回读这类实现细节。
*/
const LIMITATION_TEXT = '当前版本在多个会话同时收到消息时,极少数自动化触发可能遗漏。'
function renderScopeEditor(conversationIds: string[]): void {
const rule = createDefaultDailyReportRule(0)
rule.conditions.conversationIds = conversationIds
renderEditor(rule)
}
describe('RuleEditorPanel · 生效范围的边界提示', () => {
it('一个群都没选(所有群聊)时显示提示', () => {
renderScopeEditor([])
// 编辑页与右侧效果预览都会显示范围描述,所以这里用 getAllByText。
expect(screen.getAllByText('所有群聊').length).toBeGreaterThan(0)
expect(screen.getAllByText(LIMITATION_TEXT)).toHaveLength(1)
})
it('选了具体群聊时不显示提示', () => {
renderScopeEditor([GROUPS[0].id])
expect(screen.getAllByText('已选 1 个群').length).toBeGreaterThan(0)
expect(screen.queryAllByText(LIMITATION_TEXT)).toHaveLength(0)
})
it('提示里不出现底层实现词,也不做绝对承诺', () => {
renderScopeEditor([])
expect(LIMITATION_TEXT).not.toMatch(/WCDB|SessionTable|表变化|native|回读/)
expect(document.body.textContent).not.toMatch(/覆盖所有群聊|100%|不会遗漏/)
})
})
@@ -0,0 +1,44 @@
import { render, screen } from '@testing-library/react'
import { describe, expect, it } from 'vitest'
import { ToastProvider } from '../../src/renderer/src/components/ui'
import { AutomationWorkspace } from '../../src/renderer/src/features/automation/AutomationWorkspace'
/**
* 首页状态卡只讲用户能理解的状态。
*
* 以前这里挂着一句「当前仅能捕获最近活跃的会话」,还用 Tooltip 解释了
* 「微信底层只会上报有表发生了变化」——那是开发报告的内容,不该出现在普通用户界面。
* 真实边界改挂在「规则编辑页 → 所有群聊」那个具体上下文里。
*/
describe('Automation 首页状态卡', () => {
function renderWorkspace(): void {
render(
<ToastProvider>
<AutomationWorkspace dbReady={false} />
</ToastProvider>
)
}
it('仍然如实展示「消息监听」与发送能力两项', () => {
renderWorkspace()
expect(screen.getByText('消息监听')).toBeInTheDocument()
expect(screen.getByText('发送能力')).toBeInTheDocument()
})
it('不再出现「最近活跃的会话」这类实现说明', () => {
renderWorkspace()
expect(screen.queryByText(/最近活跃的会话/)).toBeNull()
expect(screen.queryByText(/有表发生了变化/)).toBeNull()
})
it('首页不出现底层实现词', () => {
renderWorkspace()
expect(document.body.textContent).not.toMatch(
/WCDB|SessionTable|表变化|native event|回读机制|coalesce/
)
})
})