





















@@ -11,6 +11,7 @@ import {
1111runCronJob,
1212startCronEdit,
1313startCronClone,
14+updateCronJobsFilter,
1415validateCronForm,
1516type CronState,
1617} from "./cron.ts";
@@ -24,6 +25,8 @@ function createState(overrides: Partial<CronState> = {}): CronState {
2425cronQuickCreateStep: "what",
2526cronQuickCreateDraft: null,
2627cronJobsLoadingMore: false,
28+cronJobsReloadPending: false,
29+cronJobsReloadPendingTableFilters: false,
2730cronJobs: [],
2831cronJobsTotal: 0,
2932cronJobsHasMore: false,
@@ -99,6 +102,13 @@ function requestPatch(call: readonly [method: string, payload?: unknown]) {
99102return requireRecord(requestPayload(call).patch, `${call[0]} patch`);
100103}
101104105+type EmptyCronListResponse = {
106+jobs: [];
107+total: number;
108+hasMore: boolean;
109+nextOffset: null;
110+};
111+102112describe("cron controller", () => {
103113it("loads model suggestions from the configured model view", async () => {
104114const request = vi.fn(async () => ({
@@ -1323,6 +1333,131 @@ describe("cron controller", () => {
13231333);
13241334});
132513351336+it("reloads cron jobs after filters change during an in-flight table load", async () => {
1337+let resolveFirst!: (value: EmptyCronListResponse) => void;
1338+const firstResponse = new Promise<EmptyCronListResponse>((resolve) => {
1339+resolveFirst = resolve;
1340+});
1341+const request = vi.fn(async (method: string, payload?: unknown) => {
1342+if (method !== "cron.list") {
1343+return {};
1344+}
1345+if (request.mock.calls.length === 1) {
1346+return firstResponse;
1347+}
1348+expectRecordFields(requireRecord(payload, "pending cron.list payload"), {
1349+scheduleKind: "cron",
1350+lastRunStatus: "unknown",
1351+});
1352+return { jobs: [], total: 0, hasMore: false, nextOffset: null };
1353+});
1354+const state = createState({
1355+client: { request } as unknown as CronState["client"],
1356+});
1357+1358+const firstLoad = loadCronJobsPage(state, { tableFilters: true });
1359+updateCronJobsFilter(state, {
1360+cronJobsScheduleKindFilter: "cron",
1361+cronJobsLastStatusFilter: "unknown",
1362+});
1363+await loadCronJobsPage(state, { tableFilters: true });
1364+resolveFirst({ jobs: [], total: 0, hasMore: false, nextOffset: null });
1365+await firstLoad;
1366+1367+expect(request).toHaveBeenCalledTimes(2);
1368+expect(state.cronJobsReloadPending).toBe(false);
1369+expect(state.cronJobsReloadPendingTableFilters).toBe(false);
1370+});
1371+1372+it("reloads cron jobs after filters change during an in-flight append load", async () => {
1373+let resolveAppend!: (value: EmptyCronListResponse) => void;
1374+const appendResponse = new Promise<EmptyCronListResponse>((resolve) => {
1375+resolveAppend = resolve;
1376+});
1377+const request = vi.fn(async (method: string, payload?: unknown) => {
1378+if (method !== "cron.list") {
1379+return {};
1380+}
1381+if (request.mock.calls.length === 1) {
1382+expectRecordFields(requireRecord(payload, "append cron.list payload"), {
1383+offset: 1,
1384+});
1385+return appendResponse;
1386+}
1387+expectRecordFields(requireRecord(payload, "pending append cron.list payload"), {
1388+offset: 0,
1389+scheduleKind: "cron",
1390+lastRunStatus: "unknown",
1391+});
1392+return { jobs: [], total: 0, hasMore: false, nextOffset: null };
1393+});
1394+const state = createState({
1395+client: { request } as unknown as CronState["client"],
1396+cronJobs: [
1397+{
1398+id: "existing",
1399+name: "Existing",
1400+enabled: true,
1401+createdAtMs: 0,
1402+updatedAtMs: 0,
1403+schedule: { kind: "every", everyMs: 60_000 },
1404+sessionTarget: "main",
1405+wakeMode: "next-heartbeat",
1406+payload: { kind: "systemEvent", text: "ping" },
1407+},
1408+],
1409+cronJobsHasMore: true,
1410+cronJobsNextOffset: 1,
1411+});
1412+1413+const appendLoad = loadCronJobsPage(state, { append: true, tableFilters: true });
1414+updateCronJobsFilter(state, {
1415+cronJobsScheduleKindFilter: "cron",
1416+cronJobsLastStatusFilter: "unknown",
1417+});
1418+await loadCronJobsPage(state, { tableFilters: true });
1419+resolveAppend({ jobs: [], total: 0, hasMore: false, nextOffset: null });
1420+await appendLoad;
1421+1422+expect(request).toHaveBeenCalledTimes(2);
1423+expect(state.cronJobsReloadPending).toBe(false);
1424+expect(state.cronJobsReloadPendingTableFilters).toBe(false);
1425+});
1426+1427+it("uses the latest queued cron jobs table-filter mode", async () => {
1428+let resolveFirst!: (value: EmptyCronListResponse) => void;
1429+const firstResponse = new Promise<EmptyCronListResponse>((resolve) => {
1430+resolveFirst = resolve;
1431+});
1432+const request = vi.fn(async (method: string, payload?: unknown) => {
1433+if (method !== "cron.list") {
1434+return {};
1435+}
1436+if (request.mock.calls.length === 1) {
1437+return firstResponse;
1438+}
1439+const pendingPayload = requireRecord(payload, "latest pending cron.list payload");
1440+expect(pendingPayload).not.toHaveProperty("scheduleKind");
1441+expect(pendingPayload).not.toHaveProperty("lastRunStatus");
1442+return { jobs: [], total: 0, hasMore: false, nextOffset: null };
1443+});
1444+const state = createState({
1445+client: { request } as unknown as CronState["client"],
1446+cronJobsScheduleKindFilter: "cron",
1447+cronJobsLastStatusFilter: "unknown",
1448+});
1449+1450+const firstLoad = loadCronJobsPage(state);
1451+await loadCronJobsPage(state, { tableFilters: true });
1452+await loadCronJobsPage(state);
1453+resolveFirst({ jobs: [], total: 0, hasMore: false, nextOffset: null });
1454+await firstLoad;
1455+1456+expect(request).toHaveBeenCalledTimes(2);
1457+expect(state.cronJobsReloadPending).toBe(false);
1458+expect(state.cronJobsReloadPendingTableFilters).toBe(false);
1459+});
1460+13261461it("drops malformed cron jobs before they enter UI state", async () => {
13271462const request = vi.fn(async (method: string) => {
13281463if (method === "cron.list") {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。