| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252 |
- import { Message } from './api/models';
- import {
- GROUP_QUIET_MS,
- GroupEntry,
- blockDuration,
- buildGroupTurns,
- } from './group-turns';
- const T0 = Date.UTC(2026, 10, 5, 10, 0, 0);
- function msg(id: string, sender: 'me' | 'agent', ts: number, text = ''): Message {
- return {
- id,
- conversationId: sender === 'me' ? 'group' : 'a',
- sender,
- kind: 'text',
- text,
- ts: new Date(ts).toISOString(),
- state: 'sent',
- };
- }
- /** 用户消息条目 */
- function mine(turnId: string, ts: number, text = '派活'): GroupEntry {
- return {
- id: turnId,
- turnId,
- fromAgentId: null,
- message: msg(turnId, 'me', ts, text),
- at: ts,
- };
- }
- /** 成员回复条目 */
- function reply(turnId: string, agentId: string, id: string, ts: number, text = ''): GroupEntry {
- return {
- id,
- turnId,
- fromAgentId: agentId,
- message: msg(id, 'agent', ts, text),
- at: ts,
- };
- }
- /** 构造输入:targets 一律返回给定成员 */
- function input(
- entries: GroupEntry[],
- targets: string[],
- typing: Record<string, boolean> = {},
- now = T0 + 60_000,
- ) {
- return {
- entries,
- targetsOf: () => targets,
- typingOf: (id: string) => typing[id] ?? false,
- now,
- };
- }
- describe('group-turns', () => {
- describe('buildGroupTurns', () => {
- it('两个成员各自回复 → 同一回合内两块,且不交错', () => {
- const turns = buildGroupTurns(
- input(
- [
- mine('t1', T0),
- reply('t1', 'A', 'a1', T0 + 1000, 'A 第一句'),
- reply('t1', 'B', 'b1', T0 + 1500, 'B 第一句'),
- reply('t1', 'A', 'a2', T0 + 2000, 'A 第二句'),
- reply('t1', 'B', 'b2', T0 + 2500, 'B 第二句'),
- ],
- ['A', 'B'],
- ),
- );
- expect(turns.length).toBe(1);
- expect(turns[0].blocks.length).toBe(2);
- const [blockA, blockB] = turns[0].blocks;
- expect(blockA.agentId).toBe('A');
- expect(blockA.messages.map((m) => m.text)).toEqual(['A 第一句', 'A 第二句']);
- expect(blockB.agentId).toBe('B');
- expect(blockB.messages.map((m) => m.text)).toEqual(['B 第一句', 'B 第二句']);
- });
- it('块顺序 = 目标声明顺序,而不是到达顺序', () => {
- const turns = buildGroupTurns(
- input(
- [
- mine('t1', T0),
- reply('t1', 'B', 'b1', T0 + 1000, 'B 先回'),
- reply('t1', 'A', 'a1', T0 + 2000, 'A 后回'),
- ],
- ['A', 'B'],
- ),
- );
- expect(turns[0].blocks.map((b) => b.agentId)).toEqual(['A', 'B']);
- });
- it('尚未回复的成员保留空块(能看到「已派给它」)', () => {
- const turns = buildGroupTurns(
- input([mine('t1', T0), reply('t1', 'A', 'a1', T0 + 1000)], ['A', 'B']),
- );
- expect(turns[0].blocks.length).toBe(2);
- const b = turns[0].blocks.find((x) => x.agentId === 'B')!;
- expect(b.messages.length).toBe(0);
- expect(blockDuration(b)).toBe(0);
- });
- it('🔴 跨天的脏数据不再产生超大耗时(189848s 回归)', () => {
- const day = 24 * 3600 * 1000;
- const turns = buildGroupTurns(
- input(
- [
- mine('t1', T0),
- reply('t1', 'A', 'a-old', T0 - day * 2, '两天前的历史'),
- reply('t1', 'A', 'a-new', T0 + 1000, '刚回的'),
- ],
- ['A'],
- ),
- );
- // 关键:只有一个回合、一块(不会被拆成跨天的大回合)
- expect(turns.length).toBe(1);
- expect(turns[0].blocks.length).toBe(1);
- expect(turns[0].blocks[0].messages.length).toBe(2);
- // 真正的回归断言:块末时间就是最新那条,不会被历史拉走
- expect(turns[0].blocks[0].updatedAt).toBe(T0 + 1000);
- });
- it('🔴 迟到历史会把块起点拉早 —— 入库层必须过滤历史(记录当前行为)', () => {
- const day = 24 * 3600 * 1000;
- const turns = buildGroupTurns(
- input(
- [
- mine('t1', T0),
- reply('t1', 'A', 'a1', T0 + 1000),
- reply('t1', 'A', 'a-stale', T0 - day, '过期历史'),
- ],
- ['A'],
- ),
- );
- // 这是**已知边界**:聚合层不做时间过滤,脏历史混进来依然会拉长耗时。
- // 正确做法是在 GroupStore 入库时就拒绝历史事件(见 isFreshReply)。
- expect(turns[0].blocks[0].startedAt).toBe(T0 - day);
- expect(turns[0].blocks[0].updatedAt).toBe(T0 + 1000);
- });
- it('不同回合的块互不干扰,各自耗时独立', () => {
- const turns = buildGroupTurns(
- input(
- [
- mine('t1', T0),
- reply('t1', 'A', 'a1', T0 + 1000),
- reply('t1', 'A', 'a2', T0 + 3000),
- mine('t2', T0 + 100_000),
- reply('t2', 'A', 'a3', T0 + 101_000),
- reply('t2', 'A', 'a4', T0 + 104_000),
- ],
- ['A'],
- {},
- T0 + 200_000,
- ),
- );
- expect(turns.length).toBe(2);
- expect(blockDuration(turns[0].blocks[0])).toBe(2000);
- expect(blockDuration(turns[1].blocks[0])).toBe(3000);
- });
- it('typing 未结束 → 该块保持打开', () => {
- const turns = buildGroupTurns(
- input([mine('t1', T0), reply('t1', 'A', 'a1', T0 + 1000)], ['A', 'B'], { A: true }),
- );
- const a = turns[0].blocks.find((x) => x.agentId === 'A')!;
- expect(a.typing).toBe(true);
- expect(a.closed).toBe(false);
- expect(turns[0].closed).toBe(false);
- });
- it('静默超过 quietMs 且不在 typing → 关闭', () => {
- const turns = buildGroupTurns({
- entries: [mine('t1', T0), reply('t1', 'A', 'a1', T0 + 1000)],
- targetsOf: () => ['A'],
- typingOf: () => false,
- now: T0 + 1000 + GROUP_QUIET_MS + 1,
- });
- expect(turns[0].blocks[0].closed).toBe(true);
- expect(turns[0].closed).toBe(true);
- });
- it('静默未到 quietMs → 仍打开', () => {
- const turns = buildGroupTurns({
- entries: [mine('t1', T0), reply('t1', 'A', 'a1', T0 + 1000)],
- targetsOf: () => ['A'],
- typingOf: () => false,
- now: T0 + 2000,
- });
- expect(turns[0].blocks[0].closed).toBe(false);
- });
- it('找不到锚点回合的回复被丢弃(不凭空造回合)', () => {
- const turns = buildGroupTurns(input([reply('ghost', 'A', 'x1', T0 + 1000)], ['A']));
- expect(turns.length).toBe(0);
- });
- it('重复的用户消息只认第一条', () => {
- const turns = buildGroupTurns(
- input([mine('t1', T0, '第一次'), mine('t1', T0 + 1, '重复')], ['A']),
- );
- expect(turns.length).toBe(1);
- expect(turns[0].userMessage.text).toBe('第一次');
- });
- it('条目乱序传入也能正确聚合', () => {
- const turns = buildGroupTurns(
- input(
- [
- reply('t1', 'A', 'a1', T0 + 2000),
- mine('t1', T0),
- reply('t1', 'A', 'a0', T0 + 1000),
- ],
- ['A'],
- ),
- );
- expect(turns[0].blocks[0].messages.map((m) => m.id)).toEqual(['a0', 'a1']);
- });
- it('空输入 → 空结果', () => {
- expect(buildGroupTurns(input([], ['A']))).toEqual([]);
- });
- it('回合按用户消息时间顺序返回', () => {
- const turns = buildGroupTurns(
- input([mine('t2', T0 + 50_000), mine('t1', T0)], ['A'], {}, T0 + 100_000),
- );
- expect(turns.map((t) => t.id)).toEqual(['t1', 't2']);
- });
- });
- describe('blockDuration', () => {
- it('空块为 0', () => {
- expect(
- blockDuration({ agentId: 'A', messages: [], startedAt: 0, updatedAt: 0, typing: false, closed: true }),
- ).toBe(0);
- });
- it('单条消息耗时为 0', () => {
- const turns = buildGroupTurns(input([mine('t1', T0), reply('t1', 'A', 'a1', T0 + 1000)], ['A']));
- expect(blockDuration(turns[0].blocks[0])).toBe(0);
- });
- });
- });
|