@gotgenes/pi-permission-system 5.5.0 → 5.6.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.
@@ -0,0 +1,361 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+
3
+ import type {
4
+ GateDescriptor,
5
+ GateRunnerDeps,
6
+ } from "../../../src/handlers/gates/descriptor";
7
+ import { runGateCheck } from "../../../src/handlers/gates/runner";
8
+ import type { PermissionCheckResult } from "../../../src/types";
9
+
10
+ // ── helpers ────────────────────────────────────────────────────────────────
11
+
12
+ function makeDescriptor(
13
+ overrides: Partial<GateDescriptor> = {},
14
+ ): GateDescriptor {
15
+ return {
16
+ surface: "read",
17
+ input: {},
18
+ messages: {
19
+ denyReason: "Tool 'read' is denied.",
20
+ unavailableReason: "No UI available.",
21
+ userDeniedReason: (d) => `User denied. ${d.denialReason ?? ""}`,
22
+ },
23
+ promptDetails: {
24
+ source: "tool_call",
25
+ agentName: null,
26
+ message: "Allow tool 'read'?",
27
+ toolCallId: "tc-1",
28
+ toolName: "read",
29
+ },
30
+ logContext: {
31
+ source: "tool_call",
32
+ toolCallId: "tc-1",
33
+ toolName: "read",
34
+ },
35
+ decision: {
36
+ surface: "read",
37
+ value: "read",
38
+ },
39
+ ...overrides,
40
+ };
41
+ }
42
+
43
+ function makeCheckResult(
44
+ state: "allow" | "deny" | "ask",
45
+ overrides: Partial<PermissionCheckResult> = {},
46
+ ): PermissionCheckResult {
47
+ return {
48
+ state,
49
+ toolName: "read",
50
+ source: "tool",
51
+ origin: "builtin",
52
+ matchedPattern: "*",
53
+ ...overrides,
54
+ };
55
+ }
56
+
57
+ function makeRunnerDeps(
58
+ overrides: Partial<GateRunnerDeps> = {},
59
+ ): GateRunnerDeps {
60
+ return {
61
+ checkPermission: vi.fn().mockReturnValue(makeCheckResult("allow")),
62
+ getSessionRuleset: vi.fn().mockReturnValue([]),
63
+ approveSessionRule: vi.fn(),
64
+ writeReviewLog: vi.fn(),
65
+ emitDecision: vi.fn(),
66
+ canConfirm: vi.fn().mockReturnValue(true),
67
+ promptPermission: vi
68
+ .fn()
69
+ .mockResolvedValue({ approved: true, state: "approved" }),
70
+ ...overrides,
71
+ };
72
+ }
73
+
74
+ // ── tests ──────────────────────────────────────────────────────────────────
75
+
76
+ describe("runGateCheck", () => {
77
+ it("returns allow and emits policy_allow when policy is allow", async () => {
78
+ const deps = makeRunnerDeps();
79
+ const result = await runGateCheck(makeDescriptor(), null, "tc-1", deps);
80
+ expect(result).toEqual({ action: "allow" });
81
+ expect(deps.emitDecision).toHaveBeenCalledWith(
82
+ expect.objectContaining({
83
+ surface: "read",
84
+ value: "read",
85
+ result: "allow",
86
+ resolution: "policy_allow",
87
+ }),
88
+ );
89
+ });
90
+
91
+ it("returns block and emits policy_deny when policy is deny", async () => {
92
+ const deps = makeRunnerDeps({
93
+ checkPermission: vi.fn().mockReturnValue(makeCheckResult("deny")),
94
+ });
95
+ const result = await runGateCheck(makeDescriptor(), null, "tc-1", deps);
96
+ expect(result).toMatchObject({ action: "block" });
97
+ expect(deps.emitDecision).toHaveBeenCalledWith(
98
+ expect.objectContaining({
99
+ result: "deny",
100
+ resolution: "policy_deny",
101
+ }),
102
+ );
103
+ expect(deps.writeReviewLog).toHaveBeenCalledWith(
104
+ "permission_request.blocked",
105
+ expect.objectContaining({ resolution: "policy_denied" }),
106
+ );
107
+ });
108
+
109
+ it("returns allow and emits session_approved on session hit", async () => {
110
+ const deps = makeRunnerDeps({
111
+ checkPermission: vi.fn().mockReturnValue(
112
+ makeCheckResult("allow", {
113
+ source: "session",
114
+ matchedPattern: "git *",
115
+ }),
116
+ ),
117
+ });
118
+ const result = await runGateCheck(
119
+ makeDescriptor({
120
+ surface: "bash",
121
+ input: { command: "git status" },
122
+ decision: { surface: "bash", value: "git status" },
123
+ }),
124
+ null,
125
+ "tc-1",
126
+ deps,
127
+ );
128
+ expect(result).toEqual({ action: "allow" });
129
+ expect(deps.writeReviewLog).toHaveBeenCalledWith(
130
+ "permission_request.session_approved",
131
+ expect.objectContaining({
132
+ resolution: "session_approved",
133
+ sessionApprovalPattern: "git *",
134
+ }),
135
+ );
136
+ expect(deps.emitDecision).toHaveBeenCalledWith(
137
+ expect.objectContaining({
138
+ resolution: "session_approved",
139
+ matchedPattern: "git *",
140
+ }),
141
+ );
142
+ });
143
+
144
+ it("returns allow and emits user_approved when ask + user approves", async () => {
145
+ const deps = makeRunnerDeps({
146
+ checkPermission: vi.fn().mockReturnValue(makeCheckResult("ask")),
147
+ promptPermission: vi
148
+ .fn()
149
+ .mockResolvedValue({ approved: true, state: "approved" }),
150
+ });
151
+ const result = await runGateCheck(makeDescriptor(), null, "tc-1", deps);
152
+ expect(result).toEqual({ action: "allow" });
153
+ expect(deps.emitDecision).toHaveBeenCalledWith(
154
+ expect.objectContaining({
155
+ result: "allow",
156
+ resolution: "user_approved",
157
+ }),
158
+ );
159
+ });
160
+
161
+ it("returns allow, emits user_approved_for_session, and records session rule on approved_for_session", async () => {
162
+ const deps = makeRunnerDeps({
163
+ checkPermission: vi.fn().mockReturnValue(makeCheckResult("ask")),
164
+ promptPermission: vi
165
+ .fn()
166
+ .mockResolvedValue({ approved: true, state: "approved_for_session" }),
167
+ });
168
+ const descriptor = makeDescriptor({
169
+ sessionApproval: { surface: "read", pattern: "*" },
170
+ });
171
+ const result = await runGateCheck(descriptor, null, "tc-1", deps);
172
+ expect(result).toEqual({ action: "allow" });
173
+ expect(deps.emitDecision).toHaveBeenCalledWith(
174
+ expect.objectContaining({
175
+ resolution: "user_approved_for_session",
176
+ }),
177
+ );
178
+ expect(deps.approveSessionRule).toHaveBeenCalledWith("read", "*");
179
+ });
180
+
181
+ it("calls approveSessionRule once per pattern when sessionApproval has multiple patterns", async () => {
182
+ const deps = makeRunnerDeps({
183
+ checkPermission: vi.fn().mockReturnValue(makeCheckResult("ask")),
184
+ promptPermission: vi
185
+ .fn()
186
+ .mockResolvedValue({ approved: true, state: "approved_for_session" }),
187
+ });
188
+ const descriptor = makeDescriptor({
189
+ sessionApproval: {
190
+ surface: "external_directory",
191
+ patterns: ["/outside/a/*", "/outside/b/*"],
192
+ },
193
+ });
194
+ const result = await runGateCheck(descriptor, null, "tc-1", deps);
195
+ expect(result).toEqual({ action: "allow" });
196
+ expect(deps.approveSessionRule).toHaveBeenCalledTimes(2);
197
+ expect(deps.approveSessionRule).toHaveBeenCalledWith(
198
+ "external_directory",
199
+ "/outside/a/*",
200
+ );
201
+ expect(deps.approveSessionRule).toHaveBeenCalledWith(
202
+ "external_directory",
203
+ "/outside/b/*",
204
+ );
205
+ });
206
+
207
+ it("returns block and emits user_denied when ask + user denies", async () => {
208
+ const deps = makeRunnerDeps({
209
+ checkPermission: vi.fn().mockReturnValue(makeCheckResult("ask")),
210
+ promptPermission: vi
211
+ .fn()
212
+ .mockResolvedValue({ approved: false, state: "denied" }),
213
+ });
214
+ const result = await runGateCheck(makeDescriptor(), null, "tc-1", deps);
215
+ expect(result).toMatchObject({ action: "block" });
216
+ expect(deps.emitDecision).toHaveBeenCalledWith(
217
+ expect.objectContaining({
218
+ result: "deny",
219
+ resolution: "user_denied",
220
+ }),
221
+ );
222
+ });
223
+
224
+ it("returns block and emits confirmation_unavailable when ask + no UI", async () => {
225
+ const deps = makeRunnerDeps({
226
+ checkPermission: vi.fn().mockReturnValue(makeCheckResult("ask")),
227
+ canConfirm: vi.fn().mockReturnValue(false),
228
+ });
229
+ const result = await runGateCheck(makeDescriptor(), null, "tc-1", deps);
230
+ expect(result).toMatchObject({ action: "block" });
231
+ expect(deps.emitDecision).toHaveBeenCalledWith(
232
+ expect.objectContaining({
233
+ result: "deny",
234
+ resolution: "confirmation_unavailable",
235
+ }),
236
+ );
237
+ });
238
+
239
+ it("emits auto_approved resolution when decision has autoApproved flag", async () => {
240
+ const deps = makeRunnerDeps({
241
+ checkPermission: vi.fn().mockReturnValue(makeCheckResult("ask")),
242
+ promptPermission: vi.fn().mockResolvedValue({
243
+ approved: true,
244
+ state: "approved",
245
+ autoApproved: true,
246
+ }),
247
+ });
248
+ const result = await runGateCheck(makeDescriptor(), null, "tc-1", deps);
249
+ expect(result).toEqual({ action: "allow" });
250
+ expect(deps.emitDecision).toHaveBeenCalledWith(
251
+ expect.objectContaining({
252
+ resolution: "auto_approved",
253
+ }),
254
+ );
255
+ });
256
+
257
+ it("uses preResolved.state instead of calling checkPermission", async () => {
258
+ const deps = makeRunnerDeps();
259
+ const descriptor = makeDescriptor({
260
+ preResolved: { state: "deny" },
261
+ });
262
+ const result = await runGateCheck(descriptor, null, "tc-1", deps);
263
+ expect(result).toMatchObject({ action: "block" });
264
+ expect(deps.checkPermission).not.toHaveBeenCalled();
265
+ expect(deps.emitDecision).toHaveBeenCalledWith(
266
+ expect.objectContaining({
267
+ resolution: "policy_deny",
268
+ }),
269
+ );
270
+ });
271
+
272
+ it("uses preResolved.state allow without calling checkPermission", async () => {
273
+ const deps = makeRunnerDeps();
274
+ const descriptor = makeDescriptor({
275
+ preResolved: { state: "allow" },
276
+ });
277
+ const result = await runGateCheck(descriptor, null, "tc-1", deps);
278
+ expect(result).toEqual({ action: "allow" });
279
+ expect(deps.checkPermission).not.toHaveBeenCalled();
280
+ expect(deps.emitDecision).toHaveBeenCalledWith(
281
+ expect.objectContaining({
282
+ resolution: "policy_allow",
283
+ }),
284
+ );
285
+ });
286
+
287
+ it("passes agentName to checkPermission and decision event", async () => {
288
+ const deps = makeRunnerDeps();
289
+ const result = await runGateCheck(
290
+ makeDescriptor(),
291
+ "test-agent",
292
+ "tc-1",
293
+ deps,
294
+ );
295
+ expect(result).toEqual({ action: "allow" });
296
+ expect(deps.checkPermission).toHaveBeenCalledWith(
297
+ "read",
298
+ {},
299
+ "test-agent",
300
+ [],
301
+ );
302
+ expect(deps.emitDecision).toHaveBeenCalledWith(
303
+ expect.objectContaining({
304
+ agentName: "test-agent",
305
+ }),
306
+ );
307
+ });
308
+
309
+ it("passes requestId from toolCallId to promptPermission", async () => {
310
+ const deps = makeRunnerDeps({
311
+ checkPermission: vi.fn().mockReturnValue(makeCheckResult("ask")),
312
+ });
313
+ await runGateCheck(makeDescriptor(), null, "tc-42", deps);
314
+ expect(deps.promptPermission).toHaveBeenCalledWith(
315
+ expect.objectContaining({ requestId: "tc-42" }),
316
+ );
317
+ });
318
+
319
+ it("does not call approveSessionRule when user approves once (no sessionApproval)", async () => {
320
+ const deps = makeRunnerDeps({
321
+ checkPermission: vi.fn().mockReturnValue(makeCheckResult("ask")),
322
+ promptPermission: vi
323
+ .fn()
324
+ .mockResolvedValue({ approved: true, state: "approved" }),
325
+ });
326
+ await runGateCheck(makeDescriptor(), null, "tc-1", deps);
327
+ expect(deps.approveSessionRule).not.toHaveBeenCalled();
328
+ });
329
+
330
+ it("uses preCheck result directly instead of calling checkPermission", async () => {
331
+ const deps = makeRunnerDeps();
332
+ const descriptor = makeDescriptor({
333
+ preCheck: makeCheckResult("deny", {
334
+ origin: "global",
335
+ matchedPattern: "rm *",
336
+ }),
337
+ });
338
+ const result = await runGateCheck(descriptor, null, "tc-1", deps);
339
+ expect(result).toMatchObject({ action: "block" });
340
+ expect(deps.checkPermission).not.toHaveBeenCalled();
341
+ expect(deps.emitDecision).toHaveBeenCalledWith(
342
+ expect.objectContaining({
343
+ resolution: "policy_deny",
344
+ origin: "global",
345
+ matchedPattern: "rm *",
346
+ }),
347
+ );
348
+ });
349
+
350
+ it("does not call approveSessionRule when user approves for session but no sessionApproval on descriptor", async () => {
351
+ const deps = makeRunnerDeps({
352
+ checkPermission: vi.fn().mockReturnValue(makeCheckResult("ask")),
353
+ promptPermission: vi
354
+ .fn()
355
+ .mockResolvedValue({ approved: true, state: "approved_for_session" }),
356
+ });
357
+ // No sessionApproval on descriptor
358
+ await runGateCheck(makeDescriptor(), null, "tc-1", deps);
359
+ expect(deps.approveSessionRule).not.toHaveBeenCalled();
360
+ });
361
+ });
@@ -1,9 +1,8 @@
1
1
  import { describe, expect, it, vi } from "vitest";
2
2
 
3
- import { evaluateSkillReadGate } from "../../../src/handlers/gates/skill-read";
3
+ import type { GateDescriptor } from "../../../src/handlers/gates/descriptor";
4
+ import { describeSkillReadGate } from "../../../src/handlers/gates/skill-read";
4
5
  import type { ToolCallContext } from "../../../src/handlers/gates/types";
5
- import type { HandlerDeps } from "../../../src/handlers/types";
6
- import type { PermissionEventBus } from "../../../src/permission-events";
7
6
  import type { SkillPromptEntry } from "../../../src/skill-prompt-sanitizer";
8
7
 
9
8
  // ── SDK stubs ──────────────────────────────────────────────────────────────
@@ -40,165 +39,115 @@ function makeTcc(overrides: Partial<ToolCallContext> = {}): ToolCallContext {
40
39
  };
41
40
  }
42
41
 
43
- function makeEvents(): PermissionEventBus {
44
- return { emit: vi.fn(), on: vi.fn().mockReturnValue(() => undefined) };
45
- }
46
-
47
- function makeRuntime(
48
- overrides: Record<string, unknown> = {},
49
- ): HandlerDeps["runtime"] {
50
- return {
51
- activeSkillEntries: [],
52
- writeReviewLog: vi.fn(),
53
- ...overrides,
54
- } as unknown as HandlerDeps["runtime"];
55
- }
56
-
57
- function makeDeps(overrides: Record<string, unknown> = {}): HandlerDeps {
58
- const { runtime: runtimeOverrides, events, ...rest } = overrides;
59
- return {
60
- runtime: makeRuntime(runtimeOverrides as Record<string, unknown>),
61
- events: events ?? makeEvents(),
62
- canRequestPermissionConfirmation: vi.fn().mockReturnValue(true),
63
- promptPermission: vi
64
- .fn()
65
- .mockResolvedValue({ approved: true, state: "approved" }),
66
- ...rest,
67
- } as unknown as HandlerDeps;
68
- }
69
-
70
42
  // ── tests ──────────────────────────────────────────────────────────────────
71
43
 
72
- describe("evaluateSkillReadGate", () => {
73
- it("returns null when tool is not read", async () => {
74
- const tcc = makeTcc({ toolName: "write" });
75
- const deps = makeDeps({
76
- runtime: { activeSkillEntries: [makeSkillEntry()] },
77
- });
78
- const result = await evaluateSkillReadGate(tcc, deps);
44
+ describe("describeSkillReadGate", () => {
45
+ it("returns null when tool is not read", () => {
46
+ const result = describeSkillReadGate(makeTcc({ toolName: "write" }), () => [
47
+ makeSkillEntry(),
48
+ ]);
79
49
  expect(result).toBeNull();
80
50
  });
81
51
 
82
- it("returns null when no active skill entries", async () => {
83
- const tcc = makeTcc();
84
- const deps = makeDeps({ runtime: { activeSkillEntries: [] } });
85
- const result = await evaluateSkillReadGate(tcc, deps);
52
+ it("returns null when no active skill entries", () => {
53
+ const result = describeSkillReadGate(makeTcc(), () => []);
86
54
  expect(result).toBeNull();
87
55
  });
88
56
 
89
- it("returns null when read path does not match any skill", async () => {
90
- const tcc = makeTcc({ input: { path: "/test/project/src/index.ts" } });
91
- const deps = makeDeps({
92
- runtime: { activeSkillEntries: [makeSkillEntry()] },
93
- });
94
- const result = await evaluateSkillReadGate(tcc, deps);
57
+ it("returns null when read path does not match any skill", () => {
58
+ const result = describeSkillReadGate(
59
+ makeTcc({ input: { path: "/test/project/src/index.ts" } }),
60
+ () => [makeSkillEntry()],
61
+ );
95
62
  expect(result).toBeNull();
96
63
  });
97
64
 
98
- it("returns allow when skill state is allow", async () => {
99
- const tcc = makeTcc();
100
- const deps = makeDeps({
101
- runtime: {
102
- activeSkillEntries: [makeSkillEntry({ state: "allow" })],
103
- },
104
- });
105
- const result = await evaluateSkillReadGate(tcc, deps);
106
- expect(result).toEqual({ action: "allow" });
65
+ it("returns null when input has no path", () => {
66
+ const result = describeSkillReadGate(makeTcc({ input: {} }), () => [
67
+ makeSkillEntry(),
68
+ ]);
69
+ expect(result).toBeNull();
107
70
  });
108
71
 
109
- it("returns block when skill state is deny", async () => {
110
- const tcc = makeTcc();
111
- const deps = makeDeps({
112
- runtime: {
113
- activeSkillEntries: [makeSkillEntry({ state: "deny" })],
114
- },
115
- });
116
- const result = await evaluateSkillReadGate(tcc, deps);
117
- expect(result).toMatchObject({ action: "block" });
72
+ it("returns GateDescriptor with preResolved.state matching skill entry state (ask)", () => {
73
+ const result = describeSkillReadGate(makeTcc(), () => [
74
+ makeSkillEntry({ state: "ask" }),
75
+ ]);
76
+ expect(result).not.toBeNull();
77
+ const desc = result as GateDescriptor;
78
+ expect(desc.preResolved).toEqual({ state: "ask" });
118
79
  });
119
80
 
120
- it("returns allow when state is ask and user approves", async () => {
121
- const tcc = makeTcc();
122
- const deps = makeDeps({
123
- runtime: {
124
- activeSkillEntries: [makeSkillEntry({ state: "ask" })],
125
- },
126
- promptPermission: vi
127
- .fn()
128
- .mockResolvedValue({ approved: true, state: "approved" }),
129
- });
130
- const result = await evaluateSkillReadGate(tcc, deps);
131
- expect(result).toEqual({ action: "allow" });
81
+ it("returns GateDescriptor with preResolved.state matching skill entry state (allow)", () => {
82
+ const result = describeSkillReadGate(makeTcc(), () => [
83
+ makeSkillEntry({ state: "allow" }),
84
+ ]);
85
+ expect(result).not.toBeNull();
86
+ const desc = result as GateDescriptor;
87
+ expect(desc.preResolved).toEqual({ state: "allow" });
132
88
  });
133
89
 
134
- it("returns block when state is ask and user denies", async () => {
135
- const tcc = makeTcc();
136
- const deps = makeDeps({
137
- runtime: {
138
- activeSkillEntries: [makeSkillEntry({ state: "ask" })],
139
- },
140
- promptPermission: vi
141
- .fn()
142
- .mockResolvedValue({ approved: false, state: "denied" }),
143
- });
144
- const result = await evaluateSkillReadGate(tcc, deps);
145
- expect(result).toMatchObject({ action: "block" });
90
+ it("returns GateDescriptor with preResolved.state matching skill entry state (deny)", () => {
91
+ const result = describeSkillReadGate(makeTcc(), () => [
92
+ makeSkillEntry({ state: "deny" }),
93
+ ]);
94
+ expect(result).not.toBeNull();
95
+ const desc = result as GateDescriptor;
96
+ expect(desc.preResolved).toEqual({ state: "deny" });
146
97
  });
147
98
 
148
- it("returns block when state is ask and no UI available", async () => {
149
- const tcc = makeTcc();
150
- const deps = makeDeps({
151
- runtime: {
152
- activeSkillEntries: [makeSkillEntry({ state: "ask" })],
153
- },
154
- canRequestPermissionConfirmation: vi.fn().mockReturnValue(false),
99
+ it("decision surface is 'skill' and decision value is the skill name", () => {
100
+ const result = describeSkillReadGate(makeTcc(), () => [
101
+ makeSkillEntry({ name: "my-skill" }),
102
+ ]) as GateDescriptor;
103
+ expect(result.decision.surface).toBe("skill");
104
+ expect(result.decision.value).toBe("my-skill");
105
+ });
106
+
107
+ it("messages contain the skill name", () => {
108
+ const result = describeSkillReadGate(makeTcc(), () => [
109
+ makeSkillEntry({ name: "librarian" }),
110
+ ]) as GateDescriptor;
111
+ expect(result.messages.denyReason).toContain("librarian");
112
+ expect(result.messages.unavailableReason).toContain("librarian");
113
+ const deniedMsg = result.messages.userDeniedReason({
114
+ approved: false,
115
+ state: "denied",
155
116
  });
156
- const result = await evaluateSkillReadGate(tcc, deps);
157
- expect(result).toMatchObject({ action: "block" });
117
+ expect(deniedMsg).toContain("librarian");
158
118
  });
159
119
 
160
- it("emits decision event with correct fields on deny", async () => {
161
- const events = makeEvents();
162
- const tcc = makeTcc({ agentName: "test-agent" });
163
- const deps = makeDeps({
164
- runtime: {
165
- activeSkillEntries: [makeSkillEntry({ state: "deny" })],
166
- },
167
- events,
120
+ it("promptDetails includes skill_read source and skillName", () => {
121
+ const result = describeSkillReadGate(
122
+ makeTcc({ agentName: "test-agent", toolCallId: "tc-42" }),
123
+ () => [makeSkillEntry({ name: "my-skill" })],
124
+ ) as GateDescriptor;
125
+ expect(result.promptDetails).toMatchObject({
126
+ source: "skill_read",
127
+ agentName: "test-agent",
128
+ toolCallId: "tc-42",
129
+ toolName: "read",
130
+ skillName: "my-skill",
168
131
  });
169
- await evaluateSkillReadGate(tcc, deps);
170
- expect(events.emit).toHaveBeenCalledWith(
171
- "permissions:decision",
172
- expect.objectContaining({
173
- surface: "skill",
174
- value: "librarian",
175
- result: "deny",
176
- resolution: "policy_deny",
177
- origin: null,
178
- agentName: "test-agent",
179
- matchedPattern: null,
180
- }),
181
- );
132
+ expect(result.promptDetails.message).toBeDefined();
182
133
  });
183
134
 
184
- it("emits decision event with correct fields on allow", async () => {
185
- const events = makeEvents();
186
- const tcc = makeTcc();
187
- const deps = makeDeps({
188
- runtime: {
189
- activeSkillEntries: [makeSkillEntry({ state: "allow" })],
190
- },
191
- events,
135
+ it("logContext includes skill_read source and skillName", () => {
136
+ const result = describeSkillReadGate(
137
+ makeTcc({ agentName: "agent-1" }),
138
+ () => [makeSkillEntry({ name: "librarian" })],
139
+ ) as GateDescriptor;
140
+ expect(result.logContext).toMatchObject({
141
+ source: "skill_read",
142
+ skillName: "librarian",
143
+ agentName: "agent-1",
192
144
  });
193
- await evaluateSkillReadGate(tcc, deps);
194
- expect(events.emit).toHaveBeenCalledWith(
195
- "permissions:decision",
196
- expect.objectContaining({
197
- surface: "skill",
198
- value: "librarian",
199
- result: "allow",
200
- resolution: "policy_allow",
201
- }),
202
- );
145
+ });
146
+
147
+ it("surface is 'skill' on the descriptor", () => {
148
+ const result = describeSkillReadGate(makeTcc(), () => [
149
+ makeSkillEntry(),
150
+ ]) as GateDescriptor;
151
+ expect(result.surface).toBe("skill");
203
152
  });
204
153
  });