@gotgenes/pi-permission-system 10.5.0 → 10.5.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 +7 -0
- package/package.json +1 -1
- package/src/handlers/before-agent-start.ts +11 -6
- package/src/handlers/lifecycle.ts +7 -4
- package/src/handlers/permission-gate-handler.ts +3 -3
- package/src/index.ts +8 -3
- package/src/permission-resolver.ts +0 -3
- package/src/permission-session.ts +8 -52
- package/src/session-rules.ts +3 -2
- package/src/skill-prompt-sanitizer.ts +1 -1
- package/test/handlers/before-agent-start.test.ts +56 -86
- package/test/handlers/external-directory-session-dedup.test.ts +79 -159
- package/test/handlers/input.test.ts +5 -4
- package/test/handlers/lifecycle.test.ts +79 -85
- package/test/handlers/tool-call.test.ts +3 -2
- package/test/helpers/handler-fixtures.ts +99 -102
- package/test/helpers/session-fixtures.ts +192 -0
- package/test/permission-resolver.test.ts +3 -1
- package/test/permission-session.test.ts +14 -198
- package/test/session-rules.test.ts +13 -5
- package/src/agent-prep-session.ts +0 -28
- package/src/gate-handler-session.ts +0 -13
- package/src/session-lifecycle-session.ts +0 -24
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Shared handler-level test fixtures for PermissionGateHandler tests.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* `makeHandler` builds a real PermissionSession + PermissionResolver and wires
|
|
5
|
+
* them into the handler and pipelines exactly as `index.ts` does.
|
|
6
|
+
* Call-site overrides for permission results flow through
|
|
7
|
+
* `permissionManager.checkPermission`; session state overrides are applied
|
|
8
|
+
* via vi.spyOn on the real session instance.
|
|
6
9
|
*/
|
|
7
10
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
8
11
|
import { vi } from "vitest";
|
|
9
12
|
|
|
10
13
|
import { GateDecisionReporter } from "#src/decision-reporter";
|
|
11
|
-
import { DEFAULT_EXTENSION_CONFIG } from "#src/extension-config";
|
|
12
|
-
import type { GateHandlerSession } from "#src/gate-handler-session";
|
|
13
14
|
import type { GatePrompter } from "#src/gate-prompter";
|
|
14
15
|
import { GateRunner } from "#src/handlers/gates/runner";
|
|
15
16
|
import {
|
|
@@ -24,32 +25,33 @@ import { PermissionGateHandler } from "#src/handlers/permission-gate-handler";
|
|
|
24
25
|
import type { PermissionDecisionEvent } from "#src/permission-events";
|
|
25
26
|
import { PERMISSIONS_DECISION_CHANNEL } from "#src/permission-events";
|
|
26
27
|
import type { Rule } from "#src/rule";
|
|
27
|
-
import type { SessionApprovalRecorder } from "#src/session-approval-recorder";
|
|
28
28
|
import type { SessionLogger } from "#src/session-logger";
|
|
29
|
-
import {
|
|
29
|
+
import { SessionRules } from "#src/session-rules";
|
|
30
30
|
import type { ToolRegistry } from "#src/tool-registry";
|
|
31
31
|
import type { PermissionCheckResult, PermissionState } from "#src/types";
|
|
32
|
+
import {
|
|
33
|
+
makeRealResolver,
|
|
34
|
+
makeRealSession,
|
|
35
|
+
} from "#test/helpers/session-fixtures";
|
|
36
|
+
|
|
37
|
+
// ── MockGateHandlerSession ────────────────────────────────────────────────
|
|
32
38
|
|
|
33
39
|
/**
|
|
34
|
-
*
|
|
40
|
+
* Mock type for gate-pipeline inputs (ToolCallGateInputs + SkillInputGateInputs).
|
|
35
41
|
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
42
|
+
* Used by `makeSurfaceCheck`, `makeBashCommandCheck`, and the `session`
|
|
43
|
+
* override bag in `makeHandler`. The `GateHandlerSession` role (activate +
|
|
44
|
+
* resolveAgentName) is now satisfied by the real `PermissionSession`; this
|
|
45
|
+
* type covers only the pipeline input surface.
|
|
40
46
|
*
|
|
41
|
-
* The 4-arg `checkPermission`
|
|
42
|
-
*
|
|
47
|
+
* The 4-arg `checkPermission` is a superset of `SkillInputGateInputs` —
|
|
48
|
+
* it routes through `permissionManager.checkPermission` in production.
|
|
43
49
|
*/
|
|
44
50
|
export type MockGateHandlerSession = ToolCallGateInputs &
|
|
45
|
-
SkillInputGateInputs &
|
|
46
|
-
|
|
47
|
-
GateHandlerSession & {
|
|
48
|
-
/** Logger source for the reporter the fixture builds. */
|
|
51
|
+
SkillInputGateInputs & {
|
|
52
|
+
/** Logger shape expected by GateDecisionReporter. */
|
|
49
53
|
logger: SessionLogger;
|
|
50
|
-
/**
|
|
51
|
-
getSessionRuleset(): Rule[];
|
|
52
|
-
/** 4-arg form so the resolve delegation can pass rules. */
|
|
54
|
+
/** 4-arg form so surface-check mocks can receive optional rules. */
|
|
53
55
|
checkPermission(
|
|
54
56
|
surface: string,
|
|
55
57
|
input: unknown,
|
|
@@ -58,6 +60,8 @@ export type MockGateHandlerSession = ToolCallGateInputs &
|
|
|
58
60
|
): PermissionCheckResult;
|
|
59
61
|
};
|
|
60
62
|
|
|
63
|
+
// ── Small utility factories ───────────────────────────────────────────────
|
|
64
|
+
|
|
61
65
|
export function makeEvents() {
|
|
62
66
|
return {
|
|
63
67
|
emit: vi.fn(),
|
|
@@ -117,58 +121,6 @@ export function makeCheckResult(
|
|
|
117
121
|
};
|
|
118
122
|
}
|
|
119
123
|
|
|
120
|
-
/**
|
|
121
|
-
* Full-intersection session stub.
|
|
122
|
-
*
|
|
123
|
-
* Uses per-field `??` selection (no spread) so TypeScript verifies every
|
|
124
|
-
* field against `MockGateHandlerSession` individually — a missing field fails
|
|
125
|
-
* `pnpm run check` instead of failing silently at runtime.
|
|
126
|
-
*
|
|
127
|
-
* Prompting is not part of this mock — pass `prompter` to `makeHandler`.
|
|
128
|
-
*/
|
|
129
|
-
export function makeSession(
|
|
130
|
-
overrides: Partial<MockGateHandlerSession> = {},
|
|
131
|
-
): MockGateHandlerSession {
|
|
132
|
-
const session: MockGateHandlerSession = {
|
|
133
|
-
logger: overrides.logger ?? {
|
|
134
|
-
debug: vi.fn(),
|
|
135
|
-
review: vi.fn(),
|
|
136
|
-
warn: vi.fn(),
|
|
137
|
-
},
|
|
138
|
-
activate: overrides.activate ?? vi.fn<MockGateHandlerSession["activate"]>(),
|
|
139
|
-
resolveAgentName:
|
|
140
|
-
overrides.resolveAgentName ??
|
|
141
|
-
vi.fn<MockGateHandlerSession["resolveAgentName"]>().mockReturnValue(null),
|
|
142
|
-
checkPermission:
|
|
143
|
-
overrides.checkPermission ??
|
|
144
|
-
vi
|
|
145
|
-
.fn<MockGateHandlerSession["checkPermission"]>()
|
|
146
|
-
.mockReturnValue(makeCheckResult()),
|
|
147
|
-
getSessionRuleset:
|
|
148
|
-
overrides.getSessionRuleset ??
|
|
149
|
-
vi.fn<MockGateHandlerSession["getSessionRuleset"]>().mockReturnValue([]),
|
|
150
|
-
recordSessionApproval:
|
|
151
|
-
overrides.recordSessionApproval ??
|
|
152
|
-
vi.fn<MockGateHandlerSession["recordSessionApproval"]>(),
|
|
153
|
-
getActiveSkillEntries:
|
|
154
|
-
overrides.getActiveSkillEntries ??
|
|
155
|
-
vi
|
|
156
|
-
.fn<MockGateHandlerSession["getActiveSkillEntries"]>()
|
|
157
|
-
.mockReturnValue([]),
|
|
158
|
-
getInfrastructureReadDirs:
|
|
159
|
-
overrides.getInfrastructureReadDirs ??
|
|
160
|
-
vi
|
|
161
|
-
.fn<MockGateHandlerSession["getInfrastructureReadDirs"]>()
|
|
162
|
-
.mockReturnValue(["/test/agent", "/test/agent/git"]),
|
|
163
|
-
getToolPreviewLimits:
|
|
164
|
-
overrides.getToolPreviewLimits ??
|
|
165
|
-
vi
|
|
166
|
-
.fn<MockGateHandlerSession["getToolPreviewLimits"]>()
|
|
167
|
-
.mockReturnValue(resolveToolPreviewLimits(DEFAULT_EXTENSION_CONFIG)),
|
|
168
|
-
};
|
|
169
|
-
return session;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
124
|
export function makeToolRegistry(
|
|
173
125
|
overrides: Partial<ToolRegistry> = {},
|
|
174
126
|
): ToolRegistry {
|
|
@@ -179,14 +131,14 @@ export function makeToolRegistry(
|
|
|
179
131
|
};
|
|
180
132
|
}
|
|
181
133
|
|
|
134
|
+
// ── Surface-check factories ────────────────────────────────────────────────
|
|
135
|
+
|
|
182
136
|
/**
|
|
183
137
|
* Surface-dispatching `checkPermission` mock.
|
|
184
138
|
*
|
|
185
|
-
*
|
|
186
|
-
*
|
|
187
|
-
*
|
|
188
|
-
* `origin: "builtin"` — callers override by including the field in the
|
|
189
|
-
* per-surface or default partial (e.g. `{ path: { state: "allow", source: "special" } }`).
|
|
139
|
+
* Returns the matching per-surface result or `defaultResult`.
|
|
140
|
+
* Pass the returned function as `session.checkPermission` in a `makeHandler`
|
|
141
|
+
* override bag — it is applied to `permissionManager.checkPermission`.
|
|
190
142
|
*
|
|
191
143
|
* Return type is intentionally unannotated so callers retain full `vi.fn()`
|
|
192
144
|
* mock access (`mock.calls`, `toHaveBeenCalledWith`, etc.).
|
|
@@ -216,9 +168,8 @@ export function makeSurfaceCheck(
|
|
|
216
168
|
/**
|
|
217
169
|
* Bash-surface `checkPermission` mock that dispatches on a command regex.
|
|
218
170
|
*
|
|
219
|
-
*
|
|
220
|
-
*
|
|
221
|
-
* plain allow result.
|
|
171
|
+
* Pass the returned function as `session.checkPermission` in a `makeHandler`
|
|
172
|
+
* override bag — it is applied to `permissionManager.checkPermission`.
|
|
222
173
|
*
|
|
223
174
|
* Return type is intentionally unannotated so callers retain full `vi.fn()`
|
|
224
175
|
* mock access.
|
|
@@ -251,24 +202,68 @@ export function makeBashCommandCheck(opts: {
|
|
|
251
202
|
});
|
|
252
203
|
}
|
|
253
204
|
|
|
205
|
+
// ── makeHandler ────────────────────────────────────────────────────────────
|
|
206
|
+
|
|
254
207
|
/**
|
|
255
|
-
* Constructs a PermissionGateHandler with
|
|
208
|
+
* Constructs a PermissionGateHandler wired with real collaborators.
|
|
256
209
|
*
|
|
257
|
-
*
|
|
258
|
-
*
|
|
210
|
+
* The `session` override bag maps to the real collaborators:
|
|
211
|
+
* - `checkPermission` → applied to `permissionManager.checkPermission`
|
|
212
|
+
* - `getActiveSkillEntries`, `getInfrastructureReadDirs`, `getToolPreviewLimits`
|
|
213
|
+
* → applied as vi.spyOn overrides on the real session
|
|
214
|
+
* - `resolveAgentName` → applied as a vi.spyOn override on the real session
|
|
259
215
|
*
|
|
260
|
-
*
|
|
261
|
-
*
|
|
216
|
+
* Returns `{ handler, events, session, toolRegistry, prompter, recorder,
|
|
217
|
+
* permissionManager, forwarding }` so each test file can destructure only
|
|
218
|
+
* what it needs.
|
|
219
|
+
* `session.activate` is not a mock — use `forwarding.start` to assert it
|
|
220
|
+
* was called.
|
|
262
221
|
*/
|
|
263
222
|
export function makeHandler(overrides?: {
|
|
264
|
-
session?: Partial<MockGateHandlerSession
|
|
223
|
+
session?: Partial<MockGateHandlerSession> & {
|
|
224
|
+
resolveAgentName?: (
|
|
225
|
+
ctx: ExtensionContext,
|
|
226
|
+
systemPrompt?: string,
|
|
227
|
+
) => string | null;
|
|
228
|
+
};
|
|
265
229
|
/** Override the GatePrompter passed to GateRunner. Defaults to an allow-all stub. */
|
|
266
230
|
prompter?: GatePrompter;
|
|
267
231
|
toolRegistry?: Partial<ToolRegistry>;
|
|
268
232
|
/** Sugar: builds the `getAll` mock from a list of tool names. */
|
|
269
233
|
tools?: string[];
|
|
270
234
|
}) {
|
|
271
|
-
const session =
|
|
235
|
+
const { session, permissionManager, sessionRules, forwarding, logger } =
|
|
236
|
+
makeRealSession();
|
|
237
|
+
const { resolver } = makeRealResolver(permissionManager, sessionRules);
|
|
238
|
+
|
|
239
|
+
// Apply session override bag to the real collaborators.
|
|
240
|
+
const so = overrides?.session;
|
|
241
|
+
if (so?.checkPermission) {
|
|
242
|
+
vi.mocked(permissionManager.checkPermission).mockImplementation(
|
|
243
|
+
so.checkPermission,
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
if (so?.getActiveSkillEntries) {
|
|
247
|
+
vi.spyOn(session, "getActiveSkillEntries").mockImplementation(
|
|
248
|
+
so.getActiveSkillEntries,
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
if (so?.getInfrastructureReadDirs) {
|
|
252
|
+
vi.spyOn(session, "getInfrastructureReadDirs").mockImplementation(
|
|
253
|
+
so.getInfrastructureReadDirs,
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
if (so?.getToolPreviewLimits) {
|
|
257
|
+
vi.spyOn(session, "getToolPreviewLimits").mockImplementation(
|
|
258
|
+
so.getToolPreviewLimits,
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
if (so?.resolveAgentName) {
|
|
262
|
+
vi.spyOn(session, "resolveAgentName").mockImplementation(
|
|
263
|
+
so.resolveAgentName,
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
|
|
272
267
|
const events = makeEvents();
|
|
273
268
|
const toolRegistry =
|
|
274
269
|
overrides?.tools !== undefined
|
|
@@ -278,27 +273,18 @@ export function makeHandler(overrides?: {
|
|
|
278
273
|
.mockReturnValue(overrides.tools.map((name) => ({ name }))),
|
|
279
274
|
})
|
|
280
275
|
: makeToolRegistry(overrides?.toolRegistry);
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
const resolver = {
|
|
284
|
-
resolve: (surface: string, input: unknown, agentName?: string) =>
|
|
285
|
-
session.checkPermission(
|
|
286
|
-
surface,
|
|
287
|
-
input,
|
|
288
|
-
agentName,
|
|
289
|
-
session.getSessionRuleset(),
|
|
290
|
-
),
|
|
291
|
-
};
|
|
276
|
+
|
|
277
|
+
const recorder = new SessionRules();
|
|
292
278
|
const pipeline = new ToolCallGatePipeline(resolver, session);
|
|
293
|
-
const skillInputPipeline = new SkillInputGatePipeline(
|
|
294
|
-
const reporter = new GateDecisionReporter(
|
|
279
|
+
const skillInputPipeline = new SkillInputGatePipeline(resolver);
|
|
280
|
+
const reporter = new GateDecisionReporter(logger, events);
|
|
295
281
|
const prompter: GatePrompter = overrides?.prompter ?? {
|
|
296
282
|
canConfirm: vi.fn().mockReturnValue(true),
|
|
297
283
|
prompt: vi
|
|
298
284
|
.fn<GatePrompter["prompt"]>()
|
|
299
285
|
.mockResolvedValue({ approved: true, state: "approved" }),
|
|
300
286
|
};
|
|
301
|
-
const runner = new GateRunner(resolver,
|
|
287
|
+
const runner = new GateRunner(resolver, recorder, prompter, reporter);
|
|
302
288
|
const handler = new PermissionGateHandler(
|
|
303
289
|
session,
|
|
304
290
|
toolRegistry,
|
|
@@ -306,9 +292,20 @@ export function makeHandler(overrides?: {
|
|
|
306
292
|
skillInputPipeline,
|
|
307
293
|
runner,
|
|
308
294
|
);
|
|
309
|
-
return {
|
|
295
|
+
return {
|
|
296
|
+
handler,
|
|
297
|
+
events,
|
|
298
|
+
session,
|
|
299
|
+
toolRegistry,
|
|
300
|
+
prompter,
|
|
301
|
+
recorder,
|
|
302
|
+
permissionManager,
|
|
303
|
+
forwarding,
|
|
304
|
+
};
|
|
310
305
|
}
|
|
311
306
|
|
|
307
|
+
// ── Decision-event helper ─────────────────────────────────────────────────
|
|
308
|
+
|
|
312
309
|
/** Extract all permissions:decision payloads from the events.emit mock. */
|
|
313
310
|
export function getDecisionEvents(
|
|
314
311
|
events: ReturnType<typeof makeEvents>,
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared real-instance test fixtures for PermissionSession and
|
|
3
|
+
* PermissionResolver.
|
|
4
|
+
*
|
|
5
|
+
* Use these instead of hand-rolling per-file mock intersection types.
|
|
6
|
+
* Build a real PermissionSession from small per-collaborator fakes so tests
|
|
7
|
+
* assert against actual behavior rather than mock contracts.
|
|
8
|
+
*
|
|
9
|
+
* Note: tests that exercise `resolveAgentName` must mock `active-agent` in
|
|
10
|
+
* their own file (the vi.hoisted / vi.mock pattern from permission-session.test.ts)
|
|
11
|
+
* since that mock is module-scoped.
|
|
12
|
+
*/
|
|
13
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
14
|
+
import { vi } from "vitest";
|
|
15
|
+
|
|
16
|
+
import type { SessionConfigStore } from "#src/config-store";
|
|
17
|
+
import { DEFAULT_EXTENSION_CONFIG } from "#src/extension-config";
|
|
18
|
+
import type { ExtensionPaths } from "#src/extension-paths";
|
|
19
|
+
import type { ForwardingController } from "#src/forwarding-manager";
|
|
20
|
+
import type { ScopedPermissionManager } from "#src/permission-manager";
|
|
21
|
+
import { PermissionResolver } from "#src/permission-resolver";
|
|
22
|
+
import { PermissionSession } from "#src/permission-session";
|
|
23
|
+
import type { PromptingGatewayLifecycle } from "#src/prompting-gateway";
|
|
24
|
+
import type { Ruleset } from "#src/rule";
|
|
25
|
+
import type { SessionLogger } from "#src/session-logger";
|
|
26
|
+
import { SessionRules } from "#src/session-rules";
|
|
27
|
+
import type { PermissionCheckResult, PermissionState } from "#src/types";
|
|
28
|
+
|
|
29
|
+
// ── Per-collaborator fake factories ────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
export function makePaths(
|
|
32
|
+
overrides: Partial<ExtensionPaths> = {},
|
|
33
|
+
): ExtensionPaths {
|
|
34
|
+
return {
|
|
35
|
+
agentDir: "/test/agent",
|
|
36
|
+
sessionsDir: "/test/agent/sessions",
|
|
37
|
+
subagentSessionsDir: "/test/agent/subagent-sessions",
|
|
38
|
+
forwardingDir: "/test/agent/sessions/permission-forwarding",
|
|
39
|
+
globalLogsDir: "/test/agent/logs",
|
|
40
|
+
piInfrastructureDirs: ["/test/agent", "/test/agent/git"],
|
|
41
|
+
...overrides,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function makeLogger(): SessionLogger {
|
|
46
|
+
return {
|
|
47
|
+
debug: vi.fn(),
|
|
48
|
+
review: vi.fn(),
|
|
49
|
+
warn: vi.fn(),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function makeConfigStore(
|
|
54
|
+
overrides: Partial<SessionConfigStore> = {},
|
|
55
|
+
): SessionConfigStore {
|
|
56
|
+
return {
|
|
57
|
+
current:
|
|
58
|
+
overrides.current ??
|
|
59
|
+
vi
|
|
60
|
+
.fn<() => typeof DEFAULT_EXTENSION_CONFIG>()
|
|
61
|
+
.mockReturnValue({ ...DEFAULT_EXTENSION_CONFIG }),
|
|
62
|
+
refresh: overrides.refresh ?? vi.fn<(ctx?: ExtensionContext) => void>(),
|
|
63
|
+
logResolvedPaths: overrides.logResolvedPaths ?? vi.fn<() => void>(),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function makeGateway(): PromptingGatewayLifecycle {
|
|
68
|
+
return {
|
|
69
|
+
activate: vi.fn<PromptingGatewayLifecycle["activate"]>(),
|
|
70
|
+
deactivate: vi.fn<PromptingGatewayLifecycle["deactivate"]>(),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function makeForwarding(): ForwardingController {
|
|
75
|
+
return {
|
|
76
|
+
start: vi.fn(),
|
|
77
|
+
stop: vi.fn(),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Fake `ScopedPermissionManager` with vi.fn() stubs.
|
|
83
|
+
*
|
|
84
|
+
* Return type is intentionally unannotated so callers retain full `vi.fn()`
|
|
85
|
+
* mock access (`mock.calls`, `toHaveBeenCalledWith`, `mockReturnValue`, etc.).
|
|
86
|
+
*/
|
|
87
|
+
export function makeFakePermissionManager() {
|
|
88
|
+
return {
|
|
89
|
+
configureForCwd: vi.fn<(cwd: string | undefined | null) => void>(),
|
|
90
|
+
checkPermission: vi
|
|
91
|
+
.fn<
|
|
92
|
+
(
|
|
93
|
+
toolName: string,
|
|
94
|
+
input: unknown,
|
|
95
|
+
agentName?: string,
|
|
96
|
+
sessionRules?: Ruleset,
|
|
97
|
+
) => PermissionCheckResult
|
|
98
|
+
>()
|
|
99
|
+
.mockReturnValue({
|
|
100
|
+
state: "allow",
|
|
101
|
+
toolName: "read",
|
|
102
|
+
source: "tool",
|
|
103
|
+
origin: "builtin",
|
|
104
|
+
}),
|
|
105
|
+
getToolPermission: vi
|
|
106
|
+
.fn<(toolName: string, agentName?: string) => PermissionState>()
|
|
107
|
+
.mockReturnValue("allow"),
|
|
108
|
+
getConfigIssues: vi.fn((): string[] => []),
|
|
109
|
+
getPolicyCacheStamp: vi.fn((): string => "stamp-1"),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ── Real-instance factories ────────────────────────────────────────────────
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Build a real PermissionSession from per-collaborator fakes.
|
|
117
|
+
*
|
|
118
|
+
* Returns the session and every collaborator so callers can destructure only
|
|
119
|
+
* what they need and assert against collaborator spies directly.
|
|
120
|
+
* The `permissionManager` is a `makeFakePermissionManager()` result unless
|
|
121
|
+
* the caller passes an explicit `ScopedPermissionManager`.
|
|
122
|
+
*/
|
|
123
|
+
export function makeRealSession(overrides?: {
|
|
124
|
+
paths?: Partial<ExtensionPaths>;
|
|
125
|
+
logger?: SessionLogger;
|
|
126
|
+
forwarding?: ForwardingController;
|
|
127
|
+
permissionManager?: ScopedPermissionManager;
|
|
128
|
+
sessionRules?: SessionRules;
|
|
129
|
+
configStore?: SessionConfigStore;
|
|
130
|
+
gateway?: PromptingGatewayLifecycle;
|
|
131
|
+
}): {
|
|
132
|
+
session: PermissionSession;
|
|
133
|
+
paths: ExtensionPaths;
|
|
134
|
+
logger: SessionLogger;
|
|
135
|
+
forwarding: ForwardingController;
|
|
136
|
+
permissionManager: ReturnType<typeof makeFakePermissionManager>;
|
|
137
|
+
sessionRules: SessionRules;
|
|
138
|
+
configStore: SessionConfigStore;
|
|
139
|
+
gateway: PromptingGatewayLifecycle;
|
|
140
|
+
} {
|
|
141
|
+
const paths = makePaths(overrides?.paths);
|
|
142
|
+
const logger = overrides?.logger ?? makeLogger();
|
|
143
|
+
const forwarding = overrides?.forwarding ?? makeForwarding();
|
|
144
|
+
const permissionManager =
|
|
145
|
+
(overrides?.permissionManager as
|
|
146
|
+
| ReturnType<typeof makeFakePermissionManager>
|
|
147
|
+
| undefined) ?? makeFakePermissionManager();
|
|
148
|
+
const sessionRules = overrides?.sessionRules ?? new SessionRules();
|
|
149
|
+
const configStore = overrides?.configStore ?? makeConfigStore();
|
|
150
|
+
const gateway = overrides?.gateway ?? makeGateway();
|
|
151
|
+
const session = new PermissionSession(
|
|
152
|
+
paths,
|
|
153
|
+
logger,
|
|
154
|
+
forwarding,
|
|
155
|
+
permissionManager,
|
|
156
|
+
sessionRules,
|
|
157
|
+
configStore,
|
|
158
|
+
gateway,
|
|
159
|
+
);
|
|
160
|
+
return {
|
|
161
|
+
session,
|
|
162
|
+
paths,
|
|
163
|
+
logger,
|
|
164
|
+
forwarding,
|
|
165
|
+
permissionManager,
|
|
166
|
+
sessionRules,
|
|
167
|
+
configStore,
|
|
168
|
+
gateway,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Build a real PermissionResolver from a fake manager and a SessionRules
|
|
174
|
+
* instance.
|
|
175
|
+
*
|
|
176
|
+
* When called with no arguments, creates a fresh fake manager and fresh
|
|
177
|
+
* SessionRules. Pass shared instances to connect the resolver to the same
|
|
178
|
+
* manager/rules used by a real session.
|
|
179
|
+
*/
|
|
180
|
+
export function makeRealResolver(
|
|
181
|
+
manager?: ReturnType<typeof makeFakePermissionManager>,
|
|
182
|
+
sessionRules?: SessionRules,
|
|
183
|
+
): {
|
|
184
|
+
resolver: PermissionResolver;
|
|
185
|
+
manager: ReturnType<typeof makeFakePermissionManager>;
|
|
186
|
+
sessionRules: SessionRules;
|
|
187
|
+
} {
|
|
188
|
+
const resolvedManager = manager ?? makeFakePermissionManager();
|
|
189
|
+
const resolvedRules = sessionRules ?? new SessionRules();
|
|
190
|
+
const resolver = new PermissionResolver(resolvedManager, resolvedRules);
|
|
191
|
+
return { resolver, manager: resolvedManager, sessionRules: resolvedRules };
|
|
192
|
+
}
|
|
@@ -82,7 +82,9 @@ describe("PermissionResolver", () => {
|
|
|
82
82
|
const { resolver } = makeResolver(pm, sessionRules);
|
|
83
83
|
|
|
84
84
|
// Record an approval directly into the shared SessionRules instance.
|
|
85
|
-
sessionRules.
|
|
85
|
+
sessionRules.recordSessionApproval(
|
|
86
|
+
SessionApproval.single("bash", "git *"),
|
|
87
|
+
);
|
|
86
88
|
resolver.resolve("bash", { command: "git status" });
|
|
87
89
|
|
|
88
90
|
const passedRules = vi.mocked(pm.checkPermission).mock.calls[0][3];
|