mirror of
https://github.com/openclaw/openclaw.git
synced 2026-10-03 09:39:25 +00:00
* feat(ui): add roster-first Agents home Add the /agents roster with activity, main-chat previews, identity cards, and direct chat navigation. Keep agent configuration at /settings/agents and reuse the existing creation flow. Include bounded session refreshes, synthetic fixtures, focused navigation and page tests, and Control UI documentation. * fix(ui): align agents e2e navigation with roster home Retarget settings behavior tests to /settings/agents and include Agents in default sidebar ordering expectations. Preserve existing permission, avatar, persistence, and navigation assertions. Reproduced six failures before repair; all 22 affected and sibling browser tests now pass. * perf(ui): defer agent page copy and route setup Register Agents home copy with the lazy view while keeping sidebar labels eager and preserving the complete source catalog. Defer agent page render adapters and the settings roster loader through the existing lazy route pattern, keeping loader logic and displayed copy unchanged. Reduce startup gzip by 274 bytes on the branch and 275 bytes in the local CI merge-tree replay without changing performance budgets. * feat(ui): link the agent switcher to the Agents home Add an All agents action above New agent using the existing menu navigation and dismissal flow. Keep the Agents heading unchanged, document the entry point, and cover roster navigation and action ordering. * fix(ui): restore eager agent settings route loading * feat(ui): add optional sidebar agent roster Add a browser-scoped sidebarAgentsMode preference with the existing chip as its default. The agent menu toggles a compact roster with working activity, last-active time, existing unread counts, and links to all agents or creation. Share identity, activity ordering, previews, and bounded session loading with the Agents home. Keep agent switching and selected-agent session scope on the existing sidebar path, and defer roster rendering to protect startup. Document the mode and include focused unit, mocked browser, and fixture proof. * fix(ui): share roster activity between sidebar and Agents home * feat(ui): group sidebar sessions by agent in roster mode Show every selectable agent's pinned and recent sessions under collapsible, working-first headers, with per-agent new-session and main-chat links. Share bounded session activity with Agents home, preserve current-session visibility and row ownership, and retain the context chip and list filters. Document the 300-row window and browser-local collapse preferences. Refs #141476. * refactor(ui): break sidebar session navigation import cycle * feat(ui): team mode hides Home, adds a "+" agent switcher, and scopes pages to all agents Make each agent header its canonical main-chat entry, keep collapse separate, and offer ordered agent selection from the sidebar New session controls. Default shared page scope to all agents when team mode starts, preserve manual filters during navigation, and restore the prior scope when it ends. Show shared avatar/name chips on agent-owned lists. Keep Memory, Model providers, and Skill Workshop single-agent, with concrete navigation selection. Reconcile cold saved scopes and apply Memory route ownership before requests. Document the mode and validate sidebar, scope, list, and browser behavior. Refs #141476. * fix(ui): persist the pre-team agent scope per gateway Remember the pre-team page scope in gateway-scoped browser preferences, including an explicit all-agents selection. Restore it after chat navigation, reloads, and gateway switching, then clear it when team mode ends. Validate with 424 focused unit/component tests, the sidebar browser scenario, UI typechecking, i18n, lint, and architecture checks. The full changed-file gate reproduces existing TS2352 fixture errors in the Model providers and Usage route tests at the starting commit. Refs #141476. * fix(ui): type route tests against the team-mode page contracts * fix(ui): expose agent ids on identity chips and assert them in e2e Include stable owner IDs in accessible chip labels and tooltips. Preserve same-session-id ownership proof with two identically named agents, including an empty current session, and identify row owners through the shared chip across list tests. Validate the real-Gateway Usage scenario, sidebar and Usage tests, i18n, docs, and changed checks. Refs #141476. * feat(ui): team mode replaces the agent chip with a workspace header Show the configured Gateway name or OpenClaw with the product mark in team mode. Keep chat context with the open session, preserve the header controls, and limit the workspace menu to Show one agent, Agent settings, and Help. Restore keyboard focus when switching between the workspace and agent chip. Reuse the existing help submenu and header styles, document the mode, and verify workspace identity, menu contents, context changes, and chip restoration. Validation includes 376 unit/component tests, all 31 selected browser tests, i18n, changed-file checks, architecture, and production build/performance. Refs #141476. * feat(ui): agent-first team sidebar rows and indicators Integrate the reviewed team sidebar layout, trailing activity and attention indicators, nested row geometry, and default face avatars. Preserve shared roster lifecycle and identity updates. Refs #141476. * fix(ui): keep team activity on stable compact session rows Keep configured team order and move main conversations into session rows, leaving header summaries only for collapsed groups. Keep titles on one line, child state in the right column, Online collapsed initially, and conversation actions clearly labeled. Move default face artwork into the lazy roster module and remove the obsolete shared preview path and indicator export. Preserve normal session visibility filters when restoring the canonical global stream. Verify desktop and touch geometry, nested rows, identity precedence, filters, and same-id Usage ownership. Refs #141476. * improve(ui): keep agent creation in Settings Remove the New agent shortcut and command handler from sidebar menus, keeping conversation creation and agent switching focused on existing agents. Preserve administrator-gated creation in Settings at zero, one, and multiple agents, and the Agents home action. Update menu coverage and the Settings and team-mode documentation. Refs #141476. Co-authored-by: hannesrudolph <49103247+hannesrudolph@users.noreply.github.com> * fix(ui): deduplicate trailing sidebar status indicators Keep collapsed descendant summaries in the trailing state column and suppress status kinds already represented by the parent, preserving distinct queued work and attention. Fill the global conversation menu avatar with the default face. Cover parent/child status overlap and wait for menu animation before measuring geometry. Refs #141476. * test(ui): keep roster activity fixture updates immutable Use Object.assign for row copies inside the activity-order regression to satisfy the repository map-copy lint rule without changing fixture behavior. * fix(ui): share one default agent avatar across surfaces Share image, identity emoji, and deterministic SVG face rendering across Control UI agent cards, menus, chips, chat, owners, and participants. Keep people avatars on their existing profile and initials path. Select seven crisp silhouettes and ten hues from the stable agent id, load the artwork lazily, and retire pending faces when identity emoji arrive. Preserve authenticated image ownership and error fallback. Resolve full-message transcript artwork with the same effective agent id as its replies. Replace the tiny mock images with crisp synthetic artwork. Refs #141476. * fix(ui): polish team-mode session rows and group actions Give each agent group New conversation, Open main chat, All sessions, and Collapse others actions. Keep the global team filter without the redundant Sessions heading, See all link, or section-level creation menu. Reserve aligned child-count, unread, and state slots for team rows and collapsed groups. Prioritize input and error attention, preserve each expanded row's own activity, and use plain 12px nested carets. Keep chip-mode controls and leading activity unchanged. Document the team actions and indicators. Cover keyboard, touch, shared agent scope, collapse persistence, fixed geometry, and conflict ownership. Update the stale shared-avatar assertion to the current textAvatar field. Refs #141476. * test(ui): follow the New conversation label in e2e Update the new-session transition, session ownership, navigation, and message-action tooltip flows to the reviewed sidebar accessible name. Retain keyboard coverage of the chip-mode global creation link. Refs #141476. * fix(ui): keep team-mode titles and names readable Move team session filters into the sidebar header toolbar and remove the empty filter row. Size trailing indicators to present state and let quiet session titles use the remaining row width. Share the collapsed summary and header-action area so hover and keyboard focus preserve agent names. Keep chip mode, agent-owned menus, plain carets, nested guides, and row heights. Preserve the workspace label beside four equally sized controls. Update sidebar docs and cover 258px title/name geometry, touch, keyboard, and filter behavior. Shrink the assertion baseline after removing a cast. Refs #141476. * fix(ui): keep the product mark for the system agent avatar Resolve reserved system identities to the mounted, build-versioned OpenClaw favicon in the shared avatar renderer, without image or generated-face fallback. Pass the custodian's canonical agent ID instead of threading a page-owned image. Keep the chat image contract and accessible name while removing its extra wrapper. Preserve ordinary agent image, emoji, and generated-face behavior. Cover system identities, image errors, and mounted assets, and document the product-mark rule. Refs #141476. * fix(ui): keep configured image avatars beside chat replies Restore the chat image classes and accessible name for configured avatars, including images fetched through the authenticated workspace avatar path. Reuse the existing image slot with shared emoji and generated-face fallbacks, and keep reserved system agents on the OpenClaw product mark. Cover image sources, fallback behavior, saved and streaming replies, and forwarded messages. Document configured workspace images beside replies. Refs #141476. * Revert "improve(ui): keep agent creation in Settings" This reverts commit 0d7a78db4f4cfab2cbee00515b38c30ab316780b. * docs(ui): preserve current sidebar menu wording Keep the New conversation labels and no-filter agent switcher description accurate after restoring the New agent shortcut. * fix(ui): reconcile sidebar roster integration checks Align avatar recovery tests introduced by main with the shared generated-face fallback and nested image markup, preserving authenticated loading, revision recovery, and stale-error coverage. Restore the Usage loader's narrow Gateway and agent-selection input contract after its lazy-loader extraction. Validation: reproduce the four failing avatar assertions before the fix; 52 avatar and Usage route tests pass afterward. Independent review is clean. * fix(ui): align sidebar fixtures and identity e2e with main --------- Co-authored-by: hannesrudolph <49103247+hannesrudolph@users.noreply.github.com>
386 lines
14 KiB
TypeScript
386 lines
14 KiB
TypeScript
import type {
|
|
CronJob,
|
|
CronJobsListResult,
|
|
CronRunLogEntry,
|
|
CronRunsResult,
|
|
CronStatus,
|
|
} from "../ui/src/api/types.ts";
|
|
|
|
const CRON_LIST_SNAPSHOT_REVISION = "control-ui-mock-cron";
|
|
|
|
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,
|
|
snapshotRevision: CRON_LIST_SNAPSHOT_REVISION,
|
|
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,
|
|
options: { richAttention?: boolean; secondAgentId?: string } = {},
|
|
) {
|
|
const richAttention = options.richAttention === true;
|
|
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" },
|
|
failureAlert: {
|
|
after: 2,
|
|
channel: "telegram",
|
|
to: "@operations",
|
|
cooldownMs: 3_600_000,
|
|
includeSkipped: false,
|
|
mode: "announce",
|
|
},
|
|
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 extraFailedJobs: CronJob[] = richAttention
|
|
? [
|
|
{
|
|
id: "mock-cron-release-notify",
|
|
agentId: "main",
|
|
name: "Notify release stakeholders about deployment readiness and rollback constraints",
|
|
description:
|
|
"Send the release decision, deploy window, rollback owner, and incident contact to every stakeholder group.",
|
|
enabled: true,
|
|
createdAtMs: baseTime - 24 * day,
|
|
updatedAtMs: baseTime - 7 * minute,
|
|
schedule: { kind: "every", everyMs: 20 * minute, anchorMs: baseTime - 24 * day },
|
|
sessionTarget: "isolated",
|
|
wakeMode: "now",
|
|
payload: {
|
|
kind: "agentTurn",
|
|
message:
|
|
"Prepare the release notification, verify the rollback owner, and publish the final deployment readiness summary.",
|
|
},
|
|
delivery: { mode: "announce", channel: "slack", to: "#release-operations" },
|
|
state: {
|
|
nextRunAtMs: baseTime + 20 * minute,
|
|
lastRunAtMs: baseTime - 9 * minute,
|
|
lastRunStatus: "error",
|
|
lastError:
|
|
"Delivery failed after the provider accepted the request but closed the stream before the final acknowledgement. The retry queue retained the payload, the release channel has not been notified, and the notification fan-out must be reconciled before another deployment attempt.",
|
|
lastDurationMs: 18_640,
|
|
consecutiveErrors: 3,
|
|
lastDeliveryStatus: "not-delivered",
|
|
},
|
|
},
|
|
{
|
|
id: "mock-cron-backup-verify",
|
|
agentId: "main",
|
|
name: "Verify encrypted backup rotation before the retention window closes",
|
|
description:
|
|
"Check the latest encrypted backup, key rotation receipt, and restore manifest before retention pruning.",
|
|
enabled: true,
|
|
createdAtMs: baseTime - 42 * day,
|
|
updatedAtMs: baseTime - 11 * minute,
|
|
schedule: { kind: "cron", expr: "15 * * * *", tz: "UTC" },
|
|
sessionTarget: "isolated",
|
|
wakeMode: "now",
|
|
payload: {
|
|
kind: "agentTurn",
|
|
message:
|
|
"Verify backup rotation and report any missing restore manifest or key receipt.",
|
|
},
|
|
delivery: { mode: "none" },
|
|
state: {
|
|
nextRunAtMs: baseTime + 15 * minute,
|
|
lastRunAtMs: baseTime - 13 * minute,
|
|
lastRunStatus: "error",
|
|
lastError:
|
|
"Restore verification could not read the encrypted manifest: checksum mismatch after the object store returned a partial range. Keep the current backup, do not prune the retention window, and retry after the storage replica is healthy.",
|
|
lastDurationMs: 42_900,
|
|
consecutiveErrors: 4,
|
|
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 extraOverdueJobs: CronJob[] = richAttention
|
|
? [
|
|
{
|
|
id: "mock-cron-security-digest",
|
|
agentId: "main",
|
|
name: "Prepare the daily security digest",
|
|
description: "Summarize new security advisories and unresolved remediation work.",
|
|
enabled: true,
|
|
createdAtMs: baseTime - 31 * day,
|
|
updatedAtMs: baseTime - 50 * minute,
|
|
schedule: { kind: "every", everyMs: hour, anchorMs: baseTime - 31 * day },
|
|
sessionTarget: "isolated",
|
|
wakeMode: "now",
|
|
payload: { kind: "agentTurn", message: "Prepare the daily security digest." },
|
|
delivery: { mode: "none" },
|
|
state: {
|
|
nextRunAtMs: baseTime - 42 * minute,
|
|
lastRunAtMs: baseTime - 102 * minute,
|
|
lastRunStatus: "ok",
|
|
lastDurationMs: 38_420,
|
|
lastDeliveryStatus: "not-requested",
|
|
},
|
|
},
|
|
]
|
|
: [];
|
|
const healthyJob: CronJob = {
|
|
id: "mock-cron-release-digest",
|
|
agentId: options.secondAgentId ?? "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, ...extraOverdueJobs, healthyJob, failedJob, ...extraFailedJobs];
|
|
const failedJobs = [failedJob, ...extraFailedJobs];
|
|
const failedRuns: CronRunLogEntry[] = failedJobs.map((job, index) => ({
|
|
ts: baseTime - (5 + index * 4) * minute,
|
|
runAtMs: baseTime - (5 + index * 4) * minute,
|
|
jobId: job.id,
|
|
jobName: job.name,
|
|
action: "finished",
|
|
status: "error",
|
|
durationMs: job.state?.lastDurationMs,
|
|
error: job.state?.lastError,
|
|
deliveryStatus: "not-requested",
|
|
model: index === 1 ? "claude-sonnet-4-6" : "gpt-5",
|
|
provider: index === 1 ? "anthropic" : "openai",
|
|
}));
|
|
const runs: CronRunLogEntry[] = [
|
|
...failedRuns,
|
|
{
|
|
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",
|
|
deliverySuppressionReason: "Delivery mode is none for this inbox-only automation.",
|
|
model: "gpt-5",
|
|
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",
|
|
provider: "openai",
|
|
},
|
|
}));
|
|
const status: CronStatus = {
|
|
enabled: true,
|
|
triggersEnabled: true,
|
|
jobs: jobs.length,
|
|
nextWakeAtMs: Math.min(
|
|
...jobs.flatMap((job) =>
|
|
job.state?.nextRunAtMs === undefined ? [] : [job.state.nextRunAtMs],
|
|
),
|
|
),
|
|
};
|
|
const runByJobId = new Map(runs.map((entry) => [entry.jobId, entry]));
|
|
const sortedJobLists = [
|
|
{
|
|
match: { sortBy: "nextRunAtMs", sortDir: "asc" },
|
|
jobs: jobs.toSorted(
|
|
(left, right) => (left.state?.nextRunAtMs ?? 0) - (right.state?.nextRunAtMs ?? 0),
|
|
),
|
|
},
|
|
{
|
|
match: { sortBy: "nextRunAtMs", sortDir: "desc" },
|
|
jobs: jobs.toSorted(
|
|
(left, right) => (right.state?.nextRunAtMs ?? 0) - (left.state?.nextRunAtMs ?? 0),
|
|
),
|
|
},
|
|
{
|
|
match: { sortBy: "updatedAtMs", sortDir: "asc" },
|
|
jobs: jobs.toSorted((left, right) => (left.updatedAtMs ?? 0) - (right.updatedAtMs ?? 0)),
|
|
},
|
|
{
|
|
match: { sortBy: "updatedAtMs", sortDir: "desc" },
|
|
jobs: jobs.toSorted((left, right) => (right.updatedAtMs ?? 0) - (left.updatedAtMs ?? 0)),
|
|
},
|
|
{
|
|
match: { sortBy: "name", sortDir: "asc" },
|
|
jobs: jobs.toSorted((left, right) => left.name.localeCompare(right.name)),
|
|
},
|
|
{
|
|
match: { sortBy: "name", sortDir: "desc" },
|
|
jobs: jobs.toSorted((left, right) => right.name.localeCompare(left.name)),
|
|
},
|
|
];
|
|
|
|
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(failedJobs, { limit: failedJobs.length }),
|
|
},
|
|
{ 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(failedRuns) },
|
|
{ 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 },
|
|
})),
|
|
},
|
|
};
|
|
}
|