-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Expand file tree
/
Copy pathclient_lifecycle.e2e.test.ts
More file actions
206 lines (168 loc) · 7.71 KB
/
client_lifecycle.e2e.test.ts
File metadata and controls
206 lines (168 loc) · 7.71 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
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------------------------------------------*/
import { describe, expect, it } from "vitest";
import { SessionLifecycleEvent, approveAll } from "../../src/index.js";
import { createSdkTestContext } from "./harness/sdkTestContext";
describe("Client Lifecycle", async () => {
const { copilotClient: client } = await createSdkTestContext();
function deferred<T>(): { promise: Promise<T>; resolve: (value: T) => void } {
let resolveFn!: (value: T) => void;
const promise = new Promise<T>((resolve) => {
resolveFn = resolve;
});
return { promise, resolve: resolveFn };
}
async function withTimeout<T>(promise: Promise<T>, ms: number, label: string): Promise<T> {
let timer: NodeJS.Timeout | undefined;
try {
return await Promise.race([
promise,
new Promise<T>((_, reject) => {
timer = setTimeout(() => reject(new Error(`Timeout: ${label}`)), ms);
}),
]);
} finally {
if (timer) clearTimeout(timer);
}
}
it("should return last session id after sending a message", async () => {
const session = await client.createSession({ onPermissionRequest: approveAll });
await session.sendAndWait({ prompt: "Say hello" });
// Poll until getLastSessionId returns something rather than a hard 500ms wait.
// (Using await with a polling loop keeps fast machines fast and slow CI safe.)
let lastSessionId: string | undefined;
const deadline = Date.now() + 10_000;
while (Date.now() < deadline) {
lastSessionId = await client.getLastSessionId();
if (lastSessionId) break;
await new Promise((r) => setTimeout(r, 50));
}
// In parallel test runs we can't guarantee the last session ID matches
// this specific session, since other tests may flush session data concurrently.
expect(lastSessionId).toBeTruthy();
await session.disconnect();
});
it("should return undefined for getLastSessionId with no sessions", async () => {
// On a fresh client this may return undefined or an older session ID
const lastSessionId = await client.getLastSessionId();
expect(lastSessionId === undefined || typeof lastSessionId === "string").toBe(true);
});
it("should emit session lifecycle events", async () => {
const events: SessionLifecycleEvent[] = [];
const unsubscribe = client.onLifecycle((event: SessionLifecycleEvent) => {
events.push(event);
});
try {
const session = await client.createSession({ onPermissionRequest: approveAll });
await session.sendAndWait({ prompt: "Say hello" });
// Poll for the session-specific event rather than a hard 500ms wait.
const deadline = Date.now() + 10_000;
while (
Date.now() < deadline &&
!events.some((e) => e.sessionId === session.sessionId)
) {
await new Promise((r) => setTimeout(r, 50));
}
// Lifecycle events may not fire in all runtimes
if (events.length > 0) {
const sessionEvents = events.filter((e) => e.sessionId === session.sessionId);
expect(sessionEvents.length).toBeGreaterThan(0);
}
await session.disconnect();
} finally {
unsubscribe();
}
});
it("should receive session created lifecycle event", async () => {
const created = deferred<SessionLifecycleEvent>();
const unsubscribe = client.onLifecycle((evt) => {
if (evt.type === "session.created") {
created.resolve(evt);
}
});
try {
const session = await client.createSession({ onPermissionRequest: approveAll });
const evt = await withTimeout(created.promise, 10_000, "session.created");
expect(evt.type).toBe("session.created");
expect(evt.sessionId).toBe(session.sessionId);
await session.disconnect();
} finally {
unsubscribe();
}
});
it("should filter session lifecycle events by type", async () => {
const created = deferred<SessionLifecycleEvent>();
const unsubscribe = client.onLifecycle("session.created", (evt) => {
created.resolve(evt);
});
try {
const session = await client.createSession({ onPermissionRequest: approveAll });
const evt = await withTimeout(created.promise, 10_000, "session.created (filtered)");
expect(evt.type).toBe("session.created");
expect(evt.sessionId).toBe(session.sessionId);
await session.disconnect();
} finally {
unsubscribe();
}
});
it("disposing lifecycle subscription stops receiving events", async () => {
let count = 0;
const created = deferred<SessionLifecycleEvent>();
const unsubscribeFirst = client.onLifecycle(() => {
count += 1;
});
unsubscribeFirst();
const unsubscribeActive = client.onLifecycle("session.created", (evt) => {
created.resolve(evt);
});
try {
const session = await client.createSession({ onPermissionRequest: approveAll });
const evt = await withTimeout(created.promise, 10_000, "session.created");
expect(evt.sessionId).toBe(session.sessionId);
expect(count).toBe(0);
await session.disconnect();
} finally {
unsubscribeActive();
}
});
it("should receive session updated lifecycle event for non ephemeral activity", async () => {
const session = await client.createSession({ onPermissionRequest: approveAll });
const updated = deferred<SessionLifecycleEvent>();
const unsubscribe = client.onLifecycle("session.updated", (evt) => {
if (evt.sessionId === session.sessionId) {
updated.resolve(evt);
}
});
try {
// Setting a non-ephemeral mode triggers a session.updated lifecycle event
await session.rpc.mode.set({ mode: "plan" });
const evt = await withTimeout(updated.promise, 10_000, "session.updated");
expect(evt.type).toBe("session.updated");
expect(evt.sessionId).toBe(session.sessionId);
} finally {
unsubscribe();
await session.disconnect();
}
});
it("should receive session deleted lifecycle event when deleted", async () => {
const session = await client.createSession({ onPermissionRequest: approveAll });
// Make an LLM call first to ensure the session is persisted
const message = await session.sendAndWait({ prompt: "Say SESSION_DELETED_OK exactly." });
expect(message?.data.content).toContain("SESSION_DELETED_OK");
const deleted = deferred<SessionLifecycleEvent>();
const unsubscribe = client.onLifecycle("session.deleted", (evt) => {
if (evt.sessionId === session.sessionId) {
deleted.resolve(evt);
}
});
try {
await client.deleteSession(session.sessionId);
const evt = await withTimeout(deleted.promise, 10_000, "session.deleted");
expect(evt.type).toBe("session.deleted");
expect(evt.sessionId).toBe(session.sessionId);
} finally {
unsubscribe();
}
});
});