mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-29 10:02:04 +00:00
refactor(approvals): share native delivery runtime
This commit is contained in:
104
src/infra/approval-native-runtime.test.ts
Normal file
104
src/infra/approval-native-runtime.test.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import type { ChannelApprovalNativeAdapter } from "../channels/plugins/types.adapters.js";
|
||||
import { deliverApprovalRequestViaChannelNativePlan } from "./approval-native-runtime.js";
|
||||
|
||||
const execRequest = {
|
||||
id: "approval-1",
|
||||
request: {
|
||||
command: "uname -a",
|
||||
},
|
||||
createdAtMs: 0,
|
||||
expiresAtMs: 120_000,
|
||||
};
|
||||
|
||||
describe("deliverApprovalRequestViaChannelNativePlan", () => {
|
||||
it("sends an origin notice and dedupes converged prepared targets", async () => {
|
||||
const adapter: ChannelApprovalNativeAdapter = {
|
||||
describeDeliveryCapabilities: () => ({
|
||||
enabled: true,
|
||||
preferredSurface: "approver-dm",
|
||||
supportsOriginSurface: true,
|
||||
supportsApproverDmSurface: true,
|
||||
notifyOriginWhenDmOnly: true,
|
||||
}),
|
||||
resolveOriginTarget: async () => ({ to: "origin-room" }),
|
||||
resolveApproverDmTargets: async () => [{ to: "approver-1" }, { to: "approver-2" }],
|
||||
};
|
||||
const sendOriginNotice = vi.fn().mockResolvedValue(undefined);
|
||||
const prepareTarget = vi
|
||||
.fn()
|
||||
.mockImplementation(
|
||||
async ({ plannedTarget }: { plannedTarget: { target: { to: string } } }) =>
|
||||
plannedTarget.target.to === "approver-1"
|
||||
? {
|
||||
dedupeKey: "shared-dm",
|
||||
target: { channelId: "shared-dm", recipientId: "approver-1" },
|
||||
}
|
||||
: {
|
||||
dedupeKey: "shared-dm",
|
||||
target: { channelId: "shared-dm", recipientId: "approver-2" },
|
||||
},
|
||||
);
|
||||
const deliverTarget = vi
|
||||
.fn()
|
||||
.mockImplementation(
|
||||
async ({ preparedTarget }: { preparedTarget: { channelId: string } }) => ({
|
||||
channelId: preparedTarget.channelId,
|
||||
}),
|
||||
);
|
||||
const onDuplicateSkipped = vi.fn();
|
||||
|
||||
const entries = await deliverApprovalRequestViaChannelNativePlan({
|
||||
cfg: {} as never,
|
||||
approvalKind: "exec",
|
||||
request: execRequest,
|
||||
adapter,
|
||||
sendOriginNotice: async ({ originTarget }) => {
|
||||
await sendOriginNotice(originTarget);
|
||||
},
|
||||
prepareTarget,
|
||||
deliverTarget,
|
||||
onDuplicateSkipped,
|
||||
});
|
||||
|
||||
expect(sendOriginNotice).toHaveBeenCalledWith({ to: "origin-room" });
|
||||
expect(prepareTarget).toHaveBeenCalledTimes(2);
|
||||
expect(deliverTarget).toHaveBeenCalledTimes(1);
|
||||
expect(onDuplicateSkipped).toHaveBeenCalledTimes(1);
|
||||
expect(entries).toEqual([{ channelId: "shared-dm" }]);
|
||||
});
|
||||
|
||||
it("continues after per-target delivery failures", async () => {
|
||||
const adapter: ChannelApprovalNativeAdapter = {
|
||||
describeDeliveryCapabilities: () => ({
|
||||
enabled: true,
|
||||
preferredSurface: "approver-dm",
|
||||
supportsOriginSurface: false,
|
||||
supportsApproverDmSurface: true,
|
||||
}),
|
||||
resolveApproverDmTargets: async () => [{ to: "approver-1" }, { to: "approver-2" }],
|
||||
};
|
||||
const onDeliveryError = vi.fn();
|
||||
|
||||
const entries = await deliverApprovalRequestViaChannelNativePlan({
|
||||
cfg: {} as never,
|
||||
approvalKind: "exec",
|
||||
request: execRequest,
|
||||
adapter,
|
||||
prepareTarget: ({ plannedTarget }) => ({
|
||||
dedupeKey: plannedTarget.target.to,
|
||||
target: { channelId: plannedTarget.target.to },
|
||||
}),
|
||||
deliverTarget: async ({ preparedTarget }) => {
|
||||
if (preparedTarget.channelId === "approver-1") {
|
||||
throw new Error("boom");
|
||||
}
|
||||
return { channelId: preparedTarget.channelId };
|
||||
},
|
||||
onDeliveryError,
|
||||
});
|
||||
|
||||
expect(onDeliveryError).toHaveBeenCalledTimes(1);
|
||||
expect(entries).toEqual([{ channelId: "approver-2" }]);
|
||||
});
|
||||
});
|
||||
154
src/infra/approval-native-runtime.ts
Normal file
154
src/infra/approval-native-runtime.ts
Normal file
@@ -0,0 +1,154 @@
|
||||
import type {
|
||||
ChannelApprovalKind,
|
||||
ChannelApprovalNativeAdapter,
|
||||
ChannelApprovalNativeTarget,
|
||||
} from "../channels/plugins/types.adapters.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import {
|
||||
resolveChannelNativeApprovalDeliveryPlan,
|
||||
type ChannelApprovalNativePlannedTarget,
|
||||
} from "./approval-native-delivery.js";
|
||||
import type { ExecApprovalRequest } from "./exec-approvals.js";
|
||||
import type { PluginApprovalRequest } from "./plugin-approvals.js";
|
||||
|
||||
type ApprovalRequest = ExecApprovalRequest | PluginApprovalRequest;
|
||||
|
||||
export type PreparedChannelNativeApprovalTarget<TPreparedTarget> = {
|
||||
dedupeKey: string;
|
||||
target: TPreparedTarget;
|
||||
};
|
||||
|
||||
function buildTargetKey(target: ChannelApprovalNativeTarget): string {
|
||||
return `${target.to}:${target.threadId == null ? "" : String(target.threadId)}`;
|
||||
}
|
||||
|
||||
export async function deliverApprovalRequestViaChannelNativePlan<
|
||||
TPreparedTarget,
|
||||
TPendingEntry,
|
||||
TRequest extends ApprovalRequest = ApprovalRequest,
|
||||
>(params: {
|
||||
cfg: OpenClawConfig;
|
||||
accountId?: string | null;
|
||||
approvalKind: ChannelApprovalKind;
|
||||
request: TRequest;
|
||||
adapter?: ChannelApprovalNativeAdapter | null;
|
||||
sendOriginNotice?: (params: {
|
||||
originTarget: ChannelApprovalNativeTarget;
|
||||
request: TRequest;
|
||||
}) => Promise<void>;
|
||||
prepareTarget: (params: {
|
||||
plannedTarget: ChannelApprovalNativePlannedTarget;
|
||||
request: TRequest;
|
||||
}) =>
|
||||
| PreparedChannelNativeApprovalTarget<TPreparedTarget>
|
||||
| null
|
||||
| Promise<PreparedChannelNativeApprovalTarget<TPreparedTarget> | null>;
|
||||
deliverTarget: (params: {
|
||||
plannedTarget: ChannelApprovalNativePlannedTarget;
|
||||
preparedTarget: TPreparedTarget;
|
||||
request: TRequest;
|
||||
}) => TPendingEntry | null | Promise<TPendingEntry | null>;
|
||||
onOriginNoticeError?: (params: {
|
||||
error: unknown;
|
||||
originTarget: ChannelApprovalNativeTarget;
|
||||
request: TRequest;
|
||||
}) => void;
|
||||
onDeliveryError?: (params: {
|
||||
error: unknown;
|
||||
plannedTarget: ChannelApprovalNativePlannedTarget;
|
||||
request: TRequest;
|
||||
}) => void;
|
||||
onDuplicateSkipped?: (params: {
|
||||
plannedTarget: ChannelApprovalNativePlannedTarget;
|
||||
preparedTarget: PreparedChannelNativeApprovalTarget<TPreparedTarget>;
|
||||
request: TRequest;
|
||||
}) => void;
|
||||
onDelivered?: (params: {
|
||||
plannedTarget: ChannelApprovalNativePlannedTarget;
|
||||
preparedTarget: PreparedChannelNativeApprovalTarget<TPreparedTarget>;
|
||||
request: TRequest;
|
||||
entry: TPendingEntry;
|
||||
}) => void;
|
||||
}): Promise<TPendingEntry[]> {
|
||||
const deliveryPlan = await resolveChannelNativeApprovalDeliveryPlan({
|
||||
cfg: params.cfg,
|
||||
accountId: params.accountId,
|
||||
approvalKind: params.approvalKind,
|
||||
request: params.request,
|
||||
adapter: params.adapter,
|
||||
});
|
||||
|
||||
const originTargetKey = deliveryPlan.originTarget
|
||||
? buildTargetKey(deliveryPlan.originTarget)
|
||||
: null;
|
||||
const plannedTargetKeys = new Set(
|
||||
deliveryPlan.targets.map((plannedTarget) => buildTargetKey(plannedTarget.target)),
|
||||
);
|
||||
|
||||
if (
|
||||
deliveryPlan.notifyOriginWhenDmOnly &&
|
||||
deliveryPlan.originTarget &&
|
||||
(originTargetKey == null || !plannedTargetKeys.has(originTargetKey))
|
||||
) {
|
||||
try {
|
||||
await params.sendOriginNotice?.({
|
||||
originTarget: deliveryPlan.originTarget,
|
||||
request: params.request,
|
||||
});
|
||||
} catch (error) {
|
||||
params.onOriginNoticeError?.({
|
||||
error,
|
||||
originTarget: deliveryPlan.originTarget,
|
||||
request: params.request,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const deliveredKeys = new Set<string>();
|
||||
const pendingEntries: TPendingEntry[] = [];
|
||||
for (const plannedTarget of deliveryPlan.targets) {
|
||||
try {
|
||||
const preparedTarget = await params.prepareTarget({
|
||||
plannedTarget,
|
||||
request: params.request,
|
||||
});
|
||||
if (!preparedTarget) {
|
||||
continue;
|
||||
}
|
||||
if (deliveredKeys.has(preparedTarget.dedupeKey)) {
|
||||
params.onDuplicateSkipped?.({
|
||||
plannedTarget,
|
||||
preparedTarget,
|
||||
request: params.request,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
const entry = await params.deliverTarget({
|
||||
plannedTarget,
|
||||
preparedTarget: preparedTarget.target,
|
||||
request: params.request,
|
||||
});
|
||||
if (!entry) {
|
||||
continue;
|
||||
}
|
||||
|
||||
deliveredKeys.add(preparedTarget.dedupeKey);
|
||||
pendingEntries.push(entry);
|
||||
params.onDelivered?.({
|
||||
plannedTarget,
|
||||
preparedTarget,
|
||||
request: params.request,
|
||||
entry,
|
||||
});
|
||||
} catch (error) {
|
||||
params.onDeliveryError?.({
|
||||
error,
|
||||
plannedTarget,
|
||||
request: params.request,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return pendingEntries;
|
||||
}
|
||||
@@ -13,6 +13,7 @@ export * from "../infra/exec-approval-reply.ts";
|
||||
export * from "../infra/exec-approval-session-target.ts";
|
||||
export * from "../infra/exec-approvals.ts";
|
||||
export * from "../infra/approval-native-delivery.ts";
|
||||
export * from "../infra/approval-native-runtime.ts";
|
||||
export * from "../infra/plugin-approvals.ts";
|
||||
export * from "../infra/fetch.js";
|
||||
export * from "../infra/file-lock.js";
|
||||
|
||||
Reference in New Issue
Block a user