forked from github/copilot-sdk
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathCopilotRequestHandlerE2ETest.java
More file actions
176 lines (149 loc) · 8.37 KB
/
Copy pathCopilotRequestHandlerE2ETest.java
File metadata and controls
176 lines (149 loc) · 8.37 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------------------------------------------*/
package com.github.copilot;
import static com.github.copilot.CopilotRequestTestSupport.SYNTHETIC_TEXT;
import static com.github.copilot.CopilotRequestTestSupport.assistantText;
import static com.github.copilot.CopilotRequestTestSupport.newLlmClient;
import static com.github.copilot.CopilotRequestTestSupport.setupCapiAuth;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.io.InputStream;
import java.net.URI;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import com.github.copilot.CopilotRequestTestSupport.InterceptedRequest;
import com.github.copilot.CopilotRequestTestSupport.RecordingRequestHandler;
import com.github.copilot.generated.AssistantMessageEvent;
import com.github.copilot.rpc.MessageOptions;
import com.github.copilot.rpc.PermissionHandler;
import com.github.copilot.rpc.SessionConfig;
/**
* End-to-end coverage for {@link CopilotRequestHandler}: a synthetic HTTP turn
* that the handler fully fabricates off-network, and a forwarding turn that
* relays both the HTTP and WebSocket transports to a real in-process upstream.
*/
public class CopilotRequestHandlerE2ETest {
private static E2ETestContext ctx;
@BeforeAll
static void setup() throws Exception {
ctx = E2ETestContext.create();
}
@AfterAll
static void teardown() throws Exception {
if (ctx != null) {
ctx.close();
}
}
@Test
void streamsSyntheticHttpInference() throws Exception {
setupCapiAuth(ctx);
RecordingRequestHandler handler = new RecordingRequestHandler(SYNTHETIC_TEXT);
try (CopilotClient client = newLlmClient(ctx, handler)) {
CopilotSession session = client
.createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get();
AssistantMessageEvent result = session.sendAndWait(new MessageOptions().setPrompt("Say OK.")).get(60,
TimeUnit.SECONDS);
session.close();
// The handler intercepted the startup catalog and at least one inference
// request, fully replacing the runtime's outbound model-layer calls.
List<InterceptedRequest> records = handler.records();
assertFalse(records.isEmpty(), "Expected the runtime to invoke the request handler");
assertTrue(records.stream().anyMatch(r -> r.url().toLowerCase(Locale.ROOT).endsWith("/models")),
"Expected to intercept the /models catalog request");
assertFalse(handler.inferenceRequests().isEmpty(),
"Expected at least one inference request via the handler");
// Validate the final assistant response arrived (guards against truncated
// captures)
assertTrue(assistantText(result).contains("OK from the synthetic"),
"Expected synthetic content in assistant reply, got " + assistantText(result));
}
}
@Test
void forwardsHttpAndWebSocketToUpstream() throws Exception {
setupCapiAuth(ctx);
AtomicInteger httpRequests = new AtomicInteger();
AtomicInteger httpResponses = new AtomicInteger();
AtomicInteger wsRequestMessages = new AtomicInteger();
AtomicInteger wsResponseMessages = new AtomicInteger();
try (FakeUpstreamServer upstream = new FakeUpstreamServer("OK from synthetic HTTP upstream.",
"OK from synthetic WS upstream.")) {
String httpBase = upstream.httpUrl();
String wsBase = upstream.wsUrl();
CopilotRequestHandler handler = new CopilotRequestHandler() {
@Override
protected HttpResponse<InputStream> sendRequest(HttpRequest request, CopilotRequestContext rctx)
throws Exception {
httpRequests.incrementAndGet();
URI rewritten = URI.create(rewriteHost(httpBase, request.uri()));
HttpRequest.Builder builder = HttpRequest.newBuilder().uri(rewritten);
request.bodyPublisher().ifPresentOrElse(bp -> builder.method(request.method(), bp),
() -> builder.method(request.method(), HttpRequest.BodyPublishers.noBody()));
request.headers().map().forEach((name, values) -> {
for (String value : values) {
try {
builder.header(name, value);
} catch (IllegalArgumentException ignored) {
// Restricted header rejected by java.net.http; skip it.
}
}
});
builder.header("x-test-mutated", "1");
HttpResponse<InputStream> response = httpClient()
.sendAsync(builder.build(), HttpResponse.BodyHandlers.ofInputStream()).get();
httpResponses.incrementAndGet();
return response;
}
@Override
protected CopilotWebSocketHandler openWebSocket(CopilotRequestContext rctx) {
return new CopilotWebSocketForwarder(rctx.withUrl(rewriteHost(wsBase, URI.create(rctx.url())))) {
@Override
public void sendRequestMessage(CopilotWebSocketMessage message) throws Exception {
wsRequestMessages.incrementAndGet();
super.sendRequestMessage(message);
}
@Override
public void sendResponseMessage(CopilotWebSocketMessage message) throws Exception {
wsResponseMessages.incrementAndGet();
super.sendResponseMessage(message);
}
};
}
};
try (CopilotClient client = newLlmClient(ctx, handler,
"COPILOT_EXP_COPILOT_CLI_WEBSOCKET_RESPONSES=true")) {
CopilotSession session = client
.createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get();
AssistantMessageEvent result = session.sendAndWait(new MessageOptions().setPrompt("Say OK.")).get(60,
TimeUnit.SECONDS);
session.close();
// The HTTP override fired — the runtime issued model-layer GETs (catalog,
// policy) and possibly a single-shot inference through the send override.
assertTrue(httpRequests.get() > 0, "Expected the HTTP send override to fire");
assertTrue(httpResponses.get() > 0, "Expected the HTTP response mutation to fire");
// The WebSocket override fired — the main agent turn went over the WS path
// and we observed messages in both directions.
assertTrue(wsRequestMessages.get() > 0, "Expected runtime -> upstream ws messages");
assertTrue(wsResponseMessages.get() > 0, "Expected upstream -> runtime ws messages");
assertTrue(upstream.upstreamWsRequests() > 0, "Expected the upstream WS to receive request messages");
// Validate the final assistant response arrived (guards against truncated
// captures)
String text = assistantText(result);
assertTrue(text.contains("OK from synthetic") && text.contains("upstream"),
"Expected synthetic upstream content in assistant reply, got " + text);
}
}
}
private static String rewriteHost(String base, URI original) {
String path = original.getRawPath() == null ? "" : original.getRawPath();
String query = original.getRawQuery();
return base + path + (query != null ? "?" + query : "");
}
}