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
2 changes: 1 addition & 1 deletion .lastmerge
Original file line number Diff line number Diff line change
@@ -1 +1 @@
304d812cd4c98755159da427c6701bfb7e0b7c32
5016587a62652f3d184b3c6958dfc63359921aa8
7 changes: 7 additions & 0 deletions src/main/java/com/github/copilot/sdk/CliServerManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,13 @@ ProcessInfo startCliServer() throws IOException, InterruptedException {
var pb = new ProcessBuilder(command);
pb.redirectErrorStream(false);

// Note: On Windows, console window visibility depends on how the parent Java
// process was launched. GUI applications started with 'javaw' will not create
// visible console windows for subprocesses. Console applications started with
// 'java' will share their console with subprocesses. Java's ProcessBuilder
// doesn't provide explicit CREATE_NO_WINDOW flags like native Windows APIs,
// but the default behavior is appropriate for most use cases.

if (options.getCwd() != null) {
pb.directory(new File(options.getCwd()));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -323,4 +323,32 @@ public CopilotClientOptions setUseLoggedInUser(Boolean useLoggedInUser) {
this.useLoggedInUser = useLoggedInUser;
return this;
}

/**
* Creates a shallow clone of this {@code CopilotClientOptions} instance.
* <p>
* Array properties (like {@code cliArgs}) are copied into new arrays so that
* modifications to the clone do not affect the original. The
* {@code environment} map is also copied to a new map instance. Other
* reference-type properties are shared between the original and clone.
*
* @return a clone of this options instance
*/
@Override
public CopilotClientOptions clone() {
CopilotClientOptions copy = new CopilotClientOptions();
copy.cliPath = this.cliPath;
copy.cliArgs = this.cliArgs != null ? this.cliArgs.clone() : null;
copy.cwd = this.cwd;
copy.port = this.port;
copy.useStdio = this.useStdio;
copy.cliUrl = this.cliUrl;
copy.logLevel = this.logLevel;
copy.autoStart = this.autoStart;
copy.autoRestart = this.autoRestart;
copy.environment = this.environment != null ? new java.util.HashMap<>(this.environment) : null;
copy.githubToken = this.githubToken;
copy.useLoggedInUser = this.useLoggedInUser;
return copy;
Comment on lines 327 to 352
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CopilotClientOptions.clone() copies cliArgs but shares the mutable environment Map. Since getEnvironment()/setEnvironment() expose a modifiable Map and other config clone() methods copy their Map/List fields, sharing this Map makes it easy for changes on the clone to affect the original (and vice versa). Consider cloning the environment map as well (or, if sharing is intentional, document that as an exception and add a test to lock in the behavior).

Copilot uses AI. Check for mistakes.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot apply changes based on this feedback

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot apply changes based on this feedback

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed to clone the environment map for collection independence. Updated Javadoc to reflect that the environment map is now copied. Added test copilotClientOptionsEnvironmentIndependence to validate this behavior. Commit eb573ef

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed to clone the environment map for collection independence. Updated Javadoc to reflect that the environment map is now copied. Added test copilotClientOptionsEnvironmentIndependence to validate this behavior. Commit 6e2e991

}
}
20 changes: 20 additions & 0 deletions src/main/java/com/github/copilot/sdk/json/MessageOptions.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

package com.github.copilot.sdk.json;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

Expand Down Expand Up @@ -107,4 +108,23 @@ public String getMode() {
return mode;
}

/**
* Creates a shallow clone of this {@code MessageOptions} instance.
* <p>
* Mutable collection properties are copied into new collection instances so
* that modifications to those collections on the clone do not affect the
* original. Other reference-type properties (like attachment items) are not
* deep-cloned; the original and the clone will share those objects.
*
* @return a clone of this options instance
*/
@Override
public MessageOptions clone() {
MessageOptions copy = new MessageOptions();
copy.prompt = this.prompt;
copy.attachments = this.attachments != null ? new ArrayList<>(this.attachments) : null;
copy.mode = this.mode;
return copy;
}

}
37 changes: 37 additions & 0 deletions src/main/java/com/github/copilot/sdk/json/ResumeSessionConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

package com.github.copilot.sdk.json;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
Expand Down Expand Up @@ -475,4 +476,40 @@ public ResumeSessionConfig setInfiniteSessions(InfiniteSessionConfig infiniteSes
this.infiniteSessions = infiniteSessions;
return this;
}

/**
* Creates a shallow clone of this {@code ResumeSessionConfig} instance.
* <p>
* Mutable collection properties are copied into new collection instances so
* that modifications to those collections on the clone do not affect the
* original. Other reference-type properties (like provider configuration,
* system messages, hooks, infinite session configuration, and handlers) are not
* deep-cloned; the original and the clone will share those objects.
*
* @return a clone of this config instance
*/
@Override
public ResumeSessionConfig clone() {
ResumeSessionConfig copy = new ResumeSessionConfig();
copy.model = this.model;
copy.tools = this.tools != null ? new ArrayList<>(this.tools) : null;
copy.systemMessage = this.systemMessage;
copy.availableTools = this.availableTools != null ? new ArrayList<>(this.availableTools) : null;
copy.excludedTools = this.excludedTools != null ? new ArrayList<>(this.excludedTools) : null;
copy.provider = this.provider;
copy.reasoningEffort = this.reasoningEffort;
copy.onPermissionRequest = this.onPermissionRequest;
copy.onUserInputRequest = this.onUserInputRequest;
copy.hooks = this.hooks;
copy.workingDirectory = this.workingDirectory;
copy.configDir = this.configDir;
copy.disableResume = this.disableResume;
copy.streaming = this.streaming;
copy.mcpServers = this.mcpServers != null ? new java.util.HashMap<>(this.mcpServers) : null;
copy.customAgents = this.customAgents != null ? new ArrayList<>(this.customAgents) : null;
copy.skillDirectories = this.skillDirectories != null ? new ArrayList<>(this.skillDirectories) : null;
copy.disabledSkills = this.disabledSkills != null ? new ArrayList<>(this.disabledSkills) : null;
copy.infiniteSessions = this.infiniteSessions;
return copy;
}
}
37 changes: 37 additions & 0 deletions src/main/java/com/github/copilot/sdk/json/SessionConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

package com.github.copilot.sdk.json;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
Expand Down Expand Up @@ -512,4 +513,40 @@ public SessionConfig setConfigDir(String configDir) {
this.configDir = configDir;
return this;
}

/**
* Creates a shallow clone of this {@code SessionConfig} instance.
* <p>
* Mutable collection properties are copied into new collection instances so
* that modifications to those collections on the clone do not affect the
* original. Other reference-type properties (like provider configuration,
* system messages, hooks, infinite session configuration, and handlers) are not
* deep-cloned; the original and the clone will share those objects.
*
* @return a clone of this config instance
*/
@Override
public SessionConfig clone() {
SessionConfig copy = new SessionConfig();
copy.sessionId = this.sessionId;
copy.model = this.model;
copy.reasoningEffort = this.reasoningEffort;
copy.tools = this.tools != null ? new ArrayList<>(this.tools) : null;
copy.systemMessage = this.systemMessage;
copy.availableTools = this.availableTools != null ? new ArrayList<>(this.availableTools) : null;
copy.excludedTools = this.excludedTools != null ? new ArrayList<>(this.excludedTools) : null;
copy.provider = this.provider;
copy.onPermissionRequest = this.onPermissionRequest;
copy.onUserInputRequest = this.onUserInputRequest;
copy.hooks = this.hooks;
copy.workingDirectory = this.workingDirectory;
copy.streaming = this.streaming;
copy.mcpServers = this.mcpServers != null ? new java.util.HashMap<>(this.mcpServers) : null;
copy.customAgents = this.customAgents != null ? new ArrayList<>(this.customAgents) : null;
copy.infiniteSessions = this.infiniteSessions;
copy.skillDirectories = this.skillDirectories != null ? new ArrayList<>(this.skillDirectories) : null;
copy.disabledSkills = this.disabledSkills != null ? new ArrayList<>(this.disabledSkills) : null;
copy.configDir = this.configDir;
return copy;
}
}
137 changes: 137 additions & 0 deletions src/test/java/com/github/copilot/sdk/ConfigCloneTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------------------------------------------*/

package com.github.copilot.sdk;

import static org.junit.jupiter.api.Assertions.*;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.junit.jupiter.api.Test;

import com.github.copilot.sdk.json.CopilotClientOptions;
import com.github.copilot.sdk.json.SessionConfig;
import com.github.copilot.sdk.json.ResumeSessionConfig;
import com.github.copilot.sdk.json.MessageOptions;

class ConfigCloneTest {

@Test
void copilotClientOptionsCloneBasic() {
CopilotClientOptions original = new CopilotClientOptions();
original.setCliPath("/usr/local/bin/copilot");
original.setLogLevel("debug");
original.setPort(9000);

CopilotClientOptions cloned = original.clone();

assertEquals(original.getCliPath(), cloned.getCliPath());
Comment on lines 26 to 32
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ConfigCloneTest currently includes trailing whitespace on otherwise blank lines (e.g., around this section). Spotless is configured with trimTrailingWhitespace, so this will fail formatting checks in CI; please run mvn spotless:apply or remove the trailing spaces.

Copilot uses AI. Check for mistakes.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot apply changes based on this feedback

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Applied. Spotless formatting has been run to remove trailing whitespace and fix all formatting issues. Commit eb573ef

assertEquals(original.getLogLevel(), cloned.getLogLevel());
assertEquals(original.getPort(), cloned.getPort());
}

@Test
void copilotClientOptionsArrayIndependence() {
CopilotClientOptions original = new CopilotClientOptions();
String[] args = {"--flag1", "--flag2"};
original.setCliArgs(args);

CopilotClientOptions cloned = original.clone();
cloned.getCliArgs()[0] = "--changed";

assertEquals("--flag1", original.getCliArgs()[0]);
assertEquals("--changed", cloned.getCliArgs()[0]);
}

@Test
void copilotClientOptionsEnvironmentIndependence() {
CopilotClientOptions original = new CopilotClientOptions();
Map<String, String> env = new HashMap<>();
env.put("KEY1", "value1");
original.setEnvironment(env);

CopilotClientOptions cloned = original.clone();

// Mutate the original environment map to test independence
env.put("KEY2", "value2");

// The cloned config should be unaffected by mutations to the original map
assertEquals(1, cloned.getEnvironment().size());
assertEquals(2, original.getEnvironment().size());
}

@Test
void sessionConfigCloneBasic() {
SessionConfig original = new SessionConfig();
original.setSessionId("my-session");
original.setModel("gpt-4o");
original.setStreaming(true);

SessionConfig cloned = original.clone();

assertEquals(original.getSessionId(), cloned.getSessionId());
assertEquals(original.getModel(), cloned.getModel());
assertEquals(original.isStreaming(), cloned.isStreaming());
}

@Test
void sessionConfigListIndependence() {
SessionConfig original = new SessionConfig();
List<String> toolList = new ArrayList<>();
toolList.add("grep");
toolList.add("bash");
original.setAvailableTools(toolList);

SessionConfig cloned = original.clone();

// Mutate the original list directly to test independence
toolList.add("web");

// The cloned config should be unaffected by mutations to the original list
assertEquals(2, cloned.getAvailableTools().size());
assertEquals(3, original.getAvailableTools().size());
}
Comment on lines 82 to 97
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sessionConfigListIndependence() doesn’t currently validate that SessionConfig.clone() actually copies the underlying availableTools list: it builds a new list from the clone and sets it back, which would still pass even if the clone shared the original list. To test clone semantics, mutate the original list instance (toolList) after cloning (or otherwise observe independence) and assert the cloned config is unaffected.

Copilot generated this review using guidance from repository custom instructions.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot apply changes based on this feedback

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed test to properly validate list independence by mutating the original list after cloning and verifying the cloned config is unaffected. Commit eb573ef

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed test to properly validate list independence by mutating the original list after cloning and verifying the cloned config is unaffected. Commit 6e2e991


@Test
void resumeSessionConfigCloneBasic() {
ResumeSessionConfig original = new ResumeSessionConfig();
original.setModel("o1");
original.setStreaming(false);

ResumeSessionConfig cloned = original.clone();

assertEquals(original.getModel(), cloned.getModel());
assertEquals(original.isStreaming(), cloned.isStreaming());
}

@Test
void messageOptionsCloneBasic() {
MessageOptions original = new MessageOptions();
original.setPrompt("What is 2+2?");
original.setMode("immediate");

MessageOptions cloned = original.clone();

assertEquals(original.getPrompt(), cloned.getPrompt());
assertEquals(original.getMode(), cloned.getMode());
}

@Test
void clonePreservesNullFields() {
CopilotClientOptions opts = new CopilotClientOptions();
CopilotClientOptions optsClone = opts.clone();
assertNull(optsClone.getCliPath());

SessionConfig cfg = new SessionConfig();
SessionConfig cfgClone = cfg.clone();
assertNull(cfgClone.getModel());

MessageOptions msg = new MessageOptions();
MessageOptions msgClone = msg.clone();
assertNull(msgClone.getMode());
}
}
Loading