mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-02 04:21:34 +00:00
feat(dev): add cron fixtures to the Control UI mock server (#114248)
This commit is contained in:
committed by
GitHub
parent
c59d38138e
commit
fa2b84697b
261
scripts/control-ui-mock-cron.ts
Normal file
261
scripts/control-ui-mock-cron.ts
Normal file
@@ -0,0 +1,261 @@
|
||||
import type {
|
||||
CronJob,
|
||||
CronJobsListResult,
|
||||
CronRunLogEntry,
|
||||
CronRunsResult,
|
||||
CronStatus,
|
||||
} from "../ui/src/api/types.ts";
|
||||
|
||||
function listResult(
|
||||
jobs: CronJob[],
|
||||
options: { total?: number; limit?: number; offset?: number } = {},
|
||||
): CronJobsListResult {
|
||||
const total = options.total ?? jobs.length;
|
||||
const limit = options.limit ?? 50;
|
||||
const offset = options.offset ?? 0;
|
||||
const nextOffset = offset + jobs.length;
|
||||
const hasMore = nextOffset < total;
|
||||
return {
|
||||
jobs,
|
||||
total,
|
||||
offset,
|
||||
limit,
|
||||
hasMore,
|
||||
nextOffset: hasMore ? nextOffset : null,
|
||||
};
|
||||
}
|
||||
|
||||
function runsResult(entries: CronRunLogEntry[]): CronRunsResult {
|
||||
return {
|
||||
entries,
|
||||
total: entries.length,
|
||||
offset: 0,
|
||||
limit: 50,
|
||||
hasMore: false,
|
||||
nextOffset: null,
|
||||
};
|
||||
}
|
||||
|
||||
function singleJobListCases(jobs: CronJob[], match: Record<string, unknown>) {
|
||||
return jobs.map((job, offset) => ({
|
||||
match: { ...match, offset },
|
||||
response: listResult([job], { total: jobs.length, limit: 1, offset }),
|
||||
}));
|
||||
}
|
||||
|
||||
export function buildCronMocks(baseTime: number) {
|
||||
const minute = 60_000;
|
||||
const hour = 60 * minute;
|
||||
const day = 24 * hour;
|
||||
const failedJob: CronJob = {
|
||||
id: "mock-cron-calendar-sync",
|
||||
agentId: "main",
|
||||
name: "Sync team calendar",
|
||||
description: "Refresh the shared calendar cache for the morning briefing.",
|
||||
enabled: true,
|
||||
createdAtMs: baseTime - 30 * day,
|
||||
updatedAtMs: baseTime - 4 * minute,
|
||||
schedule: { kind: "cron", expr: "0 */6 * * *", tz: "America/New_York" },
|
||||
sessionTarget: "isolated",
|
||||
wakeMode: "now",
|
||||
payload: {
|
||||
kind: "agentTurn",
|
||||
message: "Sync the team calendar and summarize schedule conflicts.",
|
||||
},
|
||||
delivery: { mode: "announce", channel: "telegram", to: "@operations" },
|
||||
state: {
|
||||
nextRunAtMs: baseTime + 5 * hour,
|
||||
lastRunAtMs: baseTime - 5 * minute,
|
||||
lastRunStatus: "error",
|
||||
lastError:
|
||||
"OAuth refresh failed: invalid_grant. The provider rejected the stored refresh token because it was revoked or expired. Reconnect Google Calendar before the next scheduled sync.",
|
||||
lastDurationMs: 8_420,
|
||||
consecutiveErrors: 2,
|
||||
lastDeliveryStatus: "not-requested",
|
||||
},
|
||||
};
|
||||
const overdueJob: CronJob = {
|
||||
id: "mock-cron-inbox-triage",
|
||||
agentId: "main",
|
||||
name: "Triage support inbox",
|
||||
description: "Classify new support mail and prepare the daily response queue.",
|
||||
enabled: true,
|
||||
createdAtMs: baseTime - 18 * day,
|
||||
updatedAtMs: baseTime - 35 * minute,
|
||||
schedule: { kind: "every", everyMs: 15 * minute, anchorMs: baseTime - 18 * day },
|
||||
sessionTarget: "isolated",
|
||||
wakeMode: "now",
|
||||
payload: { kind: "agentTurn", message: "Triage the support inbox." },
|
||||
delivery: { mode: "none" },
|
||||
state: {
|
||||
nextRunAtMs: baseTime - 20 * minute,
|
||||
lastRunAtMs: baseTime - 35 * minute,
|
||||
lastRunStatus: "ok",
|
||||
lastDurationMs: 24_180,
|
||||
lastDeliveryStatus: "not-requested",
|
||||
},
|
||||
};
|
||||
const healthyJob: CronJob = {
|
||||
id: "mock-cron-release-digest",
|
||||
agentId: "main",
|
||||
name: "Publish release digest",
|
||||
description: "Summarize merged changes for the engineering channel.",
|
||||
enabled: true,
|
||||
createdAtMs: baseTime - 9 * day,
|
||||
updatedAtMs: baseTime - 30 * minute,
|
||||
schedule: { kind: "cron", expr: "30 9 * * 1-5", tz: "America/New_York" },
|
||||
sessionTarget: "isolated",
|
||||
wakeMode: "now",
|
||||
payload: { kind: "agentTurn", message: "Draft and publish the release digest." },
|
||||
delivery: { mode: "announce", channel: "slack", to: "#engineering" },
|
||||
state: {
|
||||
nextRunAtMs: baseTime + 30 * minute,
|
||||
lastRunAtMs: baseTime - 30 * minute,
|
||||
lastRunStatus: "ok",
|
||||
lastDurationMs: 51_230,
|
||||
lastDelivered: true,
|
||||
lastDeliveryStatus: "delivered",
|
||||
},
|
||||
};
|
||||
const jobs = [overdueJob, healthyJob, failedJob];
|
||||
const failedRun: CronRunLogEntry = {
|
||||
ts: baseTime - 5 * minute,
|
||||
runAtMs: baseTime - 5 * minute,
|
||||
jobId: failedJob.id,
|
||||
jobName: failedJob.name,
|
||||
action: "finished",
|
||||
status: "error",
|
||||
durationMs: failedJob.state?.lastDurationMs,
|
||||
error: failedJob.state?.lastError,
|
||||
deliveryStatus: "not-requested",
|
||||
model: "gpt-5.6-sol",
|
||||
provider: "openai",
|
||||
};
|
||||
const runs: CronRunLogEntry[] = [
|
||||
failedRun,
|
||||
{
|
||||
ts: baseTime - 30 * minute,
|
||||
runAtMs: baseTime - 30 * minute,
|
||||
jobId: healthyJob.id,
|
||||
jobName: healthyJob.name,
|
||||
action: "finished",
|
||||
status: "ok",
|
||||
durationMs: healthyJob.state?.lastDurationMs,
|
||||
summary: "Published a digest covering 14 merged changes.",
|
||||
delivered: true,
|
||||
deliveryStatus: "delivered",
|
||||
model: "claude-sonnet-4-6",
|
||||
provider: "anthropic",
|
||||
},
|
||||
{
|
||||
ts: baseTime - 35 * minute,
|
||||
runAtMs: baseTime - 35 * minute,
|
||||
jobId: overdueJob.id,
|
||||
jobName: overdueJob.name,
|
||||
action: "finished",
|
||||
status: "ok",
|
||||
durationMs: overdueJob.state?.lastDurationMs,
|
||||
summary: "Classified 23 messages and prepared 6 replies.",
|
||||
deliveryStatus: "not-requested",
|
||||
model: "gpt-5.6-sol",
|
||||
provider: "openai",
|
||||
},
|
||||
];
|
||||
const queuedRuns: Array<{ runId: string; entry: CronRunLogEntry }> = jobs.map((job, index) => ({
|
||||
runId: `mock-cron-manual-${job.id}`,
|
||||
entry: {
|
||||
ts: baseTime + index,
|
||||
runAtMs: baseTime + index,
|
||||
jobId: job.id,
|
||||
jobName: job.name,
|
||||
action: "finished",
|
||||
status: "ok",
|
||||
durationMs: 42_000 + index * 2_500,
|
||||
summary: `Completed an on-demand run for ${job.name}.`,
|
||||
deliveryStatus: "not-requested",
|
||||
model: "gpt-5.6-sol",
|
||||
provider: "openai",
|
||||
},
|
||||
}));
|
||||
const status: CronStatus = {
|
||||
enabled: true,
|
||||
jobs: jobs.length,
|
||||
nextWakeAtMs: overdueJob.state?.nextRunAtMs,
|
||||
};
|
||||
const runByJobId = new Map(runs.map((entry) => [entry.jobId, entry]));
|
||||
const sortedJobLists = [
|
||||
{ match: { sortBy: "nextRunAtMs", sortDir: "asc" }, jobs },
|
||||
{
|
||||
match: { sortBy: "nextRunAtMs", sortDir: "desc" },
|
||||
jobs: [failedJob, healthyJob, overdueJob],
|
||||
},
|
||||
{ match: { sortBy: "updatedAtMs", sortDir: "asc" }, jobs },
|
||||
{
|
||||
match: { sortBy: "updatedAtMs", sortDir: "desc" },
|
||||
jobs: [failedJob, healthyJob, overdueJob],
|
||||
},
|
||||
{ match: { sortBy: "name", sortDir: "asc" }, jobs: [healthyJob, failedJob, overdueJob] },
|
||||
{ match: { sortBy: "name", sortDir: "desc" }, jobs: [overdueJob, failedJob, healthyJob] },
|
||||
];
|
||||
|
||||
return {
|
||||
"cron.status": status,
|
||||
"cron.list": {
|
||||
// Cases mirror the concrete queries today's Cron UI issues. Unknown combinations fall back
|
||||
// to the full fixture list; dynamic evaluation is intentionally out of scope because the
|
||||
// scenario is JSON-serialized into the page rather than installed as a live responder.
|
||||
cases: [
|
||||
{
|
||||
match: { enabled: "enabled", lastRunStatus: "error" },
|
||||
response: listResult([failedJob], { limit: 1 }),
|
||||
},
|
||||
{ match: { enabled: "disabled" }, response: listResult([]) },
|
||||
...singleJobListCases(jobs, {
|
||||
enabled: "enabled",
|
||||
sortBy: "nextRunAtMs",
|
||||
sortDir: "asc",
|
||||
limit: 1,
|
||||
}),
|
||||
...singleJobListCases(jobs, { includeDisabled: true, limit: 1 }),
|
||||
...sortedJobLists.map((entry) => ({
|
||||
match: entry.match,
|
||||
response: listResult(entry.jobs),
|
||||
})),
|
||||
{ response: listResult(jobs) },
|
||||
],
|
||||
},
|
||||
"cron.runs": {
|
||||
cases: [
|
||||
...queuedRuns.map((run) => ({
|
||||
match: { runId: run.runId },
|
||||
response: runsResult([run.entry]),
|
||||
})),
|
||||
...jobs.flatMap((job) => {
|
||||
const jobRun = runByJobId.get(job.id);
|
||||
return [
|
||||
{
|
||||
match: { scope: "job", id: job.id, statuses: ["error"] },
|
||||
response: runsResult(jobRun?.status === "error" ? [jobRun] : []),
|
||||
},
|
||||
{
|
||||
match: { scope: "job", id: job.id },
|
||||
response: runsResult(jobRun ? [jobRun] : []),
|
||||
},
|
||||
];
|
||||
}),
|
||||
{ match: { statuses: ["error"] }, response: runsResult([failedRun]) },
|
||||
{ response: runsResult(runs) },
|
||||
],
|
||||
},
|
||||
// Writes acknowledge the UI action but intentionally keep the fixture snapshot immutable.
|
||||
"cron.add": { id: "mock-cron-created" },
|
||||
"cron.update": { ok: true },
|
||||
"cron.remove": { ok: true },
|
||||
"cron.run": {
|
||||
cases: queuedRuns.map((run) => ({
|
||||
match: { id: run.entry.jobId },
|
||||
response: { ok: true, enqueued: true, runId: run.runId },
|
||||
})),
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
} from "../ui/vite.config.ts";
|
||||
import { buildBackgroundTasksMock } from "./control-ui-mock-background-tasks.ts";
|
||||
import { buildChannelsStatusMock, buildChannelWizardMocks } from "./control-ui-mock-channels.ts";
|
||||
import { buildCronMocks } from "./control-ui-mock-cron.ts";
|
||||
import { buildPluginCatalogMock } from "./control-ui-mock-plugins.ts";
|
||||
import { buildSkillWorkshopMocks } from "./control-ui-mock-skill-workshop.js";
|
||||
|
||||
@@ -1259,6 +1260,7 @@ async function createChatPickerScenario(
|
||||
const profileUsage = buildProfileUsageMocks(Date.now());
|
||||
const modelProviders = buildModelProviderMocks(Date.now());
|
||||
const skillWorkshop = buildSkillWorkshopMocks(Date.now());
|
||||
const cronMocks = buildCronMocks(Date.now());
|
||||
const channelWizard = buildChannelWizardMocks();
|
||||
const configMocks = buildConfigMocks({ swarmEnabled: fixture === "swarm" });
|
||||
return {
|
||||
@@ -1288,6 +1290,7 @@ async function createChatPickerScenario(
|
||||
],
|
||||
methodResponses: {
|
||||
...buildBackgroundTasksMock(baseTime),
|
||||
...cronMocks,
|
||||
"users.self": { profile: selfProfile },
|
||||
// Custom session group catalog so the sidebar's category zone (and its
|
||||
// drag-reordering against built-in sections) is exercised in the mock.
|
||||
|
||||
Reference in New Issue
Block a user