-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Expand file tree
/
Copy pathrpc_shell_user_requested.e2e.test.ts
More file actions
146 lines (129 loc) · 5.23 KB
/
Copy pathrpc_shell_user_requested.e2e.test.ts
File metadata and controls
146 lines (129 loc) · 5.23 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 { randomUUID } from "node:crypto";
import { existsSync, rmSync } from "node:fs";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
import { approveAll } from "../../src/index.js";
import { createSdkTestContext } from "./harness/sdkTestContext.js";
import { waitForCondition } from "./harness/sdkTestHelper.js";
describe("User-requested shell RPC", async () => {
const { copilotClient: client, homeDir } = await createSdkTestContext();
function compactUuid(): string {
return randomUUID().replace(/-/g, "");
}
function quotePowerShell(value: string): string {
return `'${value.replace(/'/g, "''")}'`;
}
function quoteSh(value: string): string {
return `'${value.replace(/'/g, "'\\''")}'`;
}
function createMarkerThenSleepCommand(markerPath: string, seconds: number): string {
if (process.platform === "win32") {
return `Set-Content -LiteralPath ${quotePowerShell(markerPath)} -Value 'running'; Start-Sleep -Seconds ${seconds}`;
}
return `echo running > ${quoteSh(markerPath)}; sleep ${seconds}`;
}
async function waitForFileExists(filePath: string): Promise<void> {
await waitForCondition(() => existsSync(filePath), {
timeoutMs: 30_000,
intervalMs: 100,
timeoutMessage: `Timed out waiting for the shell command to create '${filePath}'.`,
});
}
async function withTimeout<T>(
promise: Promise<T>,
timeoutMs: number,
message: string
): Promise<T> {
let timeout: ReturnType<typeof setTimeout> | undefined;
try {
return await Promise.race([
promise,
new Promise<never>((_, reject) => {
timeout = setTimeout(() => reject(new Error(message)), timeoutMs);
}),
]);
} finally {
if (timeout) {
clearTimeout(timeout);
}
}
}
function tryDeleteFile(filePath: string): void {
try {
rmSync(filePath, { force: true });
} catch {
// Best-effort cleanup.
}
}
it("should execute user requested shell command", { timeout: 120_000 }, async () => {
const session = await client.createSession({ onPermissionRequest: approveAll });
try {
const marker = `copilotusershell${compactUuid()}`;
const requestId = `req-${compactUuid()}`;
const result = await session.rpc.shell.executeUserRequested({
requestId,
command: `echo ${marker}`,
});
expect(result.success).toBe(true);
expect(result.exitCode).toBe(0);
expect(result.output).toContain(marker);
expect(result.toolCallId).toBeTruthy();
} finally {
await session.disconnect();
}
});
it("should cancel user requested shell command", { timeout: 120_000 }, async () => {
const session = await client.createSession({ onPermissionRequest: approveAll });
const markerPath = join(homeDir, `shell-cancel-${compactUuid()}.txt`);
let executeTask:
| Promise<Awaited<ReturnType<typeof session.rpc.shell.executeUserRequested>>>
| undefined;
let executeSettled = false;
try {
const missing = await session.rpc.shell.cancelUserRequested({
requestId: `missing-${compactUuid()}`,
});
expect(missing.cancelled).toBe(false);
const requestId = `req-${compactUuid()}`;
executeTask = session.rpc.shell.executeUserRequested({
requestId,
command: createMarkerThenSleepCommand(markerPath, 60),
});
executeTask
.finally(() => {
executeSettled = true;
})
.catch(() => {});
executeTask.catch(() => {});
await waitForFileExists(markerPath);
await waitForCondition(
async () => (await session.rpc.shell.cancelUserRequested({ requestId })).cancelled,
{
timeoutMs: 15_000,
intervalMs: 100,
timeoutMessage:
"Timed out waiting for the user-requested shell command to become cancellable.",
}
);
const result = await withTimeout(
executeTask,
30_000,
"Timed out waiting for cancelled shell command to finish."
);
expect(result.success).toBe(false);
} finally {
if (executeTask && !executeSettled) {
await withTimeout(
executeTask,
30_000,
"Timed out draining cancelled shell command."
).catch(() => {});
}
tryDeleteFile(markerPath);
await session.disconnect();
}
});
});