fix: restore full gate stability
This commit is contained in:
parent
c86de678f3
commit
83c5bc946d
@ -1,6 +1,7 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
vi.mock("./send.js", () => ({
|
||||
addRoleDiscord: vi.fn(),
|
||||
fetchChannelPermissionsDiscord: vi.fn(),
|
||||
}));
|
||||
|
||||
|
||||
@ -3,16 +3,57 @@ import { vi } from "vitest";
|
||||
|
||||
export const sendMock: MockFn = vi.fn();
|
||||
export const reactMock: MockFn = vi.fn();
|
||||
export const recordInboundSessionMock: MockFn = vi.fn();
|
||||
export const updateLastRouteMock: MockFn = vi.fn();
|
||||
export const dispatchMock: MockFn = vi.fn();
|
||||
export const readAllowFromStoreMock: MockFn = vi.fn();
|
||||
export const upsertPairingRequestMock: MockFn = vi.fn();
|
||||
|
||||
vi.mock("./send.js", () => ({
|
||||
sendMessageDiscord: (...args: unknown[]) => sendMock(...args),
|
||||
addRoleDiscord: vi.fn(),
|
||||
banMemberDiscord: vi.fn(),
|
||||
createChannelDiscord: vi.fn(),
|
||||
createScheduledEventDiscord: vi.fn(),
|
||||
createThreadDiscord: vi.fn(),
|
||||
deleteChannelDiscord: vi.fn(),
|
||||
deleteMessageDiscord: vi.fn(),
|
||||
editChannelDiscord: vi.fn(),
|
||||
editMessageDiscord: vi.fn(),
|
||||
fetchChannelInfoDiscord: vi.fn(),
|
||||
fetchChannelPermissionsDiscord: vi.fn(),
|
||||
fetchMemberInfoDiscord: vi.fn(),
|
||||
fetchMessageDiscord: vi.fn(),
|
||||
fetchReactionsDiscord: vi.fn(),
|
||||
fetchRoleInfoDiscord: vi.fn(),
|
||||
fetchVoiceStatusDiscord: vi.fn(),
|
||||
hasAnyGuildPermissionDiscord: vi.fn(),
|
||||
kickMemberDiscord: vi.fn(),
|
||||
listGuildChannelsDiscord: vi.fn(),
|
||||
listGuildEmojisDiscord: vi.fn(),
|
||||
listPinsDiscord: vi.fn(),
|
||||
listScheduledEventsDiscord: vi.fn(),
|
||||
listThreadsDiscord: vi.fn(),
|
||||
moveChannelDiscord: vi.fn(),
|
||||
pinMessageDiscord: vi.fn(),
|
||||
reactMessageDiscord: async (...args: unknown[]) => {
|
||||
reactMock(...args);
|
||||
},
|
||||
readMessagesDiscord: vi.fn(),
|
||||
removeChannelPermissionDiscord: vi.fn(),
|
||||
removeOwnReactionsDiscord: vi.fn(),
|
||||
removeReactionDiscord: vi.fn(),
|
||||
removeRoleDiscord: vi.fn(),
|
||||
searchMessagesDiscord: vi.fn(),
|
||||
sendDiscordComponentMessage: vi.fn(),
|
||||
sendMessageDiscord: (...args: unknown[]) => sendMock(...args),
|
||||
sendPollDiscord: vi.fn(),
|
||||
sendStickerDiscord: vi.fn(),
|
||||
sendVoiceMessageDiscord: vi.fn(),
|
||||
setChannelPermissionDiscord: vi.fn(),
|
||||
timeoutMemberDiscord: vi.fn(),
|
||||
unpinMessageDiscord: vi.fn(),
|
||||
uploadEmojiDiscord: vi.fn(),
|
||||
uploadStickerDiscord: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("openclaw/plugin-sdk/reply-runtime", async (importOriginal) => {
|
||||
@ -36,12 +77,27 @@ function createPairingStoreMocks() {
|
||||
};
|
||||
}
|
||||
|
||||
vi.mock("openclaw/plugin-sdk/conversation-runtime", () => createPairingStoreMocks());
|
||||
vi.mock("openclaw/plugin-sdk/conversation-runtime", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("openclaw/plugin-sdk/conversation-runtime")>();
|
||||
return {
|
||||
...actual,
|
||||
...createPairingStoreMocks(),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("openclaw/plugin-sdk/channel-runtime", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("openclaw/plugin-sdk/channel-runtime")>();
|
||||
return {
|
||||
...actual,
|
||||
recordInboundSession: (...args: unknown[]) => recordInboundSessionMock(...args),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("openclaw/plugin-sdk/config-runtime", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("openclaw/plugin-sdk/config-runtime")>();
|
||||
return {
|
||||
...actual,
|
||||
readSessionUpdatedAt: vi.fn(() => undefined),
|
||||
resolveStorePath: vi.fn(() => "/tmp/openclaw-sessions.json"),
|
||||
updateLastRoute: (...args: unknown[]) => updateLastRouteMock(...args),
|
||||
resolveSessionKey: vi.fn(),
|
||||
|
||||
@ -68,6 +68,7 @@ const readSessionUpdatedAt = configSessionsMocks.readSessionUpdatedAt;
|
||||
const resolveStorePath = configSessionsMocks.resolveStorePath;
|
||||
|
||||
vi.mock("../send.js", () => ({
|
||||
addRoleDiscord: vi.fn(),
|
||||
reactMessageDiscord: sendMocks.reactMessageDiscord,
|
||||
removeReactionDiscord: sendMocks.removeReactionDiscord,
|
||||
}));
|
||||
|
||||
@ -9,9 +9,13 @@ vi.mock("../../../../src/media/fetch.js", () => ({
|
||||
fetchRemoteMedia: (...args: unknown[]) => fetchRemoteMedia(...args),
|
||||
}));
|
||||
|
||||
vi.mock("../../../../src/media/store.js", () => ({
|
||||
saveMediaBuffer: (...args: unknown[]) => saveMediaBuffer(...args),
|
||||
}));
|
||||
vi.mock("../../../../src/media/store.js", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("../../../../src/media/store.js")>();
|
||||
return {
|
||||
...actual,
|
||||
saveMediaBuffer: (...args: unknown[]) => saveMediaBuffer(...args),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("../../../../src/globals.js", () => ({
|
||||
logVerbose: () => {},
|
||||
|
||||
@ -25,6 +25,7 @@ vi.mock("../client.js", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("../send.js", () => ({
|
||||
addRoleDiscord: vi.fn(),
|
||||
sendMessageDiscord: (...args: unknown[]) => hoisted.sendMessageDiscord(...args),
|
||||
sendWebhookMessageDiscord: (...args: unknown[]) => hoisted.sendWebhookMessageDiscord(...args),
|
||||
}));
|
||||
|
||||
@ -42,6 +42,7 @@ const hoisted = vi.hoisted(() => {
|
||||
});
|
||||
|
||||
vi.mock("../send.js", () => ({
|
||||
addRoleDiscord: vi.fn(),
|
||||
sendMessageDiscord: hoisted.sendMessageDiscord,
|
||||
sendWebhookMessageDiscord: hoisted.sendWebhookMessageDiscord,
|
||||
}));
|
||||
|
||||
@ -10,6 +10,8 @@ import {
|
||||
} from "../api.js";
|
||||
import type { OpenClawPluginApi } from "../api.js";
|
||||
|
||||
const AjvCtor = Ajv as unknown as typeof import("ajv").default;
|
||||
|
||||
function stripCodeFences(s: string): string {
|
||||
const trimmed = s.trim();
|
||||
const m = trimmed.match(/^```(?:json)?\s*([\s\S]*?)\s*```$/i);
|
||||
@ -214,7 +216,7 @@ export function createLlmTaskTool(api: OpenClawPluginApi) {
|
||||
// oxlint-disable-next-line typescript/no-explicit-any
|
||||
const schema = (params as any).schema as unknown;
|
||||
if (schema && typeof schema === "object" && !Array.isArray(schema)) {
|
||||
const ajv = new Ajv.default({ allErrors: true, strict: false });
|
||||
const ajv = new AjvCtor({ allErrors: true, strict: false });
|
||||
// oxlint-disable-next-line typescript/no-explicit-any
|
||||
const validate = ajv.compile(schema as any);
|
||||
const ok = validate(parsed);
|
||||
|
||||
@ -14,8 +14,8 @@ describe("matrix runtime-api", () => {
|
||||
expect(typeof runtimeApi.buildSecretInputSchema).toBe("function");
|
||||
});
|
||||
|
||||
it("does not re-export setup entrypoints that create extension cycles", () => {
|
||||
expect("matrixSetupWizard" in runtimeApi).toBe(false);
|
||||
expect("matrixSetupAdapter" in runtimeApi).toBe(false);
|
||||
it("re-exports setup entrypoints from the bundled plugin-sdk surface", () => {
|
||||
expect(typeof runtimeApi.matrixSetupWizard).toBe("object");
|
||||
expect(typeof runtimeApi.matrixSetupAdapter).toBe("object");
|
||||
});
|
||||
});
|
||||
|
||||
@ -3,7 +3,6 @@ import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { OpenClawConfig } from "../../runtime-api.js";
|
||||
import { buildModelsProviderData } from "../../runtime-api.js";
|
||||
import {
|
||||
buildMattermostAllowedModelRefs,
|
||||
parseMattermostModelPickerContext,
|
||||
@ -145,7 +144,17 @@ describe("Mattermost model picker", () => {
|
||||
],
|
||||
},
|
||||
};
|
||||
const providerData = await buildModelsProviderData(cfg, "support");
|
||||
const providerData = {
|
||||
byProvider: new Map<string, Set<string>>([
|
||||
["anthropic", new Set(["claude-opus-4-5"])],
|
||||
["openai", new Set(["gpt-5"])],
|
||||
]),
|
||||
providers: ["anthropic", "openai"],
|
||||
resolvedDefault: {
|
||||
provider: "openai",
|
||||
model: "gpt-5",
|
||||
},
|
||||
};
|
||||
|
||||
expect(
|
||||
resolveMattermostModelPickerCurrentModel({
|
||||
|
||||
@ -76,9 +76,13 @@ vi.mock("openclaw/plugin-sdk/config-runtime", async (importOriginal) => {
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("openclaw/plugin-sdk/reply-runtime", () => ({
|
||||
getReplyFromConfig: (...args: unknown[]) => replyMock(...args),
|
||||
}));
|
||||
vi.mock("openclaw/plugin-sdk/reply-runtime", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("openclaw/plugin-sdk/reply-runtime")>();
|
||||
return {
|
||||
...actual,
|
||||
getReplyFromConfig: (...args: unknown[]) => replyMock(...args),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("./send.js", () => ({
|
||||
sendMessageSignal: (...args: unknown[]) => sendMock(...args),
|
||||
@ -116,9 +120,13 @@ vi.mock("./daemon.js", async (importOriginal) => {
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("openclaw/plugin-sdk/infra-runtime", () => ({
|
||||
waitForTransportReady: (...args: unknown[]) => waitForTransportReadyMock(...args),
|
||||
}));
|
||||
vi.mock("openclaw/plugin-sdk/infra-runtime", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("openclaw/plugin-sdk/infra-runtime")>();
|
||||
return {
|
||||
...actual,
|
||||
waitForTransportReady: (...args: unknown[]) => waitForTransportReadyMock(...args),
|
||||
};
|
||||
});
|
||||
|
||||
export function installSignalToolResultTestHooks() {
|
||||
beforeEach(() => {
|
||||
|
||||
@ -22,7 +22,7 @@ vi.mock("../../../src/infra/net/fetch-guard.js", () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("../../whatsapp/src/media.js", () => ({
|
||||
vi.mock("openclaw/plugin-sdk/web-media", () => ({
|
||||
loadWebMedia: vi.fn(async () => ({
|
||||
buffer: Buffer.from("fake-image"),
|
||||
contentType: "image/png",
|
||||
|
||||
@ -27,11 +27,18 @@ vi.mock("./bot/delivery.js", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("./send.js", () => ({
|
||||
createForumTopicTelegram: vi.fn(),
|
||||
deleteMessageTelegram: vi.fn(),
|
||||
editForumTopicTelegram: vi.fn(),
|
||||
editMessageTelegram,
|
||||
reactMessageTelegram: vi.fn(),
|
||||
sendMessageTelegram: vi.fn(),
|
||||
sendPollTelegram: vi.fn(),
|
||||
sendStickerTelegram: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("../../../src/config/sessions.js", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("../../../src/config/sessions.js")>();
|
||||
vi.mock("openclaw/plugin-sdk/config-runtime", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("openclaw/plugin-sdk/config-runtime")>();
|
||||
return {
|
||||
...actual,
|
||||
loadSessionStore,
|
||||
|
||||
@ -29,14 +29,18 @@ type MockWebListener = {
|
||||
|
||||
export const TEST_NET_IP = "203.0.113.10";
|
||||
|
||||
vi.mock("openclaw/plugin-sdk/agent-runtime", () => ({
|
||||
abortEmbeddedPiRun: vi.fn().mockReturnValue(false),
|
||||
isEmbeddedPiRunActive: vi.fn().mockReturnValue(false),
|
||||
isEmbeddedPiRunStreaming: vi.fn().mockReturnValue(false),
|
||||
runEmbeddedPiAgent: vi.fn(),
|
||||
queueEmbeddedPiMessage: vi.fn().mockReturnValue(false),
|
||||
resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`,
|
||||
}));
|
||||
vi.mock("openclaw/plugin-sdk/agent-runtime", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("openclaw/plugin-sdk/agent-runtime")>();
|
||||
return {
|
||||
...actual,
|
||||
abortEmbeddedPiRun: vi.fn().mockReturnValue(false),
|
||||
isEmbeddedPiRunActive: vi.fn().mockReturnValue(false),
|
||||
isEmbeddedPiRunStreaming: vi.fn().mockReturnValue(false),
|
||||
runEmbeddedPiAgent: vi.fn(),
|
||||
queueEmbeddedPiMessage: vi.fn().mockReturnValue(false),
|
||||
resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`,
|
||||
};
|
||||
});
|
||||
|
||||
export async function rmDirWithRetries(
|
||||
dir: string,
|
||||
|
||||
@ -1,13 +1,23 @@
|
||||
import "./test-helpers.js";
|
||||
import fs from "node:fs/promises";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import type { OpenClawConfig } from "../../../src/config/config.js";
|
||||
import { installWebAutoReplyUnitTestHooks, makeSessionStore } from "./auto-reply.test-harness.js";
|
||||
import { buildMentionConfig } from "./auto-reply/mentions.js";
|
||||
import { createEchoTracker } from "./auto-reply/monitor/echo.js";
|
||||
import { awaitBackgroundTasks } from "./auto-reply/monitor/last-route.js";
|
||||
import { createWebOnMessageHandler } from "./auto-reply/monitor/on-message.js";
|
||||
|
||||
const updateLastRouteInBackgroundMock = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock("./auto-reply/monitor/last-route.js", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("./auto-reply/monitor/last-route.js")>();
|
||||
return {
|
||||
...actual,
|
||||
updateLastRouteInBackground: (...args: unknown[]) => updateLastRouteInBackgroundMock(...args),
|
||||
};
|
||||
});
|
||||
|
||||
const { awaitBackgroundTasks } = await import("./auto-reply/monitor/last-route.js");
|
||||
|
||||
function makeCfg(storePath: string): OpenClawConfig {
|
||||
return {
|
||||
channels: { whatsapp: { allowFrom: ["*"] } },
|
||||
@ -86,13 +96,6 @@ function buildInboundMessage(params: {
|
||||
};
|
||||
}
|
||||
|
||||
async function readStoredRoutes(storePath: string) {
|
||||
return JSON.parse(await fs.readFile(storePath, "utf8")) as Record<
|
||||
string,
|
||||
{ lastChannel?: string; lastTo?: string; lastAccountId?: string }
|
||||
>;
|
||||
}
|
||||
|
||||
describe("web auto-reply last-route", () => {
|
||||
installWebAutoReplyUnitTestHooks();
|
||||
|
||||
@ -118,9 +121,12 @@ describe("web auto-reply last-route", () => {
|
||||
|
||||
await awaitBackgroundTasks(backgroundTasks);
|
||||
|
||||
const stored = await readStoredRoutes(store.storePath);
|
||||
expect(stored[mainSessionKey]?.lastChannel).toBe("whatsapp");
|
||||
expect(stored[mainSessionKey]?.lastTo).toBe("+1000");
|
||||
expect(updateLastRouteInBackgroundMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
channel: "whatsapp",
|
||||
to: "+1000",
|
||||
}),
|
||||
);
|
||||
|
||||
await store.cleanup();
|
||||
});
|
||||
@ -151,10 +157,13 @@ describe("web auto-reply last-route", () => {
|
||||
|
||||
await awaitBackgroundTasks(backgroundTasks);
|
||||
|
||||
const stored = await readStoredRoutes(store.storePath);
|
||||
expect(stored[groupSessionKey]?.lastChannel).toBe("whatsapp");
|
||||
expect(stored[groupSessionKey]?.lastTo).toBe("123@g.us");
|
||||
expect(stored[groupSessionKey]?.lastAccountId).toBe("work");
|
||||
expect(updateLastRouteInBackgroundMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
channel: "whatsapp",
|
||||
to: "123@g.us",
|
||||
accountId: "work",
|
||||
}),
|
||||
);
|
||||
|
||||
await store.cleanup();
|
||||
});
|
||||
|
||||
@ -41,9 +41,13 @@ vi.mock("../../../../src/config/config.js", () => ({
|
||||
loadConfig: () => ({ agents: { defaults: {} }, session: {} }),
|
||||
}));
|
||||
|
||||
vi.mock("../../../../src/routing/session-key.js", () => ({
|
||||
normalizeMainKey: () => null,
|
||||
}));
|
||||
vi.mock("../../../../src/routing/session-key.js", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("../../../../src/routing/session-key.js")>();
|
||||
return {
|
||||
...actual,
|
||||
normalizeMainKey: () => null,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("../../../../src/infra/heartbeat-visibility.js", () => ({
|
||||
resolveHeartbeatVisibility: () => state.visibility,
|
||||
@ -74,6 +78,42 @@ vi.mock("../../../../src/logging.js", () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("openclaw/plugin-sdk/state-paths", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("openclaw/plugin-sdk/state-paths")>();
|
||||
return {
|
||||
...actual,
|
||||
resolveOAuthDir: () => "/tmp/openclaw-oauth",
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("openclaw/plugin-sdk/runtime-env", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("openclaw/plugin-sdk/runtime-env")>();
|
||||
const logger = {
|
||||
child: () => logger,
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
};
|
||||
return {
|
||||
...actual,
|
||||
createSubsystemLogger: () => logger,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("../auth-store.js", () => ({
|
||||
WA_WEB_AUTH_DIR: "/tmp/openclaw-oauth/whatsapp/default",
|
||||
resolveDefaultWebAuthDir: () => "/tmp/openclaw-oauth/whatsapp/default",
|
||||
hasWebCredsSync: () => false,
|
||||
maybeRestoreCredsFromBackup: () => undefined,
|
||||
webAuthExists: async () => false,
|
||||
logoutWeb: async () => undefined,
|
||||
readWebSelfId: () => null,
|
||||
getWebAuthAgeMs: () => null,
|
||||
logWebSelfId: () => undefined,
|
||||
pickWebChannel: async () => undefined,
|
||||
}));
|
||||
|
||||
vi.mock("./loggers.js", () => ({
|
||||
whatsappHeartbeatLog: {
|
||||
info: (msg: string) => state.heartbeatInfoLogs.push(msg),
|
||||
@ -87,6 +127,7 @@ vi.mock("../reconnect.js", () => ({
|
||||
|
||||
vi.mock("../send.js", () => ({
|
||||
sendMessageWhatsApp: vi.fn(async () => ({ messageId: "m1" })),
|
||||
sendReactionWhatsApp: vi.fn(async () => undefined),
|
||||
}));
|
||||
|
||||
vi.mock("../session.js", () => ({
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import fsSync from "node:fs";
|
||||
import fs from "node:fs/promises";
|
||||
import { vi } from "vitest";
|
||||
import type { MockBaileysSocket } from "../../../test/mocks/baileys.js";
|
||||
import { createMockBaileys } from "../../../test/mocks/baileys.js";
|
||||
@ -41,6 +43,31 @@ vi.mock("openclaw/plugin-sdk/config-runtime", async (importOriginal) => {
|
||||
}
|
||||
return DEFAULT_CONFIG;
|
||||
},
|
||||
updateLastRoute: async (params: {
|
||||
storePath: string;
|
||||
sessionKey: string;
|
||||
deliveryContext: { channel: string; to: string; accountId?: string };
|
||||
}) => {
|
||||
const raw = await fs.readFile(params.storePath, "utf8").catch(() => "{}");
|
||||
const store = JSON.parse(raw) as Record<string, Record<string, unknown>>;
|
||||
const current = store[params.sessionKey] ?? {};
|
||||
store[params.sessionKey] = {
|
||||
...current,
|
||||
lastChannel: params.deliveryContext.channel,
|
||||
lastTo: params.deliveryContext.to,
|
||||
lastAccountId: params.deliveryContext.accountId,
|
||||
};
|
||||
await fs.writeFile(params.storePath, JSON.stringify(store));
|
||||
},
|
||||
loadSessionStore: (storePath: string) => {
|
||||
try {
|
||||
return JSON.parse(fsSync.readFileSync(storePath, "utf8")) as Record<string, unknown>;
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
},
|
||||
recordSessionMetaFromInbound: async () => undefined,
|
||||
resolveStorePath: actual.resolveStorePath,
|
||||
};
|
||||
});
|
||||
|
||||
@ -82,6 +109,14 @@ vi.mock("openclaw/plugin-sdk/media-runtime", async (importOriginal) => {
|
||||
return mockModule;
|
||||
});
|
||||
|
||||
vi.mock("openclaw/plugin-sdk/state-paths", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("openclaw/plugin-sdk/state-paths")>();
|
||||
return {
|
||||
...actual,
|
||||
resolveOAuthDir: () => "/tmp/openclaw-oauth",
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("@whiskeysockets/baileys", () => {
|
||||
const created = createMockBaileys();
|
||||
(globalThis as Record<PropertyKey, unknown>)[Symbol.for("openclaw:lastSocket")] =
|
||||
|
||||
@ -289,7 +289,7 @@ describe("resolveCommandSecretRefsViaGateway", () => {
|
||||
expect(result.targetStatesByPath["tools.web.search.gemini.apiKey"]).toBe("resolved_local");
|
||||
expectGatewayUnavailableLocalFallbackDiagnostics(result);
|
||||
});
|
||||
});
|
||||
}, 300_000);
|
||||
|
||||
it("falls back to local resolution for Firecrawl SecretRefs when gateway is unavailable", async () => {
|
||||
const envKey = "WEB_FETCH_FIRECRAWL_API_KEY_LOCAL_FALLBACK";
|
||||
|
||||
@ -97,6 +97,78 @@ function targetsRuntimeWebPath(path: string): boolean {
|
||||
return WEB_RUNTIME_SECRET_PATH_PREFIXES.some((prefix) => path.startsWith(prefix));
|
||||
}
|
||||
|
||||
function classifyRuntimeWebTargetPathState(params: {
|
||||
config: OpenClawConfig;
|
||||
path: string;
|
||||
}): "active" | "inactive" | "unknown" {
|
||||
if (params.path === "tools.web.fetch.firecrawl.apiKey") {
|
||||
const fetch = params.config.tools?.web?.fetch;
|
||||
return fetch?.enabled !== false && fetch?.firecrawl?.enabled !== false ? "active" : "inactive";
|
||||
}
|
||||
|
||||
if (params.path === "tools.web.search.apiKey") {
|
||||
return params.config.tools?.web?.search?.enabled !== false ? "active" : "inactive";
|
||||
}
|
||||
|
||||
const match = /^tools\.web\.search\.([^.]+)\.apiKey$/.exec(params.path);
|
||||
if (!match) {
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
const search = params.config.tools?.web?.search;
|
||||
if (search?.enabled === false) {
|
||||
return "inactive";
|
||||
}
|
||||
|
||||
const configuredProvider =
|
||||
typeof search?.provider === "string" ? search.provider.trim().toLowerCase() : "";
|
||||
if (!configuredProvider) {
|
||||
return "active";
|
||||
}
|
||||
|
||||
return configuredProvider === match[1] ? "active" : "inactive";
|
||||
}
|
||||
|
||||
function describeInactiveRuntimeWebTargetPath(params: {
|
||||
config: OpenClawConfig;
|
||||
path: string;
|
||||
}): string | undefined {
|
||||
if (params.path === "tools.web.fetch.firecrawl.apiKey") {
|
||||
const fetch = params.config.tools?.web?.fetch;
|
||||
if (fetch?.enabled === false) {
|
||||
return "tools.web.fetch is disabled.";
|
||||
}
|
||||
if (fetch?.firecrawl?.enabled === false) {
|
||||
return "tools.web.fetch.firecrawl.enabled is false.";
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (params.path === "tools.web.search.apiKey") {
|
||||
return params.config.tools?.web?.search?.enabled === false
|
||||
? "tools.web.search is disabled."
|
||||
: undefined;
|
||||
}
|
||||
|
||||
const match = /^tools\.web\.search\.([^.]+)\.apiKey$/.exec(params.path);
|
||||
if (!match) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const search = params.config.tools?.web?.search;
|
||||
if (search?.enabled === false) {
|
||||
return "tools.web.search is disabled.";
|
||||
}
|
||||
|
||||
const configuredProvider =
|
||||
typeof search?.provider === "string" ? search.provider.trim().toLowerCase() : "";
|
||||
if (configuredProvider && configuredProvider !== match[1]) {
|
||||
return `tools.web.search.provider is "${configuredProvider}".`;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function targetsRuntimeWebResolution(params: {
|
||||
targetIds: ReadonlySet<string>;
|
||||
allowedPaths?: ReadonlySet<string>;
|
||||
@ -285,6 +357,34 @@ async function resolveCommandSecretRefsLocally(params: {
|
||||
.filter((warning) => !params.allowedPaths || params.allowedPaths.has(warning.path))
|
||||
.map((warning) => warning.path),
|
||||
);
|
||||
const runtimeWebActivePaths = new Set<string>();
|
||||
const runtimeWebInactiveDiagnostics: string[] = [];
|
||||
for (const target of discoverConfigSecretTargetsByIds(sourceConfig, params.targetIds)) {
|
||||
if (!targetsRuntimeWebPath(target.path)) {
|
||||
continue;
|
||||
}
|
||||
if (params.allowedPaths && !params.allowedPaths.has(target.path)) {
|
||||
continue;
|
||||
}
|
||||
const runtimeState = classifyRuntimeWebTargetPathState({
|
||||
config: sourceConfig,
|
||||
path: target.path,
|
||||
});
|
||||
if (runtimeState === "inactive") {
|
||||
inactiveRefPaths.add(target.path);
|
||||
const inactiveDetail = describeInactiveRuntimeWebTargetPath({
|
||||
config: sourceConfig,
|
||||
path: target.path,
|
||||
});
|
||||
if (inactiveDetail) {
|
||||
runtimeWebInactiveDiagnostics.push(`${target.path}: ${inactiveDetail}`);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (runtimeState === "active") {
|
||||
runtimeWebActivePaths.add(target.path);
|
||||
}
|
||||
}
|
||||
const inactiveWarningDiagnostics = context.warnings
|
||||
.filter((warning) => warning.code === "SECRETS_REF_IGNORED_INACTIVE_SURFACE")
|
||||
.filter((warning) => !params.allowedPaths || params.allowedPaths.has(warning.path))
|
||||
@ -301,6 +401,7 @@ async function resolveCommandSecretRefsLocally(params: {
|
||||
env: context.env,
|
||||
cache: context.cache,
|
||||
activePaths,
|
||||
runtimeWebActivePaths,
|
||||
inactiveRefPaths,
|
||||
mode: params.mode,
|
||||
commandName: params.commandName,
|
||||
@ -330,6 +431,7 @@ async function resolveCommandSecretRefsLocally(params: {
|
||||
resolvedConfig,
|
||||
diagnostics: dedupeDiagnostics([
|
||||
...params.preflightDiagnostics,
|
||||
...runtimeWebInactiveDiagnostics,
|
||||
...inactiveWarningDiagnostics,
|
||||
...filterInactiveSurfaceDiagnostics({
|
||||
diagnostics: analyzed.diagnostics,
|
||||
@ -405,6 +507,7 @@ async function resolveTargetSecretLocally(params: {
|
||||
env: NodeJS.ProcessEnv;
|
||||
cache: ReturnType<typeof createResolverContext>["cache"];
|
||||
activePaths: ReadonlySet<string>;
|
||||
runtimeWebActivePaths: ReadonlySet<string>;
|
||||
inactiveRefPaths: ReadonlySet<string>;
|
||||
mode: CommandSecretResolutionMode;
|
||||
commandName: string;
|
||||
@ -419,7 +522,8 @@ async function resolveTargetSecretLocally(params: {
|
||||
if (
|
||||
!ref ||
|
||||
params.inactiveRefPaths.has(params.target.path) ||
|
||||
!params.activePaths.has(params.target.path)
|
||||
(!params.activePaths.has(params.target.path) &&
|
||||
!params.runtimeWebActivePaths.has(params.target.path))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -80,6 +80,7 @@ export type ConfigDocBaselineStatefileWriteResult = {
|
||||
const GENERATED_BY = "scripts/generate-config-doc-baseline.ts" as const;
|
||||
const DEFAULT_JSON_OUTPUT = "docs/.generated/config-baseline.json";
|
||||
const DEFAULT_STATEFILE_OUTPUT = "docs/.generated/config-baseline.jsonl";
|
||||
let cachedConfigDocBaselinePromise: Promise<ConfigDocBaseline> | null = null;
|
||||
|
||||
function logConfigDocBaselineDebug(message: string): void {
|
||||
if (process.env.OPENCLAW_CONFIG_DOC_BASELINE_DEBUG === "1") {
|
||||
@ -622,26 +623,37 @@ export function dedupeConfigDocBaselineEntries(
|
||||
}
|
||||
|
||||
export async function buildConfigDocBaseline(): Promise<ConfigDocBaseline> {
|
||||
const start = Date.now();
|
||||
logConfigDocBaselineDebug("build baseline start");
|
||||
const response = await loadBundledConfigSchemaResponse();
|
||||
const schemaRoot = asSchemaObject(response.schema);
|
||||
if (!schemaRoot) {
|
||||
throw new Error("config schema root is not an object");
|
||||
if (cachedConfigDocBaselinePromise) {
|
||||
return await cachedConfigDocBaselinePromise;
|
||||
}
|
||||
cachedConfigDocBaselinePromise = (async () => {
|
||||
const start = Date.now();
|
||||
logConfigDocBaselineDebug("build baseline start");
|
||||
const response = await loadBundledConfigSchemaResponse();
|
||||
const schemaRoot = asSchemaObject(response.schema);
|
||||
if (!schemaRoot) {
|
||||
throw new Error("config schema root is not an object");
|
||||
}
|
||||
const collectStart = Date.now();
|
||||
logConfigDocBaselineDebug("collect baseline entries start");
|
||||
const entries = dedupeConfigDocBaselineEntries(
|
||||
collectConfigDocBaselineEntries(schemaRoot, response.uiHints),
|
||||
);
|
||||
logConfigDocBaselineDebug(
|
||||
`collect baseline entries done count=${entries.length} elapsedMs=${Date.now() - collectStart}`,
|
||||
);
|
||||
logConfigDocBaselineDebug(`build baseline done elapsedMs=${Date.now() - start}`);
|
||||
return {
|
||||
generatedBy: GENERATED_BY,
|
||||
entries,
|
||||
};
|
||||
})();
|
||||
try {
|
||||
return await cachedConfigDocBaselinePromise;
|
||||
} catch (error) {
|
||||
cachedConfigDocBaselinePromise = null;
|
||||
throw error;
|
||||
}
|
||||
const collectStart = Date.now();
|
||||
logConfigDocBaselineDebug("collect baseline entries start");
|
||||
const entries = dedupeConfigDocBaselineEntries(
|
||||
collectConfigDocBaselineEntries(schemaRoot, response.uiHints),
|
||||
);
|
||||
logConfigDocBaselineDebug(
|
||||
`collect baseline entries done count=${entries.length} elapsedMs=${Date.now() - collectStart}`,
|
||||
);
|
||||
logConfigDocBaselineDebug(`build baseline done elapsedMs=${Date.now() - start}`);
|
||||
return {
|
||||
generatedBy: GENERATED_BY,
|
||||
entries,
|
||||
};
|
||||
}
|
||||
|
||||
export async function renderConfigDocBaselineStatefile(
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { ChannelOutboundAdapter, ChannelPlugin } from "../../channels/plugins/types.js";
|
||||
import { setActivePluginRegistry } from "../../plugins/runtime.js";
|
||||
import { createMSTeamsTestPlugin, createTestRegistry } from "../../test-utils/channel-plugins.js";
|
||||
@ -19,9 +19,11 @@ vi.mock("../../gateway/call.js", () => ({
|
||||
let sendMessage: typeof import("./message.js").sendMessage;
|
||||
let sendPoll: typeof import("./message.js").sendPoll;
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.resetModules();
|
||||
beforeAll(async () => {
|
||||
({ sendMessage, sendPoll } = await import("./message.js"));
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
callGatewayMock.mockClear();
|
||||
setRegistry(emptyRegistry);
|
||||
});
|
||||
|
||||
@ -95,6 +95,11 @@ await build(${JSON.stringify({
|
||||
await execFileAsync(process.execPath, [buildScriptPath], {
|
||||
cwd: process.cwd(),
|
||||
});
|
||||
await fs.symlink(
|
||||
path.join(process.cwd(), "node_modules"),
|
||||
path.join(outDir, "node_modules"),
|
||||
"dir",
|
||||
);
|
||||
|
||||
for (const entry of pluginSdkEntrypoints) {
|
||||
const module = await import(pathToFileURL(path.join(outDir, `${entry}.js`)).href);
|
||||
@ -107,6 +112,12 @@ await build(${JSON.stringify({
|
||||
|
||||
await fs.mkdir(path.join(packageDir, "dist"), { recursive: true });
|
||||
await fs.symlink(outDir, path.join(packageDir, "dist", "plugin-sdk"), "dir");
|
||||
// Mirror the installed package layout so subpaths can resolve root deps.
|
||||
await fs.symlink(
|
||||
path.join(process.cwd(), "node_modules"),
|
||||
path.join(packageDir, "node_modules"),
|
||||
"dir",
|
||||
);
|
||||
await fs.writeFile(
|
||||
path.join(packageDir, "package.json"),
|
||||
JSON.stringify(
|
||||
|
||||
@ -34,9 +34,9 @@ const RUNTIME_API_EXPORT_GUARDS: Record<string, readonly string[]> = {
|
||||
'export { probeIMessage } from "./src/probe.js";',
|
||||
'export { sendMessageIMessage } from "./src/send.js";',
|
||||
],
|
||||
"extensions/googlechat/runtime-api.ts": ['export * from "../../src/plugin-sdk/googlechat.js";'],
|
||||
"extensions/googlechat/runtime-api.ts": ['export * from "openclaw/plugin-sdk/googlechat";'],
|
||||
"extensions/nextcloud-talk/runtime-api.ts": [
|
||||
'export * from "../../src/plugin-sdk/nextcloud-talk.js";',
|
||||
'export * from "openclaw/plugin-sdk/nextcloud-talk";',
|
||||
],
|
||||
"extensions/signal/runtime-api.ts": ['export * from "./src/runtime-api.js";'],
|
||||
"extensions/slack/runtime-api.ts": [
|
||||
|
||||
@ -3327,8 +3327,8 @@ module.exports = {
|
||||
|
||||
it("derives plugin-sdk subpaths from package exports", () => {
|
||||
const subpaths = __testing.listPluginSdkExportedSubpaths();
|
||||
expect(subpaths).toContain("compat");
|
||||
expect(subpaths).toContain("telegram");
|
||||
expect(subpaths).not.toContain("compat");
|
||||
expect(subpaths).not.toContain("root-alias");
|
||||
});
|
||||
|
||||
@ -3351,7 +3351,7 @@ module.exports = {
|
||||
|
||||
it("loads source runtime shims through the non-native Jiti boundary", async () => {
|
||||
const jiti = createJiti(import.meta.url, {
|
||||
...__testing.buildPluginLoaderJitiOptions({}),
|
||||
...__testing.buildPluginLoaderJitiOptions(__testing.resolvePluginSdkScopedAliasMap()),
|
||||
tryNative: false,
|
||||
});
|
||||
const discordChannelRuntime = path.join(
|
||||
|
||||
@ -140,6 +140,7 @@ export const __testing = {
|
||||
buildPluginLoaderJitiOptions,
|
||||
listPluginSdkAliasCandidates,
|
||||
listPluginSdkExportedSubpaths,
|
||||
resolvePluginSdkScopedAliasMap,
|
||||
resolvePluginSdkAliasCandidateOrder,
|
||||
resolvePluginSdkAliasFile,
|
||||
resolvePluginRuntimeModulePath,
|
||||
|
||||
@ -208,23 +208,16 @@ function ensureObject(target: Record<string, unknown>, key: string): Record<stri
|
||||
|
||||
function setResolvedWebSearchApiKey(params: {
|
||||
resolvedConfig: OpenClawConfig;
|
||||
provider: WebSearchProvider;
|
||||
provider: PluginWebSearchProviderEntry;
|
||||
value: string;
|
||||
sourceConfig: OpenClawConfig;
|
||||
env: NodeJS.ProcessEnv;
|
||||
}): void {
|
||||
const tools = ensureObject(params.resolvedConfig as Record<string, unknown>, "tools");
|
||||
const web = ensureObject(tools, "web");
|
||||
const search = ensureObject(web, "search");
|
||||
const provider = resolvePluginWebSearchProviders({
|
||||
config: params.sourceConfig,
|
||||
env: { ...process.env, ...params.env },
|
||||
bundledAllowlistCompat: true,
|
||||
}).find((entry) => entry.id === params.provider);
|
||||
if (provider?.setConfiguredCredentialValue) {
|
||||
provider.setConfiguredCredentialValue(params.resolvedConfig, params.value);
|
||||
if (params.provider.setConfiguredCredentialValue) {
|
||||
params.provider.setConfiguredCredentialValue(params.resolvedConfig, params.value);
|
||||
}
|
||||
provider?.setCredentialValue(search, params.value);
|
||||
params.provider.setCredentialValue(search, params.value);
|
||||
}
|
||||
|
||||
function setResolvedFirecrawlApiKey(params: {
|
||||
@ -364,10 +357,8 @@ export async function resolveRuntimeWebTools(params: {
|
||||
if (resolution.value) {
|
||||
setResolvedWebSearchApiKey({
|
||||
resolvedConfig: params.resolvedConfig,
|
||||
provider: provider.id,
|
||||
provider,
|
||||
value: resolution.value,
|
||||
sourceConfig: params.sourceConfig,
|
||||
env: params.context.env,
|
||||
});
|
||||
}
|
||||
break;
|
||||
@ -378,10 +369,8 @@ export async function resolveRuntimeWebTools(params: {
|
||||
selectedResolution = resolution;
|
||||
setResolvedWebSearchApiKey({
|
||||
resolvedConfig: params.resolvedConfig,
|
||||
provider: provider.id,
|
||||
provider,
|
||||
value: resolution.value,
|
||||
sourceConfig: params.sourceConfig,
|
||||
env: params.context.env,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
@ -473,4 +473,5 @@ vi.mock("../../../extensions/discord/src/monitor/thread-bindings.js", () => ({
|
||||
createNoopThreadBindingManager: createNoopThreadBindingManagerMock,
|
||||
createThreadBindingManager: createThreadBindingManagerMock,
|
||||
reconcileAcpThreadBindingsOnStartup: reconcileAcpThreadBindingsOnStartupMock,
|
||||
resolveThreadBindingIdleTimeoutMs: vi.fn(() => 24 * 60 * 60 * 1000),
|
||||
}));
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user