-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Expand file tree
/
Copy pathhooks.e2e.test.ts
More file actions
156 lines (125 loc) · 6.03 KB
/
hooks.e2e.test.ts
File metadata and controls
156 lines (125 loc) · 6.03 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 { readFile, writeFile } from "fs/promises";
import { join } from "path";
import { describe, expect, it } from "vitest";
import type {
PreToolUseHookInput,
PreToolUseHookOutput,
PostToolUseHookInput,
PostToolUseHookOutput,
} from "../../src/index.js";
import { approveAll } from "../../src/index.js";
import { createSdkTestContext } from "./harness/sdkTestContext.js";
describe("Session hooks", async () => {
const { copilotClient: client, workDir } = await createSdkTestContext();
it("should invoke preToolUse hook when model runs a tool", async () => {
const preToolUseInputs: PreToolUseHookInput[] = [];
const session = await client.createSession({
onPermissionRequest: approveAll,
hooks: {
onPreToolUse: async (input, invocation) => {
preToolUseInputs.push(input);
expect(invocation.sessionId).toBe(session.sessionId);
// Allow the tool to run
return { permissionDecision: "allow" } as PreToolUseHookOutput;
},
},
});
// Create a file for the model to read
await writeFile(join(workDir, "hello.txt"), "Hello from the test!");
await session.sendAndWait({
prompt: "Read the contents of hello.txt and tell me what it says",
});
// Should have received at least one preToolUse hook call
expect(preToolUseInputs.length).toBeGreaterThan(0);
// Should have received the tool name
expect(preToolUseInputs.some((input) => input.toolName)).toBe(true);
await session.disconnect();
});
it("should invoke postToolUse hook after model runs a tool", async () => {
const postToolUseInputs: PostToolUseHookInput[] = [];
const session = await client.createSession({
onPermissionRequest: approveAll,
hooks: {
onPostToolUse: async (input, invocation) => {
postToolUseInputs.push(input);
expect(invocation.sessionId).toBe(session.sessionId);
return null as PostToolUseHookOutput;
},
},
});
// Create a file for the model to read
await writeFile(join(workDir, "world.txt"), "World from the test!");
await session.sendAndWait({
prompt: "Read the contents of world.txt and tell me what it says",
});
// Should have received at least one postToolUse hook call
expect(postToolUseInputs.length).toBeGreaterThan(0);
// Should have received the tool name and result
expect(postToolUseInputs.some((input) => input.toolName)).toBe(true);
expect(postToolUseInputs.some((input) => input.toolResult !== undefined)).toBe(true);
await session.disconnect();
});
it("should invoke both preToolUse and postToolUse hooks for a single tool call", async () => {
const preToolUseInputs: PreToolUseHookInput[] = [];
const postToolUseInputs: PostToolUseHookInput[] = [];
const session = await client.createSession({
onPermissionRequest: approveAll,
hooks: {
onPreToolUse: async (input) => {
preToolUseInputs.push(input);
return { permissionDecision: "allow" } as PreToolUseHookOutput;
},
onPostToolUse: async (input) => {
postToolUseInputs.push(input);
return null as PostToolUseHookOutput;
},
},
});
await writeFile(join(workDir, "both.txt"), "Testing both hooks!");
await session.sendAndWait({
prompt: "Read the contents of both.txt",
});
// Both hooks should have been called
expect(preToolUseInputs.length).toBeGreaterThan(0);
expect(postToolUseInputs.length).toBeGreaterThan(0);
// The same tool should appear in both
const preToolNames = preToolUseInputs.map((i) => i.toolName);
const postToolNames = postToolUseInputs.map((i) => i.toolName);
const commonTool = preToolNames.find((name) => postToolNames.includes(name));
expect(commonTool).toBeDefined();
await session.disconnect();
});
it("should deny tool execution when preToolUse returns deny", async () => {
const preToolUseInputs: PreToolUseHookInput[] = [];
const session = await client.createSession({
onPermissionRequest: approveAll,
hooks: {
onPreToolUse: async (input) => {
preToolUseInputs.push(input);
// Deny all tool calls
return { permissionDecision: "deny" } as PreToolUseHookOutput;
},
},
});
// Create a file
const originalContent = "Original content that should not be modified";
await writeFile(join(workDir, "protected.txt"), originalContent);
const response = await session.sendAndWait({
prompt: "Edit protected.txt and replace 'Original' with 'Modified'",
});
// The hook should have been called
expect(preToolUseInputs.length).toBeGreaterThan(0);
// The response should indicate the tool was denied (behavior may vary)
// At minimum, we verify the hook was invoked
expect(response).toBeDefined();
// Strengthen: verify the actual deny behavior — the protected file was NOT
// modified by the runtime even though the LLM tried to edit it. The
// pre-tool-use hook denial blocks tool execution before it can mutate state.
const actualContent = await readFile(join(workDir, "protected.txt"), "utf-8");
expect(actualContent).toBe(originalContent);
await session.disconnect();
});
});