test(plugin-sdk): use narrow config runtime mocks

This commit is contained in:
Peter Steinberger
2026-04-27 15:13:53 +01:00
parent a2af8054e1
commit 9090457da7
45 changed files with 229 additions and 130 deletions

View File

@@ -145,8 +145,10 @@ releases.
| Secret input resolution | `openclaw/plugin-sdk/secret-input-runtime` |
| Model/session overrides | `openclaw/plugin-sdk/model-session-runtime` |
Bundled production plugins are scanner-guarded against the broad barrel so
imports stay local to the behavior they need.
Bundled plugins and their tests are scanner-guarded against the broad
barrel so imports and mocks stay local to the behavior they need. The broad
barrel still exists for external compatibility, but new code should not
depend on it.
</Step>
@@ -233,6 +235,7 @@ releases.
```bash
grep -r "plugin-sdk/compat" my-plugin/
grep -r "plugin-sdk/config-runtime" my-plugin/
grep -r "openclaw/extension-api" my-plugin/
```

View File

@@ -41,13 +41,14 @@ Persist changes with `api.runtime.config.mutateConfigFile(...)` or `api.runtime.
The mutation helpers return `afterWrite` plus a typed `followUp` summary so callers can log or test whether they requested a restart. The gateway still owns when that restart actually happens.
`api.runtime.config.loadConfig()` and `api.runtime.config.writeConfigFile(...)` are deprecated compatibility helpers under `runtime-config-load-write`. They warn once at runtime, and bundled plugins must not use them; the config boundary guards fail if production plugin code calls them or imports those helpers from plugin SDK subpaths.
`api.runtime.config.loadConfig()` and `api.runtime.config.writeConfigFile(...)` are deprecated compatibility helpers under `runtime-config-load-write`. They warn once at runtime, and remain available for old external plugins during the migration window. Bundled plugins must not use them; the config boundary guards fail if plugin code calls them or imports those helpers from plugin SDK subpaths.
For direct SDK imports, use the focused config subpaths instead of the broad
`openclaw/plugin-sdk/config-runtime` compatibility barrel: `config-types` for
types, `plugin-config-runtime` for already-loaded config assertions and plugin
entry lookup, `runtime-config-snapshot` for current process snapshots, and
`config-mutation` for writes.
`config-mutation` for writes. Bundled plugin tests should mock these focused
subpaths directly instead of mocking the broad compatibility barrel.
Internal OpenClaw runtime code has the same direction: load config once at the CLI, gateway, or process boundary, then pass that value through. Successful mutation writes refresh the process runtime snapshot and advance its internal revision; long-lived caches should key off the runtime-owned cache key instead of serializing config locally. Long-lived runtime modules have a zero-tolerance scanner for ambient `loadConfig()` calls; use a passed `cfg`, a request `context.getRuntimeConfig()`, or `getRuntimeConfig()` at an explicit process boundary.

View File

@@ -26,9 +26,9 @@ const hoisted = vi.hoisted(() => {
};
});
vi.mock("openclaw/plugin-sdk/config-runtime", async () => {
const actual = await vi.importActual<typeof import("openclaw/plugin-sdk/config-runtime")>(
"openclaw/plugin-sdk/config-runtime",
vi.mock("openclaw/plugin-sdk/session-store-runtime", async () => {
const actual = await vi.importActual<typeof import("openclaw/plugin-sdk/session-store-runtime")>(
"openclaw/plugin-sdk/session-store-runtime",
);
return {
...actual,

View File

@@ -136,10 +136,10 @@ const configMocks = vi.hoisted(() => ({
}
>(() => ({ browser: {} })),
}));
vi.mock("openclaw/plugin-sdk/config-runtime", async () => {
const actual = await vi.importActual<typeof import("openclaw/plugin-sdk/config-runtime")>(
"openclaw/plugin-sdk/config-runtime",
);
vi.mock("openclaw/plugin-sdk/runtime-config-snapshot", async () => {
const actual = await vi.importActual<
typeof import("openclaw/plugin-sdk/runtime-config-snapshot")
>("openclaw/plugin-sdk/runtime-config-snapshot");
return {
...actual,
getRuntimeConfig: configMocks.loadConfig,

View File

@@ -8,10 +8,10 @@ const { loadConfigMock, isNodeCommandAllowedMock, resolveNodeCommandAllowlistMoc
}),
);
vi.mock("openclaw/plugin-sdk/config-runtime", async () => {
const actual = await vi.importActual<typeof import("openclaw/plugin-sdk/config-runtime")>(
"openclaw/plugin-sdk/config-runtime",
);
vi.mock("openclaw/plugin-sdk/runtime-config-snapshot", async () => {
const actual = await vi.importActual<
typeof import("openclaw/plugin-sdk/runtime-config-snapshot")
>("openclaw/plugin-sdk/runtime-config-snapshot");
return {
...actual,
loadConfig: loadConfigMock,

View File

@@ -3,10 +3,10 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
const loadConfigMock = vi.hoisted(() => vi.fn());
vi.mock("openclaw/plugin-sdk/config-runtime", async () => {
const actual = await vi.importActual<typeof import("openclaw/plugin-sdk/config-runtime")>(
"openclaw/plugin-sdk/config-runtime",
);
vi.mock("openclaw/plugin-sdk/runtime-config-snapshot", async () => {
const actual = await vi.importActual<
typeof import("openclaw/plugin-sdk/runtime-config-snapshot")
>("openclaw/plugin-sdk/runtime-config-snapshot");
return {
...actual,
getRuntimeConfig: () => loadConfigMock(),

View File

@@ -24,7 +24,7 @@ vi.mock("@buape/carbon/voice", () => ({
},
}));
vi.mock("openclaw/plugin-sdk/config-runtime", () => ({
vi.mock("openclaw/plugin-sdk/dangerous-name-runtime", () => ({
isDangerousNameMatchingEnabled: () => false,
}));

View File

@@ -6,9 +6,9 @@ const hoisted = vi.hoisted(() => {
return { updateSessionStore, resolveStorePath };
});
vi.mock("openclaw/plugin-sdk/config-runtime", async () => {
const actual = await vi.importActual<typeof import("openclaw/plugin-sdk/config-runtime")>(
"openclaw/plugin-sdk/config-runtime",
vi.mock("openclaw/plugin-sdk/session-store-runtime", async () => {
const actual = await vi.importActual<typeof import("openclaw/plugin-sdk/session-store-runtime")>(
"openclaw/plugin-sdk/session-store-runtime",
);
return {
...actual,

View File

@@ -15,9 +15,9 @@ const DISCORD_TEST_CFG = {
session: { dmScope: "main" },
} as const;
vi.mock("openclaw/plugin-sdk/config-runtime", async () => {
const actual = await vi.importActual<typeof import("openclaw/plugin-sdk/config-runtime")>(
"openclaw/plugin-sdk/config-runtime",
vi.mock("openclaw/plugin-sdk/plugin-config-runtime", async () => {
const actual = await vi.importActual<typeof import("openclaw/plugin-sdk/plugin-config-runtime")>(
"openclaw/plugin-sdk/plugin-config-runtime",
);
return {
...actual,

View File

@@ -3,13 +3,13 @@ import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vite
const recordChannelActivityMock = vi.hoisted(() => vi.fn());
const loadConfigMock = vi.hoisted(() => vi.fn(() => ({ channels: { discord: {} } })));
vi.mock("openclaw/plugin-sdk/config-runtime", async () => {
const actual = await vi.importActual<typeof import("openclaw/plugin-sdk/config-runtime")>(
"openclaw/plugin-sdk/config-runtime",
vi.mock("openclaw/plugin-sdk/plugin-config-runtime", async () => {
const actual = await vi.importActual<typeof import("openclaw/plugin-sdk/plugin-config-runtime")>(
"openclaw/plugin-sdk/plugin-config-runtime",
);
return {
...actual,
loadConfig: () => loadConfigMock(),
requireRuntimeConfig: (cfg: unknown) => cfg ?? loadConfigMock(),
};
});

View File

@@ -379,19 +379,28 @@ vi.mock("openclaw/plugin-sdk/reply-runtime", async () => {
};
});
vi.mock("openclaw/plugin-sdk/config-runtime", async () => {
const actual = await vi.importActual<typeof import("openclaw/plugin-sdk/config-runtime")>(
"openclaw/plugin-sdk/config-runtime",
);
vi.mock("openclaw/plugin-sdk/native-command-config-runtime", async () => {
const actual = await vi.importActual<
typeof import("openclaw/plugin-sdk/native-command-config-runtime")
>("openclaw/plugin-sdk/native-command-config-runtime");
return {
...actual,
isNativeCommandsExplicitlyDisabled: () => false,
loadConfig: () => ({}),
resolveNativeCommandsEnabled: resolveNativeCommandsEnabledMock,
resolveNativeSkillsEnabled: resolveNativeSkillsEnabledMock,
};
});
vi.mock("openclaw/plugin-sdk/runtime-config-snapshot", async () => {
const actual = await vi.importActual<
typeof import("openclaw/plugin-sdk/runtime-config-snapshot")
>("openclaw/plugin-sdk/runtime-config-snapshot");
return {
...actual,
getRuntimeConfig: () => ({}),
};
});
vi.mock("openclaw/plugin-sdk/runtime-env", async () => {
const actual = await vi.importActual<typeof import("openclaw/plugin-sdk/runtime-env")>(
"openclaw/plugin-sdk/runtime-env",

View File

@@ -24,7 +24,7 @@ const {
mockRuntimeResolveMarkdownTableMode: vi.fn(() => "preserve"),
}));
vi.mock("openclaw/plugin-sdk/config-runtime", () => ({
vi.mock("openclaw/plugin-sdk/markdown-table-runtime", () => ({
resolveMarkdownTableMode: mockResolveMarkdownTableMode,
}));

View File

@@ -11,7 +11,7 @@ vi.mock("openclaw/plugin-sdk/provider-auth", () => ({
listProfilesForProvider: listProfilesForProviderMock,
}));
vi.mock("openclaw/plugin-sdk/config-runtime", () => ({
vi.mock("openclaw/plugin-sdk/secret-input-runtime", () => ({
resolveRequiredConfiguredSecretRefInputString: resolveRequiredConfiguredSecretRefInputStringMock,
}));

View File

@@ -9,7 +9,7 @@ vi.mock("./auth.js", () => ({
resolveFirstGithubToken: resolveFirstGithubTokenMock,
}));
vi.mock("openclaw/plugin-sdk/config-runtime", () => ({
vi.mock("openclaw/plugin-sdk/secret-input-runtime", () => ({
resolveConfiguredSecretInputString: resolveConfiguredSecretInputStringMock,
}));

View File

@@ -40,8 +40,8 @@ vi.mock("./protocol.js", async () => {
};
});
vi.mock("openclaw/plugin-sdk/config-runtime", async () => {
const original = (await vi.importActual("openclaw/plugin-sdk/config-runtime")) as Record<
vi.mock("openclaw/plugin-sdk/plugin-config-runtime", async () => {
const original = (await vi.importActual("openclaw/plugin-sdk/plugin-config-runtime")) as Record<
string,
unknown
>;

View File

@@ -103,7 +103,7 @@ vi.mock("openclaw/plugin-sdk/command-auth", () => ({
hasControlCommand && authorizers.some((entry) => entry.allowed || !entry.configured),
}),
}));
vi.mock("openclaw/plugin-sdk/config-runtime", () => ({
vi.mock("openclaw/plugin-sdk/runtime-group-policy", () => ({
resolveAllowlistProviderRuntimeGroupPolicy: ({
groupPolicy,
defaultGroupPolicy,

View File

@@ -50,7 +50,7 @@ vi.mock("@line/bot-sdk", () => ({
messagingApi: { MessagingApiClient: MessagingApiClientMock },
}));
vi.mock("openclaw/plugin-sdk/config-runtime", () => ({
vi.mock("openclaw/plugin-sdk/plugin-config-runtime", () => ({
requireRuntimeConfig: requireRuntimeConfigMock,
}));

View File

@@ -16,9 +16,9 @@ vi.mock("../../runtime.js", () => ({
}),
}));
vi.mock("openclaw/plugin-sdk/config-runtime", async () => {
const actual = await vi.importActual<typeof import("openclaw/plugin-sdk/config-runtime")>(
"openclaw/plugin-sdk/config-runtime",
vi.mock("openclaw/plugin-sdk/plugin-config-runtime", async () => {
const actual = await vi.importActual<typeof import("openclaw/plugin-sdk/plugin-config-runtime")>(
"openclaw/plugin-sdk/plugin-config-runtime",
);
return {
...actual,

View File

@@ -23,9 +23,9 @@ export const matrixClientResolverMocks: MatrixClientResolverMocks = {
resolveMatrixAuthContextMock: vi.fn(),
};
vi.mock("openclaw/plugin-sdk/config-runtime", async () => {
const actual = await vi.importActual<typeof import("openclaw/plugin-sdk/config-runtime")>(
"openclaw/plugin-sdk/config-runtime",
vi.mock("openclaw/plugin-sdk/plugin-config-runtime", async () => {
const actual = await vi.importActual<typeof import("openclaw/plugin-sdk/plugin-config-runtime")>(
"openclaw/plugin-sdk/plugin-config-runtime",
);
return {
...actual,

View File

@@ -33,9 +33,9 @@ const resolveMarkdownTableModeMock = vi.fn(() => "code");
const convertMarkdownTablesMock = vi.fn((text: string) => text);
const chunkMarkdownTextWithModeMock = vi.fn((text: string) => (text ? [text] : []));
vi.mock("openclaw/plugin-sdk/config-runtime", async () => {
const actual = await vi.importActual<typeof import("openclaw/plugin-sdk/config-runtime")>(
"openclaw/plugin-sdk/config-runtime",
vi.mock("openclaw/plugin-sdk/plugin-config-runtime", async () => {
const actual = await vi.importActual<typeof import("openclaw/plugin-sdk/plugin-config-runtime")>(
"openclaw/plugin-sdk/plugin-config-runtime",
);
return {
...actual,

View File

@@ -42,7 +42,7 @@ vi.mock("./runtime-api.js", () => ({
loadOutboundMediaFromUrl: mockState.loadOutboundMediaFromUrl,
}));
vi.mock("openclaw/plugin-sdk/config-runtime", () => ({
vi.mock("openclaw/plugin-sdk/plugin-config-runtime", () => ({
requireRuntimeConfig: (cfg: unknown) => {
if (cfg) {
return cfg;

View File

@@ -1,12 +1,13 @@
import { createHash } from "node:crypto";
import fs from "node:fs/promises";
import path from "node:path";
import * as configRuntimeModule from "openclaw/plugin-sdk/config-runtime";
import {
RequestScopedSubagentRuntimeError,
SUBAGENT_RUNTIME_REQUEST_SCOPE_ERROR_CODE,
} from "openclaw/plugin-sdk/error-runtime";
import * as memoryCoreHostRuntimeCoreModule from "openclaw/plugin-sdk/memory-core-host-runtime-core";
import * as runtimeConfigSnapshotModule from "openclaw/plugin-sdk/runtime-config-snapshot";
import * as sessionStoreRuntimeModule from "openclaw/plugin-sdk/session-store-runtime";
import { afterEach, describe, expect, it, vi } from "vitest";
import { resolveGlobalMap } from "../../../src/shared/global-singleton.js";
import {
@@ -827,14 +828,16 @@ describe("generateAndAppendDreamNarrative", () => {
await fs.utimes(orphanPath, oldDate, oldDate);
await fs.utimes(livePath, oldDate, oldDate);
vi.spyOn(configRuntimeModule, "loadConfig").mockReturnValue({ session: {} } as never);
vi.spyOn(configRuntimeModule, "resolveStorePath").mockImplementation(((
vi.spyOn(runtimeConfigSnapshotModule, "getRuntimeConfig").mockReturnValue({
session: {},
} as never);
vi.spyOn(sessionStoreRuntimeModule, "resolveStorePath").mockImplementation(((
_store: string | undefined,
{ agentId }: { agentId: string },
) => {
expect(agentId).toBe("main");
return storePath;
}) as typeof configRuntimeModule.resolveStorePath);
}) as typeof sessionStoreRuntimeModule.resolveStorePath);
vi.spyOn(memoryCoreHostRuntimeCoreModule, "resolveStateDir").mockReturnValue(stateDir);
const subagent = createMockSubagent("The repository whispered of forgotten endpoints.");

View File

@@ -23,7 +23,7 @@ vi.mock("openclaw/plugin-sdk/outbound-media", () => ({
loadOutboundMediaFromUrl: mockState.loadOutboundMediaFromUrl,
}));
vi.mock("openclaw/plugin-sdk/config-runtime", () => ({
vi.mock("openclaw/plugin-sdk/markdown-table-runtime", () => ({
resolveMarkdownTableMode: mockState.resolveMarkdownTableMode,
}));

View File

@@ -98,13 +98,22 @@ export function createMockSignalDaemonHandle(
// Use importActual so shared-worker mocks from earlier test files do not leak
// into this harness's partial overrides.
vi.mock("openclaw/plugin-sdk/config-runtime", async () => {
const actual = await vi.importActual<typeof import("openclaw/plugin-sdk/config-runtime")>(
"openclaw/plugin-sdk/config-runtime",
vi.mock("openclaw/plugin-sdk/runtime-config-snapshot", async () => {
const actual = await vi.importActual<
typeof import("openclaw/plugin-sdk/runtime-config-snapshot")
>("openclaw/plugin-sdk/runtime-config-snapshot");
return {
...actual,
getRuntimeConfig: () => config,
};
});
vi.mock("openclaw/plugin-sdk/session-store-runtime", async () => {
const actual = await vi.importActual<typeof import("openclaw/plugin-sdk/session-store-runtime")>(
"openclaw/plugin-sdk/session-store-runtime",
);
return {
...actual,
loadConfig: () => config,
resolveStorePath: vi.fn(() => signalToolResultSessionStorePath),
updateLastRoute: (...args: unknown[]) => updateLastRouteMock(...args),
readSessionUpdatedAt: vi.fn(() => undefined),

View File

@@ -2,9 +2,9 @@ import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
const rpcMock = vi.fn();
vi.mock("openclaw/plugin-sdk/config-runtime", async () => {
const actual = await vi.importActual<typeof import("openclaw/plugin-sdk/config-runtime")>(
"openclaw/plugin-sdk/config-runtime",
vi.mock("openclaw/plugin-sdk/plugin-config-runtime", async () => {
const actual = await vi.importActual<typeof import("openclaw/plugin-sdk/plugin-config-runtime")>(
"openclaw/plugin-sdk/plugin-config-runtime",
);
return {
...actual,

View File

@@ -26,16 +26,6 @@ const slackBlockTestState = vi.hoisted(() => ({
config: {},
}));
vi.mock("openclaw/plugin-sdk/config-runtime", async () => {
const actual = await vi.importActual<typeof import("openclaw/plugin-sdk/config-runtime")>(
"openclaw/plugin-sdk/config-runtime",
);
return {
...actual,
loadConfig: () => slackBlockTestState.config,
};
});
vi.mock("./accounts.js", async () => {
const actual = await vi.importActual<typeof import("./accounts.js")>("./accounts.js");
return {

View File

@@ -1591,7 +1591,7 @@ describe("slack thread.requireExplicitMention", () => {
const ctx = createCtxWithExplicitMention(true);
const { storePath } = storeFixture.makeTmpStorePath();
vi.spyOn(
await import("openclaw/plugin-sdk/config-runtime"),
await import("openclaw/plugin-sdk/session-store-runtime"),
"resolveStorePath",
).mockReturnValue(storePath);
const account = createSlackTestAccount();
@@ -1618,7 +1618,7 @@ describe("slack thread.requireExplicitMention", () => {
const ctx = createCtxWithExplicitMention(true);
const { storePath } = storeFixture.makeTmpStorePath();
vi.spyOn(
await import("openclaw/plugin-sdk/config-runtime"),
await import("openclaw/plugin-sdk/session-store-runtime"),
"resolveStorePath",
).mockReturnValue(storePath);
const account = createSlackTestAccount();
@@ -1645,7 +1645,7 @@ describe("slack thread.requireExplicitMention", () => {
const ctx = createCtxWithExplicitMention(false);
const { storePath } = storeFixture.makeTmpStorePath();
vi.spyOn(
await import("openclaw/plugin-sdk/config-runtime"),
await import("openclaw/plugin-sdk/session-store-runtime"),
"resolveStorePath",
).mockReturnValue(storePath);
const account = createSlackTestAccount();

View File

@@ -43,7 +43,7 @@ vi.mock("./bot-message-context.body.js", () => ({
const { buildTelegramMessageContextForTest } =
await import("./bot-message-context.test-harness.js");
const { clearRuntimeConfigSnapshot, setRuntimeConfigSnapshot } =
await import("openclaw/plugin-sdk/config-runtime");
await import("openclaw/plugin-sdk/runtime-config-snapshot");
beforeEach(() => {
clearRuntimeConfigSnapshot();

View File

@@ -20,7 +20,7 @@ vi.mock("./bot-message-context.body.js", () => ({
}));
let buildTelegramMessageContextForTest: typeof import("./bot-message-context.test-harness.js").buildTelegramMessageContextForTest;
let clearRuntimeConfigSnapshot: typeof import("openclaw/plugin-sdk/config-runtime").clearRuntimeConfigSnapshot;
let clearRuntimeConfigSnapshot: typeof import("openclaw/plugin-sdk/runtime-config-snapshot").clearRuntimeConfigSnapshot;
describe("buildTelegramMessageContext DM topic threadId in deliveryContext (#8891)", () => {
async function buildCtx(params: {

View File

@@ -6,8 +6,8 @@ import {
} from "./bot-message-context.route-test-support.js";
let buildTelegramMessageContextForTest: typeof import("./bot-message-context.test-harness.js").buildTelegramMessageContextForTest;
let clearRuntimeConfigSnapshot: typeof import("openclaw/plugin-sdk/config-runtime").clearRuntimeConfigSnapshot;
let setRuntimeConfigSnapshot: typeof import("openclaw/plugin-sdk/config-runtime").setRuntimeConfigSnapshot;
let clearRuntimeConfigSnapshot: typeof import("openclaw/plugin-sdk/runtime-config-snapshot").clearRuntimeConfigSnapshot;
let setRuntimeConfigSnapshot: typeof import("openclaw/plugin-sdk/runtime-config-snapshot").setRuntimeConfigSnapshot;
describe("buildTelegramMessageContext named-account DM fallback", () => {
const baseCfg = {

View File

@@ -11,10 +11,10 @@ const { defaultRouteConfig } = vi.hoisted(() => ({
},
}));
vi.mock("openclaw/plugin-sdk/config-runtime", async () => {
const actual = await vi.importActual<typeof import("openclaw/plugin-sdk/config-runtime")>(
"openclaw/plugin-sdk/config-runtime",
);
vi.mock("openclaw/plugin-sdk/runtime-config-snapshot", async () => {
const actual = await vi.importActual<
typeof import("openclaw/plugin-sdk/runtime-config-snapshot")
>("openclaw/plugin-sdk/runtime-config-snapshot");
return {
...actual,
getRuntimeConfig: vi.fn(() => defaultRouteConfig),

View File

@@ -140,9 +140,9 @@ vi.mock("openclaw/plugin-sdk/conversation-runtime", async () => {
}),
};
});
vi.mock("openclaw/plugin-sdk/config-runtime", async () => {
const actual = await vi.importActual<typeof import("openclaw/plugin-sdk/config-runtime")>(
"openclaw/plugin-sdk/config-runtime",
vi.mock("openclaw/plugin-sdk/session-store-runtime", async () => {
const actual = await vi.importActual<typeof import("openclaw/plugin-sdk/session-store-runtime")>(
"openclaw/plugin-sdk/session-store-runtime",
);
return {
...actual,

View File

@@ -6,9 +6,12 @@ import type { TelegramBotDeps } from "./bot-deps.js";
type AnyMock = ReturnType<typeof vi.fn>;
type AnyAsyncMock = ReturnType<typeof vi.fn>;
type GetRuntimeConfigFn = typeof import("openclaw/plugin-sdk/config-runtime").getRuntimeConfig;
type LoadSessionStoreFn = typeof import("openclaw/plugin-sdk/config-runtime").loadSessionStore;
type ResolveStorePathFn = typeof import("openclaw/plugin-sdk/config-runtime").resolveStorePath;
type GetRuntimeConfigFn =
typeof import("openclaw/plugin-sdk/runtime-config-snapshot").getRuntimeConfig;
type LoadSessionStoreFn =
typeof import("openclaw/plugin-sdk/session-store-runtime").loadSessionStore;
type ResolveStorePathFn =
typeof import("openclaw/plugin-sdk/session-store-runtime").resolveStorePath;
type SessionStore = ReturnType<LoadSessionStoreFn>;
type TelegramBotRuntimeForTest = NonNullable<
Parameters<typeof import("./bot.js").setTelegramBotRuntimeForTest>[0]

View File

@@ -4,7 +4,8 @@ import { escapeRegExp, formatEnvelopeTimestamp } from "../../../test/helpers/env
import type { TelegramBotOptions } from "./bot.types.js";
const harness = await import("./bot.create-telegram-bot.test-harness.js");
const conversationRuntime = await import("openclaw/plugin-sdk/conversation-runtime");
const configRuntime = await import("openclaw/plugin-sdk/config-runtime");
const configMutation = await import("openclaw/plugin-sdk/config-mutation");
const sessionStoreRuntime = await import("openclaw/plugin-sdk/session-store-runtime");
const EYES_EMOJI = "\u{1F440}";
const {
answerCallbackQuerySpy,
@@ -121,7 +122,7 @@ function installPerKeySequentializer(): void {
}
function mockTelegramConfigWrites() {
return vi.spyOn(configRuntime, "replaceConfigFile").mockResolvedValue({} as never);
return vi.spyOn(configMutation, "replaceConfigFile").mockResolvedValue({} as never);
}
async function withEnvAsync(env: Record<string, string | undefined>, fn: () => Promise<void>) {
@@ -3638,7 +3639,7 @@ describe("createTelegramBot", () => {
await dispatch(0);
};
const updateSessionStoreSpy = vi.spyOn(configRuntime, "updateSessionStore");
const updateSessionStoreSpy = vi.spyOn(sessionStoreRuntime, "updateSessionStore");
updateSessionStoreSpy.mockRejectedValueOnce(new Error("session store boom"));
const ctx = {

View File

@@ -263,7 +263,7 @@ async function monitorWithAutoAbort(opts: Omit<MonitorTelegramOpts, "abortSignal
});
}
vi.mock("openclaw/plugin-sdk/config-runtime", async () => {
vi.mock("openclaw/plugin-sdk/runtime-config-snapshot", async () => {
return {
getRuntimeConfig: getRuntimeConfigMock,
resolveAgentMaxConcurrent: (cfg: { agents?: { defaults?: { maxConcurrent?: number } } }) =>

View File

@@ -25,13 +25,13 @@ const resolveTelegramApiBase = vi.hoisted(
() => (apiRoot?: string) => apiRoot?.trim()?.replace(/\/+$/, "") || "https://api.telegram.org",
);
vi.mock("openclaw/plugin-sdk/config-runtime", async () => {
const actual = await vi.importActual<typeof import("openclaw/plugin-sdk/config-runtime")>(
"openclaw/plugin-sdk/config-runtime",
vi.mock("openclaw/plugin-sdk/plugin-config-runtime", async () => {
const actual = await vi.importActual<typeof import("openclaw/plugin-sdk/plugin-config-runtime")>(
"openclaw/plugin-sdk/plugin-config-runtime",
);
return {
...actual,
loadConfig,
requireRuntimeConfig: (cfg: unknown) => cfg ?? loadConfig(),
};
});

View File

@@ -135,9 +135,9 @@ vi.mock("undici", () => ({
setGlobalDispatcher: undiciSetGlobalDispatcher,
}));
vi.mock("openclaw/plugin-sdk/config-runtime", async () => {
const actual = await vi.importActual<typeof import("openclaw/plugin-sdk/config-runtime")>(
"openclaw/plugin-sdk/config-runtime",
vi.mock("openclaw/plugin-sdk/plugin-config-runtime", async () => {
const actual = await vi.importActual<typeof import("openclaw/plugin-sdk/plugin-config-runtime")>(
"openclaw/plugin-sdk/plugin-config-runtime",
);
return {
...actual,

View File

@@ -14,15 +14,23 @@ export const loadCronStore: AsyncUnknownMock = vi.fn();
export const resolveCronStorePath: UnknownMock = vi.fn();
export const saveCronStore: AsyncUnknownMock = vi.fn();
vi.mock("openclaw/plugin-sdk/config-runtime", async () => {
const actual = await vi.importActual<typeof import("openclaw/plugin-sdk/config-runtime")>(
"openclaw/plugin-sdk/config-runtime",
vi.mock("openclaw/plugin-sdk/config-mutation", async () => {
const actual = await vi.importActual<typeof import("openclaw/plugin-sdk/config-mutation")>(
"openclaw/plugin-sdk/config-mutation",
);
return {
...actual,
readConfigFileSnapshotForWrite,
replaceConfigFile,
writeConfigFile,
};
});
vi.mock("openclaw/plugin-sdk/cron-store-runtime", async () => {
const actual = await vi.importActual<typeof import("openclaw/plugin-sdk/cron-store-runtime")>(
"openclaw/plugin-sdk/cron-store-runtime",
);
return {
...actual,
loadCronStore,
resolveCronStorePath,
saveCronStore,

View File

@@ -1,12 +1,6 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { getActiveWebListener, resolveWebAccountId } from "./active-listener.js";
vi.mock("openclaw/plugin-sdk/config-runtime", () => ({
loadConfig: () => ({
channels: { whatsapp: { accounts: { work: { enabled: true } }, defaultAccount: "work" } },
}),
}));
const registryMocks = vi.hoisted(() => ({
getRegisteredWhatsAppConnectionController: vi.fn(),
}));

View File

@@ -29,13 +29,13 @@ let currentMockSocket:
}
| undefined;
vi.mock("openclaw/plugin-sdk/config-runtime", async () => {
const actual = await vi.importActual<typeof import("openclaw/plugin-sdk/config-runtime")>(
"openclaw/plugin-sdk/config-runtime",
);
vi.mock("openclaw/plugin-sdk/runtime-config-snapshot", async () => {
const actual = await vi.importActual<
typeof import("openclaw/plugin-sdk/runtime-config-snapshot")
>("openclaw/plugin-sdk/runtime-config-snapshot");
return {
...actual,
loadConfig: vi.fn().mockReturnValue({
getRuntimeConfig: vi.fn().mockReturnValue({
channels: {
whatsapp: {
allowFrom: ["*"], // Allow all in tests

View File

@@ -13,10 +13,10 @@ function resolveTestAuthDir() {
return testState.authDir;
}
vi.mock("openclaw/plugin-sdk/config-runtime", async () => {
const actual = await vi.importActual<typeof import("openclaw/plugin-sdk/config-runtime")>(
"openclaw/plugin-sdk/config-runtime",
);
vi.mock("openclaw/plugin-sdk/runtime-config-snapshot", async () => {
const actual = await vi.importActual<
typeof import("openclaw/plugin-sdk/runtime-config-snapshot")
>("openclaw/plugin-sdk/runtime-config-snapshot");
return {
...actual,
getRuntimeConfig: () =>

View File

@@ -18,13 +18,13 @@ export function resetPairingSecurityMocks(config: Record<string, unknown>) {
upsertPairingRequestMock.mockReset().mockResolvedValue({ code: "PAIRCODE", created: true });
}
vi.mock("openclaw/plugin-sdk/config-runtime", async () => {
const actual = await vi.importActual<typeof import("openclaw/plugin-sdk/config-runtime")>(
"openclaw/plugin-sdk/config-runtime",
);
vi.mock("openclaw/plugin-sdk/runtime-config-snapshot", async () => {
const actual = await vi.importActual<
typeof import("openclaw/plugin-sdk/runtime-config-snapshot")
>("openclaw/plugin-sdk/runtime-config-snapshot");
return {
...actual,
loadConfig: (...args: unknown[]) => loadConfigMock(...args),
getRuntimeConfig: (...args: unknown[]) => loadConfigMock(...args),
};
});

View File

@@ -7,6 +7,16 @@ import { collectFilesSync, isCodeFile, relativeToCwd } from "./check-file-utils.
// imports/exports, require/dynamic import, and test mocks (vi.mock/jest.mock).
const ROOT_IMPORT_PATTERN = /["']openclaw\/plugin-sdk["']/;
const LEGACY_COMPAT_IMPORT_PATTERN = /["']openclaw\/plugin-sdk\/compat["']/;
const LEGACY_BROAD_SUBPATH_PATTERNS = [
{
pattern: /["']openclaw\/plugin-sdk\/channel-runtime["']/,
label: "openclaw/plugin-sdk/channel-runtime",
},
{
pattern: /["']openclaw\/plugin-sdk\/config-runtime["']/,
label: "openclaw/plugin-sdk/config-runtime",
},
] as const;
function hasMonolithicRootImport(content: string): boolean {
return ROOT_IMPORT_PATTERN.test(content);
@@ -16,6 +26,12 @@ function hasLegacyCompatImport(content: string): boolean {
return LEGACY_COMPAT_IMPORT_PATTERN.test(content);
}
function findLegacyBroadSubpathImports(content: string): string[] {
return LEGACY_BROAD_SUBPATH_PATTERNS.filter(({ pattern }) => pattern.test(content)).map(
({ label }) => label,
);
}
function collectPluginSourceFiles(rootDir: string): string[] {
const srcDir = path.join(rootDir, "src");
if (!fs.existsSync(srcDir)) {
@@ -71,6 +87,7 @@ function main() {
const monolithicOffenders: string[] = [];
const legacyCompatOffenders: string[] = [];
const legacyBroadSubpathOffenders = new Map<string, string[]>();
for (const entryFile of filesToCheck) {
let content = "";
try {
@@ -84,9 +101,17 @@ function main() {
if (hasLegacyCompatImport(content)) {
legacyCompatOffenders.push(entryFile);
}
const legacyBroadSubpaths = findLegacyBroadSubpathImports(content);
if (legacyBroadSubpaths.length > 0) {
legacyBroadSubpathOffenders.set(entryFile, legacyBroadSubpaths);
}
}
if (monolithicOffenders.length > 0 || legacyCompatOffenders.length > 0) {
if (
monolithicOffenders.length > 0 ||
legacyCompatOffenders.length > 0 ||
legacyBroadSubpathOffenders.size > 0
) {
if (monolithicOffenders.length > 0) {
console.error("Bundled plugin source files must not import monolithic openclaw/plugin-sdk.");
for (const file of monolithicOffenders.toSorted()) {
@@ -101,9 +126,23 @@ function main() {
console.error(`- ${relativeToCwd(file)}`);
}
}
if (monolithicOffenders.length > 0 || legacyCompatOffenders.length > 0) {
if (legacyBroadSubpathOffenders.size > 0) {
console.error(
"Use openclaw/plugin-sdk/<domain> or openclaw/plugin-sdk/<channel> subpaths for bundled plugins; root and compat are legacy surfaces only.",
"Bundled plugin source files must not import deprecated broad plugin-sdk subpaths.",
);
for (const [file, labels] of [...legacyBroadSubpathOffenders.entries()].toSorted(
([left], [right]) => left.localeCompare(right),
)) {
console.error(`- ${relativeToCwd(file)} (${labels.join(", ")})`);
}
}
if (
monolithicOffenders.length > 0 ||
legacyCompatOffenders.length > 0 ||
legacyBroadSubpathOffenders.size > 0
) {
console.error(
"Use focused openclaw/plugin-sdk/<domain> subpaths for bundled plugins; root, compat, and broad runtime barrels are legacy surfaces only.",
);
}
process.exit(1);

View File

@@ -32,6 +32,12 @@ const PROCESS_BOUNDARY_DIRECT_CONFIG_LOAD_FILES = new Set([
"src/cli/daemon-cli/status.gather.ts",
]);
const BROAD_CONFIG_RUNTIME_COMPAT_FILES = new Set([
"scripts/check-no-monolithic-plugin-sdk-entry-imports.ts",
"src/plugins/bundled-capability-runtime.test.ts",
"src/plugins/contracts/config-boundary-guard.test.ts",
]);
function collectTypeScriptFiles(dir) {
if (!existsSync(dir)) {
return [];
@@ -176,6 +182,19 @@ function pushBroadConfigRuntimeBarrelViolations(violations, files) {
}
}
function pushBroadConfigRuntimeSpecifierViolations(violations, files) {
const moduleSpecifierPattern = /["']openclaw\/plugin-sdk\/config-runtime["']/g;
for (const { filePath, relPath } of files) {
const source = readFileSync(filePath, "utf8");
for (const line of findMatchLineNumbers(source, moduleSpecifierPattern)) {
violations.push(
`${relPath}:${line} use narrow plugin-sdk config subpaths instead of openclaw/plugin-sdk/config-runtime`,
);
}
}
}
export function collectDeprecatedInternalConfigApiViolations({
repoRoot = DEFAULT_REPO_ROOT,
} = {}) {
@@ -246,6 +265,13 @@ export function collectDeprecatedInternalConfigApiViolations({
!relPath.startsWith("test/"),
),
);
pushBroadConfigRuntimeSpecifierViolations(
violations,
repoFiles.filter(
({ relPath }) =>
!isCompatConfigApiFile(relPath) && !BROAD_CONFIG_RUNTIME_COMPAT_FILES.has(relPath),
),
);
for (const { filePath, relPath } of repoFiles.filter(
({ relPath }) => !isCompatConfigApiFile(relPath),
@@ -354,7 +380,7 @@ export function collectDeprecatedInternalConfigApiViolations({
);
}
return violations;
return [...new Set(violations)];
}
const CHANNEL_EXTENSION_IDS = new Set([

View File

@@ -99,6 +99,19 @@ describe("config boundary guard", () => {
);
});
it("flags broad config-runtime test mocks outside compat guard fixtures", () => {
const repoRoot = makeRepoFixture();
writeFixture(
repoRoot,
"extensions/telegram/src/index.test.ts",
'vi.mock("openclaw/plugin-sdk/config-runtime", () => ({}));',
);
expect(collectDeprecatedInternalConfigApiViolations({ repoRoot })).toEqual([
"extensions/telegram/src/index.test.ts:1 use narrow plugin-sdk config subpaths instead of openclaw/plugin-sdk/config-runtime",
]);
});
it("allows narrow config SDK subpaths in production code", () => {
const repoRoot = makeRepoFixture();
writeFixture(