From c1c0fdb27f7cc866ef854ddefdf4a144b4531c0a Mon Sep 17 00:00:00 2001 From: Monir Imamverdi Date: Thu, 30 Jul 2026 07:50:05 -0700 Subject: [PATCH] feat(bpmn): author runnable Integration Service activities --- skills/uipath-maestro-bpmn/SKILL.md | 12 +- .../references/cli-conventions.md | 3 + ...ration-service-activity-authoring-guide.md | 230 +++++ .../references/registry-workflow.md | 16 +- .../activity_authoring.yaml | 97 +++ .../check_activity_authoring.py | 795 ++++++++++++++++++ .../fixtures/activity-request.json | 27 + .../fixtures/mocks/responses/activities.json | 42 + .../fixtures/mocks/responses/connections.json | 30 + .../mocks/responses/login-status.json | 8 + .../fixtures/mocks/responses/manifest.json | 103 +++ .../mocks/responses/registry-get-base.json | 22 + .../mocks/responses/registry-get-curated.json | 80 ++ .../mocks/responses/registry-get-generic.json | 56 ++ .../responses/registry-get-variables.json | 24 + .../mocks/responses/registry-list.json | 24 + .../mocks/responses/registry-pull.json | 11 + .../responses/resource-curated-available.json | 25 + .../responses/resource-curated-enriched.json | 60 ++ .../responses/resource-curated-operation.json | 55 ++ .../responses/resource-generic-operation.json | 56 ++ .../mocks/responses/resource-generic.json | 24 + .../mocks/responses/resources-list.json | 24 + .../mocks/responses/validate-unavailable.json | 6 + 24 files changed, 1821 insertions(+), 9 deletions(-) create mode 100644 skills/uipath-maestro-bpmn/references/integration-service-activity-authoring-guide.md create mode 100644 tests/tasks/uipath-maestro-bpmn/connector/activity_authoring/activity_authoring.yaml create mode 100644 tests/tasks/uipath-maestro-bpmn/connector/activity_authoring/check_activity_authoring.py create mode 100644 tests/tasks/uipath-maestro-bpmn/connector/activity_authoring/fixtures/activity-request.json create mode 100644 tests/tasks/uipath-maestro-bpmn/connector/activity_authoring/fixtures/mocks/responses/activities.json create mode 100644 tests/tasks/uipath-maestro-bpmn/connector/activity_authoring/fixtures/mocks/responses/connections.json create mode 100644 tests/tasks/uipath-maestro-bpmn/connector/activity_authoring/fixtures/mocks/responses/login-status.json create mode 100644 tests/tasks/uipath-maestro-bpmn/connector/activity_authoring/fixtures/mocks/responses/manifest.json create mode 100644 tests/tasks/uipath-maestro-bpmn/connector/activity_authoring/fixtures/mocks/responses/registry-get-base.json create mode 100644 tests/tasks/uipath-maestro-bpmn/connector/activity_authoring/fixtures/mocks/responses/registry-get-curated.json create mode 100644 tests/tasks/uipath-maestro-bpmn/connector/activity_authoring/fixtures/mocks/responses/registry-get-generic.json create mode 100644 tests/tasks/uipath-maestro-bpmn/connector/activity_authoring/fixtures/mocks/responses/registry-get-variables.json create mode 100644 tests/tasks/uipath-maestro-bpmn/connector/activity_authoring/fixtures/mocks/responses/registry-list.json create mode 100644 tests/tasks/uipath-maestro-bpmn/connector/activity_authoring/fixtures/mocks/responses/registry-pull.json create mode 100644 tests/tasks/uipath-maestro-bpmn/connector/activity_authoring/fixtures/mocks/responses/resource-curated-available.json create mode 100644 tests/tasks/uipath-maestro-bpmn/connector/activity_authoring/fixtures/mocks/responses/resource-curated-enriched.json create mode 100644 tests/tasks/uipath-maestro-bpmn/connector/activity_authoring/fixtures/mocks/responses/resource-curated-operation.json create mode 100644 tests/tasks/uipath-maestro-bpmn/connector/activity_authoring/fixtures/mocks/responses/resource-generic-operation.json create mode 100644 tests/tasks/uipath-maestro-bpmn/connector/activity_authoring/fixtures/mocks/responses/resource-generic.json create mode 100644 tests/tasks/uipath-maestro-bpmn/connector/activity_authoring/fixtures/mocks/responses/resources-list.json create mode 100644 tests/tasks/uipath-maestro-bpmn/connector/activity_authoring/fixtures/mocks/responses/validate-unavailable.json diff --git a/skills/uipath-maestro-bpmn/SKILL.md b/skills/uipath-maestro-bpmn/SKILL.md index 0a48f5044e..c2ae2426c7 100644 --- a/skills/uipath-maestro-bpmn/SKILL.md +++ b/skills/uipath-maestro-bpmn/SKILL.md @@ -105,6 +105,10 @@ is per command: `login status --profile ` verifies that context but does not make later commands inherit it, so repeat the same `--profile ` on every tenant-dependent registry, queue, and connection command. Portable built-in lookups deliberately omit it. +After an exact Integration Service connection is resolved, author a runnable +`Intsvc.ActivityExecution` with the catalog, object, schema, and V1 binding +workflow in +[references/integration-service-activity-authoring-guide.md](references/integration-service-activity-authoring-guide.md). For registry-evidence-only tasks, be command-first and time-boxed: @@ -152,8 +156,11 @@ For registry-evidence-only tasks, be command-first and time-boxed: context, adding the same `--profile ` when that context is named; in portable mode, use it without a profile. When live authoritative binding metadata requires a resource, resolve it with the adapter selected in step 1 - before claiming the node is runnable. In draft mode, preserve the unresolved - fields. Do not call + before claiming the node is runnable. For `Intsvc.ActivityExecution`, + resolving a connection is only the first boundary; follow the + activity-authoring guide to resolve the catalog operation, concrete object, + method, path, request fields, and response schema. In draft mode, preserve + the unresolved fields. Do not call `registry get` for structural gaps the registry never owns: sequence flows, gateways, events, boundary events, multi-instance/loop markers, `errorMapping`/retry structure, or @@ -337,6 +344,7 @@ and honestly surfaced to the user as gaps when asked. | Topic | Read | | --- | --- | | Discover → template → bind → assemble loop | [references/registry-workflow.md](references/registry-workflow.md) | +| Runnable Integration Service activity authoring | [references/integration-service-activity-authoring-guide.md](references/integration-service-activity-authoring-guide.md) | | Structural BPMN, event matrix, boundary events, containers, multi-instance, diagram, validation | [references/structural-bpmn.md](references/structural-bpmn.md) | | Runtime expressions, `vars.`/`bindings.`/`iterator.`, `=js:` (Jint) syntax | [references/expression-authoring.md](references/expression-authoring.md) | | CLI conventions and the side-effect boundary | [references/cli-conventions.md](references/cli-conventions.md) | diff --git a/skills/uipath-maestro-bpmn/references/cli-conventions.md b/skills/uipath-maestro-bpmn/references/cli-conventions.md index b86b6ac84b..cc20ee91a8 100644 --- a/skills/uipath-maestro-bpmn/references/cli-conventions.md +++ b/skills/uipath-maestro-bpmn/references/cli-conventions.md @@ -21,6 +21,9 @@ All commands below are discovery/read-only. None mutate cloud state. | `uip maestro bpmn registry get [--profile ] [--connection-id ] [--object-name ] --output json` | Get the full spec for one extension type: `XmlTemplate`, `ContextFields`, `BindingPattern`, `BindingInfo`, and input/output patterns. `--connection-id`/`--object-name` add live Integration Service field metadata for connector types. | | `uip or queues list [--profile ] [--folder-key \|--folder-path \|--all-folders] --output json` | Resolve queue bindings in an exact supplied folder, or exhaustively when scope is unknown. | | `uip is connections list [--profile ] --all-folders [--refresh] --output json` | Resolve enabled connections for one exact, registry-discovered connector key across every accessible folder. `--refresh` bypasses the connection cache. | +| `uip is activities list [--profile ] --output json` | List the connector's curated and generic activity catalog rows. Curated rows carry a concrete `ObjectName`; generic CRUD rows carry an `Operation` and report `ObjectName: N/A`. | +| `uip is resources list [--profile ] --connection-id --operation --output json` | Resolve a concrete object for a generic CRUD activity. Do not use it for a curated row that already supplies `ObjectName`. | +| `uip is resources describe [--profile ] [--connection-id ] [--operation ] [-f ] --output json` | Resolve the selected object's exact method, path, parameters, request fields, and response fields. Repeat `-f` only for known parent values when the schema is parent-dependent. | Square brackets mark syntax that is optional only when no named profile was selected. Once the user selects a profile for live discovery, include diff --git a/skills/uipath-maestro-bpmn/references/integration-service-activity-authoring-guide.md b/skills/uipath-maestro-bpmn/references/integration-service-activity-authoring-guide.md new file mode 100644 index 0000000000..3eb4c32378 --- /dev/null +++ b/skills/uipath-maestro-bpmn/references/integration-service-activity-authoring-guide.md @@ -0,0 +1,230 @@ +# Integration Service activity authoring + +Use this guide for one live, runnable `Intsvc.ActivityExecution` after +[live resource resolution](live-resource-resolution-guide.md) has selected one +exact enabled connection. It applies to every Integration Service connector and +login context; do not infer a provider, environment, organization, tenant, or +profile. + +This is an authoring workflow. The catalog, resource, and schema commands are +read-only. Never invoke `uip is resources run`, mutate a connection, or call the +external operation merely to discover its contract. + +## 1. Require the current registry-owned V1 template + +Retrieve the activity contract in the selected context: + +```bash +uip maestro bpmn registry get Intsvc.ActivityExecution --output json +``` + +For a named login profile, repeat the same `--profile ` on this and every +other tenant-dependent command in this guide. + +The current V1 `XmlTemplate` contains: + +- `connectorKey`, `connection`, `folderKey`, `operation`, `objectName`, + `method`, `path`, and `activityConfigurationVersion` context inputs, each + with `type="string"`, plus JSON `metadata`; +- distinct `{connectionBindingId}` and `{folderBindingId}` placeholders, with + no `resourceKey` context input; +- `InputFields: []` and the `separateInputs` pattern: request fields come from + the selected operation as direct children of `uipath:activity`, after the + closed `uipath:context`, rather than as context fields or fixed grouped + path/query containers; and +- one registry-owned output with `name="response"`, `type="jsonSchema"`, and + `source="=response"`. + +If the retrieved template instead has a legacy `activity` context, untyped +string context inputs, a context-level `resourceKey`, one binding placeholder, +a literal folder value, fixed `pathParameters` / `queryParameters` inputs, or a +`result` / `custom` / `.` output, the CLI predates this runnable V1 contract. +Do not rewrite the registry payload by hand. Update the CLI or keep the node a +non-runnable draft. + +## 2. Select an exact catalog activity + +List the catalog only after the connector and connection are known: + +```bash +uip is activities list --output json +``` + +Match the requested capability to one exact row. Preserve its values verbatim: + +| Row shape | Meaning | Object resolution | +| --- | --- | --- | +| `IsCurated: Yes`, concrete `ObjectName`, concrete `MethodName`, `Operation: N/A` | Connector-specific curated activity | Use the row's `ObjectName`; do not run `resources list`. | +| `IsCurated: No`, `ObjectName: N/A`, concrete CRUD `Operation` | Generic CRUD activity | Discover a concrete object with `resources list`. | + +The BPMN context `operation` is always the selected activity row's exact +`Name`, including for generic CRUD. `Operation` on a generic row is the schema +operation used for resource discovery; it does not replace the catalog `Name`. +If multiple rows remain plausible, resolve the ambiguity instead of selecting +the first. + +For a generic row, list only resources supporting its schema operation: + +```bash +uip is resources list \ + --connection-id \ + --operation \ + --output json +``` + +Select one exact `Name` from the result and use it as the BPMN +`objectName`. Never serialize `N/A`, a display name, or an object from a +different connector. + +## 3. Resolve and verify the operation schema + +Inspect available operations independently for every selected object, curated +and generic, then describe the selected one. Do this for the generic object +even when its catalog row already names a CRUD operation: + +```bash +uip is resources describe \ + --connection-id \ + --output json + +uip is resources describe \ + --connection-id \ + --operation \ + --output json +``` + +For a curated row, select the available operation whose `method` equals the +row's exact `MethodName`, then pass that operation's exact CRUD `name` (for +example, `Create`) as ``. Verify that the described +operation's `method` still equals the catalog `MethodName`. If more than one +available operation has that method and the contract does not otherwise +disambiguate them, stop. For a generic row, require an available operation whose +exact `name` equals the catalog row's exact `Operation`; use that name as +``, and stop if it is absent. + +Some schemas depend on already chosen parent request values. When the described +schema says more fields depend on those values, repeat `resources describe` +with the same connection and operation plus one `-f ` per +known parent field. Do not guess parent values or an ObjectAction. If the CLI +cannot produce the fields required by the business payload, keep the node +blocked. + +Accept the schema only when all of these identities agree: + +- connection discovery and the two schema calls use the selected connection; +- the result `elementKey` equals the selected connector key; +- the result `name` equals the selected object name; +- the selected operation's `method` and `path` are present; and +- an enriched registry lookup for the same connection and object reports the + same connector and object: + + ```bash + uip maestro bpmn registry get Intsvc.ActivityExecution \ + --connection-id \ + --object-name \ + --output json + ``` + +An omitted `ISEnrichment`, mismatched `ElementKey`, or mismatched `Name` blocks +a runnable claim. Raw enrichment is evidence, not runtime payload. + +## 4. Fill only the retrieved template + +Preserve the retrieved template's `uipath:activity`, type, context, and output +structure. If its BPMN host tag is PascalCase, normalize only that host tag from +`bpmn:SendTask` to `bpmn:sendTask`. Fill the V1 context as follows and preserve +`type="string"` on every string row: + +| Context input | Exact source | +| --- | --- | +| `activityConfigurationVersion` | Registry literal `v1` | +| `connectorKey` | Selected catalog/schema connector key | +| `connection` | `=bindings.` | +| `folderKey` | `=bindings.` when the exact selected connection row supplies a nonempty folder key; otherwise omit it | +| `operation` | Catalog activity row `Name` | +| `objectName` | Curated row `ObjectName`, or selected generic resource `Name` | +| `method` / `path` | Selected `resources describe` operation | +| `metadata` | `{}` unless the retrieved runtime template explicitly requires another value; never paste `ISEnrichment` | + +Declare a root connection `` with `resource="Connection"`, +`type="string"`, `default=""`, and +`propertyAttribute="ConnectionId"`. When the exact selected connection row +supplies a nonempty folder key, preserve it by adding a distinct folder binding +that also uses `resource="Connection"` and `type="string"`, with +`default=""` and `propertyAttribute="folderKey"`; omit both the +folder binding and the `folderKey` context input only when discovery supplies no +folder key. Point each present context input at its binding id. When both +bindings exist, give them nonempty distinct names and the same `resourceKey`. +Never copy `resourceKey` into `uipath:context`. + +When folder context is present, the V1 validator requires a nonempty matching +`resourceKey` on the two root bindings; it does not choose that source-model +identity. Preserve an explicit root-binding resource key supplied by an +existing solution. For standalone source with no supplied key, use the selected +connection id as the deterministic fallback rather than inventing another +identifier. Reuse a binding pair only for activities with the same connection +and the same resource key; the same connection id can legitimately appear +under different solution resource keys. + +These root bindings are source model state. `bindings_v2.json`, +`entry-points.json`, `operate.json`, and `package-descriptor.json` are generated +package state: never hand-create or edit them. + +## 5. Serialize request inputs from the described schema + +Treat `requestFields` and `parameters` from the accepted operation description +as the allowlist. Add each selected field as its own `uipath:input` directly +under `uipath:activity`, as a sibling immediately after the closed +`uipath:context` and before `uipath:output`. Never put operation inputs inside +`uipath:context`; that element contains only the registry-declared context rows +from section 4. The registry template intentionally contains no fixed +request-input rows. + +```xml +... + + +``` + +- Include every required request field and every optional request field the + business request actually supplies. Exclude response-only fields and unused + optional fields. +- Use each exact `name`, not a dictionary key, display name, or provider + synonym. Presentation-only metadata such as `RequestCurated` never turns a + response-only field into a request field. +- For each operation parameter, copy its `dataType`, exact name, and target + (`path` or `query`) onto one input. Keep the context `path` exactly as + described, including its `{parameter}` tokens. +- For each selected request field, copy its described type and target. Body + fields use `target="body"`; preserve dotted names such as `record.owner.id` + as the operation describes them instead of regrouping them into one JSON + object. +- Keep file fields separate with their exact names, described types, and + `target="file"`. +- Do not emit fixed `pathParameters` or `queryParameters` inputs, empty group + placeholders, or an unused `body` input. Use JSON element content only when + the selected field itself has `type="json"`. + +If a required parameter uses an unverified target, a dotted array path cannot +be represented without changing its meaning, or a field type cannot be mapped +without guessing, stop instead of claiming the node is runnable. + +## 6. Preserve the response contract + +Keep the template's one `uipath:output` with `name="response"`, +`type="jsonSchema"`, and `source="=response"`, and point it at a response +variable. Declare that variable as `jsonSchema` and choose its scope for its +consumers: a root `uipath:output` (without `elementId`) is visible to +runtime/global inspection, while `elementId=""` limits an +`inputOutput` variable to that element. Preserve an existing variable's scope +when editing. Construct its properties from the accepted `responseFields`, +preserving exact names and types. Reconstruct dotted response names into nested +object properties. Do not add request-only fields or invent response +properties. + +Finally, wire the task structurally, add its diagram shape and edge waypoints, +and run local BPMN validation. Passing validation is structural preflight; for +dynamic Integration Service payloads it does not prove that every discovered +operation field has the right target or type. Check those against the selected +operation schema. Only an authorized live run proves the external operation's +business behavior. diff --git a/skills/uipath-maestro-bpmn/references/registry-workflow.md b/skills/uipath-maestro-bpmn/references/registry-workflow.md index ba0053fa8a..fa0ded1dc1 100644 --- a/skills/uipath-maestro-bpmn/references/registry-workflow.md +++ b/skills/uipath-maestro-bpmn/references/registry-workflow.md @@ -140,13 +140,15 @@ properties, connection binding, and schemas. - **Connector activity** (`Intsvc.ActivityExecution` / a connector-authenticated operation): use when the call goes through a tenant connection, a dynamic - connector schema, or a connector object operation. Keep the node **draft** - until enriched. The CLI-owned enrichment blockers — the ones that must be - resolved before upload or run, and that boundary notes should name explicitly - — are **connection binding**, **dynamic schemas**, generated **package - metadata** (`bindings_v2.json`, `entry-points.json`, `operate.json`, - `package-descriptor.json`). Do not hand-author any of these; follow the - [live binding boundary](live-resource-resolution-guide.md#5-preserve-the-binding-boundary). + connector schema, or a connector object operation. Connection resolution + alone does not make the node runnable. Follow + [integration-service-activity-authoring-guide.md](integration-service-activity-authoring-guide.md) + to select a curated or generic catalog row, resolve its concrete object, + describe its operation-specific schema, and fill the retrieved V1 template. + Keep the node **draft** until those identities and schemas are complete. + Author source-level root bindings from that retrieved contract, but never + hand-generate package metadata (`bindings_v2.json`, `entry-points.json`, + `operate.json`, or `package-descriptor.json`). - **Connectionless / manual HTTP** (`Intsvc.HttpExecution`, or `Intsvc.UnifiedHttpRequest` when current tooling exposes the unified shape): use when the workflow itself owns the URL, method, payload, and response diff --git a/tests/tasks/uipath-maestro-bpmn/connector/activity_authoring/activity_authoring.yaml b/tests/tasks/uipath-maestro-bpmn/connector/activity_authoring/activity_authoring.yaml new file mode 100644 index 0000000000..b1871e7bc6 --- /dev/null +++ b/tests/tasks/uipath-maestro-bpmn/connector/activity_authoring/activity_authoring.yaml @@ -0,0 +1,97 @@ +task_id: skill-bpmn-is-activity-authoring +description: > + Provider-neutral mocked Integration Service authoring eval: from one exact + enabled connection, the agent must resolve and author both a curated activity + and a generic CRUD activity. It exercises catalog identity, object and + operation schema discovery, parent-dependent request fields, distinct source + resource ownership, operation-specific direct request inputs and targets, + response schemas, + registry-template preservation, and a strict read-only boundary. +tags: [uipath-maestro-bpmn, integration, "mode:build", "lifecycle:generate", "shape:multi-node", connector, "feature:registry", "feature:connections", "feature:activities"] + +run_limits: + expected_turns: 65 + max_turns: 75 + +sandbox: + template_sources: + - type: template_dir + path: ../../../../../skills/uipath-maestro-bpmn + - type: template_dir + path: ../../_shared/mock_template + - type: template_dir + path: fixtures + mock_path_dirs: + - mocks + +initial_prompt: | + Use the `uipath-maestro-bpmn` skill to build + a plain local Maestro project at `ConnectorContract/` from + `activity-request.json`. Include both `project.uiproj` and + `ConnectorContract.bpmn`; do not generate package metadata. + Author both requested Integration Service activities in their listed order, + using the exact named connection and named profile. + + Discover the current registry, catalog, object, operation, request, and + response contracts through the installed `uip` CLI rather than guessing. + Preserve the supplied root-binding resource ownership and make the resulting + source complete under the current V1 contract. + + This is read-only authoring: do not invoke an external connector operation, + mutate a connection, package the project, or inspect `mocks/` or canned + response files directly. + +success_criteria: + - type: run_command + description: "Curated and generic activities preserve the current V1 source contracts" + command: "python3 $TASK_DIR/check_activity_authoring.py" + timeout: 30 + expected_exit_code: 0 + weight: 7.0 + pass_threshold: 1.0 + + - type: command_executed + description: "Agent discovered the connector's curated and generic activity catalog" + tool_name: "Bash" + command_pattern: '(uip|\$UIP)\s+is\s+activities\s+list\s+uipath-synthetic-records' + min_count: 1 + weight: 1.0 + pass_threshold: 1.0 + + - type: command_executed + description: "Agent resolved a concrete object only for the generic List activity" + tool_name: "Bash" + command_pattern: '(uip|\$UIP)\s+is\s+resources\s+list\s+uipath-synthetic-records[\s\S]*--operation(?:=|\s+)List' + min_count: 1 + weight: 1.0 + pass_threshold: 1.0 + + - type: command_executed + description: "Agent completed available, operation, and parent-dependent schema discovery" + tool_name: "Bash" + command_pattern: '(uip|\$UIP)\s+is\s+resources\s+describe\s+uipath-synthetic-records' + min_count: 5 + weight: 2.0 + pass_threshold: 1.0 + + - type: command_executed + description: "Agent verified both selected connector objects through enriched registry lookups" + tool_name: "Bash" + command_pattern: '(uip|\$UIP)\s+maestro\s+bpmn\s+registry\s+get\s+Intsvc\.ActivityExecution[\s\S]*--object-name' + min_count: 2 + weight: 1.0 + pass_threshold: 1.0 + + - type: command_not_executed + description: "Agent did not invoke connector operations, mutate connections, or package/deploy" + tool_name: "Bash" + command_pattern: '(uip|\$UIP)\s+(?:is\s+resources\s+run|is\s+connections\s+(?:create|edit|update|delete)|maestro\s+bpmn\s+(?:pack|publish|deploy|debug)|solution\s+(?:pack|publish|deploy|activate))' + weight: 3.0 + pass_threshold: 1.0 + + - type: command_not_executed + description: "Agent did not bypass discovery by reading mocked responses" + tool_name: "Bash" + command_pattern: 'mocks(?:/|\b)' + weight: 1.0 + pass_threshold: 1.0 diff --git a/tests/tasks/uipath-maestro-bpmn/connector/activity_authoring/check_activity_authoring.py b/tests/tasks/uipath-maestro-bpmn/connector/activity_authoring/check_activity_authoring.py new file mode 100644 index 0000000000..2311716e02 --- /dev/null +++ b/tests/tasks/uipath-maestro-bpmn/connector/activity_authoring/check_activity_authoring.py @@ -0,0 +1,795 @@ +#!/usr/bin/env python3 +"""Check provider-neutral authoring of curated and generic IS activities.""" + +from __future__ import annotations + +import json +import re +import sys +import xml.etree.ElementTree as ET +from pathlib import Path +from typing import Any + + +PROJECT = Path("ConnectorContract") +BPMN = PROJECT / "ConnectorContract.bpmn" +PROJECT_FILE = PROJECT / "project.uiproj" +CALL_LOG = Path("mocks/.calls.jsonl") + +CONNECTION_ID = "11111111-2222-4333-8444-555555555555" +FOLDER_KEY = "aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee" +SOLUTION_RESOURCE_KEY = "solution-resource-synthetic-primary" +CONNECTOR_KEY = "uipath-synthetic-records" +PROFILE = "connector-contract" + +BPMN_NS = "http://www.omg.org/spec/BPMN/20100524/MODEL" +BPMNDI_NS = "http://www.omg.org/spec/BPMN/20100524/DI" +DI_NS = "http://www.omg.org/spec/DD/20100524/DI" +DC_NS = "http://www.omg.org/spec/DD/20100524/DC" +UIPATH_NS = "http://uipath.org/schema/bpmn" + +ALLOWED_ELEMENT_NAMESPACES = {BPMN_NS, BPMNDI_NS, DI_NS, DC_NS, UIPATH_NS} +RECOGNIZED_ELEMENT_NAMESPACES = { + **dict.fromkeys( + ( + "definitions", + "process", + "extensionElements", + "startEvent", + "endEvent", + "sendTask", + "sequenceFlow", + "incoming", + "outgoing", + ), + BPMN_NS, + ), + **dict.fromkeys( + ("BPMNDiagram", "BPMNPlane", "BPMNShape", "BPMNEdge"), + BPMNDI_NS, + ), + "waypoint": DI_NS, + "Bounds": DC_NS, + **dict.fromkeys( + ( + "variables", + "inputOutput", + "output", + "bindings", + "binding", + "entryPointId", + "activity", + "type", + "context", + "input", + ), + UIPATH_NS, + ), +} + +STRING_CONTEXT_NAMES = { + "activityConfigurationVersion", + "connectorKey", + "connection", + "folderKey", + "operation", + "objectName", + "method", + "path", +} + + +def fail(message: str) -> None: + sys.exit(f"FAIL: {message}") + + +def local_name(element: ET.Element) -> str: + return element.tag.rsplit("}", 1)[-1] + + +def assert_element_namespaces(root: ET.Element) -> None: + expected_root = f"{{{BPMN_NS}}}definitions" + if root.tag != expected_root: + fail(f"BPMN root must be {expected_root!r}, got {root.tag!r}") + for element in root.iter(): + tag = element.tag + if not isinstance(tag, str) or not tag.startswith("{"): + fail(f"XML element {tag!r} must use an approved namespace") + namespace, separator, name = tag[1:].partition("}") + if not separator or not namespace or not name: + fail(f"XML element {tag!r} has an invalid expanded QName") + if namespace not in ALLOWED_ELEMENT_NAMESPACES: + fail(f"XML element {name!r} uses unsupported namespace {namespace!r}") + expected = RECOGNIZED_ELEMENT_NAMESPACES.get(name) + if expected is not None and namespace != expected: + fail( + f"recognized XML element {name!r} must use namespace " + f"{expected!r}, got {namespace!r}" + ) + + +def descendants(element: ET.Element, name: str) -> list[ET.Element]: + return [item for item in element.iter() if local_name(item) == name] + + +def direct_children(element: ET.Element, name: str) -> list[ET.Element]: + return [item for item in element if local_name(item) == name] + + +def one(items: list[ET.Element], label: str) -> ET.Element: + if len(items) != 1: + fail(f"expected exactly one {label}, found {len(items)}") + return items[0] + + +def unique_named(elements: list[ET.Element], label: str) -> dict[str, ET.Element]: + result: dict[str, ET.Element] = {} + for element in elements: + name = element.get("name") + if not name: + fail(f"{label} without a name") + if name in result: + fail(f"duplicate {label} name {name!r}") + result[name] = element + return result + + +def json_body(element: ET.Element, label: str) -> Any: + if element.get("type") != "json": + fail(f"{label} must have type=json") + text = (element.text or "").strip() + if not text: + fail(f"{label} must carry its JSON body as element content") + try: + return json.loads(text) + except json.JSONDecodeError as exc: + fail(f"{label} is not valid JSON: {exc}") + + +def binding_reference(value: str | None, label: str) -> str: + match = re.fullmatch(r"=bindings\.([A-Za-z0-9_-]+)", value or "") + if not match: + fail(f"{label} must be an exact =bindings. reference") + return match.group(1) + + +def context_for(task: ET.Element) -> tuple[ET.Element, dict[str, ET.Element]]: + extensions = one(direct_children(task, "extensionElements"), "task extensionElements") + activity = one(direct_children(extensions, "activity"), "uipath:activity") + if activity.get("version") != "v1": + fail(f"task {task.get('id')} activity version must be v1") + activity_type = one(direct_children(activity, "type"), "uipath:type") + if ( + activity_type.get("value") != "Intsvc.ActivityExecution" + or activity_type.get("version") != "v1" + ): + fail(f"task {task.get('id')} does not use Intsvc.ActivityExecution V1") + context = one(direct_children(activity, "context"), "uipath:context") + return activity, unique_named(direct_children(context, "input"), "context input") + + +def assert_context( + task: ET.Element, + context: dict[str, ET.Element], + expected: dict[str, str], +) -> None: + expected_names = { + *STRING_CONTEXT_NAMES, + "metadata", + } + if set(context) != expected_names: + fail( + f"task {task.get('id')} context names differ: " + f"expected {sorted(expected_names)}, got {sorted(context)}" + ) + for name, value in expected.items(): + if context[name].get("value") != value: + fail( + f"task {task.get('id')} context {name!r} must be {value!r}, " + f"got {context[name].get('value')!r}" + ) + for name in STRING_CONTEXT_NAMES: + if context[name].get("type") != "string": + fail( + f"task {task.get('id')} context {name!r} must use type=string" + ) + if json_body(context["metadata"], "metadata context") != {}: + fail("raw registry enrichment was serialized as runtime metadata") + + +def assert_binding_pair( + task: ET.Element, + context: dict[str, ET.Element], + bindings: dict[str, ET.Element], + resource_key: str, +) -> None: + connection_ref = binding_reference( + context["connection"].get("value"), f"{task.get('id')} connection" + ) + folder_ref = binding_reference( + context["folderKey"].get("value"), f"{task.get('id')} folderKey" + ) + if connection_ref == folder_ref: + fail(f"task {task.get('id')} reused one binding for connection and folder") + try: + connection = bindings[connection_ref] + folder = bindings[folder_ref] + except KeyError as exc: + fail(f"task {task.get('id')} references missing binding {exc.args[0]!r}") + if connection.get("name") == folder.get("name"): + fail(f"task {task.get('id')} connection and folder bindings need distinct names") + + expected_rows = ( + (connection, "ConnectionId", CONNECTION_ID), + (folder, "folderKey", FOLDER_KEY), + ) + for binding, property_attribute, default in expected_rows: + if binding.get("resource") != "Connection": + fail(f"binding {binding.get('id')} must use resource=Connection") + if binding.get("type") != "string": + fail(f"binding {binding.get('id')} must use type=string") + if not binding.get("name"): + fail(f"binding {binding.get('id')} must have a name") + if binding.get("resourceKey") != resource_key: + fail( + f"binding {binding.get('id')} must preserve resourceKey " + f"{resource_key!r}" + ) + if binding.get("propertyAttribute") != property_attribute: + fail( + f"binding {binding.get('id')} propertyAttribute must be " + f"{property_attribute!r}" + ) + if binding.get("default") != default: + fail(f"binding {binding.get('id')} has the wrong discovered default") + + +def schema_leaf_types(schema: Any, prefix: str = "") -> dict[str, str]: + if not isinstance(schema, dict): + fail("jsonSchema variable body must be an object") + properties = schema.get("properties") + if not isinstance(properties, dict): + fail("every response schema object must have a properties object") + result: dict[str, str] = {} + for name, definition in properties.items(): + if "." in name: + fail(f"response schema retained dotted key {name!r}") + if not isinstance(definition, dict): + fail(f"response schema definition for {name!r} must be an object") + path = f"{prefix}.{name}" if prefix else name + if isinstance(definition.get("properties"), dict): + if definition.get("type") not in (None, "object"): + fail(f"nested response property {path!r} must have object type") + result.update(schema_leaf_types(definition, path)) + else: + data_type = definition.get("type") + if not isinstance(data_type, str): + fail(f"response property {path!r} is missing a type") + result[path] = data_type + return result + + +def assert_response_schema( + task: ET.Element, + activity: ET.Element, + variables: dict[str, ET.Element], + expected_leaves: dict[str, str], +) -> str: + outputs = direct_children(activity, "output") + output = one(outputs, f"response output for {task.get('id')}") + if ( + output.get("name") != "response" + or output.get("type") != "jsonSchema" + or output.get("source") != "=response" + ): + fail(f"task {task.get('id')} did not preserve the registry response output") + variable_id = output.get("var") + if not variable_id or variable_id not in variables: + fail(f"task {task.get('id')} response output references a missing variable") + variable = variables[variable_id] + if variable.get("type") != "jsonSchema": + fail(f"response variable {variable_id} must use type=jsonSchema") + element_id = variable.get("elementId") + if element_id is not None and element_id != task.get("id"): + fail( + f"response variable {variable_id} has unrelated elementId " + f"{element_id!r}" + ) + try: + schema = json.loads((variable.text or "").strip()) + except json.JSONDecodeError as exc: + fail(f"response variable {variable_id} has invalid JSON schema: {exc}") + if schema.get("type") != "object": + fail(f"response variable {variable_id} schema root must have object type") + leaves = schema_leaf_types(schema) + if leaves != expected_leaves: + fail( + f"response variable {variable_id} leaves differ: " + f"expected {expected_leaves}, got {leaves}" + ) + return variable_id + + +def assert_operation_inputs( + activity: ET.Element, + label: str, + expected: dict[str, tuple[str, str, str]], +) -> None: + inputs = unique_named(direct_children(activity, "input"), f"{label} activity input") + if set(inputs) != set(expected): + fail( + f"{label} activity inputs differ: expected {sorted(expected)}, " + f"got {sorted(inputs)}" + ) + for name, (data_type, target, value) in expected.items(): + element = inputs[name] + actual = ( + element.get("type"), + element.get("target"), + element.get("value"), + ) + wanted = (data_type, target, value) + if actual != wanted: + fail(f"{label} input {name!r} must be {wanted}, got {actual}") + if (element.text or "").strip(): + fail(f"{label} scalar input {name!r} must use its value attribute") + + +def read_calls() -> list[str]: + if not CALL_LOG.is_file(): + fail("mock CLI call log is missing") + calls: list[str] = [] + for line in CALL_LOG.read_text(encoding="utf-8").splitlines(): + try: + record = json.loads(line) + except json.JSONDecodeError as exc: + fail(f"mock CLI call log is invalid JSON: {exc}") + calls.append(str(record.get("args", ""))) + return calls + + +def has_option(call: str, option: str, value: str) -> bool: + return re.search( + rf"(?:^|\s){re.escape(option)}(?:=|\s+){re.escape(value)}(?=\s|$)", + call, + ) is not None + + +def has_profile(call: str) -> bool: + return has_option(call, "--profile", PROFILE) + + +def index_of(calls: list[str], predicate: Any, label: str) -> int: + for index, call in enumerate(calls): + if predicate(call): + return index + fail(f"missing CLI discovery call: {label}") + + +def check_calls() -> None: + calls = read_calls() + login_index = index_of( + calls, + lambda call: "login status" in call and has_profile(call), + "named-profile login status", + ) + + parsed_markers = ( + "login status", + "maestro bpmn registry pull", + "maestro bpmn registry list", + "maestro bpmn registry search", + "maestro bpmn registry get", + "is connections list", + "is activities list", + "is resources list", + "is resources describe", + ) + # The required profiled status is located above and must precede discovery. + # A redundant read-only check of the default status does not select a source. + profile_markers = ( + "maestro bpmn registry pull", + "maestro bpmn registry list", + "maestro bpmn registry search", + "maestro bpmn registry get Intsvc.ActivityExecution", + "is connections list", + "is activities list", + "is resources list", + "is resources describe", + ) + for call in calls: + if any(marker in call for marker in parsed_markers): + if any(marker in call for marker in profile_markers) and not has_profile(call): + fail(f"live discovery call omitted the requested profile: {call}") + if "--output json" not in call and "--output=json" not in call: + fail(f"parsed CLI call omitted --output json: {call}") + + for index, call in enumerate(calls): + if index < login_index and any( + marker in call + for marker in profile_markers + if marker != "login status" + ): + fail("tenant-dependent discovery ran before login status") + + pulls = [call for call in calls if "maestro bpmn registry pull" in call] + if len(pulls) != 1: + fail(f"registry pull must run exactly once; observed {len(pulls)}") + + base_registry_index = index_of( + calls, + lambda call: ( + "maestro bpmn registry get Intsvc.ActivityExecution" in call + and "--object-name" not in call + ), + "base Intsvc.ActivityExecution registry contract", + ) + connection_indices = [ + index + for index, call in enumerate(calls) + if "is connections list uipath-synthetic-records" in call + ] + if len(connection_indices) != 1: + fail( + "exact connector connection discovery must run once; " + f"observed {len(connection_indices)}" + ) + connection_call = calls[connection_indices[0]] + if "--all-folders" not in connection_call: + fail("connection discovery was not exhaustive across folders") + + activity_indices = [ + index + for index, call in enumerate(calls) + if "is activities list uipath-synthetic-records" in call + ] + if len(activity_indices) != 1: + fail(f"catalog activity discovery must run once; observed {len(activity_indices)}") + + resource_list_indices = [ + index + for index, call in enumerate(calls) + if "is resources list uipath-synthetic-records" in call + ] + if len(resource_list_indices) != 1: + fail(f"generic resource discovery must run once; observed {len(resource_list_indices)}") + resource_list = calls[resource_list_indices[0]] + if ( + not has_option(resource_list, "--connection-id", CONNECTION_ID) + or not has_option(resource_list, "--operation", "List") + ): + fail("generic resource discovery omitted the exact connection or List operation") + + curated_calls = [ + (index, call) + for index, call in enumerate(calls) + if "resources describe uipath-synthetic-records curated_submit_artifact" in call + ] + generic_calls = [ + (index, call) + for index, call in enumerate(calls) + if "resources describe uipath-synthetic-records ledger_entries" in call + ] + for _, call in curated_calls + generic_calls: + if not has_option(call, "--connection-id", CONNECTION_ID): + fail(f"resource description omitted the exact connection: {call}") + + curated_available = [ + index + for index, call in curated_calls + if not re.search(r"(?:^|\s)--operation(?:=|\s)", call) + ] + curated_operation = [ + index + for index, call in curated_calls + if has_option(call, "--operation", "Create") + and not re.search(r"(?:^|\s)-f(?:=|\s)", call) + ] + curated_parent = [ + index + for index, call in curated_calls + if has_option(call, "--operation", "Create") + and has_option(call, "-f", "tenant.scope=finance") + ] + generic_available = [ + index + for index, call in generic_calls + if not re.search(r"(?:^|\s)--operation(?:=|\s)", call) + ] + generic_operation = [ + index for index, call in generic_calls + if has_option(call, "--operation", "List") + ] + phases = { + "curated available operations": curated_available, + "curated Create schema": curated_operation, + "curated parent-enriched schema": curated_parent, + "generic available operations": generic_available, + "generic List schema": generic_operation, + } + for label, indices in phases.items(): + if not indices: + fail(f"missing required resource-description phase: {label}") + if not ( + curated_available[0] < curated_operation[0] < curated_parent[0] + and generic_available[0] < generic_operation[0] + ): + fail("resource schemas were not resolved in available -> operation -> parent order") + + for object_name in ("curated_submit_artifact", "ledger_entries"): + matches = [ + index + for index, call in enumerate(calls) + if ( + "maestro bpmn registry get Intsvc.ActivityExecution" in call + and has_option(call, "--object-name", object_name) + and has_option(call, "--connection-id", CONNECTION_ID) + ) + ] + if not matches: + fail(f"missing enriched registry identity check for {object_name}") + + if not ( + login_index < base_registry_index + and connection_indices[0] < activity_indices[0] + and activity_indices[0] < resource_list_indices[0] + ): + fail("discovery did not follow context -> connection -> catalog -> resource order") + + validate_indices = [ + index for index, call in enumerate(calls) if "maestro bpmn validate" in call + ] + if not validate_indices: + fail("local BPMN validation preflight was not attempted") + if validate_indices[-1] < max( + curated_parent[0], + generic_operation[0], + ): + fail("BPMN validation preflight was attempted before schema discovery completed") + + forbidden = re.compile( + r"(?:is\s+resources\s+run|" + r"is\s+connections\s+(?:create|edit|update|delete)|" + r"maestro\s+bpmn\s+(?:pack|publish|deploy|debug)|" + r"solution\s+(?:pack|publish|deploy|activate))" + ) + for call in calls: + if forbidden.search(call): + fail(f"read-only authoring invoked a mutation or external operation: {call}") + + +def main() -> None: + if not BPMN.is_file(): + fail(f"{BPMN} is missing") + if not PROJECT_FILE.is_file(): + fail(f"{PROJECT_FILE} is missing") + try: + project = json.loads(PROJECT_FILE.read_text(encoding="utf-8")) + except json.JSONDecodeError as exc: + fail(f"project.uiproj is invalid JSON: {exc}") + if project.get("ProjectType") != "ProcessOrchestration": + fail("project.uiproj must use ProjectType=ProcessOrchestration") + if project.get("Name") != "ConnectorContract": + fail("project.uiproj must preserve the CLI-init project Name") + + try: + root = ET.parse(BPMN).getroot() + except ET.ParseError as exc: + fail(f"BPMN is not well-formed XML: {exc}") + assert_element_namespaces(root) + + process = one(descendants(root, "process"), "bpmn:process") + process_extensions = one( + direct_children(process, "extensionElements"), "process extensionElements" + ) + bindings_root = one(direct_children(process_extensions, "bindings"), "uipath:bindings") + if bindings_root.get("version") != "v1": + fail("source bindings must use version=v1") + binding_elements = direct_children(bindings_root, "binding") + if len(binding_elements) != 4: + fail( + "the distinct solution-resource and standalone identities require " + f"four bindings; found {len(binding_elements)}" + ) + bindings = { + element.get("id", ""): element + for element in binding_elements + if element.get("id") + } + if len(bindings) != 4: + fail("binding ids must be present and unique") + resource_key_counts: dict[str, int] = {} + for binding in binding_elements: + key = binding.get("resourceKey", "") + resource_key_counts[key] = resource_key_counts.get(key, 0) + 1 + if resource_key_counts != { + SOLUTION_RESOURCE_KEY: 2, + CONNECTION_ID: 2, + }: + fail( + "bindings did not preserve the existing solution resource key and " + f"the standalone connection-id fallback: {resource_key_counts}" + ) + + variables_root = one(direct_children(process_extensions, "variables"), "uipath:variables") + if variables_root.get("version") != "v1": + fail("response variables must use version=v1") + variable_elements = [ + variable + for variable in variables_root + if local_name(variable) in {"output", "inputOutput"} + ] + variables = { + variable.get("id", ""): variable + for variable in variable_elements + if variable.get("id") + } + if len(variables) != 2 or len(variable_elements) != 2: + fail("exactly two output-capable response variables are required") + + start = one(direct_children(process, "startEvent"), "start event") + end = one(direct_children(process, "endEvent"), "end event") + tasks = direct_children(process, "sendTask") + if len(tasks) != 2: + fail(f"expected exactly two Integration Service send tasks, found {len(tasks)}") + + task_records: dict[str, tuple[ET.Element, ET.Element, dict[str, ET.Element]]] = {} + for task in tasks: + activity, context = context_for(task) + operation = context.get("operation") + operation_name = operation.get("value") if operation is not None else None + if not operation_name or operation_name in task_records: + fail("tasks must have distinct catalog operation identities") + task_records[operation_name] = (task, activity, context) + if set(task_records) != {"SubmitArtifact", "ListAllRecords"}: + fail(f"wrong catalog operations authored: {sorted(task_records)}") + + curated, curated_activity, curated_context = task_records["SubmitArtifact"] + generic, generic_activity, generic_context = task_records["ListAllRecords"] + + assert_context( + curated, + curated_context, + { + "activityConfigurationVersion": "v1", + "connectorKey": CONNECTOR_KEY, + "operation": "SubmitArtifact", + "objectName": "curated_submit_artifact", + "method": "POST", + "path": "/v1/artifacts", + }, + ) + assert_binding_pair(curated, curated_context, bindings, SOLUTION_RESOURCE_KEY) + assert_operation_inputs( + curated_activity, + "curated", + { + "tenant.scope": ("string", "body", "finance"), + "artifact.title": ("string", "body", "Quarterly packet"), + "artifact.details.code": ("string", "body", "Q4-017"), + }, + ) + curated_variable_id = assert_response_schema( + curated, + curated_activity, + variables, + { + "submission.id": "string", + "submission.state": "string", + "server.receipt": "string", + }, + ) + + assert_context( + generic, + generic_context, + { + "activityConfigurationVersion": "v1", + "connectorKey": CONNECTOR_KEY, + "operation": "ListAllRecords", + "objectName": "ledger_entries", + "method": "GET", + "path": "/v2/tenants/{tenantId}/ledger-entries", + }, + ) + assert_binding_pair(generic, generic_context, bindings, CONNECTION_ID) + assert_operation_inputs( + generic_activity, + "generic", + { + "tenantId": ("string", "path", "tenant-west"), + "limit": ("integer", "query", "50"), + }, + ) + generic_variable_id = assert_response_schema( + generic, + generic_activity, + variables, + { + "items": "array", + "next.token": "string", + "count": "integer", + }, + ) + if curated_variable_id == generic_variable_id: + fail("the two activity responses must not overwrite the same variable") + + flows = direct_children(process, "sequenceFlow") + if len(flows) != 3: + fail(f"linear four-node process requires three sequence flows, found {len(flows)}") + flow_pairs = {(flow.get("sourceRef"), flow.get("targetRef")) for flow in flows} + expected_pairs = { + (start.get("id"), curated.get("id")), + (curated.get("id"), generic.get("id")), + (generic.get("id"), end.get("id")), + } + if flow_pairs != expected_pairs: + fail(f"activities are not wired in requested order: {flow_pairs}") + flow_ids_by_pair = { + (flow.get("sourceRef"), flow.get("targetRef")): flow.get("id") + for flow in flows + } + expected_node_refs = ( + (start, "outgoing", flow_ids_by_pair[(start.get("id"), curated.get("id"))]), + (curated, "incoming", flow_ids_by_pair[(start.get("id"), curated.get("id"))]), + (curated, "outgoing", flow_ids_by_pair[(curated.get("id"), generic.get("id"))]), + (generic, "incoming", flow_ids_by_pair[(curated.get("id"), generic.get("id"))]), + (generic, "outgoing", flow_ids_by_pair[(generic.get("id"), end.get("id"))]), + (end, "incoming", flow_ids_by_pair[(generic.get("id"), end.get("id"))]), + ) + for node, direction, expected_flow_id in expected_node_refs: + references = direct_children(node, direction) + if len(references) != 1 or (references[0].text or "").strip() != expected_flow_id: + fail( + f"node {node.get('id')} must have one {direction} reference " + f"to {expected_flow_id}" + ) + + diagrams = descendants(root, "BPMNDiagram") + diagram = one(diagrams, "bpmndi:BPMNDiagram") + plane = one(direct_children(diagram, "BPMNPlane"), "bpmndi:BPMNPlane") + if plane.get("bpmnElement") != process.get("id"): + fail("BPMNPlane does not reference the process") + shape_refs = { + shape.get("bpmnElement") + for shape in direct_children(plane, "BPMNShape") + } + node_ids = {start.get("id"), curated.get("id"), generic.get("id"), end.get("id")} + if not node_ids.issubset(shape_refs): + fail(f"BPMN diagram is missing node shapes: {sorted(node_ids - shape_refs)}") + edge_refs = { + edge.get("bpmnElement") + for edge in direct_children(plane, "BPMNEdge") + } + flow_ids = {flow.get("id") for flow in flows} + if not flow_ids.issubset(edge_refs): + fail(f"BPMN diagram is missing flow edges: {sorted(flow_ids - edge_refs)}") + for edge in direct_children(plane, "BPMNEdge"): + if edge.get("bpmnElement") in flow_ids and len(direct_children(edge, "waypoint")) < 2: + fail(f"diagram edge {edge.get('id')} needs at least two waypoints") + + xml_text = BPMN.read_text(encoding="utf-8") + for trap in ( + "ISEnrichment", + "RequestCurated", + "titleLookup", + "responseOnlyTrap", + 'name="resourceKey"', + 'name="pathParameters"', + 'name="queryParameters"', + 'name="result"', + 'type="custom"', + 'source="."', + ): + if trap in xml_text: + fail(f"authoring-only or stale contract field leaked into BPMN: {trap}") + + check_calls() + print( + "OK: curated + generic activities preserve registry, binding, request, " + "response, and read-only current V1 contracts" + ) + + +if __name__ == "__main__": + main() diff --git a/tests/tasks/uipath-maestro-bpmn/connector/activity_authoring/fixtures/activity-request.json b/tests/tasks/uipath-maestro-bpmn/connector/activity_authoring/fixtures/activity-request.json new file mode 100644 index 0000000000..ba2b236d35 --- /dev/null +++ b/tests/tasks/uipath-maestro-bpmn/connector/activity_authoring/fixtures/activity-request.json @@ -0,0 +1,27 @@ +{ + "profile": "connector-contract", + "project": "ConnectorContract", + "connector": { + "key": "uipath-synthetic-records", + "connectionName": "Synthetic Records Primary" + }, + "activities": [ + { + "catalogName": "SubmitArtifact", + "resourceKey": "solution-resource-synthetic-primary", + "values": { + "tenant.scope": "finance", + "artifact.title": "Quarterly packet", + "artifact.details.code": "Q4-017" + } + }, + { + "catalogName": "ListAllRecords", + "objectName": "ledger_entries", + "values": { + "tenantId": "tenant-west", + "limit": 50 + } + } + ] +} diff --git a/tests/tasks/uipath-maestro-bpmn/connector/activity_authoring/fixtures/mocks/responses/activities.json b/tests/tasks/uipath-maestro-bpmn/connector/activity_authoring/fixtures/mocks/responses/activities.json new file mode 100644 index 0000000000..9d7ea11131 --- /dev/null +++ b/tests/tasks/uipath-maestro-bpmn/connector/activity_authoring/fixtures/mocks/responses/activities.json @@ -0,0 +1,42 @@ +{ + "Result": "Success", + "Code": "ActivityList", + "Data": [ + { + "Name": "SubmitArtifact", + "DisplayName": "Submit artifact", + "Description": "Submit an artifact through a curated operation.", + "ObjectName": "curated_submit_artifact", + "MethodName": "POST", + "Operation": "N/A", + "IsCurated": "Yes" + }, + { + "Name": "SubmitArtifactPreview", + "DisplayName": "Submit artifact preview", + "Description": "Preview an artifact submission.", + "ObjectName": "curated_preview_artifact", + "MethodName": "POST", + "Operation": "N/A", + "IsCurated": "Yes" + }, + { + "Name": "ListAllRecords", + "DisplayName": "List records", + "Description": "List records for one selected object.", + "ObjectName": "N/A", + "MethodName": "N/A", + "Operation": "List", + "IsCurated": "No" + }, + { + "Name": "CreateRecord", + "DisplayName": "Create record", + "Description": "Create a record for one selected object.", + "ObjectName": "N/A", + "MethodName": "N/A", + "Operation": "Create", + "IsCurated": "No" + } + ] +} diff --git a/tests/tasks/uipath-maestro-bpmn/connector/activity_authoring/fixtures/mocks/responses/connections.json b/tests/tasks/uipath-maestro-bpmn/connector/activity_authoring/fixtures/mocks/responses/connections.json new file mode 100644 index 0000000000..014489a05c --- /dev/null +++ b/tests/tasks/uipath-maestro-bpmn/connector/activity_authoring/fixtures/mocks/responses/connections.json @@ -0,0 +1,30 @@ +{ + "Result": "Success", + "Code": "ConnectionList", + "Data": [ + { + "Id": "11111111-2222-4333-8444-555555555555", + "Name": "Synthetic Records Primary", + "ConnectorKey": "uipath-synthetic-records", + "State": "Enabled", + "FolderPath": "Shared/ContractLab", + "FolderKey": "aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee" + }, + { + "Id": "66666666-7777-4888-8999-000000000000", + "Name": "Synthetic Records Secondary", + "ConnectorKey": "uipath-synthetic-records", + "State": "Enabled", + "FolderPath": "Shared/Other", + "FolderKey": "bbbbbbbb-cccc-4ddd-8eee-ffffffffffff" + }, + { + "Id": "99999999-aaaa-4bbb-8ccc-dddddddddddd", + "Name": "Synthetic Records Primary", + "ConnectorKey": "uipath-synthetic-records", + "State": "Disabled", + "FolderPath": "Shared/ContractLab", + "FolderKey": "aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee" + } + ] +} diff --git a/tests/tasks/uipath-maestro-bpmn/connector/activity_authoring/fixtures/mocks/responses/login-status.json b/tests/tasks/uipath-maestro-bpmn/connector/activity_authoring/fixtures/mocks/responses/login-status.json new file mode 100644 index 0000000000..d00ec14096 --- /dev/null +++ b/tests/tasks/uipath-maestro-bpmn/connector/activity_authoring/fixtures/mocks/responses/login-status.json @@ -0,0 +1,8 @@ +{ + "Result": "Success", + "Data": { + "BaseUrl": "https://example.invalid", + "Organization": "contract-lab", + "Tenant": "design" + } +} diff --git a/tests/tasks/uipath-maestro-bpmn/connector/activity_authoring/fixtures/mocks/responses/manifest.json b/tests/tasks/uipath-maestro-bpmn/connector/activity_authoring/fixtures/mocks/responses/manifest.json new file mode 100644 index 0000000000..8bb98fb5cd --- /dev/null +++ b/tests/tasks/uipath-maestro-bpmn/connector/activity_authoring/fixtures/mocks/responses/manifest.json @@ -0,0 +1,103 @@ +{ + "version": 2, + "_doc": "Provider-neutral, read-only Integration Service activity authoring fixtures. Specific enrichment rules precede base registry and schema rules.", + "rules": [ + { + "match": "-f tenant.scope=finance", + "file": "resource-curated-enriched.json" + }, + { + "match": "-f=tenant.scope=finance", + "file": "resource-curated-enriched.json" + }, + { + "match": "--operation Create", + "file": "resource-curated-operation.json" + }, + { + "match": "--operation=Create", + "file": "resource-curated-operation.json" + }, + { + "match": "resources describe uipath-synthetic-records curated_submit_artifact", + "file": "resource-curated-available.json" + }, + { + "match": "is resources list uipath-synthetic-records", + "file": "resources-list.json" + }, + { + "match": "--operation List", + "file": "resource-generic-operation.json" + }, + { + "match": "--operation=List", + "file": "resource-generic-operation.json" + }, + { + "match": "resources describe uipath-synthetic-records ledger_entries", + "file": "resource-generic.json" + }, + { + "match": "maestro bpmn registry get Intsvc.ActivityExecution --connection-id 11111111-2222-4333-8444-555555555555 --object-name curated_submit_artifact", + "file": "registry-get-curated.json" + }, + { + "match": "maestro bpmn registry get Intsvc.ActivityExecution --connection-id 11111111-2222-4333-8444-555555555555 --object-name ledger_entries", + "file": "registry-get-generic.json" + }, + { + "match": "--object-name curated_submit_artifact", + "file": "registry-get-curated.json" + }, + { + "match": "--object-name=curated_submit_artifact", + "file": "registry-get-curated.json" + }, + { + "match": "--object-name ledger_entries", + "file": "registry-get-generic.json" + }, + { + "match": "--object-name=ledger_entries", + "file": "registry-get-generic.json" + }, + { + "match": "maestro bpmn registry get BPMN.Variables", + "file": "registry-get-variables.json" + }, + { + "match": "maestro bpmn registry get Intsvc.ActivityExecution", + "file": "registry-get-base.json" + }, + { + "match": "maestro bpmn registry list", + "file": "registry-list.json" + }, + { + "match": "maestro bpmn registry pull", + "file": "registry-pull.json" + }, + { + "match": "is connections list uipath-synthetic-records", + "file": "connections.json" + }, + { + "match": "is activities list uipath-synthetic-records", + "file": "activities.json" + }, + { + "match": "login status", + "file": "login-status.json" + }, + { + "match": "maestro bpmn validate", + "file": "validate-unavailable.json", + "exit_code": 1 + } + ], + "unmocked_default": { + "response": "{\"Result\":\"Failure\",\"Message\":\"Unsupported command in this read-only authoring fixture\"}\n", + "exit_code": 1 + } +} diff --git a/tests/tasks/uipath-maestro-bpmn/connector/activity_authoring/fixtures/mocks/responses/registry-get-base.json b/tests/tasks/uipath-maestro-bpmn/connector/activity_authoring/fixtures/mocks/responses/registry-get-base.json new file mode 100644 index 0000000000..d417f0f080 --- /dev/null +++ b/tests/tasks/uipath-maestro-bpmn/connector/activity_authoring/fixtures/mocks/responses/registry-get-base.json @@ -0,0 +1,22 @@ +{ + "Result": "Success", + "Code": "RegistryGetSuccess", + "Data": { + "ExtensionType": { + "ExtensionType": "Intsvc.ActivityExecution", + "Label": "Execute connector activity", + "BpmnElement": "bpmn:SendTask", + "ExtensionTag": "uipath:activity", + "BindingPattern": "connection", + "RequiresDiscovery": true, + "IsDynamic": true, + "InputFields": [], + "InputPattern": "separateInputs", + "OutputSource": "=response", + "OutputType": "jsonSchema", + "OutputName": "response", + "InputNotes": "Add each request field from the selected operation as its own uipath:input. Keep the field's type and target: path, query, body, or file.", + "XmlTemplate": "\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n {incomingEdge}\n {outgoingEdge}\n" + } + } +} diff --git a/tests/tasks/uipath-maestro-bpmn/connector/activity_authoring/fixtures/mocks/responses/registry-get-curated.json b/tests/tasks/uipath-maestro-bpmn/connector/activity_authoring/fixtures/mocks/responses/registry-get-curated.json new file mode 100644 index 0000000000..3e2ee3ef54 --- /dev/null +++ b/tests/tasks/uipath-maestro-bpmn/connector/activity_authoring/fixtures/mocks/responses/registry-get-curated.json @@ -0,0 +1,80 @@ +{ + "Result": "Success", + "Code": "RegistryGetSuccess", + "Data": { + "ExtensionType": { + "ExtensionType": "Intsvc.ActivityExecution", + "Label": "Execute connector activity", + "BpmnElement": "bpmn:SendTask", + "ExtensionTag": "uipath:activity", + "BindingPattern": "connection", + "RequiresDiscovery": true, + "IsDynamic": true, + "InputFields": [], + "InputPattern": "separateInputs", + "OutputSource": "=response", + "OutputType": "jsonSchema", + "OutputName": "response", + "InputNotes": "Add each request field from the selected operation as its own uipath:input. Keep the field's type and target: path, query, body, or file.", + "XmlTemplate": "\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n {incomingEdge}\n {outgoingEdge}\n" + }, + "ISEnrichment": { + "Name": "curated_submit_artifact", + "ElementKey": "uipath-synthetic-records", + "Metadata": { + "Method": { + "POST": { + "Path": "/v1/artifacts" + } + } + }, + "Fields": { + "scopeLookup": { + "Name": "tenant.scope", + "DisplayName": "Scope", + "Type": "string", + "Method": { + "POST": { + "Request": true, + "Required": true + } + } + }, + "titleLookup": { + "Name": "artifact.title", + "DisplayName": "Artifact title", + "Type": "string", + "Method": { + "POST": { + "Request": true, + "Required": true + } + }, + "RequestCurated": true + }, + "responseOnlyTrap": { + "Name": "server.receipt", + "DisplayName": "Server receipt", + "Type": "string", + "Method": { + "POST": { + "Response": true + } + }, + "RequestCurated": true + }, + "optionalLookup": { + "Name": "internal.note", + "DisplayName": "Internal note", + "Type": "string", + "Method": { + "POST": { + "Request": true, + "Required": false + } + } + } + } + } + } +} diff --git a/tests/tasks/uipath-maestro-bpmn/connector/activity_authoring/fixtures/mocks/responses/registry-get-generic.json b/tests/tasks/uipath-maestro-bpmn/connector/activity_authoring/fixtures/mocks/responses/registry-get-generic.json new file mode 100644 index 0000000000..964afa0f76 --- /dev/null +++ b/tests/tasks/uipath-maestro-bpmn/connector/activity_authoring/fixtures/mocks/responses/registry-get-generic.json @@ -0,0 +1,56 @@ +{ + "Result": "Success", + "Code": "RegistryGetSuccess", + "Data": { + "ExtensionType": { + "ExtensionType": "Intsvc.ActivityExecution", + "Label": "Execute connector activity", + "BpmnElement": "bpmn:SendTask", + "ExtensionTag": "uipath:activity", + "BindingPattern": "connection", + "RequiresDiscovery": true, + "IsDynamic": true, + "InputFields": [], + "InputPattern": "separateInputs", + "OutputSource": "=response", + "OutputType": "jsonSchema", + "OutputName": "response", + "InputNotes": "Add each request field from the selected operation as its own uipath:input. Keep the field's type and target: path, query, body, or file.", + "XmlTemplate": "\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n {incomingEdge}\n {outgoingEdge}\n" + }, + "ISEnrichment": { + "Name": "ledger_entries", + "ElementKey": "uipath-synthetic-records", + "Metadata": { + "Method": { + "GET": { + "Path": "/v2/tenants/{tenantId}/ledger-entries" + } + } + }, + "Fields": { + "responseItems": { + "Name": "items", + "DisplayName": "Items", + "Type": "array", + "Method": { + "GET": { + "Response": true + } + } + }, + "requestOnlyTrap": { + "Name": "unusedFilter", + "DisplayName": "Unused filter", + "Type": "string", + "Method": { + "GET": { + "Request": true, + "Required": false + } + } + } + } + } + } +} diff --git a/tests/tasks/uipath-maestro-bpmn/connector/activity_authoring/fixtures/mocks/responses/registry-get-variables.json b/tests/tasks/uipath-maestro-bpmn/connector/activity_authoring/fixtures/mocks/responses/registry-get-variables.json new file mode 100644 index 0000000000..d23f11287d --- /dev/null +++ b/tests/tasks/uipath-maestro-bpmn/connector/activity_authoring/fixtures/mocks/responses/registry-get-variables.json @@ -0,0 +1,24 @@ +{ + "Result": "Success", + "Code": "RegistryGetSuccess", + "Data": { + "ExtensionType": { + "ExtensionType": "BPMN.Variables", + "Label": "BPMN Variables", + "BpmnElement": "bpmn:Task", + "Placements": {}, + "ExtensionTag": "uipath:mapping", + "ContextFields": [], + "InputPattern": "none", + "OutputSource": null, + "OutputType": null, + "BindingPattern": "none", + "RequiresDiscovery": false, + "IsDynamic": false, + "XmlTemplate": "\n \n \n \n \n \n {incomingEdge}\n {outgoingEdge}\n", + "InputNotes": "Variable mappings use uipath:mapping with direct input/output elements, not a jsonBody pattern", + "BpmnElementNotes": "Also used on bpmn:StartEvent and bpmn:EndEvent for variable mappings. On bpmn:Task with no serviceType, defaults to BPMN.Variables.", + "ExtensionTagNotes": "Uses uipath:mapping (not uipath:activity)." + } + } +} diff --git a/tests/tasks/uipath-maestro-bpmn/connector/activity_authoring/fixtures/mocks/responses/registry-list.json b/tests/tasks/uipath-maestro-bpmn/connector/activity_authoring/fixtures/mocks/responses/registry-list.json new file mode 100644 index 0000000000..01acefbfdf --- /dev/null +++ b/tests/tasks/uipath-maestro-bpmn/connector/activity_authoring/fixtures/mocks/responses/registry-list.json @@ -0,0 +1,24 @@ +{ + "Result": "Success", + "Code": "RegistryListSuccess", + "Data": { + "ExtensionTypes": [ + { + "ExtensionType": "Intsvc.ActivityExecution", + "Label": "Execute connector activity", + "BindingPattern": "connection", + "RequiresDiscovery": "Yes" + } + ], + "Connectors": [ + { + "Key": "uipath-synthetic-records", + "Name": "Synthetic Records", + "Connections": 3, + "Activities": 4, + "Triggers": 0 + } + ], + "Processes": [] + } +} diff --git a/tests/tasks/uipath-maestro-bpmn/connector/activity_authoring/fixtures/mocks/responses/registry-pull.json b/tests/tasks/uipath-maestro-bpmn/connector/activity_authoring/fixtures/mocks/responses/registry-pull.json new file mode 100644 index 0000000000..462b628879 --- /dev/null +++ b/tests/tasks/uipath-maestro-bpmn/connector/activity_authoring/fixtures/mocks/responses/registry-pull.json @@ -0,0 +1,11 @@ +{ + "Result": "Success", + "Code": "RegistryPullSuccess", + "Data": { + "ExtensionTypeCount": 29, + "ConnectorCount": 1, + "ProcessCount": 0, + "FromCache": false, + "CacheWritten": true + } +} diff --git a/tests/tasks/uipath-maestro-bpmn/connector/activity_authoring/fixtures/mocks/responses/resource-curated-available.json b/tests/tasks/uipath-maestro-bpmn/connector/activity_authoring/fixtures/mocks/responses/resource-curated-available.json new file mode 100644 index 0000000000..189e8a47fa --- /dev/null +++ b/tests/tasks/uipath-maestro-bpmn/connector/activity_authoring/fixtures/mocks/responses/resource-curated-available.json @@ -0,0 +1,25 @@ +{ + "Result": "Success", + "Code": "ResourceMetadata", + "Data": { + "name": "curated_submit_artifact", + "displayName": "Submit artifact", + "elementKey": "uipath-synthetic-records", + "availableOperations": [ + { + "method": "POST", + "name": "Create", + "description": "Submit an artifact", + "path": "/v1/artifacts", + "curated": "Submit artifact" + }, + { + "method": "PATCH", + "name": "Update", + "description": "Update an artifact", + "path": "/v1/artifacts/{artifactId}" + } + ], + "hint": "Use --operation to see fields for a specific operation." + } +} diff --git a/tests/tasks/uipath-maestro-bpmn/connector/activity_authoring/fixtures/mocks/responses/resource-curated-enriched.json b/tests/tasks/uipath-maestro-bpmn/connector/activity_authoring/fixtures/mocks/responses/resource-curated-enriched.json new file mode 100644 index 0000000000..0717446e1d --- /dev/null +++ b/tests/tasks/uipath-maestro-bpmn/connector/activity_authoring/fixtures/mocks/responses/resource-curated-enriched.json @@ -0,0 +1,60 @@ +{ + "Result": "Success", + "Code": "ResourceMetadata", + "Data": { + "name": "curated_submit_artifact", + "displayName": "Submit artifact", + "elementKey": "uipath-synthetic-records", + "operation": { + "method": "POST", + "name": "Create", + "description": "Submit an artifact", + "path": "/v1/artifacts", + "curated": "Submit artifact" + }, + "parameters": [], + "requestFields": [ + { + "name": "tenant.scope", + "type": "string", + "displayName": "Scope", + "required": true + }, + { + "name": "artifact.title", + "type": "string", + "displayName": "Artifact title", + "required": true + }, + { + "name": "artifact.details.code", + "type": "string", + "displayName": "Artifact code", + "required": true + }, + { + "name": "internal.note", + "type": "string", + "displayName": "Internal note", + "required": false + } + ], + "responseFields": [ + { + "name": "submission.id", + "type": "string", + "displayName": "Submission ID" + }, + { + "name": "submission.state", + "type": "string", + "displayName": "Submission state" + }, + { + "name": "server.receipt", + "type": "string", + "displayName": "Server receipt" + } + ] + } +} diff --git a/tests/tasks/uipath-maestro-bpmn/connector/activity_authoring/fixtures/mocks/responses/resource-curated-operation.json b/tests/tasks/uipath-maestro-bpmn/connector/activity_authoring/fixtures/mocks/responses/resource-curated-operation.json new file mode 100644 index 0000000000..1d0be76b89 --- /dev/null +++ b/tests/tasks/uipath-maestro-bpmn/connector/activity_authoring/fixtures/mocks/responses/resource-curated-operation.json @@ -0,0 +1,55 @@ +{ + "Result": "Success", + "Code": "ResourceMetadata", + "Data": { + "name": "curated_submit_artifact", + "displayName": "Submit artifact", + "elementKey": "uipath-synthetic-records", + "operation": { + "method": "POST", + "name": "Create", + "description": "Submit an artifact", + "path": "/v1/artifacts", + "curated": "Submit artifact" + }, + "parameters": [], + "requestFields": [ + { + "name": "tenant.scope", + "type": "string", + "displayName": "Scope", + "required": true + }, + { + "name": "artifact.title", + "type": "string", + "displayName": "Artifact title", + "required": true + }, + { + "name": "internal.note", + "type": "string", + "displayName": "Internal note", + "required": false + } + ], + "responseFields": [ + { + "name": "submission.id", + "type": "string", + "displayName": "Submission ID" + }, + { + "name": "submission.state", + "type": "string", + "displayName": "Submission state" + }, + { + "name": "server.receipt", + "type": "string", + "displayName": "Server receipt" + } + ], + "hint": "Additional request fields depend on tenant.scope; repeat this command with -f tenant.scope=." + } +} diff --git a/tests/tasks/uipath-maestro-bpmn/connector/activity_authoring/fixtures/mocks/responses/resource-generic-operation.json b/tests/tasks/uipath-maestro-bpmn/connector/activity_authoring/fixtures/mocks/responses/resource-generic-operation.json new file mode 100644 index 0000000000..0a740ff28f --- /dev/null +++ b/tests/tasks/uipath-maestro-bpmn/connector/activity_authoring/fixtures/mocks/responses/resource-generic-operation.json @@ -0,0 +1,56 @@ +{ + "Result": "Success", + "Code": "ResourceMetadata", + "Data": { + "name": "ledger_entries", + "displayName": "Ledger entries", + "elementKey": "uipath-synthetic-records", + "operation": { + "method": "GET", + "name": "List", + "description": "List ledger entries", + "path": "/v2/tenants/{tenantId}/ledger-entries" + }, + "parameters": [ + { + "name": "tenantId", + "type": "path", + "dataType": "string", + "required": true, + "displayName": "Tenant ID" + }, + { + "name": "limit", + "type": "query", + "dataType": "integer", + "required": true, + "displayName": "Maximum records" + }, + { + "name": "cursor", + "type": "query", + "dataType": "string", + "required": false, + "displayName": "Cursor" + } + ], + "requestFields": [], + "responseFields": [ + { + "name": "items", + "type": "array", + "displayName": "Items" + }, + { + "name": "next.token", + "type": "string", + "displayName": "Next token" + }, + { + "name": "count", + "type": "integer", + "displayName": "Count" + } + ] + } +} diff --git a/tests/tasks/uipath-maestro-bpmn/connector/activity_authoring/fixtures/mocks/responses/resource-generic.json b/tests/tasks/uipath-maestro-bpmn/connector/activity_authoring/fixtures/mocks/responses/resource-generic.json new file mode 100644 index 0000000000..20ee13133d --- /dev/null +++ b/tests/tasks/uipath-maestro-bpmn/connector/activity_authoring/fixtures/mocks/responses/resource-generic.json @@ -0,0 +1,24 @@ +{ + "Result": "Success", + "Code": "ResourceMetadata", + "Data": { + "name": "ledger_entries", + "displayName": "Ledger entries", + "elementKey": "uipath-synthetic-records", + "availableOperations": [ + { + "method": "GET", + "name": "List", + "description": "List ledger entries", + "path": "/v2/tenants/{tenantId}/ledger-entries" + }, + { + "method": "GETBYID", + "name": "Retrieve", + "description": "Retrieve one ledger entry", + "path": "/v2/tenants/{tenantId}/ledger-entries/{entryId}" + } + ], + "hint": "Select an available operation and describe it explicitly to obtain its parameter and response contracts." + } +} diff --git a/tests/tasks/uipath-maestro-bpmn/connector/activity_authoring/fixtures/mocks/responses/resources-list.json b/tests/tasks/uipath-maestro-bpmn/connector/activity_authoring/fixtures/mocks/responses/resources-list.json new file mode 100644 index 0000000000..41cb0c8fc5 --- /dev/null +++ b/tests/tasks/uipath-maestro-bpmn/connector/activity_authoring/fixtures/mocks/responses/resources-list.json @@ -0,0 +1,24 @@ +{ + "Result": "Success", + "Code": "ResourceList", + "Data": [ + { + "Name": "ledger_entries", + "DisplayName": "Ledger entries", + "Path": "/v2/tenants/{tenantId}/ledger-entries", + "Type": "standard", + "SubType": "standard", + "Custom": "no", + "ElementKey": "uipath-synthetic-records" + }, + { + "Name": "audit_entries", + "DisplayName": "Audit entries", + "Path": "/v2/tenants/{tenantId}/audit-entries", + "Type": "standard", + "SubType": "standard", + "Custom": "no", + "ElementKey": "uipath-synthetic-records" + } + ] +} diff --git a/tests/tasks/uipath-maestro-bpmn/connector/activity_authoring/fixtures/mocks/responses/validate-unavailable.json b/tests/tasks/uipath-maestro-bpmn/connector/activity_authoring/fixtures/mocks/responses/validate-unavailable.json new file mode 100644 index 0000000000..df8623c36b --- /dev/null +++ b/tests/tasks/uipath-maestro-bpmn/connector/activity_authoring/fixtures/mocks/responses/validate-unavailable.json @@ -0,0 +1,6 @@ +{ + "Result": "Failure", + "Code": "FixtureValidationUnavailable", + "Message": "This read-only discovery fixture does not fake BPMN validation.", + "Instructions": "The deterministic task checker verifies the authored current V1 source contract; use a CLI containing current V1 validation for product preflight." +}