mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-24 10:12:44 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2920d61351 |
+50
@@ -0,0 +1,50 @@
|
||||
import { expect, test } from "vitest";
|
||||
import { mkdtemp, rm } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import pino from "pino";
|
||||
import { ClaudeAgentClient } from "./agent.js";
|
||||
import { withTimeout } from "../../../../utils/promise-timeout.js";
|
||||
|
||||
// Explicit real-provider coverage, separate from the default unit suite.
|
||||
test("answers a real plan approval synchronously from the permission subscriber", async () => {
|
||||
const cwd = await mkdtemp(path.join(tmpdir(), "paseo-permission-event-"));
|
||||
const session = await new ClaudeAgentClient({ logger: pino({ level: "silent" }) }).createSession(
|
||||
{ provider: "claude", cwd, model: "haiku", modeId: "plan" },
|
||||
undefined,
|
||||
{ persistSession: false },
|
||||
);
|
||||
let requestName: string | undefined;
|
||||
let complete!: () => void;
|
||||
let fail!: (error: unknown) => void;
|
||||
const responded = new Promise<void>((resolve, reject) => {
|
||||
complete = resolve;
|
||||
fail = reject;
|
||||
});
|
||||
void responded.catch(() => undefined);
|
||||
session.subscribe((event) => {
|
||||
if (event.type === "permission_requested") {
|
||||
requestName = event.request.name;
|
||||
void session
|
||||
.respondToPermission(event.request.id, {
|
||||
behavior: "deny",
|
||||
message: "End verification here",
|
||||
})
|
||||
.then(complete, fail);
|
||||
}
|
||||
if (event.type === "turn_failed") fail(new Error(JSON.stringify(event)));
|
||||
});
|
||||
try {
|
||||
await session.startTurn(
|
||||
"Make a one-line plan to create hello.txt containing hello, then call ExitPlanMode now to request approval. Do not use other tools or ask questions.",
|
||||
);
|
||||
await expect(
|
||||
withTimeout(responded, 90_000, "No permission within 90 seconds"),
|
||||
).resolves.toBeUndefined();
|
||||
expect(requestName).toBe("ExitPlanMode");
|
||||
expect(session.getPendingPermissions()).toEqual([]);
|
||||
} finally {
|
||||
await session.close();
|
||||
await rm(cwd, { recursive: true, force: true });
|
||||
}
|
||||
}, 120_000);
|
||||
@@ -0,0 +1,79 @@
|
||||
import { expect, test } from "vitest";
|
||||
import type { CanUseTool, Query } from "@anthropic-ai/claude-agent-sdk";
|
||||
import type { AgentStreamEvent } from "../../agent-sdk-types.js";
|
||||
import { createTestLogger } from "../../../../test-utils/test-logger.js";
|
||||
import { ClaudeAgentClient } from "./agent.js";
|
||||
|
||||
// Drive the SDK's permission callback through the injected query dependency.
|
||||
// Subscribers and permission state use the real AgentSession implementation.
|
||||
test.each([
|
||||
["Bash", { command: "echo permission" }],
|
||||
["AskUserQuestion", { questions: [{ question: "Proceed?", header: "Proceed" }] }],
|
||||
["ExitPlanMode", { plan: "Implement the plan" }],
|
||||
])("a subscriber can immediately deny %s", async (toolName, input) => {
|
||||
let requestPermission: CanUseTool | undefined;
|
||||
let finish!: () => void;
|
||||
const finished = new Promise<void>((resolve) => {
|
||||
finish = resolve;
|
||||
});
|
||||
const query = {
|
||||
async next() {
|
||||
await finished;
|
||||
return { done: true, value: undefined };
|
||||
},
|
||||
[Symbol.asyncIterator]() {
|
||||
return this;
|
||||
},
|
||||
close: finish,
|
||||
};
|
||||
const session = await new ClaudeAgentClient({
|
||||
logger: createTestLogger(),
|
||||
resolveBinary: async () => "/test/claude",
|
||||
queryFactory: ({ options }) => {
|
||||
requestPermission = options.canUseTool;
|
||||
return query as Query;
|
||||
},
|
||||
}).createSession({ provider: "claude", cwd: process.cwd(), modeId: "default" });
|
||||
const events: AgentStreamEvent[] = [];
|
||||
let response: Promise<void> | undefined;
|
||||
let pendingAtPublication: string[] = [];
|
||||
session.subscribe((event) => {
|
||||
events.push(event);
|
||||
if (event.type === "permission_requested") {
|
||||
pendingAtPublication = session.getPendingPermissions().map((request) => request.id);
|
||||
response = session.respondToPermission(event.request.id, {
|
||||
behavior: "deny",
|
||||
message: "Denied immediately",
|
||||
});
|
||||
void response.catch(() => undefined);
|
||||
}
|
||||
});
|
||||
try {
|
||||
await session.startTurn("Request permission");
|
||||
if (!requestPermission) throw new Error("SDK permission callback was not configured");
|
||||
const result = requestPermission(toolName, input, {
|
||||
signal: new AbortController().signal,
|
||||
toolUseID: "permission-tool-call",
|
||||
});
|
||||
void result.catch(() => undefined);
|
||||
expect(response).toBeDefined();
|
||||
await expect(response).resolves.toBeUndefined();
|
||||
await expect(result).resolves.toMatchObject({
|
||||
behavior: "deny",
|
||||
message: "Denied immediately",
|
||||
});
|
||||
const requested = events.find((event) => event.type === "permission_requested");
|
||||
if (requested?.type !== "permission_requested") throw new Error("No permission event");
|
||||
expect(pendingAtPublication).toContain(requested.request.id);
|
||||
expect(session.getPendingPermissions()).toEqual([]);
|
||||
expect(
|
||||
events
|
||||
.filter(
|
||||
(event) => event.type === "permission_requested" || event.type === "permission_resolved",
|
||||
)
|
||||
.map((event) => event.type),
|
||||
).toEqual(["permission_requested", "permission_resolved"]);
|
||||
} finally {
|
||||
await session.close();
|
||||
}
|
||||
});
|
||||
@@ -5072,13 +5072,16 @@ class ClaudeAgentSession implements AgentSession {
|
||||
metadata: Object.keys(metadata).length ? metadata : undefined,
|
||||
};
|
||||
|
||||
this.pushEvent({
|
||||
type: "permission_requested",
|
||||
provider: "claude",
|
||||
request,
|
||||
});
|
||||
const publishRequest = () => {
|
||||
this.pushEvent({
|
||||
type: "permission_requested",
|
||||
provider: "claude",
|
||||
request,
|
||||
});
|
||||
};
|
||||
|
||||
if (this.permissionClearingSteerUuids.size > 0) {
|
||||
publishRequest();
|
||||
return this.resolveDeniedPermission(request, {
|
||||
behavior: "deny",
|
||||
message: STEER_SUPERSEDED_PERMISSION_MESSAGE,
|
||||
@@ -5112,6 +5115,7 @@ class ClaudeAgentSession implements AgentSession {
|
||||
|
||||
if (options?.signal) {
|
||||
if (options.signal.aborted) {
|
||||
publishRequest();
|
||||
abortHandler();
|
||||
return;
|
||||
}
|
||||
@@ -5125,6 +5129,9 @@ class ClaudeAgentSession implements AgentSession {
|
||||
reject,
|
||||
cleanup,
|
||||
});
|
||||
// Subscribers can respond synchronously, so the request and its abort
|
||||
// cleanup must be installed before it becomes visible.
|
||||
publishRequest();
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user