Skip to content

Commit 2fb9d0f

Browse files
committed
Add event handler dispatch and threading tests
- Add 7 JUnit tests to SessionEventHandlingTest: duplicate typed/generic handlers, unsubscribe-one-keeps-other, all-handlers-invoked, handlers-run-on-dispatch-thread, handlers-run-off-main-thread, and concurrent dispatch from multiple threads - Add JBang standalone test (jbang-test-duplicate-handlers.java) with the same coverage for quick out-of-build verification
1 parent 412c8be commit 2fb9d0f

2 files changed

Lines changed: 417 additions & 0 deletions

File tree

jbang-test-duplicate-handlers.java

Lines changed: 269 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,269 @@
1+
2+
//DEPS io.github.copilot-community-sdk:copilot-sdk:1.0.7
3+
import com.github.copilot.sdk.*;
4+
import com.github.copilot.sdk.events.*;
5+
import java.lang.reflect.*;
6+
import java.util.*;
7+
import java.util.concurrent.atomic.*;
8+
import java.util.logging.*;
9+
10+
/**
11+
* JBang test: verifies behavior when multiple handlers are registered for the same event type.
12+
*
13+
* Run with: jbang jbang-test-duplicate-handlers.java
14+
*/
15+
class DuplicateHandlersTest {
16+
17+
private static int passed = 0;
18+
private static int failed = 0;
19+
20+
public static void main(String[] args) throws Exception {
21+
var session = createTestSession();
22+
23+
testBothTypedHandlersReceiveEvent(session);
24+
testBothGenericHandlersFire(createTestSession());
25+
testMixedGenericAndTypedBothFire(createTestSession());
26+
testUnsubscribeOneKeepsOther(createTestSession());
27+
testAllHandlersInvoked(createTestSession());
28+
testHandlerExceptionDoesNotBlockSecond(createTestSession());
29+
testHandlersRunOnDispatchThread(createTestSession());
30+
testHandlersRunOffMainThread(createTestSession());
31+
testConcurrentDispatchFromMultipleThreads(createTestSession());
32+
33+
System.out.println();
34+
System.out.println("========================================");
35+
System.out.printf("Results: %d passed, %d failed%n", passed, failed);
36+
System.out.println("========================================");
37+
38+
if (failed > 0) {
39+
System.exit(1);
40+
}
41+
}
42+
43+
// --- Tests ---
44+
45+
static void testBothTypedHandlersReceiveEvent(CopilotSession session) throws Exception {
46+
var count1 = new AtomicInteger();
47+
var count2 = new AtomicInteger();
48+
49+
session.on(AssistantMessageEvent.class, msg -> count1.incrementAndGet());
50+
session.on(AssistantMessageEvent.class, msg -> count2.incrementAndGet());
51+
52+
dispatchEvent(session, createAssistantMessageEvent("hello"));
53+
54+
assertEq("Both typed handlers called", 1, count1.get());
55+
assertEq("Both typed handlers called (2nd)", 1, count2.get());
56+
}
57+
58+
static void testBothGenericHandlersFire(CopilotSession session) throws Exception {
59+
var events1 = new ArrayList<String>();
60+
var events2 = new ArrayList<String>();
61+
62+
session.on(event -> events1.add(event.getType()));
63+
session.on(event -> events2.add(event.getType()));
64+
65+
dispatchEvent(session, createAssistantMessageEvent("test"));
66+
67+
assertEq("Generic handler 1 received event", 1, events1.size());
68+
assertEq("Generic handler 2 received event", 1, events2.size());
69+
}
70+
71+
static void testMixedGenericAndTypedBothFire(CopilotSession session) throws Exception {
72+
var genericCount = new AtomicInteger();
73+
var typedCount = new AtomicInteger();
74+
75+
session.on(event -> genericCount.incrementAndGet());
76+
session.on(AssistantMessageEvent.class, msg -> typedCount.incrementAndGet());
77+
78+
dispatchEvent(session, createAssistantMessageEvent("test"));
79+
80+
assertEq("Generic handler fired", 1, genericCount.get());
81+
assertEq("Typed handler fired", 1, typedCount.get());
82+
}
83+
84+
static void testUnsubscribeOneKeepsOther(CopilotSession session) throws Exception {
85+
var count1 = new AtomicInteger();
86+
var count2 = new AtomicInteger();
87+
88+
var sub1 = session.on(AssistantMessageEvent.class, msg -> count1.incrementAndGet());
89+
session.on(AssistantMessageEvent.class, msg -> count2.incrementAndGet());
90+
91+
dispatchEvent(session, createAssistantMessageEvent("before"));
92+
assertEq("Handler 1 before unsub", 1, count1.get());
93+
assertEq("Handler 2 before unsub", 1, count2.get());
94+
95+
// Unsubscribe handler 1
96+
sub1.close();
97+
98+
dispatchEvent(session, createAssistantMessageEvent("after"));
99+
assertEq("Handler 1 after unsub (unchanged)", 1, count1.get());
100+
assertEq("Handler 2 after unsub (incremented)", 2, count2.get());
101+
}
102+
103+
static void testAllHandlersInvoked(CopilotSession session) throws Exception {
104+
var called = new ArrayList<String>();
105+
106+
session.on(AssistantMessageEvent.class, msg -> called.add("first"));
107+
session.on(AssistantMessageEvent.class, msg -> called.add("second"));
108+
session.on(AssistantMessageEvent.class, msg -> called.add("third"));
109+
110+
dispatchEvent(session, createAssistantMessageEvent("test"));
111+
112+
assertEq("Three handlers called", 3, called.size());
113+
assertEq("All handlers invoked", true, called.containsAll(List.of("first", "second", "third")));
114+
}
115+
116+
static void testHandlerExceptionDoesNotBlockSecond(CopilotSession session) throws Exception {
117+
var reached = new AtomicInteger();
118+
119+
session.on(AssistantMessageEvent.class, msg -> {
120+
throw new RuntimeException("Boom!");
121+
});
122+
session.on(AssistantMessageEvent.class, msg -> reached.incrementAndGet());
123+
124+
// Suppress CopilotSession logger to avoid noisy stack trace output
125+
var logger = java.util.logging.Logger.getLogger(CopilotSession.class.getName());
126+
var originalLevel = logger.getLevel();
127+
logger.setLevel(java.util.logging.Level.OFF);
128+
try {
129+
dispatchEvent(session, createAssistantMessageEvent("test"));
130+
} finally {
131+
logger.setLevel(originalLevel);
132+
}
133+
134+
assertEq("Second handler still called after first threw", 1, reached.get());
135+
}
136+
137+
/**
138+
* Verifies handlers execute on the thread that calls dispatchEvent,
139+
* simulating the real jsonrpc-reader thread.
140+
*/
141+
static void testHandlersRunOnDispatchThread(CopilotSession session) throws Exception {
142+
var handlerThreadName = new AtomicReference<String>();
143+
144+
session.on(AssistantMessageEvent.class, msg -> {
145+
handlerThreadName.set(Thread.currentThread().getName());
146+
});
147+
148+
// Dispatch from a named thread to simulate the jsonrpc-reader
149+
var t = new Thread(() -> {
150+
try {
151+
dispatchEvent(session, createAssistantMessageEvent("async"));
152+
} catch (Exception e) {
153+
throw new RuntimeException(e);
154+
}
155+
}, "jsonrpc-reader-mock");
156+
t.start();
157+
t.join(5000);
158+
159+
assertEq("Handler ran on dispatch thread", "jsonrpc-reader-mock", handlerThreadName.get());
160+
}
161+
162+
/**
163+
* Verifies that when dispatched from a background thread, handlers
164+
* do NOT run on the main thread — proving async delivery.
165+
*/
166+
static void testHandlersRunOffMainThread(CopilotSession session) throws Exception {
167+
var mainThreadName = Thread.currentThread().getName();
168+
var handlerThreadName = new AtomicReference<String>();
169+
var latch = new java.util.concurrent.CountDownLatch(1);
170+
171+
session.on(AssistantMessageEvent.class, msg -> {
172+
handlerThreadName.set(Thread.currentThread().getName());
173+
latch.countDown();
174+
});
175+
176+
// Dispatch from a background thread (simulates jsonrpc-reader)
177+
new Thread(() -> {
178+
try {
179+
dispatchEvent(session, createAssistantMessageEvent("bg"));
180+
} catch (Exception e) {
181+
throw new RuntimeException(e);
182+
}
183+
}, "background-dispatcher").start();
184+
185+
var completed = latch.await(5, java.util.concurrent.TimeUnit.SECONDS);
186+
assertEq("Handler was invoked", true, completed);
187+
assertEq("Handler did NOT run on main thread", true,
188+
!mainThreadName.equals(handlerThreadName.get()));
189+
assertEq("Handler ran on background thread", "background-dispatcher",
190+
handlerThreadName.get());
191+
}
192+
193+
/**
194+
* Verifies thread safety: concurrent dispatches from multiple threads
195+
* all reach registered handlers without lost events.
196+
*/
197+
static void testConcurrentDispatchFromMultipleThreads(CopilotSession session) throws Exception {
198+
var totalEvents = 100;
199+
var receivedCount = new AtomicInteger();
200+
var threadNames = java.util.concurrent.ConcurrentHashMap.<String>newKeySet();
201+
var latch = new java.util.concurrent.CountDownLatch(totalEvents);
202+
203+
session.on(AssistantMessageEvent.class, msg -> {
204+
receivedCount.incrementAndGet();
205+
threadNames.add(Thread.currentThread().getName());
206+
latch.countDown();
207+
});
208+
209+
// Fire events from 10 concurrent threads, 10 events each
210+
var threads = new ArrayList<Thread>();
211+
for (int i = 0; i < 10; i++) {
212+
var threadIdx = i;
213+
var t = new Thread(() -> {
214+
for (int j = 0; j < 10; j++) {
215+
try {
216+
dispatchEvent(session, createAssistantMessageEvent("msg-" + threadIdx + "-" + j));
217+
} catch (Exception e) {
218+
throw new RuntimeException(e);
219+
}
220+
}
221+
}, "dispatcher-" + i);
222+
threads.add(t);
223+
}
224+
225+
// Start all threads
226+
for (var t : threads) t.start();
227+
228+
// Wait for all events to be delivered
229+
var completed = latch.await(10, java.util.concurrent.TimeUnit.SECONDS);
230+
for (var t : threads) t.join(5000);
231+
232+
assertEq("All " + totalEvents + " events delivered", totalEvents, receivedCount.get());
233+
assertEq("Latch completed", true, completed);
234+
assertEq("Multiple threads dispatched", true, threadNames.size() > 1);
235+
}
236+
237+
// --- Helpers ---
238+
239+
static CopilotSession createTestSession() throws Exception {
240+
var rpcClass = Class.forName("com.github.copilot.sdk.JsonRpcClient");
241+
var ctor = CopilotSession.class.getDeclaredConstructor(String.class, rpcClass, String.class);
242+
ctor.setAccessible(true);
243+
return ctor.newInstance("test-session", null, null);
244+
}
245+
246+
static void dispatchEvent(CopilotSession session, AbstractSessionEvent event) throws Exception {
247+
var method = CopilotSession.class.getDeclaredMethod("dispatchEvent", AbstractSessionEvent.class);
248+
method.setAccessible(true);
249+
method.invoke(session, event);
250+
}
251+
252+
static AssistantMessageEvent createAssistantMessageEvent(String content) {
253+
var event = new AssistantMessageEvent();
254+
var data = new AssistantMessageEvent.AssistantMessageData();
255+
data.setContent(content);
256+
event.setData(data);
257+
return event;
258+
}
259+
260+
static void assertEq(String testName, Object expected, Object actual) {
261+
if (Objects.equals(expected, actual)) {
262+
System.out.println(" ✓ " + testName);
263+
passed++;
264+
} else {
265+
System.out.println(" ✗ " + testName + " — expected: " + expected + ", got: " + actual);
266+
failed++;
267+
}
268+
}
269+
}

0 commit comments

Comments
 (0)