-
Notifications
You must be signed in to change notification settings - Fork 10
feat: start method on maestro process #365
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,56 @@ | ||||||||||||||
| import type { ApiResponse } from '../../base'; | ||||||||||||||
| import type { RequestSpec } from '../../../models/common/request-spec'; | ||||||||||||||
| import { | ||||||||||||||
| ProcessStartRequest, | ||||||||||||||
| ProcessStartResponse, | ||||||||||||||
| } from '../../../models/orchestrator/processes.types'; | ||||||||||||||
| import { ProcessMap } from '../../../models/orchestrator/processes.constants'; | ||||||||||||||
| import { CollectionResponse, RequestOptions } from '../../../models/common/types'; | ||||||||||||||
| import { | ||||||||||||||
| addPrefixToKeys, | ||||||||||||||
| pascalToCamelCaseKeys, | ||||||||||||||
| transformData, | ||||||||||||||
| transformRequest, | ||||||||||||||
| } from '../../../utils/transform'; | ||||||||||||||
| import { ODATA_PREFIX } from '../../../utils/constants/common'; | ||||||||||||||
| import { PROCESS_ENDPOINTS } from '../../../utils/constants/endpoints'; | ||||||||||||||
|
|
||||||||||||||
| type Poster = <T>(path: string, body?: unknown, options?: RequestSpec) => Promise<ApiResponse<T>>; | ||||||||||||||
|
|
||||||||||||||
| /** | ||||||||||||||
| * Shared implementation of the Orchestrator StartJobs request. | ||||||||||||||
| * | ||||||||||||||
| * Both `ProcessService.start` (orchestrator, folderId header) and | ||||||||||||||
| * `MaestroProcessesService.start` (maestro, folderKey header) delegate here. | ||||||||||||||
| * Callers are responsible for constructing the appropriate folder header. | ||||||||||||||
| */ | ||||||||||||||
| export async function startProcessRequest( | ||||||||||||||
| post: Poster, | ||||||||||||||
| request: ProcessStartRequest, | ||||||||||||||
| headers: Record<string, string>, | ||||||||||||||
| options: RequestOptions = {} | ||||||||||||||
| ): Promise<ProcessStartResponse[]> { | ||||||||||||||
| // Transform SDK field names to API field names (e.g., processKey → releaseKey) | ||||||||||||||
| const apiRequest = transformRequest(request, ProcessMap); | ||||||||||||||
|
|
||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These three inline comments explain self-evident code — |
||||||||||||||
| // Create the request object according to API spec | ||||||||||||||
| const requestBody = { | ||||||||||||||
| startInfo: apiRequest | ||||||||||||||
| }; | ||||||||||||||
|
|
||||||||||||||
| // Prefix all query parameter keys with '$' for OData | ||||||||||||||
| const apiOptions = addPrefixToKeys(options, ODATA_PREFIX, Object.keys(options)); | ||||||||||||||
|
|
||||||||||||||
| const response = await post<CollectionResponse<ProcessStartResponse>>( | ||||||||||||||
| PROCESS_ENDPOINTS.START_PROCESS, | ||||||||||||||
| requestBody, | ||||||||||||||
| { | ||||||||||||||
| params: apiOptions, | ||||||||||||||
| headers | ||||||||||||||
| } | ||||||||||||||
| ); | ||||||||||||||
|
|
||||||||||||||
| return response.data?.value.map(process => | ||||||||||||||
| transformData(pascalToCamelCaseKeys(process) as ProcessStartResponse, ProcessMap) | ||||||||||||||
|
Comment on lines
+52
to
+54
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The optional chain on
Suggested change
|
||||||||||||||
| ); | ||||||||||||||
| } | ||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -134,6 +134,38 @@ describe.each(modes)('Maestro Processes - Integration Tests [%s]', (mode) => { | |
| }); | ||
| }); | ||
|
|
||
| describe('start', () => { | ||
| it('should start a process and create a job using processKey', async () => { | ||
| const { maestroProcesses } = getServices(); | ||
| const config = getTestConfig(); | ||
|
|
||
| const processKey = config.orchestratorTestProcessKey; | ||
|
|
||
| expect( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The guard pattern is inconsistent with the rest of this file. The existing if (!processKey) {
console.warn('MAESTRO_TEST_PROCESS_KEY not configured, skipping test');
return;
}Per conventions: "Use |
||
| processKey, | ||
| 'ORCHESTRATOR_TEST_PROCESS_KEY must be configured to test process execution' | ||
| ).toBeDefined(); | ||
| expect(config.folderId, 'INTEGRATION_TEST_FOLDER_ID must be configured').toBeDefined(); | ||
|
|
||
| const result = await maestroProcesses.start( | ||
| { | ||
| processKey: processKey!, | ||
| inputArguments: JSON.stringify({ | ||
| testRun: true, | ||
| timestamp: new Date().toISOString(), | ||
| }), | ||
| }, | ||
|
Comment on lines
+147
to
+157
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This integration test needs a Maestro-specific string folder key. Also, the error message on line 147 says |
||
| config.folderId! | ||
| ); | ||
|
|
||
| expect(result).toBeDefined(); | ||
| expect(Array.isArray(result)).toBe(true); | ||
| if (result.length > 0) { | ||
| expect(result[0].id).toBeDefined(); | ||
| } | ||
| }); | ||
| }); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing a transform validation integration test. Per conventions: "Include a transform validation test for new methods with a transform pipeline. Verify: (a) transformed camelCase fields exist and have values, AND (b) original PascalCase API fields are absent." Add a second test in this it('should apply transform pipeline (camelCase fields present, PascalCase absent)', async () => {
if (!processKey) { console.warn('...'); return; }
const result = await maestroProcesses.start({ processKey: processKey! }, config.folderId!);
if (result.length === 0) { console.warn('No jobs returned, skipping transform check'); return; }
// camelCase present
expect(result[0].key).toBeDefined();
expect(result[0].state).toBeDefined();
// PascalCase absent
expect((result[0] as any).Key).toBeUndefined();
expect((result[0] as any).State).toBeUndefined();
expect((result[0] as any).ReleaseName).toBeUndefined();
}); |
||
|
|
||
| describe('Process metadata validation', () => { | ||
| it('should have expected fields in process objects', async () => { | ||
| const { maestroProcesses } = getServices(); | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,16 +1,22 @@ | ||||||||||||||||||||||||
| // ===== IMPORTS ===== | ||||||||||||||||||||||||
| import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; | ||||||||||||||||||||||||
| import { MaestroProcessesService } from '../../../../src/services/maestro/processes'; | ||||||||||||||||||||||||
| import { MAESTRO_ENDPOINTS } from '../../../../src/utils/constants/endpoints'; | ||||||||||||||||||||||||
| import { MAESTRO_ENDPOINTS, PROCESS_ENDPOINTS } from '../../../../src/utils/constants/endpoints'; | ||||||||||||||||||||||||
| import { ApiClient } from '../../../../src/core/http/api-client'; | ||||||||||||||||||||||||
| import { | ||||||||||||||||||||||||
| import { | ||||||||||||||||||||||||
| MAESTRO_TEST_CONSTANTS, | ||||||||||||||||||||||||
| createMockProcess, | ||||||||||||||||||||||||
| createMockProcess, | ||||||||||||||||||||||||
| createMockProcessesApiResponse, | ||||||||||||||||||||||||
| createMockError, | ||||||||||||||||||||||||
| TEST_CONSTANTS | ||||||||||||||||||||||||
| createMockError, | ||||||||||||||||||||||||
| TEST_CONSTANTS, | ||||||||||||||||||||||||
| createMockProcessStartResponse, | ||||||||||||||||||||||||
| createMockProcessStartApiResponse, | ||||||||||||||||||||||||
| } from '../../../utils/mocks'; | ||||||||||||||||||||||||
| import { PROCESS_TEST_CONSTANTS } from '../../../utils/constants'; | ||||||||||||||||||||||||
| import { createServiceTestDependencies, createMockApiClient } from '../../../utils/setup'; | ||||||||||||||||||||||||
| import { JobPriority, ProcessStartRequest } from '../../../../src/models/orchestrator/processes.types'; | ||||||||||||||||||||||||
| import { FOLDER_KEY } from '../../../../src/utils/constants/headers'; | ||||||||||||||||||||||||
| import { RequestOptions } from '../../../../src/models/common'; | ||||||||||||||||||||||||
|
Check warning on line 19 in tests/unit/services/maestro/processes.test.ts
|
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| // ===== MOCKING ===== | ||||||||||||||||||||||||
| // Mock the dependencies | ||||||||||||||||||||||||
|
|
@@ -153,4 +159,91 @@ | |||||||||||||||||||||||
| expect(result[0].name).toBe(MAESTRO_TEST_CONSTANTS.CUSTOM_PACKAGE_ID); | ||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| describe('start', () => { | ||||||||||||||||||||||||
| it('should start process by processKey successfully with transformations applied', async () => { | ||||||||||||||||||||||||
| const mockJob = createMockProcessStartResponse(); | ||||||||||||||||||||||||
| const mockResponse = createMockProcessStartApiResponse([mockJob]); | ||||||||||||||||||||||||
| mockApiClient.post.mockResolvedValue(mockResponse); | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| const request = PROCESS_TEST_CONSTANTS.PROCESS_START_REQUEST as ProcessStartRequest; | ||||||||||||||||||||||||
| const result = await service.start(request, MAESTRO_TEST_CONSTANTS.FOLDER_KEY); | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| expect(result).toBeDefined(); | ||||||||||||||||||||||||
| expect(result).toHaveLength(1); | ||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing transform-completeness assertions. Per conventions: "Validate transform completeness — verify both: (a) transformed fields have correct values AND (b) original PascalCase fields are absent." After the existing // Verify PascalCase originals are absent (transform regression guard)
expect((result[0] as any).Key).toBeUndefined();
expect((result[0] as any).ReleaseName).toBeUndefined();
expect((result[0] as any).State).toBeUndefined();See the established pattern at |
||||||||||||||||||||||||
| expect(result[0].key).toBe(PROCESS_TEST_CONSTANTS.JOB_KEY); | ||||||||||||||||||||||||
| expect(result[0].processName).toBe(PROCESS_TEST_CONSTANTS.PROCESS_NAME); | ||||||||||||||||||||||||
| expect(result[0].state).toBe('Running'); | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| expect(mockApiClient.post).toHaveBeenCalledWith( | ||||||||||||||||||||||||
| PROCESS_ENDPOINTS.START_PROCESS, | ||||||||||||||||||||||||
|
Comment on lines
+175
to
+179
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Per conventions, transform completeness tests must verify both that the SDK camelCase fields are present and that the original PascalCase fields from the API are absent. Without the absence check, a regression where
Suggested change
See the established pattern in |
||||||||||||||||||||||||
| expect.objectContaining({ | ||||||||||||||||||||||||
| startInfo: expect.objectContaining({ | ||||||||||||||||||||||||
| releaseKey: PROCESS_TEST_CONSTANTS.PROCESS_KEY, | ||||||||||||||||||||||||
| jobPriority: JobPriority.Normal, | ||||||||||||||||||||||||
| inputArguments: PROCESS_TEST_CONSTANTS.PROCESS_START_REQUEST.inputArguments | ||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||
| }), | ||||||||||||||||||||||||
| expect.objectContaining({ | ||||||||||||||||||||||||
| headers: expect.objectContaining({ | ||||||||||||||||||||||||
| [FOLDER_KEY]: MAESTRO_TEST_CONSTANTS.FOLDER_KEY | ||||||||||||||||||||||||
| }), | ||||||||||||||||||||||||
| params: expect.any(Object) | ||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| it('should start process by processName successfully with transformations applied', async () => { | ||||||||||||||||||||||||
| const mockJob = createMockProcessStartResponse(); | ||||||||||||||||||||||||
| const mockResponse = createMockProcessStartApiResponse([mockJob]); | ||||||||||||||||||||||||
| mockApiClient.post.mockResolvedValue(mockResponse); | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| const request = PROCESS_TEST_CONSTANTS.PROCESS_START_REQUEST_WITH_NAME as ProcessStartRequest; | ||||||||||||||||||||||||
| const result = await service.start(request, MAESTRO_TEST_CONSTANTS.FOLDER_KEY); | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| expect(result).toBeDefined(); | ||||||||||||||||||||||||
| expect(result).toHaveLength(1); | ||||||||||||||||||||||||
| expect(result[0].key).toBe(PROCESS_TEST_CONSTANTS.JOB_KEY); | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| expect(mockApiClient.post).toHaveBeenCalledWith( | ||||||||||||||||||||||||
| PROCESS_ENDPOINTS.START_PROCESS, | ||||||||||||||||||||||||
| expect.objectContaining({ | ||||||||||||||||||||||||
| startInfo: expect.objectContaining({ | ||||||||||||||||||||||||
| releaseName: PROCESS_TEST_CONSTANTS.PROCESS_NAME, | ||||||||||||||||||||||||
| jobPriority: JobPriority.High, | ||||||||||||||||||||||||
| inputArguments: PROCESS_TEST_CONSTANTS.PROCESS_START_REQUEST_WITH_NAME.inputArguments | ||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||
| }), | ||||||||||||||||||||||||
| expect.any(Object) | ||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| it('should handle multiple jobs returned from start', async () => { | ||||||||||||||||||||||||
| const mockJobs = [ | ||||||||||||||||||||||||
| createMockProcessStartResponse(), | ||||||||||||||||||||||||
| createMockProcessStartResponse({ | ||||||||||||||||||||||||
| key: PROCESS_TEST_CONSTANTS.JOB_KEY, | ||||||||||||||||||||||||
| id: 2 | ||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||
| ]; | ||||||||||||||||||||||||
| const mockResponse = createMockProcessStartApiResponse(mockJobs); | ||||||||||||||||||||||||
| mockApiClient.post.mockResolvedValue(mockResponse); | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| const request = PROCESS_TEST_CONSTANTS.PROCESS_START_REQUEST as ProcessStartRequest; | ||||||||||||||||||||||||
| const result = await service.start(request, MAESTRO_TEST_CONSTANTS.FOLDER_KEY); | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| expect(result).toBeDefined(); | ||||||||||||||||||||||||
| expect(result).toHaveLength(2); | ||||||||||||||||||||||||
| expect(result[1].key).toBe(PROCESS_TEST_CONSTANTS.JOB_KEY); | ||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| it('should handle API errors', async () => { | ||||||||||||||||||||||||
| const error = createMockError(TEST_CONSTANTS.ERROR_MESSAGE); | ||||||||||||||||||||||||
| mockApiClient.post.mockRejectedValue(error); | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| const request = PROCESS_TEST_CONSTANTS.PROCESS_START_REQUEST as ProcessStartRequest; | ||||||||||||||||||||||||
| await expect(service.start(request, MAESTRO_TEST_CONSTANTS.FOLDER_KEY)) | ||||||||||||||||||||||||
| .rejects.toThrow(TEST_CONSTANTS.ERROR_MESSAGE); | ||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
{@link ProcessStartResponse}is on a separate line instead of inline with@returns. Per conventions: "Link response types with{@link TypeName}in every method's JSDoc@returns."