test: remove obsolete test files for channels and knowledge page
This commit is contained in:
@@ -1,440 +0,0 @@
|
|||||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
||||||
|
|
||||||
const mocks = vi.hoisted(() => ({
|
|
||||||
configGet: vi.fn(),
|
|
||||||
listStoredChannelAccountRecords: vi.fn(),
|
|
||||||
listCronJobs: vi.fn(),
|
|
||||||
listAgentsSnapshot: vi.fn(),
|
|
||||||
assignChannelToAgent: vi.fn(),
|
|
||||||
clearAllChannelBindings: vi.fn(),
|
|
||||||
clearChannelBinding: vi.fn(),
|
|
||||||
listAgentsSnapshotResult: {
|
|
||||||
agents: [],
|
|
||||||
channelOwners: {},
|
|
||||||
channelAccountOwners: {},
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock('@electron/service/config-service', () => ({
|
|
||||||
default: {
|
|
||||||
get: mocks.configGet,
|
|
||||||
},
|
|
||||||
configManager: {
|
|
||||||
get: mocks.configGet,
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock('../electron/utils/cron-store', () => ({
|
|
||||||
listCronJobs: mocks.listCronJobs,
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock('../electron/utils/channel-config', () => ({
|
|
||||||
listStoredChannelAccountRecords: mocks.listStoredChannelAccountRecords,
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock('../electron/utils/agent-config', () => ({
|
|
||||||
assignChannelToAgent: mocks.assignChannelToAgent,
|
|
||||||
clearAllChannelBindings: mocks.clearAllChannelBindings,
|
|
||||||
clearChannelBinding: mocks.clearChannelBinding,
|
|
||||||
listAgentsSnapshot: mocks.listAgentsSnapshot,
|
|
||||||
}));
|
|
||||||
|
|
||||||
import {
|
|
||||||
getSelectedChannelsConfig,
|
|
||||||
listSelectedChannelAccountGroups,
|
|
||||||
listSelectedChannelTargets,
|
|
||||||
} from '../electron/utils/channels';
|
|
||||||
import { normalizeRequest } from '../electron/api/route-utils';
|
|
||||||
import { handleChannelRoutes } from '../electron/api/routes/channels';
|
|
||||||
|
|
||||||
function createRouteContext() {
|
|
||||||
const notifyRuntimeChanged = vi.fn();
|
|
||||||
return {
|
|
||||||
gatewayManager: {
|
|
||||||
notifyRuntimeChanged,
|
|
||||||
},
|
|
||||||
providerApiService: {
|
|
||||||
getAccounts: () => [],
|
|
||||||
getDefault: () => ({ accountId: 'default' }),
|
|
||||||
},
|
|
||||||
mainWindow: null,
|
|
||||||
notifyRuntimeChanged,
|
|
||||||
} as any;
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('channels utilities', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
mocks.configGet.mockReset();
|
|
||||||
mocks.listStoredChannelAccountRecords.mockReset();
|
|
||||||
mocks.listCronJobs.mockReset();
|
|
||||||
mocks.listAgentsSnapshot.mockReset();
|
|
||||||
mocks.assignChannelToAgent.mockReset();
|
|
||||||
mocks.clearAllChannelBindings.mockReset();
|
|
||||||
mocks.clearChannelBinding.mockReset();
|
|
||||||
mocks.listAgentsSnapshot.mockReturnValue(mocks.listAgentsSnapshotResult);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('normalizes and groups selected channels by inferred channel type', () => {
|
|
||||||
mocks.configGet.mockReturnValue([
|
|
||||||
{
|
|
||||||
id: ' acct-1 ',
|
|
||||||
channelName: ' 抖音 ',
|
|
||||||
channelUrl: ' https://life.douyin.com/live ',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'acct-1',
|
|
||||||
channelName: '抖音直播',
|
|
||||||
channelUrl: 'https://douyin.com/duplicate',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'alpha',
|
|
||||||
channelName: 'Alpha Store',
|
|
||||||
channelUrl: 'https://meituan.com/a',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'beta',
|
|
||||||
channelName: 'Beta Store',
|
|
||||||
channelUrl: 'https://me.meituan.com/b',
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
expect(getSelectedChannelsConfig()).toEqual([
|
|
||||||
{
|
|
||||||
id: 'acct-1',
|
|
||||||
channelName: '抖音',
|
|
||||||
channelUrl: 'https://life.douyin.com/live',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'alpha',
|
|
||||||
channelName: 'Alpha Store',
|
|
||||||
channelUrl: 'https://meituan.com/a',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'beta',
|
|
||||||
channelName: 'Beta Store',
|
|
||||||
channelUrl: 'https://me.meituan.com/b',
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
mocks.listStoredChannelAccountRecords.mockReturnValue([
|
|
||||||
{
|
|
||||||
channelType: 'douyin',
|
|
||||||
channelLabel: '抖音',
|
|
||||||
defaultAccountId: 'acct-1',
|
|
||||||
channelEnabled: true,
|
|
||||||
accountId: 'acct-1',
|
|
||||||
accountName: '抖音',
|
|
||||||
accountEnabled: true,
|
|
||||||
channelUrl: 'https://life.douyin.com/live',
|
|
||||||
config: {},
|
|
||||||
metadata: {},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
channelType: 'meituan',
|
|
||||||
channelLabel: 'Alpha Store',
|
|
||||||
defaultAccountId: 'alpha',
|
|
||||||
channelEnabled: true,
|
|
||||||
accountId: 'alpha',
|
|
||||||
accountName: 'Alpha Store',
|
|
||||||
accountEnabled: true,
|
|
||||||
channelUrl: 'https://meituan.com/a',
|
|
||||||
config: {},
|
|
||||||
metadata: {},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
channelType: 'meituan',
|
|
||||||
channelLabel: 'Alpha Store',
|
|
||||||
defaultAccountId: 'alpha',
|
|
||||||
channelEnabled: true,
|
|
||||||
accountId: 'beta',
|
|
||||||
accountName: 'Beta Store',
|
|
||||||
accountEnabled: true,
|
|
||||||
channelUrl: 'https://me.meituan.com/b',
|
|
||||||
config: {},
|
|
||||||
metadata: {},
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
const groups = listSelectedChannelAccountGroups({
|
|
||||||
agents: [
|
|
||||||
{ id: 'Agent-A', name: 'Ada' },
|
|
||||||
{ id: 'agent-b', name: 'Ben' },
|
|
||||||
],
|
|
||||||
channelOwners: {
|
|
||||||
meituan: 'Agent-A',
|
|
||||||
},
|
|
||||||
channelAccountOwners: {
|
|
||||||
'meituan:alpha': 'agent-b',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const byType = Object.fromEntries(groups.map((group) => [group.channelType, group]));
|
|
||||||
|
|
||||||
expect(byType.douyin).toMatchObject({
|
|
||||||
channelType: 'douyin',
|
|
||||||
channelLabel: '抖音',
|
|
||||||
defaultAccountId: 'acct-1',
|
|
||||||
status: 'degraded',
|
|
||||||
accounts: [
|
|
||||||
{
|
|
||||||
accountId: 'acct-1',
|
|
||||||
name: '抖音',
|
|
||||||
configured: true,
|
|
||||||
status: 'degraded',
|
|
||||||
isDefault: true,
|
|
||||||
channelUrl: 'https://life.douyin.com/live',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(byType.meituan).toMatchObject({
|
|
||||||
channelType: 'meituan',
|
|
||||||
channelLabel: 'Alpha Store',
|
|
||||||
defaultAccountId: 'alpha',
|
|
||||||
status: 'connected',
|
|
||||||
accounts: [
|
|
||||||
{
|
|
||||||
accountId: 'alpha',
|
|
||||||
name: 'Alpha Store',
|
|
||||||
configured: true,
|
|
||||||
status: 'connected',
|
|
||||||
isDefault: true,
|
|
||||||
agentId: 'agent-b',
|
|
||||||
bindingScope: 'account',
|
|
||||||
channelUrl: 'https://meituan.com/a',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accountId: 'beta',
|
|
||||||
name: 'Beta Store',
|
|
||||||
configured: true,
|
|
||||||
status: 'connected',
|
|
||||||
isDefault: false,
|
|
||||||
agentId: 'agent-a',
|
|
||||||
bindingScope: 'channel',
|
|
||||||
channelUrl: 'https://me.meituan.com/b',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('builds channel targets from account data, URL hints, and cron history', () => {
|
|
||||||
mocks.listStoredChannelAccountRecords.mockReturnValue([
|
|
||||||
{
|
|
||||||
id: 'acct-1',
|
|
||||||
channelType: 'douyin',
|
|
||||||
channelLabel: '抖音直播间',
|
|
||||||
defaultAccountId: 'acct-1',
|
|
||||||
channelEnabled: true,
|
|
||||||
accountId: 'acct-1',
|
|
||||||
accountName: '抖音直播间',
|
|
||||||
accountEnabled: true,
|
|
||||||
channelUrl: 'https://webhook.example.com/send?roomId=ROOM-9#panel?threadId=TH-1',
|
|
||||||
config: {},
|
|
||||||
metadata: {},
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
mocks.listCronJobs.mockReturnValue([
|
|
||||||
{
|
|
||||||
id: 'cron-1',
|
|
||||||
name: 'nightly announce',
|
|
||||||
message: 'hello',
|
|
||||||
schedule: '* * * * *',
|
|
||||||
enabled: true,
|
|
||||||
createdAt: '2026-04-18T00:00:00.000Z',
|
|
||||||
updatedAt: '2026-04-18T00:00:00.000Z',
|
|
||||||
delivery: {
|
|
||||||
mode: 'announce',
|
|
||||||
channel: 'douyin',
|
|
||||||
accountId: 'acct-1',
|
|
||||||
to: 'https://history.example.com/announce',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'cron-2',
|
|
||||||
name: 'ignored',
|
|
||||||
message: 'skip me',
|
|
||||||
schedule: '* * * * *',
|
|
||||||
enabled: true,
|
|
||||||
createdAt: '2026-04-18T00:00:00.000Z',
|
|
||||||
updatedAt: '2026-04-18T00:00:00.000Z',
|
|
||||||
delivery: {
|
|
||||||
mode: 'announce',
|
|
||||||
channel: 'fliggy',
|
|
||||||
to: 'should-not-appear',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
const targets = listSelectedChannelTargets('douyin', 'acct-1');
|
|
||||||
|
|
||||||
expect(targets[0]).toMatchObject({
|
|
||||||
value: '抖音直播间',
|
|
||||||
kind: 'name',
|
|
||||||
source: 'channel-name',
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(targets).toContainEqual(expect.objectContaining({
|
|
||||||
value: 'acct-1',
|
|
||||||
label: '账号 ID · acct-1',
|
|
||||||
kind: 'identifier',
|
|
||||||
source: 'account-id',
|
|
||||||
channelType: 'douyin',
|
|
||||||
accountId: 'acct-1',
|
|
||||||
}));
|
|
||||||
|
|
||||||
expect(targets).toContainEqual(expect.objectContaining({
|
|
||||||
value: 'ROOM-9',
|
|
||||||
label: 'Room ID · ROOM-9',
|
|
||||||
kind: 'identifier',
|
|
||||||
source: 'query-param',
|
|
||||||
channelType: 'douyin',
|
|
||||||
accountId: 'acct-1',
|
|
||||||
}));
|
|
||||||
|
|
||||||
expect(targets).toContainEqual(expect.objectContaining({
|
|
||||||
value: 'TH-1',
|
|
||||||
label: 'Thread ID · TH-1',
|
|
||||||
kind: 'identifier',
|
|
||||||
source: 'hash-param',
|
|
||||||
channelType: 'douyin',
|
|
||||||
accountId: 'acct-1',
|
|
||||||
}));
|
|
||||||
|
|
||||||
expect(targets).toContainEqual(expect.objectContaining({
|
|
||||||
kind: 'webhook',
|
|
||||||
source: 'channel-url',
|
|
||||||
channelType: 'douyin',
|
|
||||||
accountId: 'acct-1',
|
|
||||||
}));
|
|
||||||
|
|
||||||
expect(targets).toContainEqual(expect.objectContaining({
|
|
||||||
value: 'https://history.example.com/announce',
|
|
||||||
kind: 'webhook',
|
|
||||||
source: 'fallback',
|
|
||||||
channelType: 'douyin',
|
|
||||||
accountId: 'acct-1',
|
|
||||||
}));
|
|
||||||
|
|
||||||
const filtered = listSelectedChannelTargets('douyin', 'acct-1', 'history');
|
|
||||||
expect(filtered).toHaveLength(1);
|
|
||||||
expect(filtered[0]).toMatchObject({
|
|
||||||
value: 'https://history.example.com/announce',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('channel credential validation route', () => {
|
|
||||||
it('returns a valid result with warnings for a token/password/url payload', async () => {
|
|
||||||
const ctx = createRouteContext();
|
|
||||||
|
|
||||||
const response = await handleChannelRoutes(normalizeRequest({
|
|
||||||
path: '/api/channels/credentials/validate',
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify({
|
|
||||||
channelType: 'imessage',
|
|
||||||
accountId: 'acct-1',
|
|
||||||
credentials: {
|
|
||||||
serverUrl: 'http://bridge.example.com',
|
|
||||||
password: 'bridge-password',
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
}), ctx);
|
|
||||||
|
|
||||||
expect(response?.ok).toBe(true);
|
|
||||||
expect(response?.json).toMatchObject({
|
|
||||||
success: true,
|
|
||||||
valid: true,
|
|
||||||
errors: [],
|
|
||||||
warnings: ['Server URL 使用了不安全的 http URL,建议改用 https'],
|
|
||||||
details: expect.objectContaining({
|
|
||||||
channelType: 'imessage',
|
|
||||||
accountId: 'acct-1',
|
|
||||||
connectionType: 'token',
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(response?.json).toMatchObject({
|
|
||||||
details: expect.objectContaining({
|
|
||||||
fields: expect.arrayContaining([
|
|
||||||
expect.objectContaining({
|
|
||||||
key: 'serverUrl',
|
|
||||||
kind: 'url',
|
|
||||||
provided: true,
|
|
||||||
valid: true,
|
|
||||||
warnings: ['Server URL 使用了不安全的 http URL,建议改用 https'],
|
|
||||||
}),
|
|
||||||
expect.objectContaining({
|
|
||||||
key: 'password',
|
|
||||||
kind: 'password',
|
|
||||||
provided: true,
|
|
||||||
valid: true,
|
|
||||||
}),
|
|
||||||
]),
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(ctx.notifyRuntimeChanged).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns field errors for missing token and text credentials', async () => {
|
|
||||||
const ctx = createRouteContext();
|
|
||||||
|
|
||||||
const response = await handleChannelRoutes(normalizeRequest({
|
|
||||||
path: '/api/channels/credentials/validate',
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify({
|
|
||||||
channelType: 'discord',
|
|
||||||
credentials: {
|
|
||||||
token: ' ',
|
|
||||||
guildId: '',
|
|
||||||
channelId: '123456789012345678',
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
}), ctx);
|
|
||||||
|
|
||||||
expect(response?.ok).toBe(true);
|
|
||||||
expect(response?.json).toMatchObject({
|
|
||||||
success: true,
|
|
||||||
valid: false,
|
|
||||||
warnings: [],
|
|
||||||
errors: [
|
|
||||||
'Bot Token is required',
|
|
||||||
'Guild ID is required',
|
|
||||||
],
|
|
||||||
details: expect.objectContaining({
|
|
||||||
channelType: 'discord',
|
|
||||||
connectionType: 'token',
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(response?.json).toMatchObject({
|
|
||||||
details: expect.objectContaining({
|
|
||||||
fields: expect.arrayContaining([
|
|
||||||
expect.objectContaining({
|
|
||||||
key: 'token',
|
|
||||||
kind: 'password',
|
|
||||||
provided: false,
|
|
||||||
valid: false,
|
|
||||||
errors: ['Bot Token is required'],
|
|
||||||
}),
|
|
||||||
expect.objectContaining({
|
|
||||||
key: 'guildId',
|
|
||||||
kind: 'text',
|
|
||||||
provided: false,
|
|
||||||
valid: false,
|
|
||||||
errors: ['Guild ID is required'],
|
|
||||||
}),
|
|
||||||
expect.objectContaining({
|
|
||||||
key: 'channelId',
|
|
||||||
kind: 'text',
|
|
||||||
provided: true,
|
|
||||||
valid: true,
|
|
||||||
}),
|
|
||||||
]),
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(ctx.notifyRuntimeChanged).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
|
||||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
||||||
|
|
||||||
const apiMocks = vi.hoisted(() => ({
|
|
||||||
list: vi.fn(),
|
|
||||||
upload: vi.fn(),
|
|
||||||
delete: vi.fn(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock('../src/lib/knowledge-docs-api', () => ({
|
|
||||||
knowledgeDocsApi: apiMocks,
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock('../src/pages/Knowledge/copy', () => ({
|
|
||||||
useKnowledgeCopy: () => (path: string, params?: Record<string, string>, fallback?: string) => {
|
|
||||||
const dictionary: Record<string, string> = {
|
|
||||||
'knowledge.title': 'Knowledge Docs',
|
|
||||||
'knowledge.subtitle': 'Manage docs',
|
|
||||||
'knowledge.documentsLabel': 'Documents',
|
|
||||||
'knowledge.storageLabel': 'Storage',
|
|
||||||
'knowledge.refresh': 'Refresh',
|
|
||||||
'knowledge.upload': 'Upload',
|
|
||||||
'knowledge.status.loading': 'Loading documents...',
|
|
||||||
'knowledge.status.uploading': 'Uploading...',
|
|
||||||
'knowledge.status.deleting': 'Deleting...',
|
|
||||||
'knowledge.status.uploadSuccess': 'Uploaded',
|
|
||||||
'knowledge.status.deleteSuccess': 'Deleted',
|
|
||||||
'knowledge.emptyTitle': 'No documents yet',
|
|
||||||
'knowledge.emptyDescription': 'Upload a file',
|
|
||||||
'knowledge.deleteDialog.title': 'Delete document?',
|
|
||||||
'knowledge.deleteDialog.confirm': 'Delete document',
|
|
||||||
'knowledge.deleteDialog.close': 'Close dialog',
|
|
||||||
'knowledge.common.cancel': 'Cancel',
|
|
||||||
'knowledge.table.name': 'Name',
|
|
||||||
'knowledge.table.size': 'Size',
|
|
||||||
'knowledge.table.modifiedAt': 'Modified At',
|
|
||||||
'knowledge.table.type': 'Type',
|
|
||||||
'knowledge.table.actions': 'Actions',
|
|
||||||
'knowledge.table.delete': 'Delete',
|
|
||||||
};
|
|
||||||
|
|
||||||
if (path === 'knowledge.status.failed') {
|
|
||||||
return `Knowledge docs request failed: ${params?.error ?? ''}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (path === 'knowledge.deleteDialog.description') {
|
|
||||||
return `This will permanently remove "${params?.name ?? ''}" from the local knowledge docs directory.`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return dictionary[path] ?? fallback ?? path;
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
import KnowledgePage from '../src/pages/Knowledge';
|
|
||||||
|
|
||||||
describe('KnowledgePage', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
apiMocks.list.mockReset();
|
|
||||||
apiMocks.upload.mockReset();
|
|
||||||
apiMocks.delete.mockReset();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('loads and renders docs from the knowledge docs api', async () => {
|
|
||||||
apiMocks.list.mockResolvedValue([
|
|
||||||
{
|
|
||||||
name: 'guide.md',
|
|
||||||
size: 1024,
|
|
||||||
modifiedAt: '2026-04-18T10:00:00.000Z',
|
|
||||||
type: 'md',
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
render(<KnowledgePage />);
|
|
||||||
|
|
||||||
expect(await screen.findByText('guide.md')).toBeTruthy();
|
|
||||||
expect(screen.getByText('MD')).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('deletes a doc after confirmation', async () => {
|
|
||||||
apiMocks.list.mockResolvedValue([
|
|
||||||
{
|
|
||||||
name: 'guide.md',
|
|
||||||
size: 1024,
|
|
||||||
modifiedAt: '2026-04-18T10:00:00.000Z',
|
|
||||||
type: 'md',
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
apiMocks.delete.mockResolvedValue(undefined);
|
|
||||||
|
|
||||||
render(<KnowledgePage />);
|
|
||||||
|
|
||||||
const deleteButton = await screen.findByRole('button', { name: /delete/i });
|
|
||||||
fireEvent.click(deleteButton);
|
|
||||||
fireEvent.click(screen.getByRole('button', { name: 'Delete document' }));
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(apiMocks.delete).toHaveBeenCalledWith('guide.md');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
Reference in New Issue
Block a user