From 6e1e98f4ab9f1db7da25e8316aae6ab9380eee83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Malfait?= Date: Mon, 20 Jul 2026 06:39:13 +0200 Subject: [PATCH] fix(server): shard the server-test unit job to stop the intermittent crash (#23009) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Problem `server-test` fails intermittently: exit 1 with **no `FAIL` line and no `Test Suites:/Tests:` summary** — the jest run is aborted mid-way, before the reporter's `onRunComplete`. ## Root cause The `test` target runs the entire unit suite (~6,600 tests) in **one in-band jest process on a single runner VM** (`nx.json` sets `maxWorkers: 1` for the `ci` configuration). A few minutes in, that process is killed by an **external `SIGKILL`** — confirmed *not* OOM (~15 GB free at kill time, no cgroup `oom_kill`) and *not* an in-process crash (a Node diagnostic report armed with `--report-on-fatalerror` + `--report-uncaught-exception` writes nothing). The whole-run kill is why it fails intermittently with no summary. `maxWorkers=2` on one VM still dies, so the threshold is **per-VM**, not per-process. ## Fix Shard the unit suite across VMs, the same way `server-integration-test` already does: - A `twenty-server` `test:ci` target runs jest directly (so `--shard` forwards) with `dependsOn: ["^build"]` so the workspace deps are built. - `server-test` becomes a 4-way matrix; each shard runs a quarter of the suite, well under the kill threshold. - `ci-server-status-check` already aggregates `server-test`, so required checks are unchanged. Also provides two mocks a completed run needs but the SIGKILL had been masking in `ApplicationRegistrationService.upsertFromCatalog` unit tests: the `MetricsService` provider and `applicationRegistrationRepository.createQueryBuilder`. --- .github/workflows/ci-server.yaml | 11 ++++++----- packages/twenty-server/project.json | 8 ++++++++ ...ication-registration-upsert-from-catalog.spec.ts | 13 +++++++++++++ 3 files changed, 27 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci-server.yaml b/.github/workflows/ci-server.yaml index af4add5fd1..b5da2d5c48 100644 --- a/.github/workflows/ci-server.yaml +++ b/.github/workflows/ci-server.yaml @@ -376,6 +376,10 @@ jobs: needs: server-build timeout-minutes: 30 runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + shard: [1, 2, 3, 4] steps: - name: Fetch custom Github Actions and base branch history uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 @@ -389,11 +393,8 @@ jobs: key: ${{ env.SERVER_BUILD_CACHE_KEY }} - name: Build twenty-shared run: npx nx build twenty-shared - - name: Server / Run Tests - uses: ./.github/actions/nx-affected - with: - tag: scope:backend - tasks: test + - name: Server / Run Tests (shard ${{ matrix.shard }}/4) + run: npx nx run twenty-server:test:ci --shard=${{ matrix.shard }}/4 server-integration-test: timeout-minutes: 30 diff --git a/packages/twenty-server/project.json b/packages/twenty-server/project.json index 2a3a651d1f..9cf6148549 100644 --- a/packages/twenty-server/project.json +++ b/packages/twenty-server/project.json @@ -36,6 +36,14 @@ } } }, + "test:ci": { + "executor": "nx:run-commands", + "dependsOn": ["^build"], + "options": { + "cwd": "packages/twenty-server", + "command": "jest --config ./jest.config.mjs --ci --runInBand" + } + }, "build:packageJson": { "executor": "@nx/js:tsc", "options": { diff --git a/packages/twenty-server/src/engine/core-modules/application/application-registration/__tests__/application-registration-upsert-from-catalog.spec.ts b/packages/twenty-server/src/engine/core-modules/application/application-registration/__tests__/application-registration-upsert-from-catalog.spec.ts index 1e17f4a44c..b827863096 100644 --- a/packages/twenty-server/src/engine/core-modules/application/application-registration/__tests__/application-registration-upsert-from-catalog.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/application/application-registration/__tests__/application-registration-upsert-from-catalog.spec.ts @@ -10,6 +10,7 @@ import { ApplicationEntity } from 'src/engine/core-modules/application/applicati import { CacheLockService } from 'src/engine/core-modules/cache-lock/cache-lock.service'; import { CoreEntityCacheService } from 'src/engine/core-entity-cache/services/core-entity-cache.service'; import { ServerFileStorageService } from 'src/engine/core-modules/file-storage/services/server-file-storage.service'; +import { MetricsService } from 'src/engine/core-modules/metrics/metrics.service'; import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; describe('ApplicationRegistrationService - upsertFromCatalog', () => { @@ -18,6 +19,7 @@ describe('ApplicationRegistrationService - upsertFromCatalog', () => { findOne: jest.Mock; save: jest.Mock; create: jest.Mock; + createQueryBuilder: jest.Mock; }; const catalogParams = { @@ -45,6 +47,13 @@ describe('ApplicationRegistrationService - upsertFromCatalog', () => { findOne: jest.fn(), save: jest.fn(), create: jest.fn((entity) => entity), + createQueryBuilder: jest.fn(() => ({ + update: jest.fn().mockReturnThis(), + set: jest.fn().mockReturnThis(), + where: jest.fn().mockReturnThis(), + andWhere: jest.fn().mockReturnThis(), + execute: jest.fn().mockResolvedValue({ affected: 0 }), + })), }; const module: TestingModule = await Test.createTestingModule({ @@ -82,6 +91,10 @@ describe('ApplicationRegistrationService - upsertFromCatalog', () => { provide: CoreEntityCacheService, useValue: { invalidate: jest.fn() }, }, + { + provide: MetricsService, + useValue: { incrementCounterBy: jest.fn() }, + }, ], }).compile();