@gotgenes/pi-permission-system 5.5.1 → 5.6.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.
@@ -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,10 +1,8 @@
1
1
  import { describe, expect, it, vi } from "vitest";
2
2
 
3
- import { evaluateSkillReadGate } from "../../../src/handlers/gates/skill-read";
4
- import type {
5
- SkillReadGateDeps,
6
- ToolCallContext,
7
- } from "../../../src/handlers/gates/types";
3
+ import type { GateDescriptor } from "../../../src/handlers/gates/descriptor";
4
+ import { describeSkillReadGate } from "../../../src/handlers/gates/skill-read";
5
+ import type { ToolCallContext } from "../../../src/handlers/gates/types";
8
6
  import type { SkillPromptEntry } from "../../../src/skill-prompt-sanitizer";
9
7
 
10
8
  // ── SDK stubs ──────────────────────────────────────────────────────────────
@@ -41,149 +39,115 @@ function makeTcc(overrides: Partial<ToolCallContext> = {}): ToolCallContext {
41
39
  };
42
40
  }
43
41
 
44
- function makeSkillReadGateDeps(
45
- overrides: Partial<SkillReadGateDeps> = {},
46
- ): SkillReadGateDeps {
47
- return {
48
- getActiveSkillEntries: vi.fn().mockReturnValue([]),
49
- writeReviewLog: vi.fn(),
50
- emitDecision: vi.fn(),
51
- canConfirm: vi.fn().mockReturnValue(true),
52
- promptPermission: vi
53
- .fn()
54
- .mockResolvedValue({ approved: true, state: "approved" }),
55
- ...overrides,
56
- };
57
- }
58
-
59
42
  // ── tests ──────────────────────────────────────────────────────────────────
60
43
 
61
- describe("evaluateSkillReadGate", () => {
62
- it("returns null when tool is not read", async () => {
63
- const tcc = makeTcc({ toolName: "write" });
64
- const deps = makeSkillReadGateDeps({
65
- getActiveSkillEntries: vi.fn().mockReturnValue([makeSkillEntry()]),
66
- });
67
- 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
+ ]);
68
49
  expect(result).toBeNull();
69
50
  });
70
51
 
71
- it("returns null when no active skill entries", async () => {
72
- const tcc = makeTcc();
73
- const deps = makeSkillReadGateDeps({
74
- getActiveSkillEntries: vi.fn().mockReturnValue([]),
75
- });
76
- const result = await evaluateSkillReadGate(tcc, deps);
52
+ it("returns null when no active skill entries", () => {
53
+ const result = describeSkillReadGate(makeTcc(), () => []);
77
54
  expect(result).toBeNull();
78
55
  });
79
56
 
80
- it("returns null when read path does not match any skill", async () => {
81
- const tcc = makeTcc({ input: { path: "/test/project/src/index.ts" } });
82
- const deps = makeSkillReadGateDeps({
83
- getActiveSkillEntries: vi.fn().mockReturnValue([makeSkillEntry()]),
84
- });
85
- 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
+ );
86
62
  expect(result).toBeNull();
87
63
  });
88
64
 
89
- it("returns allow when skill state is allow", async () => {
90
- const tcc = makeTcc();
91
- const deps = makeSkillReadGateDeps({
92
- getActiveSkillEntries: vi
93
- .fn()
94
- .mockReturnValue([makeSkillEntry({ state: "allow" })]),
95
- });
96
- const result = await evaluateSkillReadGate(tcc, deps);
97
- 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();
98
70
  });
99
71
 
100
- it("returns block when skill state is deny", async () => {
101
- const tcc = makeTcc();
102
- const deps = makeSkillReadGateDeps({
103
- getActiveSkillEntries: vi
104
- .fn()
105
- .mockReturnValue([makeSkillEntry({ state: "deny" })]),
106
- });
107
- const result = await evaluateSkillReadGate(tcc, deps);
108
- 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" });
109
79
  });
110
80
 
111
- it("returns allow when state is ask and user approves", async () => {
112
- const tcc = makeTcc();
113
- const deps = makeSkillReadGateDeps({
114
- getActiveSkillEntries: vi
115
- .fn()
116
- .mockReturnValue([makeSkillEntry({ state: "ask" })]),
117
- promptPermission: vi
118
- .fn()
119
- .mockResolvedValue({ approved: true, state: "approved" }),
120
- });
121
- const result = await evaluateSkillReadGate(tcc, deps);
122
- 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" });
123
88
  });
124
89
 
125
- it("returns block when state is ask and user denies", async () => {
126
- const tcc = makeTcc();
127
- const deps = makeSkillReadGateDeps({
128
- getActiveSkillEntries: vi
129
- .fn()
130
- .mockReturnValue([makeSkillEntry({ state: "ask" })]),
131
- promptPermission: vi
132
- .fn()
133
- .mockResolvedValue({ approved: false, state: "denied" }),
134
- });
135
- const result = await evaluateSkillReadGate(tcc, deps);
136
- 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" });
97
+ });
98
+
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");
137
105
  });
138
106
 
139
- it("returns block when state is ask and no UI available", async () => {
140
- const tcc = makeTcc();
141
- const deps = makeSkillReadGateDeps({
142
- getActiveSkillEntries: vi
143
- .fn()
144
- .mockReturnValue([makeSkillEntry({ state: "ask" })]),
145
- canConfirm: vi.fn().mockReturnValue(false),
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",
146
116
  });
147
- const result = await evaluateSkillReadGate(tcc, deps);
148
- expect(result).toMatchObject({ action: "block" });
117
+ expect(deniedMsg).toContain("librarian");
149
118
  });
150
119
 
151
- it("emits decision event with correct fields on deny", async () => {
152
- const tcc = makeTcc({ agentName: "test-agent" });
153
- const deps = makeSkillReadGateDeps({
154
- getActiveSkillEntries: vi
155
- .fn()
156
- .mockReturnValue([makeSkillEntry({ state: "deny" })]),
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",
157
131
  });
158
- await evaluateSkillReadGate(tcc, deps);
159
- expect(deps.emitDecision).toHaveBeenCalledWith(
160
- expect.objectContaining({
161
- surface: "skill",
162
- value: "librarian",
163
- result: "deny",
164
- resolution: "policy_deny",
165
- origin: null,
166
- agentName: "test-agent",
167
- matchedPattern: null,
168
- }),
169
- );
132
+ expect(result.promptDetails.message).toBeDefined();
170
133
  });
171
134
 
172
- it("emits decision event with correct fields on allow", async () => {
173
- const tcc = makeTcc();
174
- const deps = makeSkillReadGateDeps({
175
- getActiveSkillEntries: vi
176
- .fn()
177
- .mockReturnValue([makeSkillEntry({ state: "allow" })]),
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",
178
144
  });
179
- await evaluateSkillReadGate(tcc, deps);
180
- expect(deps.emitDecision).toHaveBeenCalledWith(
181
- expect.objectContaining({
182
- surface: "skill",
183
- value: "librarian",
184
- result: "allow",
185
- resolution: "policy_allow",
186
- }),
187
- );
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");
188
152
  });
189
153
  });