From ac5546a085da1efbfb1cdaa03fd1e5f03a0c2764 Mon Sep 17 00:00:00 2001 From: zgjimhaziri Date: Thu, 16 Apr 2026 14:51:53 +0200 Subject: [PATCH 1/3] SP-45: Add asset-registry schema, examples, methodology commands MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the beta `api request` command with dedicated asset-registry subcommands that hit the new Pacman proxy endpoints: - `asset-registry schema --assetType X` - `asset-registry examples --assetType X` - `asset-registry methodology --assetType X` Remove the `api` command — no longer needed now that schema, examples, and methodology are accessible through first-class commands. Update the agentic development guide, Cursor skill, and command docs. Includes-AI-Code: true Made-with: Cursor --- .../skills/asset-registry-endpoints/SKILL.md | 159 ++++++++---------- docs/user-guide/agentic-development-guide.md | 17 +- docs/user-guide/asset-registry-commands.md | 55 +++++- src/commands/api/api.service.ts | 63 ------- src/commands/api/module.ts | 31 ---- .../asset-registry/asset-registry-api.ts | 24 +++ .../asset-registry/asset-registry.service.ts | 25 +++ src/commands/asset-registry/module.ts | 30 ++++ tests/commands/api/api-request.spec.ts | 86 ---------- .../asset-registry-examples.spec.ts | 41 +++++ .../asset-registry-methodology.spec.ts | 44 +++++ .../asset-registry-schema.spec.ts | 47 ++++++ 12 files changed, 346 insertions(+), 276 deletions(-) delete mode 100644 src/commands/api/api.service.ts delete mode 100644 src/commands/api/module.ts delete mode 100644 tests/commands/api/api-request.spec.ts create mode 100644 tests/commands/asset-registry/asset-registry-examples.spec.ts create mode 100644 tests/commands/asset-registry/asset-registry-methodology.spec.ts create mode 100644 tests/commands/asset-registry/asset-registry-schema.spec.ts diff --git a/.cursor/skills/asset-registry-endpoints/SKILL.md b/.cursor/skills/asset-registry-endpoints/SKILL.md index c01b3b8..73a378a 100644 --- a/.cursor/skills/asset-registry-endpoints/SKILL.md +++ b/.cursor/skills/asset-registry-endpoints/SKILL.md @@ -1,18 +1,18 @@ --- name: asset-registry-endpoints description: >- - Call asset service endpoints (schema, validate, methodology, examples) for any - registered asset type using the asset registry descriptor and the content-cli - api command. Also covers exporting/importing/creating packages via config - commands. Use when the user asks for a schema, wants to validate an asset, - needs methodology/best-practices, wants example configurations for an asset - type, or needs to export/import/list/create packages. + Discover asset types, fetch schemas, examples, and methodology via + content-cli asset-registry commands. Also covers exporting/importing/creating + packages via config commands. Use when the user asks for a schema, wants to + validate an asset, needs methodology/best-practices, wants example + configurations for an asset type, or needs to export/import/list/create + packages. --- # Asset Registry Endpoint Caller -Call any endpoint defined in an asset registry descriptor by combining the -service `basePath` with the endpoint path, then hitting it via `content-cli api request`. +Use `content-cli asset-registry` commands to discover asset types and fetch +schemas, examples, and methodology for any registered asset type. ## Prerequisites @@ -122,7 +122,7 @@ nothing above or around it. If the schema defines a `metadata` field inside `configuration`, that is the asset's own metadata, not a platform concept. Everything outside `configuration` is configuration-management metadata managed -by Pacman. +by the platform. ```json { @@ -170,127 +170,116 @@ Field reference: | `variables` | Package variables (e.g. DATA_MODEL bindings) | | `dependencies` | Package-level dependencies | -## Step 1 — Get the asset descriptor +## Step 1 — Discover asset types -```bash -$CLI asset-registry get --assetType -p -``` - -This returns a descriptor like: - -```json -{ - "assetType": "BOARD_V2", - "displayName": "View", - "service": { "basePath": "/blueprint/api" }, - "endpoints": { - "schema": "/validation/schema/BOARD_V2", - "validate": "/validate/BOARD_V2", - "methodology": "/methodology/BOARD_V2", - "examples": "/examples/BOARD_V2" - } -} -``` - -If you don't know which asset types exist, list them first: +List all registered asset types: ```bash $CLI asset-registry list -p ``` -## Step 2 — Build the full path - -Concatenate `service.basePath` + `endpoints.`: +Get the full descriptor for a specific type (includes schema version, service +info, and endpoint availability): -| Want | Formula | Example | -|------|---------|---------| -| Schema | `basePath + endpoints.schema` | `/blueprint/api/validation/schema/BOARD_V2` | -| Validate | `basePath + endpoints.validate` | `/blueprint/api/validate/BOARD_V2` | -| Methodology | `basePath + endpoints.methodology` | `/blueprint/api/methodology/BOARD_V2` | -| Examples | `basePath + endpoints.examples` | `/blueprint/api/examples/BOARD_V2` | - -`methodology` and `examples` are optional — check if they exist in the -descriptor before calling. - -**Not all endpoints may be available** for every asset type. Some services may -not have deployed validate, methodology, or examples endpoints yet. If you get -a 404, the endpoint is not implemented for that asset type — do not retry. +```bash +$CLI asset-registry get --assetType -p +``` -## Step 3 — Call the endpoint +## Step 2 — Fetch schema, examples, or methodology -> **Note on `api request`**: This command is **beta** and exists as a testing -> mechanism. It hits Celonis APIs using the configured profile's auth — it is -> not a general-purpose HTTP client. Do not use outside of testing. +Use these commands to get asset authoring resources directly. Each proxies +through the platform to the owning asset service — no manual path construction +needed. ### Schema (GET) Returns the full JSON Schema for the asset type's `configuration` object. ```bash -$CLI api request --path "" -p +$CLI asset-registry schema --assetType -p ``` -### Validate (POST) +Save to file: ```bash -$CLI api request --path "" --method POST \ - --body '{"assetType":"","packageKey":"","nodes":[{"key":"","configuration":{...}}]}' \ - -p +$CLI asset-registry schema --assetType --json -p ``` -### Methodology (GET) +### Examples (GET) + +Returns example configurations for the asset type. Not all asset types provide +examples — a 404 means the endpoint is not available. ```bash -$CLI api request --path "" -p +$CLI asset-registry examples --assetType -p ``` -### Examples (GET) +### Methodology (GET) + +Returns best-practices and methodology guidance. Not all asset types provide +methodology — a 404 means the endpoint is not available. ```bash -$CLI api request --path "" -p +$CLI asset-registry methodology --assetType -p ``` -### Save response to file +### Validate (POST — via config import) + +Use `config import --validate` to validate assets against their schema before +importing: ```bash -$CLI api request --path "" --json -p +$CLI config import -d --validate --overwrite -p ``` -## Troubleshooting: 403 on asset endpoints +## Troubleshooting -If an asset service endpoint returns **403**, the endpoint is likely **not on -the OAuth scope allowlist** for the token. The asset registry (Pacman) APIs may -work, but downstream asset service APIs (e.g. `/blueprint/api/...`, -`/llm-agent/api/...`) need their own allowlisting. +**404 on examples / methodology** — Not all asset services have deployed these +endpoints. The schema endpoint is required for all registered types; the others +are optional. -**Tip for asset teams**: if your endpoints return 403 via Content CLI or public -APIs, request that they be added to the OAuth `studio` scope allowlist. +**500 on proxy endpoints** — The platform proxies requests to the owning asset +service. A 500 typically means the downstream service is unavailable or returned +an unexpected response. + +**500 on import** — Ensure `spaceId` is set on every node and `schemaVersion` +matches the descriptor's `assetSchema.version`. ## Full worked example ```bash -# 1. Get descriptor -$CLI asset-registry get --assetType BOARD_V2 --json +# 1. Discover available asset types +$CLI asset-registry list -p -# 2. Fetch schema (beta command — testing only) -$CLI api request --path "/blueprint/api/validation/schema/BOARD_V2" --json +# 2. Get the descriptor (includes schema version) +$CLI asset-registry get --assetType BOARD_V2 --json -p -# 3. Export the target package -$CLI config export --packageKeys --unzip +# 3. Fetch the schema +$CLI asset-registry schema --assetType BOARD_V2 --json -p -# 4. Create a new node JSON in the export's nodes/ directory -# — configuration root must conform to the schema from step 2 +# 4. (Optional) Fetch examples for reference +$CLI asset-registry examples --assetType BOARD_V2 --json -p + +# 5. Export the target package +$CLI config export --packageKeys --unzip -p + +# 6. Create a new node JSON in the export's nodes/ directory +# — configuration root must conform to the schema from step 3 # — set spaceId to the package's space (ask the user) -# 5. Validate and import (--overwrite for existing package, omit for new) -$CLI config import -d --validate --overwrite +# 7. Validate and import (--overwrite for existing package, omit for new) +$CLI config import -d --validate --overwrite -p ``` ## Quick reference -| Endpoint | Method | Required | Notes | -|----------|--------|----------|-------| -| schema | GET | Yes | Returns full JSON Schema | -| validate | POST | Yes | May not be deployed for all types yet | -| methodology | GET | No | May not be deployed for all types yet | -| examples | GET | No | May not be deployed for all types yet | +| Command | Description | +|---------|-------------| +| `asset-registry list` | List all registered asset types | +| `asset-registry get --assetType X` | Get the full descriptor for an asset type | +| `asset-registry schema --assetType X` | Get the JSON Schema for the asset's configuration | +| `asset-registry examples --assetType X` | Get example configurations (if available) | +| `asset-registry methodology --assetType X` | Get methodology / best-practices (if available) | +| `config list` | List packages | +| `config export --packageKeys X --unzip` | Export packages | +| `config import -d --validate --overwrite` | Validate and import packages | diff --git a/docs/user-guide/agentic-development-guide.md b/docs/user-guide/agentic-development-guide.md index 9078af6..d1b1cfc 100644 --- a/docs/user-guide/agentic-development-guide.md +++ b/docs/user-guide/agentic-development-guide.md @@ -26,18 +26,23 @@ content-cli asset-registry list content-cli asset-registry get --assetType BOARD_V2 ``` -The descriptor returns the `basePath` and endpoint paths for schema, validate, methodology, and examples. +The descriptor returns metadata including the schema version needed for asset creation. ### 2. Fetch the schema -Combine `basePath` + `endpoints.schema` and call it: - ```bash -content-cli api request --path "/blueprint/api/validation/schema/BOARD_V2" --json +content-cli asset-registry schema --assetType BOARD_V2 --json ``` The schema describes the valid structure of the asset's `configuration` field. This is the only part of the asset governed by the schema — everything else is platform metadata. +You can also fetch examples and methodology when available: + +```bash +content-cli asset-registry examples --assetType BOARD_V2 --json +content-cli asset-registry methodology --assetType BOARD_V2 --json +``` + ### 3. Export the target package ```bash @@ -86,9 +91,9 @@ content-cli config export --keysByVersion _ --unzip ## Troubleshooting -**403 on asset service endpoints** — The asset registry (Pacman) APIs and asset service endpoints use separate OAuth scopes. If schema/validate endpoints return 403, the service's endpoints may not be on the `studio` scope allowlist yet. Asset teams should request their endpoints be added to the allowlist. +**404 on examples / methodology** — Not all asset services have deployed these endpoints. The schema endpoint is required for all registered types; the others are optional. -**404 on validate / methodology / examples** — Not all services have deployed all endpoints. The schema endpoint is required; the others may not be available yet. +**500 on proxy endpoints** — The platform proxies requests to the owning asset service. A 500 typically means the downstream service is unavailable or returned an unexpected response. **500 on import** — Ensure `spaceId` is set on every node and `schemaVersion` matches the descriptor's `assetSchema.version`. diff --git a/docs/user-guide/asset-registry-commands.md b/docs/user-guide/asset-registry-commands.md index f642d25..2e2a623 100644 --- a/docs/user-guide/asset-registry-commands.md +++ b/docs/user-guide/asset-registry-commands.md @@ -1,7 +1,7 @@ # Asset Registry Commands -The **asset-registry** command group allows you to discover registered asset types and their service descriptors from the Asset Registry. -This is useful for understanding which asset types are available on the platform, their configuration schema versions, and how to reach their backing services. +The **asset-registry** command group allows you to discover registered asset types, fetch their schemas, examples, and methodology from the Asset Registry. +This is useful for understanding which asset types are available on the platform, their configuration structure, and best practices for authoring assets. ## List Asset Types @@ -38,11 +38,11 @@ Example output: Asset Type: BOARD_V2 Display Name: View Group: DASHBOARDS -Schema: v2.1.0 +Schema: v2 Base Path: /blueprint/api Endpoints: - schema: /schema/board_v2 - validate: /validate/board_v2 + schema: /validation/schema/board_v2 + validate: /validate methodology: /methodology/board_v2 examples: /examples/board_v2 ``` @@ -51,3 +51,48 @@ Options: - `--assetType ` (required) – The asset type identifier (e.g., `BOARD_V2`, `SEMANTIC_MODEL`) - `--json` – Write the full response to a JSON file in the working directory + +## Get Schema + +Fetch the JSON Schema that defines the valid structure of an asset type's `configuration` object. + +``` +content-cli asset-registry schema --assetType BOARD_V2 +``` + +The response is the full JSON Schema (draft-07) for the asset type. Use `--json` to save it to a file for reference during asset authoring. + +``` +content-cli asset-registry schema --assetType BOARD_V2 --json +``` + +Options: + +- `--assetType ` (required) – The asset type identifier +- `--json` – Write the schema to a JSON file in the working directory + +## Get Examples + +Fetch example configurations for an asset type. Not all asset types provide examples. + +``` +content-cli asset-registry examples --assetType BOARD_V2 +``` + +Options: + +- `--assetType ` (required) – The asset type identifier +- `--json` – Write the examples to a JSON file in the working directory + +## Get Methodology + +Fetch methodology and best-practices guidance for an asset type. Not all asset types provide methodology. + +``` +content-cli asset-registry methodology --assetType BOARD_V2 +``` + +Options: + +- `--assetType ` (required) – The asset type identifier +- `--json` – Write the methodology to a JSON file in the working directory diff --git a/src/commands/api/api.service.ts b/src/commands/api/api.service.ts deleted file mode 100644 index a64c3cb..0000000 --- a/src/commands/api/api.service.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { HttpClient } from "../../core/http/http-client"; -import { Context } from "../../core/command/cli-context"; -import { FatalError, logger } from "../../core/utils/logger"; -import { fileService, FileService } from "../../core/utils/file-service"; -import { v4 as uuidv4 } from "uuid"; - -export class ApiService { - private httpClient: () => HttpClient; - - constructor(context: Context) { - this.httpClient = () => context.httpClient; - } - - public async request(path: string, method: string, body?: string, jsonFile?: boolean): Promise { - if (!path.startsWith("/")) { - throw new FatalError("Path must start with /"); - } - - const validMethods = ["GET", "POST", "PUT", "DELETE"]; - if (!validMethods.includes(method)) { - throw new FatalError(`Invalid method '${method}'. Must be one of: ${validMethods.join(", ")}`); - } - - logger.info(`${method} ${path}`); - - let parsedBody: any; - if (body) { - try { - parsedBody = JSON.parse(body); - } catch { - throw new FatalError("--body must be valid JSON"); - } - } - - const data = await this.execute(method, path, parsedBody); - - if (jsonFile) { - const filename = uuidv4() + ".json"; - fileService.writeToFileWithGivenName(JSON.stringify(data, null, 2), filename); - logger.info(FileService.fileDownloadedMessage + filename); - } else { - const output = typeof data === "string" ? data : JSON.stringify(data, null, 2); - logger.info(output); - } - } - - private async execute(method: string, path: string, body?: any): Promise { - try { - switch (method) { - case "GET": - return await this.httpClient().get(path); - case "POST": - return await this.httpClient().post(path, body ?? {}); - case "PUT": - return await this.httpClient().put(path, body ?? {}); - case "DELETE": - return await this.httpClient().delete(path); - } - } catch (e) { - throw new FatalError(`${method} ${path} failed: ${e}`); - } - } -} diff --git a/src/commands/api/module.ts b/src/commands/api/module.ts deleted file mode 100644 index a2deaca..0000000 --- a/src/commands/api/module.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { Configurator, IModule } from "../../core/command/module-handler"; -import { Context } from "../../core/command/cli-context"; -import { Command, OptionValues } from "commander"; -import { ApiService } from "./api.service"; - -class Module extends IModule { - - public register(context: Context, configurator: Configurator): void { - configurator.command("api") - .description("Send raw HTTP requests to Celonis APIs on the configured team (beta — testing only).") - .beta() - .command("request") - .description("Send a request to the given Celonis API path (e.g. /package-manager/api/packages)") - .requiredOption("--path ", "API path starting with / (e.g. /package-manager/api/packages)") - .option("--method ", "HTTP method: GET, POST, PUT, DELETE", "GET") - .option("--body ", "Request body as JSON string (for POST/PUT)") - .option("--json", "Write the response to a JSON file instead of printing it") - .action(this.request); - } - - private async request(context: Context, command: Command, options: OptionValues): Promise { - await new ApiService(context).request( - options.path, - (options.method as string).toUpperCase(), - options.body, - !!options.json - ); - } -} - -export = Module; diff --git a/src/commands/asset-registry/asset-registry-api.ts b/src/commands/asset-registry/asset-registry-api.ts index 12c7a9f..9777025 100644 --- a/src/commands/asset-registry/asset-registry-api.ts +++ b/src/commands/asset-registry/asset-registry-api.ts @@ -25,4 +25,28 @@ export class AssetRegistryApi { throw new FatalError(`Problem getting asset type '${assetType}': ${e}`); }); } + + public async getSchema(assetType: string): Promise { + return this.httpClient() + .get(`/pacman/api/core/asset-registry/schemas/${encodeURIComponent(assetType)}`) + .catch((e) => { + throw new FatalError(`Problem getting schema for asset type '${assetType}': ${e}`); + }); + } + + public async getExamples(assetType: string): Promise { + return this.httpClient() + .get(`/pacman/api/core/asset-registry/examples/${encodeURIComponent(assetType)}`) + .catch((e) => { + throw new FatalError(`Problem getting examples for asset type '${assetType}': ${e}`); + }); + } + + public async getMethodology(assetType: string): Promise { + return this.httpClient() + .get(`/pacman/api/core/asset-registry/methodologies/${encodeURIComponent(assetType)}`) + .catch((e) => { + throw new FatalError(`Problem getting methodology for asset type '${assetType}': ${e}`); + }); + } } diff --git a/src/commands/asset-registry/asset-registry.service.ts b/src/commands/asset-registry/asset-registry.service.ts index a4617f3..47a26f9 100644 --- a/src/commands/asset-registry/asset-registry.service.ts +++ b/src/commands/asset-registry/asset-registry.service.ts @@ -43,6 +43,31 @@ export class AssetRegistryService { } } + public async getSchema(assetType: string, jsonResponse: boolean): Promise { + const data = await this.api.getSchema(assetType); + this.outputResponse(data, jsonResponse); + } + + public async getExamples(assetType: string, jsonResponse: boolean): Promise { + const data = await this.api.getExamples(assetType); + this.outputResponse(data, jsonResponse); + } + + public async getMethodology(assetType: string, jsonResponse: boolean): Promise { + const data = await this.api.getMethodology(assetType); + this.outputResponse(data, jsonResponse); + } + + private outputResponse(data: any, jsonResponse: boolean): void { + if (jsonResponse) { + const filename = uuidv4() + ".json"; + fileService.writeToFileWithGivenName(JSON.stringify(data, null, 2), filename); + logger.info(FileService.fileDownloadedMessage + filename); + } else { + logger.info(typeof data === "string" ? data : JSON.stringify(data, null, 2)); + } + } + private logDescriptorSummary(descriptor: AssetRegistryDescriptor): void { logger.info( `${descriptor.assetType} - ${descriptor.displayName} [${descriptor.group}] (basePath: ${descriptor.service.basePath})` diff --git a/src/commands/asset-registry/module.ts b/src/commands/asset-registry/module.ts index f8543a2..6d2d92c 100644 --- a/src/commands/asset-registry/module.ts +++ b/src/commands/asset-registry/module.ts @@ -19,6 +19,24 @@ class Module extends IModule { .requiredOption("--assetType ", "The asset type identifier (e.g., BOARD_V2)") .option("--json", "Return the response as a JSON file") .action(this.getType); + + assetRegistryCommand.command("schema") + .description("Get the JSON schema for an asset type's configuration") + .requiredOption("--assetType ", "The asset type identifier (e.g., BOARD_V2)") + .option("--json", "Return the response as a JSON file") + .action(this.getSchema); + + assetRegistryCommand.command("examples") + .description("Get example configurations for an asset type") + .requiredOption("--assetType ", "The asset type identifier (e.g., BOARD_V2)") + .option("--json", "Return the response as a JSON file") + .action(this.getExamples); + + assetRegistryCommand.command("methodology") + .description("Get the methodology / best-practices guide for an asset type") + .requiredOption("--assetType ", "The asset type identifier (e.g., BOARD_V2)") + .option("--json", "Return the response as a JSON file") + .action(this.getMethodology); } private async listTypes(context: Context, command: Command, options: OptionValues): Promise { @@ -28,6 +46,18 @@ class Module extends IModule { private async getType(context: Context, command: Command, options: OptionValues): Promise { await new AssetRegistryService(context).getType(options.assetType, !!options.json); } + + private async getSchema(context: Context, command: Command, options: OptionValues): Promise { + await new AssetRegistryService(context).getSchema(options.assetType, !!options.json); + } + + private async getExamples(context: Context, command: Command, options: OptionValues): Promise { + await new AssetRegistryService(context).getExamples(options.assetType, !!options.json); + } + + private async getMethodology(context: Context, command: Command, options: OptionValues): Promise { + await new AssetRegistryService(context).getMethodology(options.assetType, !!options.json); + } } export = Module; diff --git a/tests/commands/api/api-request.spec.ts b/tests/commands/api/api-request.spec.ts deleted file mode 100644 index 59032c2..0000000 --- a/tests/commands/api/api-request.spec.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { mockAxiosGet, mockAxiosPost, mockAxiosPut } from "../../utls/http-requests-mock"; -import { ApiService } from "../../../src/commands/api/api.service"; -import { testContext } from "../../utls/test-context"; -import { loggingTestTransport, mockWriteFileSync } from "../../jest.setup"; -import { FileService } from "../../../src/core/utils/file-service"; -import * as path from "path"; - -describe("Api request command", () => { - const sampleResponse = { packages: [{ id: "pkg-1", name: "My Package" }] }; - - it("Should execute a GET request and print the response", async () => { - mockAxiosGet("https://myTeam.celonis.cloud/package-manager/api/v2/packages", sampleResponse); - - await new ApiService(testContext).request("/package-manager/api/v2/packages", "GET", undefined, false); - - expect(loggingTestTransport.logMessages.length).toBe(2); - expect(loggingTestTransport.logMessages[0].message).toContain("GET /package-manager/api/v2/packages"); - expect(loggingTestTransport.logMessages[1].message).toContain("pkg-1"); - }); - - it("Should execute a GET request and save response as JSON file", async () => { - mockAxiosGet("https://myTeam.celonis.cloud/package-manager/api/v2/packages", sampleResponse); - - await new ApiService(testContext).request("/package-manager/api/v2/packages", "GET", undefined, true); - - const expectedFileName = loggingTestTransport.logMessages[1].message.split(FileService.fileDownloadedMessage)[1]; - expect(mockWriteFileSync).toHaveBeenCalledWith( - path.resolve(process.cwd(), expectedFileName), - expect.any(String), - { encoding: "utf-8" } - ); - - const written = JSON.parse(mockWriteFileSync.mock.calls[0][1]); - expect(written.packages[0].id).toBe("pkg-1"); - }); - - it("Should execute a POST request with body", async () => { - const postBody = { name: "New Package" }; - mockAxiosPost("https://myTeam.celonis.cloud/package-manager/api/v2/packages", { id: "pkg-2" }); - - await new ApiService(testContext).request( - "/package-manager/api/v2/packages", "POST", JSON.stringify(postBody), false - ); - - expect(loggingTestTransport.logMessages[0].message).toContain("POST /package-manager/api/v2/packages"); - expect(loggingTestTransport.logMessages[1].message).toContain("pkg-2"); - }); - - it("Should execute a PUT request with body", async () => { - const putBody = { name: "Updated Package" }; - mockAxiosPut("https://myTeam.celonis.cloud/package-manager/api/v2/packages/pkg-1", { id: "pkg-1", name: "Updated Package" }); - - await new ApiService(testContext).request( - "/package-manager/api/v2/packages/pkg-1", "PUT", JSON.stringify(putBody), false - ); - - expect(loggingTestTransport.logMessages[0].message).toContain("PUT /package-manager/api/v2/packages/pkg-1"); - expect(loggingTestTransport.logMessages[1].message).toContain("Updated Package"); - }); - - it("Should reject paths that don't start with /", async () => { - await expect( - new ApiService(testContext).request("package-manager/api/v2/packages", "GET", undefined, false) - ).rejects.toThrow("Path must start with /"); - }); - - it("Should reject invalid HTTP methods", async () => { - await expect( - new ApiService(testContext).request("/some/path", "PATCH", undefined, false) - ).rejects.toThrow("Invalid method"); - }); - - it("Should reject invalid JSON body", async () => { - await expect( - new ApiService(testContext).request("/some/path", "POST", "not-json{", false) - ).rejects.toThrow("--body must be valid JSON"); - }); - - it("Should be case-insensitive on method (normalized to upper by module)", async () => { - mockAxiosGet("https://myTeam.celonis.cloud/some/path", { ok: true }); - - await new ApiService(testContext).request("/some/path", "GET", undefined, false); - - expect(loggingTestTransport.logMessages[0].message).toContain("GET /some/path"); - }); -}); diff --git a/tests/commands/asset-registry/asset-registry-examples.spec.ts b/tests/commands/asset-registry/asset-registry-examples.spec.ts new file mode 100644 index 0000000..3b9bd31 --- /dev/null +++ b/tests/commands/asset-registry/asset-registry-examples.spec.ts @@ -0,0 +1,41 @@ +import { mockAxiosGet } from "../../utls/http-requests-mock"; +import { AssetRegistryService } from "../../../src/commands/asset-registry/asset-registry.service"; +import { testContext } from "../../utls/test-context"; +import { loggingTestTransport, mockWriteFileSync } from "../../jest.setup"; +import { FileService } from "../../../src/core/utils/file-service"; +import * as path from "path"; + +describe("Asset registry examples", () => { + const examplesResponse = [ + { name: "Simple View", configuration: { components: [] } }, + { name: "KPI Dashboard", configuration: { components: [{ type: "kpi" }] } }, + ]; + + it("Should get examples and print them", async () => { + mockAxiosGet("https://myTeam.celonis.cloud/pacman/api/core/asset-registry/examples/BOARD_V2", examplesResponse); + + await new AssetRegistryService(testContext).getExamples("BOARD_V2", false); + + expect(loggingTestTransport.logMessages.length).toBe(1); + const output = loggingTestTransport.logMessages[0].message; + expect(output).toContain("Simple View"); + expect(output).toContain("KPI Dashboard"); + }); + + it("Should get examples and save as JSON file", async () => { + mockAxiosGet("https://myTeam.celonis.cloud/pacman/api/core/asset-registry/examples/BOARD_V2", examplesResponse); + + await new AssetRegistryService(testContext).getExamples("BOARD_V2", true); + + const expectedFileName = loggingTestTransport.logMessages[0].message.split(FileService.fileDownloadedMessage)[1]; + expect(mockWriteFileSync).toHaveBeenCalledWith( + path.resolve(process.cwd(), expectedFileName), + expect.any(String), + { encoding: "utf-8" } + ); + + const written = JSON.parse(mockWriteFileSync.mock.calls[0][1]); + expect(written.length).toBe(2); + expect(written[0].name).toBe("Simple View"); + }); +}); diff --git a/tests/commands/asset-registry/asset-registry-methodology.spec.ts b/tests/commands/asset-registry/asset-registry-methodology.spec.ts new file mode 100644 index 0000000..9c215d1 --- /dev/null +++ b/tests/commands/asset-registry/asset-registry-methodology.spec.ts @@ -0,0 +1,44 @@ +import { mockAxiosGet } from "../../utls/http-requests-mock"; +import { AssetRegistryService } from "../../../src/commands/asset-registry/asset-registry.service"; +import { testContext } from "../../utls/test-context"; +import { loggingTestTransport, mockWriteFileSync } from "../../jest.setup"; +import { FileService } from "../../../src/core/utils/file-service"; +import * as path from "path"; + +describe("Asset registry methodology", () => { + const methodologyResponse = { + title: "View Best Practices", + sections: [ + { heading: "Layout", content: "Use a responsive grid layout." }, + { heading: "KPIs", content: "Place KPIs at the top." }, + ], + }; + + it("Should get methodology and print it", async () => { + mockAxiosGet("https://myTeam.celonis.cloud/pacman/api/core/asset-registry/methodologies/BOARD_V2", methodologyResponse); + + await new AssetRegistryService(testContext).getMethodology("BOARD_V2", false); + + expect(loggingTestTransport.logMessages.length).toBe(1); + const output = loggingTestTransport.logMessages[0].message; + expect(output).toContain("View Best Practices"); + expect(output).toContain("Layout"); + }); + + it("Should get methodology and save as JSON file", async () => { + mockAxiosGet("https://myTeam.celonis.cloud/pacman/api/core/asset-registry/methodologies/BOARD_V2", methodologyResponse); + + await new AssetRegistryService(testContext).getMethodology("BOARD_V2", true); + + const expectedFileName = loggingTestTransport.logMessages[0].message.split(FileService.fileDownloadedMessage)[1]; + expect(mockWriteFileSync).toHaveBeenCalledWith( + path.resolve(process.cwd(), expectedFileName), + expect.any(String), + { encoding: "utf-8" } + ); + + const written = JSON.parse(mockWriteFileSync.mock.calls[0][1]); + expect(written.title).toBe("View Best Practices"); + expect(written.sections.length).toBe(2); + }); +}); diff --git a/tests/commands/asset-registry/asset-registry-schema.spec.ts b/tests/commands/asset-registry/asset-registry-schema.spec.ts new file mode 100644 index 0000000..4af0987 --- /dev/null +++ b/tests/commands/asset-registry/asset-registry-schema.spec.ts @@ -0,0 +1,47 @@ +import { mockAxiosGet } from "../../utls/http-requests-mock"; +import { AssetRegistryService } from "../../../src/commands/asset-registry/asset-registry.service"; +import { testContext } from "../../utls/test-context"; +import { loggingTestTransport, mockWriteFileSync } from "../../jest.setup"; +import { FileService } from "../../../src/core/utils/file-service"; +import * as path from "path"; + +describe("Asset registry schema", () => { + const schemaResponse = { + $schema: "http://json-schema.org/draft-07/schema#", + title: "Board", + type: "object", + properties: { + name: { type: "string" }, + components: { type: "array" }, + }, + }; + + it("Should get schema and print it", async () => { + mockAxiosGet("https://myTeam.celonis.cloud/pacman/api/core/asset-registry/schemas/BOARD_V2", schemaResponse); + + await new AssetRegistryService(testContext).getSchema("BOARD_V2", false); + + expect(loggingTestTransport.logMessages.length).toBe(1); + const output = loggingTestTransport.logMessages[0].message; + expect(output).toContain("Board"); + expect(output).toContain("draft-07"); + expect(output).toContain("properties"); + }); + + it("Should get schema and save as JSON file", async () => { + mockAxiosGet("https://myTeam.celonis.cloud/pacman/api/core/asset-registry/schemas/BOARD_V2", schemaResponse); + + await new AssetRegistryService(testContext).getSchema("BOARD_V2", true); + + const expectedFileName = loggingTestTransport.logMessages[0].message.split(FileService.fileDownloadedMessage)[1]; + expect(mockWriteFileSync).toHaveBeenCalledWith( + path.resolve(process.cwd(), expectedFileName), + expect.any(String), + { encoding: "utf-8" } + ); + + const written = JSON.parse(mockWriteFileSync.mock.calls[0][1]); + expect(written.$schema).toBe("http://json-schema.org/draft-07/schema#"); + expect(written.title).toBe("Board"); + }); +}); From 2c23fc9cb9b39483a4afcb6a595be25b0c4d3488 Mon Sep 17 00:00:00 2001 From: zgjimhaziri Date: Fri, 17 Apr 2026 11:39:27 +0200 Subject: [PATCH 2/3] Address PR review comments - SKILL.md: Instruct agents to check validation results before importing and prompt the user if validation fails (promeris feedback) - Fix "500 on import" to "Errors on import (400 / 500)" in skill and guide (promeris feedback) - Add module-level tests for asset-registry handlers to improve coverage Includes-AI-Code: true Made-with: Cursor --- .../skills/asset-registry-endpoints/SKILL.md | 10 +++- docs/user-guide/agentic-development-guide.md | 2 +- .../asset-registry-module.spec.ts | 59 +++++++++++++++++++ 3 files changed, 68 insertions(+), 3 deletions(-) create mode 100644 tests/commands/asset-registry/asset-registry-module.spec.ts diff --git a/.cursor/skills/asset-registry-endpoints/SKILL.md b/.cursor/skills/asset-registry-endpoints/SKILL.md index 73a378a..095b0ba 100644 --- a/.cursor/skills/asset-registry-endpoints/SKILL.md +++ b/.cursor/skills/asset-registry-endpoints/SKILL.md @@ -232,6 +232,12 @@ importing: $CLI config import -d --validate --overwrite -p ``` +**Important**: If validation returns errors, do **not** proceed with the import. +Instead, fix the schema violations in the node JSON and re-run the command. If +you cannot resolve the errors automatically, present the validation results to +the user and ask whether they want to continue importing with invalid +configuration or stop to fix it manually. + ## Troubleshooting **404 on examples / methodology** — Not all asset services have deployed these @@ -242,8 +248,8 @@ are optional. service. A 500 typically means the downstream service is unavailable or returned an unexpected response. -**500 on import** — Ensure `spaceId` is set on every node and `schemaVersion` -matches the descriptor's `assetSchema.version`. +**Errors on import (400 / 500)** — Ensure `spaceId` is set on every node and +`schemaVersion` matches the descriptor's `assetSchema.version`. ## Full worked example diff --git a/docs/user-guide/agentic-development-guide.md b/docs/user-guide/agentic-development-guide.md index d1b1cfc..dd7e3c4 100644 --- a/docs/user-guide/agentic-development-guide.md +++ b/docs/user-guide/agentic-development-guide.md @@ -95,7 +95,7 @@ content-cli config export --keysByVersion _ --unzip **500 on proxy endpoints** — The platform proxies requests to the owning asset service. A 500 typically means the downstream service is unavailable or returned an unexpected response. -**500 on import** — Ensure `spaceId` is set on every node and `schemaVersion` matches the descriptor's `assetSchema.version`. +**Errors on import (400 / 500)** — Ensure `spaceId` is set on every node and `schemaVersion` matches the descriptor's `assetSchema.version`. ## Further reading diff --git a/tests/commands/asset-registry/asset-registry-module.spec.ts b/tests/commands/asset-registry/asset-registry-module.spec.ts new file mode 100644 index 0000000..7585422 --- /dev/null +++ b/tests/commands/asset-registry/asset-registry-module.spec.ts @@ -0,0 +1,59 @@ +import Module = require("../../../src/commands/asset-registry/module"); +import { Command, OptionValues } from "commander"; +import { AssetRegistryService } from "../../../src/commands/asset-registry/asset-registry.service"; +import { testContext } from "../../utls/test-context"; + +jest.mock("../../../src/commands/asset-registry/asset-registry.service"); + +describe("Asset Registry Module", () => { + let module: Module; + let mockCommand: Command; + let mockService: jest.Mocked; + + beforeEach(() => { + jest.clearAllMocks(); + module = new Module(); + mockCommand = {} as Command; + + mockService = { + listTypes: jest.fn().mockResolvedValue(undefined), + getType: jest.fn().mockResolvedValue(undefined), + getSchema: jest.fn().mockResolvedValue(undefined), + getExamples: jest.fn().mockResolvedValue(undefined), + getMethodology: jest.fn().mockResolvedValue(undefined), + } as any; + + (AssetRegistryService as jest.MockedClass) + .mockImplementation(() => mockService); + }); + + it("should call getSchema with correct parameters", async () => { + const options: OptionValues = { assetType: "BOARD_V2", json: true }; + await (module as any).getSchema(testContext, mockCommand, options); + expect(mockService.getSchema).toHaveBeenCalledWith("BOARD_V2", true); + }); + + it("should call getExamples with correct parameters", async () => { + const options: OptionValues = { assetType: "BOARD_V2", json: "" }; + await (module as any).getExamples(testContext, mockCommand, options); + expect(mockService.getExamples).toHaveBeenCalledWith("BOARD_V2", false); + }); + + it("should call getMethodology with correct parameters", async () => { + const options: OptionValues = { assetType: "SEMANTIC_MODEL" }; + await (module as any).getMethodology(testContext, mockCommand, options); + expect(mockService.getMethodology).toHaveBeenCalledWith("SEMANTIC_MODEL", false); + }); + + it("should call listTypes", async () => { + const options: OptionValues = { json: true }; + await (module as any).listTypes(testContext, mockCommand, options); + expect(mockService.listTypes).toHaveBeenCalledWith(true); + }); + + it("should call getType", async () => { + const options: OptionValues = { assetType: "BOARD_V2", json: "" }; + await (module as any).getType(testContext, mockCommand, options); + expect(mockService.getType).toHaveBeenCalledWith("BOARD_V2", false); + }); +}); From 0bb130abc7de45f1255273a336b97a77ce61ec70 Mon Sep 17 00:00:00 2001 From: zgjimhaziri Date: Fri, 17 Apr 2026 15:27:09 +0200 Subject: [PATCH 3/3] SP-45: Fix http response code in the skill Includes-AI-Code: true Made-with: Cursor --- .cursor/skills/asset-registry-endpoints/SKILL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.cursor/skills/asset-registry-endpoints/SKILL.md b/.cursor/skills/asset-registry-endpoints/SKILL.md index 095b0ba..e2068ee 100644 --- a/.cursor/skills/asset-registry-endpoints/SKILL.md +++ b/.cursor/skills/asset-registry-endpoints/SKILL.md @@ -248,7 +248,7 @@ are optional. service. A 500 typically means the downstream service is unavailable or returned an unexpected response. -**Errors on import (400 / 500)** — Ensure `spaceId` is set on every node and +**Errors on import (400)** — Ensure `spaceId` is set on every node and `schemaVersion` matches the descriptor's `assetSchema.version`. ## Full worked example