This is an automated email from the ASF dual-hosted git repository.

hansva pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/hop.git


The following commit(s) were added to refs/heads/main by this push:
     new 66f7306a34 Fix the error path in the Neo4j perspective, fixes #7662 
(#7685)
66f7306a34 is described below

commit 66f7306a34b89d2ae06efc41cfea76338155fc91
Author: Bart Maertens <[email protected]>
AuthorDate: Thu Jul 30 10:50:05 2026 +0200

    Fix the error path in the Neo4j perspective, fixes #7662 (#7685)
    
    The Error path tab never showed the action that actually failed:
    
    - The workflow logging wrote every counter of an action except the
      error count, so a failed action never satisfied "err.errors > 0".
      The parameter was already being filled in, it just wasn't set on the
      node. Pipelines and transforms already logged theirs.
    - The lookup used size() on a pattern expression, which Neo4j 5 removed,
      so the query failed outright against Neo4j 5. Replaced with a pattern
      predicate, which works on both Neo4j 4 and 5.
    
    Also in the perspective:
    
    - Read strings through LoggingCore instead of Value.asString(), which
      returns the literal text "null" for a property that isn't set and so
      slipped past Const.NVL().
    - Tell the user what happened when Open finds no file behind an
      execution, instead of leaving the button without any effect.
    
    Adds unit tests for the generated Cypher, an integration test which runs
    the lookup against a Neo4j 5 container, and a Hop integration test under
    integration-tests/neo4j. The container test is skipped automatically when
    no Docker daemon is available.
---
 .../ROOT/pages/hop-gui/perspective-neo4j.adoc      |   4 +
 .../neo4j/datasets/neo4j-error-path-7662.csv       |   2 +
 .../metadata/dataset/neo4j-error-path-7662.json    |  32 +++
 .../neo4j-error-path-7662-validation.json          |  38 ++++
 .../main-neo4j-error-path-7662.hwf                 | 185 +++++++++++++++++
 .../neo4j-error-path-7662-validation.hpl           | 225 +++++++++++++++++++++
 plugins/tech/neo4j/pom.xml                         |  33 +++
 .../hop/neo4j/execution/path/PathResult.java       |   5 +-
 .../logging/xp/WorkflowLoggingExtensionPoint.java  |   4 +
 .../hop/neo4j/perspective/HistoryResult.java       |   5 +-
 .../hop/neo4j/perspective/HopNeo4jPerspective.java | 175 +++++++++-------
 .../perspective/messages/messages_en_US.properties |   3 +
 .../org/apache/hop/neo4j/CypherAssertions.java     |  51 +++++
 .../hop/neo4j/execution/path/PathResultTest.java   |  84 ++++++++
 .../hop/neo4j/perspective/ErrorPathCypherIT.java   | 150 ++++++++++++++
 .../hop/neo4j/perspective/HistoryResultTest.java   |  93 +++++++++
 .../neo4j/perspective/HopNeo4jPerspectiveTest.java |  88 ++++++++
 17 files changed, 1106 insertions(+), 71 deletions(-)

diff --git 
a/docs/hop-user-manual/modules/ROOT/pages/hop-gui/perspective-neo4j.adoc 
b/docs/hop-user-manual/modules/ROOT/pages/hop-gui/perspective-neo4j.adoc
index acd59c8811..98398b14ae 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/hop-gui/perspective-neo4j.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/hop-gui/perspective-neo4j.adoc
@@ -30,4 +30,8 @@ For the selected workflow or pipeline, or all executions if 
none selected), the
 
 In the bottom half of the perspective dialog, the logging text, path to errors 
and a number of useful cypher queries are shown.
 
+Selecting an execution and clicking Open takes you to the pipeline or workflow 
behind it, with the failing transform or action selected.
+
+IMPORTANT: This perspective only shows executions written by the Neo4j logging 
in `NEO4J_LOGGING_CONNECTION`. Executions written by a 
xref:metadata-types/neo4j/neo4j-location-type.adoc[Neo4j] 
xref:metadata-types/execution-information-location.adoc[execution information 
location] are stored with a different set of properties and do not appear here: 
they are shown in the 
xref:hop-gui/perspective-execution-information.adoc[Execution Information 
perspective], which gains extra Lineage, Error [...]
+
 TIP: While fully functional we recommend you use the 
xref:metadata-types/neo4j/neo4j-location-type.adoc[Neo4j] 
xref:metadata-types/execution-information-location.adoc[execution information 
location] plugin.
\ No newline at end of file
diff --git a/integration-tests/neo4j/datasets/neo4j-error-path-7662.csv 
b/integration-tests/neo4j/datasets/neo4j-error-path-7662.csv
new file mode 100644
index 0000000000..4ae4dd4f99
--- /dev/null
+++ b/integration-tests/neo4j/datasets/neo4j-error-path-7662.csv
@@ -0,0 +1,2 @@
+errorId,errorName,pathLength
+it-7662-failed,Table output,2
diff --git 
a/integration-tests/neo4j/metadata/dataset/neo4j-error-path-7662.json 
b/integration-tests/neo4j/metadata/dataset/neo4j-error-path-7662.json
new file mode 100644
index 0000000000..489e0d4f76
--- /dev/null
+++ b/integration-tests/neo4j/metadata/dataset/neo4j-error-path-7662.json
@@ -0,0 +1,32 @@
+{
+  "base_filename": "neo4j-error-path-7662.csv",
+  "name": "neo4j-error-path-7662",
+  "description": "The deepest execution which reported errors, as found by the 
error path lookup of the Neo4j perspective",
+  "dataset_fields": [
+    {
+      "field_comment": "",
+      "field_length": -1,
+      "field_type": 2,
+      "field_precision": 0,
+      "field_format": "",
+      "field_name": "errorId"
+    },
+    {
+      "field_comment": "",
+      "field_length": -1,
+      "field_type": 2,
+      "field_precision": 0,
+      "field_format": "",
+      "field_name": "errorName"
+    },
+    {
+      "field_comment": "",
+      "field_length": -1,
+      "field_type": 5,
+      "field_precision": 0,
+      "field_format": "####0;-####0",
+      "field_name": "pathLength"
+    }
+  ],
+  "folder_name": ""
+}
diff --git 
a/integration-tests/neo4j/metadata/unit-test/neo4j-error-path-7662-validation.json
 
b/integration-tests/neo4j/metadata/unit-test/neo4j-error-path-7662-validation.json
new file mode 100644
index 0000000000..1c52122eae
--- /dev/null
+++ 
b/integration-tests/neo4j/metadata/unit-test/neo4j-error-path-7662-validation.json
@@ -0,0 +1,38 @@
+{
+  "variableValues": [],
+  "database_replacements": [],
+  "autoOpening": true,
+  "basePath": "",
+  "golden_data_sets": [
+    {
+      "field_mappings": [
+        {
+          "transform_field": "errorId",
+          "data_set_field": "errorId"
+        },
+        {
+          "transform_field": "errorName",
+          "data_set_field": "errorName"
+        },
+        {
+          "transform_field": "pathLength",
+          "data_set_field": "pathLength"
+        }
+      ],
+      "field_order": [
+        "errorId",
+        "errorName",
+        "pathLength"
+      ],
+      "transform_name": "ERROR PATH",
+      "data_set_name": "neo4j-error-path-7662"
+    }
+  ],
+  "input_data_sets": [],
+  "name": "neo4j-error-path-7662-validation",
+  "description": "Verifies the error path lookup of the Neo4j perspective runs 
on Neo4j 5 and finds the deepest error, see issue #7662",
+  "trans_test_tweaks": [],
+  "persist_filename": "",
+  "pipeline_filename": 
"./tests/neo4j-error-path-7662/neo4j-error-path-7662-validation.hpl",
+  "test_type": "UNIT_TEST"
+}
diff --git 
a/integration-tests/neo4j/tests/neo4j-error-path-7662/main-neo4j-error-path-7662.hwf
 
b/integration-tests/neo4j/tests/neo4j-error-path-7662/main-neo4j-error-path-7662.hwf
new file mode 100644
index 0000000000..582b680bb8
--- /dev/null
+++ 
b/integration-tests/neo4j/tests/neo4j-error-path-7662/main-neo4j-error-path-7662.hwf
@@ -0,0 +1,185 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+
+-->
+<workflow>
+  <name>main-neo4j-error-path-7662</name>
+  <name_sync_with_filename>Y</name_sync_with_filename>
+  <description>Verifies the error path lookup of the Neo4j perspective against 
a live Neo4j server, see issue #7662.</description>
+  <extended_description>The Error path tab of the Neo4j perspective looks for 
the deepest executions which
+reported errors. Until issue #7662 that condition was written as 
size((err)-[:EXECUTES]->())=0,
+which Neo4j 5 rejects because size() on a pattern expression was removed.
+
+This test builds a small Execution graph, the same shape the Neo4j execution 
information
+location writes, and runs the lookup over it. It fails when the statement is 
rejected by the
+server and when the wrong execution is reported as the deepest 
error.</extended_description>
+  <created_user>-</created_user>
+  <modified_user>-</modified_user>
+  <created_date>2026/07/29 10:00:00.000</created_date>
+  <modified_date>2026/07/29 10:00:00.000</modified_date>
+  <workflow_version/>
+  <parameters/>
+  <actions>
+    <action>
+      <repeat>N</repeat>
+      <schedulerType>0</schedulerType>
+      <intervalSeconds>0</intervalSeconds>
+      <intervalMinutes>60</intervalMinutes>
+      <DayOfMonth>1</DayOfMonth>
+      <weekDay>1</weekDay>
+      <minutes>0</minutes>
+      <hour>12</hour>
+      <doNotWaitOnFirstExecution>N</doNotWaitOnFirstExecution>
+      <name>Start</name>
+      <description/>
+      <type>SPECIAL</type>
+      <attributes/>
+      <xloc>96</xloc>
+      <yloc>128</yloc>
+      <parallel>N</parallel>
+      <attributes_hac/>
+    </action>
+    <action>
+      <connections>
+        <connection>Test</connection>
+      </connections>
+      <name>Check Neo4j Connections</name>
+      <description/>
+      <type>NEO4J_CHECK_CONNECTIONS</type>
+      <attributes/>
+      <xloc>224</xloc>
+      <yloc>128</yloc>
+      <parallel>N</parallel>
+      <attributes_hac/>
+    </action>
+    <action>
+      <filename>${PROJECT_HOME}/tests/shared/cleanup-database.hwf</filename>
+      <params_from_previous>N</params_from_previous>
+      <exec_per_row>N</exec_per_row>
+      <set_logfile>N</set_logfile>
+      <logfile/>
+      <logext/>
+      <add_date>N</add_date>
+      <add_time>N</add_time>
+      <loglevel>Nothing</loglevel>
+      <set_append_logfile>N</set_append_logfile>
+      <create_parent_folder>N</create_parent_folder>
+      <wait_until_finished>Y</wait_until_finished>
+      <parameters>
+        <pass_all_parameters>Y</pass_all_parameters>
+      </parameters>
+      <run_configuration>local</run_configuration>
+      <name>cleanup-database.hwf</name>
+      <description/>
+      <type>WORKFLOW</type>
+      <attributes/>
+      <xloc>384</xloc>
+      <yloc>128</yloc>
+      <parallel>N</parallel>
+      <attributes_hac/>
+    </action>
+    <action>
+      <connection>Test</connection>
+      <script>CREATE (workflow:Execution { id : 'it-7662-workflow', name : 
'main-7662', type : 'WORKFLOW', registrationDate : '2026-07-29T10:00:00', 
errors : 1 })
+CREATE (pipeline:Execution { id : 'it-7662-pipeline', name : 
'load-customers-7662', type : 'PIPELINE', registrationDate : 
'2026-07-29T10:00:01', errors : 1 })
+CREATE (failed:Execution { id : 'it-7662-failed', name : 'Table output', type 
: 'TRANSFORM', registrationDate : '2026-07-29T10:00:02', errors : 1 })
+CREATE (ok:Execution { id : 'it-7662-ok', name : 'Dummy', type : 'TRANSFORM', 
registrationDate : '2026-07-29T10:00:02', errors : 0 })
+CREATE (workflow)-[:EXECUTES]->(pipeline)
+CREATE (pipeline)-[:EXECUTES]->(failed)
+CREATE (pipeline)-[:EXECUTES]->(ok)
+;</script>
+      <replace_variables>N</replace_variables>
+      <name>Create execution graph</name>
+      <description>Builds the Execution graph of a workflow which executed a 
pipeline with a failing transform</description>
+      <type>NEO4J_CYPHER_SCRIPT</type>
+      <attributes/>
+      <xloc>544</xloc>
+      <yloc>128</yloc>
+      <parallel>N</parallel>
+      <attributes_hac/>
+    </action>
+    <action>
+      <test_names>
+        <test_name>
+          <name>neo4j-error-path-7662-validation</name>
+        </test_name>
+      </test_names>
+      <name>Run neo4j-error-path-7662 Tests</name>
+      <description/>
+      <type>RunPipelineTests</type>
+      <attributes/>
+      <xloc>736</xloc>
+      <yloc>128</yloc>
+      <parallel>N</parallel>
+      <attributes_hac/>
+    </action>
+    <action>
+      <connection>Test</connection>
+      <script>MATCH (n:Execution) WHERE n.id STARTS WITH 'it-7662-' DETACH 
DELETE n
+;</script>
+      <replace_variables>N</replace_variables>
+      <name>Remove execution graph</name>
+      <description>Removes the executions this test created so it doesn't 
leave anything behind in the graph</description>
+      <type>NEO4J_CYPHER_SCRIPT</type>
+      <attributes/>
+      <xloc>928</xloc>
+      <yloc>128</yloc>
+      <parallel>N</parallel>
+      <attributes_hac/>
+    </action>
+  </actions>
+  <hops>
+    <hop>
+      <from>Start</from>
+      <to>Check Neo4j Connections</to>
+      <evaluation>Y</evaluation>
+      <unconditional>Y</unconditional>
+      <enabled>Y</enabled>
+    </hop>
+    <hop>
+      <from>Check Neo4j Connections</from>
+      <to>cleanup-database.hwf</to>
+      <evaluation>Y</evaluation>
+      <unconditional>N</unconditional>
+      <enabled>Y</enabled>
+    </hop>
+    <hop>
+      <from>cleanup-database.hwf</from>
+      <to>Create execution graph</to>
+      <evaluation>Y</evaluation>
+      <unconditional>N</unconditional>
+      <enabled>Y</enabled>
+    </hop>
+    <hop>
+      <from>Create execution graph</from>
+      <to>Run neo4j-error-path-7662 Tests</to>
+      <evaluation>Y</evaluation>
+      <unconditional>N</unconditional>
+      <enabled>Y</enabled>
+    </hop>
+    <hop>
+      <from>Run neo4j-error-path-7662 Tests</from>
+      <to>Remove execution graph</to>
+      <evaluation>Y</evaluation>
+      <unconditional>N</unconditional>
+      <enabled>Y</enabled>
+    </hop>
+  </hops>
+  <notepads/>
+  <attributes/>
+</workflow>
diff --git 
a/integration-tests/neo4j/tests/neo4j-error-path-7662/neo4j-error-path-7662-validation.hpl
 
b/integration-tests/neo4j/tests/neo4j-error-path-7662/neo4j-error-path-7662-validation.hpl
new file mode 100644
index 0000000000..653c918c00
--- /dev/null
+++ 
b/integration-tests/neo4j/tests/neo4j-error-path-7662/neo4j-error-path-7662-validation.hpl
@@ -0,0 +1,225 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+
+-->
+<pipeline>
+  <info>
+    <name>neo4j-error-path-7662-validation</name>
+    <name_sync_with_filename>Y</name_sync_with_filename>
+    <description>Runs the error path lookup of the Neo4j perspective, see 
issue #7662</description>
+    <extended_description>The WHERE clause is the one from 
HopNeo4jPerspective.getErrorPathCypher(),
+including the "AND NOT (err)-[:EXECUTES]-&gt;()" predicate which replaced the 
size() on a pattern
+expression that Neo4j 5 rejects. See issue #7662.
+
+Two things differ from the statement in the perspective, neither touches the 
predicate under test:
+the RETURN is projected onto scalar fields so the result can be compared 
against a golden data set,
+and the variable length patterns are bounded to 20 hops. Without that bound 
Neo4j returns an
+UnboundedVariableLengthPattern notification, and the Cypher transform reports 
every notification
+below WARNING severity as a transform error.</extended_description>
+    <pipeline_version/>
+    <pipeline_type>Normal</pipeline_type>
+    <parameters>
+    </parameters>
+    <capture_transform_performance>N</capture_transform_performance>
+    
<transform_performance_capturing_delay>1000</transform_performance_capturing_delay>
+    
<transform_performance_capturing_size_limit>100</transform_performance_capturing_size_limit>
+    <created_user>-</created_user>
+    <created_date>2026/07/29 10:00:00.000</created_date>
+    <modified_user>-</modified_user>
+    <modified_date>2026/07/29 10:00:00.000</modified_date>
+  </info>
+  <notepads>
+  </notepads>
+  <order>
+    <hop>
+      <from>Subject execution</from>
+      <to>Look up error path</to>
+      <enabled>Y</enabled>
+    </hop>
+    <hop>
+      <from>Look up error path</from>
+      <to>ERROR PATH</to>
+      <enabled>Y</enabled>
+    </hop>
+  </order>
+  <transform>
+    <name>Subject execution</name>
+    <type>RowGenerator</type>
+    <description>The execution the Neo4j perspective was opened 
on</description>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <fields>
+      <field>
+        <name>subjectName</name>
+        <type>String</type>
+        <format/>
+        <currency/>
+        <decimal/>
+        <group/>
+        <nullif>main-7662</nullif>
+        <length>-1</length>
+        <precision>-1</precision>
+        <set_empty_string>N</set_empty_string>
+      </field>
+      <field>
+        <name>subjectType</name>
+        <type>String</type>
+        <format/>
+        <currency/>
+        <decimal/>
+        <group/>
+        <nullif>WORKFLOW</nullif>
+        <length>-1</length>
+        <precision>-1</precision>
+        <set_empty_string>N</set_empty_string>
+      </field>
+      <field>
+        <name>subjectId</name>
+        <type>String</type>
+        <format/>
+        <currency/>
+        <decimal/>
+        <group/>
+        <nullif>it-7662-workflow</nullif>
+        <length>-1</length>
+        <precision>-1</precision>
+        <set_empty_string>N</set_empty_string>
+      </field>
+    </fields>
+    <limit>1</limit>
+    <never_ending>N</never_ending>
+    <interval_in_ms>5000</interval_in_ms>
+    <row_time_field>now</row_time_field>
+    <last_time_field>FiveSecondsAgo</last_time_field>
+    <attributes/>
+    <GUI>
+      <xloc>144</xloc>
+      <yloc>96</yloc>
+    </GUI>
+  </transform>
+  <transform>
+    <name>Look up error path</name>
+    <type>Neo4jCypherOutput</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <connection>Test</connection>
+    <cypher>MATCH(top:Execution { name : $subjectName, type : $subjectType, id 
: $subjectId })-[rel:EXECUTES*1..20]-(err:Execution)
+   , p=shortestpath((top)-[:EXECUTES*1..20]-(err))
+WHERE top.registrationDate IS NOT NULL
+  AND err.errors &gt; 0
+  AND NOT (err)-[:EXECUTES]-&gt;()
+RETURN err.id AS errorId, err.name AS errorName, size(RELATIONSHIPS(p)) AS 
pathLength
+ORDER BY size(RELATIONSHIPS(p)) DESC
+LIMIT 10</cypher>
+    <batch_size>1</batch_size>
+    <read_only>Y</read_only>
+    <nr_retries_on_error>5</nr_retries_on_error>
+    <retry>Y</retry>
+    <cypher_from_field>N</cypher_from_field>
+    <cypher_field/>
+    <unwind>N</unwind>
+    <unwind_map/>
+    <returning_graph>N</returning_graph>
+    <return_graph_field/>
+    <mappings>
+      <mapping>
+        <parameter>subjectName</parameter>
+        <field>subjectName</field>
+        <type>String</type>
+      </mapping>
+      <mapping>
+        <parameter>subjectType</parameter>
+        <field>subjectType</field>
+        <type>String</type>
+      </mapping>
+      <mapping>
+        <parameter>subjectId</parameter>
+        <field>subjectId</field>
+        <type>String</type>
+      </mapping>
+    </mappings>
+    <returns>
+      <return>
+        <name>errorId</name>
+        <type>String</type>
+        <source_type>String</source_type>
+      </return>
+      <return>
+        <name>errorName</name>
+        <type>String</type>
+        <source_type>String</source_type>
+      </return>
+      <return>
+        <name>pathLength</name>
+        <type>Integer</type>
+        <source_type>Integer</source_type>
+      </return>
+    </returns>
+    <attributes/>
+    <GUI>
+      <xloc>352</xloc>
+      <yloc>96</yloc>
+    </GUI>
+  </transform>
+  <transform>
+    <name>ERROR PATH</name>
+    <type>SelectValues</type>
+    <description>Drops the lookup parameters so only the result of the error 
path is compared</description>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <fields>
+      <field>
+        <name>errorId</name>
+        <rename/>
+      </field>
+      <field>
+        <name>errorName</name>
+        <rename/>
+      </field>
+      <field>
+        <name>pathLength</name>
+        <rename/>
+      </field>
+      <select_unspecified>N</select_unspecified>
+    </fields>
+    <attributes/>
+    <GUI>
+      <xloc>560</xloc>
+      <yloc>96</yloc>
+    </GUI>
+  </transform>
+  <transform_error_handling>
+  </transform_error_handling>
+  <attributes/>
+</pipeline>
diff --git a/plugins/tech/neo4j/pom.xml b/plugins/tech/neo4j/pom.xml
index 774bcb0a77..bee4fe33f6 100755
--- a/plugins/tech/neo4j/pom.xml
+++ b/plugins/tech/neo4j/pom.xml
@@ -30,6 +30,7 @@
 
     <properties>
         <neo4j.driver.version>5.15.0</neo4j.driver.version>
+        <testcontainers.version>1.21.4</testcontainers.version>
     </properties>
 
     <dependencyManagement>
@@ -66,6 +67,38 @@
             <artifactId>reactive-streams</artifactId>
             <scope>provided</scope>
         </dependency>
+        <dependency>
+            <groupId>org.testcontainers</groupId>
+            <artifactId>junit-jupiter</artifactId>
+            <version>${testcontainers.version}</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.testcontainers</groupId>
+            <artifactId>neo4j</artifactId>
+            <version>${testcontainers.version}</version>
+            <scope>test</scope>
+        </dependency>
     </dependencies>
 
+    <build>
+        <plugins>
+            <!-- The *IT tests start a Neo4j server in a container. They are 
skipped automatically
+                 when no Docker daemon is available, so a build without Docker 
is unaffected.
+                 Use -DskipITs to skip them explicitly. -->
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-failsafe-plugin</artifactId>
+                <executions>
+                    <execution>
+                        <goals>
+                            <goal>integration-test</goal>
+                            <goal>verify</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+        </plugins>
+    </build>
+
 </project>
diff --git 
a/plugins/tech/neo4j/src/main/java/org/apache/hop/neo4j/execution/path/PathResult.java
 
b/plugins/tech/neo4j/src/main/java/org/apache/hop/neo4j/execution/path/PathResult.java
index 8cdb91ed1d..130a6f3b34 100644
--- 
a/plugins/tech/neo4j/src/main/java/org/apache/hop/neo4j/execution/path/PathResult.java
+++ 
b/plugins/tech/neo4j/src/main/java/org/apache/hop/neo4j/execution/path/PathResult.java
@@ -229,7 +229,10 @@ public class PathResult {
     cmd.append("   , p=shortestpath((top)-[:EXECUTES*]-(err)) 
").append(Const.CR);
     cmd.append("WHERE top.registrationDate IS NOT NULL ").append(Const.CR);
     cmd.append("  AND err.errors > 0 ").append(Const.CR);
-    cmd.append("  AND size((err)-[:EXECUTES]->())=0 ").append(Const.CR);
+    // Only keep the leaf executions: Neo4j 5 no longer supports size() on a 
pattern expression,
+    // a pattern predicate is the equivalent and works on Neo4j 4 and 5 alike.
+    //
+    cmd.append("  AND NOT (err)-[:EXECUTES]->() ").append(Const.CR);
 
     // Now link the metadata...
     //
diff --git 
a/plugins/tech/neo4j/src/main/java/org/apache/hop/neo4j/logging/xp/WorkflowLoggingExtensionPoint.java
 
b/plugins/tech/neo4j/src/main/java/org/apache/hop/neo4j/logging/xp/WorkflowLoggingExtensionPoint.java
index 816892901e..4d9b36e225 100644
--- 
a/plugins/tech/neo4j/src/main/java/org/apache/hop/neo4j/logging/xp/WorkflowLoggingExtensionPoint.java
+++ 
b/plugins/tech/neo4j/src/main/java/org/apache/hop/neo4j/logging/xp/WorkflowLoggingExtensionPoint.java
@@ -367,6 +367,10 @@ public class WorkflowLoggingExtensionPoint
                             + ", e.loggingText = $loggingText "
                             + ", e.comment = $comment "
                             + ", e.reason = $reason "
+                            // Without the error count a failed action never 
shows up in the
+                            // error path of the Neo4j perspective. See issue 
#7662.
+                            //
+                            + ", e.errors = $errors "
                             + ", e.linesRead = $linesRead "
                             + ", e.linesWritten = $linesWritten "
                             + ", e.linesInput = $linesInput "
diff --git 
a/plugins/tech/neo4j/src/main/java/org/apache/hop/neo4j/perspective/HistoryResult.java
 
b/plugins/tech/neo4j/src/main/java/org/apache/hop/neo4j/perspective/HistoryResult.java
index d819a20a65..1a409f2355 100644
--- 
a/plugins/tech/neo4j/src/main/java/org/apache/hop/neo4j/perspective/HistoryResult.java
+++ 
b/plugins/tech/neo4j/src/main/java/org/apache/hop/neo4j/perspective/HistoryResult.java
@@ -334,7 +334,10 @@ public class HistoryResult {
     cmd.append("   , p=shortestpath((top)-[:EXECUTES*]-(err)) 
").append(Const.CR);
     cmd.append("WHERE top.registrationDate IS NOT NULL ").append(Const.CR);
     cmd.append("  AND err.errors > 0 ").append(Const.CR);
-    cmd.append("  AND size((err)-[:EXECUTES]->())=0 ").append(Const.CR);
+    // Only keep the leaf executions: Neo4j 5 no longer supports size() on a 
pattern expression,
+    // a pattern predicate is the equivalent and works on Neo4j 4 and 5 alike.
+    //
+    cmd.append("  AND NOT (err)-[:EXECUTES]->() ").append(Const.CR);
 
     // Now link the metadata...
     //
diff --git 
a/plugins/tech/neo4j/src/main/java/org/apache/hop/neo4j/perspective/HopNeo4jPerspective.java
 
b/plugins/tech/neo4j/src/main/java/org/apache/hop/neo4j/perspective/HopNeo4jPerspective.java
index 0bb7dc96ad..fe7b76d702 100644
--- 
a/plugins/tech/neo4j/src/main/java/org/apache/hop/neo4j/perspective/HopNeo4jPerspective.java
+++ 
b/plugins/tech/neo4j/src/main/java/org/apache/hop/neo4j/perspective/HopNeo4jPerspective.java
@@ -70,6 +70,7 @@ import org.eclipse.swt.widgets.Composite;
 import org.eclipse.swt.widgets.Control;
 import org.eclipse.swt.widgets.Event;
 import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.MessageBox;
 import org.eclipse.swt.widgets.TableItem;
 import org.eclipse.swt.widgets.Text;
 import org.eclipse.swt.widgets.Tree;
@@ -526,9 +527,14 @@ public class HopNeo4jPerspective implements 
IHopPerspective {
           Result result = tx.run(loggingCypher.toString(), loggingParameters);
           while (result.hasNext()) {
             Record record = result.next();
-            Value value = record.get(0);
-            String loggingText = value.asString();
-            wLogging.setText(Const.NVL(loggingText, "<no logging found>"));
+            // Not via Value.asString(): that returns the literal text "null" 
for a missing
+            // property, which then slips past the Const.NVL() below.
+            //
+            String loggingText = LoggingCore.getStringValue(record, 0);
+            wLogging.setText(
+                Const.NVL(
+                    loggingText,
+                    BaseMessages.getString(PKG, 
"Neo4jPerspectiveDialog.NoLoggingFound.Message")));
           }
           return null;
         });
@@ -561,20 +567,11 @@ public class HopNeo4jPerspective implements 
IHopPerspective {
       errorPathParams.put(CONST_SUBJECT_TYPE, type);
       errorPathParams.put(CONST_SUBJECT_ID, id);
 
-      StringBuilder errorPathCypher = new StringBuilder();
-      errorPathCypher.append(
-          "MATCH(top:Execution { name : $subjectName, type : $subjectType, id 
: $subjectId })-[rel:EXECUTES*]-(err:Execution) ");
-      errorPathCypher.append("   , p=shortestpath((top)-[:EXECUTES*]-(err)) ");
-      errorPathCypher.append("WHERE top.registrationDate IS NOT NULL ");
-      errorPathCypher.append("  AND err.errors > 0 ");
-      errorPathCypher.append("  AND size((err)-[:EXECUTES]->())=0 ");
-      errorPathCypher.append("RETURN p ");
-      errorPathCypher.append("ORDER BY size(RELATIONSHIPS(p)) DESC ");
-      errorPathCypher.append("LIMIT 10");
+      String errorPathCypher = getErrorPathCypher();
 
       session.executeRead(
           tx -> {
-            Result pathResult = tx.run(errorPathCypher.toString(), 
errorPathParams);
+            Result pathResult = tx.run(errorPathCypher, errorPathParams);
 
             while (pathResult.hasNext()) {
               Record pathRecord = pathResult.next();
@@ -645,6 +642,28 @@ public class HopNeo4jPerspective implements 
IHopPerspective {
     return shortestPaths;
   }
 
+  /**
+   * The Cypher which looks up the shortest paths from an execution to the 
executions which reported
+   * errors.
+   *
+   * <p>An error node only qualifies when it is a leaf: it did not execute 
anything itself. That
+   * condition used to be expressed as 
<code>size((err)-[:EXECUTES]-&gt;())=0</code>, but Neo4j 5
+   * removed <code>size()</code> on a pattern expression. A pattern predicate 
is the equivalent and
+   * is supported by both Neo4j 4 and 5.
+   *
+   * @return the parameterized Cypher statement
+   */
+  static String getErrorPathCypher() {
+    return "MATCH(top:Execution { name : $subjectName, type : $subjectType, id 
: $subjectId })-[rel:EXECUTES*]-(err:Execution) "
+        + "   , p=shortestpath((top)-[:EXECUTES*]-(err)) "
+        + "WHERE top.registrationDate IS NOT NULL "
+        + "  AND err.errors > 0 "
+        + "  AND NOT (err)-[:EXECUTES]->() "
+        + "RETURN p "
+        + "ORDER BY size(RELATIONSHIPS(p)) DESC "
+        + "LIMIT 10";
+  }
+
   private void analyzeCypherStatements(
       NeoConnection connection,
       Session session,
@@ -810,31 +829,28 @@ public class HopNeo4jPerspective implements 
IHopPerspective {
                 while (result.hasNext()) {
                   Record record = result.next();
                   TableItem item = new TableItem(wResults.table, SWT.NONE);
-                  int pos = 0;
-                  Value vId = record.get(pos++);
-                  item.setText(pos, Const.NVL(vId.asString(), ""));
-                  Value vName = record.get(pos++);
-                  item.setText(pos, Const.NVL(vName.asString(), ""));
-                  Value vType = record.get(pos++);
-                  item.setText(pos, Const.NVL(vType.asString(), ""));
-                  Value vLinesRead = record.get(pos++);
-                  item.setText(pos, Long.toString(vLinesRead.asLong(0)));
-                  Value vLinesWritten = record.get(pos++);
-                  item.setText(pos, Long.toString(vLinesWritten.asLong(0)));
-                  Value vLinesInput = record.get(pos++);
-                  item.setText(pos, Long.toString(vLinesInput.asLong(0)));
-                  Value vLinesOutput = record.get(pos++);
-                  item.setText(pos, Long.toString(vLinesOutput.asLong(0)));
-                  Value vLinesRejected = record.get(pos++);
-                  item.setText(pos, Long.toString(vLinesRejected.asLong(0)));
-                  Value vErrors = record.get(pos++);
-                  long errors = vErrors.asLong(0);
-                  item.setText(pos, Long.toString(vErrors.asLong(0)));
-                  Value vExecutionStart = record.get(pos++);
-                  item.setText(pos, Const.NVL(vExecutionStart.asString(), 
"").replace("T", " "));
-                  Value vDurationMs = record.get(pos++);
-                  String durationHMS = 
LoggingCore.getFancyDurationFromMs(vDurationMs.asLong(0));
-                  item.setText(pos, durationHMS);
+
+                  // Column 0 of the table holds the row number, the values 
start at column 1.
+                  // The strings are read with LoggingCore and not with 
Value.asString(): the
+                  // latter returns the literal text "null" for a property 
which isn't set,
+                  // which then slips past Const.NVL().
+                  //
+                  int column = 1;
+                  item.setText(column++, 
Const.NVL(LoggingCore.getStringValue(record, 0), ""));
+                  item.setText(column++, 
Const.NVL(LoggingCore.getStringValue(record, 1), ""));
+                  item.setText(column++, 
Const.NVL(LoggingCore.getStringValue(record, 2), ""));
+                  item.setText(column++, 
Long.toString(record.get(3).asLong(0)));
+                  item.setText(column++, 
Long.toString(record.get(4).asLong(0)));
+                  item.setText(column++, 
Long.toString(record.get(5).asLong(0)));
+                  item.setText(column++, 
Long.toString(record.get(6).asLong(0)));
+                  item.setText(column++, 
Long.toString(record.get(7).asLong(0)));
+                  long errors = record.get(8).asLong(0);
+                  item.setText(column++, Long.toString(errors));
+                  item.setText(
+                      column++,
+                      Const.NVL(LoggingCore.getStringValue(record, 9), 
"").replace("T", " "));
+                  item.setText(
+                      column, 
LoggingCore.getFancyDurationFromMs(record.get(10).asLong(0)));
 
                   if (errors != 0) {
                     item.setBackground(errorLineBackground);
@@ -914,20 +930,39 @@ public class HopNeo4jPerspective implements 
IHopPerspective {
       try (Session session =
           connection.getSession(hopGui.getLog(), driver, 
hopGui.getVariables())) {
 
+        boolean opened;
         if ("PIPELINE".equals(type)) {
-          openPipelineOrWorkflow(session, name, type, id, "Pipeline", 
"EXECUTION_OF_PIPELINE");
+          opened =
+              openPipelineOrWorkflow(session, name, type, id, "Pipeline", 
"EXECUTION_OF_PIPELINE");
         } else if ("WORKFLOW".equals(type)) {
-          openPipelineOrWorkflow(session, name, type, id, "Workflow", 
"EXECUTION_OF_WORKFLOW");
+          opened =
+              openPipelineOrWorkflow(session, name, type, id, "Workflow", 
"EXECUTION_OF_WORKFLOW");
         } else if ("TRANSFORM".equals(type)) {
-          openTransform(session, name, type, id);
+          opened = openTransform(session, name, type, id);
         } else if ("ACTION".equals(type)) {
-          openAction(session, name, type, id);
+          opened = openAction(session, name, type, id);
+        } else {
+          opened = false;
+        }
+
+        if (!opened) {
+          // The execution is in the graph but the file behind it isn't. That 
happens when the
+          // execution was logged by an execution information location: only 
the Neo4j logging
+          // in NEO4J_LOGGING_CONNECTION stores the metadata this needs. Say 
so instead of
+          // leaving the button without any effect.
+          //
+          MessageBox box = new MessageBox(hopGui.getShell(), 
SWT.ICON_INFORMATION | SWT.OK);
+          box.setText(BaseMessages.getString(PKG, 
"Neo4jPerspectiveDialog.NoFileFound.Header"));
+          box.setMessage(
+              BaseMessages.getString(
+                  PKG, "Neo4jPerspectiveDialog.NoFileFound.Message", name, 
type));
+          box.open();
         }
       }
     }
   }
 
-  private void openTransform(Session session, String name, String type, String 
id) {
+  private boolean openTransform(Session session, String name, String type, 
String id) {
 
     LogChannel.UI.logDetailed("Open transform : " + id + ", name : " + name + 
", type: " + type);
 
@@ -962,32 +997,31 @@ public class HopNeo4jPerspective implements 
IHopPerspective {
             });
 
     if (names == null) {
-      return;
+      return false;
     }
 
     String filename = names[0];
     String transformName = names[1];
 
     if (StringUtils.isEmpty(filename)) {
-      return;
+      return false;
     }
 
     try {
       hopGui.fileDelegate.fileOpen(filename);
 
       if (StringUtils.isEmpty(transformName)) {
-        return;
+        return true;
       }
 
       IHopFileTypeHandler typeHandler = hopGui.getActiveFileTypeHandler();
-      if (typeHandler == null || !(typeHandler instanceof 
HopGuiPipelineGraph)) {
-        return;
+      if (!(typeHandler instanceof HopGuiPipelineGraph graph)) {
+        return true;
       }
-      HopGuiPipelineGraph graph = (HopGuiPipelineGraph) typeHandler;
       PipelineMeta pipelineMeta = graph.getPipelineMeta();
       TransformMeta transformMeta = pipelineMeta.findTransform(transformName);
       if (transformMeta == null) {
-        return;
+        return true;
       }
       pipelineMeta.unselectAll();
       transformMeta.setSelected(true);
@@ -999,9 +1033,10 @@ public class HopNeo4jPerspective implements 
IHopPerspective {
           BaseMessages.getString(PKG, 
"Neo4jPerspectiveDialog.OpeningTransform.Dialog.Message"),
           e);
     }
+    return true;
   }
 
-  private void openAction(Session session, String name, String type, String 
id) {
+  private boolean openAction(Session session, String name, String type, String 
id) {
 
     LogChannel.UI.logDetailed("Open action : " + id + ", name : " + name + ", 
type: " + type);
 
@@ -1034,24 +1069,23 @@ public class HopNeo4jPerspective implements 
IHopPerspective {
               };
             });
     if (names == null) {
-      return;
+      return false;
     }
 
     String filename = names[0];
     String actionName = names[1];
 
     if (StringUtils.isEmpty(filename)) {
-      return;
+      return false;
     }
 
     try {
       hopGui.fileDelegate.fileOpen(filename);
       if (StringUtils.isNotEmpty(actionName)) {
         IHopFileTypeHandler typeHandler = 
HopGui.getInstance().getActiveFileTypeHandler();
-        if (typeHandler == null || !(typeHandler instanceof 
HopGuiWorkflowGraph)) {
-          return;
+        if (!(typeHandler instanceof HopGuiWorkflowGraph graph)) {
+          return true;
         }
-        HopGuiWorkflowGraph graph = (HopGuiWorkflowGraph) typeHandler;
         WorkflowMeta workflowMeta = graph.getWorkflowMeta();
         ActionMeta actionMeta = workflowMeta.findAction(actionName);
         if (actionMeta != null) {
@@ -1067,9 +1101,10 @@ public class HopNeo4jPerspective implements 
IHopPerspective {
           BaseMessages.getString(PKG, 
"Neo4jPerspectiveDialog.OpeningAction.Dialog.Message"),
           e);
     }
+    return true;
   }
 
-  private void openPipelineOrWorkflow(
+  private boolean openPipelineOrWorkflow(
       Session session, String name, String type, String id, String nodeLabel, 
String relationship) {
     Map<String, Object> params = new HashMap<>();
     params.put(CONST_SUBJECT_NAME, name);
@@ -1098,18 +1133,20 @@ public class HopNeo4jPerspective implements 
IHopPerspective {
               return LoggingCore.getStringValue(record, 0);
             });
 
-    if (StringUtils.isNotEmpty(filename)) {
-      try {
-        hopGui.fileDelegate.fileOpen(filename);
-      } catch (Exception e) {
-        new ErrorDialog(
-            hopGui.getShell(),
-            BaseMessages.getString(PKG, 
"Neo4jPerspectiveDialog.OpeningFile.Dialog.Header"),
-            BaseMessages.getString(
-                PKG, "Neo4jPerspectiveDialog.OpeningFile.Dialog.Message", 
filename),
-            e);
-      }
+    if (StringUtils.isEmpty(filename)) {
+      return false;
+    }
+    try {
+      hopGui.fileDelegate.fileOpen(filename);
+    } catch (Exception e) {
+      new ErrorDialog(
+          hopGui.getShell(),
+          BaseMessages.getString(PKG, 
"Neo4jPerspectiveDialog.OpeningFile.Dialog.Header"),
+          BaseMessages.getString(
+              PKG, "Neo4jPerspectiveDialog.OpeningFile.Dialog.Message", 
filename),
+          e);
     }
+    return true;
   }
 
   /**
diff --git 
a/plugins/tech/neo4j/src/main/resources/org/apache/hop/neo4j/perspective/messages/messages_en_US.properties
 
b/plugins/tech/neo4j/src/main/resources/org/apache/hop/neo4j/perspective/messages/messages_en_US.properties
index 7d7305b5ce..3b7ec85451 100644
--- 
a/plugins/tech/neo4j/src/main/resources/org/apache/hop/neo4j/perspective/messages/messages_en_US.properties
+++ 
b/plugins/tech/neo4j/src/main/resources/org/apache/hop/neo4j/perspective/messages/messages_en_US.properties
@@ -38,6 +38,9 @@ Neo4jPerspectiveDialog.ErrorSearching.Dialog.Message=Error 
searching
 Neo4jPerspectiveDialog.Lineage.Tab=Execution lineage 
 Neo4jPerspectiveDialog.Logging.Tab=Logging 
 Neo4jPerspectiveDialog.Neo4J.Logging.Path.Label=NEO4J_LOGGING_CONNECTION is 
pointing to 
+Neo4jPerspectiveDialog.NoFileFound.Header=Nothing to open
+Neo4jPerspectiveDialog.NoFileFound.Message=No file was found in the graph for 
''{0}'' ({1}).\n\nThis perspective opens files through the metadata which the 
Neo4j logging in NEO4J_LOGGING_CONNECTION writes. Executions logged by a Neo4j 
execution information location don''t have that metadata.
+Neo4jPerspectiveDialog.NoLoggingFound.Message=<no logging found>
 Neo4jPerspectiveDialog.OnlyRoot.Label=Show only root executions 
 Neo4jPerspectiveDialog.Open.Button=Open  
 Neo4jPerspectiveDialog.OpeningAction.Dialog.Header=Error
diff --git 
a/plugins/tech/neo4j/src/test/java/org/apache/hop/neo4j/CypherAssertions.java 
b/plugins/tech/neo4j/src/test/java/org/apache/hop/neo4j/CypherAssertions.java
new file mode 100644
index 0000000000..63a4e149ef
--- /dev/null
+++ 
b/plugins/tech/neo4j/src/test/java/org/apache/hop/neo4j/CypherAssertions.java
@@ -0,0 +1,51 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.neo4j;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+
+import java.util.regex.Pattern;
+
+/** Assertions shared by the tests which verify generated Cypher statements. */
+public class CypherAssertions {
+
+  /**
+   * Matches size() applied to a pattern expression, for example {@code
+   * size((err)-[:EXECUTES]->())}. Neo4j 5 removed that in favour of {@code 
COUNT {}} and pattern
+   * predicates. size() applied to a list, for example {@code 
size(RELATIONSHIPS(p))}, is still
+   * valid and is deliberately not matched here.
+   */
+  private static final Pattern SIZE_OF_PATTERN_EXPRESSION = 
Pattern.compile("size\\s*\\(\\s*\\(");
+
+  private CypherAssertions() {
+    // Static utility class
+  }
+
+  /**
+   * Asserts that a Cypher statement does not use size() on a pattern 
expression, which fails on
+   * Neo4j 5 and later.
+   *
+   * @param cypher the Cypher statement to verify
+   */
+  public static void assertNoSizeOfPatternExpression(String cypher) {
+    assertFalse(
+        SIZE_OF_PATTERN_EXPRESSION.matcher(cypher).find(),
+        "Neo4j 5 removed size() on a pattern expression, use a pattern 
predicate instead: "
+            + cypher);
+  }
+}
diff --git 
a/plugins/tech/neo4j/src/test/java/org/apache/hop/neo4j/execution/path/PathResultTest.java
 
b/plugins/tech/neo4j/src/test/java/org/apache/hop/neo4j/execution/path/PathResultTest.java
new file mode 100644
index 0000000000..ef2bb88446
--- /dev/null
+++ 
b/plugins/tech/neo4j/src/test/java/org/apache/hop/neo4j/execution/path/PathResultTest.java
@@ -0,0 +1,84 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.neo4j.execution.path;
+
+import static 
org.apache.hop.neo4j.CypherAssertions.assertNoSizeOfPatternExpression;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.List;
+import org.junit.jupiter.api.Test;
+
+/**
+ * The Cypher generated here is not executed by Hop, it is presented in the 
execution information
+ * viewer for the user to run in the Neo4j browser. It still has to be valid 
Cypher.
+ */
+class PathResultTest {
+
+  private PathResult createWorkflowWithErrorPath() {
+    PathResult pipeline = new PathResult();
+    pipeline.setId("pipeline-id");
+    pipeline.setName("load-customers");
+    pipeline.setType("PIPELINE");
+
+    PathResult transform = new PathResult();
+    transform.setId("transform-id");
+    transform.setName("Table output");
+    transform.setType("TRANSFORM");
+
+    PathResult workflow = new PathResult();
+    workflow.setId("workflow-id");
+    workflow.setName("main");
+    workflow.setType("WORKFLOW");
+    workflow.setShortestPaths(List.of(List.of(pipeline, transform)));
+
+    return workflow;
+  }
+
+  @Test
+  void errorPathWithMetadataCommandDoesNotUseSizeOnAPatternExpression() {
+    assertNoSizeOfPatternExpression(
+        createWorkflowWithErrorPath().getErrorPathWithMetadataCommand(0));
+  }
+
+  @Test
+  void errorPathWithMetadataCommandOnlyKeepsLeafErrors() {
+    assertTrue(
+        createWorkflowWithErrorPath()
+            .getErrorPathWithMetadataCommand(0)
+            .contains("AND NOT (err)-[:EXECUTES]->()"),
+        "The error node has to be a leaf, expressed as a pattern predicate");
+  }
+
+  @Test
+  void errorPathWithMetadataCommandLinksTheMetadataOfEveryEntryInThePath() {
+    String cypher = 
createWorkflowWithErrorPath().getErrorPathWithMetadataCommand(0);
+
+    assertTrue(
+        cypher.contains(
+            "MATCH (:Execution { type : \"PIPELINE\", id : 
\"pipeline-id\"})-[metaRel1]->(meta1:Pipeline)"));
+    assertTrue(
+        cypher.contains(
+            "MATCH (:Execution { type : \"TRANSFORM\", id : 
\"transform-id\"})-[metaRel2]->(meta2:Transform)"));
+    assertTrue(cypher.contains("RETURN p, metaRel1, meta1, metaRel2, meta2"));
+  }
+
+  @Test
+  void errorPathCommandDoesNotUseSizeOnAPatternExpression() {
+    
assertNoSizeOfPatternExpression(createWorkflowWithErrorPath().getErrorPathCommand());
+  }
+}
diff --git 
a/plugins/tech/neo4j/src/test/java/org/apache/hop/neo4j/perspective/ErrorPathCypherIT.java
 
b/plugins/tech/neo4j/src/test/java/org/apache/hop/neo4j/perspective/ErrorPathCypherIT.java
new file mode 100644
index 0000000000..e4592bea27
--- /dev/null
+++ 
b/plugins/tech/neo4j/src/test/java/org/apache/hop/neo4j/perspective/ErrorPathCypherIT.java
@@ -0,0 +1,150 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.neo4j.perspective;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.neo4j.driver.AuthTokens;
+import org.neo4j.driver.Driver;
+import org.neo4j.driver.GraphDatabase;
+import org.neo4j.driver.Record;
+import org.neo4j.driver.Session;
+import org.neo4j.driver.exceptions.ClientException;
+import org.neo4j.driver.types.Node;
+import org.testcontainers.containers.Neo4jContainer;
+import org.testcontainers.junit.jupiter.Container;
+import org.testcontainers.junit.jupiter.Testcontainers;
+import org.testcontainers.utility.DockerImageName;
+
+/**
+ * Runs the error path lookup of the Neo4j perspective against a real Neo4j 5 
server.
+ *
+ * <p>Neo4j 5 removed size() on a pattern expression, which made the Error 
path tab of the Neo4j
+ * perspective fail. This verifies both that the statement is accepted by 
Neo4j 5 and that it still
+ * selects the deepest execution which reported errors.
+ */
+@Testcontainers(disabledWithoutDocker = true)
+class ErrorPathCypherIT {
+
+  @Container
+  static Neo4jContainer<?> neo4j =
+      new 
Neo4jContainer<>(DockerImageName.parse("neo4j:5.26")).withoutAuthentication();
+
+  private static Driver driver;
+
+  @BeforeAll
+  static void setUp() {
+    driver = GraphDatabase.driver(neo4j.getBoltUrl(), AuthTokens.none());
+
+    // A workflow which executes a pipeline, which in turn executes a 
transform that failed.
+    // The pipeline reports the errors of the transform, so both have errors > 
0 and only the
+    // transform is a leaf. A second transform completed without errors.
+    //
+    try (Session session = driver.session()) {
+      session.executeWrite(
+          tx -> {
+            tx.run(
+                """
+                CREATE (workflow:Execution { id : 'workflow-id', name : 
'main', type : 'WORKFLOW',\
+                 registrationDate : '2026-07-29T10:00:00', errors : 1 })
+                CREATE (pipeline:Execution { id : 'pipeline-id', name : 
'load-customers',\
+                 type : 'PIPELINE', registrationDate : '2026-07-29T10:00:01', 
errors : 1 })
+                CREATE (failed:Execution { id : 'failed-id', name : 'Table 
output',\
+                 type : 'TRANSFORM', registrationDate : '2026-07-29T10:00:02', 
errors : 1 })
+                CREATE (ok:Execution { id : 'ok-id', name : 'Dummy', type : 
'TRANSFORM',\
+                 registrationDate : '2026-07-29T10:00:02', errors : 0 })
+                CREATE (workflow)-[:EXECUTES]->(pipeline)
+                CREATE (pipeline)-[:EXECUTES]->(failed)
+                CREATE (pipeline)-[:EXECUTES]->(ok)
+                """);
+            return null;
+          });
+    }
+  }
+
+  @AfterAll
+  static void tearDown() {
+    if (driver != null) {
+      driver.close();
+    }
+  }
+
+  private List<Record> runErrorPathCypher(String cypher) {
+    try (Session session = driver.session()) {
+      return session.executeRead(
+          tx ->
+              tx.run(
+                      cypher,
+                      Map.of(
+                          HopNeo4jPerspective.CONST_SUBJECT_NAME, "main",
+                          HopNeo4jPerspective.CONST_SUBJECT_TYPE, "WORKFLOW",
+                          HopNeo4jPerspective.CONST_SUBJECT_ID, "workflow-id"))
+                  .list());
+    }
+  }
+
+  private List<String> executionIds(Record record) {
+    List<String> ids = new ArrayList<>();
+    for (Node node : record.get(0).asPath().nodes()) {
+      ids.add(node.get("id").asString());
+    }
+    return ids;
+  }
+
+  @Test
+  void errorPathCypherReturnsThePathToTheDeepestError() {
+    List<Record> records = 
runErrorPathCypher(HopNeo4jPerspective.getErrorPathCypher());
+
+    // Only the failed transform is a leaf with errors, so there is exactly 
one path.
+    //
+    assertEquals(1, records.size());
+    assertEquals(List.of("workflow-id", "pipeline-id", "failed-id"), 
executionIds(records.get(0)));
+  }
+
+  @Test
+  void errorPathCypherSkipsExecutionsWhichAreNotLeaves() {
+    List<String> ids =
+        
executionIds(runErrorPathCypher(HopNeo4jPerspective.getErrorPathCypher()).get(0));
+
+    // The pipeline reports the errors of the transform it executes, but it is 
not the deepest
+    // error, so the path must not stop there.
+    //
+    assertEquals("failed-id", ids.get(ids.size() - 1));
+    assertFalse(
+        ids.contains("ok-id"), "An execution without errors must not show up 
in the error path");
+  }
+
+  @Test
+  void sizeOnAPatternExpressionIsRejectedByNeo4j5() {
+    // This is what the statement used to look like, kept here to document why 
it was changed.
+    //
+    String removedSyntax =
+        HopNeo4jPerspective.getErrorPathCypher()
+            .replace("NOT (err)-[:EXECUTES]->()", 
"size((err)-[:EXECUTES]->())=0");
+
+    assertThrows(ClientException.class, () -> 
runErrorPathCypher(removedSyntax));
+  }
+}
diff --git 
a/plugins/tech/neo4j/src/test/java/org/apache/hop/neo4j/perspective/HistoryResultTest.java
 
b/plugins/tech/neo4j/src/test/java/org/apache/hop/neo4j/perspective/HistoryResultTest.java
new file mode 100644
index 0000000000..33982d8c49
--- /dev/null
+++ 
b/plugins/tech/neo4j/src/test/java/org/apache/hop/neo4j/perspective/HistoryResultTest.java
@@ -0,0 +1,93 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.neo4j.perspective;
+
+import static 
org.apache.hop.neo4j.CypherAssertions.assertNoSizeOfPatternExpression;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.List;
+import org.junit.jupiter.api.Test;
+
+/**
+ * The Cypher generated here is not executed by Hop, it is presented in the 
Neo4j perspective for
+ * the user to run in the Neo4j browser. It still has to be valid Cypher.
+ */
+class HistoryResultTest {
+
+  private HistoryResult createWorkflowWithErrorPath() {
+    HistoryResult pipeline = new HistoryResult();
+    pipeline.setId("pipeline-id");
+    pipeline.setName("load-customers");
+    pipeline.setType("PIPELINE");
+
+    HistoryResult transform = new HistoryResult();
+    transform.setId("transform-id");
+    transform.setName("Table output");
+    transform.setType("TRANSFORM");
+
+    HistoryResult workflow = new HistoryResult();
+    workflow.setId("workflow-id");
+    workflow.setName("main");
+    workflow.setType("WORKFLOW");
+    workflow.setShortestPaths(List.of(List.of(pipeline, transform)));
+
+    return workflow;
+  }
+
+  @Test
+  void errorPathWithMetadataCommandDoesNotUseSizeOnAPatternExpression() {
+    assertNoSizeOfPatternExpression(
+        createWorkflowWithErrorPath().getErrorPathWithMetadataCommand(0));
+  }
+
+  @Test
+  void errorPathWithMetadataCommandOnlyKeepsLeafErrors() {
+    assertTrue(
+        createWorkflowWithErrorPath()
+            .getErrorPathWithMetadataCommand(0)
+            .contains("AND NOT (err)-[:EXECUTES]->()"),
+        "The error node has to be a leaf, expressed as a pattern predicate");
+  }
+
+  @Test
+  void errorPathWithMetadataCommandLinksTheMetadataOfEveryEntryInThePath() {
+    String cypher = 
createWorkflowWithErrorPath().getErrorPathWithMetadataCommand(0);
+
+    assertTrue(
+        cypher.contains(
+            "MATCH (:Execution { type : \"PIPELINE\", id : 
\"pipeline-id\"})-[metaRel1]->(meta1:Pipeline)"));
+    assertTrue(
+        cypher.contains(
+            "MATCH (:Execution { type : \"TRANSFORM\", id : 
\"transform-id\"})-[metaRel2]->(meta2:Transform)"));
+    assertTrue(cypher.contains("RETURN p, metaRel1, meta1, metaRel2, meta2"));
+  }
+
+  @Test
+  void errorPathCommandDoesNotUseSizeOnAPatternExpression() {
+    
assertNoSizeOfPatternExpression(createWorkflowWithErrorPath().getErrorPathCommand());
+  }
+
+  @Test
+  void executionInfoCommandLooksUpTheSubject() {
+    assertTrue(
+        createWorkflowWithErrorPath()
+            .getExecutionInfoCommand()
+            .contains(
+                "MATCH(ex:Execution { name : \"main\", type : \"WORKFLOW\", id 
: \"workflow-id\"})"));
+  }
+}
diff --git 
a/plugins/tech/neo4j/src/test/java/org/apache/hop/neo4j/perspective/HopNeo4jPerspectiveTest.java
 
b/plugins/tech/neo4j/src/test/java/org/apache/hop/neo4j/perspective/HopNeo4jPerspectiveTest.java
new file mode 100644
index 0000000000..3b163deb6a
--- /dev/null
+++ 
b/plugins/tech/neo4j/src/test/java/org/apache/hop/neo4j/perspective/HopNeo4jPerspectiveTest.java
@@ -0,0 +1,88 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.neo4j.perspective;
+
+import static 
org.apache.hop.neo4j.CypherAssertions.assertNoSizeOfPatternExpression;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.apache.hop.i18n.BaseMessages;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+class HopNeo4jPerspectiveTest {
+
+  /** BaseMessages returns !key! when a key is missing from the bundle. */
+  @ParameterizedTest
+  @ValueSource(
+      strings = {
+        "Neo4jPerspectiveDialog.NoFileFound.Header",
+        "Neo4jPerspectiveDialog.NoFileFound.Message",
+        "Neo4jPerspectiveDialog.NoLoggingFound.Message"
+      })
+  void messageKeysAreTranslated(String key) {
+    String message = BaseMessages.getString(HopNeo4jPerspective.PKG, key, 
"a-name", "PIPELINE");
+
+    assertFalse(message.startsWith("!"), "Missing key in the message bundle: " 
+ key);
+  }
+
+  @Test
+  void errorPathCypherDoesNotUseSizeOnAPatternExpression() {
+    assertNoSizeOfPatternExpression(HopNeo4jPerspective.getErrorPathCypher());
+  }
+
+  @Test
+  void errorPathCypherOnlyKeepsLeafErrors() {
+    assertTrue(
+        HopNeo4jPerspective.getErrorPathCypher().contains("AND NOT 
(err)-[:EXECUTES]->()"),
+        "The error node has to be a leaf, expressed as a pattern predicate");
+  }
+
+  @Test
+  void errorPathCypherStillSortsOnTheLengthOfThePath() {
+    // size() on a list is not affected by the Neo4j 5 removal and has to stay 
as it is.
+    //
+    assertTrue(
+        HopNeo4jPerspective.getErrorPathCypher().contains("ORDER BY 
size(RELATIONSHIPS(p)) DESC"));
+  }
+
+  @Test
+  void errorPathCypherPassesTheSubjectAsParameters() {
+    String cypher = HopNeo4jPerspective.getErrorPathCypher();
+
+    assertTrue(cypher.contains("$" + HopNeo4jPerspective.CONST_SUBJECT_NAME));
+    assertTrue(cypher.contains("$" + HopNeo4jPerspective.CONST_SUBJECT_TYPE));
+    assertTrue(cypher.contains("$" + HopNeo4jPerspective.CONST_SUBJECT_ID));
+  }
+
+  @Test
+  void errorPathCypherIsComplete() {
+    assertEquals(
+        "MATCH(top:Execution { name : $subjectName, type : $subjectType, id : 
$subjectId })-[rel:EXECUTES*]-(err:Execution) "
+            + "   , p=shortestpath((top)-[:EXECUTES*]-(err)) "
+            + "WHERE top.registrationDate IS NOT NULL "
+            + "  AND err.errors > 0 "
+            + "  AND NOT (err)-[:EXECUTES]->() "
+            + "RETURN p "
+            + "ORDER BY size(RELATIONSHIPS(p)) DESC "
+            + "LIMIT 10",
+        HopNeo4jPerspective.getErrorPathCypher());
+  }
+}

Reply via email to