From eab83faf0ddbd4392f333a3b97bb891a676e04e5 Mon Sep 17 00:00:00 2001 From: jujn <2087687391@qq.com> Date: Wed, 2 Sep 2026 13:34:03 +0800 Subject: [PATCH 1/2] test(harness): drain fire-and-forget memory flush before @TempDir teardown to stop flaky temp-dir deletion --- .../HarnessAgentDistributedSandboxTest.java | 2 + .../HarnessAgentDynamicHookBuilderTest.java | 2 + .../HarnessAgentIntegrationExampleTest.java | 2 + .../agent/HarnessAgentModelStringTest.java | 2 + .../HarnessAgentSubagentStreamEventsTest.java | 2 + .../agent/HarnessAgentSubagentStreamTest.java | 2 + .../harness/agent/HarnessAgentTest.java | 2 + .../agent/JsonSessionDefaultLocationTest.java | 2 + .../PlanModeSubagentPropagationTest.java | 2 + ...ilesystemPersonalAssistantExampleTest.java | 2 + ...calFilesystemUserIsolationExampleTest.java | 2 + ...teFilesystemIsolationScopeExampleTest.java | 2 + ...oxFilesystemIsolationScopeExampleTest.java | 2 + ...bagentRegistryRecoveryIntegrationTest.java | 2 + .../SubagentIsolationIntegrationTest.java | 2 + ...nessBackgroundTaskQuiescenceExtension.java | 52 +++++++++++++++++++ .../agent/testing/HarnessQuiescence.java | 41 +++++++++++++++ .../tools/HarnessAgentToolsConfigTest.java | 2 + 18 files changed, 125 insertions(+) create mode 100644 agentscope-harness/src/test/java/io/agentscope/harness/agent/testing/HarnessBackgroundTaskQuiescenceExtension.java create mode 100644 agentscope-harness/src/test/java/io/agentscope/harness/agent/testing/HarnessQuiescence.java 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}) path instead of the deprecated {@code stream()} ({@code Flux}) * path. */ +@HarnessQuiescence class HarnessAgentSubagentStreamEventsTest { @TempDir Path workspace; diff --git a/agentscope-harness/src/test/java/io/agentscope/harness/agent/HarnessAgentSubagentStreamTest.java b/agentscope-harness/src/test/java/io/agentscope/harness/agent/HarnessAgentSubagentStreamTest.java index 5031cdad66..f202e0bbad 100644 --- a/agentscope-harness/src/test/java/io/agentscope/harness/agent/HarnessAgentSubagentStreamTest.java +++ b/agentscope-harness/src/test/java/io/agentscope/harness/agent/HarnessAgentSubagentStreamTest.java @@ -37,6 +37,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.util.List; @@ -61,6 +62,7 @@ * child → parent turn 2) yields the appropriate {@link ChatResponse}. This mirrors how * {@code buildDeclaredFactory} captures {@code this.model} for child agents. */ +@HarnessQuiescence class HarnessAgentSubagentStreamTest { @TempDir Path workspace; diff --git a/agentscope-harness/src/test/java/io/agentscope/harness/agent/HarnessAgentTest.java b/agentscope-harness/src/test/java/io/agentscope/harness/agent/HarnessAgentTest.java index c3d30ed225..331464ef39 100644 --- a/agentscope-harness/src/test/java/io/agentscope/harness/agent/HarnessAgentTest.java +++ b/agentscope-harness/src/test/java/io/agentscope/harness/agent/HarnessAgentTest.java @@ -64,6 +64,7 @@ import io.agentscope.harness.agent.subagent.AgentSpecLoader; import io.agentscope.harness.agent.subagent.SubagentDeclaration; import io.agentscope.harness.agent.subagent.WorkspaceMode; +import io.agentscope.harness.agent.testing.HarnessQuiescence; import io.agentscope.harness.agent.workspace.WorkspaceConstants; import java.io.IOException; import java.nio.file.Files; @@ -89,6 +90,7 @@ * Tests for {@link HarnessAgent} workspace wiring: {@code AGENTS.md} context and subagent * discovery ({@code subagents/*.md}). */ +@HarnessQuiescence class HarnessAgentTest { @TempDir Path workspace; diff --git a/agentscope-harness/src/test/java/io/agentscope/harness/agent/JsonSessionDefaultLocationTest.java b/agentscope-harness/src/test/java/io/agentscope/harness/agent/JsonSessionDefaultLocationTest.java index a56909034e..e63d0a967e 100644 --- a/agentscope-harness/src/test/java/io/agentscope/harness/agent/JsonSessionDefaultLocationTest.java +++ b/agentscope-harness/src/test/java/io/agentscope/harness/agent/JsonSessionDefaultLocationTest.java @@ -29,6 +29,7 @@ import io.agentscope.core.message.TextBlock; import io.agentscope.core.model.ChatResponse; import io.agentscope.core.model.Model; +import io.agentscope.harness.agent.testing.HarnessQuiescence; import java.nio.file.Files; import java.nio.file.Path; import java.util.List; @@ -50,6 +51,7 @@ * TempDir} so we can both (a) assert state lands at the expected location and (b) avoid sharing * state across tests / polluting the surefire-shared {@code target/test-state-home/}. */ +@HarnessQuiescence class JsonSessionDefaultLocationTest { @TempDir Path stateHome; diff --git a/agentscope-harness/src/test/java/io/agentscope/harness/agent/PlanModeSubagentPropagationTest.java b/agentscope-harness/src/test/java/io/agentscope/harness/agent/PlanModeSubagentPropagationTest.java index e383940eab..da57f813d4 100644 --- a/agentscope-harness/src/test/java/io/agentscope/harness/agent/PlanModeSubagentPropagationTest.java +++ b/agentscope-harness/src/test/java/io/agentscope/harness/agent/PlanModeSubagentPropagationTest.java @@ -31,6 +31,7 @@ import io.agentscope.harness.agent.middleware.SubagentEntry; import io.agentscope.harness.agent.subagent.SubagentDeclaration; import io.agentscope.harness.agent.subagent.WorkspaceMode; +import io.agentscope.harness.agent.testing.HarnessQuiescence; import java.nio.file.Files; import java.nio.file.Path; import java.util.List; @@ -41,6 +42,7 @@ import reactor.core.publisher.Flux; /** Regression coverage for plan-mode capabilities inherited by automatic subagent factories. */ +@HarnessQuiescence class PlanModeSubagentPropagationTest { private static final String PLAN_DIR = "review-plans"; diff --git a/agentscope-harness/src/test/java/io/agentscope/harness/agent/example/LocalFilesystemPersonalAssistantExampleTest.java b/agentscope-harness/src/test/java/io/agentscope/harness/agent/example/LocalFilesystemPersonalAssistantExampleTest.java index 7e59decf27..e7710de45e 100644 --- a/agentscope-harness/src/test/java/io/agentscope/harness/agent/example/LocalFilesystemPersonalAssistantExampleTest.java +++ b/agentscope-harness/src/test/java/io/agentscope/harness/agent/example/LocalFilesystemPersonalAssistantExampleTest.java @@ -31,6 +31,7 @@ import io.agentscope.core.model.Model; import io.agentscope.harness.agent.HarnessAgent; import io.agentscope.harness.agent.filesystem.local.LocalFilesystemWithShell; +import io.agentscope.harness.agent.testing.HarnessQuiescence; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; @@ -65,6 +66,7 @@ *

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 @@ *

  • Glob/ls/grep return round-trippable paths (no double namespace on read).
  • * */ +@HarnessQuiescence class LocalFilesystemUserIsolationExampleTest { @TempDir Path workspace; diff --git a/agentscope-harness/src/test/java/io/agentscope/harness/agent/example/RemoteFilesystemIsolationScopeExampleTest.java b/agentscope-harness/src/test/java/io/agentscope/harness/agent/example/RemoteFilesystemIsolationScopeExampleTest.java index 3fd0fe28f9..d2b4b812ee 100644 --- a/agentscope-harness/src/test/java/io/agentscope/harness/agent/example/RemoteFilesystemIsolationScopeExampleTest.java +++ b/agentscope-harness/src/test/java/io/agentscope/harness/agent/example/RemoteFilesystemIsolationScopeExampleTest.java @@ -35,6 +35,7 @@ import io.agentscope.harness.agent.IsolationScope; import io.agentscope.harness.agent.filesystem.remote.store.InMemoryStore; import io.agentscope.harness.agent.filesystem.spec.RemoteFilesystemSpec; +import io.agentscope.harness.agent.testing.HarnessQuiescence; import java.nio.file.Files; import java.nio.file.Path; import java.util.List; @@ -75,6 +76,7 @@ * directly where possible to keep the example focused on namespace routing rather than agent * conversation mechanics. */ +@HarnessQuiescence class RemoteFilesystemIsolationScopeExampleTest { @TempDir Path workspace; diff --git a/agentscope-harness/src/test/java/io/agentscope/harness/agent/example/SandboxFilesystemIsolationScopeExampleTest.java b/agentscope-harness/src/test/java/io/agentscope/harness/agent/example/SandboxFilesystemIsolationScopeExampleTest.java index af81a7dd35..9fec52dd4d 100644 --- a/agentscope-harness/src/test/java/io/agentscope/harness/agent/example/SandboxFilesystemIsolationScopeExampleTest.java +++ b/agentscope-harness/src/test/java/io/agentscope/harness/agent/example/SandboxFilesystemIsolationScopeExampleTest.java @@ -31,6 +31,7 @@ import io.agentscope.harness.agent.IsolationScope; import io.agentscope.harness.agent.example.support.InMemorySandboxClient; import io.agentscope.harness.agent.example.support.InMemorySandboxFilesystemSpec; +import io.agentscope.harness.agent.testing.HarnessQuiescence; import java.nio.file.Files; import java.nio.file.Path; import java.util.List; @@ -65,6 +66,7 @@ * step. The assertions count {@link InMemorySandboxClient#getCreateCount()} and * {@link InMemorySandboxClient#getResumeCount()} to verify isolation behaviour. */ +@HarnessQuiescence class SandboxFilesystemIsolationScopeExampleTest { @TempDir Path workspace; diff --git a/agentscope-harness/src/test/java/io/agentscope/harness/agent/gateway/SubagentRegistryRecoveryIntegrationTest.java b/agentscope-harness/src/test/java/io/agentscope/harness/agent/gateway/SubagentRegistryRecoveryIntegrationTest.java index da4b8ba187..033adda20a 100644 --- a/agentscope-harness/src/test/java/io/agentscope/harness/agent/gateway/SubagentRegistryRecoveryIntegrationTest.java +++ b/agentscope-harness/src/test/java/io/agentscope/harness/agent/gateway/SubagentRegistryRecoveryIntegrationTest.java @@ -31,6 +31,7 @@ import io.agentscope.harness.agent.filesystem.local.LocalFilesystem; import io.agentscope.harness.agent.filesystem.remote.store.BaseStore; import io.agentscope.harness.agent.filesystem.remote.store.InMemoryStore; +import io.agentscope.harness.agent.testing.HarnessQuiescence; import java.nio.file.Path; import java.time.Instant; import java.util.ArrayList; @@ -60,6 +61,7 @@ * sees in its context: node B observing {@code turns=2} can only happen if it resolved the handle * from the shared registry and loaded node A's prior turn from the shared state store. */ +@HarnessQuiescence class SubagentRegistryRecoveryIntegrationTest { @TempDir Path workspace; diff --git a/agentscope-harness/src/test/java/io/agentscope/harness/agent/subagent/SubagentIsolationIntegrationTest.java b/agentscope-harness/src/test/java/io/agentscope/harness/agent/subagent/SubagentIsolationIntegrationTest.java index 96ae688ef5..6da7ba69aa 100644 --- a/agentscope-harness/src/test/java/io/agentscope/harness/agent/subagent/SubagentIsolationIntegrationTest.java +++ b/agentscope-harness/src/test/java/io/agentscope/harness/agent/subagent/SubagentIsolationIntegrationTest.java @@ -24,6 +24,7 @@ import io.agentscope.core.agent.test.MockModel; import io.agentscope.harness.agent.HarnessAgent; import io.agentscope.harness.agent.middleware.SubagentEntry; +import io.agentscope.harness.agent.testing.HarnessQuiescence; import java.nio.file.Files; import java.nio.file.Path; import java.util.List; @@ -37,6 +38,7 @@ * (userId, sessionId), distinct IDs are sufficient to guarantee state isolation across all * configured AgentStateStore stores. */ +@HarnessQuiescence class SubagentIsolationIntegrationTest { @TempDir Path workspace; diff --git a/agentscope-harness/src/test/java/io/agentscope/harness/agent/testing/HarnessBackgroundTaskQuiescenceExtension.java b/agentscope-harness/src/test/java/io/agentscope/harness/agent/testing/HarnessBackgroundTaskQuiescenceExtension.java new file mode 100644 index 0000000000..e1608882db --- /dev/null +++ b/agentscope-harness/src/test/java/io/agentscope/harness/agent/testing/HarnessBackgroundTaskQuiescenceExtension.java @@ -0,0 +1,52 @@ +/* + * 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 io.agentscope.harness.agent.memory.MemoryBackgroundTasks; +import io.agentscope.harness.agent.memory.session.SessionTree; +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.extension.AfterEachCallback; +import org.junit.jupiter.api.extension.ExtensionContext; + +/** + * Auto-registered JUnit Jupiter extension that drains fire-and-forget harness background + * tasks (memory flush/maintenance and session/transcript mirrors) after every test method. + * + *

    {@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. + */ +public class HarnessBackgroundTaskQuiescenceExtension implements AfterEachCallback { + + private static final long TIMEOUT_SECONDS = 5; + + @Override + public void afterEach(ExtensionContext context) { + SessionTree.awaitMirrorQuiescence(TIMEOUT_SECONDS, TimeUnit.SECONDS); + MemoryBackgroundTasks.awaitQuiescence(TIMEOUT_SECONDS, TimeUnit.SECONDS); + } +} 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; From 9878549f34b1569cf0fba668c5dffb2adccc9941 Mon Sep 17 00:00:00 2001 From: jujn <2087687391@qq.com> Date: Wed, 2 Sep 2026 15:18:23 +0800 Subject: [PATCH 2/2] test(harness): fail test when background tasks do not quiesce within timeout --- ...arnessBackgroundTaskQuiescenceExtension.java | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/agentscope-harness/src/test/java/io/agentscope/harness/agent/testing/HarnessBackgroundTaskQuiescenceExtension.java b/agentscope-harness/src/test/java/io/agentscope/harness/agent/testing/HarnessBackgroundTaskQuiescenceExtension.java index e1608882db..bae991cc4e 100644 --- a/agentscope-harness/src/test/java/io/agentscope/harness/agent/testing/HarnessBackgroundTaskQuiescenceExtension.java +++ b/agentscope-harness/src/test/java/io/agentscope/harness/agent/testing/HarnessBackgroundTaskQuiescenceExtension.java @@ -38,7 +38,10 @@ * 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. + * 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 { @@ -46,7 +49,15 @@ public class HarnessBackgroundTaskQuiescenceExtension implements AfterEachCallba @Override public void afterEach(ExtensionContext context) { - SessionTree.awaitMirrorQuiescence(TIMEOUT_SECONDS, TimeUnit.SECONDS); - MemoryBackgroundTasks.awaitQuiescence(TIMEOUT_SECONDS, TimeUnit.SECONDS); + 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"); + } } }