Skip to content

Commit b523d0a

Browse files
Copilotbrunoborges
andcommitted
Port upstream changes: PermissionRequestResultKind, CI detection fix, E2E test updates
Co-authored-by: brunoborges <129743+brunoborges@users.noreply.github.com>
1 parent c93a990 commit b523d0a

File tree

9 files changed

+483
-73
lines changed

9 files changed

+483
-73
lines changed

src/main/java/com/github/copilot/sdk/RpcHandlerDispatcher.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import com.github.copilot.sdk.events.AbstractSessionEvent;
1818
import com.github.copilot.sdk.events.SessionEventParser;
1919
import com.github.copilot.sdk.json.PermissionRequestResult;
20+
import com.github.copilot.sdk.json.PermissionRequestResultKind;
2021
import com.github.copilot.sdk.json.SessionLifecycleEvent;
2122
import com.github.copilot.sdk.json.SessionLifecycleEventMetadata;
2223
import com.github.copilot.sdk.json.ToolDefinition;
@@ -183,7 +184,7 @@ private void handlePermissionRequest(JsonRpcClient rpc, String requestId, JsonNo
183184
CopilotSession session = sessions.get(sessionId);
184185
if (session == null) {
185186
var result = new PermissionRequestResult()
186-
.setKind("denied-no-approval-rule-and-could-not-request-from-user");
187+
.setKind(PermissionRequestResultKind.DENIED_COULD_NOT_REQUEST_FROM_USER);
187188
rpc.sendResponse(Long.parseLong(requestId), Map.of("result", result));
188189
return;
189190
}
@@ -197,7 +198,7 @@ private void handlePermissionRequest(JsonRpcClient rpc, String requestId, JsonNo
197198
}).exceptionally(ex -> {
198199
try {
199200
var result = new PermissionRequestResult()
200-
.setKind("denied-no-approval-rule-and-could-not-request-from-user");
201+
.setKind(PermissionRequestResultKind.DENIED_COULD_NOT_REQUEST_FROM_USER);
201202
rpc.sendResponse(Long.parseLong(requestId), Map.of("result", result));
202203
} catch (IOException e) {
203204
LOG.log(Level.SEVERE, "Error sending permission denied", e);

src/main/java/com/github/copilot/sdk/json/PermissionHandler.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ public interface PermissionHandler {
4646
* @since 1.0.11
4747
*/
4848
PermissionHandler APPROVE_ALL = (request, invocation) -> CompletableFuture
49-
.completedFuture(new PermissionRequestResult().setKind("approved"));
49+
.completedFuture(new PermissionRequestResult().setKind(PermissionRequestResultKind.APPROVED));
5050

5151
/**
5252
* Handles a permission request from the assistant.

src/main/java/com/github/copilot/sdk/json/PermissionRequestResult.java

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,17 @@
1717
*
1818
* <h2>Common Result Kinds</h2>
1919
* <ul>
20-
* <li>"user-approved" - User approved the permission request</li>
21-
* <li>"user-denied" - User denied the permission request</li>
22-
* <li>"denied-no-approval-rule-and-could-not-request-from-user" - No handler
23-
* and couldn't ask user</li>
20+
* <li>{@link PermissionRequestResultKind#APPROVED} — approved</li>
21+
* <li>{@link PermissionRequestResultKind#DENIED_BY_RULES} — denied by
22+
* rules</li>
23+
* <li>{@link PermissionRequestResultKind#DENIED_COULD_NOT_REQUEST_FROM_USER} —
24+
* no handler and couldn't ask user</li>
25+
* <li>{@link PermissionRequestResultKind#DENIED_INTERACTIVELY_BY_USER} — denied
26+
* by the user interactively</li>
2427
* </ul>
2528
*
2629
* @see PermissionHandler
30+
* @see PermissionRequestResultKind
2731
* @since 1.0.0
2832
*/
2933
@JsonInclude(JsonInclude.Include.NON_NULL)
@@ -36,7 +40,7 @@ public final class PermissionRequestResult {
3640
private List<Object> rules;
3741

3842
/**
39-
* Gets the result kind.
43+
* Gets the result kind as a string.
4044
*
4145
* @return the result kind indicating approval or denial
4246
*/
@@ -45,11 +49,24 @@ public String getKind() {
4549
}
4650

4751
/**
48-
* Sets the result kind.
52+
* Sets the result kind using a {@link PermissionRequestResultKind} value.
4953
*
5054
* @param kind
5155
* the result kind
5256
* @return this result for method chaining
57+
* @since 1.1.0
58+
*/
59+
public PermissionRequestResult setKind(PermissionRequestResultKind kind) {
60+
this.kind = kind != null ? kind.getValue() : null;
61+
return this;
62+
}
63+
64+
/**
65+
* Sets the result kind using a raw string value.
66+
*
67+
* @param kind
68+
* the result kind string
69+
* @return this result for method chaining
5370
*/
5471
public PermissionRequestResult setKind(String kind) {
5572
this.kind = kind;
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
*--------------------------------------------------------------------------------------------*/
4+
5+
package com.github.copilot.sdk.json;
6+
7+
import java.util.Objects;
8+
9+
import com.fasterxml.jackson.annotation.JsonCreator;
10+
import com.fasterxml.jackson.annotation.JsonValue;
11+
12+
/**
13+
* Describes the outcome kind of a permission request result.
14+
*
15+
* <p>
16+
* This is a string-backed value type that can hold both well-known kinds (via
17+
* the static constants) and arbitrary extension values forwarded by the server.
18+
* Comparisons are case-insensitive to match server behaviour.
19+
*
20+
* <h2>Well-known kinds</h2>
21+
* <ul>
22+
* <li>{@link #APPROVED} — the permission was approved.</li>
23+
* <li>{@link #DENIED_BY_RULES} — the permission was denied by policy
24+
* rules.</li>
25+
* <li>{@link #DENIED_COULD_NOT_REQUEST_FROM_USER} — the permission was denied
26+
* because no approval rule was found and the user could not be prompted.</li>
27+
* <li>{@link #DENIED_INTERACTIVELY_BY_USER} — the permission was denied
28+
* interactively by the user.</li>
29+
* </ul>
30+
*
31+
* @see PermissionRequestResult
32+
* @since 1.1.0
33+
*/
34+
public final class PermissionRequestResultKind {
35+
36+
/** The permission was approved. */
37+
public static final PermissionRequestResultKind APPROVED = new PermissionRequestResultKind("approved");
38+
39+
/** The permission was denied by policy rules. */
40+
public static final PermissionRequestResultKind DENIED_BY_RULES = new PermissionRequestResultKind(
41+
"denied-by-rules");
42+
43+
/**
44+
* The permission was denied because no approval rule was found and the user
45+
* could not be prompted.
46+
*/
47+
public static final PermissionRequestResultKind DENIED_COULD_NOT_REQUEST_FROM_USER = new PermissionRequestResultKind(
48+
"denied-no-approval-rule-and-could-not-request-from-user");
49+
50+
/** The permission was denied interactively by the user. */
51+
public static final PermissionRequestResultKind DENIED_INTERACTIVELY_BY_USER = new PermissionRequestResultKind(
52+
"denied-interactively-by-user");
53+
54+
private final String value;
55+
56+
/**
57+
* Creates a new {@code PermissionRequestResultKind} with the given string
58+
* value. Useful for extension kinds not covered by the well-known constants.
59+
*
60+
* @param value
61+
* the string value; {@code null} is treated as an empty string
62+
*/
63+
@JsonCreator
64+
public PermissionRequestResultKind(String value) {
65+
this.value = value != null ? value : "";
66+
}
67+
68+
/**
69+
* Returns the underlying string value of this kind.
70+
*
71+
* @return the string value, never {@code null}
72+
*/
73+
@JsonValue
74+
public String getValue() {
75+
return value;
76+
}
77+
78+
@Override
79+
public String toString() {
80+
return value;
81+
}
82+
83+
@Override
84+
public boolean equals(Object obj) {
85+
if (this == obj) {
86+
return true;
87+
}
88+
if (!(obj instanceof PermissionRequestResultKind)) {
89+
return false;
90+
}
91+
PermissionRequestResultKind other = (PermissionRequestResultKind) obj;
92+
return value.equalsIgnoreCase(other.value);
93+
}
94+
95+
@Override
96+
public int hashCode() {
97+
return Objects.hashCode(value.toLowerCase(java.util.Locale.ROOT));
98+
}
99+
}

src/site/markdown/advanced.md

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -557,16 +557,28 @@ Approve or deny permission requests from the AI.
557557

558558
```java
559559
var session = client.createSession(
560-
new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)
561-
.setOnPermissionRequest((request, invocation) -> {
562-
// Inspect request and approve/deny
563-
var result = new PermissionRequestResult();
564-
result.setKind("user-approved");
565-
return CompletableFuture.completedFuture(result);
566-
})
560+
new SessionConfig().setOnPermissionRequest((request, invocation) -> {
561+
// Inspect request and approve/deny using typed constants
562+
var result = new PermissionRequestResult();
563+
result.setKind(PermissionRequestResultKind.APPROVED);
564+
return CompletableFuture.completedFuture(result);
565+
})
567566
).get();
568567
```
569568

569+
The `PermissionRequestResultKind` class provides well-known constants for common outcomes:
570+
571+
| Constant | Value | Meaning |
572+
|---|---|---|
573+
| `PermissionRequestResultKind.APPROVED` | `"approved"` | The permission was approved |
574+
| `PermissionRequestResultKind.DENIED_BY_RULES` | `"denied-by-rules"` | Denied by policy rules |
575+
| `PermissionRequestResultKind.DENIED_COULD_NOT_REQUEST_FROM_USER` | `"denied-no-approval-rule-and-could-not-request-from-user"` | No rule and user could not be prompted |
576+
| `PermissionRequestResultKind.DENIED_INTERACTIVELY_BY_USER` | `"denied-interactively-by-user"` | User denied interactively |
577+
578+
You can also pass a raw string to `setKind(String)` for custom or extension values. Use
579+
[`PermissionHandler.APPROVE_ALL`](apidocs/com/github/copilot/sdk/json/PermissionHandler.html) to approve all
580+
requests without writing a handler.
581+
570582
---
571583

572584
## Session Hooks

src/test/java/com/github/copilot/sdk/CopilotSessionTest.java

Lines changed: 27 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@
2323

2424
import com.github.copilot.sdk.events.AbstractSessionEvent;
2525
import com.github.copilot.sdk.events.AbortEvent;
26-
import com.github.copilot.sdk.events.AssistantMessageDeltaEvent;
2726
import com.github.copilot.sdk.events.AssistantMessageEvent;
2827
import com.github.copilot.sdk.events.SessionIdleEvent;
2928
import com.github.copilot.sdk.events.SessionStartEvent;
@@ -286,6 +285,14 @@ void testShouldResumeSessionUsingTheSameClient() throws Exception {
286285
.map(m -> (AssistantMessageEvent) m).anyMatch(m -> m.getData().content().contains("2"));
287286
assertTrue(hasAssistantMessage, "Should find previous assistant message containing 2");
288287

288+
// Can continue the conversation statefully
289+
AssistantMessageEvent answer2 = session2
290+
.sendAndWait(new MessageOptions().setPrompt("Now if you double that, what do you get?"))
291+
.get(60, TimeUnit.SECONDS);
292+
assertNotNull(answer2);
293+
assertTrue(answer2.getData().content().contains("4"),
294+
"Follow-up response should contain 4: " + answer2.getData().content());
295+
289296
session2.close();
290297
}
291298
}
@@ -327,6 +334,14 @@ void testShouldResumeSessionUsingNewClient() throws Exception {
327334
assertTrue(messages.stream().anyMatch(m -> "session.resume".equals(m.getType())),
328335
"Should contain session.resume event");
329336

337+
// Can continue the conversation statefully
338+
AssistantMessageEvent answer2 = session2
339+
.sendAndWait(new MessageOptions().setPrompt("Now if you double that, what do you get?"))
340+
.get(60, TimeUnit.SECONDS);
341+
assertNotNull(answer2);
342+
assertTrue(answer2.getData().content().contains("4"),
343+
"Follow-up response should contain 4: " + answer2.getData().content());
344+
330345
session2.close();
331346
}
332347
}
@@ -394,44 +409,6 @@ void testShouldCreateSessionWithReplacedSystemMessageConfig() throws Exception {
394409
}
395410
}
396411

397-
/**
398-
* Verifies that streaming delta events are received when streaming is enabled.
399-
*
400-
* @see Snapshot:
401-
* session/should_receive_streaming_delta_events_when_streaming_is_enabled
402-
*/
403-
@Test
404-
void testShouldReceiveStreamingDeltaEventsWhenStreamingIsEnabled() throws Exception {
405-
ctx.configureForTest("session", "should_receive_streaming_delta_events_when_streaming_is_enabled");
406-
407-
try (CopilotClient client = ctx.createClient()) {
408-
SessionConfig config = new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)
409-
.setStreaming(true);
410-
411-
CopilotSession session = client.createSession(config).get();
412-
413-
var receivedEvents = new ArrayList<AbstractSessionEvent>();
414-
var idleReceived = new CompletableFuture<Void>();
415-
416-
session.on(evt -> {
417-
receivedEvents.add(evt);
418-
if (evt instanceof SessionIdleEvent) {
419-
idleReceived.complete(null);
420-
}
421-
});
422-
423-
session.send(new MessageOptions().setPrompt("What is 2+2?")).get();
424-
425-
idleReceived.get(60, TimeUnit.SECONDS);
426-
427-
// Should have received delta events when streaming is enabled
428-
boolean hasDeltaEvents = receivedEvents.stream().anyMatch(e -> e instanceof AssistantMessageDeltaEvent);
429-
assertTrue(hasDeltaEvents, "Should receive streaming delta events when streaming is enabled");
430-
431-
session.close();
432-
}
433-
}
434-
435412
/**
436413
* Verifies that a session can be aborted during tool execution.
437414
*
@@ -764,29 +741,24 @@ void testShouldCreateSessionWithCustomTool() throws Exception {
764741
}
765742

766743
/**
767-
* Verifies that streaming option is passed to session creation.
744+
* Verifies that getLastSessionId returns the ID of the most recently used
745+
* session.
768746
*
769-
* @see Snapshot: session/should_pass_streaming_option_to_session_creation
747+
* @see Snapshot: session/should_get_last_session_id
770748
*/
771749
@Test
772-
void testShouldPassStreamingOptionToSessionCreation() throws Exception {
773-
ctx.configureForTest("session", "should_pass_streaming_option_to_session_creation");
750+
void testShouldGetLastSessionId() throws Exception {
751+
ctx.configureForTest("session", "should_get_last_session_id");
774752

775753
try (CopilotClient client = ctx.createClient()) {
776-
// Verify that the streaming option is accepted without errors
777-
CopilotSession session = client.createSession(
778-
new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setStreaming(true)).get();
779-
780-
assertNotNull(session.getSessionId());
781-
assertTrue(session.getSessionId().matches("^[a-f0-9-]+$"));
754+
CopilotSession session = client
755+
.createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get();
782756

783-
// Session should still work normally
784-
AssistantMessageEvent response = session.sendAndWait(new MessageOptions().setPrompt("What is 1+1?")).get(60,
785-
TimeUnit.SECONDS);
757+
session.sendAndWait(new MessageOptions().setPrompt("Say hello")).get(60, TimeUnit.SECONDS);
786758

787-
assertNotNull(response);
788-
assertTrue(response.getData().content().contains("2"),
789-
"Response should contain 2: " + response.getData().content());
759+
String lastId = client.getLastSessionId().get(30, TimeUnit.SECONDS);
760+
assertNotNull(lastId, "Last session ID should not be null");
761+
assertEquals(session.getSessionId(), lastId, "Last session ID should match the current session ID");
790762

791763
session.close();
792764
}

src/test/java/com/github/copilot/sdk/E2ETestContext.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -263,8 +263,8 @@ public CopilotClient createClient() {
263263
CopilotClientOptions options = new CopilotClientOptions().setCliPath(cliPath).setCwd(workDir.toString())
264264
.setEnvironment(getEnvironment());
265265

266-
// In CI, use a fake token to avoid auth issues
267-
String ci = System.getenv("CI");
266+
// In CI (GitHub Actions), use a fake token to avoid auth issues
267+
String ci = System.getenv("GITHUB_ACTIONS");
268268
if (ci != null && !ci.isEmpty()) {
269269
options.setGitHubToken("fake-token-for-e2e-tests");
270270
}

0 commit comments

Comments
 (0)