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

Rikkola pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/incubator-kie.git


The following commit(s) were added to refs/heads/main by this push:
     new 9330215b7dc fix(drools): exists(A() and not B()) stops firing when 
sibling package wraps same CE in its own exists (#7116)
9330215b7dc is described below

commit 9330215b7dc699e87881f930d24022541cb53609
Author: Toni Rikkola <[email protected]>
AuthorDate: Thu Oct 1 12:36:22 2026 +0300

    fix(drools): exists(A() and not B()) stops firing when sibling package 
wraps same CE in its own exists (#7116)
    
    When rules from different packages are loaded sequentially, a rule 
containing
      exists(SharedFact() and not NegatedFact())
    silently stops firing if a later package adds a sibling rule containing
      exists(SharedFact() and OtherFact())
    
    Root cause (split path): SegmentPrototype.splitEagerProtos bailed out early
    when proto1 was not eager, even when the split produced an eager proto2. The
    early-exit comment 'if it wasn't eager before, nothing can be eager after' 
was
    incorrect for the case where proto1 contained both a JoinNode and a NotNode
    (suppressing the eager requirement) and the split removed the JoinNode from
    proto2's half. Without proto2 in the eager list, initializePathMemory 
skipped
    creating the NotNode's segment, leaving the ExistsNode permanently unlinked.
    
    Root cause (merge path): the inverse operation, mergeEagerProtos(), had a
    symmetric gap. When the sibling rule is later removed and segments merge 
back,
    the stale eager prototype for proto2 was not cleaned up in the case where
    proto1 no longer requiresEager() after the merge. The stale entry caused an
    ArrayIndexOutOfBoundsException in 
SegmentMemorySupportImpl.initializePathMemory()
    when a new session was created after the merge.
    
    Fix: add an else-if branch in splitEagerProtos for the !proto1WasEager &&
    proto2.requiresEager() case, and extend the removal condition in 
mergeEagerProtos
    to also cover the (!requiresEager() && proto2WasEager) case.
    
    Closes: https://github.com/apache/incubator-kie/issues/7115
    
    Tests:
    - ExistsWithNotSubnetworkCrossPackageTest (5 methods x 2 configs = 10 
instances)
    - 
SubnetworkSegmentSplitIntegrationTest#testTP04_removeSiblingRule_afterTopLevelNegationSegmentSplit
 (2 configs)
    
    Co-authored-by: Toni Rikkola <[email protected]>
    Co-authored-by: IBM Bob <[email protected]>
    Co-authored-by: Toshiya Kobayashi <[email protected]>
---
 .../java/org/drools/core/reteoo/SegmentMemory.java |  20 +-
 .../ExistsWithNotSubnetworkCrossPackageTest.java   | 466 +++++++++++++++++++++
 .../SubnetworkSegmentSplitIntegrationTest.java     | 159 +++++++
 3 files changed, 640 insertions(+), 5 deletions(-)

diff --git 
a/drools-core/src/main/java/org/drools/core/reteoo/SegmentMemory.java 
b/drools-core/src/main/java/org/drools/core/reteoo/SegmentMemory.java
index a909328c292..1d7c727dbb6 100644
--- a/drools-core/src/main/java/org/drools/core/reteoo/SegmentMemory.java
+++ b/drools-core/src/main/java/org/drools/core/reteoo/SegmentMemory.java
@@ -853,7 +853,7 @@ public class SegmentMemory extends LinkedList<SegmentMemory>
         }
 
         public void splitEagerProtos(boolean proto1WasEager, SegmentPrototype 
other, PathEndNode endNode) {
-            if (proto1WasEager) { // if it wasn't eager before, nothing can be 
eager after
+            if (proto1WasEager) { // proto1 was eager before the split
                 SegmentPrototype[] eager = endNode.getEagerSegmentPrototypes();
                 if (requiresEager() && other.requiresEager()) {
                     // keep proto1 and add proto2
@@ -862,14 +862,24 @@ public class SegmentMemory extends 
LinkedList<SegmentMemory>
                     newEager[newEager.length - 1] = other; // I don't think 
order matters, so just add to the end
                     endNode.setEagerSegmentPrototypes(newEager);
                 } else if (other.requiresEager()) {
-                    // proto2 is no longer eager, find proto1 and swap proto1 
with proto2
+                    // proto1 is no longer eager, find proto1 and swap proto1 
with proto2
                     for (int i = 0; i < eager.length; i++) {
                         if (eager[i] == this) {
                             eager[i] = other;
                             break;
                         }
                     }
-                } // else if ( requiresEager() && !proto2.requiresEager()) do 
nothing as proto1 already in the array
+                }
+            } else if (other.requiresEager()) {
+                // proto1 was not eager, but the split produced an eager 
proto2.
+                // This happens when proto1 contained both a NotNode and a 
JoinNode (suppressing the
+                // eager requirement), and the split removed the JoinNode from 
proto2's portion.
+                // proto2 must be added to the endNode's eager list so it gets 
eagerly initialised.
+                SegmentPrototype[] eager = endNode.getEagerSegmentPrototypes();
+                SegmentPrototype[] newEager = new 
SegmentPrototype[eager.length + 1];
+                System.arraycopy(eager, 0, newEager, 0, eager.length);
+                newEager[newEager.length - 1] = other;
+                endNode.setEagerSegmentPrototypes(newEager);
             }
         }
 
@@ -912,8 +922,8 @@ public class SegmentMemory extends LinkedList<SegmentMemory>
             }
 
             SegmentPrototype[] eager = endNode.getEagerSegmentPrototypes();
-            if (requiresEager() && proto2.requiresEager()) {
-                // keep proto1 and remove proto2
+            if ((!requiresEager() && proto2WasEager) || (requiresEager() && 
proto2.requiresEager())) {
+                // remove proto2 from the eager array
                 SegmentPrototype[] newEager = new 
SegmentPrototype[eager.length - 1];
                 copyWithRemoval(eager, newEager, proto2);
                 endNode.setEagerSegmentPrototypes(newEager);
diff --git 
a/drools-test-coverage/test-compiler-integration/src/test/java/org/drools/compiler/integrationtests/ExistsWithNotSubnetworkCrossPackageTest.java
 
b/drools-test-coverage/test-compiler-integration/src/test/java/org/drools/compiler/integrationtests/ExistsWithNotSubnetworkCrossPackageTest.java
new file mode 100644
index 00000000000..a04f5863022
--- /dev/null
+++ 
b/drools-test-coverage/test-compiler-integration/src/test/java/org/drools/compiler/integrationtests/ExistsWithNotSubnetworkCrossPackageTest.java
@@ -0,0 +1,466 @@
+/*
+ * 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.drools.compiler.integrationtests;
+
+import java.util.stream.Stream;
+
+import org.drools.testcoverage.common.util.KieBaseTestConfiguration;
+import org.drools.testcoverage.common.util.KieBaseUtil;
+import org.drools.testcoverage.common.util.TestParametersUtil2;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.kie.api.KieBase;
+import org.kie.api.definition.type.FactType;
+import org.kie.api.runtime.KieSession;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class ExistsWithNotSubnetworkCrossPackageTest {
+
+    public static Stream<KieBaseTestConfiguration> parameters() {
+        return 
TestParametersUtil2.getKieBaseCloudConfigurations(true).stream();
+    }
+
+    private static final String DRL_TYPES =
+            "package repro.types;\n" +
+            "declare Seed end\n" +
+            "declare SharedFact end\n" +
+            "declare NegatedFact end\n" +
+            "declare OtherFact end\n" +
+            "declare Result end\n";
+
+    private static final String DRL_A =
+            "package repro.a;\n" +
+            "import repro.types.*;\n" +
+            "rule \"produce-shared-fact\"\n" +
+            "    when\n" +
+            "        Seed()\n" +
+            "    then\n" +
+            "        insert(new SharedFact());\n" +
+            "end\n";
+
+    private static final String DRL_B =
+            "package repro.b;\n" +
+            "import repro.types.*;\n" +
+            "rule \"subject\"\n" +
+            "    when\n" +
+            "        exists ( SharedFact() and not NegatedFact() )\n" +
+            "    then\n" +
+            "        insert(new Result());\n" +
+            "end\n";
+
+    @ParameterizedTest(name = "KieBase type={0}")
+    @MethodSource("parameters")
+    public void testExistsWithNotSubnetwork_subjectAloneFiresCorrectly(
+            KieBaseTestConfiguration kieBaseTestConfiguration) throws 
Exception {
+
+        final KieBase kbase = KieBaseUtil.getKieBaseFromKieModuleFromDrl(
+                "exists-not-solo-test", kieBaseTestConfiguration,
+                DRL_TYPES, DRL_A, DRL_B);
+
+        final KieSession ksession = kbase.newKieSession();
+        try {
+            final FactType seedType = kbase.getFactType("repro.types", "Seed");
+            ksession.insert(seedType.newInstance());
+            final int fired = ksession.fireAllRules();
+            assertThat(fired)
+                    .as("Without sibling, subject must fire (2 rules: 
produce-shared-fact + subject)")
+                    .isEqualTo(2);
+        } finally {
+            ksession.dispose();
+        }
+    }
+
+    @ParameterizedTest(name = "KieBase type={0}")
+    @MethodSource("parameters")
+    public void testExistsWithNotSubnetworkNotSuppressedByCrossPackageSibling(
+            KieBaseTestConfiguration kieBaseTestConfiguration) throws 
Exception {
+
+        final String drlC =
+                "package repro.c;\n" +
+                "import repro.types.*;\n" +
+                "rule \"sibling\"\n" +
+                "    when\n" +
+                "        exists ( SharedFact() and OtherFact() )\n" +
+                "    then\n" +
+                "end\n";
+
+        final KieBase kbase = KieBaseUtil.getKieBaseFromKieModuleFromDrl(
+                "exists-not-cross-package-test", kieBaseTestConfiguration,
+                DRL_TYPES, DRL_A, DRL_B, drlC);
+
+        final KieSession ksession = kbase.newKieSession();
+        try {
+            final FactType seedType = kbase.getFactType("repro.types", "Seed");
+            ksession.insert(seedType.newInstance());
+            final int fired = ksession.fireAllRules();
+
+            assertThat(fired)
+                    .as("Expected 2 rule firings (produce-shared-fact + 
subject); sibling must not fire")
+                    .isEqualTo(2);
+
+            final FactType resultType = kbase.getFactType("repro.types", 
"Result");
+            final long resultCount = ksession.getObjects().stream()
+                    .filter(o -> resultType.getFactClass().isInstance(o))
+                    .count();
+            assertThat(resultCount)
+                    .as("subject rule must insert exactly one Result")
+                    .isEqualTo(1);
+        } finally {
+            ksession.dispose();
+        }
+    }
+
+    @ParameterizedTest(name = "KieBase type={0}")
+    @MethodSource("parameters")
+    public void testMinimal_directSharedFactInsert(
+            KieBaseTestConfiguration kieBaseTestConfiguration) throws 
Exception {
+
+        final String drlB2 =
+                "package repro.b;\n" +
+                "import repro.types.*;\n" +
+                "rule \"subject\"\n" +
+                "    when\n" +
+                "        exists ( SharedFact() and not NegatedFact() )\n" +
+                "    then\n" +
+                "        insert(new Result());\n" +
+                "end\n";
+
+        final String drlC2 =
+                "package repro.c;\n" +
+                "import repro.types.*;\n" +
+                "rule \"sibling\"\n" +
+                "    when\n" +
+                "        exists ( SharedFact() and OtherFact() )\n" +
+                "    then\n" +
+                "end\n";
+
+        final KieBase kbaseAlone = KieBaseUtil.getKieBaseFromKieModuleFromDrl(
+                "minimal-alone-test", kieBaseTestConfiguration,
+                DRL_TYPES, drlB2);
+        final KieSession sessAlone = kbaseAlone.newKieSession();
+        try {
+            sessAlone.insert(kbaseAlone.getFactType("repro.types", 
"SharedFact").newInstance());
+            final int firedAlone = sessAlone.fireAllRules();
+            assertThat(firedAlone)
+                    .as("Without sibling, subject fires directly on SharedFact 
insert: expected 1")
+                    .isEqualTo(1);
+        } finally {
+            sessAlone.dispose();
+        }
+
+        final KieBase kbaseWith = KieBaseUtil.getKieBaseFromKieModuleFromDrl(
+                "minimal-with-sibling-test", kieBaseTestConfiguration,
+                DRL_TYPES, drlB2, drlC2);
+        final KieSession sessWith = kbaseWith.newKieSession();
+        try {
+            sessWith.insert(kbaseWith.getFactType("repro.types", 
"SharedFact").newInstance());
+            final int firedWith = sessWith.fireAllRules();
+            assertThat(firedWith)
+                    .as("With sibling (cross-pkg), subject must still fire: 
expected 1")
+                    .isEqualTo(1);
+        } finally {
+            sessWith.dispose();
+        }
+    }
+
+    @ParameterizedTest(name = "KieBase type={0}")
+    @MethodSource("parameters")
+    public void 
testExistsWithNotSubnetwork_samePkgSiblingDoesNotSuppressSubject(
+            KieBaseTestConfiguration kieBaseTestConfiguration) throws 
Exception {
+
+        final String drlCSamePkg =
+                "package repro.b;\n" +
+                "import repro.types.*;\n" +
+                "rule \"sibling-same-pkg\"\n" +
+                "    when\n" +
+                "        exists ( SharedFact() and OtherFact() )\n" +
+                "    then\n" +
+                "end\n";
+
+        final KieBase kbase = KieBaseUtil.getKieBaseFromKieModuleFromDrl(
+                "exists-not-samepkg-test", kieBaseTestConfiguration,
+                DRL_TYPES, DRL_A, DRL_B, drlCSamePkg);
+
+        final KieSession ksession = kbase.newKieSession();
+        try {
+            final FactType seedType = kbase.getFactType("repro.types", "Seed");
+            ksession.insert(seedType.newInstance());
+            final int fired = ksession.fireAllRules();
+            assertThat(fired)
+                    .as("Same-pkg sibling must not suppress subject: expected 
2 firings (produce-shared-fact + subject)")
+                    .isEqualTo(2);
+        } finally {
+            ksession.dispose();
+        }
+    }
+
+    @ParameterizedTest(name = "KieBase type={0}")
+    @MethodSource("parameters")
+    public void testMinimal_javaBeans_crossPackage(
+            KieBaseTestConfiguration kieBaseTestConfiguration) throws 
Exception {
+
+        final String drlBJava =
+                "package repro.bj;\n" +
+                "import " + java.util.ArrayList.class.getCanonicalName() + 
";\n" +
+                "import " + java.util.LinkedList.class.getCanonicalName() + 
";\n" +
+                "import " + java.util.HashMap.class.getCanonicalName() + ";\n" 
+
+                "rule \"subjectJ\"\n" +
+                "    when\n" +
+                "        exists ( ArrayList() and not LinkedList() )\n" +
+                "    then\n" +
+                "end\n";
+
+        final String drlCJava =
+                "package repro.cj;\n" +
+                "import " + java.util.ArrayList.class.getCanonicalName() + 
";\n" +
+                "import " + java.util.HashMap.class.getCanonicalName() + ";\n" 
+
+                "rule \"siblingJ\"\n" +
+                "    when\n" +
+                "        exists ( ArrayList() and HashMap() )\n" +
+                "    then\n" +
+                "end\n";
+
+        final KieBase aloneBase = KieBaseUtil.getKieBaseFromKieModuleFromDrl(
+                "java-alone-test", kieBaseTestConfiguration, drlBJava);
+        final KieSession aloneSession = aloneBase.newKieSession();
+        try {
+            aloneSession.insert(new java.util.ArrayList<>());
+            assertThat(aloneSession.fireAllRules())
+                    .as("Java classes, no sibling: subject must fire")
+                    .isEqualTo(1);
+        } finally {
+            aloneSession.dispose();
+        }
+
+        final KieBase withBase = KieBaseUtil.getKieBaseFromKieModuleFromDrl(
+                "java-with-sibling-test", kieBaseTestConfiguration, drlBJava, 
drlCJava);
+        final KieSession withSession = withBase.newKieSession();
+        try {
+            withSession.insert(new java.util.ArrayList<>());
+            assertThat(withSession.fireAllRules())
+                    .as("Java classes, with cross-pkg sibling: subject must 
still fire")
+                    .isEqualTo(1);
+        } finally {
+            withSession.dispose();
+        }
+    }
+
+    @ParameterizedTest(name = "KieBase type={0}")
+    @MethodSource("parameters")
+    public void testRemoveSiblingRule_subjectStillFires(
+            KieBaseTestConfiguration kieBaseTestConfiguration) throws 
Exception {
+
+        final String drlB =
+                "package repro.b;\n" +
+                "import repro.types.*;\n" +
+                "rule \"subject\"\n" +
+                "    when\n" +
+                "        exists ( SharedFact() and not NegatedFact() )\n" +
+                "    then\n" +
+                "        insert(new Result());\n" +
+                "end\n";
+
+        final String drlC =
+                "package repro.c;\n" +
+                "import repro.types.*;\n" +
+                "rule \"sibling\"\n" +
+                "    when\n" +
+                "        exists ( SharedFact() and OtherFact() )\n" +
+                "    then\n" +
+                "end\n";
+
+        final KieBase kbase = KieBaseUtil.getKieBaseFromKieModuleFromDrl(
+                "remove-sibling-test", kieBaseTestConfiguration,
+                DRL_TYPES, drlB, drlC);
+
+        kbase.removeRule("repro.c", "sibling");
+
+        final KieSession session = kbase.newKieSession();
+        try {
+            session.insert(kbase.getFactType("repro.types", 
"SharedFact").newInstance());
+            final int fired = session.fireAllRules();
+            assertThat(fired)
+                    .as("After removing sibling, subject must still fire: 
expected 1")
+                    .isEqualTo(1);
+        } finally {
+            session.dispose();
+        }
+    }
+
+    @ParameterizedTest(name = "KieBase type={0}")
+    @MethodSource("parameters")
+    public void testNotWithNotSubnetwork_crossPackage(
+            KieBaseTestConfiguration kieBaseTestConfiguration) throws 
Exception {
+
+        final String drlB =
+                "package repro.b;\n" +
+                "import repro.types.*;\n" +
+                "rule \"subject-not\"\n" +
+                "    when\n" +
+                "        not ( SharedFact() and not NegatedFact() )\n" +
+                "    then\n" +
+                "        insert(new Result());\n" +
+                "end\n";
+
+        final String drlC =
+                "package repro.c;\n" +
+                "import repro.types.*;\n" +
+                "rule \"sibling\"\n" +
+                "    when\n" +
+                "        exists ( SharedFact() and OtherFact() )\n" +
+                "    then\n" +
+                "end\n";
+
+        final KieBase kbase = KieBaseUtil.getKieBaseFromKieModuleFromDrl(
+                "not-with-not-subnetwork-test", kieBaseTestConfiguration,
+                DRL_TYPES, drlB, drlC);
+
+        final KieSession session = kbase.newKieSession();
+        try {
+            final int fired = session.fireAllRules();
+            assertThat(fired)
+                    .as("No SharedFact inserted: not(SharedFact and not 
NegatedFact) must fire")
+                    .isEqualTo(1);
+        } finally {
+            session.dispose();
+        }
+    }
+
+    @ParameterizedTest(name = "KieBase type={0}")
+    @MethodSource("parameters")
+    public void testMultiJoinWithNegation_crossPackage(
+            KieBaseTestConfiguration kieBaseTestConfiguration) throws 
Exception {
+
+        final String drlB =
+                "package repro.b;\n" +
+                "import repro.types.*;\n" +
+                "rule \"subject-multijoin\"\n" +
+                "    when\n" +
+                "        exists ( SharedFact() and OtherFact() and not 
NegatedFact() )\n" +
+                "    then\n" +
+                "        insert(new Result());\n" +
+                "end\n";
+
+        final String drlC =
+                "package repro.c;\n" +
+                "import repro.types.*;\n" +
+                "rule \"sibling-shared-only\"\n" +
+                "    when\n" +
+                "        exists ( SharedFact() and Seed() )\n" +
+                "    then\n" +
+                "end\n";
+
+        final KieBase kbase = KieBaseUtil.getKieBaseFromKieModuleFromDrl(
+                "multijoin-not-test", kieBaseTestConfiguration,
+                DRL_TYPES, drlB, drlC);
+
+        final KieSession session = kbase.newKieSession();
+        try {
+            session.insert(kbase.getFactType("repro.types", 
"SharedFact").newInstance());
+            session.insert(kbase.getFactType("repro.types", 
"OtherFact").newInstance());
+            final int fired = session.fireAllRules();
+            assertThat(fired)
+                    .as("SharedFact + OtherFact inserted: subject-multijoin 
must fire")
+                    .isEqualTo(1);
+        } finally {
+            session.dispose();
+        }
+    }
+
+    @ParameterizedTest(name = "KieBase type={0}")
+    @MethodSource("parameters")
+    public void testMultipleChainedNegations_crossPackage(
+            KieBaseTestConfiguration kieBaseTestConfiguration) throws 
Exception {
+
+        final String drlB =
+                "package repro.b;\n" +
+                "import repro.types.*;\n" +
+                "rule \"subject-double-not\"\n" +
+                "    when\n" +
+                "        exists ( SharedFact() and not NegatedFact() and not 
OtherFact() )\n" +
+                "    then\n" +
+                "        insert(new Result());\n" +
+                "end\n";
+
+        final String drlC =
+                "package repro.c;\n" +
+                "import repro.types.*;\n" +
+                "rule \"sibling\"\n" +
+                "    when\n" +
+                "        exists ( SharedFact() and Seed() )\n" +
+                "    then\n" +
+                "end\n";
+
+        final KieBase kbase = KieBaseUtil.getKieBaseFromKieModuleFromDrl(
+                "double-not-test", kieBaseTestConfiguration,
+                DRL_TYPES, drlB, drlC);
+
+        final KieSession session = kbase.newKieSession();
+        try {
+            session.insert(kbase.getFactType("repro.types", 
"SharedFact").newInstance());
+            final int fired = session.fireAllRules();
+            assertThat(fired)
+                    .as("SharedFact inserted without negated facts: 
subject-double-not must fire")
+                    .isEqualTo(1);
+        } finally {
+            session.dispose();
+        }
+    }
+
+    @ParameterizedTest(name = "KieBase type={0}")
+    @MethodSource("parameters")
+    public void testAccumulateWithNotSubnetwork_crossPackage(
+            KieBaseTestConfiguration kieBaseTestConfiguration) throws 
Exception {
+
+        final String drlB =
+                "package repro.b;\n" +
+                "import repro.types.*;\n" +
+                "rule \"subject-accumulate\"\n" +
+                "    when\n" +
+                "        accumulate( SharedFact() and not NegatedFact(); $c : 
count() )\n" +
+                "    then\n" +
+                "        insert(new Result());\n" +
+                "end\n";
+
+        final String drlC =
+                "package repro.c;\n" +
+                "import repro.types.*;\n" +
+                "rule \"sibling\"\n" +
+                "    when\n" +
+                "        exists ( SharedFact() and OtherFact() )\n" +
+                "    then\n" +
+                "end\n";
+
+        final KieBase kbase = KieBaseUtil.getKieBaseFromKieModuleFromDrl(
+                "accumulate-not-test", kieBaseTestConfiguration,
+                DRL_TYPES, drlB, drlC);
+
+        final KieSession session = kbase.newKieSession();
+        try {
+            session.insert(kbase.getFactType("repro.types", 
"SharedFact").newInstance());
+            final int fired = session.fireAllRules();
+            assertThat(fired)
+                    .as("SharedFact inserted: accumulate should count 1 and 
fire")
+                    .isEqualTo(1);
+        } finally {
+            session.dispose();
+        }
+    }
+}
diff --git 
a/drools-test-coverage/test-compiler-integration/src/test/java/org/drools/compiler/integrationtests/SubnetworkSegmentSplitIntegrationTest.java
 
b/drools-test-coverage/test-compiler-integration/src/test/java/org/drools/compiler/integrationtests/SubnetworkSegmentSplitIntegrationTest.java
new file mode 100644
index 00000000000..817cbcfc42f
--- /dev/null
+++ 
b/drools-test-coverage/test-compiler-integration/src/test/java/org/drools/compiler/integrationtests/SubnetworkSegmentSplitIntegrationTest.java
@@ -0,0 +1,159 @@
+/*
+ * 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.drools.compiler.integrationtests;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Stream;
+
+import org.drools.testcoverage.common.util.KieBaseTestConfiguration;
+import org.drools.testcoverage.common.util.KieBaseUtil;
+import org.drools.testcoverage.common.util.TestParametersUtil2;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.drools.kiesession.rulebase.InternalKnowledgeBase;
+import org.kie.api.KieBase;
+import org.kie.api.runtime.KieSession;
+import org.kie.api.runtime.rule.FactHandle;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class SubnetworkSegmentSplitIntegrationTest {
+
+    public static Stream<KieBaseTestConfiguration> parameters() {
+        return 
TestParametersUtil2.getKieBaseCloudConfigurations(true).stream();
+    }
+
+    public static class Container {
+        private final List<String> items;
+        public Container(String... items) { this.items = new 
ArrayList<>(Arrays.asList(items)); }
+        public List<String> getItems() { return items; }
+    }
+
+    // =======================================================================
+    // TP-01: Mixed exists + not sharing on cross-package subnetwork
+    // =======================================================================
+    private static final String DRL_TP01_PKG_A =
+            "package repro.tp01.a;\n" +
+            "import " + Container.class.getCanonicalName() + ";\n" +
+            "global java.util.List results;\n" +
+            "rule \"not-rule\"\n" +
+            "when\n" +
+            "    not ( $c : Container() and String( this == \"match\" ) from 
$c.items )\n" +
+            "then\n" +
+            "    results.add(\"not-rule\");\n" +
+            "end\n";
+
+    private static final String DRL_TP01_PKG_B =
+            "package repro.tp01.b;\n" +
+            "import " + Container.class.getCanonicalName() + ";\n" +
+            "global java.util.List results;\n" +
+            "rule \"exists-rule\"\n" +
+            "when\n" +
+            "    exists ( $c : Container() and String( this == \"match\" ) 
from $c.items )\n" +
+            "then\n" +
+            "    results.add(\"exists-rule\");\n" +
+            "end\n";
+
+    @ParameterizedTest(name = "KieBase type={0}")
+    @MethodSource("parameters")
+    public void 
testTP01_mixedExistsAndNot_crossPackageSharing(KieBaseTestConfiguration cfg) {
+        KieBase kbase = KieBaseUtil.getKieBaseFromKieModuleFromDrl("tp01", 
cfg, DRL_TP01_PKG_A, DRL_TP01_PKG_B);
+        KieSession ks = kbase.newKieSession();
+        try {
+            List<String> results = new ArrayList<>();
+            ks.setGlobal("results", results);
+
+            // Initial state: no Container inserted.
+            // not-rule must fire (no matching items), exists-rule must not 
fire.
+            ks.fireAllRules();
+            assertThat(results).as("Empty session: not-rule fires, exists does 
not").containsExactly("not-rule");
+            results.clear();
+
+            // Insert Container with matching item
+            FactHandle fh = ks.insert(new Container("match"));
+            ks.fireAllRules();
+            assertThat(results).as("With matching container: exists-rule 
fires, not does not").containsExactly("exists-rule");
+            results.clear();
+
+            // Retract Container
+            ks.delete(fh);
+            ks.fireAllRules();
+            assertThat(results).as("After retraction: not-rule fires again, 
exists does not").containsExactly("not-rule");
+        } finally {
+            ks.dispose();
+        }
+    }
+
+    // =======================================================================
+    // TP-04: removeRule cleans up stale eager prototype after segment merge
+    // (Toshiya's comment on PR #7116)
+    // =======================================================================
+    @ParameterizedTest(name = "KieBase type={0}")
+    @MethodSource("parameters")
+    public void testTP04_removeSiblingRule_afterTopLevelNegationSegmentSplit(
+            KieBaseTestConfiguration kieBaseTestConfiguration) {
+
+        final String drlSubject =
+                "package repro.b;\n" +
+                "import java.util.ArrayList;\n" +
+                "import java.util.LinkedList;\n" +
+                "rule \"subject\"\n" +
+                "    when\n" +
+                "        String()\n" +
+                "        ArrayList()\n" +
+                "        not LinkedList()\n" +
+                "    then\n" +
+                "end\n";
+
+        final String drlSibling =
+                "package repro.c;\n" +
+                "import java.util.ArrayList;\n" +
+                "import java.util.HashMap;\n" +
+                "rule \"sibling\"\n" +
+                "    when\n" +
+                "        String()\n" +
+                "        ArrayList()\n" +
+                "        HashMap()\n" +
+                "    then\n" +
+                "end\n";
+
+        final InternalKnowledgeBase kbase = (InternalKnowledgeBase) 
KieBaseUtil.getKieBaseFromKieModuleFromDrl(
+                "remove-sibling-top-level-not-test", kieBaseTestConfiguration, 
drlSubject);
+        final KieBase siblingBase = KieBaseUtil.getKieBaseFromKieModuleFromDrl(
+                "remove-sibling-top-level-not-sibling", 
kieBaseTestConfiguration, drlSibling);
+
+        // Add the sibling after the subject's segment exists, forcing a split 
that makes the NotNode segment eager.
+        kbase.addPackages(siblingBase.getKiePackages());
+        // Merging the segments again must remove the obsolete eager prototype 
before a new session is created.
+        kbase.removeRule("repro.c", "sibling");
+
+        final KieSession session = kbase.newKieSession();
+        try {
+            session.insert("start");
+            session.insert(new ArrayList<>());
+            assertThat(session.fireAllRules())
+                    .as("After removing sibling and merging segments, subject 
must still fire")
+                    .isEqualTo(1);
+        } finally {
+            session.dispose();
+        }
+    }
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to