


























11// Copilot tests cover permission bridge plugin behavior.
22import type {
33PermissionRequest as SdkPermissionRequest,
4-PermissionRequestResult as SdkPermissionRequestResult,
54} from "@github/copilot-sdk";
65import { describe, expect, it, vi } from "vitest";
76import {
8-allowListPolicy,
9-allowOncePolicy,
10-composePolicies,
117createPermissionBridge,
12-delegatingPolicy,
138rejectAllPolicy,
149REJECT_ALL_FEEDBACK,
1510type CopilotPermissionContext,
@@ -52,168 +47,9 @@ describe("rejectAllPolicy", () => {
5247});
5348});
544955-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-21450describe("createPermissionBridge", () => {
21551it("adapts a policy to the SDK PermissionHandler shape", async () => {
216-const handler = createPermissionBridge(allowOncePolicy);
52+const handler = createPermissionBridge(() => ({ kind: "approve-once" }));
21753const result = await handler(makeRequest(), { sessionId: "sess-1" });
21854expect(result).toEqual({ kind: "approve-once" });
21955});
@@ -251,7 +87,7 @@ describe("createPermissionBridge", () => {
25187});
2528825389it("handles all SDK permission kinds without throwing", async () => {
254-const handler = createPermissionBridge(allowOncePolicy);
90+const handler = createPermissionBridge(() => ({ kind: "approve-once" }));
25591for (const kind of [
25692"shell",
25793"write",
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。