fix(ci): restore boundary and test seams

This commit is contained in:
Peter Steinberger
2026-03-27 15:08:33 +00:00
parent 454f094c36
commit 97297049e7
6 changed files with 108 additions and 64 deletions

View File

@@ -1,49 +1,46 @@
export { resolveAckReaction } from "../../../src/agents/identity.js";
export { resolveAckReaction } from "openclaw/plugin-sdk/bluebubbles";
export {
createActionGate,
jsonResult,
readNumberParam,
readReactionParams,
readStringParam,
} from "../../../src/agents/tools/common.js";
export type { HistoryEntry } from "../../../src/auto-reply/reply/history.js";
} from "openclaw/plugin-sdk/bluebubbles";
export type { HistoryEntry } from "openclaw/plugin-sdk/bluebubbles";
export {
evictOldHistoryKeys,
recordPendingHistoryEntryIfEnabled,
} from "../../../src/auto-reply/reply/history.js";
export { resolveControlCommandGate } from "../../../src/channels/command-gating.js";
export { logAckFailure, logInboundDrop, logTypingFailure } from "../../../src/channels/logging.js";
export {
BLUEBUBBLES_ACTION_NAMES,
BLUEBUBBLES_ACTIONS,
} from "../../../src/channels/plugins/bluebubbles-actions.js";
export { resolveChannelMediaMaxBytes } from "../../../src/channels/plugins/media-limits.js";
export { PAIRING_APPROVED_MESSAGE } from "../../../src/channels/plugins/pairing-message.js";
export { collectBlueBubblesStatusIssues } from "../../../src/channels/plugins/status-issues/bluebubbles.js";
} from "openclaw/plugin-sdk/bluebubbles";
export { resolveControlCommandGate } from "openclaw/plugin-sdk/bluebubbles";
export { logAckFailure, logInboundDrop, logTypingFailure } from "openclaw/plugin-sdk/bluebubbles";
export { BLUEBUBBLES_ACTION_NAMES, BLUEBUBBLES_ACTIONS } from "openclaw/plugin-sdk/bluebubbles";
export { resolveChannelMediaMaxBytes } from "openclaw/plugin-sdk/bluebubbles";
export { PAIRING_APPROVED_MESSAGE } from "openclaw/plugin-sdk/bluebubbles";
export { collectBlueBubblesStatusIssues } from "openclaw/plugin-sdk/bluebubbles";
export type {
BaseProbeResult,
ChannelAccountSnapshot,
ChannelMessageActionAdapter,
ChannelMessageActionName,
} from "../../../src/channels/plugins/types.js";
export type { ChannelPlugin } from "../../../src/channels/plugins/types.plugin.js";
export type { OpenClawConfig } from "../../../src/config/config.js";
export { parseFiniteNumber } from "../../../src/infra/parse-finite-number.js";
export type { PluginRuntime } from "../../../src/plugins/runtime/types.js";
export { DEFAULT_ACCOUNT_ID } from "../../../src/routing/session-key.js";
} from "openclaw/plugin-sdk/bluebubbles";
export type { ChannelPlugin } from "openclaw/plugin-sdk/bluebubbles";
export type { OpenClawConfig } from "openclaw/plugin-sdk/bluebubbles";
export { parseFiniteNumber } from "openclaw/plugin-sdk/bluebubbles";
export type { PluginRuntime } from "openclaw/plugin-sdk/bluebubbles";
export { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/bluebubbles";
export {
DM_GROUP_ACCESS_REASON,
readStoreAllowFromForDmPolicy,
resolveDmGroupAccessWithLists,
} from "../../../src/security/dm-policy-shared.js";
export { readBooleanParam } from "../../../src/plugin-sdk/boolean-param.js";
export { mapAllowFromEntries } from "../../../src/plugin-sdk/channel-config-helpers.js";
export { createChannelPairingController } from "../../../src/plugin-sdk/channel-pairing.js";
export { createChannelReplyPipeline } from "../../../src/plugin-sdk/channel-reply-pipeline.js";
export { resolveRequestUrl } from "../../../src/plugin-sdk/request-url.js";
export { buildProbeChannelStatusSummary } from "../../../src/plugin-sdk/status-helpers.js";
export { stripMarkdown } from "../../../src/plugin-sdk/text-runtime.js";
export { extractToolSend } from "../../../src/plugin-sdk/tool-send.js";
} from "openclaw/plugin-sdk/bluebubbles";
export { readBooleanParam } from "openclaw/plugin-sdk/bluebubbles";
export { mapAllowFromEntries } from "openclaw/plugin-sdk/bluebubbles";
export { createChannelPairingController } from "openclaw/plugin-sdk/bluebubbles";
export { createChannelReplyPipeline } from "openclaw/plugin-sdk/bluebubbles";
export { resolveRequestUrl } from "openclaw/plugin-sdk/bluebubbles";
export { buildProbeChannelStatusSummary } from "openclaw/plugin-sdk/bluebubbles";
export { stripMarkdown } from "openclaw/plugin-sdk/bluebubbles";
export { extractToolSend } from "openclaw/plugin-sdk/bluebubbles";
export {
WEBHOOK_RATE_LIMIT_DEFAULTS,
createFixedWindowRateLimiter,
@@ -53,4 +50,4 @@ export {
resolveRequestClientIp,
resolveWebhookTargetWithAuthOrRejectSync,
withResolvedWebhookRequestPipeline,
} from "../../../src/plugin-sdk/webhook-ingress.js";
} from "openclaw/plugin-sdk/bluebubbles";

View File

@@ -14,14 +14,21 @@ const sendReactionSignal = vi.hoisted(() => vi.fn(async (..._args: unknown[]) =>
const removeReactionSignal = vi.hoisted(() => vi.fn(async (..._args: unknown[]) => ({ ok: true })));
const handleSlackAction = vi.hoisted(() => vi.fn(async (..._args: unknown[]) => actionResult()));
vi.mock("../../../../extensions/discord/src/actions/runtime.js", () => ({
handleDiscordAction: async (...args: unknown[]) => await handleDiscordAction(...args),
}));
vi.mock("../../../../extensions/signal/src/send-reactions.js", () => ({
sendReactionSignal: async (...args: unknown[]) => await sendReactionSignal(...args),
removeReactionSignal: async (...args: unknown[]) => await removeReactionSignal(...args),
}));
let discordMessageActions: typeof import("../../../../extensions/discord/runtime-api.js").discordMessageActions;
let handleDiscordMessageAction: typeof import("./discord/handle-action.js").handleDiscordMessageAction;
let telegramMessageActions: typeof import("../../../../extensions/telegram/runtime-api.js").telegramMessageActions;
let signalMessageActions: typeof import("../../../../extensions/signal/api.js").signalMessageActions;
let createSlackActions: typeof import("../../../../extensions/slack/test-api.js").createSlackActions;
let discordRuntimeModule: typeof import("../../../../extensions/discord/runtime-api.js");
let telegramTestApiModule: typeof import("../../../../extensions/telegram/test-api.js");
let signalReactionModule: typeof import("../../../../extensions/signal/api.js");
function getDescribedActions(params: {
describeMessageTool?: ChannelMessageActionAdapter["describeMessageTool"];
@@ -198,28 +205,17 @@ beforeAll(async () => {
vi.resetModules();
({ discordMessageActions } = await import("../../../../extensions/discord/runtime-api.js"));
({ handleDiscordMessageAction } = await import("./discord/handle-action.js"));
discordRuntimeModule = await import("../../../../extensions/discord/runtime-api.js");
({ telegramMessageActions } = await import("../../../../extensions/telegram/runtime-api.js"));
telegramTestApiModule = await import("../../../../extensions/telegram/test-api.js");
({ signalMessageActions } = await import("../../../../extensions/signal/api.js"));
signalReactionModule = await import("../../../../extensions/signal/api.js");
({ createSlackActions } = await import("../../../../extensions/slack/test-api.js"));
});
beforeEach(() => {
vi.clearAllMocks();
vi.restoreAllMocks();
vi.spyOn(discordRuntimeModule, "handleDiscordAction").mockImplementation(
async (...args) => await handleDiscordAction(...args),
);
telegramTestApiModule.telegramMessageActionRuntime.handleTelegramAction = async (...args) =>
await handleTelegramAction(...args);
vi.spyOn(signalReactionModule, "sendReactionSignal").mockImplementation(
async (...args) => await sendReactionSignal(...args),
);
vi.spyOn(signalReactionModule, "removeReactionSignal").mockImplementation(
async (...args) => await removeReactionSignal(...args),
);
});
describe("discord message actions", () => {

View File

@@ -6,7 +6,8 @@ import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi }
import type { SessionEntry } from "./types.js";
// Keep integration tests deterministic: never read a real openclaw.json.
vi.mock("../config.js", () => ({
vi.mock("../config.js", async (importOriginal) => ({
...(await importOriginal<typeof import("../config.js")>()),
loadConfig: vi.fn().mockReturnValue({}),
}));
@@ -18,6 +19,15 @@ let saveSessionStore: typeof import("./store.js").saveSessionStore;
let mockLoadConfig: ReturnType<typeof vi.fn>;
const DAY_MS = 24 * 60 * 60 * 1000;
const ENFORCED_MAINTENANCE_OVERRIDE = {
mode: "enforce" as const,
pruneAfterMs: 7 * DAY_MS,
maxEntries: 500,
rotateBytes: 10_485_760,
resetArchiveRetentionMs: 7 * DAY_MS,
maxDiskBytes: null,
highWaterBytes: null,
};
const archiveTimestamp = (ms: number) => new Date(ms).toISOString().replaceAll(":", "-");
@@ -109,9 +119,11 @@ describe("Integration: saveSessionStore with pruning", () => {
const store = createStaleAndFreshStore();
await saveSessionStore(storePath, store);
await saveSessionStore(storePath, store, {
maintenanceOverride: ENFORCED_MAINTENANCE_OVERRIDE,
});
const loaded = loadSessionStore(storePath);
const loaded = loadSessionStore(storePath, { skipCache: true });
expect(loaded.stale).toBeUndefined();
expect(loaded.fresh).toBeDefined();
});

View File

@@ -6,6 +6,11 @@ import {
resolveGatewayPort,
resolveStateDir,
} from "../config/config.js";
import {
resolveConfigPath as resolveConfigPathFromPaths,
resolveGatewayPort as resolveGatewayPortFromPaths,
resolveStateDir as resolveStateDirFromPaths,
} from "../config/paths.js";
import { resolveSecretInputRef } from "../config/types.secrets.js";
import { loadOrCreateDeviceIdentity } from "../infra/device-identity.js";
import { loadGatewayTlsRuntime } from "../infra/tls/gateway.js";
@@ -95,6 +100,30 @@ const gatewayCallDeps = {
...defaultGatewayCallDeps,
};
function resolveGatewayStateDir(env: NodeJS.ProcessEnv): string {
const resolveStateDirFn =
typeof gatewayCallDeps.resolveStateDir === "function"
? gatewayCallDeps.resolveStateDir
: resolveStateDirFromPaths;
return resolveStateDirFn(env);
}
function resolveGatewayConfigPath(env: NodeJS.ProcessEnv): string {
const resolveConfigPathFn =
typeof gatewayCallDeps.resolveConfigPath === "function"
? gatewayCallDeps.resolveConfigPath
: resolveConfigPathFromPaths;
return resolveConfigPathFn(env, resolveGatewayStateDir(env));
}
function resolveGatewayPortValue(config?: OpenClawConfig, env?: NodeJS.ProcessEnv): number {
const resolveGatewayPortFn =
typeof gatewayCallDeps.resolveGatewayPort === "function"
? gatewayCallDeps.resolveGatewayPort
: resolveGatewayPortFromPaths;
return resolveGatewayPortFn(config, env);
}
export const __testing = {
setDepsForTests(deps: Partial<typeof defaultGatewayCallDeps> | undefined): void {
gatewayCallDeps.createGatewayClient =
@@ -203,13 +232,11 @@ export function buildGatewayConnectionDetails(
} = {},
): GatewayConnectionDetails {
const config = options.config ?? gatewayCallDeps.loadConfig();
const configPath =
options.configPath ??
gatewayCallDeps.resolveConfigPath(process.env, gatewayCallDeps.resolveStateDir(process.env));
const configPath = options.configPath ?? resolveGatewayConfigPath(process.env);
const isRemoteMode = config.gateway?.mode === "remote";
const remote = isRemoteMode ? config.gateway?.remote : undefined;
const tlsEnabled = config.gateway?.tls?.enabled === true;
const localPort = gatewayCallDeps.resolveGatewayPort(config);
const localPort = resolveGatewayPortValue(config);
const bindMode = config.gateway?.bind ?? "loopback";
const scheme = tlsEnabled ? "wss" : "ws";
// Self-connections should always target loopback; bind mode only controls listener exposure.
@@ -322,9 +349,7 @@ function resolveGatewayCallTimeout(timeoutValue: unknown): {
function resolveGatewayCallContext(opts: CallGatewayBaseOptions): ResolvedGatewayCallContext {
const config = opts.config ?? gatewayCallDeps.loadConfig();
const configPath =
opts.configPath ??
gatewayCallDeps.resolveConfigPath(process.env, gatewayCallDeps.resolveStateDir(process.env));
const configPath = opts.configPath ?? resolveGatewayConfigPath(process.env);
const isRemoteMode = config.gateway?.mode === "remote";
const remote = isRemoteMode
? (config.gateway?.remote as GatewayRemoteSettings | undefined)
@@ -732,10 +757,7 @@ export async function resolveGatewayCredentialsWithSecretInputs(params: {
: undefined;
const context: ResolvedGatewayCallContext = {
config: params.config,
configPath: gatewayCallDeps.resolveConfigPath(
process.env,
gatewayCallDeps.resolveStateDir(process.env),
),
configPath: resolveGatewayConfigPath(process.env),
isRemoteMode,
remote: remoteFromOverride ?? remoteFromConfig,
urlOverride: trimToUndefined(params.urlOverride),

View File

@@ -4,17 +4,23 @@ type TestMock = ReturnType<typeof vi.fn>;
export const loadConfigMock: TestMock = vi.fn();
export const resolveGatewayPortMock: TestMock = vi.fn();
export const resolveStateDirMock: TestMock = vi.fn(
(env: NodeJS.ProcessEnv) => env.OPENCLAW_STATE_DIR ?? "/tmp/openclaw",
);
export const resolveConfigPathMock: TestMock = vi.fn(
(env: NodeJS.ProcessEnv, stateDir: string) =>
env.OPENCLAW_CONFIG_PATH ?? `${stateDir}/openclaw.json`,
);
export const pickPrimaryTailnetIPv4Mock: TestMock = vi.fn();
export const pickPrimaryLanIPv4Mock: TestMock = vi.fn();
vi.mock("../config/config.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../config/config.js")>();
return {
...actual,
loadConfig: loadConfigMock,
resolveGatewayPort: resolveGatewayPortMock,
};
});
vi.mock("../config/config.js", async (importOriginal) => ({
...(await importOriginal<typeof import("../config/config.js")>()),
loadConfig: loadConfigMock,
resolveGatewayPort: resolveGatewayPortMock,
resolveStateDir: resolveStateDirMock,
resolveConfigPath: resolveConfigPathMock,
}));
vi.mock("../infra/tailnet.js", () => ({
pickPrimaryTailnetIPv4: pickPrimaryTailnetIPv4Mock,

View File

@@ -4,7 +4,9 @@ import path from "node:path";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import {
loadConfigMock as loadConfig,
resolveConfigPathMock as resolveConfigPath,
resolveGatewayPortMock as resolveGatewayPort,
resolveStateDirMock as resolveStateDir,
} from "../gateway/gateway-connection.test-mocks.js";
import { captureEnv, withEnvAsync } from "../test-utils/env.js";
@@ -91,7 +93,16 @@ describe("resolveGatewayConnection", () => {
]);
loadConfig.mockReset();
resolveGatewayPort.mockReset();
resolveStateDir.mockReset();
resolveConfigPath.mockReset();
resolveGatewayPort.mockReturnValue(18789);
resolveStateDir.mockImplementation(
(env: NodeJS.ProcessEnv) => env.OPENCLAW_STATE_DIR ?? "/tmp/openclaw",
);
resolveConfigPath.mockImplementation(
(env: NodeJS.ProcessEnv, stateDir: string) =>
env.OPENCLAW_CONFIG_PATH ?? `${stateDir}/openclaw.json`,
);
delete process.env.OPENCLAW_GATEWAY_URL;
delete process.env.OPENCLAW_GATEWAY_TOKEN;
delete process.env.OPENCLAW_GATEWAY_PASSWORD;