@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.
@@ -1,15 +1,16 @@
1
1
  /**
2
2
  * Shared handler-level test fixtures for PermissionGateHandler tests.
3
3
  *
4
- * All factories use override bags so callers can specialize any field
5
- * without constructing the full object from scratch.
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 { resolveToolPreviewLimits } from "#src/tool-preview-formatter";
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
- * Precise mock boundary for PermissionGateHandler integration tests.
40
+ * Mock type for gate-pipeline inputs (ToolCallGateInputs + SkillInputGateInputs).
35
41
  *
36
- * Intersection of every role the handler and its collaborators require.
37
- * Prompting is not included here — it moved to `PromptingGateway` (#339).
38
- * Pass a `prompter` override to `makeHandler` to steer GateRunner's prompting
39
- * role; `makeHandler` creates a clean default prompter when none is supplied.
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` overrides the 3-arg version from
42
- * GateHandlerSession so the `resolve` delegation can forward session rules.
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
- SessionApprovalRecorder &
47
- GateHandlerSession & {
48
- /** Logger source for the reporter the fixture builds. */
51
+ SkillInputGateInputs & {
52
+ /** Logger shape expected by GateDecisionReporter. */
49
53
  logger: SessionLogger;
50
- /** Session-rule accessor used by the resolve delegation. */
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
- * Builds a `vi.fn()` that returns a `PermissionCheckResult` for each surface,
186
- * using `bySurface[surface]` when matched and `defaultResult` otherwise.
187
- * Default fields: `toolName` = the surface string, `source: "tool"`,
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
- * For the `bash` surface: returns a deny result when `opts.deny` matches the
220
- * command, and an allow result otherwise. For all other surfaces, returns a
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 mocked collaborators.
208
+ * Constructs a PermissionGateHandler wired with real collaborators.
256
209
  *
257
- * Returns all collaborators so each test file can destructure only what
258
- * it needs handler, events, session, toolRegistry, and prompter are all available.
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
- * The default prompter approves all requests. Pass `prompter` explicitly to
261
- * steer canConfirm/prompt behavior for the test.
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 = makeSession(overrides?.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
- // Resolver delegates to session's checkPermission + getSessionRuleset —
282
- // overriding session.checkPermission steers resolve automatically.
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(session);
294
- const reporter = new GateDecisionReporter(session.logger, events);
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, session, prompter, reporter);
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 { handler, events, session, toolRegistry, prompter };
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.record(SessionApproval.single("bash", "git *"));
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];