@gotgenes/pi-permission-system 5.10.0 → 5.11.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 +15 -0
- package/package.json +1 -1
- package/src/forwarding-manager.ts +1 -1
- package/src/handlers/before-agent-start.ts +73 -60
- package/src/handlers/gates/descriptor.ts +1 -1
- package/src/handlers/index.ts +6 -15
- package/src/handlers/lifecycle.ts +54 -42
- package/src/handlers/permission-gate-handler.ts +346 -0
- package/src/index.ts +33 -38
- package/src/permission-prompter.ts +23 -6
- package/src/permission-session.ts +29 -0
- package/src/session-logger.ts +1 -1
- package/src/tool-registry.ts +6 -0
- package/tests/handlers/before-agent-start.test.ts +70 -90
- package/tests/handlers/input-events.test.ts +70 -63
- package/tests/handlers/input.test.ts +85 -83
- package/tests/handlers/lifecycle.test.ts +60 -72
- package/tests/handlers/tool-call-events.test.ts +128 -122
- package/tests/handlers/tool-call.test.ts +84 -58
- package/tests/permission-prompter.test.ts +1 -1
- package/tests/permission-session.test.ts +61 -0
- package/src/handlers/input.ts +0 -126
- package/src/handlers/tool-call.ts +0 -203
- package/src/handlers/types.ts +0 -63
|
@@ -2,11 +2,11 @@ import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
|
2
2
|
import { describe, expect, it, vi } from "vitest";
|
|
3
3
|
|
|
4
4
|
import {
|
|
5
|
-
|
|
5
|
+
AgentPrepHandler,
|
|
6
6
|
shouldExposeTool,
|
|
7
7
|
} from "../../src/handlers/before-agent-start";
|
|
8
|
-
import type { HandlerDeps } from "../../src/handlers/types";
|
|
9
8
|
import type { PermissionSession } from "../../src/permission-session";
|
|
9
|
+
import type { ToolRegistry } from "../../src/tool-registry";
|
|
10
10
|
import type { PermissionState } from "../../src/types";
|
|
11
11
|
|
|
12
12
|
// ── SDK stubs ──────────────────────────────────────────────────────────────
|
|
@@ -65,22 +65,28 @@ function makeSession(
|
|
|
65
65
|
} as unknown as PermissionSession;
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
function
|
|
68
|
+
function makeToolRegistry(overrides: Partial<ToolRegistry> = {}): ToolRegistry {
|
|
69
69
|
return {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
canRequestPermissionConfirmation: vi.fn().mockReturnValue(false),
|
|
73
|
-
promptPermission: vi
|
|
74
|
-
.fn()
|
|
75
|
-
.mockResolvedValue({ approved: true, state: "approved" }),
|
|
76
|
-
createPermissionRequestId: vi.fn().mockReturnValue("test-id"),
|
|
77
|
-
stopPermissionRpcHandlers: vi.fn(),
|
|
78
|
-
getAllTools: vi.fn().mockReturnValue([]),
|
|
79
|
-
setActiveTools: vi.fn(),
|
|
70
|
+
getAll: vi.fn().mockReturnValue([]),
|
|
71
|
+
setActive: vi.fn(),
|
|
80
72
|
...overrides,
|
|
81
73
|
};
|
|
82
74
|
}
|
|
83
75
|
|
|
76
|
+
function makeHandler(overrides?: {
|
|
77
|
+
session?: Partial<Record<keyof PermissionSession, unknown>>;
|
|
78
|
+
toolRegistry?: Partial<ToolRegistry>;
|
|
79
|
+
}): {
|
|
80
|
+
handler: AgentPrepHandler;
|
|
81
|
+
session: PermissionSession;
|
|
82
|
+
toolRegistry: ToolRegistry;
|
|
83
|
+
} {
|
|
84
|
+
const session = makeSession(overrides?.session);
|
|
85
|
+
const toolRegistry = makeToolRegistry(overrides?.toolRegistry);
|
|
86
|
+
const handler = new AgentPrepHandler(session, toolRegistry);
|
|
87
|
+
return { handler, session, toolRegistry };
|
|
88
|
+
}
|
|
89
|
+
|
|
84
90
|
// ── shouldExposeTool (pure helper) ─────────────────────────────────────────
|
|
85
91
|
|
|
86
92
|
describe("shouldExposeTool", () => {
|
|
@@ -112,136 +118,110 @@ describe("shouldExposeTool", () => {
|
|
|
112
118
|
});
|
|
113
119
|
});
|
|
114
120
|
|
|
115
|
-
// ──
|
|
121
|
+
// ── AgentPrepHandler.handle ────────────────────────────────────────────────
|
|
116
122
|
|
|
117
|
-
describe("
|
|
123
|
+
describe("AgentPrepHandler.handle", () => {
|
|
118
124
|
it("activates the session with ctx", async () => {
|
|
119
125
|
const ctx = makeCtx();
|
|
120
|
-
const
|
|
121
|
-
await
|
|
122
|
-
expect(
|
|
126
|
+
const { handler, session } = makeHandler();
|
|
127
|
+
await handler.handle(makeEvent(), ctx);
|
|
128
|
+
expect(session.activate).toHaveBeenCalledWith(ctx);
|
|
123
129
|
});
|
|
124
130
|
|
|
125
131
|
it("refreshes config with ctx", async () => {
|
|
126
132
|
const ctx = makeCtx();
|
|
127
|
-
const
|
|
128
|
-
await
|
|
129
|
-
expect(
|
|
133
|
+
const { handler, session } = makeHandler();
|
|
134
|
+
await handler.handle(makeEvent(), ctx);
|
|
135
|
+
expect(session.refreshConfig).toHaveBeenCalledWith(ctx);
|
|
130
136
|
});
|
|
131
137
|
|
|
132
138
|
it("resolves agent name using systemPrompt", async () => {
|
|
133
139
|
const ctx = makeCtx();
|
|
134
|
-
const
|
|
135
|
-
await
|
|
136
|
-
|
|
137
|
-
makeEvent("<active_agent name='x'>"),
|
|
138
|
-
ctx,
|
|
139
|
-
);
|
|
140
|
-
expect(deps.session.resolveAgentName).toHaveBeenCalledWith(
|
|
140
|
+
const { handler, session } = makeHandler();
|
|
141
|
+
await handler.handle(makeEvent("<active_agent name='x'>"), ctx);
|
|
142
|
+
expect(session.resolveAgentName).toHaveBeenCalledWith(
|
|
141
143
|
ctx,
|
|
142
144
|
"<active_agent name='x'>",
|
|
143
145
|
);
|
|
144
146
|
});
|
|
145
147
|
|
|
146
148
|
it("filters out denied tools from allowed list", async () => {
|
|
147
|
-
const
|
|
148
|
-
getToolPermission: vi.fn().mockReturnValue("deny"),
|
|
149
|
+
const { handler, toolRegistry } = makeHandler({
|
|
150
|
+
session: { getToolPermission: vi.fn().mockReturnValue("deny") },
|
|
151
|
+
toolRegistry: {
|
|
152
|
+
getAll: vi.fn().mockReturnValue([{ name: "write" }, { name: "read" }]),
|
|
153
|
+
},
|
|
149
154
|
});
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
getAllTools: vi
|
|
153
|
-
.fn()
|
|
154
|
-
.mockReturnValue([{ name: "write" }, { name: "read" }]),
|
|
155
|
-
});
|
|
156
|
-
await handleBeforeAgentStart(deps, makeEvent(), makeCtx());
|
|
157
|
-
expect(deps.setActiveTools).toHaveBeenCalledWith([]);
|
|
155
|
+
await handler.handle(makeEvent(), makeCtx());
|
|
156
|
+
expect(toolRegistry.setActive).toHaveBeenCalledWith([]);
|
|
158
157
|
});
|
|
159
158
|
|
|
160
159
|
it("includes allowed and ask tools in the active list", async () => {
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
.fn()
|
|
164
|
-
|
|
160
|
+
const { handler, toolRegistry } = makeHandler({
|
|
161
|
+
toolRegistry: {
|
|
162
|
+
getAll: vi.fn().mockReturnValue([{ name: "read" }, { name: "write" }]),
|
|
163
|
+
},
|
|
165
164
|
});
|
|
166
|
-
await
|
|
167
|
-
expect(
|
|
165
|
+
await handler.handle(makeEvent(), makeCtx());
|
|
166
|
+
expect(toolRegistry.setActive).toHaveBeenCalledWith(["read", "write"]);
|
|
168
167
|
});
|
|
169
168
|
|
|
170
169
|
it("commits active-tools cache key after applying", async () => {
|
|
171
|
-
const
|
|
172
|
-
|
|
170
|
+
const { handler, session } = makeHandler({
|
|
171
|
+
toolRegistry: {
|
|
172
|
+
getAll: vi.fn().mockReturnValue([{ name: "read" }]),
|
|
173
|
+
},
|
|
173
174
|
});
|
|
174
|
-
await
|
|
175
|
-
expect(
|
|
175
|
+
await handler.handle(makeEvent(), makeCtx());
|
|
176
|
+
expect(session.commitActiveToolsCacheKey).toHaveBeenCalled();
|
|
176
177
|
});
|
|
177
178
|
|
|
178
|
-
it("skips
|
|
179
|
-
const session =
|
|
180
|
-
shouldUpdateActiveTools: vi.fn().mockReturnValue(false),
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
getAllTools: vi.fn().mockReturnValue([{ name: "read" }]),
|
|
179
|
+
it("skips setActive when cache key is unchanged", async () => {
|
|
180
|
+
const { handler, session, toolRegistry } = makeHandler({
|
|
181
|
+
session: { shouldUpdateActiveTools: vi.fn().mockReturnValue(false) },
|
|
182
|
+
toolRegistry: {
|
|
183
|
+
getAll: vi.fn().mockReturnValue([{ name: "read" }]),
|
|
184
|
+
},
|
|
185
185
|
});
|
|
186
|
-
await
|
|
187
|
-
expect(
|
|
186
|
+
await handler.handle(makeEvent(), makeCtx());
|
|
187
|
+
expect(toolRegistry.setActive).not.toHaveBeenCalled();
|
|
188
188
|
expect(session.commitActiveToolsCacheKey).not.toHaveBeenCalled();
|
|
189
189
|
});
|
|
190
190
|
|
|
191
191
|
it("returns empty object when prompt cache is unchanged", async () => {
|
|
192
|
-
const session =
|
|
193
|
-
shouldUpdatePromptState: vi.fn().mockReturnValue(false),
|
|
194
|
-
});
|
|
195
|
-
const deps = makeDeps({
|
|
196
|
-
session,
|
|
197
|
-
getAllTools: vi.fn().mockReturnValue([]),
|
|
192
|
+
const { handler, session } = makeHandler({
|
|
193
|
+
session: { shouldUpdatePromptState: vi.fn().mockReturnValue(false) },
|
|
198
194
|
});
|
|
199
|
-
const result = await
|
|
195
|
+
const result = await handler.handle(makeEvent(), makeCtx());
|
|
200
196
|
expect(result).toEqual({});
|
|
201
197
|
expect(session.commitPromptStateCacheKey).not.toHaveBeenCalled();
|
|
202
198
|
});
|
|
203
199
|
|
|
204
200
|
it("commits prompt-state cache key and processes prompt when cache is new", async () => {
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
await handleBeforeAgentStart(deps, makeEvent(), makeCtx());
|
|
209
|
-
expect(deps.session.commitPromptStateCacheKey).toHaveBeenCalled();
|
|
201
|
+
const { handler, session } = makeHandler();
|
|
202
|
+
await handler.handle(makeEvent(), makeCtx());
|
|
203
|
+
expect(session.commitPromptStateCacheKey).toHaveBeenCalled();
|
|
210
204
|
});
|
|
211
205
|
|
|
212
206
|
it("stores resolved skill entries on the session", async () => {
|
|
213
|
-
const
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
await handleBeforeAgentStart(deps, makeEvent(), makeCtx());
|
|
217
|
-
expect(deps.session.setActiveSkillEntries).toHaveBeenCalledWith(
|
|
207
|
+
const { handler, session } = makeHandler();
|
|
208
|
+
await handler.handle(makeEvent(), makeCtx());
|
|
209
|
+
expect(session.setActiveSkillEntries).toHaveBeenCalledWith(
|
|
218
210
|
expect.any(Array),
|
|
219
211
|
);
|
|
220
212
|
});
|
|
221
213
|
|
|
222
214
|
it("returns modified systemPrompt when prompt changes", async () => {
|
|
223
215
|
const systemPrompt = `You are an assistant.\n\nAvailable tools:\n- read\n- write\n`;
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
});
|
|
227
|
-
const result = await handleBeforeAgentStart(
|
|
228
|
-
deps,
|
|
229
|
-
makeEvent(systemPrompt),
|
|
230
|
-
makeCtx(),
|
|
231
|
-
);
|
|
216
|
+
const { handler } = makeHandler();
|
|
217
|
+
const result = await handler.handle(makeEvent(systemPrompt), makeCtx());
|
|
232
218
|
expect(result).toHaveProperty("systemPrompt");
|
|
233
219
|
});
|
|
234
220
|
|
|
235
221
|
it("returns empty object when systemPrompt is unchanged", async () => {
|
|
236
222
|
const prompt = "No tools section here.";
|
|
237
|
-
const
|
|
238
|
-
|
|
239
|
-
});
|
|
240
|
-
const result = await handleBeforeAgentStart(
|
|
241
|
-
deps,
|
|
242
|
-
makeEvent(prompt),
|
|
243
|
-
makeCtx(),
|
|
244
|
-
);
|
|
223
|
+
const { handler } = makeHandler();
|
|
224
|
+
const result = await handler.handle(makeEvent(prompt), makeCtx());
|
|
245
225
|
expect(result).toEqual({});
|
|
246
226
|
});
|
|
247
227
|
});
|
|
@@ -4,11 +4,11 @@
|
|
|
4
4
|
import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
5
5
|
import { describe, expect, it, vi } from "vitest";
|
|
6
6
|
|
|
7
|
-
import {
|
|
8
|
-
import type { HandlerDeps } from "../../src/handlers/types";
|
|
7
|
+
import { PermissionGateHandler } from "../../src/handlers/permission-gate-handler";
|
|
9
8
|
import type { PermissionDecisionEvent } from "../../src/permission-events";
|
|
10
9
|
import { PERMISSIONS_DECISION_CHANNEL } from "../../src/permission-events";
|
|
11
10
|
import type { PermissionSession } from "../../src/permission-session";
|
|
11
|
+
import type { ToolRegistry } from "../../src/tool-registry";
|
|
12
12
|
import type { PermissionState } from "../../src/types";
|
|
13
13
|
|
|
14
14
|
// ── helpers ────────────────────────────────────────────────────────────────
|
|
@@ -41,6 +41,7 @@ function makeCtx(overrides: Partial<ExtensionContext> = {}): ExtensionContext {
|
|
|
41
41
|
|
|
42
42
|
function makeSession(
|
|
43
43
|
state: "allow" | "deny" | "ask" = "allow",
|
|
44
|
+
overrides: Partial<Record<keyof PermissionSession, unknown>> = {},
|
|
44
45
|
): PermissionSession {
|
|
45
46
|
return {
|
|
46
47
|
logger: { debug: vi.fn(), review: vi.fn(), warn: vi.fn() },
|
|
@@ -56,31 +57,42 @@ function makeSession(
|
|
|
56
57
|
getToolPermission: vi.fn().mockReturnValue("allow" as PermissionState),
|
|
57
58
|
getSessionRuleset: vi.fn().mockReturnValue([]),
|
|
58
59
|
approveSessionRule: vi.fn(),
|
|
60
|
+
canPrompt: vi.fn().mockReturnValue(true),
|
|
61
|
+
prompt: vi.fn().mockResolvedValue({ approved: true, state: "approved" }),
|
|
62
|
+
createPermissionRequestId: vi.fn().mockReturnValue("req-id"),
|
|
63
|
+
...overrides,
|
|
59
64
|
} as unknown as PermissionSession;
|
|
60
65
|
}
|
|
61
66
|
|
|
62
|
-
function
|
|
63
|
-
state: "allow" | "deny" | "ask" = "allow",
|
|
64
|
-
overrides: Partial<HandlerDeps> = {},
|
|
65
|
-
): HandlerDeps {
|
|
67
|
+
function makeToolRegistry(): ToolRegistry {
|
|
66
68
|
return {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
canRequestPermissionConfirmation: vi.fn().mockReturnValue(true),
|
|
70
|
-
promptPermission: vi
|
|
71
|
-
.fn()
|
|
72
|
-
.mockResolvedValue({ approved: true, state: "approved" }),
|
|
73
|
-
createPermissionRequestId: vi.fn().mockReturnValue("req-id"),
|
|
74
|
-
stopPermissionRpcHandlers: vi.fn(),
|
|
75
|
-
getAllTools: vi.fn().mockReturnValue([]),
|
|
76
|
-
setActiveTools: vi.fn(),
|
|
77
|
-
...overrides,
|
|
69
|
+
getAll: vi.fn().mockReturnValue([]),
|
|
70
|
+
setActive: vi.fn(),
|
|
78
71
|
};
|
|
79
72
|
}
|
|
80
73
|
|
|
81
|
-
function
|
|
82
|
-
|
|
83
|
-
|
|
74
|
+
function makeHandler(
|
|
75
|
+
state: "allow" | "deny" | "ask" = "allow",
|
|
76
|
+
sessionOverrides: Partial<Record<keyof PermissionSession, unknown>> = {},
|
|
77
|
+
): {
|
|
78
|
+
handler: PermissionGateHandler;
|
|
79
|
+
events: ReturnType<typeof makeEvents>;
|
|
80
|
+
} {
|
|
81
|
+
const session = makeSession(state, sessionOverrides);
|
|
82
|
+
const events = makeEvents();
|
|
83
|
+
const handler = new PermissionGateHandler(
|
|
84
|
+
session,
|
|
85
|
+
events,
|
|
86
|
+
makeToolRegistry(),
|
|
87
|
+
);
|
|
88
|
+
return { handler, events };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Extract all permissions:decision payloads from the events.emit mock. */
|
|
92
|
+
function getDecisionEvents(
|
|
93
|
+
events: ReturnType<typeof makeEvents>,
|
|
94
|
+
): PermissionDecisionEvent[] {
|
|
95
|
+
return events.emit.mock.calls
|
|
84
96
|
.filter(([channel]) => channel === PERMISSIONS_DECISION_CHANNEL)
|
|
85
97
|
.map(([, payload]) => payload as PermissionDecisionEvent);
|
|
86
98
|
}
|
|
@@ -89,18 +101,18 @@ function getDecisionEvents(deps: HandlerDeps): PermissionDecisionEvent[] {
|
|
|
89
101
|
|
|
90
102
|
describe("handleInput decision events — skill gate", () => {
|
|
91
103
|
it("does not emit when input is not a skill invocation", async () => {
|
|
92
|
-
const
|
|
93
|
-
await handleInput(
|
|
94
|
-
expect(getDecisionEvents(
|
|
104
|
+
const { handler, events } = makeHandler();
|
|
105
|
+
await handler.handleInput({ text: "hello world" }, makeCtx());
|
|
106
|
+
expect(getDecisionEvents(events)).toHaveLength(0);
|
|
95
107
|
});
|
|
96
108
|
|
|
97
109
|
it("emits allow with policy_allow for an allowed skill", async () => {
|
|
98
|
-
const
|
|
99
|
-
await handleInput(
|
|
110
|
+
const { handler, events } = makeHandler("allow");
|
|
111
|
+
await handler.handleInput({ text: "/skill:librarian" }, makeCtx());
|
|
100
112
|
|
|
101
|
-
const
|
|
102
|
-
expect(
|
|
103
|
-
expect(
|
|
113
|
+
const decisions = getDecisionEvents(events);
|
|
114
|
+
expect(decisions).toHaveLength(1);
|
|
115
|
+
expect(decisions[0]).toMatchObject({
|
|
104
116
|
surface: "skill",
|
|
105
117
|
value: "librarian",
|
|
106
118
|
result: "allow",
|
|
@@ -109,12 +121,12 @@ describe("handleInput decision events — skill gate", () => {
|
|
|
109
121
|
});
|
|
110
122
|
|
|
111
123
|
it("emits deny with policy_deny for a denied skill", async () => {
|
|
112
|
-
const
|
|
113
|
-
await handleInput(
|
|
124
|
+
const { handler, events } = makeHandler("deny");
|
|
125
|
+
await handler.handleInput({ text: "/skill:restricted" }, makeCtx());
|
|
114
126
|
|
|
115
|
-
const
|
|
116
|
-
expect(
|
|
117
|
-
expect(
|
|
127
|
+
const decisions = getDecisionEvents(events);
|
|
128
|
+
expect(decisions).toHaveLength(1);
|
|
129
|
+
expect(decisions[0]).toMatchObject({
|
|
118
130
|
surface: "skill",
|
|
119
131
|
value: "restricted",
|
|
120
132
|
result: "deny",
|
|
@@ -123,16 +135,14 @@ describe("handleInput decision events — skill gate", () => {
|
|
|
123
135
|
});
|
|
124
136
|
|
|
125
137
|
it("emits allow with user_approved when state=ask and user approves", async () => {
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
.fn()
|
|
129
|
-
.mockResolvedValue({ approved: true, state: "approved" }),
|
|
138
|
+
const { handler, events } = makeHandler("ask", {
|
|
139
|
+
prompt: vi.fn().mockResolvedValue({ approved: true, state: "approved" }),
|
|
130
140
|
});
|
|
131
|
-
await handleInput(
|
|
141
|
+
await handler.handleInput({ text: "/skill:explorer" }, makeCtx());
|
|
132
142
|
|
|
133
|
-
const
|
|
134
|
-
expect(
|
|
135
|
-
expect(
|
|
143
|
+
const decisions = getDecisionEvents(events);
|
|
144
|
+
expect(decisions).toHaveLength(1);
|
|
145
|
+
expect(decisions[0]).toMatchObject({
|
|
136
146
|
surface: "skill",
|
|
137
147
|
value: "explorer",
|
|
138
148
|
result: "allow",
|
|
@@ -141,16 +151,14 @@ describe("handleInput decision events — skill gate", () => {
|
|
|
141
151
|
});
|
|
142
152
|
|
|
143
153
|
it("emits deny with user_denied when state=ask and user denies", async () => {
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
.fn()
|
|
147
|
-
.mockResolvedValue({ approved: false, state: "denied" }),
|
|
154
|
+
const { handler, events } = makeHandler("ask", {
|
|
155
|
+
prompt: vi.fn().mockResolvedValue({ approved: false, state: "denied" }),
|
|
148
156
|
});
|
|
149
|
-
await handleInput(
|
|
157
|
+
await handler.handleInput({ text: "/skill:explorer" }, makeCtx());
|
|
150
158
|
|
|
151
|
-
const
|
|
152
|
-
expect(
|
|
153
|
-
expect(
|
|
159
|
+
const decisions = getDecisionEvents(events);
|
|
160
|
+
expect(decisions).toHaveLength(1);
|
|
161
|
+
expect(decisions[0]).toMatchObject({
|
|
154
162
|
surface: "skill",
|
|
155
163
|
value: "explorer",
|
|
156
164
|
result: "deny",
|
|
@@ -159,18 +167,17 @@ describe("handleInput decision events — skill gate", () => {
|
|
|
159
167
|
});
|
|
160
168
|
|
|
161
169
|
it("emits deny with confirmation_unavailable when state=ask but no UI", async () => {
|
|
162
|
-
const
|
|
163
|
-
|
|
170
|
+
const { handler, events } = makeHandler("ask", {
|
|
171
|
+
canPrompt: vi.fn().mockReturnValue(false),
|
|
164
172
|
});
|
|
165
|
-
await handleInput(
|
|
166
|
-
deps,
|
|
173
|
+
await handler.handleInput(
|
|
167
174
|
{ text: "/skill:explorer" },
|
|
168
175
|
makeCtx({ hasUI: false }),
|
|
169
176
|
);
|
|
170
177
|
|
|
171
|
-
const
|
|
172
|
-
expect(
|
|
173
|
-
expect(
|
|
178
|
+
const decisions = getDecisionEvents(events);
|
|
179
|
+
expect(decisions).toHaveLength(1);
|
|
180
|
+
expect(decisions[0]).toMatchObject({
|
|
174
181
|
surface: "skill",
|
|
175
182
|
value: "explorer",
|
|
176
183
|
result: "deny",
|
|
@@ -178,19 +185,19 @@ describe("handleInput decision events — skill gate", () => {
|
|
|
178
185
|
});
|
|
179
186
|
});
|
|
180
187
|
|
|
181
|
-
it("emits allow with auto_approved when
|
|
182
|
-
const
|
|
183
|
-
|
|
188
|
+
it("emits allow with auto_approved when prompt returns autoApproved:true", async () => {
|
|
189
|
+
const { handler, events } = makeHandler("ask", {
|
|
190
|
+
prompt: vi.fn().mockResolvedValue({
|
|
184
191
|
approved: true,
|
|
185
192
|
state: "approved",
|
|
186
193
|
autoApproved: true,
|
|
187
194
|
}),
|
|
188
195
|
});
|
|
189
|
-
await handleInput(
|
|
196
|
+
await handler.handleInput({ text: "/skill:explorer" }, makeCtx());
|
|
190
197
|
|
|
191
|
-
const
|
|
192
|
-
expect(
|
|
193
|
-
expect(
|
|
198
|
+
const decisions = getDecisionEvents(events);
|
|
199
|
+
expect(decisions).toHaveLength(1);
|
|
200
|
+
expect(decisions[0]).toMatchObject({
|
|
194
201
|
surface: "skill",
|
|
195
202
|
value: "explorer",
|
|
196
203
|
result: "allow",
|