Repository: kestra-io/kestra (java)

Issue:
Possible to save bad flow which runs idefinitely
### Describe the issue

Consider the parent and subflow below. Notice that the type is incorrect, it should be Subflow, not subflow. Kestra UI allows us to save this flow. 
```
id: parent
namespace: abc

tasks:
  - id: hello
    type: io.kestra.plugin.core.flow.subflow
    flowId: child
    namespace: abc
-------
id: child
namespace: abc

tasks:
  - id: hello
    type: io.kestra.plugin.core.log.Log
    message: Hello World! 🚀
```

The subflow is executed correctly, but the parent flow remains running indefinitely (until manually killed). 

What should be the correct behavior here? (stop flow from being saved in the first place, or allow case-insensitive type definition) 

![Image](https://github.com/user-attachments/assets/ccbd1134-ce0f-4804-92f1-e685c7aed436)

### Environment

- Kestra Version: 0.21.2


Pull request:
fix(core): no longer lowercasing PluginClassIdentifier to have proper validation upon Plugin deserialization
closes #7387

Patch:
diff --git a/README.md b/README.md
index 1c4df62a6..42ea4e69f 100644
--- a/README.md
+++ b/README.md
@@ -24,6 +24,12 @@
 <a href="https://www.youtube.com/@kestra-io"><img height="25" src="https://kestra.io/youtube.svg" alt="youtube" /></a> &nbsp;
 </p>
 
+<p align="center">
+  <a href="https://trendshift.io/repositories/2714" target="_blank">
+    <img src="https://trendshift.io/api/badge/repositories/2714" alt="kestra-io%2Fkestra | Trendshift" width="250" height="55"/>
+  </a>
+</p>
+
 <p align="center">
     <a href="https://go.kestra.io/video/product-overview" target="_blank">
         <img src="https://kestra.io/startvideo.png" alt="Get started in 4 minutes with Kestra" width="640px" />
@@ -47,7 +53,7 @@ Kestra is an open-source, event-driven orchestration platform that makes both **
 - **Structure & Resilience**: tame chaos and bring resilience to your workflows with **namespaces**, **labels**, **subflows**, **retries**, **timeout**, **error handling**, **inputs**, **outputs** that generate artifacts in the UI, **variables**, **conditional branching**, **advanced scheduling**, **event triggers**, **backfills**, **dynamic tasks**, **sequential and parallel tasks**, and skip tasks or triggers when needed by setting the flag `disabled` to `true`.
 
 
-🧑‍💻 The YAML definition gets automatically adjusted any time you make changes to a workflow from the UI or via an API call. Therefore, the orchestration logic is **always managed declaratively in code**, even if you modify your workflows in other ways (UI, CI/CD, Terraform, API calls). 
+🧑‍💻 The YAML definition gets automatically adjusted any time you make changes to a workflow from the UI or via an API call. Therefore, the orchestration logic is **always managed declaratively in code**, even if you modify your workflows in other ways (UI, CI/CD, Terraform, API calls).
 
 
 <p align="center">
diff --git a/core/src/main/java/io/kestra/core/plugins/DefaultPluginRegistry.java b/core/src/main/java/io/kestra/core/plugins/DefaultPluginRegistry.java
index 580cb586f..63e0c1dff 100644
--- a/core/src/main/java/io/kestra/core/plugins/DefaultPluginRegistry.java
+++ b/core/src/main/java/io/kestra/core/plugins/DefaultPluginRegistry.java
@@ -217,7 +217,7 @@ public class DefaultPluginRegistry implements PluginRegistry {
             if (identifier == null || identifier.isBlank()) {
                 throw new IllegalArgumentException("Cannot create plugin identifier from null or empty string");
             }
-            return new ClassTypeIdentifier(identifier.toLowerCase(Locale.ROOT));
+            return new ClassTypeIdentifier(identifier);
         }
 
         /**
diff --git a/core/src/main/java/io/kestra/core/runners/FilesService.java b/core/src/main/java/io/kestra/core/runners/FilesService.java
index f3ed7d4ef..3e79d154e 100644
--- a/core/src/main/java/io/kestra/core/runners/FilesService.java
+++ b/core/src/main/java/io/kestra/core/runners/FilesService.java
@@ -71,7 +71,7 @@ public abstract class FilesService {
             .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
 
         if (runContext.logger().isTraceEnabled()) {
-            runContext.logger().trace("Captured {} output(s).", allFilesMatching.size());
+            runContext.logger().trace("Captured {} output file(s).", allFilesMatching.size());
         }
 
         return outputFiles;
diff --git a/ui/src/components/executions/Overview.vue b/ui/src/components/executions/Overview.vue
index b029bd514..55e7c5c1c 100644
--- a/ui/src/components/executions/Overview.vue
+++ b/ui/src/components/executions/Overview.vue
@@ -306,7 +306,8 @@
                     const params = {
                         namespace: this.execution.namespace,
                         flowId: this.execution.flowId,
-                        pageSize: 100
+                        pageSize: 100,
+                        sort: "state.startDate:desc"
                     };
                     
                     const result = await this.$store.dispatch("execution/findExecutions", params);
@@ -331,6 +332,8 @@
                 if (!result) return;
 
                 const {executions, currentIndex} = result;
+                // Since executions are sorted by startDate desc here. (opposite of default ASC sort as in Execution Table)
+                // "next" means newer (lower index) and "previous" means older (higher index)
                 const targetIndex = direction === "previous" ? currentIndex + 1 : currentIndex - 1;
 
                 if (targetIndex >= 0 && targetIndex < executions.length) {
@@ -354,7 +357,9 @@
                 }
 
                 const {executions, currentIndex} = result;
+                // Previous means we can go to older executions.
                 this.hasPreviousExecution = currentIndex < executions.length - 1;
+                // Next means we can go to newer executions.
                 this.hasNextExecution = currentIndex > 0;
             },
         },


Test patch:
diff --git a/core/src/test/java/io/kestra/core/plugins/ClassTypeIdentifierTest.java b/core/src/test/java/io/kestra/core/plugins/ClassTypeIdentifierTest.java
new file mode 100644
index 000000000..312c5700e
--- /dev/null
+++ b/core/src/test/java/io/kestra/core/plugins/ClassTypeIdentifierTest.java
@@ -0,0 +1,17 @@
+package io.kestra.core.plugins;
+
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+
+public class ClassTypeIdentifierTest {
+    @Test
+    void caseMatters() {
+        String identifier = "io.kestra.core.plugins.serdes.PluginDeserializerTest.TestPlugin";
+        assertThat(
+            DefaultPluginRegistry.ClassTypeIdentifier.create(identifier).type(),
+            is(identifier)
+        );
+    }
+}