fix(test): reduce startup-heavy hotspot retention (#52381)

This commit is contained in:
Vincent Koc
2026-03-22 12:28:55 -07:00
committed by GitHub
parent 26d400bea6
commit dbd26e49f1
12 changed files with 380 additions and 212 deletions

View File

@@ -7,9 +7,14 @@ const registerFeishuWikiToolsMock = vi.hoisted(() => vi.fn());
const registerFeishuDriveToolsMock = vi.hoisted(() => vi.fn());
const registerFeishuPermToolsMock = vi.hoisted(() => vi.fn());
const registerFeishuBitableToolsMock = vi.hoisted(() => vi.fn());
const feishuPluginMock = vi.hoisted(() => ({ id: "feishu-test-plugin" }));
const setFeishuRuntimeMock = vi.hoisted(() => vi.fn());
const registerFeishuSubagentHooksMock = vi.hoisted(() => vi.fn());
vi.mock("./src/channel.js", () => ({
feishuPlugin: feishuPluginMock,
}));
vi.mock("./src/docx.js", () => ({
registerFeishuDocTools: registerFeishuDocToolsMock,
}));
@@ -58,6 +63,7 @@ describe("feishu plugin register", () => {
expect(setFeishuRuntimeMock).toHaveBeenCalledWith(api.runtime);
expect(registerChannel).toHaveBeenCalledTimes(1);
expect(registerChannel).toHaveBeenCalledWith({ plugin: feishuPluginMock });
expect(registerFeishuSubagentHooksMock).toHaveBeenCalledWith(api);
expect(registerFeishuDocToolsMock).toHaveBeenCalledWith(api);
expect(registerFeishuChatToolsMock).toHaveBeenCalledWith(api);

View File

@@ -3,7 +3,7 @@ import type { Server } from "node:http";
import { createConnection, type AddressInfo } from "node:net";
import express from "express";
import { describe, expect, it } from "vitest";
import { applyMSTeamsWebhookTimeouts } from "./monitor.js";
import { applyMSTeamsWebhookTimeouts } from "./webhook-timeouts.js";
async function closeServer(server: Server): Promise<void> {
await new Promise<void>((resolve) => {

View File

@@ -1,4 +1,3 @@
import type { Server } from "node:http";
import type { Request, Response } from "express";
import {
DEFAULT_WEBHOOK_MAX_BODY_BYTES,
@@ -21,6 +20,10 @@ import {
import { getMSTeamsRuntime } from "./runtime.js";
import { createMSTeamsAdapter, loadMSTeamsSdkWithAuth } from "./sdk.js";
import { resolveMSTeamsCredentials } from "./token.js";
import {
applyMSTeamsWebhookTimeouts,
type ApplyMSTeamsWebhookTimeoutsOpts,
} from "./webhook-timeouts.js";
export type MonitorMSTeamsOpts = {
cfg: OpenClawConfig;
@@ -36,32 +39,6 @@ export type MonitorMSTeamsResult = {
};
const MSTEAMS_WEBHOOK_MAX_BODY_BYTES = DEFAULT_WEBHOOK_MAX_BODY_BYTES;
const MSTEAMS_WEBHOOK_INACTIVITY_TIMEOUT_MS = 30_000;
const MSTEAMS_WEBHOOK_REQUEST_TIMEOUT_MS = 30_000;
const MSTEAMS_WEBHOOK_HEADERS_TIMEOUT_MS = 15_000;
export type ApplyMSTeamsWebhookTimeoutsOpts = {
inactivityTimeoutMs?: number;
requestTimeoutMs?: number;
headersTimeoutMs?: number;
};
export function applyMSTeamsWebhookTimeouts(
httpServer: Server,
opts?: ApplyMSTeamsWebhookTimeoutsOpts,
): void {
const inactivityTimeoutMs = opts?.inactivityTimeoutMs ?? MSTEAMS_WEBHOOK_INACTIVITY_TIMEOUT_MS;
const requestTimeoutMs = opts?.requestTimeoutMs ?? MSTEAMS_WEBHOOK_REQUEST_TIMEOUT_MS;
const headersTimeoutMs = Math.min(
opts?.headersTimeoutMs ?? MSTEAMS_WEBHOOK_HEADERS_TIMEOUT_MS,
requestTimeoutMs,
);
httpServer.setTimeout(inactivityTimeoutMs);
httpServer.requestTimeout = requestTimeoutMs;
httpServer.headersTimeout = headersTimeoutMs;
}
export async function monitorMSTeamsProvider(
opts: MonitorMSTeamsOpts,
): Promise<MonitorMSTeamsResult> {

View File

@@ -0,0 +1,27 @@
import type { Server } from "node:http";
const MSTEAMS_WEBHOOK_INACTIVITY_TIMEOUT_MS = 30_000;
const MSTEAMS_WEBHOOK_REQUEST_TIMEOUT_MS = 30_000;
const MSTEAMS_WEBHOOK_HEADERS_TIMEOUT_MS = 15_000;
export type ApplyMSTeamsWebhookTimeoutsOpts = {
inactivityTimeoutMs?: number;
requestTimeoutMs?: number;
headersTimeoutMs?: number;
};
export function applyMSTeamsWebhookTimeouts(
httpServer: Server,
opts?: ApplyMSTeamsWebhookTimeoutsOpts,
): void {
const inactivityTimeoutMs = opts?.inactivityTimeoutMs ?? MSTEAMS_WEBHOOK_INACTIVITY_TIMEOUT_MS;
const requestTimeoutMs = opts?.requestTimeoutMs ?? MSTEAMS_WEBHOOK_REQUEST_TIMEOUT_MS;
const headersTimeoutMs = Math.min(
opts?.headersTimeoutMs ?? MSTEAMS_WEBHOOK_HEADERS_TIMEOUT_MS,
requestTimeoutMs,
);
httpServer.setTimeout(inactivityTimeoutMs);
httpServer.requestTimeout = requestTimeoutMs;
httpServer.headersTimeout = headersTimeoutMs;
}

View File

@@ -54,6 +54,9 @@ const unitBehaviorIsolatedFiles = existingUnitConfigFiles(behaviorManifest.unit.
const unitSingletonIsolatedFiles = existingUnitConfigFiles(behaviorManifest.unit.singletonIsolated);
const unitThreadSingletonFiles = existingUnitConfigFiles(behaviorManifest.unit.threadSingleton);
const unitVmForkSingletonFiles = existingUnitConfigFiles(behaviorManifest.unit.vmForkSingleton);
const extensionSingletonIsolatedFiles = existingFiles(
behaviorManifest.extensions.singletonIsolated,
);
const unitBehaviorOverrideSet = new Set([
...unitBehaviorIsolatedFiles,
...unitSingletonIsolatedFiles,
@@ -440,6 +443,10 @@ const unitFastExcludedFileSet = new Set(unitFastExcludedFiles);
const unitFastCandidateFiles = allKnownUnitFiles.filter(
(file) => !unitFastExcludedFileSet.has(file),
);
const extensionSingletonExcludedFileSet = new Set(extensionSingletonIsolatedFiles);
const extensionSharedCandidateFiles = allKnownTestFiles.filter(
(file) => file.startsWith("extensions/") && !extensionSingletonExcludedFileSet.has(file),
);
const defaultUnitFastLaneCount = isCI && !isWindows ? 3 : 1;
const unitFastLaneCount = Math.max(
1,
@@ -516,6 +523,10 @@ const unitThreadEntries =
},
]
: [];
const extensionSingletonEntries = extensionSingletonIsolatedFiles.map((file) => ({
name: `${path.basename(file, ".test.ts")}-extensions-isolated`,
args: ["vitest", "run", "--config", "vitest.extensions.config.ts", "--pool=forks", file],
}));
const baseRuns = [
...(shouldSplitUnitRuns
? [
@@ -583,6 +594,15 @@ const baseRuns = [
? [
{
name: "extensions",
env:
extensionSharedCandidateFiles.length > 0
? {
OPENCLAW_VITEST_INCLUDE_FILE: writeTempJsonArtifact(
"vitest-extensions-include",
extensionSharedCandidateFiles,
),
}
: undefined,
args: [
"vitest",
"run",
@@ -591,6 +611,7 @@ const baseRuns = [
...(useVmForks ? ["--pool=vmForks"] : []),
],
},
...extensionSingletonEntries,
]
: []),
...(includeGatewaySuite

View File

@@ -31,6 +31,7 @@ export function loadTestRunnerBehavior() {
const raw = tryReadJsonFile(behaviorManifestPath, {});
const unit = raw.unit ?? {};
const base = raw.base ?? {};
const extensions = raw.extensions ?? {};
return {
base: {
threadSingleton: normalizeManifestEntries(base.threadSingleton ?? []),
@@ -41,6 +42,9 @@ export function loadTestRunnerBehavior() {
threadSingleton: normalizeManifestEntries(unit.threadSingleton ?? []),
vmForkSingleton: normalizeManifestEntries(unit.vmForkSingleton ?? []),
},
extensions: {
singletonIsolated: normalizeManifestEntries(extensions.singletonIsolated ?? []),
},
};
}

View File

@@ -1,89 +1,38 @@
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
const mocks = vi.hoisted(() => ({
executeSendAction: vi.fn(),
recordSessionMetaFromInbound: vi.fn(async () => ({ ok: true })),
}));
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../../config/config.js";
import {
prepareOutboundMirrorRoute,
resolveAndApplyOutboundThreadId,
} from "./message-action-threading.js";
vi.mock("./outbound-send-service.js", async () => {
const actual = await vi.importActual<typeof import("./outbound-send-service.js")>(
"./outbound-send-service.js",
);
return {
...actual,
executeSendAction: mocks.executeSendAction,
};
});
const ensureOutboundSessionEntry = vi.fn(async () => undefined);
const resolveOutboundSessionRoute = vi.fn();
vi.mock("../../config/sessions.js", async () => {
const actual = await vi.importActual<typeof import("../../config/sessions.js")>(
"../../config/sessions.js",
);
return {
...actual,
recordSessionMetaFromInbound: mocks.recordSessionMetaFromInbound,
};
});
const slackConfig = {
channels: {
slack: {
botToken: "xoxb-test",
},
},
} as OpenClawConfig;
type MessageActionRunnerModule = typeof import("./message-action-runner.js");
type MessageActionRunnerTestHelpersModule =
typeof import("./message-action-runner.test-helpers.js");
let runMessageAction: MessageActionRunnerModule["runMessageAction"];
let installMessageActionRunnerTestRegistry: MessageActionRunnerTestHelpersModule["installMessageActionRunnerTestRegistry"];
let resetMessageActionRunnerTestRegistry: MessageActionRunnerTestHelpersModule["resetMessageActionRunnerTestRegistry"];
let slackConfig: MessageActionRunnerTestHelpersModule["slackConfig"];
let telegramConfig: MessageActionRunnerTestHelpersModule["telegramConfig"];
async function runThreadingAction(params: {
cfg: MessageActionRunnerTestHelpersModule["slackConfig"];
actionParams: Record<string, unknown>;
toolContext?: Record<string, unknown>;
}) {
await runMessageAction({
cfg: params.cfg,
action: "send",
params: params.actionParams as never,
toolContext: params.toolContext as never,
agentId: "main",
});
return mocks.executeSendAction.mock.calls[0]?.[0] as {
threadId?: string;
replyToId?: string;
ctx?: { agentId?: string; mirror?: { sessionKey?: string }; params?: Record<string, unknown> };
};
}
function mockHandledSendAction() {
mocks.executeSendAction.mockResolvedValue({
handledBy: "plugin",
payload: {},
});
}
const telegramConfig = {
channels: {
telegram: {
botToken: "telegram-test",
},
},
} as OpenClawConfig;
const defaultTelegramToolContext = {
currentChannelId: "telegram:123",
currentThreadTs: "42",
} as const;
describe("runMessageAction threading auto-injection", () => {
beforeAll(async () => {
({ runMessageAction } = await import("./message-action-runner.js"));
({
installMessageActionRunnerTestRegistry,
resetMessageActionRunnerTestRegistry,
slackConfig,
telegramConfig,
} = await import("./message-action-runner.test-helpers.js"));
});
describe("message action threading helpers", () => {
beforeEach(() => {
installMessageActionRunnerTestRegistry();
});
afterEach(() => {
resetMessageActionRunnerTestRegistry?.();
mocks.executeSendAction.mockClear();
mocks.recordSessionMetaFromInbound.mockClear();
ensureOutboundSessionEntry.mockClear();
resolveOutboundSessionRoute.mockReset();
});
it.each([
@@ -99,25 +48,42 @@ describe("runMessageAction threading auto-injection", () => {
threadTs: "333.444",
expectedSessionKey: "agent:main:slack:channel:c123:thread:333.444",
},
] as const)("auto-threads slack using $name", async (testCase) => {
mockHandledSendAction();
] as const)("prepares outbound routes for slack using $name", async (testCase) => {
const actionParams: Record<string, unknown> = {
channel: "slack",
target: testCase.target,
message: "hi",
};
resolveOutboundSessionRoute.mockResolvedValue({
sessionKey: testCase.expectedSessionKey,
baseSessionKey: "base",
peer: { id: "peer", kind: "channel" },
chatType: "channel",
from: "from",
to: testCase.target,
threadId: testCase.threadTs,
});
const call = await runThreadingAction({
const result = await prepareOutboundMirrorRoute({
cfg: slackConfig,
actionParams: {
channel: "slack",
target: testCase.target,
message: "hi",
},
channel: "slack",
to: testCase.target,
actionParams,
toolContext: {
currentChannelId: "C123",
currentThreadTs: testCase.threadTs,
replyToMode: "all",
},
agentId: "main",
resolveAutoThreadId: ({ toolContext }) => toolContext?.currentThreadTs,
resolveOutboundSessionRoute,
ensureOutboundSessionEntry,
});
expect(call?.ctx?.agentId).toBe("main");
expect(call?.ctx?.mirror?.sessionKey).toBe(testCase.expectedSessionKey);
expect(result.outboundRoute?.sessionKey).toBe(testCase.expectedSessionKey);
expect(actionParams.__sessionKey).toBe(testCase.expectedSessionKey);
expect(actionParams.__agentId).toBe("main");
expect(ensureOutboundSessionEntry).toHaveBeenCalledTimes(1);
});
it.each([
@@ -136,58 +102,66 @@ describe("runMessageAction threading auto-injection", () => {
target: "telegram:999",
expectedThreadId: undefined,
},
] as const)("telegram auto-threading: $name", async (testCase) => {
mockHandledSendAction();
] as const)("telegram auto-threading: $name", (testCase) => {
const actionParams: Record<string, unknown> = {
channel: "telegram",
target: testCase.target,
message: "hi",
};
const call = await runThreadingAction({
const resolved = resolveAndApplyOutboundThreadId(actionParams, {
cfg: telegramConfig,
actionParams: {
channel: "telegram",
target: testCase.target,
message: "hi",
},
to: testCase.target,
toolContext: defaultTelegramToolContext,
resolveAutoThreadId: ({ to, toolContext }) =>
to.includes("123") ? toolContext?.currentThreadTs : undefined,
});
expect(call?.ctx?.params?.threadId).toBe(testCase.expectedThreadId);
if (testCase.expectedThreadId !== undefined) {
expect(call?.threadId).toBe(testCase.expectedThreadId);
}
expect(actionParams.threadId).toBe(testCase.expectedThreadId);
expect(resolved).toBe(testCase.expectedThreadId);
});
it("uses explicit telegram threadId when provided", async () => {
mockHandledSendAction();
it("uses explicit telegram threadId when provided", () => {
const actionParams: Record<string, unknown> = {
channel: "telegram",
target: "telegram:123",
message: "hi",
threadId: "999",
};
const call = await runThreadingAction({
const resolved = resolveAndApplyOutboundThreadId(actionParams, {
cfg: telegramConfig,
actionParams: {
channel: "telegram",
target: "telegram:123",
message: "hi",
threadId: "999",
},
to: "telegram:123",
toolContext: defaultTelegramToolContext,
resolveAutoThreadId: () => "42",
});
expect(call?.threadId).toBe("999");
expect(call?.ctx?.params?.threadId).toBe("999");
expect(actionParams.threadId).toBe("999");
expect(resolved).toBe("999");
});
it("threads explicit replyTo through executeSendAction", async () => {
mockHandledSendAction();
it("passes explicit replyTo into auto-thread resolution", () => {
const resolveAutoThreadId = vi.fn(() => "thread-777");
const actionParams: Record<string, unknown> = {
channel: "telegram",
target: "telegram:123",
message: "hi",
replyTo: "777",
};
const call = await runThreadingAction({
const resolved = resolveAndApplyOutboundThreadId(actionParams, {
cfg: telegramConfig,
actionParams: {
channel: "telegram",
target: "telegram:123",
message: "hi",
replyTo: "777",
},
to: "telegram:123",
toolContext: defaultTelegramToolContext,
resolveAutoThreadId,
});
expect(call?.replyToId).toBe("777");
expect(call?.ctx?.params?.replyTo).toBe("777");
expect(resolveAutoThreadId).toHaveBeenCalledWith(
expect.objectContaining({
replyToId: "777",
}),
);
expect(resolved).toBe("thread-777");
expect(actionParams.threadId).toBe("thread-777");
});
});

View File

@@ -40,6 +40,10 @@ import {
readBooleanParam,
resolveAttachmentMediaPolicy,
} from "./message-action-params.js";
import {
prepareOutboundMirrorRoute,
resolveAndApplyOutboundThreadId,
} from "./message-action-threading.js";
import type { MessagePollResult, MessageSendResult } from "./message.js";
import {
applyCrossContextDecoration,
@@ -62,34 +66,6 @@ export type MessageActionRunnerGateway = {
mode: GatewayClientMode;
};
function resolveAndApplyOutboundThreadId(
params: Record<string, unknown>,
ctx: {
cfg: OpenClawConfig;
channel: ChannelId;
to: string;
accountId?: string | null;
toolContext?: ChannelThreadingToolContext;
},
): string | undefined {
const threadId = readStringParam(params, "threadId");
const resolved =
threadId ??
getChannelPlugin(ctx.channel)?.threading?.resolveAutoThreadId?.({
cfg: ctx.cfg,
accountId: ctx.accountId,
to: ctx.to,
toolContext: ctx.toolContext,
replyToId: readStringParam(params, "replyTo"),
});
// Write auto-resolved threadId back into params so downstream dispatch
// (plugin `readStringParam(params, "threadId")`) picks it up.
if (resolved && !params.threadId) {
params.threadId = resolved;
}
return resolved ?? undefined;
}
export type RunMessageActionParams = {
cfg: OpenClawConfig;
action: ChannelMessageActionName;
@@ -501,41 +477,20 @@ async function handleSendAction(ctx: ResolvedActionContext): Promise<MessageActi
const silent = readBooleanParam(params, "silent");
const replyToId = readStringParam(params, "replyTo");
const resolvedThreadId = resolveAndApplyOutboundThreadId(params, {
const { resolvedThreadId, outboundRoute } = await prepareOutboundMirrorRoute({
cfg,
channel,
to,
actionParams: params,
accountId,
toolContext: input.toolContext,
agentId,
dryRun,
resolvedTarget,
resolveAutoThreadId: getChannelPlugin(channel)?.threading?.resolveAutoThreadId,
resolveOutboundSessionRoute,
ensureOutboundSessionEntry,
});
const outboundRoute =
agentId && !dryRun
? await resolveOutboundSessionRoute({
cfg,
channel,
agentId,
accountId,
target: to,
resolvedTarget,
replyToId,
threadId: resolvedThreadId,
})
: null;
if (outboundRoute && agentId && !dryRun) {
await ensureOutboundSessionEntry({
cfg,
agentId,
channel,
accountId,
route: outboundRoute,
});
}
if (outboundRoute && !dryRun) {
params.__sessionKey = outboundRoute.sessionKey;
}
if (agentId) {
params.__agentId = agentId;
}
const mirrorMediaUrls =
mergedMediaUrls.length > 0 ? mergedMediaUrls : mediaUrl ? [mediaUrl] : undefined;
throwIfAborted(abortSignal);
@@ -595,10 +550,10 @@ async function handlePollAction(ctx: ResolvedActionContext): Promise<MessageActi
const resolvedThreadId = resolveAndApplyOutboundThreadId(params, {
cfg,
channel,
to,
accountId,
toolContext: input.toolContext,
resolveAutoThreadId: getChannelPlugin(channel)?.threading?.resolveAutoThreadId,
});
const base = typeof params.message === "string" ? params.message : "";

View File

@@ -0,0 +1,107 @@
import { readStringParam } from "../../agents/tools/common.js";
import type {
ChannelId,
ChannelThreadingAdapter,
ChannelThreadingToolContext,
} from "../../channels/plugins/types.js";
import type { OpenClawConfig } from "../../config/config.js";
import type {
OutboundSessionRoute,
ResolveOutboundSessionRouteParams,
} from "./outbound-session.js";
import type { ResolvedMessagingTarget } from "./target-resolver.js";
type ResolveAutoThreadId = NonNullable<ChannelThreadingAdapter["resolveAutoThreadId"]>;
export function resolveAndApplyOutboundThreadId(
actionParams: Record<string, unknown>,
context: {
cfg: OpenClawConfig;
to: string;
accountId?: string | null;
toolContext?: ChannelThreadingToolContext;
resolveAutoThreadId?: ResolveAutoThreadId;
},
): string | undefined {
const threadId = readStringParam(actionParams, "threadId");
const resolved =
threadId ??
context.resolveAutoThreadId?.({
cfg: context.cfg,
accountId: context.accountId,
to: context.to,
toolContext: context.toolContext,
replyToId: readStringParam(actionParams, "replyTo"),
});
if (resolved && !actionParams.threadId) {
actionParams.threadId = resolved;
}
return resolved ?? undefined;
}
export async function prepareOutboundMirrorRoute(params: {
cfg: OpenClawConfig;
channel: ChannelId;
to: string;
actionParams: Record<string, unknown>;
accountId?: string | null;
toolContext?: ChannelThreadingToolContext;
agentId?: string;
dryRun?: boolean;
resolvedTarget?: ResolvedMessagingTarget;
resolveAutoThreadId?: ResolveAutoThreadId;
resolveOutboundSessionRoute: (
params: ResolveOutboundSessionRouteParams,
) => Promise<OutboundSessionRoute | null>;
ensureOutboundSessionEntry: (params: {
cfg: OpenClawConfig;
agentId: string;
channel: ChannelId;
accountId?: string | null;
route: OutboundSessionRoute;
}) => Promise<void>;
}): Promise<{
resolvedThreadId?: string;
outboundRoute: OutboundSessionRoute | null;
}> {
const replyToId = readStringParam(params.actionParams, "replyTo");
const resolvedThreadId = resolveAndApplyOutboundThreadId(params.actionParams, {
cfg: params.cfg,
to: params.to,
accountId: params.accountId,
toolContext: params.toolContext,
resolveAutoThreadId: params.resolveAutoThreadId,
});
const outboundRoute =
params.agentId && !params.dryRun
? await params.resolveOutboundSessionRoute({
cfg: params.cfg,
channel: params.channel,
agentId: params.agentId,
accountId: params.accountId,
target: params.to,
resolvedTarget: params.resolvedTarget,
replyToId,
threadId: resolvedThreadId,
})
: null;
if (outboundRoute && params.agentId && !params.dryRun) {
await params.ensureOutboundSessionEntry({
cfg: params.cfg,
agentId: params.agentId,
channel: params.channel,
accountId: params.accountId,
route: outboundRoute,
});
}
if (outboundRoute && !params.dryRun) {
params.actionParams.__sessionKey = outboundRoute.sessionKey;
}
if (params.agentId) {
params.actionParams.__agentId = params.agentId;
}
return {
resolvedThreadId,
outboundRoute,
};
}

View File

@@ -39,6 +39,10 @@
"file": "src/cli/command-secret-gateway.test.ts",
"reason": "Clean in isolation, but can hang after sharing the broad lane."
},
{
"file": "src/plugins/provider-wizard.test.ts",
"reason": "Retained ~318 MiB in an isolated memory trace and is safer outside the shared unit-fast lane."
},
{
"file": "src/config/doc-baseline.integration.test.ts",
"reason": "Builds the full bundled config schema graph and is safer outside the shared unit-fast heap."
@@ -984,5 +988,21 @@
"reason": "Needs the vmForks lane when targeted."
}
]
},
"extensions": {
"singletonIsolated": [
{
"file": "extensions/bluebubbles/src/setup-surface.test.ts",
"reason": "Loads the setup-wizard/plugin-sdk graph and retained ~576 MiB in an isolated memory trace."
},
{
"file": "extensions/feishu/index.test.ts",
"reason": "Plugin entry registration coverage loads a broad channel graph and is safer outside the shared extensions lane."
},
{
"file": "extensions/msteams/src/monitor.test.ts",
"reason": "Webhook timeout coverage is startup-heavy but showed no material retained heap growth across repeated snapshots."
}
]
}
}

View File

@@ -0,0 +1,55 @@
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { loadIncludePatternsFromEnv } from "../vitest.extensions.config.ts";
const tempDirs = new Set<string>();
afterEach(() => {
for (const dir of tempDirs) {
fs.rmSync(dir, { recursive: true, force: true });
}
tempDirs.clear();
});
const writePatternFile = (basename: string, value: unknown) => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-vitest-extensions-config-"));
tempDirs.add(dir);
const filePath = path.join(dir, basename);
fs.writeFileSync(filePath, `${JSON.stringify(value)}\n`, "utf8");
return filePath;
};
describe("extensions vitest include patterns", () => {
it("returns null when no include file is configured", () => {
expect(loadIncludePatternsFromEnv({})).toBeNull();
});
it("loads include patterns from a JSON file", () => {
const filePath = writePatternFile("include.json", [
"extensions/feishu/index.test.ts",
42,
"",
"extensions/msteams/src/monitor.test.ts",
]);
expect(
loadIncludePatternsFromEnv({
OPENCLAW_VITEST_INCLUDE_FILE: filePath,
}),
).toEqual(["extensions/feishu/index.test.ts", "extensions/msteams/src/monitor.test.ts"]);
});
it("throws when the configured file is not a JSON array", () => {
const filePath = writePatternFile("include.json", {
include: ["extensions/feishu/index.test.ts"],
});
expect(() =>
loadIncludePatternsFromEnv({
OPENCLAW_VITEST_INCLUDE_FILE: filePath,
}),
).toThrow(/JSON array/u);
});
});

View File

@@ -1,9 +1,31 @@
import fs from "node:fs";
import { channelTestExclude } from "./vitest.channel-paths.mjs";
import { createScopedVitestConfig } from "./vitest.scoped-config.ts";
export default createScopedVitestConfig(["extensions/**/*.test.ts"], {
// Channel implementations live under extensions/ but are tested by
// vitest.channels.config.ts (pnpm test:channels) which provides
// the heavier mock scaffolding they need.
exclude: channelTestExclude.filter((pattern) => pattern.startsWith("extensions/")),
});
function loadPatternListFile(filePath: string, label: string): string[] {
const parsed = JSON.parse(fs.readFileSync(filePath, "utf8")) as unknown;
if (!Array.isArray(parsed)) {
throw new TypeError(`${label} must point to a JSON array: ${filePath}`);
}
return parsed.filter((value): value is string => typeof value === "string" && value.length > 0);
}
export function loadIncludePatternsFromEnv(
env: Record<string, string | undefined> = process.env,
): string[] | null {
const includeFile = env.OPENCLAW_VITEST_INCLUDE_FILE?.trim();
if (!includeFile) {
return null;
}
return loadPatternListFile(includeFile, "OPENCLAW_VITEST_INCLUDE_FILE");
}
export default createScopedVitestConfig(
loadIncludePatternsFromEnv() ?? ["extensions/**/*.test.ts"],
{
// Channel implementations live under extensions/ but are tested by
// vitest.channels.config.ts (pnpm test:channels) which provides
// the heavier mock scaffolding they need.
exclude: channelTestExclude.filter((pattern) => pattern.startsWith("extensions/")),
},
);