leader-health.spec.ts 3.3 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495
  1. import { Agent } from './api/models';
  2. import { LEADER_STALE_MS, leaderHealth } from './leader-health';
  3. /**
  4. * 组长健康度。
  5. *
  6. * 组长制下所有需求都走组长,一旦掉线整组停摆。
  7. * 重点回归:
  8. * ① 心跳超时能被识别(光看 status 会误判成「在忙」)
  9. * ② 没设组长要提示
  10. * ③ 在忙 ≠ 掉线(提示要区别开)
  11. */
  12. describe('leader-health', () => {
  13. const NOW = new Date('2026-10-27T12:00:00Z').getTime();
  14. function agent(over: Partial<Agent> = {}): Agent {
  15. return {
  16. id: 'lj',
  17. name: 'lj',
  18. status: 'idle',
  19. matrixReady: true,
  20. unread: 0,
  21. heartbeatAt: new Date(NOW - 30_000).toISOString(), // 30 秒前,新鲜
  22. ...over,
  23. };
  24. }
  25. it('没设组长 → 提示去设置', () => {
  26. const r = leaderHealth(undefined, NOW);
  27. expect(r.health).toBe('none');
  28. expect(r.warn).toBeTrue();
  29. expect(r.suggestReplace).toBeTrue();
  30. expect(r.message).toContain('还没有设置组长');
  31. });
  32. it('组长正常 → 不提示', () => {
  33. const r = leaderHealth(agent(), NOW);
  34. expect(r.health).toBe('ok');
  35. expect(r.warn).toBeFalse();
  36. });
  37. it('组长在忙(有具体任务)→ 提示,但不建议更换', () => {
  38. const r = leaderHealth(agent({ status: 'running', currentTask: '处理企业微信对话' }), NOW);
  39. expect(r.health).toBe('busy');
  40. expect(r.warn).toBeTrue();
  41. expect(r.suggestReplace).toBeFalse();
  42. expect(r.message).toContain('正在处理');
  43. });
  44. it('🔴 status=running 但 currentTask 为空 → 不算忙(实测:上报器常态)', () => {
  45. const r = leaderHealth(agent({ status: 'running', currentTask: '' }), NOW);
  46. expect(r.health).toBe('ok');
  47. expect(r.warn).toBeFalse();
  48. });
  49. it('🔴 组长离线 → 建议更换', () => {
  50. const r = leaderHealth(agent({ status: 'offline' }), NOW);
  51. expect(r.health).toBe('offline');
  52. expect(r.suggestReplace).toBeTrue();
  53. expect(r.message).toContain('建议更换');
  54. });
  55. it('组长已停止 → 建议更换', () => {
  56. expect(leaderHealth(agent({ status: 'stopped' }), NOW).suggestReplace).toBeTrue();
  57. });
  58. it('🔴 status 是 running 但心跳超时 → 判定掉线(不能当成在忙)', () => {
  59. const stale = new Date(NOW - LEADER_STALE_MS - 60_000).toISOString();
  60. const r = leaderHealth(agent({ status: 'running', heartbeatAt: stale }), NOW);
  61. expect(r.health).toBe('stale');
  62. expect(r.suggestReplace).toBeTrue();
  63. expect(r.message).toContain('没有上报');
  64. });
  65. it('心跳刚好在阈值内 → 不算掉线', () => {
  66. const fresh = new Date(NOW - LEADER_STALE_MS + 10_000).toISOString();
  67. const r = leaderHealth(agent({ heartbeatAt: fresh }), NOW);
  68. expect(r.health).toBe('ok');
  69. });
  70. it('没有心跳字段 → 不据此判掉线(避免误报)', () => {
  71. const r = leaderHealth(agent({ heartbeatAt: undefined }), NOW);
  72. expect(r.health).toBe('ok');
  73. });
  74. it('心跳时间无法解析 → 不据此判掉线', () => {
  75. const r = leaderHealth(agent({ heartbeatAt: 'not-a-date' }), NOW);
  76. expect(r.health).toBe('ok');
  77. });
  78. it('提示里带上组长名字(用户知道是哪个)', () => {
  79. const r = leaderHealth(agent({ name: 'lj', status: 'offline' }), NOW);
  80. expect(r.message).toContain('lj');
  81. });
  82. });