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)_ <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23416?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. -->
This commit is contained in:
+2
-2
@@ -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:
|
||||
|
||||
+22
@@ -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",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
+7
-1
@@ -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;
|
||||
|
||||
+114
@@ -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');
|
||||
});
|
||||
});
|
||||
+81
@@ -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}`;
|
||||
};
|
||||
+51
@@ -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<String>,
|
||||
"extensions": {
|
||||
"action": {
|
||||
"flatEntity": {
|
||||
"applicationUniversalIdentifier": Any<String>,
|
||||
"createdAt": Any<String>,
|
||||
"indexType": "BTREE",
|
||||
"indexWhereClause": null,
|
||||
"isCustom": true,
|
||||
"isSystemSideEffect": true,
|
||||
"isUnique": true,
|
||||
"name": "IDX_UNIQUE_85951922a2bfc9f7811d8c5cdb8",
|
||||
"objectMetadataUniversalIdentifier": Any<String>,
|
||||
"universalFlatIndexFieldMetadatas": [
|
||||
{
|
||||
"createdAt": Any<String>,
|
||||
"fieldMetadataUniversalIdentifier": Any<String>,
|
||||
"indexMetadataUniversalIdentifier": Any<String>,
|
||||
"order": 0,
|
||||
"subFieldName": null,
|
||||
"updatedAt": Any<String>,
|
||||
},
|
||||
],
|
||||
"universalIdentifier": Any<String>,
|
||||
"updatedAt": Any<String>,
|
||||
},
|
||||
"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<String>,
|
||||
"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",
|
||||
}
|
||||
`;
|
||||
+105
@@ -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);
|
||||
});
|
||||
Reference in New Issue
Block a user