diff --git a/blackbox-global-scope-teardown/pom.xml b/blackbox-global-scope-teardown/pom.xml
new file mode 100644
index 000000000..55b2dbd8b
--- /dev/null
+++ b/blackbox-global-scope-teardown/pom.xml
@@ -0,0 +1,90 @@
+
+
+
+ avaje-inject-parent
+ io.avaje
+ 12.7-RC2
+
+ 4.0.0
+
+ blackbox-global-scope-teardown
+
+
+ Reproduces global test scope corruption with real JVM-global state: ServerStatus is a
+ singleton that registers itself on the platform MBeanServer in PostConstruct and
+ unregisters in PreDestroy, the standard JMX pattern. T1 (plain InjectTest) passes.
+ T2 adds one Mockito mock of an unrelated interface and never references ServerStatus;
+ wiring its test scope rebuilds the entire application beside the live global scope,
+ constructs ServerStatus a second time, and fails with InstanceAlreadyExistsException.
+ Closing that failed scope runs the duplicate's PreDestroy, which unregisters (by
+ ObjectName) the MBean the live global scope owns, so T3 (identical to T1) then fails
+ with InstanceNotFoundException. T1 and T3 pass in isolation; T2 fails even alone.
+
+
+
+
+ io.avaje
+ avaje-inject
+ ${project.version}
+
+
+ jakarta.annotation
+ jakarta.annotation-api
+ 3.0.0
+
+
+ io.avaje
+ avaje-inject-test
+ ${project.version}
+ test
+
+
+ io.avaje
+ junit
+ 1.8
+ test
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+
+
+ io.avaje
+ avaje-inject-generator
+ ${project.version}
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+
+ alphabetical
+
+
+
+
+ io.avaje
+ avaje-inject-maven-plugin
+ ${project.version}
+
+
+
+ process-sources
+
+ provides
+
+
+
+
+
+
+
diff --git a/blackbox-global-scope-teardown/src/main/java/org/example/globalscope/DefaultGreeter.java b/blackbox-global-scope-teardown/src/main/java/org/example/globalscope/DefaultGreeter.java
new file mode 100644
index 000000000..6b49bac50
--- /dev/null
+++ b/blackbox-global-scope-teardown/src/main/java/org/example/globalscope/DefaultGreeter.java
@@ -0,0 +1,11 @@
+package org.example.globalscope;
+
+import jakarta.inject.Singleton;
+
+@Singleton
+public class DefaultGreeter implements Greeter {
+ @Override
+ public String greet() {
+ return "hello";
+ }
+}
diff --git a/blackbox-global-scope-teardown/src/main/java/org/example/globalscope/Greeter.java b/blackbox-global-scope-teardown/src/main/java/org/example/globalscope/Greeter.java
new file mode 100644
index 000000000..822edcc88
--- /dev/null
+++ b/blackbox-global-scope-teardown/src/main/java/org/example/globalscope/Greeter.java
@@ -0,0 +1,5 @@
+package org.example.globalscope;
+
+public interface Greeter {
+ String greet();
+}
diff --git a/blackbox-global-scope-teardown/src/main/java/org/example/globalscope/ServerStatus.java b/blackbox-global-scope-teardown/src/main/java/org/example/globalscope/ServerStatus.java
new file mode 100644
index 000000000..0aaf91915
--- /dev/null
+++ b/blackbox-global-scope-teardown/src/main/java/org/example/globalscope/ServerStatus.java
@@ -0,0 +1,43 @@
+package org.example.globalscope;
+
+import java.lang.management.ManagementFactory;
+
+import javax.management.JMException;
+import javax.management.ObjectName;
+
+import jakarta.annotation.PostConstruct;
+import jakarta.annotation.PreDestroy;
+import jakarta.inject.Singleton;
+
+/**
+ * Publishes a management view of this component on the JVM-wide platform MBeanServer,
+ * the standard JMX pattern (HikariCP pools, Kafka clients, Jetty, and the JDK itself).
+ */
+@Singleton
+public class ServerStatus implements ServerStatusMBean {
+
+ public static final String OBJECT_NAME = "org.example.globalscope:type=ServerStatus";
+
+ @PostConstruct
+ void register() {
+ try {
+ ManagementFactory.getPlatformMBeanServer().registerMBean(this, new ObjectName(OBJECT_NAME));
+ } catch (JMException e) {
+ throw new IllegalStateException("failed to register " + OBJECT_NAME, e);
+ }
+ }
+
+ @PreDestroy
+ void unregister() {
+ try {
+ ManagementFactory.getPlatformMBeanServer().unregisterMBean(new ObjectName(OBJECT_NAME));
+ } catch (JMException e) {
+ throw new IllegalStateException("failed to unregister " + OBJECT_NAME, e);
+ }
+ }
+
+ @Override
+ public String getStatus() {
+ return "OK";
+ }
+}
diff --git a/blackbox-global-scope-teardown/src/main/java/org/example/globalscope/ServerStatusMBean.java b/blackbox-global-scope-teardown/src/main/java/org/example/globalscope/ServerStatusMBean.java
new file mode 100644
index 000000000..fa29e2fa3
--- /dev/null
+++ b/blackbox-global-scope-teardown/src/main/java/org/example/globalscope/ServerStatusMBean.java
@@ -0,0 +1,7 @@
+package org.example.globalscope;
+
+/** Standard MBean interface for {@link ServerStatus}. */
+public interface ServerStatusMBean {
+
+ String getStatus();
+}
diff --git a/blackbox-global-scope-teardown/src/test/java/org/example/globalscope/T1GlobalTest.java b/blackbox-global-scope-teardown/src/test/java/org/example/globalscope/T1GlobalTest.java
new file mode 100644
index 000000000..f3d3e26b3
--- /dev/null
+++ b/blackbox-global-scope-teardown/src/test/java/org/example/globalscope/T1GlobalTest.java
@@ -0,0 +1,23 @@
+package org.example.globalscope;
+
+import io.avaje.inject.test.InjectTest;
+import jakarta.inject.Inject;
+import org.junit.jupiter.api.Test;
+
+import javax.management.ObjectName;
+import java.lang.management.ManagementFactory;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+@InjectTest
+class T1GlobalTest {
+
+ @Inject ServerStatus serverStatus;
+
+ @Test
+ void mbeanIsRegistered() throws Exception {
+ Object status = ManagementFactory.getPlatformMBeanServer()
+ .getAttribute(new ObjectName(ServerStatus.OBJECT_NAME), "Status");
+ assertEquals("OK", status);
+ }
+}
diff --git a/blackbox-global-scope-teardown/src/test/java/org/example/globalscope/T2MockTest.java b/blackbox-global-scope-teardown/src/test/java/org/example/globalscope/T2MockTest.java
new file mode 100644
index 000000000..c48ff52fa
--- /dev/null
+++ b/blackbox-global-scope-teardown/src/test/java/org/example/globalscope/T2MockTest.java
@@ -0,0 +1,25 @@
+package org.example.globalscope;
+
+import io.avaje.inject.test.InjectTest;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mock;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.when;
+
+/**
+ * Never references ServerStatus. Adding one mock of an unrelated interface rebuilds the
+ * entire application beside the live global scope, so ServerStatus is constructed a second
+ * time and its duplicate MBean registration fails the wiring.
+ */
+@InjectTest
+class T2MockTest {
+
+ @Mock Greeter greeter;
+
+ @Test
+ void mockGreeter() {
+ when(greeter.greet()).thenReturn("mocked");
+ assertEquals("mocked", greeter.greet());
+ }
+}
diff --git a/blackbox-global-scope-teardown/src/test/java/org/example/globalscope/T3GlobalAgainTest.java b/blackbox-global-scope-teardown/src/test/java/org/example/globalscope/T3GlobalAgainTest.java
new file mode 100644
index 000000000..9bf76e01e
--- /dev/null
+++ b/blackbox-global-scope-teardown/src/test/java/org/example/globalscope/T3GlobalAgainTest.java
@@ -0,0 +1,28 @@
+package org.example.globalscope;
+
+import io.avaje.inject.test.InjectTest;
+import jakarta.inject.Inject;
+import org.junit.jupiter.api.Test;
+
+import javax.management.ObjectName;
+import java.lang.management.ManagementFactory;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * Same test as T1, back on the shared global scope. Fails after T2: closing the
+ * failed mock scope ran the duplicate ServerStatus preDestroy, which unregistered
+ * (by ObjectName) the MBean the still-live global scope instance owns.
+ */
+@InjectTest
+class T3GlobalAgainTest {
+
+ @Inject ServerStatus serverStatus;
+
+ @Test
+ void mbeanStillRegistered() throws Exception {
+ Object status = ManagementFactory.getPlatformMBeanServer()
+ .getAttribute(new ObjectName(ServerStatus.OBJECT_NAME), "Status");
+ assertEquals("OK", status);
+ }
+}
diff --git a/pom.xml b/pom.xml
index bcb853b9f..15dbe1f97 100644
--- a/pom.xml
+++ b/pom.xml
@@ -55,6 +55,7 @@
blackbox-aspect
blackbox-test-inject
blackbox-multi-scope
+ blackbox-global-scope-teardown