@gotgenes/pi-permission-system 5.3.4 → 5.4.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,247 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+
3
+ import { evaluateBashExternalDirectoryGate } from "../../../src/handlers/gates/bash-external-directory";
4
+ 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
+ import type { PermissionCheckResult } from "../../../src/types";
8
+
9
+ // ── helpers ────────────────────────────────────────────────────────────────
10
+
11
+ function makeTcc(overrides: Partial<ToolCallContext> = {}): ToolCallContext {
12
+ return {
13
+ toolName: "bash",
14
+ agentName: null,
15
+ input: { command: "cat /outside/project/file.ts" },
16
+ toolCallId: "tc-1",
17
+ cwd: "/test/project",
18
+ ...overrides,
19
+ };
20
+ }
21
+
22
+ function makeCheckResult(
23
+ state: "allow" | "deny" | "ask",
24
+ overrides: Partial<PermissionCheckResult> = {},
25
+ ): PermissionCheckResult {
26
+ return {
27
+ state,
28
+ toolName: "external_directory",
29
+ source: "special",
30
+ origin: "builtin",
31
+ ...overrides,
32
+ };
33
+ }
34
+
35
+ function makeEvents(): PermissionEventBus {
36
+ return { emit: vi.fn(), on: vi.fn().mockReturnValue(() => undefined) };
37
+ }
38
+
39
+ function makeRuntime(
40
+ overrides: Record<string, unknown> = {},
41
+ ): HandlerDeps["runtime"] {
42
+ return {
43
+ config: { debugLog: false, permissionReviewLog: true, yoloMode: false },
44
+ runtimeContext: {} as HandlerDeps["runtime"]["runtimeContext"],
45
+ permissionManager: {
46
+ checkPermission: vi.fn().mockReturnValue(makeCheckResult("ask")),
47
+ },
48
+ sessionRules: {
49
+ approve: vi.fn(),
50
+ getRuleset: vi.fn().mockReturnValue([]),
51
+ clear: vi.fn(),
52
+ },
53
+ writeReviewLog: vi.fn(),
54
+ ...overrides,
55
+ } as unknown as HandlerDeps["runtime"];
56
+ }
57
+
58
+ function makeDeps(overrides: Record<string, unknown> = {}): HandlerDeps {
59
+ const { runtime: runtimeOverrides, events, ...rest } = overrides;
60
+ return {
61
+ runtime: makeRuntime(runtimeOverrides as Record<string, unknown>),
62
+ events: events ?? makeEvents(),
63
+ canRequestPermissionConfirmation: vi.fn().mockReturnValue(true),
64
+ promptPermission: vi
65
+ .fn()
66
+ .mockResolvedValue({ approved: true, state: "approved" }),
67
+ ...rest,
68
+ } as unknown as HandlerDeps;
69
+ }
70
+
71
+ // ── tests ──────────────────────────────────────────────────────────────────
72
+
73
+ describe("evaluateBashExternalDirectoryGate", () => {
74
+ it("returns null when tool is not bash", async () => {
75
+ const tcc = makeTcc({ toolName: "read" });
76
+ const result = await evaluateBashExternalDirectoryGate(tcc, makeDeps());
77
+ expect(result).toBeNull();
78
+ });
79
+
80
+ it("returns null when no CWD", async () => {
81
+ const tcc = makeTcc({ cwd: undefined });
82
+ const result = await evaluateBashExternalDirectoryGate(tcc, makeDeps());
83
+ expect(result).toBeNull();
84
+ });
85
+
86
+ it("returns null when command has no external paths", async () => {
87
+ const tcc = makeTcc({ input: { command: "ls -la" } });
88
+ const result = await evaluateBashExternalDirectoryGate(tcc, makeDeps());
89
+ expect(result).toBeNull();
90
+ });
91
+
92
+ it("returns null and logs when all external paths are session-covered", async () => {
93
+ const writeReviewLog = vi.fn();
94
+ const deps = makeDeps({
95
+ runtime: {
96
+ permissionManager: {
97
+ checkPermission: vi
98
+ .fn()
99
+ .mockReturnValue(makeCheckResult("allow", { source: "session" })),
100
+ },
101
+ writeReviewLog,
102
+ },
103
+ });
104
+ const tcc = makeTcc();
105
+ const result = await evaluateBashExternalDirectoryGate(tcc, deps);
106
+ expect(result).toBeNull();
107
+ expect(writeReviewLog).toHaveBeenCalledWith(
108
+ "permission_request.session_approved",
109
+ expect.objectContaining({ resolution: "session_approved" }),
110
+ );
111
+ });
112
+
113
+ it("blocks when policy is deny", async () => {
114
+ const deps = makeDeps({
115
+ runtime: {
116
+ permissionManager: {
117
+ checkPermission: vi.fn().mockReturnValue(makeCheckResult("deny")),
118
+ },
119
+ },
120
+ });
121
+ const tcc = makeTcc();
122
+ const result = await evaluateBashExternalDirectoryGate(tcc, deps);
123
+ expect(result).toMatchObject({ action: "block" });
124
+ });
125
+
126
+ it("allows without recording session rules when user approves once", async () => {
127
+ const sessionRules = {
128
+ approve: vi.fn(),
129
+ getRuleset: vi.fn().mockReturnValue([]),
130
+ clear: vi.fn(),
131
+ };
132
+ const deps = makeDeps({
133
+ runtime: {
134
+ permissionManager: {
135
+ checkPermission: vi.fn().mockReturnValue(makeCheckResult("ask")),
136
+ },
137
+ sessionRules,
138
+ },
139
+ promptPermission: vi
140
+ .fn()
141
+ .mockResolvedValue({ approved: true, state: "approved" }),
142
+ });
143
+ const tcc = makeTcc();
144
+ const result = await evaluateBashExternalDirectoryGate(tcc, deps);
145
+ expect(result).toEqual({ action: "allow" });
146
+ expect(sessionRules.approve).not.toHaveBeenCalled();
147
+ });
148
+
149
+ it("records one session rule per uncovered path on approved_for_session", async () => {
150
+ const sessionRules = {
151
+ approve: vi.fn(),
152
+ getRuleset: vi.fn().mockReturnValue([]),
153
+ clear: vi.fn(),
154
+ };
155
+ const deps = makeDeps({
156
+ runtime: {
157
+ permissionManager: {
158
+ checkPermission: vi.fn().mockReturnValue(makeCheckResult("ask")),
159
+ },
160
+ sessionRules,
161
+ },
162
+ promptPermission: vi
163
+ .fn()
164
+ .mockResolvedValue({ approved: true, state: "approved_for_session" }),
165
+ });
166
+ // Command referencing two external paths
167
+ const tcc = makeTcc({
168
+ input: {
169
+ command: "diff /outside/a.ts /outside/b.ts",
170
+ },
171
+ });
172
+ const result = await evaluateBashExternalDirectoryGate(tcc, deps);
173
+ expect(result).toEqual({ action: "allow" });
174
+ // Each uncovered path gets its own session rule
175
+ expect(sessionRules.approve).toHaveBeenCalledTimes(2);
176
+ for (const call of (sessionRules.approve as ReturnType<typeof vi.fn>).mock
177
+ .calls) {
178
+ expect(call[0]).toBe("external_directory");
179
+ }
180
+ });
181
+
182
+ it("blocks when user denies", async () => {
183
+ const deps = makeDeps({
184
+ runtime: {
185
+ permissionManager: {
186
+ checkPermission: vi.fn().mockReturnValue(makeCheckResult("ask")),
187
+ },
188
+ },
189
+ promptPermission: vi
190
+ .fn()
191
+ .mockResolvedValue({ approved: false, state: "denied" }),
192
+ });
193
+ const tcc = makeTcc();
194
+ const result = await evaluateBashExternalDirectoryGate(tcc, deps);
195
+ expect(result).toMatchObject({ action: "block" });
196
+ });
197
+
198
+ it("blocks when no UI available", async () => {
199
+ const deps = makeDeps({
200
+ runtime: {
201
+ permissionManager: {
202
+ checkPermission: vi.fn().mockReturnValue(makeCheckResult("ask")),
203
+ },
204
+ },
205
+ canRequestPermissionConfirmation: vi.fn().mockReturnValue(false),
206
+ });
207
+ const tcc = makeTcc();
208
+ const result = await evaluateBashExternalDirectoryGate(tcc, deps);
209
+ expect(result).toMatchObject({ action: "block" });
210
+ });
211
+
212
+ it("only prompts about uncovered paths when some are session-covered", async () => {
213
+ // First call (for getRuleset path filter): session covers /outside/a.ts
214
+ // Second call (for config-level policy): returns ask
215
+ const checkPermission = vi
216
+ .fn()
217
+ .mockImplementation(
218
+ (
219
+ surface: string,
220
+ input: Record<string, unknown>,
221
+ ): PermissionCheckResult => {
222
+ if (
223
+ surface === "external_directory" &&
224
+ input.path === "/outside/a.ts"
225
+ ) {
226
+ return makeCheckResult("allow", { source: "session" });
227
+ }
228
+ return makeCheckResult("ask");
229
+ },
230
+ );
231
+ const deps = makeDeps({
232
+ runtime: {
233
+ permissionManager: { checkPermission },
234
+ },
235
+ promptPermission: vi
236
+ .fn()
237
+ .mockResolvedValue({ approved: true, state: "approved" }),
238
+ });
239
+ const tcc = makeTcc({
240
+ input: { command: "diff /outside/a.ts /outside/b.ts" },
241
+ });
242
+ const result = await evaluateBashExternalDirectoryGate(tcc, deps);
243
+ expect(result).toEqual({ action: "allow" });
244
+ // The prompt should have been called (for uncovered /outside/b.ts)
245
+ expect(deps.promptPermission).toHaveBeenCalled();
246
+ });
247
+ });
@@ -0,0 +1,320 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+
3
+ import { evaluateExternalDirectoryGate } from "../../../src/handlers/gates/external-directory";
4
+ 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
+ import type { PermissionCheckResult } from "../../../src/types";
8
+
9
+ // ── helpers ────────────────────────────────────────────────────────────────
10
+
11
+ function makeTcc(overrides: Partial<ToolCallContext> = {}): ToolCallContext {
12
+ return {
13
+ toolName: "read",
14
+ agentName: null,
15
+ input: { path: "/outside/project/file.ts" },
16
+ toolCallId: "tc-1",
17
+ cwd: "/test/project",
18
+ ...overrides,
19
+ };
20
+ }
21
+
22
+ function makeCheckResult(
23
+ state: "allow" | "deny" | "ask",
24
+ overrides: Partial<PermissionCheckResult> = {},
25
+ ): PermissionCheckResult {
26
+ return {
27
+ state,
28
+ toolName: "external_directory",
29
+ source: "special",
30
+ origin: "builtin",
31
+ ...overrides,
32
+ };
33
+ }
34
+
35
+ function makeEvents(): PermissionEventBus {
36
+ return { emit: vi.fn(), on: vi.fn().mockReturnValue(() => undefined) };
37
+ }
38
+
39
+ function makeRuntime(
40
+ overrides: Record<string, unknown> = {},
41
+ ): HandlerDeps["runtime"] {
42
+ return {
43
+ piInfrastructureDirs: ["/test/agent", "/test/agent/git"],
44
+ config: { debugLog: false, permissionReviewLog: true, yoloMode: false },
45
+ runtimeContext: {} as HandlerDeps["runtime"]["runtimeContext"],
46
+ permissionManager: {
47
+ checkPermission: vi.fn().mockReturnValue(makeCheckResult("ask")),
48
+ },
49
+ sessionRules: {
50
+ approve: vi.fn(),
51
+ getRuleset: vi.fn().mockReturnValue([]),
52
+ clear: vi.fn(),
53
+ },
54
+ writeReviewLog: vi.fn(),
55
+ ...overrides,
56
+ } as unknown as HandlerDeps["runtime"];
57
+ }
58
+
59
+ function makeDeps(overrides: Record<string, unknown> = {}): HandlerDeps {
60
+ const { runtime: runtimeOverrides, events, ...rest } = overrides;
61
+ return {
62
+ runtime: makeRuntime(runtimeOverrides as Record<string, unknown>),
63
+ events: events ?? makeEvents(),
64
+ canRequestPermissionConfirmation: vi.fn().mockReturnValue(true),
65
+ promptPermission: vi
66
+ .fn()
67
+ .mockResolvedValue({ approved: true, state: "approved" }),
68
+ ...rest,
69
+ } as unknown as HandlerDeps;
70
+ }
71
+
72
+ // ── tests ──────────────────────────────────────────────────────────────────
73
+
74
+ describe("evaluateExternalDirectoryGate", () => {
75
+ it("returns null when no CWD", async () => {
76
+ const tcc = makeTcc({ cwd: undefined });
77
+ const result = await evaluateExternalDirectoryGate(tcc, makeDeps());
78
+ expect(result).toBeNull();
79
+ });
80
+
81
+ it("returns null when tool is not path-bearing", async () => {
82
+ const tcc = makeTcc({ toolName: "bash", input: { command: "ls" } });
83
+ const result = await evaluateExternalDirectoryGate(tcc, makeDeps());
84
+ expect(result).toBeNull();
85
+ });
86
+
87
+ it("returns null when path is inside CWD", async () => {
88
+ const tcc = makeTcc({ input: { path: "/test/project/src/index.ts" } });
89
+ const result = await evaluateExternalDirectoryGate(tcc, makeDeps());
90
+ expect(result).toBeNull();
91
+ });
92
+
93
+ // ── Pi infrastructure read bypass ──────────────────────────────────────
94
+
95
+ it("allows and emits infrastructure_auto_allowed for read targeting infra dir", async () => {
96
+ const events = makeEvents();
97
+ const deps = makeDeps({ events });
98
+ const tcc = makeTcc({
99
+ toolName: "read",
100
+ input: { path: "/test/agent/git/some-package/SKILL.md" },
101
+ });
102
+ const result = await evaluateExternalDirectoryGate(tcc, deps);
103
+ expect(result).toEqual({ action: "allow" });
104
+ expect(events.emit).toHaveBeenCalledWith(
105
+ "permissions:decision",
106
+ expect.objectContaining({
107
+ resolution: "infrastructure_auto_allowed",
108
+ result: "allow",
109
+ }),
110
+ );
111
+ });
112
+
113
+ it("respects config.piInfrastructureReadPaths for bypass", async () => {
114
+ const events = makeEvents();
115
+ const deps = makeDeps({
116
+ runtime: {
117
+ piInfrastructureDirs: [],
118
+ config: {
119
+ debugLog: false,
120
+ permissionReviewLog: true,
121
+ yoloMode: false,
122
+ piInfrastructureReadPaths: ["/custom/infra"],
123
+ },
124
+ },
125
+ events,
126
+ });
127
+ const tcc = makeTcc({
128
+ toolName: "read",
129
+ input: { path: "/custom/infra/SKILL.md" },
130
+ });
131
+ const result = await evaluateExternalDirectoryGate(tcc, deps);
132
+ expect(result).toEqual({ action: "allow" });
133
+ });
134
+
135
+ it("does NOT bypass for write tools targeting infra dirs", async () => {
136
+ const deps = makeDeps({
137
+ runtime: {
138
+ permissionManager: {
139
+ checkPermission: vi.fn().mockReturnValue(makeCheckResult("deny")),
140
+ },
141
+ },
142
+ });
143
+ const tcc = makeTcc({
144
+ toolName: "write",
145
+ input: { path: "/test/agent/git/some-file.ts", content: "x" },
146
+ });
147
+ const result = await evaluateExternalDirectoryGate(tcc, deps);
148
+ expect(result).toMatchObject({ action: "block" });
149
+ });
150
+
151
+ // ── Session-rule hit ─────────────────────────────────────────────────────
152
+
153
+ it("allows and emits session_approved when session rule covers the path", async () => {
154
+ const events = makeEvents();
155
+ const deps = makeDeps({
156
+ runtime: {
157
+ permissionManager: {
158
+ checkPermission: vi.fn().mockReturnValue(
159
+ makeCheckResult("allow", {
160
+ source: "session",
161
+ matchedPattern: "/outside/project/*",
162
+ }),
163
+ ),
164
+ },
165
+ sessionRules: {
166
+ approve: vi.fn(),
167
+ getRuleset: vi.fn().mockReturnValue([
168
+ {
169
+ surface: "external_directory",
170
+ pattern: "/outside/project/*",
171
+ action: "allow",
172
+ },
173
+ ]),
174
+ clear: vi.fn(),
175
+ },
176
+ },
177
+ events,
178
+ });
179
+ const tcc = makeTcc();
180
+ const result = await evaluateExternalDirectoryGate(tcc, deps);
181
+ expect(result).toEqual({ action: "allow" });
182
+ expect(events.emit).toHaveBeenCalledWith(
183
+ "permissions:decision",
184
+ expect.objectContaining({
185
+ resolution: "session_approved",
186
+ matchedPattern: "/outside/project/*",
187
+ }),
188
+ );
189
+ });
190
+
191
+ // ── Policy deny ──────────────────────────────────────────────────────────
192
+
193
+ it("blocks and emits policy_deny when policy is deny", async () => {
194
+ const events = makeEvents();
195
+ const deps = makeDeps({
196
+ runtime: {
197
+ permissionManager: {
198
+ checkPermission: vi.fn().mockReturnValue(makeCheckResult("deny")),
199
+ },
200
+ },
201
+ events,
202
+ });
203
+ const tcc = makeTcc();
204
+ const result = await evaluateExternalDirectoryGate(tcc, deps);
205
+ expect(result).toMatchObject({ action: "block" });
206
+ expect(events.emit).toHaveBeenCalledWith(
207
+ "permissions:decision",
208
+ expect.objectContaining({
209
+ surface: "external_directory",
210
+ result: "deny",
211
+ resolution: "policy_deny",
212
+ }),
213
+ );
214
+ });
215
+
216
+ // ── Policy ask — user approves once ──────────────────────────────────────
217
+
218
+ it("allows without recording session rule when user approves once", async () => {
219
+ const sessionRules = {
220
+ approve: vi.fn(),
221
+ getRuleset: vi.fn().mockReturnValue([]),
222
+ clear: vi.fn(),
223
+ };
224
+ const deps = makeDeps({
225
+ runtime: {
226
+ permissionManager: {
227
+ checkPermission: vi.fn().mockReturnValue(makeCheckResult("ask")),
228
+ },
229
+ sessionRules,
230
+ },
231
+ promptPermission: vi
232
+ .fn()
233
+ .mockResolvedValue({ approved: true, state: "approved" }),
234
+ });
235
+ const tcc = makeTcc();
236
+ const result = await evaluateExternalDirectoryGate(tcc, deps);
237
+ expect(result).toEqual({ action: "allow" });
238
+ expect(sessionRules.approve).not.toHaveBeenCalled();
239
+ });
240
+
241
+ // ── Policy ask — user approves for session ───────────────────────────────
242
+
243
+ it("records session rule when user approves for session", async () => {
244
+ const sessionRules = {
245
+ approve: vi.fn(),
246
+ getRuleset: vi.fn().mockReturnValue([]),
247
+ clear: vi.fn(),
248
+ };
249
+ const deps = makeDeps({
250
+ runtime: {
251
+ permissionManager: {
252
+ checkPermission: vi.fn().mockReturnValue(makeCheckResult("ask")),
253
+ },
254
+ sessionRules,
255
+ },
256
+ promptPermission: vi
257
+ .fn()
258
+ .mockResolvedValue({ approved: true, state: "approved_for_session" }),
259
+ });
260
+ const tcc = makeTcc();
261
+ const result = await evaluateExternalDirectoryGate(tcc, deps);
262
+ expect(result).toEqual({ action: "allow" });
263
+ expect(sessionRules.approve).toHaveBeenCalledWith(
264
+ "external_directory",
265
+ expect.any(String),
266
+ );
267
+ });
268
+
269
+ // ── Policy ask — user denies ─────────────────────────────────────────────
270
+
271
+ it("blocks and emits user_denied when user denies", async () => {
272
+ const events = makeEvents();
273
+ const deps = makeDeps({
274
+ runtime: {
275
+ permissionManager: {
276
+ checkPermission: vi.fn().mockReturnValue(makeCheckResult("ask")),
277
+ },
278
+ },
279
+ events,
280
+ promptPermission: vi
281
+ .fn()
282
+ .mockResolvedValue({ approved: false, state: "denied" }),
283
+ });
284
+ const tcc = makeTcc();
285
+ const result = await evaluateExternalDirectoryGate(tcc, deps);
286
+ expect(result).toMatchObject({ action: "block" });
287
+ expect(events.emit).toHaveBeenCalledWith(
288
+ "permissions:decision",
289
+ expect.objectContaining({
290
+ result: "deny",
291
+ resolution: "user_denied",
292
+ }),
293
+ );
294
+ });
295
+
296
+ // ── Policy ask — no UI ───────────────────────────────────────────────────
297
+
298
+ it("blocks and emits confirmation_unavailable when no UI", async () => {
299
+ const events = makeEvents();
300
+ const deps = makeDeps({
301
+ runtime: {
302
+ permissionManager: {
303
+ checkPermission: vi.fn().mockReturnValue(makeCheckResult("ask")),
304
+ },
305
+ },
306
+ events,
307
+ canRequestPermissionConfirmation: vi.fn().mockReturnValue(false),
308
+ });
309
+ const tcc = makeTcc();
310
+ const result = await evaluateExternalDirectoryGate(tcc, deps);
311
+ expect(result).toMatchObject({ action: "block" });
312
+ expect(events.emit).toHaveBeenCalledWith(
313
+ "permissions:decision",
314
+ expect.objectContaining({
315
+ result: "deny",
316
+ resolution: "confirmation_unavailable",
317
+ }),
318
+ );
319
+ });
320
+ });
@@ -0,0 +1,71 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import {
4
+ deriveDecisionValue,
5
+ deriveResolution,
6
+ } from "../../../src/handlers/gates/helpers";
7
+
8
+ describe("deriveDecisionValue", () => {
9
+ it("returns command for bash", () => {
10
+ expect(deriveDecisionValue("bash", { command: "git status" })).toBe(
11
+ "git status",
12
+ );
13
+ });
14
+
15
+ it("falls back to toolName when bash has no command", () => {
16
+ expect(deriveDecisionValue("bash", {})).toBe("bash");
17
+ });
18
+
19
+ it("returns target for mcp", () => {
20
+ expect(deriveDecisionValue("mcp", { target: "exa:search" })).toBe(
21
+ "exa:search",
22
+ );
23
+ });
24
+
25
+ it("falls back to toolName when mcp has no target", () => {
26
+ expect(deriveDecisionValue("mcp", {})).toBe("mcp");
27
+ });
28
+
29
+ it("returns toolName for other tools", () => {
30
+ expect(deriveDecisionValue("read", {})).toBe("read");
31
+ expect(deriveDecisionValue("write", { command: "ignored" })).toBe("write");
32
+ });
33
+ });
34
+
35
+ describe("deriveResolution", () => {
36
+ it("returns policy_allow for allow state", () => {
37
+ expect(deriveResolution("allow", "allow", false, true)).toBe(
38
+ "policy_allow",
39
+ );
40
+ });
41
+
42
+ it("returns policy_deny for deny state", () => {
43
+ expect(deriveResolution("deny", "block", false, true)).toBe("policy_deny");
44
+ });
45
+
46
+ it("returns user_approved for ask + allow without session", () => {
47
+ expect(deriveResolution("ask", "allow", false, true)).toBe("user_approved");
48
+ });
49
+
50
+ it("returns user_approved_for_session for ask + allow with session", () => {
51
+ expect(deriveResolution("ask", "allow", true, true)).toBe(
52
+ "user_approved_for_session",
53
+ );
54
+ });
55
+
56
+ it("returns auto_approved when autoApproved flag is set", () => {
57
+ expect(deriveResolution("ask", "allow", false, true, true)).toBe(
58
+ "auto_approved",
59
+ );
60
+ });
61
+
62
+ it("returns user_denied for ask + block with canConfirm", () => {
63
+ expect(deriveResolution("ask", "block", false, true)).toBe("user_denied");
64
+ });
65
+
66
+ it("returns confirmation_unavailable for ask + block without canConfirm", () => {
67
+ expect(deriveResolution("ask", "block", false, false)).toBe(
68
+ "confirmation_unavailable",
69
+ );
70
+ });
71
+ });