From 90dacb2928477010ded3a607f267f8acec0b4b18 Mon Sep 17 00:00:00 2001 From: Petar Dimov Date: Tue, 21 Oct 2025 14:41:57 +0300 Subject: [PATCH 01/31] feat: Add manifest validation tool --- src/registerTools.ts | 3 + src/tools/run_manifest_validation/index.ts | 34 +++++ .../run_manifest_validation/runValidation.ts | 139 ++++++++++++++++++ src/tools/run_manifest_validation/schema.ts | 61 ++++++++ 4 files changed, 237 insertions(+) create mode 100644 src/tools/run_manifest_validation/index.ts create mode 100644 src/tools/run_manifest_validation/runValidation.ts create mode 100644 src/tools/run_manifest_validation/schema.ts diff --git a/src/registerTools.ts b/src/registerTools.ts index 9adcd3c1..8614e513 100644 --- a/src/registerTools.ts +++ b/src/registerTools.ts @@ -11,6 +11,7 @@ import registerGetGuidelinesTool from "./tools/get_guidelines/index.js"; import registerGetVersionInfoTool from "./tools/get_version_info/index.js"; import registerGetIntegrationCardsGuidelinesTool from "./tools/get_integration_cards_guidelines/index.js"; import registerCreateIntegrationCardTool from "./tools/create_integration_card/index.js"; +import registerRunManifestValidationTool from "./tools/run_manifest_validation/index.js"; import registerGetTypescriptConversionGuidelinesTool from "./tools/get_typescript_conversion_guidelines/index.js"; interface Options { @@ -56,6 +57,8 @@ export default function (server: McpServer, context: Context, options: Options) registerCreateIntegrationCardTool(registerTool, context); + registerRunManifestValidationTool(registerTool, context); + registerGetTypescriptConversionGuidelinesTool(registerTool, context); } diff --git a/src/tools/run_manifest_validation/index.ts b/src/tools/run_manifest_validation/index.ts new file mode 100644 index 00000000..5914c91c --- /dev/null +++ b/src/tools/run_manifest_validation/index.ts @@ -0,0 +1,34 @@ +import runValidation from "./runValidation.js"; +import {inputSchema, outputSchema} from "./schema.js"; +import {getLogger} from "@ui5/logger"; +import Context from "../../Context.js"; +import {RegisterTool} from "../../registerTools.js"; + +const log = getLogger("tools:run_manifest_validation"); + +export default function registerTool(registerTool: RegisterTool, _context: Context) { + registerTool("run_manifest_validation", { + description: + "Validates UI5 manifest file." + + "After making changes, you should always run the validation again " + + "to verify that no new problems have been introduced.", + annotations: { + title: "Manifest Validation", + readOnlyHint: false, + }, + inputSchema, + outputSchema, + }, async ({manifestPath}) => { + log.info(`Running manifest validation on ${manifestPath}...`); + + const result = await runValidation(manifestPath); + + return { + content: [{ + type: "text", + text: JSON.stringify(result), + }], + structuredContent: result, + }; + }); +} diff --git a/src/tools/run_manifest_validation/runValidation.ts b/src/tools/run_manifest_validation/runValidation.ts new file mode 100644 index 00000000..ff19ac78 --- /dev/null +++ b/src/tools/run_manifest_validation/runValidation.ts @@ -0,0 +1,139 @@ +import {fetchCdn} from "../../utils/cdnHelper.js"; +import {RunSchemaValidationResult} from "./schema.js"; +import Ajv2020, {AnySchemaObject} from "ajv/dist/2020.js"; +import {readFile} from "fs/promises"; +import {getLogger} from "@ui5/logger"; +import {InvalidInputError} from "../../utils.js"; + +const log = getLogger("tools:run_manifest_validation:runValidation"); +const schemaCache = new Map>(); + +async function createUI5ManifestValidateFunction() { + const ajv = new Ajv2020.default({ + allErrors: true, // Collect all errors, not just the first one + strict: false, // Allow additional properties that are not in schema + unicodeRegExp: false, + loadSchema: async (uri) => { + // Check cache first to prevent infinite loops + if (schemaCache.has(uri)) { + log.info(`Loading cached schema: ${uri}`); + + try { + const schema = await schemaCache.get(uri)!; + return schema; + } catch { + schemaCache.delete(uri); + } + } + + log.info(`Loading external schema: ${uri}`); + let fetchSchema: Promise; + + try { + if (uri.includes("adaptive-card.json")) { + // Special handling for Adaptive Card schema to fix unsupported "id" property + // According to the JSON Schema spec Draft 06 (used by Adaptive Card schema), + // "$id" should be used instead of "id" + fetchSchema = fetchCdn(uri) + .then((response) => { + if ("id" in response && typeof response.id === "string") { + const typedResponse = response as Record; + typedResponse.$id = response.id; + delete typedResponse.id; + } + return response; + }); + } else { + fetchSchema = fetchCdn(uri); + } + + schemaCache.set(uri, fetchSchema); + return fetchSchema; + } catch (error) { + log.warn(`Failed to load external schema ${uri}:` + + `${error instanceof Error ? error.message : String(error)}`); + + throw error; + } + }, + }); + const draft06MetaSchema = JSON.parse( + await readFile("node_modules/ajv/dist/refs/json-schema-draft-06.json", "utf-8") + ) as AnySchemaObject; + const draft07MetaSchema = JSON.parse( + await readFile("node_modules/ajv/dist/refs/json-schema-draft-07.json", "utf-8") + ) as AnySchemaObject; + + ajv.addMetaSchema(draft06MetaSchema, "http://json-schema.org/draft-06/schema#"); + ajv.addMetaSchema(draft07MetaSchema, "http://json-schema.org/draft-07/schema#"); + + // Fetch the UI5 manifest schema + const schemaUrl = "https://raw.githubusercontent.com/SAP/ui5-manifest/master/schema.json"; + const schema = await fetchCdn(schemaUrl); + log.info(`Fetched UI5 manifest schema from ${schemaUrl}`); + + const validate = await ajv.compileAsync(schema); + + return validate; +} + +async function readManifest(path: string) { + let content: string; + let json: object; + + try { + content = await readFile(path, "utf-8"); + } catch (error) { + throw new InvalidInputError(`Failed to read manifest file at ${path}: ` + + `${error instanceof Error ? error.message : String(error)}`); + } + + try { + json = JSON.parse(content) as object; + } catch (error) { + throw new InvalidInputError(`Failed to parse manifest file at ${path} as JSON: ` + + `${error instanceof Error ? error.message : String(error)}`); + } + + return json; +} + +export default async function runValidation(manifestPath: string): Promise { + log.info(`Starting manifest validation for file: ${manifestPath}`); + + const manifest = await readManifest(manifestPath); + const validate = await createUI5ManifestValidateFunction(); + const isValid = validate(manifest); + + if (isValid) { + log.info("Manifest validation successful"); + + return { + isValid: true, + errors: [], + }; + } + + // Map AJV errors to our schema format + const validationErrors = validate.errors ?? []; + const errors = validationErrors.map((error) => { + return { + keyword: error.keyword ?? "", + instancePath: error.instancePath ?? "", + schemaPath: error.schemaPath ?? "", + params: error.params ?? {}, + propertyName: error.propertyName, + message: error.message, + schema: error.schema, + parentSchema: error.parentSchema, + data: error.data, + }; + }); + + log.info(`Manifest validation failed with ${errors.length} error(s)`); + + return { + isValid: false, + errors: errors, + }; +} diff --git a/src/tools/run_manifest_validation/schema.ts b/src/tools/run_manifest_validation/schema.ts new file mode 100644 index 00000000..1601e3c1 --- /dev/null +++ b/src/tools/run_manifest_validation/schema.ts @@ -0,0 +1,61 @@ +import {z} from "zod"; + +export const inputSchema = { + manifestPath: z.string() + .describe("Path to the manifest file to validate."), +}; + +export const outputSchema = { + isValid: z.boolean() + .describe("Whether the manifest is valid according to the UI5 Manifest schema."), + errors: z.array( + z.object({ + keyword: z.string() + .describe("Validation keyword."), + instancePath: z.string() + .describe("JSON Pointer to the location in the data instance (e.g., `/prop/1/subProp`)."), + schemaPath: z.string() + .describe("JSON Pointer to the location of the failing keyword in the schema."), + params: z.record(z.any()) + .describe("An object with additional information about the error."), + propertyName: z.string() + .optional() + .describe("Set for errors in `propertyNames` keyword schema."), + message: z.string() + .optional() + .describe("The error message."), + schema: z.any() + .optional() + .describe("The value of the failing keyword in the schema."), + parentSchema: z.record(z.any()) + .optional() + .describe("The schema containing the keyword."), + data: z.any() + .optional() + .describe("The data validated by the keyword."), + }) + ).describe("Array of validation error objects as returned by Ajv."), + + // errors: z.array( + // z.object({ + // path: z.array( + // z.any() + // ).describe("An array of property keys or array offsets," + + // "indicating where inside objects or arrays the instance was found"), + // property: z.string() + // .describe("Describes the property path. Starts with instance, and is delimited with a dot (.)"), + // message: z.string() + // .describe("A human-readable message for debugging use."), + // instance: z.any() + // .describe("The instance that failed"), + // name: z.string() + // .describe("The keyword within the schema that failed."), + // argument: z.any() + // .describe("Provides information about the keyword that failed."), + // stack: z.string() + // .describe("A human-readable string representing the error."), + // }).describe("Single schema error object.") + // ), +}; +export const outputSchemaObject = z.object(outputSchema); +export type RunSchemaValidationResult = z.infer; From fc517802161c6185f7e0afa4bb49c682c4ecadc1 Mon Sep 17 00:00:00 2001 From: Petar Dimov Date: Tue, 21 Oct 2025 16:56:08 +0300 Subject: [PATCH 02/31] refactor: Move manifest schema fetching to ui5Manifest.ts --- .../run_manifest_validation/runValidation.ts | 15 +++++++-------- src/utils/ui5Manifest.ts | 15 +++++++++++++++ 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/src/tools/run_manifest_validation/runValidation.ts b/src/tools/run_manifest_validation/runValidation.ts index ff19ac78..b0355fbb 100644 --- a/src/tools/run_manifest_validation/runValidation.ts +++ b/src/tools/run_manifest_validation/runValidation.ts @@ -4,11 +4,12 @@ import Ajv2020, {AnySchemaObject} from "ajv/dist/2020.js"; import {readFile} from "fs/promises"; import {getLogger} from "@ui5/logger"; import {InvalidInputError} from "../../utils.js"; +import {getManifestSchema} from "../../utils/ui5Manifest.js"; const log = getLogger("tools:run_manifest_validation:runValidation"); const schemaCache = new Map>(); -async function createUI5ManifestValidateFunction() { +async function createUI5ManifestValidateFunction(ui5Schema: object) { const ajv = new Ajv2020.default({ allErrors: true, // Collect all errors, not just the first one strict: false, // Allow additional properties that are not in schema @@ -67,12 +68,7 @@ async function createUI5ManifestValidateFunction() { ajv.addMetaSchema(draft06MetaSchema, "http://json-schema.org/draft-06/schema#"); ajv.addMetaSchema(draft07MetaSchema, "http://json-schema.org/draft-07/schema#"); - // Fetch the UI5 manifest schema - const schemaUrl = "https://raw.githubusercontent.com/SAP/ui5-manifest/master/schema.json"; - const schema = await fetchCdn(schemaUrl); - log.info(`Fetched UI5 manifest schema from ${schemaUrl}`); - - const validate = await ajv.compileAsync(schema); + const validate = await ajv.compileAsync(ui5Schema); return validate; } @@ -102,7 +98,10 @@ export default async function runValidation(manifestPath: string): Promise Date: Wed, 22 Oct 2025 15:30:28 +0300 Subject: [PATCH 03/31] test(index.ts): Add tests --- src/tools/run_manifest_validation/index.ts | 1 + .../tools/run_manifest_validation/index.ts | 147 ++++++++++++++++++ 2 files changed, 148 insertions(+) create mode 100644 test/lib/tools/run_manifest_validation/index.ts diff --git a/src/tools/run_manifest_validation/index.ts b/src/tools/run_manifest_validation/index.ts index 5914c91c..6ae82842 100644 --- a/src/tools/run_manifest_validation/index.ts +++ b/src/tools/run_manifest_validation/index.ts @@ -8,6 +8,7 @@ const log = getLogger("tools:run_manifest_validation"); export default function registerTool(registerTool: RegisterTool, _context: Context) { registerTool("run_manifest_validation", { + title: "Manifest Validation", description: "Validates UI5 manifest file." + "After making changes, you should always run the validation again " + diff --git a/test/lib/tools/run_manifest_validation/index.ts b/test/lib/tools/run_manifest_validation/index.ts new file mode 100644 index 00000000..e29af86e --- /dev/null +++ b/test/lib/tools/run_manifest_validation/index.ts @@ -0,0 +1,147 @@ +import anyTest, {TestFn} from "ava"; +import esmock from "esmock"; +import sinonGlobal from "sinon"; +import TestContext from "../../../utils/TestContext.js"; + +// Define test context type +const test = anyTest as TestFn<{ + sinon: sinonGlobal.SinonSandbox; + registerToolCallback: sinonGlobal.SinonStub; + loggerMock: { + silly: sinonGlobal.SinonStub; + verbose: sinonGlobal.SinonStub; + perf: sinonGlobal.SinonStub; + info: sinonGlobal.SinonStub; + warn: sinonGlobal.SinonStub; + error: sinonGlobal.SinonStub; + isLevelEnabled: sinonGlobal.SinonStub; + }; + runValidationStub: sinonGlobal.SinonStub; + registerRunManifestValidationTool: typeof import( + "../../../../src/tools/run_manifest_validation/index.js" + ).default; +}>; + +// Setup test context before each test +test.beforeEach(async (t) => { + // Create a sandbox for sinon stubs + t.context.sinon = sinonGlobal.createSandbox(); + + t.context.registerToolCallback = t.context.sinon.stub(); + + // Create logger mock + const loggerMock = { + silly: t.context.sinon.stub(), + verbose: t.context.sinon.stub(), + perf: t.context.sinon.stub(), + info: t.context.sinon.stub(), + warn: t.context.sinon.stub(), + error: t.context.sinon.stub(), + isLevelEnabled: t.context.sinon.stub().returns(true), + }; + t.context.loggerMock = loggerMock; + + const runValidationStub = t.context.sinon.stub(); + t.context.runValidationStub = runValidationStub; + + // Import the tool registration function with mocked dependencies + const {default: registerRunManifestValidationTool} = await esmock( + "../../../../src/tools/run_manifest_validation/index.js", { + "../../../../src/tools/run_manifest_validation/runValidation.js": { + default: runValidationStub, + }, + } + ); + + t.context.registerRunManifestValidationTool = registerRunManifestValidationTool; +}); + +// Clean up after each test +test.afterEach.always((t) => { + t.context.sinon.restore(); +}); + +test("registerRunManifestValidationTool registers the tool with correct parameters", (t) => { + const {registerToolCallback, registerRunManifestValidationTool} = t.context; + + registerRunManifestValidationTool(registerToolCallback, new TestContext()); + + t.true(registerToolCallback.calledOnce); + t.is(registerToolCallback.firstCall.args[0], "run_manifest_validation"); + + // Verify tool configuration + const toolConfig = registerToolCallback.firstCall.args[1]; + t.true(toolConfig?.title?.includes("Manifest Validation")); + t.true(toolConfig?.description?.includes("Validates UI5 manifest file")); + t.is(toolConfig?.annotations?.title, "Manifest Validation"); + t.false(toolConfig?.annotations?.readOnlyHint); +}); + +test("run_manifest_validation tool returns validation result on success", async (t) => { + const { + registerToolCallback, + registerRunManifestValidationTool, + runValidationStub, + } = t.context; + + // Setup runValidation to return a sample result + const sampleResult = { + valid: true, + issues: [], + }; + runValidationStub.resolves(sampleResult); + + // Register the tool and capture the execute function + registerRunManifestValidationTool(registerToolCallback, new TestContext()); + const executeFunction = registerToolCallback.firstCall.args[2]; + + const mockExtra = { + signal: new AbortController().signal, + requestId: "test-request-id", + sendNotification: t.context.sinon.stub(), + sendRequest: t.context.sinon.stub(), + }; + + // Execute the tool + const manifestPath = "/path/to/valid/manifest.json"; + const result = await executeFunction({manifestPath}, mockExtra); + + t.deepEqual(result, { + content: [{ + type: "text", + text: JSON.stringify(sampleResult), + }], + structuredContent: sampleResult, + }); +}); + +test("run_manifest_validation tool handles errors correctly", async (t) => { + const { + registerToolCallback, + registerRunManifestValidationTool, + runValidationStub, + } = t.context; + + // Setup readFile to throw an error + const errorMessage = "Failed to read manifest file"; + runValidationStub.rejects(new Error(errorMessage)); + + // Register the tool and capture the execute function + registerRunManifestValidationTool(registerToolCallback, new TestContext()); + const executeFunction = registerToolCallback.firstCall.args[2]; + + const mockExtra = { + signal: new AbortController().signal, + requestId: "test-request-id", + sendNotification: t.context.sinon.stub(), + sendRequest: t.context.sinon.stub(), + }; + + // Execute the tool + const manifestPath = "/path/to/invalid/manifest.json"; + await t.throwsAsync(async () => { + await executeFunction({manifestPath}, mockExtra); + }, { + message: errorMessage, + }); +}); From a5992fcc86c8a40d1642328a1caebcde578ea265 Mon Sep 17 00:00:00 2001 From: Petar Dimov Date: Wed, 22 Oct 2025 15:31:05 +0300 Subject: [PATCH 04/31] refactor(ui5Manifest.ts): Cache the schema --- src/utils/ui5Manifest.ts | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/src/utils/ui5Manifest.ts b/src/utils/ui5Manifest.ts index 217f2282..e17cde25 100644 --- a/src/utils/ui5Manifest.ts +++ b/src/utils/ui5Manifest.ts @@ -1,9 +1,10 @@ import {getLogger} from "@ui5/logger"; import {fetchCdn} from "./cdnHelper.js"; -const log = getLogger("utils:dataStorageHelper"); +const log = getLogger("utils:ui5Manifest"); const MAPPING_URL = "https://raw.githubusercontent.com/SAP/ui5-manifest/main/mapping.json"; const LATEST_SCHEMA_URL = "https://raw.githubusercontent.com/SAP/ui5-manifest/main/schema.json"; +const schemaCache = new Map>(); async function getUI5toManifestVersionMap() { const mapping = await fetchCdn(MAPPING_URL); @@ -11,6 +12,26 @@ async function getUI5toManifestVersionMap() { return mapping as Record; } +async function fetchSchema(manifestVersion: string) { + if (schemaCache.has(manifestVersion)) { + log.info(`Loading cached schema for manifest version: ${manifestVersion}`); + + try { + const schema = await schemaCache.get(manifestVersion)!; + return schema; + } catch { + schemaCache.delete(manifestVersion); + } + } + + log.info(`Fetching schema for manifest version: ${manifestVersion}`); + schemaCache.set(manifestVersion, fetchCdn(LATEST_SCHEMA_URL)); + const schema = await schemaCache.get(manifestVersion)!; + log.info(`Fetched UI5 manifest schema from ${LATEST_SCHEMA_URL}`); + + return schema; +} + export async function getLatestManifestVersion() { const versionMap = await getUI5toManifestVersionMap(); @@ -26,9 +47,5 @@ export async function getManifestSchema(manifestVersion: string) { throw new Error(`Only 'latest' manifest version is supported, but got '${manifestVersion}'.`); } - // Fetch the UI5 manifest schema - const schema = await fetchCdn(LATEST_SCHEMA_URL); - log.info(`Fetched UI5 manifest schema from ${LATEST_SCHEMA_URL}`); - - return schema; + return await fetchSchema(manifestVersion); } From e5ecb5944574f3347dac15778b3a79ba05dfce1d Mon Sep 17 00:00:00 2001 From: Petar Dimov Date: Mon, 27 Oct 2025 15:07:27 +0200 Subject: [PATCH 05/31] docs(README.md): List run_manifest_validation --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 267fa8ea..0b91ba36 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,7 @@ The UI5 [Model Context Protocol](https://modelcontextprotocol.io/) server offers - `get_typescript_conversion_guidelines`: Provides guidelines for converting UI5 applications and controls from JavaScript to TypeScript. - `get_integration_cards_guidelines`: Provides access to UI Integration Cards development best practices. - `create_integration_card`: Scaffolds a new UI Integration Card. +- `run_manifest_validation`: Validates the manifest against the UI5 Manifest schema. ## Requirements From 7ef41cab6fd1be83962948b27e871c019528dcd4 Mon Sep 17 00:00:00 2001 From: Petar Dimov Date: Mon, 27 Oct 2025 15:41:38 +0200 Subject: [PATCH 06/31] refactor(ui5Manifest): Add cache --- src/utils/ui5Manifest.ts | 62 ++++++++++++++++++--------- test/lib/utils/ui5Manifest.ts | 81 ++++++++++++++++++++++++++++++++++- 2 files changed, 122 insertions(+), 21 deletions(-) diff --git a/src/utils/ui5Manifest.ts b/src/utils/ui5Manifest.ts index e17cde25..3f2bb7e7 100644 --- a/src/utils/ui5Manifest.ts +++ b/src/utils/ui5Manifest.ts @@ -1,39 +1,61 @@ import {getLogger} from "@ui5/logger"; import {fetchCdn} from "./cdnHelper.js"; +import {Mutex} from "async-mutex"; const log = getLogger("utils:ui5Manifest"); -const MAPPING_URL = "https://raw.githubusercontent.com/SAP/ui5-manifest/main/mapping.json"; + const LATEST_SCHEMA_URL = "https://raw.githubusercontent.com/SAP/ui5-manifest/main/schema.json"; -const schemaCache = new Map>(); +const schemaCache = new Map(); +const fetchSchemaMutex = new Mutex(); + +let UI5ToManifestVersionMapping: Record | null = null; +const MAPPING_URL = "https://raw.githubusercontent.com/SAP/ui5-manifest/main/mapping.json"; +const ui5ToManifestVersionMappingMutex = new Mutex(); + +async function getUI5toManifestVersionMapping() { + const release = await ui5ToManifestVersionMappingMutex.acquire(); + + try { + if (UI5ToManifestVersionMapping) { + log.info("Loading cached UI5 to manifest version mapping"); + return UI5ToManifestVersionMapping; + } -async function getUI5toManifestVersionMap() { - const mapping = await fetchCdn(MAPPING_URL); + log.info("Fetching UI5 to manifest version mapping"); + const mapping = await fetchCdn(MAPPING_URL); + log.info(`Fetched UI5 to manifest version mapping from ${MAPPING_URL}`); - return mapping as Record; + UI5ToManifestVersionMapping = mapping as Record; + + return UI5ToManifestVersionMapping; + } finally { + release(); + } } async function fetchSchema(manifestVersion: string) { - if (schemaCache.has(manifestVersion)) { - log.info(`Loading cached schema for manifest version: ${manifestVersion}`); - - try { - const schema = await schemaCache.get(manifestVersion)!; - return schema; - } catch { - schemaCache.delete(manifestVersion); + const release = await fetchSchemaMutex.acquire(); + + try { + if (schemaCache.has(manifestVersion)) { + log.info(`Loading cached schema for manifest version: ${manifestVersion}`); + return schemaCache.get(manifestVersion)!; } - } - log.info(`Fetching schema for manifest version: ${manifestVersion}`); - schemaCache.set(manifestVersion, fetchCdn(LATEST_SCHEMA_URL)); - const schema = await schemaCache.get(manifestVersion)!; - log.info(`Fetched UI5 manifest schema from ${LATEST_SCHEMA_URL}`); + log.info(`Fetching schema for manifest version: ${manifestVersion}`); + const schema = await fetchCdn(LATEST_SCHEMA_URL); + log.info(`Fetched UI5 manifest schema from ${LATEST_SCHEMA_URL}`); + + schemaCache.set(manifestVersion, schema); - return schema; + return schema; + } finally { + release(); + } } export async function getLatestManifestVersion() { - const versionMap = await getUI5toManifestVersionMap(); + const versionMap = await getUI5toManifestVersionMapping(); if (!versionMap.latest) { throw new Error("Could not determine latest manifest version."); diff --git a/test/lib/utils/ui5Manifest.ts b/test/lib/utils/ui5Manifest.ts index fece738a..e3905af1 100644 --- a/test/lib/utils/ui5Manifest.ts +++ b/test/lib/utils/ui5Manifest.ts @@ -6,6 +6,7 @@ const test = anyTest as TestFn<{ sinon: sinonGlobal.SinonSandbox; fetchCdnStub: sinonGlobal.SinonStub; getLatestManifestVersion: typeof import("../../../src/utils/ui5Manifest.js").getLatestManifestVersion; + getManifestSchema: typeof import("../../../src/utils/ui5Manifest.js").getManifestSchema; }>; test.beforeEach(async (t) => { @@ -15,13 +16,14 @@ test.beforeEach(async (t) => { t.context.fetchCdnStub = fetchCdnStub; // Import the module with mocked dependencies - const {getLatestManifestVersion} = await esmock("../../../src/utils/ui5Manifest.js", { + const {getLatestManifestVersion, getManifestSchema} = await esmock("../../../src/utils/ui5Manifest.js", { "../../../src/utils/cdnHelper.js": { fetchCdn: fetchCdnStub, }, }); t.context.getLatestManifestVersion = getLatestManifestVersion; + t.context.getManifestSchema = getManifestSchema; }); test.afterEach.always((t) => { @@ -43,6 +45,23 @@ test("getLatestManifestVersion returns correct version from CDN data", async (t) t.true(fetchCdnStub.calledOnce); }); +test("getLatestManifestVersion uses cache on subsequent calls", async (t) => { + const {fetchCdnStub, getLatestManifestVersion} = t.context; + const mockData = { + "latest": "1.79.0", + "1.141": "1.79.0", + "1.140": "1.78.0", + }; + fetchCdnStub.resolves(mockData); + + const latestVersion1 = await getLatestManifestVersion(); + const latestVersion2 = await getLatestManifestVersion(); + + t.is(latestVersion1, "1.79.0"); + t.is(latestVersion2, "1.79.0"); + t.true(fetchCdnStub.calledOnce); +}); + test("getLatestManifestVersion handles fetch errors", async (t) => { const {fetchCdnStub, getLatestManifestVersion} = t.context; @@ -78,3 +97,63 @@ test("getLatestManifestVersion handles missing latest version", async (t) => { ); t.true(fetchCdnStub.calledOnce); }); + +test("getManifestSchema throws error for unsupported versions", async (t) => { + const {getManifestSchema} = t.context; + + await t.throwsAsync( + async () => { + await getManifestSchema("1.78.0"); + }, + { + message: "Only 'latest' manifest version is supported, but got '1.78.0'.", + } + ); +}); + +test("getManifestSchema fetches schema for 'latest' version", async (t) => { + const {fetchCdnStub, getManifestSchema} = t.context; + const mockSchema = { + $schema: "http://json-schema.org/draft-07/schema#", + type: "object", + }; + fetchCdnStub.resolves(mockSchema); + + const schema = await getManifestSchema("latest"); + + t.deepEqual(schema, mockSchema); + t.true(fetchCdnStub.calledOnce); +}); + +test("getManifestSchema uses cache on subsequent calls", async (t) => { + const {fetchCdnStub, getManifestSchema} = t.context; + const mockSchema = { + $schema: "http://json-schema.org/draft-07/schema#", + type: "object", + }; + fetchCdnStub.resolves(mockSchema); + + const schema1 = await getManifestSchema("latest"); + const schema2 = await getManifestSchema("latest"); + + t.deepEqual(schema1, mockSchema); + t.deepEqual(schema2, mockSchema); + t.true(fetchCdnStub.calledOnce); +}); + +test("getManifestSchema handles fetch errors", async (t) => { + const {fetchCdnStub, getManifestSchema} = t.context; + + // Mock fetch error + fetchCdnStub.rejects(new Error("Network error")); + + await t.throwsAsync( + async () => { + await getManifestSchema("latest"); + }, + { + message: "Network error", + } + ); + t.true(fetchCdnStub.calledOnce); +}); From b2ed9a3da3536e62bd1f171fbd51dcfc9e85c126 Mon Sep 17 00:00:00 2001 From: Petar Dimov Date: Mon, 27 Oct 2025 15:56:09 +0200 Subject: [PATCH 07/31] refactor: Improve error handling --- .../run_manifest_validation/runValidation.ts | 121 ++++++++++-------- src/tools/run_manifest_validation/schema.ts | 25 +--- 2 files changed, 68 insertions(+), 78 deletions(-) diff --git a/src/tools/run_manifest_validation/runValidation.ts b/src/tools/run_manifest_validation/runValidation.ts index b0355fbb..dadd9148 100644 --- a/src/tools/run_manifest_validation/runValidation.ts +++ b/src/tools/run_manifest_validation/runValidation.ts @@ -9,68 +9,79 @@ import {getManifestSchema} from "../../utils/ui5Manifest.js"; const log = getLogger("tools:run_manifest_validation:runValidation"); const schemaCache = new Map>(); -async function createUI5ManifestValidateFunction(ui5Schema: object) { - const ajv = new Ajv2020.default({ - allErrors: true, // Collect all errors, not just the first one - strict: false, // Allow additional properties that are not in schema - unicodeRegExp: false, - loadSchema: async (uri) => { - // Check cache first to prevent infinite loops - if (schemaCache.has(uri)) { - log.info(`Loading cached schema: ${uri}`); +// Configuration constants +const AJV_SCHEMA_PATHS = { + draft06: "node_modules/ajv/dist/refs/json-schema-draft-06.json", + draft07: "node_modules/ajv/dist/refs/json-schema-draft-07.json", +} as const; - try { - const schema = await schemaCache.get(uri)!; - return schema; - } catch { - schemaCache.delete(uri); - } - } - - log.info(`Loading external schema: ${uri}`); - let fetchSchema: Promise; - - try { - if (uri.includes("adaptive-card.json")) { - // Special handling for Adaptive Card schema to fix unsupported "id" property - // According to the JSON Schema spec Draft 06 (used by Adaptive Card schema), - // "$id" should be used instead of "id" - fetchSchema = fetchCdn(uri) - .then((response) => { - if ("id" in response && typeof response.id === "string") { - const typedResponse = response as Record; - typedResponse.$id = response.id; - delete typedResponse.id; - } - return response; - }); - } else { - fetchSchema = fetchCdn(uri); +async function createUI5ManifestValidateFunction(ui5Schema: object) { + try { + const ajv = new Ajv2020.default({ + allErrors: true, // Collect all errors, not just the first one + strict: false, // Allow additional properties that are not in schema + unicodeRegExp: false, + loadSchema: async (uri) => { + // Check cache first to prevent infinite loops + if (schemaCache.has(uri)) { + log.info(`Loading cached schema: ${uri}`); + + try { + const schema = await schemaCache.get(uri)!; + return schema; + } catch { + schemaCache.delete(uri); + } } - schemaCache.set(uri, fetchSchema); - return fetchSchema; - } catch (error) { - log.warn(`Failed to load external schema ${uri}:` + - `${error instanceof Error ? error.message : String(error)}`); + log.info(`Loading external schema: ${uri}`); + let fetchSchema: Promise; - throw error; - } - }, - }); - const draft06MetaSchema = JSON.parse( - await readFile("node_modules/ajv/dist/refs/json-schema-draft-06.json", "utf-8") - ) as AnySchemaObject; - const draft07MetaSchema = JSON.parse( - await readFile("node_modules/ajv/dist/refs/json-schema-draft-07.json", "utf-8") - ) as AnySchemaObject; + try { + if (uri.includes("adaptive-card.json")) { + // Special handling for Adaptive Card schema to fix unsupported "id" property + // According to the JSON Schema spec Draft 06 (used by Adaptive Card schema), + // "$id" should be used instead of "id" + fetchSchema = fetchCdn(uri) + .then((response) => { + if ("id" in response && typeof response.id === "string") { + const typedResponse = response as Record; + typedResponse.$id = response.id; + delete typedResponse.id; + } + return response; + }); + } else { + fetchSchema = fetchCdn(uri); + } + + schemaCache.set(uri, fetchSchema); + return fetchSchema; + } catch (error) { + log.warn(`Failed to load external schema ${uri}:` + + `${error instanceof Error ? error.message : String(error)}`); + + throw error; + } + }, + }); + const draft06MetaSchema = JSON.parse( + await readFile(AJV_SCHEMA_PATHS.draft06, "utf-8") + ) as AnySchemaObject; + const draft07MetaSchema = JSON.parse( + await readFile(AJV_SCHEMA_PATHS.draft07, "utf-8") + ) as AnySchemaObject; - ajv.addMetaSchema(draft06MetaSchema, "http://json-schema.org/draft-06/schema#"); - ajv.addMetaSchema(draft07MetaSchema, "http://json-schema.org/draft-07/schema#"); + ajv.addMetaSchema(draft06MetaSchema, "http://json-schema.org/draft-06/schema#"); + ajv.addMetaSchema(draft07MetaSchema, "http://json-schema.org/draft-07/schema#"); - const validate = await ajv.compileAsync(ui5Schema); + const validate = await ajv.compileAsync(ui5Schema); - return validate; + return validate; + } catch (error) { + throw new Error(`Failed to create UI5 manifest validate function: ` + + `${error instanceof Error ? error.message : String(error)}`); + } } async function readManifest(path: string) { diff --git a/src/tools/run_manifest_validation/schema.ts b/src/tools/run_manifest_validation/schema.ts index 1601e3c1..5220ebbe 100644 --- a/src/tools/run_manifest_validation/schema.ts +++ b/src/tools/run_manifest_validation/schema.ts @@ -35,27 +35,6 @@ export const outputSchema = { .describe("The data validated by the keyword."), }) ).describe("Array of validation error objects as returned by Ajv."), - - // errors: z.array( - // z.object({ - // path: z.array( - // z.any() - // ).describe("An array of property keys or array offsets," + - // "indicating where inside objects or arrays the instance was found"), - // property: z.string() - // .describe("Describes the property path. Starts with instance, and is delimited with a dot (.)"), - // message: z.string() - // .describe("A human-readable message for debugging use."), - // instance: z.any() - // .describe("The instance that failed"), - // name: z.string() - // .describe("The keyword within the schema that failed."), - // argument: z.any() - // .describe("Provides information about the keyword that failed."), - // stack: z.string() - // .describe("A human-readable string representing the error."), - // }).describe("Single schema error object.") - // ), }; -export const outputSchemaObject = z.object(outputSchema); -export type RunSchemaValidationResult = z.infer; +const _outputSchemaObject = z.object(outputSchema); +export type RunSchemaValidationResult = z.infer; From e9009ebe70d17f44b25b0e9f94e1285fac4ac397 Mon Sep 17 00:00:00 2001 From: Petar Dimov Date: Mon, 27 Oct 2025 16:32:29 +0200 Subject: [PATCH 08/31] test(runValidation): Add tests --- resources/integration_cards_guidelines.md | 1 + .../run_manifest_validation/runValidation.ts | 50 ++-- test/fixtures/manifest_validation/schema.json | 46 ++++ .../manifest_validation/valid-manifest.json | 26 ++ .../runValidation.integration.ts | 45 +++ .../run_manifest_validation/runValidation.ts | 256 ++++++++++++++++++ 6 files changed, 394 insertions(+), 30 deletions(-) create mode 100644 test/fixtures/manifest_validation/schema.json create mode 100644 test/fixtures/manifest_validation/valid-manifest.json create mode 100644 test/lib/tools/run_manifest_validation/runValidation.integration.ts create mode 100644 test/lib/tools/run_manifest_validation/runValidation.ts diff --git a/resources/integration_cards_guidelines.md b/resources/integration_cards_guidelines.md index e480aae1..6372d662 100644 --- a/resources/integration_cards_guidelines.md +++ b/resources/integration_cards_guidelines.md @@ -42,6 +42,7 @@ ## 2. Validation - **ALWAYS** ensure that `manifest.json` file is valid JSON. - **ALWAYS** ensure that in `manifest.json` file the property `sap.app/type` is set to `"card"`. +- **ALWAYS** validate the `manifest.json` against the UI5 Manifest schema. You must do it using the `run_manifest_validation` tool. - **ALWAYS** avoid using deprecated properties in `manifest.json` and elsewhere. - **NEVER** treat Integration Cards' project as UI5 project, except for cards of type "Component". diff --git a/src/tools/run_manifest_validation/runValidation.ts b/src/tools/run_manifest_validation/runValidation.ts index dadd9148..e374918c 100644 --- a/src/tools/run_manifest_validation/runValidation.ts +++ b/src/tools/run_manifest_validation/runValidation.ts @@ -5,11 +5,12 @@ import {readFile} from "fs/promises"; import {getLogger} from "@ui5/logger"; import {InvalidInputError} from "../../utils.js"; import {getManifestSchema} from "../../utils/ui5Manifest.js"; +import {Mutex} from "async-mutex"; const log = getLogger("tools:run_manifest_validation:runValidation"); -const schemaCache = new Map>(); +const schemaCache = new Map(); +const fetchSchemaMutex = new Mutex(); -// Configuration constants const AJV_SCHEMA_PATHS = { draft06: "node_modules/ajv/dist/refs/json-schema-draft-06.json", draft07: "node_modules/ajv/dist/refs/json-schema-draft-07.json", @@ -22,46 +23,35 @@ async function createUI5ManifestValidateFunction(ui5Schema: object) { strict: false, // Allow additional properties that are not in schema unicodeRegExp: false, loadSchema: async (uri) => { - // Check cache first to prevent infinite loops + const release = await fetchSchemaMutex.acquire(); + if (schemaCache.has(uri)) { log.info(`Loading cached schema: ${uri}`); - - try { - const schema = await schemaCache.get(uri)!; - return schema; - } catch { - schemaCache.delete(uri); - } + return schemaCache.get(uri)!; } - log.info(`Loading external schema: ${uri}`); - let fetchSchema: Promise; - try { - if (uri.includes("adaptive-card.json")) { - // Special handling for Adaptive Card schema to fix unsupported "id" property - // According to the JSON Schema spec Draft 06 (used by Adaptive Card schema), - // "$id" should be used instead of "id" - fetchSchema = fetchCdn(uri) - .then((response) => { - if ("id" in response && typeof response.id === "string") { - const typedResponse = response as Record; - typedResponse.$id = response.id; - delete typedResponse.id; - } - return response; - }); - } else { - fetchSchema = fetchCdn(uri); + log.info(`Loading external schema: ${uri}`); + const schema = await fetchCdn(uri) as AnySchemaObject; + + // Special handling for Adaptive Card schema to fix unsupported "id" property + // According to the JSON Schema spec Draft 06 (used by Adaptive Card schema), + // "$id" should be used instead of "id" + if (uri.includes("adaptive-card.json") && typeof schema.id === "string") { + schema.$id = schema.id; + delete schema.id; } - schemaCache.set(uri, fetchSchema); - return fetchSchema; + schemaCache.set(uri, schema); + + return schema; } catch (error) { log.warn(`Failed to load external schema ${uri}:` + `${error instanceof Error ? error.message : String(error)}`); throw error; + } finally { + release(); } }, }); diff --git a/test/fixtures/manifest_validation/schema.json b/test/fixtures/manifest_validation/schema.json new file mode 100644 index 00000000..bcc2288e --- /dev/null +++ b/test/fixtures/manifest_validation/schema.json @@ -0,0 +1,46 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "required": ["_version", "sap.app"], + "properties": { + "_version": { + "type": "string" + }, + "sap.app": { + "type": "object", + "required": ["id", "type", "applicationVersion"], + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "applicationVersion": { + "type": "object", + "required": ["version"], + "properties": { + "version": { + "type": "string" + } + } + }, + "dataSources": { + "type": "object" + } + } + }, + "sap.ui": { + "type": "object" + }, + "sap.ui5": { + "type": "object" + } + } +} diff --git a/test/fixtures/manifest_validation/valid-manifest.json b/test/fixtures/manifest_validation/valid-manifest.json new file mode 100644 index 00000000..69fca7c7 --- /dev/null +++ b/test/fixtures/manifest_validation/valid-manifest.json @@ -0,0 +1,26 @@ +{ + "_version": "1.59.0", + "sap.app": { + "id": "com.example.app", + "type": "application", + "applicationVersion": { + "version": "1.0.0" + } + }, + "sap.ui": { + "technology": "UI5", + "deviceTypes": { + "desktop": true, + "tablet": true, + "phone": true + } + }, + "sap.ui5": { + "dependencies": { + "minUI5Version": "1.120.0", + "libs": { + "sap.m": {} + } + } + } +} diff --git a/test/lib/tools/run_manifest_validation/runValidation.integration.ts b/test/lib/tools/run_manifest_validation/runValidation.integration.ts new file mode 100644 index 00000000..15eeab3b --- /dev/null +++ b/test/lib/tools/run_manifest_validation/runValidation.integration.ts @@ -0,0 +1,45 @@ +import anyTest, {TestFn} from "ava"; +import * as sinon from "sinon"; +import esmock from "esmock"; +import {readFile} from "fs/promises"; +import path from "path"; +import {fileURLToPath} from "url"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const fixturesPath = path.join(__dirname, "..", "..", "..", "fixtures", "manifest_validation"); + +const test = anyTest as TestFn<{ + sinon: sinon.SinonSandbox; + runValidation: typeof import("../../../../src/tools/run_manifest_validation/runValidation.js").default; +}>; + +test.beforeEach(async (t) => { + t.context.sinon = sinon.createSandbox(); + + const schemaFixture = await readFile(path.join(fixturesPath, "schema.json"), "utf-8"); + const getManifestSchemaStub = t.context.sinon.stub().resolves(JSON.parse(schemaFixture)); + + // Import the runValidation function + t.context.runValidation = (await esmock( + "../../../../src/tools/run_manifest_validation/runValidation.js", { + "../../../../src/utils/ui5Manifest.js": { + getManifestSchema: getManifestSchemaStub, + }, + } + )).default; +}); + +test.afterEach.always((t) => { + t.context.sinon.restore(); +}); + +test("runValidation successfully validates valid manifest", async (t) => { + const {runValidation} = t.context; + + const result = await runValidation(path.join(fixturesPath, "valid-manifest.json")); + + t.deepEqual(result, { + isValid: true, + errors: [], + }); +}); diff --git a/test/lib/tools/run_manifest_validation/runValidation.ts b/test/lib/tools/run_manifest_validation/runValidation.ts new file mode 100644 index 00000000..71529108 --- /dev/null +++ b/test/lib/tools/run_manifest_validation/runValidation.ts @@ -0,0 +1,256 @@ +import anyTest, {TestFn} from "ava"; +import * as sinon from "sinon"; +import esmock from "esmock"; +import {readFile} from "fs/promises"; + +const test = anyTest as TestFn<{ + sinon: sinon.SinonSandbox; + readFileStub: sinon.SinonStub; + manifestFileContent: string; + getManifestSchemaStub: sinon.SinonStub; + fetchCdnStub: sinon.SinonStub; + runValidation: typeof import("../../../../src/tools/run_manifest_validation/runValidation.js").default; +}>; + +test.beforeEach(async (t) => { + t.context.sinon = sinon.createSandbox(); + t.context.manifestFileContent = ""; + + // Create a stub that only intercepts specific manifest paths, otherwise calls real readFile + t.context.readFileStub = t.context.sinon.stub().callsFake(async ( + path: string, + encoding?: BufferEncoding | null + ) => { + // Only handle specific manifest paths that we explicitly stub + if (path === "/path/to/manifest.json") { + // These will be handled by withArgs() stubs below + return t.context.manifestFileContent; + } + // For all other files (including AJV schema files), call the real readFile + return readFile(path, encoding ?? "utf-8"); + }); + + t.context.getManifestSchemaStub = t.context.sinon.stub(); + t.context.fetchCdnStub = t.context.sinon.stub(); + + // Import the runValidation function + t.context.runValidation = (await esmock( + "../../../../src/tools/run_manifest_validation/runValidation.js", { + "fs/promises": { + readFile: t.context.readFileStub, + }, + "../../../../src/utils/ui5Manifest.js": { + getManifestSchema: t.context.getManifestSchemaStub, + }, + "../../../../src/utils/cdnHelper.js": { + fetchCdn: t.context.fetchCdnStub, + }, + } + )).default; +}); + +test.afterEach.always((t) => { + t.context.sinon.restore(); +}); + +test("runValidation successfully validates valid manifest", async (t) => { + const {runValidation, getManifestSchemaStub} = t.context; + + // Stub the readFile function to return a valid manifest + const validManifest = { + "sap.app": { + id: "my.app.id", + type: "application", + }, + }; + t.context.manifestFileContent = JSON.stringify(validManifest); + + getManifestSchemaStub.resolves({ + type: "object", + properties: { + "sap.app": { + type: "object", + properties: { + id: {type: "string"}, + type: {type: "string"}, + }, + required: ["id", "type"], + }, + }, + required: ["sap.app"], + }); + + const result = await runValidation("/path/to/manifest.json"); + + t.deepEqual(result, { + isValid: true, + errors: [], + }); +}); + +test("runValidation successfully validates invalid manifest", async (t) => { + const {runValidation, getManifestSchemaStub} = t.context; + + // Stub the readFile function to return an invalid manifest + const invalidManifest = { + "sap.app": { + id: "my.app.id", + // Missing required field "type" + }, + }; + t.context.manifestFileContent = JSON.stringify(invalidManifest); + + getManifestSchemaStub.resolves({ + type: "object", + properties: { + "sap.app": { + type: "object", + properties: { + id: {type: "string"}, + type: {type: "string"}, + }, + required: ["id", "type"], + }, + }, + required: ["sap.app"], + additionalProperties: false, + }); + + const result = await runValidation("/path/to/manifest.json"); + + t.deepEqual(result, { + isValid: false, + errors: [ + { + params: {missingProperty: "type"}, + keyword: "required", + instancePath: "/sap.app", + schemaPath: "#/properties/sap.app/required", + message: "must have required property 'type'", + propertyName: undefined, + schema: undefined, + parentSchema: undefined, + data: undefined, + }, + ], + }); +}); + +test("runValidation throws error when manifest file path is not correct", async (t) => { + const {runValidation, readFileStub} = t.context; + + // Stub the readFile function to throw an error + readFileStub.rejects(new Error("File not found")); + + await t.throwsAsync(async () => { + const result = await runValidation("/nonexistent/path"); + return result; + }, { + instanceOf: Error, + message: /Failed to read manifest file at .+: .+/, + }); +}); + +test("runValidation throws error when manifest file content is invalid JSON", async (t) => { + const {runValidation} = t.context; + + t.context.manifestFileContent = "Invalid JSON Content"; + + await t.throwsAsync(async () => { + const result = await runValidation("/path/to/manifest.json"); + return result; + }, { + instanceOf: Error, + message: /Failed to parse manifest file at .+ as JSON: .+/, + }); +}); + +test("runValidation throws error when schema validation function cannot be compiled", async (t) => { + const {runValidation, getManifestSchemaStub} = t.context; + + t.context.manifestFileContent = JSON.stringify({}); + getManifestSchemaStub.resolves(null); // Simulate invalid schema + + await t.throwsAsync(async () => { + const result = await runValidation("/path/to/manifest.json"); + return result; + }, { + instanceOf: Error, + message: /Failed to create UI5 manifest validate function: .+/, + }); +}); + +test("runValidation successfully validates valid manifest against external schema", async (t) => { + const {runValidation, getManifestSchemaStub, fetchCdnStub} = t.context; + + t.context.manifestFileContent = JSON.stringify({ + "sap.app": { + id: "my.app.id", + type: "application", + }, + }); + + // Schema that references an external schema + getManifestSchemaStub.resolves({ + type: "object", + properties: { + "sap.app": { + $ref: "externalSchema.json", + }, + }, + required: ["sap.app"], + }); + + // Stub the readFile function to return the external schema when requested + const externalSchema = { + type: "object", + properties: { + id: {type: "string"}, + type: {type: "string"}, + }, + required: ["id", "type"], + }; + fetchCdnStub.withArgs("externalSchema.json") + .resolves(externalSchema); + + const result = await runValidation("/path/to/manifest.json"); + + t.deepEqual(result, { + isValid: true, + errors: [], + }); +}); + +test("runValidation throws error when external schema cannot be fetched", async (t) => { + const {runValidation, getManifestSchemaStub, fetchCdnStub} = t.context; + + t.context.manifestFileContent = JSON.stringify({ + "sap.app": { + id: "my.app.id", + type: "application", + }, + }); + + // Schema that references an external schema + getManifestSchemaStub.resolves({ + type: "object", + properties: { + "sap.app": { + $ref: "externalSchema.json", + }, + }, + required: ["sap.app"], + }); + + // Stub the fetchCdn function to throw an error when fetching the external schema + fetchCdnStub.withArgs("externalSchema.json") + .rejects(new Error("Failed to fetch external schema")); + + await t.throwsAsync(async () => { + const result = await runValidation("/path/to/manifest.json"); + return result; + }, { + instanceOf: Error, + message: /Failed to create UI5 manifest validate function: .+/, + }); +}); From 51e2239cc7ddd30839ed675edb02ace02822141f Mon Sep 17 00:00:00 2001 From: Petar Dimov Date: Thu, 30 Oct 2025 10:30:18 +0200 Subject: [PATCH 09/31] refactor: Add comment containing link to AdaptiveCards issue --- src/tools/run_manifest_validation/runValidation.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/tools/run_manifest_validation/runValidation.ts b/src/tools/run_manifest_validation/runValidation.ts index e374918c..cb3e45a2 100644 --- a/src/tools/run_manifest_validation/runValidation.ts +++ b/src/tools/run_manifest_validation/runValidation.ts @@ -37,6 +37,7 @@ async function createUI5ManifestValidateFunction(ui5Schema: object) { // Special handling for Adaptive Card schema to fix unsupported "id" property // According to the JSON Schema spec Draft 06 (used by Adaptive Card schema), // "$id" should be used instead of "id" + // See https://github.com/microsoft/AdaptiveCards/issues/9274 if (uri.includes("adaptive-card.json") && typeof schema.id === "string") { schema.$id = schema.id; delete schema.id; From 0acf46b186ddf061a528470a71abe8aa9746ad59 Mon Sep 17 00:00:00 2001 From: Petar Dimov Date: Thu, 30 Oct 2025 10:34:50 +0200 Subject: [PATCH 10/31] fix(package.json): List ajv as dependency --- npm-shrinkwrap.json | 1 + package.json | 1 + 2 files changed, 2 insertions(+) diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 0822403a..5776cbec 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -13,6 +13,7 @@ "@ui5/linter": "^1.20.6", "@ui5/logger": "^4.0.2", "@ui5/project": "^4.0.9", + "ajv": "^8.17.1", "async-mutex": "^0.5.0", "ejs": "^3.1.10", "execa": "^9.6.1", diff --git a/package.json b/package.json index e8bc4a5e..d2c8cd24 100644 --- a/package.json +++ b/package.json @@ -79,6 +79,7 @@ "@ui5/linter": "^1.20.6", "@ui5/logger": "^4.0.2", "@ui5/project": "^4.0.9", + "ajv": "^8.17.1", "async-mutex": "^0.5.0", "ejs": "^3.1.10", "execa": "^9.6.1", From 67db4fbecda8fcd471c667592024c79862f536fa Mon Sep 17 00:00:00 2001 From: Petar Dimov Date: Thu, 30 Oct 2025 16:02:27 +0200 Subject: [PATCH 11/31] fix(runValidation): Resolve meta schemas paths using import.meta.resolve --- src/tools/run_manifest_validation/runValidation.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/tools/run_manifest_validation/runValidation.ts b/src/tools/run_manifest_validation/runValidation.ts index cb3e45a2..070296e7 100644 --- a/src/tools/run_manifest_validation/runValidation.ts +++ b/src/tools/run_manifest_validation/runValidation.ts @@ -6,14 +6,15 @@ import {getLogger} from "@ui5/logger"; import {InvalidInputError} from "../../utils.js"; import {getManifestSchema} from "../../utils/ui5Manifest.js"; import {Mutex} from "async-mutex"; +import {fileURLToPath} from "url"; const log = getLogger("tools:run_manifest_validation:runValidation"); const schemaCache = new Map(); const fetchSchemaMutex = new Mutex(); const AJV_SCHEMA_PATHS = { - draft06: "node_modules/ajv/dist/refs/json-schema-draft-06.json", - draft07: "node_modules/ajv/dist/refs/json-schema-draft-07.json", + draft06: fileURLToPath(import.meta.resolve("ajv/dist/refs/json-schema-draft-06.json")), + draft07: fileURLToPath(import.meta.resolve("ajv/dist/refs/json-schema-draft-07.json")), } as const; async function createUI5ManifestValidateFunction(ui5Schema: object) { From 86873ea61e9082c20e212e8088ccb5af3fab4ee8 Mon Sep 17 00:00:00 2001 From: Petar Dimov Date: Thu, 30 Oct 2025 16:13:53 +0200 Subject: [PATCH 12/31] fix(runValidation): Throw error if manifest path is not absolute --- .../run_manifest_validation/runValidation.ts | 5 ++++ src/tools/run_manifest_validation/schema.ts | 2 +- .../run_manifest_validation/runValidation.ts | 28 ++++++++++++------- 3 files changed, 24 insertions(+), 11 deletions(-) diff --git a/src/tools/run_manifest_validation/runValidation.ts b/src/tools/run_manifest_validation/runValidation.ts index 070296e7..64fbd76a 100644 --- a/src/tools/run_manifest_validation/runValidation.ts +++ b/src/tools/run_manifest_validation/runValidation.ts @@ -7,6 +7,7 @@ import {InvalidInputError} from "../../utils.js"; import {getManifestSchema} from "../../utils/ui5Manifest.js"; import {Mutex} from "async-mutex"; import {fileURLToPath} from "url"; +import {isAbsolute} from "path"; const log = getLogger("tools:run_manifest_validation:runValidation"); const schemaCache = new Map(); @@ -80,6 +81,10 @@ async function readManifest(path: string) { let content: string; let json: object; + if (!isAbsolute(path)) { + throw new InvalidInputError(`The manifest path must be absolute: '${path}'`); + } + try { content = await readFile(path, "utf-8"); } catch (error) { diff --git a/src/tools/run_manifest_validation/schema.ts b/src/tools/run_manifest_validation/schema.ts index 5220ebbe..2d0bdd79 100644 --- a/src/tools/run_manifest_validation/schema.ts +++ b/src/tools/run_manifest_validation/schema.ts @@ -2,7 +2,7 @@ import {z} from "zod"; export const inputSchema = { manifestPath: z.string() - .describe("Path to the manifest file to validate."), + .describe("Absolute path to the manifest file to validate."), }; export const outputSchema = { diff --git a/test/lib/tools/run_manifest_validation/runValidation.ts b/test/lib/tools/run_manifest_validation/runValidation.ts index 71529108..bfe206d9 100644 --- a/test/lib/tools/run_manifest_validation/runValidation.ts +++ b/test/lib/tools/run_manifest_validation/runValidation.ts @@ -2,6 +2,7 @@ import anyTest, {TestFn} from "ava"; import * as sinon from "sinon"; import esmock from "esmock"; import {readFile} from "fs/promises"; +import {InvalidInputError} from "../../../../src/utils.js"; const test = anyTest as TestFn<{ sinon: sinon.SinonSandbox; @@ -136,6 +137,17 @@ test("runValidation successfully validates invalid manifest", async (t) => { }); }); +test("runValidation throws error when manifest file path is not absolute", async (t) => { + const {runValidation} = t.context; + + await t.throwsAsync(async () => { + return await runValidation("relativeManifest.json"); + }, { + instanceOf: InvalidInputError, + message: "The manifest path must be absolute: 'relativeManifest.json'", + }); +}); + test("runValidation throws error when manifest file path is not correct", async (t) => { const {runValidation, readFileStub} = t.context; @@ -143,10 +155,9 @@ test("runValidation throws error when manifest file path is not correct", async readFileStub.rejects(new Error("File not found")); await t.throwsAsync(async () => { - const result = await runValidation("/nonexistent/path"); - return result; + return await runValidation("/nonexistent/path"); }, { - instanceOf: Error, + instanceOf: InvalidInputError, message: /Failed to read manifest file at .+: .+/, }); }); @@ -157,10 +168,9 @@ test("runValidation throws error when manifest file content is invalid JSON", as t.context.manifestFileContent = "Invalid JSON Content"; await t.throwsAsync(async () => { - const result = await runValidation("/path/to/manifest.json"); - return result; + return await runValidation("/path/to/manifest.json"); }, { - instanceOf: Error, + instanceOf: InvalidInputError, message: /Failed to parse manifest file at .+ as JSON: .+/, }); }); @@ -172,8 +182,7 @@ test("runValidation throws error when schema validation function cannot be compi getManifestSchemaStub.resolves(null); // Simulate invalid schema await t.throwsAsync(async () => { - const result = await runValidation("/path/to/manifest.json"); - return result; + return await runValidation("/path/to/manifest.json"); }, { instanceOf: Error, message: /Failed to create UI5 manifest validate function: .+/, @@ -247,8 +256,7 @@ test("runValidation throws error when external schema cannot be fetched", async .rejects(new Error("Failed to fetch external schema")); await t.throwsAsync(async () => { - const result = await runValidation("/path/to/manifest.json"); - return result; + return await runValidation("/path/to/manifest.json"); }, { instanceOf: Error, message: /Failed to create UI5 manifest validate function: .+/, From 3b7ab28707337ed2e3e3e3984fdb00d6ba820bb6 Mon Sep 17 00:00:00 2001 From: Petar Dimov Date: Wed, 5 Nov 2025 11:13:48 +0200 Subject: [PATCH 13/31] fix(run_manifest_validation): Normalize manifest path --- src/tools/run_manifest_validation/index.ts | 5 ++- .../tools/run_manifest_validation/index.ts | 40 +++++++++++++++++++ 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/src/tools/run_manifest_validation/index.ts b/src/tools/run_manifest_validation/index.ts index 6ae82842..cb208002 100644 --- a/src/tools/run_manifest_validation/index.ts +++ b/src/tools/run_manifest_validation/index.ts @@ -6,7 +6,7 @@ import {RegisterTool} from "../../registerTools.js"; const log = getLogger("tools:run_manifest_validation"); -export default function registerTool(registerTool: RegisterTool, _context: Context) { +export default function registerTool(registerTool: RegisterTool, context: Context) { registerTool("run_manifest_validation", { title: "Manifest Validation", description: @@ -22,7 +22,8 @@ export default function registerTool(registerTool: RegisterTool, _context: Conte }, async ({manifestPath}) => { log.info(`Running manifest validation on ${manifestPath}...`); - const result = await runValidation(manifestPath); + const normalizedManifestPath = await context.normalizePath(manifestPath); + const result = await runValidation(normalizedManifestPath); return { content: [{ diff --git a/test/lib/tools/run_manifest_validation/index.ts b/test/lib/tools/run_manifest_validation/index.ts index e29af86e..1b01d58d 100644 --- a/test/lib/tools/run_manifest_validation/index.ts +++ b/test/lib/tools/run_manifest_validation/index.ts @@ -145,3 +145,43 @@ test("run_manifest_validation tool handles errors correctly", async (t) => { message: errorMessage, }); }); + +test("run_manifest_validation tool normalizes manifest path before validation", async (t) => { + const { + registerToolCallback, + registerRunManifestValidationTool, + runValidationStub, + } = t.context; + + // Setup runValidation to return a sample result + const sampleResult = { + valid: true, + issues: [], + }; + runValidationStub.resolves(sampleResult); + + class CustomTestContext extends TestContext { + async normalizePath(path: string): Promise { + return Promise.resolve(`/normalized${path}`); + } + } + + // Register the tool and capture the execute function + registerRunManifestValidationTool(registerToolCallback, new CustomTestContext()); + const executeFunction = registerToolCallback.firstCall.args[2]; + + const mockExtra = { + signal: new AbortController().signal, + requestId: "test-request-id", + sendNotification: t.context.sinon.stub(), + sendRequest: t.context.sinon.stub(), + }; + + // Execute the tool + const manifestPath = "/path/to/manifest.json"; + await executeFunction({manifestPath}, mockExtra); + + // Verify that runValidation was called with the normalized path + t.true(runValidationStub.calledOnce); + t.is(runValidationStub.firstCall.args[0], "/normalized/path/to/manifest.json"); +}); From 626675be3b3aef780b7d4120252e803c7630a8ab Mon Sep 17 00:00:00 2001 From: Petar Dimov Date: Mon, 10 Nov 2025 14:20:17 +0200 Subject: [PATCH 14/31] refactor: Fetch concrete manifest schema --- .../run_manifest_validation/runValidation.ts | 4 +- src/utils/ui5Manifest.ts | 87 ++++++++++++++++--- .../missing-version-manifest.json | 25 ++++++ .../runValidation.integration.ts | 80 +++++++++++++++-- .../run_manifest_validation/runValidation.ts | 15 +++- test/lib/utils/ui5Manifest.ts | 67 +++++++++++--- 6 files changed, 244 insertions(+), 34 deletions(-) create mode 100644 test/fixtures/manifest_validation/missing-version-manifest.json diff --git a/src/tools/run_manifest_validation/runValidation.ts b/src/tools/run_manifest_validation/runValidation.ts index 64fbd76a..75716e76 100644 --- a/src/tools/run_manifest_validation/runValidation.ts +++ b/src/tools/run_manifest_validation/runValidation.ts @@ -4,7 +4,7 @@ import Ajv2020, {AnySchemaObject} from "ajv/dist/2020.js"; import {readFile} from "fs/promises"; import {getLogger} from "@ui5/logger"; import {InvalidInputError} from "../../utils.js"; -import {getManifestSchema} from "../../utils/ui5Manifest.js"; +import {getManifestSchema, getManifestVersion} from "../../utils/ui5Manifest.js"; import {Mutex} from "async-mutex"; import {fileURLToPath} from "url"; import {isAbsolute} from "path"; @@ -106,7 +106,7 @@ export default async function runValidation(manifestPath: string): Promise(); const fetchSchemaMutex = new Mutex(); @@ -12,6 +12,10 @@ let UI5ToManifestVersionMapping: Record | null = null; const MAPPING_URL = "https://raw.githubusercontent.com/SAP/ui5-manifest/main/mapping.json"; const ui5ToManifestVersionMappingMutex = new Mutex(); +function getSchemaURL(manifestVersion: string) { + return `https://raw.githubusercontent.com/SAP/ui5-manifest/v${manifestVersion}/schema.json`; +} + async function getUI5toManifestVersionMapping() { const release = await ui5ToManifestVersionMappingMutex.acquire(); @@ -43,8 +47,9 @@ async function fetchSchema(manifestVersion: string) { } log.info(`Fetching schema for manifest version: ${manifestVersion}`); - const schema = await fetchCdn(LATEST_SCHEMA_URL); - log.info(`Fetched UI5 manifest schema from ${LATEST_SCHEMA_URL}`); + const schemaURL = getSchemaURL(manifestVersion); + const schema = await fetchCdn(schemaURL); + log.info(`Fetched UI5 manifest schema from ${schemaURL}`); schemaCache.set(manifestVersion, schema); @@ -54,6 +59,74 @@ async function fetchSchema(manifestVersion: string) { } } +/** + * Get the manifest schema for a specific manifest version. + * @param manifestVersion The manifest version + * @returns The manifest schema + * @throws Error if the manifest version is unsupported + */ +export async function getManifestSchema(manifestVersion: string) { + if (semver.lt(manifestVersion, "1.48.0")) { + throw new Error( + `Manifest version '${manifestVersion}' is not supported. Please upgrade to a newer one.` + ); + } + + try { + return await fetchSchema(manifestVersion); + } catch (error) { + let supportedVersions; + + try { + const versionMap = await getUI5toManifestVersionMapping(); + supportedVersions = Object.values(versionMap); + } catch (error) { + log.warn(`Failed to fetch UI5 to manifest version mapping: ` + + `${error instanceof Error ? error.message : String(error)}`); + }; + + // try to hint which versions are supported + if (supportedVersions && !supportedVersions.includes(manifestVersion)) { + throw new Error( + `Failed to fetch schema for manifest version '${manifestVersion}': ` + + `This version is not supported. ` + + `Supported versions are: ${supportedVersions.join(", ")}. ` + + `${error instanceof Error ? error.message : String(error)}` + ); + } + + throw new Error( + `Failed to fetch schema for manifest version '${manifestVersion}': ` + + `${error instanceof Error ? error.message : String(error)}` + ); + } +} + +/** + * Get the manifest version from the manifest object. + * @param manifest The manifest object + * @returns The manifest version + * @throws Error if the manifest version is missing or invalid + */ +export function getManifestVersion(manifest: object) { + if (!("_version" in manifest)) { + throw new Error("Manifest does not contain a '_version' property."); + } + + if (typeof manifest._version !== "string") { + throw new Error("Manifest '_version' property is not a string."); + } + + if (!semver.valid(manifest._version)) { + throw new Error("Manifest '_version' property is not a valid semantic version."); + } + + return manifest._version; +} + +/** + * @returns The latest manifest version + */ export async function getLatestManifestVersion() { const versionMap = await getUI5toManifestVersionMapping(); @@ -63,11 +136,3 @@ export async function getLatestManifestVersion() { return versionMap.latest; } - -export async function getManifestSchema(manifestVersion: string) { - if (manifestVersion !== "latest") { - throw new Error(`Only 'latest' manifest version is supported, but got '${manifestVersion}'.`); - } - - return await fetchSchema(manifestVersion); -} diff --git a/test/fixtures/manifest_validation/missing-version-manifest.json b/test/fixtures/manifest_validation/missing-version-manifest.json new file mode 100644 index 00000000..2b73004a --- /dev/null +++ b/test/fixtures/manifest_validation/missing-version-manifest.json @@ -0,0 +1,25 @@ +{ + "sap.app": { + "id": "com.example.app", + "type": "application", + "applicationVersion": { + "version": "1.0.0" + } + }, + "sap.ui": { + "technology": "UI5", + "deviceTypes": { + "desktop": true, + "tablet": true, + "phone": true + } + }, + "sap.ui5": { + "dependencies": { + "minUI5Version": "1.120.0", + "libs": { + "sap.m": {} + } + } + } +} diff --git a/test/lib/tools/run_manifest_validation/runValidation.integration.ts b/test/lib/tools/run_manifest_validation/runValidation.integration.ts index 15eeab3b..06076aa9 100644 --- a/test/lib/tools/run_manifest_validation/runValidation.integration.ts +++ b/test/lib/tools/run_manifest_validation/runValidation.integration.ts @@ -7,23 +7,26 @@ import {fileURLToPath} from "url"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const fixturesPath = path.join(__dirname, "..", "..", "..", "fixtures", "manifest_validation"); +const schemaFixture = JSON.parse(await readFile(path.join(fixturesPath, "schema.json"), "utf-8")); const test = anyTest as TestFn<{ sinon: sinon.SinonSandbox; runValidation: typeof import("../../../../src/tools/run_manifest_validation/runValidation.js").default; + fetchCdnStub: sinon.SinonStub; }>; test.beforeEach(async (t) => { t.context.sinon = sinon.createSandbox(); - const schemaFixture = await readFile(path.join(fixturesPath, "schema.json"), "utf-8"); - const getManifestSchemaStub = t.context.sinon.stub().resolves(JSON.parse(schemaFixture)); + t.context.fetchCdnStub = t.context.sinon.stub(); - // Import the runValidation function + // Import the runValidation function with cdnHelper mocked globally t.context.runValidation = (await esmock( - "../../../../src/tools/run_manifest_validation/runValidation.js", { - "../../../../src/utils/ui5Manifest.js": { - getManifestSchema: getManifestSchemaStub, + "../../../../src/tools/run_manifest_validation/runValidation.js", + {}, + { + "../../../../src/utils/cdnHelper.js": { + fetchCdn: t.context.fetchCdnStub, }, } )).default; @@ -34,7 +37,15 @@ test.afterEach.always((t) => { }); test("runValidation successfully validates valid manifest", async (t) => { - const {runValidation} = t.context; + const {runValidation, fetchCdnStub} = t.context; + + fetchCdnStub.withArgs("https://raw.githubusercontent.com/SAP/ui5-manifest/main/mapping.json") + .resolves({ + "1.59.0": "1.59.0", + }); + + fetchCdnStub.withArgs("https://raw.githubusercontent.com/SAP/ui5-manifest/v1.59.0/schema.json") + .resolves(schemaFixture); const result = await runValidation(path.join(fixturesPath, "valid-manifest.json")); @@ -43,3 +54,58 @@ test("runValidation successfully validates valid manifest", async (t) => { errors: [], }); }); + +test("runValidation successfully validates valid manifest after first attempt ending with exception", async (t) => { + const {runValidation, fetchCdnStub} = t.context; + + fetchCdnStub.withArgs("https://raw.githubusercontent.com/SAP/ui5-manifest/main/mapping.json") + .resolves({ + "1.59.0": "1.59.0", + }); + + fetchCdnStub.withArgs("https://raw.githubusercontent.com/SAP/ui5-manifest/v1.59.0/schema.json") + .resolves(schemaFixture); + + await t.throwsAsync(async () => { + await runValidation(path.join(fixturesPath, "missing-version-manifest.json")); + }, { + message: "Manifest does not contain a '_version' property.", + }); + + const result = await runValidation(path.join(fixturesPath, "valid-manifest.json")); + + t.deepEqual(result, { + isValid: true, + errors: [], + }); +}); + +test("runValidation successfully validates valid manifest after first attempt ending with schema fetch error", + async (t) => { + const {runValidation, fetchCdnStub} = t.context; + + fetchCdnStub.withArgs("https://raw.githubusercontent.com/SAP/ui5-manifest/main/mapping.json") + .resolves({ + "1.59.0": "1.59.0", + }); + + fetchCdnStub.withArgs("https://raw.githubusercontent.com/SAP/ui5-manifest/v1.59.0/schema.json") + .onFirstCall() + .rejects(new Error("Failed to fetch schema")) + .onSecondCall() + .resolves(schemaFixture); + + await t.throwsAsync(async () => { + await runValidation(path.join(fixturesPath, "valid-manifest.json")); + }, { + message: "Failed to fetch schema for manifest version '1.59.0': Failed to fetch schema", + }); + + const result = await runValidation(path.join(fixturesPath, "valid-manifest.json")); + + t.deepEqual(result, { + isValid: true, + errors: [], + }); + } +); diff --git a/test/lib/tools/run_manifest_validation/runValidation.ts b/test/lib/tools/run_manifest_validation/runValidation.ts index bfe206d9..8170e285 100644 --- a/test/lib/tools/run_manifest_validation/runValidation.ts +++ b/test/lib/tools/run_manifest_validation/runValidation.ts @@ -59,6 +59,7 @@ test("runValidation successfully validates valid manifest", async (t) => { // Stub the readFile function to return a valid manifest const validManifest = { + "_version": "1.0.0", "sap.app": { id: "my.app.id", type: "application", @@ -69,6 +70,7 @@ test("runValidation successfully validates valid manifest", async (t) => { getManifestSchemaStub.resolves({ type: "object", properties: { + "_version": {type: "string"}, "sap.app": { type: "object", properties: { @@ -79,6 +81,7 @@ test("runValidation successfully validates valid manifest", async (t) => { }, }, required: ["sap.app"], + additionalProperties: false, }); const result = await runValidation("/path/to/manifest.json"); @@ -94,6 +97,7 @@ test("runValidation successfully validates invalid manifest", async (t) => { // Stub the readFile function to return an invalid manifest const invalidManifest = { + "_version": "1.0.0", "sap.app": { id: "my.app.id", // Missing required field "type" @@ -104,6 +108,7 @@ test("runValidation successfully validates invalid manifest", async (t) => { getManifestSchemaStub.resolves({ type: "object", properties: { + "_version": {type: "string"}, "sap.app": { type: "object", properties: { @@ -178,7 +183,9 @@ test("runValidation throws error when manifest file content is invalid JSON", as test("runValidation throws error when schema validation function cannot be compiled", async (t) => { const {runValidation, getManifestSchemaStub} = t.context; - t.context.manifestFileContent = JSON.stringify({}); + t.context.manifestFileContent = JSON.stringify({ + _version: "1.0.0", + }); getManifestSchemaStub.resolves(null); // Simulate invalid schema await t.throwsAsync(async () => { @@ -193,6 +200,7 @@ test("runValidation successfully validates valid manifest against external schem const {runValidation, getManifestSchemaStub, fetchCdnStub} = t.context; t.context.manifestFileContent = JSON.stringify({ + "_version": "1.0.0", "sap.app": { id: "my.app.id", type: "application", @@ -203,11 +211,13 @@ test("runValidation successfully validates valid manifest against external schem getManifestSchemaStub.resolves({ type: "object", properties: { + "_version": {type: "string"}, "sap.app": { $ref: "externalSchema.json", }, }, required: ["sap.app"], + additionalProperties: false, }); // Stub the readFile function to return the external schema when requested @@ -234,6 +244,7 @@ test("runValidation throws error when external schema cannot be fetched", async const {runValidation, getManifestSchemaStub, fetchCdnStub} = t.context; t.context.manifestFileContent = JSON.stringify({ + "_version": "1.0.0", "sap.app": { id: "my.app.id", type: "application", @@ -244,11 +255,13 @@ test("runValidation throws error when external schema cannot be fetched", async getManifestSchemaStub.resolves({ type: "object", properties: { + "_version": {type: "string"}, "sap.app": { $ref: "externalSchema.json", }, }, required: ["sap.app"], + additionalProperties: false, }); // Stub the fetchCdn function to throw an error when fetching the external schema diff --git a/test/lib/utils/ui5Manifest.ts b/test/lib/utils/ui5Manifest.ts index e3905af1..6c85df15 100644 --- a/test/lib/utils/ui5Manifest.ts +++ b/test/lib/utils/ui5Manifest.ts @@ -98,28 +98,38 @@ test("getLatestManifestVersion handles missing latest version", async (t) => { t.true(fetchCdnStub.calledOnce); }); -test("getManifestSchema throws error for unsupported versions", async (t) => { +test("getManifestSchema throws error for unsupported versions 1.x.x versions", async (t) => { const {getManifestSchema} = t.context; await t.throwsAsync( async () => { - await getManifestSchema("1.78.0"); + await getManifestSchema("1.47.0"); }, { - message: "Only 'latest' manifest version is supported, but got '1.78.0'.", + message: "Manifest version '1.47.0' is not supported. Please upgrade to a newer one.", } ); + + await t.notThrowsAsync(async () => { + await getManifestSchema("1.48.0"); + }); + + await t.notThrowsAsync(async () => { + await getManifestSchema("2.0.0"); + }); }); -test("getManifestSchema fetches schema for 'latest' version", async (t) => { +test("getManifestSchema fetches schema for specific version", async (t) => { const {fetchCdnStub, getManifestSchema} = t.context; const mockSchema = { $schema: "http://json-schema.org/draft-07/schema#", type: "object", }; - fetchCdnStub.resolves(mockSchema); - const schema = await getManifestSchema("latest"); + fetchCdnStub.withArgs("https://raw.githubusercontent.com/SAP/ui5-manifest/v1.48.0/schema.json") + .resolves(mockSchema); + + const schema = await getManifestSchema("1.48.0"); t.deepEqual(schema, mockSchema); t.true(fetchCdnStub.calledOnce); @@ -131,10 +141,12 @@ test("getManifestSchema uses cache on subsequent calls", async (t) => { $schema: "http://json-schema.org/draft-07/schema#", type: "object", }; - fetchCdnStub.resolves(mockSchema); - const schema1 = await getManifestSchema("latest"); - const schema2 = await getManifestSchema("latest"); + fetchCdnStub.withArgs("https://raw.githubusercontent.com/SAP/ui5-manifest/v1.48.0/schema.json") + .resolves(mockSchema); + + const schema1 = await getManifestSchema("1.48.0"); + const schema2 = await getManifestSchema("1.48.0"); t.deepEqual(schema1, mockSchema); t.deepEqual(schema2, mockSchema); @@ -145,15 +157,44 @@ test("getManifestSchema handles fetch errors", async (t) => { const {fetchCdnStub, getManifestSchema} = t.context; // Mock fetch error - fetchCdnStub.rejects(new Error("Network error")); + fetchCdnStub.withArgs("https://raw.githubusercontent.com/SAP/ui5-manifest/main/mapping.json") + .rejects(new Error("Mapping.json error")); + + fetchCdnStub.withArgs("https://raw.githubusercontent.com/SAP/ui5-manifest/v1.48.0/schema.json") + .rejects(new Error("Network error")); await t.throwsAsync( async () => { - await getManifestSchema("latest"); + await getManifestSchema("1.48.0"); }, { - message: "Network error", + message: "Failed to fetch schema for manifest version '1.48.0': Network error", } ); - t.true(fetchCdnStub.calledOnce); + t.true(fetchCdnStub.calledTwice); +}); + +test("getManifestSchema handles fetch errors and gives more details about supported versions", async (t) => { + const {fetchCdnStub, getManifestSchema} = t.context; + + // Mock fetch error + fetchCdnStub.withArgs("https://raw.githubusercontent.com/SAP/ui5-manifest/main/mapping.json") + .resolves({ + "1.49.0": "1.49.0", + "1.50.0": "1.50.0", + }); + + fetchCdnStub.withArgs("https://raw.githubusercontent.com/SAP/ui5-manifest/v1.48.0/schema.json") + .rejects(new Error("Network error")); + + await t.throwsAsync( + async () => { + await getManifestSchema("1.48.0"); + }, + { + message: "Failed to fetch schema for manifest version '1.48.0': " + + "This version is not supported. Supported versions are: 1.49.0, 1.50.0. Network error", + } + ); + t.true(fetchCdnStub.calledTwice); }); From 71032b291a32ea127f0a6285d76b839e9887786e Mon Sep 17 00:00:00 2001 From: Petar Dimov Date: Mon, 17 Nov 2025 13:44:49 +0200 Subject: [PATCH 15/31] fix(runValidation): Properly release mutex --- src/tools/run_manifest_validation/runValidation.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/tools/run_manifest_validation/runValidation.ts b/src/tools/run_manifest_validation/runValidation.ts index 75716e76..6af7b9f5 100644 --- a/src/tools/run_manifest_validation/runValidation.ts +++ b/src/tools/run_manifest_validation/runValidation.ts @@ -27,12 +27,12 @@ async function createUI5ManifestValidateFunction(ui5Schema: object) { loadSchema: async (uri) => { const release = await fetchSchemaMutex.acquire(); - if (schemaCache.has(uri)) { - log.info(`Loading cached schema: ${uri}`); - return schemaCache.get(uri)!; - } - try { + if (schemaCache.has(uri)) { + log.info(`Loading cached schema: ${uri}`); + return schemaCache.get(uri)!; + } + log.info(`Loading external schema: ${uri}`); const schema = await fetchCdn(uri) as AnySchemaObject; From ee562f36ce7045ff1f0d4f17d5404a5fd96057c6 Mon Sep 17 00:00:00 2001 From: Petar Dimov Date: Mon, 17 Nov 2025 15:07:35 +0200 Subject: [PATCH 16/31] fix(ui5Manifest): Throw errors for versions lt 1.68.0 --- src/utils/ui5Manifest.ts | 46 +++++++-------- .../manifest_validation/valid-manifest.json | 2 +- .../runValidation.integration.ts | 15 ++--- test/lib/utils/ui5Manifest.ts | 58 ++++++++++++------- 4 files changed, 69 insertions(+), 52 deletions(-) diff --git a/src/utils/ui5Manifest.ts b/src/utils/ui5Manifest.ts index 8aa68b2e..a92f8000 100644 --- a/src/utils/ui5Manifest.ts +++ b/src/utils/ui5Manifest.ts @@ -59,15 +59,33 @@ async function fetchSchema(manifestVersion: string) { } } +async function failWithSupportedVersionsHint(errorMessage: string): Promise { + let supportedVersions; + + try { + const versionMap = await getUI5toManifestVersionMapping(); + supportedVersions = Object.values(versionMap).filter((version) => semver.gte(version, "1.68.0")); + } catch (_) { + supportedVersions = null; + }; + + throw new Error( + errorMessage + + (supportedVersions ? + `\nSupported versions are: ${supportedVersions.join(", ")}.` : + "") + ); +} + /** * Get the manifest schema for a specific manifest version. * @param manifestVersion The manifest version * @returns The manifest schema * @throws Error if the manifest version is unsupported */ -export async function getManifestSchema(manifestVersion: string) { - if (semver.lt(manifestVersion, "1.48.0")) { - throw new Error( +export async function getManifestSchema(manifestVersion: string): Promise { + if (semver.lt(manifestVersion, "1.68.0")) { + return failWithSupportedVersionsHint( `Manifest version '${manifestVersion}' is not supported. Please upgrade to a newer one.` ); } @@ -75,27 +93,7 @@ export async function getManifestSchema(manifestVersion: string) { try { return await fetchSchema(manifestVersion); } catch (error) { - let supportedVersions; - - try { - const versionMap = await getUI5toManifestVersionMapping(); - supportedVersions = Object.values(versionMap); - } catch (error) { - log.warn(`Failed to fetch UI5 to manifest version mapping: ` + - `${error instanceof Error ? error.message : String(error)}`); - }; - - // try to hint which versions are supported - if (supportedVersions && !supportedVersions.includes(manifestVersion)) { - throw new Error( - `Failed to fetch schema for manifest version '${manifestVersion}': ` + - `This version is not supported. ` + - `Supported versions are: ${supportedVersions.join(", ")}. ` + - `${error instanceof Error ? error.message : String(error)}` - ); - } - - throw new Error( + return failWithSupportedVersionsHint( `Failed to fetch schema for manifest version '${manifestVersion}': ` + `${error instanceof Error ? error.message : String(error)}` ); diff --git a/test/fixtures/manifest_validation/valid-manifest.json b/test/fixtures/manifest_validation/valid-manifest.json index 69fca7c7..837b4c1b 100644 --- a/test/fixtures/manifest_validation/valid-manifest.json +++ b/test/fixtures/manifest_validation/valid-manifest.json @@ -1,5 +1,5 @@ { - "_version": "1.59.0", + "_version": "1.79.0", "sap.app": { "id": "com.example.app", "type": "application", diff --git a/test/lib/tools/run_manifest_validation/runValidation.integration.ts b/test/lib/tools/run_manifest_validation/runValidation.integration.ts index 06076aa9..20ae8ef7 100644 --- a/test/lib/tools/run_manifest_validation/runValidation.integration.ts +++ b/test/lib/tools/run_manifest_validation/runValidation.integration.ts @@ -41,10 +41,10 @@ test("runValidation successfully validates valid manifest", async (t) => { fetchCdnStub.withArgs("https://raw.githubusercontent.com/SAP/ui5-manifest/main/mapping.json") .resolves({ - "1.59.0": "1.59.0", + "1.79.0": "1.79.0", }); - fetchCdnStub.withArgs("https://raw.githubusercontent.com/SAP/ui5-manifest/v1.59.0/schema.json") + fetchCdnStub.withArgs("https://raw.githubusercontent.com/SAP/ui5-manifest/v1.79.0/schema.json") .resolves(schemaFixture); const result = await runValidation(path.join(fixturesPath, "valid-manifest.json")); @@ -60,10 +60,10 @@ test("runValidation successfully validates valid manifest after first attempt en fetchCdnStub.withArgs("https://raw.githubusercontent.com/SAP/ui5-manifest/main/mapping.json") .resolves({ - "1.59.0": "1.59.0", + "1.79.0": "1.79.0", }); - fetchCdnStub.withArgs("https://raw.githubusercontent.com/SAP/ui5-manifest/v1.59.0/schema.json") + fetchCdnStub.withArgs("https://raw.githubusercontent.com/SAP/ui5-manifest/v1.79.0/schema.json") .resolves(schemaFixture); await t.throwsAsync(async () => { @@ -86,10 +86,10 @@ test("runValidation successfully validates valid manifest after first attempt en fetchCdnStub.withArgs("https://raw.githubusercontent.com/SAP/ui5-manifest/main/mapping.json") .resolves({ - "1.59.0": "1.59.0", + "1.79.0": "1.79.0", }); - fetchCdnStub.withArgs("https://raw.githubusercontent.com/SAP/ui5-manifest/v1.59.0/schema.json") + fetchCdnStub.withArgs("https://raw.githubusercontent.com/SAP/ui5-manifest/v1.79.0/schema.json") .onFirstCall() .rejects(new Error("Failed to fetch schema")) .onSecondCall() @@ -98,7 +98,8 @@ test("runValidation successfully validates valid manifest after first attempt en await t.throwsAsync(async () => { await runValidation(path.join(fixturesPath, "valid-manifest.json")); }, { - message: "Failed to fetch schema for manifest version '1.59.0': Failed to fetch schema", + message: "Failed to fetch schema for manifest version '1.79.0': Failed to fetch schema" + + "\nSupported versions are: 1.79.0.", }); const result = await runValidation(path.join(fixturesPath, "valid-manifest.json")); diff --git a/test/lib/utils/ui5Manifest.ts b/test/lib/utils/ui5Manifest.ts index 6c85df15..68ee7df1 100644 --- a/test/lib/utils/ui5Manifest.ts +++ b/test/lib/utils/ui5Manifest.ts @@ -99,19 +99,37 @@ test("getLatestManifestVersion handles missing latest version", async (t) => { }); test("getManifestSchema throws error for unsupported versions 1.x.x versions", async (t) => { - const {getManifestSchema} = t.context; + const {getManifestSchema, fetchCdnStub} = t.context; await t.throwsAsync( async () => { - await getManifestSchema("1.47.0"); + await getManifestSchema("1.67.0"); }, { - message: "Manifest version '1.47.0' is not supported. Please upgrade to a newer one.", + message: "Manifest version '1.67.0' is not supported. Please upgrade to a newer one.", + } + ); + + fetchCdnStub.withArgs("https://raw.githubusercontent.com/SAP/ui5-manifest/main/mapping.json") + .resolves({ + "1.55.0": "1.55.0", + "1.67.0": "1.67.0", + "1.68.0": "1.68.0", + "1.69.0": "1.69.0", + }); + + await t.throwsAsync( + async () => { + await getManifestSchema("1.45.0"); + }, + { + message: "Manifest version '1.45.0' is not supported. Please upgrade to a newer one." + + "\nSupported versions are: 1.68.0, 1.69.0.", } ); await t.notThrowsAsync(async () => { - await getManifestSchema("1.48.0"); + await getManifestSchema("1.68.0"); }); await t.notThrowsAsync(async () => { @@ -122,14 +140,14 @@ test("getManifestSchema throws error for unsupported versions 1.x.x versions", a test("getManifestSchema fetches schema for specific version", async (t) => { const {fetchCdnStub, getManifestSchema} = t.context; const mockSchema = { - $schema: "http://json-schema.org/draft-07/schema#", + $schema: "https://json-schema.org/draft/2020-12/schema", type: "object", }; - fetchCdnStub.withArgs("https://raw.githubusercontent.com/SAP/ui5-manifest/v1.48.0/schema.json") + fetchCdnStub.withArgs("https://raw.githubusercontent.com/SAP/ui5-manifest/v1.68.0/schema.json") .resolves(mockSchema); - const schema = await getManifestSchema("1.48.0"); + const schema = await getManifestSchema("1.68.0"); t.deepEqual(schema, mockSchema); t.true(fetchCdnStub.calledOnce); @@ -138,15 +156,15 @@ test("getManifestSchema fetches schema for specific version", async (t) => { test("getManifestSchema uses cache on subsequent calls", async (t) => { const {fetchCdnStub, getManifestSchema} = t.context; const mockSchema = { - $schema: "http://json-schema.org/draft-07/schema#", + $schema: "https://json-schema.org/draft/2020-12/schema", type: "object", }; - fetchCdnStub.withArgs("https://raw.githubusercontent.com/SAP/ui5-manifest/v1.48.0/schema.json") + fetchCdnStub.withArgs("https://raw.githubusercontent.com/SAP/ui5-manifest/v1.68.0/schema.json") .resolves(mockSchema); - const schema1 = await getManifestSchema("1.48.0"); - const schema2 = await getManifestSchema("1.48.0"); + const schema1 = await getManifestSchema("1.68.0"); + const schema2 = await getManifestSchema("1.68.0"); t.deepEqual(schema1, mockSchema); t.deepEqual(schema2, mockSchema); @@ -160,15 +178,15 @@ test("getManifestSchema handles fetch errors", async (t) => { fetchCdnStub.withArgs("https://raw.githubusercontent.com/SAP/ui5-manifest/main/mapping.json") .rejects(new Error("Mapping.json error")); - fetchCdnStub.withArgs("https://raw.githubusercontent.com/SAP/ui5-manifest/v1.48.0/schema.json") + fetchCdnStub.withArgs("https://raw.githubusercontent.com/SAP/ui5-manifest/v1.68.0/schema.json") .rejects(new Error("Network error")); await t.throwsAsync( async () => { - await getManifestSchema("1.48.0"); + await getManifestSchema("1.68.0"); }, { - message: "Failed to fetch schema for manifest version '1.48.0': Network error", + message: "Failed to fetch schema for manifest version '1.68.0': Network error", } ); t.true(fetchCdnStub.calledTwice); @@ -180,20 +198,20 @@ test("getManifestSchema handles fetch errors and gives more details about suppor // Mock fetch error fetchCdnStub.withArgs("https://raw.githubusercontent.com/SAP/ui5-manifest/main/mapping.json") .resolves({ - "1.49.0": "1.49.0", - "1.50.0": "1.50.0", + "1.70.0": "1.70.0", + "1.71.0": "1.71.0", }); - fetchCdnStub.withArgs("https://raw.githubusercontent.com/SAP/ui5-manifest/v1.48.0/schema.json") + fetchCdnStub.withArgs("https://raw.githubusercontent.com/SAP/ui5-manifest/v1.69.0/schema.json") .rejects(new Error("Network error")); await t.throwsAsync( async () => { - await getManifestSchema("1.48.0"); + await getManifestSchema("1.69.0"); }, { - message: "Failed to fetch schema for manifest version '1.48.0': " + - "This version is not supported. Supported versions are: 1.49.0, 1.50.0. Network error", + message: "Failed to fetch schema for manifest version '1.69.0': Network error" + + "\nSupported versions are: 1.70.0, 1.71.0.", } ); t.true(fetchCdnStub.calledTwice); From e633b9db80fc598156eab8ef45173078c5ce8b94 Mon Sep 17 00:00:00 2001 From: Petar Dimov Date: Mon, 17 Nov 2025 15:29:37 +0200 Subject: [PATCH 17/31] fix(runValidation): Include ajv-formats --- npm-shrinkwrap.json | 1 + package.json | 1 + src/tools/run_manifest_validation/runValidation.ts | 4 ++++ 3 files changed, 6 insertions(+) diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 5776cbec..54f91890 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -14,6 +14,7 @@ "@ui5/logger": "^4.0.2", "@ui5/project": "^4.0.9", "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", "async-mutex": "^0.5.0", "ejs": "^3.1.10", "execa": "^9.6.1", diff --git a/package.json b/package.json index d2c8cd24..d3e9ea4c 100644 --- a/package.json +++ b/package.json @@ -80,6 +80,7 @@ "@ui5/logger": "^4.0.2", "@ui5/project": "^4.0.9", "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", "async-mutex": "^0.5.0", "ejs": "^3.1.10", "execa": "^9.6.1", diff --git a/src/tools/run_manifest_validation/runValidation.ts b/src/tools/run_manifest_validation/runValidation.ts index 6af7b9f5..0e958e1f 100644 --- a/src/tools/run_manifest_validation/runValidation.ts +++ b/src/tools/run_manifest_validation/runValidation.ts @@ -1,6 +1,7 @@ import {fetchCdn} from "../../utils/cdnHelper.js"; import {RunSchemaValidationResult} from "./schema.js"; import Ajv2020, {AnySchemaObject} from "ajv/dist/2020.js"; +import addFormats from "ajv-formats"; import {readFile} from "fs/promises"; import {getLogger} from "@ui5/logger"; import {InvalidInputError} from "../../utils.js"; @@ -58,6 +59,9 @@ async function createUI5ManifestValidateFunction(ui5Schema: object) { } }, }); + + addFormats.default(ajv); + const draft06MetaSchema = JSON.parse( await readFile(AJV_SCHEMA_PATHS.draft06, "utf-8") ) as AnySchemaObject; From 86ec80138dd56560de32184194652fe26f0a3ca0 Mon Sep 17 00:00:00 2001 From: Petar Dimov Date: Mon, 17 Nov 2025 15:46:41 +0200 Subject: [PATCH 18/31] refactor(ui5Manifest): Enhance more errors with supported versions info --- src/tools/run_manifest_validation/runValidation.ts | 2 +- src/utils/ui5Manifest.ts | 8 ++++---- .../run_manifest_validation/runValidation.integration.ts | 3 ++- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/tools/run_manifest_validation/runValidation.ts b/src/tools/run_manifest_validation/runValidation.ts index 0e958e1f..99a9abcf 100644 --- a/src/tools/run_manifest_validation/runValidation.ts +++ b/src/tools/run_manifest_validation/runValidation.ts @@ -110,7 +110,7 @@ export default async function runValidation(manifestPath: string): Promise { if (!("_version" in manifest)) { - throw new Error("Manifest does not contain a '_version' property."); + return failWithSupportedVersionsHint("Manifest does not contain a '_version' property."); } if (typeof manifest._version !== "string") { - throw new Error("Manifest '_version' property is not a string."); + return failWithSupportedVersionsHint("Manifest '_version' property is not a string."); } if (!semver.valid(manifest._version)) { - throw new Error("Manifest '_version' property is not a valid semantic version."); + return failWithSupportedVersionsHint("Manifest '_version' property is not a valid semantic version."); } return manifest._version; diff --git a/test/lib/tools/run_manifest_validation/runValidation.integration.ts b/test/lib/tools/run_manifest_validation/runValidation.integration.ts index 20ae8ef7..f26bad93 100644 --- a/test/lib/tools/run_manifest_validation/runValidation.integration.ts +++ b/test/lib/tools/run_manifest_validation/runValidation.integration.ts @@ -69,7 +69,8 @@ test("runValidation successfully validates valid manifest after first attempt en await t.throwsAsync(async () => { await runValidation(path.join(fixturesPath, "missing-version-manifest.json")); }, { - message: "Manifest does not contain a '_version' property.", + message: "Manifest does not contain a '_version' property." + + "\nSupported versions are: 1.79.0.", }); const result = await runValidation(path.join(fixturesPath, "valid-manifest.json")); From 595268b6435936f0acae1d1657a54d66240ee668 Mon Sep 17 00:00:00 2001 From: Petar Dimov Date: Mon, 24 Nov 2025 15:01:09 +0200 Subject: [PATCH 19/31] test(runValidation): Increase coverage for external schemas --- .../run_manifest_validation/runValidation.ts | 91 +++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/test/lib/tools/run_manifest_validation/runValidation.ts b/test/lib/tools/run_manifest_validation/runValidation.ts index 8170e285..be942737 100644 --- a/test/lib/tools/run_manifest_validation/runValidation.ts +++ b/test/lib/tools/run_manifest_validation/runValidation.ts @@ -275,3 +275,94 @@ test("runValidation throws error when external schema cannot be fetched", async message: /Failed to create UI5 manifest validate function: .+/, }); }); + +test("runValidation uses cache on subsequent calls for external schemas", async (t) => { + const {runValidation, getManifestSchemaStub, fetchCdnStub} = t.context; + + t.context.manifestFileContent = JSON.stringify({ + "_version": "1.0.0", + "sap.app": { + id: "my.app.id", + type: "application", + }, + }); + + // Schema that references an external schema + getManifestSchemaStub.resolves({ + type: "object", + properties: { + "_version": {type: "string"}, + "sap.app": { + $ref: "externalSchema.json", + }, + }, + required: ["sap.app"], + additionalProperties: false, + }); + + // Stub the fetchCdn function to return the external schema when requested + const externalSchema = { + type: "object", + properties: { + id: {type: "string"}, + type: {type: "string"}, + }, + required: ["id", "type"], + }; + fetchCdnStub.withArgs("externalSchema.json") + .resolves(externalSchema); + + const result1 = await runValidation("/path/to/manifest.json"); + const result2 = await runValidation("/path/to/manifest.json"); + + t.deepEqual(result1, { + isValid: true, + errors: [], + }); + t.deepEqual(result2, { + isValid: true, + errors: [], + }); + t.true(fetchCdnStub.calledOnce); // External schema fetched only once +}); + +test("runValidation patches external adaptive-card.json schema", async (t) => { + const {runValidation, getManifestSchemaStub, fetchCdnStub} = t.context; + + t.context.manifestFileContent = JSON.stringify({ + _version: "1.0.0", + adaptiveCards: { + type: "AdaptiveCard", + }, + }); + + // Schema that references the adaptive-card.json schema + getManifestSchemaStub.resolves({ + type: "object", + properties: { + _version: {type: "string"}, + adaptiveCards: { + $ref: "https://adaptivecards.io/schemas/adaptive-card.json", + }, + }, + }); + + // Stub the fetchCdn function to return the adaptive-card.json schema when requested + const adaptiveCardSchema = { + type: "object", + id: "https://adaptivecards.io/schemas/adaptive-card.json", // Note the "id" property + properties: { + type: {type: "string"}, + }, + required: ["type"], + }; + fetchCdnStub.withArgs("https://adaptivecards.io/schemas/adaptive-card.json") + .resolves(adaptiveCardSchema); + + const result = await runValidation("/path/to/manifest.json"); + + t.deepEqual(result, { + isValid: true, + errors: [], + }); +}); From d1b82909a5156dec597e497fd88b1c1f131da3d9 Mon Sep 17 00:00:00 2001 From: Petar Dimov Date: Mon, 24 Nov 2025 15:35:44 +0200 Subject: [PATCH 20/31] refactor(runValidation): Add explanation why meta schemas are needed --- src/tools/run_manifest_validation/runValidation.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/tools/run_manifest_validation/runValidation.ts b/src/tools/run_manifest_validation/runValidation.ts index 99a9abcf..5f427d46 100644 --- a/src/tools/run_manifest_validation/runValidation.ts +++ b/src/tools/run_manifest_validation/runValidation.ts @@ -69,6 +69,9 @@ async function createUI5ManifestValidateFunction(ui5Schema: object) { await readFile(AJV_SCHEMA_PATHS.draft07, "utf-8") ) as AnySchemaObject; + // Add meta-schemas for draft-06 and draft-07. + // These are required to support schemas that reference these drafts, + // for example the Adaptive Card schema and some sap.bpa.task properties. ajv.addMetaSchema(draft06MetaSchema, "http://json-schema.org/draft-06/schema#"); ajv.addMetaSchema(draft07MetaSchema, "http://json-schema.org/draft-07/schema#"); From 9bae0aadd2cd1b6f2ab6605211af694574731dde Mon Sep 17 00:00:00 2001 From: Petar Dimov Date: Mon, 24 Nov 2025 15:49:21 +0200 Subject: [PATCH 21/31] refactor(runValidation): Add explanation for unicodeRegExp setting --- src/tools/run_manifest_validation/runValidation.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/tools/run_manifest_validation/runValidation.ts b/src/tools/run_manifest_validation/runValidation.ts index 5f427d46..c9f5a42a 100644 --- a/src/tools/run_manifest_validation/runValidation.ts +++ b/src/tools/run_manifest_validation/runValidation.ts @@ -24,7 +24,8 @@ async function createUI5ManifestValidateFunction(ui5Schema: object) { const ajv = new Ajv2020.default({ allErrors: true, // Collect all errors, not just the first one strict: false, // Allow additional properties that are not in schema - unicodeRegExp: false, + unicodeRegExp: false, // Don't use Unicode-aware regular expressions, + // otherwise compilation fails with "Invalid escape" errors loadSchema: async (uri) => { const release = await fetchSchemaMutex.acquire(); From 615ef9eab9e5ce04e74b24ffa0380e4d74f436c1 Mon Sep 17 00:00:00 2001 From: Petar Dimov Date: Mon, 24 Nov 2025 16:40:24 +0200 Subject: [PATCH 22/31] refactor(runValidation): Add coment why strict flag of ajv is off --- src/tools/run_manifest_validation/index.ts | 2 +- src/tools/run_manifest_validation/runValidation.ts | 10 +++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/tools/run_manifest_validation/index.ts b/src/tools/run_manifest_validation/index.ts index cb208002..d89e9c2f 100644 --- a/src/tools/run_manifest_validation/index.ts +++ b/src/tools/run_manifest_validation/index.ts @@ -10,7 +10,7 @@ export default function registerTool(registerTool: RegisterTool, context: Contex registerTool("run_manifest_validation", { title: "Manifest Validation", description: - "Validates UI5 manifest file." + + "Validates UI5 manifest file. " + "After making changes, you should always run the validation again " + "to verify that no new problems have been introduced.", annotations: { diff --git a/src/tools/run_manifest_validation/runValidation.ts b/src/tools/run_manifest_validation/runValidation.ts index c9f5a42a..1822b106 100644 --- a/src/tools/run_manifest_validation/runValidation.ts +++ b/src/tools/run_manifest_validation/runValidation.ts @@ -22,10 +22,14 @@ const AJV_SCHEMA_PATHS = { async function createUI5ManifestValidateFunction(ui5Schema: object) { try { const ajv = new Ajv2020.default({ - allErrors: true, // Collect all errors, not just the first one - strict: false, // Allow additional properties that are not in schema - unicodeRegExp: false, // Don't use Unicode-aware regular expressions, + // Collect all errors, not just the first one + allErrors: true, + // Allow additional properties that are not in schema such as "i18n", + // otherwise compilation fails + strict: false, + // Don't use Unicode-aware regular expressions, // otherwise compilation fails with "Invalid escape" errors + unicodeRegExp: false, loadSchema: async (uri) => { const release = await fetchSchemaMutex.acquire(); From 7e58636b390824e10d769c79645c3f88fc6645e3 Mon Sep 17 00:00:00 2001 From: Petar Dimov Date: Mon, 24 Nov 2025 17:14:04 +0200 Subject: [PATCH 23/31] refactor(ui5Manifest): Add comments for lowest supported version 1.68.0 --- src/utils/ui5Manifest.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/utils/ui5Manifest.ts b/src/utils/ui5Manifest.ts index 1d5a2e6f..c72dbe53 100644 --- a/src/utils/ui5Manifest.ts +++ b/src/utils/ui5Manifest.ts @@ -12,6 +12,9 @@ let UI5ToManifestVersionMapping: Record | null = null; const MAPPING_URL = "https://raw.githubusercontent.com/SAP/ui5-manifest/main/mapping.json"; const ui5ToManifestVersionMappingMutex = new Mutex(); +// Manifests prior to 1.68.0 use older meta-schema, which is not supported by the current implementation +const LOWEST_SUPPORTED_MANIFEST_VERSION = "1.68.0"; + function getSchemaURL(manifestVersion: string) { return `https://raw.githubusercontent.com/SAP/ui5-manifest/v${manifestVersion}/schema.json`; } @@ -64,7 +67,9 @@ async function failWithSupportedVersionsHint(errorMessage: string): Promise semver.gte(version, "1.68.0")); + supportedVersions = Object.values(versionMap).filter( + (version) => semver.gte(version, LOWEST_SUPPORTED_MANIFEST_VERSION) + ); } catch (_) { supportedVersions = null; }; @@ -84,7 +89,7 @@ async function failWithSupportedVersionsHint(errorMessage: string): Promise { - if (semver.lt(manifestVersion, "1.68.0")) { + if (semver.lt(manifestVersion, LOWEST_SUPPORTED_MANIFEST_VERSION)) { return failWithSupportedVersionsHint( `Manifest version '${manifestVersion}' is not supported. Please upgrade to a newer one.` ); From 31e4aac068f92ed1e608d774091e364385127c26 Mon Sep 17 00:00:00 2001 From: Petar Dimov Date: Tue, 25 Nov 2025 11:36:11 +0200 Subject: [PATCH 24/31] fix(run_manifest_validation): Mark the tool as read-only --- src/tools/run_manifest_validation/index.ts | 2 +- test/lib/tools/run_manifest_validation/index.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tools/run_manifest_validation/index.ts b/src/tools/run_manifest_validation/index.ts index d89e9c2f..f2e5e61d 100644 --- a/src/tools/run_manifest_validation/index.ts +++ b/src/tools/run_manifest_validation/index.ts @@ -15,7 +15,7 @@ export default function registerTool(registerTool: RegisterTool, context: Contex "to verify that no new problems have been introduced.", annotations: { title: "Manifest Validation", - readOnlyHint: false, + readOnlyHint: true, }, inputSchema, outputSchema, diff --git a/test/lib/tools/run_manifest_validation/index.ts b/test/lib/tools/run_manifest_validation/index.ts index 1b01d58d..6b5e11cc 100644 --- a/test/lib/tools/run_manifest_validation/index.ts +++ b/test/lib/tools/run_manifest_validation/index.ts @@ -74,7 +74,7 @@ test("registerRunManifestValidationTool registers the tool with correct paramete t.true(toolConfig?.title?.includes("Manifest Validation")); t.true(toolConfig?.description?.includes("Validates UI5 manifest file")); t.is(toolConfig?.annotations?.title, "Manifest Validation"); - t.false(toolConfig?.annotations?.readOnlyHint); + t.true(toolConfig?.annotations?.readOnlyHint); }); test("run_manifest_validation tool returns validation result on success", async (t) => { From e41177c48ac4d7bc85d667a9baa64dc7ee48adbd Mon Sep 17 00:00:00 2001 From: Petar Dimov Date: Tue, 25 Nov 2025 12:47:40 +0200 Subject: [PATCH 25/31] refactor(runValidation): Remove undefined properties from the result --- .../run_manifest_validation/runValidation.ts | 5 +-- src/tools/run_manifest_validation/schema.ts | 9 ------ .../run_manifest_validation/runValidation.ts | 31 +++++++++---------- 3 files changed, 16 insertions(+), 29 deletions(-) diff --git a/src/tools/run_manifest_validation/runValidation.ts b/src/tools/run_manifest_validation/runValidation.ts index 1822b106..489359e3 100644 --- a/src/tools/run_manifest_validation/runValidation.ts +++ b/src/tools/run_manifest_validation/runValidation.ts @@ -135,7 +135,7 @@ export default async function runValidation(manifestPath: string): Promise { + const errors = validationErrors.map((error): RunSchemaValidationResult["errors"][number] => { return { keyword: error.keyword ?? "", instancePath: error.instancePath ?? "", @@ -143,9 +143,6 @@ export default async function runValidation(manifestPath: string): Promise { const invalidManifest = { "_version": "1.0.0", "sap.app": { - id: "my.app.id", - // Missing required field "type" + Bad: "value", }, }; t.context.manifestFileContent = JSON.stringify(invalidManifest); @@ -108,18 +107,13 @@ test("runValidation successfully validates invalid manifest", async (t) => { getManifestSchemaStub.resolves({ type: "object", properties: { - "_version": {type: "string"}, "sap.app": { type: "object", - properties: { - id: {type: "string"}, - type: {type: "string"}, + propertyNames: { + pattern: "^[a-z]+$", // Enforce lowercase property names }, - required: ["id", "type"], }, }, - required: ["sap.app"], - additionalProperties: false, }); const result = await runValidation("/path/to/manifest.json"); @@ -128,15 +122,20 @@ test("runValidation successfully validates invalid manifest", async (t) => { isValid: false, errors: [ { - params: {missingProperty: "type"}, - keyword: "required", + params: {pattern: "^[a-z]+$"}, + keyword: "pattern", + instancePath: "/sap.app", + schemaPath: "#/properties/sap.app/propertyNames/pattern", + message: "must match pattern \"^[a-z]+$\"", + propertyName: "Bad", + }, + { + params: {propertyName: "Bad"}, + keyword: "propertyNames", instancePath: "/sap.app", - schemaPath: "#/properties/sap.app/required", - message: "must have required property 'type'", + schemaPath: "#/properties/sap.app/propertyNames", + message: "property name must be valid", propertyName: undefined, - schema: undefined, - parentSchema: undefined, - data: undefined, }, ], }); From a068ff048a201ef3f2555d145b255af3e80a1ee5 Mon Sep 17 00:00:00 2001 From: Petar Dimov Date: Fri, 19 Dec 2025 10:13:52 +0200 Subject: [PATCH 26/31] fix(schema.ts): Resolve ts error --- src/tools/run_manifest_validation/schema.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tools/run_manifest_validation/schema.ts b/src/tools/run_manifest_validation/schema.ts index 8aa7804e..2701eb48 100644 --- a/src/tools/run_manifest_validation/schema.ts +++ b/src/tools/run_manifest_validation/schema.ts @@ -16,7 +16,7 @@ export const outputSchema = { .describe("JSON Pointer to the location in the data instance (e.g., `/prop/1/subProp`)."), schemaPath: z.string() .describe("JSON Pointer to the location of the failing keyword in the schema."), - params: z.record(z.any()) + params: z.record(z.any(), z.any()) .describe("An object with additional information about the error."), propertyName: z.string() .optional() From 2f3e782b2717e18d8371a0a68498a5dbd4398a65 Mon Sep 17 00:00:00 2001 From: Petar Dimov Date: Fri, 19 Dec 2025 11:54:17 +0200 Subject: [PATCH 27/31] docs(architecture.md): Describe all cards tools --- docs/architecture.md | 114 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 114 insertions(+) diff --git a/docs/architecture.md b/docs/architecture.md index e319ebf0..0d09bb0d 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -78,6 +78,15 @@ Tool Name | Description `get_typescript_conversion_guidelines` | Provides guidelines for converting UI5 applications and controls from JavaScript to TypeScript `get_version_info` | Retrieves version information for the UI5 framework `run_ui5_linter` | Integrates with `@ui5/linter` to analyze and report issues in UI5 code +`run_manifest_validation` | Validates the manifest against the UI5 Manifest schema + +### create_integration_card + +The `create_integration_card` tool is designed to scaffold new UI Integration Cards using predefined templates. It automates the setup process, ensuring that developers can quickly start building integration cards without manually configuring the project structure. + +Templates are stored in the `resources/` directory of the project. There is currently one template for every declarative card type (List Card, Table Card, etc.). + +For rendering the templates with the provided data, the [EJS](https://github.com/mde/ejs) templating engine is used. ### create_ui5_app @@ -187,6 +196,10 @@ Drawbacks of the current approach using the semantic model: The `get_guidelines` tool returns a single markdown resource containing best practices and guidelines for UI5 development, particularly targeted towards AI agents. The document can be found in the `resources/` directory of the project. +### get_integration_cards_guidelines + +The `get_integration_cards_guidelines` tool returns a single markdown resource containing best practices and guidelines for UI Integration Cards development. The content is particularly targeted towards AI agents. The document can be found in the `resources/` directory of the project. + ### get_project_info The `get_project_info` tool extracts metadata and configuration from a UI5 project. It provides insights into the project's structure, dependencies, and configuration settings, helping developers understand the context of their application. @@ -485,6 +498,107 @@ In the future, these guides should be moved into the UI5 linter project. See als } ``` +### run_manifest_validation + +The `run_manifest_validation` tool provides comprehensive schema validation for UI5 manifest files (`manifest.json`). It ensures that manifest files conform to the official UI5 Manifest JSON Schema, helping developers catch configuration errors early in the development process. + +#### Overview + +This tool uses the [Ajv JSON schema validator](https://www.npmjs.com/package/ajv) (specifically Ajv 2020-12) to perform validation against the official manifest schema. The schema is dynamically fetched from the [UI5 Manifest repository](https://github.com/SAP/ui5-manifest) based on the `_version` property declared in the manifest file. + +#### Schema Management + +**Version Detection:** +- The tool automatically detects the manifest version from the `_version` property +- If the `_version` property is missing, malformed, or not a valid semantic version, validation fails with a helpful error message listing supported versions +- The minimum supported manifest version is **1.68.0** (earlier versions use incompatible meta-schemas) + +**Schema Retrieval:** +- Schemas are fetched from: `https://raw.githubusercontent.com/SAP/ui5-manifest/v{version}/schema.json` +- A version mapping is maintained at: `https://raw.githubusercontent.com/SAP/ui5-manifest/main/mapping.json` +- Schemas are cached locally after first fetch to improve performance and reduce network requests +- External schemas referenced by the UI5 manifest schema (e.g., Adaptive Card schema) are also fetched and cached as needed. + +#### Validation Process + +- Reads the manifest file from the provided absolute path +- Parses the JSON content and extracts the `_version` property +- Fetches the corresponding schema based on the version +- Uses Ajv to validate the manifest against the schema +- Returns a detailed report of validation results, including specific error messages for any violations found + +#### Performance Characteristics + +**Caching Strategy:** +- Schema files are cached in memory after first retrieval +- Cache is shared across multiple validation calls in the same process +- Mutex locks prevent duplicate concurrent downloads of the same schema +- Network requests are only made once per schema version per process lifecycle + +#### Error Handling + +**Input Errors:** +- Non-absolute paths: `InvalidInputError` with clear message +- File not found: `InvalidInputError` indicating the file doesn't exist +- Invalid JSON: `InvalidInputError` with JSON parsing error details +- Missing `_version`: Detailed error with list of supported versions +- Unsupported version: Error message with version requirements and supported versions list + +**Network Errors:** +- Schema fetch failures are caught and reported with helpful context +- The tool provides fallback error messages even if the supported versions list cannot be fetched +- Cached schemas allow continued operation even if the network is unavailable after initial setup + +#### Example Input + +```json +{ + "manifestPath": "/absolute/path/to/project/webapp/manifest.json" +} +``` + +#### Example Output for Invalid Manifest + +```json +{ + "isValid": false, + "errors": [ + { + "keyword": "required", + "instancePath": "", + "schemaPath": "#/required", + "params": { + "missingProperty": "sap.ui" + }, + "message": "must have required property 'sap.ui'" + }, + { + "keyword": "required", + "instancePath": "/sap.app", + "schemaPath": "#/properties/sap.app/required", + "params": { + "missingProperty": "title" + }, + "message": "must have required property 'title'" + } + ] +} +``` + +#### Example Output for Valid Manifest + +```json +{ + "isValid": true, + "errors": [] +} +``` + +#### Requirements + +- **Network Access**: Initial schema fetch requires internet connectivity +- **Manifest Versioning**: The manifest must declare a valid `_version` property + ## Resources ### UI5 Documentation From e68d9e173cf2e73421980daf8b0440a932207eea Mon Sep 17 00:00:00 2001 From: Petar Dimov Date: Fri, 19 Dec 2025 12:03:31 +0200 Subject: [PATCH 28/31] fix(ui5Manifest.ts): Avoid duplicate entries --- src/utils/ui5Manifest.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/utils/ui5Manifest.ts b/src/utils/ui5Manifest.ts index c72dbe53..6c73b59a 100644 --- a/src/utils/ui5Manifest.ts +++ b/src/utils/ui5Manifest.ts @@ -67,9 +67,13 @@ async function failWithSupportedVersionsHint(errorMessage: string): Promise semver.gte(version, LOWEST_SUPPORTED_MANIFEST_VERSION) - ); + supportedVersions = [ + ...new Set( + Object.values(versionMap).filter( + (version) => semver.gte(version, LOWEST_SUPPORTED_MANIFEST_VERSION) + ) + ), + ]; } catch (_) { supportedVersions = null; }; From 02c51e310fd2317ddb3851053356271360cec9e6 Mon Sep 17 00:00:00 2001 From: Matthias Osswald Date: Fri, 19 Dec 2025 13:20:41 +0100 Subject: [PATCH 29/31] refactor: Use new UI5 Manifest repo URL --- docs/architecture.md | 6 +++--- src/utils/ui5Manifest.ts | 4 ++-- .../runValidation.integration.ts | 12 ++++++------ test/lib/utils/ui5Manifest.ts | 14 +++++++------- 4 files changed, 18 insertions(+), 18 deletions(-) diff --git a/docs/architecture.md b/docs/architecture.md index 0d09bb0d..a7135de0 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -504,7 +504,7 @@ The `run_manifest_validation` tool provides comprehensive schema validation for #### Overview -This tool uses the [Ajv JSON schema validator](https://www.npmjs.com/package/ajv) (specifically Ajv 2020-12) to perform validation against the official manifest schema. The schema is dynamically fetched from the [UI5 Manifest repository](https://github.com/SAP/ui5-manifest) based on the `_version` property declared in the manifest file. +This tool uses the [Ajv JSON schema validator](https://www.npmjs.com/package/ajv) (specifically Ajv 2020-12) to perform validation against the official manifest schema. The schema is dynamically fetched from the [UI5 Manifest repository](https://github.com/UI5/manifest) based on the `_version` property declared in the manifest file. #### Schema Management @@ -514,8 +514,8 @@ This tool uses the [Ajv JSON schema validator](https://www.npmjs.com/package/ajv - The minimum supported manifest version is **1.68.0** (earlier versions use incompatible meta-schemas) **Schema Retrieval:** -- Schemas are fetched from: `https://raw.githubusercontent.com/SAP/ui5-manifest/v{version}/schema.json` -- A version mapping is maintained at: `https://raw.githubusercontent.com/SAP/ui5-manifest/main/mapping.json` +- Schemas are fetched from: `https://raw.githubusercontent.com/UI5/manifest/v{version}/schema.json` +- A version mapping is maintained at: `https://raw.githubusercontent.com/UI5/manifest/main/mapping.json` - Schemas are cached locally after first fetch to improve performance and reduce network requests - External schemas referenced by the UI5 manifest schema (e.g., Adaptive Card schema) are also fetched and cached as needed. diff --git a/src/utils/ui5Manifest.ts b/src/utils/ui5Manifest.ts index 6c73b59a..70b16e4b 100644 --- a/src/utils/ui5Manifest.ts +++ b/src/utils/ui5Manifest.ts @@ -9,14 +9,14 @@ const schemaCache = new Map(); const fetchSchemaMutex = new Mutex(); let UI5ToManifestVersionMapping: Record | null = null; -const MAPPING_URL = "https://raw.githubusercontent.com/SAP/ui5-manifest/main/mapping.json"; +const MAPPING_URL = "https://raw.githubusercontent.com/UI5/manifest/main/mapping.json"; const ui5ToManifestVersionMappingMutex = new Mutex(); // Manifests prior to 1.68.0 use older meta-schema, which is not supported by the current implementation const LOWEST_SUPPORTED_MANIFEST_VERSION = "1.68.0"; function getSchemaURL(manifestVersion: string) { - return `https://raw.githubusercontent.com/SAP/ui5-manifest/v${manifestVersion}/schema.json`; + return `https://raw.githubusercontent.com/UI5/manifest/v${manifestVersion}/schema.json`; } async function getUI5toManifestVersionMapping() { diff --git a/test/lib/tools/run_manifest_validation/runValidation.integration.ts b/test/lib/tools/run_manifest_validation/runValidation.integration.ts index f26bad93..a79d9557 100644 --- a/test/lib/tools/run_manifest_validation/runValidation.integration.ts +++ b/test/lib/tools/run_manifest_validation/runValidation.integration.ts @@ -39,12 +39,12 @@ test.afterEach.always((t) => { test("runValidation successfully validates valid manifest", async (t) => { const {runValidation, fetchCdnStub} = t.context; - fetchCdnStub.withArgs("https://raw.githubusercontent.com/SAP/ui5-manifest/main/mapping.json") + fetchCdnStub.withArgs("https://raw.githubusercontent.com/UI5/manifest/main/mapping.json") .resolves({ "1.79.0": "1.79.0", }); - fetchCdnStub.withArgs("https://raw.githubusercontent.com/SAP/ui5-manifest/v1.79.0/schema.json") + fetchCdnStub.withArgs("https://raw.githubusercontent.com/UI5/manifest/v1.79.0/schema.json") .resolves(schemaFixture); const result = await runValidation(path.join(fixturesPath, "valid-manifest.json")); @@ -58,12 +58,12 @@ test("runValidation successfully validates valid manifest", async (t) => { test("runValidation successfully validates valid manifest after first attempt ending with exception", async (t) => { const {runValidation, fetchCdnStub} = t.context; - fetchCdnStub.withArgs("https://raw.githubusercontent.com/SAP/ui5-manifest/main/mapping.json") + fetchCdnStub.withArgs("https://raw.githubusercontent.com/UI5/manifest/main/mapping.json") .resolves({ "1.79.0": "1.79.0", }); - fetchCdnStub.withArgs("https://raw.githubusercontent.com/SAP/ui5-manifest/v1.79.0/schema.json") + fetchCdnStub.withArgs("https://raw.githubusercontent.com/UI5/manifest/v1.79.0/schema.json") .resolves(schemaFixture); await t.throwsAsync(async () => { @@ -85,12 +85,12 @@ test("runValidation successfully validates valid manifest after first attempt en async (t) => { const {runValidation, fetchCdnStub} = t.context; - fetchCdnStub.withArgs("https://raw.githubusercontent.com/SAP/ui5-manifest/main/mapping.json") + fetchCdnStub.withArgs("https://raw.githubusercontent.com/UI5/manifest/main/mapping.json") .resolves({ "1.79.0": "1.79.0", }); - fetchCdnStub.withArgs("https://raw.githubusercontent.com/SAP/ui5-manifest/v1.79.0/schema.json") + fetchCdnStub.withArgs("https://raw.githubusercontent.com/UI5/manifest/v1.79.0/schema.json") .onFirstCall() .rejects(new Error("Failed to fetch schema")) .onSecondCall() diff --git a/test/lib/utils/ui5Manifest.ts b/test/lib/utils/ui5Manifest.ts index 68ee7df1..f7a5589c 100644 --- a/test/lib/utils/ui5Manifest.ts +++ b/test/lib/utils/ui5Manifest.ts @@ -110,7 +110,7 @@ test("getManifestSchema throws error for unsupported versions 1.x.x versions", a } ); - fetchCdnStub.withArgs("https://raw.githubusercontent.com/SAP/ui5-manifest/main/mapping.json") + fetchCdnStub.withArgs("https://raw.githubusercontent.com/UI5/manifest/main/mapping.json") .resolves({ "1.55.0": "1.55.0", "1.67.0": "1.67.0", @@ -144,7 +144,7 @@ test("getManifestSchema fetches schema for specific version", async (t) => { type: "object", }; - fetchCdnStub.withArgs("https://raw.githubusercontent.com/SAP/ui5-manifest/v1.68.0/schema.json") + fetchCdnStub.withArgs("https://raw.githubusercontent.com/UI5/manifest/v1.68.0/schema.json") .resolves(mockSchema); const schema = await getManifestSchema("1.68.0"); @@ -160,7 +160,7 @@ test("getManifestSchema uses cache on subsequent calls", async (t) => { type: "object", }; - fetchCdnStub.withArgs("https://raw.githubusercontent.com/SAP/ui5-manifest/v1.68.0/schema.json") + fetchCdnStub.withArgs("https://raw.githubusercontent.com/UI5/manifest/v1.68.0/schema.json") .resolves(mockSchema); const schema1 = await getManifestSchema("1.68.0"); @@ -175,10 +175,10 @@ test("getManifestSchema handles fetch errors", async (t) => { const {fetchCdnStub, getManifestSchema} = t.context; // Mock fetch error - fetchCdnStub.withArgs("https://raw.githubusercontent.com/SAP/ui5-manifest/main/mapping.json") + fetchCdnStub.withArgs("https://raw.githubusercontent.com/UI5/manifest/main/mapping.json") .rejects(new Error("Mapping.json error")); - fetchCdnStub.withArgs("https://raw.githubusercontent.com/SAP/ui5-manifest/v1.68.0/schema.json") + fetchCdnStub.withArgs("https://raw.githubusercontent.com/UI5/manifest/v1.68.0/schema.json") .rejects(new Error("Network error")); await t.throwsAsync( @@ -196,13 +196,13 @@ test("getManifestSchema handles fetch errors and gives more details about suppor const {fetchCdnStub, getManifestSchema} = t.context; // Mock fetch error - fetchCdnStub.withArgs("https://raw.githubusercontent.com/SAP/ui5-manifest/main/mapping.json") + fetchCdnStub.withArgs("https://raw.githubusercontent.com/UI5/manifest/main/mapping.json") .resolves({ "1.70.0": "1.70.0", "1.71.0": "1.71.0", }); - fetchCdnStub.withArgs("https://raw.githubusercontent.com/SAP/ui5-manifest/v1.69.0/schema.json") + fetchCdnStub.withArgs("https://raw.githubusercontent.com/UI5/manifest/v1.69.0/schema.json") .rejects(new Error("Network error")); await t.throwsAsync( From c7e5dbe14af854b192f674a0109419702b9fc982 Mon Sep 17 00:00:00 2001 From: Petar Dimov Date: Fri, 19 Dec 2025 15:51:34 +0200 Subject: [PATCH 30/31] test: Add test for formatters --- .../run_manifest_validation/runValidation.ts | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/test/lib/tools/run_manifest_validation/runValidation.ts b/test/lib/tools/run_manifest_validation/runValidation.ts index 0ecc3d35..3bb04e01 100644 --- a/test/lib/tools/run_manifest_validation/runValidation.ts +++ b/test/lib/tools/run_manifest_validation/runValidation.ts @@ -365,3 +365,31 @@ test("runValidation patches external adaptive-card.json schema", async (t) => { errors: [], }); }); + +test("runValidation handles properties with 'format=uri'", async (t) => { + const {runValidation, getManifestSchemaStub} = t.context; + + // Stub the readFile function to return a manifest with invalid "$schema" URI + t.context.manifestFileContent = JSON.stringify({ + _version: "1.0.0", + $schema: "invalid-uri-format", + }); + + getManifestSchemaStub.resolves({ + type: "object", + properties: { + _version: {type: "string"}, + $schema: { + description: "A URI to the schema", + format: "uri", + type: "string", + }, + }, + required: ["$schema"], + additionalProperties: false, + }); + + const result = await runValidation("/path/to/manifest.json"); + + t.is(result.isValid, false); +}); From e4d45f9b02203f11a37fcb7c54a6345458bd3bc0 Mon Sep 17 00:00:00 2001 From: Petar Dimov Date: Fri, 19 Dec 2025 19:59:47 +0200 Subject: [PATCH 31/31] docs: Improve wording --- docs/architecture.md | 4 ++-- resources/integration_cards_guidelines.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/architecture.md b/docs/architecture.md index a7135de0..ba3661a6 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -198,7 +198,7 @@ The `get_guidelines` tool returns a single markdown resource containing best pra ### get_integration_cards_guidelines -The `get_integration_cards_guidelines` tool returns a single markdown resource containing best practices and guidelines for UI Integration Cards development. The content is particularly targeted towards AI agents. The document can be found in the `resources/` directory of the project. +The `get_integration_cards_guidelines` tool returns a single markdown resource containing best practices and guidelines for UI Integration Cards development. The content is particularly targeted towards AI agents. This document can be found in the `resources/` directory of the project. ### get_project_info @@ -517,7 +517,7 @@ This tool uses the [Ajv JSON schema validator](https://www.npmjs.com/package/ajv - Schemas are fetched from: `https://raw.githubusercontent.com/UI5/manifest/v{version}/schema.json` - A version mapping is maintained at: `https://raw.githubusercontent.com/UI5/manifest/main/mapping.json` - Schemas are cached locally after first fetch to improve performance and reduce network requests -- External schemas referenced by the UI5 manifest schema (e.g., Adaptive Card schema) are also fetched and cached as needed. +- External schemas referenced by the UI5 manifest schema (e.g., Adaptive Card schema) are also fetched and cached as needed #### Validation Process diff --git a/resources/integration_cards_guidelines.md b/resources/integration_cards_guidelines.md index 6372d662..d1c6c99d 100644 --- a/resources/integration_cards_guidelines.md +++ b/resources/integration_cards_guidelines.md @@ -42,7 +42,7 @@ ## 2. Validation - **ALWAYS** ensure that `manifest.json` file is valid JSON. - **ALWAYS** ensure that in `manifest.json` file the property `sap.app/type` is set to `"card"`. -- **ALWAYS** validate the `manifest.json` against the UI5 Manifest schema. You must do it using the `run_manifest_validation` tool. +- **ALWAYS** validate the `manifest.json` against the UI5 Manifest schema. Use the `run_manifest_validation` tool to do this. - **ALWAYS** avoid using deprecated properties in `manifest.json` and elsewhere. - **NEVER** treat Integration Cards' project as UI5 project, except for cards of type "Component".