@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.
- package/CHANGELOG.md +15 -0
- package/package.json +1 -1
- package/src/handlers/before-agent-start.ts +19 -32
- package/src/handlers/input.ts +5 -5
- package/src/handlers/lifecycle.ts +17 -33
- package/src/handlers/tool-call.ts +11 -18
- package/src/handlers/types.ts +11 -38
- package/src/index.ts +14 -17
- package/src/permission-session.ts +252 -0
- package/src/runtime.ts +5 -30
- package/src/skill-prompt-sanitizer.ts +15 -4
- package/tests/handlers/before-agent-start.test.ts +79 -110
- package/tests/handlers/input-events.test.ts +19 -31
- package/tests/handlers/input.test.ts +41 -73
- package/tests/handlers/lifecycle.test.ts +61 -179
- package/tests/handlers/tool-call-events.test.ts +66 -92
- package/tests/handlers/tool-call.test.ts +40 -61
- package/tests/permission-session.test.ts +546 -0
- package/tests/runtime.test.ts +2 -77
|
@@ -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 {
|
|
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(
|
|
42
|
+
function makeSession(
|
|
43
|
+
state: "allow" | "deny" | "ask" = "allow",
|
|
44
|
+
): PermissionSession {
|
|
42
45
|
return {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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 {
|
|
10
|
-
import type {
|
|
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(
|
|
37
|
+
function makeSession(
|
|
38
|
+
overrides: Partial<Record<keyof PermissionSession, unknown>> = {},
|
|
39
|
+
): PermissionSession {
|
|
38
40
|
return {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
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("
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
197
|
-
|
|
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
|
|
175
|
+
const session = makeSession({
|
|
176
|
+
checkPermission: vi.fn().mockReturnValue({ state: "ask" }),
|
|
177
|
+
});
|
|
208
178
|
const deps = makeDeps({
|
|
209
|
-
session
|
|
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
|
|
191
|
+
const session = makeSession({
|
|
192
|
+
checkPermission: vi.fn().mockReturnValue({ state: "ask" }),
|
|
193
|
+
});
|
|
224
194
|
const deps = makeDeps({
|
|
225
|
-
session
|
|
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
|
|
211
|
+
const session = makeSession({
|
|
212
|
+
checkPermission: vi.fn().mockReturnValue({ state: "ask" }),
|
|
213
|
+
});
|
|
244
214
|
const deps = makeDeps({
|
|
245
|
-
session
|
|
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
|
|
263
|
-
|
|
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 {
|
|
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
|
-
// ──
|
|
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
|
|
55
|
-
|
|
56
|
-
):
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
79
|
+
expect(deps.session.refreshConfig).toHaveBeenCalledWith(ctx);
|
|
159
80
|
});
|
|
160
81
|
|
|
161
|
-
it("
|
|
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.
|
|
86
|
+
expect(deps.session.resetForNewSession).toHaveBeenCalledWith(ctx);
|
|
167
87
|
});
|
|
168
88
|
|
|
169
|
-
it("
|
|
170
|
-
const ctx = makeCtx();
|
|
89
|
+
it("logs resolved config paths", async () => {
|
|
171
90
|
const deps = makeDeps();
|
|
172
|
-
await handleSessionStart(deps, { reason: "startup" },
|
|
173
|
-
expect(deps.
|
|
91
|
+
await handleSessionStart(deps, { reason: "startup" }, makeCtx());
|
|
92
|
+
expect(deps.session.logResolvedConfigPaths).toHaveBeenCalledOnce();
|
|
174
93
|
});
|
|
175
94
|
|
|
176
|
-
it("
|
|
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.
|
|
99
|
+
expect(deps.session.resolveAgentName).toHaveBeenCalledWith(ctx);
|
|
181
100
|
});
|
|
182
101
|
|
|
183
102
|
it("notifies each policy issue", async () => {
|
|
184
|
-
const
|
|
185
|
-
|
|
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(
|
|
190
|
-
expect(
|
|
108
|
+
expect(session.logger.warn).toHaveBeenCalledWith("issue A");
|
|
109
|
+
expect(session.logger.warn).toHaveBeenCalledWith("issue B");
|
|
191
110
|
});
|
|
192
111
|
|
|
193
|
-
it("does not
|
|
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.
|
|
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("
|
|
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.
|
|
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
|
|
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(
|
|
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
|
|
190
|
+
it("clears UI status when runtime context is present", async () => {
|
|
281
191
|
const ctx = makeCtx();
|
|
282
|
-
const
|
|
283
|
-
|
|
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("
|
|
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.
|
|
308
|
-
expect(deps.session.lastActiveToolsCacheKey).toBeNull();
|
|
309
|
-
expect(deps.session.lastPromptStateCacheKey).toBeNull();
|
|
211
|
+
expect(deps.session.shutdown).toHaveBeenCalledOnce();
|
|
310
212
|
});
|
|
311
213
|
|
|
312
|
-
it("
|
|
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
|
});
|