|
1 | 1 | // Copilot tests cover permission bridge plugin behavior. |
2 | 2 | import type { |
3 | 3 | PermissionRequest as SdkPermissionRequest, |
4 | | - PermissionRequestResult as SdkPermissionRequestResult, |
5 | 4 | } from "@github/copilot-sdk"; |
6 | 5 | import { describe, expect, it, vi } from "vitest"; |
7 | 6 | import { |
8 | | - allowListPolicy, |
9 | | - allowOncePolicy, |
10 | | - composePolicies, |
11 | 7 | createPermissionBridge, |
12 | | - delegatingPolicy, |
13 | 8 | rejectAllPolicy, |
14 | 9 | REJECT_ALL_FEEDBACK, |
15 | 10 | type CopilotPermissionContext, |
@@ -52,168 +47,9 @@ describe("rejectAllPolicy", () => { |
52 | 47 | }); |
53 | 48 | }); |
54 | 49 |
|
55 | | -describe("allowOncePolicy", () => { |
56 | | - it("returns approve-once for every request kind", async () => { |
57 | | - for (const kind of [ |
58 | | - "shell", |
59 | | - "write", |
60 | | - "mcp", |
61 | | - "read", |
62 | | - "url", |
63 | | - "custom-tool", |
64 | | - "memory", |
65 | | - "hook", |
66 | | - ] as const) { |
67 | | - const result = await allowOncePolicy(makeCtx({ request: makeRequest({ kind }) })); |
68 | | - expect(result).toEqual({ kind: "approve-once" }); |
69 | | - } |
70 | | - }); |
71 | | -}); |
72 | | - |
73 | | -describe("allowListPolicy", () => { |
74 | | - it("approves listed kinds and rejects others with default feedback", async () => { |
75 | | - const policy = allowListPolicy({ kinds: ["read"] }); |
76 | | - const approved = await policy(makeCtx({ request: makeRequest({ kind: "read" }) })); |
77 | | - expect(approved).toEqual({ kind: "approve-once" }); |
78 | | - const rejected = await policy(makeCtx({ request: makeRequest({ kind: "shell" }) })); |
79 | | - expect(rejected).toEqual({ kind: "reject", feedback: REJECT_ALL_FEEDBACK }); |
80 | | - }); |
81 | | - |
82 | | - it("uses custom rejectFeedback when provided", async () => { |
83 | | - const policy = allowListPolicy({ |
84 | | - kinds: ["read"], |
85 | | - rejectFeedback: "only reads allowed", |
86 | | - }); |
87 | | - const result = await policy(makeCtx({ request: makeRequest({ kind: "write" }) })); |
88 | | - expect(result).toEqual({ kind: "reject", feedback: "only reads allowed" }); |
89 | | - }); |
90 | | - |
91 | | - it("supports multiple kinds in the allow-list", async () => { |
92 | | - const policy = allowListPolicy({ kinds: ["read", "write"] }); |
93 | | - expect(await policy(makeCtx({ request: makeRequest({ kind: "read" }) }))).toEqual({ |
94 | | - kind: "approve-once", |
95 | | - }); |
96 | | - expect(await policy(makeCtx({ request: makeRequest({ kind: "write" }) }))).toEqual({ |
97 | | - kind: "approve-once", |
98 | | - }); |
99 | | - expect((await policy(makeCtx({ request: makeRequest({ kind: "mcp" }) })))?.kind).toBe("reject"); |
100 | | - }); |
101 | | - |
102 | | - it("rejects all when given an empty allow-list", async () => { |
103 | | - const policy = allowListPolicy({ kinds: [] }); |
104 | | - for (const kind of ["shell", "read", "write"] as const) { |
105 | | - const result = await policy(makeCtx({ request: makeRequest({ kind }) })); |
106 | | - expect(result?.kind).toBe("reject"); |
107 | | - } |
108 | | - }); |
109 | | -}); |
110 | | - |
111 | | -describe("delegatingPolicy", () => { |
112 | | - it("forwards the request to the host callback and returns its decision", async () => { |
113 | | - const onRequest = vi.fn<CopilotPermissionPolicy>().mockResolvedValue({ |
114 | | - kind: "approve-for-session", |
115 | | - } satisfies SdkPermissionRequestResult); |
116 | | - const policy = delegatingPolicy({ onRequest }); |
117 | | - const ctx = makeCtx({ sessionId: "sess-xyz", request: makeRequest({ kind: "write" }) }); |
118 | | - const result = await policy(ctx); |
119 | | - expect(result).toEqual({ kind: "approve-for-session" }); |
120 | | - expect(onRequest).toHaveBeenCalledTimes(1); |
121 | | - expect(onRequest).toHaveBeenCalledWith(ctx); |
122 | | - }); |
123 | | - |
124 | | - it("returns the rejectAll default when host callback returns undefined", async () => { |
125 | | - const onRequest = vi.fn<CopilotPermissionPolicy>().mockResolvedValue(undefined); |
126 | | - const policy = delegatingPolicy({ onRequest }); |
127 | | - const result = await policy(makeCtx()); |
128 | | - expect(result).toEqual({ kind: "reject", feedback: REJECT_ALL_FEEDBACK }); |
129 | | - }); |
130 | | - |
131 | | - it("rejects with the error message when host callback throws", async () => { |
132 | | - const onRequest = vi |
133 | | - .fn<CopilotPermissionPolicy>() |
134 | | - .mockRejectedValue(new Error("host policy boom")); |
135 | | - const policy = delegatingPolicy({ onRequest }); |
136 | | - const result = await policy(makeCtx()); |
137 | | - expect(result?.kind).toBe("reject"); |
138 | | - expect((result as { feedback?: string }).feedback).toContain("host policy boom"); |
139 | | - }); |
140 | | - |
141 | | - it("falls back to onError policy when host callback throws", async () => { |
142 | | - const onError = vi.fn<CopilotPermissionPolicy>().mockResolvedValue({ kind: "approve-once" }); |
143 | | - const policy = delegatingPolicy({ |
144 | | - onRequest: () => { |
145 | | - throw new Error("host policy boom"); |
146 | | - }, |
147 | | - onError, |
148 | | - }); |
149 | | - const result = await policy(makeCtx()); |
150 | | - expect(result).toEqual({ kind: "approve-once" }); |
151 | | - expect(onError).toHaveBeenCalledTimes(1); |
152 | | - }); |
153 | | - |
154 | | - it("falls through to a hard-coded reject if onError also throws", async () => { |
155 | | - const policy = delegatingPolicy({ |
156 | | - onRequest: () => { |
157 | | - throw new Error("host boom"); |
158 | | - }, |
159 | | - onError: () => { |
160 | | - throw new Error("fallback boom"); |
161 | | - }, |
162 | | - }); |
163 | | - const result = await policy(makeCtx()); |
164 | | - expect(result?.kind).toBe("reject"); |
165 | | - expect((result as { feedback?: string }).feedback).toContain("host boom"); |
166 | | - }); |
167 | | - |
168 | | - it("formats non-Error throws via JSON.stringify", async () => { |
169 | | - const policy = delegatingPolicy({ |
170 | | - onRequest: () => { |
171 | | - throw { code: 42, msg: "weird" } as unknown as Error; |
172 | | - }, |
173 | | - }); |
174 | | - const result = await policy(makeCtx()); |
175 | | - expect((result as { feedback?: string }).feedback).toContain('"code":42'); |
176 | | - }); |
177 | | -}); |
178 | | - |
179 | | -describe("composePolicies", () => { |
180 | | - it("returns the first non-undefined result and skips subsequent policies", async () => { |
181 | | - const a: CopilotPermissionPolicy = () => undefined; |
182 | | - const b: CopilotPermissionPolicy = () => ({ kind: "approve-once" }); |
183 | | - const c = vi.fn<CopilotPermissionPolicy>(() => ({ |
184 | | - kind: "reject", |
185 | | - feedback: "should never run", |
186 | | - })); |
187 | | - const policy = composePolicies(a, b, c); |
188 | | - const result = await policy(makeCtx()); |
189 | | - expect(result).toEqual({ kind: "approve-once" }); |
190 | | - expect(c).not.toHaveBeenCalled(); |
191 | | - }); |
192 | | - |
193 | | - it("falls through to fail-closed reject when all policies return undefined", async () => { |
194 | | - const policy = composePolicies( |
195 | | - () => undefined, |
196 | | - () => undefined, |
197 | | - ); |
198 | | - const result = await policy(makeCtx()); |
199 | | - expect(result).toEqual({ kind: "reject", feedback: REJECT_ALL_FEEDBACK }); |
200 | | - }); |
201 | | - |
202 | | - it("short-circuits to reject if any policy throws (does not consult later policies)", async () => { |
203 | | - const later = vi.fn<CopilotPermissionPolicy>(() => ({ kind: "approve-once" })); |
204 | | - const policy = composePolicies(() => { |
205 | | - throw new Error("nope"); |
206 | | - }, later); |
207 | | - const result = await policy(makeCtx()); |
208 | | - expect(result?.kind).toBe("reject"); |
209 | | - expect((result as { feedback?: string }).feedback).toContain("nope"); |
210 | | - expect(later).not.toHaveBeenCalled(); |
211 | | - }); |
212 | | -}); |
213 | | - |
214 | 50 | describe("createPermissionBridge", () => { |
215 | 51 | it("adapts a policy to the SDK PermissionHandler shape", async () => { |
216 | | - const handler = createPermissionBridge(allowOncePolicy); |
| 52 | + const handler = createPermissionBridge(() => ({ kind: "approve-once" })); |
217 | 53 | const result = await handler(makeRequest(), { sessionId: "sess-1" }); |
218 | 54 | expect(result).toEqual({ kind: "approve-once" }); |
219 | 55 | }); |
@@ -251,7 +87,7 @@ describe("createPermissionBridge", () => { |
251 | 87 | }); |
252 | 88 |
|
253 | 89 | it("handles all SDK permission kinds without throwing", async () => { |
254 | | - const handler = createPermissionBridge(allowOncePolicy); |
| 90 | + const handler = createPermissionBridge(() => ({ kind: "approve-once" })); |
255 | 91 | for (const kind of [ |
256 | 92 | "shell", |
257 | 93 | "write", |
|
0 commit comments