forked from github/copilot-sdk-java
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathTimeoutEdgeCaseTest.java
More file actions
142 lines (121 loc) · 5.73 KB
/
TimeoutEdgeCaseTest.java
File metadata and controls
142 lines (121 loc) · 5.73 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
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------------------------------------------*/
package com.github.copilot.sdk;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.Socket;
import java.util.concurrent.CompletableFuture;
import org.junit.jupiter.api.Test;
import com.github.copilot.sdk.events.AssistantMessageEvent;
import com.github.copilot.sdk.json.MessageOptions;
/**
* Regression tests for timeout edge cases in
* {@link CopilotSession#sendAndWait}.
* <p>
* These tests assert two behavioral contracts of the shared
* {@code ScheduledExecutorService} approach:
* <ol>
* <li>A pending timeout must NOT fire after {@code close()} and must NOT
* complete the returned future with a {@code TimeoutException}.</li>
* <li>Multiple {@code sendAndWait} calls must reuse a single shared scheduler
* thread rather than spawning a new OS thread per call.</li>
* </ol>
*/
public class TimeoutEdgeCaseTest {
/**
* Creates a {@link JsonRpcClient} whose {@code invoke()} returns futures that
* never complete. The reader thread blocks forever on the input stream, and
* writes go to a no-op output stream.
*/
private JsonRpcClient createHangingRpcClient() throws Exception {
InputStream blockingInput = new InputStream() {
@Override
public int read() throws IOException {
try {
Thread.sleep(Long.MAX_VALUE);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return -1;
}
return -1;
}
};
ByteArrayOutputStream sinkOutput = new ByteArrayOutputStream();
var ctor = JsonRpcClient.class.getDeclaredConstructor(InputStream.class, java.io.OutputStream.class,
Socket.class, Process.class);
ctor.setAccessible(true);
return (JsonRpcClient) ctor.newInstance(blockingInput, sinkOutput, null, null);
}
/**
* After {@code close()}, the future returned by {@code sendAndWait} must NOT be
* completed by a stale timeout.
* <p>
* Contract: {@code close()} shuts down the timeout scheduler before the
* blocking {@code session.destroy} RPC call, so any pending timeout task is
* cancelled and the future remains incomplete (not exceptionally completed with
* {@code TimeoutException}).
*/
@Test
void testTimeoutDoesNotFireAfterSessionClose() throws Exception {
JsonRpcClient rpc = createHangingRpcClient();
try {
try (CopilotSession session = new CopilotSession("test-timeout-id", rpc)) {
CompletableFuture<AssistantMessageEvent> result = session
.sendAndWait(new MessageOptions().setPrompt("hello"), 2000);
assertFalse(result.isDone(), "Future should be pending before timeout fires");
// close() blocks up to 5s on session.destroy RPC. The 2s timeout
// fires during that window with the current per-call scheduler.
session.close();
assertFalse(result.isDone(), "Future should not be completed by a timeout after session is closed. "
+ "The per-call ScheduledExecutorService leaked a TimeoutException.");
}
} finally {
rpc.close();
}
}
/**
* A shared scheduler must reuse a single thread across multiple
* {@code sendAndWait} calls, rather than spawning a new OS thread per call.
* <p>
* Contract: after two consecutive {@code sendAndWait} calls the number of live
* {@code sendAndWait-timeout} threads must not increase after the second call.
*/
@Test
void testSendAndWaitReusesTimeoutThread() throws Exception {
JsonRpcClient rpc = createHangingRpcClient();
try {
try (CopilotSession session = new CopilotSession("test-thread-count-id", rpc)) {
long baselineCount = countTimeoutThreads();
CompletableFuture<AssistantMessageEvent> result1 = session
.sendAndWait(new MessageOptions().setPrompt("hello1"), 30000);
Thread.sleep(100);
long afterFirst = countTimeoutThreads();
assertTrue(afterFirst >= baselineCount + 1,
"Expected at least one new sendAndWait-timeout thread after first call. " + "Baseline: "
+ baselineCount + ", after: " + afterFirst);
CompletableFuture<AssistantMessageEvent> result2 = session
.sendAndWait(new MessageOptions().setPrompt("hello2"), 30000);
Thread.sleep(100);
long afterSecond = countTimeoutThreads();
assertTrue(afterSecond == afterFirst,
"Shared scheduler should reuse the same thread — no new threads after second call. "
+ "After first: " + afterFirst + ", after second: " + afterSecond);
result1.cancel(true);
result2.cancel(true);
}
} finally {
rpc.close();
}
}
/**
* Counts the number of live threads whose name contains "sendAndWait-timeout".
*/
private long countTimeoutThreads() {
return Thread.getAllStackTraces().keySet().stream().filter(t -> t.getName().contains("sendAndWait-timeout"))
.filter(Thread::isAlive).count();
}
}