Add tests for centered modal adoption, search modal results, and accounts batch actions

- Implement tests for the CenteredModal component to ensure it follows the shared modal shell pattern.
- Add comprehensive tests for the SearchModal results, including API interactions and UI behavior.
- Create tests for accounts batch actions, verifying the functionality of refreshing balances and selecting accounts via row clicks.
- Introduce tests for the accounts centered modal to confirm the use of CenteredModal for add/edit/rebind flows.
- Add tests for the accounts edit panel to ensure proper rendering and functionality when opening the edit modal.
- Implement tests for the accounts tokens header to verify the reuse of the accounts page header in the tokens segment.
- Create tests for the system proxy settings, ensuring the correct saving of the proxy URL.
- Add tests for the tokens actions layout to confirm the proper layout and styling of token row actions.
- Implement tests for the tokens batch actions, including deletion of selected tokens and row selection behavior.
- Introduce tests for the tokens focus navigation to ensure the correct highlighting of focused token rows.
This commit is contained in:
cita
2026-03-07 18:04:05 +08:00
parent c31bbcbb51
commit c437d93715
65 changed files with 3984 additions and 486 deletions
+4 -4
View File
@@ -8,7 +8,7 @@ import { isTokenExpiredError } from '../../services/alertRules.js';
import { shouldRetryProxyRequest } from '../../services/proxyRetryPolicy.js';
import { resolveProxyUsageWithSelfLogFallback } from '../../services/proxyUsageFallbackService.js';
import { mergeProxyUsage, parseProxyUsage } from '../../services/proxyUsageParser.js';
import { withExplicitProxyRequestInit } from '../../services/siteProxy.js';
import { resolveProxyUrlForSite, withSiteRecordProxyRequestInit } from '../../services/siteProxy.js';
import {
type DownstreamFormat,
createStreamTransformContext,
@@ -183,7 +183,7 @@ async function handleChatProxyRequest(
try {
const endpointResult = await executeEndpointFlow({
siteUrl: selected.site.url,
proxyUrl: selected.site.proxyUrl,
proxyUrl: resolveProxyUrlForSite(selected.site),
endpointCandidates,
buildRequest: (endpoint) => {
const endpointRequest = buildUpstreamEndpointRequest({
@@ -225,7 +225,7 @@ async function handleChatProxyRequest(
downstreamHeaders: request.headers as Record<string, unknown>,
});
const normalizedTargetUrl = `${selected.site.url}${normalizedClaudeRequest.path}`;
const normalizedResponse = await fetch(normalizedTargetUrl, withExplicitProxyRequestInit(selected.site.proxyUrl, {
const normalizedResponse = await fetch(normalizedTargetUrl, withSiteRecordProxyRequestInit(selected.site, {
method: 'POST',
headers: normalizedClaudeRequest.headers,
body: JSON.stringify(normalizedClaudeRequest.body),
@@ -264,7 +264,7 @@ async function handleChatProxyRequest(
return null;
}
const minimalResponse = await fetch(ctx.targetUrl, withExplicitProxyRequestInit(selected.site.proxyUrl, {
const minimalResponse = await fetch(ctx.targetUrl, withSiteRecordProxyRequestInit(selected.site, {
method: 'POST',
headers: minimalHeaders,
body: JSON.stringify(ctx.request.body),
+2 -2
View File
@@ -9,7 +9,7 @@ import { shouldRetryProxyRequest } from '../../services/proxyRetryPolicy.js';
import { resolveProxyUsageWithSelfLogFallback } from '../../services/proxyUsageFallbackService.js';
import { mergeProxyUsage, parseProxyUsage, pullSseDataEvents } from '../../services/proxyUsageParser.js';
import { ensureModelAllowedForDownstreamKey, getDownstreamRoutingPolicy, recordDownstreamCostUsage } from './downstreamPolicy.js';
import { withExplicitProxyRequestInit } from '../../services/siteProxy.js';
import { withSiteRecordProxyRequestInit } from '../../services/siteProxy.js';
import { composeProxyLogMessage } from './logPathMeta.js';
import { formatUtcSqlDateTime } from '../../services/localTimeService.js';
import { resolveProxyLogBilling } from './proxyBilling.js';
@@ -57,7 +57,7 @@ export async function completionsProxyRoute(app: FastifyInstance) {
const startTime = Date.now();
try {
const upstream = await fetch(targetUrl, withExplicitProxyRequestInit(selected.site.proxyUrl, {
const upstream = await fetch(targetUrl, withSiteRecordProxyRequestInit(selected.site, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
+2 -2
View File
@@ -9,7 +9,7 @@ import { shouldRetryProxyRequest } from '../../services/proxyRetryPolicy.js';
import { resolveProxyUsageWithSelfLogFallback } from '../../services/proxyUsageFallbackService.js';
import { parseProxyUsage } from '../../services/proxyUsageParser.js';
import { ensureModelAllowedForDownstreamKey, getDownstreamRoutingPolicy, recordDownstreamCostUsage } from './downstreamPolicy.js';
import { withExplicitProxyRequestInit } from '../../services/siteProxy.js';
import { withSiteRecordProxyRequestInit } from '../../services/siteProxy.js';
import { composeProxyLogMessage } from './logPathMeta.js';
import { formatUtcSqlDateTime } from '../../services/localTimeService.js';
import { resolveProxyLogBilling } from './proxyBilling.js';
@@ -54,7 +54,7 @@ export async function embeddingsProxyRoute(app: FastifyInstance) {
const startTime = Date.now();
try {
const upstream = await fetch(targetUrl, withExplicitProxyRequestInit(selected.site.proxyUrl, {
const upstream = await fetch(targetUrl, withSiteRecordProxyRequestInit(selected.site, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
+2 -2
View File
@@ -8,7 +8,7 @@ import { isTokenExpiredError } from '../../services/alertRules.js';
import { estimateProxyCost } from '../../services/modelPricingService.js';
import { shouldRetryProxyRequest } from '../../services/proxyRetryPolicy.js';
import { ensureModelAllowedForDownstreamKey, getDownstreamRoutingPolicy, recordDownstreamCostUsage } from './downstreamPolicy.js';
import { withExplicitProxyRequestInit } from '../../services/siteProxy.js';
import { withSiteRecordProxyRequestInit } from '../../services/siteProxy.js';
import { composeProxyLogMessage } from './logPathMeta.js';
import { formatUtcSqlDateTime } from '../../services/localTimeService.js';
@@ -50,7 +50,7 @@ export async function imagesProxyRoute(app: FastifyInstance) {
const startTime = Date.now();
try {
const upstream = await fetch(targetUrl, withExplicitProxyRequestInit(selected.site.proxyUrl, {
const upstream = await fetch(targetUrl, withSiteRecordProxyRequestInit(selected.site, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@@ -0,0 +1,140 @@
import Fastify, { type FastifyInstance } from 'fastify';
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
const fetchMock = vi.fn();
const selectChannelMock = vi.fn();
const selectNextChannelMock = vi.fn();
const recordSuccessMock = vi.fn();
const recordFailureMock = vi.fn();
const refreshModelsAndRebuildRoutesMock = vi.fn();
const reportProxyAllFailedMock = vi.fn();
const reportTokenExpiredMock = vi.fn();
const estimateProxyCostMock = vi.fn(async (_arg?: any) => 0);
const buildProxyBillingDetailsMock = vi.fn(async (_arg?: any) => null);
const fetchModelPricingCatalogMock = vi.fn(async (_arg?: any): Promise<any> => null);
const resolveProxyUsageWithSelfLogFallbackMock = vi.fn(async ({ usage }: any) => ({
...usage,
estimatedCostFromQuota: 0,
recoveredFromSelfLog: false,
}));
const dbInsertMock = vi.fn((_arg?: any) => ({
values: () => ({
run: () => undefined,
}),
}));
vi.mock('undici', () => ({
fetch: (...args: unknown[]) => fetchMock(...args),
}));
vi.mock('../../services/tokenRouter.js', () => ({
tokenRouter: {
selectChannel: (...args: unknown[]) => selectChannelMock(...args),
selectNextChannel: (...args: unknown[]) => selectNextChannelMock(...args),
recordSuccess: (...args: unknown[]) => recordSuccessMock(...args),
recordFailure: (...args: unknown[]) => recordFailureMock(...args),
},
}));
vi.mock('../../services/modelService.js', () => ({
refreshModelsAndRebuildRoutes: (...args: unknown[]) => refreshModelsAndRebuildRoutesMock(...args),
}));
vi.mock('../../services/alertService.js', () => ({
reportProxyAllFailed: (...args: unknown[]) => reportProxyAllFailedMock(...args),
reportTokenExpired: (...args: unknown[]) => reportTokenExpiredMock(...args),
}));
vi.mock('../../services/alertRules.js', () => ({
isTokenExpiredError: () => false,
}));
vi.mock('../../services/modelPricingService.js', () => ({
estimateProxyCost: (arg: any) => estimateProxyCostMock(arg),
buildProxyBillingDetails: (arg: any) => buildProxyBillingDetailsMock(arg),
fetchModelPricingCatalog: (arg: any) => fetchModelPricingCatalogMock(arg),
}));
vi.mock('../../services/proxyRetryPolicy.js', () => ({
shouldRetryProxyRequest: () => false,
}));
vi.mock('../../services/proxyUsageFallbackService.js', () => ({
resolveProxyUsageWithSelfLogFallback: (arg: any) => resolveProxyUsageWithSelfLogFallbackMock(arg),
}));
vi.mock('../../db/index.js', () => ({
db: {
insert: (arg: any) => dbInsertMock(arg),
},
schema: {
proxyLogs: {},
},
}));
describe('responses proxy compact upstream routing', () => {
let app: FastifyInstance;
beforeAll(async () => {
const { responsesProxyRoute } = await import('./responses.js');
app = Fastify();
await app.register(responsesProxyRoute);
});
beforeEach(() => {
fetchMock.mockReset();
selectChannelMock.mockReset();
selectNextChannelMock.mockReset();
recordSuccessMock.mockReset();
recordFailureMock.mockReset();
refreshModelsAndRebuildRoutesMock.mockReset();
reportProxyAllFailedMock.mockReset();
reportTokenExpiredMock.mockReset();
estimateProxyCostMock.mockClear();
buildProxyBillingDetailsMock.mockClear();
fetchModelPricingCatalogMock.mockReset();
resolveProxyUsageWithSelfLogFallbackMock.mockClear();
dbInsertMock.mockClear();
selectChannelMock.mockReturnValue({
channel: { id: 11, routeId: 22 },
site: { id: 44, name: 'demo-site', url: 'https://upstream.example.com', platform: 'openai' },
account: { id: 33, username: 'demo-user' },
tokenName: 'default',
tokenValue: 'sk-demo',
actualModel: 'upstream-gpt',
});
selectNextChannelMock.mockReturnValue(null);
fetchModelPricingCatalogMock.mockResolvedValue(null);
});
afterAll(async () => {
await app.close();
});
it('forwards compact requests to the upstream /v1/responses/compact path first', async () => {
fetchMock.mockResolvedValue(new Response(JSON.stringify({
id: 'resp_123',
object: 'response',
output_text: 'hello from compact',
usage: { input_tokens: 10, output_tokens: 5, total_tokens: 15 },
}), {
status: 200,
headers: { 'content-type': 'application/json' },
}));
const response = await app.inject({
method: 'POST',
url: '/v1/responses/compact',
payload: {
model: 'gpt-5.2',
input: 'hello',
},
});
expect(response.statusCode).toBe(200);
expect(fetchMock).toHaveBeenCalledTimes(1);
const [targetUrl] = fetchMock.mock.calls[0] as [string, any];
expect(targetUrl).toContain('/v1/responses/compact');
});
});
@@ -0,0 +1,32 @@
import Fastify, { type FastifyInstance } from 'fastify';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
describe('responses proxy compact route', () => {
let app: FastifyInstance;
beforeAll(async () => {
const { responsesProxyRoute } = await import('./responses.js');
app = Fastify();
await app.register(responsesProxyRoute);
});
afterAll(async () => {
await app.close();
});
it('accepts /v1/responses/compact requests instead of returning 404', async () => {
const response = await app.inject({
method: 'POST',
url: '/v1/responses/compact',
payload: {},
});
expect(response.statusCode).toBe(400);
expect(response.json()).toEqual({
error: {
message: 'model is required',
type: 'invalid_request_error',
},
});
});
});
+22 -8
View File
@@ -8,7 +8,7 @@ import { isTokenExpiredError } from '../../services/alertRules.js';
import { shouldRetryProxyRequest } from '../../services/proxyRetryPolicy.js';
import { resolveProxyUsageWithSelfLogFallback } from '../../services/proxyUsageFallbackService.js';
import { mergeProxyUsage, parseProxyUsage } from '../../services/proxyUsageParser.js';
import { withExplicitProxyRequestInit } from '../../services/siteProxy.js';
import { resolveProxyUrlForSite, withSiteRecordProxyRequestInit } from '../../services/siteProxy.js';
import {
createStreamTransformContext,
normalizeUpstreamFinalResponse,
@@ -1137,15 +1137,19 @@ function serializeConvertedResponsesEvents(input: {
}
export async function responsesProxyRoute(app: FastifyInstance) {
app.post('/v1/responses', async (request: FastifyRequest, reply: FastifyReply) => {
const handleResponsesRequest = async (
request: FastifyRequest,
reply: FastifyReply,
downstreamPath: string,
) => {
const body = request.body as any;
const requestedModel = typeof body?.model === 'string' ? body.model.trim() : '';
const downstreamPath = '/v1/responses';
if (!requestedModel) {
return reply.code(400).send({ error: { message: 'model is required', type: 'invalid_request_error' } });
}
if (!await ensureModelAllowedForDownstreamKey(request, reply, requestedModel)) return;
const downstreamPolicy = getDownstreamRoutingPolicy(request);
const isCompactRequest = downstreamPath === '/v1/responses/compact';
const isStream = body.stream === true;
const excludeChannelIds: number[] = [];
@@ -1193,7 +1197,7 @@ export async function responsesProxyRoute(app: FastifyInstance) {
try {
const endpointResult = await executeEndpointFlow({
siteUrl: selected.site.url,
proxyUrl: selected.site.proxyUrl,
proxyUrl: resolveProxyUrlForSite(selected.site),
endpointCandidates,
buildRequest: (endpoint) => {
const endpointRequest = buildUpstreamEndpointRequest({
@@ -1208,9 +1212,14 @@ export async function responsesProxyRoute(app: FastifyInstance) {
responsesOriginalBody: body,
downstreamHeaders: request.headers as Record<string, unknown>,
});
const upstreamPath = (
isCompactRequest && endpoint === 'responses'
? `${endpointRequest.path}/compact`
: endpointRequest.path
);
return {
endpoint,
path: endpointRequest.path,
path: upstreamPath,
headers: endpointRequest.headers,
body: endpointRequest.body as Record<string, unknown>,
};
@@ -1231,7 +1240,7 @@ export async function responsesProxyRoute(app: FastifyInstance) {
for (const compatibilityBody of compatibilityBodies) {
const compatibilityResponse = await fetch(
ctx.targetUrl,
withExplicitProxyRequestInit(selected.site.proxyUrl, {
withSiteRecordProxyRequestInit(selected.site, {
method: 'POST',
headers: compatibilityHeadersCandidate,
body: JSON.stringify(compatibilityBody),
@@ -1266,7 +1275,7 @@ export async function responsesProxyRoute(app: FastifyInstance) {
});
const minimalResponse = await fetch(
ctx.targetUrl,
withExplicitProxyRequestInit(selected.site.proxyUrl, {
withSiteRecordProxyRequestInit(selected.site, {
method: 'POST',
headers: minimalHeaders,
body: JSON.stringify(ctx.request.body),
@@ -1554,7 +1563,12 @@ export async function responsesProxyRoute(app: FastifyInstance) {
});
}
}
});
};
app.post('/v1/responses', async (request: FastifyRequest, reply: FastifyReply) =>
handleResponsesRequest(request, reply, '/v1/responses'));
app.post('/v1/responses/compact', async (request: FastifyRequest, reply: FastifyReply) =>
handleResponsesRequest(request, reply, '/v1/responses/compact'));
}
async function logProxy(