From 58e2f27c9acd2fdea4997daf2a78b36178c41774 Mon Sep 17 00:00:00 2001 From: Gong Zhang Date: Wed, 6 May 2026 18:28:21 -0700 Subject: [PATCH] Add Maestro BPMN validation fixtures --- CODEOWNERS | 1 + .../.maintenance/README.md | 12 +- .../.maintenance/check-all.sh | 1 + .../.maintenance/check-validation-fixtures.py | 445 ++++++++++++++++++ .../.maintenance/check-validation-fixtures.sh | 8 + .../fixtures/validation/README.md | 39 ++ .../gateway-boundary-error/bindings_v2.json | 16 + .../gateway-boundary-error/entry-points.json | 25 + .../gateway-boundary-error.bpmn | 89 ++++ .../gateway-boundary-error/operate.json | 9 + .../package-descriptor.json | 10 + .../gateway-boundary-error/project.uiproj | 7 + .../bindings_v2.json | 45 ++ .../entry-points.json | 25 + .../integration-service-enriched.bpmn | 65 +++ .../integration-service-enriched/operate.json | 9 + .../package-descriptor.json | 10 + .../project.uiproj | 7 + .../linear-process/bindings_v2.json | 4 + .../linear-process/entry-points.json | 25 + .../linear-process/linear-process.bpmn | 55 +++ .../validation/linear-process/operate.json | 9 + .../linear-process/package-descriptor.json | 10 + .../validation/linear-process/project.uiproj | 7 + .../bindings_v2.json | 4 + .../entry-points.json | 28 ++ .../subprocess-multi-instance/operate.json | 9 + .../package-descriptor.json | 10 + .../subprocess-multi-instance/project.uiproj | 7 + .../subprocess-multi-instance.bpmn | 95 ++++ tests/tasks/uipath-maestro-bpmn/README.md | 28 ++ .../smoke/validation_fixtures.yaml | 74 +++ 32 files changed, 1187 insertions(+), 1 deletion(-) create mode 100755 skills/uipath-maestro-bpmn/.maintenance/check-validation-fixtures.py create mode 100755 skills/uipath-maestro-bpmn/.maintenance/check-validation-fixtures.sh create mode 100644 skills/uipath-maestro-bpmn/fixtures/validation/README.md create mode 100644 skills/uipath-maestro-bpmn/fixtures/validation/gateway-boundary-error/bindings_v2.json create mode 100644 skills/uipath-maestro-bpmn/fixtures/validation/gateway-boundary-error/entry-points.json create mode 100644 skills/uipath-maestro-bpmn/fixtures/validation/gateway-boundary-error/gateway-boundary-error.bpmn create mode 100644 skills/uipath-maestro-bpmn/fixtures/validation/gateway-boundary-error/operate.json create mode 100644 skills/uipath-maestro-bpmn/fixtures/validation/gateway-boundary-error/package-descriptor.json create mode 100644 skills/uipath-maestro-bpmn/fixtures/validation/gateway-boundary-error/project.uiproj create mode 100644 skills/uipath-maestro-bpmn/fixtures/validation/integration-service-enriched/bindings_v2.json create mode 100644 skills/uipath-maestro-bpmn/fixtures/validation/integration-service-enriched/entry-points.json create mode 100644 skills/uipath-maestro-bpmn/fixtures/validation/integration-service-enriched/integration-service-enriched.bpmn create mode 100644 skills/uipath-maestro-bpmn/fixtures/validation/integration-service-enriched/operate.json create mode 100644 skills/uipath-maestro-bpmn/fixtures/validation/integration-service-enriched/package-descriptor.json create mode 100644 skills/uipath-maestro-bpmn/fixtures/validation/integration-service-enriched/project.uiproj create mode 100644 skills/uipath-maestro-bpmn/fixtures/validation/linear-process/bindings_v2.json create mode 100644 skills/uipath-maestro-bpmn/fixtures/validation/linear-process/entry-points.json create mode 100644 skills/uipath-maestro-bpmn/fixtures/validation/linear-process/linear-process.bpmn create mode 100644 skills/uipath-maestro-bpmn/fixtures/validation/linear-process/operate.json create mode 100644 skills/uipath-maestro-bpmn/fixtures/validation/linear-process/package-descriptor.json create mode 100644 skills/uipath-maestro-bpmn/fixtures/validation/linear-process/project.uiproj create mode 100644 skills/uipath-maestro-bpmn/fixtures/validation/subprocess-multi-instance/bindings_v2.json create mode 100644 skills/uipath-maestro-bpmn/fixtures/validation/subprocess-multi-instance/entry-points.json create mode 100644 skills/uipath-maestro-bpmn/fixtures/validation/subprocess-multi-instance/operate.json create mode 100644 skills/uipath-maestro-bpmn/fixtures/validation/subprocess-multi-instance/package-descriptor.json create mode 100644 skills/uipath-maestro-bpmn/fixtures/validation/subprocess-multi-instance/project.uiproj create mode 100644 skills/uipath-maestro-bpmn/fixtures/validation/subprocess-multi-instance/subprocess-multi-instance.bpmn create mode 100644 tests/tasks/uipath-maestro-bpmn/README.md create mode 100644 tests/tasks/uipath-maestro-bpmn/smoke/validation_fixtures.yaml diff --git a/CODEOWNERS b/CODEOWNERS index 2bc460910..5d842451d 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -42,6 +42,7 @@ # Maestro BPMN skill /skills/uipath-maestro-bpmn/ @bai-uipath @rockymadden @gozhang2 @tmatup @akshaylive @jiyangzh @nikhil-maryala @baishalighosh +/tests/tasks/uipath-maestro-bpmn/ @bai-uipath @rockymadden @gozhang2 @tmatup @akshaylive @jiyangzh @nikhil-maryala @baishalighosh # HITL skill /skills/uipath-human-in-the-loop/ @dushyant-uipath diff --git a/skills/uipath-maestro-bpmn/.maintenance/README.md b/skills/uipath-maestro-bpmn/.maintenance/README.md index dafdca58f..721009075 100644 --- a/skills/uipath-maestro-bpmn/.maintenance/README.md +++ b/skills/uipath-maestro-bpmn/.maintenance/README.md @@ -162,6 +162,16 @@ bash .maintenance/check-plugin-pairs.sh Returns `plugins_checked=N missing_files=M`. Exits non-zero if any plugin folder is missing a required file. Catches half-deleted plugins or new plugin folders that haven't been completed. +## Verifying BPMN validation fixtures + +Run the validation-fixtures checker to verify the public-safe synthetic BPMN corpus and generated package metadata: + +```bash +bash .maintenance/check-validation-fixtures.sh +``` + +Returns `validation_fixture_projects=N bpmn_files=N errors=0` on success. The checker validates standard BPMN parseability, UiPath moddle/extension elements, resource binding references, Integration Service enrichment fields, generated `bindings_v2.json`, `entry-points.json`, `operate.json`, and `package-descriptor.json`, plus public-safety guardrails for fixture content. + ## Verifying `uip` command references Run the uip-command checker to verify every `uip ...` invocation resolves to a real command in the installed CLI: @@ -202,7 +212,7 @@ For table rows, place the marker **inside a cell** so it doesn't break table str ## Running the full suite -Run all eight checkers in one invocation: +Run all nine checkers in one invocation: ```bash bash .maintenance/check-all.sh diff --git a/skills/uipath-maestro-bpmn/.maintenance/check-all.sh b/skills/uipath-maestro-bpmn/.maintenance/check-all.sh index cc1697f7d..8372ee7a7 100755 --- a/skills/uipath-maestro-bpmn/.maintenance/check-all.sh +++ b/skills/uipath-maestro-bpmn/.maintenance/check-all.sh @@ -17,6 +17,7 @@ CHECKERS=( "check-template.sh" "check-orphans.sh" "check-plugin-pairs.sh" + "check-validation-fixtures.sh" "check-uip-commands.sh" ) diff --git a/skills/uipath-maestro-bpmn/.maintenance/check-validation-fixtures.py b/skills/uipath-maestro-bpmn/.maintenance/check-validation-fixtures.py new file mode 100755 index 000000000..1894e99c2 --- /dev/null +++ b/skills/uipath-maestro-bpmn/.maintenance/check-validation-fixtures.py @@ -0,0 +1,445 @@ +#!/usr/bin/env python3 +"""Validate the synthetic Maestro BPMN fixture corpus. + +The checker intentionally stays dependency-free so contributors and CI can run +it without access to PO.FrontEnd or private exported BPMN. It validates the +public contract shape these fixtures are meant to preserve. +""" + +from __future__ import annotations + +import json +import re +import sys +import xml.etree.ElementTree as ET +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +FIXTURES = ROOT / "fixtures" / "validation" +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" +UIPATH_NS = "http://uipath.org/schema/bpmn" + +NODE_TYPES = { + "startEvent", + "endEvent", + "intermediateCatchEvent", + "intermediateThrowEvent", + "boundaryEvent", + "task", + "serviceTask", + "sendTask", + "receiveTask", + "userTask", + "manualTask", + "businessRuleTask", + "scriptTask", + "callActivity", + "subProcess", + "adHocSubProcess", + "exclusiveGateway", + "inclusiveGateway", + "parallelGateway", + "eventBasedGateway", + "complexGateway", +} + +ALLOWED_URLS = ( + "http://www.omg.org/spec/BPMN/20100524/MODEL", + "http://www.omg.org/spec/BPMN/20100524/DI", + "http://www.omg.org/spec/DD/20100524/DC", + "http://www.omg.org/spec/DD/20100524/DI", + "http://www.w3.org/2001/XMLSchema-instance", + "http://uipath.org/schema/bpmn", + "http://uipath.com/synthetic/maestro-bpmn/", +) + + +def local(tag: str) -> str: + return tag.rsplit("}", 1)[-1] if "}" in tag else tag + + +def ns(tag: str) -> str: + if tag.startswith("{"): + return tag[1:].split("}", 1)[0] + return "" + + +class Validator: + def __init__(self) -> None: + self.errors: list[str] = [] + self.projects = 0 + self.bpmn_files = 0 + + def error(self, path: Path, message: str) -> None: + self.errors.append(f"{path.relative_to(ROOT)}: {message}") + + def validate(self) -> int: + if not FIXTURES.is_dir(): + print(f"ERROR: fixtures directory not found: {FIXTURES}", file=sys.stderr) + return 2 + + for project in sorted(p for p in FIXTURES.iterdir() if p.is_dir()): + self.projects += 1 + self.validate_project(project) + + for err in self.errors: + print(f"ERROR: {err}") + print( + f"validation_fixture_projects={self.projects} " + f"bpmn_files={self.bpmn_files} errors={len(self.errors)}" + ) + return 1 if self.errors else 0 + + def validate_project(self, project: Path) -> None: + expected = [ + "project.uiproj", + "bindings_v2.json", + "entry-points.json", + "operate.json", + "package-descriptor.json", + ] + for name in expected: + if not (project / name).is_file(): + self.error(project, f"missing {name}") + + bpmn_files = sorted(project.glob("*.bpmn")) + if len(bpmn_files) != 1: + self.error(project, f"expected exactly one .bpmn file, found {len(bpmn_files)}") + return + + bpmn = bpmn_files[0] + self.bpmn_files += 1 + text = bpmn.read_text(encoding="utf-8") + self.validate_public_safety(bpmn, text) + + try: + tree = ET.parse(bpmn) + except ET.ParseError as exc: + self.error(bpmn, f"XML parse failed: {exc}") + return + + root = tree.getroot() + self.validate_bpmn_document(bpmn, root) + self.validate_package_files(project, bpmn.name, root) + + def validate_public_safety(self, path: Path, text: str) -> None: + patterns = { + "email address": r"[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}", + "local absolute path": r"(/Users/|/home/|C:\\\\Users\\\\)", + "guid-like identifier": r"\b[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}\b", + } + for label, pattern in patterns.items(): + if re.search(pattern, text): + self.error(path, f"contains {label}") + + for match in re.finditer(r"https?://[^\s\"'>]+", text): + url = match.group(0) + if not any(url.startswith(allowed) for allowed in ALLOWED_URLS): + self.error(path, f"contains non-allowlisted URL {url}") + + def validate_bpmn_document(self, path: Path, root: ET.Element) -> None: + if local(root.tag) != "definitions" or ns(root.tag) != BPMN_NS: + self.error(path, "root element is not bpmn:definitions") + + if ( + root.attrib.get("targetNamespace", "").startswith("http://uipath.com/synthetic/") + is False + ): + self.error(path, "targetNamespace must be synthetic") + + elements_by_id = { + elem.attrib["id"]: elem + for elem in root.iter() + if "id" in elem.attrib + and ns(elem.tag) in {BPMN_NS, BPMNDI_NS} + and local(elem.tag) != "BPMNShape" + and local(elem.tag) != "BPMNEdge" + } + + processes = root.findall(f"{{{BPMN_NS}}}process") + executable = [p for p in processes if p.attrib.get("isExecutable") == "true"] + if len(executable) != 1: + self.error(path, f"expected one executable process, found {len(executable)}") + return + process = executable[0] + + bindings = self.collect_root_bindings(process) + variables = self.collect_variables(process) + self.validate_diagram(path, root, elements_by_id) + self.validate_sequence_flows(path, root, elements_by_id) + self.validate_start_events(path, process) + self.validate_entry_points(path, process) + self.validate_gateway_conditions(path, root) + self.validate_error_events(path, root, elements_by_id) + self.validate_uipath_extensions(path, root, bindings, variables) + + def collect_root_bindings(self, process: ET.Element) -> dict[str, ET.Element]: + result: dict[str, ET.Element] = {} + for binding in process.findall( + f"./{{{BPMN_NS}}}extensionElements/{{{UIPATH_NS}}}bindings/{{{UIPATH_NS}}}binding" + ): + if binding.attrib.get("propertyAttribute") in {"folderKey", "folderPath"}: + continue + result[binding.attrib["id"]] = binding + return result + + def collect_variables(self, root: ET.Element) -> dict[str, set[str]]: + variables: dict[str, set[str]] = {"root": set(), "all": set()} + for vars_elem in root.findall(f".//{{{UIPATH_NS}}}variables"): + for child in list(vars_elem): + if child.attrib.get("name"): + variables["all"].add(child.attrib["name"]) + if vars_elem in root.findall( + f"./{{{BPMN_NS}}}extensionElements/{{{UIPATH_NS}}}variables" + ): + variables["root"].add(child.attrib["name"]) + return variables + + def validate_diagram( + self, path: Path, root: ET.Element, elements_by_id: dict[str, ET.Element] + ) -> None: + planes = root.findall(f".//{{{BPMNDI_NS}}}BPMNPlane") + if not planes: + self.error(path, "missing BPMNPlane") + return + for plane in planes: + ref = plane.attrib.get("bpmnElement") + if ref not in elements_by_id: + self.error(path, f"BPMNPlane references missing element {ref}") + + shape_refs = { + shape.attrib.get("bpmnElement") + for shape in root.findall(f".//{{{BPMNDI_NS}}}BPMNShape") + } + edge_refs = { + edge.attrib.get("bpmnElement") for edge in root.findall(f".//{{{BPMNDI_NS}}}BPMNEdge") + } + + for elem_id, elem in elements_by_id.items(): + kind = local(elem.tag) + if kind in NODE_TYPES and elem_id not in shape_refs: + self.error(path, f"missing BPMNShape for {elem_id}") + if kind == "sequenceFlow" and elem_id not in edge_refs: + self.error(path, f"missing BPMNEdge for {elem_id}") + + for edge in root.findall(f".//{{{BPMNDI_NS}}}BPMNEdge"): + if len(edge.findall(f"{{{DI_NS}}}waypoint")) < 2: + self.error(path, f"BPMNEdge {edge.attrib.get('id')} has fewer than two waypoints") + + def validate_sequence_flows( + self, path: Path, root: ET.Element, elements_by_id: dict[str, ET.Element] + ) -> None: + for flow in root.findall(f".//{{{BPMN_NS}}}sequenceFlow"): + source = elements_by_id.get(flow.attrib.get("sourceRef", "")) + target = elements_by_id.get(flow.attrib.get("targetRef", "")) + if source is None or target is None: + self.error( + path, f"sequenceFlow {flow.attrib.get('id')} has missing source or target" + ) + continue + if local(source.tag) == "endEvent": + self.error(path, f"sequenceFlow {flow.attrib.get('id')} starts at an end event") + if local(target.tag) == "startEvent": + self.error(path, f"sequenceFlow {flow.attrib.get('id')} targets a start event") + + def validate_start_events(self, path: Path, process: ET.Element) -> None: + scopes = [process] + process.findall(f".//{{{BPMN_NS}}}subProcess") + for scope in scopes: + starts = [c for c in list(scope) if local(c.tag) == "startEvent"] + blank = [ + s + for s in starts + if not any(local(c.tag).endswith("EventDefinition") for c in list(s)) + ] + if len(blank) > 1: + self.error(path, f"scope {scope.attrib.get('id')} has multiple blank start events") + if scope.attrib.get("triggeredByEvent") == "true" and len(starts) != 1: + self.error( + path, + f"event subprocess {scope.attrib.get('id')} must have exactly one start event", + ) + + def validate_entry_points(self, path: Path, process: ET.Element) -> None: + entry_ids: dict[str, str] = {} + root_starts = [c for c in list(process) if local(c.tag) == "startEvent"] + for start in root_starts: + ep = start.find(f"./{{{BPMN_NS}}}extensionElements/{{{UIPATH_NS}}}entryPointId") + if ep is None: + continue + value = ep.attrib.get("value") + if not value: + self.error(path, f"start event {start.attrib.get('id')} has empty entryPointId") + elif value in entry_ids: + self.error(path, f"duplicate entryPointId {value}") + else: + entry_ids[value] = start.attrib["id"] + + for var in process.findall( + f"./{{{BPMN_NS}}}extensionElements/{{{UIPATH_NS}}}variables/{{{UIPATH_NS}}}input" + ): + element_id = var.attrib.get("elementId") + if element_id and element_id not in {s.attrib["id"] for s in root_starts}: + self.error( + path, + f"entry input variable {var.attrib.get('name')} references non-root start {element_id}", + ) + + def validate_gateway_conditions(self, path: Path, root: ET.Element) -> None: + for gateway in root.findall(f".//{{{BPMN_NS}}}exclusiveGateway"): + outgoing = [ + child.text for child in gateway.findall(f"{{{BPMN_NS}}}outgoing") if child.text + ] + if len(outgoing) < 2: + continue + default = gateway.attrib.get("default") + if default and default not in outgoing: + self.error( + path, f"exclusiveGateway {gateway.attrib.get('id')} default is not outgoing" + ) + for flow_id in outgoing: + if flow_id == default: + continue + flow = root.find(f".//{{{BPMN_NS}}}sequenceFlow[@id='{flow_id}']") + if flow is not None and flow.find(f"{{{BPMN_NS}}}conditionExpression") is None: + self.error(path, f"gateway flow {flow_id} is missing conditionExpression") + + def validate_error_events( + self, path: Path, root: ET.Element, elements_by_id: dict[str, ET.Element] + ) -> None: + for event_def in root.findall(f".//{{{BPMN_NS}}}errorEventDefinition"): + ref = event_def.attrib.get("errorRef") + if ref and ref not in elements_by_id: + self.error(path, f"errorEventDefinition references missing error {ref}") + + for boundary in root.findall(f".//{{{BPMN_NS}}}boundaryEvent"): + attached = boundary.attrib.get("attachedToRef") + if attached not in elements_by_id: + self.error( + path, + f"boundaryEvent {boundary.attrib.get('id')} references missing activity {attached}", + ) + + def validate_uipath_extensions( + self, + path: Path, + root: ET.Element, + bindings: dict[str, ET.Element], + variables: dict[str, set[str]], + ) -> None: + for elem in root.iter(): + if ns(elem.tag) != UIPATH_NS: + continue + if local(elem.tag) in {"activity", "event"}: + self.validate_activity_or_event(path, elem, bindings) + if local(elem.tag) == "output": + target = elem.attrib.get("var") or elem.attrib.get("target") + if target and target not in variables["all"]: + self.error(path, f"uipath:output targets undeclared variable {target}") + value = elem.attrib.get("value", "") + for binding_ref in re.findall(r"=bindings\.([A-Za-z0-9_]+)", value): + if binding_ref not in bindings: + self.error( + path, f"binding expression references undeclared binding {binding_ref}" + ) + if "=" in value and re.search(r"(?])=(?!=)", value[1:]): + self.error(path, f"expression may contain assignment: {value}") + + def validate_activity_or_event( + self, path: Path, elem: ET.Element, bindings: dict[str, ET.Element] + ) -> None: + type_elem = elem.find(f"{{{UIPATH_NS}}}type") + service_type = type_elem.attrib.get("value") if type_elem is not None else None + if not service_type: + self.error(path, "uipath activity/event missing type") + return + + context = elem.find(f"{{{UIPATH_NS}}}context") + context_inputs = { + child.attrib.get("name"): child.attrib.get("value", "") + for child in list(context) + if context is not None and local(child.tag) == "input" + } + + if service_type.startswith("Intsvc."): + connection = context_inputs.get("connection", "") + match = re.fullmatch(r"=bindings\.([A-Za-z0-9_]+)", connection) + if not match or match.group(1) not in bindings: + self.error(path, f"{service_type} missing generated connection binding") + if "connectorKey" not in context_inputs: + self.error(path, f"{service_type} missing connectorKey") + if service_type in {"Intsvc.ActivityExecution", "Intsvc.AsyncExecution"}: + for required in ("activity", "operation"): + if required not in context_inputs: + self.error(path, f"{service_type} missing {required}") + if service_type == "Intsvc.EventTrigger": + for required in ("trigger", "eventName"): + if required not in context_inputs: + self.error(path, f"{service_type} missing {required}") + + def validate_package_files(self, project: Path, bpmn_name: str, root: ET.Element) -> None: + data: dict[str, object] = {} + for name in ( + "project.uiproj", + "bindings_v2.json", + "entry-points.json", + "operate.json", + "package-descriptor.json", + ): + path = project / name + if not path.is_file(): + return + try: + data[name] = json.loads(path.read_text(encoding="utf-8")) + except json.JSONDecodeError as exc: + self.error(path, f"JSON parse failed: {exc}") + return + + if data["project.uiproj"].get("main") != bpmn_name: # type: ignore[union-attr] + self.error(project / "project.uiproj", "main does not match BPMN file") + if data["operate.json"].get("main") != bpmn_name: # type: ignore[union-attr] + self.error(project / "operate.json", "main does not match BPMN file") + if data["operate.json"].get("contentType") != "ProcessOrchestration": # type: ignore[union-attr] + self.error(project / "operate.json", "contentType must be ProcessOrchestration") + + descriptor_content = set(data["package-descriptor.json"].get("content", [])) # type: ignore[union-attr] + for required in ( + f"content/{bpmn_name}", + "content/bindings_v2.json", + "content/entry-points.json", + "content/operate.json", + ): + if required not in descriptor_content: + self.error(project / "package-descriptor.json", f"missing content entry {required}") + + process = root.find(f"{{{BPMN_NS}}}process") + if process is None: + return + root_bindings = self.collect_root_bindings(process) + package_bindings = { + resource.get("id") + for resource in data["bindings_v2.json"].get("resources", []) # type: ignore[union-attr] + } + for binding_id in root_bindings: + if binding_id not in package_bindings: + self.error( + project / "bindings_v2.json", f"missing resource for binding {binding_id}" + ) + + entry_points = data["entry-points.json"].get("entryPoints", []) # type: ignore[union-attr] + package_eps = {ep.get("id"): ep for ep in entry_points} + for start in [c for c in list(process) if local(c.tag) == "startEvent"]: + ep = start.find(f"./{{{BPMN_NS}}}extensionElements/{{{UIPATH_NS}}}entryPointId") + if ep is None: + continue + ep_id = ep.attrib.get("value") + file_path = f"/content/{bpmn_name}#{start.attrib.get('id')}" + if ep_id not in package_eps: + self.error(project / "entry-points.json", f"missing entry point {ep_id}") + elif package_eps[ep_id].get("filePath") != file_path: + self.error(project / "entry-points.json", f"entry point {ep_id} has wrong filePath") + + +if __name__ == "__main__": + sys.exit(Validator().validate()) diff --git a/skills/uipath-maestro-bpmn/.maintenance/check-validation-fixtures.sh b/skills/uipath-maestro-bpmn/.maintenance/check-validation-fixtures.sh new file mode 100755 index 000000000..989c7f03e --- /dev/null +++ b/skills/uipath-maestro-bpmn/.maintenance/check-validation-fixtures.sh @@ -0,0 +1,8 @@ +#!/bin/bash +# Validate the synthetic BPMN/package fixture corpus for this skill. +# Usage: bash .maintenance/check-validation-fixtures.sh + +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +cd "$ROOT" || exit 1 + +python3 .maintenance/check-validation-fixtures.py diff --git a/skills/uipath-maestro-bpmn/fixtures/validation/README.md b/skills/uipath-maestro-bpmn/fixtures/validation/README.md new file mode 100644 index 000000000..074874bd5 --- /dev/null +++ b/skills/uipath-maestro-bpmn/fixtures/validation/README.md @@ -0,0 +1,39 @@ +# Maestro BPMN Validation Fixtures + +Public-safe fixture corpus for the `uipath-maestro-bpmn` skill maintenance checks. These files are synthetic, but they intentionally cover the same structural families summarized from PO.Frontend mocks, private exported BPMN reviews, and generated BPMN package outputs. + +## Fixture Set + +| Fixture | Coverage | +| --- | --- | +| `linear-process/` | Minimal executable process, root variables, entry point ID, BPMN DI, and generated package metadata. | +| `gateway-boundary-error/` | Exclusive gateway conditions/defaults, service task retry/error mapping, boundary error event, terminate end, tags, and package manifest checks. | +| `integration-service-enriched/` | Integration Service trigger and activity extensions, root connection/property bindings, generated `bindings_v2.json` resources, entry point schema, and package metadata. | +| `subprocess-multi-instance/` | Subprocess scoped variables, multi-instance loop metadata, script task metadata, mappings, message event, and diagram/waypoint coverage. | + +## Maintenance Commands + +Contributor check from the repository root: + +```bash +bash skills/uipath-maestro-bpmn/.maintenance/check-validation-fixtures.sh +``` + +Full skill maintenance suite from the repository root: + +```bash +bash skills/uipath-maestro-bpmn/.maintenance/check-all.sh +``` + +CI should run the same two commands before skill evals. The smoke eval task for this corpus is: + +```bash +cd tests +make tags TAGS="uipath-maestro-bpmn smoke" EXPERIMENT=experiments/default.yaml +``` + +## Public-Safety Rules + +- Do not copy raw exported BPMN, screenshots, tenant metadata, connection IDs, folder keys, URLs, user names, private process names, or temporary mission notes into these fixtures. +- Keep IDs readable and synthetic, for example `Task_CreateTicket` and `Binding_ServiceDeskConnection`. +- Keep package metadata deterministic and local to the fixture folder. diff --git a/skills/uipath-maestro-bpmn/fixtures/validation/gateway-boundary-error/bindings_v2.json b/skills/uipath-maestro-bpmn/fixtures/validation/gateway-boundary-error/bindings_v2.json new file mode 100644 index 000000000..afcf23598 --- /dev/null +++ b/skills/uipath-maestro-bpmn/fixtures/validation/gateway-boundary-error/bindings_v2.json @@ -0,0 +1,16 @@ +{ + "version": "2.0", + "resources": [ + { + "id": "Binding_ReviewProcess", + "name": "Review Process", + "kind": "process", + "resourceKey": "review-process", + "metadata": { + "BindingsVersion": "v1", + "DisplayLabel": "Review Process", + "SolutionsSupport": "Required" + } + } + ] +} diff --git a/skills/uipath-maestro-bpmn/fixtures/validation/gateway-boundary-error/entry-points.json b/skills/uipath-maestro-bpmn/fixtures/validation/gateway-boundary-error/entry-points.json new file mode 100644 index 000000000..0bd594083 --- /dev/null +++ b/skills/uipath-maestro-bpmn/fixtures/validation/gateway-boundary-error/entry-points.json @@ -0,0 +1,25 @@ +{ + "entryPoints": [ + { + "id": "Entry_GatewayBoundary", + "name": "Start", + "filePath": "/content/gateway-boundary-error.bpmn#Start_Manual", + "inputSchema": { + "type": "object", + "properties": { + "requestType": { + "type": "string" + } + } + }, + "outputSchema": { + "type": "object", + "properties": { + "result": { + "type": "string" + } + } + } + } + ] +} diff --git a/skills/uipath-maestro-bpmn/fixtures/validation/gateway-boundary-error/gateway-boundary-error.bpmn b/skills/uipath-maestro-bpmn/fixtures/validation/gateway-boundary-error/gateway-boundary-error.bpmn new file mode 100644 index 000000000..65f3ee91a --- /dev/null +++ b/skills/uipath-maestro-bpmn/fixtures/validation/gateway-boundary-error/gateway-boundary-error.bpmn @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + Flow_Start_To_Gateway + + + Flow_Start_To_Gateway + Flow_Gateway_To_Review + Flow_Gateway_To_Archive + + + + + + + + + + + + + + + + + gateway-boundary-error + + + Flow_Gateway_To_Review + Flow_Review_To_End + + + Flow_Gateway_To_Archive + Flow_Archive_To_End + + + Flow_Boundary_To_Terminate + + + + Flow_Review_To_End + Flow_Archive_To_End + + + Flow_Boundary_To_Terminate + + + + + =requestType == "review" + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/skills/uipath-maestro-bpmn/fixtures/validation/gateway-boundary-error/operate.json b/skills/uipath-maestro-bpmn/fixtures/validation/gateway-boundary-error/operate.json new file mode 100644 index 000000000..dac7fe7ad --- /dev/null +++ b/skills/uipath-maestro-bpmn/fixtures/validation/gateway-boundary-error/operate.json @@ -0,0 +1,9 @@ +{ + "projectId": "Fixture.GatewayBoundaryError", + "main": "gateway-boundary-error.bpmn", + "contentType": "ProcessOrchestration", + "targetFramework": "Portable", + "runtimeOptions": { + "requiresUserInteraction": false + } +} diff --git a/skills/uipath-maestro-bpmn/fixtures/validation/gateway-boundary-error/package-descriptor.json b/skills/uipath-maestro-bpmn/fixtures/validation/gateway-boundary-error/package-descriptor.json new file mode 100644 index 000000000..a7c87ce04 --- /dev/null +++ b/skills/uipath-maestro-bpmn/fixtures/validation/gateway-boundary-error/package-descriptor.json @@ -0,0 +1,10 @@ +{ + "id": "Fixture.GatewayBoundaryError", + "version": "1.0.0", + "content": [ + "content/gateway-boundary-error.bpmn", + "content/bindings_v2.json", + "content/entry-points.json", + "content/operate.json" + ] +} diff --git a/skills/uipath-maestro-bpmn/fixtures/validation/gateway-boundary-error/project.uiproj b/skills/uipath-maestro-bpmn/fixtures/validation/gateway-boundary-error/project.uiproj new file mode 100644 index 000000000..033c472a6 --- /dev/null +++ b/skills/uipath-maestro-bpmn/fixtures/validation/gateway-boundary-error/project.uiproj @@ -0,0 +1,7 @@ +{ + "projectVersion": "1.0.0", + "projectType": "ProcessOrchestration", + "name": "GatewayBoundaryError", + "description": "Synthetic gateway and boundary-error validation fixture.", + "main": "gateway-boundary-error.bpmn" +} diff --git a/skills/uipath-maestro-bpmn/fixtures/validation/integration-service-enriched/bindings_v2.json b/skills/uipath-maestro-bpmn/fixtures/validation/integration-service-enriched/bindings_v2.json new file mode 100644 index 000000000..7c5a893ab --- /dev/null +++ b/skills/uipath-maestro-bpmn/fixtures/validation/integration-service-enriched/bindings_v2.json @@ -0,0 +1,45 @@ +{ + "version": "2.0", + "resources": [ + { + "id": "Binding_ServiceDeskConnection", + "name": "Service Desk Connection", + "kind": "connection", + "resourceKey": "service-desk-connection", + "metadata": { + "BindingsVersion": "v1", + "ActivityName": "Create Ticket", + "DisplayLabel": "Service Desk Connection", + "SolutionsSupport": "Required", + "SubType": "connection", + "Connector": "service-desk" + } + }, + { + "id": "Binding_RecordCreatedTrigger", + "name": "Record Created Trigger", + "kind": "eventTrigger", + "resourceKey": "record-created-trigger", + "metadata": { + "BindingsVersion": "v1", + "ActivityName": "Record Created", + "DisplayLabel": "Record Created Trigger", + "SolutionsSupport": "Required", + "SubType": "eventTrigger", + "Connector": "service-desk" + } + }, + { + "id": "Binding_RecordObjectProperty", + "name": "Record Object Property", + "kind": "property", + "resourceKey": "record-object-property", + "metadata": { + "BindingsVersion": "v1", + "DisplayLabel": "Record Object Property", + "ParentResourceKey": "record-created-trigger", + "SubType": "property" + } + } + ] +} diff --git a/skills/uipath-maestro-bpmn/fixtures/validation/integration-service-enriched/entry-points.json b/skills/uipath-maestro-bpmn/fixtures/validation/integration-service-enriched/entry-points.json new file mode 100644 index 000000000..e3690ee4c --- /dev/null +++ b/skills/uipath-maestro-bpmn/fixtures/validation/integration-service-enriched/entry-points.json @@ -0,0 +1,25 @@ +{ + "entryPoints": [ + { + "id": "Entry_RecordCreated", + "name": "Record Created", + "filePath": "/content/integration-service-enriched.bpmn#Start_RecordCreated", + "inputSchema": { + "type": "object", + "properties": { + "recordId": { + "type": "string" + } + } + }, + "outputSchema": { + "type": "object", + "properties": { + "ticketId": { + "type": "string" + } + } + } + } + ] +} diff --git a/skills/uipath-maestro-bpmn/fixtures/validation/integration-service-enriched/integration-service-enriched.bpmn b/skills/uipath-maestro-bpmn/fixtures/validation/integration-service-enriched/integration-service-enriched.bpmn new file mode 100644 index 000000000..2f5df41e3 --- /dev/null +++ b/skills/uipath-maestro-bpmn/fixtures/validation/integration-service-enriched/integration-service-enriched.bpmn @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Flow_Start_To_CreateTicket + + + + + + + + + + + + + + + + + Flow_Start_To_CreateTicket + Flow_CreateTicket_To_End + + + Flow_CreateTicket_To_End + + + + + + + + + + + + + + diff --git a/skills/uipath-maestro-bpmn/fixtures/validation/integration-service-enriched/operate.json b/skills/uipath-maestro-bpmn/fixtures/validation/integration-service-enriched/operate.json new file mode 100644 index 000000000..3f961c08f --- /dev/null +++ b/skills/uipath-maestro-bpmn/fixtures/validation/integration-service-enriched/operate.json @@ -0,0 +1,9 @@ +{ + "projectId": "Fixture.IntegrationServiceEnriched", + "main": "integration-service-enriched.bpmn", + "contentType": "ProcessOrchestration", + "targetFramework": "Portable", + "runtimeOptions": { + "requiresUserInteraction": false + } +} diff --git a/skills/uipath-maestro-bpmn/fixtures/validation/integration-service-enriched/package-descriptor.json b/skills/uipath-maestro-bpmn/fixtures/validation/integration-service-enriched/package-descriptor.json new file mode 100644 index 000000000..2c3f7ab82 --- /dev/null +++ b/skills/uipath-maestro-bpmn/fixtures/validation/integration-service-enriched/package-descriptor.json @@ -0,0 +1,10 @@ +{ + "id": "Fixture.IntegrationServiceEnriched", + "version": "1.0.0", + "content": [ + "content/integration-service-enriched.bpmn", + "content/bindings_v2.json", + "content/entry-points.json", + "content/operate.json" + ] +} diff --git a/skills/uipath-maestro-bpmn/fixtures/validation/integration-service-enriched/project.uiproj b/skills/uipath-maestro-bpmn/fixtures/validation/integration-service-enriched/project.uiproj new file mode 100644 index 000000000..3b08f3497 --- /dev/null +++ b/skills/uipath-maestro-bpmn/fixtures/validation/integration-service-enriched/project.uiproj @@ -0,0 +1,7 @@ +{ + "projectVersion": "1.0.0", + "projectType": "ProcessOrchestration", + "name": "IntegrationServiceEnriched", + "description": "Synthetic Integration Service enrichment validation fixture.", + "main": "integration-service-enriched.bpmn" +} diff --git a/skills/uipath-maestro-bpmn/fixtures/validation/linear-process/bindings_v2.json b/skills/uipath-maestro-bpmn/fixtures/validation/linear-process/bindings_v2.json new file mode 100644 index 000000000..5e9beeb01 --- /dev/null +++ b/skills/uipath-maestro-bpmn/fixtures/validation/linear-process/bindings_v2.json @@ -0,0 +1,4 @@ +{ + "version": "2.0", + "resources": [] +} diff --git a/skills/uipath-maestro-bpmn/fixtures/validation/linear-process/entry-points.json b/skills/uipath-maestro-bpmn/fixtures/validation/linear-process/entry-points.json new file mode 100644 index 000000000..3604f4c34 --- /dev/null +++ b/skills/uipath-maestro-bpmn/fixtures/validation/linear-process/entry-points.json @@ -0,0 +1,25 @@ +{ + "entryPoints": [ + { + "id": "Entry_LinearManual", + "name": "Start", + "filePath": "/content/linear-process.bpmn#Start_Manual", + "inputSchema": { + "type": "object", + "properties": { + "request": { + "type": "string" + } + } + }, + "outputSchema": { + "type": "object", + "properties": { + "status": { + "type": "string" + } + } + } + } + ] +} diff --git a/skills/uipath-maestro-bpmn/fixtures/validation/linear-process/linear-process.bpmn b/skills/uipath-maestro-bpmn/fixtures/validation/linear-process/linear-process.bpmn new file mode 100644 index 000000000..e2454fb0f --- /dev/null +++ b/skills/uipath-maestro-bpmn/fixtures/validation/linear-process/linear-process.bpmn @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + Flow_Start_To_Task + + + + + + + + + Flow_Start_To_Task + Flow_Task_To_End + + + Flow_Task_To_End + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/skills/uipath-maestro-bpmn/fixtures/validation/linear-process/operate.json b/skills/uipath-maestro-bpmn/fixtures/validation/linear-process/operate.json new file mode 100644 index 000000000..c8bd58791 --- /dev/null +++ b/skills/uipath-maestro-bpmn/fixtures/validation/linear-process/operate.json @@ -0,0 +1,9 @@ +{ + "projectId": "Fixture.LinearProcess", + "main": "linear-process.bpmn", + "contentType": "ProcessOrchestration", + "targetFramework": "Portable", + "runtimeOptions": { + "requiresUserInteraction": false + } +} diff --git a/skills/uipath-maestro-bpmn/fixtures/validation/linear-process/package-descriptor.json b/skills/uipath-maestro-bpmn/fixtures/validation/linear-process/package-descriptor.json new file mode 100644 index 000000000..ef13aa775 --- /dev/null +++ b/skills/uipath-maestro-bpmn/fixtures/validation/linear-process/package-descriptor.json @@ -0,0 +1,10 @@ +{ + "id": "Fixture.LinearProcess", + "version": "1.0.0", + "content": [ + "content/linear-process.bpmn", + "content/bindings_v2.json", + "content/entry-points.json", + "content/operate.json" + ] +} diff --git a/skills/uipath-maestro-bpmn/fixtures/validation/linear-process/project.uiproj b/skills/uipath-maestro-bpmn/fixtures/validation/linear-process/project.uiproj new file mode 100644 index 000000000..1d097c59a --- /dev/null +++ b/skills/uipath-maestro-bpmn/fixtures/validation/linear-process/project.uiproj @@ -0,0 +1,7 @@ +{ + "projectVersion": "1.0.0", + "projectType": "ProcessOrchestration", + "name": "LinearProcess", + "description": "Synthetic Maestro BPMN validation fixture.", + "main": "linear-process.bpmn" +} diff --git a/skills/uipath-maestro-bpmn/fixtures/validation/subprocess-multi-instance/bindings_v2.json b/skills/uipath-maestro-bpmn/fixtures/validation/subprocess-multi-instance/bindings_v2.json new file mode 100644 index 000000000..5e9beeb01 --- /dev/null +++ b/skills/uipath-maestro-bpmn/fixtures/validation/subprocess-multi-instance/bindings_v2.json @@ -0,0 +1,4 @@ +{ + "version": "2.0", + "resources": [] +} diff --git a/skills/uipath-maestro-bpmn/fixtures/validation/subprocess-multi-instance/entry-points.json b/skills/uipath-maestro-bpmn/fixtures/validation/subprocess-multi-instance/entry-points.json new file mode 100644 index 000000000..9d34ac1bf --- /dev/null +++ b/skills/uipath-maestro-bpmn/fixtures/validation/subprocess-multi-instance/entry-points.json @@ -0,0 +1,28 @@ +{ + "entryPoints": [ + { + "id": "Entry_SubprocessMultiInstance", + "name": "Start", + "filePath": "/content/subprocess-multi-instance.bpmn#Start_Manual", + "inputSchema": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "type": "object" + } + } + } + }, + "outputSchema": { + "type": "object", + "properties": { + "summary": { + "type": "string" + } + } + } + } + ] +} diff --git a/skills/uipath-maestro-bpmn/fixtures/validation/subprocess-multi-instance/operate.json b/skills/uipath-maestro-bpmn/fixtures/validation/subprocess-multi-instance/operate.json new file mode 100644 index 000000000..2a378563c --- /dev/null +++ b/skills/uipath-maestro-bpmn/fixtures/validation/subprocess-multi-instance/operate.json @@ -0,0 +1,9 @@ +{ + "projectId": "Fixture.SubprocessMultiInstance", + "main": "subprocess-multi-instance.bpmn", + "contentType": "ProcessOrchestration", + "targetFramework": "Portable", + "runtimeOptions": { + "requiresUserInteraction": false + } +} diff --git a/skills/uipath-maestro-bpmn/fixtures/validation/subprocess-multi-instance/package-descriptor.json b/skills/uipath-maestro-bpmn/fixtures/validation/subprocess-multi-instance/package-descriptor.json new file mode 100644 index 000000000..184932419 --- /dev/null +++ b/skills/uipath-maestro-bpmn/fixtures/validation/subprocess-multi-instance/package-descriptor.json @@ -0,0 +1,10 @@ +{ + "id": "Fixture.SubprocessMultiInstance", + "version": "1.0.0", + "content": [ + "content/subprocess-multi-instance.bpmn", + "content/bindings_v2.json", + "content/entry-points.json", + "content/operate.json" + ] +} diff --git a/skills/uipath-maestro-bpmn/fixtures/validation/subprocess-multi-instance/project.uiproj b/skills/uipath-maestro-bpmn/fixtures/validation/subprocess-multi-instance/project.uiproj new file mode 100644 index 000000000..ceeb1de00 --- /dev/null +++ b/skills/uipath-maestro-bpmn/fixtures/validation/subprocess-multi-instance/project.uiproj @@ -0,0 +1,7 @@ +{ + "projectVersion": "1.0.0", + "projectType": "ProcessOrchestration", + "name": "SubprocessMultiInstance", + "description": "Synthetic subprocess and multi-instance validation fixture.", + "main": "subprocess-multi-instance.bpmn" +} diff --git a/skills/uipath-maestro-bpmn/fixtures/validation/subprocess-multi-instance/subprocess-multi-instance.bpmn b/skills/uipath-maestro-bpmn/fixtures/validation/subprocess-multi-instance/subprocess-multi-instance.bpmn new file mode 100644 index 000000000..dd4d91445 --- /dev/null +++ b/skills/uipath-maestro-bpmn/fixtures/validation/subprocess-multi-instance/subprocess-multi-instance.bpmn @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + Flow_Start_To_Subprocess + + + + + + + + Flow_Start_To_Subprocess + Flow_Subprocess_To_Wait + + + + + + + Flow_Sub_Start_To_Script + + + + + + + + + + + + + + Flow_Sub_Start_To_Script + Flow_Script_To_Sub_End + + + + Flow_Script_To_Sub_End + + + + + + + + + + + + + + + Flow_Subprocess_To_Wait + Flow_Wait_To_End + + + + Flow_Wait_To_End + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/tasks/uipath-maestro-bpmn/README.md b/tests/tasks/uipath-maestro-bpmn/README.md new file mode 100644 index 000000000..8783e517c --- /dev/null +++ b/tests/tasks/uipath-maestro-bpmn/README.md @@ -0,0 +1,28 @@ +# Maestro BPMN Skill Eval Tasks + +These tasks exercise the `uipath-maestro-bpmn` skill and its public-safe validation fixture corpus. + +## Contributor Commands + +From the repository root: + +```bash +bash skills/uipath-maestro-bpmn/.maintenance/check-validation-fixtures.sh +bash skills/uipath-maestro-bpmn/.maintenance/check-all.sh +``` + +Run the Maestro BPMN smoke eval: + +```bash +cd tests +make tags TAGS="uipath-maestro-bpmn smoke" EXPERIMENT=experiments/default.yaml +``` + +Run all tests for this skill: + +```bash +cd tests +make test-uipath-maestro-bpmn +``` + +CI should run the two maintenance commands before evals so malformed fixture or documentation drift fails before an agent run starts. diff --git a/tests/tasks/uipath-maestro-bpmn/smoke/validation_fixtures.yaml b/tests/tasks/uipath-maestro-bpmn/smoke/validation_fixtures.yaml new file mode 100644 index 000000000..de408f55f --- /dev/null +++ b/tests/tasks/uipath-maestro-bpmn/smoke/validation_fixtures.yaml @@ -0,0 +1,74 @@ +task_id: skill-bpmn-validation-fixtures +description: > + Skill-guided smoke test: agent uses the uipath-maestro-bpmn skill to inspect + the synthetic validation fixture corpus and run the local fixture checker. + This verifies the skill's validation guidance, generated package metadata + expectations, Integration Service enrichment boundary, and public-safety + requirements without cloud-side effects. +tags: [uipath-maestro-bpmn, smoke, mode:build, lifecycle:validate, connector, feature:connections] + +agent: + type: claude-code + max_turns: 20 + +sandbox: + driver: tempdir + python: {} + template_sources: + - type: template_dir + path: ../../../../skills/uipath-maestro-bpmn + target: skills/uipath-maestro-bpmn + +initial_prompt: | + Load and follow the uipath-maestro-bpmn skill. + + Inspect the validation fixtures under: + skills/uipath-maestro-bpmn/fixtures/validation + + Run the local fixture validation command: + bash skills/uipath-maestro-bpmn/.maintenance/check-validation-fixtures.sh + + Report whether the fixtures pass. Do not upload, publish, deploy, debug, + run a process, ask for approval, or call cloud-side UiPath commands. + +success_criteria: + - type: skill_triggered + description: "Agent invoked the uipath-maestro-bpmn skill" + skill_name: "uipath:uipath-maestro-bpmn" + expected: "yes" + weight: 1.0 + + - type: command_executed + description: "Agent ran the Maestro BPMN validation fixture checker" + tool_name: "Bash" + command_pattern: 'bash\s+skills/uipath-maestro-bpmn/\.maintenance/check-validation-fixtures\.sh' + min_count: 1 + weight: 2.0 + pass_threshold: 1.0 + + - type: command_not_executed + description: "Agent did not run cloud-side UiPath lifecycle commands" + tool_name: "Bash" + command_pattern: 'uip\s+maestro\s+bpmn\s+(upload|publish|deploy|debug|run)|uip\s+maestro\s+(upload|publish|deploy|debug|run)' + weight: 1.0 + pass_threshold: 1.0 + + - type: file_exists + description: "Linear BPMN fixture is available" + path: "skills/uipath-maestro-bpmn/fixtures/validation/linear-process/linear-process.bpmn" + weight: 0.5 + pass_threshold: 1.0 + + - type: file_exists + description: "Integration Service package bindings fixture is available" + path: "skills/uipath-maestro-bpmn/fixtures/validation/integration-service-enriched/bindings_v2.json" + weight: 0.5 + pass_threshold: 1.0 + + - type: run_command + description: "Fixture checker passes in the scored workspace" + command: "bash skills/uipath-maestro-bpmn/.maintenance/check-validation-fixtures.sh" + timeout: 20 + expected_exit_code: 0 + weight: 2.0 + pass_threshold: 1.0