feat(cron): enable WeChat as a supported delivery channel (#789)

This commit is contained in:
paisley
2026-04-07 18:56:54 +08:00
committed by GitHub
Unverified
parent 97d29ab23c
commit 3021ad5089
10 changed files with 92 additions and 23 deletions

View File

@@ -158,7 +158,7 @@ describe('handleCronRoutes', () => {
});
});
it('rejects WeChat scheduled delivery because the plugin requires a live context token', async () => {
it('allows WeChat scheduled delivery', async () => {
parseJsonBodyMock.mockResolvedValue({
name: 'WeChat delivery',
message: 'Send update',
@@ -172,7 +172,17 @@ describe('handleCronRoutes', () => {
enabled: true,
});
const rpc = vi.fn();
const rpc = vi.fn().mockResolvedValue({
id: 'job-wechat',
name: 'WeChat delivery',
enabled: true,
createdAtMs: 1,
updatedAtMs: 2,
schedule: { kind: 'cron', expr: '0 10 * * *' },
payload: { kind: 'agentTurn', message: 'Send update' },
delivery: { mode: 'announce', channel: 'openclaw-weixin', to: 'wechat:wxid_target', accountId: 'wechat-bot' },
state: {},
});
const { handleCronRoutes } = await import('@electron/api/routes/cron');
const handled = await handleCronRoutes(
@@ -185,13 +195,14 @@ describe('handleCronRoutes', () => {
);
expect(handled).toBe(true);
expect(rpc).not.toHaveBeenCalled();
expect(rpc).toHaveBeenCalledWith('cron.add', expect.objectContaining({
delivery: expect.objectContaining({ mode: 'announce', to: 'wechat:wxid_target' }),
}));
expect(sendJsonMock).toHaveBeenCalledWith(
expect.anything(),
400,
200,
expect.objectContaining({
success: false,
error: expect.stringContaining('WeChat scheduled delivery is not supported'),
id: 'job-wechat',
}),
);
});

View File

@@ -1,5 +1,6 @@
import { describe, expect, it } from 'vitest';
import {
getGatewayStartupRecoveryAction,
hasInvalidConfigFailureSignal,
isInvalidConfigSignal,
shouldAttemptConfigAutoRepair,
@@ -50,3 +51,62 @@ describe('gateway startup recovery heuristics', () => {
});
});
describe('getGatewayStartupRecoveryAction', () => {
const configInvalidStderr = ['Config invalid', 'Run: openclaw doctor --fix'];
const transientError = new Error('Gateway process exited before becoming ready (code=1)');
it('returns repair on first config-invalid failure', () => {
const action = getGatewayStartupRecoveryAction({
startupError: transientError,
startupStderrLines: configInvalidStderr,
configRepairAttempted: false,
attempt: 1,
maxAttempts: 3,
});
expect(action).toBe('repair');
});
it('returns retry when repair was attempted but error is still transient', () => {
const action = getGatewayStartupRecoveryAction({
startupError: transientError,
startupStderrLines: configInvalidStderr,
configRepairAttempted: true,
attempt: 1,
maxAttempts: 3,
});
expect(action).toBe('retry');
});
it('returns retry for transient errors after successful repair (no config signal)', () => {
const action = getGatewayStartupRecoveryAction({
startupError: transientError,
startupStderrLines: ['Gateway process exited (code=1, expected=no)'],
configRepairAttempted: true,
attempt: 1,
maxAttempts: 3,
});
expect(action).toBe('retry');
});
it('returns fail when max attempts exceeded even for transient errors', () => {
const action = getGatewayStartupRecoveryAction({
startupError: transientError,
startupStderrLines: [],
configRepairAttempted: false,
attempt: 3,
maxAttempts: 3,
});
expect(action).toBe('fail');
});
it('returns fail for non-transient, non-config errors', () => {
const action = getGatewayStartupRecoveryAction({
startupError: new Error('Unknown fatal error'),
startupStderrLines: [],
configRepairAttempted: false,
attempt: 1,
maxAttempts: 3,
});
expect(action).toBe('fail');
});
});