-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathdelete-file-plugin.test.ts
More file actions
144 lines (114 loc) · 5.27 KB
/
Copy pathdelete-file-plugin.test.ts
File metadata and controls
144 lines (114 loc) · 5.27 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
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
import { chmod, mkdtemp, mkdir, rm, stat, symlink, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import type { ToolCall, ToolResult } from "@intx/types/runtime";
import { deleteFilePlugin } from "./delete-file-plugin.js";
import { pathEscapePlugin } from "./path-escape-plugin.js";
import { createPermissionGate } from "../permission/gate.js";
import { permissionPlugin } from "./permission-plugin.js";
function call(path: unknown): ToolCall {
return { id: "delete-call", name: "delete_file", arguments: { path } };
}
async function exists(path: string): Promise<boolean> {
try {
await stat(path);
return true;
} catch {
return false;
}
}
describe("deleteFilePlugin", () => {
let cwd: string;
beforeEach(async () => {
cwd = await mkdtemp(join(tmpdir(), "corbits-delete-file-"));
});
afterEach(async () => {
await chmod(cwd, 0o700).catch(() => {});
await rm(cwd, { recursive: true, force: true });
});
function handler(): (call: ToolCall, signal: AbortSignal) => Promise<ToolResult> {
const tool = deleteFilePlugin(cwd).tools?.[0];
if (tool === undefined) throw new Error("delete_file tool was not registered");
return tool.handler;
}
test("deletes an existing file with an explicit outcome", async () => {
const path = join(cwd, "old.txt");
await writeFile(path, "old");
const result = await handler()(call("old.txt"), new AbortController().signal);
expect(result).toEqual({ callId: "delete-call", content: "Deleted file: old.txt" });
expect(await exists(path)).toBe(false);
});
test("reports an absent file as a successful no-op", async () => {
const result = await handler()(call("missing.txt"), new AbortController().signal);
expect(result).toEqual({ callId: "delete-call", content: "File already absent: missing.txt (no action needed)" });
});
test("refuses to delete directories", async () => {
await mkdir(join(cwd, "folder"));
const result = await handler()(call("folder"), new AbortController().signal);
expect(result.isError).toBe(true);
expect(result.content).toContain("is a directory");
expect(await exists(join(cwd, "folder"))).toBe(true);
});
test("restricted paths are blocked before deletion", async () => {
const outside = await mkdtemp(join(tmpdir(), "corbits-delete-outside-"));
const path = join(outside, "keep.txt");
await writeFile(path, "keep");
const next = handler();
const guarded = pathEscapePlugin(cwd).middleware?.(next) ?? next;
const result = await guarded(call(path), new AbortController().signal);
expect(result.isError).toBe(true);
expect(result.content).toContain("escapes working directory");
expect(await exists(path)).toBe(true);
await rm(outside, { recursive: true, force: true });
});
test("refuses files reached through a directory symlink outside the workspace", async () => {
const outside = await mkdtemp(join(tmpdir(), "corbits-delete-symlink-outside-"));
const path = join(outside, "keep.txt");
await writeFile(path, "keep");
await symlink(outside, join(cwd, "linked-outside"));
const result = await handler()(call("linked-outside/keep.txt"), new AbortController().signal);
expect(result.isError).toBe(true);
expect(result.content).toContain("resolves outside the working directory");
expect(await exists(path)).toBe(true);
await rm(outside, { recursive: true, force: true });
});
test("allowOutside deletes a file outside the working directory", async () => {
const outside = await mkdtemp(join(tmpdir(), "corbits-delete-yolo-"));
const path = join(outside, "gone.txt");
await writeFile(path, "gone");
const tool = deleteFilePlugin(cwd, { allowOutside: true }).tools?.[0];
if (tool === undefined) throw new Error("delete_file tool was not registered");
const result = await tool.handler(call(path), new AbortController().signal);
expect(result).toEqual({ callId: "delete-call", content: `Deleted file: ${path}` });
expect(await exists(path)).toBe(false);
await rm(outside, { recursive: true, force: true });
});
test("permission denial prevents deletion", async () => {
const path = join(cwd, "keep.txt");
await writeFile(path, "keep");
const next = handler();
const gate = createPermissionGate({
approvals: [],
interactive: true,
skipPermissions: false,
cwd,
requestApproval: async () => ({ allow: false }),
});
const guarded = permissionPlugin(gate).middleware?.(next) ?? next;
const result = await guarded(call("keep.txt"), new AbortController().signal);
expect(result.isError).toBe(true);
expect(result.content).toContain("Operator declined");
expect(await exists(path)).toBe(true);
});
test("preserves filesystem failure details", async () => {
const path = join(cwd, "locked.txt");
await writeFile(path, "keep");
await chmod(cwd, 0o500);
const result = await handler()(call("locked.txt"), new AbortController().signal);
await chmod(cwd, 0o700);
expect(result.isError).toBe(true);
expect(String(result.content)).toMatch(/EACCES|EPERM|permission denied|operation not permitted/i);
expect(await exists(path)).toBe(true);
});
});