diff --git a/.editorconfig b/.editorconfig
index 9293c5b4f5..781452eac0 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -36,7 +36,7 @@ indent_size = 2
[*.{yml,yaml}]
indent_style = space
indent_size = 2
-
+ij_yaml_spaces_within_brackets = false
########################################
# Markdown
diff --git a/pom.xml b/pom.xml
index 25b6a3c5a2..e2f2af287b 100644
--- a/pom.xml
+++ b/pom.xml
@@ -72,24 +72,25 @@
- step-commons
step-agent
+ step-ap-ide
+ step-automation-packages
+ step-cli
+ step-commons
step-constants
- step-core-model
+ step-controller
+ step-controller-plugins
step-core
- step-automation-packages
+ step-core-model
step-functions
step-functions-plugins
- step-plans
- step-controller
- step-controller-plugins
- step-maven-plugin
- step-repositories
step-ide
- step-libs-maven-client
step-json-schema
- step-cli
+ step-libs-maven-client
step-livereporting
+ step-maven-plugin
+ step-plans
+ step-repositories
diff --git a/step-ap-ide/.gitignore b/step-ap-ide/.gitignore
new file mode 100644
index 0000000000..69fc4a95eb
--- /dev/null
+++ b/step-ap-ide/.gitignore
@@ -0,0 +1 @@
+filemanager/
diff --git a/step-ap-ide/README.md b/step-ap-ide/README.md
new file mode 100644
index 0000000000..e01867da80
--- /dev/null
+++ b/step-ap-ide/README.md
@@ -0,0 +1,12 @@
+This is still very early Work in progress, so some of it is quick and dirty to at least get an initial prototype to work.
+
+Basically, it's a modified and stripped down version of the step controller, meant to be executed lika a local application.
+Technically, it still consists of a backend and frontend. For development, they will be separated (as they are for the main Step product),
+so you'll also need to launch both of them separately.
+
+To start the frontend, check out branch SED-4429-step-ap-ide of the step-frontend-workspace project, then run
+npm run serve:os:cli:local . For a little more information, see the SED-4557 ticket.
+
+To start the backend and use the application, start the StepUp main class. This technically launches the actual controller,
+and a client app that is basically a browser. Once started, you can also use a regular browser pointed to http://localhost:4201 .
+
diff --git a/step-ap-ide/TESTS b/step-ap-ide/TESTS
new file mode 100644
index 0000000000..5473711052
--- /dev/null
+++ b/step-ap-ide/TESTS
@@ -0,0 +1,14 @@
+Plan:
+
+Mode (PER_OBJECT / FRAGMENT), foreach:
+create new with different name (3x); foreach
+ rename to longer name
+ rename back to shorter name
+ delete
+
+rename to existing (ie conflicting) name
+
+
+BUGS:
+1. Adding Plan to fragment does not make the fragment visible in AP (i.e. does not add to fragments section)
+2.
diff --git a/step-ap-ide/pom.xml b/step-ap-ide/pom.xml
new file mode 100644
index 0000000000..4e32a62cc3
--- /dev/null
+++ b/step-ap-ide/pom.xml
@@ -0,0 +1,130 @@
+
+ 4.0.0
+
+ step-ap-ide
+ ${project.groupId}:${project.artifactId}
+ jar
+
+
+ ch.exense.step
+ step
+ 0.0.0-SNAPSHOT
+
+
+
+ 21.0.2
+
+
+
+
+ ch.exense.step
+ step-framework-server
+
+
+ ch.exense.step
+ step-core
+ ${project.version}
+
+
+ ch.exense.step
+ step-controller-backend
+ ${project.version}
+
+
+ ch.exense.step
+ step-automation-packages-collections
+ ${project.version}
+
+
+
+ org.openjfx
+ javafx-controls
+ ${javafx.version}
+ win
+
+
+ org.openjfx
+ javafx-web
+ ${javafx.version}
+ win
+
+
+
+ org.openjfx
+ javafx-controls
+ ${javafx.version}
+ linux
+
+
+ org.openjfx
+ javafx-web
+ ${javafx.version}
+ linux
+
+
+
+ org.openjfx
+ javafx-controls
+ ${javafx.version}
+ mac
+
+
+ org.openjfx
+ javafx-web
+ ${javafx.version}
+ mac
+
+
+
+ org.openjfx
+ javafx-controls
+ ${javafx.version}
+ mac-aarch64
+
+
+ org.openjfx
+ javafx-web
+ ${javafx.version}
+ mac-aarch64
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-shade-plugin
+ 3.5.2
+
+
+ package
+
+ shade
+
+
+ false
+
+
+
+
+ step.ap_ide.StepUp
+
+
+
+
+ *:*
+
+ META-INF/*.SF
+ META-INF/*.DSA
+ META-INF/*.RSA
+
+
+
+
+
+
+
+
+
+
diff --git a/step-ap-ide/src/main/java/step/ap_ide/CurrentlyOpenedAutomationPackageCollectionFactory.java b/step-ap-ide/src/main/java/step/ap_ide/CurrentlyOpenedAutomationPackageCollectionFactory.java
new file mode 100644
index 0000000000..c7cb2ee986
--- /dev/null
+++ b/step-ap-ide/src/main/java/step/ap_ide/CurrentlyOpenedAutomationPackageCollectionFactory.java
@@ -0,0 +1,63 @@
+package step.ap_ide;
+
+import step.core.collections.AutomationPackageCollectionFactory;
+import step.core.collections.Collection;
+import step.core.collections.CollectionFactory;
+import step.core.collections.EntityVersion;
+
+import java.io.IOException;
+import java.util.Properties;
+import java.util.concurrent.ConcurrentHashMap;
+
+public class CurrentlyOpenedAutomationPackageCollectionFactory implements CollectionFactory {
+
+ private static CurrentlyOpenedAutomationPackageCollectionFactory INSTANCE;
+
+ private AutomationPackageCollectionFactory currentAPFactory;
+ private final ConcurrentHashMap> collectionsByName = new ConcurrentHashMap<>();
+
+ public CurrentlyOpenedAutomationPackageCollectionFactory(Properties ignored) {
+ if (INSTANCE == null) {
+ INSTANCE = this;
+ } else {
+ throw new IllegalStateException("Only one instance is allowed");
+ }
+ }
+
+ public static CurrentlyOpenedAutomationPackageCollectionFactory getInstance() {
+ if (INSTANCE == null) {
+ throw new IllegalStateException("No instance created yet");
+ }
+ return INSTANCE;
+ }
+
+ public void setCurrentFactory(AutomationPackageCollectionFactory currentAPFactory) {
+ if (this.currentAPFactory != null) {
+ try {
+ this.currentAPFactory.close();
+ } catch (IOException e) {
+ // TODO better logging
+ e.printStackTrace();
+ }
+ }
+ this.currentAPFactory = currentAPFactory;
+ collectionsByName.values().forEach(collection -> collection.setFromCurrentFactory(currentAPFactory));
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public Collection getCollection(String name, Class entityClass) {
+ return (Collection) collectionsByName.computeIfAbsent(name, n -> new DynamicallyDelegatingCollection(name, entityClass, currentAPFactory));
+ }
+
+ @SuppressWarnings("rawtypes")
+ @Override
+ public Collection getVersionedCollection(String name) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void close() throws IOException {
+
+ }
+}
diff --git a/step-ap-ide/src/main/java/step/ap_ide/DynamicallyDelegatingCollection.java b/step-ap-ide/src/main/java/step/ap_ide/DynamicallyDelegatingCollection.java
new file mode 100644
index 0000000000..a56fb47e8c
--- /dev/null
+++ b/step-ap-ide/src/main/java/step/ap_ide/DynamicallyDelegatingCollection.java
@@ -0,0 +1,140 @@
+package step.ap_ide;
+
+import step.core.collections.Collection;
+import step.core.collections.CollectionFactory;
+import step.core.collections.Filter;
+import step.core.collections.IndexField;
+import step.core.collections.Order;
+import step.core.collections.SearchOrder;
+import step.core.collections.inmemory.InMemoryCollection;
+
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Optional;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.stream.Stream;
+
+public class DynamicallyDelegatingCollection implements Collection {
+ private final String name;
+ private final Class type;
+ private final Collection fallback = new InMemoryCollection<>();
+ private final AtomicReference> currentCollection = new AtomicReference<>(null);
+
+ public DynamicallyDelegatingCollection(String name, Class type, CollectionFactory currentFactory) {
+ this.name = name;
+ this.type = type;
+ setFromCurrentFactory(currentFactory);
+ }
+
+ public void setFromCurrentFactory(CollectionFactory currentFactory) {
+ if (currentFactory != null) {
+ currentCollection.set(currentFactory.getCollection(name, type));
+ } else {
+ currentCollection.set(null);
+ }
+ }
+
+ private Collection current() {
+ return Optional.ofNullable(currentCollection.get()).orElse(fallback);
+ }
+
+ @Override
+ public String getName() {
+ return name;
+ }
+
+ @Override
+ public long count(Filter filter, Integer limit) {
+ return current().count(filter, limit);
+ }
+
+ @Override
+ public long estimatedCount() {
+ return current().estimatedCount();
+ }
+
+ @Override
+ public Stream find(Filter filter, SearchOrder order, Integer skip, Integer limit, int maxTime) {
+ return current().find(filter, order, skip, limit, maxTime);
+ }
+
+ @Override
+ public Stream findLazy(Filter filter, SearchOrder order, Integer skip, Integer limit, int maxTime) {
+ return current().findLazy(filter, order, skip, limit, maxTime);
+ }
+
+ @Override
+ public Stream findReduced(Filter filter, SearchOrder order, Integer skip, Integer limit, int maxTime, List reduceFields) {
+ return current().findReduced(filter, order, skip, limit, maxTime, reduceFields);
+ }
+
+ @Override
+ public List distinct(String columnName, Filter filter) {
+ return current().distinct(columnName, filter);
+ }
+
+ @Override
+ public void remove(Filter filter) {
+ current().remove(filter);
+ }
+
+ @Override
+ public T save(T entity) {
+ return current().save(entity);
+ }
+
+ @Override
+ public void save(Iterable entities) {
+ current().save(entities);
+ }
+
+ @Override
+ public void createOrUpdateIndex(String field) {
+ // no-op
+ }
+
+ @Override
+ public void createOrUpdateIndex(IndexField indexField) {
+ // no-op
+
+ }
+
+ @Override
+ public void createOrUpdateIndex(String field, Order order) {
+ // no-op
+
+ }
+
+ @Override
+ public void createOrUpdateCompoundIndex(String... fields) {
+ // no-op
+
+ }
+
+ @Override
+ public void createOrUpdateCompoundIndex(LinkedHashSet linkedHashSet) {
+ // no-op
+
+ }
+
+ @Override
+ public void rename(String newName) {
+ // no-op
+
+ }
+
+ @Override
+ public void drop() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Class getEntityClass() {
+ return type;
+ }
+
+ @Override
+ public void dropIndex(String indexName) {
+ //no-op
+ }
+}
diff --git a/step-ap-ide/src/main/java/step/ap_ide/FXApp.java b/step-ap-ide/src/main/java/step/ap_ide/FXApp.java
new file mode 100644
index 0000000000..3c37d5aaaf
--- /dev/null
+++ b/step-ap-ide/src/main/java/step/ap_ide/FXApp.java
@@ -0,0 +1,129 @@
+package step.ap_ide;
+
+import javafx.application.Application;
+import javafx.concurrent.Worker;
+import javafx.scene.Scene;
+import javafx.scene.control.Button;
+import javafx.scene.control.Menu;
+import javafx.scene.control.MenuBar;
+import javafx.scene.control.MenuItem;
+import javafx.scene.control.ToolBar;
+import javafx.scene.layout.BorderPane;
+import javafx.scene.layout.VBox;
+import javafx.scene.web.WebView;
+import javafx.stage.Stage;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.nio.file.Files;
+
+public class FXApp extends Application {
+
+ private static final Logger logger = LoggerFactory.getLogger(FXApp.class);
+
+ private final WebView webView = new WebView();
+ private final MenuBar menuBar = new MenuBar();
+ private final ToolBar toolBar = new ToolBar();
+
+ private void initWebView() {
+ // Route JS alerts and errors to log
+ webView.getEngine().setOnAlert(event -> logger.warn("JS Alert: {}", event.getData()));
+ webView.getEngine().setOnError(event -> logger.error("JS Error: {}", event.getMessage()));
+
+ webView.getEngine().getLoadWorker().stateProperty().addListener((obs, oldState, newState) -> {
+ logger.debug("Webview load state: " + newState);
+ if (newState == Worker.State.FAILED) {
+ logger.error("Webview Network Error: " + webView.getEngine().getLoadWorker().getException());
+ }
+ });
+
+ if (false) {
+ // Not sure what this was good for, but keeping it for now.
+ // 3. Inject JS error handler exactly when the Document object is created
+ webView.getEngine().documentProperty().addListener((obs, oldDoc, newDoc) -> {
+ if (newDoc != null) {
+ try {
+ webView.getEngine().executeScript("window.onerror = function(msg, url, line) { alert(msg + ' at line ' + line); };");
+ } catch (Exception e) {
+ logger.error("Could not inject error handler: " + e.getMessage());
+ }
+ }
+ });
+ }
+ }
+
+ private void initMenuBar() {
+ Menu fileMenu = new Menu("File");
+ MenuItem exitItem = new MenuItem("Exit");
+ exitItem.setOnAction(e -> System.exit(0));
+ fileMenu.getItems().add(exitItem);
+ menuBar.getMenus().add(fileMenu);
+ }
+
+ private void initToolBar() {
+ Button reloadButton = new Button("Reload");
+ reloadButton.setOnAction(e -> webView.getEngine().reload());
+
+ Button dumpHtmlButton = new Button("Dump HTML");
+ dumpHtmlButton.setOnAction(e -> {
+ try {
+ String html = (String) webView.getEngine().executeScript("document.documentElement.outerHTML");
+ System.out.println("--- CURRENT HTML ---");
+ System.out.println(html);
+ System.out.println("--------------------");
+ } catch (Exception ex) {
+ System.err.println("Cannot read HTML yet: " + ex.getMessage());
+ }
+ });
+
+ Button newEmptyApButton = new Button("New Empty AP");
+ newEmptyApButton.setOnAction(e -> {
+ try {
+ File workDir = Files.createTempDirectory("automationPackageCollectionTest").toFile();
+ System.err.println("CREATED AND NOW USING NEW WORKDIR: " + workDir.getAbsolutePath());
+ StepUp.useAutomationPackageDirectory(workDir);
+ webView.getEngine().reload();
+ } catch (Exception ex) {
+ logger.error("error", ex);
+ }
+ });
+
+ toolBar.getItems().addAll(reloadButton, dumpHtmlButton, newEmptyApButton);
+ }
+
+ private Scene initScene() {
+ VBox topContainer = new VBox(menuBar, toolBar);
+ BorderPane root = new BorderPane();
+ root.setTop(topContainer);
+ root.setCenter(webView);
+
+ return new Scene(root, 1600, 1000);
+ }
+
+ @Override
+ public void start(Stage stage) {
+ stage.setTitle("StepUp");
+ stage.setOnCloseRequest(event -> {
+ logger.info("Window closed, terminating JVM...");
+ System.exit(0);
+ });
+
+ initWebView();
+ initMenuBar();
+ initToolBar();
+
+ stage.setScene(initScene());
+ stage.show();
+
+ webView.getEngine().load("http://127.0.0.1:4201");
+ }
+
+ public static void main(String[] args) {
+ // absolutely required when using dev frontend, otherwise
+ // FX webview will (wrongly) try to upgrade non-SSL HTTP requests to HTTP2,
+ // and Frontend will (wrongly) never answer these upgrade requests. State of IT in 2026.
+ System.setProperty("com.sun.webkit.useHTTP2Loader", "false");
+ launch(args);
+ }
+}
diff --git a/step-ap-ide/src/main/java/step/ap_ide/StepUp.java b/step-ap-ide/src/main/java/step/ap_ide/StepUp.java
new file mode 100644
index 0000000000..938961b7f2
--- /dev/null
+++ b/step-ap-ide/src/main/java/step/ap_ide/StepUp.java
@@ -0,0 +1,120 @@
+package step.ap_ide;
+
+import ch.exense.commons.app.Configuration;
+import org.apache.commons.io.FileUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import step.automation.packages.AutomationPackageHookRegistry;
+import step.automation.packages.JavaAutomationPackageArchive;
+import step.automation.packages.JavaAutomationPackageReader;
+import step.automation.packages.deserialization.AutomationPackageSerializationRegistry;
+import step.automation.packages.yaml.AutomationPackageYamlFragmentManager;
+import step.automation.packages.yaml.YamlAutomationPackageVersions;
+import step.core.collections.AutomationPackageCollectionFactory;
+import step.framework.server.ControllerServer;
+import step.parameter.Parameter;
+import step.parameter.automation.AutomationPackageParametersRegistration;
+import step.plans.parser.yaml.YamlPlan;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.InputStream;
+import java.io.PrintWriter;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.Objects;
+import java.util.Properties;
+
+public class StepUp {
+
+ private static final Logger logger = LoggerFactory.getLogger(StepUp.class);
+
+ // These are meant for development to have something to play with without having to recreate everything from scratch
+ private static final String workDirName = "work";
+ private static final String initialDirName = "src/main/resources/work-initial";
+
+ public static void main(String[] args) throws Exception {
+ Configuration configuration = new Configuration();
+ // this uses the existing methods, we could also refactor the implementation to make it a little easier.
+ InputStream propsStream = StepUp.class.getClassLoader().getResourceAsStream("step.properties");
+ configuration.getUnderlyingPropertyObject().load(propsStream);
+ new ControllerServer(configuration).start();
+ initWorkdir();
+ FXApp.main(args); // this will never return
+ }
+
+ private static final JavaAutomationPackageReader READER;
+
+ static {
+ AutomationPackageSerializationRegistry serializationRegistry = new AutomationPackageSerializationRegistry();
+ AutomationPackageHookRegistry hookRegistry = new AutomationPackageHookRegistry();
+ // required for reading parameters, apparently the manager can be null
+ AutomationPackageParametersRegistration.registerParametersHooks(hookRegistry, serializationRegistry, null);
+ READER = new JavaAutomationPackageReader(YamlAutomationPackageVersions.ACTUAL_JSON_SCHEMA_PATH, hookRegistry, serializationRegistry, new Configuration());
+ }
+
+ private static void initWorkdir() throws Exception {
+ File workDir = new File(workDirName);
+ File initialDir = new File(initialDirName);
+ if (!workDir.isDirectory() || (workDir.isDirectory() && Objects.requireNonNull(workDir.listFiles()).length == 0)) {
+ logger.info("Work directory is not present or empty, initializing from " + initialDirName);
+ if (!initialDir.isDirectory()) {
+ workDir = Files.createTempDirectory("step-ap-ide-").toFile();
+ logger.warn("initialDir is not present, using temporary directory: {}", workDir.getAbsolutePath());
+ } else {
+ FileUtils.copyDirectory(initialDir, workDir);
+ if (!workDir.isDirectory()) {
+ throw new RuntimeException("Something went wrong while initializing directory: " + workDir.getAbsolutePath());
+ }
+ }
+ } else {
+ logger.info("Using existing work directory at: " + workDir.getAbsolutePath());
+ logger.info("You may delete this directory to start over from scratch");
+ }
+ useAutomationPackageDirectory(workDir);
+ }
+
+ static void useAutomationPackageDirectory(File apDir) throws Exception {
+ verifyOrCreateMainAPFile(apDir);
+ var fragmentManager = StepUp.READER.getAutomationPackageYamlFragmentManager(apDir);
+ Properties properties = new Properties();
+
+ // variant 1:
+ // parameters all go into parameters.yml, plans go into separate files in plans/$PLAN_NAME.yml
+ // Only works if the target files/directories already exist, so disabled for now
+ if (1 == 0) {
+ properties.setProperty(String.format(AutomationPackageYamlFragmentManager.PROPERTY_NEW_OBJECT_FRAGMENT_MODE, Parameter.ENTITY_NAME), AutomationPackageYamlFragmentManager.NewObjectFragmentMode.FRAGMENT.name());
+ properties.setProperty(String.format(AutomationPackageYamlFragmentManager.PROPERTY_NEW_OBJECT_FRAGMENT_PATH, Parameter.ENTITY_NAME), "parameters.yml");
+ properties.setProperty(String.format(AutomationPackageYamlFragmentManager.PROPERTY_NEW_OBJECT_FRAGMENT_MODE, YamlPlan.PLANS_ENTITY_NAME), AutomationPackageYamlFragmentManager.NewObjectFragmentMode.PER_OBJECT.name());
+ // keywords seem to use PER_OBJECT by default?
+ }
+ // variant 2: simple, everything goes into main descriptor
+ if (1 == 1) {
+ String mainFile = Paths.get(fragmentManager.descriptorYaml.getFragmentUrl().toURI()).toFile().getName();
+ properties.setProperty(String.format(AutomationPackageYamlFragmentManager.PROPERTY_NEW_OBJECT_FRAGMENT_MODE, Parameter.ENTITY_NAME), AutomationPackageYamlFragmentManager.NewObjectFragmentMode.FRAGMENT.name());
+ properties.setProperty(String.format(AutomationPackageYamlFragmentManager.PROPERTY_NEW_OBJECT_FRAGMENT_PATH, Parameter.ENTITY_NAME), mainFile);
+ properties.setProperty(String.format(AutomationPackageYamlFragmentManager.PROPERTY_NEW_OBJECT_FRAGMENT_MODE, YamlPlan.PLANS_ENTITY_NAME), AutomationPackageYamlFragmentManager.NewObjectFragmentMode.FRAGMENT.name());
+ properties.setProperty(String.format(AutomationPackageYamlFragmentManager.PROPERTY_NEW_OBJECT_FRAGMENT_PATH, YamlPlan.PLANS_ENTITY_NAME), mainFile);
+ properties.setProperty(String.format(AutomationPackageYamlFragmentManager.PROPERTY_NEW_OBJECT_FRAGMENT_MODE, "keywords"), AutomationPackageYamlFragmentManager.NewObjectFragmentMode.FRAGMENT.name());
+ properties.setProperty(String.format(AutomationPackageYamlFragmentManager.PROPERTY_NEW_OBJECT_FRAGMENT_PATH, "keywords"), mainFile);
+ }
+ fragmentManager.setProperties(properties);
+ var automationPackageCollectionFactory = new AutomationPackageCollectionFactory(new Properties(), fragmentManager);
+
+ CurrentlyOpenedAutomationPackageCollectionFactory.getInstance().setCurrentFactory(automationPackageCollectionFactory);
+ }
+
+ private static void verifyOrCreateMainAPFile(File apDir) throws Exception {
+ for (String fileName : JavaAutomationPackageArchive.METADATA_FILES) {
+ if (new File(apDir, fileName).isFile()) {
+ return;
+ }
+ }
+ File descriptor = new File(apDir, JavaAutomationPackageArchive.METADATA_FILES.getFirst());
+ logger.info("Initializing AP directory with new descriptor: {}", descriptor.getAbsolutePath());
+ PrintWriter pw = new PrintWriter(new FileOutputStream(descriptor));
+ pw.println("schemaVersion: 1.0.0");
+ pw.println("name: \"My package\""); // TODO: make this configurable somehow
+ pw.close();
+ }
+}
diff --git a/step-ap-ide/src/main/resources/logback.xml b/step-ap-ide/src/main/resources/logback.xml
new file mode 100644
index 0000000000..3b6d6bcaed
--- /dev/null
+++ b/step-ap-ide/src/main/resources/logback.xml
@@ -0,0 +1,14 @@
+
+
+
+ %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
+
+
+
+
+
+
+
+
+
+
diff --git a/step-ap-ide/src/main/resources/step.properties b/step-ap-ide/src/main/resources/step.properties
new file mode 100644
index 0000000000..9681de1e36
--- /dev/null
+++ b/step-ap-ide/src/main/resources/step.properties
@@ -0,0 +1,69 @@
+controller.url=http://localhost:8080/
+port=8080
+#These cannot be disabled
+plugins.StepControllerPlugin.enabled=true
+plugins.MigrationExecutionPlugin.enabled=true
+plugins.MigrationManagerPlugin.enabled=true
+#These are required to get the non-disabled ones to work (in order of crashes i.e. dependencies)
+plugins.VersionManagerPlugin.enabled=true
+plugins.SchedulerPlugin.enabled=true
+plugins.ControllerSettingPlugin.enabled=true
+plugins.ObjectHookControllerPlugin.enabled=true
+plugins.AutomationPackagePlugin.enabled=true
+plugins.FunctionControllerPlugin.enabled=true
+plugins.GridPlugin.enabled=true
+plugins.AsyncTaskManagerPlugin.enabled=true
+# The following are needed too as they cause runtime exceptions or other errors if absent
+plugins.ScreenTemplatePlugin.enabled=true
+plugins.PlanPlugin.enabled=true
+plugins.TablePlugin.enabled=true
+plugins.TableSettingsPlugin.enabled=true
+# UI errors (only???)
+plugins.BookmarkPlugin.enabled=true
+plugins.ParameterManagerControllerPlugin.enabled=true
+plugins.ExecutionPlugin.enabled=true
+plugins.ExecutionTypeControllerPlugin.enabled=true
+#Required functionality
+plugins.CompositeFunctionTypeControllerPlugin.enabled=true
+#Disable all other plugins for now
+plugins.ArtifactRepositoryPlugin.enabled=false
+plugins.AutomationPackageParameterPlugin.enabled=false
+plugins.AutomationPackageRepositoriesPlugin.enabled=false
+plugins.AutomationPackageSchedulerPlugin.enabled=false
+plugins.BaseExecutionTypeControllerPlugin.enabled=false
+plugins.DataPoolPlugin.enabled=false
+plugins.EncryptionManagerDependencyPlugin.enabled=false
+plugins.EntityLockingPlugin.enabled=false
+plugins.ExportManagerPlugin.enabled=false
+plugins.FunctionPackagePlugin.enabled=false
+plugins.GeneralScriptFunctionControllerPlugin.enabled=false
+plugins.InteractivePlugin.enabled=false
+plugins.JMeterPlugin.enabled=false
+plugins.LiveReportingControllerPlugin.enabled=false
+plugins.MeasurementControllerPlugin.enabled=false
+plugins.MigrationManagerTasksPlugin.enabled=false
+plugins.MigrationTasksRegistrationPlugin.enabled=false
+plugins.NodePlugin.enabled=false
+plugins.QuotaManagerControllerPlugin.enabled=false
+plugins.RawMeasurementsControllerPlugin.enabled=false
+plugins.ReferenceFinderPlugin.enabled=false
+plugins.RemoteCollectionPlugin.enabled=false
+plugins.ResourceManagerControllerPlugin.enabled=false
+plugins.ReportLayoutPlugin.enabled=false
+plugins.ScriptEditorPlugin.enabled=false
+plugins.StagingRepositoryPlugin.enabled=false
+plugins.StreamingResourcesControllerPlugin.enabled=false
+plugins.ThreadManagerControllerPlugin.enabled=false
+plugins.TimeSeriesControllerPlugin.enabled=false
+plugins.VersionProviderPlugin.enabled=false
+plugins.ViewControllerPlugin.enabled=false
+plugins.YamlEditorPlanPlugin.enabled=false
+#datasource.db.type=step.core.collections.mongodb.MongoDBCollectionFactory
+#datasource.db.collections=all
+#datasource.db.properties.host=localhost
+#datasource.db.properties.database=wtf
+datasource.mem.type=step.core.collections.inmemory.InMemoryCollectionFactory
+#datasource.mem.collections=controllerlogs
+datasource.mem.collections=all
+datasource.ap.type=step.ap_ide.CurrentlyOpenedAutomationPackageCollectionFactory
+datasource.ap.collections=wtf,plans,functions,parameters
diff --git a/step-ap-ide/src/main/resources/work-initial/.apignore b/step-ap-ide/src/main/resources/work-initial/.apignore
new file mode 100644
index 0000000000..319325c32d
--- /dev/null
+++ b/step-ap-ide/src/main/resources/work-initial/.apignore
@@ -0,0 +1,2 @@
+/ignored
+/ignoredFile.yml
\ No newline at end of file
diff --git a/step-ap-ide/src/main/resources/work-initial/automation-package.yml b/step-ap-ide/src/main/resources/work-initial/automation-package.yml
new file mode 100644
index 0000000000..dc1f2ce474
--- /dev/null
+++ b/step-ap-ide/src/main/resources/work-initial/automation-package.yml
@@ -0,0 +1,25 @@
+schemaVersion: 1.0.0
+name: "My package"
+alertingRules:
+ - name: "Rule1"
+ description: "My test alerting rule"
+ eventClass: ExecutionEndedEvent
+ conditions:
+ - BindingCondition:
+ description: "condition 1"
+ bindingKey: "myKey"
+ negate: false
+ predicate:
+ BindingValueEqualsPredicate:
+ value: "myValue"
+plans: []
+parameters:
+ - key: "parameterInMainDescriptor"
+ value: "value1"
+fragments:
+ - "keywords.yml"
+ - "plans/*.yml"
+ - "schedules.yml"
+ - "parameters.yml"
+ - "parameters2.yml"
+ - "unknown.yml"
diff --git a/step-ap-ide/src/main/resources/work-initial/ignoredFile.yml b/step-ap-ide/src/main/resources/work-initial/ignoredFile.yml
new file mode 100644
index 0000000000..06f858207b
--- /dev/null
+++ b/step-ap-ide/src/main/resources/work-initial/ignoredFile.yml
@@ -0,0 +1 @@
+#I should be ignored
\ No newline at end of file
diff --git a/step-ap-ide/src/main/resources/work-initial/keywords.yml b/step-ap-ide/src/main/resources/work-initial/keywords.yml
new file mode 100644
index 0000000000..5d33a14488
--- /dev/null
+++ b/step-ap-ide/src/main/resources/work-initial/keywords.yml
@@ -0,0 +1,30 @@
+keywords:
+ - JMeter:
+ name: "JMeter keyword from automation package"
+ description: "JMeter keyword 1"
+ executeLocally: false
+ useCustomTemplate: true
+ callTimeout: 1000
+ jmeterTestplan: "jmeterProject1/jmeterProject1.xml"
+ - Composite:
+ name: "Composite keyword from AP"
+ plan:
+ root:
+ testCase:
+ children:
+ - echo:
+ text: "Just echo"
+ - return:
+ output:
+ - output1: "value"
+ - output2:
+ expression: "'some thing dynamic'"
+ - GeneralScript:
+ name: "GeneralScript keyword from AP"
+ scriptLanguage: javascript
+ scriptFile: "jsProject/jsSample.js"
+ librariesFile: "lib/fakeLib.jar"
+ - Node:
+ name: "NodeAutomation"
+ jsfile: "nodeProject/nodeSample.ts"
+
\ No newline at end of file
diff --git a/step-ap-ide/src/main/resources/work-initial/parameters.yml b/step-ap-ide/src/main/resources/work-initial/parameters.yml
new file mode 100644
index 0000000000..1909ed829c
--- /dev/null
+++ b/step-ap-ide/src/main/resources/work-initial/parameters.yml
@@ -0,0 +1,14 @@
+parameters:
+ - key: myKey
+ value: myValue
+ description: some description
+ activationScript: abc
+ priority: 10
+ protectedValue: true
+ scope: APPLICATION
+ scopeEntity: entity
+ - key: mySimpleKey
+ value: mySimpleValue
+ - key: myDynamicParam
+ value:
+ expression: "mySimpleKey"
diff --git a/step-ap-ide/src/main/resources/work-initial/parameters2.yml b/step-ap-ide/src/main/resources/work-initial/parameters2.yml
new file mode 100644
index 0000000000..8754e85903
--- /dev/null
+++ b/step-ap-ide/src/main/resources/work-initial/parameters2.yml
@@ -0,0 +1,9 @@
+parameters:
+ - key: myKey2
+ value: myValue2
+ description: some description 2
+ activationScript: abc
+ priority: 10
+ protectedValue: true
+ scope: APPLICATION
+ scopeEntity: entity
\ No newline at end of file
diff --git a/step-ap-ide/src/main/resources/work-initial/plan.plan b/step-ap-ide/src/main/resources/work-initial/plan.plan
new file mode 100644
index 0000000000..66b7ca6d84
--- /dev/null
+++ b/step-ap-ide/src/main/resources/work-initial/plan.plan
@@ -0,0 +1,3 @@
+Sequence
+Echo "Testing annotated plan"
+End
\ No newline at end of file
diff --git a/step-ap-ide/src/main/resources/work-initial/plans/plan1.yml b/step-ap-ide/src/main/resources/work-initial/plans/plan1.yml
new file mode 100644
index 0000000000..26e6c014f5
--- /dev/null
+++ b/step-ap-ide/src/main/resources/work-initial/plans/plan1.yml
@@ -0,0 +1,26 @@
+---
+fragments: []
+keywords: []
+plans:
+- name: "Test Plan"
+ root:
+ testCase:
+ children:
+ - echo:
+ text: "Just echo"
+ - echo:
+ text:
+ expression: "mySimpleKey"
+ - callKeyword:
+ nodeName: "CallMyKeyword2"
+ inputs:
+ - myInput: "myValue"
+ keyword: "MyKeyword2"
+ categories:
+ - "Yaml Plan"
+plansPlainText:
+- name: "Plain text plan"
+ rootType: "Sequence"
+ categories:
+ - "PlainTextPlan"
+ file: "plans/plan2.plan"
diff --git a/step-ap-ide/src/main/resources/work-initial/plans/plan2.plan b/step-ap-ide/src/main/resources/work-initial/plans/plan2.plan
new file mode 100644
index 0000000000..66b7ca6d84
--- /dev/null
+++ b/step-ap-ide/src/main/resources/work-initial/plans/plan2.plan
@@ -0,0 +1,3 @@
+Sequence
+Echo "Testing annotated plan"
+End
\ No newline at end of file
diff --git a/step-ap-ide/src/main/resources/work-initial/plans/plan2.yml b/step-ap-ide/src/main/resources/work-initial/plans/plan2.yml
new file mode 100644
index 0000000000..2a576d02da
--- /dev/null
+++ b/step-ap-ide/src/main/resources/work-initial/plans/plan2.yml
@@ -0,0 +1,17 @@
+plans:
+ - name: Test Plan with Composite
+ categories:
+ - Yaml Plan
+ - Composite
+ root:
+ testCase:
+ children:
+ - echo:
+ text: "Calling composite"
+ - callKeyword:
+ keyword: "Composite keyword from AP"
+ children:
+ - check:
+ expression: "output.output1.equals('value')"
+ - check:
+ expression: "output.output2.equals('some thing dynamic')"
\ No newline at end of file
diff --git a/step-ap-ide/src/main/resources/work-initial/plansPlainText/firstPlainText.plan b/step-ap-ide/src/main/resources/work-initial/plansPlainText/firstPlainText.plan
new file mode 100644
index 0000000000..e9dd736886
--- /dev/null
+++ b/step-ap-ide/src/main/resources/work-initial/plansPlainText/firstPlainText.plan
@@ -0,0 +1,3 @@
+Sequence
+Echo "First plain text plan"
+End
\ No newline at end of file
diff --git a/step-ap-ide/src/main/resources/work-initial/plansPlainText/secondPlainText.plan b/step-ap-ide/src/main/resources/work-initial/plansPlainText/secondPlainText.plan
new file mode 100644
index 0000000000..00c3bacb0d
--- /dev/null
+++ b/step-ap-ide/src/main/resources/work-initial/plansPlainText/secondPlainText.plan
@@ -0,0 +1,3 @@
+Sequence
+Echo "Second plain text plan"
+End
\ No newline at end of file
diff --git a/step-ap-ide/src/main/resources/work-initial/schedules.yml b/step-ap-ide/src/main/resources/work-initial/schedules.yml
new file mode 100644
index 0000000000..6d95ed7161
--- /dev/null
+++ b/step-ap-ide/src/main/resources/work-initial/schedules.yml
@@ -0,0 +1,7 @@
+schedules:
+ - name: "firstSchedule"
+ cron: "0 15 10 ? * *"
+ cronExclusions:
+ - "0 0 9 25 * ?"
+ - "0 0 9 20 * ?"
+ planName: "Test Plan"
\ No newline at end of file
diff --git a/step-ap-ide/src/main/resources/work-initial/unknown.yml b/step-ap-ide/src/main/resources/work-initial/unknown.yml
new file mode 100644
index 0000000000..d378e6078d
--- /dev/null
+++ b/step-ap-ide/src/main/resources/work-initial/unknown.yml
@@ -0,0 +1,3 @@
+unknown:
+ - someFieldA: valueA
+ someFieldB: valueB
\ No newline at end of file
diff --git a/step-automation-packages/pom.xml b/step-automation-packages/pom.xml
index a5210c3099..3b9e7fbb26 100644
--- a/step-automation-packages/pom.xml
+++ b/step-automation-packages/pom.xml
@@ -42,9 +42,11 @@
step-automation-packages-manager
step-automation-packages-client
step-automation-packages-controller
+ step-automation-packages-collections
+
diff --git a/step-automation-packages/step-automation-packages-collections/pom.xml b/step-automation-packages/step-automation-packages-collections/pom.xml
new file mode 100644
index 0000000000..b2066ba5f8
--- /dev/null
+++ b/step-automation-packages/step-automation-packages-collections/pom.xml
@@ -0,0 +1,58 @@
+
+
+ 4.0.0
+
+ ch.exense.step
+ step-automation-packages
+ 0.0.0-SNAPSHOT
+
+
+ step-automation-packages-collections
+
+
+
+ ch.exense.step
+ step-plans-base-artefacts
+ ${project.version}
+
+
+ ch.exense.step
+ step-automation-packages-yaml
+ ${project.version}
+
+
+
+
+ org.mockito
+ mockito-core
+ test
+
+
+ ch.exense.step
+ step-plans-core
+ ${project.version}
+ test
+
+
+ ch.exense.step
+ step-functions-plugins-jmeter-def
+ ${project.version}
+ test
+
+
+ ch.exense.step
+ step-functions-plugins-node-def
+ ${project.version}
+ test
+
+
+ ch.exense.step
+ step-automation-packages-controller
+ ${project.version}
+ test
+
+
+
+
diff --git a/step-automation-packages/step-automation-packages-collections/src/main/java/step/core/collections/AutomationPackageCollectionFactory.java b/step-automation-packages/step-automation-packages-collections/src/main/java/step/core/collections/AutomationPackageCollectionFactory.java
new file mode 100644
index 0000000000..b5869177dd
--- /dev/null
+++ b/step-automation-packages/step-automation-packages-collections/src/main/java/step/core/collections/AutomationPackageCollectionFactory.java
@@ -0,0 +1,70 @@
+/*******************************************************************************
+ * Copyright (C) 2026, exense GmbH
+ *
+ * This file is part of STEP
+ *
+ * STEP is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * STEP is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with STEP. If not, see .
+ ******************************************************************************/
+package step.core.collections;
+
+import step.automation.packages.yaml.AutomationPackageYamlFragmentManager;
+import step.core.collections.inmemory.InMemoryCollectionFactory;
+import step.core.plans.Plan;
+import step.functions.Function;
+import step.parameter.Parameter;
+
+import java.io.IOException;
+import java.util.Map;
+import java.util.Properties;
+import java.util.concurrent.ConcurrentHashMap;
+
+public class AutomationPackageCollectionFactory implements CollectionFactory {
+
+ private final InMemoryCollectionFactory baseFactory;
+ private final AutomationPackageYamlFragmentManager fragmentManager;
+ private final Map> collectionsByName = new ConcurrentHashMap<>();
+
+ public AutomationPackageCollectionFactory(Properties properties, AutomationPackageYamlFragmentManager fragmentManager) {
+ this.fragmentManager = fragmentManager;
+ this.baseFactory = new InMemoryCollectionFactory(properties);
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public Collection getCollection(String name, Class entityClass) {
+ return (Collection) collectionsByName.computeIfAbsent(name, (_name) -> {
+ if (Plan.class.isAssignableFrom(entityClass)) {
+ return new AutomationPackagePlanCollection(fragmentManager);
+ } else if (Parameter.class.isAssignableFrom(entityClass)) {
+ return new AutomationPackageParameterCollection(fragmentManager);
+ } else if (Function.class.isAssignableFrom(entityClass)) {
+ return new AutomationPackageFunctionCollection(fragmentManager);
+ }
+ return baseFactory.getCollection(name, entityClass);
+ });
+ }
+
+ @SuppressWarnings("rawtypes")
+ @Override
+ public Collection getVersionedCollection(String name) {
+ // TODO: I'm pretty sure the previous implementation was incorrect.
+ // Fix this once we need it and know what the correct implementation is.
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void close() throws IOException {
+ baseFactory.close();
+ }
+}
diff --git a/step-automation-packages/step-automation-packages-collections/src/main/java/step/core/collections/AutomationPackageFunctionCollection.java b/step-automation-packages/step-automation-packages-collections/src/main/java/step/core/collections/AutomationPackageFunctionCollection.java
new file mode 100644
index 0000000000..813aad256b
--- /dev/null
+++ b/step-automation-packages/step-automation-packages-collections/src/main/java/step/core/collections/AutomationPackageFunctionCollection.java
@@ -0,0 +1,59 @@
+/*******************************************************************************
+ * Copyright (C) 2026, exense GmbH
+ *
+ * This file is part of STEP
+ *
+ * STEP is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * STEP is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with STEP. If not, see .
+ ******************************************************************************/
+package step.core.collections;
+
+import step.automation.packages.model.YamlAutomationPackageKeyword;
+import step.automation.packages.yaml.AutomationPackageYamlFragmentManager;
+import step.core.collections.inmemory.InMemoryCollection;
+import step.functions.Function;
+
+public class AutomationPackageFunctionCollection extends InMemoryCollection implements Collection {
+
+
+ private final AutomationPackageYamlFragmentManager fragmentManager;
+
+ public AutomationPackageFunctionCollection(AutomationPackageYamlFragmentManager fragmentManager) {
+ super(true, YamlAutomationPackageKeyword.KEYWORDS_ENTITY_NAME);
+ this.fragmentManager = fragmentManager;
+ initialzeRecordsFromFragments(fragmentManager);
+ }
+
+ private void initialzeRecordsFromFragments(AutomationPackageYamlFragmentManager fragmentManager) {
+ // initialization into the collection memory. Calls super save to avoid calling fragmentManager.savePlan
+ fragmentManager.getBusinessObjects(Function.class).forEach(super::save);
+ }
+
+ @Override
+ public Function save(Function p) {
+ return super.save(fragmentManager.saveFunction(p));
+ }
+
+ @Override
+ public void save(Iterable iterable) {
+ for (Function p : iterable) {
+ save(p);
+ }
+ }
+
+ @Override
+ public void remove(Filter filter) {
+ find(filter, null, null, null, 0).forEach(fragmentManager::removeFunction);
+ super.remove(filter);
+ }
+}
diff --git a/step-automation-packages/step-automation-packages-collections/src/main/java/step/core/collections/AutomationPackageParameterCollection.java b/step-automation-packages/step-automation-packages-collections/src/main/java/step/core/collections/AutomationPackageParameterCollection.java
new file mode 100644
index 0000000000..ad7bfdfc2f
--- /dev/null
+++ b/step-automation-packages/step-automation-packages-collections/src/main/java/step/core/collections/AutomationPackageParameterCollection.java
@@ -0,0 +1,61 @@
+/*******************************************************************************
+ * Copyright (C) 2026, exense GmbH
+ *
+ * This file is part of STEP
+ *
+ * STEP is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * STEP is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with STEP. If not, see .
+ ******************************************************************************/
+package step.core.collections;
+
+import step.automation.packages.yaml.AutomationPackageYamlFragmentManager;
+import step.core.collections.inmemory.InMemoryCollection;
+import step.parameter.Parameter;
+import step.parameter.automation.AutomationPackageParameter;
+
+public class AutomationPackageParameterCollection extends InMemoryCollection implements Collection {
+
+
+ private final AutomationPackageYamlFragmentManager fragmentManager;
+
+ public AutomationPackageParameterCollection(AutomationPackageYamlFragmentManager fragmentManager) {
+ super(true, Parameter.ENTITY_NAME);
+ this.fragmentManager = fragmentManager;
+ initialzeRecordsFromFragments(fragmentManager);
+ }
+
+ private void initialzeRecordsFromFragments(AutomationPackageYamlFragmentManager fragmentManager) {
+ // initialization into the collection memory. Calls super save to avoid calling fragmentManager.savePlan
+ fragmentManager.getBusinessObjects(Parameter.class).forEach(super::save);
+ }
+
+ @Override
+ public Parameter save(Parameter parameter) {
+ return super.save(fragmentManager.saveAdditionalFieldObject(parameter, context -> AutomationPackageParameter.forContext(context, parameter), Parameter.ENTITY_NAME));
+ }
+
+ @Override
+ public void save(Iterable iterable) {
+ for (Parameter p : iterable) {
+ save(p);
+ }
+ }
+
+ @Override
+ public void remove(Filter filter) {
+ find(filter, null, null, null, 0).forEach(parameter ->
+ fragmentManager.removeAdditionalFieldObject(parameter, Parameter.ENTITY_NAME)
+ );
+ super.remove(filter);
+ }
+}
diff --git a/step-automation-packages/step-automation-packages-collections/src/main/java/step/core/collections/AutomationPackagePlanCollection.java b/step-automation-packages/step-automation-packages-collections/src/main/java/step/core/collections/AutomationPackagePlanCollection.java
new file mode 100644
index 0000000000..a68b9488a6
--- /dev/null
+++ b/step-automation-packages/step-automation-packages-collections/src/main/java/step/core/collections/AutomationPackagePlanCollection.java
@@ -0,0 +1,59 @@
+/*******************************************************************************
+ * Copyright (C) 2026, exense GmbH
+ *
+ * This file is part of STEP
+ *
+ * STEP is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * STEP is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with STEP. If not, see .
+ ******************************************************************************/
+package step.core.collections;
+
+import step.automation.packages.yaml.AutomationPackageYamlFragmentManager;
+import step.core.collections.inmemory.InMemoryCollection;
+import step.core.plans.Plan;
+import step.plans.parser.yaml.YamlPlan;
+
+public class AutomationPackagePlanCollection extends InMemoryCollection implements Collection {
+
+
+ private final AutomationPackageYamlFragmentManager fragmentManager;
+
+ public AutomationPackagePlanCollection(AutomationPackageYamlFragmentManager fragmentManager) {
+ super(true, YamlPlan.PLANS_ENTITY_NAME);
+ this.fragmentManager = fragmentManager;
+ initialzeRecordsFromFragments(fragmentManager);
+ }
+
+ private void initialzeRecordsFromFragments(AutomationPackageYamlFragmentManager fragmentManager) {
+ // initialization into the collection memory. Calls super save to avoid calling fragmentManager.savePlan
+ fragmentManager.getBusinessObjects(Plan.class).forEach(super::save);
+ }
+
+ @Override
+ public Plan save(Plan p) {
+ return super.save(fragmentManager.savePlan(p));
+ }
+
+ @Override
+ public void save(Iterable iterable) {
+ for (Plan p : iterable) {
+ save(p);
+ }
+ }
+
+ @Override
+ public void remove(Filter filter) {
+ find(filter, null, null, null, 0).forEach(fragmentManager::removePlan);
+ super.remove(filter);
+ }
+}
diff --git a/step-automation-packages/step-automation-packages-collections/src/test/java/step/core/collections/AutomationPackageCollectionFactoryTest.java b/step-automation-packages/step-automation-packages-collections/src/test/java/step/core/collections/AutomationPackageCollectionFactoryTest.java
new file mode 100644
index 0000000000..e9fbe009c5
--- /dev/null
+++ b/step-automation-packages/step-automation-packages-collections/src/test/java/step/core/collections/AutomationPackageCollectionFactoryTest.java
@@ -0,0 +1,20 @@
+package step.core.collections;
+
+import org.junit.Assert;
+import org.junit.Test;
+import step.core.plans.Plan;
+import step.plans.parser.yaml.YamlPlan;
+
+import java.util.Properties;
+
+public class AutomationPackageCollectionFactoryTest extends AutomationPackageCollectionTestBase {
+
+ @Test
+ public void testCollectionIdempotence() {
+ AutomationPackageCollectionFactory cf = new AutomationPackageCollectionFactory(new Properties(), fragmentManager);
+ Collection c1 = cf.getCollection(YamlPlan.PLANS_ENTITY_NAME, Plan.class);
+ Assert.assertTrue(c1.estimatedCount() > 0); // just for good measure
+ Collection c2 = cf.getCollection(YamlPlan.PLANS_ENTITY_NAME, Plan.class);
+ Assert.assertSame(c1, c2);
+ }
+}
diff --git a/step-automation-packages/step-automation-packages-collections/src/test/java/step/core/collections/AutomationPackageCollectionTestBase.java b/step-automation-packages/step-automation-packages-collections/src/test/java/step/core/collections/AutomationPackageCollectionTestBase.java
new file mode 100644
index 0000000000..3ed25186ed
--- /dev/null
+++ b/step-automation-packages/step-automation-packages-collections/src/test/java/step/core/collections/AutomationPackageCollectionTestBase.java
@@ -0,0 +1,93 @@
+/*******************************************************************************
+ * Copyright (C) 2026, exense GmbH
+ *
+ * This file is part of STEP
+ *
+ * STEP is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * STEP is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with STEP. If not, see .
+ ******************************************************************************/
+package step.core.collections;
+
+import ch.exense.commons.app.Configuration;
+import org.apache.commons.io.FileUtils;
+import org.junit.After;
+import org.junit.Before;
+import org.mockito.Mockito;
+import step.automation.packages.AutomationPackageHookRegistry;
+import step.automation.packages.AutomationPackageReadingException;
+import step.automation.packages.JavaAutomationPackageReader;
+import step.automation.packages.deserialization.AutomationPackageSerializationRegistry;
+import step.automation.packages.yaml.AutomationPackageYamlFragmentManager;
+import step.automation.packages.yaml.YamlAutomationPackageVersions;
+import step.parameter.ParameterManager;
+import step.parameter.automation.AutomationPackageParametersRegistration;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Properties;
+
+import static org.junit.Assert.assertEquals;
+
+public class AutomationPackageCollectionTestBase {
+
+ private final JavaAutomationPackageReader reader;
+
+ // To use a different source directory, override in subclass constructor
+ protected File sourceDirectory = new File("src/test/resources/testdata/ap1");
+ protected File destinationDirectory;
+ protected Path expectedFilesPath = new File("src/test/resources/expected").toPath();
+ protected AutomationPackageYamlFragmentManager fragmentManager;
+
+ public AutomationPackageCollectionTestBase() {
+ AutomationPackageSerializationRegistry serializationRegistry = new AutomationPackageSerializationRegistry();
+ AutomationPackageHookRegistry hookRegistry = new AutomationPackageHookRegistry();
+
+ // accessor is not required in this test - we only read the yaml and don't store the result anywhere
+ AutomationPackageParametersRegistration.registerParametersHooks(hookRegistry, serializationRegistry, Mockito.mock(ParameterManager.class));
+
+ this.reader = new JavaAutomationPackageReader(YamlAutomationPackageVersions.ACTUAL_JSON_SCHEMA_PATH, hookRegistry, serializationRegistry, new Configuration());
+ }
+
+ @Before
+ public void setUp() throws IOException, AutomationPackageReadingException {
+ destinationDirectory = Files.createTempDirectory("automationPackageCollectionTest").toFile();
+ FileUtils.copyDirectory(sourceDirectory, destinationDirectory);
+
+ fragmentManager = reader.getAutomationPackageYamlFragmentManager(destinationDirectory);
+ }
+
+ @After
+ public void tearDown() throws IOException, AutomationPackageReadingException {
+ // Attempt to re-read the just written Automation package from scratch
+ reader.getAutomationPackageYamlFragmentManager(destinationDirectory);
+ FileUtils.deleteDirectory(destinationDirectory);
+ }
+
+
+ protected void assertFilesEqual(Path expected, Path actual) throws IOException {
+ String expectedLines = Files.readString(expected);
+ String actualLines = Files.readString(actual);
+
+ assertEquals(expectedLines, actualLines);
+ }
+
+ protected void setPropertiesWriteToFragment(String entityName, String fragment) {
+ Properties properties = new Properties();
+ properties.setProperty(String.format(AutomationPackageYamlFragmentManager.PROPERTY_NEW_OBJECT_FRAGMENT_PATH, entityName), fragment);
+ properties.setProperty(String.format(AutomationPackageYamlFragmentManager.PROPERTY_NEW_OBJECT_FRAGMENT_MODE, entityName), AutomationPackageYamlFragmentManager.NewObjectFragmentMode.FRAGMENT.name());
+
+ fragmentManager.setProperties(properties);
+ }
+}
diff --git a/step-automation-packages/step-automation-packages-collections/src/test/java/step/core/collections/AutomationPackageKeywordCollectionTest.java b/step-automation-packages/step-automation-packages-collections/src/test/java/step/core/collections/AutomationPackageKeywordCollectionTest.java
new file mode 100644
index 0000000000..5a52ed56b5
--- /dev/null
+++ b/step-automation-packages/step-automation-packages-collections/src/test/java/step/core/collections/AutomationPackageKeywordCollectionTest.java
@@ -0,0 +1,85 @@
+/*******************************************************************************
+ * Copyright (C) 2026, exense GmbH
+ *
+ * This file is part of STEP
+ *
+ * STEP is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * STEP is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with STEP. If not, see .
+ ******************************************************************************/
+package step.core.collections;
+
+import org.junit.Before;
+import org.junit.Test;
+import step.artefacts.Echo;
+import step.automation.packages.AutomationPackageReadingException;
+import step.automation.packages.model.YamlAutomationPackageKeyword;
+import step.core.accessors.AbstractOrganizableObject;
+import step.core.dynamicbeans.DynamicValue;
+import step.functions.Function;
+import step.plugins.functions.types.CompositeFunction;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Optional;
+import java.util.Properties;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+public class AutomationPackageKeywordCollectionTest extends AutomationPackageCollectionTestBase {
+
+ private Collection functionCollection;
+
+ public AutomationPackageKeywordCollectionTest() {
+ super();
+ }
+
+ @Before
+ public void setUp() throws IOException, AutomationPackageReadingException {
+ super.setUp();
+ AutomationPackageCollectionFactory collectionFactory = new AutomationPackageCollectionFactory(new Properties(), fragmentManager);
+ functionCollection = collectionFactory.getCollection(YamlAutomationPackageKeyword.KEYWORDS_ENTITY_NAME, Function.class);
+ }
+
+ @Test
+ public void testLoadAllKeywords() throws IOException {
+ List functions = functionCollection.find(Filters.empty(), null, null, null, 100).collect(Collectors.toList());
+
+ assertEquals(4, functions.size());
+ Set functionNames = functions.stream().map(f -> f.getAttribute(AbstractOrganizableObject.NAME)).collect(Collectors.toSet());
+
+ assertTrue(functionNames.contains("NodeAutomation"));
+ assertTrue(functionNames.contains("JMeter keyword from automation package"));
+ assertTrue(functionNames.contains("Composite keyword from AP"));
+ assertTrue(functionNames.contains("GeneralScript keyword from AP"));
+ }
+
+ @Test
+ public void testModifyCompositeKeyword() throws IOException {
+ Optional optionalFunction = functionCollection.find(Filters.equals("attributes.name", "Composite keyword from AP"), null, null, null, 100).findFirst();
+ assertTrue(optionalFunction.isPresent());
+
+ CompositeFunction compositeFunction = (CompositeFunction) optionalFunction.get();
+
+ Echo echo = (Echo) compositeFunction.getPlan().getRoot().getChildren().getFirst();
+ echo.setText(new DynamicValue<>("Modified Echo"));
+
+ setPropertiesWriteToFragment(YamlAutomationPackageKeyword.KEYWORDS_ENTITY_NAME, "keywords.yml");
+ functionCollection.save(compositeFunction);
+
+ assertFilesEqual(expectedFilesPath.resolve("keywordsAfterCompositeModification.yml"), destinationDirectory.toPath().resolve("keywords.yml"));
+ }
+
+}
diff --git a/step-automation-packages/step-automation-packages-collections/src/test/java/step/core/collections/AutomationPackageParameterCollectionTest.java b/step-automation-packages/step-automation-packages-collections/src/test/java/step/core/collections/AutomationPackageParameterCollectionTest.java
new file mode 100644
index 0000000000..8c33637c55
--- /dev/null
+++ b/step-automation-packages/step-automation-packages-collections/src/test/java/step/core/collections/AutomationPackageParameterCollectionTest.java
@@ -0,0 +1,94 @@
+/*******************************************************************************
+ * Copyright (C) 2026, exense GmbH
+ *
+ * This file is part of STEP
+ *
+ * STEP is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * STEP is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with STEP. If not, see .
+ ******************************************************************************/
+package step.core.collections;
+
+import org.junit.Before;
+import org.junit.Test;
+import step.automation.packages.AutomationPackageReadingException;
+import step.core.dynamicbeans.DynamicValue;
+import step.core.yaml.deserialization.AutomationPackagePerObjectSaveUnsupportedException;
+import step.parameter.Parameter;
+
+import java.io.IOException;
+import java.util.Optional;
+import java.util.Properties;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+
+public class AutomationPackageParameterCollectionTest extends AutomationPackageCollectionTestBase {
+
+ private Collection parameterCollection;
+
+ public AutomationPackageParameterCollectionTest() {
+ super();
+ }
+
+ @Before
+ public void setUp() throws IOException, AutomationPackageReadingException {
+ super.setUp();
+ AutomationPackageCollectionFactory collectionFactory = new AutomationPackageCollectionFactory(new Properties(), fragmentManager);
+ parameterCollection = collectionFactory.getCollection(Parameter.ENTITY_NAME, Parameter.class);
+ }
+
+ @Test
+ public void testParameterModify() throws IOException {
+ Optional optionalParameter = parameterCollection.find(Filters.equals("key", "mySimpleKey"), null, null, null, 100).findFirst();
+
+ assertTrue(optionalParameter.isPresent());
+
+ Parameter parameter = optionalParameter.get();
+
+ parameter.getValue().setValue("myModifiedValue");
+ parameterCollection.save(parameter);
+
+ assertFilesEqual(expectedFilesPath.resolve("parametersAfterModification.yml"), destinationDirectory.toPath().resolve("parameters.yml"));
+ }
+
+
+ @Test
+ public void testParameterAddAndModify() throws IOException {
+
+
+ Parameter parameter = new Parameter(null, "addedParameter", "test", "This is an added Parameter before modification");
+ assertThrows(AutomationPackagePerObjectSaveUnsupportedException.class, () -> parameterCollection.save(parameter));
+
+
+ setPropertiesWriteToFragment(Parameter.ENTITY_NAME, "parameters.yml");
+ parameterCollection.save(parameter);
+
+ assertFilesEqual(expectedFilesPath.resolve("parametersAfterAdd.yml"), destinationDirectory.toPath().resolve("parameters.yml"));
+
+ parameter.setDescription("This is an added Parameter with a new description");
+ parameterCollection.save(parameter);
+
+ parameter.setValue(new DynamicValue<>("foo"));
+ parameterCollection.save(parameter);
+
+ assertFilesEqual(expectedFilesPath.resolve("parametersAfterAddAndModification.yml"), destinationDirectory.toPath().resolve("parameters.yml"));
+ }
+
+ @Test
+ public void testParametersInRootApAreAddedOnlyOnce() throws Exception {
+ // SED-4681
+ assertEquals(1, parameterCollection.find(Filters.equals("key", "paramInMainAP"), null, null, null, 0).count());
+ }
+
+}
diff --git a/step-automation-packages/step-automation-packages-collections/src/test/java/step/core/collections/AutomationPackagePlanCollectionTest.java b/step-automation-packages/step-automation-packages-collections/src/test/java/step/core/collections/AutomationPackagePlanCollectionTest.java
new file mode 100644
index 0000000000..0abe4b2e61
--- /dev/null
+++ b/step-automation-packages/step-automation-packages-collections/src/test/java/step/core/collections/AutomationPackagePlanCollectionTest.java
@@ -0,0 +1,268 @@
+/*******************************************************************************
+ * Copyright (C) 2026, exense GmbH
+ *
+ * This file is part of STEP
+ *
+ * STEP is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * STEP is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with STEP. If not, see .
+ ******************************************************************************/
+package step.core.collections;
+
+import org.junit.Before;
+import org.junit.Test;
+import step.artefacts.Echo;
+import step.artefacts.Sequence;
+import step.automation.packages.AutomationPackageReadingException;
+import step.core.dynamicbeans.DynamicValue;
+import step.core.plans.Plan;
+import step.core.yaml.deserialization.AutomationPackageConcurrentEditException;
+import step.plans.parser.yaml.YamlPlan;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.StandardCopyOption;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Properties;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+
+public class AutomationPackagePlanCollectionTest extends AutomationPackageCollectionTestBase {
+
+ private Collection planCollection;
+
+ public AutomationPackagePlanCollectionTest() {
+ super();
+ }
+
+ @Before
+ public void setUp() throws IOException, AutomationPackageReadingException {
+ super.setUp();
+ AutomationPackageCollectionFactory collectionFactory = new AutomationPackageCollectionFactory(new Properties(), fragmentManager);
+ planCollection = collectionFactory.getCollection(YamlPlan.PLANS_ENTITY_NAME, Plan.class);
+ }
+
+ @Test
+ public void testReadAllPlans() {
+ long count = planCollection.count(Filters.empty(), 100);
+ List plans = planCollection.find(Filters.empty(), null, null, null, 100).collect(Collectors.toList());
+
+ assertEquals(2, count);
+ Set names = plans.stream().map(p -> p.getAttributes().get("name")).collect(Collectors.toUnmodifiableSet());
+
+ assertEquals(2, names.size());
+
+ assertTrue(names.contains("Test Plan"));
+ assertTrue(names.contains("Test Plan with Composite"));
+ }
+
+ @Test
+ public void testPlanModify() throws IOException {
+ Optional optionalPlan = planCollection.find(Filters.equals("attributes.name", "Test Plan"), null, null, null, 100).findFirst();
+
+ assertTrue(optionalPlan.isPresent());
+
+ Plan plan = optionalPlan.get();
+
+ Echo firstEcho = (Echo) plan.getRoot().getChildren().get(0);
+ DynamicValue