unread.service.spec.ts 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151
  1. import { TestBed } from '@angular/core/testing';
  2. import { UnreadService } from './unread.service';
  3. import { SessionService } from './session.service';
  4. /**
  5. * 未读计数单测 —— **直接守护本次开发抓出的两个 bug**
  6. *
  7. * ① 「刚连接就有 39 条未读」:初次 `/sync` 的历史回填被算成未读
  8. * → 用 `catchingUp` 分界覆盖
  9. * ② `data.liveEvent` 不能区分历史与实时(实测初次同步历史也为 `true`)
  10. * → 改用「初次同步完成分界 + 已读时间戳」双判据
  11. *
  12. * 覆盖 SPEC FR-04 的未读口径与 FR-14(每域唯一 owner)。
  13. */
  14. describe('UnreadService', () => {
  15. let svc: UnreadService;
  16. let session: SessionService;
  17. beforeEach(() => {
  18. localStorage.clear();
  19. TestBed.configureTestingModule({});
  20. session = TestBed.inject(SessionService);
  21. svc = TestBed.inject(UnreadService);
  22. });
  23. afterEach(() => localStorage.clear());
  24. describe('catchingUp 分界(bug①回归)', () => {
  25. it('初次同步回填阶段:历史消息一律不计未读', () => {
  26. // 模拟 5 条历史按时间升序回填 —— 全部不应计数
  27. const base = 1_700_000_000_000;
  28. for (let i = 0; i < 5; i++) {
  29. const counted = svc.onIncoming('agent-a', base + i * 1000, /*catchingUp*/ true);
  30. expect(counted).toBe(false);
  31. }
  32. expect(svc.countOf('agent-a')).toBe(0);
  33. });
  34. it('分界之后:晚于基线的消息计入', () => {
  35. const base = 1_700_000_000_000;
  36. svc.onIncoming('agent-a', base, true); // 回填阶段建立基线
  37. const counted = svc.onIncoming('agent-a', base + 5000, false);
  38. expect(counted).toBe(true);
  39. expect(svc.countOf('agent-a')).toBe(1);
  40. });
  41. });
  42. describe('时间戳口径(bug②回归)', () => {
  43. it('早于已读基线的消息不计(已读 / 重复投递)', () => {
  44. const base = 1_700_000_000_000;
  45. svc.onIncoming('a', base, true);
  46. expect(svc.onIncoming('a', base - 1000, false)).toBe(false);
  47. expect(svc.onIncoming('a', base, false)).toBe(false); // 等于基线也不计
  48. expect(svc.countOf('a')).toBe(0);
  49. });
  50. it('首次见到房间时只推进基线,不计未读', () => {
  51. expect(svc.onIncoming('new-agent', 1_700_000_000_000, false)).toBe(false);
  52. expect(svc.countOf('new-agent')).toBe(0);
  53. // 基线已建立,后续更新的消息才计
  54. expect(svc.onIncoming('new-agent', 1_700_000_001_000, false)).toBe(true);
  55. expect(svc.countOf('new-agent')).toBe(1);
  56. });
  57. it('离线期间积累的消息(晚于基线)全部计入', () => {
  58. const base = 1_700_000_000_000;
  59. svc.onIncoming('a', base, true);
  60. for (let i = 1; i <= 7; i++) svc.onIncoming('a', base + i * 1000, false);
  61. expect(svc.countOf('a')).toBe(7);
  62. });
  63. });
  64. describe('markRead', () => {
  65. it('清零该 agent 的未读并推进基线', () => {
  66. const base = 1_700_000_000_000;
  67. svc.onIncoming('a', base, true);
  68. svc.onIncoming('a', base + 1000, false);
  69. svc.onIncoming('a', base + 2000, false);
  70. expect(svc.countOf('a')).toBe(2);
  71. svc.markRead('a', base + 2000);
  72. expect(svc.countOf('a')).toBe(0);
  73. // 基线已推进 → 早于它的消息不再计
  74. expect(svc.onIncoming('a', base + 1500, false)).toBe(false);
  75. });
  76. it('只影响目标 agent,不误伤其它', () => {
  77. const t = 1_700_000_000_000;
  78. svc.onIncoming('a', t, true);
  79. svc.onIncoming('a', t + 1, false);
  80. svc.onIncoming('b', t, true);
  81. svc.onIncoming('b', t + 1, false);
  82. svc.markRead('a', t + 1);
  83. expect(svc.countOf('a')).toBe(0);
  84. expect(svc.countOf('b')).toBe(1);
  85. });
  86. });
  87. describe('total 与多 agent', () => {
  88. it('total 为各 agent 之和', () => {
  89. const t = 1_700_000_000_000;
  90. for (const id of ['a', 'b', 'c']) {
  91. svc.onIncoming(id, t, true);
  92. svc.onIncoming(id, t + 1, false);
  93. svc.onIncoming(id, t + 2, false);
  94. }
  95. expect(svc.countOf('a')).toBe(2);
  96. expect(svc.total()).toBe(6);
  97. });
  98. });
  99. describe('持久化(退出浏览器不丢)', () => {
  100. it('按用户隔离:不同用户互不串号', () => {
  101. // 用户1
  102. session.setSession('t1', { id: 'u1', name: 'A' });
  103. svc.reload();
  104. const t = 1_700_000_000_000;
  105. svc.onIncoming('a', t, true);
  106. svc.onIncoming('a', t + 1, false);
  107. expect(svc.countOf('a')).toBe(1);
  108. // 换用户2 → 重新载入应为空
  109. session.setSession('t2', { id: 'u2', name: 'B' });
  110. svc.reload();
  111. expect(svc.countOf('a')).toBe(0);
  112. // 切回用户1 → 计数恢复
  113. session.setSession('t1', { id: 'u1', name: 'A' });
  114. svc.reload();
  115. expect(svc.countOf('a')).toBe(1);
  116. });
  117. it('reset 清空并移除存储', () => {
  118. session.setSession('t1', { id: 'u1', name: 'A' });
  119. const t = 1_700_000_000_000;
  120. svc.onIncoming('a', t, true);
  121. svc.onIncoming('a', t + 1, false);
  122. svc.reset();
  123. expect(svc.countOf('a')).toBe(0);
  124. expect(svc.total()).toBe(0);
  125. });
  126. });
  127. describe('边界', () => {
  128. it('空 agentId 不计数', () => {
  129. expect(svc.onIncoming('', 1_700_000_000_000, false)).toBe(false);
  130. expect(svc.total()).toBe(0);
  131. });
  132. });
  133. });