-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Expand file tree
/
Copy pathmulti_turn.e2e.test.ts
More file actions
146 lines (127 loc) · 6.14 KB
/
multi_turn.e2e.test.ts
File metadata and controls
146 lines (127 loc) · 6.14 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
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------------------------------------------*/
import { writeFile } from "fs/promises";
import { join } from "path";
import { describe, expect, it } from "vitest";
import { SessionEvent, approveAll } from "../../src/index.js";
import { createSdkTestContext } from "./harness/sdkTestContext";
describe("Multi-turn Tool Usage", async () => {
const { copilotClient: client, workDir } = await createSdkTestContext();
function snapshotAndClearEvents(events: SessionEvent[]): SessionEvent[] {
const snapshot = [...events];
events.length = 0;
return snapshot;
}
function assertToolTurnOrdering(turnEvents: SessionEvent[], turnDescription: string): void {
const types = turnEvents.map((e) => e.type);
const observedTypes = types.join(", ");
const userMsgIdx = types.indexOf("user.message");
expect(
userMsgIdx,
`Expected user.message in ${turnDescription}. Observed: ${observedTypes}`
).toBeGreaterThanOrEqual(0);
const toolStarts = turnEvents
.map((e, i) => ({ e, i }))
.filter(({ e }) => e.type === "tool.execution_start");
const toolCompletes = turnEvents
.map((e, i) => ({ e, i }))
.filter(({ e }) => e.type === "tool.execution_complete");
expect(
toolStarts.length,
`Expected tool starts in ${turnDescription}. Observed: ${observedTypes}`
).toBeGreaterThan(0);
expect(
toolCompletes.length,
`Expected tool completes in ${turnDescription}. Observed: ${observedTypes}`
).toBeGreaterThan(0);
const firstToolStartIdx = Math.min(...toolStarts.map(({ i }) => i));
expect(
userMsgIdx,
`Expected user.message before first tool start in ${turnDescription}. Observed: ${observedTypes}`
).toBeLessThan(firstToolStartIdx);
for (const { e: complete, i: completeIdx } of toolCompletes) {
const matchingStart = toolStarts.find(
({ e: start, i: startIdx }) =>
start.data.toolCallId === complete.data.toolCallId && startIdx < completeIdx
);
expect(
matchingStart,
`Expected matching tool start for tool complete with id ${complete.data.toolCallId}`
).toBeDefined();
}
const lastToolCompleteIdx = Math.max(...toolCompletes.map(({ i }) => i));
let assistantAfterToolsIdx = -1;
for (let i = lastToolCompleteIdx + 1; i < turnEvents.length; i++) {
if (turnEvents[i]!.type === "assistant.message") {
assistantAfterToolsIdx = i;
break;
}
}
let sessionIdleIdx = -1;
const searchFrom = assistantAfterToolsIdx >= 0 ? assistantAfterToolsIdx + 1 : 0;
for (let i = searchFrom; i < turnEvents.length; i++) {
if (turnEvents[i]!.type === "session.idle") {
sessionIdleIdx = i;
break;
}
}
expect(
assistantAfterToolsIdx,
`Expected assistant.message after tool completion in ${turnDescription}. Observed: ${observedTypes}`
).toBeGreaterThanOrEqual(0);
expect(
sessionIdleIdx,
`Expected session.idle after assistant.message in ${turnDescription}. Observed: ${observedTypes}`
).toBeGreaterThanOrEqual(0);
expect(
lastToolCompleteIdx,
`Expected final tool completion before final assistant message in ${turnDescription}. Observed: ${observedTypes}`
).toBeLessThan(assistantAfterToolsIdx);
expect(
assistantAfterToolsIdx,
`Expected final assistant message before idle in ${turnDescription}. Observed: ${observedTypes}`
).toBeLessThan(sessionIdleIdx);
}
it("should use tool results from previous turns", async () => {
// Write a file, then ask the model to read it and reason about its content
await writeFile(join(workDir, "secret.txt"), "The magic number is 42.");
const session = await client.createSession({ onPermissionRequest: approveAll });
const events: SessionEvent[] = [];
session.on((event) => {
events.push(event);
});
const msg1 = await session.sendAndWait({
prompt: "Read the file 'secret.txt' and tell me what the magic number is.",
});
expect(msg1?.data.content).toContain("42");
assertToolTurnOrdering(snapshotAndClearEvents(events), "file read turn");
// Follow-up that requires context from the previous turn
const msg2 = await session.sendAndWait({
prompt: "What is that magic number multiplied by 2?",
});
expect(msg2?.data.content).toContain("84");
});
it("should handle file creation then reading across turns", async () => {
const session = await client.createSession({ onPermissionRequest: approveAll });
const events: SessionEvent[] = [];
session.on((event) => {
events.push(event);
});
// First turn: create a file
await session.sendAndWait({
prompt: "Create a file called 'greeting.txt' with the content 'Hello from multi-turn test'.",
});
// Verify file was created with correct content before checking ordering
const { readFile } = await import("fs/promises");
const createdContent = await readFile(join(workDir, "greeting.txt"), "utf-8");
expect(createdContent).toBe("Hello from multi-turn test");
assertToolTurnOrdering(snapshotAndClearEvents(events), "file creation turn");
// Second turn: read the file
const msg = await session.sendAndWait({
prompt: "Read the file 'greeting.txt' and tell me its exact contents.",
});
expect(msg?.data.content).toContain("Hello from multi-turn test");
assertToolTurnOrdering(snapshotAndClearEvents(events), "file read turn");
});
});