跳到正文
cccskills
免费在 GitHub 公开

syncable-entity-testing

Create integration tests for syncable metadata entities in Twenty itself. Cover invalid inputs, validation errors, and successful data operations, using reusable GraphQL request helpers, error snapshots, and cleanup routines to organize the test suite.

中文简介正在准备中。当前显示英语简介。

原文查看原文说明

Contributing to the Twenty codebase itself (twentyhq/twenty server internals), not for building apps on top of Twenty. Create comprehensive integration tests for syncable entities in Twenty. Use when writing integration tests for metadata entities, covering validator exceptions, input transpilation errors, and CRUD operations. Tests are MANDATORY for all syncable entities.

查看安装方法

适用场景

  • Add integration tests for a new syncable entity
  • Test required fields, duplicates, and invalid references
  • Verify successful creation, updates, and deletion
  • Build reusable GraphQL test helpers
  • Record expected errors with snapshot tests

包含的文件(1)

  • SKILL.md13.4 KB

SKILL.md(原文)

安装前可以查看交给 AI 代理的指令内容。

Syncable Entity: Integration Testing (Step 6/6 - MANDATORY)

Purpose: Create comprehensive test suite covering all validation scenarios, input transpilation exceptions, and successful use cases.

When to use: After completing Steps 1-5. Integration tests are REQUIRED for all syncable entities.


Quick Start

Tests must cover:

  1. Failing scenarios - All validator exceptions and input transpilation errors
  2. Successful scenarios - All CRUD operations and edge cases
  3. Test utilities - Reusable query factories and helper functions

Test pattern: Two-file pattern (query factory + wrapper) for each operation.


Step 1: Create Test Utilities

Pattern: Query Factory

File: test/integration/metadata/suites/my-entity/utils/create-my-entity-query-factory.util.ts

import gql from 'graphql-tag';
import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type';
import { type CreateMyEntityInput } from 'src/engine/metadata-modules/my-entity/dtos/create-my-entity.input';

export type CreateMyEntityFactoryInput = CreateMyEntityInput;

const DEFAULT_MY_ENTITY_GQL_FIELDS = `
  id
  name
  label
  description
  isCustom
  createdAt
  updatedAt
`;

export const createMyEntityQueryFactory = ({
  input,
  gqlFields = DEFAULT_MY_ENTITY_GQL_FIELDS,
}: PerformMetadataQueryParams<CreateMyEntityFactoryInput>) => ({
  query: gql`
    mutation CreateMyEntity($input: CreateMyEntityInput!) {
      createMyEntity(input: $input) {
        ${gqlFields}
      }
    }
  `,
  variables: {
    input,
  },
});

Pattern: Wrapper Utility

File: test/integration/metadata/suites/my-entity/utils/create-my-entity.util.ts

import {
  type CreateMyEntityFactoryInput,
  createMyEntityQueryFactory,
} from 'test/integration/metadata/suites/my-entity/utils/create-my-entity-query-factory.util';
import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util';
import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type';
import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type';
import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util';
import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util';
import { type MyEntityDto } from 'src/engine/metadata-modules/my-entity/dtos/my-entity.dto';

export const createMyEntity = async ({
  input,
  gqlFields,
  expectToFail = false,
  token,
}: PerformMetadataQueryParams<CreateMyEntityFactoryInput>): CommonResponseBody<{
  createMyEntity: MyEntityDto;
}> => {
  const graphqlOperation = createMyEntityQueryFactory({
    input,
    gqlFields,
  });

  const response = await makeMetadataAPIRequest(graphqlOperation, token);

  if (expectToFail === true) {
    warnIfNoErrorButExpectedToFail({
      response,
      errorMessage: 'My entity creation should have failed but did not',
    });
  }

  if (expectToFail === false) {
    warnIfErrorButNotExpectedToFail({
      response,
      errorMessage: 'My entity creation has failed but should not',
    });
  }

  return { data: response.body.data, errors: response.body.errors };
};

Required utilities (follow same pattern):

  • update-my-entity-query-factory.util.ts + update-my-entity.util.ts
  • delete-my-entity-query-factory.util.ts + delete-my-entity.util.ts

Step 2: Failing Creation Tests

File: test/integration/metadata/suites/my-entity/failing-my-entity-creation.integration-spec.ts

import { expectOneNotInternalServerErrorSnapshot } from 'test/integration/graphql/utils/expect-one-not-internal-server-error-snapshot.util';
import { createMyEntity } from 'test/integration/metadata/suites/my-entity/utils/create-my-entity.util';
import { deleteMyEntity } from 'test/integration/metadata/suites/my-entity/utils/delete-my-entity.util';
import {
  eachTestingContextFilter,
  type EachTestingContext,
} from 'twenty-shared/testing';
import { isDefined } from 'twenty-shared/utils';
import { type CreateMyEntityInput } from 'src/engine/metadata-modules/my-entity/dtos/create-my-entity.input';

type TestContext = {
  input: CreateMyEntityInput;
};

type GlobalTestContext = {
  existingEntityLabel: string;
  existingEntityName: string;
};

const globalTestContext: GlobalTestContext = {
  existingEntityLabel: 'Existing Test Entity',
  existingEntityName: 'existingTestEntity',
};

type CreateMyEntityTestingContext = EachTestingContext<TestContext>[];

describe('My entity creation should fail', () => {
  let existingEntityId: string | undefined;

  beforeAll(async () => {
    // Setup: Create entity for uniqueness tests
    const { data } = await createMyEntity({
      expectToFail: false,
      input: {
        name: globalTestContext.existingEntityName,
        label: globalTestContext.existingEntityLabel,
      },
    });

    existingEntityId = data.createMyEntity.id;
  });

  afterAll(async () => {
    // Cleanup
    if (isDefined(existingEntityId)) {
      await deleteMyEntity({
        expectToFail: false,
        input: { id: existingEntityId },
      });
    }
  });

  const failingMyEntityCreationTestCases: CreateMyEntityTestingContext = [
    // Input transpilation validation
    {
      title: 'when name is missing',
      context: {
        input: {
          label: 'Entity Missing Name',
        } as CreateMyEntityInput,
      },
    },
    {
      title: 'when label is missing',
      context: {
        input: {
          name: 'entityMissingLabel',
        } as CreateMyEntityInput,
      },
    },
    {
      title: 'when name is empty string',
      context: {
        input: {
          name: '',
          label: 'Empty Name Entity',
        },
      },
    },

    // Validator business logic
    {
      title: 'when name already exists (uniqueness)',
      context: {
        input: {
          name: globalTestContext.existingEntityName,
          label: 'Duplicate Name Entity',
        },
      },
    },
    {
      title: 'when trying to create standard entity',
      context: {
        input: {
          name: 'myEntity',
          label: 'Standard Entity',
          isCustom: false,
        } as CreateMyEntityInput,
      },
    },

    // Foreign key validation
    {
      title: 'when parentEntityId does not exist',
      context: {
        input: {
          name: 'invalidParentEntity',
          label: 'Invalid Parent Entity',
          parentEntityId: '00000000-0000-0000-0000-000000000000',
        },
      },
    },
  ];

  it.each(eachTestingContextFilter(failingMyEntityCreationTestCases))(
    '$title',
    async ({ context }) => {
      const { errors } = await createMyEntity({
        expectToFail: true,
        input: context.input,
      });

      expectOneNotInternalServerErrorSnapshot({
        errors,
      });
    },
  );
});

Test coverage requirements:

  • ✅ Missing required fields
  • ✅ Empty strings
  • ✅ Invalid format
  • ✅ Uniqueness violations
  • ✅ Standard entity protection
  • ✅ Foreign key validation

Step 3: Successful Creation Tests

File: test/integration/metadata/suites/my-entity/successful-my-entity-creation.integration-spec.ts

import { createMyEntity } from 'test/integration/metadata/suites/my-entity/utils/create-my-entity.util';
import { deleteMyEntity } from 'test/integration/metadata/suites/my-entity/utils/delete-my-entity.util';
import { type CreateMyEntityInput } from 'src/engine/metadata-modules/my-entity/dtos/create-my-entity.input';

describe('My entity creation should succeed', () => {
  let createdEntityId: string;

  afterEach(async () => {
    if (createdEntityId) {
      await deleteMyEntity({
        expectToFail: false,
        input: { id: createdEntityId },
      });
    }
  });

  it('should create entity with minimal required input', async () => {
    const { data } = await createMyEntity({
      expectToFail: false,
      input: {
        name: 'minimalEntity',
        label: 'Minimal Entity',
      },
    });

    createdEntityId = data?.createMyEntity?.id;

    expect(data.createMyEntity).toMatchObject({
      id: expect.any(String),
      name: 'minimalEntity',
      label: 'Minimal Entity',
      description: null,
      isCustom: true,
      createdAt: expect.any(String),
      updatedAt: expect.any(String),
    });
  });

  it('should create entity with all optional fields', async () => {
    const input = {
      name: 'fullEntity',
      label: 'Full Entity',
      description: 'Entity with all fields specified',
    } as const satisfies CreateMyEntityInput;

    const { data } = await createMyEntity({
      expectToFail: false,
      input,
    });

    createdEntityId = data?.createMyEntity?.id;

    expect(data.createMyEntity).toMatchObject({
      id: expect.any(String),
      name: 'fullEntity',
      label: 'Full Entity',
      description: 'Entity with all fields specified',
      isCustom: true,
    });
  });

  it('should sanitize input by trimming whitespace', async () => {
    const { data } = await createMyEntity({
      expectToFail: false,
      input: {
        name: '  entityWithSpaces  ',
        label: '  Entity With Spaces  ',
        description: '  Description with spaces  ',
      },
    });

    createdEntityId = data?.createMyEntity?.id;

    expect(data.createMyEntity).toMatchObject({
      id: expect.any(String),
      name: 'entityWithSpaces',
      label: 'Entity With Spaces',
      description: 'Description with spaces',
    });
  });

  it('should handle long text content', async () => {
    const longDescription = 'A'.repeat(1000);

    const { data } = await createMyEntity({
      expectToFail: false,
      input: {
        name: 'longDescEntity',
        label: 'Long Description Entity',
        description: longDescription,
      },
    });

    createdEntityId = data?.createMyEntity?.id;

    expect(data.createMyEntity).toMatchObject({
      id: expect.any(String),
      description: longDescription,
    });
  });
});

Test coverage requirements:

  • ✅ Minimal required input
  • ✅ All optional fields
  • ✅ Input sanitization
  • ✅ Long text content
  • ✅ Special characters

Step 4: Update and Delete Tests

Create similar test files for update and delete operations:

Required files:

  • failing-my-entity-update.integration-spec.ts
  • successful-my-entity-update.integration-spec.ts
  • failing-my-entity-deletion.integration-spec.ts
  • successful-my-entity-deletion.integration-spec.ts

Testing Best Practices

Pattern: Cleanup

afterEach(async () => {
  if (createdEntityId) {
    await deleteMyEntity({
      expectToFail: false,
      input: { id: createdEntityId },
    });
  }
});

Pattern: Type-Safe Inputs

const input = {
  name: 'myEntity',
  label: 'My Entity',
} as const satisfies CreateMyEntityInput;

Pattern: Snapshot Testing

expectOneNotInternalServerErrorSnapshot({
  errors,
});

Running Tests

# Run all entity tests
npx jest test/integration/metadata/suites/my-entity --config=packages/twenty-server/jest.config.mjs

# Run specific test file
npx jest test/integration/metadata/suites/my-entity/failing-my-entity-creation.integration-spec.ts --config=packages/twenty-server/jest.config.mjs

# Update snapshots
npx jest test/integration/metadata/suites/my-entity --updateSnapshot --config=packages/twenty-server/jest.config.mjs

Complete Test Checklist

Test Utilities

  • create-my-entity-query-factory.util.ts created
  • create-my-entity.util.ts created
  • update-my-entity-query-factory.util.ts created
  • update-my-entity.util.ts created
  • delete-my-entity-query-factory.util.ts created
  • delete-my-entity.util.ts created

Failing Tests Coverage

  • Missing required fields
  • Empty string validation
  • Uniqueness violations
  • Standard entity protection
  • Foreign key validation
  • JSONB property validation (if applicable)

Successful Tests Coverage

  • Create with minimal input
  • Create with all optional fields
  • Input sanitization (whitespace)
  • Long text content
  • Update single field
  • Update multiple fields
  • Successful deletion

Snapshot Tests

  • All failing tests use expectOneNotInternalServerErrorSnapshot
  • Snapshots committed to __snapshots__/ directory

Success Criteria

Your integration tests are complete when:

✅ All test utilities created (minimum 6 files) ✅ Failing creation tests cover all validators ✅ Failing update tests cover business rules ✅ Failing deletion tests cover protection rules ✅ Successful tests cover all use cases ✅ All snapshots generated and committed ✅ All tests pass consistently ✅ Test coverage meets requirements (>80%)


Final Step

✅ Step 6 Complete! → Your syncable entity is fully tested and production-ready!

Congratulations! You've successfully created a new syncable entity in Twenty's workspace migration system.

For complete workflow, see @creating-syncable-entity rule.

评价

还没有评价。欢迎分享你的使用感受。

评论

无需登录即可发表。欢迎分享使用感受、提问,或说明在哪些环境下无法运行。

还没有评论。来写第一条吧。

提交后会立即公开。请勿填写邮箱、电话等个人信息。URL 最多 2 个。

0 / 1000 字

此表单受 reCAPTCHA 保护,适用 Google 的隐私权政策和服务条款。

同一仓库的技能

Create a new app that extends a running Twenty instance. Guide project naming and instance selection, then use the scaffolder to install dependencies, authenticate, initialize Git, and complete the initial sync and installation.

中文简介正在准备中。当前显示英语简介。

  • Start a new Twenty app project
  • Scaffold a CRM extension or integration
  • Create an app connected to an existing Twenty instance
twentyhq/twenty5.9万2026年10月19日 更新

Extend an existing Twenty app with data fields, layouts, custom screens, automated logic, workflows, and permissions. The skill guides code organization, entity creation, type checking, linting, and syncing definitions to the active remote.

中文简介正在准备中。当前显示英语简介。

  • Add custom fields and objects to an existing Twenty app
  • Build custom screens and standalone pages
  • Run logic when records are created
twentyhq/twenty5.9万2026年10月19日 更新

Operate an existing Twenty app by syncing changes, managing server connections, building and deploying releases, and inspecting function logs. It also guides troubleshooting and CI/CD, the workflows used to automate testing and deployment.

中文简介正在准备中。当前显示英语简介。

  • Sync app changes to a Twenty server
  • Prepare and deploy an app update
  • Investigate failed builds or deployments
twentyhq/twenty5.9万2026年10月19日 更新

Prepare an existing Twenty app for npm or marketplace publication by reviewing its README, listing details, package metadata, logos, and screenshots. The workflow includes checking publication settings, validating the build, and running the publishing command.

中文简介正在准备中。当前显示英语简介。

  • Prepare a Twenty app marketplace listing
  • Write or review README and About text
  • Check logos, screenshots, and public assets
twentyhq/twenty5.9万2026年10月19日 更新

qa-scout

免费

Check a Twenty pull request through real browser scenarios derived from its changes. Verify database effects and server logs alongside visible results, then produce screenshots, a structured verdict, and a report of any failures or uncertainty.

中文简介正在准备中。当前显示英语简介。

  • Validate user flows before or after merging a Twenty pull request
  • Check database writes and timeline activity after saving records
  • Investigate backend errors hidden behind a successful screen
twentyhq/twenty5.9万2026年10月19日 更新

Implement validation and migration action builders for syncable entities, data types handled by Twenty's synchronization system. This skill covers required fields, uniqueness, reference checks, and orchestration wiring, with validators that return errors without throwing exceptions or changing data.

中文简介正在准备中。当前显示英语简介。

  • Add required-field and duplicate checks
  • Validate references to parent entities
  • Build create, update, and delete migration actions
twentyhq/twenty5.9万2026年10月19日 更新

查看 twentyhq 的全部技能

报告此技能的问题