templates.test.ts 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199
  1. import { describe, expect, it } from "vitest";
  2. import { buildCircuitBreakerMessage } from "@/lib/webhook/templates/circuit-breaker";
  3. import { buildCostAlertMessage } from "@/lib/webhook/templates/cost-alert";
  4. import { buildDailyLeaderboardMessage } from "@/lib/webhook/templates/daily-leaderboard";
  5. import type {
  6. CircuitBreakerAlertData,
  7. CostAlertData,
  8. DailyLeaderboardData,
  9. } from "@/lib/webhook/types";
  10. describe("Message Templates", () => {
  11. describe("buildCircuitBreakerMessage", () => {
  12. it("should create structured message for circuit breaker alert", () => {
  13. const data: CircuitBreakerAlertData = {
  14. providerName: "OpenAI",
  15. providerId: 1,
  16. failureCount: 5,
  17. retryAt: "2025-01-02T12:30:00Z",
  18. lastError: "Connection timeout",
  19. };
  20. const message = buildCircuitBreakerMessage(data);
  21. expect(message.header.level).toBe("error");
  22. expect(message.header.icon).toBe("🔌");
  23. expect(message.header.title).toContain("熔断");
  24. expect(message.timestamp).toBeInstanceOf(Date);
  25. const sectionsStr = JSON.stringify(message.sections);
  26. expect(sectionsStr).toContain("OpenAI");
  27. expect(sectionsStr).toContain("5");
  28. });
  29. it("should handle missing lastError", () => {
  30. const data: CircuitBreakerAlertData = {
  31. providerName: "Anthropic",
  32. providerId: 2,
  33. failureCount: 3,
  34. retryAt: "2025-01-02T13:00:00Z",
  35. };
  36. const message = buildCircuitBreakerMessage(data);
  37. expect(message.header.level).toBe("error");
  38. });
  39. it("should default to provider source when incidentSource is not set", () => {
  40. const data: CircuitBreakerAlertData = {
  41. providerName: "OpenAI",
  42. providerId: 1,
  43. failureCount: 5,
  44. retryAt: "2025-01-02T12:30:00Z",
  45. };
  46. const message = buildCircuitBreakerMessage(data);
  47. const sectionsStr = JSON.stringify(message.sections);
  48. expect(sectionsStr).toContain("OpenAI");
  49. // Default should use provider-style title
  50. expect(message.header.title).toMatch(/供应商/);
  51. });
  52. it("should produce provider-specific message when incidentSource is provider", () => {
  53. const data: CircuitBreakerAlertData = {
  54. providerName: "Anthropic",
  55. providerId: 2,
  56. failureCount: 3,
  57. retryAt: "2025-01-02T13:00:00Z",
  58. incidentSource: "provider",
  59. };
  60. const message = buildCircuitBreakerMessage(data);
  61. expect(message.header.title).toMatch(/供应商/);
  62. const sectionsStr = JSON.stringify(message.sections);
  63. expect(sectionsStr).toContain("Anthropic");
  64. expect(sectionsStr).toContain("ID: 2");
  65. });
  66. it("should produce endpoint-specific message when incidentSource is endpoint", () => {
  67. const data: CircuitBreakerAlertData = {
  68. providerName: "OpenAI",
  69. providerId: 1,
  70. failureCount: 3,
  71. retryAt: "2025-01-02T13:00:00Z",
  72. incidentSource: "endpoint",
  73. endpointId: 42,
  74. endpointUrl: "https://api.openai.com/v1",
  75. };
  76. const message = buildCircuitBreakerMessage(data);
  77. expect(message.header.title).toMatch(/端点/);
  78. const sectionsStr = JSON.stringify(message.sections);
  79. expect(sectionsStr).toContain("42");
  80. expect(sectionsStr).toContain("https://api.openai.com/v1");
  81. });
  82. it("should include endpoint fields in details when source is endpoint", () => {
  83. const data: CircuitBreakerAlertData = {
  84. providerName: "OpenAI",
  85. providerId: 1,
  86. failureCount: 5,
  87. retryAt: "2025-01-02T12:30:00Z",
  88. lastError: "Connection refused",
  89. incidentSource: "endpoint",
  90. endpointId: 99,
  91. endpointUrl: "https://custom-proxy.example.com/v1",
  92. };
  93. const message = buildCircuitBreakerMessage(data);
  94. const sectionsStr = JSON.stringify(message.sections);
  95. // Should still include failure count and error
  96. expect(sectionsStr).toContain("5");
  97. expect(sectionsStr).toContain("Connection refused");
  98. // Should include endpoint-specific info
  99. expect(sectionsStr).toContain("99");
  100. expect(sectionsStr).toContain("https://custom-proxy.example.com/v1");
  101. });
  102. });
  103. describe("buildCostAlertMessage", () => {
  104. it("should create structured message for user cost alert", () => {
  105. const data: CostAlertData = {
  106. targetType: "user",
  107. targetName: "张三",
  108. targetId: 100,
  109. currentCost: 8.5,
  110. quotaLimit: 10,
  111. threshold: 0.8,
  112. period: "本周",
  113. };
  114. const message = buildCostAlertMessage(data);
  115. expect(message.header.level).toBe("warning");
  116. expect(message.header.icon).toBe("💰");
  117. expect(message.header.title).toContain("成本预警");
  118. const sectionsStr = JSON.stringify(message.sections);
  119. expect(sectionsStr).toContain("张三");
  120. expect(sectionsStr).toContain("8.5");
  121. expect(sectionsStr).toContain("本周");
  122. });
  123. it("should create structured message for provider cost alert", () => {
  124. const data: CostAlertData = {
  125. targetType: "provider",
  126. targetName: "GPT-4",
  127. targetId: 1,
  128. currentCost: 950,
  129. quotaLimit: 1000,
  130. threshold: 0.9,
  131. period: "本月",
  132. };
  133. const message = buildCostAlertMessage(data);
  134. expect(message.header.level).toBe("warning");
  135. const sectionsStr = JSON.stringify(message.sections);
  136. expect(sectionsStr).toContain("供应商");
  137. });
  138. });
  139. describe("buildDailyLeaderboardMessage", () => {
  140. it("should create structured message for leaderboard", () => {
  141. const data: DailyLeaderboardData = {
  142. date: "2025-01-02",
  143. entries: [
  144. { userId: 1, userName: "用户A", totalRequests: 100, totalCost: 5.0, totalTokens: 50000 },
  145. { userId: 2, userName: "用户B", totalRequests: 80, totalCost: 4.0, totalTokens: 40000 },
  146. ],
  147. totalRequests: 180,
  148. totalCost: 9.0,
  149. };
  150. const message = buildDailyLeaderboardMessage(data);
  151. expect(message.header.level).toBe("info");
  152. expect(message.header.icon).toBe("📊");
  153. expect(message.header.title).toContain("排行榜");
  154. const sectionsStr = JSON.stringify(message.sections);
  155. expect(sectionsStr).toContain("用户A");
  156. expect(sectionsStr).toContain("🥇");
  157. });
  158. it("should handle empty entries", () => {
  159. const data: DailyLeaderboardData = {
  160. date: "2025-01-02",
  161. entries: [],
  162. totalRequests: 0,
  163. totalCost: 0,
  164. };
  165. const message = buildDailyLeaderboardMessage(data);
  166. expect(message.header.level).toBe("info");
  167. const sectionsStr = JSON.stringify(message.sections);
  168. expect(sectionsStr).toContain("暂无数据");
  169. });
  170. });
  171. });