mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-07 17:04:42 +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,
|
metadata: Object.keys(metadata).length ? metadata : undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.pushEvent({
|
const publishRequest = () => {
|
||||||
type: "permission_requested",
|
this.pushEvent({
|
||||||
provider: "claude",
|
type: "permission_requested",
|
||||||
request,
|
provider: "claude",
|
||||||
});
|
request,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
if (this.permissionClearingSteerUuids.size > 0) {
|
if (this.permissionClearingSteerUuids.size > 0) {
|
||||||
|
publishRequest();
|
||||||
return this.resolveDeniedPermission(request, {
|
return this.resolveDeniedPermission(request, {
|
||||||
behavior: "deny",
|
behavior: "deny",
|
||||||
message: STEER_SUPERSEDED_PERMISSION_MESSAGE,
|
message: STEER_SUPERSEDED_PERMISSION_MESSAGE,
|
||||||
@@ -5112,6 +5115,7 @@ class ClaudeAgentSession implements AgentSession {
|
|||||||
|
|
||||||
if (options?.signal) {
|
if (options?.signal) {
|
||||||
if (options.signal.aborted) {
|
if (options.signal.aborted) {
|
||||||
|
publishRequest();
|
||||||
abortHandler();
|
abortHandler();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -5125,6 +5129,9 @@ class ClaudeAgentSession implements AgentSession {
|
|||||||
reject,
|
reject,
|
||||||
cleanup,
|
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