From 668d157b3432766be357e006abe0630f8abbaf63 Mon Sep 17 00:00:00 2001 From: yuluo-yx Date: Wed, 11 Nov 2026 23:47:41 +0800 Subject: [PATCH 1/2] test(agent): make lease-loss cancellation checks deterministic --- .../node/AgentToolNodeExecutionLeaseTest.java | 45 ++++++++++++++----- 1 file changed, 35 insertions(+), 10 deletions(-) diff --git a/argi-orchestration/argi-agent-framework/src/test/java/io/github/agentic/ai/agent/orchestration/node/AgentToolNodeExecutionLeaseTest.java b/argi-orchestration/argi-agent-framework/src/test/java/io/github/agentic/ai/agent/orchestration/node/AgentToolNodeExecutionLeaseTest.java index f0507e2f7..200f218cd 100644 --- a/argi-orchestration/argi-agent-framework/src/test/java/io/github/agentic/ai/agent/orchestration/node/AgentToolNodeExecutionLeaseTest.java +++ b/argi-orchestration/argi-agent-framework/src/test/java/io/github/agentic/ai/agent/orchestration/node/AgentToolNodeExecutionLeaseTest.java @@ -33,6 +33,8 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import java.time.Duration; import java.util.ArrayList; @@ -201,11 +203,23 @@ void queuedParallelToolDoesNotStartAfterLeaseLoss() throws Exception { assertEquals(0, delegateCalls.get(), "queued delegate must not start after guard loss"); } - @Test - void activeCancellableToolGetsCancellationOnLeaseLossAndLateOutputIsNotMerged() throws Exception { + @ParameterizedTest + @ValueSource(booleans = { false, true }) + void activeCancellableToolGetsCancellationOnLeaseLossAndLateOutputIsNotMerged(boolean delayStageReturn) + throws Exception { ManualGuard guard = new ManualGuard(); CountDownLatch toolStarted = new CountDownLatch(1); + CountDownLatch toolCancelled = new CountDownLatch(1); + CompletableFuture releaseStage = new CompletableFuture<>(); + if (!delayStageReturn) { + releaseStage.complete(null); + } CompletableFuture original = new CompletableFuture<>(); + original.whenComplete((value, failure) -> { + if (original.isCancelled()) { + toolCancelled.countDown(); + } + }); Tool tool = new Tool() { @Override public ToolDefinition definition() { @@ -215,6 +229,8 @@ public ToolDefinition definition() { @Override public CompletionStage execute(ToolCall call, ToolContext context) { toolStarted.countDown(); + // Model a tool that has started but has not handed its stage to the node. + releaseStage.join(); return original; } }; @@ -229,14 +245,23 @@ public CompletionStage execute(ToolCall call, ToolContext context) { throw new CompletionException(ex); } }, invocationExecutor); - assertTrue(toolStarted.await(5, TimeUnit.SECONDS), "tool must start before ownership is lost"); - guard.lose("active async tool lost ownership"); - - ExecutionException error = assertThrows(ExecutionException.class, () -> result.get(5, TimeUnit.SECONDS)); - LeaseLostException loss = assertRootLeaseLoss(error); - assertEquals("active async tool lost ownership", loss.getReason()); - assertTrue(original.isCancelled(), "lease loss must request original stage cancellation"); - assertTrue(!original.complete(ToolResult.text("late")), "late output must not be published"); + try { + assertTrue(toolStarted.await(5, TimeUnit.SECONDS), "tool must start before ownership is lost"); + guard.lose("active async tool lost ownership"); + + ExecutionException error = assertThrows(ExecutionException.class, () -> result.get(5, TimeUnit.SECONDS)); + LeaseLostException loss = assertRootLeaseLoss(error); + assertEquals("active async tool lost ownership", loss.getReason()); + // Lease loss can end the node before the tool returns its stage. Once the + // stage is available, wait for cancellation propagation before inspecting it. + releaseStage.complete(null); + assertTrue(toolCancelled.await(5, TimeUnit.SECONDS), "original stage must receive cancellation"); + assertTrue(original.isCancelled(), "lease loss must request original stage cancellation"); + assertTrue(!original.complete(ToolResult.text("late")), "late output must not be published"); + } + finally { + releaseStage.complete(null); + } } private static AgentToolNode.Builder builder() { From 2fe0848cca519ca957d300676adf72ac688bda28 Mon Sep 17 00:00:00 2001 From: yuluo-yx Date: Thu, 12 Nov 2026 22:22:36 +0800 Subject: [PATCH 2/2] fix(ci): install pinned ripgrep without Ubuntu mirrors --- .github/workflows/build-and-test.yml | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index c6a1248a6..1c4547c38 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -55,9 +55,18 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: ./tools/github-actions/setup-deps - name: Install ripgrep + shell: bash run: | - sudo apt-get update - sudo apt-get install --yes ripgrep + # Use the pinned upstream binary to avoid Ubuntu mirror DNS failures. + archive="$RUNNER_TEMP/ripgrep-13.0.0-x86_64-unknown-linux-musl.tar.gz" + curl --fail --location --retry 2 --connect-timeout 20 --max-time 180 \ + --output "$archive" \ + https://github.com/BurntSushi/ripgrep/releases/download/13.0.0/ripgrep-13.0.0-x86_64-unknown-linux-musl.tar.gz + printf '%s %s\n' ee4e0751ab108b6da4f47c52da187d5177dc371f0f512a7caaec5434e711c091 "$archive" \ + | sha256sum --check + tar -xzf "$archive" -C "$RUNNER_TEMP" ripgrep-13.0.0-x86_64-unknown-linux-musl/rg + sudo install -m 0755 "$RUNNER_TEMP/ripgrep-13.0.0-x86_64-unknown-linux-musl/rg" /usr/local/bin/rg + rg --version - run: make tools - run: ./mvnw -B install build: