|
2 | 2 | * Copyright (c) Microsoft Corporation. All rights reserved. |
3 | 3 | *--------------------------------------------------------------------------------------------*/ |
4 | 4 |
|
5 | | -import { describe, expect, it, onTestFinished, vi } from "vitest"; |
| 5 | +import { beforeEach, describe, expect, it, onTestFinished } from "vitest"; |
6 | 6 | import { CopilotClient } from "../../src/client.js"; |
7 | 7 | import { approveAll, type SessionFsConfig } from "../../src/index.js"; |
8 | 8 | import { createSdkTestContext } from "./harness/sdkTestContext.js"; |
@@ -30,6 +30,14 @@ class InMemorySessionFs { |
30 | 30 | rename: 0, |
31 | 31 | }; |
32 | 32 |
|
| 33 | + public reset() { |
| 34 | + this.files.clear(); |
| 35 | + this.dirs.clear(); |
| 36 | + for (const key in this.calls) { |
| 37 | + this.calls[key as keyof typeof this.calls] = 0; |
| 38 | + } |
| 39 | + } |
| 40 | + |
33 | 41 | private getSessionFiles(sessionId: string): Map<string, string> { |
34 | 42 | let m = this.files.get(sessionId); |
35 | 43 | if (!m) { |
@@ -203,109 +211,71 @@ class InMemorySessionFs { |
203 | 211 | } |
204 | 212 | } |
205 | 213 |
|
206 | | -// These tests require a runtime built with SessionFs support. |
207 | | -// Skip when COPILOT_CLI_PATH is not set (CI uses the published CLI which |
208 | | -// doesn't include this feature yet). |
209 | | -const runTests = process.env.COPILOT_CLI_PATH ? describe : describe.skip; |
210 | | - |
211 | | -runTests("Session Fs", async () => { |
212 | | - const { env } = await createSdkTestContext(); |
| 214 | +describe("Session Fs", async () => { |
| 215 | + const fs = new InMemorySessionFs(); |
| 216 | + beforeEach(() => fs.reset()); |
213 | 217 |
|
214 | | - it("should route file operations through the session fs provider", async () => { |
215 | | - const fs = new InMemorySessionFs(); |
216 | | - const client1 = new CopilotClient({ |
217 | | - env, |
218 | | - logLevel: "error", |
219 | | - cliPath: process.env.COPILOT_CLI_PATH, |
| 218 | + const { copilotClient: client, env } = await createSdkTestContext({ |
| 219 | + copilotClientOptions: { |
220 | 220 | sessionFs: fs.toConfig("/projects/test", "/session-state"), |
221 | | - }); |
222 | | - onTestFinished(() => client1.forceStop()); |
| 221 | + }, |
| 222 | + }); |
223 | 223 |
|
224 | | - const session = await client1.createSession({ |
225 | | - onPermissionRequest: approveAll, |
226 | | - }); |
| 224 | + it("should route file operations through the session fs provider", async () => { |
| 225 | + const session = await client.createSession({ onPermissionRequest: approveAll }); |
227 | 226 |
|
228 | 227 | // Send a message and wait for the response |
229 | 228 | const msg = await session.sendAndWait({ prompt: "What is 100 + 200?" }); |
230 | 229 | expect(msg?.data.content).toContain("300"); |
231 | 230 |
|
232 | 231 | // Verify file operations were routed through our fs provider. |
233 | 232 | // The runtime writes events as JSONL through appendFile/writeFile. |
234 | | - await vi.waitFor( |
235 | | - () => { |
236 | | - const paths = fs.getFilePaths(session.sessionId); |
237 | | - const hasEvents = paths.some((p) => p.includes("events")); |
238 | | - expect(hasEvents).toBe(true); |
239 | | - }, |
240 | | - { timeout: 10_000, interval: 200 }, |
241 | | - ); |
242 | | - expect(fs.calls.writeFile + fs.calls.appendFile).toBeGreaterThan(0); |
243 | | - expect(fs.calls.mkdir).toBeGreaterThan(0); |
| 233 | + // TODO: Replace these assertions with reading the events.jsonl file |
| 234 | + await expect.poll(() => fs.calls.writeFile + fs.calls.appendFile).toBeGreaterThan(0); |
244 | 235 | }); |
245 | 236 |
|
246 | 237 | it("should load session data from fs provider on resume", async () => { |
247 | | - const sessionFs = new InMemorySessionFs(); |
248 | | - |
249 | | - const client2 = new CopilotClient({ |
250 | | - env, |
251 | | - logLevel: "error", |
252 | | - cliPath: process.env.COPILOT_CLI_PATH, |
253 | | - sessionFs: sessionFs.toConfig("/projects/test", "/session-state"), |
254 | | - }); |
255 | | - onTestFinished(() => client2.forceStop()); |
256 | | - |
257 | | - // Create a session and send a message |
258 | | - const session1 = await client2.createSession({ |
259 | | - onPermissionRequest: approveAll, |
260 | | - }); |
| 238 | + const session1 = await client.createSession({ onPermissionRequest: approveAll }); |
261 | 239 | const sessionId = session1.sessionId; |
262 | 240 |
|
263 | | - const msg1 = await session1.sendAndWait({ prompt: "What is 50 + 50?" }); |
264 | | - expect(msg1?.data.content).toContain("100"); |
| 241 | + const msg = await session1.sendAndWait({ prompt: "What is 50 + 50?" }); |
| 242 | + expect(msg?.data.content).toContain("100"); |
265 | 243 | await session1.disconnect(); |
266 | 244 |
|
267 | 245 | // Verify readFile is called when resuming (to load events) |
268 | | - const readCountBefore = sessionFs.calls.readFile; |
269 | | - const session2 = await client2.resumeSession(sessionId, { |
| 246 | + const readCountBefore = fs.calls.readFile; |
| 247 | + const session2 = await client.resumeSession(sessionId, { |
270 | 248 | onPermissionRequest: approveAll, |
271 | 249 | }); |
272 | 250 |
|
273 | | - expect(sessionFs.calls.readFile).toBeGreaterThan(readCountBefore); |
| 251 | + expect(fs.calls.readFile).toBeGreaterThan(readCountBefore); |
274 | 252 |
|
275 | 253 | // Send another message to verify the session is functional |
276 | 254 | const msg2 = await session2.sendAndWait({ prompt: "What is that times 3?" }); |
277 | 255 | expect(msg2?.data.content).toContain("300"); |
278 | 256 | }); |
279 | 257 |
|
280 | 258 | it("should reject setProvider when sessions already exist", async () => { |
281 | | - // First client uses TCP so a second client can connect to the same runtime |
282 | | - const client5 = new CopilotClient({ |
| 259 | + const client = new CopilotClient({ |
| 260 | + useStdio: false, // Use TCP so we can connect from a second client |
283 | 261 | env, |
284 | | - logLevel: "error", |
285 | | - cliPath: process.env.COPILOT_CLI_PATH, |
286 | | - useStdio: false, |
287 | 262 | }); |
288 | | - onTestFinished(() => client5.forceStop()); |
289 | | - |
290 | | - const session = await client5.createSession({ |
291 | | - onPermissionRequest: approveAll, |
292 | | - }); |
293 | | - await session.sendAndWait({ prompt: "Hello" }); |
| 263 | + await client.createSession({ onPermissionRequest: approveAll }); |
294 | 264 |
|
295 | 265 | // Get the port the first client's runtime is listening on |
296 | | - const port = (client5 as unknown as { actualPort: number }).actualPort; |
| 266 | + const port = (client as unknown as { actualPort: number }).actualPort; |
297 | 267 |
|
298 | 268 | // Second client tries to connect with a session fs — should fail |
299 | 269 | // because sessions already exist on the runtime. |
300 | 270 | const sessionFs = new InMemorySessionFs(); |
301 | | - const client6 = new CopilotClient({ |
| 271 | + const client2 = new CopilotClient({ |
302 | 272 | env, |
303 | 273 | logLevel: "error", |
304 | 274 | cliUrl: `localhost:${port}`, |
305 | 275 | sessionFs: sessionFs.toConfig("/projects/test", "/session-state"), |
306 | 276 | }); |
307 | | - onTestFinished(() => client6.forceStop()); |
| 277 | + onTestFinished(() => client2.forceStop()); |
308 | 278 |
|
309 | | - await expect(client6.start()).rejects.toThrow(); |
| 279 | + await expect(client2.start()).rejects.toThrow(); |
310 | 280 | }); |
311 | 281 | }); |
0 commit comments