mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-22 18:35:21 -06:00
fix(ui): use active scheduler trigger capability (#126945)
Control UI automation trigger authoring now reflects the running scheduler across unsaved and saved-but-unapplied config edits and reconnects.
This commit is contained in:
committed by
GitHub
parent
aea1ca60e6
commit
a4b3f63a87
@@ -182,6 +182,7 @@ export function buildCronMocks(baseTime: number) {
|
||||
}));
|
||||
const status: CronStatus = {
|
||||
enabled: true,
|
||||
triggersEnabled: true,
|
||||
jobs: jobs.length,
|
||||
nextWakeAtMs: overdueJob.state?.nextRunAtMs,
|
||||
};
|
||||
|
||||
@@ -21,12 +21,14 @@ function streamJob(overrides: Partial<CronJobCreate> = {}): CronJobCreate {
|
||||
};
|
||||
}
|
||||
|
||||
async function createCron(triggersEnabled: boolean) {
|
||||
async function createCron(triggersEnabled: boolean | undefined, cronEnabled = true) {
|
||||
const { storePath } = await makeStorePath();
|
||||
const cron = new CronService({
|
||||
storePath,
|
||||
cronEnabled: true,
|
||||
cronConfig: { triggers: { enabled: triggersEnabled } },
|
||||
cronEnabled,
|
||||
...(triggersEnabled === undefined
|
||||
? {}
|
||||
: { cronConfig: { triggers: { enabled: triggersEnabled } } }),
|
||||
log: logger,
|
||||
enqueueSystemEvent: vi.fn(),
|
||||
requestHeartbeat: vi.fn(),
|
||||
@@ -37,6 +39,27 @@ async function createCron(triggersEnabled: boolean) {
|
||||
}
|
||||
|
||||
describe("cron stream schedule validation", () => {
|
||||
it.each([
|
||||
{ cronEnabled: true, configured: undefined, triggersEnabled: true },
|
||||
{ cronEnabled: true, configured: true, triggersEnabled: true },
|
||||
{ cronEnabled: true, configured: false, triggersEnabled: false },
|
||||
{ cronEnabled: false, configured: true, triggersEnabled: true },
|
||||
{ cronEnabled: false, configured: false, triggersEnabled: false },
|
||||
])(
|
||||
"reports active trigger capability independently of scheduler enablement ($cronEnabled/$configured)",
|
||||
async ({ cronEnabled, configured, triggersEnabled }) => {
|
||||
const cron = await createCron(configured, cronEnabled);
|
||||
try {
|
||||
await expect(cron.status()).resolves.toMatchObject({
|
||||
enabled: cronEnabled,
|
||||
triggersEnabled,
|
||||
});
|
||||
} finally {
|
||||
cron.stop();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
it("rejects creation while cron triggers are disabled", async () => {
|
||||
const cron = await createCron(false);
|
||||
try {
|
||||
|
||||
@@ -42,6 +42,7 @@ export async function status(state: CronServiceState) {
|
||||
const sqlitePath = resolveOpenClawStateSqlitePath();
|
||||
return {
|
||||
enabled: state.deps.cronEnabled,
|
||||
triggersEnabled: state.deps.cronConfig?.triggers?.enabled !== false,
|
||||
storePath: sqlitePath,
|
||||
storage: "sqlite" as const,
|
||||
sqlitePath,
|
||||
|
||||
@@ -372,6 +372,7 @@ export type CronWakeMode = "now" | "next-heartbeat";
|
||||
/** Lightweight service status returned to gateway/control surfaces. */
|
||||
export type CronStatusSummary = {
|
||||
enabled: boolean;
|
||||
triggersEnabled: boolean;
|
||||
/** @deprecated Alias for `sqlitePath`. */
|
||||
storePath: string;
|
||||
/** Storage backend identifier. */
|
||||
|
||||
@@ -701,6 +701,10 @@ describe("gateway server cron", () => {
|
||||
const cronState = await createDirectCronState();
|
||||
|
||||
try {
|
||||
await expect(directCronReq(cronState, "cron.status", {})).resolves.toMatchObject({
|
||||
ok: true,
|
||||
payload: { enabled: false, triggersEnabled: false },
|
||||
});
|
||||
const response = await directCronReq(cronState, "cron.add", {
|
||||
name: "disabled watcher",
|
||||
enabled: true,
|
||||
|
||||
@@ -93,6 +93,7 @@ function createMockCronService(): CronServiceContract {
|
||||
stop: vi.fn(),
|
||||
status: vi.fn(async () => ({
|
||||
enabled: true,
|
||||
triggersEnabled: true,
|
||||
storePath: "/tmp/openclaw-test-cron.json",
|
||||
storage: "sqlite" as const,
|
||||
sqlitePath: "/tmp/openclaw-test-state/state/openclaw.sqlite",
|
||||
|
||||
@@ -566,6 +566,7 @@ export type CronPayload = ProtocolCronJob["payload"];
|
||||
|
||||
export type CronStatus = {
|
||||
enabled: boolean;
|
||||
triggersEnabled: boolean;
|
||||
jobs: number;
|
||||
nextWakeAtMs?: number | null;
|
||||
};
|
||||
|
||||
@@ -3,6 +3,7 @@ import { mkdir } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import type { Page } from "playwright";
|
||||
import { expect, it } from "vitest";
|
||||
import type { ApplicationContext } from "../app/context.ts";
|
||||
import { installMockGateway } from "../test-helpers/control-ui-e2e.ts";
|
||||
import { createControlUiE2eSuite } from "./control-ui-e2e-suite.test-support.ts";
|
||||
|
||||
@@ -14,6 +15,7 @@ const suite = createControlUiE2eSuite({
|
||||
|
||||
const proofDirectory = process.env.OPENCLAW_TRIGGER_UI_PROOF_DIR;
|
||||
const proofStage = process.env.OPENCLAW_TRIGGER_UI_PROOF_STAGE ?? "after";
|
||||
type CronTriggerTestApp = HTMLElement & { runtime?: { context: ApplicationContext } };
|
||||
|
||||
const scriptJob = {
|
||||
id: "existing-script-automation",
|
||||
@@ -52,6 +54,14 @@ async function captureProof(page: Page, name: string) {
|
||||
});
|
||||
}
|
||||
|
||||
async function captureTriggerCapabilityProof(page: Page, name: string) {
|
||||
await page
|
||||
.locator(".settings-row__title")
|
||||
.filter({ hasText: "Condition trigger" })
|
||||
.evaluate((element) => element.scrollIntoView({ block: "center" }));
|
||||
await captureProof(page, name);
|
||||
}
|
||||
|
||||
async function selectSeconds(page: Page) {
|
||||
const unit = page.locator("wa-select").filter({
|
||||
has: page.locator('[slot="label"]', { hasText: "Unit" }),
|
||||
@@ -82,7 +92,7 @@ suite.define(() => {
|
||||
hasMore: false,
|
||||
nextOffset: null,
|
||||
},
|
||||
"cron.status": { enabled: true, jobs: 1, nextWakeAtMs: null },
|
||||
"cron.status": { enabled: true, triggersEnabled: true, jobs: 1, nextWakeAtMs: null },
|
||||
},
|
||||
});
|
||||
|
||||
@@ -162,4 +172,117 @@ suite.define(() => {
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps saved and unsaved trigger drafts separate from reconnect-refreshed scheduler capability", async () => {
|
||||
await suite.withPage(
|
||||
{ locale: "en-US", serviceWorkers: "block", viewport: { height: 1_050, width: 1_440 } },
|
||||
async ({ page }) => {
|
||||
const initialConfig = { cron: { triggers: { enabled: true } } };
|
||||
const gateway = await installMockGateway(page, {
|
||||
methodResponses: {
|
||||
"config.get": {
|
||||
appliedConfigHash: "trigger-config-1",
|
||||
config: initialConfig,
|
||||
configRevisionHash: "trigger-config-1",
|
||||
hash: "trigger-config-1",
|
||||
issues: [],
|
||||
raw: JSON.stringify(initialConfig),
|
||||
valid: true,
|
||||
},
|
||||
"cron.list": listResponse([]),
|
||||
"cron.runs": { entries: [], total: 0, offset: 0, limit: 50, hasMore: false },
|
||||
"cron.status": { enabled: true, triggersEnabled: true, jobs: 0, nextWakeAtMs: null },
|
||||
},
|
||||
});
|
||||
|
||||
await page.goto(`${suite.server.baseUrl}cron`);
|
||||
await page.locator('[data-test-id="cron-new-task"]').click();
|
||||
await page.locator("details.cron-advanced > summary").click();
|
||||
const triggerToggle = page
|
||||
.locator(".settings-row--toggle")
|
||||
.filter({ hasText: "Condition trigger" });
|
||||
await expect.poll(() => triggerToggle.count()).toBe(1);
|
||||
|
||||
const unsaved = await page.evaluate(async () => {
|
||||
const config = (document.querySelector("openclaw-app") as CronTriggerTestApp).runtime
|
||||
?.context.runtimeConfig;
|
||||
if (!config) {
|
||||
throw new Error("Runtime config capability is unavailable");
|
||||
}
|
||||
await config.ensureLoaded();
|
||||
config.setWritesSuspended(true);
|
||||
config.patchForm(["cron", "triggers", "enabled"], false);
|
||||
return { dirty: config.state.configFormDirty, needsApply: config.state.configNeedsApply };
|
||||
});
|
||||
expect(unsaved).toEqual({ dirty: true, needsApply: false });
|
||||
expect(await gateway.getRequests("config.set")).toHaveLength(0);
|
||||
await expect.poll(() => triggerToggle.count()).toBe(1);
|
||||
await captureTriggerCapabilityProof(page, "05-unsaved-disable-keeps-active-trigger");
|
||||
|
||||
const saveResult = await page.evaluate(async () => {
|
||||
const config = (document.querySelector("openclaw-app") as CronTriggerTestApp).runtime
|
||||
?.context.runtimeConfig;
|
||||
if (!config) {
|
||||
throw new Error("Runtime config capability is unavailable");
|
||||
}
|
||||
config.setWritesSuspended(false);
|
||||
const saved = await config.save();
|
||||
return {
|
||||
dirty: config.state.configFormDirty,
|
||||
needsApply: config.state.configNeedsApply,
|
||||
saved,
|
||||
};
|
||||
});
|
||||
expect(saveResult).toEqual({ dirty: false, needsApply: true, saved: true });
|
||||
const savedRequest = await gateway.waitForRequest("config.set");
|
||||
expect(JSON.parse(String((savedRequest.params as { raw?: string }).raw))).toEqual({
|
||||
cron: { triggers: { enabled: false } },
|
||||
});
|
||||
expect(await gateway.getRequests("config.apply")).toHaveLength(0);
|
||||
await expect.poll(() => triggerToggle.count()).toBe(1);
|
||||
await captureTriggerCapabilityProof(page, "06-saved-unapplied-keeps-active-trigger");
|
||||
|
||||
const previousStatuses = (await gateway.getRequests("cron.status")).length;
|
||||
await gateway.setMethodResponse("cron.status", {
|
||||
enabled: true,
|
||||
triggersEnabled: false,
|
||||
jobs: 0,
|
||||
nextWakeAtMs: null,
|
||||
});
|
||||
await gateway.closeLatest(1012, "refresh effective trigger capability");
|
||||
await expect
|
||||
.poll(async () => (await gateway.getRequests("cron.status")).length)
|
||||
.toBeGreaterThan(previousStatuses);
|
||||
await page.locator('[data-test-id="cron-new-task"]').click();
|
||||
await page.locator("details.cron-advanced > summary").click();
|
||||
await expect.poll(() => triggerToggle.count()).toBe(0);
|
||||
await page.getByText("Condition triggers are disabled by cron.triggers.enabled.").waitFor();
|
||||
await captureTriggerCapabilityProof(page, "07-reconnect-refreshes-disabled-trigger");
|
||||
|
||||
const oppositeDraft = await page.evaluate(async () => {
|
||||
const config = (document.querySelector("openclaw-app") as CronTriggerTestApp).runtime
|
||||
?.context.runtimeConfig;
|
||||
if (!config) {
|
||||
throw new Error("Runtime config capability is unavailable");
|
||||
}
|
||||
await config.ensureLoaded();
|
||||
config.setWritesSuspended(true);
|
||||
config.patchForm(["cron", "triggers", "enabled"], true);
|
||||
return config.state.configFormDirty;
|
||||
});
|
||||
expect(oppositeDraft).toBe(true);
|
||||
await expect.poll(() => triggerToggle.count()).toBe(0);
|
||||
await captureTriggerCapabilityProof(
|
||||
page,
|
||||
"08-unsaved-enable-cannot-author-disabled-trigger",
|
||||
);
|
||||
await page.evaluate(async () => {
|
||||
const config = (document.querySelector("openclaw-app") as CronTriggerTestApp).runtime
|
||||
?.context.runtimeConfig;
|
||||
await config?.discardDraft();
|
||||
config?.setWritesSuspended(false);
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -61,7 +61,7 @@ suite.define(() => {
|
||||
],
|
||||
},
|
||||
"cron.runs": { entries: [], total: 0, offset: 0, limit: 50, hasMore: false },
|
||||
"cron.status": { enabled: true, jobs: 2, nextWakeAtMs: null },
|
||||
"cron.status": { enabled: true, triggersEnabled: false, jobs: 2, nextWakeAtMs: null },
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -145,8 +145,8 @@ describe("renderAgents", () => {
|
||||
const job = createCronJob("implicit-default-job", {
|
||||
name: "Implicit default-agent reminder",
|
||||
});
|
||||
const globalNextWakeAtMs = Date.now() + 60_000;
|
||||
const scopedNextWakeAtMs = globalNextWakeAtMs + 3_600_000;
|
||||
const nextWakeAtMs = Date.now() + 60_000;
|
||||
const scopedNextWakeAtMs = nextWakeAtMs + 3_600_000;
|
||||
const container = document.createElement("div");
|
||||
render(
|
||||
renderAgents(
|
||||
@@ -154,7 +154,7 @@ describe("renderAgents", () => {
|
||||
activePanel: "cron",
|
||||
selectedAgentId: "alpha",
|
||||
cron: {
|
||||
status: { enabled: true, jobs: 51, nextWakeAtMs: globalNextWakeAtMs },
|
||||
status: { enabled: true, triggersEnabled: true, jobs: 51, nextWakeAtMs },
|
||||
jobs: [job],
|
||||
jobsTotal: 1,
|
||||
jobsHasMore: false,
|
||||
@@ -188,7 +188,7 @@ describe("renderAgents", () => {
|
||||
expect(nextWakeRow?.querySelector(".settings-row__control")?.textContent?.trim()).toBe(
|
||||
formatNextRun(scopedNextWakeAtMs),
|
||||
);
|
||||
expect(nextWakeRow?.textContent).not.toContain(formatNextRun(globalNextWakeAtMs));
|
||||
expect(nextWakeRow?.textContent).not.toContain(formatNextRun(nextWakeAtMs));
|
||||
});
|
||||
|
||||
it("loads and renders the selected agent's 51st cron job when Load more is clicked", async () => {
|
||||
@@ -227,7 +227,7 @@ describe("renderAgents", () => {
|
||||
activePanel: "cron",
|
||||
selectedAgentId: "alpha",
|
||||
cron: {
|
||||
status: { enabled: true, jobs: 80, nextWakeAtMs: null },
|
||||
status: { enabled: true, triggersEnabled: true, jobs: 80, nextWakeAtMs: null },
|
||||
jobs: cronState.cronJobs,
|
||||
jobsTotal: cronState.cronJobsTotal,
|
||||
jobsHasMore: cronState.cronJobsHasMore,
|
||||
|
||||
@@ -148,8 +148,17 @@ function cronListResponse(jobs: CronJob[]): CronJobsListResult {
|
||||
};
|
||||
}
|
||||
|
||||
function createRequest() {
|
||||
function createRequest(
|
||||
cronStatus: { enabled: boolean; jobs: number; triggersEnabled: boolean } = {
|
||||
enabled: true,
|
||||
jobs: 0,
|
||||
triggersEnabled: true,
|
||||
},
|
||||
) {
|
||||
return vi.fn(async (method: string) => {
|
||||
if (method === "cron.status") {
|
||||
return { ...cronStatus };
|
||||
}
|
||||
if (method === "cron.list") {
|
||||
return cronListResponse([]);
|
||||
}
|
||||
@@ -169,6 +178,39 @@ afterEach(() => {
|
||||
});
|
||||
|
||||
describe("CronPage editor state sync", () => {
|
||||
it.each([
|
||||
{ scenario: "an unsaved enable edit", active: false, edited: true, saved: false },
|
||||
{ scenario: "an unsaved disable edit", active: true, edited: false, saved: false },
|
||||
{ scenario: "a saved-but-unapplied enable edit", active: false, edited: true, saved: true },
|
||||
{ scenario: "a saved-but-unapplied disable edit", active: true, edited: false, saved: true },
|
||||
])("keeps trigger authoring owned by cron.status during $scenario", async (scenario) => {
|
||||
const request = createRequest({ enabled: true, jobs: 0, triggersEnabled: scenario.active });
|
||||
const gateway = createGateway({ request } as unknown as GatewayBrowserClient, true);
|
||||
const context = createContext(gateway);
|
||||
const editedConfig = { cron: { triggers: { enabled: scenario.edited } } };
|
||||
Object.assign(context.runtimeConfig.state, {
|
||||
configForm: editedConfig,
|
||||
configFormDirty: !scenario.saved,
|
||||
configNeedsApply: scenario.saved,
|
||||
configSnapshot: scenario.saved ? { config: editedConfig, sourceConfig: editedConfig } : null,
|
||||
});
|
||||
const page = createPage(context, { render: true });
|
||||
|
||||
await waitForCronPage(() =>
|
||||
expect(page.cron.cronStatus).toMatchObject({ triggersEnabled: scenario.active }),
|
||||
);
|
||||
(page.querySelector('[data-test-id="cron-new-task"]') as HTMLButtonElement).click();
|
||||
await waitForCronPage(() => expect(page.querySelector("fieldset.cron-editor")).not.toBeNull());
|
||||
|
||||
const triggerToggle = Array.from(page.querySelectorAll("wa-switch.settings-toggle")).find(
|
||||
(toggle) => toggle.textContent?.includes("Condition trigger"),
|
||||
);
|
||||
expect(Boolean(triggerToggle)).toBe(scenario.active);
|
||||
if (!scenario.active) {
|
||||
expect(page.textContent).toContain("disabled by cron.triggers.enabled");
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps conflict detail attached to the authoritative job outside active filters", async () => {
|
||||
const staleJob: CronJob = {
|
||||
id: "filtered-conflict-job",
|
||||
@@ -663,7 +705,7 @@ describe("CronPage lifecycle", () => {
|
||||
const connectedState = page.cron;
|
||||
page.cron = {
|
||||
...connectedState,
|
||||
cronStatus: { enabled: true, jobs: 1 },
|
||||
cronStatus: { enabled: true, triggersEnabled: true, jobs: 1 },
|
||||
cronJobs: [{ id: "old" } as never],
|
||||
cronCreateOpen: true,
|
||||
};
|
||||
@@ -682,6 +724,40 @@ describe("CronPage lifecycle", () => {
|
||||
expect(page.cron).not.toBe(disconnectedState);
|
||||
});
|
||||
|
||||
it("refreshes trigger authoring from scheduler status after reconnect", async () => {
|
||||
const schedulerStatus = { enabled: true, jobs: 0, triggersEnabled: true };
|
||||
const request = createRequest(schedulerStatus);
|
||||
const client = { request } as unknown as GatewayBrowserClient;
|
||||
const gateway = createGateway(client, true);
|
||||
const context = createContext(gateway);
|
||||
Object.assign(context.runtimeConfig.state, {
|
||||
configForm: { cron: { triggers: { enabled: true } } },
|
||||
configNeedsApply: true,
|
||||
});
|
||||
const page = createPage(context, { render: true });
|
||||
|
||||
await waitForCronPage(() =>
|
||||
expect(page.cron.cronStatus).toMatchObject({ triggersEnabled: true }),
|
||||
);
|
||||
schedulerStatus.triggersEnabled = false;
|
||||
gateway.emitSnapshot({ phase: "stopped" });
|
||||
expect(page.cron.cronStatus).toBeNull();
|
||||
gateway.emitSnapshot({ phase: "connected" });
|
||||
|
||||
await waitForCronPage(() =>
|
||||
expect(page.cron.cronStatus).toMatchObject({ triggersEnabled: false }),
|
||||
);
|
||||
expect(request.mock.calls.filter(([method]) => method === "cron.status")).toHaveLength(2);
|
||||
(page.querySelector('[data-test-id="cron-new-task"]') as HTMLButtonElement).click();
|
||||
await waitForCronPage(() => expect(page.querySelector("fieldset.cron-editor")).not.toBeNull());
|
||||
|
||||
const triggerToggle = Array.from(page.querySelectorAll("wa-switch.settings-toggle")).find(
|
||||
(toggle) => toggle.textContent?.includes("Condition trigger"),
|
||||
);
|
||||
expect(triggerToggle).toBeUndefined();
|
||||
expect(page.textContent).toContain("disabled by cron.triggers.enabled");
|
||||
});
|
||||
|
||||
it("rejects model suggestions from an earlier connection epoch", async () => {
|
||||
const staleModels = createDeferred<{ models: Array<{ id: string }> }>();
|
||||
let modelRequestCount = 0;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { consume } from "@lit/context";
|
||||
import { isRecord } from "@openclaw/normalization-core/record-coerce";
|
||||
import { html } from "lit";
|
||||
import { state } from "lit/decorators.js";
|
||||
import type { AgentsListResult, CronJob } from "../../api/types.ts";
|
||||
@@ -11,7 +10,6 @@ import { showConfirmDialog } from "../../components/confirm-dialog.ts";
|
||||
import { renderSettingsWorkspace } from "../../components/settings-workspace.ts";
|
||||
import { t } from "../../i18n/index.ts";
|
||||
import { watchAgentScope } from "../../lib/agents/index.ts";
|
||||
import { currentConfigObject } from "../../lib/config/config-state-model.ts";
|
||||
import {
|
||||
addCronJob,
|
||||
cancelCronEdit,
|
||||
@@ -48,13 +46,6 @@ import { SubscriptionsController } from "../../lit/subscriptions-controller.ts";
|
||||
import { buildCronSuggestions, THINKING_SUGGESTIONS } from "./form-suggestions.ts";
|
||||
import { renderCron, type CronDetailTab, type CronListTab } from "./view.ts";
|
||||
|
||||
function resolveCronTriggersEnabled(context: ApplicationContext): boolean {
|
||||
const config = currentConfigObject(context.runtimeConfig.state) ?? {};
|
||||
const cron = isRecord(config.cron) ? config.cron : undefined;
|
||||
const triggers = cron && isRecord(cron.triggers) ? cron.triggers : undefined;
|
||||
return triggers?.enabled !== false;
|
||||
}
|
||||
|
||||
class CronPage extends OpenClawLightDomElement {
|
||||
@consume({ context: applicationContext, subscribe: true })
|
||||
private context!: ApplicationContext;
|
||||
@@ -408,7 +399,6 @@ class CronPage extends OpenClawLightDomElement {
|
||||
agentId: fallbackAgentId,
|
||||
loading: this.cron.cronLoading,
|
||||
canManage,
|
||||
triggersEnabled: resolveCronTriggersEnabled(this.context),
|
||||
status: this.cron.cronStatus,
|
||||
failingCount: this.cron.cronFailingCount,
|
||||
agentScoped: this.cron.cronAgentId !== null,
|
||||
|
||||
@@ -24,7 +24,7 @@ describe("cron view run history", () => {
|
||||
{ ts: 1_000, jobId: "job-1", action: "finished", status: "ok", summary: "older run" },
|
||||
{ ts: 2_000, jobId: "job-2", action: "finished", status: "ok", summary: "newer run" },
|
||||
],
|
||||
status: { enabled: true, jobs: 2 },
|
||||
status: { enabled: true, triggersEnabled: true, jobs: 2 },
|
||||
});
|
||||
|
||||
const titles = Array.from(container.querySelectorAll(".cron-run-entry__title")).map((el) =>
|
||||
|
||||
@@ -26,9 +26,12 @@ function createCronViewProps(overrides: Partial<CronProps> = {}): CronProps {
|
||||
agentId: "main",
|
||||
loading: false,
|
||||
canManage: true,
|
||||
triggersEnabled: true,
|
||||
jobsLoadingMore: false,
|
||||
status: null,
|
||||
status: {
|
||||
enabled: true,
|
||||
triggersEnabled: true,
|
||||
jobs: Math.max(overrides.jobsTotal ?? 0, overrides.jobs?.length ?? 0),
|
||||
},
|
||||
failingCount: null,
|
||||
agentScoped: false,
|
||||
scopedTotal: null,
|
||||
|
||||
@@ -54,7 +54,7 @@ describe("cron view list pane", () => {
|
||||
agentScoped: true,
|
||||
scopedTotal: 3,
|
||||
scopedNextWakeAtMs: Date.now() + 60_000,
|
||||
status: { enabled: true, jobs: 99, nextWakeAtMs: null },
|
||||
status: { enabled: true, triggersEnabled: true, jobs: 99, nextWakeAtMs: null },
|
||||
});
|
||||
const values = [...container.querySelectorAll(".cron-stat__value")].map((entry) =>
|
||||
entry.textContent?.trim(),
|
||||
@@ -68,7 +68,7 @@ describe("cron view list pane", () => {
|
||||
const container = renderView({
|
||||
agentScoped: true,
|
||||
scopedNextWakeAtMs: Date.now() + 60_000,
|
||||
status: { enabled: false, jobs: 3, nextWakeAtMs: null },
|
||||
status: { enabled: false, triggersEnabled: true, jobs: 3, nextWakeAtMs: null },
|
||||
});
|
||||
const values = [...container.querySelectorAll(".cron-stat__value")].map((entry) =>
|
||||
entry.textContent?.trim(),
|
||||
@@ -303,7 +303,7 @@ describe("cron view list pane", () => {
|
||||
|
||||
it("shows a scheduler banner only while the scheduler is off", () => {
|
||||
const off = renderView({
|
||||
status: { enabled: false, jobs: 2 },
|
||||
status: { enabled: false, triggersEnabled: true, jobs: 2 },
|
||||
jobs: [createJob("job-1")],
|
||||
jobsTotal: 2,
|
||||
});
|
||||
@@ -313,7 +313,7 @@ describe("cron view list pane", () => {
|
||||
const footer = getElement(off, ".cron-table__footer", HTMLDivElement);
|
||||
expect(footer.textContent).toContain("1 of 2");
|
||||
|
||||
const on = renderView({ status: { enabled: true, jobs: 2 } });
|
||||
const on = renderView({ status: { enabled: true, triggersEnabled: true, jobs: 2 } });
|
||||
expect(on.querySelector('[data-test-id="cron-scheduler-banner"]')).toBeNull();
|
||||
});
|
||||
|
||||
@@ -704,15 +704,23 @@ describe("cron view editor", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("waits for scheduler status before presenting trigger capability", () => {
|
||||
const pending = renderView({ createOpen: true, status: null });
|
||||
|
||||
expect(findToggleByLabel(pending, "Condition trigger")).toBeNull();
|
||||
expect(pending.textContent).not.toContain("disabled by cron.triggers.enabled");
|
||||
});
|
||||
|
||||
it("hides trigger authoring when the operator disabled triggers but keeps clear available", () => {
|
||||
const onFormChange = vi.fn();
|
||||
const disabled = renderView({ createOpen: true, triggersEnabled: false, onFormChange });
|
||||
const status = { enabled: true, triggersEnabled: false, jobs: 0 };
|
||||
const disabled = renderView({ createOpen: true, status, onFormChange });
|
||||
expect(disabled.querySelector("#cron-trigger-script")).toBeNull();
|
||||
expect(disabled.textContent).toContain("disabled by cron.triggers.enabled");
|
||||
|
||||
const configured = renderView({
|
||||
createOpen: true,
|
||||
triggersEnabled: false,
|
||||
status,
|
||||
onFormChange,
|
||||
form: {
|
||||
...DEFAULT_CRON_FORM,
|
||||
|
||||
@@ -72,7 +72,6 @@ type CronProps = {
|
||||
loading: boolean;
|
||||
/** Canonical gateway capability for every mutation-capable cron control. */
|
||||
canManage: boolean;
|
||||
triggersEnabled: boolean;
|
||||
jobsLoadingMore: boolean;
|
||||
status: CronStatus | null;
|
||||
failingCount: number | null;
|
||||
@@ -1736,7 +1735,10 @@ function renderAdvanced(
|
||||
|
||||
function renderTriggerRows(props: CronProps) {
|
||||
const scriptPayload = props.form.payloadKind === "script";
|
||||
if (!props.triggersEnabled || scriptPayload) {
|
||||
if (!scriptPayload && props.status === null) {
|
||||
return nothing;
|
||||
}
|
||||
if (props.status?.triggersEnabled !== true || scriptPayload) {
|
||||
return renderSettingsRow({
|
||||
title: t("cron.form.conditionTrigger"),
|
||||
description: scriptPayload
|
||||
|
||||
Reference in New Issue
Block a user