Skip to content

Commit e3cbdaa

Browse files
ellismgCopilot
andcommitted
Fix .NET clone dropping ExpAssignments + add clone regression coverage
The SessionConfigBase copy constructor (used by SessionConfig.Clone() and ResumeSessionConfig.Clone()) did not copy the newly added ExpAssignments property, so cloning a config silently dropped it and it would not be forwarded on create/resume. Copy it alongside the other base properties. Cross-language audit of every config copy/clone path: - .NET: had the gap (fixed here). - Java: SessionConfig/ResumeSessionConfig clone() already copy expAssignments (safe). - Rust: SessionConfig/ResumeSessionConfig derive Clone, no manual impl (safe). - Node: createSession/resumeSession normalize via { ...defaults, ...config } spread, which preserves all own properties (safe). - Go: create/resume requests assign req.ExpAssignments = config.ExpAssignments directly from the config pointer (safe). - Python: exp_assignments is an explicit kwarg mapped straight into the payload, no intermediate dict copy (safe). Add clone regression tests for .NET, Java, and Rust (the three with explicit Clone()/clone() methods) that set expAssignments, clone, and assert it survives and is forwarded on the resulting create/resume request. The .NET tests fail against the pre-fix copy constructor. Node's existing forward test already exercises the spread-normalization copy path. Switch the two new Java expAssignments tests off the deprecated buildCreateRequest(SessionConfig) overload to the current buildCreateRequest(SessionConfig, String) form to avoid new CodeQL debt. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent ba36305 commit e3cbdaa

4 files changed

Lines changed: 96 additions & 2 deletions

File tree

dotnet/src/Types.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2667,6 +2667,7 @@ protected SessionConfigBase(SessionConfigBase? other)
26672667
CreateSessionFsProvider = other.CreateSessionFsProvider;
26682668
GitHubToken = other.GitHubToken;
26692669
RemoteSession = other.RemoteSession;
2670+
ExpAssignments = other.ExpAssignments;
26702671
#pragma warning disable GHCP001
26712672
Canvases = other.Canvases is not null ? [.. other.Canvases] : null;
26722673
RequestCanvasRenderer = other.RequestCanvasRenderer;

dotnet/test/Unit/SerializationTests.cs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -496,6 +496,43 @@ public void SessionRequests_OmitExpAssignments_WhenUnset()
496496
Assert.False(resumeDocument.RootElement.TryGetProperty("expAssignments", out _));
497497
}
498498

499+
[Fact]
500+
public void SessionConfigClone_PreservesExpAssignments()
501+
{
502+
using var assignments = JsonDocument.Parse("""{"Configs":[{"Id":"exp-create"}]}""");
503+
504+
var config = new SessionConfig
505+
{
506+
SessionId = "session-id",
507+
ExpAssignments = assignments.RootElement.Clone(),
508+
};
509+
510+
var clone = config.Clone();
511+
512+
Assert.True(clone.ExpAssignments.HasValue);
513+
Assert.Equal(
514+
"exp-create",
515+
clone.ExpAssignments!.Value.GetProperty("Configs")[0].GetProperty("Id").GetString());
516+
}
517+
518+
[Fact]
519+
public void ResumeSessionConfigClone_PreservesExpAssignments()
520+
{
521+
using var assignments = JsonDocument.Parse("""{"Configs":[{"Id":"exp-resume"}]}""");
522+
523+
var config = new ResumeSessionConfig
524+
{
525+
ExpAssignments = assignments.RootElement.Clone(),
526+
};
527+
528+
var clone = config.Clone();
529+
530+
Assert.True(clone.ExpAssignments.HasValue);
531+
Assert.Equal(
532+
"exp-resume",
533+
clone.ExpAssignments!.Value.GetProperty("Configs")[0].GetProperty("Id").GetString());
534+
}
535+
499536
[Fact]
500537
public void CreateSessionRequest_CanSerializeEnableSessionTelemetry_WithSdkOptions()
501538
{

java/src/test/java/com/github/copilot/SessionRequestBuilderTest.java

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -876,7 +876,7 @@ void testBuildRequestsPropagateAndSerializeExpAssignments() throws Exception {
876876
JsonNode resumeAssignments = mapper.readTree("{\"Configs\":[{\"Id\":\"exp-resume\"}]}");
877877

878878
var createConfig = new SessionConfig().setExpAssignments(createAssignments);
879-
CreateSessionRequest createRequest = SessionRequestBuilder.buildCreateRequest(createConfig);
879+
CreateSessionRequest createRequest = SessionRequestBuilder.buildCreateRequest(createConfig, "session-1");
880880
assertEquals(createAssignments, createRequest.getExpAssignments());
881881
var createJson = mapper.writeValueAsString(createRequest);
882882
assertTrue(createJson.contains("\"expAssignments\""));
@@ -894,7 +894,7 @@ void testBuildRequestsPropagateAndSerializeExpAssignments() throws Exception {
894894
void testBuildRequestsOmitExpAssignmentsWhenUnset() throws Exception {
895895
var mapper = JsonRpcClient.getObjectMapper();
896896

897-
CreateSessionRequest createRequest = SessionRequestBuilder.buildCreateRequest(new SessionConfig());
897+
CreateSessionRequest createRequest = SessionRequestBuilder.buildCreateRequest(new SessionConfig(), "session-1");
898898
assertNull(createRequest.getExpAssignments());
899899
var createJson = mapper.writeValueAsString(createRequest);
900900
assertFalse(createJson.contains("\"expAssignments\""), "expAssignments should be omitted when null");
@@ -905,4 +905,25 @@ void testBuildRequestsOmitExpAssignmentsWhenUnset() throws Exception {
905905
var resumeJson = mapper.writeValueAsString(resumeRequest);
906906
assertFalse(resumeJson.contains("\"expAssignments\""), "expAssignments should be omitted when null");
907907
}
908+
909+
@Test
910+
void testClonePreservesAndForwardsExpAssignments() throws Exception {
911+
var mapper = JsonRpcClient.getObjectMapper();
912+
JsonNode createAssignments = mapper.readTree("{\"Configs\":[{\"Id\":\"exp-create\"}]}");
913+
JsonNode resumeAssignments = mapper.readTree("{\"Configs\":[{\"Id\":\"exp-resume\"}]}");
914+
915+
var createConfig = new SessionConfig().setExpAssignments(createAssignments);
916+
SessionConfig createClone = createConfig.clone();
917+
assertEquals(createAssignments, createClone.getExpAssignments());
918+
CreateSessionRequest createRequest = SessionRequestBuilder.buildCreateRequest(createClone, "session-1");
919+
assertEquals(createAssignments, createRequest.getExpAssignments());
920+
assertTrue(mapper.writeValueAsString(createRequest).contains("\"Id\":\"exp-create\""));
921+
922+
var resumeConfig = new ResumeSessionConfig().setExpAssignments(resumeAssignments);
923+
ResumeSessionConfig resumeClone = resumeConfig.clone();
924+
assertEquals(resumeAssignments, resumeClone.getExpAssignments());
925+
ResumeSessionRequest resumeRequest = SessionRequestBuilder.buildResumeRequest("session-1", resumeClone);
926+
assertEquals(resumeAssignments, resumeRequest.getExpAssignments());
927+
assertTrue(mapper.writeValueAsString(resumeRequest).contains("\"Id\":\"exp-resume\""));
928+
}
908929
}

rust/src/types.rs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5030,6 +5030,41 @@ mod tests {
50305030
assert!(empty_json.get("expAssignments").is_none());
50315031
}
50325032

5033+
#[test]
5034+
fn session_config_clone_preserves_exp_assignments() {
5035+
let assignments = serde_json::json!({
5036+
"Parameters": { "copilot_exp_flag": "treatment" },
5037+
"AssignmentContext": "ctx-clone",
5038+
});
5039+
let config = SessionConfig::default().with_exp_assignments(assignments.clone());
5040+
let cloned = config.clone();
5041+
5042+
assert_eq!(cloned.exp_assignments.as_ref(), Some(&assignments));
5043+
5044+
let (wire, _runtime) = cloned
5045+
.into_wire(Some(SessionId::from("exp-clone")))
5046+
.expect("no duplicate handlers");
5047+
let json = serde_json::to_value(&wire).unwrap();
5048+
assert_eq!(json["expAssignments"], assignments);
5049+
}
5050+
5051+
#[test]
5052+
fn resume_session_config_clone_preserves_exp_assignments() {
5053+
let assignments = serde_json::json!({
5054+
"Parameters": { "copilot_exp_flag": "treatment" },
5055+
"AssignmentContext": "ctx-clone-resume",
5056+
});
5057+
let config = ResumeSessionConfig::new(SessionId::from("resume-exp-clone"))
5058+
.with_exp_assignments(assignments.clone());
5059+
let cloned = config.clone();
5060+
5061+
assert_eq!(cloned.exp_assignments.as_ref(), Some(&assignments));
5062+
5063+
let (wire, _runtime) = cloned.into_wire().expect("no duplicate handlers");
5064+
let json = serde_json::to_value(&wire).unwrap();
5065+
assert_eq!(json["expAssignments"], assignments);
5066+
}
5067+
50335068
#[test]
50345069
#[allow(clippy::field_reassign_with_default)]
50355070
fn session_config_into_wire_serializes_bucket_b_fields() {

0 commit comments

Comments
 (0)