Files
upstream-mirror e7da97194a
Some checks failed
App Prod-Parity E2E Dispatch / dispatch (push) Failing after 10s
CD Storybook UI / build (push) Failing after 3m25s
CD Storybook UI / dispatch (push) Has been skipped
CD deploy main / website-changed-files (push) Successful in 18m8s
CI Agent Skills / changed-files-check (push) Successful in 18m9s
CI Create App E2E minimal / changed-files-check (push) Successful in 24s
CI Create App / changed-files-check (push) Successful in 26s
CI Docs / changed-files-check (push) Successful in 26s
CI Emails / changed-files-check (push) Successful in 28s
CI Example App Hello World / changed-files-check (push) Successful in 23s
CI Front / changed-files-check (push) Has been skipped
CI Example App Postcard / changed-files-check (push) Successful in 27s
CI UI / changed-files-check (push) Has been skipped
CI Utils Test / changed-files-check (push) Successful in 45s
CI Twenty Apps / discover (push) Successful in 1m3s
CD deploy main / deploy-main (push) Failing after 3s
CI Agent Skills / agent-skills-validate (push) Has been skipped
CI Create App / create-app-test (lint) (push) Has been skipped
CI Emails / emails-test (push) Has been skipped
CI Front / front-task (lingui:compile) (push) Has been skipped
CI Front / front-task (lint) (push) Has been skipped
CI Example App Postcard / example-app-postcard (push) Has been skipped
CI UI / ui-task (lint) (push) Has been skipped
CI UI / ui-task (pack-size) (push) Has been skipped
CI UI / ui-task (size) (push) Has been skipped
CI UI / ui-task (test) (push) Has been skipped
CI UI / ui-task (typecheck) (push) Has been skipped
CI Utils Test / utils-test (push) Has been skipped
CI Twenty Apps / ${{ matrix.app.name }} (push) Has been skipped
CI Emails / ci-emails-status-check (push) Successful in 2s
CI Example App Hello World / ci-example-app-hello-world-status-check (push) Successful in 2s
CI Example App Postcard / ci-example-app-postcard-status-check (push) Successful in 3s
CI UI / ui-sb-test (push) Has been skipped
CI Utils Test / ci-utils-test-status-check (push) Successful in 2s
CI Twenty Apps / ci-twenty-apps-status-check (push) Successful in 2s
CI UI / ci-ui-status-check (push) Successful in 2s
CI Desktop / desktop (push) Failing after 1m8s
Push docs to Crowdin / Push documentation to Crowdin (push) Failing after 55s
Push translations to Crowdin / Extract and upload translations (push) Failing after 5s
Push website translations to Crowdin / Extract and upload website translations (push) Failing after 5s
CI Create App / create-app-test (test) (push) Has been skipped
CI Create App / create-app-test (typecheck) (push) Has been skipped
CI Docs / docs-lint (push) Has been skipped
CI Example App Hello World / example-app-hello-world (push) Has been skipped
CI Front / front-task (lingui:extract) (push) Has been skipped
CI Front / front-task (test) (push) Has been skipped
CI Front / front-task (typecheck) (push) Has been skipped
CI UI / ui-sb-build (push) Failing after 1m32s
CI Twenty Apps / ${{ matrix.app.name }} (${{ matrix.server-source }}) (push) Has been skipped
CI Agent Skills / agent-skills-publish (push) Has been skipped
CI Create App / ci-create-app-status-check (push) Successful in 2s
CI Agent Skills / agent-skills-remote-source (push) Has been skipped
CI Create App E2E minimal / create-app-e2e-minimal (push) Has been cancelled
CI E2E Main / e2e-test (push) Has been cancelled
CI Front / front-sb-build (push) Has been cancelled
CI Front / front-build (push) Has been cancelled
CI Create App E2E minimal / ci-create-app-e2e-minimal-status-check (push) Has been cancelled
CI E2E Main / ci-e2e-main-status-check (push) Has been cancelled
CI E2E Main / notify-main-ci-failure (push) Has been cancelled
CI E2E Main / qa-scout-comment (push) Has been cancelled
CI Front / front-sb-test (push) Has been cancelled
CI Front / front-sb-screenshots (push) Has been cancelled
CI Front / ci-front-status-check (push) Has been cancelled
DPA Sub-Processor Sync / sync-subprocessors (push) Failing after 3s
AI Catalog Sync / sync-catalog (push) Failing after 2s
snapshot of https://github.com/twentyhq/twenty.git (main, 2026-09-17; blobs > 131072B omitted)
2026-09-17 23:28:13 +05:00

721 lines
22 KiB
TypeScript

import { existsSync, mkdirSync, readFileSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { randomUUID } from 'crypto';
import gql from 'graphql-tag';
import { activateWorkspace } from 'test/integration/graphql/utils/activate-workspace.util';
import { deleteUser } from 'test/integration/graphql/utils/delete-user.util';
import { findManyApplications } from 'test/integration/graphql/utils/find-many-applications.util';
import { getAuthTokensFromLoginToken } from 'test/integration/graphql/utils/get-auth-tokens-from-login-token.util';
import { signUpInNewWorkspace } from 'test/integration/graphql/utils/sign-up-in-new-workspace.util';
import { signUp } from 'test/integration/graphql/utils/sign-up.util';
import { generateApplicationToken } from 'test/integration/metadata/suites/application/utils/generate-application-token.util';
import { createOneLogicFunction } from 'test/integration/metadata/suites/logic-function/utils/create-logic-function.util';
import { deleteLogicFunction } from 'test/integration/metadata/suites/logic-function/utils/delete-logic-function.util';
import { executeLogicFunction } from 'test/integration/metadata/suites/logic-function/utils/execute-logic-function.util';
import { updateLogicFunctionSource } from 'test/integration/metadata/suites/logic-function/utils/update-logic-function-source.util';
import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util';
import { expectEventually } from 'test/integration/utils/expect-eventually.util';
import { waitForAllJobsToFinish } from 'test/integration/utils/wait-for-all-jobs-to-finish.util';
import { isDefined } from 'twenty-shared/utils';
import { v4 as uuidv4 } from 'uuid';
import { WORKSPACE_CUSTOM_APPLICATION_NAME } from 'src/engine/core-modules/application/constants/workspace-custom-application.constant';
import { TWENTY_STANDARD_APPLICATION } from 'src/engine/workspace-manager/twenty-standard-application/constants/twenty-standard-applications';
const MARKER_DIRECTORY = join(tmpdir(), `enqueue-job-${uuidv4()}`);
const TARGET_SOURCE_CODE = `import { writeFileSync } from 'node:fs';
export const main = async (params: { markerPath: string }): Promise<object> => {
writeFileSync(params.markerPath, 'ran', 'utf-8');
return { ok: true };
};`;
const APPENDING_TARGET_SOURCE_CODE = `import { appendFileSync } from 'node:fs';
export const main = async (params: { markerPath: string }): Promise<object> => {
appendFileSync(params.markerPath, 'ran\\n', 'utf-8');
return { ok: true };
};`;
const RETRYABLE_TARGET_SOURCE_CODE = `import { appendFileSync } from 'node:fs';
type RetryableTargetParams = {
markerPath: string;
shouldRetry: boolean;
};
type RetryContext = {
retryCount: number;
maxRetries: number;
};
export const main = async (
params: RetryableTargetParams,
context: RetryContext,
): Promise<object> => {
appendFileSync(
params.markerPath,
context.retryCount + '/' + context.maxRetries + '\\n',
'utf-8',
);
if (params.shouldRetry) {
const retryableError = new Error('Dependency is temporarily unavailable');
retryableError.name = 'RetryableLogicFunctionError';
throw retryableError;
}
return { ok: true };
};`;
const ENQUEUE_JOB = gql`
mutation EnqueueJob($input: EnqueueJobInput!) {
enqueueJob(input: $input) {
enqueued
logicFunctionUniversalIdentifier
jobId
}
}
`;
const ENQUEUE_JOBS = gql`
mutation EnqueueJobs($input: EnqueueJobsInput!) {
enqueueJobs(input: $input) {
enqueued
logicFunctionUniversalIdentifier
enqueuedJobsCount
jobIds
}
}
`;
const GET_JOBS = gql`
query GetJobs($jobIds: [String!]!) {
getJobs(jobIds: $jobIds) {
jobId
state
attemptsMade
failedReason
enqueuedAt
startedAt
finishedAt
}
}
`;
describe('enqueueJob (e2e)', () => {
let customApplicationToken: string;
let standardApplicationToken: string;
let logicFunctionId: string;
let logicFunctionUniversalIdentifier: string;
let retryableLogicFunctionId: string;
let retryableLogicFunctionUniversalIdentifier: string;
let appendingLogicFunctionId: string;
let appendingLogicFunctionUniversalIdentifier: string;
let otherWorkspaceUserAccessToken: string | undefined;
const createOtherWorkspaceApplicationToken = async () => {
const email = `enqueue-job-other-workspace-${randomUUID()}@example.com`;
const { data: signUpData } = await signUp({
input: { email, password: 'Test123!@#' },
expectToFail: false,
});
otherWorkspaceUserAccessToken =
signUpData.signUp.tokens.accessOrWorkspaceAgnosticToken.token;
await globalThis.testDataSource.query(
'UPDATE core."user" SET "isEmailVerified" = true WHERE email = $1',
[email],
);
const { data: newWorkspaceData } = await signUpInNewWorkspace({
accessToken: otherWorkspaceUserAccessToken,
expectToFail: false,
});
const { data: authTokensData } = await getAuthTokensFromLoginToken({
loginToken: newWorkspaceData.signUpInNewWorkspace.loginToken.token,
origin:
newWorkspaceData.signUpInNewWorkspace.workspace.workspaceUrls
.subdomainUrl,
expectToFail: false,
});
const workspaceAccessToken =
authTokensData.getAuthTokensFromLoginToken.tokens
.accessOrWorkspaceAgnosticToken.token;
await activateWorkspace({
accessToken: workspaceAccessToken,
expectToFail: false,
});
const { data: applicationsData } = await findManyApplications({
accessToken: workspaceAccessToken,
expectToFail: false,
});
const otherWorkspaceApplication =
applicationsData.findManyApplications.find(
(application) => application.name === WORKSPACE_CUSTOM_APPLICATION_NAME,
);
expect(otherWorkspaceApplication).toBeDefined();
const { data: tokenData } = await generateApplicationToken({
applicationId: otherWorkspaceApplication!.id,
expectToFail: false,
token: workspaceAccessToken,
});
return tokenData.generateApplicationToken.applicationAccessToken.token;
};
beforeAll(async () => {
mkdirSync(MARKER_DIRECTORY, { recursive: true });
const { data } = await findManyApplications({ expectToFail: false });
const customApplication = data.findManyApplications.find(
(application) => application.name === WORKSPACE_CUSTOM_APPLICATION_NAME,
);
const standardApplication = data.findManyApplications.find(
(application) =>
application.universalIdentifier ===
TWENTY_STANDARD_APPLICATION.universalIdentifier,
);
expect(customApplication).toBeDefined();
expect(standardApplication).toBeDefined();
const [{ data: customTokenData }, { data: standardTokenData }] =
await Promise.all([
generateApplicationToken({
applicationId: customApplication!.id,
expectToFail: false,
}),
generateApplicationToken({
applicationId: standardApplication!.id,
expectToFail: false,
}),
]);
customApplicationToken =
customTokenData.generateApplicationToken.applicationAccessToken.token;
standardApplicationToken =
standardTokenData.generateApplicationToken.applicationAccessToken.token;
const { data: createData } = await createOneLogicFunction({
input: { name: `enqueue-job-target-${uuidv4()}` },
gqlFields: 'id universalIdentifier',
expectToFail: false,
});
const { data: retryableCreateData } = await createOneLogicFunction({
input: { name: `enqueue-job-retryable-target-${uuidv4()}` },
gqlFields: 'id universalIdentifier',
expectToFail: false,
});
const { data: appendingCreateData } = await createOneLogicFunction({
input: { name: `enqueue-job-appending-target-${uuidv4()}` },
gqlFields: 'id universalIdentifier',
expectToFail: false,
});
expect(createData.createOneLogicFunction.universalIdentifier).toBeDefined();
expect(
retryableCreateData.createOneLogicFunction.universalIdentifier,
).toBeDefined();
expect(
appendingCreateData.createOneLogicFunction.universalIdentifier,
).toBeDefined();
logicFunctionId = createData.createOneLogicFunction.id;
logicFunctionUniversalIdentifier =
createData.createOneLogicFunction.universalIdentifier!;
retryableLogicFunctionId = retryableCreateData.createOneLogicFunction.id;
retryableLogicFunctionUniversalIdentifier =
retryableCreateData.createOneLogicFunction.universalIdentifier!;
appendingLogicFunctionId = appendingCreateData.createOneLogicFunction.id;
appendingLogicFunctionUniversalIdentifier =
appendingCreateData.createOneLogicFunction.universalIdentifier!;
await updateLogicFunctionSource({
input: {
id: retryableLogicFunctionId,
update: { sourceHandlerCode: RETRYABLE_TARGET_SOURCE_CODE },
},
expectToFail: false,
});
await updateLogicFunctionSource({
input: {
id: appendingLogicFunctionId,
update: { sourceHandlerCode: APPENDING_TARGET_SOURCE_CODE },
},
expectToFail: false,
});
const { data: retryableBuildData } = await executeLogicFunction({
input: {
id: retryableLogicFunctionId,
payload: {
markerPath: join(MARKER_DIRECTORY, 'retryable-build.txt'),
shouldRetry: false,
},
},
expectToFail: false,
});
expect(retryableBuildData.executeOneLogicFunction.error).toBeNull();
const { data: appendingBuildData } = await executeLogicFunction({
input: {
id: appendingLogicFunctionId,
payload: {
markerPath: join(MARKER_DIRECTORY, 'appending-build.txt'),
},
},
expectToFail: false,
});
expect(appendingBuildData.executeOneLogicFunction.error).toBeNull();
});
afterAll(async () => {
const teardownErrors: unknown[] = [];
for (const id of [
logicFunctionId,
retryableLogicFunctionId,
appendingLogicFunctionId,
]) {
try {
await deleteLogicFunction({ input: { id }, expectToFail: false });
} catch (error) {
teardownErrors.push(error);
}
}
if (teardownErrors.length > 0) {
throw teardownErrors[0];
}
if (isDefined(otherWorkspaceUserAccessToken)) {
await deleteUser({ accessToken: otherWorkspaceUserAccessToken });
}
rmSync(MARKER_DIRECTORY, { recursive: true, force: true });
});
it('rejects requests that do not carry an APPLICATION_ACCESS token', async () => {
const response = await makeMetadataAPIRequest({
query: ENQUEUE_JOB,
variables: { input: { logicFunctionUniversalIdentifier } },
});
expect(response.body.errors).toBeDefined();
expect(response.body.errors[0].message).toContain('APPLICATION_ACCESS');
});
it('enqueues a logic function owned by the calling application and the worker runs it', async () => {
const markerPath = join(MARKER_DIRECTORY, 'enqueued.txt');
await updateLogicFunctionSource({
input: {
id: logicFunctionId,
update: { sourceHandlerCode: TARGET_SOURCE_CODE },
},
expectToFail: false,
});
const { data: buildData } = await executeLogicFunction({
input: {
id: logicFunctionId,
payload: { markerPath: join(MARKER_DIRECTORY, 'build.txt') },
},
expectToFail: false,
});
expect(buildData.executeOneLogicFunction.error).toBeNull();
const response = await makeMetadataAPIRequest(
{
query: ENQUEUE_JOB,
variables: {
input: { logicFunctionUniversalIdentifier, payload: { markerPath } },
},
},
customApplicationToken,
);
expect(response.body.errors).toBeUndefined();
expect(response.body.data.enqueueJob).toEqual({
enqueued: true,
logicFunctionUniversalIdentifier,
jobId: expect.any(String),
});
await waitForAllJobsToFinish();
await expectEventually(() => {
expect(existsSync(markerPath)).toBe(true);
});
});
it('caps application-requested retries independently from the overall queue budget', async () => {
const markerPath = join(MARKER_DIRECTORY, 'application-retry-cap.txt');
const response = await makeMetadataAPIRequest(
{
query: ENQUEUE_JOB,
variables: {
input: {
logicFunctionUniversalIdentifier:
retryableLogicFunctionUniversalIdentifier,
payload: { markerPath, shouldRetry: true },
retryLimit: 10,
},
},
},
customApplicationToken,
);
expect(response.body.errors).toBeUndefined();
expect(response.body.data.enqueueJob.enqueued).toBe(true);
await waitForAllJobsToFinish();
expect(readFileSync(markerPath, 'utf-8').trim().split('\n')).toEqual([
'0/3',
'1/3',
'2/3',
'3/3',
]);
}, 60_000);
it('uses a smaller overall queue budget as the application retry maximum', async () => {
const markerPath = join(MARKER_DIRECTORY, 'queue-retry-cap.txt');
const response = await makeMetadataAPIRequest(
{
query: ENQUEUE_JOB,
variables: {
input: {
logicFunctionUniversalIdentifier:
retryableLogicFunctionUniversalIdentifier,
payload: { markerPath, shouldRetry: true },
retryLimit: 1,
},
},
},
customApplicationToken,
);
expect(response.body.errors).toBeUndefined();
expect(response.body.data.enqueueJob.enqueued).toBe(true);
await waitForAllJobsToFinish();
expect(readFileSync(markerPath, 'utf-8').trim().split('\n')).toEqual([
'0/1',
'1/1',
]);
}, 60_000);
it('rejects a logic function that belongs to another application', async () => {
const response = await makeMetadataAPIRequest(
{
query: ENQUEUE_JOB,
variables: {
input: { logicFunctionUniversalIdentifier },
},
},
standardApplicationToken,
);
expect(response.body.errors).toBeDefined();
expect(response.body.errors[0].message).toContain('not found');
});
it('rejects an unknown logic function', async () => {
const response = await makeMetadataAPIRequest(
{
query: ENQUEUE_JOB,
variables: {
input: { logicFunctionUniversalIdentifier: uuidv4() },
},
},
customApplicationToken,
);
expect(response.body.errors).toBeDefined();
expect(response.body.errors[0].message).toContain('not found');
});
it('enqueues a batch of payloads in a single call and the worker runs them all', async () => {
const markerPaths = [
join(MARKER_DIRECTORY, 'batch-0.txt'),
join(MARKER_DIRECTORY, 'batch-1.txt'),
];
const response = await makeMetadataAPIRequest(
{
query: ENQUEUE_JOBS,
variables: {
input: {
logicFunctionUniversalIdentifier,
payloads: markerPaths.map((markerPath) => ({ markerPath })),
},
},
},
customApplicationToken,
);
expect(response.body.errors).toBeUndefined();
expect(response.body.data.enqueueJobs).toEqual({
enqueued: true,
logicFunctionUniversalIdentifier,
enqueuedJobsCount: markerPaths.length,
jobIds: markerPaths.map(() => expect.any(String)),
});
await waitForAllJobsToFinish();
await expectEventually(() => {
for (const markerPath of markerPaths) {
expect(existsSync(markerPath)).toBe(true);
}
});
});
it('rejects a batch targeting an unknown logic function', async () => {
const response = await makeMetadataAPIRequest(
{
query: ENQUEUE_JOBS,
variables: {
input: {
logicFunctionUniversalIdentifier: uuidv4(),
payloads: [{}],
},
},
},
customApplicationToken,
);
expect(response.body.errors).toBeDefined();
expect(response.body.errors[0].message).toContain('not found');
});
it('rejects an empty batch', async () => {
const response = await makeMetadataAPIRequest(
{
query: ENQUEUE_JOBS,
variables: {
input: { logicFunctionUniversalIdentifier, payloads: [] },
},
},
customApplicationToken,
);
expect(response.body.errors).toBeDefined();
expect(response.body.errors[0].message).toContain('payloads');
});
it('reports a caller-supplied job id as completed once the worker ran it', async () => {
const markerPath = join(MARKER_DIRECTORY, 'job-status.txt');
const jobId = `job-status-${uuidv4()}`;
const enqueueResponse = await makeMetadataAPIRequest(
{
query: ENQUEUE_JOBS,
variables: {
input: {
logicFunctionUniversalIdentifier,
jobs: [{ payload: { markerPath }, jobId }],
},
},
},
customApplicationToken,
);
expect(enqueueResponse.body.errors).toBeUndefined();
expect(enqueueResponse.body.data.enqueueJobs.jobIds).toEqual([jobId]);
await waitForAllJobsToFinish();
const statusResponse = await makeMetadataAPIRequest(
{ query: GET_JOBS, variables: { jobIds: [jobId] } },
customApplicationToken,
);
expect(statusResponse.body.errors).toBeUndefined();
expect(statusResponse.body.data.getJobs).toHaveLength(1);
expect(statusResponse.body.data.getJobs[0]).toMatchObject({
jobId,
state: 'COMPLETED',
});
});
it('omits unknown job ids instead of failing the whole read', async () => {
const response = await makeMetadataAPIRequest(
{ query: GET_JOBS, variables: { jobIds: [uuidv4()] } },
customApplicationToken,
);
expect(response.body.errors).toBeUndefined();
expect(response.body.data.getJobs).toEqual([]);
});
it('does not run a caller-supplied job id a second time', async () => {
const markerPath = join(MARKER_DIRECTORY, 'idempotent.txt');
const jobId = `idempotent-${uuidv4()}`;
const variables = {
input: {
logicFunctionUniversalIdentifier:
appendingLogicFunctionUniversalIdentifier,
jobs: [{ payload: { markerPath }, jobId }],
},
};
const firstResponse = await makeMetadataAPIRequest(
{ query: ENQUEUE_JOBS, variables },
customApplicationToken,
);
expect(firstResponse.body.errors).toBeUndefined();
expect(firstResponse.body.data.enqueueJobs.jobIds).toEqual([jobId]);
await waitForAllJobsToFinish();
await expectEventually(() => {
expect(existsSync(markerPath)).toBe(true);
});
const secondResponse = await makeMetadataAPIRequest(
{ query: ENQUEUE_JOBS, variables },
customApplicationToken,
);
expect(secondResponse.body.errors).toBeUndefined();
expect(secondResponse.body.data.enqueueJobs.jobIds).toEqual([jobId]);
await waitForAllJobsToFinish();
expect(readFileSync(markerPath, 'utf-8').trim().split('\n')).toEqual([
'ran',
]);
}, 60_000);
it('does not expose a job to another workspace', async () => {
const markerPath = join(MARKER_DIRECTORY, 'cross-workspace.txt');
const jobId = `cross-workspace-${uuidv4()}`;
const enqueueResponse = await makeMetadataAPIRequest(
{
query: ENQUEUE_JOBS,
variables: {
input: {
logicFunctionUniversalIdentifier,
jobs: [{ payload: { markerPath }, jobId }],
},
},
},
customApplicationToken,
);
expect(enqueueResponse.body.errors).toBeUndefined();
await waitForAllJobsToFinish();
await expectEventually(() => {
expect(existsSync(markerPath)).toBe(true);
});
const ownWorkspaceResponse = await makeMetadataAPIRequest(
{ query: GET_JOBS, variables: { jobIds: [jobId] } },
customApplicationToken,
);
expect(ownWorkspaceResponse.body.errors).toBeUndefined();
expect(ownWorkspaceResponse.body.data.getJobs).toHaveLength(1);
const otherWorkspaceApplicationToken =
await createOtherWorkspaceApplicationToken();
const otherWorkspaceResponse = await makeMetadataAPIRequest(
{ query: GET_JOBS, variables: { jobIds: [jobId] } },
otherWorkspaceApplicationToken,
);
expect(otherWorkspaceResponse.body.errors).toBeUndefined();
expect(otherWorkspaceResponse.body.data.getJobs).toEqual([]);
}, 120_000);
it('rejects a batch that repeats a caller-supplied job id', async () => {
const jobId = `duplicate-${uuidv4()}`;
const response = await makeMetadataAPIRequest(
{
query: ENQUEUE_JOBS,
variables: {
input: {
logicFunctionUniversalIdentifier,
jobs: [
{ payload: {}, jobId },
{ payload: {}, jobId },
],
},
},
},
customApplicationToken,
);
expect(response.body.errors).toBeDefined();
expect(response.body.errors[0].message).toContain('unique');
});
it('rejects a batch that sets both payloads and jobs', async () => {
const response = await makeMetadataAPIRequest(
{
query: ENQUEUE_JOBS,
variables: {
input: {
logicFunctionUniversalIdentifier,
payloads: [{}],
jobs: [{ payload: {} }],
},
},
},
customApplicationToken,
);
expect(response.body.errors).toBeDefined();
expect(response.body.errors[0].message).toContain('not both');
});
it('rejects job options outside of their allowed range', async () => {
const response = await makeMetadataAPIRequest(
{
query: ENQUEUE_JOB,
variables: {
input: { logicFunctionUniversalIdentifier, retryLimit: 99 },
},
},
customApplicationToken,
);
expect(response.body.errors).toBeDefined();
expect(response.body.errors[0].message).toContain('retryLimit');
});
});