@gotgenes/pi-permission-system 3.0.0 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/package.json +1 -1
- package/schemas/permissions.schema.json +96 -12
- package/src/active-agent.ts +58 -0
- package/src/external-directory.ts +113 -0
- package/src/forwarded-permissions/io.ts +328 -0
- package/src/forwarded-permissions/polling.ts +334 -0
- package/src/index.ts +59 -1062
- package/src/permission-prompts.ts +131 -0
- package/src/subagent-context.ts +52 -0
- package/src/tool-input-preview.ts +206 -0
- package/tests/active-agent.test.ts +160 -0
- package/tests/bash-filter.test.ts +137 -0
- package/tests/common.test.ts +189 -0
- package/tests/external-directory.test.ts +250 -0
- package/tests/permission-prompts.test.ts +301 -0
- package/tests/skill-prompt-sanitizer.test.ts +244 -0
- package/tests/subagent-context.test.ts +124 -0
- package/tests/system-prompt-sanitizer.test.ts +186 -0
- package/tests/tool-input-preview.test.ts +452 -0
- package/tests/tool-registry.test.ts +155 -0
- package/tests/wildcard-matcher.test.ts +180 -0
- package/tests/yolo-mode.test.ts +110 -0
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
extractFrontmatter,
|
|
5
|
+
getNonEmptyString,
|
|
6
|
+
isPermissionState,
|
|
7
|
+
parseSimpleYamlMap,
|
|
8
|
+
toRecord,
|
|
9
|
+
} from "../src/common.js";
|
|
10
|
+
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
vi.restoreAllMocks();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
describe("toRecord", () => {
|
|
16
|
+
test("returns empty object for null", () => {
|
|
17
|
+
expect(toRecord(null)).toEqual({});
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test("returns empty object for undefined", () => {
|
|
21
|
+
expect(toRecord(undefined)).toEqual({});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("returns empty object for a string", () => {
|
|
25
|
+
expect(toRecord("hello")).toEqual({});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("returns empty object for a number", () => {
|
|
29
|
+
expect(toRecord(42)).toEqual({});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("returns empty object for an array", () => {
|
|
33
|
+
expect(toRecord(["a", "b"])).toEqual({});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("returns the object itself for a plain object", () => {
|
|
37
|
+
const input = { a: 1, b: "two" };
|
|
38
|
+
expect(toRecord(input)).toBe(input);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("returns the object for a nested object", () => {
|
|
42
|
+
const input = { x: { y: 3 } };
|
|
43
|
+
expect(toRecord(input)).toBe(input);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe("getNonEmptyString", () => {
|
|
48
|
+
test("returns null for non-string values", () => {
|
|
49
|
+
expect(getNonEmptyString(null)).toBeNull();
|
|
50
|
+
expect(getNonEmptyString(undefined)).toBeNull();
|
|
51
|
+
expect(getNonEmptyString(42)).toBeNull();
|
|
52
|
+
expect(getNonEmptyString({})).toBeNull();
|
|
53
|
+
expect(getNonEmptyString([])).toBeNull();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("returns null for empty string", () => {
|
|
57
|
+
expect(getNonEmptyString("")).toBeNull();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("returns null for whitespace-only string", () => {
|
|
61
|
+
expect(getNonEmptyString(" ")).toBeNull();
|
|
62
|
+
expect(getNonEmptyString("\t\n")).toBeNull();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("returns trimmed string for valid string", () => {
|
|
66
|
+
expect(getNonEmptyString("hello")).toBe("hello");
|
|
67
|
+
expect(getNonEmptyString(" hello ")).toBe("hello");
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("returns single non-whitespace character", () => {
|
|
71
|
+
expect(getNonEmptyString("a")).toBe("a");
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe("isPermissionState", () => {
|
|
76
|
+
test("returns true for 'allow'", () => {
|
|
77
|
+
expect(isPermissionState("allow")).toBe(true);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("returns true for 'deny'", () => {
|
|
81
|
+
expect(isPermissionState("deny")).toBe(true);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("returns true for 'ask'", () => {
|
|
85
|
+
expect(isPermissionState("ask")).toBe(true);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("returns false for unrecognized strings", () => {
|
|
89
|
+
expect(isPermissionState("ALLOW")).toBe(false);
|
|
90
|
+
expect(isPermissionState("permit")).toBe(false);
|
|
91
|
+
expect(isPermissionState("")).toBe(false);
|
|
92
|
+
expect(isPermissionState("block")).toBe(false);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("returns false for non-string types", () => {
|
|
96
|
+
expect(isPermissionState(null)).toBe(false);
|
|
97
|
+
expect(isPermissionState(undefined)).toBe(false);
|
|
98
|
+
expect(isPermissionState(1)).toBe(false);
|
|
99
|
+
expect(isPermissionState({})).toBe(false);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe("extractFrontmatter", () => {
|
|
104
|
+
test("returns empty string when no frontmatter delimiter", () => {
|
|
105
|
+
expect(extractFrontmatter("# Hello\nSome content")).toBe("");
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("returns empty string when only opening delimiter with no closing", () => {
|
|
109
|
+
expect(extractFrontmatter("---\nkey: value")).toBe("");
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("returns frontmatter body between delimiters", () => {
|
|
113
|
+
const markdown = "---\nissue: 1\ntitle: Test\n---\n# Content";
|
|
114
|
+
expect(extractFrontmatter(markdown)).toBe("issue: 1\ntitle: Test");
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("returns empty string when file does not start with ---", () => {
|
|
118
|
+
expect(extractFrontmatter("content\n---\nkey: val\n---")).toBe("");
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("handles CRLF line endings", () => {
|
|
122
|
+
const markdown = "---\r\nissue: 5\r\n---\r\n# Content";
|
|
123
|
+
expect(extractFrontmatter(markdown)).toBe("issue: 5");
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("returns empty string for empty string input", () => {
|
|
127
|
+
expect(extractFrontmatter("")).toBe("");
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("returns empty frontmatter for --- \\n--- with nothing between", () => {
|
|
131
|
+
const markdown = "---\n---\n# Content";
|
|
132
|
+
expect(extractFrontmatter(markdown)).toBe("");
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
describe("parseSimpleYamlMap", () => {
|
|
137
|
+
test("returns empty object for empty string", () => {
|
|
138
|
+
expect(parseSimpleYamlMap("")).toEqual({});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test("parses simple key-value pairs", () => {
|
|
142
|
+
const yaml = "issue: 21\ntitle: Test";
|
|
143
|
+
expect(parseSimpleYamlMap(yaml)).toEqual({ issue: "21", title: "Test" });
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test("strips surrounding quotes from values", () => {
|
|
147
|
+
const yaml = 'title: "My Title"';
|
|
148
|
+
expect(parseSimpleYamlMap(yaml)).toEqual({ title: "My Title" });
|
|
149
|
+
|
|
150
|
+
const yaml2 = "title: 'My Title'";
|
|
151
|
+
expect(parseSimpleYamlMap(yaml2)).toEqual({ title: "My Title" });
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test("skips lines without colon or with colon at position 0", () => {
|
|
155
|
+
const yaml = "no separator here\n:starts-with-colon: val\nkey: val";
|
|
156
|
+
const result = parseSimpleYamlMap(yaml);
|
|
157
|
+
expect(result.key).toBe("val");
|
|
158
|
+
expect(result["no separator here"]).toBeUndefined();
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test("skips comment lines", () => {
|
|
162
|
+
const yaml = "# This is a comment\nkey: value";
|
|
163
|
+
expect(parseSimpleYamlMap(yaml)).toEqual({ key: "value" });
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test("skips blank lines", () => {
|
|
167
|
+
const yaml = "\n\nkey: value\n\n";
|
|
168
|
+
expect(parseSimpleYamlMap(yaml)).toEqual({ key: "value" });
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test("parses nested map (child indented under parent)", () => {
|
|
172
|
+
const yaml = "parent:\n child: nested_value";
|
|
173
|
+
const result = parseSimpleYamlMap(yaml);
|
|
174
|
+
expect(result.parent).toEqual({ child: "nested_value" });
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test("handles multi-line values correctly (second line is new key)", () => {
|
|
178
|
+
const yaml = "key1: val1\nkey2: val2";
|
|
179
|
+
const result = parseSimpleYamlMap(yaml);
|
|
180
|
+
expect(result.key1).toBe("val1");
|
|
181
|
+
expect(result.key2).toBe("val2");
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test("strips quotes from keys", () => {
|
|
185
|
+
const yaml = '"quoted-key": value';
|
|
186
|
+
const result = parseSimpleYamlMap(yaml);
|
|
187
|
+
expect(result["quoted-key"]).toBe("value");
|
|
188
|
+
});
|
|
189
|
+
});
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { afterEach, describe, expect, test, vi } from "vitest";
|
|
3
|
+
|
|
4
|
+
// Mock node:os so tilde-expansion is deterministic across platforms.
|
|
5
|
+
vi.mock("node:os", () => ({
|
|
6
|
+
homedir: vi.fn(() => "/mock/home"),
|
|
7
|
+
}));
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
formatExternalDirectoryAskPrompt,
|
|
11
|
+
formatExternalDirectoryDenyReason,
|
|
12
|
+
formatExternalDirectoryHardStopHint,
|
|
13
|
+
formatExternalDirectoryUserDeniedReason,
|
|
14
|
+
getPathBearingToolPath,
|
|
15
|
+
isPathOutsideWorkingDirectory,
|
|
16
|
+
isPathWithinDirectory,
|
|
17
|
+
normalizePathForComparison,
|
|
18
|
+
PATH_BEARING_TOOLS,
|
|
19
|
+
} from "../src/external-directory.js";
|
|
20
|
+
|
|
21
|
+
afterEach(() => {
|
|
22
|
+
vi.restoreAllMocks();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe("PATH_BEARING_TOOLS", () => {
|
|
26
|
+
test("contains the expected tool names", () => {
|
|
27
|
+
for (const tool of ["read", "write", "edit", "find", "grep", "ls"]) {
|
|
28
|
+
expect(PATH_BEARING_TOOLS.has(tool)).toBe(true);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("does not contain bash or mcp", () => {
|
|
33
|
+
expect(PATH_BEARING_TOOLS.has("bash")).toBe(false);
|
|
34
|
+
expect(PATH_BEARING_TOOLS.has("mcp")).toBe(false);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe("normalizePathForComparison", () => {
|
|
39
|
+
const cwd = "/projects/my-app";
|
|
40
|
+
|
|
41
|
+
test("resolves absolute path unchanged", () => {
|
|
42
|
+
expect(normalizePathForComparison("/usr/local/bin", cwd)).toBe(
|
|
43
|
+
"/usr/local/bin",
|
|
44
|
+
);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("resolves relative path against cwd", () => {
|
|
48
|
+
expect(normalizePathForComparison("src/foo.ts", cwd)).toBe(
|
|
49
|
+
"/projects/my-app/src/foo.ts",
|
|
50
|
+
);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("expands bare ~ to homedir", () => {
|
|
54
|
+
expect(normalizePathForComparison("~", cwd)).toBe("/mock/home");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("expands ~/... to homedir-relative path", () => {
|
|
58
|
+
expect(normalizePathForComparison("~/docs/readme.md", cwd)).toBe(
|
|
59
|
+
join("/mock/home", "docs/readme.md"),
|
|
60
|
+
);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("strips leading @ before resolving", () => {
|
|
64
|
+
expect(normalizePathForComparison("@/usr/local/bin", cwd)).toBe(
|
|
65
|
+
"/usr/local/bin",
|
|
66
|
+
);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("strips surrounding quotes", () => {
|
|
70
|
+
expect(normalizePathForComparison("'/usr/local/bin'", cwd)).toBe(
|
|
71
|
+
"/usr/local/bin",
|
|
72
|
+
);
|
|
73
|
+
expect(normalizePathForComparison('"/usr/local/bin"', cwd)).toBe(
|
|
74
|
+
"/usr/local/bin",
|
|
75
|
+
);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("returns empty string for blank/whitespace-only path", () => {
|
|
79
|
+
expect(normalizePathForComparison("", cwd)).toBe("");
|
|
80
|
+
expect(normalizePathForComparison(" ", cwd)).toBe("");
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe("isPathWithinDirectory", () => {
|
|
85
|
+
test("returns true when path equals directory", () => {
|
|
86
|
+
expect(isPathWithinDirectory("/a/b", "/a/b")).toBe(true);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("returns true when path is a direct child", () => {
|
|
90
|
+
expect(isPathWithinDirectory("/a/b/c", "/a/b")).toBe(true);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("returns true when path is a deep descendant", () => {
|
|
94
|
+
expect(isPathWithinDirectory("/a/b/c/d/e", "/a/b")).toBe(true);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("returns false when path is a sibling directory", () => {
|
|
98
|
+
expect(isPathWithinDirectory("/a/bc", "/a/b")).toBe(false);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("returns false when path is outside the directory", () => {
|
|
102
|
+
expect(isPathWithinDirectory("/other/path", "/a/b")).toBe(false);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("returns false for empty path", () => {
|
|
106
|
+
expect(isPathWithinDirectory("", "/a/b")).toBe(false);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("returns false for empty directory", () => {
|
|
110
|
+
expect(isPathWithinDirectory("/a/b", "")).toBe(false);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
describe("getPathBearingToolPath", () => {
|
|
115
|
+
test("returns path for a path-bearing tool", () => {
|
|
116
|
+
expect(getPathBearingToolPath("read", { path: "/src/foo.ts" })).toBe(
|
|
117
|
+
"/src/foo.ts",
|
|
118
|
+
);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("returns null for a non-path-bearing tool", () => {
|
|
122
|
+
expect(getPathBearingToolPath("bash", { path: "/src/foo.ts" })).toBeNull();
|
|
123
|
+
expect(getPathBearingToolPath("mcp", { path: "/src/foo.ts" })).toBeNull();
|
|
124
|
+
expect(getPathBearingToolPath("task", { path: "/src/foo.ts" })).toBeNull();
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("returns null when input has no path", () => {
|
|
128
|
+
expect(getPathBearingToolPath("read", {})).toBeNull();
|
|
129
|
+
expect(getPathBearingToolPath("read", { path: "" })).toBeNull();
|
|
130
|
+
expect(getPathBearingToolPath("read", null)).toBeNull();
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe("isPathOutsideWorkingDirectory", () => {
|
|
135
|
+
const cwd = "/projects/my-app";
|
|
136
|
+
|
|
137
|
+
test("returns false when path is inside cwd", () => {
|
|
138
|
+
expect(isPathOutsideWorkingDirectory("/projects/my-app/src", cwd)).toBe(
|
|
139
|
+
false,
|
|
140
|
+
);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("returns false when path equals cwd", () => {
|
|
144
|
+
expect(isPathOutsideWorkingDirectory("/projects/my-app", cwd)).toBe(false);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test("returns true when path is outside cwd", () => {
|
|
148
|
+
expect(isPathOutsideWorkingDirectory("/etc/passwd", cwd)).toBe(true);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test("returns true for home directory when outside cwd", () => {
|
|
152
|
+
expect(isPathOutsideWorkingDirectory("~/secrets", cwd)).toBe(true);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test("returns false for relative path resolving inside cwd", () => {
|
|
156
|
+
expect(isPathOutsideWorkingDirectory("src/index.ts", cwd)).toBe(false);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("returns false for empty path (normalizes to empty string)", () => {
|
|
160
|
+
expect(isPathOutsideWorkingDirectory("", cwd)).toBe(false);
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
describe("formatExternalDirectoryHardStopHint", () => {
|
|
165
|
+
test("returns the hard stop instruction string", () => {
|
|
166
|
+
const hint = formatExternalDirectoryHardStopHint();
|
|
167
|
+
expect(hint).toContain("Hard stop");
|
|
168
|
+
expect(hint).toContain("external directory");
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
describe("formatExternalDirectoryAskPrompt", () => {
|
|
173
|
+
test("uses 'Current agent' when no agent name provided", () => {
|
|
174
|
+
const result = formatExternalDirectoryAskPrompt(
|
|
175
|
+
"read",
|
|
176
|
+
"/etc/passwd",
|
|
177
|
+
"/projects/my-app",
|
|
178
|
+
);
|
|
179
|
+
expect(result).toContain("Current agent");
|
|
180
|
+
expect(result).toContain("read");
|
|
181
|
+
expect(result).toContain("/etc/passwd");
|
|
182
|
+
expect(result).toContain("/projects/my-app");
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test("uses agent name when provided", () => {
|
|
186
|
+
const result = formatExternalDirectoryAskPrompt(
|
|
187
|
+
"write",
|
|
188
|
+
"/tmp/out.txt",
|
|
189
|
+
"/projects/my-app",
|
|
190
|
+
"my-agent",
|
|
191
|
+
);
|
|
192
|
+
expect(result).toContain("Agent 'my-agent'");
|
|
193
|
+
expect(result).toContain("write");
|
|
194
|
+
expect(result).toContain("/tmp/out.txt");
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
describe("formatExternalDirectoryDenyReason", () => {
|
|
199
|
+
test("includes tool name, path, cwd, agent name, and hard stop hint", () => {
|
|
200
|
+
const result = formatExternalDirectoryDenyReason(
|
|
201
|
+
"read",
|
|
202
|
+
"/etc/passwd",
|
|
203
|
+
"/projects/my-app",
|
|
204
|
+
"sec-agent",
|
|
205
|
+
);
|
|
206
|
+
expect(result).toContain("Agent 'sec-agent'");
|
|
207
|
+
expect(result).toContain("read");
|
|
208
|
+
expect(result).toContain("/etc/passwd");
|
|
209
|
+
expect(result).toContain("/projects/my-app");
|
|
210
|
+
expect(result).toContain("Hard stop");
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
test("uses 'Current agent' without agent name", () => {
|
|
214
|
+
const result = formatExternalDirectoryDenyReason(
|
|
215
|
+
"read",
|
|
216
|
+
"/etc",
|
|
217
|
+
"/projects",
|
|
218
|
+
);
|
|
219
|
+
expect(result).toContain("Current agent");
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
describe("formatExternalDirectoryUserDeniedReason", () => {
|
|
224
|
+
test("includes tool name and path", () => {
|
|
225
|
+
const result = formatExternalDirectoryUserDeniedReason(
|
|
226
|
+
"edit",
|
|
227
|
+
"/etc/hosts",
|
|
228
|
+
);
|
|
229
|
+
expect(result).toContain("edit");
|
|
230
|
+
expect(result).toContain("/etc/hosts");
|
|
231
|
+
expect(result).toContain("Hard stop");
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
test("appends denial reason when provided", () => {
|
|
235
|
+
const result = formatExternalDirectoryUserDeniedReason(
|
|
236
|
+
"edit",
|
|
237
|
+
"/etc/hosts",
|
|
238
|
+
"too risky",
|
|
239
|
+
);
|
|
240
|
+
expect(result).toContain("Reason: too risky");
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test("omits reason suffix when not provided", () => {
|
|
244
|
+
const result = formatExternalDirectoryUserDeniedReason(
|
|
245
|
+
"edit",
|
|
246
|
+
"/etc/hosts",
|
|
247
|
+
);
|
|
248
|
+
expect(result).not.toContain("Reason:");
|
|
249
|
+
});
|
|
250
|
+
});
|