feat: update desktop workflows and app center
This commit is contained in:
@@ -3,15 +3,31 @@ import type { IncomingMessage, ServerResponse } from 'http';
|
||||
|
||||
const parseJsonBodyMock = vi.fn();
|
||||
const sendJsonMock = vi.fn();
|
||||
const { readFileMock } = vi.hoisted(() => ({
|
||||
readFileMock: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('@electron/api/route-utils', () => ({
|
||||
parseJsonBody: (...args: unknown[]) => parseJsonBodyMock(...args),
|
||||
sendJson: (...args: unknown[]) => sendJsonMock(...args),
|
||||
}));
|
||||
|
||||
vi.mock('node:fs/promises', async () => {
|
||||
const actual = await vi.importActual<typeof import('node:fs/promises')>('node:fs/promises');
|
||||
return {
|
||||
...actual,
|
||||
readFile: readFileMock,
|
||||
default: {
|
||||
...actual,
|
||||
readFile: readFileMock,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
describe('handleCronRoutes', () => {
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
readFileMock.mockRejectedValue(Object.assign(new Error('ENOENT'), { code: 'ENOENT' }));
|
||||
});
|
||||
|
||||
it('creates cron jobs with external delivery configuration', async () => {
|
||||
@@ -206,4 +222,224 @@ describe('handleCronRoutes', () => {
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('builds failed cron run fallback messages as visible assistant messages', async () => {
|
||||
const { buildCronSessionFallbackMessages } = await import('@electron/api/routes/cron');
|
||||
|
||||
const messages = buildCronSessionFallbackMessages({
|
||||
sessionKey: 'agent:main:cron:job-failed',
|
||||
runs: [{
|
||||
jobId: 'job-failed',
|
||||
status: 'error',
|
||||
error: '模型超时',
|
||||
ts: 1773281732751,
|
||||
}],
|
||||
limit: 20,
|
||||
});
|
||||
|
||||
expect(messages).toContainEqual(expect.objectContaining({
|
||||
id: 'cron-run-1773281732751',
|
||||
role: 'assistant',
|
||||
content: 'Run failed: 模型超时',
|
||||
isError: true,
|
||||
}));
|
||||
});
|
||||
|
||||
it('builds cron fallback query and running messages as visible chat messages', async () => {
|
||||
const { buildCronSessionFallbackMessages } = await import('@electron/api/routes/cron');
|
||||
|
||||
const messages = buildCronSessionFallbackMessages({
|
||||
sessionKey: 'agent:main:cron:job-running',
|
||||
job: {
|
||||
name: '写日报',
|
||||
payload: { kind: 'agentTurn', message: '生成今天的经营日报' },
|
||||
state: { runningAtMs: 1778650080011 },
|
||||
},
|
||||
runs: [],
|
||||
limit: 20,
|
||||
});
|
||||
|
||||
expect(messages).toContainEqual(expect.objectContaining({
|
||||
id: 'cron-query-job-running-1778650080011',
|
||||
role: 'user',
|
||||
content: '生成今天的经营日报',
|
||||
}));
|
||||
expect(messages).toContainEqual(expect.objectContaining({
|
||||
id: 'cron-running-job-running',
|
||||
role: 'assistant',
|
||||
content: '任务正在执行,完成后会自动在这里显示结果。',
|
||||
}));
|
||||
});
|
||||
|
||||
it('reconciles a failed cron state when the run trajectory finished successfully', async () => {
|
||||
const rpc = vi.fn().mockResolvedValue({
|
||||
jobs: [{
|
||||
id: 'job-recovered',
|
||||
name: 'Recovered job',
|
||||
enabled: true,
|
||||
createdAtMs: 1,
|
||||
updatedAtMs: 2,
|
||||
schedule: { kind: 'cron', expr: '0 9 * * *' },
|
||||
payload: { kind: 'agentTurn', message: 'Create a document' },
|
||||
delivery: { mode: 'none' },
|
||||
agentId: 'main',
|
||||
state: {
|
||||
lastRunAtMs: 1778650080011,
|
||||
lastStatus: 'error',
|
||||
lastError: 'Edit failed',
|
||||
lastDurationMs: 77388,
|
||||
},
|
||||
}],
|
||||
});
|
||||
readFileMock.mockImplementation(async (path: unknown) => {
|
||||
const textPath = String(path);
|
||||
if (textPath.endsWith('/cron/runs/job-recovered.jsonl')) {
|
||||
return JSON.stringify({
|
||||
ts: 1778650157404,
|
||||
jobId: 'job-recovered',
|
||||
action: 'finished',
|
||||
status: 'error',
|
||||
error: 'Edit failed',
|
||||
summary: 'Edit failed',
|
||||
sessionId: 'run-1',
|
||||
runAtMs: 1778650080011,
|
||||
});
|
||||
}
|
||||
if (textPath.endsWith('/agents/main/sessions/run-1.trajectory.jsonl')) {
|
||||
return JSON.stringify({
|
||||
type: 'trace.artifacts',
|
||||
data: {
|
||||
finalStatus: 'success',
|
||||
assistantTexts: ['Done. Saved to the desktop.'],
|
||||
},
|
||||
});
|
||||
}
|
||||
throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' });
|
||||
});
|
||||
|
||||
const { handleCronRoutes } = await import('@electron/api/routes/cron');
|
||||
const handled = await handleCronRoutes(
|
||||
{ method: 'GET' } as IncomingMessage,
|
||||
{} as ServerResponse,
|
||||
new URL('http://127.0.0.1:13210/api/cron/jobs'),
|
||||
{
|
||||
gatewayManager: { rpc },
|
||||
} as never,
|
||||
);
|
||||
|
||||
expect(handled).toBe(true);
|
||||
expect(sendJsonMock).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
200,
|
||||
[expect.objectContaining({
|
||||
id: 'job-recovered',
|
||||
lastRun: expect.objectContaining({
|
||||
success: true,
|
||||
reconciled: true,
|
||||
warning: 'Edit failed',
|
||||
}),
|
||||
})],
|
||||
);
|
||||
const response = sendJsonMock.mock.calls.at(-1)?.[2] as Array<{ lastRun?: { error?: string } }>;
|
||||
expect(response[0]?.lastRun?.error).toBeUndefined();
|
||||
});
|
||||
|
||||
it('returns a successful trigger response when cron.run reports a stale tool error after recovery', async () => {
|
||||
parseJsonBodyMock.mockResolvedValue({ id: 'job-recovered' });
|
||||
const rpc = vi.fn()
|
||||
.mockRejectedValueOnce(new Error('Edit failed'))
|
||||
.mockResolvedValueOnce({
|
||||
jobs: [{
|
||||
id: 'job-recovered',
|
||||
name: 'Recovered job',
|
||||
enabled: true,
|
||||
createdAtMs: 1,
|
||||
updatedAtMs: 2,
|
||||
schedule: { kind: 'cron', expr: '0 9 * * *' },
|
||||
payload: { kind: 'agentTurn', message: 'Create a document' },
|
||||
delivery: { mode: 'none' },
|
||||
agentId: 'main',
|
||||
state: {
|
||||
lastRunAtMs: 1778650080011,
|
||||
lastStatus: 'error',
|
||||
lastError: 'Edit failed',
|
||||
lastDurationMs: 77388,
|
||||
},
|
||||
}],
|
||||
});
|
||||
readFileMock.mockImplementation(async (path: unknown) => {
|
||||
const textPath = String(path);
|
||||
if (textPath.endsWith('/cron/runs/job-recovered.jsonl')) {
|
||||
return JSON.stringify({
|
||||
ts: 1778650157404,
|
||||
jobId: 'job-recovered',
|
||||
action: 'finished',
|
||||
status: 'error',
|
||||
error: 'Edit failed',
|
||||
summary: 'Edit failed',
|
||||
sessionId: 'run-1',
|
||||
runAtMs: 1778650080011,
|
||||
});
|
||||
}
|
||||
if (textPath.endsWith('/agents/main/sessions/run-1.trajectory.jsonl')) {
|
||||
return JSON.stringify({
|
||||
type: 'trace.artifacts',
|
||||
data: {
|
||||
finalStatus: 'success',
|
||||
assistantTexts: ['Done. Saved to the desktop.'],
|
||||
},
|
||||
});
|
||||
}
|
||||
throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' });
|
||||
});
|
||||
|
||||
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:13210/api/cron/trigger'),
|
||||
{
|
||||
gatewayManager: { rpc },
|
||||
} as never,
|
||||
);
|
||||
|
||||
expect(handled).toBe(true);
|
||||
expect(rpc).toHaveBeenNthCalledWith(1, 'cron.run', { id: 'job-recovered', mode: 'force' });
|
||||
expect(rpc).toHaveBeenNthCalledWith(2, 'cron.list', { includeDisabled: true }, 8000);
|
||||
expect(sendJsonMock).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
200,
|
||||
expect.objectContaining({
|
||||
success: true,
|
||||
recovered: true,
|
||||
warning: 'Edit failed',
|
||||
summary: 'Done. Saved to the desktop.',
|
||||
triggerWarning: 'Error: Edit failed',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('uses recovered trajectory summaries for cron fallback messages', async () => {
|
||||
const { buildCronSessionFallbackMessages } = await import('@electron/api/routes/cron');
|
||||
|
||||
const messages = buildCronSessionFallbackMessages({
|
||||
sessionKey: 'agent:main:cron:job-recovered',
|
||||
runs: [{
|
||||
jobId: 'job-recovered',
|
||||
status: 'error',
|
||||
resolvedStatus: 'ok',
|
||||
error: 'Edit failed',
|
||||
resolvedSummary: 'Done. Saved to the desktop.',
|
||||
ts: 1778650157404,
|
||||
}],
|
||||
limit: 20,
|
||||
});
|
||||
|
||||
expect(messages).toContainEqual(expect.objectContaining({
|
||||
id: 'cron-run-1778650157404',
|
||||
role: 'assistant',
|
||||
content: 'Done. Saved to the desktop.',
|
||||
}));
|
||||
expect(messages.find((message) => message.id === 'cron-run-1778650157404')?.isError).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user