Add unit tests for channel utilities and configure testing environment

- Created a new test file `channels.test.ts` to cover utilities related to channel configurations and targets.
- Implemented tests for normalizing and grouping selected channels by type, as well as building channel targets from account data and cron history.
- Mocked necessary dependencies to isolate tests and ensure accurate results.
- Updated `vite.config.ts` to set up the testing environment with jsdom and enable global variables for tests.
This commit is contained in:
duanshuwen
2026-04-18 16:12:49 +08:00
parent ee72cf7261
commit ef46c73c3e
26 changed files with 4056 additions and 186 deletions

View File

@@ -2,6 +2,11 @@ import { CONFIG_KEYS } from '@runtime/lib/constants';
import { normalizeAgentId, type AgentsSnapshot } from '@runtime/lib/models';
import configManager from '@electron/service/config-service';
import { listCronJobs } from './cron-store';
import { listStoredChannelAccountRecords } from './channel-config';
import {
buildChannelStatusSummary,
inferChannelConnectionStatus,
} from './channel-status';
import type {
ChannelAccountCatalogGroup,
ChannelConnectionStatus,
@@ -23,6 +28,12 @@ export interface LocalChannelAccount {
channelName: string;
channelUrl: string;
label: string;
configured: boolean;
enabled: boolean;
channelEnabled: boolean;
status: ChannelConnectionStatus;
isDefault: boolean;
lastError?: string;
ownerAgentId: string | null;
ownerAgentName: string | null;
bindingScope: 'account' | 'channel' | null;
@@ -280,32 +291,93 @@ export function getSelectedChannelsConfig(): SelectedChannelConfigItem[] {
}
export function listSelectedChannelAccounts(snapshot?: Pick<AgentsSnapshot, 'agents' | 'channelOwners' | 'channelAccountOwners'>): LocalChannelAccount[] {
const channels = getSelectedChannelsConfig();
const agentNameById = new Map<string, string>(
Array.isArray(snapshot?.agents)
? snapshot.agents.map((agent) => [normalizeAgentId(agent.id), agent.name || normalizeAgentId(agent.id)])
: [],
);
const accounts = new Map<string, LocalChannelAccount>();
return channels.map((item) => {
for (const record of listStoredChannelAccountRecords()) {
const accountOwnerKey = `${record.channelType}:${record.accountId}`;
const accountOwnerId = snapshot?.channelAccountOwners?.[accountOwnerKey];
const channelOwnerId = snapshot?.channelOwners?.[record.channelType];
const ownerAgentId = accountOwnerId || channelOwnerId || null;
const normalizedOwnerId = ownerAgentId ? normalizeAgentId(ownerAgentId) : null;
const key = `${record.channelType}:${record.accountId}`;
const status = inferChannelConnectionStatus({
configured: true,
channelUrl: record.channelUrl ?? '',
status: record.channelEnabled && record.accountEnabled ? 'connected' : 'disconnected',
hasBinding: Boolean(accountOwnerId || channelOwnerId),
degraded: !record.channelEnabled || !record.accountEnabled,
});
accounts.set(key, {
id: record.accountId,
accountId: record.accountId,
channelType: record.channelType,
channelName: record.channelLabel,
channelUrl: record.channelUrl ?? '',
label: record.accountName || record.accountId,
configured: true,
enabled: record.channelEnabled && record.accountEnabled,
channelEnabled: record.channelEnabled,
status,
isDefault: record.accountId === record.defaultAccountId,
lastError: status === 'error'
? '渠道链接格式无效'
: undefined,
ownerAgentId: normalizedOwnerId,
ownerAgentName: normalizedOwnerId ? agentNameById.get(normalizedOwnerId) ?? null : null,
bindingScope: accountOwnerId ? 'account' : channelOwnerId ? 'channel' : null,
});
}
const legacyChannels = getSelectedChannelsConfig();
for (const item of legacyChannels) {
const channelType = inferChannelType(item);
const key = `${channelType}:${item.id}`;
if (accounts.has(key)) continue;
const accountOwnerKey = `${channelType}:${item.id}`;
const accountOwnerId = snapshot?.channelAccountOwners?.[accountOwnerKey];
const channelOwnerId = snapshot?.channelOwners?.[channelType];
const ownerAgentId = accountOwnerId || channelOwnerId || null;
const normalizedOwnerId = ownerAgentId ? normalizeAgentId(ownerAgentId) : null;
const status = inferChannelConnectionStatus({
configured: true,
channelUrl: item.channelUrl,
status: ownerAgentId ? 'connected' : undefined,
hasBinding: Boolean(accountOwnerId || channelOwnerId),
});
return {
accounts.set(key, {
id: item.id,
accountId: item.id,
channelType,
channelName: item.channelName,
channelUrl: item.channelUrl,
label: item.channelName,
configured: true,
enabled: true,
channelEnabled: true,
status,
isDefault: false,
lastError: status === 'error'
? '渠道链接格式无效'
: undefined,
ownerAgentId: normalizedOwnerId,
ownerAgentName: normalizedOwnerId ? agentNameById.get(normalizedOwnerId) ?? null : null,
bindingScope: accountOwnerId ? 'account' : channelOwnerId ? 'channel' : null,
};
});
}
return Array.from(accounts.values()).sort((left, right) => {
if (left.channelName !== right.channelName) {
return left.channelName.localeCompare(right.channelName, 'zh-CN');
}
return left.label.localeCompare(right.label, 'zh-CN');
});
}
@@ -319,22 +391,38 @@ export function listSelectedChannelAccountGroups(
const existing = groups.get(account.channelType) ?? {
channelType: account.channelType,
channelLabel: account.channelName || formatChannelLabel(account.channelType),
defaultAccountId: account.accountId,
status: 'connected' as ChannelConnectionStatus,
defaultAccountId: account.isDefault ? account.accountId : '',
enabled: account.channelEnabled,
status: account.status,
accounts: [],
};
existing.enabled = existing.enabled !== false && account.channelEnabled;
existing.accounts.push({
accountId: account.accountId,
name: account.label || account.channelName || account.accountId,
configured: true,
status: 'connected',
isDefault: false,
configured: account.configured,
enabled: account.enabled,
status: account.status,
lastError: account.lastError,
isDefault: account.isDefault,
agentId: account.ownerAgentId ?? undefined,
bindingScope: account.bindingScope ?? undefined,
channelUrl: account.channelUrl,
});
if (!existing.defaultAccountId && account.isDefault) {
existing.defaultAccountId = account.accountId;
}
if (existing.status !== 'error' && account.status === 'error') {
existing.status = 'error';
} else if (existing.status === 'disconnected' && account.status !== 'disconnected') {
existing.status = account.status;
} else if (existing.status !== 'connected' && account.status === 'connected') {
existing.status = 'connected';
}
groups.set(account.channelType, existing);
}
@@ -342,13 +430,20 @@ export function listSelectedChannelAccountGroups(
.map((group) => {
const sortedAccounts = [...group.accounts].sort((left, right) => left.name.localeCompare(right.name));
const defaultAccountId = group.defaultAccountId || sortedAccounts[0]?.accountId || 'default';
const status = buildChannelStatusSummary([
{
...group,
accounts: sortedAccounts,
},
]).status;
return {
...group,
defaultAccountId,
status,
accounts: sortedAccounts.map((account) => ({
...account,
isDefault: account.accountId === defaultAccountId,
isDefault: account.isDefault || account.accountId === defaultAccountId,
})),
};
})