-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Expand file tree
/
Copy pathhooks.test.ts
More file actions
145 lines (115 loc) · 5.41 KB
/
Copy pathhooks.test.ts
File metadata and controls
145 lines (115 loc) · 5.41 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
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------------------------------------------*/
import { 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 { 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({
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.destroy();
});
it("should invoke postToolUse hook after model runs a tool", async () => {
const postToolUseInputs: PostToolUseHookInput[] = [];
const session = await client.createSession({
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.destroy();
});
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({
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.destroy();
});
it("should deny tool execution when preToolUse returns deny", async () => {
const preToolUseInputs: PreToolUseHookInput[] = [];
const session = await client.createSession({
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();
await session.destroy();
});
});