Skip to content

Commit 2010042

Browse files
committed
Address copilot feedback
1 parent 03e0611 commit 2010042

File tree

2 files changed

+268
-11
lines changed

2 files changed

+268
-11
lines changed

dotnet/src/Types.cs

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ protected CopilotClientOptions(CopilotClientOptions? other)
4343
CliPath = other.CliPath;
4444
CliUrl = other.CliUrl;
4545
Cwd = other.Cwd;
46-
Environment = other.Environment is not null ? new Dictionary<string, string>(other.Environment) : null;
46+
Environment = other.Environment;
4747
GithubToken = other.GithubToken;
4848
Logger = other.Logger;
4949
LogLevel = other.LogLevel;
@@ -86,8 +86,10 @@ protected CopilotClientOptions(CopilotClientOptions? other)
8686
/// Creates a shallow clone of this <see cref="CopilotClientOptions"/> instance.
8787
/// </summary>
8888
/// <remarks>
89-
/// Collection properties are copied into new collections so that modifications
90-
/// to the clone do not affect the original.
89+
/// Mutable collection properties are copied into new collection instances so that modifications
90+
/// to those collections on the clone do not affect the original.
91+
/// Other reference-type properties (for example delegates and the logger) are not
92+
/// deep-cloned; the original and the clone will share those objects.
9193
/// </remarks>
9294
public virtual CopilotClientOptions Clone() => new(this);
9395
}
@@ -749,7 +751,9 @@ protected SessionConfig(SessionConfig? other)
749751
ExcludedTools = other.ExcludedTools is not null ? [.. other.ExcludedTools] : null;
750752
Hooks = other.Hooks;
751753
InfiniteSessions = other.InfiniteSessions;
752-
McpServers = other.McpServers is not null ? new Dictionary<string, object>(other.McpServers) : null;
754+
McpServers = other.McpServers is not null
755+
? new Dictionary<string, object>(other.McpServers, other.McpServers.Comparer)
756+
: null;
753757
Model = other.Model;
754758
OnPermissionRequest = other.OnPermissionRequest;
755759
OnUserInputRequest = other.OnUserInputRequest;
@@ -845,8 +849,11 @@ protected SessionConfig(SessionConfig? other)
845849
/// Creates a shallow clone of this <see cref="SessionConfig"/> instance.
846850
/// </summary>
847851
/// <remarks>
848-
/// Collection properties are copied into new collections so that modifications
849-
/// to the clone do not affect the original.
852+
/// Mutable collection properties are copied into new collection instances so that modifications
853+
/// to those collections on the clone do not affect the original.
854+
/// Other reference-type properties (for example provider configuration, system messages,
855+
/// hooks, infinite session configuration, and delegates) are not deep-cloned; the original
856+
/// and the clone will share those nested objects, and changes to them may affect both.
850857
/// </remarks>
851858
public virtual SessionConfig Clone() => new(this);
852859
}
@@ -874,7 +881,9 @@ protected ResumeSessionConfig(ResumeSessionConfig? other)
874881
ExcludedTools = other.ExcludedTools is not null ? [.. other.ExcludedTools] : null;
875882
Hooks = other.Hooks;
876883
InfiniteSessions = other.InfiniteSessions;
877-
McpServers = other.McpServers is not null ? new Dictionary<string, object>(other.McpServers) : null;
884+
McpServers = other.McpServers is not null
885+
? new Dictionary<string, object>(other.McpServers, other.McpServers.Comparer)
886+
: null;
878887
Model = other.Model;
879888
OnPermissionRequest = other.OnPermissionRequest;
880889
OnUserInputRequest = other.OnUserInputRequest;
@@ -989,8 +998,11 @@ protected ResumeSessionConfig(ResumeSessionConfig? other)
989998
/// Creates a shallow clone of this <see cref="ResumeSessionConfig"/> instance.
990999
/// </summary>
9911000
/// <remarks>
992-
/// Collection properties are copied into new collections so that modifications
993-
/// to the clone do not affect the original.
1001+
/// Mutable collection properties are copied into new collection instances so that modifications
1002+
/// to those collections on the clone do not affect the original.
1003+
/// Other reference-type properties (for example provider configuration, system messages,
1004+
/// hooks, infinite session configuration, and delegates) are not deep-cloned; the original
1005+
/// and the clone will share those nested objects, and changes to them may affect both.
9941006
/// </remarks>
9951007
public virtual ResumeSessionConfig Clone() => new(this);
9961008
}
@@ -1023,8 +1035,10 @@ protected MessageOptions(MessageOptions? other)
10231035
/// Creates a shallow clone of this <see cref="MessageOptions"/> instance.
10241036
/// </summary>
10251037
/// <remarks>
1026-
/// Collection properties are copied into new collections so that modifications
1027-
/// to the clone do not affect the original.
1038+
/// Mutable collection properties are copied into new collection instances so that modifications
1039+
/// to those collections on the clone do not affect the original.
1040+
/// Other reference-type properties (for example attachment items) are not deep-cloned;
1041+
/// the original and the clone will share those nested objects.
10281042
/// </remarks>
10291043
public virtual MessageOptions Clone() => new(this);
10301044
}

dotnet/test/CloneTests.cs

Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
*--------------------------------------------------------------------------------------------*/
4+
5+
using Microsoft.Extensions.AI;
6+
using Xunit;
7+
8+
namespace GitHub.Copilot.SDK.Test;
9+
10+
public class CloneTests
11+
{
12+
[Fact]
13+
public void CopilotClientOptions_Clone_CopiesAllProperties()
14+
{
15+
var original = new CopilotClientOptions
16+
{
17+
CliPath = "/usr/bin/copilot",
18+
CliArgs = ["--verbose", "--debug"],
19+
Cwd = "/home/user",
20+
Port = 8080,
21+
UseStdio = false,
22+
CliUrl = "http://localhost:8080",
23+
LogLevel = "debug",
24+
AutoStart = false,
25+
AutoRestart = false,
26+
Environment = new Dictionary<string, string> { ["KEY"] = "value" },
27+
GithubToken = "ghp_test",
28+
UseLoggedInUser = false,
29+
};
30+
31+
var clone = original.Clone();
32+
33+
Assert.Equal(original.CliPath, clone.CliPath);
34+
Assert.Equal(original.CliArgs, clone.CliArgs);
35+
Assert.Equal(original.Cwd, clone.Cwd);
36+
Assert.Equal(original.Port, clone.Port);
37+
Assert.Equal(original.UseStdio, clone.UseStdio);
38+
Assert.Equal(original.CliUrl, clone.CliUrl);
39+
Assert.Equal(original.LogLevel, clone.LogLevel);
40+
Assert.Equal(original.AutoStart, clone.AutoStart);
41+
Assert.Equal(original.AutoRestart, clone.AutoRestart);
42+
Assert.Equal(original.Environment, clone.Environment);
43+
Assert.Equal(original.GithubToken, clone.GithubToken);
44+
Assert.Equal(original.UseLoggedInUser, clone.UseLoggedInUser);
45+
}
46+
47+
[Fact]
48+
public void CopilotClientOptions_Clone_CollectionsAreIndependent()
49+
{
50+
var original = new CopilotClientOptions
51+
{
52+
CliArgs = ["--verbose"],
53+
};
54+
55+
var clone = original.Clone();
56+
57+
// Mutate clone array
58+
clone.CliArgs![0] = "--quiet";
59+
60+
// Original is unaffected
61+
Assert.Equal("--verbose", original.CliArgs![0]);
62+
}
63+
64+
[Fact]
65+
public void CopilotClientOptions_Clone_EnvironmentIsShared()
66+
{
67+
var env = new Dictionary<string, string> { ["key"] = "value" };
68+
var original = new CopilotClientOptions { Environment = env };
69+
70+
var clone = original.Clone();
71+
72+
Assert.Same(original.Environment, clone.Environment);
73+
}
74+
75+
[Fact]
76+
public void SessionConfig_Clone_CopiesAllProperties()
77+
{
78+
var original = new SessionConfig
79+
{
80+
SessionId = "test-session",
81+
Model = "gpt-4",
82+
ReasoningEffort = "high",
83+
ConfigDir = "/config",
84+
AvailableTools = ["tool1", "tool2"],
85+
ExcludedTools = ["tool3"],
86+
WorkingDirectory = "/workspace",
87+
Streaming = true,
88+
McpServers = new Dictionary<string, object> { ["server1"] = new object() },
89+
CustomAgents = [new CustomAgentConfig { Name = "agent1" }],
90+
SkillDirectories = ["/skills"],
91+
DisabledSkills = ["skill1"],
92+
};
93+
94+
var clone = original.Clone();
95+
96+
Assert.Equal(original.SessionId, clone.SessionId);
97+
Assert.Equal(original.Model, clone.Model);
98+
Assert.Equal(original.ReasoningEffort, clone.ReasoningEffort);
99+
Assert.Equal(original.ConfigDir, clone.ConfigDir);
100+
Assert.Equal(original.AvailableTools, clone.AvailableTools);
101+
Assert.Equal(original.ExcludedTools, clone.ExcludedTools);
102+
Assert.Equal(original.WorkingDirectory, clone.WorkingDirectory);
103+
Assert.Equal(original.Streaming, clone.Streaming);
104+
Assert.Equal(original.McpServers.Count, clone.McpServers!.Count);
105+
Assert.Equal(original.CustomAgents.Count, clone.CustomAgents!.Count);
106+
Assert.Equal(original.SkillDirectories, clone.SkillDirectories);
107+
Assert.Equal(original.DisabledSkills, clone.DisabledSkills);
108+
}
109+
110+
[Fact]
111+
public void SessionConfig_Clone_CollectionsAreIndependent()
112+
{
113+
var original = new SessionConfig
114+
{
115+
AvailableTools = ["tool1"],
116+
ExcludedTools = ["tool2"],
117+
McpServers = new Dictionary<string, object> { ["s1"] = new object() },
118+
CustomAgents = [new CustomAgentConfig { Name = "a1" }],
119+
SkillDirectories = ["/skills"],
120+
DisabledSkills = ["skill1"],
121+
};
122+
123+
var clone = original.Clone();
124+
125+
// Mutate clone collections
126+
clone.AvailableTools!.Add("tool99");
127+
clone.ExcludedTools!.Add("tool99");
128+
clone.McpServers!["s2"] = new object();
129+
clone.CustomAgents!.Add(new CustomAgentConfig { Name = "a2" });
130+
clone.SkillDirectories!.Add("/more");
131+
clone.DisabledSkills!.Add("skill99");
132+
133+
// Original is unaffected
134+
Assert.Single(original.AvailableTools!);
135+
Assert.Single(original.ExcludedTools!);
136+
Assert.Single(original.McpServers!);
137+
Assert.Single(original.CustomAgents!);
138+
Assert.Single(original.SkillDirectories!);
139+
Assert.Single(original.DisabledSkills!);
140+
}
141+
142+
[Fact]
143+
public void SessionConfig_Clone_PreservesMcpServersComparer()
144+
{
145+
var servers = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase) { ["server"] = new object() };
146+
var original = new SessionConfig { McpServers = servers };
147+
148+
var clone = original.Clone();
149+
150+
Assert.True(clone.McpServers!.ContainsKey("SERVER")); // case-insensitive lookup works
151+
}
152+
153+
[Fact]
154+
public void ResumeSessionConfig_Clone_CollectionsAreIndependent()
155+
{
156+
var original = new ResumeSessionConfig
157+
{
158+
AvailableTools = ["tool1"],
159+
ExcludedTools = ["tool2"],
160+
McpServers = new Dictionary<string, object> { ["s1"] = new object() },
161+
CustomAgents = [new CustomAgentConfig { Name = "a1" }],
162+
SkillDirectories = ["/skills"],
163+
DisabledSkills = ["skill1"],
164+
};
165+
166+
var clone = original.Clone();
167+
168+
// Mutate clone collections
169+
clone.AvailableTools!.Add("tool99");
170+
clone.ExcludedTools!.Add("tool99");
171+
clone.McpServers!["s2"] = new object();
172+
clone.CustomAgents!.Add(new CustomAgentConfig { Name = "a2" });
173+
clone.SkillDirectories!.Add("/more");
174+
clone.DisabledSkills!.Add("skill99");
175+
176+
// Original is unaffected
177+
Assert.Single(original.AvailableTools!);
178+
Assert.Single(original.ExcludedTools!);
179+
Assert.Single(original.McpServers!);
180+
Assert.Single(original.CustomAgents!);
181+
Assert.Single(original.SkillDirectories!);
182+
Assert.Single(original.DisabledSkills!);
183+
}
184+
185+
[Fact]
186+
public void ResumeSessionConfig_Clone_PreservesMcpServersComparer()
187+
{
188+
var servers = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase) { ["server"] = new object() };
189+
var original = new ResumeSessionConfig { McpServers = servers };
190+
191+
var clone = original.Clone();
192+
193+
Assert.True(clone.McpServers!.ContainsKey("SERVER"));
194+
}
195+
196+
[Fact]
197+
public void MessageOptions_Clone_CopiesAllProperties()
198+
{
199+
var original = new MessageOptions
200+
{
201+
Prompt = "Hello",
202+
Attachments = [new UserMessageDataAttachmentsItemFile { Path = "/test.txt", DisplayName = "test.txt" }],
203+
Mode = "chat",
204+
};
205+
206+
var clone = original.Clone();
207+
208+
Assert.Equal(original.Prompt, clone.Prompt);
209+
Assert.Equal(original.Mode, clone.Mode);
210+
Assert.Single(clone.Attachments!);
211+
}
212+
213+
[Fact]
214+
public void MessageOptions_Clone_AttachmentsAreIndependent()
215+
{
216+
var original = new MessageOptions
217+
{
218+
Attachments = [new UserMessageDataAttachmentsItemFile { Path = "/test.txt", DisplayName = "test.txt" }],
219+
};
220+
221+
var clone = original.Clone();
222+
223+
clone.Attachments!.Add(new UserMessageDataAttachmentsItemFile { Path = "/other.txt", DisplayName = "other.txt" });
224+
225+
Assert.Single(original.Attachments!);
226+
}
227+
228+
[Fact]
229+
public void Clone_WithNullCollections_ReturnsNullCollections()
230+
{
231+
var original = new SessionConfig();
232+
233+
var clone = original.Clone();
234+
235+
Assert.Null(clone.AvailableTools);
236+
Assert.Null(clone.ExcludedTools);
237+
Assert.Null(clone.McpServers);
238+
Assert.Null(clone.CustomAgents);
239+
Assert.Null(clone.SkillDirectories);
240+
Assert.Null(clone.DisabledSkills);
241+
Assert.Null(clone.Tools);
242+
}
243+
}

0 commit comments

Comments
 (0)