mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-26 20:35:39 -06:00
fix(gateway): async trajectory export approvals
This commit is contained in:
@@ -1069,6 +1069,51 @@ EOF`,
|
||||
expect(approvalInput?.outcome?.exitCode).toBe(0);
|
||||
});
|
||||
|
||||
it("uses async agent followups for explicit webchat approval mode", async () => {
|
||||
buildExecApprovalFollowupTargetMock.mockImplementation((value) => value);
|
||||
resolveExecHostApprovalContextMock.mockReturnValue({
|
||||
approvals: { allowlist: [], file: { version: 1, agents: {} } },
|
||||
hostSecurity: "allowlist",
|
||||
hostAsk: "always",
|
||||
askFallback: "deny",
|
||||
});
|
||||
resolveApprovalDecisionOrUndefinedMock.mockResolvedValue("allow-once");
|
||||
createExecApprovalDecisionStateMock.mockReturnValue({
|
||||
baseDecision: { timedOut: false },
|
||||
approvedByAsk: true,
|
||||
deniedReason: null,
|
||||
});
|
||||
runExecProcessMock.mockResolvedValue({
|
||||
session: { id: "sess-1" },
|
||||
promise: Promise.resolve({
|
||||
status: "completed",
|
||||
exitCode: 0,
|
||||
timedOut: false,
|
||||
aggregated: "done",
|
||||
}),
|
||||
});
|
||||
|
||||
const result = await runGatewayAllowlist({
|
||||
command: "openclaw sessions export-trajectory --json",
|
||||
approvalFollowupMode: "agent",
|
||||
sessionId: "approval-session",
|
||||
sessionStore: "/tmp/openclaw-sessions.json",
|
||||
turnSourceChannel: "webchat",
|
||||
});
|
||||
|
||||
expect(result.pendingResult?.details.status).toBe("approval-pending");
|
||||
await vi.waitFor(() => {
|
||||
expect(sendExecApprovalFollowupResultMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
expect(requireBuildFollowupTargetInput(0)).toMatchObject({
|
||||
direct: false,
|
||||
expectedSessionId: "approval-session",
|
||||
sessionStore: "/tmp/openclaw-sessions.json",
|
||||
});
|
||||
expect(requireSentFollowupTarget(0)?.direct).toBe(false);
|
||||
expect(requireSentFollowupText(0)).toContain("done");
|
||||
});
|
||||
|
||||
it("waits inline for webchat approval so the exec tool can return real output to the model", async () => {
|
||||
resolveApprovalDecisionOrUndefinedMock.mockResolvedValue("allow-once");
|
||||
createExecApprovalDecisionStateMock.mockReturnValue({
|
||||
|
||||
@@ -295,7 +295,7 @@ function shouldAwaitGatewayApprovalInline(params: {
|
||||
turnSourceChannel?: string;
|
||||
approvalFollowupMode?: "agent" | "direct";
|
||||
}): boolean {
|
||||
if (params.approvalFollowupMode === "direct") {
|
||||
if (params.approvalFollowupMode !== undefined) {
|
||||
return false;
|
||||
}
|
||||
return normalizeMessageChannel(params.turnSourceChannel) === INTERNAL_MESSAGE_CHANNEL;
|
||||
|
||||
@@ -89,7 +89,11 @@ function makeTempDir(): string {
|
||||
|
||||
function makeParams(workspaceDir = makeTempDir()): HandleCommandsParams {
|
||||
return {
|
||||
cfg: {},
|
||||
cfg: {
|
||||
session: {
|
||||
store: "/tmp/openclaw-sessions.json",
|
||||
},
|
||||
},
|
||||
ctx: {
|
||||
SessionKey: "agent:main:slash-session",
|
||||
AccountId: "account-1",
|
||||
@@ -370,6 +374,9 @@ describe("buildExportTrajectoryCommandReply", () => {
|
||||
expect(execCall.defaults.security).toBe("allowlist");
|
||||
expect(execCall.defaults.ask).toBe("always");
|
||||
expect(execCall.defaults.trigger).toBe("export-trajectory");
|
||||
expect(execCall.defaults.approvalFollowupMode).toBe("agent");
|
||||
expect(execCall.defaults.sessionId).toBe("session-1");
|
||||
expect(execCall.defaults.sessionStore).toBe("/tmp/openclaw-sessions.json");
|
||||
expect(execCall.defaults.currentChannelId).toBe("bot");
|
||||
expect(execCall.defaults.accountId).toBe("account-1");
|
||||
expect(execCall.params.security).toBe("allowlist");
|
||||
|
||||
@@ -244,10 +244,13 @@ async function requestTrajectoryExportApproval(
|
||||
trigger: "export-trajectory",
|
||||
scopeKey: EXPORT_TRAJECTORY_EXEC_SCOPE_KEY,
|
||||
allowBackground: true,
|
||||
approvalFollowupMode: "agent",
|
||||
timeoutSec,
|
||||
cwd: params.workspaceDir,
|
||||
agentId,
|
||||
sessionKey: params.sessionKey,
|
||||
sessionId: params.sessionEntry?.sessionId,
|
||||
sessionStore: params.cfg.session?.store,
|
||||
mainKey: params.cfg.session?.mainKey,
|
||||
sessionScope: params.cfg.session?.scope,
|
||||
messageProvider: options.privateApprovalTarget?.channel ?? params.command.channel,
|
||||
|
||||
@@ -229,10 +229,24 @@ function getTrajectoryExportApprovalCommands(entry: TrajectoryExportApprovalEntr
|
||||
].filter((value): value is string => typeof value === "string" && value.trim().length > 0);
|
||||
}
|
||||
|
||||
function hasTrajectoryExportArgv(argv: string[] | undefined): boolean {
|
||||
if (!argv) {
|
||||
return false;
|
||||
}
|
||||
return argv.some((arg, index) => arg === "sessions" && argv[index + 1] === "export-trajectory");
|
||||
}
|
||||
|
||||
function isTrajectoryExportApproval(entry: TrajectoryExportApprovalEntry): boolean {
|
||||
return getTrajectoryExportApprovalCommands(entry).some((command) =>
|
||||
command.includes("sessions export-trajectory"),
|
||||
);
|
||||
if (
|
||||
hasTrajectoryExportArgv(entry.request?.commandArgv) ||
|
||||
hasTrajectoryExportArgv(entry.commandArgv)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return getTrajectoryExportApprovalCommands(entry).some((command) => {
|
||||
const normalized = command.replaceAll(/['"]/gu, "");
|
||||
return normalized.includes("sessions export-trajectory");
|
||||
});
|
||||
}
|
||||
|
||||
function summarizeTrajectoryExportApproval(
|
||||
@@ -401,9 +415,7 @@ async function approveTrajectoryExport(
|
||||
`expected trajectory export approval id; approvals=${JSON.stringify(lastApprovalSummaries)}`,
|
||||
);
|
||||
}
|
||||
expect(getTrajectoryExportApprovalCommands(approval).join("\n")).toContain(
|
||||
"sessions export-trajectory",
|
||||
);
|
||||
expect(isTrajectoryExportApproval(approval)).toBe(true);
|
||||
await client.request(
|
||||
"exec.approval.resolve",
|
||||
{ id: approval.id, decision: "allow-once" },
|
||||
|
||||
Reference in New Issue
Block a user