mirror of
https://github.com/moltbot/moltbot.git
synced 2026-04-23 14:45:46 +00:00
Plugins: add binding resolution callbacks (#48678)
Merged via squash.
Prepared head SHA: 6d7b32b184
Co-authored-by: huntharo <5617868+huntharo@users.noreply.github.com>
Co-authored-by: huntharo <5617868+huntharo@users.noreply.github.com>
Reviewed-by: @huntharo
This commit is contained in:
@@ -143,6 +143,18 @@ async function resolveRequestedBinding(request: PluginBindingRequest) {
|
||||
throw new Error("expected pending or bound bind result");
|
||||
}
|
||||
|
||||
async function flushMicrotasks(): Promise<void> {
|
||||
await new Promise<void>((resolve) => setImmediate(resolve));
|
||||
}
|
||||
|
||||
function createDeferredVoid(): { promise: Promise<void>; resolve: () => void } {
|
||||
let resolve = () => {};
|
||||
const promise = new Promise<void>((innerResolve) => {
|
||||
resolve = innerResolve;
|
||||
});
|
||||
return { promise, resolve };
|
||||
}
|
||||
|
||||
describe("plugin conversation binding approvals", () => {
|
||||
beforeEach(() => {
|
||||
sessionBindingState.reset();
|
||||
@@ -406,6 +418,7 @@ describe("plugin conversation binding approvals", () => {
|
||||
});
|
||||
|
||||
expect(approved.status).toBe("approved");
|
||||
await flushMicrotasks();
|
||||
expect(onResolved).toHaveBeenCalledWith({
|
||||
status: "approved",
|
||||
binding: expect.objectContaining({
|
||||
@@ -464,6 +477,7 @@ describe("plugin conversation binding approvals", () => {
|
||||
});
|
||||
|
||||
expect(denied.status).toBe("denied");
|
||||
await flushMicrotasks();
|
||||
expect(onResolved).toHaveBeenCalledWith({
|
||||
status: "denied",
|
||||
binding: undefined,
|
||||
@@ -481,6 +495,108 @@ describe("plugin conversation binding approvals", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("does not wait for an approved bind callback before returning", async () => {
|
||||
const registry = createEmptyPluginRegistry();
|
||||
const callbackGate = createDeferredVoid();
|
||||
const onResolved = vi.fn(async () => callbackGate.promise);
|
||||
registry.conversationBindingResolvedHandlers.push({
|
||||
pluginId: "codex",
|
||||
pluginRoot: "/plugins/callback-slow-approve",
|
||||
handler: onResolved,
|
||||
source: "/plugins/callback-slow-approve/index.ts",
|
||||
rootDir: "/plugins/callback-slow-approve",
|
||||
});
|
||||
setActivePluginRegistry(registry);
|
||||
|
||||
const request = await requestPluginConversationBinding({
|
||||
pluginId: "codex",
|
||||
pluginName: "Codex App Server",
|
||||
pluginRoot: "/plugins/callback-slow-approve",
|
||||
requestedBySenderId: "user-1",
|
||||
conversation: {
|
||||
channel: "discord",
|
||||
accountId: "isolated",
|
||||
conversationId: "channel:slow-approve",
|
||||
},
|
||||
binding: { summary: "Bind this conversation to Codex thread slow-approve." },
|
||||
});
|
||||
|
||||
expect(request.status).toBe("pending");
|
||||
if (request.status !== "pending") {
|
||||
throw new Error("expected pending bind request");
|
||||
}
|
||||
|
||||
let settled = false;
|
||||
const resolutionPromise = resolvePluginConversationBindingApproval({
|
||||
approvalId: request.approvalId,
|
||||
decision: "allow-once",
|
||||
senderId: "user-1",
|
||||
}).then((result) => {
|
||||
settled = true;
|
||||
return result;
|
||||
});
|
||||
|
||||
await flushMicrotasks();
|
||||
|
||||
expect(settled).toBe(true);
|
||||
expect(onResolved).toHaveBeenCalledTimes(1);
|
||||
|
||||
callbackGate.resolve();
|
||||
const approved = await resolutionPromise;
|
||||
expect(approved.status).toBe("approved");
|
||||
});
|
||||
|
||||
it("does not wait for a denied bind callback before returning", async () => {
|
||||
const registry = createEmptyPluginRegistry();
|
||||
const callbackGate = createDeferredVoid();
|
||||
const onResolved = vi.fn(async () => callbackGate.promise);
|
||||
registry.conversationBindingResolvedHandlers.push({
|
||||
pluginId: "codex",
|
||||
pluginRoot: "/plugins/callback-slow-deny",
|
||||
handler: onResolved,
|
||||
source: "/plugins/callback-slow-deny/index.ts",
|
||||
rootDir: "/plugins/callback-slow-deny",
|
||||
});
|
||||
setActivePluginRegistry(registry);
|
||||
|
||||
const request = await requestPluginConversationBinding({
|
||||
pluginId: "codex",
|
||||
pluginName: "Codex App Server",
|
||||
pluginRoot: "/plugins/callback-slow-deny",
|
||||
requestedBySenderId: "user-1",
|
||||
conversation: {
|
||||
channel: "telegram",
|
||||
accountId: "default",
|
||||
conversationId: "slow-deny",
|
||||
},
|
||||
binding: { summary: "Bind this conversation to Codex thread slow-deny." },
|
||||
});
|
||||
|
||||
expect(request.status).toBe("pending");
|
||||
if (request.status !== "pending") {
|
||||
throw new Error("expected pending bind request");
|
||||
}
|
||||
|
||||
let settled = false;
|
||||
const resolutionPromise = resolvePluginConversationBindingApproval({
|
||||
approvalId: request.approvalId,
|
||||
decision: "deny",
|
||||
senderId: "user-1",
|
||||
}).then((result) => {
|
||||
settled = true;
|
||||
return result;
|
||||
});
|
||||
|
||||
await flushMicrotasks();
|
||||
|
||||
expect(settled).toBe(true);
|
||||
expect(onResolved).toHaveBeenCalledTimes(1);
|
||||
|
||||
callbackGate.resolve();
|
||||
const denied = await resolutionPromise;
|
||||
expect(denied.status).toBe("denied");
|
||||
});
|
||||
|
||||
it("returns and detaches only bindings owned by the requesting plugin root", async () => {
|
||||
const request = await requestPluginConversationBinding({
|
||||
pluginId: "codex",
|
||||
|
||||
@@ -722,7 +722,7 @@ export async function resolvePluginConversationBindingApproval(params: {
|
||||
}
|
||||
pendingRequests.delete(params.approvalId);
|
||||
if (params.decision === "deny") {
|
||||
await notifyPluginConversationBindingResolved({
|
||||
dispatchPluginConversationBindingResolved({
|
||||
status: "denied",
|
||||
decision: "deny",
|
||||
request,
|
||||
@@ -755,7 +755,7 @@ export async function resolvePluginConversationBindingApproval(params: {
|
||||
log.info(
|
||||
`plugin binding approved plugin=${request.pluginId} root=${request.pluginRoot} decision=${params.decision} channel=${request.conversation.channel} account=${request.conversation.accountId} conversation=${request.conversation.conversationId}`,
|
||||
);
|
||||
await notifyPluginConversationBindingResolved({
|
||||
dispatchPluginConversationBindingResolved({
|
||||
status: "approved",
|
||||
binding,
|
||||
decision: params.decision,
|
||||
@@ -769,6 +769,20 @@ export async function resolvePluginConversationBindingApproval(params: {
|
||||
};
|
||||
}
|
||||
|
||||
function dispatchPluginConversationBindingResolved(params: {
|
||||
status: "approved" | "denied";
|
||||
binding?: PluginConversationBinding;
|
||||
decision: PluginConversationBindingResolutionDecision;
|
||||
request: PendingPluginBindingRequest;
|
||||
}): void {
|
||||
// Keep platform interaction acks fast even if the plugin does slow post-bind work.
|
||||
queueMicrotask(() => {
|
||||
void notifyPluginConversationBindingResolved(params).catch((error) => {
|
||||
log.warn(`plugin binding resolved dispatch failed: ${String(error)}`);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function notifyPluginConversationBindingResolved(params: {
|
||||
status: "approved" | "denied";
|
||||
binding?: PluginConversationBinding;
|
||||
|
||||
Reference in New Issue
Block a user