snapshot of https://github.com/twentyhq/twenty.git (main, 2026-09-17; blobs > 131072B omitted)
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

This commit is contained in:
upstream-mirror
2026-09-17 23:28:13 +05:00
commit e7da97194a
32688 changed files with 4208681 additions and 0 deletions

View File

@@ -0,0 +1,27 @@
import { gql } from 'graphql-tag';
import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util';
export const getWorkflowStepConnectedAccountHandle = async ({
connectedAccountId,
}: {
connectedAccountId: string;
}) => {
const response = await makeGraphqlAPIRequest({
query: gql`
query WorkflowStepConnectedAccountHandle($connectedAccountId: UUID!) {
workflowStepConnectedAccountHandle(
connectedAccountId: $connectedAccountId
) {
id
handle
provider
}
}
`,
variables: { connectedAccountId },
});
expect(response.body.errors).toBeUndefined();
return response.body.data.workflowStepConnectedAccountHandle;
};

View File

@@ -0,0 +1,34 @@
import { workflowGraphqlRequest } from 'test/integration/graphql/suites/workflow/utils/workflow-graphql-request.util';
const POLL_ATTEMPTS = 20;
const POLL_INTERVAL_MS = 250;
export const pollWorkflowGraphqlRequest = async <TData, TResult>({
query,
variables,
extract,
until,
}: {
query: string;
variables?: object;
extract: (data: TData | undefined) => TResult;
until: (result: TResult) => boolean;
}): Promise<TResult> => {
let result: TResult = extract(undefined);
for (let attempt = 0; attempt < POLL_ATTEMPTS; attempt++) {
const response = await workflowGraphqlRequest(query, variables);
expect(response.body.errors).toBeUndefined();
result = extract(response.body.data as TData | undefined);
if (until(result)) {
return result;
}
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
}
return result;
};

View File

@@ -0,0 +1,207 @@
import gql from 'graphql-tag';
import request from 'supertest';
import { updateWorkflowVersionTrigger } from 'test/integration/graphql/suites/workflow/utils/update-workflow-version-trigger.util';
import {
destroyWorkflowRun,
waitForWorkflowCompletion,
type WorkflowRunStatusType,
} from 'test/integration/graphql/suites/workflow/utils/workflow-run-test.util';
import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util';
import { SEED_APPLE_WORKSPACE_ID } from 'src/engine/workspace-manager/dev-seeder/core/constants/seeder-workspaces.constant';
type WorkflowVersionStep = {
id: string;
type: string;
settings: { input: Record<string, unknown> };
};
export type WebhookTriggeredActionStepRun = {
status?: WorkflowRunStatusType;
stepStatus?: string;
stepResult?: Record<string, unknown>;
stepError?: string;
};
export const runWebhookTriggeredActionStep = async ({
name,
stepType,
input,
}: {
name: string;
stepType: 'SEND_EMAIL' | 'DRAFT_EMAIL';
input: Record<string, unknown>;
}): Promise<WebhookTriggeredActionStepRun> => {
const createWorkflowResponse = await makeGraphqlAPIRequest({
query: gql`
mutation CreateWorkflow($name: String!) {
createWorkflow(data: { name: $name }) {
id
}
}
`,
variables: { name },
});
expect(createWorkflowResponse.body.errors).toBeUndefined();
const workflowId = createWorkflowResponse.body.data.createWorkflow.id;
let workflowRunId: string | undefined;
try {
const workflowVersionsResponse = await makeGraphqlAPIRequest({
query: gql`
query FindDraftWorkflowVersion($workflowId: UUID!) {
workflowVersions(
filter: {
workflowId: { eq: $workflowId }
status: { in: ["DRAFT"] }
}
) {
edges {
node {
id
}
}
}
}
`,
variables: { workflowId },
});
expect(workflowVersionsResponse.body.errors).toBeUndefined();
const workflowVersionId =
workflowVersionsResponse.body.data.workflowVersions.edges[0].node.id;
await updateWorkflowVersionTrigger({
workflowVersionId,
trigger: {
name: 'Webhook Trigger',
type: 'WEBHOOK',
settings: {
outputSchema: {},
httpMethod: 'GET',
authentication: null,
},
nextStepIds: [],
position: { x: 0, y: 0 },
},
});
const createStepResponse = await makeGraphqlAPIRequest({
query: gql`
mutation CreateWorkflowVersionStep(
$input: CreateWorkflowVersionStepInput!
) {
createWorkflowVersionStep(input: $input) {
stepsDiff
}
}
`,
variables: {
input: {
workflowVersionId,
stepType,
parentStepId: 'trigger',
position: { x: 200, y: 0 },
},
},
});
expect(createStepResponse.body.errors).toBeUndefined();
const stepsResponse = await makeGraphqlAPIRequest({
query: gql`
query FindWorkflowVersionSteps($workflowVersionId: UUID!) {
workflowVersion(filter: { id: { eq: $workflowVersionId } }) {
steps
}
}
`,
variables: { workflowVersionId },
});
expect(stepsResponse.body.errors).toBeUndefined();
const step: WorkflowVersionStep =
stepsResponse.body.data.workflowVersion.steps.find(
(workflowVersionStep: WorkflowVersionStep) =>
workflowVersionStep.type === stepType,
);
expect(step).toBeDefined();
const updateStepResponse = await makeGraphqlAPIRequest({
query: gql`
mutation UpdateWorkflowVersionStep(
$input: UpdateWorkflowVersionStepInput!
) {
updateWorkflowVersionStep(input: $input) {
id
}
}
`,
variables: {
input: {
workflowVersionId,
step: {
...step,
settings: {
...step.settings,
input: { ...step.settings.input, ...input },
},
},
},
},
});
expect(updateStepResponse.body.errors).toBeUndefined();
const activateResponse = await makeGraphqlAPIRequest({
query: gql`
mutation ActivateWorkflowVersion($workflowVersionId: UUID!) {
activateWorkflowVersion(workflowVersionId: $workflowVersionId)
}
`,
variables: { workflowVersionId },
});
expect(activateResponse.body.errors).toBeUndefined();
const webhookResponse = await request(`http://localhost:${APP_PORT}`).get(
`/webhooks/workflows/${SEED_APPLE_WORKSPACE_ID}/${workflowId}`,
);
expect(webhookResponse.body.success).toBe(true);
workflowRunId = webhookResponse.body.workflowRunId;
const workflowRun = await waitForWorkflowCompletion(workflowRunId!);
const stepInfo = workflowRun?.state?.stepInfos?.[step.id];
return {
status: workflowRun?.status,
stepStatus: stepInfo?.status,
stepResult: stepInfo?.result,
stepError: stepInfo?.error,
};
} finally {
if (workflowRunId !== undefined) {
await destroyWorkflowRun(workflowRunId);
}
await makeGraphqlAPIRequest({
query: gql`
mutation DestroyWorkflow($id: ID!) {
destroyWorkflow(id: $id) {
id
}
}
`,
variables: { id: workflowId },
});
}
};

View File

@@ -0,0 +1,234 @@
import gql from 'graphql-tag';
import { updateWorkflowVersionTrigger } from 'test/integration/graphql/suites/workflow/utils/update-workflow-version-trigger.util';
import { isDefined } from 'twenty-shared/utils';
import {
destroyWorkflowRun,
runWorkflowVersion,
waitForWorkflowCompletion,
type WorkflowRunStatusType,
} from 'test/integration/graphql/suites/workflow/utils/workflow-run-test.util';
import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util';
type WorkflowActionStepType =
| 'SEND_EMAIL'
| 'DRAFT_EMAIL'
| 'CREATE_CALENDAR_EVENT';
type WorkflowVersionStep = {
id: string;
type: string;
settings: { input: Record<string, unknown> };
};
export type WorkflowActionStepRun = {
status?: WorkflowRunStatusType;
stepStatus?: string;
stepResult?: Record<string, unknown>;
stepError?: string;
};
const createWorkflow = async (name: string): Promise<string> => {
const response = await makeGraphqlAPIRequest({
query: gql`
mutation CreateWorkflow($name: String!) {
createWorkflow(data: { name: $name }) {
id
}
}
`,
variables: { name },
});
expect(response.body.errors).toBeUndefined();
return response.body.data.createWorkflow.id;
};
const destroyWorkflow = async (workflowId: string): Promise<void> => {
await makeGraphqlAPIRequest({
query: gql`
mutation DestroyWorkflow($id: ID!) {
destroyWorkflow(id: $id) {
id
}
}
`,
variables: { id: workflowId },
});
};
const findDraftWorkflowVersionId = async (
workflowId: string,
): Promise<string> => {
const response = await makeGraphqlAPIRequest({
query: gql`
query FindDraftWorkflowVersion($workflowId: UUID!) {
workflowVersions(
filter: { workflowId: { eq: $workflowId }, status: { in: ["DRAFT"] } }
) {
edges {
node {
id
}
}
}
}
`,
variables: { workflowId },
});
expect(response.body.errors).toBeUndefined();
return response.body.data.workflowVersions.edges[0].node.id;
};
const createWorkflowVersionStep = async ({
workflowVersionId,
stepType,
}: {
workflowVersionId: string;
stepType: WorkflowActionStepType;
}): Promise<void> => {
const response = await makeGraphqlAPIRequest({
query: gql`
mutation CreateWorkflowVersionStep(
$input: CreateWorkflowVersionStepInput!
) {
createWorkflowVersionStep(input: $input) {
stepsDiff
}
}
`,
variables: {
input: {
workflowVersionId,
stepType,
parentStepId: 'trigger',
position: { x: 200, y: 0 },
},
},
});
expect(response.body.errors).toBeUndefined();
};
const findWorkflowVersionStep = async ({
workflowVersionId,
stepType,
}: {
workflowVersionId: string;
stepType: WorkflowActionStepType;
}): Promise<WorkflowVersionStep> => {
const response = await makeGraphqlAPIRequest({
query: gql`
query FindWorkflowVersionSteps($workflowVersionId: UUID!) {
workflowVersion(filter: { id: { eq: $workflowVersionId } }) {
steps
}
}
`,
variables: { workflowVersionId },
});
expect(response.body.errors).toBeUndefined();
const step = response.body.data.workflowVersion.steps.find(
(workflowVersionStep: WorkflowVersionStep) =>
workflowVersionStep.type === stepType,
);
expect(step).toBeDefined();
return step;
};
const updateWorkflowVersionStepInput = async ({
workflowVersionId,
step,
input,
}: {
workflowVersionId: string;
step: WorkflowVersionStep;
input: Record<string, unknown>;
}): Promise<void> => {
const response = await makeGraphqlAPIRequest({
query: gql`
mutation UpdateWorkflowVersionStep(
$input: UpdateWorkflowVersionStepInput!
) {
updateWorkflowVersionStep(input: $input) {
id
}
}
`,
variables: {
input: {
workflowVersionId,
step: {
...step,
settings: {
...step.settings,
input: { ...step.settings.input, ...input },
},
},
},
},
});
expect(response.body.errors).toBeUndefined();
};
export const runWorkflowActionStep = async ({
name,
stepType,
input,
payload,
}: {
name: string;
stepType: WorkflowActionStepType;
input: Record<string, unknown>;
payload?: object;
}): Promise<WorkflowActionStepRun> => {
const workflowId = await createWorkflow(name);
let workflowRunId: string | undefined;
try {
const workflowVersionId = await findDraftWorkflowVersionId(workflowId);
await updateWorkflowVersionTrigger({
workflowVersionId,
trigger: {
name: 'Manual Trigger',
type: 'MANUAL',
settings: { outputSchema: {} },
nextStepIds: [],
position: { x: 0, y: 0 },
},
});
await createWorkflowVersionStep({ workflowVersionId, stepType });
const step = await findWorkflowVersionStep({ workflowVersionId, stepType });
await updateWorkflowVersionStepInput({ workflowVersionId, step, input });
workflowRunId = await runWorkflowVersion({ workflowVersionId, payload });
const workflowRun = await waitForWorkflowCompletion(workflowRunId);
const stepInfo = workflowRun?.state?.stepInfos?.[step.id];
return {
status: workflowRun?.status,
stepStatus: stepInfo?.status,
stepResult: stepInfo?.result,
stepError: stepInfo?.error,
};
} finally {
if (isDefined(workflowRunId)) {
await destroyWorkflowRun(workflowRunId);
}
await destroyWorkflow(workflowId);
}
};

View File

@@ -0,0 +1,42 @@
import request from 'supertest';
const client = request(`http://localhost:${APP_PORT}`);
export const updateWorkflowVersionTrigger = async ({
workflowVersionId,
trigger,
}: {
workflowVersionId: string;
trigger: object;
}) => {
const response = await client
.post('/graphql')
.set('Authorization', `Bearer ${APPLE_JANE_ADMIN_ACCESS_TOKEN}`)
.send({
query: `
mutation UpdateWorkflowVersionTrigger(
$input: UpdateWorkflowVersionTriggerInput!
) {
updateWorkflowVersionTrigger(input: $input) {
trigger
}
}
`,
variables: {
input: {
workflowVersionId,
trigger,
},
},
});
if (response.body.errors) {
throw new Error(
`Failed to update workflow version trigger: ${JSON.stringify(
response.body.errors,
)}`,
);
}
return response;
};

View File

@@ -0,0 +1,9 @@
import request from 'supertest';
const client = request(`http://localhost:${APP_PORT}`);
export const workflowGraphqlRequest = (query: string, variables?: object) =>
client
.post('/graphql')
.set('Authorization', `Bearer ${APPLE_JANE_ADMIN_ACCESS_TOKEN}`)
.send({ query, variables });

View File

@@ -0,0 +1,195 @@
import request from 'supertest';
import { WORKFLOW_RUN_GQL_FIELDS } from 'test/integration/constants/workflow-gql-fields.constants';
const client = request(`http://localhost:${APP_PORT}`);
export type WorkflowRunStatusType =
| 'NOT_STARTED'
| 'RUNNING'
| 'COMPLETED'
| 'FAILED'
| 'ENQUEUED'
| 'STOPPING'
| 'STOPPED';
export type WorkflowRunState = {
stepInfos?: Record<
string,
{
status: string;
result?: Record<string, unknown>;
error?: string;
history?: Array<{ status: string; error?: string }>;
}
>;
flow?: {
trigger?: {
type: string;
nextStepIds: string[];
};
steps?: Array<{
id: string;
type: string;
name: string;
}>;
};
};
export type WorkflowRunResponse = {
id: string;
status: WorkflowRunStatusType;
state: WorkflowRunState;
workflowVersionId: string;
};
export const getWorkflowRun = async (
workflowRunId: string,
): Promise<WorkflowRunResponse | null> => {
const response = await client
.post('/graphql')
.set('Authorization', `Bearer ${APPLE_JANE_ADMIN_ACCESS_TOKEN}`)
.send({
query: `
query FindWorkflowRun($id: UUID!) {
workflowRun(filter: { id: { eq: $id } }) {
${WORKFLOW_RUN_GQL_FIELDS}
}
}
`,
variables: { id: workflowRunId },
});
if (response.body.errors || !response.body.data?.workflowRun) {
return null;
}
return response.body.data.workflowRun;
};
export const runWorkflowVersion = async ({
workflowVersionId,
payload,
}: {
workflowVersionId: string;
payload?: object;
}): Promise<string> => {
const response = await client
.post('/graphql')
.set('Authorization', `Bearer ${APPLE_JANE_ADMIN_ACCESS_TOKEN}`)
.send({
query: `
mutation RunWorkflowVersion($input: RunWorkflowVersionInput!) {
runWorkflowVersion(input: $input) {
workflowRunId
}
}
`,
variables: {
input: {
workflowVersionId,
payload,
},
},
});
if (response.body.errors || !response.body.data?.runWorkflowVersion) {
throw new Error(
`Failed to run workflow version: ${JSON.stringify(response.body.errors)}`,
);
}
return response.body.data.runWorkflowVersion.workflowRunId;
};
export const destroyWorkflowRun = async (
workflowRunId: string,
): Promise<void> => {
await client
.post('/graphql')
.set('Authorization', `Bearer ${APPLE_JANE_ADMIN_ACCESS_TOKEN}`)
.send({
query: `
mutation DestroyWorkflowRun($id: ID!) {
destroyWorkflowRun(id: $id) {
id
}
}
`,
variables: { id: workflowRunId },
});
};
const PENDING_WORKFLOW_RUN_STATUSES: WorkflowRunStatusType[] = [
'NOT_STARTED',
'ENQUEUED',
'RUNNING',
'STOPPING',
];
export const waitForWorkflowCompletion = async (
workflowRunId: string,
maxAttempts = 30,
intervalMs = 500,
): Promise<WorkflowRunResponse | null> => {
let workflowRun = await getWorkflowRun(workflowRunId);
let attempts = 0;
while (
workflowRun !== null &&
PENDING_WORKFLOW_RUN_STATUSES.includes(workflowRun.status) &&
attempts < maxAttempts
) {
await new Promise((resolve) => setTimeout(resolve, intervalMs));
workflowRun = await getWorkflowRun(workflowRunId);
attempts++;
}
return workflowRun;
};
export const waitForWorkflowRunStepStatus = async (
workflowRunId: string,
stepId: string,
expectedStatus: string,
maxAttempts = 30,
intervalMs = 500,
): Promise<WorkflowRunResponse | null> => {
let workflowRun = await getWorkflowRun(workflowRunId);
let attempts = 0;
while (
attempts < maxAttempts &&
workflowRun?.state?.stepInfos?.[stepId]?.status !== expectedStatus &&
(workflowRun === null ||
PENDING_WORKFLOW_RUN_STATUSES.includes(workflowRun.status))
) {
await new Promise((resolve) => setTimeout(resolve, intervalMs));
workflowRun = await getWorkflowRun(workflowRunId);
attempts++;
}
return workflowRun;
};
export const waitForWorkflowRunStatus = async (
workflowRunId: string,
expectedStatus: WorkflowRunStatusType,
maxAttempts = 30,
intervalMs = 500,
): Promise<WorkflowRunResponse | null> => {
let workflowRun = await getWorkflowRun(workflowRunId);
let attempts = 0;
while (
attempts < maxAttempts &&
workflowRun?.status !== expectedStatus &&
(workflowRun === null ||
PENDING_WORKFLOW_RUN_STATUSES.includes(workflowRun.status))
) {
await new Promise((resolve) => setTimeout(resolve, intervalMs));
workflowRun = await getWorkflowRun(workflowRunId);
attempts++;
}
return workflowRun;
};