From 0115fc78aedef72fc109483a66a614b0824786a1 Mon Sep 17 00:00:00 2001 From: Ed Burns Date: Thu, 25 Jun 2026 12:36:27 -0400 Subject: [PATCH 1/4] Fix Java tool-processor test generation and stabilize session-id test Address the Java test failures observed in the Java 17 surefire/failsafe run by fixing how annotation-processing output is discovered in CopilotToolProcessor tests and by hardening one timing-sensitive session test. Changes included: - CopilotToolProcessor: resolve @CopilotTool elements via TypeElement lookup and reuse that element list through validation and generation passes, making annotation discovery robust across compiler/module contexts. - CopilotToolProcessorTest: force annotation processing in the in-memory compile harness (-proc:full, explicit processor), close the file manager with try-with-resources, and add a collecting forwarding file manager that captures generated source content from getJavaFileForOutput to avoid missing generated CopilotToolMeta classes in tests. - CopilotSessionTest#testShouldGetLastSessionId: add bounded retry for session creation (including timeout and execution-timeout-cause handling) to absorb transient startup delays while preserving failure behavior on persistent errors. Result: - CopilotToolProcessorTest now consistently observes generated CopilotToolMeta output and passes. - The full requested Maven workflow (jacoco prepare/report + surefire + failsafe under Java 17, with prior Java 25 compile) completes successfully. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../copilot/tool/CopilotToolProcessor.java | 14 +++- .../github/copilot/CopilotSessionTest.java | 20 ++++- .../tool/CopilotToolProcessorTest.java | 73 +++++++++++++++++-- 3 files changed, 96 insertions(+), 11 deletions(-) diff --git a/java/src/main/java/com/github/copilot/tool/CopilotToolProcessor.java b/java/src/main/java/com/github/copilot/tool/CopilotToolProcessor.java index eb5182472f..27d05eae5b 100644 --- a/java/src/main/java/com/github/copilot/tool/CopilotToolProcessor.java +++ b/java/src/main/java/com/github/copilot/tool/CopilotToolProcessor.java @@ -49,7 +49,8 @@ public class CopilotToolProcessor extends AbstractProcessor { @Override public boolean process(Set annotations, RoundEnvironment roundEnv) { - for (Element element : roundEnv.getElementsAnnotatedWith(CopilotTool.class)) { + List annotatedElements = getCopilotToolAnnotatedElements(roundEnv); + for (Element element : annotatedElements) { if (element.getKind() != ElementKind.METHOD) { continue; } @@ -75,7 +76,7 @@ public boolean process(Set annotations, RoundEnvironment // Group methods by enclosing type Map> methodsByClass = new LinkedHashMap<>(); - for (Element element : roundEnv.getElementsAnnotatedWith(CopilotTool.class)) { + for (Element element : annotatedElements) { if (element.getKind() != ElementKind.METHOD) { continue; } @@ -95,6 +96,15 @@ public boolean process(Set annotations, RoundEnvironment return false; } + private List getCopilotToolAnnotatedElements(RoundEnvironment roundEnv) { + TypeElement copilotToolType = processingEnv.getElementUtils() + .getTypeElement("com.github.copilot.tool.CopilotTool"); + if (copilotToolType != null) { + return new ArrayList<>(roundEnv.getElementsAnnotatedWith(copilotToolType)); + } + return new ArrayList<>(roundEnv.getElementsAnnotatedWith(CopilotTool.class)); + } + private void generateMetaClass(TypeElement classElement, List methods) { String packageName = processingEnv.getElementUtils().getPackageOf(classElement).getQualifiedName().toString(); String simpleClassName = classElement.getSimpleName().toString(); diff --git a/java/src/test/java/com/github/copilot/CopilotSessionTest.java b/java/src/test/java/com/github/copilot/CopilotSessionTest.java index 44a7373ec7..9b41a27e58 100644 --- a/java/src/test/java/com/github/copilot/CopilotSessionTest.java +++ b/java/src/test/java/com/github/copilot/CopilotSessionTest.java @@ -756,8 +756,24 @@ void testShouldGetLastSessionId() throws Exception { ctx.configureForTest("session", "should_get_last_session_id"); try (CopilotClient client = ctx.createClient()) { - CopilotSession session = client - .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); + CopilotSession session = null; + for (int attempt = 1; attempt <= 2; attempt++) { + try { + session = client + .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)) + .get(45, TimeUnit.SECONDS); + break; + } catch (java.util.concurrent.TimeoutException e) { + if (attempt == 2) { + throw e; + } + } catch (java.util.concurrent.ExecutionException e) { + if (!(e.getCause() instanceof java.util.concurrent.TimeoutException) || attempt == 2) { + throw e; + } + } + } + assertNotNull(session, "Session should be created"); session.sendAndWait(new MessageOptions().setPrompt("Say hello")).get(60, TimeUnit.SECONDS); String sessionId = session.getSessionId(); diff --git a/java/src/test/java/com/github/copilot/tool/CopilotToolProcessorTest.java b/java/src/test/java/com/github/copilot/tool/CopilotToolProcessorTest.java index af4481ac19..b431470e5c 100644 --- a/java/src/test/java/com/github/copilot/tool/CopilotToolProcessorTest.java +++ b/java/src/test/java/com/github/copilot/tool/CopilotToolProcessorTest.java @@ -10,16 +10,24 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.File; +import java.io.FilterWriter; +import java.io.IOException; +import java.io.Writer; import java.net.URI; import java.nio.file.Path; import java.security.CodeSource; import java.util.ArrayList; +import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; +import java.util.Map; import java.util.Set; import javax.tools.Diagnostic; import javax.tools.DiagnosticCollector; +import javax.tools.FileObject; +import javax.tools.ForwardingJavaFileManager; +import javax.tools.ForwardingJavaFileObject; import javax.tools.JavaCompiler; import javax.tools.JavaFileObject; import javax.tools.SimpleJavaFileObject; @@ -540,25 +548,28 @@ private CompilationResult compileWithProcessor(List sources) { String classpath = resolveClasspath(); List options = new ArrayList<>(); + options.add("-proc:full"); + options.addAll(List.of("-processor", "com.github.copilot.tool.CopilotToolProcessor")); options.addAll(List.of("-classpath", classpath)); options.addAll(List.of("-d", tempDir.toString())); options.addAll(List.of("-s", tempDir.toString())); // Allow experimental APIs during test compilation options.add("-Acopilot.experimental.allowed=true"); - try { - StandardJavaFileManager fileManager = compiler.getStandardFileManager(diagnostics, null, null); + try (StandardJavaFileManager fileManager = compiler.getStandardFileManager(diagnostics, null, null)) { fileManager.setLocation(StandardLocation.SOURCE_OUTPUT, List.of(tempDir.toFile())); fileManager.setLocation(StandardLocation.CLASS_OUTPUT, List.of(tempDir.toFile())); + CollectingFileManager collectingFileManager = new CollectingFileManager(fileManager); - JavaCompiler.CompilationTask task = compiler.getTask(null, fileManager, diagnostics, options, null, + JavaCompiler.CompilationTask task = compiler.getTask(null, collectingFileManager, diagnostics, options, null, sources); - task.setProcessors(List.of(new CopilotToolProcessor())); task.call(); - // Collect generated sources - List generatedSources = new ArrayList<>(); - collectGeneratedFiles(tempDir, generatedSources); + List generatedSources = collectingFileManager.getGeneratedSources(); + if (generatedSources.isEmpty()) { + // Fallback for file-manager implementations that only materialize on disk. + collectGeneratedFiles(tempDir, generatedSources); + } return new CompilationResult(diagnostics.getDiagnostics(), generatedSources, tempDir); } catch (Exception e) { @@ -666,4 +677,52 @@ String getGeneratedSource(String qualifiedName) { return null; } } + + private static class CollectingFileManager extends ForwardingJavaFileManager { + private final Map generatedByClass = new LinkedHashMap<>(); + + CollectingFileManager(StandardJavaFileManager fileManager) { + super(fileManager); + } + + @Override + public JavaFileObject getJavaFileForOutput(Location location, String className, JavaFileObject.Kind kind, + FileObject sibling) throws IOException { + JavaFileObject delegate = super.getJavaFileForOutput(location, className, kind, sibling); + if (kind != JavaFileObject.Kind.SOURCE) { + return delegate; + } + StringBuilder captured = new StringBuilder(); + generatedByClass.put(className, captured); + return new ForwardingJavaFileObject<>(delegate) { + @Override + public Writer openWriter() throws IOException { + Writer target = delegate.openWriter(); + return new FilterWriter(target) { + @Override + public void write(char[] cbuf, int off, int len) throws IOException { + captured.append(cbuf, off, len); + super.write(cbuf, off, len); + } + + @Override + public void write(int c) throws IOException { + captured.append((char) c); + super.write(c); + } + + @Override + public void write(String str, int off, int len) throws IOException { + captured.append(str, off, off + len); + super.write(str, off, len); + } + }; + } + }; + } + + List getGeneratedSources() { + return generatedByClass.values().stream().map(StringBuilder::toString).toList(); + } + } } From a228c577e3f28a9578f46c3fec3a121d3c56a549 Mon Sep 17 00:00:00 2001 From: Ed Burns Date: Thu, 25 Jun 2026 12:40:30 -0400 Subject: [PATCH 2/4] Spotless --- .../com/github/copilot/tool/CopilotToolProcessorTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/java/src/test/java/com/github/copilot/tool/CopilotToolProcessorTest.java b/java/src/test/java/com/github/copilot/tool/CopilotToolProcessorTest.java index b431470e5c..12077f7187 100644 --- a/java/src/test/java/com/github/copilot/tool/CopilotToolProcessorTest.java +++ b/java/src/test/java/com/github/copilot/tool/CopilotToolProcessorTest.java @@ -561,8 +561,8 @@ private CompilationResult compileWithProcessor(List sources) { fileManager.setLocation(StandardLocation.CLASS_OUTPUT, List.of(tempDir.toFile())); CollectingFileManager collectingFileManager = new CollectingFileManager(fileManager); - JavaCompiler.CompilationTask task = compiler.getTask(null, collectingFileManager, diagnostics, options, null, - sources); + JavaCompiler.CompilationTask task = compiler.getTask(null, collectingFileManager, diagnostics, options, + null, sources); task.call(); List generatedSources = collectingFileManager.getGeneratedSources(); From 7da68850213d780a831bf878015576af83903ea4 Mon Sep 17 00:00:00 2001 From: Ed Burns Date: Thu, 25 Jun 2026 12:48:38 -0400 Subject: [PATCH 3/4] Avoid leaking session. The retry on session creation uses `future.get(timeout)` but does not cancel the in-flight `createSession` future when a timeout occurs. If attempt 1 eventually completes after attempt 2 starts, it can leave an orphaned session registered in the client (and potentially race `getLastSessionId` persistence), reintroducing flakiness and leaking resources. Capture the future and cancel it on timeout before retrying. Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../java/com/github/copilot/CopilotSessionTest.java | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/java/src/test/java/com/github/copilot/CopilotSessionTest.java b/java/src/test/java/com/github/copilot/CopilotSessionTest.java index 9b41a27e58..eb061b029d 100644 --- a/java/src/test/java/com/github/copilot/CopilotSessionTest.java +++ b/java/src/test/java/com/github/copilot/CopilotSessionTest.java @@ -758,19 +758,22 @@ void testShouldGetLastSessionId() throws Exception { try (CopilotClient client = ctx.createClient()) { CopilotSession session = null; for (int attempt = 1; attempt <= 2; attempt++) { + CompletableFuture createFuture = client + .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)); try { - session = client - .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)) - .get(45, TimeUnit.SECONDS); + session = createFuture.get(45, TimeUnit.SECONDS); break; } catch (java.util.concurrent.TimeoutException e) { + createFuture.cancel(true); if (attempt == 2) { throw e; } } catch (java.util.concurrent.ExecutionException e) { - if (!(e.getCause() instanceof java.util.concurrent.TimeoutException) || attempt == 2) { - throw e; + if (e.getCause() instanceof java.util.concurrent.TimeoutException && attempt < 2) { + createFuture.cancel(true); + continue; } + throw e; } } assertNotNull(session, "Session should be created"); From ab46aa757c887e4f6c812b2d770d5d902c2eca7c Mon Sep 17 00:00:00 2001 From: Ed Burns Date: Thu, 25 Jun 2026 13:50:19 -0400 Subject: [PATCH 4/4] Add abort-session snapshot variant for interrupted tool calls Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../session/should_abort_a_session.yaml | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/test/snapshots/session/should_abort_a_session.yaml b/test/snapshots/session/should_abort_a_session.yaml index dbbbd32aa7..f1217f7f62 100644 --- a/test/snapshots/session/should_abort_a_session.yaml +++ b/test/snapshots/session/should_abort_a_session.yaml @@ -50,3 +50,31 @@ conversations: content: What is 2+2? - role: assistant content: 2 + 2 = 4 + - messages: + - role: system + content: ${system} + - role: user + content: run the shell command 'sleep 100' (note this works on both bash and PowerShell) + - role: assistant + content: I'll run the sleep command for 100 seconds. + tool_calls: + - id: toolcall_0 + type: function + function: + name: report_intent + arguments: '{"intent":"Running sleep command"}' + - id: toolcall_1 + type: function + function: + name: ${shell} + arguments: '{"command":"sleep 100","description":"Run sleep 100 command","mode":"sync","initial_wait":105}' + - role: tool + tool_call_id: toolcall_0 + content: The execution of this tool, or a previous tool was interrupted. + - role: tool + tool_call_id: toolcall_1 + content: The execution of this tool, or a previous tool was interrupted. + - role: user + content: What is 2+2? + - role: assistant + content: 2 + 2 = 4