-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Expand file tree
/
Copy pathui_elicitation.e2e.test.ts
More file actions
189 lines (162 loc) · 7.25 KB
/
Copy pathui_elicitation.e2e.test.ts
File metadata and controls
189 lines (162 loc) · 7.25 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
177
178
179
180
181
182
183
184
185
186
187
188
189
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------------------------------------------*/
import { afterAll, describe, expect, it } from "vitest";
import { CopilotClient, approveAll, RuntimeConnection } from "../../src/index.js";
import type { SessionEvent } from "../../src/index.js";
import { createSdkTestContext, isInProcessTransport } from "./harness/sdkTestContext.js";
describe("UI Elicitation", async () => {
const { copilotClient: client } = await createSdkTestContext();
it("elicitation methods throw in headless mode", async () => {
const session = await client.createSession({
onPermissionRequest: approveAll,
});
// The SDK spawns the CLI headless - no TUI means no elicitation support.
expect(session.capabilities.ui?.elicitation).toBeFalsy();
await expect(session.ui.confirm("test")).rejects.toThrow(/not supported/);
});
});
describe("UI Elicitation Callback", async () => {
const ctx = await createSdkTestContext();
const client = ctx.copilotClient;
it(
"session created with onElicitationRequest reports elicitation capability",
{ timeout: 60_000 },
async () => {
const session = await client.createSession({
onPermissionRequest: approveAll,
onElicitationRequest: async () => ({ action: "accept", content: {} }),
});
expect(session.capabilities.ui?.elicitation).toBe(true);
}
);
it(
"session created without onElicitationRequest reports no elicitation capability",
{ timeout: 60_000 },
async () => {
const session = await client.createSession({
onPermissionRequest: approveAll,
});
expect(session.capabilities.ui?.elicitation).toBe(false);
}
);
});
describe("UI Elicitation Multi-Client Capabilities", async () => {
// Use TCP mode so a second client can connect to the same CLI process
const tcpConnectionToken = "ui-elicitation-test-token";
const ctx = await createSdkTestContext({
useStdio: false,
copilotClientOptions: {
connection: RuntimeConnection.forTcp({ connectionToken: tcpConnectionToken }),
},
});
const client1 = ctx.copilotClient;
// Trigger connection so we can read the port
const initSession = await client1.createSession({ onPermissionRequest: approveAll });
await initSession.disconnect();
const { runtimePort } = client1 as unknown as { runtimePort: number };
const client2 = new CopilotClient({
connection: RuntimeConnection.forUri(`localhost:${runtimePort}`, {
connectionToken: tcpConnectionToken,
}),
});
afterAll(async () => {
await client2.stop();
});
it(
"capabilities.changed fires when second client joins with elicitation handler",
{ timeout: 60_000 },
async () => {
// Client1 creates session without elicitation
const session1 = await client1.createSession({
onPermissionRequest: approveAll,
});
expect(session1.capabilities.ui?.elicitation).toBe(false);
// Listen for capabilities.changed event
let unsubscribe: (() => void) | undefined;
const capChangedPromise = new Promise<SessionEvent>((resolve) => {
unsubscribe = session1.on((event) => {
if ((event as { type: string }).type === "capabilities.changed") {
resolve(event);
}
});
});
// Client2 joins WITH elicitation handler — triggers capabilities.changed
const session2 = await client2.resumeSession(session1.sessionId, {
onPermissionRequest: approveAll,
onElicitationRequest: async () => ({ action: "accept", content: {} }),
suppressResumeEvent: true,
});
const capEvent = await capChangedPromise;
unsubscribe?.();
const data = (capEvent as { data: { ui?: { elicitation?: boolean } } }).data;
expect(data.ui?.elicitation).toBe(true);
// Client1's capabilities should have been auto-updated
expect(session1.capabilities.ui?.elicitation).toBe(true);
await session2.disconnect();
}
);
it.skipIf(isInProcessTransport)(
"capabilities.changed fires when elicitation provider disconnects",
{ timeout: 60_000 },
async () => {
// Client1 creates session without elicitation
const session1 = await client1.createSession({
onPermissionRequest: approveAll,
});
expect(session1.capabilities.ui?.elicitation).toBe(false);
// Wait for elicitation to become available
let unsubEnabled: (() => void) | undefined;
const capEnabledPromise = new Promise<void>((resolve) => {
unsubEnabled = session1.on((event) => {
const data = event as {
type: string;
data: { ui?: { elicitation?: boolean } };
};
if (
data.type === "capabilities.changed" &&
data.data.ui?.elicitation === true
) {
resolve();
}
});
});
// Use a dedicated client so we can stop it without affecting shared client2
const client3 = new CopilotClient({
connection: RuntimeConnection.forUri(`localhost:${runtimePort}`, {
connectionToken: tcpConnectionToken,
}),
});
// Client3 joins WITH elicitation handler
await client3.resumeSession(session1.sessionId, {
onPermissionRequest: approveAll,
onElicitationRequest: async () => ({ action: "accept", content: {} }),
suppressResumeEvent: true,
});
await capEnabledPromise;
unsubEnabled?.();
expect(session1.capabilities.ui?.elicitation).toBe(true);
// Now listen for the capability being removed
let unsubDisabled: (() => void) | undefined;
const capDisabledPromise = new Promise<void>((resolve) => {
unsubDisabled = session1.on((event) => {
const data = event as {
type: string;
data: { ui?: { elicitation?: boolean } };
};
if (
data.type === "capabilities.changed" &&
data.data.ui?.elicitation === false
) {
resolve();
}
});
});
// Force-stop client3 — destroys the socket, triggering server-side cleanup
await client3.forceStop();
await capDisabledPromise;
unsubDisabled?.();
expect(session1.capabilities.ui?.elicitation).toBe(false);
}
);
});