@gotgenes/pi-permission-system 5.9.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.
@@ -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 { SessionState } from "../../src/runtime";
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(state: "allow" | "deny" | "ask" = "allow"): SessionState {
42
+ function makeSession(
43
+ state: "allow" | "deny" | "ask" = "allow",
44
+ ): PermissionSession {
42
45
  return {
43
- runtimeContext: null,
44
- permissionManager: {
45
- checkPermission: vi.fn().mockReturnValue({
46
- state,
47
- toolName: "skill",
48
- source: "skill",
49
- origin: "global",
50
- matchedPattern: "*",
51
- }),
52
- } as unknown as SessionState["permissionManager"],
53
- activeSkillEntries: [],
54
- lastKnownActiveAgentName: null,
55
- lastActiveToolsCacheKey: null,
56
- lastPromptStateCacheKey: null,
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,20 +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
- forwarding: { start: vi.fn(), stop: vi.fn() },
85
74
  stopPermissionRpcHandlers: vi.fn(),
86
75
  getAllTools: vi.fn().mockReturnValue([]),
87
76
  setActiveTools: vi.fn(),
@@ -191,7 +180,6 @@ describe("handleInput decision events — skill gate", () => {
191
180
 
192
181
  it("emits allow with auto_approved when promptPermission returns autoApproved:true", async () => {
193
182
  const deps = makeDeps("ask", {
194
- // Simulate what PermissionPrompter returns in yolo mode
195
183
  promptPermission: vi.fn().mockResolvedValue({
196
184
  approved: true,
197
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 { SessionState } from "../../src/runtime";
10
- import type { SkillPromptEntry } from "../../src/skill-prompt-sanitizer";
9
+ import type { PermissionSession } from "../../src/permission-session";
10
+ import type { PermissionState } from "../../src/types";
11
11
 
12
12
  // ── helpers ────────────────────────────────────────────────────────────────
13
13
 
@@ -34,42 +34,30 @@ function makeInputEvent(text: string) {
34
34
  return { text };
35
35
  }
36
36
 
37
- function makeSession(overrides: Partial<SessionState> = {}): SessionState {
37
+ function makeSession(
38
+ overrides: Partial<Record<keyof PermissionSession, unknown>> = {},
39
+ ): PermissionSession {
38
40
  return {
39
- runtimeContext: null,
40
- permissionManager: {
41
- checkPermission: vi.fn().mockReturnValue({ state: "allow" }),
42
- } as unknown as SessionState["permissionManager"],
43
- activeSkillEntries: [] as SkillPromptEntry[],
44
- lastKnownActiveAgentName: null,
45
- lastActiveToolsCacheKey: null,
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
- logger: { debug: vi.fn(), review: vi.fn(), warn: vi.fn() },
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
- forwarding: { start: vi.fn(), stop: vi.fn() },
73
61
  stopPermissionRpcHandlers: vi.fn(),
74
62
  getAllTools: vi.fn().mockReturnValue([]),
75
63
  setActiveTools: vi.fn(),
@@ -114,18 +102,11 @@ describe("extractSkillNameFromInput", () => {
114
102
  // ── handleInput ───────────────────────────────────────────────────────────
115
103
 
116
104
  describe("handleInput", () => {
117
- it("sets runtime context", async () => {
105
+ it("activates session with ctx", async () => {
118
106
  const ctx = makeCtx();
119
107
  const deps = makeDeps();
120
108
  await handleInput(deps, makeInputEvent("hello"), ctx);
121
- expect(deps.session.runtimeContext).toBe(ctx);
122
- });
123
-
124
- it("starts forwarded permission polling", async () => {
125
- const ctx = makeCtx();
126
- const deps = makeDeps();
127
- await handleInput(deps, makeInputEvent("hello"), ctx);
128
- expect(deps.forwarding.start).toHaveBeenCalledWith(ctx);
109
+ expect(deps.session.activate).toHaveBeenCalledWith(ctx);
129
110
  });
130
111
 
131
112
  it("returns continue for non-skill input", async () => {
@@ -141,14 +122,11 @@ describe("handleInput", () => {
141
122
  it("does not check permissions for non-skill input", async () => {
142
123
  const deps = makeDeps();
143
124
  await handleInput(deps, makeInputEvent("just a message"), makeCtx());
144
- expect(
145
- deps.session.permissionManager.checkPermission,
146
- ).not.toHaveBeenCalled();
125
+ expect(deps.session.checkPermission).not.toHaveBeenCalled();
147
126
  });
148
127
 
149
128
  it("returns continue when skill is allowed", async () => {
150
129
  const deps = makeDeps();
151
- // default makeRuntime() has checkPermission → { state: "allow" }
152
130
  const result = await handleInput(
153
131
  deps,
154
132
  makeInputEvent("/skill:librarian"),
@@ -158,14 +136,10 @@ describe("handleInput", () => {
158
136
  });
159
137
 
160
138
  it("returns handled when skill is denied", async () => {
161
- const pm = {
139
+ const session = makeSession({
162
140
  checkPermission: vi.fn().mockReturnValue({ state: "deny" }),
163
- };
164
- const deps = makeDeps({
165
- session: makeSession({
166
- permissionManager: pm as unknown as SessionState["permissionManager"],
167
- }),
168
141
  });
142
+ const deps = makeDeps({ session });
169
143
  const result = await handleInput(
170
144
  deps,
171
145
  makeInputEvent("/skill:librarian"),
@@ -176,14 +150,10 @@ describe("handleInput", () => {
176
150
 
177
151
  it("shows a warning notification when skill is denied and UI is available", async () => {
178
152
  const ctx = makeCtx({ hasUI: true });
179
- const pm = {
153
+ const session = makeSession({
180
154
  checkPermission: vi.fn().mockReturnValue({ state: "deny" }),
181
- };
182
- const deps = makeDeps({
183
- session: makeSession({
184
- permissionManager: pm as unknown as SessionState["permissionManager"],
185
- }),
186
155
  });
156
+ const deps = makeDeps({ session });
187
157
  await handleInput(deps, makeInputEvent("/skill:librarian"), ctx);
188
158
  expect(ctx.ui.notify).toHaveBeenCalledWith(
189
159
  expect.stringContaining("librarian"),
@@ -193,22 +163,20 @@ describe("handleInput", () => {
193
163
 
194
164
  it("does not show a warning notification when skill is denied and UI is absent", async () => {
195
165
  const ctx = makeCtx({ hasUI: false });
196
- const pm = { checkPermission: vi.fn().mockReturnValue({ state: "deny" }) };
197
- const deps = makeDeps({
198
- session: makeSession({
199
- permissionManager: pm as unknown as SessionState["permissionManager"],
200
- }),
166
+ const session = makeSession({
167
+ checkPermission: vi.fn().mockReturnValue({ state: "deny" }),
201
168
  });
169
+ const deps = makeDeps({ session });
202
170
  await handleInput(deps, makeInputEvent("/skill:librarian"), ctx);
203
171
  expect(ctx.ui.notify).not.toHaveBeenCalled();
204
172
  });
205
173
 
206
174
  it("returns handled when skill requires approval but no UI is available", async () => {
207
- const pm = { checkPermission: vi.fn().mockReturnValue({ state: "ask" }) };
175
+ const session = makeSession({
176
+ checkPermission: vi.fn().mockReturnValue({ state: "ask" }),
177
+ });
208
178
  const deps = makeDeps({
209
- session: makeSession({
210
- permissionManager: pm as unknown as SessionState["permissionManager"],
211
- }),
179
+ session,
212
180
  canRequestPermissionConfirmation: vi.fn().mockReturnValue(false),
213
181
  });
214
182
  const result = await handleInput(
@@ -220,11 +188,11 @@ describe("handleInput", () => {
220
188
  });
221
189
 
222
190
  it("prompts and returns continue when skill ask is approved", async () => {
223
- const pm = { checkPermission: vi.fn().mockReturnValue({ state: "ask" }) };
191
+ const session = makeSession({
192
+ checkPermission: vi.fn().mockReturnValue({ state: "ask" }),
193
+ });
224
194
  const deps = makeDeps({
225
- session: makeSession({
226
- permissionManager: pm as unknown as SessionState["permissionManager"],
227
- }),
195
+ session,
228
196
  canRequestPermissionConfirmation: vi.fn().mockReturnValue(true),
229
197
  promptPermission: vi
230
198
  .fn()
@@ -240,11 +208,11 @@ describe("handleInput", () => {
240
208
  });
241
209
 
242
210
  it("returns handled when skill ask is denied by user", async () => {
243
- const pm = { checkPermission: vi.fn().mockReturnValue({ state: "ask" }) };
211
+ const session = makeSession({
212
+ checkPermission: vi.fn().mockReturnValue({ state: "ask" }),
213
+ });
244
214
  const deps = makeDeps({
245
- session: makeSession({
246
- permissionManager: pm as unknown as SessionState["permissionManager"],
247
- }),
215
+ session,
248
216
  canRequestPermissionConfirmation: vi.fn().mockReturnValue(true),
249
217
  promptPermission: vi
250
218
  .fn()
@@ -259,12 +227,12 @@ describe("handleInput", () => {
259
227
  });
260
228
 
261
229
  it("passes agentName in the prompt permission request", async () => {
262
- const pm = { checkPermission: vi.fn().mockReturnValue({ state: "ask" }) };
263
- const deps = makeDeps({
264
- session: makeSession({
265
- permissionManager: pm as unknown as SessionState["permissionManager"],
266
- }),
230
+ const session = makeSession({
231
+ checkPermission: vi.fn().mockReturnValue({ state: "ask" }),
267
232
  resolveAgentName: vi.fn().mockReturnValue("code-agent"),
233
+ });
234
+ const deps = makeDeps({
235
+ session,
268
236
  canRequestPermissionConfirmation: vi.fn().mockReturnValue(true),
269
237
  promptPermission: vi
270
238
  .fn()
@@ -6,24 +6,9 @@ import {
6
6
  handleSessionStart,
7
7
  } from "../../src/handlers/lifecycle";
8
8
  import type { HandlerDeps } from "../../src/handlers/types";
9
- import type { PermissionManager } from "../../src/permission-manager";
10
- import type { SessionState } from "../../src/runtime";
11
- import type { SessionRules } from "../../src/session-rules";
12
- import type { SkillPromptEntry } from "../../src/skill-prompt-sanitizer";
9
+ import type { PermissionSession } from "../../src/permission-session";
13
10
 
14
- // ── active-agent stub ──────────────────────────────────────────────────────
15
- const { mockGetActiveAgentName } = vi.hoisted(() => ({
16
- mockGetActiveAgentName: vi.fn<(ctx: ExtensionContext) => string | null>(),
17
- }));
18
-
19
- vi.mock("../../src/active-agent", () => ({
20
- getActiveAgentName: mockGetActiveAgentName,
21
- getActiveAgentNameFromSystemPrompt: vi.fn().mockReturnValue(null),
22
- }));
23
-
24
- // ── PERMISSION_SYSTEM_STATUS_KEY stub ──────────────────────────────────────
25
- // status.ts is re-exported through the handler; the key value doesn't matter
26
- // for these tests.
11
+ // ── status stub ────────────────────────────────────────────────────────────
27
12
  vi.mock("../../src/status", () => ({
28
13
  PERMISSION_SYSTEM_STATUS_KEY: "permission-system",
29
14
  syncPermissionSystemStatus: vi.fn(),
@@ -51,54 +36,32 @@ function makeCtx(overrides: Partial<ExtensionContext> = {}): ExtensionContext {
51
36
  } as unknown as ExtensionContext;
52
37
  }
53
38
 
54
- function makePermissionManager(
55
- issues: string[] = [],
56
- ): Pick<PermissionManager, "getConfigIssues"> {
57
- return {
58
- getConfigIssues: vi.fn().mockReturnValue(issues),
59
- };
60
- }
61
-
62
- function makeSessionRules(): SessionRules {
63
- return {
64
- approve: vi.fn(),
65
- getRuleset: vi.fn().mockReturnValue([]),
66
- clear: vi.fn(),
67
- } as unknown as SessionRules;
68
- }
69
-
70
- function makeSession(overrides: Partial<SessionState> = {}): SessionState {
39
+ function makeSession(
40
+ overrides: Partial<Record<keyof PermissionSession, unknown>> = {},
41
+ ): PermissionSession {
71
42
  return {
72
- runtimeContext: null,
73
- permissionManager: makePermissionManager() as unknown as PermissionManager,
74
- activeSkillEntries: [] as SkillPromptEntry[],
75
- lastKnownActiveAgentName: null,
76
- lastActiveToolsCacheKey: null,
77
- lastPromptStateCacheKey: null,
78
- sessionRules: makeSessionRules(),
43
+ logger: { debug: vi.fn(), review: vi.fn(), warn: vi.fn() },
44
+ refreshConfig: vi.fn(),
45
+ resetForNewSession: vi.fn(),
46
+ logResolvedConfigPaths: vi.fn(),
47
+ resolveAgentName: vi.fn().mockReturnValue(null),
48
+ getConfigIssues: vi.fn().mockReturnValue([]),
49
+ reload: vi.fn(),
50
+ getRuntimeContext: vi.fn().mockReturnValue(null),
51
+ shutdown: vi.fn(),
79
52
  ...overrides,
80
- };
53
+ } as unknown as PermissionSession;
81
54
  }
82
55
 
83
56
  function makeDeps(overrides: Partial<HandlerDeps> = {}): HandlerDeps {
84
57
  return {
85
58
  session: makeSession(),
86
- logger: { debug: vi.fn(), review: vi.fn(), warn: vi.fn() },
87
- piInfrastructureDirs: ["/test/agent", "/test/agent/git"],
88
- getPiInfrastructureReadPaths: vi.fn().mockReturnValue([]),
89
- createPermissionManagerForCwd: vi
90
- .fn()
91
- .mockReturnValue(makePermissionManager()),
92
- refreshExtensionConfig: vi.fn(),
93
- logResolvedConfigPaths: vi.fn(),
94
- resolveAgentName: vi.fn().mockReturnValue(null),
59
+ events: { emit: vi.fn(), on: vi.fn().mockReturnValue(() => undefined) },
95
60
  canRequestPermissionConfirmation: vi.fn().mockReturnValue(false),
96
61
  promptPermission: vi
97
62
  .fn()
98
63
  .mockResolvedValue({ approved: true, state: "approved" }),
99
64
  createPermissionRequestId: vi.fn().mockReturnValue("test-id"),
100
- events: { emit: vi.fn(), on: vi.fn().mockReturnValue(() => undefined) },
101
- forwarding: { start: vi.fn(), stop: vi.fn() },
102
65
  stopPermissionRpcHandlers: vi.fn(),
103
66
  getAllTools: vi.fn().mockReturnValue([]),
104
67
  setActiveTools: vi.fn(),
@@ -109,98 +72,54 @@ function makeDeps(overrides: Partial<HandlerDeps> = {}): HandlerDeps {
109
72
  // ── handleSessionStart ─────────────────────────────────────────────────────
110
73
 
111
74
  describe("handleSessionStart", () => {
112
- beforeEach(() => {
113
- mockGetActiveAgentName.mockReset();
114
- mockGetActiveAgentName.mockReturnValue(null);
115
- });
116
-
117
- it("sets the runtime context", async () => {
118
- const ctx = makeCtx();
119
- const deps = makeDeps();
120
- await handleSessionStart(deps, { reason: "startup" }, ctx);
121
- expect(deps.session.runtimeContext).toBe(ctx);
122
- });
123
-
124
- it("refreshes extension config with ctx", async () => {
125
- const ctx = makeCtx();
126
- const deps = makeDeps();
127
- await handleSessionStart(deps, { reason: "startup" }, ctx);
128
- expect(deps.refreshExtensionConfig).toHaveBeenCalledWith(ctx);
129
- });
130
-
131
- it("creates a new permission manager for ctx.cwd and stores it", async () => {
132
- const ctx = makeCtx({ cwd: "/my/project" });
133
- const newPm = makePermissionManager();
134
- const deps = makeDeps({
135
- createPermissionManagerForCwd: vi.fn().mockReturnValue(newPm),
136
- });
137
- await handleSessionStart(deps, { reason: "startup" }, ctx);
138
- expect(deps.createPermissionManagerForCwd).toHaveBeenCalledWith(
139
- "/my/project",
140
- );
141
- expect(deps.session.permissionManager).toBe(newPm);
142
- });
143
-
144
- it("clears the before_agent_start cache", async () => {
145
- const ctx = makeCtx();
146
- const deps = makeDeps();
147
- await handleSessionStart(deps, { reason: "startup" }, ctx);
148
- expect(deps.session.activeSkillEntries).toEqual([]);
149
- expect(deps.session.lastActiveToolsCacheKey).toBeNull();
150
- expect(deps.session.lastPromptStateCacheKey).toBeNull();
151
- });
152
-
153
- it("sets lastKnownActiveAgentName from getActiveAgentName", async () => {
154
- mockGetActiveAgentName.mockReturnValue("my-agent");
75
+ it("refreshes config with ctx", async () => {
155
76
  const ctx = makeCtx();
156
77
  const deps = makeDeps();
157
78
  await handleSessionStart(deps, { reason: "startup" }, ctx);
158
- expect(deps.session.lastKnownActiveAgentName).toBe("my-agent");
79
+ expect(deps.session.refreshConfig).toHaveBeenCalledWith(ctx);
159
80
  });
160
81
 
161
- it("sets lastKnownActiveAgentName to null when no agent is active", async () => {
162
- mockGetActiveAgentName.mockReturnValue(null);
82
+ it("calls resetForNewSession with ctx", async () => {
163
83
  const ctx = makeCtx();
164
84
  const deps = makeDeps();
165
85
  await handleSessionStart(deps, { reason: "startup" }, ctx);
166
- expect(deps.session.lastKnownActiveAgentName).toBeNull();
86
+ expect(deps.session.resetForNewSession).toHaveBeenCalledWith(ctx);
167
87
  });
168
88
 
169
- it("starts forwarded permission polling", async () => {
170
- const ctx = makeCtx();
89
+ it("logs resolved config paths", async () => {
171
90
  const deps = makeDeps();
172
- await handleSessionStart(deps, { reason: "startup" }, ctx);
173
- expect(deps.forwarding.start).toHaveBeenCalledWith(ctx);
91
+ await handleSessionStart(deps, { reason: "startup" }, makeCtx());
92
+ expect(deps.session.logResolvedConfigPaths).toHaveBeenCalledOnce();
174
93
  });
175
94
 
176
- it("logs resolved config paths", async () => {
95
+ it("resolves agent name from ctx", async () => {
177
96
  const ctx = makeCtx();
178
97
  const deps = makeDeps();
179
98
  await handleSessionStart(deps, { reason: "startup" }, ctx);
180
- expect(deps.logResolvedConfigPaths).toHaveBeenCalledOnce();
99
+ expect(deps.session.resolveAgentName).toHaveBeenCalledWith(ctx);
181
100
  });
182
101
 
183
102
  it("notifies each policy issue", async () => {
184
- const pm = makePermissionManager(["issue A", "issue B"]);
185
- const deps = makeDeps({
186
- createPermissionManagerForCwd: vi.fn().mockReturnValue(pm),
103
+ const session = makeSession({
104
+ getConfigIssues: vi.fn().mockReturnValue(["issue A", "issue B"]),
187
105
  });
106
+ const deps = makeDeps({ session });
188
107
  await handleSessionStart(deps, { reason: "startup" }, makeCtx());
189
- expect(deps.logger.warn).toHaveBeenCalledWith("issue A");
190
- expect(deps.logger.warn).toHaveBeenCalledWith("issue B");
108
+ expect(session.logger.warn).toHaveBeenCalledWith("issue A");
109
+ expect(session.logger.warn).toHaveBeenCalledWith("issue B");
191
110
  });
192
111
 
193
- it("does not call notifyWarning when there are no policy issues", async () => {
112
+ it("does not warn when there are no policy issues", async () => {
194
113
  const deps = makeDeps();
195
114
  await handleSessionStart(deps, { reason: "startup" }, makeCtx());
196
- expect(deps.logger.warn).not.toHaveBeenCalled();
115
+ expect(deps.session.logger.warn).not.toHaveBeenCalled();
197
116
  });
198
117
 
199
118
  it("writes lifecycle.reload debug log when reason is reload", async () => {
200
119
  const ctx = makeCtx({ cwd: "/proj" });
201
120
  const deps = makeDeps();
202
121
  await handleSessionStart(deps, { reason: "reload" }, ctx);
203
- expect(deps.logger.debug).toHaveBeenCalledWith("lifecycle.reload", {
122
+ expect(deps.session.logger.debug).toHaveBeenCalledWith("lifecycle.reload", {
204
123
  triggeredBy: "session_start",
205
124
  reason: "reload",
206
125
  cwd: "/proj",
@@ -210,7 +129,18 @@ describe("handleSessionStart", () => {
210
129
  it("does not write lifecycle.reload debug log for non-reload reasons", async () => {
211
130
  const deps = makeDeps();
212
131
  await handleSessionStart(deps, { reason: "startup" }, makeCtx());
213
- expect(deps.logger.debug).not.toHaveBeenCalled();
132
+ expect(deps.session.logger.debug).not.toHaveBeenCalled();
133
+ });
134
+
135
+ it("calls refreshConfig before resetForNewSession", async () => {
136
+ const callOrder: string[] = [];
137
+ const session = makeSession({
138
+ refreshConfig: vi.fn(() => callOrder.push("refreshConfig")),
139
+ resetForNewSession: vi.fn(() => callOrder.push("resetForNewSession")),
140
+ });
141
+ const deps = makeDeps({ session });
142
+ await handleSessionStart(deps, { reason: "startup" }, makeCtx());
143
+ expect(callOrder).toEqual(["refreshConfig", "resetForNewSession"]);
214
144
  });
215
145
  });
216
146
 
@@ -220,43 +150,23 @@ describe("handleResourcesDiscover", () => {
220
150
  it("does nothing when reason is not reload", async () => {
221
151
  const deps = makeDeps();
222
152
  await handleResourcesDiscover(deps, { reason: "startup" });
223
- expect(deps.createPermissionManagerForCwd).not.toHaveBeenCalled();
224
- expect(deps.logger.debug).not.toHaveBeenCalled();
225
- });
226
-
227
- it("creates and stores a new PM using runtimeContext.cwd on reload", async () => {
228
- const ctx = makeCtx({ cwd: "/runtime/cwd" });
229
- const newPm = makePermissionManager();
230
- const deps = makeDeps({
231
- session: makeSession({ runtimeContext: ctx }),
232
- createPermissionManagerForCwd: vi.fn().mockReturnValue(newPm),
233
- });
234
- await handleResourcesDiscover(deps, { reason: "reload" });
235
- expect(deps.createPermissionManagerForCwd).toHaveBeenCalledWith(
236
- "/runtime/cwd",
237
- );
238
- expect(deps.session.permissionManager).toBe(newPm);
153
+ expect(deps.session.reload).not.toHaveBeenCalled();
239
154
  });
240
155
 
241
- it("uses undefined cwd when runtimeContext is null on reload", async () => {
156
+ it("calls reload on the session on reload", async () => {
242
157
  const deps = makeDeps();
243
158
  await handleResourcesDiscover(deps, { reason: "reload" });
244
- expect(deps.createPermissionManagerForCwd).toHaveBeenCalledWith(undefined);
245
- });
246
-
247
- it("clears the before_agent_start cache on reload", async () => {
248
- const deps = makeDeps();
249
- await handleResourcesDiscover(deps, { reason: "reload" });
250
- expect(deps.session.activeSkillEntries).toEqual([]);
251
- expect(deps.session.lastActiveToolsCacheKey).toBeNull();
252
- expect(deps.session.lastPromptStateCacheKey).toBeNull();
159
+ expect(deps.session.reload).toHaveBeenCalledOnce();
253
160
  });
254
161
 
255
162
  it("writes lifecycle.reload debug log on reload", async () => {
256
163
  const ctx = makeCtx({ cwd: "/proj" });
257
- const deps = makeDeps({ session: makeSession({ runtimeContext: ctx }) });
164
+ const session = makeSession({
165
+ getRuntimeContext: vi.fn().mockReturnValue(ctx),
166
+ });
167
+ const deps = makeDeps({ session });
258
168
  await handleResourcesDiscover(deps, { reason: "reload" });
259
- expect(deps.logger.debug).toHaveBeenCalledWith("lifecycle.reload", {
169
+ expect(session.logger.debug).toHaveBeenCalledWith("lifecycle.reload", {
260
170
  triggeredBy: "resources_discover",
261
171
  reason: "reload",
262
172
  cwd: "/proj",
@@ -266,7 +176,7 @@ describe("handleResourcesDiscover", () => {
266
176
  it("logs cwd as null when runtimeContext is null on reload", async () => {
267
177
  const deps = makeDeps();
268
178
  await handleResourcesDiscover(deps, { reason: "reload" });
269
- expect(deps.logger.debug).toHaveBeenCalledWith("lifecycle.reload", {
179
+ expect(deps.session.logger.debug).toHaveBeenCalledWith("lifecycle.reload", {
270
180
  triggeredBy: "resources_discover",
271
181
  reason: "reload",
272
182
  cwd: null,
@@ -277,11 +187,12 @@ describe("handleResourcesDiscover", () => {
277
187
  // ── handleSessionShutdown ──────────────────────────────────────────────────
278
188
 
279
189
  describe("handleSessionShutdown", () => {
280
- it("clears the UI status when a runtime context is present", async () => {
190
+ it("clears UI status when runtime context is present", async () => {
281
191
  const ctx = makeCtx();
282
- const deps = makeDeps({
283
- session: makeSession({ runtimeContext: ctx }),
192
+ const session = makeSession({
193
+ getRuntimeContext: vi.fn().mockReturnValue(ctx),
284
194
  });
195
+ const deps = makeDeps({ session });
285
196
  await handleSessionShutdown(deps);
286
197
  expect(ctx.ui.setStatus).toHaveBeenCalledWith(
287
198
  "permission-system",
@@ -294,44 +205,15 @@ describe("handleSessionShutdown", () => {
294
205
  await expect(handleSessionShutdown(deps)).resolves.not.toThrow();
295
206
  });
296
207
 
297
- it("sets runtime context to null", async () => {
298
- const ctx = makeCtx();
299
- const deps = makeDeps({ session: makeSession({ runtimeContext: ctx }) });
300
- await handleSessionShutdown(deps);
301
- expect(deps.session.runtimeContext).toBeNull();
302
- });
303
-
304
- it("clears the before_agent_start cache", async () => {
208
+ it("calls shutdown on the session", async () => {
305
209
  const deps = makeDeps();
306
210
  await handleSessionShutdown(deps);
307
- expect(deps.session.activeSkillEntries).toEqual([]);
308
- expect(deps.session.lastActiveToolsCacheKey).toBeNull();
309
- expect(deps.session.lastPromptStateCacheKey).toBeNull();
211
+ expect(deps.session.shutdown).toHaveBeenCalledOnce();
310
212
  });
311
213
 
312
- it("clears the session rules", async () => {
313
- const deps = makeDeps();
314
- await handleSessionShutdown(deps);
315
- expect(deps.session.sessionRules.clear).toHaveBeenCalledOnce();
316
- });
317
-
318
- it("stops forwarded permission polling", async () => {
319
- const deps = makeDeps();
320
- await handleSessionShutdown(deps);
321
- expect(deps.forwarding.stop).toHaveBeenCalledOnce();
322
- });
323
-
324
- it("calls stopPermissionRpcHandlers on shutdown", async () => {
214
+ it("calls stopPermissionRpcHandlers", async () => {
325
215
  const deps = makeDeps();
326
216
  await handleSessionShutdown(deps);
327
217
  expect(deps.stopPermissionRpcHandlers).toHaveBeenCalledOnce();
328
218
  });
329
-
330
- it("does not reset lastKnownActiveAgentName", async () => {
331
- const deps = makeDeps({
332
- session: makeSession({ lastKnownActiveAgentName: "remembered" }),
333
- });
334
- await handleSessionShutdown(deps);
335
- expect(deps.session.lastKnownActiveAgentName).toBe("remembered");
336
- });
337
219
  });