@gotgenes/pi-permission-system 5.8.0 → 5.10.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 +28 -0
- package/package.json +1 -1
- package/src/forwarding-manager.ts +76 -0
- package/src/handlers/before-agent-start.ts +19 -32
- package/src/handlers/input.ts +5 -5
- package/src/handlers/lifecycle.ts +17 -33
- package/src/handlers/tool-call.ts +11 -18
- package/src/handlers/types.ts +11 -38
- package/src/index.ts +15 -19
- package/src/permission-session.ts +252 -0
- package/src/runtime.ts +5 -96
- package/src/skill-prompt-sanitizer.ts +15 -4
- package/tests/forwarding-manager.test.ts +211 -0
- package/tests/handlers/before-agent-start.test.ts +79 -111
- package/tests/handlers/input-events.test.ts +19 -32
- package/tests/handlers/input.test.ts +41 -74
- package/tests/handlers/lifecycle.test.ts +61 -180
- package/tests/handlers/tool-call-events.test.ts +66 -93
- package/tests/handlers/tool-call.test.ts +40 -62
- package/tests/permission-session.test.ts +546 -0
- package/tests/runtime.test.ts +2 -92
|
@@ -6,9 +6,8 @@ import {
|
|
|
6
6
|
shouldExposeTool,
|
|
7
7
|
} from "../../src/handlers/before-agent-start";
|
|
8
8
|
import type { HandlerDeps } from "../../src/handlers/types";
|
|
9
|
-
import type {
|
|
10
|
-
import type {
|
|
11
|
-
import type { SkillPromptEntry } from "../../src/skill-prompt-sanitizer";
|
|
9
|
+
import type { PermissionSession } from "../../src/permission-session";
|
|
10
|
+
import type { PermissionState } from "../../src/types";
|
|
12
11
|
|
|
13
12
|
// ── SDK stubs ──────────────────────────────────────────────────────────────
|
|
14
13
|
vi.mock("@mariozechner/pi-coding-agent", async (importOriginal) => {
|
|
@@ -45,53 +44,36 @@ function makeEvent(systemPrompt = "You are an assistant.") {
|
|
|
45
44
|
return { systemPrompt };
|
|
46
45
|
}
|
|
47
46
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
): PermissionManager {
|
|
47
|
+
function makeSession(
|
|
48
|
+
overrides: Partial<Record<keyof PermissionSession, unknown>> = {},
|
|
49
|
+
): PermissionSession {
|
|
52
50
|
return {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
51
|
+
logger: { debug: vi.fn(), review: vi.fn(), warn: vi.fn() },
|
|
52
|
+
activate: vi.fn(),
|
|
53
|
+
refreshConfig: vi.fn(),
|
|
54
|
+
resolveAgentName: vi.fn().mockReturnValue(null),
|
|
55
|
+
getToolPermission: vi.fn().mockReturnValue("allow" as PermissionState),
|
|
56
56
|
checkPermission: vi.fn().mockReturnValue({ state: "allow" }),
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
activeSkillEntries: [] as SkillPromptEntry[],
|
|
65
|
-
lastKnownActiveAgentName: null,
|
|
66
|
-
lastActiveToolsCacheKey: null,
|
|
67
|
-
lastPromptStateCacheKey: null,
|
|
68
|
-
sessionRules: {
|
|
69
|
-
approve: vi.fn(),
|
|
70
|
-
getRuleset: vi.fn().mockReturnValue([]),
|
|
71
|
-
clear: vi.fn(),
|
|
72
|
-
} as unknown as SessionState["sessionRules"],
|
|
57
|
+
shouldUpdateActiveTools: vi.fn().mockReturnValue(true),
|
|
58
|
+
commitActiveToolsCacheKey: vi.fn(),
|
|
59
|
+
getPolicyCacheStamp: vi.fn().mockReturnValue("stamp-1"),
|
|
60
|
+
shouldUpdatePromptState: vi.fn().mockReturnValue(true),
|
|
61
|
+
commitPromptStateCacheKey: vi.fn(),
|
|
62
|
+
setActiveSkillEntries: vi.fn(),
|
|
63
|
+
getActiveSkillEntries: vi.fn().mockReturnValue([]),
|
|
73
64
|
...overrides,
|
|
74
|
-
};
|
|
65
|
+
} as unknown as PermissionSession;
|
|
75
66
|
}
|
|
76
67
|
|
|
77
68
|
function makeDeps(overrides: Partial<HandlerDeps> = {}): HandlerDeps {
|
|
78
69
|
return {
|
|
79
70
|
session: makeSession(),
|
|
80
|
-
|
|
81
|
-
piInfrastructureDirs: ["/test/agent", "/test/agent/git"],
|
|
82
|
-
getPiInfrastructureReadPaths: vi.fn().mockReturnValue([]),
|
|
83
|
-
createPermissionManagerForCwd: vi.fn().mockReturnValue(makePm()),
|
|
84
|
-
refreshExtensionConfig: vi.fn(),
|
|
85
|
-
logResolvedConfigPaths: vi.fn(),
|
|
86
|
-
resolveAgentName: vi.fn().mockReturnValue(null),
|
|
71
|
+
events: { emit: vi.fn(), on: vi.fn().mockReturnValue(() => undefined) },
|
|
87
72
|
canRequestPermissionConfirmation: vi.fn().mockReturnValue(false),
|
|
88
73
|
promptPermission: vi
|
|
89
74
|
.fn()
|
|
90
75
|
.mockResolvedValue({ approved: true, state: "approved" }),
|
|
91
76
|
createPermissionRequestId: vi.fn().mockReturnValue("test-id"),
|
|
92
|
-
events: { emit: vi.fn(), on: vi.fn().mockReturnValue(() => undefined) },
|
|
93
|
-
startForwardedPermissionPolling: vi.fn(),
|
|
94
|
-
stopForwardedPermissionPolling: vi.fn(),
|
|
95
77
|
stopPermissionRpcHandlers: vi.fn(),
|
|
96
78
|
getAllTools: vi.fn().mockReturnValue([]),
|
|
97
79
|
setActiveTools: vi.fn(),
|
|
@@ -103,48 +85,48 @@ function makeDeps(overrides: Partial<HandlerDeps> = {}): HandlerDeps {
|
|
|
103
85
|
|
|
104
86
|
describe("shouldExposeTool", () => {
|
|
105
87
|
it("returns true when tool permission is allow", () => {
|
|
106
|
-
const
|
|
107
|
-
expect(shouldExposeTool("read", null,
|
|
88
|
+
const getter = vi.fn().mockReturnValue("allow");
|
|
89
|
+
expect(shouldExposeTool("read", null, getter)).toBe(true);
|
|
108
90
|
});
|
|
109
91
|
|
|
110
92
|
it("returns true when tool permission is ask", () => {
|
|
111
|
-
const
|
|
112
|
-
expect(shouldExposeTool("bash", "agent-x",
|
|
93
|
+
const getter = vi.fn().mockReturnValue("ask");
|
|
94
|
+
expect(shouldExposeTool("bash", "agent-x", getter)).toBe(true);
|
|
113
95
|
});
|
|
114
96
|
|
|
115
97
|
it("returns false when tool permission is deny", () => {
|
|
116
|
-
const
|
|
117
|
-
expect(shouldExposeTool("write", null,
|
|
98
|
+
const getter = vi.fn().mockReturnValue("deny");
|
|
99
|
+
expect(shouldExposeTool("write", null, getter)).toBe(false);
|
|
118
100
|
});
|
|
119
101
|
|
|
120
102
|
it("passes agentName through to getToolPermission", () => {
|
|
121
|
-
const
|
|
122
|
-
shouldExposeTool("read", "my-agent",
|
|
123
|
-
expect(
|
|
103
|
+
const getter = vi.fn().mockReturnValue("allow");
|
|
104
|
+
shouldExposeTool("read", "my-agent", getter);
|
|
105
|
+
expect(getter).toHaveBeenCalledWith("read", "my-agent");
|
|
124
106
|
});
|
|
125
107
|
|
|
126
108
|
it("converts null agentName to undefined for getToolPermission", () => {
|
|
127
|
-
const
|
|
128
|
-
shouldExposeTool("read", null,
|
|
129
|
-
expect(
|
|
109
|
+
const getter = vi.fn().mockReturnValue("allow");
|
|
110
|
+
shouldExposeTool("read", null, getter);
|
|
111
|
+
expect(getter).toHaveBeenCalledWith("read", undefined);
|
|
130
112
|
});
|
|
131
113
|
});
|
|
132
114
|
|
|
133
115
|
// ── handleBeforeAgentStart ─────────────────────────────────────────────────
|
|
134
116
|
|
|
135
117
|
describe("handleBeforeAgentStart", () => {
|
|
136
|
-
it("
|
|
118
|
+
it("activates the session with ctx", async () => {
|
|
137
119
|
const ctx = makeCtx();
|
|
138
120
|
const deps = makeDeps();
|
|
139
121
|
await handleBeforeAgentStart(deps, makeEvent(), ctx);
|
|
140
|
-
expect(deps.
|
|
122
|
+
expect(deps.session.activate).toHaveBeenCalledWith(ctx);
|
|
141
123
|
});
|
|
142
124
|
|
|
143
|
-
it("
|
|
125
|
+
it("refreshes config with ctx", async () => {
|
|
144
126
|
const ctx = makeCtx();
|
|
145
127
|
const deps = makeDeps();
|
|
146
128
|
await handleBeforeAgentStart(deps, makeEvent(), ctx);
|
|
147
|
-
expect(deps.
|
|
129
|
+
expect(deps.session.refreshConfig).toHaveBeenCalledWith(ctx);
|
|
148
130
|
});
|
|
149
131
|
|
|
150
132
|
it("resolves agent name using systemPrompt", async () => {
|
|
@@ -155,33 +137,28 @@ describe("handleBeforeAgentStart", () => {
|
|
|
155
137
|
makeEvent("<active_agent name='x'>"),
|
|
156
138
|
ctx,
|
|
157
139
|
);
|
|
158
|
-
expect(deps.resolveAgentName).toHaveBeenCalledWith(
|
|
140
|
+
expect(deps.session.resolveAgentName).toHaveBeenCalledWith(
|
|
159
141
|
ctx,
|
|
160
142
|
"<active_agent name='x'>",
|
|
161
143
|
);
|
|
162
144
|
});
|
|
163
145
|
|
|
164
146
|
it("filters out denied tools from allowed list", async () => {
|
|
165
|
-
const
|
|
147
|
+
const session = makeSession({
|
|
148
|
+
getToolPermission: vi.fn().mockReturnValue("deny"),
|
|
149
|
+
});
|
|
166
150
|
const deps = makeDeps({
|
|
167
|
-
session
|
|
168
|
-
permissionManager: pm as unknown as PermissionManager,
|
|
169
|
-
}),
|
|
151
|
+
session,
|
|
170
152
|
getAllTools: vi
|
|
171
153
|
.fn()
|
|
172
154
|
.mockReturnValue([{ name: "write" }, { name: "read" }]),
|
|
173
155
|
});
|
|
174
|
-
// write is deny, read is deny (same pm stub — both denied)
|
|
175
156
|
await handleBeforeAgentStart(deps, makeEvent(), makeCtx());
|
|
176
157
|
expect(deps.setActiveTools).toHaveBeenCalledWith([]);
|
|
177
158
|
});
|
|
178
159
|
|
|
179
160
|
it("includes allowed and ask tools in the active list", async () => {
|
|
180
|
-
const pm = makePm("allow");
|
|
181
161
|
const deps = makeDeps({
|
|
182
|
-
session: makeSession({
|
|
183
|
-
permissionManager: pm as unknown as PermissionManager,
|
|
184
|
-
}),
|
|
185
162
|
getAllTools: vi
|
|
186
163
|
.fn()
|
|
187
164
|
.mockReturnValue([{ name: "read" }, { name: "write" }]),
|
|
@@ -190,31 +167,59 @@ describe("handleBeforeAgentStart", () => {
|
|
|
190
167
|
expect(deps.setActiveTools).toHaveBeenCalledWith(["read", "write"]);
|
|
191
168
|
});
|
|
192
169
|
|
|
193
|
-
it("
|
|
170
|
+
it("commits active-tools cache key after applying", async () => {
|
|
194
171
|
const deps = makeDeps({
|
|
195
172
|
getAllTools: vi.fn().mockReturnValue([{ name: "read" }]),
|
|
196
173
|
});
|
|
197
174
|
await handleBeforeAgentStart(deps, makeEvent(), makeCtx());
|
|
198
|
-
expect(deps.session.
|
|
175
|
+
expect(deps.session.commitActiveToolsCacheKey).toHaveBeenCalled();
|
|
199
176
|
});
|
|
200
177
|
|
|
201
178
|
it("skips setActiveTools when cache key is unchanged", async () => {
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
);
|
|
206
|
-
const key = createActiveToolsCacheKey(["read"]);
|
|
179
|
+
const session = makeSession({
|
|
180
|
+
shouldUpdateActiveTools: vi.fn().mockReturnValue(false),
|
|
181
|
+
});
|
|
207
182
|
const deps = makeDeps({
|
|
208
|
-
session
|
|
183
|
+
session,
|
|
209
184
|
getAllTools: vi.fn().mockReturnValue([{ name: "read" }]),
|
|
210
185
|
});
|
|
211
186
|
await handleBeforeAgentStart(deps, makeEvent(), makeCtx());
|
|
212
187
|
expect(deps.setActiveTools).not.toHaveBeenCalled();
|
|
188
|
+
expect(session.commitActiveToolsCacheKey).not.toHaveBeenCalled();
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it("returns empty object when prompt cache is unchanged", async () => {
|
|
192
|
+
const session = makeSession({
|
|
193
|
+
shouldUpdatePromptState: vi.fn().mockReturnValue(false),
|
|
194
|
+
});
|
|
195
|
+
const deps = makeDeps({
|
|
196
|
+
session,
|
|
197
|
+
getAllTools: vi.fn().mockReturnValue([]),
|
|
198
|
+
});
|
|
199
|
+
const result = await handleBeforeAgentStart(deps, makeEvent(), makeCtx());
|
|
200
|
+
expect(result).toEqual({});
|
|
201
|
+
expect(session.commitPromptStateCacheKey).not.toHaveBeenCalled();
|
|
213
202
|
});
|
|
214
203
|
|
|
215
|
-
it("
|
|
216
|
-
|
|
217
|
-
|
|
204
|
+
it("commits prompt-state cache key and processes prompt when cache is new", async () => {
|
|
205
|
+
const deps = makeDeps({
|
|
206
|
+
getAllTools: vi.fn().mockReturnValue([]),
|
|
207
|
+
});
|
|
208
|
+
await handleBeforeAgentStart(deps, makeEvent(), makeCtx());
|
|
209
|
+
expect(deps.session.commitPromptStateCacheKey).toHaveBeenCalled();
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it("stores resolved skill entries on the session", async () => {
|
|
213
|
+
const deps = makeDeps({
|
|
214
|
+
getAllTools: vi.fn().mockReturnValue([]),
|
|
215
|
+
});
|
|
216
|
+
await handleBeforeAgentStart(deps, makeEvent(), makeCtx());
|
|
217
|
+
expect(deps.session.setActiveSkillEntries).toHaveBeenCalledWith(
|
|
218
|
+
expect.any(Array),
|
|
219
|
+
);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it("returns modified systemPrompt when prompt changes", async () => {
|
|
218
223
|
const systemPrompt = `You are an assistant.\n\nAvailable tools:\n- read\n- write\n`;
|
|
219
224
|
const deps = makeDeps({
|
|
220
225
|
getAllTools: vi.fn().mockReturnValue([]),
|
|
@@ -224,9 +229,7 @@ describe("handleBeforeAgentStart", () => {
|
|
|
224
229
|
makeEvent(systemPrompt),
|
|
225
230
|
makeCtx(),
|
|
226
231
|
);
|
|
227
|
-
// The prompt was modified, so systemPrompt should be returned
|
|
228
232
|
expect(result).toHaveProperty("systemPrompt");
|
|
229
|
-
expect(deps.session.lastPromptStateCacheKey).not.toBeNull();
|
|
230
233
|
});
|
|
231
234
|
|
|
232
235
|
it("returns empty object when systemPrompt is unchanged", async () => {
|
|
@@ -241,39 +244,4 @@ describe("handleBeforeAgentStart", () => {
|
|
|
241
244
|
);
|
|
242
245
|
expect(result).toEqual({});
|
|
243
246
|
});
|
|
244
|
-
|
|
245
|
-
it("stores resolved skill entries on deps", async () => {
|
|
246
|
-
const deps = makeDeps({
|
|
247
|
-
getAllTools: vi.fn().mockReturnValue([]),
|
|
248
|
-
});
|
|
249
|
-
await handleBeforeAgentStart(deps, makeEvent(), makeCtx());
|
|
250
|
-
expect(deps.session.activeSkillEntries).toEqual(expect.any(Array));
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
it("returns empty object and skips prompt work when prompt cache key is unchanged", async () => {
|
|
254
|
-
const { createBeforeAgentStartPromptStateKey } = await import(
|
|
255
|
-
"../../src/before-agent-start-cache"
|
|
256
|
-
);
|
|
257
|
-
const pm = makePm("allow");
|
|
258
|
-
const ctx = makeCtx({ cwd: "/proj" });
|
|
259
|
-
const allowedTools: string[] = ["read"];
|
|
260
|
-
const key = createBeforeAgentStartPromptStateKey({
|
|
261
|
-
agentName: null,
|
|
262
|
-
cwd: "/proj",
|
|
263
|
-
permissionStamp: "stamp-1",
|
|
264
|
-
systemPrompt: "hello",
|
|
265
|
-
allowedToolNames: allowedTools,
|
|
266
|
-
});
|
|
267
|
-
const deps = makeDeps({
|
|
268
|
-
session: makeSession({
|
|
269
|
-
permissionManager: pm as unknown as PermissionManager,
|
|
270
|
-
lastPromptStateCacheKey: key,
|
|
271
|
-
}),
|
|
272
|
-
getAllTools: vi.fn().mockReturnValue([{ name: "read" }]),
|
|
273
|
-
});
|
|
274
|
-
const result = await handleBeforeAgentStart(deps, makeEvent("hello"), ctx);
|
|
275
|
-
expect(result).toEqual({});
|
|
276
|
-
// activeSkillEntries was not assigned by the handler (early return)
|
|
277
|
-
expect(deps.session.activeSkillEntries).toEqual([]);
|
|
278
|
-
});
|
|
279
247
|
});
|
|
@@ -8,7 +8,8 @@ import { handleInput } from "../../src/handlers/input";
|
|
|
8
8
|
import type { HandlerDeps } from "../../src/handlers/types";
|
|
9
9
|
import type { PermissionDecisionEvent } from "../../src/permission-events";
|
|
10
10
|
import { PERMISSIONS_DECISION_CHANNEL } from "../../src/permission-events";
|
|
11
|
-
import type {
|
|
11
|
+
import type { PermissionSession } from "../../src/permission-session";
|
|
12
|
+
import type { PermissionState } from "../../src/types";
|
|
12
13
|
|
|
13
14
|
// ── helpers ────────────────────────────────────────────────────────────────
|
|
14
15
|
|
|
@@ -38,28 +39,24 @@ function makeCtx(overrides: Partial<ExtensionContext> = {}): ExtensionContext {
|
|
|
38
39
|
} as unknown as ExtensionContext;
|
|
39
40
|
}
|
|
40
41
|
|
|
41
|
-
function makeSession(
|
|
42
|
+
function makeSession(
|
|
43
|
+
state: "allow" | "deny" | "ask" = "allow",
|
|
44
|
+
): PermissionSession {
|
|
42
45
|
return {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
sessionRules: {
|
|
58
|
-
approve: vi.fn(),
|
|
59
|
-
getRuleset: vi.fn().mockReturnValue([]),
|
|
60
|
-
clear: vi.fn(),
|
|
61
|
-
} as unknown as SessionState["sessionRules"],
|
|
62
|
-
};
|
|
46
|
+
logger: { debug: vi.fn(), review: vi.fn(), warn: vi.fn() },
|
|
47
|
+
activate: vi.fn(),
|
|
48
|
+
resolveAgentName: vi.fn().mockReturnValue(null),
|
|
49
|
+
checkPermission: vi.fn().mockReturnValue({
|
|
50
|
+
state,
|
|
51
|
+
toolName: "skill",
|
|
52
|
+
source: "skill",
|
|
53
|
+
origin: "global",
|
|
54
|
+
matchedPattern: "*",
|
|
55
|
+
}),
|
|
56
|
+
getToolPermission: vi.fn().mockReturnValue("allow" as PermissionState),
|
|
57
|
+
getSessionRuleset: vi.fn().mockReturnValue([]),
|
|
58
|
+
approveSessionRule: vi.fn(),
|
|
59
|
+
} as unknown as PermissionSession;
|
|
63
60
|
}
|
|
64
61
|
|
|
65
62
|
function makeDeps(
|
|
@@ -68,21 +65,12 @@ function makeDeps(
|
|
|
68
65
|
): HandlerDeps {
|
|
69
66
|
return {
|
|
70
67
|
session: makeSession(state),
|
|
71
|
-
logger: { debug: vi.fn(), review: vi.fn(), warn: vi.fn() },
|
|
72
|
-
piInfrastructureDirs: ["/test/agent"],
|
|
73
|
-
getPiInfrastructureReadPaths: vi.fn().mockReturnValue([]),
|
|
74
68
|
events: makeEvents(),
|
|
75
|
-
createPermissionManagerForCwd: vi.fn(),
|
|
76
|
-
refreshExtensionConfig: vi.fn(),
|
|
77
|
-
logResolvedConfigPaths: vi.fn(),
|
|
78
|
-
resolveAgentName: vi.fn().mockReturnValue(null),
|
|
79
69
|
canRequestPermissionConfirmation: vi.fn().mockReturnValue(true),
|
|
80
70
|
promptPermission: vi
|
|
81
71
|
.fn()
|
|
82
72
|
.mockResolvedValue({ approved: true, state: "approved" }),
|
|
83
73
|
createPermissionRequestId: vi.fn().mockReturnValue("req-id"),
|
|
84
|
-
startForwardedPermissionPolling: vi.fn(),
|
|
85
|
-
stopForwardedPermissionPolling: vi.fn(),
|
|
86
74
|
stopPermissionRpcHandlers: vi.fn(),
|
|
87
75
|
getAllTools: vi.fn().mockReturnValue([]),
|
|
88
76
|
setActiveTools: vi.fn(),
|
|
@@ -192,7 +180,6 @@ describe("handleInput decision events — skill gate", () => {
|
|
|
192
180
|
|
|
193
181
|
it("emits allow with auto_approved when promptPermission returns autoApproved:true", async () => {
|
|
194
182
|
const deps = makeDeps("ask", {
|
|
195
|
-
// Simulate what PermissionPrompter returns in yolo mode
|
|
196
183
|
promptPermission: vi.fn().mockResolvedValue({
|
|
197
184
|
approved: true,
|
|
198
185
|
state: "approved",
|
|
@@ -6,8 +6,8 @@ import {
|
|
|
6
6
|
handleInput,
|
|
7
7
|
} from "../../src/handlers/input";
|
|
8
8
|
import type { HandlerDeps } from "../../src/handlers/types";
|
|
9
|
-
import type {
|
|
10
|
-
import type {
|
|
9
|
+
import type { PermissionSession } from "../../src/permission-session";
|
|
10
|
+
import type { PermissionState } from "../../src/types";
|
|
11
11
|
|
|
12
12
|
// ── helpers ────────────────────────────────────────────────────────────────
|
|
13
13
|
|
|
@@ -34,43 +34,30 @@ function makeInputEvent(text: string) {
|
|
|
34
34
|
return { text };
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
function makeSession(
|
|
37
|
+
function makeSession(
|
|
38
|
+
overrides: Partial<Record<keyof PermissionSession, unknown>> = {},
|
|
39
|
+
): PermissionSession {
|
|
38
40
|
return {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
lastPromptStateCacheKey: null,
|
|
47
|
-
sessionRules: {
|
|
48
|
-
approve: vi.fn(),
|
|
49
|
-
getRuleset: vi.fn().mockReturnValue([]),
|
|
50
|
-
clear: vi.fn(),
|
|
51
|
-
} as unknown as SessionState["sessionRules"],
|
|
41
|
+
logger: { debug: vi.fn(), review: vi.fn(), warn: vi.fn() },
|
|
42
|
+
activate: vi.fn(),
|
|
43
|
+
resolveAgentName: vi.fn().mockReturnValue(null),
|
|
44
|
+
checkPermission: vi.fn().mockReturnValue({ state: "allow" }),
|
|
45
|
+
getToolPermission: vi.fn().mockReturnValue("allow" as PermissionState),
|
|
46
|
+
getSessionRuleset: vi.fn().mockReturnValue([]),
|
|
47
|
+
approveSessionRule: vi.fn(),
|
|
52
48
|
...overrides,
|
|
53
|
-
};
|
|
49
|
+
} as unknown as PermissionSession;
|
|
54
50
|
}
|
|
55
51
|
|
|
56
52
|
function makeDeps(overrides: Partial<HandlerDeps> = {}): HandlerDeps {
|
|
57
53
|
return {
|
|
58
54
|
session: makeSession(),
|
|
59
|
-
|
|
60
|
-
piInfrastructureDirs: ["/test/agent", "/test/agent/git"],
|
|
61
|
-
getPiInfrastructureReadPaths: vi.fn().mockReturnValue([]),
|
|
62
|
-
createPermissionManagerForCwd: vi.fn(),
|
|
63
|
-
refreshExtensionConfig: vi.fn(),
|
|
64
|
-
logResolvedConfigPaths: vi.fn(),
|
|
65
|
-
resolveAgentName: vi.fn().mockReturnValue(null),
|
|
55
|
+
events: { emit: vi.fn(), on: vi.fn().mockReturnValue(() => undefined) },
|
|
66
56
|
canRequestPermissionConfirmation: vi.fn().mockReturnValue(true),
|
|
67
57
|
promptPermission: vi
|
|
68
58
|
.fn()
|
|
69
59
|
.mockResolvedValue({ approved: true, state: "approved" }),
|
|
70
60
|
createPermissionRequestId: vi.fn().mockReturnValue("req-id"),
|
|
71
|
-
events: { emit: vi.fn(), on: vi.fn().mockReturnValue(() => undefined) },
|
|
72
|
-
startForwardedPermissionPolling: vi.fn(),
|
|
73
|
-
stopForwardedPermissionPolling: vi.fn(),
|
|
74
61
|
stopPermissionRpcHandlers: vi.fn(),
|
|
75
62
|
getAllTools: vi.fn().mockReturnValue([]),
|
|
76
63
|
setActiveTools: vi.fn(),
|
|
@@ -115,18 +102,11 @@ describe("extractSkillNameFromInput", () => {
|
|
|
115
102
|
// ── handleInput ───────────────────────────────────────────────────────────
|
|
116
103
|
|
|
117
104
|
describe("handleInput", () => {
|
|
118
|
-
it("
|
|
105
|
+
it("activates session with ctx", async () => {
|
|
119
106
|
const ctx = makeCtx();
|
|
120
107
|
const deps = makeDeps();
|
|
121
108
|
await handleInput(deps, makeInputEvent("hello"), ctx);
|
|
122
|
-
expect(deps.session.
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
it("starts forwarded permission polling", async () => {
|
|
126
|
-
const ctx = makeCtx();
|
|
127
|
-
const deps = makeDeps();
|
|
128
|
-
await handleInput(deps, makeInputEvent("hello"), ctx);
|
|
129
|
-
expect(deps.startForwardedPermissionPolling).toHaveBeenCalledWith(ctx);
|
|
109
|
+
expect(deps.session.activate).toHaveBeenCalledWith(ctx);
|
|
130
110
|
});
|
|
131
111
|
|
|
132
112
|
it("returns continue for non-skill input", async () => {
|
|
@@ -142,14 +122,11 @@ describe("handleInput", () => {
|
|
|
142
122
|
it("does not check permissions for non-skill input", async () => {
|
|
143
123
|
const deps = makeDeps();
|
|
144
124
|
await handleInput(deps, makeInputEvent("just a message"), makeCtx());
|
|
145
|
-
expect(
|
|
146
|
-
deps.session.permissionManager.checkPermission,
|
|
147
|
-
).not.toHaveBeenCalled();
|
|
125
|
+
expect(deps.session.checkPermission).not.toHaveBeenCalled();
|
|
148
126
|
});
|
|
149
127
|
|
|
150
128
|
it("returns continue when skill is allowed", async () => {
|
|
151
129
|
const deps = makeDeps();
|
|
152
|
-
// default makeRuntime() has checkPermission → { state: "allow" }
|
|
153
130
|
const result = await handleInput(
|
|
154
131
|
deps,
|
|
155
132
|
makeInputEvent("/skill:librarian"),
|
|
@@ -159,14 +136,10 @@ describe("handleInput", () => {
|
|
|
159
136
|
});
|
|
160
137
|
|
|
161
138
|
it("returns handled when skill is denied", async () => {
|
|
162
|
-
const
|
|
139
|
+
const session = makeSession({
|
|
163
140
|
checkPermission: vi.fn().mockReturnValue({ state: "deny" }),
|
|
164
|
-
};
|
|
165
|
-
const deps = makeDeps({
|
|
166
|
-
session: makeSession({
|
|
167
|
-
permissionManager: pm as unknown as SessionState["permissionManager"],
|
|
168
|
-
}),
|
|
169
141
|
});
|
|
142
|
+
const deps = makeDeps({ session });
|
|
170
143
|
const result = await handleInput(
|
|
171
144
|
deps,
|
|
172
145
|
makeInputEvent("/skill:librarian"),
|
|
@@ -177,14 +150,10 @@ describe("handleInput", () => {
|
|
|
177
150
|
|
|
178
151
|
it("shows a warning notification when skill is denied and UI is available", async () => {
|
|
179
152
|
const ctx = makeCtx({ hasUI: true });
|
|
180
|
-
const
|
|
153
|
+
const session = makeSession({
|
|
181
154
|
checkPermission: vi.fn().mockReturnValue({ state: "deny" }),
|
|
182
|
-
};
|
|
183
|
-
const deps = makeDeps({
|
|
184
|
-
session: makeSession({
|
|
185
|
-
permissionManager: pm as unknown as SessionState["permissionManager"],
|
|
186
|
-
}),
|
|
187
155
|
});
|
|
156
|
+
const deps = makeDeps({ session });
|
|
188
157
|
await handleInput(deps, makeInputEvent("/skill:librarian"), ctx);
|
|
189
158
|
expect(ctx.ui.notify).toHaveBeenCalledWith(
|
|
190
159
|
expect.stringContaining("librarian"),
|
|
@@ -194,22 +163,20 @@ describe("handleInput", () => {
|
|
|
194
163
|
|
|
195
164
|
it("does not show a warning notification when skill is denied and UI is absent", async () => {
|
|
196
165
|
const ctx = makeCtx({ hasUI: false });
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
session: makeSession({
|
|
200
|
-
permissionManager: pm as unknown as SessionState["permissionManager"],
|
|
201
|
-
}),
|
|
166
|
+
const session = makeSession({
|
|
167
|
+
checkPermission: vi.fn().mockReturnValue({ state: "deny" }),
|
|
202
168
|
});
|
|
169
|
+
const deps = makeDeps({ session });
|
|
203
170
|
await handleInput(deps, makeInputEvent("/skill:librarian"), ctx);
|
|
204
171
|
expect(ctx.ui.notify).not.toHaveBeenCalled();
|
|
205
172
|
});
|
|
206
173
|
|
|
207
174
|
it("returns handled when skill requires approval but no UI is available", async () => {
|
|
208
|
-
const
|
|
175
|
+
const session = makeSession({
|
|
176
|
+
checkPermission: vi.fn().mockReturnValue({ state: "ask" }),
|
|
177
|
+
});
|
|
209
178
|
const deps = makeDeps({
|
|
210
|
-
session
|
|
211
|
-
permissionManager: pm as unknown as SessionState["permissionManager"],
|
|
212
|
-
}),
|
|
179
|
+
session,
|
|
213
180
|
canRequestPermissionConfirmation: vi.fn().mockReturnValue(false),
|
|
214
181
|
});
|
|
215
182
|
const result = await handleInput(
|
|
@@ -221,11 +188,11 @@ describe("handleInput", () => {
|
|
|
221
188
|
});
|
|
222
189
|
|
|
223
190
|
it("prompts and returns continue when skill ask is approved", async () => {
|
|
224
|
-
const
|
|
191
|
+
const session = makeSession({
|
|
192
|
+
checkPermission: vi.fn().mockReturnValue({ state: "ask" }),
|
|
193
|
+
});
|
|
225
194
|
const deps = makeDeps({
|
|
226
|
-
session
|
|
227
|
-
permissionManager: pm as unknown as SessionState["permissionManager"],
|
|
228
|
-
}),
|
|
195
|
+
session,
|
|
229
196
|
canRequestPermissionConfirmation: vi.fn().mockReturnValue(true),
|
|
230
197
|
promptPermission: vi
|
|
231
198
|
.fn()
|
|
@@ -241,11 +208,11 @@ describe("handleInput", () => {
|
|
|
241
208
|
});
|
|
242
209
|
|
|
243
210
|
it("returns handled when skill ask is denied by user", async () => {
|
|
244
|
-
const
|
|
211
|
+
const session = makeSession({
|
|
212
|
+
checkPermission: vi.fn().mockReturnValue({ state: "ask" }),
|
|
213
|
+
});
|
|
245
214
|
const deps = makeDeps({
|
|
246
|
-
session
|
|
247
|
-
permissionManager: pm as unknown as SessionState["permissionManager"],
|
|
248
|
-
}),
|
|
215
|
+
session,
|
|
249
216
|
canRequestPermissionConfirmation: vi.fn().mockReturnValue(true),
|
|
250
217
|
promptPermission: vi
|
|
251
218
|
.fn()
|
|
@@ -260,12 +227,12 @@ describe("handleInput", () => {
|
|
|
260
227
|
});
|
|
261
228
|
|
|
262
229
|
it("passes agentName in the prompt permission request", async () => {
|
|
263
|
-
const
|
|
264
|
-
|
|
265
|
-
session: makeSession({
|
|
266
|
-
permissionManager: pm as unknown as SessionState["permissionManager"],
|
|
267
|
-
}),
|
|
230
|
+
const session = makeSession({
|
|
231
|
+
checkPermission: vi.fn().mockReturnValue({ state: "ask" }),
|
|
268
232
|
resolveAgentName: vi.fn().mockReturnValue("code-agent"),
|
|
233
|
+
});
|
|
234
|
+
const deps = makeDeps({
|
|
235
|
+
session,
|
|
269
236
|
canRequestPermissionConfirmation: vi.fn().mockReturnValue(true),
|
|
270
237
|
promptPermission: vi
|
|
271
238
|
.fn()
|