@gotgenes/pi-permission-system 5.11.0 → 5.14.0
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 +78 -0
- package/package.json +7 -7
- package/src/active-agent.ts +1 -1
- package/src/config-modal.ts +2 -2
- package/src/extension-config.ts +2 -139
- package/src/forwarded-permissions/polling.ts +1 -1
- package/src/forwarding-manager.ts +1 -1
- package/src/handlers/before-agent-start.ts +1 -1
- package/src/handlers/lifecycle.ts +1 -1
- package/src/handlers/permission-gate-handler.ts +1 -1
- package/src/index.ts +1 -1
- package/src/logging.ts +4 -12
- package/src/permission-event-rpc.ts +1 -1
- package/src/permission-prompter.ts +1 -1
- package/src/permission-session.ts +1 -1
- package/src/policy-loader.ts +1 -1
- package/src/runtime.ts +1 -1
- package/src/status.ts +1 -1
- package/src/subagent-context.ts +1 -1
- package/src/wildcard-matcher.ts +11 -3
- package/tests/active-agent.test.ts +1 -1
- package/tests/config-modal.test.ts +22 -12
- package/tests/config-reporter.test.ts +2 -0
- package/tests/extension-config.test.ts +1 -60
- package/tests/forwarding-manager.test.ts +1 -1
- package/tests/handlers/before-agent-start.test.ts +3 -3
- package/tests/handlers/external-directory-integration.test.ts +609 -0
- package/tests/handlers/external-directory-session-dedup.test.ts +367 -0
- package/tests/handlers/gates/skill-read.test.ts +2 -2
- package/tests/handlers/input-events.test.ts +1 -1
- package/tests/handlers/input.test.ts +1 -1
- package/tests/handlers/lifecycle.test.ts +1 -1
- package/tests/handlers/tool-call-events.test.ts +1 -1
- package/tests/handlers/tool-call.test.ts +3 -3
- package/tests/permission-event-rpc.test.ts +1 -1
- package/tests/permission-prompter.test.ts +1 -1
- package/tests/permission-session.test.ts +1 -1
- package/tests/permission-system.test.ts +1 -122
- package/tests/runtime.test.ts +1 -1
- package/tests/subagent-context.test.ts +1 -1
- package/tests/wildcard-matcher.test.ts +91 -0
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration tests verifying that sequential tool calls to the same
|
|
3
|
+
* external path only prompt once — the session-approval recorded by the
|
|
4
|
+
* first call covers the second.
|
|
5
|
+
*
|
|
6
|
+
* These tests use stateful mocks: `approveSessionRule` records rules,
|
|
7
|
+
* and `checkPermission` consults them via `getSessionRuleset`, mirroring
|
|
8
|
+
* the real interaction between PermissionSession, SessionRules, and
|
|
9
|
+
* PermissionManager.
|
|
10
|
+
*/
|
|
11
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
12
|
+
import { describe, expect, it, vi } from "vitest";
|
|
13
|
+
|
|
14
|
+
import { PermissionGateHandler } from "../../src/handlers/permission-gate-handler";
|
|
15
|
+
import type { PermissionSession } from "../../src/permission-session";
|
|
16
|
+
import type { Rule } from "../../src/rule";
|
|
17
|
+
import type { ToolRegistry } from "../../src/tool-registry";
|
|
18
|
+
import type { PermissionCheckResult } from "../../src/types";
|
|
19
|
+
import { wildcardMatch } from "../../src/wildcard-matcher";
|
|
20
|
+
|
|
21
|
+
// ── SDK stub ───────────────────────────────────────────────────────────────
|
|
22
|
+
vi.mock("@earendil-works/pi-coding-agent", async (importOriginal) => {
|
|
23
|
+
const original =
|
|
24
|
+
await importOriginal<typeof import("@earendil-works/pi-coding-agent")>();
|
|
25
|
+
return { ...original };
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// ── helpers ────────────────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
const CWD = "/test/project";
|
|
31
|
+
|
|
32
|
+
function makeCtx(overrides: Partial<ExtensionContext> = {}): ExtensionContext {
|
|
33
|
+
return {
|
|
34
|
+
cwd: CWD,
|
|
35
|
+
hasUI: true,
|
|
36
|
+
ui: {
|
|
37
|
+
setStatus: vi.fn(),
|
|
38
|
+
notify: vi.fn(),
|
|
39
|
+
select: vi.fn(),
|
|
40
|
+
input: vi.fn(),
|
|
41
|
+
},
|
|
42
|
+
sessionManager: {
|
|
43
|
+
getEntries: vi.fn().mockReturnValue([]),
|
|
44
|
+
getSessionDir: vi.fn().mockReturnValue("/sessions/test"),
|
|
45
|
+
addEntry: vi.fn(),
|
|
46
|
+
},
|
|
47
|
+
...overrides,
|
|
48
|
+
} as unknown as ExtensionContext;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Build a PermissionSession mock with stateful session-rule tracking.
|
|
53
|
+
*
|
|
54
|
+
* `checkPermission` returns "ask" for `external_directory` unless a
|
|
55
|
+
* matching session rule exists (via `approveSessionRule`), in which case
|
|
56
|
+
* it returns "allow" with `source: "session"`. All other surfaces return
|
|
57
|
+
* "allow" by default.
|
|
58
|
+
*/
|
|
59
|
+
function makeStatefulSession(
|
|
60
|
+
overrides: Partial<Record<keyof PermissionSession, unknown>> = {},
|
|
61
|
+
): PermissionSession {
|
|
62
|
+
const sessionRules: Rule[] = [];
|
|
63
|
+
|
|
64
|
+
const checkPermission = vi
|
|
65
|
+
.fn()
|
|
66
|
+
.mockImplementation(
|
|
67
|
+
(
|
|
68
|
+
surface: string,
|
|
69
|
+
input: unknown,
|
|
70
|
+
_agentName?: string,
|
|
71
|
+
rules?: Rule[],
|
|
72
|
+
): PermissionCheckResult => {
|
|
73
|
+
// Merge stored session rules with any passed-in rules
|
|
74
|
+
const allRules = [...sessionRules, ...(rules ?? [])];
|
|
75
|
+
|
|
76
|
+
if (surface === "external_directory") {
|
|
77
|
+
const record = (input ?? {}) as Record<string, unknown>;
|
|
78
|
+
const pathValue =
|
|
79
|
+
typeof record.path === "string" ? record.path : null;
|
|
80
|
+
|
|
81
|
+
if (pathValue && allRules.length > 0) {
|
|
82
|
+
const match = allRules.findLast(
|
|
83
|
+
(r) =>
|
|
84
|
+
r.surface === "external_directory" &&
|
|
85
|
+
wildcardMatch(r.pattern, pathValue),
|
|
86
|
+
);
|
|
87
|
+
if (match) {
|
|
88
|
+
return {
|
|
89
|
+
state: "allow",
|
|
90
|
+
toolName: surface,
|
|
91
|
+
source: "session",
|
|
92
|
+
origin: "session",
|
|
93
|
+
matchedPattern: match.pattern,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// No session match → config-level "ask"
|
|
99
|
+
return {
|
|
100
|
+
state: "ask",
|
|
101
|
+
toolName: surface,
|
|
102
|
+
source: "special",
|
|
103
|
+
origin: "global",
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// All other surfaces: allow
|
|
108
|
+
return {
|
|
109
|
+
state: "allow",
|
|
110
|
+
toolName: surface,
|
|
111
|
+
source: "tool",
|
|
112
|
+
origin: "builtin",
|
|
113
|
+
};
|
|
114
|
+
},
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
const approveSessionRule = vi
|
|
118
|
+
.fn()
|
|
119
|
+
.mockImplementation((surface: string, pattern: string) => {
|
|
120
|
+
sessionRules.push({
|
|
121
|
+
surface,
|
|
122
|
+
pattern,
|
|
123
|
+
action: "allow",
|
|
124
|
+
layer: "session",
|
|
125
|
+
origin: "session",
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const getSessionRuleset = vi.fn().mockImplementation(() => [...sessionRules]);
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
logger: { debug: vi.fn(), review: vi.fn(), warn: vi.fn() },
|
|
133
|
+
activate: vi.fn(),
|
|
134
|
+
resolveAgentName: vi.fn().mockReturnValue(null),
|
|
135
|
+
checkPermission,
|
|
136
|
+
getToolPermission: vi.fn().mockReturnValue("allow"),
|
|
137
|
+
getSessionRuleset,
|
|
138
|
+
approveSessionRule,
|
|
139
|
+
getActiveSkillEntries: vi.fn().mockReturnValue([]),
|
|
140
|
+
getInfrastructureDirs: vi.fn().mockReturnValue([]),
|
|
141
|
+
getInfrastructureReadPaths: vi.fn().mockReturnValue([]),
|
|
142
|
+
canPrompt: vi.fn().mockReturnValue(true),
|
|
143
|
+
prompt: vi
|
|
144
|
+
.fn()
|
|
145
|
+
.mockResolvedValue({ approved: true, state: "approved_for_session" }),
|
|
146
|
+
...overrides,
|
|
147
|
+
} as unknown as PermissionSession;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function makeEvents() {
|
|
151
|
+
return {
|
|
152
|
+
emit: vi.fn(),
|
|
153
|
+
on: vi.fn().mockReturnValue(() => undefined),
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function makeToolRegistry(): ToolRegistry {
|
|
158
|
+
return {
|
|
159
|
+
getAll: vi
|
|
160
|
+
.fn()
|
|
161
|
+
.mockReturnValue([
|
|
162
|
+
{ name: "read" },
|
|
163
|
+
{ name: "write" },
|
|
164
|
+
{ name: "edit" },
|
|
165
|
+
{ name: "bash" },
|
|
166
|
+
]),
|
|
167
|
+
setActive: vi.fn(),
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ── tests ──────────────────────────────────────────────────────────────────
|
|
172
|
+
|
|
173
|
+
describe("external-directory session dedup", () => {
|
|
174
|
+
describe("path-bearing tools (read, write, edit)", () => {
|
|
175
|
+
it("does not re-prompt for the same external path after session approval", async () => {
|
|
176
|
+
const session = makeStatefulSession();
|
|
177
|
+
const handler = new PermissionGateHandler(
|
|
178
|
+
session,
|
|
179
|
+
makeEvents(),
|
|
180
|
+
makeToolRegistry(),
|
|
181
|
+
);
|
|
182
|
+
const ctx = makeCtx();
|
|
183
|
+
const externalPath = "/outside/project/data.txt";
|
|
184
|
+
|
|
185
|
+
// First call — should prompt
|
|
186
|
+
const event1 = {
|
|
187
|
+
type: "tool_call",
|
|
188
|
+
toolCallId: "tc-1",
|
|
189
|
+
toolName: "read",
|
|
190
|
+
input: { path: externalPath },
|
|
191
|
+
};
|
|
192
|
+
const result1 = await handler.handleToolCall(event1, ctx);
|
|
193
|
+
expect(result1).toEqual({});
|
|
194
|
+
expect(session.prompt).toHaveBeenCalledTimes(1);
|
|
195
|
+
|
|
196
|
+
// Second call — same path, should hit session rule, no prompt
|
|
197
|
+
const event2 = {
|
|
198
|
+
type: "tool_call",
|
|
199
|
+
toolCallId: "tc-2",
|
|
200
|
+
toolName: "read",
|
|
201
|
+
input: { path: externalPath },
|
|
202
|
+
};
|
|
203
|
+
const result2 = await handler.handleToolCall(event2, ctx);
|
|
204
|
+
expect(result2).toEqual({});
|
|
205
|
+
expect(session.prompt).toHaveBeenCalledTimes(1);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it("does not re-prompt for a different file in the same external directory", async () => {
|
|
209
|
+
const session = makeStatefulSession();
|
|
210
|
+
const handler = new PermissionGateHandler(
|
|
211
|
+
session,
|
|
212
|
+
makeEvents(),
|
|
213
|
+
makeToolRegistry(),
|
|
214
|
+
);
|
|
215
|
+
const ctx = makeCtx();
|
|
216
|
+
|
|
217
|
+
// First call — prompt for /outside/project/a.txt
|
|
218
|
+
const event1 = {
|
|
219
|
+
type: "tool_call",
|
|
220
|
+
toolCallId: "tc-1",
|
|
221
|
+
toolName: "read",
|
|
222
|
+
input: { path: "/outside/project/a.txt" },
|
|
223
|
+
};
|
|
224
|
+
await handler.handleToolCall(event1, ctx);
|
|
225
|
+
expect(session.prompt).toHaveBeenCalledTimes(1);
|
|
226
|
+
|
|
227
|
+
// Second call — /outside/project/b.txt is in the same directory
|
|
228
|
+
const event2 = {
|
|
229
|
+
type: "tool_call",
|
|
230
|
+
toolCallId: "tc-2",
|
|
231
|
+
toolName: "read",
|
|
232
|
+
input: { path: "/outside/project/b.txt" },
|
|
233
|
+
};
|
|
234
|
+
await handler.handleToolCall(event2, ctx);
|
|
235
|
+
expect(session.prompt).toHaveBeenCalledTimes(1);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it("does prompt for a file in a different external directory", async () => {
|
|
239
|
+
const session = makeStatefulSession();
|
|
240
|
+
const handler = new PermissionGateHandler(
|
|
241
|
+
session,
|
|
242
|
+
makeEvents(),
|
|
243
|
+
makeToolRegistry(),
|
|
244
|
+
);
|
|
245
|
+
const ctx = makeCtx();
|
|
246
|
+
|
|
247
|
+
// First call — /outside/alpha/file.txt
|
|
248
|
+
const event1 = {
|
|
249
|
+
type: "tool_call",
|
|
250
|
+
toolCallId: "tc-1",
|
|
251
|
+
toolName: "read",
|
|
252
|
+
input: { path: "/outside/alpha/file.txt" },
|
|
253
|
+
};
|
|
254
|
+
await handler.handleToolCall(event1, ctx);
|
|
255
|
+
expect(session.prompt).toHaveBeenCalledTimes(1);
|
|
256
|
+
|
|
257
|
+
// Second call — /outside/beta/file.txt is a different directory
|
|
258
|
+
const event2 = {
|
|
259
|
+
type: "tool_call",
|
|
260
|
+
toolCallId: "tc-2",
|
|
261
|
+
toolName: "read",
|
|
262
|
+
input: { path: "/outside/beta/file.txt" },
|
|
263
|
+
};
|
|
264
|
+
await handler.handleToolCall(event2, ctx);
|
|
265
|
+
expect(session.prompt).toHaveBeenCalledTimes(2);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it("re-prompts when user approved once (not for session)", async () => {
|
|
269
|
+
const session = makeStatefulSession({
|
|
270
|
+
prompt: vi
|
|
271
|
+
.fn()
|
|
272
|
+
.mockResolvedValue({ approved: true, state: "approved" }),
|
|
273
|
+
});
|
|
274
|
+
const handler = new PermissionGateHandler(
|
|
275
|
+
session,
|
|
276
|
+
makeEvents(),
|
|
277
|
+
makeToolRegistry(),
|
|
278
|
+
);
|
|
279
|
+
const ctx = makeCtx();
|
|
280
|
+
const externalPath = "/outside/project/data.txt";
|
|
281
|
+
|
|
282
|
+
// First call — prompt, approved once
|
|
283
|
+
const event1 = {
|
|
284
|
+
type: "tool_call",
|
|
285
|
+
toolCallId: "tc-1",
|
|
286
|
+
toolName: "read",
|
|
287
|
+
input: { path: externalPath },
|
|
288
|
+
};
|
|
289
|
+
await handler.handleToolCall(event1, ctx);
|
|
290
|
+
expect(session.prompt).toHaveBeenCalledTimes(1);
|
|
291
|
+
|
|
292
|
+
// Second call — no session rule recorded, should prompt again
|
|
293
|
+
const event2 = {
|
|
294
|
+
type: "tool_call",
|
|
295
|
+
toolCallId: "tc-2",
|
|
296
|
+
toolName: "read",
|
|
297
|
+
input: { path: externalPath },
|
|
298
|
+
};
|
|
299
|
+
await handler.handleToolCall(event2, ctx);
|
|
300
|
+
expect(session.prompt).toHaveBeenCalledTimes(2);
|
|
301
|
+
});
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
describe("bash commands with external paths", () => {
|
|
305
|
+
it("does not re-prompt for a bash command referencing the same external path after session approval", async () => {
|
|
306
|
+
const session = makeStatefulSession();
|
|
307
|
+
const handler = new PermissionGateHandler(
|
|
308
|
+
session,
|
|
309
|
+
makeEvents(),
|
|
310
|
+
makeToolRegistry(),
|
|
311
|
+
);
|
|
312
|
+
const ctx = makeCtx();
|
|
313
|
+
|
|
314
|
+
// First call — bash referencing /tmp/out.txt
|
|
315
|
+
const event1 = {
|
|
316
|
+
type: "tool_call",
|
|
317
|
+
toolCallId: "tc-1",
|
|
318
|
+
toolName: "bash",
|
|
319
|
+
input: { command: "echo hello > /tmp/out.txt" },
|
|
320
|
+
};
|
|
321
|
+
const result1 = await handler.handleToolCall(event1, ctx);
|
|
322
|
+
expect(result1).toEqual({});
|
|
323
|
+
expect(session.prompt).toHaveBeenCalledTimes(1);
|
|
324
|
+
|
|
325
|
+
// Second call — different bash command, same external path
|
|
326
|
+
const event2 = {
|
|
327
|
+
type: "tool_call",
|
|
328
|
+
toolCallId: "tc-2",
|
|
329
|
+
toolName: "bash",
|
|
330
|
+
input: { command: "cat /tmp/out.txt" },
|
|
331
|
+
};
|
|
332
|
+
const result2 = await handler.handleToolCall(event2, ctx);
|
|
333
|
+
expect(result2).toEqual({});
|
|
334
|
+
expect(session.prompt).toHaveBeenCalledTimes(1);
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
it("does not re-prompt for read after bash already approved the same directory", async () => {
|
|
338
|
+
const session = makeStatefulSession();
|
|
339
|
+
const handler = new PermissionGateHandler(
|
|
340
|
+
session,
|
|
341
|
+
makeEvents(),
|
|
342
|
+
makeToolRegistry(),
|
|
343
|
+
);
|
|
344
|
+
const ctx = makeCtx();
|
|
345
|
+
|
|
346
|
+
// First call — bash writes to /tmp/out.txt
|
|
347
|
+
const event1 = {
|
|
348
|
+
type: "tool_call",
|
|
349
|
+
toolCallId: "tc-1",
|
|
350
|
+
toolName: "bash",
|
|
351
|
+
input: { command: "echo hello > /tmp/out.txt" },
|
|
352
|
+
};
|
|
353
|
+
await handler.handleToolCall(event1, ctx);
|
|
354
|
+
expect(session.prompt).toHaveBeenCalledTimes(1);
|
|
355
|
+
|
|
356
|
+
// Second call — read from /tmp/out.txt (same directory, different tool)
|
|
357
|
+
const event2 = {
|
|
358
|
+
type: "tool_call",
|
|
359
|
+
toolCallId: "tc-2",
|
|
360
|
+
toolName: "read",
|
|
361
|
+
input: { path: "/tmp/out.txt" },
|
|
362
|
+
};
|
|
363
|
+
await handler.handleToolCall(event2, ctx);
|
|
364
|
+
expect(session.prompt).toHaveBeenCalledTimes(1);
|
|
365
|
+
});
|
|
366
|
+
});
|
|
367
|
+
});
|
|
@@ -6,9 +6,9 @@ import type { ToolCallContext } from "../../../src/handlers/gates/types";
|
|
|
6
6
|
import type { SkillPromptEntry } from "../../../src/skill-prompt-sanitizer";
|
|
7
7
|
|
|
8
8
|
// ── SDK stubs ──────────────────────────────────────────────────────────────
|
|
9
|
-
vi.mock("@
|
|
9
|
+
vi.mock("@earendil-works/pi-coding-agent", async (importOriginal) => {
|
|
10
10
|
const original =
|
|
11
|
-
await importOriginal<typeof import("@
|
|
11
|
+
await importOriginal<typeof import("@earendil-works/pi-coding-agent")>();
|
|
12
12
|
return { ...original };
|
|
13
13
|
});
|
|
14
14
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Tests that handleInput emits permissions:decision events for skill input gates.
|
|
3
3
|
*/
|
|
4
|
-
import type { ExtensionContext } from "@
|
|
4
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
5
5
|
import { describe, expect, it, vi } from "vitest";
|
|
6
6
|
|
|
7
7
|
import { PermissionGateHandler } from "../../src/handlers/permission-gate-handler";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ExtensionContext } from "@
|
|
1
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { describe, expect, it, vi } from "vitest";
|
|
3
3
|
import { SessionLifecycleHandler } from "../../src/handlers/lifecycle";
|
|
4
4
|
import type { PermissionSession } from "../../src/permission-session";
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Tests that handleToolCall emits permissions:decision events at every
|
|
3
3
|
* gate resolution and fast-path site.
|
|
4
4
|
*/
|
|
5
|
-
import type { ExtensionContext } from "@
|
|
5
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
6
6
|
import { describe, expect, it, vi } from "vitest";
|
|
7
7
|
|
|
8
8
|
import { PermissionGateHandler } from "../../src/handlers/permission-gate-handler";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ExtensionContext } from "@
|
|
1
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { describe, expect, it, vi } from "vitest";
|
|
3
3
|
|
|
4
4
|
import {
|
|
@@ -10,9 +10,9 @@ import type { ToolRegistry } from "../../src/tool-registry";
|
|
|
10
10
|
import type { PermissionCheckResult, PermissionState } from "../../src/types";
|
|
11
11
|
|
|
12
12
|
// ── SDK stubs ──────────────────────────────────────────────────────────────
|
|
13
|
-
vi.mock("@
|
|
13
|
+
vi.mock("@earendil-works/pi-coding-agent", async (importOriginal) => {
|
|
14
14
|
const original =
|
|
15
|
-
await importOriginal<typeof import("@
|
|
15
|
+
await importOriginal<typeof import("@earendil-works/pi-coding-agent")>();
|
|
16
16
|
return { ...original };
|
|
17
17
|
});
|
|
18
18
|
|
|
@@ -13,7 +13,7 @@ vi.mock("../src/forwarded-permissions/polling", () => ({
|
|
|
13
13
|
|
|
14
14
|
// ── Imports (after mocks) ───────────────────────────────────────────────────
|
|
15
15
|
|
|
16
|
-
import type { ExtensionContext } from "@
|
|
16
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
17
17
|
import { DEFAULT_EXTENSION_CONFIG } from "../src/extension-config";
|
|
18
18
|
import type { PermissionPromptDecision } from "../src/permission-dialog";
|
|
19
19
|
import type { PromptPermissionDetails } from "../src/permission-prompter";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ExtensionContext } from "@
|
|
1
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
3
|
|
|
4
4
|
// ── Module mocks (hoisted) ─────────────────────────────────────────────────
|
|
@@ -17,11 +17,7 @@ import {
|
|
|
17
17
|
shouldApplyCachedAgentStartState,
|
|
18
18
|
} from "../src/before-agent-start-cache";
|
|
19
19
|
import { getGlobalConfigPath } from "../src/config-paths";
|
|
20
|
-
import {
|
|
21
|
-
DEFAULT_EXTENSION_CONFIG,
|
|
22
|
-
loadPermissionSystemConfig,
|
|
23
|
-
savePermissionSystemConfig,
|
|
24
|
-
} from "../src/extension-config";
|
|
20
|
+
import { DEFAULT_EXTENSION_CONFIG } from "../src/extension-config";
|
|
25
21
|
import piPermissionSystemExtension from "../src/index";
|
|
26
22
|
import { createPermissionSystemLogger } from "../src/logging";
|
|
27
23
|
import {
|
|
@@ -244,123 +240,6 @@ async function runToolCall(
|
|
|
244
240
|
return (result ?? {}) as Record<string, unknown>;
|
|
245
241
|
}
|
|
246
242
|
|
|
247
|
-
test("Permission-system extension config defaults debug off, review log on, and yolo mode off", () => {
|
|
248
|
-
const baseDir = mkdtempSync(join(tmpdir(), "pi-permission-system-config-"));
|
|
249
|
-
const configPath = join(baseDir, "config.json");
|
|
250
|
-
|
|
251
|
-
try {
|
|
252
|
-
const result = loadPermissionSystemConfig(configPath);
|
|
253
|
-
assert.equal(result.created, true);
|
|
254
|
-
assert.equal(result.warning, undefined);
|
|
255
|
-
assert.deepEqual(result.config, DEFAULT_EXTENSION_CONFIG);
|
|
256
|
-
assert.equal(existsSync(configPath), true);
|
|
257
|
-
|
|
258
|
-
const raw = JSON.parse(readFileSync(configPath, "utf8")) as Record<
|
|
259
|
-
string,
|
|
260
|
-
unknown
|
|
261
|
-
>;
|
|
262
|
-
assert.equal(raw.debugLog, false);
|
|
263
|
-
assert.equal(raw.permissionReviewLog, true);
|
|
264
|
-
assert.equal(raw.yoloMode, false);
|
|
265
|
-
} finally {
|
|
266
|
-
rmSync(baseDir, { recursive: true, force: true });
|
|
267
|
-
}
|
|
268
|
-
});
|
|
269
|
-
|
|
270
|
-
test("Permission-system extension config loads yolo mode when explicitly enabled", () => {
|
|
271
|
-
const baseDir = mkdtempSync(
|
|
272
|
-
join(tmpdir(), "pi-permission-system-config-yolo-"),
|
|
273
|
-
);
|
|
274
|
-
const configPath = join(baseDir, "config.json");
|
|
275
|
-
|
|
276
|
-
try {
|
|
277
|
-
writeFileSync(
|
|
278
|
-
configPath,
|
|
279
|
-
`${JSON.stringify(
|
|
280
|
-
{
|
|
281
|
-
debugLog: true,
|
|
282
|
-
permissionReviewLog: false,
|
|
283
|
-
yoloMode: true,
|
|
284
|
-
},
|
|
285
|
-
null,
|
|
286
|
-
2,
|
|
287
|
-
)}\n`,
|
|
288
|
-
"utf8",
|
|
289
|
-
);
|
|
290
|
-
|
|
291
|
-
const result = loadPermissionSystemConfig(configPath);
|
|
292
|
-
assert.equal(result.created, false);
|
|
293
|
-
assert.equal(result.warning, undefined);
|
|
294
|
-
assert.deepEqual(result.config, {
|
|
295
|
-
debugLog: true,
|
|
296
|
-
permissionReviewLog: false,
|
|
297
|
-
yoloMode: true,
|
|
298
|
-
});
|
|
299
|
-
} finally {
|
|
300
|
-
rmSync(baseDir, { recursive: true, force: true });
|
|
301
|
-
}
|
|
302
|
-
});
|
|
303
|
-
|
|
304
|
-
test("Permission-system extension config normalizes invalid persisted values back to defaults", () => {
|
|
305
|
-
const baseDir = mkdtempSync(
|
|
306
|
-
join(tmpdir(), "pi-permission-system-config-invalid-"),
|
|
307
|
-
);
|
|
308
|
-
const configPath = join(baseDir, "config.json");
|
|
309
|
-
|
|
310
|
-
try {
|
|
311
|
-
writeFileSync(
|
|
312
|
-
configPath,
|
|
313
|
-
`${JSON.stringify(
|
|
314
|
-
{
|
|
315
|
-
debugLog: "true",
|
|
316
|
-
permissionReviewLog: null,
|
|
317
|
-
yoloMode: 1,
|
|
318
|
-
},
|
|
319
|
-
null,
|
|
320
|
-
2,
|
|
321
|
-
)}\n`,
|
|
322
|
-
"utf8",
|
|
323
|
-
);
|
|
324
|
-
|
|
325
|
-
const result = loadPermissionSystemConfig(configPath);
|
|
326
|
-
assert.equal(result.created, false);
|
|
327
|
-
assert.equal(result.warning, undefined);
|
|
328
|
-
assert.deepEqual(result.config, DEFAULT_EXTENSION_CONFIG);
|
|
329
|
-
} finally {
|
|
330
|
-
rmSync(baseDir, { recursive: true, force: true });
|
|
331
|
-
}
|
|
332
|
-
});
|
|
333
|
-
|
|
334
|
-
test("Permission-system extension config save persists normalized config", () => {
|
|
335
|
-
const baseDir = mkdtempSync(
|
|
336
|
-
join(tmpdir(), "pi-permission-system-config-save-"),
|
|
337
|
-
);
|
|
338
|
-
const configPath = join(baseDir, "config.json");
|
|
339
|
-
|
|
340
|
-
try {
|
|
341
|
-
const saved = savePermissionSystemConfig(
|
|
342
|
-
{
|
|
343
|
-
debugLog: true,
|
|
344
|
-
permissionReviewLog: false,
|
|
345
|
-
yoloMode: true,
|
|
346
|
-
},
|
|
347
|
-
configPath,
|
|
348
|
-
);
|
|
349
|
-
|
|
350
|
-
assert.equal(saved.success, true);
|
|
351
|
-
|
|
352
|
-
const result = loadPermissionSystemConfig(configPath);
|
|
353
|
-
assert.equal(result.warning, undefined);
|
|
354
|
-
assert.deepEqual(result.config, {
|
|
355
|
-
debugLog: true,
|
|
356
|
-
permissionReviewLog: false,
|
|
357
|
-
yoloMode: true,
|
|
358
|
-
});
|
|
359
|
-
} finally {
|
|
360
|
-
rmSync(baseDir, { recursive: true, force: true });
|
|
361
|
-
}
|
|
362
|
-
});
|
|
363
|
-
|
|
364
243
|
test("Yolo mode only auto-approves ask-state permissions", () => {
|
|
365
244
|
assert.equal(
|
|
366
245
|
shouldAutoApprovePermissionState("ask", DEFAULT_EXTENSION_CONFIG),
|
package/tests/runtime.test.ts
CHANGED
|
@@ -66,7 +66,7 @@ vi.mock("../src/session-rules", () => ({
|
|
|
66
66
|
deriveApprovalPattern: vi.fn(),
|
|
67
67
|
}));
|
|
68
68
|
|
|
69
|
-
import type { ExtensionContext } from "@
|
|
69
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
70
70
|
import {
|
|
71
71
|
getGlobalConfigPath,
|
|
72
72
|
getGlobalLogsDir,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ExtensionContext } from "@
|
|
1
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { afterEach, describe, expect, test, vi } from "vitest";
|
|
3
3
|
import { SUBAGENT_ENV_HINT_KEYS } from "../src/permission-forwarding";
|
|
4
4
|
import {
|