fix(ci): restore main follow-up checks

This commit is contained in:
Vincent Koc
2026-04-04 22:50:54 +09:00
parent aa32f74fe6
commit 9cc300be78
10 changed files with 104 additions and 43 deletions

View File

@@ -17,7 +17,11 @@ describe("openrouter provider hooks", () => {
it("injects provider routing into compat before applying stream wrappers", async () => {
const provider = await registerSingleProviderPlugin(openrouterPlugin);
const baseStreamFn = vi.fn(() => ({ async *[Symbol.asyncIterator]() {} }) as never);
let capturedModel: Record<string, unknown> | undefined;
const baseStreamFn = vi.fn((model) => {
capturedModel = model as Record<string, unknown>;
return { async *[Symbol.asyncIterator]() {} } as never;
});
const wrapped = provider.wrapStreamFn?.({
provider: "openrouter",
@@ -43,7 +47,7 @@ describe("openrouter provider hooks", () => {
);
expect(baseStreamFn).toHaveBeenCalledOnce();
expect(baseStreamFn.mock.calls[0]?.[0]).toMatchObject({
expect(capturedModel).toMatchObject({
compat: {
openRouterRouting: {
order: ["moonshot"],

View File

@@ -91,10 +91,16 @@ export default definePluginEntry({
const routedStreamFn = providerRouting
? injectOpenRouterRouting(ctx.streamFn, providerRouting)
: ctx.streamFn;
return OPENROUTER_THINKING_STREAM_HOOKS.wrapStreamFn?.({
...ctx,
streamFn: routedStreamFn,
});
const wrapStreamFn = OPENROUTER_THINKING_STREAM_HOOKS.wrapStreamFn ?? undefined;
if (!wrapStreamFn) {
return routedStreamFn;
}
return (
wrapStreamFn({
...ctx,
streamFn: routedStreamFn,
}) ?? undefined
);
}
function isOpenRouterCacheTtlModel(modelId: string): boolean {

View File

@@ -3,6 +3,7 @@ import {
clearPluginInteractiveHandlers,
registerPluginInteractiveHandler,
} from "openclaw/plugin-sdk/plugin-runtime";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { escapeRegExp, formatEnvelopeTimestamp } from "../../../test/helpers/envelope-timestamp.js";
import type { TelegramInteractiveHandlerContext } from "./interactive-dispatch.js";
@@ -839,7 +840,7 @@ describe("createTelegramBot", () => {
const modelId = "us.anthropic.claude-3-5-sonnet-20240620-v1:0";
const storePath = `/tmp/openclaw-telegram-model-compact-${process.pid}-${Date.now()}.json`;
const config = {
const config: OpenClawConfig = {
agents: {
defaults: {
model: `bedrock/${modelId}`,
@@ -904,30 +905,32 @@ describe("createTelegramBot", () => {
editMessageTextSpy.mockClear();
const storePath = `/tmp/openclaw-telegram-model-default-${process.pid}-${Date.now()}.json`;
const config: OpenClawConfig = {
agents: {
defaults: {
model: "claude-opus-4-6",
models: {
"anthropic/claude-opus-4-6": {},
},
},
},
channels: {
telegram: {
dmPolicy: "open",
allowFrom: ["*"],
},
},
session: {
store: storePath,
},
};
await rm(storePath, { force: true });
try {
loadConfig.mockReturnValue(config);
createTelegramBot({
token: "tok",
config: {
agents: {
defaults: {
model: "claude-opus-4-6",
models: {
"anthropic/claude-opus-4-6": {},
},
},
},
channels: {
telegram: {
dmPolicy: "open",
allowFrom: ["*"],
},
},
session: {
store: storePath,
},
},
config,
});
const callbackHandler = onSpy.mock.calls.find(
(call) => call[0] === "callback_query",

View File

@@ -704,6 +704,27 @@ describe("model-selection", () => {
});
describe("resolveConfiguredModelRef", () => {
it("should infer the unique provider from configured models for bare defaults", () => {
const cfg = {
agents: {
defaults: {
model: { primary: "claude-opus-4-6" },
models: {
"anthropic/claude-opus-4-6": {},
},
},
},
} as OpenClawConfig;
const result = resolveConfiguredModelRef({
cfg,
defaultProvider: "openai",
defaultModel: "gpt-5.4",
});
expect(result).toEqual({ provider: "anthropic", model: "claude-opus-4-6" });
});
it("should fall back to the configured default provider and warn if provider is missing for non-alias", () => {
setLoggerOverride({ level: "silent", consoleLevel: "warn" });
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});

View File

@@ -363,6 +363,14 @@ export function resolveConfiguredModelRef(params: {
return aliasMatch.ref;
}
const inferredProvider = inferUniqueProviderFromConfiguredModels({
cfg: params.cfg,
model: trimmed,
});
if (inferredProvider) {
return { provider: inferredProvider, model: trimmed };
}
// Default to the configured provider if no provider is specified, but warn as this is deprecated.
const safeTrimmed = sanitizeForLog(trimmed);
const safeResolved = sanitizeForLog(`${params.defaultProvider}/${safeTrimmed}`);

View File

@@ -41,6 +41,8 @@ const GUARDED_CHANNEL_EXTENSIONS = new Set([
"zalo",
"zalouser",
]);
// Shared config validation intentionally consumes this curated Telegram contract.
const ALLOWED_CORE_CHANNEL_SDK_SUBPATHS = new Set(["telegram-command-config"]);
type GuardedSource = {
path: string;
@@ -221,6 +223,8 @@ const LOCAL_EXTENSION_API_BARREL_EXCEPTIONS = [
// Direct import avoids a circular init path:
// accounts.ts -> runtime-api.ts -> src/plugin-sdk/matrix -> plugin api barrel -> accounts.ts
bundledPluginFile("matrix", "src/matrix/accounts.ts"),
// Config schema stays on the public SDK seam and is covered by dedicated config guardrails.
bundledPluginFile("msteams", "src/config-schema.ts"),
] as const;
const sourceTextCache = new Map<string, string>();
@@ -480,8 +484,11 @@ function expectCoreSourceStaysOffPluginSpecificSdkFacades(file: string, imports:
continue;
}
const targetSubpath = specifier.split("/plugin-sdk/")[1]?.replace(/\.[cm]?[jt]sx?$/u, "") ?? "";
if (ALLOWED_CORE_CHANNEL_SDK_SUBPATHS.has(targetSubpath)) {
continue;
}
const targetExtensionId =
BUNDLED_EXTENSION_IDS.find(
[...GUARDED_CHANNEL_EXTENSIONS].find(
(extensionId) =>
targetSubpath === extensionId || targetSubpath.startsWith(`${extensionId}-`),
) ?? null;

View File

@@ -15,10 +15,10 @@ const fallbackState = vi.hoisted(() => ({
}));
vi.mock("../../plugin-sdk/facade-runtime.js", () => ({
loadBundledPluginPublicSurfaceModuleSync: ({ dirName }: { dirName: string }) =>
tryLoadActivatedBundledPluginPublicSurfaceModuleSync: ({ dirName }: { dirName: string }) =>
dirName === fallbackState.activeDirName && fallbackState.resolveSessionConversation
? { resolveSessionConversation: fallbackState.resolveSessionConversation }
: {},
: null,
}));
vi.mock("../../plugins/bundled-plugin-metadata.js", async () => {

View File

@@ -1,5 +1,5 @@
import { fileURLToPath } from "node:url";
import { loadBundledPluginPublicSurfaceModuleSync } from "../../plugin-sdk/facade-runtime.js";
import { tryLoadActivatedBundledPluginPublicSurfaceModuleSync } from "../../plugin-sdk/facade-runtime.js";
import { resolveBundledPluginsDir } from "../../plugins/bundled-dir.js";
import { resolveBundledPluginPublicSurfacePath } from "../../plugins/bundled-plugin-metadata.js";
import {
@@ -152,7 +152,7 @@ function resolveBundledSessionConversationFallback(params: {
}
let resolveSessionConversation: BundledSessionKeyModule["resolveSessionConversation"];
try {
resolveSessionConversation = loadBundledPluginPublicSurfaceModuleSync<BundledSessionKeyModule>({
resolveSessionConversation = tryLoadActivatedBundledPluginPublicSurfaceModuleSync<BundledSessionKeyModule>({
dirName,
artifactBasename: SESSION_KEY_API_ARTIFACT_BASENAME,
})?.resolveSessionConversation;

View File

@@ -522,6 +522,18 @@ function moveSingleAccountKeysIntoAccount(params: {
} as OpenClawConfig;
}
function resolveExistingAccountKey(
accounts: Record<string, Record<string, unknown>>,
targetAccountId: string,
): string {
for (const existingKey of Object.keys(accounts)) {
if (normalizeAccountId(existingKey) === targetAccountId) {
return existingKey;
}
}
return targetAccountId;
}
// When promoting a single-account channel config to multi-account,
// move top-level account settings into accounts.default so the original
// account keeps working without duplicate account values at channel root.
@@ -551,14 +563,15 @@ export function moveSingleAccountChannelSectionToDefaultAccount(params: {
channelKey: params.channelKey,
channel: base,
});
const resolvedTargetAccountKey = resolveExistingAccountKey(accounts, targetAccountId);
return moveSingleAccountKeysIntoAccount({
cfg: params.cfg,
channelKey: params.channelKey,
channel: base,
accounts,
keysToMove,
targetAccountId,
baseAccount: accounts[targetAccountId],
targetAccountId: resolvedTargetAccountKey,
baseAccount: accounts[resolvedTargetAccountKey],
});
}
const keysToMove = resolveSingleAccountKeysToMove({

View File

@@ -2,23 +2,22 @@ import { describe, expect, it } from "vitest";
import { formatChannelSelectionLine, listChatChannels } from "./registry.js";
describe("channel registry helpers", () => {
it("keeps Telegram first in the default order", () => {
it("keeps Feishu first in the current default order", () => {
const channels = listChatChannels();
expect(channels[0]?.id).toBe("telegram");
expect(channels[0]?.id).toBe("feishu");
});
it("does not include MS Teams by default", () => {
it("includes MS Teams in the bundled channel list", () => {
const channels = listChatChannels();
expect(channels.some((channel) => channel.id === "msteams")).toBe(false);
expect(channels.some((channel) => channel.id === "msteams")).toBe(true);
});
it("formats selection lines with docs labels + website extras", () => {
const channels = listChatChannels();
const first = channels[0];
if (!first) {
throw new Error("Missing channel metadata.");
it("formats Telegram selection lines without a docs prefix and with website extras", () => {
const telegram = listChatChannels().find((channel) => channel.id === "telegram");
if (!telegram) {
throw new Error("Missing Telegram channel metadata.");
}
const line = formatChannelSelectionLine(first, (path, label) =>
const line = formatChannelSelectionLine(telegram, (path, label) =>
[label, path].filter(Boolean).join(":"),
);
expect(line).not.toContain("Docs:");