refactor(channels): centralize runtime binding routes

This commit is contained in:
Peter Steinberger
2026-04-22 23:16:44 +01:00
parent 85d2a9ec1f
commit f88da75ed9
13 changed files with 351 additions and 148 deletions

View File

@@ -1,14 +1,9 @@
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
import {
getSessionBindingService,
isPluginOwnedSessionBindingRecord,
resolveConfiguredBindingRoute,
resolveRuntimeConversationBindingRoute,
} from "openclaw/plugin-sdk/conversation-runtime";
import {
deriveLastRoutePolicy,
resolveAgentIdFromSessionKey,
resolveAgentRoute,
} from "openclaw/plugin-sdk/routing";
import { resolveAgentRoute } from "openclaw/plugin-sdk/routing";
import { logVerbose } from "openclaw/plugin-sdk/runtime-env";
import { resolveBlueBubblesInboundConversationId } from "./conversation-id.js";
@@ -53,31 +48,21 @@ export function resolveBlueBubblesConversationRoute(params: {
},
}).route;
const runtimeBinding = getSessionBindingService().resolveByConversation({
channel: "bluebubbles",
accountId: params.accountId,
conversationId,
const runtimeRoute = resolveRuntimeConversationBindingRoute({
route,
conversation: {
channel: "bluebubbles",
accountId: params.accountId,
conversationId,
},
});
const boundSessionKey = runtimeBinding?.targetSessionKey?.trim();
if (!runtimeBinding || !boundSessionKey) {
return route;
}
getSessionBindingService().touch(runtimeBinding.bindingId);
if (isPluginOwnedSessionBindingRecord(runtimeBinding)) {
route = runtimeRoute.route;
if (runtimeRoute.bindingRecord && !runtimeRoute.boundSessionKey) {
logVerbose(`bluebubbles: plugin-bound conversation ${conversationId}`);
return route;
} else if (runtimeRoute.boundSessionKey) {
logVerbose(
`bluebubbles: routed via bound conversation ${conversationId} -> ${runtimeRoute.boundSessionKey}`,
);
}
logVerbose(`bluebubbles: routed via bound conversation ${conversationId} -> ${boundSessionKey}`);
return {
...route,
sessionKey: boundSessionKey,
agentId: resolveAgentIdFromSessionKey(boundSessionKey),
lastRoutePolicy: deriveLastRoutePolicy({
sessionKey: boundSessionKey,
mainSessionKey: route.mainSessionKey,
}),
matchedBy: "binding.channel",
};
return route;
}

View File

@@ -9,11 +9,50 @@ export function installAgentContractHooks() {
installBrowserControlServerHooks();
}
function isTransientStartupFetchError(error: unknown): boolean {
if (!error || typeof error !== "object") {
return false;
}
const record = error as { code?: unknown; cause?: unknown };
if (record.code === "ECONNRESET" || record.code === "ECONNREFUSED") {
return true;
}
return isTransientStartupFetchError(record.cause);
}
async function sleep(ms: number): Promise<void> {
await new Promise((resolve) => setTimeout(resolve, ms));
}
async function postStartWithRetry(params: {
fetch: ReturnType<typeof getBrowserTestFetch>;
url: string;
}): Promise<void> {
const delaysMs = [0, 25, 50, 100, 200] as const;
let lastError: unknown;
for (const delayMs of delaysMs) {
if (delayMs > 0) {
await sleep(delayMs);
}
try {
const response = await params.fetch(params.url, { method: "POST" });
await response.json();
return;
} catch (error) {
lastError = error;
if (!isTransientStartupFetchError(error)) {
throw error;
}
}
}
throw lastError;
}
export async function startServerAndBase(): Promise<string> {
await startBrowserControlServerFromConfig();
const base = getBrowserControlServerBaseUrl();
const realFetch = getBrowserTestFetch();
await realFetch(`${base}/start`, { method: "POST" }).then((r) => r.json());
await postStartWithRetry({ fetch: realFetch, url: `${base}/start` });
return base;
}

View File

@@ -629,13 +629,16 @@ export async function preflightDiscordMessage(
}) ?? `user:${author.id}`)
: messageChannelId;
let threadBinding: SessionBindingRecord | undefined;
threadBinding =
conversationRuntime.getSessionBindingService().resolveByConversation({
const runtimeRoute = conversationRuntime.resolveRuntimeConversationBindingRoute({
route,
conversation: {
channel: "discord",
accountId: params.accountId,
conversationId: bindingConversationId,
parentConversationId: earlyThreadParentId,
}) ?? undefined;
},
});
threadBinding = runtimeRoute.bindingRecord ?? undefined;
const configuredRoute =
threadBinding == null
? conversationRuntime.resolveConfiguredBindingRoute({
@@ -666,13 +669,15 @@ export async function preflightDiscordMessage(
}
const boundSessionKey = conversationRuntime.isPluginOwnedSessionBindingRecord(threadBinding)
? ""
: threadBinding?.targetSessionKey?.trim();
const effectiveRoute = resolveDiscordEffectiveRoute({
route,
boundSessionKey,
configuredRoute,
matchedBy: "binding.channel",
});
: (runtimeRoute.boundSessionKey ?? threadBinding?.targetSessionKey?.trim());
const effectiveRoute = runtimeRoute.boundSessionKey
? runtimeRoute.route
: resolveDiscordEffectiveRoute({
route,
boundSessionKey,
configuredRoute,
matchedBy: "binding.channel",
});
const boundAgentId = boundSessionKey ? effectiveRoute.agentId : undefined;
const isBoundThreadSession = Boolean(threadBinding && earlyThreadChannel);
const bypassMentionRequirement = isBoundThreadSession;

View File

@@ -262,7 +262,7 @@ const {
mockEnsureConfiguredBindingRouteReady: vi.fn(
async (_params?: unknown): Promise<BindingReadiness> => ({ ok: true }),
),
mockResolveBoundConversation: vi.fn(() => null as BoundConversation),
mockResolveBoundConversation: vi.fn((_ref?: unknown) => null as BoundConversation),
mockTouchBinding: vi.fn(),
mockResolveFeishuReasoningPreviewEnabled: vi.fn(() => false),
}));
@@ -297,6 +297,30 @@ vi.mock("openclaw/plugin-sdk/conversation-runtime", async () => {
...actual,
resolveConfiguredBindingRoute: (params: unknown) =>
mockResolveConfiguredBindingRoute(params as { route: ResolvedAgentRoute }),
resolveRuntimeConversationBindingRoute: (params: {
route: ResolvedAgentRoute;
conversation: Parameters<
ReturnType<typeof actual.getSessionBindingService>["resolveByConversation"]
>[0];
}) => {
const bindingRecord = mockResolveBoundConversation(params.conversation);
const boundSessionKey = bindingRecord?.targetSessionKey?.trim();
if (!bindingRecord || !boundSessionKey) {
return { bindingRecord: null, route: params.route };
}
mockTouchBinding(bindingRecord.bindingId);
return {
bindingRecord,
boundSessionKey,
boundAgentId: params.route.agentId,
route: {
...params.route,
sessionKey: boundSessionKey,
lastRoutePolicy: boundSessionKey === params.route.mainSessionKey ? "main" : "session",
matchedBy: "binding.channel",
},
};
},
ensureConfiguredBindingRouteReady: (params: unknown) =>
mockEnsureConfiguredBindingRouteReady(params),
getSessionBindingService: () => ({

View File

@@ -2,8 +2,8 @@ import { createChannelPairingController } from "openclaw/plugin-sdk/channel-pair
import {
ensureConfiguredBindingRouteReady,
resolveConfiguredBindingRoute,
resolveRuntimeConversationBindingRoute,
} from "openclaw/plugin-sdk/conversation-runtime";
import { getSessionBindingService } from "openclaw/plugin-sdk/conversation-runtime";
import { resolveAgentOutboundIdentity } from "openclaw/plugin-sdk/outbound-runtime";
import {
buildPendingHistoryContextFromMap,
@@ -12,8 +12,6 @@ import {
recordPendingHistoryEntryIfEnabled,
type HistoryEntry,
} from "openclaw/plugin-sdk/reply-history";
import { deriveLastRoutePolicy } from "openclaw/plugin-sdk/routing";
import { resolveAgentIdFromSessionKey } from "openclaw/plugin-sdk/routing";
import {
resolveDefaultGroupPolicy,
resolveOpenProviderRuntimeGroupPolicy,
@@ -651,28 +649,22 @@ export async function handleFeishuMessage(params: {
// Bound Feishu conversations intentionally require an exact live conversation-id match.
// Sender-scoped topic sessions therefore bind on `chat:topic:root:sender:user`, while
// configured ACP bindings may still inherit the shared `chat:topic:root` topic session.
const threadBinding = getSessionBindingService().resolveByConversation({
channel: "feishu",
accountId: account.accountId,
conversationId: currentConversationId,
...(parentConversationId ? { parentConversationId } : {}),
const runtimeRoute = resolveRuntimeConversationBindingRoute({
route,
conversation: {
channel: "feishu",
accountId: account.accountId,
conversationId: currentConversationId,
...(parentConversationId ? { parentConversationId } : {}),
},
});
const boundSessionKey = threadBinding?.targetSessionKey?.trim();
if (threadBinding && boundSessionKey) {
route = {
...route,
sessionKey: boundSessionKey,
agentId: resolveAgentIdFromSessionKey(boundSessionKey) || route.agentId,
lastRoutePolicy: deriveLastRoutePolicy({
sessionKey: boundSessionKey,
mainSessionKey: route.mainSessionKey,
}),
matchedBy: "binding.channel",
};
route = runtimeRoute.route;
if (runtimeRoute.bindingRecord) {
configuredBinding = null;
getSessionBindingService().touch(threadBinding.bindingId);
log(
`feishu[${account.accountId}]: routed via bound conversation ${currentConversationId} -> ${boundSessionKey}`,
runtimeRoute.boundSessionKey
? `feishu[${account.accountId}]: routed via bound conversation ${currentConversationId} -> ${runtimeRoute.boundSessionKey}`
: `feishu[${account.accountId}]: plugin-bound conversation ${currentConversationId}`,
);
}
}

View File

@@ -45,7 +45,7 @@ type FeishuLifecycleTestMocks = {
monitorWebhookMock: AsyncUnknownMock;
createFeishuThreadBindingManagerMock: UnknownMock;
createFeishuReplyDispatcherMock: CreateFeishuReplyDispatcherMock;
resolveBoundConversationMock: Mock<() => BoundConversation | null>;
resolveBoundConversationMock: Mock<(ref?: unknown) => BoundConversation | null>;
touchBindingMock: UnknownMock;
resolveAgentRouteMock: UnknownMock;
resolveConfiguredBindingRouteMock: UnknownMock;
@@ -66,7 +66,7 @@ const feishuLifecycleTestMocks = vi.hoisted(
monitorWebhookMock: vi.fn(async () => {}),
createFeishuThreadBindingManagerMock: vi.fn(() => ({ stop: vi.fn() })),
createFeishuReplyDispatcherMock: vi.fn(),
resolveBoundConversationMock: vi.fn<() => BoundConversation | null>(() => null),
resolveBoundConversationMock: vi.fn<(ref?: unknown) => BoundConversation | null>(() => null),
touchBindingMock: vi.fn(),
resolveAgentRouteMock: vi.fn(),
resolveConfiguredBindingRouteMock: vi.fn(),
@@ -155,6 +155,36 @@ vi.mock("openclaw/plugin-sdk/conversation-runtime", async () => {
resolveConfiguredBindingRouteMock.getMockImplementation()
? resolveConfiguredBindingRouteMock(params)
: actual.resolveConfiguredBindingRoute(params),
resolveRuntimeConversationBindingRoute: (
params: Parameters<typeof actual.resolveRuntimeConversationBindingRoute>[0],
) => {
const conversation =
"conversation" in params
? params.conversation
: {
channel: params.channel,
accountId: params.accountId,
conversationId: params.conversationId,
parentConversationId: params.parentConversationId,
};
const bindingRecord = resolveBoundConversationMock(conversation);
const boundSessionKey = bindingRecord?.targetSessionKey?.trim();
if (!bindingRecord || !boundSessionKey) {
return { bindingRecord: null, route: params.route };
}
touchBindingMock(bindingRecord.bindingId);
return {
bindingRecord,
boundSessionKey,
boundAgentId: params.route.agentId,
route: {
...params.route,
sessionKey: boundSessionKey,
lastRoutePolicy: boundSessionKey === params.route.mainSessionKey ? "main" : "session",
matchedBy: "binding.channel",
},
};
},
ensureConfiguredBindingRouteReady: (
params: Parameters<typeof actual.ensureConfiguredBindingRouteReady>[0],
) =>

View File

@@ -1,14 +1,9 @@
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
import {
getSessionBindingService,
isPluginOwnedSessionBindingRecord,
resolveConfiguredBindingRoute,
resolveRuntimeConversationBindingRoute,
} from "openclaw/plugin-sdk/conversation-runtime";
import {
deriveLastRoutePolicy,
resolveAgentIdFromSessionKey,
resolveAgentRoute,
} from "openclaw/plugin-sdk/routing";
import { resolveAgentRoute } from "openclaw/plugin-sdk/routing";
import { logVerbose } from "openclaw/plugin-sdk/runtime-env";
import { resolveIMessageInboundConversationId } from "./conversation-id.js";
@@ -49,31 +44,21 @@ export function resolveIMessageConversationRoute(params: {
},
}).route;
const runtimeBinding = getSessionBindingService().resolveByConversation({
channel: "imessage",
accountId: params.accountId,
conversationId,
const runtimeRoute = resolveRuntimeConversationBindingRoute({
route,
conversation: {
channel: "imessage",
accountId: params.accountId,
conversationId,
},
});
const boundSessionKey = runtimeBinding?.targetSessionKey?.trim();
if (!runtimeBinding || !boundSessionKey) {
return route;
}
getSessionBindingService().touch(runtimeBinding.bindingId);
if (isPluginOwnedSessionBindingRecord(runtimeBinding)) {
route = runtimeRoute.route;
if (runtimeRoute.bindingRecord && !runtimeRoute.boundSessionKey) {
logVerbose(`imessage: plugin-bound conversation ${conversationId}`);
return route;
} else if (runtimeRoute.boundSessionKey) {
logVerbose(
`imessage: routed via bound conversation ${conversationId} -> ${runtimeRoute.boundSessionKey}`,
);
}
logVerbose(`imessage: routed via bound conversation ${conversationId} -> ${boundSessionKey}`);
return {
...route,
sessionKey: boundSessionKey,
agentId: resolveAgentIdFromSessionKey(boundSessionKey),
lastRoutePolicy: deriveLastRoutePolicy({
sessionKey: boundSessionKey,
mainSessionKey: route.mainSessionKey,
}),
matchedBy: "binding.channel",
};
return route;
}

View File

@@ -8,19 +8,15 @@ import {
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
import {
ensureConfiguredBindingRouteReady,
getSessionBindingService,
recordInboundSession,
resolvePinnedMainDmOwnerFromAllowlist,
resolveConfiguredBindingRoute,
resolveRuntimeConversationBindingRoute,
} from "openclaw/plugin-sdk/conversation-runtime";
import { recordChannelActivity } from "openclaw/plugin-sdk/infra-runtime";
import { finalizeInboundContext } from "openclaw/plugin-sdk/reply-dispatch-runtime";
import type { HistoryEntry } from "openclaw/plugin-sdk/reply-history";
import {
deriveLastRoutePolicy,
resolveAgentIdFromSessionKey,
resolveAgentRoute,
} from "openclaw/plugin-sdk/routing";
import { resolveAgentRoute } from "openclaw/plugin-sdk/routing";
import { logVerbose, shouldLogVerbose } from "openclaw/plugin-sdk/runtime-env";
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
import { normalizeAllowFrom } from "./bot-access.js";
@@ -132,26 +128,22 @@ async function resolveLineInboundRoute(params: {
const configuredBindingSessionKey = configuredRoute.boundSessionKey ?? "";
route = configuredRoute.route;
const boundConversation = getSessionBindingService().resolveByConversation({
channel: "line",
accountId: params.account.accountId,
conversationId: peerId,
const runtimeRoute = resolveRuntimeConversationBindingRoute({
route,
conversation: {
channel: "line",
accountId: params.account.accountId,
conversationId: peerId,
},
});
const boundSessionKey = boundConversation?.targetSessionKey?.trim();
if (boundConversation && boundSessionKey) {
route = {
...route,
sessionKey: boundSessionKey,
agentId: resolveAgentIdFromSessionKey(boundSessionKey) || route.agentId,
lastRoutePolicy: deriveLastRoutePolicy({
sessionKey: boundSessionKey,
mainSessionKey: route.mainSessionKey,
}),
matchedBy: "binding.channel",
};
route = runtimeRoute.route;
if (runtimeRoute.bindingRecord) {
configuredBinding = null;
getSessionBindingService().touch(boundConversation.bindingId);
logVerbose(`line: routed via bound conversation ${peerId} -> ${boundSessionKey}`);
logVerbose(
runtimeRoute.boundSessionKey
? `line: routed via bound conversation ${peerId} -> ${runtimeRoute.boundSessionKey}`
: `line: plugin-bound conversation ${peerId}`,
);
}
if (configuredBinding) {

View File

@@ -71,6 +71,36 @@ vi.mock("openclaw/plugin-sdk/conversation-runtime", async () => {
return {
...actual,
resolveConfiguredBindingRoute: persistentBindingMocks.resolveConfiguredBindingRoute,
resolveRuntimeConversationBindingRoute: (
params: Parameters<typeof actual.resolveRuntimeConversationBindingRoute>[0],
) => {
const conversation =
"conversation" in params
? params.conversation
: {
channel: params.channel,
accountId: params.accountId,
conversationId: params.conversationId,
parentConversationId: params.parentConversationId,
};
const bindingRecord = sessionBindingMocks.resolveByConversation(conversation);
const boundSessionKey = bindingRecord?.targetSessionKey?.trim();
if (!bindingRecord || !boundSessionKey) {
return { bindingRecord: null, route: params.route };
}
sessionBindingMocks.touch(bindingRecord.bindingId, undefined);
return {
bindingRecord,
boundSessionKey,
boundAgentId: params.route.agentId,
route: {
...params.route,
sessionKey: boundSessionKey,
lastRoutePolicy: boundSessionKey === params.route.mainSessionKey ? "main" : "session",
matchedBy: "binding.channel",
},
};
},
ensureConfiguredBindingRouteReady: persistentBindingMocks.ensureConfiguredBindingRouteReady,
recordInboundSessionMetaSafe: vi.fn(
async (params: {

View File

@@ -108,6 +108,10 @@ vi.mock("openclaw/plugin-sdk/conversation-runtime", () => ({
bindingResolution: null,
boundSessionKey: "",
})),
resolveRuntimeConversationBindingRoute: vi.fn(({ route }: { route: unknown }) => ({
bindingRecord: null,
route,
})),
getSessionBindingService: vi.fn(() => ({
resolveByConversation: vi.fn(() => null),
touch: vi.fn(),

View File

@@ -1,10 +1,9 @@
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
import {
resolveConfiguredBindingRoute,
resolveRuntimeConversationBindingRoute,
type ConfiguredBindingRouteResult,
} from "openclaw/plugin-sdk/conversation-runtime";
import { getSessionBindingService } from "openclaw/plugin-sdk/conversation-runtime";
import { isPluginOwnedSessionBindingRecord } from "openclaw/plugin-sdk/conversation-runtime";
import {
buildAgentSessionKey,
deriveLastRoutePolicy,
@@ -13,7 +12,6 @@ import {
import {
buildAgentMainSessionKey,
DEFAULT_ACCOUNT_ID,
resolveAgentIdFromSessionKey,
sanitizeAgentId,
} from "openclaw/plugin-sdk/routing";
import { logVerbose } from "openclaw/plugin-sdk/runtime-env";
@@ -116,32 +114,22 @@ export function resolveTelegramConversationRoute(params: {
? String(params.chatId)
: undefined;
if (threadBindingConversationId) {
const threadBinding = getSessionBindingService().resolveByConversation({
channel: "telegram",
accountId: params.accountId,
conversationId: threadBindingConversationId,
const runtimeRoute = resolveRuntimeConversationBindingRoute({
route,
conversation: {
channel: "telegram",
accountId: params.accountId,
conversationId: threadBindingConversationId,
},
});
const boundSessionKey = threadBinding?.targetSessionKey?.trim();
if (threadBinding && boundSessionKey) {
if (!isPluginOwnedSessionBindingRecord(threadBinding)) {
route = {
...route,
sessionKey: boundSessionKey,
agentId: resolveAgentIdFromSessionKey(boundSessionKey),
lastRoutePolicy: deriveLastRoutePolicy({
sessionKey: boundSessionKey,
mainSessionKey: route.mainSessionKey,
}),
matchedBy: "binding.channel",
};
}
route = runtimeRoute.route;
if (runtimeRoute.bindingRecord) {
configuredBinding = null;
configuredBindingSessionKey = "";
getSessionBindingService().touch(threadBinding.bindingId);
logVerbose(
isPluginOwnedSessionBindingRecord(threadBinding)
? `telegram: plugin-bound conversation ${threadBindingConversationId}`
: `telegram: routed via bound conversation ${threadBindingConversationId} -> ${boundSessionKey}`,
runtimeRoute.boundSessionKey
? `telegram: routed via bound conversation ${threadBindingConversationId} -> ${runtimeRoute.boundSessionKey}`
: `telegram: plugin-bound conversation ${threadBindingConversationId}`,
);
}
}

View File

@@ -0,0 +1,114 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import {
__testing,
registerSessionBindingAdapter,
type SessionBindingAdapter,
type SessionBindingRecord,
} from "../../infra/outbound/session-binding-service.js";
import type { ResolvedAgentRoute } from "../../routing/resolve-route.js";
import { resolveRuntimeConversationBindingRoute } from "./binding-routing.js";
function createRoute(): ResolvedAgentRoute {
return {
agentId: "main",
channel: "demo",
accountId: "default",
sessionKey: "agent:main:main",
mainSessionKey: "agent:main:main",
lastRoutePolicy: "main",
matchedBy: "default",
};
}
function createBinding(overrides?: Partial<SessionBindingRecord>): SessionBindingRecord {
return {
bindingId: "binding-1",
targetSessionKey: "agent:review:acp:session-1",
targetKind: "session",
conversation: {
channel: "demo",
accountId: "default",
conversationId: "room-1",
},
status: "active",
boundAt: 1,
...overrides,
};
}
function registerAdapter(record: SessionBindingRecord | null): {
resolveByConversation: ReturnType<typeof vi.fn>;
touch: ReturnType<typeof vi.fn>;
} {
const resolveByConversation = vi.fn<SessionBindingAdapter["resolveByConversation"]>(() => record);
const touch = vi.fn<NonNullable<SessionBindingAdapter["touch"]>>();
registerSessionBindingAdapter({
channel: "demo",
accountId: "default",
listBySession: () => [],
resolveByConversation,
touch,
});
return { resolveByConversation, touch };
}
describe("runtime conversation binding route", () => {
beforeEach(() => {
__testing.resetSessionBindingAdaptersForTests();
});
it("rewrites the route to a runtime-bound ACP session and touches the binding", () => {
const binding = createBinding();
const { resolveByConversation, touch } = registerAdapter(binding);
const result = resolveRuntimeConversationBindingRoute({
route: createRoute(),
conversation: {
channel: "demo",
accountId: "default",
conversationId: "room-1",
},
});
expect(resolveByConversation).toHaveBeenCalledWith({
channel: "demo",
accountId: "default",
conversationId: "room-1",
});
expect(touch).toHaveBeenCalledWith("binding-1", undefined);
expect(result.boundSessionKey).toBe("agent:review:acp:session-1");
expect(result.boundAgentId).toBe("review");
expect(result.route).toMatchObject({
agentId: "review",
sessionKey: "agent:review:acp:session-1",
lastRoutePolicy: "session",
matchedBy: "binding.channel",
});
});
it("touches plugin-owned bindings without rewriting the channel route", () => {
const route = createRoute();
const binding = createBinding({
metadata: {
pluginBindingOwner: "plugin",
pluginId: "demo-plugin",
pluginRoot: "/tmp/demo-plugin",
},
});
const { touch } = registerAdapter(binding);
const result = resolveRuntimeConversationBindingRoute({
route,
conversation: {
channel: "demo",
accountId: "default",
conversationId: "room-1",
},
});
expect(touch).toHaveBeenCalledWith("binding-1", undefined);
expect(result.bindingRecord).toBe(binding);
expect(result.boundSessionKey).toBeUndefined();
expect(result.route).toBe(route);
});
});

View File

@@ -1,6 +1,8 @@
import path from "node:path";
import { providerOpenAiExtensionTestRoots } from "./vitest.extension-provider-paths.mjs";
import { loadPatternListFromEnv } from "./vitest.pattern-file.ts";
import { createScopedVitestConfig } from "./vitest.scoped-config.ts";
import { repoRoot } from "./vitest.shared.config.ts";
export function loadIncludePatternsFromEnv(
env: Record<string, string | undefined> = process.env,
@@ -11,7 +13,7 @@ export function loadIncludePatternsFromEnv(
export function createExtensionProviderOpenAiVitestConfig(
env: Record<string, string | undefined> = process.env,
) {
return createScopedVitestConfig(
const config = createScopedVitestConfig(
loadIncludePatternsFromEnv(env) ??
providerOpenAiExtensionTestRoots.map((root) => `${root}/**/*.test.ts`),
{
@@ -22,6 +24,19 @@ export function createExtensionProviderOpenAiVitestConfig(
setupFiles: ["test/setup.extensions.ts"],
},
);
return {
...config,
resolve: {
...config.resolve,
alias: [
...(Array.isArray(config.resolve?.alias) ? config.resolve.alias : []),
{
find: /^ws$/u,
replacement: path.join(repoRoot, "node_modules", "ws", "wrapper.mjs"),
},
],
},
};
}
export default createExtensionProviderOpenAiVitestConfig();