From b26630d688cc12a55390b097fe986b0117e7dcb3 Mon Sep 17 00:00:00 2001 From: Adam Date: Sat, 15 Nov 2025 10:35:05 +0100 Subject: [PATCH 01/10] fix: improve validation error details in validateCommonProjectManifest Replace TODO comment with detailed error formatting for validation failures. Error messages now include property names and constraint details instead of generic toString() output, making it easier to identify validation issues. --- packages/common/src/project/load.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/common/src/project/load.ts b/packages/common/src/project/load.ts index 67e0d401cf..25c7c6eeda 100644 --- a/packages/common/src/project/load.ts +++ b/packages/common/src/project/load.ts @@ -5,7 +5,7 @@ import fs from 'fs'; import path from 'path'; import {ProjectManifestV1_0_0} from '@subql/types-core'; import {plainToClass} from 'class-transformer'; -import {validateSync} from 'class-validator'; +import {validateSync, ValidationError} from 'class-validator'; import yaml from 'js-yaml'; import {gte} from 'semver'; import {CommonProjectManifestV1_0_0Impl} from '../'; @@ -91,8 +91,11 @@ export function validateCommonProjectManifest(raw: unknown): void { const projectManifest = plainToClass(CommonProjectManifestV1_0_0Impl, raw); const errors = validateSync(projectManifest, {whitelist: true}); if (errors?.length) { - // TODO: print error details - const errorMsgs = errors.map((e) => e.toString()).join('\n'); + const errorMsgs = errors.map((error: ValidationError) => { + const property = error.property; + const constraints = error.constraints ? Object.values(error.constraints).join(', ') : 'unknown constraint'; + return ` - ${property}: ${constraints}`; + }).join('\n'); throw new Error(`project validation failed.\n${errorMsgs}`); } } From ff310079a5aec07c33a15f9cc845b7bcd85ff2e4 Mon Sep 17 00:00:00 2001 From: Adam Date: Sun, 7 Dec 2025 13:54:09 +0100 Subject: [PATCH 02/10] Create load.spec.ts --- packages/common/src/project/load.spec.ts | 218 +++++++++++++++++++++++ 1 file changed, 218 insertions(+) create mode 100644 packages/common/src/project/load.spec.ts diff --git a/packages/common/src/project/load.spec.ts b/packages/common/src/project/load.spec.ts new file mode 100644 index 0000000000..36d4200ad9 --- /dev/null +++ b/packages/common/src/project/load.spec.ts @@ -0,0 +1,218 @@ +// Copyright 2020-2025 SubQuery Pte Ltd authors & contributors +// SPDX-License-Identifier: GPL-3.0 + +import {validateCommonProjectManifest} from './load'; + +describe('validateCommonProjectManifest', () => { + /** + * Example of error output format improvement: + * + * BEFORE (old format using toString()): + * project validation failed. + * An instance of CommonProjectManifestV1_0_0Impl has failed the validation: + * - property version has failed the following constraints: isString + * - property schema has failed the following constraints: nested property schema must be either object or array + * + * AFTER (new structured format): + * project validation failed. + * - version: version must be a string + * - schema: schema must be an object + * - network: network must be an object + * - runner: runner must be an object + * - dataSources: dataSources must be an array + */ + it('should throw error with structured format for missing required fields', () => { + const invalidManifest = { + specVersion: '1.0.0', + // Missing required fields: version, schema, network, runner, dataSources + }; + + expect(() => validateCommonProjectManifest(invalidManifest)).toThrow(); + + try { + validateCommonProjectManifest(invalidManifest); + } catch (error: any) { + const errorMessage = error.message; + + // Check that error message starts with the expected prefix + expect(errorMessage).toContain('project validation failed.'); + + // Check that error message contains structured format with property names + // The new format should include property names and constraints + expect(errorMessage).toMatch(/ - \w+:/); + + // Verify that multiple properties are listed + const lines = errorMessage.split('\n').filter((line: string) => line.trim().startsWith('-')); + expect(lines.length).toBeGreaterThan(0); + + // Each line should follow the format: " - propertyName: constraint message" + lines.forEach((line: string) => { + expect(line).toMatch(/^\s+-\s+\w+:\s+.+$/); + }); + + // Verify specific properties are mentioned + expect(errorMessage).toMatch(/version|schema|network|runner|dataSources/); + } + }); + + it('should throw error with property-specific constraints for invalid specVersion', () => { + const invalidManifest = { + specVersion: '2.0.0', // Invalid: must be '1.0.0' + version: '1.0.0', + schema: {file: 'schema.graphql'}, + network: {chainId: '0x123'}, + runner: { + node: {name: '@subql/node', version: '1.0.0'}, + query: {name: '@subql/query', version: '1.0.0'}, + }, + dataSources: [], + }; + + expect(() => validateCommonProjectManifest(invalidManifest)).toThrow(); + + try { + validateCommonProjectManifest(invalidManifest); + } catch (error: any) { + const errorMessage = error.message; + + // Should contain specVersion property in error + expect(errorMessage).toContain('specVersion'); + + // Should contain constraint information in structured format + expect(errorMessage).toMatch(/specVersion:\s*.+/); + + // Verify the format: " - specVersion: constraint message" + const specVersionLine = errorMessage.split('\n').find((line: string) => line.includes('specVersion')); + expect(specVersionLine).toMatch(/^\s+-\s+specVersion:\s+.+$/); + } + }); + + it('should throw error with structured format for invalid runner query name', () => { + const invalidManifest = { + specVersion: '1.0.0', + version: '1.0.0', + schema: {file: 'schema.graphql'}, + network: {chainId: '0x123'}, + runner: { + node: {name: '@subql/node', version: '1.0.0'}, + query: {name: '@subql/invalid-query', version: '1.0.0'}, // Invalid query name + }, + dataSources: [], + }; + + expect(() => validateCommonProjectManifest(invalidManifest)).toThrow(); + + try { + validateCommonProjectManifest(invalidManifest); + } catch (error: any) { + const errorMessage = error.message; + + // Should contain query property in error (nested in runner.query.name) + expect(errorMessage).toContain('query'); + + // Error should be structured with property path + expect(errorMessage).toMatch(/query/); + + // Verify structured format for nested property + const queryLine = errorMessage.split('\n').find((line: string) => line.includes('query')); + if (queryLine) { + expect(queryLine).toMatch(/^\s+-\s+query/); + } + } + }); + + it('should throw error with structured format for missing chainId', () => { + const invalidManifest = { + specVersion: '1.0.0', + version: '1.0.0', + schema: {file: 'schema.graphql'}, + network: { + // Missing required chainId + endpoint: 'wss://example.com', + }, + runner: { + node: {name: '@subql/node', version: '1.0.0'}, + query: {name: '@subql/query', version: '1.0.0'}, + }, + dataSources: [], + }; + + expect(() => validateCommonProjectManifest(invalidManifest)).toThrow(); + + try { + validateCommonProjectManifest(invalidManifest); + } catch (error: any) { + const errorMessage = error.message; + + // Should contain chainId or network property in error + expect(errorMessage).toMatch(/chainId|network/); + + // Should be in structured format + expect(errorMessage).toMatch(/ - \w+:/); + + // Verify the error format shows property name and constraint + const chainIdLine = errorMessage.split('\n').find((line: string) => + line.includes('chainId') || line.includes('network') + ); + if (chainIdLine) { + expect(chainIdLine).toMatch(/^\s+-\s+\w+:\s+.+$/); + } + } + }); + + it('should demonstrate improved error message format with actual output', () => { + const invalidManifest = { + specVersion: '1.0.0', + version: '', // Empty string should fail validation + schema: {file: 'schema.graphql'}, + network: {chainId: '0x123'}, + runner: { + node: {name: '@subql/node', version: '1.0.0'}, + query: {name: '@subql/query', version: '1.0.0'}, + }, + dataSources: [], + }; + + try { + validateCommonProjectManifest(invalidManifest); + fail('Expected validation to throw an error'); + } catch (error: any) { + const errorMessage = error.message; + + // The new format should be clear and structured + // Example output: + // project validation failed. + // - version: version must be a string + + expect(errorMessage).toContain('project validation failed.'); + expect(errorMessage).toContain('version'); + + // Verify the structured format is present + const versionErrorLine = errorMessage.split('\n').find((line: string) => + line.includes('version') && line.includes(':') + ); + expect(versionErrorLine).toBeDefined(); + expect(versionErrorLine).toMatch(/^\s+-\s+version:\s+.+$/); + } + }); + + it('should not throw error for valid manifest', () => { + const validManifest = { + specVersion: '1.0.0', + version: '1.0.0', + schema: {file: 'schema.graphql'}, + network: { + chainId: '0x123', + endpoint: 'wss://example.com', + }, + runner: { + node: {name: '@subql/node', version: '1.0.0'}, + query: {name: '@subql/query', version: '1.0.0'}, + }, + dataSources: [], + }; + + expect(() => validateCommonProjectManifest(validManifest)).not.toThrow(); + }); +}); + From 3dc59e70be4094938e5bea7abc5814dc4132bbd7 Mon Sep 17 00:00:00 2001 From: Adam Date: Wed, 10 Dec 2025 14:09:09 +0100 Subject: [PATCH 03/10] Update load.ts --- packages/common/src/project/load.ts | 37 +++++++++++++++++++++++++---- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/packages/common/src/project/load.ts b/packages/common/src/project/load.ts index 25c7c6eeda..8c6ed82ec5 100644 --- a/packages/common/src/project/load.ts +++ b/packages/common/src/project/load.ts @@ -86,16 +86,43 @@ export function getFileContent(path: string, identifier: string): string { } } +/** + * Recursively formats validation errors into a structured format. + * Handles nested errors (errors with children) by recursively processing them. + * Formats array indices with brackets for better readability (e.g., dataSources[0].mapping.handlers[1].filter). + */ +function formatValidationErrors(errors: ValidationError[], parentPath = ''): string[] { + const errorMessages: string[] = []; + + for (const error of errors) { + // Check if property is a numeric string (array index) + const isArrayIndex = /^\d+$/.test(error.property); + const propertyPath = parentPath + ? isArrayIndex + ? `${parentPath}[${error.property}]` + : `${parentPath}.${error.property}` + : error.property; + + if (error.constraints && Object.keys(error.constraints).length > 0) { + const constraints = Object.values(error.constraints).join(', '); + errorMessages.push(` - ${propertyPath}: ${constraints}`); + } + + // Recursively handle nested errors + if (error.children && error.children.length > 0) { + errorMessages.push(...formatValidationErrors(error.children, propertyPath)); + } + } + + return errorMessages; +} + // Validate generic/common section for project manifest export function validateCommonProjectManifest(raw: unknown): void { const projectManifest = plainToClass(CommonProjectManifestV1_0_0Impl, raw); const errors = validateSync(projectManifest, {whitelist: true}); if (errors?.length) { - const errorMsgs = errors.map((error: ValidationError) => { - const property = error.property; - const constraints = error.constraints ? Object.values(error.constraints).join(', ') : 'unknown constraint'; - return ` - ${property}: ${constraints}`; - }).join('\n'); + const errorMsgs = formatValidationErrors(errors).join('\n'); throw new Error(`project validation failed.\n${errorMsgs}`); } } From 0616ebe14f5a3f6af1ee8dcab56cc0565000a7f8 Mon Sep 17 00:00:00 2001 From: Adam Date: Wed, 10 Dec 2025 14:09:28 +0100 Subject: [PATCH 04/10] Update utils.ts --- packages/common/src/project/utils.ts | 34 +++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/packages/common/src/project/utils.ts b/packages/common/src/project/utils.ts index 451bc3042b..71b5dcaeeb 100644 --- a/packages/common/src/project/utils.ts +++ b/packages/common/src/project/utils.ts @@ -16,6 +16,7 @@ import { registerDecorator, validateSync, ValidationArguments, + ValidationError, ValidationOptions, ValidatorConstraint, ValidatorConstraintInterface, @@ -199,10 +200,41 @@ export async function delay(sec: number): Promise { }); } +/** + * Recursively formats validation errors into a structured format. + * Handles nested errors (errors with children) by recursively processing them. + * Formats array indices with brackets for better readability (e.g., dataSources[0].mapping.handlers[1].filter). + */ +function formatValidationErrors(errors: ValidationError[], parentPath = ''): string[] { + const errorMessages: string[] = []; + + for (const error of errors) { + // Check if property is a numeric string (array index) + const isArrayIndex = /^\d+$/.test(error.property); + const propertyPath = parentPath + ? isArrayIndex + ? `${parentPath}[${error.property}]` + : `${parentPath}.${error.property}` + : error.property; + + if (error.constraints && Object.keys(error.constraints).length > 0) { + const constraints = Object.values(error.constraints).join(', '); + errorMessages.push(` - ${propertyPath}: ${constraints}`); + } + + // Recursively handle nested errors + if (error.children && error.children.length > 0) { + errorMessages.push(...formatValidationErrors(error.children, propertyPath)); + } + } + + return errorMessages; +} + export function validateObject(object: any, errorMessage = 'failed to validate object.'): void { const errors = validateSync(object, {whitelist: true, forbidNonWhitelisted: true}); if (errors?.length) { - const errorMsgs = errors.map((e) => e.toString()).join('\n'); + const errorMsgs = formatValidationErrors(errors).join('\n'); throw new Error(`${errorMessage}\n${errorMsgs}`); } } From 8f8b20fd589fffc1fc8c03ee1f978ed1091127a1 Mon Sep 17 00:00:00 2001 From: Adam Date: Wed, 10 Dec 2025 14:09:46 +0100 Subject: [PATCH 05/10] Update base.ts --- packages/common/src/project/versioned/base.ts | 34 ++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/packages/common/src/project/versioned/base.ts b/packages/common/src/project/versioned/base.ts index 473908c13b..3db4731d19 100644 --- a/packages/common/src/project/versioned/base.ts +++ b/packages/common/src/project/versioned/base.ts @@ -18,6 +18,7 @@ import { IsString, Validate, ValidateNested, + ValidationError, validateSync, } from 'class-validator'; import yaml from 'js-yaml'; @@ -50,10 +51,41 @@ export abstract class ProjectManifestBaseImpl return this._deployment; } + /** + * Recursively formats validation errors into a structured format. + * Handles nested errors (errors with children) by recursively processing them. + * Formats array indices with brackets for better readability (e.g., dataSources[0].mapping.handlers[1].filter). + */ + private formatValidationErrors(errors: ValidationError[], parentPath = ''): string[] { + const errorMessages: string[] = []; + + for (const error of errors) { + // Check if property is a numeric string (array index) + const isArrayIndex = /^\d+$/.test(error.property); + const propertyPath = parentPath + ? isArrayIndex + ? `${parentPath}[${error.property}]` + : `${parentPath}.${error.property}` + : error.property; + + if (error.constraints && Object.keys(error.constraints).length > 0) { + const constraints = Object.values(error.constraints).join(', '); + errorMessages.push(` - ${propertyPath}: ${constraints}`); + } + + // Recursively handle nested errors + if (error.children && error.children.length > 0) { + errorMessages.push(...this.formatValidationErrors(error.children, propertyPath)); + } + } + + return errorMessages; + } + validate(): void { const errors = validateSync(this.deployment, {whitelist: true, forbidNonWhitelisted: true}); if (errors?.length) { - const errorMsgs = errors.map((e) => e.toString()).join('\n'); + const errorMsgs = this.formatValidationErrors(errors).join('\n'); throw new Error(`Failed to parse project. Please see below for more information.\n${errorMsgs}`); } } From 7aa34f3ca42a64c548e26d06eb83d690f504e025 Mon Sep 17 00:00:00 2001 From: Adam Date: Wed, 10 Dec 2025 14:10:16 +0100 Subject: [PATCH 06/10] Update load.spec.ts --- packages/common/src/project/load.spec.ts | 62 ++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/packages/common/src/project/load.spec.ts b/packages/common/src/project/load.spec.ts index 36d4200ad9..cd61930036 100644 --- a/packages/common/src/project/load.spec.ts +++ b/packages/common/src/project/load.spec.ts @@ -214,5 +214,67 @@ describe('validateCommonProjectManifest', () => { expect(() => validateCommonProjectManifest(validManifest)).not.toThrow(); }); + + it('should format errors for nested child objects with array indices', () => { + // This test demonstrates how errors are formatted for deeply nested objects, + // such as an invalid filter on a mapping handler. + // The error path should use bracket notation for array indices: dataSources[0].mapping.handlers[1].filter + const invalidManifest = { + specVersion: '1.0.0', + version: '1.0.0', + schema: {file: 'schema.graphql'}, + network: { + chainId: '0x123', + endpoint: 'wss://example.com', + }, + runner: { + node: {name: '@subql/node', version: '1.0.0'}, + query: {name: '@subql/query', version: '1.0.0'}, + }, + dataSources: [ + { + kind: 'substrate/Runtime', + mapping: { + file: 'dist/index.js', + handlers: [ + { + handler: 'handleBlock', + kind: 'substrate/BlockHandler', + }, + { + handler: 'handleEvent', + kind: 'substrate/EventHandler', + filter: { + // Invalid: specVersion should be an array of 2 numbers, not a single number + specVersion: 123, + }, + }, + ], + }, + }, + ], + }; + + expect(() => validateCommonProjectManifest(invalidManifest)).toThrow(); + + try { + validateCommonProjectManifest(invalidManifest); + fail('Expected validation to throw an error'); + } catch (error: any) { + const errorMessage = error.message; + + // Error should contain the structured format + expect(errorMessage).toContain('project validation failed.'); + + // For nested array errors, the path should use bracket notation + // Example: dataSources[0].mapping.handlers[1].filter.specVersion + // Note: The exact path depends on class-validator's error structure, + // but we verify that array indices are formatted with brackets + const hasArrayIndexFormat = /\[\d+\]/.test(errorMessage); + + // The error message should be structured and readable + expect(errorMessage).toMatch(/ - .+:/); + } + }); }); From dfc70a0d73bc7e7eb253804b6e1a1ce0e2043ce0 Mon Sep 17 00:00:00 2001 From: Adam Date: Sat, 20 Dec 2025 23:15:04 +0100 Subject: [PATCH 07/10] Update utils.ts --- packages/common/src/project/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/common/src/project/utils.ts b/packages/common/src/project/utils.ts index 71b5dcaeeb..248c7666c1 100644 --- a/packages/common/src/project/utils.ts +++ b/packages/common/src/project/utils.ts @@ -205,7 +205,7 @@ export async function delay(sec: number): Promise { * Handles nested errors (errors with children) by recursively processing them. * Formats array indices with brackets for better readability (e.g., dataSources[0].mapping.handlers[1].filter). */ -function formatValidationErrors(errors: ValidationError[], parentPath = ''): string[] { +export function formatValidationErrors(errors: ValidationError[], parentPath = ''): string[] { const errorMessages: string[] = []; for (const error of errors) { From de9ef189a97fa217ffcb88b20be1f9f9addf02d1 Mon Sep 17 00:00:00 2001 From: Adam Date: Sat, 20 Dec 2025 23:15:22 +0100 Subject: [PATCH 08/10] Update load.ts --- packages/common/src/project/load.ts | 35 ++--------------------------- 1 file changed, 2 insertions(+), 33 deletions(-) diff --git a/packages/common/src/project/load.ts b/packages/common/src/project/load.ts index 8c6ed82ec5..91da153d56 100644 --- a/packages/common/src/project/load.ts +++ b/packages/common/src/project/load.ts @@ -5,12 +5,12 @@ import fs from 'fs'; import path from 'path'; import {ProjectManifestV1_0_0} from '@subql/types-core'; import {plainToClass} from 'class-transformer'; -import {validateSync, ValidationError} from 'class-validator'; +import {validateSync} from 'class-validator'; import yaml from 'js-yaml'; import {gte} from 'semver'; import {CommonProjectManifestV1_0_0Impl} from '../'; import {NETWORK_FAMILY, runnerMapping} from '../constants'; -import {DEFAULT_MANIFEST, DEFAULT_TS_MANIFEST, extensionIsYamlOrJSON} from './utils'; +import {DEFAULT_MANIFEST, DEFAULT_TS_MANIFEST, extensionIsYamlOrJSON, formatValidationErrors} from './utils'; export function loadFromJsonOrYaml(file: string): unknown { const {ext} = path.parse(file); if (!extensionIsYamlOrJSON(ext)) { @@ -86,37 +86,6 @@ export function getFileContent(path: string, identifier: string): string { } } -/** - * Recursively formats validation errors into a structured format. - * Handles nested errors (errors with children) by recursively processing them. - * Formats array indices with brackets for better readability (e.g., dataSources[0].mapping.handlers[1].filter). - */ -function formatValidationErrors(errors: ValidationError[], parentPath = ''): string[] { - const errorMessages: string[] = []; - - for (const error of errors) { - // Check if property is a numeric string (array index) - const isArrayIndex = /^\d+$/.test(error.property); - const propertyPath = parentPath - ? isArrayIndex - ? `${parentPath}[${error.property}]` - : `${parentPath}.${error.property}` - : error.property; - - if (error.constraints && Object.keys(error.constraints).length > 0) { - const constraints = Object.values(error.constraints).join(', '); - errorMessages.push(` - ${propertyPath}: ${constraints}`); - } - - // Recursively handle nested errors - if (error.children && error.children.length > 0) { - errorMessages.push(...formatValidationErrors(error.children, propertyPath)); - } - } - - return errorMessages; -} - // Validate generic/common section for project manifest export function validateCommonProjectManifest(raw: unknown): void { const projectManifest = plainToClass(CommonProjectManifestV1_0_0Impl, raw); From 02c3d2a2437790df4f6dc911f4d70eff0e5090a1 Mon Sep 17 00:00:00 2001 From: Adam Date: Sat, 20 Dec 2025 23:15:40 +0100 Subject: [PATCH 09/10] Update base.ts --- packages/common/src/project/versioned/base.ts | 36 ++----------------- 1 file changed, 2 insertions(+), 34 deletions(-) diff --git a/packages/common/src/project/versioned/base.ts b/packages/common/src/project/versioned/base.ts index 3db4731d19..0b6d06a078 100644 --- a/packages/common/src/project/versioned/base.ts +++ b/packages/common/src/project/versioned/base.ts @@ -18,11 +18,10 @@ import { IsString, Validate, ValidateNested, - ValidationError, validateSync, } from 'class-validator'; import yaml from 'js-yaml'; -import {IsEndBlockGreater, toJsonObject} from '../utils'; +import {IsEndBlockGreater, toJsonObject, formatValidationErrors} from '../utils'; import {ParentProjectModel} from './v1_0_0/models'; export abstract class ProjectManifestBaseImpl @@ -51,41 +50,10 @@ export abstract class ProjectManifestBaseImpl return this._deployment; } - /** - * Recursively formats validation errors into a structured format. - * Handles nested errors (errors with children) by recursively processing them. - * Formats array indices with brackets for better readability (e.g., dataSources[0].mapping.handlers[1].filter). - */ - private formatValidationErrors(errors: ValidationError[], parentPath = ''): string[] { - const errorMessages: string[] = []; - - for (const error of errors) { - // Check if property is a numeric string (array index) - const isArrayIndex = /^\d+$/.test(error.property); - const propertyPath = parentPath - ? isArrayIndex - ? `${parentPath}[${error.property}]` - : `${parentPath}.${error.property}` - : error.property; - - if (error.constraints && Object.keys(error.constraints).length > 0) { - const constraints = Object.values(error.constraints).join(', '); - errorMessages.push(` - ${propertyPath}: ${constraints}`); - } - - // Recursively handle nested errors - if (error.children && error.children.length > 0) { - errorMessages.push(...this.formatValidationErrors(error.children, propertyPath)); - } - } - - return errorMessages; - } - validate(): void { const errors = validateSync(this.deployment, {whitelist: true, forbidNonWhitelisted: true}); if (errors?.length) { - const errorMsgs = this.formatValidationErrors(errors).join('\n'); + const errorMsgs = formatValidationErrors(errors).join('\n'); throw new Error(`Failed to parse project. Please see below for more information.\n${errorMsgs}`); } } From c5c19c7c461a0f69121b3a2b1f2f1de9bc3211f8 Mon Sep 17 00:00:00 2001 From: Adam Date: Sat, 20 Dec 2025 23:15:54 +0100 Subject: [PATCH 10/10] Update load.spec.ts --- packages/common/src/project/load.spec.ts | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/packages/common/src/project/load.spec.ts b/packages/common/src/project/load.spec.ts index cd61930036..7f0363a893 100644 --- a/packages/common/src/project/load.spec.ts +++ b/packages/common/src/project/load.spec.ts @@ -31,6 +31,7 @@ describe('validateCommonProjectManifest', () => { try { validateCommonProjectManifest(invalidManifest); + fail('Expected validation to throw an error'); } catch (error: any) { const errorMessage = error.message; @@ -72,6 +73,7 @@ describe('validateCommonProjectManifest', () => { try { validateCommonProjectManifest(invalidManifest); + fail('Expected validation to throw an error'); } catch (error: any) { const errorMessage = error.message; @@ -104,6 +106,7 @@ describe('validateCommonProjectManifest', () => { try { validateCommonProjectManifest(invalidManifest); + fail('Expected validation to throw an error'); } catch (error: any) { const errorMessage = error.message; @@ -141,6 +144,7 @@ describe('validateCommonProjectManifest', () => { try { validateCommonProjectManifest(invalidManifest); + fail('Expected validation to throw an error'); } catch (error: any) { const errorMessage = error.message; @@ -268,12 +272,18 @@ describe('validateCommonProjectManifest', () => { // For nested array errors, the path should use bracket notation // Example: dataSources[0].mapping.handlers[1].filter.specVersion - // Note: The exact path depends on class-validator's error structure, - // but we verify that array indices are formatted with brackets - const hasArrayIndexFormat = /\[\d+\]/.test(errorMessage); + // Verify that array indices are formatted with brackets + expect(errorMessage).toMatch(/\[\d+\]/); + + // The error message should be structured and readable with full example + // Expected format: " - dataSources[0].mapping.handlers[1].filter.specVersion: " + const errorLines = errorMessage.split('\n').filter((line: string) => line.trim().startsWith('-')); + expect(errorLines.length).toBeGreaterThan(0); - // The error message should be structured and readable - expect(errorMessage).toMatch(/ - .+:/); + // Verify at least one line contains array bracket notation and follows the expected format + const arrayErrorLine = errorLines.find((line: string) => /\[\d+\]/.test(line)); + expect(arrayErrorLine).toBeDefined(); + expect(arrayErrorLine).toMatch(/^\s+-\s+.+\[\d+\].+:\s+.+$/); } }); });