fix(runtime): lazy-load setup shims and align contracts

This commit is contained in:
Vincent Koc
2026-03-19 12:17:25 -07:00
parent 7bbd01379e
commit 3b79494cbf
22 changed files with 909 additions and 160 deletions

View File

@@ -1,4 +1,4 @@
import fs from "node:fs/promises";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { expect, vi } from "vitest";
@@ -7,7 +7,11 @@ import {
createThreadBindingManager as createDiscordThreadBindingManager,
} from "../../../../extensions/discord/runtime-api.js";
import { createFeishuThreadBindingManager } from "../../../../extensions/feishu/api.js";
import { createMatrixThreadBindingManager } from "../../../../extensions/matrix/api.js";
import {
createMatrixThreadBindingManager,
resetMatrixThreadBindingsForTests,
} from "../../../../extensions/matrix/api.js";
import { setMatrixRuntime } from "../../../../extensions/matrix/index.js";
import { createTelegramThreadBindingManager } from "../../../../extensions/telegram/runtime-api.js";
import type { OpenClawConfig } from "../../../config/config.js";
import {
@@ -181,6 +185,12 @@ function expectClearedSessionBinding(params: {
const telegramDescribeMessageToolMock = vi.fn();
const discordDescribeMessageToolMock = vi.fn();
const sendMessageMatrixMock = vi.hoisted(() =>
vi.fn(async (to: string, _message: string, opts?: { threadId?: string }) => ({
messageId: opts?.threadId ? "$matrix-thread" : "$matrix-root",
roomId: to.replace(/^room:/, ""),
})),
);
bundledChannelRuntimeSetters.setTelegramRuntime({
channel: {
@@ -213,6 +223,48 @@ bundledChannelRuntimeSetters.setLineRuntime({
},
} as never);
vi.mock("../../../../extensions/matrix/src/matrix/send.js", async () => {
const actual = await vi.importActual<
typeof import("../../../../extensions/matrix/src/matrix/send.js")
>("../../../../extensions/matrix/src/matrix/send.js");
return {
...actual,
sendMessageMatrix: sendMessageMatrixMock,
};
});
const matrixSessionBindingStateDir = fs.mkdtempSync(
path.join(os.tmpdir(), "openclaw-matrix-session-binding-contract-"),
);
const matrixSessionBindingAuth = {
accountId: "ops",
homeserver: "https://matrix.example.org",
userId: "@bot:example.org",
accessToken: "token",
} as const;
function resetMatrixSessionBindingStateDir() {
fs.rmSync(matrixSessionBindingStateDir, { recursive: true, force: true });
fs.mkdirSync(matrixSessionBindingStateDir, { recursive: true });
}
async function createContractMatrixThreadBindingManager() {
resetMatrixSessionBindingStateDir();
setMatrixRuntime({
state: {
resolveStateDir: () => matrixSessionBindingStateDir,
},
} as never);
return await createMatrixThreadBindingManager({
accountId: matrixSessionBindingAuth.accountId,
auth: matrixSessionBindingAuth,
client: {} as never,
idleTimeoutMs: 24 * 60 * 60 * 1000,
maxAgeMs: 0,
enableSweeper: false,
});
}
export const pluginContractRegistry: PluginContractEntry[] = bundledChannelPlugins.map(
(plugin) => ({
id: plugin.id,
@@ -595,24 +647,6 @@ const baseSessionBindingCfg = {
session: { mainKey: "main", scope: "per-sender" },
} satisfies OpenClawConfig;
async function createContractMatrixThreadBindingManager() {
const stateDir = await fs.mkdtemp(path.join(os.tmpdir(), "matrix-contract-thread-bindings-"));
return await createMatrixThreadBindingManager({
accountId: "ops",
auth: {
accountId: "ops",
homeserver: "https://matrix.example.org",
userId: "@bot:example.org",
accessToken: "token",
},
client: {} as never,
stateDir,
idleTimeoutMs: 24 * 60 * 60 * 1000,
maxAgeMs: 0,
enableSweeper: false,
});
}
export const sessionBindingContractRegistry: SessionBindingContractEntry[] = [
{
id: "discord",
@@ -744,47 +778,43 @@ export const sessionBindingContractRegistry: SessionBindingContractEntry[] = [
await createContractMatrixThreadBindingManager();
return getSessionBindingService().getCapabilities({
channel: "matrix",
accountId: "ops",
accountId: matrixSessionBindingAuth.accountId,
});
},
bindAndResolve: async () => {
await createContractMatrixThreadBindingManager();
const service = getSessionBindingService();
const binding = await service.bind({
targetSessionKey: "agent:matrix:subagent:child-1",
targetSessionKey: "agent:matrix:child:thread-1",
targetKind: "subagent",
conversation: {
channel: "matrix",
accountId: "ops",
conversationId: "!room:example",
accountId: matrixSessionBindingAuth.accountId,
conversationId: "$thread",
parentConversationId: "!room:example",
},
placement: "child",
placement: "current",
metadata: {
label: "codex-matrix",
introText: "intro root",
},
});
expectResolvedSessionBinding({
channel: "matrix",
accountId: "ops",
conversationId: "$root",
parentConversationId: "!room:example",
targetSessionKey: "agent:matrix:subagent:child-1",
accountId: matrixSessionBindingAuth.accountId,
conversationId: "$thread",
targetSessionKey: "agent:matrix:child:thread-1",
});
return binding;
},
unbindAndVerify: unbindAndExpectClearedSessionBinding,
cleanup: async () => {
const manager = await createContractMatrixThreadBindingManager();
manager.stop();
expect(
getSessionBindingService().resolveByConversation({
channel: "matrix",
accountId: "ops",
conversationId: "$root",
parentConversationId: "!room:example",
}),
).toBeNull();
resetMatrixThreadBindingsForTests();
resetMatrixSessionBindingStateDir();
expectClearedSessionBinding({
channel: "matrix",
accountId: matrixSessionBindingAuth.accountId,
conversationId: "$thread",
});
},
},
{

View File

@@ -485,7 +485,7 @@ export function installSessionBindingContractSuite(params: {
expectedCapabilities: SessionBindingCapabilities;
}) {
it("registers the expected session binding capabilities", async () => {
expect(await params.getCapabilities()).toEqual(params.expectedCapabilities);
expect(await Promise.resolve(params.getCapabilities())).toEqual(params.expectedCapabilities);
});
it("binds and resolves a session binding through the shared service", async () => {

View File

@@ -1,10 +1,6 @@
import type { OpenClawConfig } from "../../config/config.js";
import type { DmPolicy, GroupPolicy } from "../../config/types.js";
import type { SecretInput } from "../../config/types.secrets.js";
import {
promptSecretRefForSetup,
resolveSecretInputModeForEnvSelection,
} from "../../plugins/provider-auth-input.js";
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../../routing/session-key.js";
import type { WizardPrompter } from "../../wizard/prompts.js";
import {
@@ -18,6 +14,15 @@ import type {
} from "./setup-wizard-types.js";
import type { ChannelSetupWizard, ChannelSetupWizardAllowFromEntry } from "./setup-wizard.js";
let providerAuthInputPromise:
| Promise<typeof import("../../plugins/provider-auth-input.js")>
| undefined;
function loadProviderAuthInput() {
providerAuthInputPromise ??= import("../../plugins/provider-auth-input.js");
return providerAuthInputPromise;
}
export const promptAccountId: PromptAccountId = async (params: PromptAccountIdParams) => {
const existingIds = params.listAccountIds(params.cfg);
const initial = params.currentId?.trim() || params.defaultAccountId || DEFAULT_ACCOUNT_ID;
@@ -994,6 +999,8 @@ export async function promptSingleChannelSecretInput(params: {
inputPrompt: string;
preferredEnvVar?: string;
}): Promise<SingleChannelSecretInputPromptResult> {
const { promptSecretRefForSetup, resolveSecretInputModeForEnvSelection } =
await loadProviderAuthInput();
const selectedMode = await resolveSecretInputModeForEnvSelection({
prompter: params.prompter as WizardPrompter,
explicitMode: params.secretInputMode,