/** * **后端发生的事,前端得自己知道** —— 广播面的回归。 * * 用户实测报的:任务列表、消息气泡、工作区、对话区都不自己动,得手点刷新。 * 前端那一侧是好的(单条 SSE + 按前缀失效缓存),缺的全在后端 —— 它压根没说。 * * 三处各有各的成因,所以分三段守: * ① 推进器只在**状态换名字**时广播,而案例活动连跑十几拍全程都是 `running`; * ② `interaction` 的推送(AI 发言、换画面)一个订阅者都没有,全广播给了空气; * ③ 发消息不经事件投递(那条路才有广播),于是收件人的消息气泡不动。 */ import { describe, expect, it } from 'vitest'; import { createBroadcaster } from '../broadcast.ts'; import { worthBroadcasting } from '../workers/task-advancer.ts'; describe('① 推进器:推动了就说一声', () => { const running = { runtime: 'running' }; it('状态没换名字但这一拍干了事 → 广播', () => { // 案例活动的常态:连跑十几拍,runtime 一直是 running,而台账/对话/画面每拍都在变 expect(worthBroadcasting(running, { progressed: true, task: { runtime: 'running' } })).toBe(true); }); it('空拍不广播 —— 不给在线浏览器发噪声', () => { expect(worthBroadcasting(running, { progressed: false, task: { runtime: 'running' } })).toBe(false); }); it('挂起去等人:没推动但状态换了 → 广播', () => { expect(worthBroadcasting(running, { progressed: false, task: { runtime: 'awaiting-human' } })).toBe(true); }); it('走到终态 → 广播(「办完的活自己挪到已完成」靠它)', () => { expect( worthBroadcasting(running, { progressed: false, task: { runtime: 'terminal', terminal: 'released' } }), ).toBe(true); }); it('第一次见到这个任务(没有前态)→ 广播', () => { expect(worthBroadcasting(undefined, { progressed: false, task: { runtime: 'running' } })).toBe(true); }); }); describe('广播器:一个订阅者出错不牵连别人', () => { it('抛错的订阅者被摘掉,其余照收', () => { const b = createBroadcaster(); const got: string[] = []; b.subscribe(() => { throw new Error('这个订阅者坏了'); }); b.subscribe((t) => got.push(t.reason)); b.publish({ kind: 'task', id: 't1', reason: 'advanced' }); b.publish({ kind: 'task', id: 't1', reason: 'again' }); expect(got).toEqual(['advanced', 'again']); expect(b.size).toBe(1); }); }); /* * ④ 收尾这一下动的不只是任务(2026-07-30)。 * * 放行那一刻草稿合并进正式版、动作意向落地、输出口发射 —— 数据域多出真行、 * 驾驶舱的完成数变了。那几屏的键都在 `gov` 下、不以任务为轴,只按任务广播 * 它们一个也收不到(实测:一单放行完,数据域里还是空的,直到手动刷新)。 */ describe('④ 终态要额外发一次 system —— 台账与指标不以任务为轴', () => { it('走到终态时,除任务/流之外还有一条全局推送', () => { const b = createBroadcaster(); const kinds: string[] = []; b.subscribe((t) => kinds.push(t.kind)); // 照 task-advancer 的顺序重放一次「这一拍走到了终态」 const outcome = { progressed: true, task: { runtime: 'terminal', terminal: 'released', workflowId: 'W1' } }; if (worthBroadcasting({ runtime: 'running' }, outcome)) { b.publish({ kind: 'task', id: 'T1', reason: 'advanced' }); if (outcome.task.workflowId !== undefined) b.publish({ kind: 'workflow', id: 'W1', reason: 'task-advanced' }); if (outcome.task.terminal !== undefined) b.publish({ kind: 'system', id: 'terminal', reason: outcome.task.terminal }); } expect(kinds).toEqual(['task', 'workflow', 'system']); }); it('没到终态就不发 system —— 每拍都发等于让治理台整屏重取', () => { const b = createBroadcaster(); const kinds: string[] = []; b.subscribe((t) => kinds.push(t.kind)); const outcome = { progressed: true, task: { runtime: 'running' as string, terminal: undefined } }; b.publish({ kind: 'task', id: 'T1', reason: 'advanced' }); if (outcome.task.terminal !== undefined) b.publish({ kind: 'system', id: 'terminal', reason: 'x' }); expect(kinds).toEqual(['task']); }); });