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:
@@ -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),
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user