diff --git a/agentscope-harness/src/test/java/io/agentscope/harness/agent/HarnessAgentDistributedSandboxTest.java b/agentscope-harness/src/test/java/io/agentscope/harness/agent/HarnessAgentDistributedSandboxTest.java index d9ead0bf7d..59fb5515c3 100644 --- a/agentscope-harness/src/test/java/io/agentscope/harness/agent/HarnessAgentDistributedSandboxTest.java +++ b/agentscope-harness/src/test/java/io/agentscope/harness/agent/HarnessAgentDistributedSandboxTest.java @@ -31,6 +31,7 @@ import io.agentscope.harness.agent.filesystem.spec.RemoteFilesystemSpec; import io.agentscope.harness.agent.sandbox.impl.docker.DockerFilesystemSpec; import io.agentscope.harness.agent.sandbox.snapshot.LocalSnapshotSpec; +import io.agentscope.harness.agent.testing.HarnessQuiescence; import java.nio.file.Path; import java.util.List; import java.util.Map; @@ -38,6 +39,7 @@ import org.junit.jupiter.api.io.TempDir; import reactor.core.publisher.Flux; +@HarnessQuiescence class HarnessAgentDistributedSandboxTest { @TempDir Path workspace; diff --git a/agentscope-harness/src/test/java/io/agentscope/harness/agent/HarnessAgentDynamicHookBuilderTest.java b/agentscope-harness/src/test/java/io/agentscope/harness/agent/HarnessAgentDynamicHookBuilderTest.java index 12f6183a60..f4b765c34c 100644 --- a/agentscope-harness/src/test/java/io/agentscope/harness/agent/HarnessAgentDynamicHookBuilderTest.java +++ b/agentscope-harness/src/test/java/io/agentscope/harness/agent/HarnessAgentDynamicHookBuilderTest.java @@ -42,6 +42,7 @@ import io.agentscope.harness.agent.filesystem.local.LocalFilesystem; import io.agentscope.harness.agent.middleware.DynamicSubagentsMiddleware; import io.agentscope.harness.agent.middleware.SubagentsMiddleware; +import io.agentscope.harness.agent.testing.HarnessQuiescence; import java.nio.file.Files; import java.nio.file.Path; import java.util.Collections; @@ -72,6 +73,7 @@ *
The contract under test is the middleware list registered on the underlying
* {@code ReActAgent}.
*/
+@HarnessQuiescence
class HarnessAgentDynamicHookBuilderTest {
@TempDir Path workspace;
diff --git a/agentscope-harness/src/test/java/io/agentscope/harness/agent/HarnessAgentIntegrationExampleTest.java b/agentscope-harness/src/test/java/io/agentscope/harness/agent/HarnessAgentIntegrationExampleTest.java
index 3731ec3984..a36516de7e 100644
--- a/agentscope-harness/src/test/java/io/agentscope/harness/agent/HarnessAgentIntegrationExampleTest.java
+++ b/agentscope-harness/src/test/java/io/agentscope/harness/agent/HarnessAgentIntegrationExampleTest.java
@@ -33,6 +33,7 @@
import io.agentscope.core.model.ChatResponse;
import io.agentscope.core.model.Model;
import io.agentscope.harness.agent.middleware.SubagentEntry;
+import io.agentscope.harness.agent.testing.HarnessQuiescence;
import io.agentscope.harness.agent.workspace.WorkspaceConstants;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -66,6 +67,7 @@
* them in the IDE or via JUnit Platform if you add {@code groups} later.
*/
@Tag("integration")
+@HarnessQuiescence
class HarnessAgentIntegrationExampleTest {
@TempDir Path workspace;
diff --git a/agentscope-harness/src/test/java/io/agentscope/harness/agent/HarnessAgentModelStringTest.java b/agentscope-harness/src/test/java/io/agentscope/harness/agent/HarnessAgentModelStringTest.java
index 66ea5ceb0c..0d23db486c 100644
--- a/agentscope-harness/src/test/java/io/agentscope/harness/agent/HarnessAgentModelStringTest.java
+++ b/agentscope-harness/src/test/java/io/agentscope/harness/agent/HarnessAgentModelStringTest.java
@@ -29,6 +29,7 @@
import io.agentscope.harness.agent.filesystem.local.LocalFilesystem;
import io.agentscope.harness.agent.middleware.SubagentEntry;
import io.agentscope.harness.agent.subagent.SubagentDeclaration;
+import io.agentscope.harness.agent.testing.HarnessQuiescence;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
@@ -39,6 +40,7 @@
import org.junit.jupiter.api.io.TempDir;
import reactor.core.publisher.Flux;
+@HarnessQuiescence
class HarnessAgentModelStringTest {
@TempDir Path workspace;
diff --git a/agentscope-harness/src/test/java/io/agentscope/harness/agent/HarnessAgentSubagentStreamEventsTest.java b/agentscope-harness/src/test/java/io/agentscope/harness/agent/HarnessAgentSubagentStreamEventsTest.java
index c675f241e8..115dae5351 100644
--- a/agentscope-harness/src/test/java/io/agentscope/harness/agent/HarnessAgentSubagentStreamEventsTest.java
+++ b/agentscope-harness/src/test/java/io/agentscope/harness/agent/HarnessAgentSubagentStreamEventsTest.java
@@ -35,6 +35,7 @@
import io.agentscope.core.model.ChatResponse;
import io.agentscope.core.model.Model;
import io.agentscope.harness.agent.filesystem.local.LocalFilesystem;
+import io.agentscope.harness.agent.testing.HarnessQuiescence;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration;
@@ -56,6 +57,7 @@
* ({@code Flux Configure via {@link HarnessAgent.Builder#abstractFilesystem} with a
* {@link LocalFilesystemWithShell} instance pointing at your desired workspace directory.
*/
+@HarnessQuiescence
class LocalFilesystemPersonalAssistantExampleTest {
@TempDir Path workspace;
diff --git a/agentscope-harness/src/test/java/io/agentscope/harness/agent/example/LocalFilesystemUserIsolationExampleTest.java b/agentscope-harness/src/test/java/io/agentscope/harness/agent/example/LocalFilesystemUserIsolationExampleTest.java
index cc6ae6bdb3..5acf74da21 100644
--- a/agentscope-harness/src/test/java/io/agentscope/harness/agent/example/LocalFilesystemUserIsolationExampleTest.java
+++ b/agentscope-harness/src/test/java/io/agentscope/harness/agent/example/LocalFilesystemUserIsolationExampleTest.java
@@ -36,6 +36,7 @@
import io.agentscope.harness.agent.filesystem.model.GlobResult;
import io.agentscope.harness.agent.filesystem.model.GrepResult;
import io.agentscope.harness.agent.filesystem.model.ReadResult;
+import io.agentscope.harness.agent.testing.HarnessQuiescence;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -62,6 +63,7 @@
* {@link io.agentscope.harness.agent.HarnessAgent#close()} drains the same trackers, but
+ * many harness tests build a transient {@code HarnessAgent}, call {@code .block()}, and let it
+ * be garbage-collected without closing it. The fire-and-forget memory flush dispatched in
+ * {@code MemoryFlushMiddleware#onAgent}'s {@code doOnComplete} then races with JUnit's
+ * {@code @TempDir} teardown: the async write still holds file handles (or creates files after
+ * the walk) when the temp directory is deleted, producing the flaky
+ * {@code "Failed to delete temp directory"} / {@code "Failed to close extension context"}
+ * errors seen across harness integration tests on both Linux and Windows runners.
+ *
+ * This callback runs after the test method and after {@code @AfterEach} methods, but
+ * before the JUnit {@code TempDir} extension closes the extension context and deletes
+ * the temp directory, so the background writes have quiesced first. When nothing is in flight
+ * both {@code await*} calls return immediately, making this a no-op for tests that never
+ * trigger a flush. If the trackers fail to quiesce within the timeout (or the thread is
+ * interrupted), the callback throws {@link AssertionError} so the failure is deterministic and
+ * points at the root cause rather than surfacing later as an opaque
+ * {@code "Failed to delete temp directory"}.
+ */
+public class HarnessBackgroundTaskQuiescenceExtension implements AfterEachCallback {
+
+ private static final long TIMEOUT_SECONDS = 5;
+
+ @Override
+ public void afterEach(ExtensionContext context) {
+ boolean mirrorsQuiet = SessionTree.awaitMirrorQuiescence(TIMEOUT_SECONDS, TimeUnit.SECONDS);
+ boolean flushQuiet =
+ MemoryBackgroundTasks.awaitQuiescence(TIMEOUT_SECONDS, TimeUnit.SECONDS);
+ if (!mirrorsQuiet || !flushQuiet) {
+ throw new AssertionError(
+ "Harness background tasks did not quiesce within "
+ + TIMEOUT_SECONDS
+ + "s; fire-and-forget memory flush may still race with @TempDir"
+ + " teardown");
+ }
+ }
+}
diff --git a/agentscope-harness/src/test/java/io/agentscope/harness/agent/testing/HarnessQuiescence.java b/agentscope-harness/src/test/java/io/agentscope/harness/agent/testing/HarnessQuiescence.java
new file mode 100644
index 0000000000..7e7d5b129c
--- /dev/null
+++ b/agentscope-harness/src/test/java/io/agentscope/harness/agent/testing/HarnessQuiescence.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2024-2026 the original author or authors.
+ *
+ * Licensed 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 io.agentscope.harness.agent.testing;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+/**
+ * Composed annotation that registers {@link HarnessBackgroundTaskQuiescenceExtension} so
+ * fire-and-forget harness background tasks (memory flush/maintenance, session/transcript
+ * mirrors) are drained after each test, before JUnit deletes its {@code @TempDir}.
+ *
+ * Apply to any test that builds a {@code HarnessAgent}, drives it to completion
+ * ({@code .block()} / {@code .stream()...block()}), and uses {@code @TempDir} for the
+ * workspace or state home. Without this, the async memory flush dispatched in
+ * {@code MemoryFlushMiddleware#onAgent}'s {@code doOnComplete} races with
+ * {@code @TempDir} teardown and surfaces as the flaky
+ * {@code "Failed to delete temp directory"} / {@code "Failed to close extension context"}
+ * error. Calling {@code HarnessAgent.close()} has the same effect; this annotation covers
+ * tests that build a transient agent and never close it.
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+@ExtendWith(HarnessBackgroundTaskQuiescenceExtension.class)
+public @interface HarnessQuiescence {}
diff --git a/agentscope-harness/src/test/java/io/agentscope/harness/agent/tools/HarnessAgentToolsConfigTest.java b/agentscope-harness/src/test/java/io/agentscope/harness/agent/tools/HarnessAgentToolsConfigTest.java
index 30e5e3a577..e114f29495 100644
--- a/agentscope-harness/src/test/java/io/agentscope/harness/agent/tools/HarnessAgentToolsConfigTest.java
+++ b/agentscope-harness/src/test/java/io/agentscope/harness/agent/tools/HarnessAgentToolsConfigTest.java
@@ -28,6 +28,7 @@
import io.agentscope.core.model.ToolSchema;
import io.agentscope.harness.agent.HarnessAgent;
import io.agentscope.harness.agent.filesystem.local.LocalFilesystem;
+import io.agentscope.harness.agent.testing.HarnessQuiescence;
import io.agentscope.harness.agent.workspace.WorkspaceConstants;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -37,6 +38,7 @@
import org.junit.jupiter.api.io.TempDir;
import reactor.core.publisher.Flux;
+@HarnessQuiescence
class HarnessAgentToolsConfigTest {
@TempDir Path workspace;