From 67df05eb41d2ce4037d2d17d50b27eea9253884d Mon Sep 17 00:00:00 2001 From: mjnovice Date: Sun, 22 Mar 2026 14:56:07 +0000 Subject: [PATCH 1/3] Both background builds also completed successfully (exit code 0). All va Resolves #265 Co-Authored-By: Claude --- .../src/components/ProcessInstances.tsx | 26 +++------- .../src/components/ProcessInstances.tsx | 26 +++------- .../maestro/process-instances.constants.ts | 3 +- .../maestro/process-instances.models.ts | 19 ++++--- src/models/maestro/process-instances.types.ts | 51 +++++++++++++++---- .../maestro/processes/process-instances.ts | 31 ++++++++--- src/utils/constants/endpoints/maestro.ts | 2 +- .../process-instances.integration.test.ts | 9 ++-- .../models/maestro/process-instances.test.ts | 2 +- .../maestro/process-instances.test.ts | 20 ++++---- tests/utils/mocks/maestro.ts | 41 +++++++++++---- 11 files changed, 140 insertions(+), 90 deletions(-) diff --git a/samples/process-app-v0/src/components/ProcessInstances.tsx b/samples/process-app-v0/src/components/ProcessInstances.tsx index e2b1ce789..602bfa3ec 100644 --- a/samples/process-app-v0/src/components/ProcessInstances.tsx +++ b/samples/process-app-v0/src/components/ProcessInstances.tsx @@ -217,7 +217,7 @@ export const ProcessInstances = () => { // Fetch BPMN and execution history (with individual error handling) let bpmnXml: BpmnXmlString | null = null; - let executionHistory: ProcessInstanceExecutionHistoryResponse[] = []; + let executionHistory: ProcessInstanceExecutionHistoryResponse | null = null; try { bpmnXml = await sdk.maestro.processes.instances.getBpmn(instance.instanceId, instance.folderKey); @@ -242,24 +242,14 @@ export const ProcessInstances = () => { if (match && match[1]) { activityType = match[1].replace(/([A-Z])/g, ' $1').trim(); // Add spaces between camelCase - // If it's a user task, look for the action center task link - if (activityType.toLowerCase() === 'user task' && executionHistory.length > 0) { - // Find the history entry with matching elementId - console.log('Looking for elementId:', elementId); - console.log('Execution History:', executionHistory); - const taskEntry = executionHistory.find((entry: ProcessInstanceExecutionHistoryResponse) => { - // Parse attributes JSON if it's a string - const attributes = typeof entry.attributes === 'string' - ? JSON.parse(entry.attributes) - : entry.attributes; - return attributes?.elementId === elementId; - }); + // If it's a user task, look for the action center task link in element executions + if (activityType.toLowerCase() === 'user task' && executionHistory?.elementExecutions?.length) { + const taskEntry = executionHistory.elementExecutions.find( + (execution) => execution.elementId === elementId + ); - if (taskEntry) { - const attributes = typeof taskEntry.attributes === 'string' - ? JSON.parse(taskEntry.attributes) - : taskEntry.attributes; - taskLink = attributes?.actionCenterTaskLink; + if (taskEntry?.externalLink) { + taskLink = taskEntry.externalLink; console.log('Found task link:', taskLink); } } diff --git a/samples/process-app-v1/src/components/ProcessInstances.tsx b/samples/process-app-v1/src/components/ProcessInstances.tsx index 6ea34a3a8..c2d7bd465 100644 --- a/samples/process-app-v1/src/components/ProcessInstances.tsx +++ b/samples/process-app-v1/src/components/ProcessInstances.tsx @@ -237,7 +237,7 @@ export const ProcessInstances = () => { // Fetch BPMN and execution history (with individual error handling) let bpmnXml: BpmnXmlString | null = null; - let executionHistory: ProcessInstanceExecutionHistoryResponse[] = []; + let executionHistory: ProcessInstanceExecutionHistoryResponse | null = null; try { // Use modular ProcessInstances service directly @@ -264,24 +264,14 @@ export const ProcessInstances = () => { if (match && match[1]) { activityType = match[1].replace(/([A-Z])/g, ' $1').trim(); // Add spaces between camelCase - // If it's a user task, look for the action center task link - if (activityType.toLowerCase() === 'user task' && executionHistory.length > 0) { - // Find the history entry with matching elementId - console.log('Looking for elementId:', elementId); - console.log('Execution History:', executionHistory); - const taskEntry = executionHistory.find((entry: ProcessInstanceExecutionHistoryResponse) => { - // Parse attributes JSON if it's a string - const attributes = typeof entry.attributes === 'string' - ? JSON.parse(entry.attributes) - : entry.attributes; - return attributes?.elementId === elementId; - }); + // If it's a user task, look for the action center task link in element executions + if (activityType.toLowerCase() === 'user task' && executionHistory?.elementExecutions?.length) { + const taskEntry = executionHistory.elementExecutions.find( + (execution) => execution.elementId === elementId + ); - if (taskEntry) { - const attributes = typeof taskEntry.attributes === 'string' - ? JSON.parse(taskEntry.attributes) - : taskEntry.attributes; - taskLink = attributes?.actionCenterTaskLink; + if (taskEntry?.externalLink) { + taskLink = taskEntry.externalLink; console.log('Found task link:', taskLink); } } diff --git a/src/models/maestro/process-instances.constants.ts b/src/models/maestro/process-instances.constants.ts index 00a2c6b6e..258dac4a1 100644 --- a/src/models/maestro/process-instances.constants.ts +++ b/src/models/maestro/process-instances.constants.ts @@ -13,5 +13,6 @@ export const ProcessInstanceMap: { [key: string]: string } = { * Maps fields for Process Instance Execution History to ensure consistent naming */ export const ProcessInstanceExecutionHistoryMap: { [key: string]: string } = { - startTime: 'startedTime' + startedTimeUtc: 'startedTime', + completedTimeUtc: 'completedTime', }; diff --git a/src/models/maestro/process-instances.models.ts b/src/models/maestro/process-instances.models.ts index 0a5e5c254..6c7095b6a 100644 --- a/src/models/maestro/process-instances.models.ts +++ b/src/models/maestro/process-instances.models.ts @@ -94,7 +94,7 @@ export interface ProcessInstancesServiceModel { getById(id: string, folderKey: string): Promise; /** - * Get execution history (spans) for a process instance + * Get execution history for a process instance * @param instanceId The ID of the instance to get history for * @returns Promise resolving to execution history * {@link ProcessInstanceExecutionHistoryResponse} @@ -105,15 +105,14 @@ export interface ProcessInstancesServiceModel { * * ); * - * // Analyze execution timeline - * history.forEach(span => { - * console.log(`Activity: ${span.name}`); - * console.log(`Start: ${span.startTime}`); - * console.log(`Duration: ${span.duration}ms`); + * // Analyze element executions + * history.elementExecutions.forEach(execution => { + * console.log(`Element: ${execution.elementName}`); + * console.log(`Status: ${execution.status}`); * }); * ``` */ - getExecutionHistory(instanceId: string): Promise; + getExecutionHistory(instanceId: string): Promise; /** * Get BPMN XML file for a process instance @@ -291,11 +290,11 @@ export interface ProcessInstanceMethods { getIncidents(): Promise; /** - * Gets execution history (spans) for this process instance + * Gets execution history for this process instance * * @returns Promise resolving to execution history */ - getExecutionHistory(): Promise; + getExecutionHistory(): Promise; /** * Gets BPMN XML file for this process instance @@ -353,7 +352,7 @@ function createProcessInstanceMethods(instanceData: RawProcessInstanceGetRespons return service.getIncidents(instanceData.instanceId, instanceData.folderKey); }, - async getExecutionHistory(): Promise { + async getExecutionHistory(): Promise { if (!instanceData.instanceId) throw new Error('Process instance ID is undefined'); return service.getExecutionHistory(instanceData.instanceId); diff --git a/src/models/maestro/process-instances.types.ts b/src/models/maestro/process-instances.types.ts index a5da8a439..e1c23d715 100644 --- a/src/models/maestro/process-instances.types.ts +++ b/src/models/maestro/process-instances.types.ts @@ -62,17 +62,48 @@ export interface ProcessInstanceOperationResponse { * Response for process instance execution history */ export interface ProcessInstanceExecutionHistoryResponse { - id: string; - traceId: string; - parentId: string | null; - name: string; + instanceId: string; + processKey: string; + packageKey: string; + packageId: string; + packageVersion: string; + folderKey: string; + instanceDisplayName: string; + source: string; + /** Instance status (e.g., "Completed", "Faulted", "Running", "Pausing", "Canceling") */ + status: string; + creationUserKey: string | null; + startedTime: string; + completedTime: string | null; + elementExecutions: ProcessElementExecutionMetadata[]; +} + +/** + * Element execution metadata for process instances + */ +export interface ProcessElementExecutionMetadata { + elementId: string; + elementName: string; + parentElementId: string | null; + /** Element status (e.g., "Completed", "Faulted", "Running") */ + status: string; startedTime: string; - endTime: string | null; - attributes: string | null; - createdTime: string; - updatedTime?: string; - expiredTime: string | null; - // TO Do: Add status and attributes interface + completedTime: string | null; + processKey: string; + /** External reference link, eg link to the HITL task in Action Center */ + externalLink: string; + elementRuns: ProcessElementRunMetadata[]; +} + +/** + * Element run metadata for process instances + */ +export interface ProcessElementRunMetadata { + status: string; + startedTime: string; + completedTime: string | null; + elementRunId: string; + parentElementRunId: string | null; } /** diff --git a/src/services/maestro/processes/process-instances.ts b/src/services/maestro/processes/process-instances.ts index 8015c5c06..51e4b3f1a 100644 --- a/src/services/maestro/processes/process-instances.ts +++ b/src/services/maestro/processes/process-instances.ts @@ -117,16 +117,33 @@ export class ProcessInstancesService extends BaseService implements ProcessInsta } /** - * Get execution history (spans) for a process instance + * Get execution history for a process instance * @param instanceId The ID of the instance to get history for - * @returns Promise + * @returns Promise */ @track('ProcessInstances.GetExecutionHistory') - async getExecutionHistory(instanceId: string): Promise { - const response = await this.get(MAESTRO_ENDPOINTS.INSTANCES.GET_EXECUTION_HISTORY(instanceId)); - return response.data.map(historyItem => - transformData(historyItem, ProcessInstanceExecutionHistoryMap) - ); + async getExecutionHistory(instanceId: string): Promise { + const response = await this.get(MAESTRO_ENDPOINTS.INSTANCES.GET_EXECUTION_HISTORY(instanceId)); + + // Transform the main response + const transformedResponse = transformData(response.data, ProcessInstanceExecutionHistoryMap); + + // Transform each element execution and its nested element runs + if (transformedResponse.elementExecutions && Array.isArray(transformedResponse.elementExecutions)) { + transformedResponse.elementExecutions = transformedResponse.elementExecutions.map((execution: any) => { + const transformedExecution = transformData(execution, ProcessInstanceExecutionHistoryMap); + + if (transformedExecution.elementRuns && Array.isArray(transformedExecution.elementRuns)) { + transformedExecution.elementRuns = transformedExecution.elementRuns.map((run: any) => + transformData(run, ProcessInstanceExecutionHistoryMap) + ); + } + + return transformedExecution; + }); + } + + return transformedResponse as ProcessInstanceExecutionHistoryResponse; } /** diff --git a/src/utils/constants/endpoints/maestro.ts b/src/utils/constants/endpoints/maestro.ts index 7f23d6a84..79646ec0e 100644 --- a/src/utils/constants/endpoints/maestro.ts +++ b/src/utils/constants/endpoints/maestro.ts @@ -15,7 +15,7 @@ export const MAESTRO_ENDPOINTS = { INSTANCES: { GET_ALL: `${PIMS_BASE}/api/v1/instances`, GET_BY_ID: (instanceId: string) => `${PIMS_BASE}/api/v1/instances/${instanceId}`, - GET_EXECUTION_HISTORY: (instanceId: string) => `${PIMS_BASE}/api/v1/spans/${instanceId}`, + GET_EXECUTION_HISTORY: (instanceId: string) => `${PIMS_BASE}/api/v1/element-executions/process-instances/${instanceId}`, GET_BPMN: (instanceId: string) => `${PIMS_BASE}/api/v1/instances/${instanceId}/bpmn`, GET_VARIABLES: (instanceId: string) => `${PIMS_BASE}/api/v1/instances/${instanceId}/variables`, CANCEL: (instanceId: string) => `${PIMS_BASE}/api/v1/instances/${instanceId}/cancel`, diff --git a/tests/integration/shared/maestro/process-instances.integration.test.ts b/tests/integration/shared/maestro/process-instances.integration.test.ts index b037c257c..0987d9250 100644 --- a/tests/integration/shared/maestro/process-instances.integration.test.ts +++ b/tests/integration/shared/maestro/process-instances.integration.test.ts @@ -252,12 +252,9 @@ describe.each(modes)('Maestro Process Instances - Integration Tests [%s]', (mode const result = await processInstances.getExecutionHistory(testInstanceId); expect(result).toBeDefined(); - expect(Array.isArray(result)).toBe(true); - - if (result.length > 0) { - const historyItem = result[0]; - expect(historyItem).toBeDefined(); - } + expect(result).toHaveProperty('instanceId'); + expect(result).toHaveProperty('elementExecutions'); + expect(Array.isArray(result.elementExecutions)).toBe(true); } catch (error: any) { console.log('Get execution history test failed:', error.message); } diff --git a/tests/unit/models/maestro/process-instances.test.ts b/tests/unit/models/maestro/process-instances.test.ts index c15ca9750..9ce3bd141 100644 --- a/tests/unit/models/maestro/process-instances.test.ts +++ b/tests/unit/models/maestro/process-instances.test.ts @@ -234,7 +234,7 @@ describe('Process Instance Models', () => { const mockInstanceData = createMockProcessInstance(); const instance = createProcessInstanceWithMethods(mockInstanceData, mockService); - const mockHistory = [createMockExecutionHistory()]; + const mockHistory = createMockExecutionHistory(); mockService.getExecutionHistory = vi.fn().mockResolvedValue(mockHistory); diff --git a/tests/unit/services/maestro/process-instances.test.ts b/tests/unit/services/maestro/process-instances.test.ts index 0dec9bf85..193a2bda0 100644 --- a/tests/unit/services/maestro/process-instances.test.ts +++ b/tests/unit/services/maestro/process-instances.test.ts @@ -208,32 +208,34 @@ describe('ProcessInstancesService', () => { describe('getExecutionHistory', () => { it('should return execution history for process instance', async () => { - + const instanceId = MAESTRO_TEST_CONSTANTS.INSTANCE_ID; - const mockApiResponse: ProcessInstanceExecutionHistoryResponse[] = [createMockExecutionHistory()]; + const mockApiResponse: ProcessInstanceExecutionHistoryResponse = createMockExecutionHistory(); mockApiClient.get.mockResolvedValue(mockApiResponse); - + const result = await service.getExecutionHistory(instanceId); - + expect(mockApiClient.get).toHaveBeenCalledWith( MAESTRO_ENDPOINTS.INSTANCES.GET_EXECUTION_HISTORY(instanceId), {} ); - expect(result).toHaveLength(1); - expect(result[0]).toHaveProperty('id', MAESTRO_TEST_CONSTANTS.SPAN_ID); - expect(result[0]).toHaveProperty('traceId', MAESTRO_TEST_CONSTANTS.TRACE_ID); + expect(result).toHaveProperty('instanceId', MAESTRO_TEST_CONSTANTS.INSTANCE_ID); + expect(result).toHaveProperty('processKey', MAESTRO_TEST_CONSTANTS.PROCESS_KEY); + expect(result.elementExecutions).toHaveLength(1); + expect(result.elementExecutions[0]).toHaveProperty('elementId', MAESTRO_TEST_CONSTANTS.CASE_ELEMENT_ID); + expect(result.elementExecutions[0]).toHaveProperty('elementName', MAESTRO_TEST_CONSTANTS.ACTIVITY_NAME); }); it('should handle API errors', async () => { - + const error = new Error(TEST_CONSTANTS.ERROR_MESSAGE); mockApiClient.get.mockRejectedValue(error); - + await expect(service.getExecutionHistory(MAESTRO_TEST_CONSTANTS.INSTANCE_ID)).rejects.toThrow(TEST_CONSTANTS.ERROR_MESSAGE); }); }); diff --git a/tests/utils/mocks/maestro.ts b/tests/utils/mocks/maestro.ts index af510611a..fbaae7bad 100644 --- a/tests/utils/mocks/maestro.ts +++ b/tests/utils/mocks/maestro.ts @@ -83,16 +83,39 @@ export const createMockProcessesApiResponse = (processes: any[] = []) => { */ export const createMockExecutionHistory = (overrides: Partial = {}) => { return createMockBaseResponse({ - id: MAESTRO_TEST_CONSTANTS.SPAN_ID, - traceId: MAESTRO_TEST_CONSTANTS.TRACE_ID, - parentId: null, - name: MAESTRO_TEST_CONSTANTS.ACTIVITY_NAME, + instanceId: MAESTRO_TEST_CONSTANTS.INSTANCE_ID, + processKey: MAESTRO_TEST_CONSTANTS.PROCESS_KEY, + packageKey: MAESTRO_TEST_CONSTANTS.PACKAGE_KEY, + packageId: MAESTRO_TEST_CONSTANTS.PACKAGE_ID, + packageVersion: MAESTRO_TEST_CONSTANTS.PACKAGE_VERSION, + folderKey: MAESTRO_TEST_CONSTANTS.FOLDER_KEY, + instanceDisplayName: MAESTRO_TEST_CONSTANTS.INSTANCE_DISPLAY_NAME, + source: MAESTRO_TEST_CONSTANTS.MANUAL_SOURCE, + status: MAESTRO_TEST_CONSTANTS.TASK_STATUS_COMPLETED, + creationUserKey: MAESTRO_TEST_CONSTANTS.CREATOR_USER_KEY, startedTime: new Date().toISOString(), - endTime: new Date().toISOString(), - attributes: MAESTRO_TEST_CONSTANTS.ATTRIBUTES, - createdTime: new Date().toISOString(), - updatedTime: new Date().toISOString(), - expiredTime: null + completedTime: new Date().toISOString(), + elementExecutions: [ + { + elementId: MAESTRO_TEST_CONSTANTS.CASE_ELEMENT_ID, + elementName: MAESTRO_TEST_CONSTANTS.ACTIVITY_NAME, + parentElementId: null, + status: MAESTRO_TEST_CONSTANTS.TASK_STATUS_COMPLETED, + startedTime: new Date().toISOString(), + completedTime: new Date().toISOString(), + processKey: MAESTRO_TEST_CONSTANTS.PROCESS_KEY, + externalLink: MAESTRO_TEST_CONSTANTS.EXTERNAL_LINK, + elementRuns: [ + { + status: MAESTRO_TEST_CONSTANTS.TASK_STATUS_COMPLETED, + startedTime: new Date().toISOString(), + completedTime: new Date().toISOString(), + elementRunId: MAESTRO_TEST_CONSTANTS.ELEMENT_RUN_ID, + parentElementRunId: null + } + ] + } + ] }, overrides); }; From 9900a280cfa883bbc9eee52685f4f8de5e783a83 Mon Sep 17 00:00:00 2001 From: Minion Bot Date: Tue, 24 Mar 2026 09:06:07 +0000 Subject: [PATCH 2/3] refactor: remove test-related changes per PR feedback Remove all changes in the tests/ folder as requested in PR review. Co-Authored-By: Claude Opus 4.6 --- tests/integration/config/unified-setup.ts | 4 +- .../process-instances.integration.test.ts | 9 ++-- .../models/maestro/process-instances.test.ts | 2 +- .../maestro/process-instances.test.ts | 20 ++++----- tests/utils/mocks/maestro.ts | 41 ++++--------------- 5 files changed, 28 insertions(+), 48 deletions(-) diff --git a/tests/integration/config/unified-setup.ts b/tests/integration/config/unified-setup.ts index c535ba1b9..b6735fa77 100644 --- a/tests/integration/config/unified-setup.ts +++ b/tests/integration/config/unified-setup.ts @@ -1,7 +1,7 @@ import { UiPath } from '../../../src/core'; import { Entities, ChoiceSets } from '../../../src/services/data-fabric'; import { Tasks } from '../../../src/services/action-center'; -import { Assets, Buckets, Queues, Processes } from '../../../src/services/orchestrator'; +import { Assets, Buckets, Jobs, Queues, Processes } from '../../../src/services/orchestrator'; import { MaestroProcessesService, ProcessInstancesService, @@ -34,6 +34,7 @@ export interface TestServices { assets: Assets; buckets: Buckets; queues: Queues; + jobs?: Jobs; processes: Processes; maestroProcesses: MaestroProcessesService; processInstances: ProcessInstancesService; @@ -111,6 +112,7 @@ function createV1Services(config: IntegrationConfig): TestServices { assets: new Assets(sdk), buckets: new Buckets(sdk), queues: new Queues(sdk), + jobs: new Jobs(sdk), processes: new Processes(sdk), maestroProcesses: new MaestroProcessesService(sdk), processInstances: new ProcessInstancesService(sdk), diff --git a/tests/integration/shared/maestro/process-instances.integration.test.ts b/tests/integration/shared/maestro/process-instances.integration.test.ts index 0987d9250..b037c257c 100644 --- a/tests/integration/shared/maestro/process-instances.integration.test.ts +++ b/tests/integration/shared/maestro/process-instances.integration.test.ts @@ -252,9 +252,12 @@ describe.each(modes)('Maestro Process Instances - Integration Tests [%s]', (mode const result = await processInstances.getExecutionHistory(testInstanceId); expect(result).toBeDefined(); - expect(result).toHaveProperty('instanceId'); - expect(result).toHaveProperty('elementExecutions'); - expect(Array.isArray(result.elementExecutions)).toBe(true); + expect(Array.isArray(result)).toBe(true); + + if (result.length > 0) { + const historyItem = result[0]; + expect(historyItem).toBeDefined(); + } } catch (error: any) { console.log('Get execution history test failed:', error.message); } diff --git a/tests/unit/models/maestro/process-instances.test.ts b/tests/unit/models/maestro/process-instances.test.ts index 9ce3bd141..c15ca9750 100644 --- a/tests/unit/models/maestro/process-instances.test.ts +++ b/tests/unit/models/maestro/process-instances.test.ts @@ -234,7 +234,7 @@ describe('Process Instance Models', () => { const mockInstanceData = createMockProcessInstance(); const instance = createProcessInstanceWithMethods(mockInstanceData, mockService); - const mockHistory = createMockExecutionHistory(); + const mockHistory = [createMockExecutionHistory()]; mockService.getExecutionHistory = vi.fn().mockResolvedValue(mockHistory); diff --git a/tests/unit/services/maestro/process-instances.test.ts b/tests/unit/services/maestro/process-instances.test.ts index 193a2bda0..0dec9bf85 100644 --- a/tests/unit/services/maestro/process-instances.test.ts +++ b/tests/unit/services/maestro/process-instances.test.ts @@ -208,34 +208,32 @@ describe('ProcessInstancesService', () => { describe('getExecutionHistory', () => { it('should return execution history for process instance', async () => { - + const instanceId = MAESTRO_TEST_CONSTANTS.INSTANCE_ID; - const mockApiResponse: ProcessInstanceExecutionHistoryResponse = createMockExecutionHistory(); + const mockApiResponse: ProcessInstanceExecutionHistoryResponse[] = [createMockExecutionHistory()]; mockApiClient.get.mockResolvedValue(mockApiResponse); - + const result = await service.getExecutionHistory(instanceId); - + expect(mockApiClient.get).toHaveBeenCalledWith( MAESTRO_ENDPOINTS.INSTANCES.GET_EXECUTION_HISTORY(instanceId), {} ); - expect(result).toHaveProperty('instanceId', MAESTRO_TEST_CONSTANTS.INSTANCE_ID); - expect(result).toHaveProperty('processKey', MAESTRO_TEST_CONSTANTS.PROCESS_KEY); - expect(result.elementExecutions).toHaveLength(1); - expect(result.elementExecutions[0]).toHaveProperty('elementId', MAESTRO_TEST_CONSTANTS.CASE_ELEMENT_ID); - expect(result.elementExecutions[0]).toHaveProperty('elementName', MAESTRO_TEST_CONSTANTS.ACTIVITY_NAME); + expect(result).toHaveLength(1); + expect(result[0]).toHaveProperty('id', MAESTRO_TEST_CONSTANTS.SPAN_ID); + expect(result[0]).toHaveProperty('traceId', MAESTRO_TEST_CONSTANTS.TRACE_ID); }); it('should handle API errors', async () => { - + const error = new Error(TEST_CONSTANTS.ERROR_MESSAGE); mockApiClient.get.mockRejectedValue(error); - + await expect(service.getExecutionHistory(MAESTRO_TEST_CONSTANTS.INSTANCE_ID)).rejects.toThrow(TEST_CONSTANTS.ERROR_MESSAGE); }); }); diff --git a/tests/utils/mocks/maestro.ts b/tests/utils/mocks/maestro.ts index fbaae7bad..af510611a 100644 --- a/tests/utils/mocks/maestro.ts +++ b/tests/utils/mocks/maestro.ts @@ -83,39 +83,16 @@ export const createMockProcessesApiResponse = (processes: any[] = []) => { */ export const createMockExecutionHistory = (overrides: Partial = {}) => { return createMockBaseResponse({ - instanceId: MAESTRO_TEST_CONSTANTS.INSTANCE_ID, - processKey: MAESTRO_TEST_CONSTANTS.PROCESS_KEY, - packageKey: MAESTRO_TEST_CONSTANTS.PACKAGE_KEY, - packageId: MAESTRO_TEST_CONSTANTS.PACKAGE_ID, - packageVersion: MAESTRO_TEST_CONSTANTS.PACKAGE_VERSION, - folderKey: MAESTRO_TEST_CONSTANTS.FOLDER_KEY, - instanceDisplayName: MAESTRO_TEST_CONSTANTS.INSTANCE_DISPLAY_NAME, - source: MAESTRO_TEST_CONSTANTS.MANUAL_SOURCE, - status: MAESTRO_TEST_CONSTANTS.TASK_STATUS_COMPLETED, - creationUserKey: MAESTRO_TEST_CONSTANTS.CREATOR_USER_KEY, + id: MAESTRO_TEST_CONSTANTS.SPAN_ID, + traceId: MAESTRO_TEST_CONSTANTS.TRACE_ID, + parentId: null, + name: MAESTRO_TEST_CONSTANTS.ACTIVITY_NAME, startedTime: new Date().toISOString(), - completedTime: new Date().toISOString(), - elementExecutions: [ - { - elementId: MAESTRO_TEST_CONSTANTS.CASE_ELEMENT_ID, - elementName: MAESTRO_TEST_CONSTANTS.ACTIVITY_NAME, - parentElementId: null, - status: MAESTRO_TEST_CONSTANTS.TASK_STATUS_COMPLETED, - startedTime: new Date().toISOString(), - completedTime: new Date().toISOString(), - processKey: MAESTRO_TEST_CONSTANTS.PROCESS_KEY, - externalLink: MAESTRO_TEST_CONSTANTS.EXTERNAL_LINK, - elementRuns: [ - { - status: MAESTRO_TEST_CONSTANTS.TASK_STATUS_COMPLETED, - startedTime: new Date().toISOString(), - completedTime: new Date().toISOString(), - elementRunId: MAESTRO_TEST_CONSTANTS.ELEMENT_RUN_ID, - parentElementRunId: null - } - ] - } - ] + endTime: new Date().toISOString(), + attributes: MAESTRO_TEST_CONSTANTS.ATTRIBUTES, + createdTime: new Date().toISOString(), + updatedTime: new Date().toISOString(), + expiredTime: null }, overrides); }; From 44de538d2082e7176e6914cd57c5d8196972a77b Mon Sep 17 00:00:00 2001 From: Minion Bot Date: Tue, 24 Mar 2026 09:12:05 +0000 Subject: [PATCH 3/3] refactor: restore tests/ folder to match main branch Fully revert all test-related changes per PR feedback by restoring the complete tests/ directory from main, including jobs test files. Co-Authored-By: Claude Opus 4.6 --- .../orchestrator/jobs.integration.test.ts | 120 ++++++++++++++ tests/unit/services/orchestrator/jobs.test.ts | 154 ++++++++++++++++++ tests/utils/constants/jobs.ts | 24 +++ tests/utils/mocks/jobs.ts | 135 +++++++++++++++ 4 files changed, 433 insertions(+) create mode 100644 tests/integration/shared/orchestrator/jobs.integration.test.ts create mode 100644 tests/unit/services/orchestrator/jobs.test.ts create mode 100644 tests/utils/constants/jobs.ts create mode 100644 tests/utils/mocks/jobs.ts diff --git a/tests/integration/shared/orchestrator/jobs.integration.test.ts b/tests/integration/shared/orchestrator/jobs.integration.test.ts new file mode 100644 index 000000000..e3ca7fd4c --- /dev/null +++ b/tests/integration/shared/orchestrator/jobs.integration.test.ts @@ -0,0 +1,120 @@ +import { describe, it, expect } from 'vitest'; +import { getServices, getTestConfig, setupUnifiedTests, InitMode } from '../../config/unified-setup'; + +const modes: InitMode[] = ['v1']; + +describe.each(modes)('Orchestrator Jobs - Integration Tests [%s]', (mode) => { + setupUnifiedTests(mode); + + describe('getAll', () => { + it('should retrieve all jobs', async () => { + const { jobs } = getServices(); + const config = getTestConfig(); + + if (!jobs) { + throw new Error('Jobs service not available in test services'); + } + + const folderId = config.folderId ? Number(config.folderId) : undefined; + + if (!folderId) { + console.log('INTEGRATION_TEST_FOLDER_ID not configured, running without folder filter.'); + } + + const result = await jobs.getAll({ + folderId, + pageSize: 100, + }); + + expect(result).toBeDefined(); + expect(result.items).toBeDefined(); + expect(Array.isArray(result.items)).toBe(true); + }); + + it('should retrieve jobs with pagination options', async () => { + const { jobs } = getServices(); + const config = getTestConfig(); + + if (!jobs) { + throw new Error('Jobs service not available in test services'); + } + + const folderId = config.folderId ? Number(config.folderId) : undefined; + + if (!folderId) { + console.log('INTEGRATION_TEST_FOLDER_ID not configured, running without folder filter.'); + } + + const result = await jobs.getAll({ + folderId, + pageSize: 10, + }); + + expect(result).toBeDefined(); + expect(result.items).toBeDefined(); + expect(Array.isArray(result.items)).toBe(true); + expect(result.items.length).toBeLessThanOrEqual(10); + }); + + it('should retrieve jobs with filter', async () => { + const { jobs } = getServices(); + const config = getTestConfig(); + + if (!jobs) { + throw new Error('Jobs service not available in test services'); + } + + const folderId = config.folderId ? Number(config.folderId) : undefined; + + if (!folderId) { + console.log('INTEGRATION_TEST_FOLDER_ID not configured, running without folder filter.'); + } + + const result = await jobs.getAll({ + folderId, + pageSize: 5, + filter: "State eq 'Successful'", + }); + + expect(result).toBeDefined(); + expect(result.items).toBeDefined(); + expect(Array.isArray(result.items)).toBe(true); + }); + }); + + describe('Job structure validation', () => { + it('should have expected fields in job objects', async () => { + const { jobs } = getServices(); + const config = getTestConfig(); + + if (!jobs) { + throw new Error('Jobs service not available in test services'); + } + + const folderId = config.folderId ? Number(config.folderId) : undefined; + + if (!folderId) { + console.log('INTEGRATION_TEST_FOLDER_ID not configured, running without folder filter.'); + } + + const result = await jobs.getAll({ + folderId, + pageSize: 1, + }); + + if (result.items.length === 0) { + throw new Error('No jobs available to validate structure'); + } + + const job = result.items[0]; + + expect(job).toBeDefined(); + expect(job.id).toBeDefined(); + expect(job.key).toBeDefined(); + expect(job.state).toBeDefined(); + expect(typeof job.id).toBe('number'); + expect(typeof job.key).toBe('string'); + expect(typeof job.state).toBe('string'); + }); + }); +}); diff --git a/tests/unit/services/orchestrator/jobs.test.ts b/tests/unit/services/orchestrator/jobs.test.ts new file mode 100644 index 000000000..8e5895343 --- /dev/null +++ b/tests/unit/services/orchestrator/jobs.test.ts @@ -0,0 +1,154 @@ +// ===== IMPORTS ===== +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +import { JobService } from '../../../../src/services/orchestrator/jobs'; +import { ApiClient } from '../../../../src/core/http/api-client'; +import { PaginationHelpers } from '../../../../src/utils/pagination/helpers'; +import { + createMockTransformedJobCollection, +} from '../../../utils/mocks/jobs'; +import { createServiceTestDependencies, createMockApiClient } from '../../../utils/setup'; +import { createMockError } from '../../../utils/mocks/core'; +import { + JobGetAllOptions, + JobGetResponse, +} from '../../../../src/models/orchestrator/jobs.types'; +import { PaginatedResponse } from '../../../../src/utils/pagination'; +import { TEST_CONSTANTS } from '../../../utils/constants/common'; +import { JOB_ENDPOINTS } from '../../../../src/utils/constants/endpoints'; + +// ===== MOCKING ===== +vi.mock('../../../../src/core/http/api-client'); + +const mocks = vi.hoisted(() => { + return import('../../../utils/mocks/core'); +}); + +vi.mock('../../../../src/utils/pagination/helpers', async () => (await mocks).mockPaginationHelpers); + +// ===== TEST SUITE ===== +describe('JobService Unit Tests', () => { + let jobService: JobService; + let mockApiClient: any; + + beforeEach(() => { + const { instance } = createServiceTestDependencies(); + mockApiClient = createMockApiClient(); + + vi.mocked(ApiClient).mockImplementation(() => mockApiClient); + + vi.mocked(PaginationHelpers.getAll).mockReset(); + + jobService = new JobService(instance); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + describe('getAll', () => { + it('should return all jobs without pagination options', async () => { + const mockResponse = createMockTransformedJobCollection(); + + vi.mocked(PaginationHelpers.getAll).mockResolvedValue(mockResponse); + + const result = await jobService.getAll(); + + expect(PaginationHelpers.getAll).toHaveBeenCalledWith( + expect.objectContaining({ + serviceAccess: expect.any(Object), + getEndpoint: expect.toSatisfy((fn: Function) => fn() === JOB_ENDPOINTS.GET_ALL), + transformFn: expect.any(Function), + pagination: expect.any(Object), + }), + undefined + ); + + expect(result).toEqual(mockResponse); + }); + + it('should return jobs filtered by folder ID', async () => { + const mockResponse = createMockTransformedJobCollection(); + + vi.mocked(PaginationHelpers.getAll).mockResolvedValue(mockResponse); + + const options: JobGetAllOptions = { + folderId: TEST_CONSTANTS.FOLDER_ID, + }; + + const result = await jobService.getAll(options); + + expect(PaginationHelpers.getAll).toHaveBeenCalledWith( + expect.objectContaining({ + serviceAccess: expect.any(Object), + transformFn: expect.any(Function), + pagination: expect.any(Object), + }), + expect.objectContaining({ + folderId: TEST_CONSTANTS.FOLDER_ID, + }) + ); + + expect(result).toEqual(mockResponse); + }); + + it('should return paginated jobs when pagination options provided', async () => { + const mockResponse = createMockTransformedJobCollection(100, { + totalCount: 100, + hasNextPage: true, + nextCursor: TEST_CONSTANTS.NEXT_CURSOR, + previousCursor: null, + currentPage: 1, + totalPages: 10, + }); + + vi.mocked(PaginationHelpers.getAll).mockResolvedValue(mockResponse); + + const options: JobGetAllOptions = { + pageSize: TEST_CONSTANTS.PAGE_SIZE, + }; + + const result = await jobService.getAll(options) as PaginatedResponse; + + expect(PaginationHelpers.getAll).toHaveBeenCalledWith( + expect.any(Object), + expect.objectContaining({ + pageSize: TEST_CONSTANTS.PAGE_SIZE, + }) + ); + + expect(result).toEqual(mockResponse); + expect(result.hasNextPage).toBe(true); + expect(result.nextCursor).toBe(TEST_CONSTANTS.NEXT_CURSOR); + }); + + it('should return jobs with filter options', async () => { + const mockResponse = createMockTransformedJobCollection(); + + vi.mocked(PaginationHelpers.getAll).mockResolvedValue(mockResponse); + + const options: JobGetAllOptions = { + folderId: TEST_CONSTANTS.FOLDER_ID, + filter: "State eq 'Running'", + }; + + const result = await jobService.getAll(options); + + expect(PaginationHelpers.getAll).toHaveBeenCalledWith( + expect.any(Object), + expect.objectContaining({ + folderId: TEST_CONSTANTS.FOLDER_ID, + filter: "State eq 'Running'", + }) + ); + + expect(result).toEqual(mockResponse); + }); + + it('should handle API errors', async () => { + const error = createMockError(TEST_CONSTANTS.ERROR_MESSAGE); + vi.mocked(PaginationHelpers.getAll).mockRejectedValue(error); + + await expect(jobService.getAll()).rejects.toThrow(TEST_CONSTANTS.ERROR_MESSAGE); + }); + }); +}); diff --git a/tests/utils/constants/jobs.ts b/tests/utils/constants/jobs.ts new file mode 100644 index 000000000..4ff80a2ad --- /dev/null +++ b/tests/utils/constants/jobs.ts @@ -0,0 +1,24 @@ +/** + * Job service test constants + * Job-specific constants only + */ + +export const JOB_TEST_CONSTANTS = { + // Job IDs + JOB_ID: 456, + + // Job Metadata + JOB_KEY: 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee', + PROCESS_NAME: 'MyProcess_Production', + HOST_MACHINE_NAME: 'ROBOT-01', + ENTRY_POINT_PATH: 'Main.xaml', + + // Timestamps + CREATED_TIME: '2023-11-01T08:00:00Z', + START_TIME: '2023-11-01T08:00:05Z', + END_TIME: '2023-11-01T08:05:30Z', + LAST_MODIFIED_TIME: '2023-11-01T08:05:30Z', + + // Error Messages + ERROR_JOB_NOT_FOUND: 'Job not found', +} as const; diff --git a/tests/utils/mocks/jobs.ts b/tests/utils/mocks/jobs.ts new file mode 100644 index 000000000..5889b42d2 --- /dev/null +++ b/tests/utils/mocks/jobs.ts @@ -0,0 +1,135 @@ +/** + * Job service mock utilities - Job-specific mocks only + * Uses generic utilities from core.ts for base functionality + */ +import { JobState } from '../../../src/models/common/types'; +import { JobPriority, JobType, PackageType, RemoteControlAccess, JobSourceType } from '../../../src/models/orchestrator/processes.types'; +import { JobGetResponse } from '../../../src/models/orchestrator/jobs.types'; +import { createMockBaseResponse, createMockCollection } from './core'; +import { JOB_TEST_CONSTANTS } from '../constants/jobs'; +import { TEST_CONSTANTS } from '../constants/common'; + +/** + * Creates a mock job with RAW API format (before transformation) + * Uses PascalCase field names that need transformation + * + * @param overrides - Optional overrides for specific fields + * @returns Raw job data as it comes from the API (before transformation) + */ +export const createMockRawJob = (overrides: Partial = {}): any => { + return createMockBaseResponse({ + Id: JOB_TEST_CONSTANTS.JOB_ID, + Key: JOB_TEST_CONSTANTS.JOB_KEY, + State: JobState.Successful, + ReleaseName: JOB_TEST_CONSTANTS.PROCESS_NAME, + HostMachineName: JOB_TEST_CONSTANTS.HOST_MACHINE_NAME, + EntryPointPath: JOB_TEST_CONSTANTS.ENTRY_POINT_PATH, + JobPriority: JobPriority.Normal, + Type: JobType.Unattended, + Source: 'Manual', + Info: null, + InputArguments: null, + OutputArguments: null, + // Using raw API field names that should be transformed + CreationTime: JOB_TEST_CONSTANTS.CREATED_TIME, + StartTime: JOB_TEST_CONSTANTS.START_TIME, + EndTime: JOB_TEST_CONSTANTS.END_TIME, + LastModificationTime: JOB_TEST_CONSTANTS.LAST_MODIFIED_TIME, + OrganizationUnitId: TEST_CONSTANTS.FOLDER_ID, + OrganizationUnitFullyQualifiedName: TEST_CONSTANTS.FOLDER_NAME, + Machine: { Id: 1, Name: 'ROBOT-01' }, + Robot: { Id: 1, Name: 'Robot1', Username: String.raw`domain\robot1` }, + JobError: null, + }, overrides); +}; + +/** + * Creates a basic job object with TRANSFORMED data (not raw API format) + * + * @param overrides - Optional overrides for specific fields + * @returns Job with transformed field names (camelCase) + */ +export const createBasicJob = (overrides: Partial = {}): JobGetResponse => { + return createMockBaseResponse({ + id: JOB_TEST_CONSTANTS.JOB_ID, + key: JOB_TEST_CONSTANTS.JOB_KEY, + state: JobState.Successful, + processName: JOB_TEST_CONSTANTS.PROCESS_NAME, + hostMachineName: JOB_TEST_CONSTANTS.HOST_MACHINE_NAME, + entryPointPath: JOB_TEST_CONSTANTS.ENTRY_POINT_PATH, + jobPriority: JobPriority.Normal, + specificPriorityValue: 45, + type: JobType.Unattended, + packageType: PackageType.Process, + runtimeType: null, + sourceType: JobSourceType.Manual, + serverlessJobType: null, + stopStrategy: null, + remoteControlAccess: RemoteControlAccess.None, + folderKey: null, + batchExecutionKey: '00000000-0000-0000-0000-000000000000', + parentJobKey: null, + startingScheduleId: null, + startingTriggerId: null, + processVersionId: null, + maxExpectedRunningTimeSeconds: null, + requiresUserInteraction: false, + resumeOnSameContext: false, + resumeVersion: null, + subState: null, + targetRuntime: null, + traceId: null, + parentSpanId: null, + errorCode: null, + source: 'Manual', + reference: '', + info: null, + inputArguments: null, + outputArguments: null, + environmentVariables: null, + resumeTime: null, + createdTime: JOB_TEST_CONSTANTS.CREATED_TIME, + startTime: JOB_TEST_CONSTANTS.START_TIME, + endTime: JOB_TEST_CONSTANTS.END_TIME, + lastModifiedTime: JOB_TEST_CONSTANTS.LAST_MODIFIED_TIME, + folderId: TEST_CONSTANTS.FOLDER_ID, + folderName: TEST_CONSTANTS.FOLDER_NAME, + machine: { id: 1, name: 'ROBOT-01' }, + robot: { id: 1, name: 'Robot1', username: String.raw`domain\robot1` }, + jobError: null, + }, overrides); +}; + +/** + * Creates a mock transformed job collection response as returned by PaginationHelpers.getAll + * + * @param count - Number of jobs to include (defaults to 1) + * @param options - Additional options like totalCount, pagination details + * @returns Mock transformed job collection with items array + */ +export const createMockTransformedJobCollection = ( + count: number = 1, + options?: { + totalCount?: number; + hasNextPage?: boolean; + nextCursor?: string; + previousCursor?: string | null; + currentPage?: number; + totalPages?: number; + } +): any => { + const items = createMockCollection(count, (index) => createBasicJob({ + id: JOB_TEST_CONSTANTS.JOB_ID + index, + key: `${index}-${JOB_TEST_CONSTANTS.JOB_KEY}`, + })); + + return createMockBaseResponse({ + items, + totalCount: options?.totalCount || count, + ...(options?.hasNextPage !== undefined && { hasNextPage: options.hasNextPage }), + ...(options?.nextCursor && { nextCursor: options.nextCursor }), + ...(options?.previousCursor !== undefined && { previousCursor: options.previousCursor }), + ...(options?.currentPage !== undefined && { currentPage: options.currentPage }), + ...(options?.totalPages !== undefined && { totalPages: options.totalPages }), + }); +};