dashboard-logs-warmup-ui.test.tsx 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157
  1. /**
  2. * @vitest-environment happy-dom
  3. */
  4. import fs from "node:fs";
  5. import path from "node:path";
  6. import type { ReactNode } from "react";
  7. import { act } from "react";
  8. import { createRoot } from "react-dom/client";
  9. import { NextIntlClientProvider } from "next-intl";
  10. import { describe, expect, test, vi } from "vitest";
  11. import { UsageLogsTable } from "@/app/[locale]/dashboard/logs/_components/usage-logs-table";
  12. import type { UsageLogRow } from "@/repository/usage-logs";
  13. // 测试环境不加载 next-intl/navigation -> next/navigation 的真实实现(避免 Next.js 运行时依赖)
  14. vi.mock("@/i18n/routing", () => ({
  15. Link: ({ children }: { children: ReactNode }) => children,
  16. }));
  17. const dashboardMessages = JSON.parse(
  18. fs.readFileSync(path.join(process.cwd(), "messages/en/dashboard.json"), "utf8")
  19. );
  20. const providerChainMessages = JSON.parse(
  21. fs.readFileSync(path.join(process.cwd(), "messages/en/provider-chain.json"), "utf8")
  22. );
  23. function renderWithIntl(node: ReactNode) {
  24. const container = document.createElement("div");
  25. document.body.appendChild(container);
  26. const root = createRoot(container);
  27. act(() => {
  28. root.render(
  29. <NextIntlClientProvider
  30. locale="en"
  31. messages={{ dashboard: dashboardMessages, "provider-chain": providerChainMessages }}
  32. timeZone="UTC"
  33. >
  34. {node}
  35. </NextIntlClientProvider>
  36. );
  37. });
  38. return {
  39. container,
  40. unmount: () => {
  41. act(() => root.unmount());
  42. container.remove();
  43. },
  44. };
  45. }
  46. describe("UsageLogsTable - warmup 跳过展示", () => {
  47. test("blockedBy=warmup 时应在 Provider/Cost 列显示 Skipped 标记", () => {
  48. const warmupLog: UsageLogRow = {
  49. id: 1,
  50. createdAt: new Date(),
  51. sessionId: "session_test",
  52. requestSequence: 1,
  53. userName: "user",
  54. keyName: "key",
  55. providerName: null,
  56. model: "claude-sonnet-4-5-20250929",
  57. originalModel: "claude-sonnet-4-5-20250929",
  58. endpoint: "/v1/messages",
  59. statusCode: 200,
  60. inputTokens: 0,
  61. outputTokens: 0,
  62. cacheCreationInputTokens: 0,
  63. cacheReadInputTokens: 0,
  64. cacheCreation5mInputTokens: 0,
  65. cacheCreation1hInputTokens: 0,
  66. cacheTtlApplied: null,
  67. totalTokens: 0,
  68. costUsd: null,
  69. costMultiplier: null,
  70. durationMs: 0,
  71. ttfbMs: 0,
  72. errorMessage: null,
  73. providerChain: null,
  74. blockedBy: "warmup",
  75. blockedReason: JSON.stringify({ reason: "anthropic_warmup_intercepted" }),
  76. userAgent: "claude_cli/1.0",
  77. messagesCount: 1,
  78. context1mApplied: false,
  79. };
  80. const { container, unmount } = renderWithIntl(
  81. <UsageLogsTable
  82. logs={[warmupLog]}
  83. total={1}
  84. page={1}
  85. pageSize={50}
  86. onPageChange={() => {}}
  87. isPending={false}
  88. />
  89. );
  90. expect(container.textContent).toContain("Skipped");
  91. expect(container.textContent).toContain("Warmup");
  92. unmount();
  93. });
  94. });
  95. describe("UsageLogsTable - cache badge alignment", () => {
  96. test("badge renders before numbers and keeps right-aligned tokens", () => {
  97. const cacheLog: UsageLogRow = {
  98. id: 2,
  99. createdAt: new Date(),
  100. sessionId: "session_cache",
  101. requestSequence: 1,
  102. userName: "user",
  103. keyName: "key",
  104. providerName: "provider",
  105. model: "claude-sonnet-4-5-20250929",
  106. originalModel: "claude-sonnet-4-5-20250929",
  107. endpoint: "/v1/messages",
  108. statusCode: 200,
  109. inputTokens: 10,
  110. outputTokens: 5,
  111. cacheCreationInputTokens: 10,
  112. cacheReadInputTokens: 5,
  113. cacheCreation5mInputTokens: 10,
  114. cacheCreation1hInputTokens: 0,
  115. cacheTtlApplied: "1h",
  116. totalTokens: 15,
  117. costUsd: "0.000001",
  118. costMultiplier: null,
  119. durationMs: 10,
  120. ttfbMs: 5,
  121. errorMessage: null,
  122. providerChain: null,
  123. blockedBy: null,
  124. blockedReason: null,
  125. userAgent: "claude_cli/1.0",
  126. messagesCount: 1,
  127. context1mApplied: false,
  128. };
  129. const { container, unmount } = renderWithIntl(
  130. <UsageLogsTable
  131. logs={[cacheLog]}
  132. total={1}
  133. page={1}
  134. pageSize={50}
  135. onPageChange={() => {}}
  136. isPending={false}
  137. />
  138. );
  139. expect(container.innerHTML).toContain("1h");
  140. expect(container.innerHTML).toContain("ml-auto");
  141. unmount();
  142. });
  143. });