mirror of
https://gh-proxy.org/https://github.com/klinsmaya/openclaw.git
synced 2026-11-08 02:57:08 +08:00
fix: restore ci gates
This commit is contained in:
@@ -1,12 +1,20 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const runDuckDuckGoSearch = vi.fn(async (params: Record<string, unknown>) => params);
|
||||
const { runDuckDuckGoSearch } = vi.hoisted(() => ({
|
||||
runDuckDuckGoSearch: vi.fn(async (params: Record<string, unknown>) => params),
|
||||
}));
|
||||
|
||||
vi.mock("./ddg-client.js", () => ({
|
||||
runDuckDuckGoSearch,
|
||||
}));
|
||||
|
||||
describe("duckduckgo web search provider", () => {
|
||||
beforeEach(() => {
|
||||
vi.resetModules();
|
||||
runDuckDuckGoSearch.mockReset();
|
||||
runDuckDuckGoSearch.mockImplementation(async (params: Record<string, unknown>) => params);
|
||||
});
|
||||
|
||||
it("exposes keyless metadata and enables the plugin in config", async () => {
|
||||
const { createDuckDuckGoWebSearchProvider } = await import("./ddg-search-provider.js");
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const runFirecrawlScrape = vi.fn(async (params: Record<string, unknown>) => ({
|
||||
ok: true,
|
||||
params,
|
||||
const { runFirecrawlScrape } = vi.hoisted(() => ({
|
||||
runFirecrawlScrape: vi.fn(async (params: Record<string, unknown>) => ({
|
||||
ok: true,
|
||||
params,
|
||||
})),
|
||||
}));
|
||||
|
||||
vi.mock("./firecrawl-client.js", () => ({
|
||||
@@ -10,6 +12,15 @@ vi.mock("./firecrawl-client.js", () => ({
|
||||
}));
|
||||
|
||||
describe("firecrawl scrape tool", () => {
|
||||
beforeEach(() => {
|
||||
vi.resetModules();
|
||||
runFirecrawlScrape.mockReset();
|
||||
runFirecrawlScrape.mockImplementation(async (params: Record<string, unknown>) => ({
|
||||
ok: true,
|
||||
params,
|
||||
}));
|
||||
});
|
||||
|
||||
it("maps scrape params and defaults extract mode to markdown", async () => {
|
||||
const { createFirecrawlScrapeTool } = await import("./firecrawl-scrape-tool.js");
|
||||
const tool = createFirecrawlScrapeTool({
|
||||
|
||||
@@ -1,12 +1,20 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const runFirecrawlSearch = vi.fn(async (params: Record<string, unknown>) => params);
|
||||
const { runFirecrawlSearch } = vi.hoisted(() => ({
|
||||
runFirecrawlSearch: vi.fn(async (params: Record<string, unknown>) => params),
|
||||
}));
|
||||
|
||||
vi.mock("./firecrawl-client.js", () => ({
|
||||
runFirecrawlSearch,
|
||||
}));
|
||||
|
||||
describe("firecrawl web search provider", () => {
|
||||
beforeEach(() => {
|
||||
vi.resetModules();
|
||||
runFirecrawlSearch.mockReset();
|
||||
runFirecrawlSearch.mockImplementation(async (params: Record<string, unknown>) => params);
|
||||
});
|
||||
|
||||
it("exposes selection metadata and enables the plugin in config", async () => {
|
||||
const { createFirecrawlWebSearchProvider } = await import("./firecrawl-search-provider.js");
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const runFirecrawlSearch = vi.fn(async (params: Record<string, unknown>) => ({
|
||||
ok: true,
|
||||
params,
|
||||
const { runFirecrawlSearch } = vi.hoisted(() => ({
|
||||
runFirecrawlSearch: vi.fn(async (params: Record<string, unknown>) => ({
|
||||
ok: true,
|
||||
params,
|
||||
})),
|
||||
}));
|
||||
|
||||
vi.mock("./firecrawl-client.js", () => ({
|
||||
@@ -10,6 +12,15 @@ vi.mock("./firecrawl-client.js", () => ({
|
||||
}));
|
||||
|
||||
describe("firecrawl search tool", () => {
|
||||
beforeEach(() => {
|
||||
vi.resetModules();
|
||||
runFirecrawlSearch.mockReset();
|
||||
runFirecrawlSearch.mockImplementation(async (params: Record<string, unknown>) => ({
|
||||
ok: true,
|
||||
params,
|
||||
}));
|
||||
});
|
||||
|
||||
it("normalizes optional search parameters before invoking Firecrawl", async () => {
|
||||
const { createFirecrawlSearchTool } = await import("./firecrawl-search-tool.js");
|
||||
const tool = createFirecrawlSearchTool({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { MessageEvent, PostbackEvent } from "@line/bot-sdk";
|
||||
import type { HistoryEntry } from "openclaw/plugin-sdk/reply-history";
|
||||
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { LineAccountConfig } from "./types.js";
|
||||
|
||||
// Avoid pulling in globals/pairing/media dependencies; this suite only asserts
|
||||
@@ -209,18 +209,24 @@ async function startInflightReplayDuplicate(params: {
|
||||
}
|
||||
|
||||
describe("handleLineWebhookEvents", () => {
|
||||
beforeAll(async () => {
|
||||
beforeEach(async () => {
|
||||
vi.resetModules();
|
||||
buildLineMessageContextMock.mockReset();
|
||||
buildLineMessageContextMock.mockImplementation(async () => ({
|
||||
ctxPayload: { From: "line:group:group-1" },
|
||||
replyToken: "reply-token",
|
||||
route: { agentId: "default" },
|
||||
isGroup: true,
|
||||
accountId: "default",
|
||||
}));
|
||||
buildLinePostbackContextMock.mockReset();
|
||||
buildLinePostbackContextMock.mockImplementation(async () => null as unknown);
|
||||
readAllowFromStoreMock.mockReset();
|
||||
readAllowFromStoreMock.mockImplementation(async () => [] as string[]);
|
||||
upsertPairingRequestMock.mockReset();
|
||||
upsertPairingRequestMock.mockImplementation(async () => ({ code: "CODE", created: true }));
|
||||
({ handleLineWebhookEvents, createLineWebhookReplayCache } = await import("./bot-handlers.js"));
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
buildLineMessageContextMock.mockReset().mockResolvedValue(buildDefaultLineMessageContext());
|
||||
buildLinePostbackContextMock.mockReset().mockResolvedValue(null as unknown);
|
||||
readAllowFromStoreMock.mockReset().mockResolvedValue([]);
|
||||
upsertPairingRequestMock.mockReset().mockResolvedValue({ code: "CODE", created: true });
|
||||
});
|
||||
|
||||
it("blocks group messages when groupPolicy is disabled", async () => {
|
||||
const processMessage = vi.fn();
|
||||
const event = {
|
||||
|
||||
@@ -29,7 +29,7 @@ vi.mock("openclaw/plugin-sdk/runtime-env", () => ({
|
||||
logVerbose: () => {},
|
||||
}));
|
||||
|
||||
import { downloadLineMedia } from "./download.js";
|
||||
let downloadLineMedia: typeof import("./download.js").downloadLineMedia;
|
||||
|
||||
async function* chunks(parts: Buffer[]): AsyncGenerator<Buffer> {
|
||||
for (const part of parts) {
|
||||
@@ -38,8 +38,11 @@ async function* chunks(parts: Buffer[]): AsyncGenerator<Buffer> {
|
||||
}
|
||||
|
||||
describe("downloadLineMedia", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
beforeEach(async () => {
|
||||
vi.restoreAllMocks();
|
||||
vi.resetModules();
|
||||
getMessageContentMock.mockReset();
|
||||
({ downloadLineMedia } = await import("./download.js"));
|
||||
});
|
||||
|
||||
it("does not derive temp file path from external messageId", async () => {
|
||||
|
||||
@@ -11,6 +11,8 @@ const { createLineBotMock, registerPluginHttpRouteMock, unregisterHttpMock } = v
|
||||
unregisterHttpMock: vi.fn(),
|
||||
}));
|
||||
|
||||
let monitorLineProvider: typeof import("./monitor.js").monitorLineProvider;
|
||||
|
||||
vi.mock("./bot.js", () => ({
|
||||
createLineBot: createLineBotMock,
|
||||
}));
|
||||
@@ -74,14 +76,19 @@ vi.mock("./template-messages.js", () => ({
|
||||
}));
|
||||
|
||||
describe("monitorLineProvider lifecycle", () => {
|
||||
beforeEach(() => {
|
||||
createLineBotMock.mockClear();
|
||||
unregisterHttpMock.mockClear();
|
||||
registerPluginHttpRouteMock.mockClear().mockReturnValue(unregisterHttpMock);
|
||||
beforeEach(async () => {
|
||||
vi.resetModules();
|
||||
createLineBotMock.mockReset();
|
||||
createLineBotMock.mockReturnValue({
|
||||
account: { accountId: "default" },
|
||||
handleWebhook: vi.fn(),
|
||||
});
|
||||
unregisterHttpMock.mockReset();
|
||||
registerPluginHttpRouteMock.mockReset().mockReturnValue(unregisterHttpMock);
|
||||
({ monitorLineProvider } = await import("./monitor.js"));
|
||||
});
|
||||
|
||||
it("waits for abort before resolving", async () => {
|
||||
const { monitorLineProvider } = await import("./monitor.js");
|
||||
const abort = new AbortController();
|
||||
let resolved = false;
|
||||
|
||||
@@ -108,7 +115,6 @@ describe("monitorLineProvider lifecycle", () => {
|
||||
});
|
||||
|
||||
it("stops immediately when signal is already aborted", async () => {
|
||||
const { monitorLineProvider } = await import("./monitor.js");
|
||||
const abort = new AbortController();
|
||||
abort.abort();
|
||||
|
||||
@@ -124,8 +130,6 @@ describe("monitorLineProvider lifecycle", () => {
|
||||
});
|
||||
|
||||
it("returns immediately without abort signal and stop is idempotent", async () => {
|
||||
const { monitorLineProvider } = await import("./monitor.js");
|
||||
|
||||
const monitor = await monitorLineProvider({
|
||||
channelAccessToken: "token",
|
||||
channelSecret: "secret", // pragma: allowlist secret
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
const { getBotInfoMock, MessagingApiClientMock } = vi.hoisted(() => {
|
||||
const getBotInfoMock = vi.fn();
|
||||
const MessagingApiClientMock = vi.fn(function () {
|
||||
@@ -19,7 +19,13 @@ afterEach(() => {
|
||||
});
|
||||
|
||||
describe("probeLineBot", () => {
|
||||
beforeAll(async () => {
|
||||
beforeEach(async () => {
|
||||
vi.resetModules();
|
||||
getBotInfoMock.mockReset();
|
||||
MessagingApiClientMock.mockReset();
|
||||
MessagingApiClientMock.mockImplementation(function () {
|
||||
return { getBotInfo: getBotInfoMock };
|
||||
});
|
||||
({ probeLineBot } = await import("./probe.js"));
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const {
|
||||
pushMessageMock,
|
||||
@@ -74,28 +74,34 @@ vi.mock("openclaw/plugin-sdk/runtime-env", async (importOriginal) => {
|
||||
let sendModule: typeof import("./send.js");
|
||||
|
||||
describe("LINE send helpers", () => {
|
||||
beforeAll(async () => {
|
||||
sendModule = await import("./send.js");
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
vi.resetModules();
|
||||
pushMessageMock.mockReset();
|
||||
replyMessageMock.mockReset();
|
||||
showLoadingAnimationMock.mockReset();
|
||||
getProfileMock.mockReset();
|
||||
MessagingApiClientMock.mockClear();
|
||||
MessagingApiClientMock.mockReset();
|
||||
loadConfigMock.mockReset();
|
||||
resolveLineAccountMock.mockReset();
|
||||
resolveLineChannelAccessTokenMock.mockReset();
|
||||
recordChannelActivityMock.mockReset();
|
||||
logVerboseMock.mockReset();
|
||||
|
||||
MessagingApiClientMock.mockImplementation(function () {
|
||||
return {
|
||||
pushMessage: pushMessageMock,
|
||||
replyMessage: replyMessageMock,
|
||||
showLoadingAnimation: showLoadingAnimationMock,
|
||||
getProfile: getProfileMock,
|
||||
};
|
||||
});
|
||||
loadConfigMock.mockReturnValue({});
|
||||
resolveLineAccountMock.mockReturnValue({ accountId: "default" });
|
||||
resolveLineChannelAccessTokenMock.mockReturnValue("line-token");
|
||||
pushMessageMock.mockResolvedValue({});
|
||||
replyMessageMock.mockResolvedValue({});
|
||||
showLoadingAnimationMock.mockResolvedValue({});
|
||||
sendModule = await import("./send.js");
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-runtime";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
vi.mock("./tavily-client.js", () => ({
|
||||
const { runTavilyExtract } = vi.hoisted(() => ({
|
||||
runTavilyExtract: vi.fn(async (params: unknown) => ({ ok: true, params })),
|
||||
}));
|
||||
|
||||
import { runTavilyExtract } from "./tavily-client.js";
|
||||
import { createTavilyExtractTool } from "./tavily-extract-tool.js";
|
||||
vi.mock("./tavily-client.js", () => ({
|
||||
runTavilyExtract,
|
||||
}));
|
||||
|
||||
let createTavilyExtractTool: typeof import("./tavily-extract-tool.js").createTavilyExtractTool;
|
||||
|
||||
function fakeApi(): OpenClawPluginApi {
|
||||
return {
|
||||
@@ -15,8 +18,11 @@ function fakeApi(): OpenClawPluginApi {
|
||||
}
|
||||
|
||||
describe("tavily_extract", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
beforeEach(async () => {
|
||||
vi.resetModules();
|
||||
runTavilyExtract.mockReset();
|
||||
runTavilyExtract.mockImplementation(async (params: unknown) => ({ ok: true, params }));
|
||||
({ createTavilyExtractTool } = await import("./tavily-extract-tool.js"));
|
||||
});
|
||||
|
||||
it("rejects chunks_per_source without query", async () => {
|
||||
|
||||
@@ -1,12 +1,20 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const runTavilySearch = vi.fn(async (params: Record<string, unknown>) => params);
|
||||
const { runTavilySearch } = vi.hoisted(() => ({
|
||||
runTavilySearch: vi.fn(async (params: Record<string, unknown>) => params),
|
||||
}));
|
||||
|
||||
vi.mock("./tavily-client.js", () => ({
|
||||
runTavilySearch,
|
||||
}));
|
||||
|
||||
describe("tavily web search provider", () => {
|
||||
beforeEach(() => {
|
||||
vi.resetModules();
|
||||
runTavilySearch.mockReset();
|
||||
runTavilySearch.mockImplementation(async (params: Record<string, unknown>) => params);
|
||||
});
|
||||
|
||||
it("exposes the expected metadata and selection wiring", async () => {
|
||||
const { createTavilyWebSearchProvider } = await import("./tavily-search-provider.js");
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const runTavilySearch = vi.fn(async (params: Record<string, unknown>) => ({
|
||||
ok: true,
|
||||
params,
|
||||
const { runTavilySearch } = vi.hoisted(() => ({
|
||||
runTavilySearch: vi.fn(async (params: Record<string, unknown>) => ({
|
||||
ok: true,
|
||||
params,
|
||||
})),
|
||||
}));
|
||||
|
||||
vi.mock("./tavily-client.js", () => ({
|
||||
@@ -10,6 +12,15 @@ vi.mock("./tavily-client.js", () => ({
|
||||
}));
|
||||
|
||||
describe("tavily search tool", () => {
|
||||
beforeEach(() => {
|
||||
vi.resetModules();
|
||||
runTavilySearch.mockReset();
|
||||
runTavilySearch.mockImplementation(async (params: Record<string, unknown>) => ({
|
||||
ok: true,
|
||||
params,
|
||||
}));
|
||||
});
|
||||
|
||||
it("normalizes optional parameters before invoking Tavily", async () => {
|
||||
const { createTavilySearchTool } = await import("./tavily-search-tool.js");
|
||||
const tool = createTavilySearchTool({
|
||||
|
||||
+1
-1
@@ -692,7 +692,7 @@
|
||||
"test:auth:compat": "vitest run --config vitest.gateway.config.ts src/gateway/server.auth.compat-baseline.test.ts src/gateway/client.test.ts src/gateway/reconnect-gating.test.ts src/gateway/protocol/connect-error-details.test.ts",
|
||||
"test:build:singleton": "node scripts/test-built-plugin-singleton.mjs",
|
||||
"test:changed": "pnpm test -- --changed origin/main",
|
||||
"test:channels": "vitest run --config vitest.channels.config.ts",
|
||||
"test:channels": "OPENCLAW_TEST_SKIP_DEFAULT=1 OPENCLAW_TEST_INCLUDE_CHANNELS=1 node scripts/test-parallel.mjs",
|
||||
"test:contracts": "pnpm test:contracts:channels && pnpm test:contracts:plugins",
|
||||
"test:contracts:channels": "OPENCLAW_TEST_PROFILE=low pnpm test -- src/channels/plugins/contracts",
|
||||
"test:contracts:plugins": "OPENCLAW_TEST_PROFILE=low pnpm test -- src/plugins/contracts",
|
||||
|
||||
+45
-15
@@ -50,11 +50,12 @@ const cleanupTempArtifacts = () => {
|
||||
};
|
||||
const existingUnitConfigFiles = (entries) => existingFiles(entries).filter(isUnitConfigTestFile);
|
||||
const baseThreadPinnedFiles = existingFiles(behaviorManifest.base?.threadPinned ?? []);
|
||||
const channelIsolatedManifestFiles = existingFiles(behaviorManifest.channels?.isolated ?? []);
|
||||
const channelIsolatedPrefixes = behaviorManifest.channels?.isolatedPrefixes ?? [];
|
||||
const extensionForkIsolatedFiles = existingFiles(behaviorManifest.extensions?.isolated ?? []);
|
||||
const unitForkIsolatedFiles = existingUnitConfigFiles(behaviorManifest.unit.isolated);
|
||||
const unitThreadPinnedFiles = existingUnitConfigFiles(behaviorManifest.unit.threadPinned);
|
||||
const unitBehaviorOverrideSet = new Set([...unitForkIsolatedFiles, ...unitThreadPinnedFiles]);
|
||||
const channelSingletonFiles = [];
|
||||
|
||||
const children = new Set();
|
||||
const isCI = process.env.CI === "true" || process.env.GITHUB_ACTIONS === "true";
|
||||
@@ -91,6 +92,7 @@ const disableIsolation =
|
||||
process.env.OPENCLAW_TEST_NO_ISOLATE !== "0" &&
|
||||
process.env.OPENCLAW_TEST_NO_ISOLATE !== "false";
|
||||
const includeGatewaySuite = process.env.OPENCLAW_TEST_INCLUDE_GATEWAY === "1";
|
||||
const includeChannelsSuite = process.env.OPENCLAW_TEST_INCLUDE_CHANNELS === "1";
|
||||
const includeExtensionsSuite = process.env.OPENCLAW_TEST_INCLUDE_EXTENSIONS === "1";
|
||||
const noIsolateArgs = disableIsolation ? ["--isolate=false"] : [];
|
||||
const skipDefaultRuns = process.env.OPENCLAW_TEST_SKIP_DEFAULT === "1";
|
||||
@@ -260,12 +262,21 @@ const allKnownTestFiles = [
|
||||
...walkTestFiles(path.join("ui", "src", "ui")),
|
||||
]),
|
||||
];
|
||||
const channelIsolatedFiles = dedupeFilesPreserveOrder([
|
||||
...channelIsolatedManifestFiles,
|
||||
...allKnownTestFiles.filter((file) =>
|
||||
channelIsolatedPrefixes.some((prefix) => file.startsWith(prefix)),
|
||||
),
|
||||
]);
|
||||
const channelIsolatedFileSet = new Set(channelIsolatedFiles);
|
||||
const defaultUnitPool = parsePoolOverride(process.env.OPENCLAW_TEST_UNIT_DEFAULT_POOL, "threads");
|
||||
const isTargetedIsolatedUnitFile = (fileFilter) =>
|
||||
unitForkIsolatedFiles.includes(fileFilter) || unitMemoryIsolatedFiles.includes(fileFilter);
|
||||
const inferTarget = (fileFilter) => {
|
||||
const isolated =
|
||||
isTargetedIsolatedUnitFile(fileFilter) || extensionForkIsolatedFiles.includes(fileFilter);
|
||||
isTargetedIsolatedUnitFile(fileFilter) ||
|
||||
extensionForkIsolatedFiles.includes(fileFilter) ||
|
||||
channelIsolatedFileSet.has(fileFilter);
|
||||
if (isUnitConfigTestFile(fileFilter)) {
|
||||
return { owner: "unit", isolated };
|
||||
}
|
||||
@@ -362,7 +373,7 @@ const unitMemoryIsolatedFiles = dedupeFilesPreserveOrder(
|
||||
);
|
||||
const unitSchedulingOverrideSet = new Set([...unitBehaviorOverrideSet, ...memoryHeavyUnitFiles]);
|
||||
const unitFastExcludedFiles = [
|
||||
...new Set([...unitSchedulingOverrideSet, ...timedHeavyUnitFiles, ...channelSingletonFiles]),
|
||||
...new Set([...unitSchedulingOverrideSet, ...timedHeavyUnitFiles, ...channelIsolatedFiles]),
|
||||
];
|
||||
const estimateUnitDurationMs = (file) =>
|
||||
unitTimingManifest.files[file]?.durationMs ?? unitTimingManifest.defaultDurationMs;
|
||||
@@ -400,10 +411,19 @@ const extensionForkIsolatedFileSet = new Set(extensionForkIsolatedFiles);
|
||||
const extensionSharedCandidateFiles = allKnownTestFiles.filter(
|
||||
(file) => file.startsWith("extensions/") && !extensionForkIsolatedFileSet.has(file),
|
||||
);
|
||||
const channelSharedCandidateFiles = allKnownTestFiles.filter(
|
||||
(file) =>
|
||||
channelTestPrefixes.some((prefix) => file.startsWith(prefix)) &&
|
||||
!channelIsolatedFileSet.has(file),
|
||||
);
|
||||
const extensionIsolatedEntries = extensionForkIsolatedFiles.map((file) => ({
|
||||
name: `extensions-${path.basename(file, ".test.ts")}-isolated`,
|
||||
args: ["vitest", "run", "--config", "vitest.extensions.config.ts", "--pool=forks", file],
|
||||
}));
|
||||
const channelIsolatedEntries = channelIsolatedFiles.map((file) => ({
|
||||
name: `${path.basename(file, ".test.ts")}-channels-isolated`,
|
||||
args: ["vitest", "run", "--config", "vitest.channels.config.ts", "--pool=forks", file],
|
||||
}));
|
||||
const defaultUnitFastLaneCount = isCI && !isWindows ? 3 : 1;
|
||||
const unitFastLaneCount = Math.max(
|
||||
1,
|
||||
@@ -517,18 +537,6 @@ const baseRuns = [
|
||||
],
|
||||
})),
|
||||
...unitThreadEntries,
|
||||
...channelSingletonFiles.map((file) => ({
|
||||
name: `${path.basename(file, ".test.ts")}-channels-isolated`,
|
||||
args: [
|
||||
"vitest",
|
||||
"run",
|
||||
"--config",
|
||||
"vitest.channels.config.ts",
|
||||
"--pool=forks",
|
||||
...noIsolateArgs,
|
||||
file,
|
||||
],
|
||||
})),
|
||||
]
|
||||
: [
|
||||
{
|
||||
@@ -561,6 +569,27 @@ const baseRuns = [
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(includeChannelsSuite
|
||||
? [
|
||||
...channelIsolatedEntries.map((entry) => ({
|
||||
...entry,
|
||||
args: [...entry.args.slice(0, 5), ...noIsolateArgs, ...entry.args.slice(5)],
|
||||
})),
|
||||
{
|
||||
name: "channels",
|
||||
env:
|
||||
channelSharedCandidateFiles.length > 0
|
||||
? {
|
||||
OPENCLAW_VITEST_INCLUDE_FILE: writeTempJsonArtifact(
|
||||
"vitest-channels-include",
|
||||
channelSharedCandidateFiles,
|
||||
),
|
||||
}
|
||||
: undefined,
|
||||
args: ["vitest", "run", "--config", "vitest.channels.config.ts", ...noIsolateArgs],
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(includeGatewaySuite
|
||||
? [
|
||||
{
|
||||
@@ -684,6 +713,7 @@ const createTargetedEntry = (owner, isolated, filters) => {
|
||||
"run",
|
||||
"--config",
|
||||
"vitest.channels.config.ts",
|
||||
...(forceForks ? ["--pool=forks"] : []),
|
||||
...noIsolateArgs,
|
||||
...filters,
|
||||
],
|
||||
|
||||
@@ -43,15 +43,40 @@ const mergeManifestEntries = (section, keys) => {
|
||||
return merged;
|
||||
};
|
||||
|
||||
const mergeManifestStrings = (section, keys) => {
|
||||
const merged = [];
|
||||
const seen = new Set();
|
||||
for (const key of keys) {
|
||||
const values = Array.isArray(section?.[key]) ? section[key] : [];
|
||||
for (const value of values) {
|
||||
if (typeof value !== "string") {
|
||||
continue;
|
||||
}
|
||||
const normalizedValue = normalizeTrackedRepoPath(value);
|
||||
if (normalizedValue.length === 0 || seen.has(normalizedValue)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(normalizedValue);
|
||||
merged.push(normalizedValue);
|
||||
}
|
||||
}
|
||||
return merged;
|
||||
};
|
||||
|
||||
export function loadTestRunnerBehavior() {
|
||||
const raw = tryReadJsonFile(behaviorManifestPath, {});
|
||||
const unit = raw.unit ?? {};
|
||||
const base = raw.base ?? {};
|
||||
const channels = raw.channels ?? {};
|
||||
const extensions = raw.extensions ?? {};
|
||||
return {
|
||||
base: {
|
||||
threadPinned: mergeManifestEntries(base, ["threadPinned", "threadSingleton"]),
|
||||
},
|
||||
channels: {
|
||||
isolated: mergeManifestEntries(channels, ["isolated"]),
|
||||
isolatedPrefixes: mergeManifestStrings(channels, ["isolatedPrefixes"]),
|
||||
},
|
||||
extensions: {
|
||||
isolated: mergeManifestEntries(extensions, ["isolated"]),
|
||||
},
|
||||
|
||||
+105
@@ -15,6 +15,67 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"channels": {
|
||||
"isolatedPrefixes": ["extensions/discord/src/monitor/"],
|
||||
"isolated": [
|
||||
{
|
||||
"file": "extensions/discord/src/monitor/message-handler.preflight.acp-bindings.test.ts",
|
||||
"reason": "This Discord preflight suite hoists conversation-runtime ACP binding mocks; keep it in its own forked channel lane so shared workers do not reuse monitor modules with incompatible runtime stubs."
|
||||
},
|
||||
{
|
||||
"file": "extensions/discord/src/monitor/monitor.threading-utils.test.ts",
|
||||
"reason": "This Discord threading utility suite exercises gateway and presence caches plus auto-thread helpers; keep it in its own forked channel lane so shared workers cannot inherit mutated monitor module state."
|
||||
},
|
||||
{
|
||||
"file": "extensions/discord/src/monitor/monitor.agent-components.test.ts",
|
||||
"reason": "This Discord agent-components suite hoists security and conversation runtime mocks for DM component authorization; keep it in its own forked channel lane so shared workers do not inherit incompatible Discord runtime stubs."
|
||||
},
|
||||
{
|
||||
"file": "extensions/discord/src/monitor/native-command.model-picker.test.ts",
|
||||
"reason": "This Discord model-picker interaction suite relies on isolated native-command fallback state and DM channel enums; keep it in its own forked channel lane so shared workers do not reuse polluted command runtime modules."
|
||||
},
|
||||
{
|
||||
"file": "extensions/discord/src/monitor/native-command.plugin-dispatch.test.ts",
|
||||
"reason": "This Discord native plugin-dispatch suite hoists conversation and reply runtime mocks for slash-command routing; keep it in its own forked channel lane so shared workers do not reuse mismatched native-command modules."
|
||||
},
|
||||
{
|
||||
"file": "extensions/discord/src/monitor/agent-components.wildcard.test.ts",
|
||||
"reason": "This Discord wildcard component suite is green alone but can inherit polluted component-registration parser state from the shared channel lane; keep it in its own forked lane for deterministic CI."
|
||||
},
|
||||
{
|
||||
"file": "extensions/discord/src/send.creates-thread.test.ts",
|
||||
"reason": "This Discord send/create-thread suite hoists web-media mocks and retries against rate-limit seams; keep it isolated so shared channel workers do not leak send-module state into its retry assertions."
|
||||
},
|
||||
{
|
||||
"file": "extensions/discord/src/components.test.ts",
|
||||
"reason": "This Discord component builder suite is green alone but can inherit polluted component constructor state from the shared channel lane; keep it in its own forked lane for deterministic CI."
|
||||
},
|
||||
{
|
||||
"file": "extensions/discord/src/monitor/provider.skill-dedupe.test.ts",
|
||||
"reason": "This Discord provider skill-dedupe suite depends on agent-component classes during provider import; keep it isolated so shared channel workers do not reuse partially mocked component modules."
|
||||
},
|
||||
{
|
||||
"file": "extensions/discord/src/monitor/monitor.test.ts",
|
||||
"reason": "This Discord monitor interaction suite imports agent-component classes directly and is green alone but can inherit partially mocked Discord component constructors from the shared channel lane."
|
||||
},
|
||||
{
|
||||
"file": "extensions/whatsapp/src/inbound.test.ts",
|
||||
"reason": "This WhatsApp inbound helper suite is green alone but can inherit polluted web-message parsing state from the shared channel lane; keep it in its own forked lane for deterministic CI."
|
||||
},
|
||||
{
|
||||
"file": "src/browser/chrome.test.ts",
|
||||
"reason": "This Chrome helper suite is green alone but can inherit stale fetch, websocket, or timer state from the shared channel lane; keep it isolated so its CDP timeout assertions stay deterministic."
|
||||
},
|
||||
{
|
||||
"file": "extensions/telegram/src/bot.create-telegram-bot.test.ts",
|
||||
"reason": "This Telegram bot bootstrap suite is green alone but can inherit stale command-registration spies and bot runtime state from the shared channel lane; keep it isolated for deterministic command assertions."
|
||||
},
|
||||
{
|
||||
"file": "extensions/telegram/src/webhook.test.ts",
|
||||
"reason": "This Telegram webhook suite hoists grammY bot and InputFile mocks; keep it in its own forked channel lane so shared workers do not reuse telegram runtime modules with mismatched constructors."
|
||||
}
|
||||
]
|
||||
},
|
||||
"extensions": {
|
||||
"isolated": [
|
||||
{
|
||||
@@ -28,6 +89,50 @@
|
||||
{
|
||||
"file": "extensions/nextcloud-talk/src/monitor.replay.test.ts",
|
||||
"reason": "The replay-handling regression is green alone but can inherit disturbed global stream/Response state from the shared extensions lane, so keep it in its own forked lane for deterministic CI."
|
||||
},
|
||||
{
|
||||
"file": "extensions/duckduckgo/src/ddg-search-provider.test.ts",
|
||||
"reason": "This provider test hoists a client mock and imports the provider lazily; keep it in its own forked lane so shared extension workers do not reuse a previously cached module under test."
|
||||
},
|
||||
{
|
||||
"file": "extensions/firecrawl/src/firecrawl-scrape-tool.test.ts",
|
||||
"reason": "This scrape-tool suite hoists client mocks and imports the tool lazily; keep it in its own forked lane so shared extension workers cannot bypass the mocked client via cached modules."
|
||||
},
|
||||
{
|
||||
"file": "extensions/firecrawl/src/firecrawl-search-provider.test.ts",
|
||||
"reason": "This provider suite hoists the Firecrawl client mock and imports lazily; keep it isolated so shared extension workers do not reuse stale provider modules."
|
||||
},
|
||||
{
|
||||
"file": "extensions/firecrawl/src/firecrawl-search-tool.test.ts",
|
||||
"reason": "This tool suite hoists the Firecrawl client mock and imports lazily; keep it isolated so shared extension workers cannot leak cached implementations across files."
|
||||
},
|
||||
{
|
||||
"file": "extensions/line/src/download.test.ts",
|
||||
"reason": "This LINE media-download suite depends on hoisted SDK mocks; run it in its own forked lane so shared extension workers do not fall back to the real SDK client."
|
||||
},
|
||||
{
|
||||
"file": "extensions/line/src/monitor.lifecycle.test.ts",
|
||||
"reason": "This LINE lifecycle suite hoists runtime and route-registration mocks; keep it isolated so shared extension workers do not reuse cached monitor modules with real dependencies."
|
||||
},
|
||||
{
|
||||
"file": "extensions/line/src/probe.test.ts",
|
||||
"reason": "This LINE probe suite depends on hoisted SDK mocks; keep it isolated so shared extension workers do not reuse cached probe modules with the real SDK client."
|
||||
},
|
||||
{
|
||||
"file": "extensions/line/src/send.test.ts",
|
||||
"reason": "This LINE send suite hoists SDK and account/token mocks; keep it isolated so shared extension workers do not reuse cached send modules that bypass those mocks."
|
||||
},
|
||||
{
|
||||
"file": "extensions/tavily/src/tavily-extract-tool.test.ts",
|
||||
"reason": "This extract-tool suite hoists the Tavily client mock and imports lazily; keep it isolated so shared extension workers do not reuse cached tool modules and hit real API-key resolution."
|
||||
},
|
||||
{
|
||||
"file": "extensions/tavily/src/tavily-search-provider.test.ts",
|
||||
"reason": "This provider suite hoists the Tavily client mock and imports lazily; keep it isolated so shared extension workers do not reuse stale provider modules."
|
||||
},
|
||||
{
|
||||
"file": "extensions/tavily/src/tavily-search-tool.test.ts",
|
||||
"reason": "This search-tool suite hoists the Tavily client mock and imports lazily; keep it isolated so shared extension workers do not bypass the mocked client via cached modules."
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { loadTestRunnerBehavior } from "../scripts/test-runner-manifest.mjs";
|
||||
|
||||
describe("loadTestRunnerBehavior", () => {
|
||||
it("loads channel isolated entries from the behavior manifest", () => {
|
||||
const behavior = loadTestRunnerBehavior();
|
||||
const files = behavior.channels.isolated.map((entry) => entry.file);
|
||||
|
||||
expect(files).toContain(
|
||||
"extensions/discord/src/monitor/message-handler.preflight.acp-bindings.test.ts",
|
||||
);
|
||||
});
|
||||
|
||||
it("loads channel isolated prefixes from the behavior manifest", () => {
|
||||
const behavior = loadTestRunnerBehavior();
|
||||
|
||||
expect(behavior.channels.isolatedPrefixes).toContain("extensions/discord/src/monitor/");
|
||||
});
|
||||
});
|
||||
@@ -1,3 +1,6 @@
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { createChannelsVitestConfig } from "../vitest.channels.config.ts";
|
||||
import { createExtensionsVitestConfig } from "../vitest.extensions.config.ts";
|
||||
@@ -20,6 +23,7 @@ describe("createScopedVitestConfig", () => {
|
||||
it("applies non-isolated mode by default", () => {
|
||||
const config = createScopedVitestConfig(["src/example.test.ts"], { env: {} });
|
||||
expect(config.test?.isolate).toBe(false);
|
||||
expect(config.test?.runner).toBe("./test/non-isolated-runner.ts");
|
||||
});
|
||||
|
||||
it("passes through a scoped root dir when provided", () => {
|
||||
@@ -52,6 +56,30 @@ describe("scoped vitest configs", () => {
|
||||
expect(defaultChannelsConfig.test?.isolate).toBe(false);
|
||||
});
|
||||
|
||||
it("loads channel include overrides from OPENCLAW_VITEST_INCLUDE_FILE", () => {
|
||||
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-vitest-channels-"));
|
||||
try {
|
||||
const includeFile = path.join(tempDir, "include.json");
|
||||
fs.writeFileSync(
|
||||
includeFile,
|
||||
JSON.stringify([
|
||||
"extensions/discord/src/monitor/message-handler.preflight.acp-bindings.test.ts",
|
||||
]),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
const config = createChannelsVitestConfig({
|
||||
OPENCLAW_VITEST_INCLUDE_FILE: includeFile,
|
||||
});
|
||||
|
||||
expect(config.test?.include).toEqual([
|
||||
"extensions/discord/src/monitor/message-handler.preflight.acp-bindings.test.ts",
|
||||
]);
|
||||
} finally {
|
||||
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("defaults extension tests to non-isolated mode", () => {
|
||||
expect(defaultExtensionsConfig.test?.isolate).toBe(false);
|
||||
});
|
||||
|
||||
@@ -1,11 +1,31 @@
|
||||
import fs from "node:fs";
|
||||
import { channelTestInclude } from "./vitest.channel-paths.mjs";
|
||||
import { createScopedVitestConfig } from "./vitest.scoped-config.ts";
|
||||
|
||||
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 function createChannelsVitestConfig(env?: Record<string, string | undefined>) {
|
||||
return createScopedVitestConfig(channelTestInclude, {
|
||||
return createScopedVitestConfig(loadIncludePatternsFromEnv(env) ?? channelTestInclude, {
|
||||
env,
|
||||
pool: "threads",
|
||||
exclude: ["src/gateway/**"],
|
||||
passWithNoTests: true,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -64,12 +64,14 @@ export function createScopedVitestConfig(
|
||||
[...(baseTest.exclude ?? []), ...(options?.exclude ?? [])],
|
||||
scopedDir,
|
||||
);
|
||||
const isolate = resolveVitestIsolation(options?.env);
|
||||
|
||||
return defineConfig({
|
||||
...base,
|
||||
test: {
|
||||
...baseTest,
|
||||
isolate: resolveVitestIsolation(options?.env),
|
||||
isolate,
|
||||
runner: "./test/non-isolated-runner.ts",
|
||||
...(scopedDir ? { dir: scopedDir } : {}),
|
||||
include: relativizeScopedPatterns(include, scopedDir),
|
||||
exclude,
|
||||
|
||||
Reference in New Issue
Block a user