format.spec.ts 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120
  1. import {
  2. fmtBalance,
  3. fmtPercent,
  4. fmtSize,
  5. fmtTokens,
  6. isMatrixReady,
  7. normProgress,
  8. usageOf,
  9. } from './format';
  10. import { Agent } from './api/models';
  11. /**
  12. * 纯函数单测 —— 格式化与派生口径
  13. *
  14. * 依据 SPEC NFR-06(可测)与《数据访问层设计.md》§五(格式化唯一实现)。
  15. * 这些口径曾在存量项目里出现 4 份重复实现,故必须有回归保护。
  16. */
  17. describe('format 纯函数', () => {
  18. describe('fmtSize', () => {
  19. it('按 1024 进制换算并保留 1 位小数', () => {
  20. expect(fmtSize(0)).toBe('0 B');
  21. expect(fmtSize(512)).toBe('512 B');
  22. expect(fmtSize(1024)).toBe('1.0 KB');
  23. expect(fmtSize(1536)).toBe('1.5 KB');
  24. expect(fmtSize(1024 * 1024)).toBe('1.0 MB');
  25. });
  26. it('空值返回占位符', () => {
  27. expect(fmtSize(undefined)).toBe('—');
  28. });
  29. });
  30. describe('fmtTokens', () => {
  31. it('按 1000 进制分档(k / M)', () => {
  32. expect(fmtTokens(0)).toBe('0');
  33. expect(fmtTokens(999)).toBe('999');
  34. expect(fmtTokens(1500)).toBe('1.5k');
  35. expect(fmtTokens(1_500_000)).toBe('1.5M');
  36. });
  37. it('空值返回占位符', () => {
  38. expect(fmtTokens(undefined)).toBe('—');
  39. });
  40. });
  41. describe('fmtBalance(FR-07 AC-07.4:余额转人民币)', () => {
  42. it('有记账时显示 ¥ xx.xx(两位小数)', () => {
  43. expect(fmtBalance({ remainCny: 17842.31 })).toBe('¥ 17842.31');
  44. expect(fmtBalance({ remainCny: 0 })).toBe('¥ 0.00');
  45. expect(fmtBalance({ remainCny: 86.4 })).toBe('¥ 86.40');
  46. });
  47. it('无记账时显示占位符,不伪造 ¥0', () => {
  48. expect(fmtBalance(undefined)).toBe('—');
  49. expect(fmtBalance({})).toBe('—');
  50. });
  51. it('负值如实显示为 -¥(不截断为 0,2026-11-11 余额口径事件)', () => {
  52. expect(fmtBalance({ remainCny: -76.12 })).toBe('-¥ 76.12');
  53. expect(fmtBalance({ remainCny: -0.5 })).toBe('-¥ 0.50');
  54. });
  55. });
  56. describe('normProgress(契约 §六-5:百分比 or 比例语义推断)', () => {
  57. it('0<x<=1 视为小数比例,乘 100', () => {
  58. expect(normProgress(0.5)).toBe(50);
  59. expect(normProgress(0.25)).toBe(25);
  60. expect(normProgress(1)).toBe(100);
  61. });
  62. it('>1 视为整数百分比,原样返回', () => {
  63. expect(normProgress(50)).toBe(50);
  64. expect(normProgress(100)).toBe(100);
  65. });
  66. it('越界被夹到 [0,100](防脏数据撑破进度条)', () => {
  67. expect(normProgress(150)).toBe(100);
  68. expect(normProgress(-5)).toBe(0);
  69. });
  70. it('0 与空值:0 返回 0,空值返回 undefined', () => {
  71. expect(normProgress(0)).toBe(0);
  72. expect(normProgress(undefined)).toBeUndefined();
  73. expect(normProgress(NaN)).toBeUndefined();
  74. });
  75. });
  76. describe('fmtPercent', () => {
  77. it('归一后带百分号', () => {
  78. expect(fmtPercent(0.5)).toBe('50%');
  79. expect(fmtPercent(72)).toBe('72%');
  80. });
  81. it('空值返回占位符', () => {
  82. expect(fmtPercent(undefined)).toBe('—');
  83. });
  84. });
  85. describe('usageOf / isMatrixReady(映射口径)', () => {
  86. it('usageOf 容忍缺失的 usage 字段', () => {
  87. expect(usageOf({ id: 'a', name: 'a', status: 'idle', matrixReady: false, unread: 0 })).toEqual(
  88. {},
  89. );
  90. });
  91. it('isMatrixReady 采用**账号级**口径(matrixConfig.userId),不只看 homeserver 可达', () => {
  92. const base = { id: 'a', name: 'a', status: 'idle', unread: 0 } as unknown as Agent;
  93. // 未接入
  94. expect(isMatrixReady({ ...base, matrixReady: false })).toBe(false);
  95. // 仅 channels.matrix=true 不算就绪(homeserver 可达 ≠ 该 Agent 有账号)
  96. expect(isMatrixReady({ ...base, channels: { matrix: true } })).toBe(false);
  97. // 账号级就绪:matrixConfig.userId 存在
  98. expect(
  99. isMatrixReady({ ...base, channels: { matrixConfig: { userId: '@x:fmode.cn' } } }),
  100. ).toBe(true);
  101. // 映射层已判定就绪时直接放行
  102. expect(isMatrixReady({ ...base, matrixReady: true })).toBe(true);
  103. });
  104. });
  105. });