mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-30 13:13:25 +00:00
fix(server): resolve canceled permission requests (#3792)
Provider callbacks can be canceled without a user response. Publish a terminal resolution for that path so every surfaced request has a matching lifecycle event, while preserving the existing interruption cleanup.
This commit is contained in:
@@ -24,6 +24,12 @@ interface TestClaudeSession {
|
||||
close(): Promise<void>;
|
||||
}
|
||||
|
||||
function isPermissionResolvedEvent(
|
||||
event: AgentStreamEvent,
|
||||
): event is Extract<AgentStreamEvent, { type: "permission_resolved" }> {
|
||||
return event.type === "permission_resolved";
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
@@ -629,6 +635,80 @@ describe("ClaudeAgentSession features", () => {
|
||||
return { queryFactory, queryMock, launches };
|
||||
}
|
||||
|
||||
test("publishes a resolution when the SDK aborts a permission callback", async () => {
|
||||
const { queryFactory } = createQueryMock();
|
||||
const session = await new ClaudeAgentClient({
|
||||
logger,
|
||||
queryFactory,
|
||||
resolveBinary: async () => "/test/claude/bin",
|
||||
}).createSession({ provider: "claude", cwd: process.cwd(), modeId: "default" });
|
||||
const events: AgentStreamEvent[] = [];
|
||||
const unsubscribe = session.subscribe((event) => events.push(event));
|
||||
|
||||
try {
|
||||
await session.startTurn("run the tool");
|
||||
const canUseTool = queryFactory.mock.calls[0]?.[0].options.canUseTool;
|
||||
if (!canUseTool) throw new Error("Expected canUseTool callback");
|
||||
const abort = new AbortController();
|
||||
const permission = canUseTool(
|
||||
"Bash",
|
||||
{ command: "printf test" },
|
||||
{ signal: abort.signal, toolUseID: "tool-aborted" },
|
||||
);
|
||||
abort.abort();
|
||||
|
||||
await expect(permission).rejects.toThrow("Permission request aborted");
|
||||
expect(events.find(isPermissionResolvedEvent)).toMatchObject({
|
||||
type: "permission_resolved",
|
||||
provider: "claude",
|
||||
requestId: expect.any(String),
|
||||
resolution: { behavior: "deny", message: "Permission request canceled" },
|
||||
});
|
||||
expect(session.getPendingPermissions()).toEqual([]);
|
||||
} finally {
|
||||
unsubscribe();
|
||||
await session.close();
|
||||
}
|
||||
});
|
||||
|
||||
test("does not duplicate a resolution when interruption later aborts the SDK callback", async () => {
|
||||
const { queryFactory } = createQueryMock();
|
||||
const session = await new ClaudeAgentClient({
|
||||
logger,
|
||||
queryFactory,
|
||||
resolveBinary: async () => "/test/claude/bin",
|
||||
}).createSession({ provider: "claude", cwd: process.cwd(), modeId: "default" });
|
||||
const events: AgentStreamEvent[] = [];
|
||||
const unsubscribe = session.subscribe((event) => events.push(event));
|
||||
|
||||
try {
|
||||
await session.startTurn("run the tool");
|
||||
const canUseTool = queryFactory.mock.calls[0]?.[0].options.canUseTool;
|
||||
if (!canUseTool) throw new Error("Expected canUseTool callback");
|
||||
const abort = new AbortController();
|
||||
const permission = canUseTool(
|
||||
"Bash",
|
||||
{ command: "printf test" },
|
||||
{ signal: abort.signal, toolUseID: "tool-interrupted" },
|
||||
);
|
||||
|
||||
await session.interrupt();
|
||||
abort.abort();
|
||||
|
||||
await expect(permission).rejects.toThrow("Permission request canceled");
|
||||
expect(events.filter(isPermissionResolvedEvent)).toEqual([
|
||||
expect.objectContaining({
|
||||
provider: "claude",
|
||||
resolution: { behavior: "deny", message: "Permission request canceled" },
|
||||
}),
|
||||
]);
|
||||
expect(session.getPendingPermissions()).toEqual([]);
|
||||
} finally {
|
||||
unsubscribe();
|
||||
await session.close();
|
||||
}
|
||||
});
|
||||
|
||||
test("passes exact configured Fable 5 IDs through to Claude Code", async () => {
|
||||
const { queryFactory, queryMock } = createQueryMock();
|
||||
const client = new ClaudeAgentClient({
|
||||
|
||||
@@ -2232,7 +2232,7 @@ class ClaudeAgentSession implements AgentSession {
|
||||
if (this.cancelCurrentTurn === requestCancel) {
|
||||
this.cancelCurrentTurn = null;
|
||||
}
|
||||
this.rejectAllPendingPermissions(new Error("Permission request aborted"));
|
||||
this.rejectAllPendingPermissions(new Error("Permission request canceled"));
|
||||
this.finishForegroundTurn({
|
||||
type: "turn_canceled",
|
||||
provider: "claude",
|
||||
@@ -4635,6 +4635,12 @@ class ClaudeAgentSession implements AgentSession {
|
||||
const abortHandler = () => {
|
||||
this.pendingPermissions.delete(requestId);
|
||||
cleanup();
|
||||
this.pushEvent({
|
||||
type: "permission_resolved",
|
||||
provider: "claude",
|
||||
requestId,
|
||||
resolution: { behavior: "deny", message: "Permission request canceled" },
|
||||
});
|
||||
reject(new Error("Permission request aborted"));
|
||||
};
|
||||
|
||||
@@ -4775,6 +4781,12 @@ class ClaudeAgentSession implements AgentSession {
|
||||
pending.cleanup?.();
|
||||
pending.reject(error);
|
||||
this.pendingPermissions.delete(id);
|
||||
this.pushEvent({
|
||||
type: "permission_resolved",
|
||||
provider: "claude",
|
||||
requestId: id,
|
||||
resolution: { behavior: "deny", message: error.message },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user