@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 CHANGED
@@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [10.5.1](https://github.com/gotgenes/pi-packages/compare/pi-permission-system-v10.5.0...pi-permission-system-v10.5.1) (2026-06-07)
9
+
10
+
11
+ ### Documentation
12
+
13
+ * correct SkillPermissionChecker comment after resolver rewire ([#341](https://github.com/gotgenes/pi-packages/issues/341)) ([1528382](https://github.com/gotgenes/pi-packages/commit/15283820a920fead92b348410828332b69f0a0d9))
14
+
8
15
  ## [10.5.0](https://github.com/gotgenes/pi-packages/compare/pi-permission-system-v10.4.0...pi-permission-system-v10.5.0) (2026-06-07)
9
16
 
10
17
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gotgenes/pi-permission-system",
3
- "version": "10.5.0",
3
+ "version": "10.5.1",
4
4
  "description": "Permission enforcement extension for the Pi coding agent.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -2,11 +2,12 @@ import type {
2
2
  BeforeAgentStartEventResult,
3
3
  ExtensionContext,
4
4
  } from "@earendil-works/pi-coding-agent";
5
- import type { AgentPrepSession } from "#src/agent-prep-session";
6
5
  import {
7
6
  createActiveToolsCacheKey,
8
7
  createBeforeAgentStartPromptStateKey,
9
8
  } from "#src/before-agent-start-cache";
9
+ import type { PermissionResolver } from "#src/permission-resolver";
10
+ import type { PermissionSession } from "#src/permission-session";
10
11
  import { resolveSkillPromptEntries } from "#src/skill-prompt-sanitizer";
11
12
  import { sanitizeAvailableToolsSection } from "#src/system-prompt-sanitizer";
12
13
  import { getToolNameFromValue, type ToolRegistry } from "#src/tool-registry";
@@ -35,12 +36,14 @@ export function shouldExposeTool(
35
36
  * Handles the `before_agent_start` event: tool filtering + prompt sanitization.
36
37
  *
37
38
  * Constructor deps:
38
- * - `session` — encapsulates all mutable session state
39
+ * - `session` — encapsulates all mutable session state and lifecycle operations
40
+ * - `resolver` — owns permission-query surface: `getToolPermission`, `getPolicyCacheStamp`, skill check
39
41
  * - `toolRegistry` — Pi tool API subset (getAll + setActive)
40
42
  */
41
43
  export class AgentPrepHandler {
42
44
  constructor(
43
- private readonly session: AgentPrepSession,
45
+ private readonly session: PermissionSession,
46
+ private readonly resolver: PermissionResolver,
44
47
  private readonly toolRegistry: ToolRegistry,
45
48
  ) {}
46
49
 
@@ -63,7 +66,7 @@ export class AgentPrepHandler {
63
66
  }
64
67
  if (
65
68
  shouldExposeTool(toolName, agentName, (t, a) =>
66
- this.session.getToolPermission(t, a),
69
+ this.resolver.getToolPermission(t, a),
67
70
  )
68
71
  ) {
69
72
  allowedTools.push(toolName);
@@ -79,7 +82,9 @@ export class AgentPrepHandler {
79
82
  const promptStateCacheKey = createBeforeAgentStartPromptStateKey({
80
83
  agentName,
81
84
  cwd: ctx.cwd,
82
- permissionStamp: this.session.getPolicyCacheStamp(agentName ?? undefined),
85
+ permissionStamp: this.resolver.getPolicyCacheStamp(
86
+ agentName ?? undefined,
87
+ ),
83
88
  systemPrompt: event.systemPrompt,
84
89
  allowedToolNames: allowedTools,
85
90
  });
@@ -96,7 +101,7 @@ export class AgentPrepHandler {
96
101
  );
97
102
  const skillPromptResult = resolveSkillPromptEntries(
98
103
  toolPromptResult.prompt,
99
- this.session,
104
+ this.resolver,
100
105
  agentName,
101
106
  ctx.cwd,
102
107
  );
@@ -1,7 +1,8 @@
1
1
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
2
 
3
+ import type { PermissionResolver } from "#src/permission-resolver";
4
+ import type { PermissionSession } from "#src/permission-session";
3
5
  import type { ServiceLifecycle } from "#src/service-lifecycle";
4
- import type { SessionLifecycleSession } from "#src/session-lifecycle-session";
5
6
  import { PERMISSION_SYSTEM_STATUS_KEY } from "#src/status";
6
7
 
7
8
  /** Minimal subset of SessionStartEvent used by this handler. */
@@ -18,14 +19,16 @@ interface ResourcesDiscoverPayload {
18
19
  * Handles session lifecycle events: start, reload, and shutdown.
19
20
  *
20
21
  * Constructor deps:
21
- * - `session` — encapsulates all mutable session state
22
+ * - `session` — encapsulates all mutable session state and lifecycle operations
23
+ * - `resolver` — owns permission-query surface: `getConfigIssues`
22
24
  * - `serviceLifecycle` — owns the process-global service publication;
23
25
  * `activate` publishes (skipped for registered subagent children) and emits
24
26
  * the ready event; `teardown` unsubscribes all session listeners and unpublishes
25
27
  */
26
28
  export class SessionLifecycleHandler {
27
29
  constructor(
28
- private readonly session: SessionLifecycleSession,
30
+ private readonly session: PermissionSession,
31
+ private readonly resolver: PermissionResolver,
29
32
  private readonly serviceLifecycle: ServiceLifecycle,
30
33
  ) {}
31
34
 
@@ -38,7 +41,7 @@ export class SessionLifecycleHandler {
38
41
  this.session.logResolvedConfigPaths();
39
42
 
40
43
  const agentName = this.session.resolveAgentName(ctx);
41
- const policyIssues = this.session.getConfigIssues(agentName ?? undefined);
44
+ const policyIssues = this.resolver.getConfigIssues(agentName ?? undefined);
42
45
  for (const issue of policyIssues) {
43
46
  this.session.logger.warn(issue);
44
47
  }
@@ -4,11 +4,11 @@ import type {
4
4
  } from "@earendil-works/pi-coding-agent";
5
5
 
6
6
  import { toRecord } from "#src/common";
7
- import type { GateHandlerSession } from "#src/gate-handler-session";
8
7
  import {
9
8
  formatMissingToolNameReason,
10
9
  formatUnknownToolReason,
11
10
  } from "#src/permission-prompts";
11
+ import type { PermissionSession } from "#src/permission-session";
12
12
  import {
13
13
  checkRequestedToolRegistration,
14
14
  getToolNameFromValue,
@@ -31,7 +31,7 @@ interface InputPayload {
31
31
  * Handles permission gate events: tool_call and input.
32
32
  *
33
33
  * Constructor deps:
34
- * - `session` — narrow two-method context role: bind per-event context, resolve agent name
34
+ * - `session` — state/lifecycle owner: bind per-event context, resolve agent name
35
35
  * - `toolRegistry` — Pi tool API subset (getAll + setActive)
36
36
  * - `pipeline` — owns tool-call gate-producer assembly and the run loop
37
37
  * - `skillInputPipeline` — owns skill-input gate assembly (pre-check, notify, run)
@@ -39,7 +39,7 @@ interface InputPayload {
39
39
  */
40
40
  export class PermissionGateHandler {
41
41
  constructor(
42
- private readonly session: GateHandlerSession,
42
+ private readonly session: PermissionSession,
43
43
  private readonly toolRegistry: ToolRegistry,
44
44
  private readonly pipeline: ToolCallGatePipeline,
45
45
  private readonly skillInputPipeline: SkillInputGatePipeline,
package/src/index.ts CHANGED
@@ -157,12 +157,17 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
157
157
  setActive: (names: string[]) => pi.setActiveTools(names),
158
158
  };
159
159
 
160
- const lifecycle = new SessionLifecycleHandler(session, serviceLifecycle);
161
- const agentPrep = new AgentPrepHandler(session, toolRegistry);
162
160
  const resolver = new PermissionResolver(permissionManager, sessionRules);
163
161
 
162
+ const lifecycle = new SessionLifecycleHandler(
163
+ session,
164
+ resolver,
165
+ serviceLifecycle,
166
+ );
167
+ const agentPrep = new AgentPrepHandler(session, resolver, toolRegistry);
168
+
164
169
  const reporter = new GateDecisionReporter(session.logger, pi.events);
165
- const gateRunner = new GateRunner(resolver, session, gateway, reporter);
170
+ const gateRunner = new GateRunner(resolver, sessionRules, gateway, reporter);
166
171
  const toolCallGatePipeline = new ToolCallGatePipeline(
167
172
  resolver,
168
173
  session,
@@ -67,17 +67,14 @@ export class PermissionResolver implements ScopedPermissionResolver {
67
67
  );
68
68
  }
69
69
 
70
- // fallow-ignore-next-line unused-class-member
71
70
  getToolPermission(toolName: string, agentName?: string): PermissionState {
72
71
  return this.permissionManager.getToolPermission(toolName, agentName);
73
72
  }
74
73
 
75
- // fallow-ignore-next-line unused-class-member
76
74
  getConfigIssues(agentName?: string): string[] {
77
75
  return this.permissionManager.getConfigIssues(agentName);
78
76
  }
79
77
 
80
- // fallow-ignore-next-line unused-class-member
81
78
  getPolicyCacheStamp(agentName?: string): string {
82
79
  return this.permissionManager.getPolicyCacheStamp(agentName);
83
80
  }
@@ -4,18 +4,15 @@ import {
4
4
  getActiveAgentName,
5
5
  getActiveAgentNameFromSystemPrompt,
6
6
  } from "./active-agent";
7
- import type { AgentPrepSession } from "./agent-prep-session";
7
+
8
8
  import type { SessionConfigStore } from "./config-store";
9
9
  import type { PermissionSystemExtensionConfig } from "./extension-config";
10
10
  import type { ExtensionPaths } from "./extension-paths";
11
11
  import type { ForwardingController } from "./forwarding-manager";
12
- import type { GateHandlerSession } from "./gate-handler-session";
12
+ import type { ToolCallGateInputs } from "./handlers/gates/tool-call-gate-pipeline";
13
13
  import type { ScopedPermissionManager } from "./permission-manager";
14
14
  import type { PromptingGatewayLifecycle } from "./prompting-gateway";
15
- import type { Rule } from "./rule";
16
- import type { SessionApproval } from "./session-approval";
17
- import type { SessionApprovalRecorder } from "./session-approval-recorder";
18
- import type { SessionLifecycleSession } from "./session-lifecycle-session";
15
+
19
16
  import type { SessionLogger } from "./session-logger";
20
17
  import type { SessionRules } from "./session-rules";
21
18
  import type { SkillPromptEntry } from "./skill-prompt-sanitizer";
@@ -23,7 +20,6 @@ import {
23
20
  resolveToolPreviewLimits,
24
21
  type ToolPreviewFormatterOptions,
25
22
  } from "./tool-preview-formatter";
26
- import type { PermissionCheckResult, PermissionState } from "./types";
27
23
 
28
24
  /**
29
25
  * Encapsulates all mutable session state and exposes operations instead of
@@ -40,13 +36,7 @@ import type { PermissionCheckResult, PermissionState } from "./types";
40
36
  * - `SessionConfigStore` — owns extension config; provides refresh, log, read
41
37
  * - `PromptingGatewayLifecycle` — prompting lifecycle forwarded via activate/deactivate
42
38
  */
43
- export class PermissionSession
44
- implements
45
- SessionApprovalRecorder,
46
- GateHandlerSession,
47
- AgentPrepSession,
48
- SessionLifecycleSession
49
- {
39
+ export class PermissionSession implements ToolCallGateInputs {
50
40
  private context: ExtensionContext | null = null;
51
41
  private skillEntries: SkillPromptEntry[] = [];
52
42
  private knownAgentName: string | null = null;
@@ -84,44 +74,6 @@ export class PermissionSession
84
74
  return this.context;
85
75
  }
86
76
 
87
- // ── Permission checking (delegates to PermissionManager) ───────────────
88
-
89
- checkPermission(
90
- surface: string,
91
- input: unknown,
92
- agentName?: string,
93
- sessionRules?: Rule[],
94
- ): PermissionCheckResult {
95
- return this.permissionManager.checkPermission(
96
- surface,
97
- input,
98
- agentName,
99
- sessionRules,
100
- );
101
- }
102
-
103
- getToolPermission(toolName: string, agentName?: string): PermissionState {
104
- return this.permissionManager.getToolPermission(toolName, agentName);
105
- }
106
-
107
- getConfigIssues(agentName?: string): string[] {
108
- return this.permissionManager.getConfigIssues(agentName);
109
- }
110
-
111
- getPolicyCacheStamp(agentName?: string): string {
112
- return this.permissionManager.getPolicyCacheStamp(agentName);
113
- }
114
-
115
- // ── Session rules (delegates to SessionRules) ──────────────────────────
116
-
117
- getSessionRuleset(): Rule[] {
118
- return this.sessionRules.getRuleset();
119
- }
120
-
121
- recordSessionApproval(approval: SessionApproval): void {
122
- this.sessionRules.record(approval);
123
- }
124
-
125
77
  // ── Session lifecycle ────────────────────────────────────────────────────
126
78
 
127
79
  /**
@@ -212,6 +164,10 @@ export class PermissionSession
212
164
  return this.knownAgentName;
213
165
  }
214
166
 
167
+ // Read by config-modal (`controller.session.lastKnownActiveAgentName`).
168
+ // fallow cannot trace the getter through the command's object-literal
169
+ // wiring, so it reports a false positive here.
170
+ // fallow-ignore-next-line unused-class-member
215
171
  get lastKnownActiveAgentName(): string | null {
216
172
  return this.knownAgentName;
217
173
  }
@@ -2,6 +2,7 @@ import { dirname, sep } from "node:path";
2
2
 
3
3
  import type { Ruleset } from "./rule";
4
4
  import type { SessionApproval } from "./session-approval";
5
+ import type { SessionApprovalRecorder } from "./session-approval-recorder";
5
6
 
6
7
  /**
7
8
  * Ephemeral in-memory store of session-scoped permission approvals.
@@ -11,7 +12,7 @@ import type { SessionApproval } from "./session-approval";
11
12
  *
12
13
  * Cleared on session_shutdown — never persisted to disk.
13
14
  */
14
- export class SessionRules {
15
+ export class SessionRules implements SessionApprovalRecorder {
15
16
  private rules: Ruleset = [];
16
17
 
17
18
  /** Record a wildcard pattern as approved for the given surface. */
@@ -36,7 +37,7 @@ export class SessionRules {
36
37
  * The loop lives here so callers never need to know whether an approval
37
38
  * carries one pattern or many — they just tell the store to record it.
38
39
  */
39
- record(approval: SessionApproval): void {
40
+ recordSessionApproval(approval: SessionApproval): void {
40
41
  for (const pattern of approval.patterns) {
41
42
  this.approve(approval.surface, pattern);
42
43
  }
@@ -8,7 +8,7 @@ import type { PermissionCheckResult, PermissionState } from "./types";
8
8
 
9
9
  /**
10
10
  * Narrow interface for the permission checker used by skill prompt resolution.
11
- * Both `PermissionManager` and `PermissionSession` satisfy this structurally.
11
+ * Both `PermissionManager` and `PermissionResolver` satisfy this structurally.
12
12
  */
13
13
  export interface SkillPermissionChecker {
14
14
  checkPermission(
@@ -1,6 +1,5 @@
1
1
  import { describe, expect, it, vi } from "vitest";
2
2
 
3
- import type { AgentPrepSession } from "#src/agent-prep-session";
4
3
  import {
5
4
  AgentPrepHandler,
6
5
  shouldExposeTool,
@@ -8,6 +7,10 @@ import {
8
7
  import type { ToolRegistry } from "#src/tool-registry";
9
8
 
10
9
  import { makeCheckResult, makeCtx } from "#test/helpers/handler-fixtures";
10
+ import {
11
+ makeRealResolver,
12
+ makeRealSession,
13
+ } from "#test/helpers/session-fixtures";
11
14
 
12
15
  // ── SDK stubs ──────────────────────────────────────────────────────────────
13
16
  vi.mock("@earendil-works/pi-coding-agent", async (importOriginal) => {
@@ -25,51 +28,6 @@ function makeEvent(systemPrompt = "You are an assistant.") {
25
28
  return { systemPrompt };
26
29
  }
27
30
 
28
- function makeSession(
29
- overrides: Partial<AgentPrepSession> = {},
30
- ): AgentPrepSession {
31
- return {
32
- activate: overrides.activate ?? vi.fn<AgentPrepSession["activate"]>(),
33
- refreshConfig:
34
- overrides.refreshConfig ?? vi.fn<AgentPrepSession["refreshConfig"]>(),
35
- resolveAgentName:
36
- overrides.resolveAgentName ??
37
- vi.fn<AgentPrepSession["resolveAgentName"]>().mockReturnValue(null),
38
- checkPermission:
39
- overrides.checkPermission ??
40
- vi
41
- .fn<AgentPrepSession["checkPermission"]>()
42
- .mockReturnValue(makeCheckResult()),
43
- getToolPermission:
44
- overrides.getToolPermission ??
45
- vi.fn<AgentPrepSession["getToolPermission"]>().mockReturnValue("allow"),
46
- shouldUpdateActiveTools:
47
- overrides.shouldUpdateActiveTools ??
48
- vi
49
- .fn<AgentPrepSession["shouldUpdateActiveTools"]>()
50
- .mockReturnValue(true),
51
- commitActiveToolsCacheKey:
52
- overrides.commitActiveToolsCacheKey ??
53
- vi.fn<AgentPrepSession["commitActiveToolsCacheKey"]>(),
54
- getPolicyCacheStamp:
55
- overrides.getPolicyCacheStamp ??
56
- vi
57
- .fn<AgentPrepSession["getPolicyCacheStamp"]>()
58
- .mockReturnValue("stamp-1"),
59
- shouldUpdatePromptState:
60
- overrides.shouldUpdatePromptState ??
61
- vi
62
- .fn<AgentPrepSession["shouldUpdatePromptState"]>()
63
- .mockReturnValue(true),
64
- commitPromptStateCacheKey:
65
- overrides.commitPromptStateCacheKey ??
66
- vi.fn<AgentPrepSession["commitPromptStateCacheKey"]>(),
67
- setActiveSkillEntries:
68
- overrides.setActiveSkillEntries ??
69
- vi.fn<AgentPrepSession["setActiveSkillEntries"]>(),
70
- };
71
- }
72
-
73
31
  function makeToolRegistry(overrides: Partial<ToolRegistry> = {}): ToolRegistry {
74
32
  return {
75
33
  getAll: vi.fn().mockReturnValue([]),
@@ -78,18 +36,33 @@ function makeToolRegistry(overrides: Partial<ToolRegistry> = {}): ToolRegistry {
78
36
  };
79
37
  }
80
38
 
81
- function makeHandler(overrides?: {
82
- session?: Partial<AgentPrepSession>;
39
+ function makeSetup(opts?: {
40
+ toolPermission?: "allow" | "deny" | "ask";
83
41
  toolRegistry?: Partial<ToolRegistry>;
84
- }): {
85
- handler: AgentPrepHandler;
86
- session: AgentPrepSession;
87
- toolRegistry: ToolRegistry;
88
- } {
89
- const session = makeSession(overrides?.session);
90
- const toolRegistry = makeToolRegistry(overrides?.toolRegistry);
91
- const handler = new AgentPrepHandler(session, toolRegistry);
92
- return { handler, session, toolRegistry };
42
+ }) {
43
+ const { session, permissionManager, sessionRules, configStore, forwarding } =
44
+ makeRealSession();
45
+ const { resolver } = makeRealResolver(permissionManager, sessionRules);
46
+ if (opts?.toolPermission !== undefined) {
47
+ vi.mocked(permissionManager.getToolPermission).mockReturnValue(
48
+ opts.toolPermission,
49
+ );
50
+ }
51
+ // Default checkPermission returns allow (for skill-prompt sanitizer)
52
+ vi.mocked(permissionManager.checkPermission).mockReturnValue(
53
+ makeCheckResult(),
54
+ );
55
+ const toolRegistry = makeToolRegistry(opts?.toolRegistry);
56
+ const handler = new AgentPrepHandler(session, resolver, toolRegistry);
57
+ return {
58
+ handler,
59
+ session,
60
+ resolver,
61
+ permissionManager,
62
+ configStore,
63
+ forwarding,
64
+ toolRegistry,
65
+ };
93
66
  }
94
67
 
95
68
  // ── shouldExposeTool (pure helper) ─────────────────────────────────────────
@@ -128,31 +101,30 @@ describe("shouldExposeTool", () => {
128
101
  describe("AgentPrepHandler.handle", () => {
129
102
  it("activates the session with ctx", async () => {
130
103
  const ctx = makeCtx();
131
- const { handler, session } = makeHandler();
104
+ const { handler, forwarding } = makeSetup();
132
105
  await handler.handle(makeEvent(), ctx);
133
- expect(session.activate).toHaveBeenCalledWith(ctx);
106
+ // Real session.activate calls forwarding.start
107
+ expect(forwarding.start).toHaveBeenCalledWith(ctx);
134
108
  });
135
109
 
136
110
  it("refreshes config with ctx", async () => {
137
111
  const ctx = makeCtx();
138
- const { handler, session } = makeHandler();
112
+ const { handler, configStore } = makeSetup();
139
113
  await handler.handle(makeEvent(), ctx);
140
- expect(session.refreshConfig).toHaveBeenCalledWith(ctx);
114
+ expect(configStore.refresh).toHaveBeenCalledWith(ctx);
141
115
  });
142
116
 
143
117
  it("resolves agent name using systemPrompt", async () => {
144
118
  const ctx = makeCtx();
145
- const { handler, session } = makeHandler();
119
+ const { handler, session } = makeSetup();
120
+ const spy = vi.spyOn(session, "resolveAgentName");
146
121
  await handler.handle(makeEvent("<active_agent name='x'>"), ctx);
147
- expect(session.resolveAgentName).toHaveBeenCalledWith(
148
- ctx,
149
- "<active_agent name='x'>",
150
- );
122
+ expect(spy).toHaveBeenCalledWith(ctx, "<active_agent name='x'>");
151
123
  });
152
124
 
153
125
  it("filters out denied tools from allowed list", async () => {
154
- const { handler, toolRegistry } = makeHandler({
155
- session: { getToolPermission: vi.fn().mockReturnValue("deny") },
126
+ const { handler, toolRegistry } = makeSetup({
127
+ toolPermission: "deny",
156
128
  toolRegistry: {
157
129
  getAll: vi.fn().mockReturnValue([{ name: "write" }, { name: "read" }]),
158
130
  },
@@ -162,7 +134,7 @@ describe("AgentPrepHandler.handle", () => {
162
134
  });
163
135
 
164
136
  it("includes allowed and ask tools in the active list", async () => {
165
- const { handler, toolRegistry } = makeHandler({
137
+ const { handler, toolRegistry } = makeSetup({
166
138
  toolRegistry: {
167
139
  getAll: vi.fn().mockReturnValue([{ name: "read" }, { name: "write" }]),
168
140
  },
@@ -172,60 +144,58 @@ describe("AgentPrepHandler.handle", () => {
172
144
  });
173
145
 
174
146
  it("commits active-tools cache key after applying", async () => {
175
- const { handler, session } = makeHandler({
147
+ const { handler, session } = makeSetup({
176
148
  toolRegistry: {
177
149
  getAll: vi.fn().mockReturnValue([{ name: "read" }]),
178
150
  },
179
151
  });
152
+ const spy = vi.spyOn(session, "commitActiveToolsCacheKey");
180
153
  await handler.handle(makeEvent(), makeCtx());
181
- expect(session.commitActiveToolsCacheKey).toHaveBeenCalled();
154
+ expect(spy).toHaveBeenCalled();
182
155
  });
183
156
 
184
157
  it("skips setActive when cache key is unchanged", async () => {
185
- const { handler, session, toolRegistry } = makeHandler({
186
- session: { shouldUpdateActiveTools: vi.fn().mockReturnValue(false) },
158
+ const { handler, session, toolRegistry } = makeSetup({
187
159
  toolRegistry: {
188
160
  getAll: vi.fn().mockReturnValue([{ name: "read" }]),
189
161
  },
190
162
  });
163
+ vi.spyOn(session, "shouldUpdateActiveTools").mockReturnValue(false);
191
164
  await handler.handle(makeEvent(), makeCtx());
192
165
  expect(toolRegistry.setActive).not.toHaveBeenCalled();
193
- expect(session.commitActiveToolsCacheKey).not.toHaveBeenCalled();
194
166
  });
195
167
 
196
168
  it("returns empty object when prompt cache is unchanged", async () => {
197
- const { handler, session } = makeHandler({
198
- session: { shouldUpdatePromptState: vi.fn().mockReturnValue(false) },
199
- });
169
+ const { handler, session } = makeSetup();
170
+ vi.spyOn(session, "shouldUpdatePromptState").mockReturnValue(false);
200
171
  const result = await handler.handle(makeEvent(), makeCtx());
201
172
  expect(result).toEqual({});
202
- expect(session.commitPromptStateCacheKey).not.toHaveBeenCalled();
203
173
  });
204
174
 
205
175
  it("commits prompt-state cache key and processes prompt when cache is new", async () => {
206
- const { handler, session } = makeHandler();
176
+ const { handler, session } = makeSetup();
177
+ const spy = vi.spyOn(session, "commitPromptStateCacheKey");
207
178
  await handler.handle(makeEvent(), makeCtx());
208
- expect(session.commitPromptStateCacheKey).toHaveBeenCalled();
179
+ expect(spy).toHaveBeenCalled();
209
180
  });
210
181
 
211
182
  it("stores resolved skill entries on the session", async () => {
212
- const { handler, session } = makeHandler();
183
+ const { handler, session } = makeSetup();
184
+ const spy = vi.spyOn(session, "setActiveSkillEntries");
213
185
  await handler.handle(makeEvent(), makeCtx());
214
- expect(session.setActiveSkillEntries).toHaveBeenCalledWith(
215
- expect.any(Array),
216
- );
186
+ expect(spy).toHaveBeenCalledWith(expect.any(Array));
217
187
  });
218
188
 
219
189
  it("returns modified systemPrompt when prompt changes", async () => {
220
190
  const systemPrompt = `You are an assistant.\n\nAvailable tools:\n- read\n- write\n`;
221
- const { handler } = makeHandler();
191
+ const { handler } = makeSetup();
222
192
  const result = await handler.handle(makeEvent(systemPrompt), makeCtx());
223
193
  expect(result).toHaveProperty("systemPrompt");
224
194
  });
225
195
 
226
196
  it("returns empty object when systemPrompt is unchanged", async () => {
227
197
  const prompt = "No tools section here.";
228
- const { handler } = makeHandler();
198
+ const { handler } = makeSetup();
229
199
  const result = await handler.handle(makeEvent(prompt), makeCtx());
230
200
  expect(result).toEqual({});
231
201
  });