Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,15 @@
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;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import reactor.core.publisher.Flux;

@HarnessQuiescence
class HarnessAgentDistributedSandboxTest {

@TempDir Path workspace;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -72,6 +73,7 @@
* <p>The contract under test is the middleware list registered on the underlying
* {@code ReActAgent}.
*/
@HarnessQuiescence
class HarnessAgentDynamicHookBuilderTest {

@TempDir Path workspace;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -39,6 +40,7 @@
import org.junit.jupiter.api.io.TempDir;
import reactor.core.publisher.Flux;

@HarnessQuiescence
class HarnessAgentModelStringTest {

@TempDir Path workspace;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -56,6 +57,7 @@
* ({@code Flux<AgentEvent>}) path instead of the deprecated {@code stream()} ({@code Flux<Event>})
* path.
*/
@HarnessQuiescence
class HarnessAgentSubagentStreamEventsTest {

@TempDir Path workspace;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -65,6 +66,7 @@
* <p>Configure via {@link HarnessAgent.Builder#abstractFilesystem} with a
* {@link LocalFilesystemWithShell} instance pointing at your desired workspace directory.
*/
@HarnessQuiescence
class LocalFilesystemPersonalAssistantExampleTest {

@TempDir Path workspace;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -62,6 +63,7 @@
* <li>Glob/ls/grep return round-trippable paths (no double namespace on read).</li>
* </ul>
*/
@HarnessQuiescence
class LocalFilesystemUserIsolationExampleTest {

@TempDir Path workspace;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 <em>and</em> loaded node A's prior turn from the shared state store.
*/
@HarnessQuiescence
class SubagentRegistryRecoveryIntegrationTest {

@TempDir Path workspace;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
* 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.
*
* <p>{@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.
*
* <p>This callback runs after the test method and after {@code @AfterEach} methods, but
* <em>before</em> 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");
}
}
}
Original file line number Diff line number Diff line change
@@ -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}.
*
* <p>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 {}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -37,6 +38,7 @@
import org.junit.jupiter.api.io.TempDir;
import reactor.core.publisher.Flux;

@HarnessQuiescence
class HarnessAgentToolsConfigTest {

@TempDir Path workspace;
Expand Down
Loading