Skip to content

feat: start method on maestro process#365

Draft
vnaren23 wants to merge 2 commits into
mainfrom
users/naren/maestroprocessstart
Draft

feat: start method on maestro process#365
vnaren23 wants to merge 2 commits into
mainfrom
users/naren/maestroprocessstart

Conversation

@vnaren23
Copy link
Copy Markdown
Collaborator

Prompt used 1. Under maestro Processes I want to add a new method named start. It should internally call the same start logic done within Processes. One difference, instead of folderId: number as an argument, take folderKey: string 2. Orchestrator process start and Maestro process start both has same implementation with only headers being the difference. Is there a way to reduce code duplication across both methods
Generated claude plan

Plan: Deduplicate Orchestrator + Maestro start process logic

Context

ProcessService.start (orchestrator) and MaestroProcessesService.start (maestro) currently contain near-identical implementations. The only meaningful difference is the folder header:

  • Orchestrator: X-UIPATH-OrganizationUnitId (numeric folderId)
  • Maestro: X-UIPATH-FolderKey (string folderKey)

Everything else — transformRequest(request, ProcessMap), the { startInfo: ... } body wrap, OData $-prefixing of options, the POST to PROCESS_ENDPOINTS.START_PROCESS, and the response transform pipeline (pascalToCamelCaseKeys + transformData) — is duplicated verbatim. Any future change (new transform field, response shape change, error handling tweak) must currently be made in two places.

The goal is to extract the shared body into one helper so both public methods become thin wrappers that only differ in their folder-header construction and @track label.

Approach

Extract a free helper function startProcessRequest() that takes the work that's identical between the two services. Both ProcessService.start and MaestroProcessesService.start keep their public signatures and @track decorators; their bodies shrink to building the headers and delegating.

Why a free function (and not a base class / static method)

BaseService.post is protected, so the helper cannot call it from outside the class hierarchy. The cleanest workaround — already used elsewhere in the codebase (createPaginationServiceAccess on base.ts:90) — is to have the calling service hand the helper a bound post callable. This avoids inheritance gymnastics, keeps BaseService free of domain-specific code, and preserves protected-method encapsulation.

Helper signature

New file: src/services/orchestrator/processes/helpers.ts (orchestrator owns the underlying API).

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>>;

export async function startProcessRequest(
  post: Poster,
  request: ProcessStartRequest,
  headers: Record<string, string>,
  options: RequestOptions = {}
): Promise<ProcessStartResponse[]> {
  const apiRequest = transformRequest(request, ProcessMap);
  const requestBody = { startInfo: apiRequest };
  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)
  );
}

Caller shape

src/services/orchestrator/processes/processes.tsstart() becomes:

@track('Processes.Start')
async start(request: ProcessStartRequest, folderId: number, options: RequestOptions = {}): Promise<ProcessStartResponse[]> {
  return startProcessRequest(
    this.post.bind(this),
    request,
    createHeaders({ [FOLDER_ID]: folderId }),
    options
  );
}

src/services/maestro/processes/processes.tsstart() becomes:

@track('MaestroProcesses.Start')
async start(request: ProcessStartRequest, folderKey: string, options: RequestOptions = {}): Promise<ProcessStartResponse[]> {
  return startProcessRequest(
    this.post.bind(this),
    request,
    createHeaders({ [FOLDER_KEY]: folderKey }),
    options
  );
}

After the refactor, the maestro service can drop its now-unused imports of transformRequest, pascalToCamelCaseKeys, transformData, addPrefixToKeys, ODATA_PREFIX, CollectionResponse, ProcessMap, and PROCESS_ENDPOINTS (only RequestOptions, the response/request types, FOLDER_KEY, createHeaders, and the new helper remain). Same housekeeping for orchestrator.

Files to modify

File Change
src/services/orchestrator/processes/helpers.ts new — exports startProcessRequest
src/services/orchestrator/processes/processes.ts start() body delegates to helper; trim imports
src/services/maestro/processes/processes.ts start() body delegates to helper; trim imports

No model, endpoint, type, or test changes required — public signatures and behavior are unchanged.

Verification

  1. npm run typecheck — clean
  2. npm run lint — 0 warnings/errors
  3. npm run test:unit — all existing orchestrator + maestro start tests pass unchanged. The 11 maestro tests and 5 orchestrator start tests already exercise: processKey path, processName path, OData option $ prefixing, multi-job response, and error propagation. No new tests needed because the public contract is identical.
  4. npm run build — Rollup completes without import-resolution errors (the new helpers.ts is internal to its module and not exported from any subpath).

Out of scope

  • Renaming the helper or relocating it to a shared/ directory — orchestrator owns the endpoint, so co-locating with orchestrator processes is correct.
  • Touching getById / getAll — they don't share enough surface to justify deduplication.
  • Adding integration tests for the helper directly — it has no public surface.
Local Testing

Done on playground app

@vnaren23 vnaren23 self-assigned this Apr 13, 2026
@vnaren23 vnaren23 requested a review from a team April 13, 2026 15:05
): Promise<ProcessStartResponse[]> {
// Transform SDK field names to API field names (e.g., processKey → releaseKey)
const apiRequest = transformRequest(request, ProcessMap);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These three inline comments explain self-evident code — transformRequest, addPrefixToKeys, and the startInfo body shape are all well-named and need no narration. Per project conventions: "Only add comments where the logic isn't self-evident." Please remove them.

*
* @param request - Process start request body (provide either `processKey` or `processName`)
* @param folderKey - Required folder key
* @param options - Optional query parameters
Copy link
Copy Markdown
Contributor

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."

* @returns Promise resolving to the created jobs  see {@link ProcessStartResponse}

const result = await service.start(request, MAESTRO_TEST_CONSTANTS.FOLDER_KEY);

expect(result).toBeDefined();
expect(result).toHaveLength(1);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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 expect(result[0].state).toBe('Running'), add:

// 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 assets.test.ts:94 and queues.test.ts:88.


const processKey = config.orchestratorTestProcessKey;

expect(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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 getIncidents test uses:

if (!processKey) {
  console.warn('MAESTRO_TEST_PROCESS_KEY not configured, skipping test');
  return;
}

Per conventions: "Use console.warn() + skip for setup preconditions outside the test's control." expect().toBeDefined() throws and marks the test as failed rather than skipped when the env var is missing. Use if (!processKey) { console.warn(...); return; } to be consistent.

expect(result[0].id).toBeDefined();
}
});
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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 describe('start') block (guarded by the same processKey check):

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();
});

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Apr 13, 2026

Reviewed against the project conventions in agent_docs/rules.md and agent_docs/conventions.md.

Required changes:

  1. Remove unnecessary inline comments from helpers.ts — the three comments explain self-evident function calls and violate the "only comment non-obvious logic" rule.
  2. Fix @returns JSDoc in processes.models.ts{@link ProcessStartResponse} must be inline with the @returns line.
  3. Add transform-completeness assertions to the first unit test — verify PascalCase originals are absent after the transform pipeline (per assets.test.ts:94 pattern).
  4. Use console.warn + return guard in the integration test instead of expect().toBeDefined() — consistent with getIncidents test pattern in the same file.
  5. Add a transform validation integration test in the describe('start') block.

The core implementation (helper extraction, deduplication, header differences, @track decorators, oauth-scopes.md placement) is correct.

@sonarqubecloud
Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
6.9% Duplication on New Code (required ≤ 3%)

See analysis details on SonarQube Cloud

@vnaren23 vnaren23 marked this pull request as draft April 13, 2026 15:17
Comment on lines +52 to +54

return response.data?.value.map(process =>
transformData(pascalToCamelCaseKeys(process) as ProcessStartResponse, ProcessMap)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The optional chain on response.data?.value.map(...) means this can return undefined when response.data is nullish, but the declared return type is Promise<ProcessStartResponse[]>. That's a type lie — callers doing result[0] will get a runtime crash if the API ever returns an empty/null body.

Suggested change
return response.data?.value.map(process =>
transformData(pascalToCamelCaseKeys(process) as ProcessStartResponse, ProcessMap)
return response.data?.value.map(process =>
transformData(pascalToCamelCaseKeys(process) as ProcessStartResponse, ProcessMap)
) ?? [];

Comment on lines +175 to +179
expect(result[0].processName).toBe(PROCESS_TEST_CONSTANTS.PROCESS_NAME);
expect(result[0].state).toBe('Running');

expect(mockApiClient.post).toHaveBeenCalledWith(
PROCESS_ENDPOINTS.START_PROCESS,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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 pascalToCamelCaseKeys is dropped would still pass (the raw fields would just carry through under different names).

Suggested change
expect(result[0].processName).toBe(PROCESS_TEST_CONSTANTS.PROCESS_NAME);
expect(result[0].state).toBe('Running');
expect(mockApiClient.post).toHaveBeenCalledWith(
PROCESS_ENDPOINTS.START_PROCESS,
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');
// Verify PascalCase originals were removed by the transform pipeline
expect((result[0] as any).ProcessName).toBeUndefined();
expect((result[0] as any).State).toBeUndefined();

See the established pattern in assets.test.ts:94, queues.test.ts:88, choicesets.test.ts:243.

Comment on lines +147 to +157
).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(),
}),
},
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

config.folderId maps to the INTEGRATION_TEST_FOLDER_ID env var, which is a numeric Orchestrator folder ID (stored as string). Maestro's start() takes folderKey: string — a named folder key like "Accounting", not a numeric ID. These are semantically different and the API won't accept an ID where a key is expected.

This integration test needs a Maestro-specific string folder key. IntegrationConfig will need a new field (e.g., maestroFolderKey?: string backed by a MAESTRO_FOLDER_KEY env var), or you can reuse an existing string-keyed config field if one already exists in the test environment.

Also, the error message on line 147 says 'INTEGRATION_TEST_FOLDER_ID must be configured' — if you add the new field, update the message to match.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant