-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Expand file tree
/
Copy pathabort.e2e.test.ts
More file actions
156 lines (131 loc) · 5.69 KB
/
Copy pathabort.e2e.test.ts
File metadata and controls
156 lines (131 loc) · 5.69 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
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------------------------------------------*/
import { describe, expect, it } from "vitest";
import { z } from "zod";
import { approveAll, defineTool } from "../../src/index.js";
import { createSdkTestContext } from "./harness/sdkTestContext.js";
describe("Abort", async () => {
const { copilotClient: client } = await createSdkTestContext();
const TEST_TIMEOUT_MS = 120_000;
async function withTimeout<T>(promise: Promise<T>, ms: number, label: string): Promise<T> {
let timer: ReturnType<typeof setTimeout> | 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 abort during active streaming", { timeout: TEST_TIMEOUT_MS }, async () => {
const session = await client.createSession({
onPermissionRequest: approveAll,
streaming: true,
});
let firstDeltaResolve!: (value: void) => void;
const firstDeltaReceived = new Promise<void>((resolve) => {
firstDeltaResolve = resolve;
});
const events: { type: string }[] = [];
session.on((event) => {
events.push({ type: event.type });
if (event.type === "assistant.message_delta") {
firstDeltaResolve();
}
});
// Fire-and-forget — we'll abort before it finishes
void session.send({
prompt: "Write a very long essay about the history of computing, covering every decade from the 1940s to the 2020s in great detail.",
});
// Wait for at least one delta to arrive (proves streaming started)
await withTimeout(firstDeltaReceived, 60_000, "first assistant.message_delta");
const deltaEvents = events.filter((e) => e.type === "assistant.message_delta");
expect(deltaEvents.length).toBeGreaterThanOrEqual(1);
// Abort mid-stream
await session.abort();
// Session should be usable after abort. Wait for the specific recovery
// message rather than racing against a late idle from the aborted turn.
let recoveryResolve!: (content: string) => void;
const recoveryReceived = new Promise<string>((resolve) => {
recoveryResolve = resolve;
});
const unsubscribeRecovery = session.on((event) => {
if (event.type === "assistant.message") {
const content = event.data.content ?? "";
if (content.toLowerCase().includes("abort_recovery_ok")) {
recoveryResolve(content);
}
}
});
try {
await session.send({ prompt: "Say 'abort_recovery_ok'." });
const recoveryContent = await withTimeout(
recoveryReceived,
60_000,
"assistant.message containing abort_recovery_ok"
);
expect(recoveryContent.toLowerCase()).toContain("abort_recovery_ok");
} finally {
unsubscribeRecovery();
}
await session.disconnect();
});
it("should abort during active tool execution", { timeout: TEST_TIMEOUT_MS }, async () => {
let toolStartedResolve!: (value: string) => void;
const toolStarted = new Promise<string>((resolve) => {
toolStartedResolve = resolve;
});
let releaseToolResolve!: (value: string) => void;
const releaseTool = new Promise<string>((resolve) => {
releaseToolResolve = resolve;
});
const session = await client.createSession({
onPermissionRequest: approveAll,
tools: [
defineTool("slow_analysis", {
description: "A slow analysis tool that blocks until released",
parameters: z.object({
value: z.string().describe("Value to analyze"),
}),
handler: async ({ value }) => {
toolStartedResolve(value);
return await releaseTool;
},
}),
],
});
// Fire-and-forget
void session.send({
prompt: "Use slow_analysis with value 'test_abort'. Wait for the result.",
});
// Wait for the tool to start executing
const toolValue = await withTimeout(toolStarted, 60_000, "slow_analysis start");
expect(toolValue).toBe("test_abort");
// Abort while the tool is running
await session.abort();
// Release the tool so its task doesn't leak
releaseToolResolve("RELEASED_AFTER_ABORT");
// Session should be usable after abort — verify with a follow-up
let recoveryResolve!: (value: void) => void;
const recoveryReceived = new Promise<void>((resolve) => {
recoveryResolve = resolve;
});
session.on((event) => {
if (
event.type === "assistant.message" &&
event.data.content?.includes("tool_abort_recovery_ok")
) {
recoveryResolve();
}
});
void session.send({
prompt: "Say 'tool_abort_recovery_ok'.",
});
await withTimeout(recoveryReceived, 60_000, "tool abort recovery message");
await session.disconnect();
});
});