From 25d20731ac77c4c18b3884a550b4a85696e66cef Mon Sep 17 00:00:00 2001 From: Paul Rastoin <45004772+prastoin@users.noreply.github.com> Date: Wed, 29 Jul 2026 11:15:30 +0200 Subject: [PATCH] Include root cause errors in workspace migration runner exception message (#23416) closes https://github.com/twentyhq/core-team-issues/issues/2733 ```diff - "message": "Migration action 'create' for 'index' (universalIdentifier: 67c6811e-...) failed" + "message": "Migration action 'create' for 'index' (universalIdentifier: 67c6811e-...) failed: [workspaceSchema] could not create unique index "IDX_UNIQUE_85951922a2..." (pg code: 23505, detail: Key ("externalId")=(DUPLICATED-VALUE) is duplicated.)" ``` ## Problem When a workspace migration action fails, the `EXECUTION_FAILED` exception message only states which action failed: ``` Migration action 'create' for 'index' (universalIdentifier: 9e20a0f6-7a18-51c5-a422-5dc4dbd1d972) failed ``` The underlying errors (`metadata`, `workspaceSchema`, `actionTranspilation`) are attached to the exception instance but dropped by most surfaces: the SDK CLI only prints `errors[0].message`, server logs only log `error.message`, and the REST/GraphQL paths lose the postgres driver details. Debugging a failed app install (like the stale index universalIdentifier case in the issue) requires guessing. ## Change - New `formatWorkspaceMigrationRunnerExecutionErrors` util that builds a compact one-line summary of the underlying execution errors, including the postgres error code and `detail` for `QueryFailedError`, capped at 1500 chars. - The `EXECUTION_FAILED` exception message now appends that summary: ``` Migration action 'create' for 'index' (universalIdentifier: 9e20a0f6-...) failed: [workspaceSchema] relation "IDX_..." already exists (pg code: 42P07) ``` Since every surface (CLI, server logs, Sentry, REST, GraphQL) shows `error.message`, the root cause now propagates everywhere without touching those surfaces. - Since actions run inside a single transaction, when one branch fails with `25P02 current transaction is aborted` (collateral of the other branch's statement aborting the transaction), the summary keeps only the real root cause. A lone 25P02 error is still shown. ## Notes - The SDK's `getSyncErrorRecoveryHint` matching (`/migration action .* failed/`) still works with the suffixed format. - Commit-time failures from `DEFERRABLE INITIALLY DEFERRED` FK constraints still bypass this path (wrapped as `INTERNAL_SERVER_ERROR` with no action attribution) and are left as a follow-up. ## Tests - New spec for the formatter util (labels, pg code/detail, 25P02 demotion, truncation). - Extended `workspace-migration-runner.exception.spec.ts` with a root-cause message assertion. - Updated `format-upgrade-error-for-storage` snapshots (first line now carries the enriched message). --- _Generated by [Claude Code](https://claude.ai/code/session_01Mu8t74X14oYVkLrFBZHs2o)_ Review in cubic --- ...pgrade-error-for-storage.util.spec.ts.snap | 4 +- ...rkspace-migration-runner.exception.spec.ts | 22 ++++ .../workspace-migration-runner.exception.ts | 8 +- ...ation-runner-execution-errors.util.spec.ts | 114 ++++++++++++++++++ ...-migration-runner-execution-errors.util.ts | 81 +++++++++++++ ...ex-duplicate-data.integration-spec.ts.snap | 51 ++++++++ ...e-index-duplicate-data.integration-spec.ts | 105 ++++++++++++++++ 7 files changed, 382 insertions(+), 3 deletions(-) create mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/__tests__/format-workspace-migration-runner-execution-errors.util.spec.ts create mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/format-workspace-migration-runner-execution-errors.util.ts create mode 100644 packages/twenty-server/test/integration/metadata/suites/application/__snapshots__/failing-sync-application-unique-index-duplicate-data.integration-spec.ts.snap create mode 100644 packages/twenty-server/test/integration/metadata/suites/application/failing-sync-application-unique-index-duplicate-data.integration-spec.ts diff --git a/packages/twenty-server/src/engine/core-modules/upgrade/utils/__tests__/__snapshots__/format-upgrade-error-for-storage.util.spec.ts.snap b/packages/twenty-server/src/engine/core-modules/upgrade/utils/__tests__/__snapshots__/format-upgrade-error-for-storage.util.spec.ts.snap index 17aba7bc51..05a2541aa0 100644 --- a/packages/twenty-server/src/engine/core-modules/upgrade/utils/__tests__/__snapshots__/format-upgrade-error-for-storage.util.spec.ts.snap +++ b/packages/twenty-server/src/engine/core-modules/upgrade/utils/__tests__/__snapshots__/format-upgrade-error-for-storage.util.spec.ts.snap @@ -31,7 +31,7 @@ Report: { `; exports[`formatUpgradeErrorForStorage should format a WorkspaceMigrationRunnerException with EXECUTION_FAILED 1`] = ` -"[WorkspaceMigrationRunnerException] Migration action 'create' for 'objectMetadata' (universalIdentifier: test-object-uid) failed +"[WorkspaceMigrationRunnerException] Migration action 'create' for 'objectMetadata' (universalIdentifier: test-object-uid) failed: [metadata] column "label" cannot be null; [workspaceSchema] table already exists Code: EXECUTION_FAILED Action: create on objectMetadata Metadata error: @@ -54,7 +54,7 @@ exports[`formatUpgradeErrorForStorage should format a string value 1`] = `"raw s exports[`formatUpgradeErrorForStorage should format an undefined value 1`] = `"undefined"`; exports[`formatUpgradeErrorForStorage should surface driver details of a QueryFailedError nested in an EXECUTION_FAILED 1`] = ` -"[WorkspaceMigrationRunnerException] Migration action 'create' for 'pageLayoutWidget' (universalIdentifier: f473b435-e2d4-4928-8d90-1db0094389f7) failed +"[WorkspaceMigrationRunnerException] Migration action 'create' for 'pageLayoutWidget' (universalIdentifier: f473b435-e2d4-4928-8d90-1db0094389f7) failed: [metadata] duplicate key value violates unique constraint "IDX_PAGE_LAYOUT_WIDGET_UNIVERSAL_ID" (pg code: 23505, detail: Key (universalIdentifier)=(f473b435-e2d4-4928-8d90-1db0094389f7) already exists.) Code: EXECUTION_FAILED Action: create on pageLayoutWidget Metadata error: diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/exceptions/__tests__/workspace-migration-runner.exception.spec.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/exceptions/__tests__/workspace-migration-runner.exception.spec.ts index 9f6b7cb491..e0b7eace84 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/exceptions/__tests__/workspace-migration-runner.exception.spec.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/exceptions/__tests__/workspace-migration-runner.exception.spec.ts @@ -42,4 +42,26 @@ describe('WorkspaceMigrationRunnerException', () => { "Migration action 'delete' for 'pageLayout' (universalIdentifier: uid-page-layout) failed", ); }); + + it('includes the underlying execution errors in the message', () => { + const action = { + type: 'create', + metadataName: 'index', + flatEntity: { + universalIdentifier: '9e20a0f6-7a18-51c5-a422-5dc4dbd1d972', + }, + } as unknown as AllUniversalWorkspaceMigrationAction; + + const exception = new WorkspaceMigrationRunnerException({ + code: WorkspaceMigrationRunnerExceptionCode.EXECUTION_FAILED, + action, + errors: { + workspaceSchema: new Error('relation "IDX_abc" already exists'), + }, + }); + + expect(exception.message).toBe( + "Migration action 'create' for 'index' (universalIdentifier: 9e20a0f6-7a18-51c5-a422-5dc4dbd1d972) failed: [workspaceSchema] relation \"IDX_abc\" already exists", + ); + }); }); diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/exceptions/workspace-migration-runner.exception.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/exceptions/workspace-migration-runner.exception.ts index 8613f1e9c2..b2cf9a3d79 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/exceptions/workspace-migration-runner.exception.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/exceptions/workspace-migration-runner.exception.ts @@ -4,6 +4,7 @@ import { assertUnreachable, CustomError } from 'twenty-shared/utils'; import { type FlatEntityMapsExceptionContext } from 'src/engine/metadata-modules/flat-entity/exceptions/flat-entity-maps.exception'; import { type AllUniversalWorkspaceMigrationAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/workspace-migration-action-common'; +import { formatWorkspaceMigrationRunnerExecutionErrors } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/format-workspace-migration-runner-execution-errors.util'; export const WorkspaceMigrationRunnerExceptionCode = { INTERNAL_SERVER_ERROR: 'INTERNAL_SERVER_ERROR', @@ -88,9 +89,14 @@ export class WorkspaceMigrationRunnerException extends CustomError { args.action, ); const identifierClause = ` (universalIdentifier: ${universalIdentifier})`; + const executionErrorsSummary = + formatWorkspaceMigrationRunnerExecutionErrors(args.errors); + const causeClause = executionErrorsSummary + ? `: ${executionErrorsSummary}` + : ''; super( - `Migration action '${args.action.type}' for '${args.action.metadataName}'${identifierClause} failed`, + `Migration action '${args.action.type}' for '${args.action.metadataName}'${identifierClause} failed${causeClause}`, ); this.code = args.code; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/__tests__/format-workspace-migration-runner-execution-errors.util.spec.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/__tests__/format-workspace-migration-runner-execution-errors.util.spec.ts new file mode 100644 index 0000000000..002db1f29d --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/__tests__/format-workspace-migration-runner-execution-errors.util.spec.ts @@ -0,0 +1,114 @@ +import { QueryFailedError } from 'typeorm'; + +import { formatWorkspaceMigrationRunnerExecutionErrors } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/format-workspace-migration-runner-execution-errors.util'; + +const buildQueryFailedError = ({ + message, + code, + detail, +}: { + message: string; + code?: string; + detail?: string; +}): QueryFailedError => { + const driverError = new Error(message); + + Object.assign(driverError, { code, detail }); + + return new QueryFailedError('INSERT INTO "core"."index"', [], driverError); +}; + +describe('formatWorkspaceMigrationRunnerExecutionErrors', () => { + it('returns undefined when no error is set', () => { + expect(formatWorkspaceMigrationRunnerExecutionErrors({})).toBeUndefined(); + }); + + it('formats a plain error with its origin label', () => { + expect( + formatWorkspaceMigrationRunnerExecutionErrors({ + metadata: new Error('column "label" cannot be null'), + }), + ).toBe('[metadata] column "label" cannot be null'); + }); + + it('includes postgres code and detail of a QueryFailedError', () => { + expect( + formatWorkspaceMigrationRunnerExecutionErrors({ + workspaceSchema: buildQueryFailedError({ + message: 'relation "IDX_abc" already exists', + code: '42P07', + }), + }), + ).toBe( + '[workspaceSchema] relation "IDX_abc" already exists (pg code: 42P07)', + ); + + expect( + formatWorkspaceMigrationRunnerExecutionErrors({ + metadata: buildQueryFailedError({ + message: 'duplicate key value violates unique constraint "UQ_name"', + code: '23505', + detail: 'Key (name)=(foo) already exists.', + }), + }), + ).toBe( + '[metadata] duplicate key value violates unique constraint "UQ_name" (pg code: 23505, detail: Key (name)=(foo) already exists.)', + ); + }); + + it('joins multiple errors', () => { + expect( + formatWorkspaceMigrationRunnerExecutionErrors({ + metadata: new Error('column "label" cannot be null'), + workspaceSchema: new Error('table already exists'), + }), + ).toBe( + '[metadata] column "label" cannot be null; [workspaceSchema] table already exists', + ); + }); + + it('hides a 25P02 aborted-transaction error when a root cause is available', () => { + expect( + formatWorkspaceMigrationRunnerExecutionErrors({ + metadata: buildQueryFailedError({ + message: 'current transaction is aborted', + code: '25P02', + }), + workspaceSchema: buildQueryFailedError({ + message: 'relation "IDX_abc" already exists', + code: '42P07', + }), + }), + ).toBe( + '[workspaceSchema] relation "IDX_abc" already exists (pg code: 42P07)', + ); + }); + + it('keeps a 25P02 error when it is the only one', () => { + expect( + formatWorkspaceMigrationRunnerExecutionErrors({ + metadata: buildQueryFailedError({ + message: 'current transaction is aborted', + code: '25P02', + }), + }), + ).toBe('[metadata] current transaction is aborted (pg code: 25P02)'); + }); + + it('truncates an oversized summary to the cap, marker included', () => { + const summary = formatWorkspaceMigrationRunnerExecutionErrors({ + metadata: new Error('x'.repeat(5_000)), + }); + + expect(summary).toHaveLength(1_500); + expect(summary?.endsWith(' [truncated]')).toBe(true); + }); + + it('stringifies a non-Error rejection value', () => { + expect( + formatWorkspaceMigrationRunnerExecutionErrors({ + metadata: 'plain string rejection' as unknown as Error, + }), + ).toBe('[metadata] plain string rejection'); + }); +}); diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/format-workspace-migration-runner-execution-errors.util.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/format-workspace-migration-runner-execution-errors.util.ts new file mode 100644 index 0000000000..d09d72186f --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/format-workspace-migration-runner-execution-errors.util.ts @@ -0,0 +1,81 @@ +import { isDefined } from 'twenty-shared/utils'; +import { QueryFailedError } from 'typeorm'; + +import { type WorkspaceMigrationRunnerExecutionErrors } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/exceptions/workspace-migration-runner.exception'; + +const MAX_EXECUTION_ERRORS_SUMMARY_LENGTH = 1_500; + +const TRUNCATION_MARKER = ' [truncated]'; + +const POSTGRES_TRANSACTION_ABORTED_CODE = '25P02'; + +// Ordered by execution phase: transpilation runs before the metadata and +// workspace schema writes. +const EXECUTION_ERROR_LABELS = [ + 'actionTranspilation', + 'metadata', + 'workspaceSchema', +] as const satisfies readonly (keyof WorkspaceMigrationRunnerExecutionErrors)[]; + +type ExecutionErrorEntry = { + label: (typeof EXECUTION_ERROR_LABELS)[number]; + error: Error; +}; + +const getPostgresDriverError = ( + error: Error, +): { code?: string; detail?: string } | undefined => + error instanceof QueryFailedError + ? (error.driverError as { code?: string; detail?: string } | undefined) + : undefined; + +const isTransactionAbortedError = (error: Error): boolean => + getPostgresDriverError(error)?.code === POSTGRES_TRANSACTION_ABORTED_CODE; + +const formatSingleExecutionError = (error: Error): string => { + // Rejection reasons are typed as Error but nothing guarantees it at runtime. + const baseMessage = error instanceof Error ? error.message : String(error); + const driverError = getPostgresDriverError(error); + const driverErrorParts = [ + isDefined(driverError?.code) ? `pg code: ${driverError.code}` : null, + isDefined(driverError?.detail) ? `detail: ${driverError.detail}` : null, + ].filter(isDefined); + + return driverErrorParts.length > 0 + ? `${baseMessage} (${driverErrorParts.join(', ')})` + : baseMessage; +}; + +export const formatWorkspaceMigrationRunnerExecutionErrors = ( + errors: WorkspaceMigrationRunnerExecutionErrors, +): string | undefined => { + const entries = EXECUTION_ERROR_LABELS.map((label) => ({ + label, + error: errors[label], + })).filter((entry): entry is ExecutionErrorEntry => isDefined(entry.error)); + + if (entries.length === 0) { + return undefined; + } + + // A 25P02 failure is collateral noise from the statement that aborted the + // transaction; hide it whenever a root-cause error is available. + const rootCauseEntries = entries.filter( + (entry) => !isTransactionAbortedError(entry.error), + ); + const relevantEntries = + rootCauseEntries.length > 0 ? rootCauseEntries : entries; + + const summary = relevantEntries + .map( + (entry) => `[${entry.label}] ${formatSingleExecutionError(entry.error)}`, + ) + .join('; '); + + return summary.length <= MAX_EXECUTION_ERRORS_SUMMARY_LENGTH + ? summary + : `${summary.slice( + 0, + MAX_EXECUTION_ERRORS_SUMMARY_LENGTH - TRUNCATION_MARKER.length, + )}${TRUNCATION_MARKER}`; +}; diff --git a/packages/twenty-server/test/integration/metadata/suites/application/__snapshots__/failing-sync-application-unique-index-duplicate-data.integration-spec.ts.snap b/packages/twenty-server/test/integration/metadata/suites/application/__snapshots__/failing-sync-application-unique-index-duplicate-data.integration-spec.ts.snap new file mode 100644 index 0000000000..e572a9cd54 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/application/__snapshots__/failing-sync-application-unique-index-duplicate-data.integration-spec.ts.snap @@ -0,0 +1,51 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Sync application should surface the root cause when a unique index cannot be created over duplicated data should fail with the underlying postgres error in the migration runner error message 1`] = ` +{ + "eventId": Any, + "extensions": { + "action": { + "flatEntity": { + "applicationUniversalIdentifier": Any, + "createdAt": Any, + "indexType": "BTREE", + "indexWhereClause": null, + "isCustom": true, + "isSystemSideEffect": true, + "isUnique": true, + "name": "IDX_UNIQUE_85951922a2bfc9f7811d8c5cdb8", + "objectMetadataUniversalIdentifier": Any, + "universalFlatIndexFieldMetadatas": [ + { + "createdAt": Any, + "fieldMetadataUniversalIdentifier": Any, + "indexMetadataUniversalIdentifier": Any, + "order": 0, + "subFieldName": null, + "updatedAt": Any, + }, + ], + "universalIdentifier": Any, + "updatedAt": Any, + }, + "metadataName": "index", + "type": "create", + }, + "code": "APPLICATION_INSTALLATION_FAILED", + "errors": { + "metadata": { + "code": "25P02", + "message": "current transaction is aborted, commands ignored until end of transaction block", + }, + "workspaceSchema": { + "code": "23505", + "message": "could not create unique index "IDX_UNIQUE_85951922a2bfc9f7811d8c5cdb8"", + }, + }, + "exceptionEventId": Any, + "userFriendlyMessage": "Migration execution failed.", + }, + "message": "Migration action 'create' for 'index' (universalIdentifier: 67c6811e-9c4e-578a-ad3a-ea4ef152c0fd) failed: [workspaceSchema] could not create unique index "IDX_UNIQUE_85951922a2bfc9f7811d8c5cdb8" (pg code: 23505, detail: Key ("externalId")=(DUPLICATED-VALUE) is duplicated.)", + "name": "GraphQLError", +} +`; diff --git a/packages/twenty-server/test/integration/metadata/suites/application/failing-sync-application-unique-index-duplicate-data.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/application/failing-sync-application-unique-index-duplicate-data.integration-spec.ts new file mode 100644 index 0000000000..a5583d9485 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/application/failing-sync-application-unique-index-duplicate-data.integration-spec.ts @@ -0,0 +1,105 @@ +import { createOneOperationFactory } from 'test/integration/graphql/utils/create-one-operation-factory.util'; +import { expectOneNotInternalServerErrorSnapshot } from 'test/integration/graphql/utils/expect-one-not-internal-server-error-snapshot.util'; +import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util'; +import { buildBaseManifest } from 'test/integration/metadata/suites/application/utils/build-base-manifest.util'; +import { buildDefaultObjectManifest } from 'test/integration/metadata/suites/application/utils/build-default-object-manifest.util'; +import { cleanupApplicationAndAppRegistration } from 'test/integration/metadata/suites/application/utils/cleanup-application-and-app-registration.util'; +import { setupApplicationForSync } from 'test/integration/metadata/suites/application/utils/setup-application-for-sync.util'; +import { syncApplication } from 'test/integration/metadata/suites/application/utils/sync-application.util'; +import { type Manifest } from 'twenty-shared/application'; +import { FieldMetadataType } from 'twenty-shared/types'; +import { capitalize } from 'twenty-shared/utils'; +import { v4 as uuidv4 } from 'uuid'; + +// Fixed identifiers keep the deterministic index name (hashed from the +// application universal identifier) stable across runs for the snapshot. +const TEST_APP_ID = '3d05deeb-e0b6-4b7a-abe9-cea3b81dc1a1'; +const TEST_ROLE_ID = 'e37f849e-04a1-4fbf-b463-90b3131de79f'; +const TEST_FIELD_ID = 'c2a24a3a-3960-4c4b-bd91-c22e1e1e2f31'; + +const TEST_OBJECT = buildDefaultObjectManifest({ + applicationUniversalIdentifier: TEST_APP_ID, + universalIdentifier: '0e1f18ce-6273-4e83-bc9e-6cdeb2b57d81', + nameSingular: 'duplicatedDataObject', + namePlural: 'duplicatedDataObjects', + labelSingular: 'Duplicated Data Object', + labelPlural: 'Duplicated Data Objects', + description: 'Object used to test unique index creation over duplicated data', +}); + +const buildManifest = ({ isUnique }: { isUnique: boolean }): Manifest => + buildBaseManifest({ + appId: TEST_APP_ID, + roleId: TEST_ROLE_ID, + overrides: { + objects: [TEST_OBJECT], + fields: [ + { + universalIdentifier: TEST_FIELD_ID, + type: FieldMetadataType.TEXT, + name: 'externalId', + label: 'External ID', + description: 'External identifier', + icon: 'IconId', + isUnique, + isNullable: true, + objectUniversalIdentifier: TEST_OBJECT.universalIdentifier, + }, + ], + }, + }); + +const createRecordWithExternalId = async (externalId: string) => { + const response = await makeGraphqlAPIRequest( + createOneOperationFactory({ + objectMetadataSingularName: TEST_OBJECT.nameSingular, + gqlFields: ` + id + externalId + `, + data: { id: uuidv4(), externalId }, + }), + ); + + return response.body.data?.[`create${capitalize(TEST_OBJECT.nameSingular)}`]; +}; + +describe('Sync application should surface the root cause when a unique index cannot be created over duplicated data', () => { + beforeAll(async () => { + await setupApplicationForSync({ + applicationUniversalIdentifier: TEST_APP_ID, + name: 'Test Unique Index Duplicate Data App', + description: 'App for testing unique index creation over duplicated data', + sourcePath: 'test-unique-index-duplicate-data', + }); + }, 60000); + + afterAll(async () => { + await cleanupApplicationAndAppRegistration({ + applicationUniversalIdentifier: TEST_APP_ID, + }); + }); + + it('should fail with the underlying postgres error in the migration runner error message', async () => { + await syncApplication({ + manifest: buildManifest({ isUnique: false }), + expectToFail: false, + }); + + const firstRecord = await createRecordWithExternalId('DUPLICATED-VALUE'); + const secondRecord = await createRecordWithExternalId('DUPLICATED-VALUE'); + + expect(firstRecord?.externalId).toBe('DUPLICATED-VALUE'); + expect(secondRecord?.externalId).toBe('DUPLICATED-VALUE'); + + // Turning the field unique generates a create action for its backing + // unique index; the index cannot be created over duplicated data so the + // migration runner fails at workspace schema level with a 23505. + const { errors } = await syncApplication({ + manifest: buildManifest({ isUnique: true }), + expectToFail: true, + }); + + expectOneNotInternalServerErrorSnapshot({ errors }); + }, 60000); +});