feat: unify cron delivery account and target selection (#642)
This commit is contained in:
committed by
GitHub
Unverified
parent
9aea3c9441
commit
9d40e1fa05
@@ -18,25 +18,29 @@ const mockElectron = {
|
||||
isDev: true,
|
||||
};
|
||||
|
||||
Object.defineProperty(window, 'electron', {
|
||||
value: mockElectron,
|
||||
writable: true,
|
||||
});
|
||||
if (typeof window !== 'undefined') {
|
||||
Object.defineProperty(window, 'electron', {
|
||||
value: mockElectron,
|
||||
writable: true,
|
||||
});
|
||||
}
|
||||
|
||||
// Mock matchMedia
|
||||
Object.defineProperty(window, 'matchMedia', {
|
||||
writable: true,
|
||||
value: vi.fn().mockImplementation((query) => ({
|
||||
matches: false,
|
||||
media: query,
|
||||
onchange: null,
|
||||
addListener: vi.fn(),
|
||||
removeListener: vi.fn(),
|
||||
addEventListener: vi.fn(),
|
||||
removeEventListener: vi.fn(),
|
||||
dispatchEvent: vi.fn(),
|
||||
})),
|
||||
});
|
||||
if (typeof window !== 'undefined') {
|
||||
Object.defineProperty(window, 'matchMedia', {
|
||||
writable: true,
|
||||
value: vi.fn().mockImplementation((query) => ({
|
||||
matches: false,
|
||||
media: query,
|
||||
onchange: null,
|
||||
addListener: vi.fn(),
|
||||
removeListener: vi.fn(),
|
||||
addEventListener: vi.fn(),
|
||||
removeEventListener: vi.fn(),
|
||||
dispatchEvent: vi.fn(),
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
// Reset mocks after each test
|
||||
afterEach(() => {
|
||||
|
||||
@@ -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',
|
||||
}),
|
||||
],
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
198
tests/unit/cron-routes.test.ts
Normal file
198
tests/unit/cron-routes.test.ts
Normal file
@@ -0,0 +1,198 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import type { IncomingMessage, ServerResponse } from 'http';
|
||||
|
||||
const parseJsonBodyMock = vi.fn();
|
||||
const sendJsonMock = vi.fn();
|
||||
|
||||
vi.mock('@electron/api/route-utils', () => ({
|
||||
parseJsonBody: (...args: unknown[]) => parseJsonBodyMock(...args),
|
||||
sendJson: (...args: unknown[]) => sendJsonMock(...args),
|
||||
}));
|
||||
|
||||
describe('handleCronRoutes', () => {
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
it('creates cron jobs with external delivery configuration', async () => {
|
||||
parseJsonBodyMock.mockResolvedValue({
|
||||
name: 'Weather delivery',
|
||||
message: 'Summarize today',
|
||||
schedule: '0 9 * * *',
|
||||
delivery: {
|
||||
mode: 'announce',
|
||||
channel: 'feishu',
|
||||
to: 'user:ou_weather',
|
||||
},
|
||||
enabled: true,
|
||||
});
|
||||
|
||||
const rpc = vi.fn().mockResolvedValue({
|
||||
id: 'job-1',
|
||||
name: 'Weather delivery',
|
||||
enabled: true,
|
||||
createdAtMs: 1,
|
||||
updatedAtMs: 2,
|
||||
schedule: { kind: 'cron', expr: '0 9 * * *' },
|
||||
payload: { kind: 'agentTurn', message: 'Summarize today' },
|
||||
delivery: { mode: 'announce', channel: 'feishu', to: 'user:ou_weather' },
|
||||
state: {},
|
||||
});
|
||||
|
||||
const { handleCronRoutes } = await import('@electron/api/routes/cron');
|
||||
const handled = await handleCronRoutes(
|
||||
{ method: 'POST' } as IncomingMessage,
|
||||
{} as ServerResponse,
|
||||
new URL('http://127.0.0.1:3210/api/cron/jobs'),
|
||||
{
|
||||
gatewayManager: { rpc },
|
||||
} as never,
|
||||
);
|
||||
|
||||
expect(handled).toBe(true);
|
||||
expect(rpc).toHaveBeenCalledWith('cron.add', expect.objectContaining({
|
||||
delivery: { mode: 'announce', channel: 'feishu', to: 'user:ou_weather' },
|
||||
}));
|
||||
expect(sendJsonMock).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
200,
|
||||
expect.objectContaining({
|
||||
id: 'job-1',
|
||||
delivery: { mode: 'announce', channel: 'feishu', to: 'user:ou_weather' },
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('updates cron jobs with transformed payload and delivery fields', async () => {
|
||||
parseJsonBodyMock.mockResolvedValue({
|
||||
message: 'Updated prompt',
|
||||
delivery: {
|
||||
mode: 'announce',
|
||||
channel: 'feishu',
|
||||
to: 'user:ou_next',
|
||||
},
|
||||
});
|
||||
|
||||
const rpc = vi.fn().mockResolvedValue({
|
||||
id: 'job-2',
|
||||
name: 'Updated job',
|
||||
enabled: true,
|
||||
createdAtMs: 1,
|
||||
updatedAtMs: 3,
|
||||
schedule: { kind: 'cron', expr: '0 9 * * *' },
|
||||
payload: { kind: 'agentTurn', message: 'Updated prompt' },
|
||||
delivery: { mode: 'announce', channel: 'feishu', to: 'user:ou_next' },
|
||||
state: {},
|
||||
});
|
||||
|
||||
const { handleCronRoutes } = await import('@electron/api/routes/cron');
|
||||
await handleCronRoutes(
|
||||
{ method: 'PUT' } as IncomingMessage,
|
||||
{} as ServerResponse,
|
||||
new URL('http://127.0.0.1:3210/api/cron/jobs/job-2'),
|
||||
{
|
||||
gatewayManager: { rpc },
|
||||
} as never,
|
||||
);
|
||||
|
||||
expect(rpc).toHaveBeenCalledWith('cron.update', {
|
||||
id: 'job-2',
|
||||
patch: {
|
||||
payload: { kind: 'agentTurn', message: 'Updated prompt' },
|
||||
delivery: { mode: 'announce', channel: 'feishu', to: 'user:ou_next' },
|
||||
},
|
||||
});
|
||||
expect(sendJsonMock).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
200,
|
||||
expect.objectContaining({
|
||||
id: 'job-2',
|
||||
message: 'Updated prompt',
|
||||
delivery: { mode: 'announce', channel: 'feishu', to: 'user:ou_next' },
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('passes through delivery.accountId for multi-account cron jobs', async () => {
|
||||
parseJsonBodyMock.mockResolvedValue({
|
||||
delivery: {
|
||||
mode: 'announce',
|
||||
channel: 'feishu',
|
||||
to: 'user:ou_owner',
|
||||
accountId: 'feishu-0d009958',
|
||||
},
|
||||
});
|
||||
|
||||
const rpc = vi.fn().mockResolvedValue({
|
||||
id: 'job-account',
|
||||
name: 'Account job',
|
||||
enabled: true,
|
||||
createdAtMs: 1,
|
||||
updatedAtMs: 4,
|
||||
schedule: { kind: 'cron', expr: '0 9 * * *' },
|
||||
payload: { kind: 'agentTurn', message: 'Prompt' },
|
||||
delivery: { mode: 'announce', channel: 'feishu', accountId: 'feishu-0d009958', to: 'user:ou_owner' },
|
||||
state: {},
|
||||
});
|
||||
|
||||
const { handleCronRoutes } = await import('@electron/api/routes/cron');
|
||||
await handleCronRoutes(
|
||||
{ method: 'PUT' } as IncomingMessage,
|
||||
{} as ServerResponse,
|
||||
new URL('http://127.0.0.1:3210/api/cron/jobs/job-account'),
|
||||
{
|
||||
gatewayManager: { rpc },
|
||||
} as never,
|
||||
);
|
||||
|
||||
expect(rpc).toHaveBeenCalledWith('cron.update', {
|
||||
id: 'job-account',
|
||||
patch: {
|
||||
delivery: {
|
||||
mode: 'announce',
|
||||
channel: 'feishu',
|
||||
to: 'user:ou_owner',
|
||||
accountId: 'feishu-0d009958',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects WeChat scheduled delivery because the plugin requires a live context token', async () => {
|
||||
parseJsonBodyMock.mockResolvedValue({
|
||||
name: 'WeChat delivery',
|
||||
message: 'Send update',
|
||||
schedule: '0 10 * * *',
|
||||
delivery: {
|
||||
mode: 'announce',
|
||||
channel: 'wechat',
|
||||
to: 'wechat:wxid_target',
|
||||
accountId: 'wechat-bot',
|
||||
},
|
||||
enabled: true,
|
||||
});
|
||||
|
||||
const rpc = vi.fn();
|
||||
|
||||
const { handleCronRoutes } = await import('@electron/api/routes/cron');
|
||||
const handled = await handleCronRoutes(
|
||||
{ method: 'POST' } as IncomingMessage,
|
||||
{} as ServerResponse,
|
||||
new URL('http://127.0.0.1:3210/api/cron/jobs'),
|
||||
{
|
||||
gatewayManager: { rpc },
|
||||
} as never,
|
||||
);
|
||||
|
||||
expect(handled).toBe(true);
|
||||
expect(rpc).not.toHaveBeenCalled();
|
||||
expect(sendJsonMock).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
400,
|
||||
expect.objectContaining({
|
||||
success: false,
|
||||
error: expect.stringContaining('WeChat scheduled delivery is not supported'),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,3 +1,4 @@
|
||||
// @vitest-environment node
|
||||
import { readFile, rm } from 'fs/promises';
|
||||
import { join } from 'path';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
Reference in New Issue
Block a user