From dffaf708e910c5fd76e35b34bb728b98bb539a4f Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 15 Aug 2025 16:58:14 +0200 Subject: [PATCH 1/6] backend-test-utils: add mock implementation for scheduler service Signed-off-by: Patrik Oldsberg --- .changeset/clean-clocks-thank.md | 5 + .../backend-system/core-services/scheduler.md | 28 +++ packages/backend-test-utils/report.api.md | 8 +- .../src/services/MockSchedulerService.test.ts | 208 ++++++++++++++++++ .../src/services/MockSchedulerService.ts | 164 ++++++++++++++ .../src/services/mockServices.ts | 11 +- 6 files changed, 421 insertions(+), 3 deletions(-) create mode 100644 .changeset/clean-clocks-thank.md create mode 100644 packages/backend-test-utils/src/services/MockSchedulerService.test.ts create mode 100644 packages/backend-test-utils/src/services/MockSchedulerService.ts diff --git a/.changeset/clean-clocks-thank.md b/.changeset/clean-clocks-thank.md new file mode 100644 index 0000000000..47e99c4e41 --- /dev/null +++ b/.changeset/clean-clocks-thank.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-test-utils': minor +--- + +Switched out `mockServices.scheduler` to use a mocked implementation instead of the default scheduler implementation. This implementation runs any scheduled tasks immediately on startup, as long as they don't have an initial delay or a manual trigger. After the initial run, the tasks are never run again unless manually triggered. diff --git a/docs/backend-system/core-services/scheduler.md b/docs/backend-system/core-services/scheduler.md index eb25692698..b8f8c6aac1 100644 --- a/docs/backend-system/core-services/scheduler.md +++ b/docs/backend-system/core-services/scheduler.md @@ -129,3 +129,31 @@ Responds with - `200 OK` if successful - `404 Not Found` if there was no such registered task for this plugin - `409 Conflict` if the task was already in a running state + +## Testing + +The `@backstage/backend-test-utils` package provides `mockServices.scheduler`, which provides a mocked implementation of the scheduler service that can be used in tests. This mocked implementation is used by default in `startTestBackend`, and it will immediately run any registered tasks on startup as long as they're not configured to run manually or with an initial delay. + +A dedicated instance can be used for more control during testing, with the mock implementation providing additional utilities to trigger and wait for tasks to complete: + +```ts +it('should trigger a task', async () => { + const scheduler = mockServices.scheduler(); + + const { server } = await startTestBackend({ + features: [scheduler.factory()], + }); + + // Start waiting for some task to complete + const waitForTask = scheduler.waitForTask('some-task-id'); + + // Call an endpoit that triggers a task + const res = await request(server).post( + '/api/my-plugin/route-that-triggers-a-task', + ); + expect(res.status).toBe(200); + + // Wait for the task to complete + await waitForTask; +}); +``` diff --git a/packages/backend-test-utils/report.api.md b/packages/backend-test-utils/report.api.md index fb22e5c850..eeb5009641 100644 --- a/packages/backend-test-utils/report.api.md +++ b/packages/backend-test-utils/report.api.md @@ -366,9 +366,15 @@ export namespace mockServices { ) => ServiceMock; } // (undocumented) + export function scheduler(): SchedulerService; + // (undocumented) export namespace scheduler { const // (undocumented) - factory: () => ServiceFactory; + factory: (options?: { + skipTaskRunOnStartup?: boolean; + includeManualTasksOnStartup?: boolean; + includeInitialDelayedTasksOnStartup?: boolean; + }) => ServiceFactory; const // (undocumented) mock: ( partialImpl?: Partial | undefined, diff --git a/packages/backend-test-utils/src/services/MockSchedulerService.test.ts b/packages/backend-test-utils/src/services/MockSchedulerService.test.ts new file mode 100644 index 0000000000..8e26fd770e --- /dev/null +++ b/packages/backend-test-utils/src/services/MockSchedulerService.test.ts @@ -0,0 +1,208 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + coreServices, + createBackendPlugin, +} from '@backstage/backend-plugin-api'; +import { startTestBackend } from '../wiring'; +import { MockSchedulerService } from './MockSchedulerService'; +import { mockServices } from './mockServices'; +import { setTimeout } from 'timers/promises'; + +const baseOpts = { + frequency: { seconds: 10 }, + timeout: { seconds: 10 }, +}; + +describe('MockSchedulerService', () => { + it('should run a task', async () => { + const scheduler = new MockSchedulerService(); + expect(scheduler).toBeDefined(); + + const taskFn = jest.fn(); + scheduler.scheduleTask({ + ...baseOpts, + id: 'test', + fn: taskFn, + }); + + expect(taskFn).not.toHaveBeenCalled(); + + await scheduler.triggerTask('test'); + + expect(taskFn).toHaveBeenCalled(); + }); + + it('should run tasks on startup', async () => { + const testFnPlain = jest.fn(); + const testFnInitialDelay = jest.fn(); + const testFnManual = jest.fn(); + const testFnLocal = jest.fn(); + + // Relying on the fact that the mock scheduler service is used by default + await startTestBackend({ + features: [ + createBackendPlugin({ + pluginId: 'tester', + register(reg) { + reg.registerInit({ + deps: { scheduler: coreServices.scheduler }, + async init({ scheduler }) { + scheduler.scheduleTask({ + ...baseOpts, + id: 'test-plain', + fn: testFnPlain, + }); + scheduler.scheduleTask({ + ...baseOpts, + id: 'test-local', + scope: 'local', + fn: testFnLocal, + }); + + // Should not run by default + scheduler.scheduleTask({ + ...baseOpts, + id: 'test-with-initial-delay', + initialDelay: { seconds: 1 }, + fn: testFnInitialDelay, + }); + scheduler.scheduleTask({ + ...baseOpts, + id: 'test-manual', + frequency: { trigger: 'manual' }, + fn: testFnManual, + }); + }, + }); + }, + }), + ], + }); + + expect(testFnPlain).toHaveBeenCalled(); + expect(testFnLocal).toHaveBeenCalled(); + expect(testFnInitialDelay).not.toHaveBeenCalled(); + expect(testFnManual).not.toHaveBeenCalled(); + }); + + it('should not run tasks on startup if skipped', async () => { + const testFnPlain = jest.fn(); + + await startTestBackend({ + features: [ + new MockSchedulerService().factory({ skipTaskRunOnStartup: true }), + createBackendPlugin({ + pluginId: 'tester', + register(reg) { + reg.registerInit({ + deps: { scheduler: coreServices.scheduler }, + async init({ scheduler }) { + scheduler.scheduleTask({ + ...baseOpts, + id: 'test-plain', + fn: testFnPlain, + }); + }, + }); + }, + }), + ], + }); + + expect(testFnPlain).not.toHaveBeenCalled(); + }); + + it('should run all tasks on startup if configured', async () => { + const testFnPlain = jest.fn(); + const testFnInitialDelay = jest.fn(); + const testFnManual = jest.fn(); + const testFnLocal = jest.fn(); + + await startTestBackend({ + features: [ + mockServices.scheduler.factory({ + includeManualTasksOnStartup: true, + includeInitialDelayedTasksOnStartup: true, + }), + createBackendPlugin({ + pluginId: 'tester', + register(reg) { + reg.registerInit({ + deps: { scheduler: coreServices.scheduler }, + async init({ scheduler }) { + scheduler.scheduleTask({ + ...baseOpts, + id: 'test-plain', + fn: testFnPlain, + }); + scheduler.scheduleTask({ + ...baseOpts, + id: 'test-local', + scope: 'local', + fn: testFnLocal, + }); + + // Should not run by default + scheduler.scheduleTask({ + ...baseOpts, + id: 'test-with-initial-delay', + initialDelay: { seconds: 1 }, + fn: testFnInitialDelay, + }); + scheduler.scheduleTask({ + ...baseOpts, + id: 'test-manual', + frequency: { trigger: 'manual' }, + fn: testFnManual, + }); + }, + }); + }, + }), + ], + }); + + expect(testFnPlain).toHaveBeenCalled(); + expect(testFnLocal).toHaveBeenCalled(); + expect(testFnInitialDelay).toHaveBeenCalled(); + expect(testFnManual).toHaveBeenCalled(); + }); + + it('should wait for a specific task to complete', async () => { + const scheduler = new MockSchedulerService(); + const taskFn = jest.fn(); + scheduler.scheduleTask({ + ...baseOpts, + id: 'test', + fn: taskFn, + }); + + const wait = scheduler.waitForTask('test'); + + const isDone = () => + Promise.race([wait.then(() => true), setTimeout(1, false)]); + + expect(taskFn).not.toHaveBeenCalled(); + await expect(isDone()).resolves.toBe(false); + + await scheduler.triggerTask('test'); + + expect(taskFn).toHaveBeenCalled(); + await expect(isDone()).resolves.toBe(true); + }); +}); diff --git a/packages/backend-test-utils/src/services/MockSchedulerService.ts b/packages/backend-test-utils/src/services/MockSchedulerService.ts new file mode 100644 index 0000000000..7648770eb5 --- /dev/null +++ b/packages/backend-test-utils/src/services/MockSchedulerService.ts @@ -0,0 +1,164 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + coreServices, + createServiceFactory, + SchedulerService, + SchedulerServiceTaskDescriptor, + SchedulerServiceTaskInvocationDefinition, + SchedulerServiceTaskRunner, + SchedulerServiceTaskScheduleDefinition, +} from '@backstage/backend-plugin-api'; +import { createDeferred, DeferredPromise } from '@backstage/types'; + +export class MockSchedulerService implements SchedulerService { + readonly #tasks = new Map< + string, + SchedulerServiceTaskInvocationDefinition & + SchedulerServiceTaskScheduleDefinition & { + descriptor: SchedulerServiceTaskDescriptor; + } + >(); + readonly #runningTasks = new Set(); + readonly #deferredTaskCompletions = new Map>(); + + /** + * Creates a service factory for this mock scheduler instance, which can be installed in a test backend + */ + factory(options?: { + skipTaskRunOnStartup?: boolean; + includeManualTasksOnStartup?: boolean; + includeInitialDelayedTasksOnStartup?: boolean; + }) { + return createServiceFactory({ + service: coreServices.scheduler, + deps: { lifecycle: coreServices.lifecycle }, + factory: async ({ lifecycle }) => { + if (!options?.skipTaskRunOnStartup) { + lifecycle.addStartupHook(async () => { + await this.triggerAllTasks({ + includeManualTasks: options?.includeManualTasksOnStartup, + includeInitialDelayedTasks: + options?.includeInitialDelayedTasksOnStartup, + }); + }); + } + return this; + }, + }); + } + + createScheduledTaskRunner( + schedule: SchedulerServiceTaskScheduleDefinition, + ): SchedulerServiceTaskRunner { + return { + run: async task => { + await this.scheduleTask({ ...task, ...schedule }); + }, + }; + } + + async getScheduledTasks(): Promise { + return Array.from(this.#tasks.values()).map(({ descriptor }) => descriptor); + } + + async scheduleTask( + task: SchedulerServiceTaskScheduleDefinition & + SchedulerServiceTaskInvocationDefinition, + ): Promise { + this.#tasks.set(task.id, { + ...task, + descriptor: { + id: task.id, + scope: task.scope ?? 'global', + settings: { version: 1 }, + }, + }); + } + + async triggerTask(id: string): Promise { + const task = this.#tasks.get(id); + if (!task) { + throw new Error(`Task ${id} not found`); + } + if (this.#runningTasks.has(id)) { + return; + } + this.#runningTasks.add(id); + try { + await task.fn(new AbortController().signal); + this.#deferredTaskCompletions.get(id)?.resolve(); + } catch (error) { + this.#deferredTaskCompletions.get(id)?.reject(error); + } finally { + this.#runningTasks.delete(id); + } + } + + /** + * Trigger all tasks that match the given options, and wait for them to complete. + * + * @param options - The options to filter the tasks to trigger + */ + async triggerAllTasks(options?: { + scope?: 'all' | 'global' | 'local'; + includeInitialDelayedTasks?: boolean; + includeManualTasks?: boolean; + }): Promise { + const { + scope = 'all', + includeManualTasks = false, + includeInitialDelayedTasks = false, + } = options ?? {}; + + const selectedTaskIds = new Array(); + for (const task of this.#tasks.values()) { + if (task.initialDelay && !includeInitialDelayedTasks) { + continue; + } + if ('trigger' in task.frequency && task.frequency.trigger === 'manual') { + if (includeManualTasks) { + selectedTaskIds.push(task.id); + } + continue; + } + if (scope === 'all' || scope === task.scope) { + selectedTaskIds.push(task.id); + } + } + + await Promise.all(selectedTaskIds.map(id => this.triggerTask(id))); + } + + /** + * Wait for the task with the given ID to complete. + * + * If the task has not yet been scheduler or started, this will wait for it to be scheduled, started, and completed + * + * @param id - The task ID to wait for + * @returns A promise that resolves when the task is completed + */ + async waitForTask(id: string): Promise { + const existing = this.#deferredTaskCompletions.get(id); + if (existing) { + return existing; + } + const defferred = createDeferred(); + this.#deferredTaskCompletions.set(id, defferred); + return defferred; + } +} diff --git a/packages/backend-test-utils/src/services/mockServices.ts b/packages/backend-test-utils/src/services/mockServices.ts index 06bf8f9a29..587db30934 100644 --- a/packages/backend-test-utils/src/services/mockServices.ts +++ b/packages/backend-test-utils/src/services/mockServices.ts @@ -26,7 +26,6 @@ import { permissionsRegistryServiceFactory } from '@backstage/backend-defaults/p import { rootHealthServiceFactory } from '@backstage/backend-defaults/rootHealth'; import { rootHttpRouterServiceFactory } from '@backstage/backend-defaults/rootHttpRouter'; import { rootLifecycleServiceFactory } from '@backstage/backend-defaults/rootLifecycle'; -import { schedulerServiceFactory } from '@backstage/backend-defaults/scheduler'; import { urlReaderServiceFactory } from '@backstage/backend-defaults/urlReader'; import { AuthService, @@ -57,6 +56,7 @@ import { mockCredentials } from './mockCredentials'; import { MockEventsService } from './MockEventsService'; import { MockPermissionsService } from './MockPermissionsService'; import { simpleMock } from './simpleMock'; +import { MockSchedulerService } from './MockSchedulerService'; /** @internal */ function createLoggerMock() { @@ -496,8 +496,15 @@ export namespace mockServices { })); } + export function scheduler(): MockSchedulerService { + return new MockSchedulerService(); + } export namespace scheduler { - export const factory = () => schedulerServiceFactory; + export const factory = (options?: { + skipTaskRunOnStartup?: boolean; + includeManualTasksOnStartup?: boolean; + includeInitialDelayedTasksOnStartup?: boolean; + }) => new MockSchedulerService().factory(options); export const mock = simpleMock(coreServices.scheduler, () => ({ createScheduledTaskRunner: jest.fn(), getScheduledTasks: jest.fn(), From 6883a90b29e23caba470154bc84fe717186f1078 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 18 Aug 2025 11:32:38 +0200 Subject: [PATCH 2/6] backend-test-utils: abort mock tasks on shutdown Signed-off-by: Patrik Oldsberg --- .../src/services/MockSchedulerService.test.ts | 35 +++++++++++++++++++ .../src/services/MockSchedulerService.ts | 17 ++++++++- 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/packages/backend-test-utils/src/services/MockSchedulerService.test.ts b/packages/backend-test-utils/src/services/MockSchedulerService.test.ts index 8e26fd770e..87390520b0 100644 --- a/packages/backend-test-utils/src/services/MockSchedulerService.test.ts +++ b/packages/backend-test-utils/src/services/MockSchedulerService.test.ts @@ -205,4 +205,39 @@ describe('MockSchedulerService', () => { expect(taskFn).toHaveBeenCalled(); await expect(isDone()).resolves.toBe(true); }); + + it('should abort tasks when shutting down', async () => { + let taskSignal: AbortSignal | undefined; + + const backend = await startTestBackend({ + features: [ + mockServices.scheduler.factory({ + includeManualTasksOnStartup: true, + includeInitialDelayedTasksOnStartup: true, + }), + createBackendPlugin({ + pluginId: 'tester', + register(reg) { + reg.registerInit({ + deps: { scheduler: coreServices.scheduler }, + async init({ scheduler }) { + scheduler.scheduleTask({ + ...baseOpts, + id: 'test-plain', + fn: async signal => { + taskSignal = signal; + }, + }); + }, + }); + }, + }), + ], + }); + + expect(taskSignal).toBeDefined(); + expect(taskSignal?.aborted).toBe(false); + await backend.stop(); + expect(taskSignal?.aborted).toBe(true); + }); }); diff --git a/packages/backend-test-utils/src/services/MockSchedulerService.ts b/packages/backend-test-utils/src/services/MockSchedulerService.ts index 7648770eb5..ec02748e79 100644 --- a/packages/backend-test-utils/src/services/MockSchedulerService.ts +++ b/packages/backend-test-utils/src/services/MockSchedulerService.ts @@ -31,6 +31,7 @@ export class MockSchedulerService implements SchedulerService { SchedulerServiceTaskInvocationDefinition & SchedulerServiceTaskScheduleDefinition & { descriptor: SchedulerServiceTaskDescriptor; + abortControllers: AbortController[]; } >(); readonly #runningTasks = new Set(); @@ -57,6 +58,9 @@ export class MockSchedulerService implements SchedulerService { }); }); } + lifecycle.addShutdownHook(async () => { + await this.#shutdownAllTasks(); + }); return this; }, }); @@ -87,6 +91,7 @@ export class MockSchedulerService implements SchedulerService { scope: task.scope ?? 'global', settings: { version: 1 }, }, + abortControllers: [], }); } @@ -100,7 +105,9 @@ export class MockSchedulerService implements SchedulerService { } this.#runningTasks.add(id); try { - await task.fn(new AbortController().signal); + const abortController = new AbortController(); + task.abortControllers.push(abortController); + await task.fn(abortController.signal); this.#deferredTaskCompletions.get(id)?.resolve(); } catch (error) { this.#deferredTaskCompletions.get(id)?.reject(error); @@ -144,6 +151,14 @@ export class MockSchedulerService implements SchedulerService { await Promise.all(selectedTaskIds.map(id => this.triggerTask(id))); } + async #shutdownAllTasks() { + for (const task of this.#tasks.values()) { + for (const abortController of task.abortControllers) { + abortController.abort(); + } + } + } + /** * Wait for the task with the given ID to complete. * From 201581cc7ef6ffc69d12ab0115eca7ef40b92fdf Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 18 Aug 2025 12:36:38 +0200 Subject: [PATCH 3/6] backend-test-utils: move MockSchedulerService back to internal Signed-off-by: Patrik Oldsberg --- .../backend-test-utils/src/services/MockSchedulerService.ts | 4 ++-- packages/backend-test-utils/src/services/mockServices.ts | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/backend-test-utils/src/services/MockSchedulerService.ts b/packages/backend-test-utils/src/services/MockSchedulerService.ts index ec02748e79..b3269b84e3 100644 --- a/packages/backend-test-utils/src/services/MockSchedulerService.ts +++ b/packages/backend-test-utils/src/services/MockSchedulerService.ts @@ -51,7 +51,7 @@ export class MockSchedulerService implements SchedulerService { factory: async ({ lifecycle }) => { if (!options?.skipTaskRunOnStartup) { lifecycle.addStartupHook(async () => { - await this.triggerAllTasks({ + await this.#triggerAllTasks({ includeManualTasks: options?.includeManualTasksOnStartup, includeInitialDelayedTasks: options?.includeInitialDelayedTasksOnStartup, @@ -121,7 +121,7 @@ export class MockSchedulerService implements SchedulerService { * * @param options - The options to filter the tasks to trigger */ - async triggerAllTasks(options?: { + async #triggerAllTasks(options?: { scope?: 'all' | 'global' | 'local'; includeInitialDelayedTasks?: boolean; includeManualTasks?: boolean; diff --git a/packages/backend-test-utils/src/services/mockServices.ts b/packages/backend-test-utils/src/services/mockServices.ts index 587db30934..1d3a1578cc 100644 --- a/packages/backend-test-utils/src/services/mockServices.ts +++ b/packages/backend-test-utils/src/services/mockServices.ts @@ -37,6 +37,7 @@ import { LoggerService, PermissionsService, RootConfigService, + SchedulerService, ServiceFactory, ServiceRef, UserInfoService, @@ -496,7 +497,7 @@ export namespace mockServices { })); } - export function scheduler(): MockSchedulerService { + export function scheduler(): SchedulerService { return new MockSchedulerService(); } export namespace scheduler { From 66389bc5c67de290f19457ab171df8d069235756 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 18 Aug 2025 15:16:28 +0200 Subject: [PATCH 4/6] backend-test-utils: simplify MockSchedulerService aborts Signed-off-by: Patrik Oldsberg --- .../src/services/MockSchedulerService.ts | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/packages/backend-test-utils/src/services/MockSchedulerService.ts b/packages/backend-test-utils/src/services/MockSchedulerService.ts index b3269b84e3..750bee4c09 100644 --- a/packages/backend-test-utils/src/services/MockSchedulerService.ts +++ b/packages/backend-test-utils/src/services/MockSchedulerService.ts @@ -31,7 +31,7 @@ export class MockSchedulerService implements SchedulerService { SchedulerServiceTaskInvocationDefinition & SchedulerServiceTaskScheduleDefinition & { descriptor: SchedulerServiceTaskDescriptor; - abortControllers: AbortController[]; + abortControllers: AbortController; } >(); readonly #runningTasks = new Set(); @@ -91,7 +91,7 @@ export class MockSchedulerService implements SchedulerService { scope: task.scope ?? 'global', settings: { version: 1 }, }, - abortControllers: [], + abortControllers: new AbortController(), }); } @@ -105,9 +105,7 @@ export class MockSchedulerService implements SchedulerService { } this.#runningTasks.add(id); try { - const abortController = new AbortController(); - task.abortControllers.push(abortController); - await task.fn(abortController.signal); + await task.fn(task.abortControllers.signal); this.#deferredTaskCompletions.get(id)?.resolve(); } catch (error) { this.#deferredTaskCompletions.get(id)?.reject(error); @@ -153,9 +151,7 @@ export class MockSchedulerService implements SchedulerService { async #shutdownAllTasks() { for (const task of this.#tasks.values()) { - for (const abortController of task.abortControllers) { - abortController.abort(); - } + task.abortControllers.abort(); } } From e545c21ef6baecf3e13c94ac5e20f47a7e27598e Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 18 Aug 2025 16:21:41 +0200 Subject: [PATCH 5/6] backend-test-utils: review fixes Signed-off-by: Patrik Oldsberg --- .../src/services/MockSchedulerService.ts | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/packages/backend-test-utils/src/services/MockSchedulerService.ts b/packages/backend-test-utils/src/services/MockSchedulerService.ts index 750bee4c09..aafd6d98ec 100644 --- a/packages/backend-test-utils/src/services/MockSchedulerService.ts +++ b/packages/backend-test-utils/src/services/MockSchedulerService.ts @@ -110,6 +110,7 @@ export class MockSchedulerService implements SchedulerService { } catch (error) { this.#deferredTaskCompletions.get(id)?.reject(error); } finally { + this.#deferredTaskCompletions.delete(id); this.#runningTasks.delete(id); } } @@ -135,10 +136,11 @@ export class MockSchedulerService implements SchedulerService { if (task.initialDelay && !includeInitialDelayedTasks) { continue; } - if ('trigger' in task.frequency && task.frequency.trigger === 'manual') { - if (includeManualTasks) { - selectedTaskIds.push(task.id); - } + if ( + 'trigger' in task.frequency && + task.frequency.trigger === 'manual' && + !includeManualTasks + ) { continue; } if (scope === 'all' || scope === task.scope) { @@ -158,7 +160,7 @@ export class MockSchedulerService implements SchedulerService { /** * Wait for the task with the given ID to complete. * - * If the task has not yet been scheduler or started, this will wait for it to be scheduled, started, and completed + * If the task has not yet been scheduled or started, this will wait for it to be scheduled, started, and completed * * @param id - The task ID to wait for * @returns A promise that resolves when the task is completed @@ -168,8 +170,8 @@ export class MockSchedulerService implements SchedulerService { if (existing) { return existing; } - const defferred = createDeferred(); - this.#deferredTaskCompletions.set(id, defferred); - return defferred; + const deferred = createDeferred(); + this.#deferredTaskCompletions.set(id, deferred); + return deferred; } } From 3d6c89c36081630c0051842880a8749258469a1f Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 19 Aug 2025 10:09:27 +0200 Subject: [PATCH 6/6] docs/backend-system: remove usage of waitForTask in scheduler docs Signed-off-by: Patrik Oldsberg --- docs/backend-system/core-services/scheduler.md | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/docs/backend-system/core-services/scheduler.md b/docs/backend-system/core-services/scheduler.md index b8f8c6aac1..f61f77cc7e 100644 --- a/docs/backend-system/core-services/scheduler.md +++ b/docs/backend-system/core-services/scheduler.md @@ -134,7 +134,7 @@ Responds with The `@backstage/backend-test-utils` package provides `mockServices.scheduler`, which provides a mocked implementation of the scheduler service that can be used in tests. This mocked implementation is used by default in `startTestBackend`, and it will immediately run any registered tasks on startup as long as they're not configured to run manually or with an initial delay. -A dedicated instance can be used for more control during testing, with the mock implementation providing additional utilities to trigger and wait for tasks to complete: +A dedicated instance can be used for more control during testing: ```ts it('should trigger a task', async () => { @@ -144,16 +144,9 @@ it('should trigger a task', async () => { features: [scheduler.factory()], }); - // Start waiting for some task to complete - const waitForTask = scheduler.waitForTask('some-task-id'); + await scheduler.triggerTask('some-task-id'); - // Call an endpoit that triggers a task - const res = await request(server).post( - '/api/my-plugin/route-that-triggers-a-task', - ); - expect(res.status).toBe(200); - - // Wait for the task to complete - await waitForTask; + // Next verify that the plugin state is updated accordingly + // e.g. by calling the API or verifying database state }); ```