feat: unify cron delivery account and target selection (#642)
This commit is contained in:
committed by
GitHub
Unverified
parent
9aea3c9441
commit
9d40e1fa05
@@ -1,11 +1,16 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import type { IncomingMessage, ServerResponse } from 'http';
|
||||
import { mkdirSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { tmpdir } from 'node:os';
|
||||
|
||||
const listConfiguredChannelsMock = vi.fn();
|
||||
const listConfiguredChannelAccountsMock = vi.fn();
|
||||
const readOpenClawConfigMock = vi.fn();
|
||||
const listAgentsSnapshotMock = vi.fn();
|
||||
const sendJsonMock = vi.fn();
|
||||
const proxyAwareFetchMock = vi.fn();
|
||||
const testOpenClawConfigDir = join(tmpdir(), 'clawx-tests', 'channel-routes-openclaw');
|
||||
|
||||
vi.mock('@electron/utils/channel-config', () => ({
|
||||
cleanupDanglingWeChatPluginState: vi.fn(),
|
||||
@@ -56,9 +61,19 @@ vi.mock('@electron/api/route-utils', () => ({
|
||||
sendJson: (...args: unknown[]) => sendJsonMock(...args),
|
||||
}));
|
||||
|
||||
vi.mock('@electron/utils/paths', () => ({
|
||||
getOpenClawConfigDir: () => testOpenClawConfigDir,
|
||||
}));
|
||||
|
||||
vi.mock('@electron/utils/proxy-fetch', () => ({
|
||||
proxyAwareFetch: (...args: unknown[]) => proxyAwareFetchMock(...args),
|
||||
}));
|
||||
|
||||
describe('handleChannelRoutes', () => {
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
rmSync(testOpenClawConfigDir, { recursive: true, force: true });
|
||||
proxyAwareFetchMock.mockReset();
|
||||
listAgentsSnapshotMock.mockResolvedValue({
|
||||
agents: [],
|
||||
channelAccountOwners: {},
|
||||
@@ -68,6 +83,10 @@ describe('handleChannelRoutes', () => {
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
rmSync(testOpenClawConfigDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('reports healthy running multi-account channels as connected', async () => {
|
||||
listConfiguredChannelsMock.mockResolvedValue(['feishu']);
|
||||
listConfiguredChannelAccountsMock.mockResolvedValue({
|
||||
@@ -235,4 +254,333 @@ describe('handleChannelRoutes', () => {
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('lists known QQ Bot targets for a configured account', async () => {
|
||||
const knownUsersPath = join(testOpenClawConfigDir, 'qqbot', 'data');
|
||||
mkdirSync(knownUsersPath, { recursive: true });
|
||||
writeFileSync(join(knownUsersPath, 'known-users.json'), JSON.stringify([
|
||||
{
|
||||
openid: '207A5B8339D01F6582911C014668B77B',
|
||||
type: 'c2c',
|
||||
nickname: 'Alice',
|
||||
accountId: 'default',
|
||||
lastSeenAt: 200,
|
||||
},
|
||||
{
|
||||
openid: 'member-openid',
|
||||
type: 'group',
|
||||
nickname: 'Weather Group',
|
||||
groupOpenid: 'GROUP_OPENID_123',
|
||||
accountId: 'default',
|
||||
lastSeenAt: 100,
|
||||
},
|
||||
]), 'utf8');
|
||||
|
||||
const { handleChannelRoutes } = await import('@electron/api/routes/channels');
|
||||
const handled = await handleChannelRoutes(
|
||||
{ method: 'GET' } as IncomingMessage,
|
||||
{} as ServerResponse,
|
||||
new URL('http://127.0.0.1:3210/api/channels/targets?channelType=qqbot&accountId=default'),
|
||||
{
|
||||
gatewayManager: {
|
||||
rpc: vi.fn(),
|
||||
getStatus: () => ({ state: 'running' }),
|
||||
debouncedReload: vi.fn(),
|
||||
debouncedRestart: vi.fn(),
|
||||
},
|
||||
} as never,
|
||||
);
|
||||
|
||||
expect(handled).toBe(true);
|
||||
expect(sendJsonMock).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
200,
|
||||
expect.objectContaining({
|
||||
success: true,
|
||||
channelType: 'qqbot',
|
||||
accountId: 'default',
|
||||
targets: [
|
||||
expect.objectContaining({
|
||||
value: 'qqbot:c2c:207A5B8339D01F6582911C014668B77B',
|
||||
kind: 'user',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
value: 'qqbot:group:GROUP_OPENID_123',
|
||||
kind: 'group',
|
||||
}),
|
||||
],
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('lists Feishu targets for a configured account', async () => {
|
||||
readOpenClawConfigMock.mockResolvedValue({
|
||||
channels: {
|
||||
feishu: {
|
||||
appId: 'cli_app_id',
|
||||
appSecret: 'cli_app_secret',
|
||||
allowFrom: ['ou_config_user'],
|
||||
groups: {
|
||||
oc_config_group: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
proxyAwareFetchMock.mockImplementation(async (url: string, init?: RequestInit) => {
|
||||
if (url.includes('/tenant_access_token/internal')) {
|
||||
const body = JSON.parse(String(init?.body || '{}')) as { app_id?: string };
|
||||
if (body.app_id === 'cli_app_id') {
|
||||
return {
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
code: 0,
|
||||
tenant_access_token: 'tenant-token',
|
||||
}),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (url.includes('/applications/cli_app_id')) {
|
||||
return {
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
code: 0,
|
||||
data: {
|
||||
app: {
|
||||
creator_id: 'ou_owner',
|
||||
owner: {
|
||||
owner_type: 2,
|
||||
owner_id: 'ou_owner',
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
if (url.includes('/contact/v3/users')) {
|
||||
return {
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
code: 0,
|
||||
data: {
|
||||
items: [
|
||||
{ open_id: 'ou_live_user', name: 'Alice Feishu' },
|
||||
],
|
||||
},
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
if (url.includes('/im/v1/chats')) {
|
||||
return {
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
code: 0,
|
||||
data: {
|
||||
items: [
|
||||
{ chat_id: 'oc_live_chat', name: 'Project Chat' },
|
||||
],
|
||||
},
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
throw new Error(`Unexpected fetch: ${url}`);
|
||||
});
|
||||
|
||||
const { handleChannelRoutes } = await import('@electron/api/routes/channels');
|
||||
const handled = await handleChannelRoutes(
|
||||
{ method: 'GET' } as IncomingMessage,
|
||||
{} as ServerResponse,
|
||||
new URL('http://127.0.0.1:3210/api/channels/targets?channelType=feishu&accountId=default'),
|
||||
{
|
||||
gatewayManager: {
|
||||
rpc: vi.fn(),
|
||||
getStatus: () => ({ state: 'running' }),
|
||||
debouncedReload: vi.fn(),
|
||||
debouncedRestart: vi.fn(),
|
||||
},
|
||||
} as never,
|
||||
);
|
||||
|
||||
expect(handled).toBe(true);
|
||||
expect(sendJsonMock).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
200,
|
||||
expect.objectContaining({
|
||||
success: true,
|
||||
channelType: 'feishu',
|
||||
accountId: 'default',
|
||||
targets: expect.arrayContaining([
|
||||
expect.objectContaining({ value: 'user:ou_owner', kind: 'user' }),
|
||||
expect.objectContaining({ value: 'user:ou_live_user', kind: 'user' }),
|
||||
expect.objectContaining({ value: 'chat:oc_live_chat', kind: 'group' }),
|
||||
]),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('lists WeCom targets from reqid cache and session history', async () => {
|
||||
mkdirSync(join(testOpenClawConfigDir, 'wecom'), { recursive: true });
|
||||
writeFileSync(
|
||||
join(testOpenClawConfigDir, 'wecom', 'reqid-map-default.json'),
|
||||
JSON.stringify({
|
||||
'chat-alpha': { reqId: 'req-1', ts: 100 },
|
||||
}),
|
||||
'utf8',
|
||||
);
|
||||
mkdirSync(join(testOpenClawConfigDir, 'agents', 'main', 'sessions'), { recursive: true });
|
||||
writeFileSync(
|
||||
join(testOpenClawConfigDir, 'agents', 'main', 'sessions', 'sessions.json'),
|
||||
JSON.stringify({
|
||||
'agent:main:wecom:chat-bravo': {
|
||||
updatedAt: 200,
|
||||
chatType: 'group',
|
||||
displayName: 'Ops Group',
|
||||
deliveryContext: {
|
||||
channel: 'wecom',
|
||||
accountId: 'default',
|
||||
to: 'wecom:chat-bravo',
|
||||
},
|
||||
},
|
||||
}),
|
||||
'utf8',
|
||||
);
|
||||
|
||||
const { handleChannelRoutes } = await import('@electron/api/routes/channels');
|
||||
const handled = await handleChannelRoutes(
|
||||
{ method: 'GET' } as IncomingMessage,
|
||||
{} as ServerResponse,
|
||||
new URL('http://127.0.0.1:3210/api/channels/targets?channelType=wecom&accountId=default'),
|
||||
{
|
||||
gatewayManager: {
|
||||
rpc: vi.fn(),
|
||||
getStatus: () => ({ state: 'running' }),
|
||||
debouncedReload: vi.fn(),
|
||||
debouncedRestart: vi.fn(),
|
||||
},
|
||||
} as never,
|
||||
);
|
||||
|
||||
expect(handled).toBe(true);
|
||||
expect(sendJsonMock).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
200,
|
||||
expect.objectContaining({
|
||||
success: true,
|
||||
channelType: 'wecom',
|
||||
accountId: 'default',
|
||||
targets: expect.arrayContaining([
|
||||
expect.objectContaining({ value: 'wecom:chat-bravo', kind: 'group' }),
|
||||
expect.objectContaining({ value: 'wecom:chat-alpha', kind: 'channel' }),
|
||||
]),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('lists DingTalk targets from session history', async () => {
|
||||
mkdirSync(join(testOpenClawConfigDir, 'agents', 'main', 'sessions'), { recursive: true });
|
||||
writeFileSync(
|
||||
join(testOpenClawConfigDir, 'agents', 'main', 'sessions', 'sessions.json'),
|
||||
JSON.stringify({
|
||||
'agent:main:dingtalk:cid-group': {
|
||||
updatedAt: 300,
|
||||
chatType: 'group',
|
||||
displayName: 'DingTalk Dev Group',
|
||||
deliveryContext: {
|
||||
channel: 'dingtalk',
|
||||
accountId: 'default',
|
||||
to: 'cidDeVGroup=',
|
||||
},
|
||||
},
|
||||
}),
|
||||
'utf8',
|
||||
);
|
||||
|
||||
const { handleChannelRoutes } = await import('@electron/api/routes/channels');
|
||||
const handled = await handleChannelRoutes(
|
||||
{ method: 'GET' } as IncomingMessage,
|
||||
{} as ServerResponse,
|
||||
new URL('http://127.0.0.1:3210/api/channels/targets?channelType=dingtalk&accountId=default'),
|
||||
{
|
||||
gatewayManager: {
|
||||
rpc: vi.fn(),
|
||||
getStatus: () => ({ state: 'running' }),
|
||||
debouncedReload: vi.fn(),
|
||||
debouncedRestart: vi.fn(),
|
||||
},
|
||||
} as never,
|
||||
);
|
||||
|
||||
expect(handled).toBe(true);
|
||||
expect(sendJsonMock).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
200,
|
||||
expect.objectContaining({
|
||||
success: true,
|
||||
channelType: 'dingtalk',
|
||||
accountId: 'default',
|
||||
targets: [
|
||||
expect.objectContaining({
|
||||
value: 'cidDeVGroup=',
|
||||
kind: 'group',
|
||||
}),
|
||||
],
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('lists WeChat targets from session history via the UI alias', async () => {
|
||||
mkdirSync(join(testOpenClawConfigDir, 'agents', 'main', 'sessions'), { recursive: true });
|
||||
writeFileSync(
|
||||
join(testOpenClawConfigDir, 'agents', 'main', 'sessions', 'sessions.json'),
|
||||
JSON.stringify({
|
||||
'agent:main:wechat:wxid_target': {
|
||||
updatedAt: 400,
|
||||
chatType: 'direct',
|
||||
displayName: 'Alice WeChat',
|
||||
deliveryContext: {
|
||||
channel: 'openclaw-weixin',
|
||||
accountId: 'wechat-bot',
|
||||
to: 'wechat:wxid_target',
|
||||
},
|
||||
},
|
||||
}),
|
||||
'utf8',
|
||||
);
|
||||
|
||||
const { handleChannelRoutes } = await import('@electron/api/routes/channels');
|
||||
const handled = await handleChannelRoutes(
|
||||
{ method: 'GET' } as IncomingMessage,
|
||||
{} as ServerResponse,
|
||||
new URL('http://127.0.0.1:3210/api/channels/targets?channelType=wechat&accountId=wechat-bot'),
|
||||
{
|
||||
gatewayManager: {
|
||||
rpc: vi.fn(),
|
||||
getStatus: () => ({ state: 'running' }),
|
||||
debouncedReload: vi.fn(),
|
||||
debouncedRestart: vi.fn(),
|
||||
},
|
||||
} as never,
|
||||
);
|
||||
|
||||
expect(handled).toBe(true);
|
||||
expect(sendJsonMock).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
200,
|
||||
expect.objectContaining({
|
||||
success: true,
|
||||
channelType: 'wechat',
|
||||
accountId: 'wechat-bot',
|
||||
targets: [
|
||||
expect.objectContaining({
|
||||
value: 'wechat:wxid_target',
|
||||
kind: 'user',
|
||||
}),
|
||||
],
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user