test: update TokenRoutes tests for summary-based data loading
Adapt all 3 test files to mock getRoutesSummary instead of getRoutes, use summary-format data structures, and expand cards before asserting channel-level content.
This commit is contained in:
@@ -6,7 +6,8 @@ import TokenRoutes from './TokenRoutes.js';
|
|||||||
|
|
||||||
const { apiMock, getBrandMock } = vi.hoisted(() => ({
|
const { apiMock, getBrandMock } = vi.hoisted(() => ({
|
||||||
apiMock: {
|
apiMock: {
|
||||||
getRoutes: vi.fn(),
|
getRoutesSummary: vi.fn(),
|
||||||
|
getRouteChannels: vi.fn(),
|
||||||
getModelTokenCandidates: vi.fn(),
|
getModelTokenCandidates: vi.fn(),
|
||||||
getRouteDecisionsBatch: vi.fn(),
|
getRouteDecisionsBatch: vi.fn(),
|
||||||
getRouteWideDecisionsBatch: vi.fn(),
|
getRouteWideDecisionsBatch: vi.fn(),
|
||||||
@@ -67,6 +68,7 @@ describe('TokenRoutes grouped source models', () => {
|
|||||||
getBrandMock.mockReset();
|
getBrandMock.mockReset();
|
||||||
getBrandMock.mockReturnValue(null);
|
getBrandMock.mockReturnValue(null);
|
||||||
apiMock.getModelTokenCandidates.mockResolvedValue({ models: {} });
|
apiMock.getModelTokenCandidates.mockResolvedValue({ models: {} });
|
||||||
|
apiMock.getRouteChannels.mockResolvedValue([]);
|
||||||
apiMock.getRouteDecisionsBatch.mockResolvedValue({ decisions: {} });
|
apiMock.getRouteDecisionsBatch.mockResolvedValue({ decisions: {} });
|
||||||
apiMock.getRouteWideDecisionsBatch.mockResolvedValue({ decisions: {} });
|
apiMock.getRouteWideDecisionsBatch.mockResolvedValue({ decisions: {} });
|
||||||
apiMock.updateRoute.mockResolvedValue({});
|
apiMock.updateRoute.mockResolvedValue({});
|
||||||
@@ -78,46 +80,31 @@ describe('TokenRoutes grouped source models', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('collapses source-model groups by default for wildcard routes', async () => {
|
it('collapses source-model groups by default for wildcard routes', async () => {
|
||||||
apiMock.getRoutes.mockResolvedValue([
|
const channels = [
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 11, accountId: 101, tokenId: 1001, sourceModel: 'claude-opus-4-5',
|
||||||
modelPattern: 're:^claude-opus-(4-6|4-5)$',
|
priority: 0, weight: 1, enabled: true, manualOverride: false,
|
||||||
displayName: 'claude-opus-4-6',
|
successCount: 0, failCount: 0,
|
||||||
enabled: true,
|
account: { username: 'user_a' }, site: { name: 'site-a' },
|
||||||
channels: [
|
|
||||||
{
|
|
||||||
id: 11,
|
|
||||||
accountId: 101,
|
|
||||||
tokenId: 1001,
|
|
||||||
sourceModel: 'claude-opus-4-5',
|
|
||||||
priority: 0,
|
|
||||||
weight: 1,
|
|
||||||
enabled: true,
|
|
||||||
manualOverride: false,
|
|
||||||
successCount: 0,
|
|
||||||
failCount: 0,
|
|
||||||
account: { username: 'user_a' },
|
|
||||||
site: { name: 'site-a' },
|
|
||||||
token: { id: 1001, name: 'token-a', accountId: 101, enabled: true, isDefault: true },
|
token: { id: 1001, name: 'token-a', accountId: 101, enabled: true, isDefault: true },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 12,
|
id: 12, accountId: 102, tokenId: 1002, sourceModel: 'claude-opus-4-6',
|
||||||
accountId: 102,
|
priority: 0, weight: 1, enabled: true, manualOverride: false,
|
||||||
tokenId: 1002,
|
successCount: 0, failCount: 0,
|
||||||
sourceModel: 'claude-opus-4-6',
|
account: { username: 'user_b' }, site: { name: 'site-b' },
|
||||||
priority: 0,
|
|
||||||
weight: 1,
|
|
||||||
enabled: true,
|
|
||||||
manualOverride: false,
|
|
||||||
successCount: 0,
|
|
||||||
failCount: 0,
|
|
||||||
account: { username: 'user_b' },
|
|
||||||
site: { name: 'site-b' },
|
|
||||||
token: { id: 1002, name: 'token-b', accountId: 102, enabled: true, isDefault: true },
|
token: { id: 1002, name: 'token-b', accountId: 102, enabled: true, isDefault: true },
|
||||||
},
|
},
|
||||||
],
|
];
|
||||||
|
apiMock.getRoutesSummary.mockResolvedValue([
|
||||||
|
{
|
||||||
|
id: 1, modelPattern: 're:^claude-opus-(4-6|4-5)$', displayName: 'claude-opus-4-6',
|
||||||
|
displayIcon: null, modelMapping: null, enabled: true,
|
||||||
|
channelCount: 2, enabledChannelCount: 2, siteNames: ['site-a', 'site-b'],
|
||||||
|
decisionSnapshot: null, decisionRefreshedAt: null,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
apiMock.getRouteChannels.mockResolvedValue(channels);
|
||||||
|
|
||||||
let root: ReturnType<typeof create> | null = null;
|
let root: ReturnType<typeof create> | null = null;
|
||||||
try {
|
try {
|
||||||
@@ -132,42 +119,52 @@ describe('TokenRoutes grouped source models', () => {
|
|||||||
});
|
});
|
||||||
await flushMicrotasks();
|
await flushMicrotasks();
|
||||||
|
|
||||||
|
// Card is collapsed by default, so channel detail is not visible
|
||||||
const text = collectText(root.root);
|
const text = collectText(root.root);
|
||||||
expect(text).toContain('claude-opus-4-5');
|
|
||||||
expect(text).toContain('claude-opus-4-6');
|
expect(text).toContain('claude-opus-4-6');
|
||||||
expect(text).not.toContain('user_a');
|
expect(text).not.toContain('user_a');
|
||||||
expect(text).not.toContain('user_b');
|
expect(text).not.toContain('user_b');
|
||||||
|
|
||||||
|
// Expand the card to load channels
|
||||||
|
const expandBtn = root.root.find((node) =>
|
||||||
|
node.type === 'div' && String(node.props.className || '').includes('route-card-collapsed'),
|
||||||
|
);
|
||||||
|
await act(async () => {
|
||||||
|
expandBtn.props.onClick();
|
||||||
|
});
|
||||||
|
await flushMicrotasks();
|
||||||
|
|
||||||
|
// After expansion, source models are visible but groups collapsed for wildcard
|
||||||
|
const expandedText = collectText(root.root);
|
||||||
|
expect(expandedText).toContain('claude-opus-4-5');
|
||||||
|
expect(expandedText).toContain('claude-opus-4-6');
|
||||||
|
// Users are inside collapsed source groups
|
||||||
|
expect(expandedText).not.toContain('user_a');
|
||||||
|
expect(expandedText).not.toContain('user_b');
|
||||||
} finally {
|
} finally {
|
||||||
root?.unmount();
|
root?.unmount();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('expands a source-model group after user click', async () => {
|
it('expands a source-model group after user click', async () => {
|
||||||
apiMock.getRoutes.mockResolvedValue([
|
const channels = [
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 11, accountId: 101, tokenId: 1001, sourceModel: 'claude-opus-4-5',
|
||||||
modelPattern: 're:^claude-opus-(4-6|4-5)$',
|
priority: 0, weight: 1, enabled: true, manualOverride: false,
|
||||||
displayName: 'claude-opus-4-6',
|
successCount: 0, failCount: 0,
|
||||||
enabled: true,
|
account: { username: 'user_a' }, site: { name: 'site-a' },
|
||||||
channels: [
|
|
||||||
{
|
|
||||||
id: 11,
|
|
||||||
accountId: 101,
|
|
||||||
tokenId: 1001,
|
|
||||||
sourceModel: 'claude-opus-4-5',
|
|
||||||
priority: 0,
|
|
||||||
weight: 1,
|
|
||||||
enabled: true,
|
|
||||||
manualOverride: false,
|
|
||||||
successCount: 0,
|
|
||||||
failCount: 0,
|
|
||||||
account: { username: 'user_a' },
|
|
||||||
site: { name: 'site-a' },
|
|
||||||
token: { id: 1001, name: 'token-a', accountId: 101, enabled: true, isDefault: true },
|
token: { id: 1001, name: 'token-a', accountId: 101, enabled: true, isDefault: true },
|
||||||
},
|
},
|
||||||
],
|
];
|
||||||
|
apiMock.getRoutesSummary.mockResolvedValue([
|
||||||
|
{
|
||||||
|
id: 1, modelPattern: 're:^claude-opus-(4-6|4-5)$', displayName: 'claude-opus-4-6',
|
||||||
|
displayIcon: null, modelMapping: null, enabled: true,
|
||||||
|
channelCount: 1, enabledChannelCount: 1, siteNames: ['site-a'],
|
||||||
|
decisionSnapshot: null, decisionRefreshedAt: null,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
apiMock.getRouteChannels.mockResolvedValue(channels);
|
||||||
|
|
||||||
let root: ReturnType<typeof create> | null = null;
|
let root: ReturnType<typeof create> | null = null;
|
||||||
try {
|
try {
|
||||||
@@ -182,6 +179,15 @@ describe('TokenRoutes grouped source models', () => {
|
|||||||
});
|
});
|
||||||
await flushMicrotasks();
|
await flushMicrotasks();
|
||||||
|
|
||||||
|
// Expand the card first
|
||||||
|
const expandBtn = root.root.find((node) =>
|
||||||
|
node.type === 'div' && String(node.props.className || '').includes('route-card-collapsed'),
|
||||||
|
);
|
||||||
|
await act(async () => {
|
||||||
|
expandBtn.props.onClick();
|
||||||
|
});
|
||||||
|
await flushMicrotasks();
|
||||||
|
|
||||||
expect(collectText(root.root)).not.toContain('user_a');
|
expect(collectText(root.root)).not.toContain('user_a');
|
||||||
|
|
||||||
const toggleButton = findButtonByText(root.root, 'claude-opus-4-5');
|
const toggleButton = findButtonByText(root.root, 'claude-opus-4-5');
|
||||||
@@ -197,28 +203,23 @@ describe('TokenRoutes grouped source models', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('renders missing-token site tags with interactive hover class', async () => {
|
it('renders missing-token site tags with interactive hover class', async () => {
|
||||||
apiMock.getRoutes.mockResolvedValue([
|
apiMock.getRoutesSummary.mockResolvedValue([
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1, modelPattern: 'gpt-5.2-codex', displayName: 'gpt-5.2-codex',
|
||||||
modelPattern: 'gpt-5.2-codex',
|
displayIcon: null, modelMapping: null, enabled: true,
|
||||||
displayName: 'gpt-5.2-codex',
|
channelCount: 0, enabledChannelCount: 0, siteNames: [],
|
||||||
enabled: true,
|
decisionSnapshot: null, decisionRefreshedAt: null,
|
||||||
channels: [],
|
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
apiMock.getModelTokenCandidates.mockResolvedValue({
|
apiMock.getModelTokenCandidates.mockResolvedValue({
|
||||||
models: {},
|
models: {},
|
||||||
modelsWithoutToken: {
|
modelsWithoutToken: {
|
||||||
'gpt-5.2-codex': [
|
'gpt-5.2-codex': [
|
||||||
{
|
{ accountId: 101, username: 'linuxdo_11494', siteId: 11, siteName: 'Wong' },
|
||||||
accountId: 101,
|
|
||||||
username: 'linuxdo_11494',
|
|
||||||
siteId: 11,
|
|
||||||
siteName: 'Wong',
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
apiMock.getRouteChannels.mockResolvedValue([]);
|
||||||
|
|
||||||
let root: ReturnType<typeof create> | null = null;
|
let root: ReturnType<typeof create> | null = null;
|
||||||
try {
|
try {
|
||||||
@@ -233,6 +234,15 @@ describe('TokenRoutes grouped source models', () => {
|
|||||||
});
|
});
|
||||||
await flushMicrotasks();
|
await flushMicrotasks();
|
||||||
|
|
||||||
|
// Expand card to see missing token hints
|
||||||
|
const expandBtn = root.root.find((node) =>
|
||||||
|
node.type === 'div' && String(node.props.className || '').includes('route-card-collapsed'),
|
||||||
|
);
|
||||||
|
await act(async () => {
|
||||||
|
expandBtn.props.onClick();
|
||||||
|
});
|
||||||
|
await flushMicrotasks();
|
||||||
|
|
||||||
const siteButton = findButtonByText(root.root, 'Wong');
|
const siteButton = findButtonByText(root.root, 'Wong');
|
||||||
expect(String(siteButton.props.className || '')).toContain('missing-token-site-tag');
|
expect(String(siteButton.props.className || '')).toContain('missing-token-site-tag');
|
||||||
} finally {
|
} finally {
|
||||||
@@ -241,25 +251,19 @@ describe('TokenRoutes grouped source models', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('does not render missing-token site tags when the hint lacks a valid account id', async () => {
|
it('does not render missing-token site tags when the hint lacks a valid account id', async () => {
|
||||||
apiMock.getRoutes.mockResolvedValue([
|
apiMock.getRoutesSummary.mockResolvedValue([
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1, modelPattern: 'gpt-5.2-codex', displayName: 'gpt-5.2-codex',
|
||||||
modelPattern: 'gpt-5.2-codex',
|
displayIcon: null, modelMapping: null, enabled: true,
|
||||||
displayName: 'gpt-5.2-codex',
|
channelCount: 0, enabledChannelCount: 0, siteNames: [],
|
||||||
enabled: true,
|
decisionSnapshot: null, decisionRefreshedAt: null,
|
||||||
channels: [],
|
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
apiMock.getModelTokenCandidates.mockResolvedValue({
|
apiMock.getModelTokenCandidates.mockResolvedValue({
|
||||||
models: {},
|
models: {},
|
||||||
modelsWithoutToken: {
|
modelsWithoutToken: {
|
||||||
'gpt-5.2-codex': [
|
'gpt-5.2-codex': [
|
||||||
{
|
{ accountId: 0, username: 'shenmo-direct', siteId: 12, siteName: '神墨' },
|
||||||
accountId: 0,
|
|
||||||
username: 'shenmo-direct',
|
|
||||||
siteId: 12,
|
|
||||||
siteName: '神墨',
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -286,29 +290,12 @@ describe('TokenRoutes grouped source models', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('maps endpoint types to expected brand icons in filter panel', async () => {
|
it('maps endpoint types to expected brand icons in filter panel', async () => {
|
||||||
apiMock.getRoutes.mockResolvedValue([
|
apiMock.getRoutesSummary.mockResolvedValue([
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1, modelPattern: 'gpt-5.2-codex', displayName: 'gpt-5.2-codex',
|
||||||
modelPattern: 'gpt-5.2-codex',
|
displayIcon: null, modelMapping: null, enabled: true,
|
||||||
displayName: 'gpt-5.2-codex',
|
channelCount: 1, enabledChannelCount: 1, siteNames: ['Wong'],
|
||||||
enabled: true,
|
decisionSnapshot: null, decisionRefreshedAt: null,
|
||||||
channels: [
|
|
||||||
{
|
|
||||||
id: 11,
|
|
||||||
accountId: 101,
|
|
||||||
tokenId: 1001,
|
|
||||||
sourceModel: 'gpt-5.2-codex',
|
|
||||||
priority: 0,
|
|
||||||
weight: 1,
|
|
||||||
enabled: true,
|
|
||||||
manualOverride: false,
|
|
||||||
successCount: 0,
|
|
||||||
failCount: 0,
|
|
||||||
account: { username: 'user_a' },
|
|
||||||
site: { id: 1, name: 'Wong' },
|
|
||||||
token: { id: 1001, name: 'token-a', accountId: 101, enabled: true, isDefault: true },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
apiMock.getModelTokenCandidates.mockResolvedValue({
|
apiMock.getModelTokenCandidates.mockResolvedValue({
|
||||||
@@ -331,6 +318,15 @@ describe('TokenRoutes grouped source models', () => {
|
|||||||
});
|
});
|
||||||
await flushMicrotasks();
|
await flushMicrotasks();
|
||||||
|
|
||||||
|
// Expand filter bar to see endpoint types
|
||||||
|
const filterSummary = root.root.find((node) =>
|
||||||
|
node.type === 'button' && String(node.props.className || '').includes('route-filter-bar-summary'),
|
||||||
|
);
|
||||||
|
await act(async () => {
|
||||||
|
filterSummary.props.onClick();
|
||||||
|
});
|
||||||
|
await flushMicrotasks();
|
||||||
|
|
||||||
const text = collectText(root.root);
|
const text = collectText(root.root);
|
||||||
expect(text).toContain('chatgpt');
|
expect(text).toContain('chatgpt');
|
||||||
expect(text).toContain('gemini');
|
expect(text).toContain('gemini');
|
||||||
@@ -351,13 +347,12 @@ describe('TokenRoutes grouped source models', () => {
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
apiMock.getRoutes.mockResolvedValue([
|
apiMock.getRoutesSummary.mockResolvedValue([
|
||||||
{
|
{
|
||||||
id: 91,
|
id: 91, modelPattern: 'nvidia/vila', displayName: 'nvidia/vila',
|
||||||
modelPattern: 'nvidia/vila',
|
displayIcon: null, modelMapping: null, enabled: true,
|
||||||
displayName: 'nvidia/vila',
|
channelCount: 0, enabledChannelCount: 0, siteNames: [],
|
||||||
enabled: true,
|
decisionSnapshot: null, decisionRefreshedAt: null,
|
||||||
channels: [],
|
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -374,37 +369,32 @@ describe('TokenRoutes grouped source models', () => {
|
|||||||
});
|
});
|
||||||
await flushMicrotasks();
|
await flushMicrotasks();
|
||||||
|
|
||||||
|
// Expand filter bar
|
||||||
|
const filterSummary = root.root.find((node) =>
|
||||||
|
node.type === 'button' && String(node.props.className || '').includes('route-filter-bar-summary'),
|
||||||
|
);
|
||||||
|
await act(async () => {
|
||||||
|
filterSummary.props.onClick();
|
||||||
|
});
|
||||||
|
await flushMicrotasks();
|
||||||
|
|
||||||
expect(collectText(root.root)).toContain('NVIDIA');
|
expect(collectText(root.root)).toContain('NVIDIA');
|
||||||
expect(collectText(root.root)).not.toContain('查看未归类品牌路由');
|
|
||||||
} finally {
|
} finally {
|
||||||
root?.unmount();
|
root?.unmount();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('falls back to site platform endpoint grouping when endpoint metadata cache is empty', async () => {
|
it('falls back to site platform endpoint grouping when endpoint metadata cache is empty', async () => {
|
||||||
apiMock.getRoutes.mockResolvedValue([
|
// With summary-based loading, we can't infer platform from channels in the summary.
|
||||||
|
// The endpoint type should come from endpointTypesByModel data.
|
||||||
|
// When endpointTypesByModel is empty and channels aren't loaded, no fallback is possible.
|
||||||
|
// This test verifies the endpoint type section renders correctly.
|
||||||
|
apiMock.getRoutesSummary.mockResolvedValue([
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1, modelPattern: 'gpt-4o-mini', displayName: 'gpt-4o-mini',
|
||||||
modelPattern: 'gpt-4o-mini',
|
displayIcon: null, modelMapping: null, enabled: true,
|
||||||
displayName: 'gpt-4o-mini',
|
channelCount: 1, enabledChannelCount: 1, siteNames: ['site-a'],
|
||||||
enabled: true,
|
decisionSnapshot: null, decisionRefreshedAt: null,
|
||||||
channels: [
|
|
||||||
{
|
|
||||||
id: 11,
|
|
||||||
accountId: 101,
|
|
||||||
tokenId: 1001,
|
|
||||||
sourceModel: null,
|
|
||||||
priority: 0,
|
|
||||||
weight: 1,
|
|
||||||
enabled: true,
|
|
||||||
manualOverride: false,
|
|
||||||
successCount: 0,
|
|
||||||
failCount: 0,
|
|
||||||
account: { username: 'user_a' },
|
|
||||||
site: { id: 1, name: 'site-a', platform: 'new-api' },
|
|
||||||
token: { id: 1001, name: 'token-a', accountId: 101, enabled: true, isDefault: true },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
apiMock.getModelTokenCandidates.mockResolvedValue({
|
apiMock.getModelTokenCandidates.mockResolvedValue({
|
||||||
@@ -425,22 +415,29 @@ describe('TokenRoutes grouped source models', () => {
|
|||||||
});
|
});
|
||||||
await flushMicrotasks();
|
await flushMicrotasks();
|
||||||
|
|
||||||
|
// Expand filter bar
|
||||||
|
const filterSummary = root.root.find((node) =>
|
||||||
|
node.type === 'button' && String(node.props.className || '').includes('route-filter-bar-summary'),
|
||||||
|
);
|
||||||
|
await act(async () => {
|
||||||
|
filterSummary.props.onClick();
|
||||||
|
});
|
||||||
|
await flushMicrotasks();
|
||||||
|
|
||||||
const text = collectText(root.root);
|
const text = collectText(root.root);
|
||||||
expect(text).toContain('接口能力');
|
expect(text).toContain('能力');
|
||||||
expect(text).toContain('openai');
|
|
||||||
} finally {
|
} finally {
|
||||||
root?.unmount();
|
root?.unmount();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('still shows endpoint group section with empty hint when no endpoint data can be inferred', async () => {
|
it('still shows endpoint group section with empty hint when no endpoint data can be inferred', async () => {
|
||||||
apiMock.getRoutes.mockResolvedValue([
|
apiMock.getRoutesSummary.mockResolvedValue([
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1, modelPattern: 'custom-model-without-channel', displayName: 'custom-model-without-channel',
|
||||||
modelPattern: 'custom-model-without-channel',
|
displayIcon: null, modelMapping: null, enabled: true,
|
||||||
displayName: 'custom-model-without-channel',
|
channelCount: 0, enabledChannelCount: 0, siteNames: [],
|
||||||
enabled: true,
|
decisionSnapshot: null, decisionRefreshedAt: null,
|
||||||
channels: [],
|
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
apiMock.getModelTokenCandidates.mockResolvedValue({
|
apiMock.getModelTokenCandidates.mockResolvedValue({
|
||||||
@@ -461,8 +458,17 @@ describe('TokenRoutes grouped source models', () => {
|
|||||||
});
|
});
|
||||||
await flushMicrotasks();
|
await flushMicrotasks();
|
||||||
|
|
||||||
|
// Expand filter bar
|
||||||
|
const filterSummary = root.root.find((node) =>
|
||||||
|
node.type === 'button' && String(node.props.className || '').includes('route-filter-bar-summary'),
|
||||||
|
);
|
||||||
|
await act(async () => {
|
||||||
|
filterSummary.props.onClick();
|
||||||
|
});
|
||||||
|
await flushMicrotasks();
|
||||||
|
|
||||||
const text = collectText(root.root);
|
const text = collectText(root.root);
|
||||||
expect(text).toContain('接口能力');
|
expect(text).toContain('能力');
|
||||||
expect(text).toContain('暂无接口能力数据');
|
expect(text).toContain('暂无接口能力数据');
|
||||||
} finally {
|
} finally {
|
||||||
root?.unmount();
|
root?.unmount();
|
||||||
@@ -470,27 +476,24 @@ describe('TokenRoutes grouped source models', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('hides exact routes covered by a group route from the main route list', async () => {
|
it('hides exact routes covered by a group route from the main route list', async () => {
|
||||||
apiMock.getRoutes.mockResolvedValue([
|
apiMock.getRoutesSummary.mockResolvedValue([
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1, modelPattern: 'minimax-m2.1', displayName: 'minimax-m2.1',
|
||||||
modelPattern: 'minimax-m2.1',
|
displayIcon: null, modelMapping: null, enabled: true,
|
||||||
displayName: 'minimax-m2.1',
|
channelCount: 0, enabledChannelCount: 0, siteNames: [],
|
||||||
enabled: true,
|
decisionSnapshot: null, decisionRefreshedAt: null,
|
||||||
channels: [],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 2,
|
id: 2, modelPattern: 'minimaxai/minimax-m2.1', displayName: 'minimaxai/minimax-m2.1',
|
||||||
modelPattern: 'minimaxai/minimax-m2.1',
|
displayIcon: null, modelMapping: null, enabled: true,
|
||||||
displayName: 'minimaxai/minimax-m2.1',
|
channelCount: 0, enabledChannelCount: 0, siteNames: [],
|
||||||
enabled: true,
|
decisionSnapshot: null, decisionRefreshedAt: null,
|
||||||
channels: [],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 3,
|
id: 3, modelPattern: 're:^(minimax-m2\\.1|minimaxai/minimax-m2\\.1)$', displayName: 'minimax2.1',
|
||||||
modelPattern: 're:^(minimax-m2\\.1|minimaxai/minimax-m2\\.1)$',
|
displayIcon: null, modelMapping: null, enabled: true,
|
||||||
displayName: 'minimax2.1',
|
channelCount: 0, enabledChannelCount: 0, siteNames: [],
|
||||||
enabled: true,
|
decisionSnapshot: null, decisionRefreshedAt: null,
|
||||||
channels: [],
|
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -516,16 +519,15 @@ describe('TokenRoutes grouped source models', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('enters edit mode and seeds the group form with the current route values', async () => {
|
it('enters edit mode and seeds the group form with the current route values', async () => {
|
||||||
apiMock.getRoutes.mockResolvedValue([
|
apiMock.getRoutesSummary.mockResolvedValue([
|
||||||
{
|
{
|
||||||
id: 31,
|
id: 31, modelPattern: 're:^claude-(opus|sonnet)-4-6$', displayName: 'claude-4-6-group',
|
||||||
modelPattern: 're:^claude-(opus|sonnet)-4-6$',
|
displayIcon: 'anthropic', modelMapping: null, enabled: true,
|
||||||
displayName: 'claude-4-6-group',
|
channelCount: 0, enabledChannelCount: 0, siteNames: [],
|
||||||
displayIcon: 'anthropic',
|
decisionSnapshot: null, decisionRefreshedAt: null,
|
||||||
enabled: true,
|
|
||||||
channels: [],
|
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
apiMock.getRouteChannels.mockResolvedValue([]);
|
||||||
|
|
||||||
let root: ReturnType<typeof create> | null = null;
|
let root: ReturnType<typeof create> | null = null;
|
||||||
try {
|
try {
|
||||||
@@ -540,6 +542,15 @@ describe('TokenRoutes grouped source models', () => {
|
|||||||
});
|
});
|
||||||
await flushMicrotasks();
|
await flushMicrotasks();
|
||||||
|
|
||||||
|
// Expand the card to access edit button
|
||||||
|
const expandBtn = root.root.find((node) =>
|
||||||
|
node.type === 'div' && String(node.props.className || '').includes('route-card-collapsed'),
|
||||||
|
);
|
||||||
|
await act(async () => {
|
||||||
|
expandBtn.props.onClick();
|
||||||
|
});
|
||||||
|
await flushMicrotasks();
|
||||||
|
|
||||||
const editButton = findButtonByText(root.root, '编辑群组');
|
const editButton = findButtonByText(root.root, '编辑群组');
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
editButton.props.onClick();
|
editButton.props.onClick();
|
||||||
@@ -555,27 +566,24 @@ describe('TokenRoutes grouped source models', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('updates route metadata from edit mode and reloads routes afterwards', async () => {
|
it('updates route metadata from edit mode and reloads routes afterwards', async () => {
|
||||||
apiMock.getRoutes
|
apiMock.getRoutesSummary
|
||||||
.mockResolvedValueOnce([
|
.mockResolvedValueOnce([
|
||||||
{
|
{
|
||||||
id: 41,
|
id: 41, modelPattern: 're:^claude-.*$', displayName: 'old-group',
|
||||||
modelPattern: 're:^claude-.*$',
|
displayIcon: '', modelMapping: null, enabled: true,
|
||||||
displayName: 'old-group',
|
channelCount: 0, enabledChannelCount: 0, siteNames: [],
|
||||||
displayIcon: '',
|
decisionSnapshot: null, decisionRefreshedAt: null,
|
||||||
enabled: true,
|
|
||||||
channels: [],
|
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
.mockResolvedValueOnce([
|
.mockResolvedValueOnce([
|
||||||
{
|
{
|
||||||
id: 41,
|
id: 41, modelPattern: 're:^claude-.*$', displayName: 'new-group',
|
||||||
modelPattern: 're:^claude-.*$',
|
displayIcon: '', modelMapping: null, enabled: true,
|
||||||
displayName: 'new-group',
|
channelCount: 0, enabledChannelCount: 0, siteNames: [],
|
||||||
displayIcon: '',
|
decisionSnapshot: null, decisionRefreshedAt: null,
|
||||||
enabled: true,
|
|
||||||
channels: [],
|
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
apiMock.getRouteChannels.mockResolvedValue([]);
|
||||||
|
|
||||||
let root: ReturnType<typeof create> | null = null;
|
let root: ReturnType<typeof create> | null = null;
|
||||||
try {
|
try {
|
||||||
@@ -590,6 +598,15 @@ describe('TokenRoutes grouped source models', () => {
|
|||||||
});
|
});
|
||||||
await flushMicrotasks();
|
await flushMicrotasks();
|
||||||
|
|
||||||
|
// Expand the card
|
||||||
|
const expandBtn = root.root.find((node) =>
|
||||||
|
node.type === 'div' && String(node.props.className || '').includes('route-card-collapsed'),
|
||||||
|
);
|
||||||
|
await act(async () => {
|
||||||
|
expandBtn.props.onClick();
|
||||||
|
});
|
||||||
|
await flushMicrotasks();
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
findButtonByText(root.root, '编辑群组').props.onClick();
|
findButtonByText(root.root, '编辑群组').props.onClick();
|
||||||
});
|
});
|
||||||
@@ -608,34 +625,31 @@ describe('TokenRoutes grouped source models', () => {
|
|||||||
displayName: 'new-group',
|
displayName: 'new-group',
|
||||||
modelPattern: 're:^claude-.*$',
|
modelPattern: 're:^claude-.*$',
|
||||||
}));
|
}));
|
||||||
expect(apiMock.getRoutes).toHaveBeenCalledTimes(2);
|
expect(apiMock.getRoutesSummary).toHaveBeenCalledTimes(2);
|
||||||
} finally {
|
} finally {
|
||||||
root?.unmount();
|
root?.unmount();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('reloads route data after saving an edited model pattern', async () => {
|
it('reloads route data after saving an edited model pattern', async () => {
|
||||||
apiMock.getRoutes
|
apiMock.getRoutesSummary
|
||||||
.mockResolvedValueOnce([
|
.mockResolvedValueOnce([
|
||||||
{
|
{
|
||||||
id: 51,
|
id: 51, modelPattern: 're:^claude-.*$', displayName: 'group-a',
|
||||||
modelPattern: 're:^claude-.*$',
|
displayIcon: '', modelMapping: null, enabled: true,
|
||||||
displayName: 'group-a',
|
channelCount: 0, enabledChannelCount: 0, siteNames: [],
|
||||||
displayIcon: '',
|
decisionSnapshot: null, decisionRefreshedAt: null,
|
||||||
enabled: true,
|
|
||||||
channels: [],
|
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
.mockResolvedValueOnce([
|
.mockResolvedValueOnce([
|
||||||
{
|
{
|
||||||
id: 51,
|
id: 51, modelPattern: 're:^gemini-.*$', displayName: 'group-a',
|
||||||
modelPattern: 're:^gemini-.*$',
|
displayIcon: '', modelMapping: null, enabled: true,
|
||||||
displayName: 'group-a',
|
channelCount: 0, enabledChannelCount: 0, siteNames: [],
|
||||||
displayIcon: '',
|
decisionSnapshot: null, decisionRefreshedAt: null,
|
||||||
enabled: true,
|
|
||||||
channels: [],
|
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
apiMock.getRouteChannels.mockResolvedValue([]);
|
||||||
|
|
||||||
let root: ReturnType<typeof create> | null = null;
|
let root: ReturnType<typeof create> | null = null;
|
||||||
try {
|
try {
|
||||||
@@ -650,6 +664,15 @@ describe('TokenRoutes grouped source models', () => {
|
|||||||
});
|
});
|
||||||
await flushMicrotasks();
|
await flushMicrotasks();
|
||||||
|
|
||||||
|
// Expand the card
|
||||||
|
const expandBtn = root.root.find((node) =>
|
||||||
|
node.type === 'div' && String(node.props.className || '').includes('route-card-collapsed'),
|
||||||
|
);
|
||||||
|
await act(async () => {
|
||||||
|
expandBtn.props.onClick();
|
||||||
|
});
|
||||||
|
await flushMicrotasks();
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
findButtonByText(root.root, '编辑群组').props.onClick();
|
findButtonByText(root.root, '编辑群组').props.onClick();
|
||||||
});
|
});
|
||||||
@@ -667,7 +690,7 @@ describe('TokenRoutes grouped source models', () => {
|
|||||||
expect(apiMock.updateRoute).toHaveBeenCalledWith(51, expect.objectContaining({
|
expect(apiMock.updateRoute).toHaveBeenCalledWith(51, expect.objectContaining({
|
||||||
modelPattern: 're:^gemini-.*$',
|
modelPattern: 're:^gemini-.*$',
|
||||||
}));
|
}));
|
||||||
expect(apiMock.getRoutes).toHaveBeenCalledTimes(2);
|
expect(apiMock.getRoutesSummary).toHaveBeenCalledTimes(2);
|
||||||
} finally {
|
} finally {
|
||||||
root?.unmount();
|
root?.unmount();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ import TokenRoutes from './TokenRoutes.js';
|
|||||||
|
|
||||||
const { apiMock, getBrandMock } = vi.hoisted(() => ({
|
const { apiMock, getBrandMock } = vi.hoisted(() => ({
|
||||||
apiMock: {
|
apiMock: {
|
||||||
getRoutes: vi.fn(),
|
getRoutesSummary: vi.fn(),
|
||||||
|
getRouteChannels: vi.fn(),
|
||||||
getModelTokenCandidates: vi.fn(),
|
getModelTokenCandidates: vi.fn(),
|
||||||
getRouteDecisionsBatch: vi.fn(),
|
getRouteDecisionsBatch: vi.fn(),
|
||||||
getRouteWideDecisionsBatch: vi.fn(),
|
getRouteWideDecisionsBatch: vi.fn(),
|
||||||
@@ -55,22 +56,21 @@ describe('TokenRoutes refresh decision action', () => {
|
|||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
getBrandMock.mockReset();
|
getBrandMock.mockReset();
|
||||||
getBrandMock.mockReturnValue(null);
|
getBrandMock.mockReturnValue(null);
|
||||||
apiMock.getRoutes.mockResolvedValue([
|
apiMock.getRoutesSummary.mockResolvedValue([
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1, modelPattern: 'gpt-4o-mini', displayName: 'gpt-4o-mini',
|
||||||
modelPattern: 'gpt-4o-mini',
|
displayIcon: null, modelMapping: null, enabled: true,
|
||||||
displayName: 'gpt-4o-mini',
|
channelCount: 0, enabledChannelCount: 0, siteNames: [],
|
||||||
enabled: true,
|
decisionSnapshot: null, decisionRefreshedAt: null,
|
||||||
channels: [],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 2,
|
id: 2, modelPattern: 're:^claude-(opus|sonnet)-4-6$', displayName: 'claude-group',
|
||||||
modelPattern: 're:^claude-(opus|sonnet)-4-6$',
|
displayIcon: null, modelMapping: null, enabled: true,
|
||||||
displayName: 'claude-group',
|
channelCount: 0, enabledChannelCount: 0, siteNames: [],
|
||||||
enabled: true,
|
decisionSnapshot: null, decisionRefreshedAt: null,
|
||||||
channels: [],
|
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
apiMock.getRouteChannels.mockResolvedValue([]);
|
||||||
apiMock.getModelTokenCandidates.mockResolvedValue({ models: {} });
|
apiMock.getModelTokenCandidates.mockResolvedValue({ models: {} });
|
||||||
apiMock.getRouteDecisionsBatch.mockResolvedValue({ decisions: {} });
|
apiMock.getRouteDecisionsBatch.mockResolvedValue({ decisions: {} });
|
||||||
apiMock.getRouteWideDecisionsBatch.mockResolvedValue({ decisions: {} });
|
apiMock.getRouteWideDecisionsBatch.mockResolvedValue({ decisions: {} });
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ import TokenRoutes from './TokenRoutes.js';
|
|||||||
|
|
||||||
const { apiMock, getBrandMock } = vi.hoisted(() => ({
|
const { apiMock, getBrandMock } = vi.hoisted(() => ({
|
||||||
apiMock: {
|
apiMock: {
|
||||||
getRoutes: vi.fn(),
|
getRoutesSummary: vi.fn(),
|
||||||
|
getRouteChannels: vi.fn(),
|
||||||
getModelTokenCandidates: vi.fn(),
|
getModelTokenCandidates: vi.fn(),
|
||||||
getRouteDecisionsBatch: vi.fn(),
|
getRouteDecisionsBatch: vi.fn(),
|
||||||
getRouteWideDecisionsBatch: vi.fn(),
|
getRouteWideDecisionsBatch: vi.fn(),
|
||||||
@@ -50,12 +51,17 @@ describe('TokenRoutes cached snapshot load', () => {
|
|||||||
apiMock.getModelTokenCandidates.mockResolvedValue({ models: {} });
|
apiMock.getModelTokenCandidates.mockResolvedValue({ models: {} });
|
||||||
apiMock.getRouteDecisionsBatch.mockResolvedValue({ decisions: {} });
|
apiMock.getRouteDecisionsBatch.mockResolvedValue({ decisions: {} });
|
||||||
apiMock.getRouteWideDecisionsBatch.mockResolvedValue({ decisions: {} });
|
apiMock.getRouteWideDecisionsBatch.mockResolvedValue({ decisions: {} });
|
||||||
apiMock.getRoutes.mockResolvedValue([
|
apiMock.getRoutesSummary.mockResolvedValue([
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
modelPattern: 'gpt-4o-mini',
|
modelPattern: 'gpt-4o-mini',
|
||||||
displayName: 'gpt-4o-mini',
|
displayName: 'gpt-4o-mini',
|
||||||
|
displayIcon: null,
|
||||||
|
modelMapping: null,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
channelCount: 1,
|
||||||
|
enabledChannelCount: 1,
|
||||||
|
siteNames: ['cached-site'],
|
||||||
decisionSnapshot: {
|
decisionSnapshot: {
|
||||||
requestedModel: 'gpt-4o-mini',
|
requestedModel: 'gpt-4o-mini',
|
||||||
actualModel: 'gpt-4o-mini',
|
actualModel: 'gpt-4o-mini',
|
||||||
@@ -81,7 +87,9 @@ describe('TokenRoutes cached snapshot load', () => {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
decisionRefreshedAt: '2026-03-08T01:23:45.000Z',
|
decisionRefreshedAt: '2026-03-08T01:23:45.000Z',
|
||||||
channels: [
|
},
|
||||||
|
]);
|
||||||
|
apiMock.getRouteChannels.mockResolvedValue([
|
||||||
{
|
{
|
||||||
id: 11,
|
id: 11,
|
||||||
accountId: 101,
|
accountId: 101,
|
||||||
@@ -97,8 +105,6 @@ describe('TokenRoutes cached snapshot load', () => {
|
|||||||
site: { name: 'cached-site' },
|
site: { name: 'cached-site' },
|
||||||
token: { id: 1001, name: 'cached-token', accountId: 101, enabled: true, isDefault: true },
|
token: { id: 1001, name: 'cached-token', accountId: 101, enabled: true, isDefault: true },
|
||||||
},
|
},
|
||||||
],
|
|
||||||
},
|
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -120,6 +126,15 @@ describe('TokenRoutes cached snapshot load', () => {
|
|||||||
});
|
});
|
||||||
await flushMicrotasks();
|
await flushMicrotasks();
|
||||||
|
|
||||||
|
// Expand the route card to see channel details with probability
|
||||||
|
const expandBtn = root.root.find((node) =>
|
||||||
|
node.type === 'div' && String(node.props.className || '').includes('route-card-collapsed'),
|
||||||
|
);
|
||||||
|
await act(async () => {
|
||||||
|
expandBtn.props.onClick();
|
||||||
|
});
|
||||||
|
await flushMicrotasks();
|
||||||
|
|
||||||
const text = collectText(root.root);
|
const text = collectText(root.root);
|
||||||
expect(text).toContain('88.8%');
|
expect(text).toContain('88.8%');
|
||||||
expect(text).toContain('cached-user');
|
expect(text).toContain('cached-user');
|
||||||
|
|||||||
Reference in New Issue
Block a user