mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-21 02:06:43 +00:00
fix(test): make broad shard execution deterministic (#111416)
* fix(test): harden broad shard execution * test: allow broad-profile contention * style(telegram): remove redundant chat id cast * style(test): preserve setup test spacing
This commit is contained in:
@@ -322,6 +322,12 @@ describe("Google Chat durable ingress", () => {
|
||||
await stopping;
|
||||
expect(stopped).toBe(true);
|
||||
expect(await queue.listClaims()).toHaveLength(0);
|
||||
expect(await queue.listPending()).toEqual([
|
||||
expect.objectContaining({
|
||||
id: "spaces/AAA/messages/active",
|
||||
lastError: expect.any(String),
|
||||
}),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -241,12 +241,12 @@ describe("IRC durable ingress", () => {
|
||||
const pausing = ingress.pause().then(() => {
|
||||
pauseSettled = true;
|
||||
});
|
||||
await connection.accept(CHANNEL_LINE, "bot");
|
||||
const secondAdmission = connection.accept(CHANNEL_LINE, "bot");
|
||||
await Promise.resolve();
|
||||
expect(pauseSettled).toBe(false);
|
||||
|
||||
releaseDispatch.resolve();
|
||||
await pausing;
|
||||
await Promise.all([pausing, secondAdmission]);
|
||||
expect(dispatch).toHaveBeenCalledOnce();
|
||||
expect(await queue.listPending({ limit: "all" })).toEqual([
|
||||
expect.objectContaining({
|
||||
|
||||
+3
@@ -7,6 +7,7 @@ import {
|
||||
installSignalToolResultTestHooks,
|
||||
setSignalToolResultTestConfig,
|
||||
toSignalToolResultTestError,
|
||||
waitForSignalToolResultIngressIdle,
|
||||
} from "./monitor.tool-result.test-harness.js";
|
||||
|
||||
installSignalToolResultTestHooks();
|
||||
@@ -63,6 +64,7 @@ describe("monitorSignalProvider tool results", () => {
|
||||
event: "receive",
|
||||
data: JSON.stringify(payload),
|
||||
});
|
||||
await waitForSignalToolResultIngressIdle();
|
||||
abortController.abort();
|
||||
});
|
||||
|
||||
@@ -264,6 +266,7 @@ describe("monitorSignalProvider tool results", () => {
|
||||
},
|
||||
}),
|
||||
});
|
||||
await waitForSignalToolResultIngressIdle();
|
||||
abortController.abort();
|
||||
});
|
||||
|
||||
|
||||
@@ -2509,7 +2509,10 @@ Second paragraph should still reach the agent after Slack's preview cutoff.`;
|
||||
const { storePath } = storeFixture.makeTmpStorePath();
|
||||
const now = Date.now();
|
||||
const cfg = {
|
||||
session: { store: storePath },
|
||||
session: {
|
||||
store: storePath,
|
||||
resetByType: { thread: { mode: "idle", idleMinutes: 60 } },
|
||||
},
|
||||
channels: { slack: { enabled: true, replyToMode: "all", groupPolicy: "open" } },
|
||||
} as OpenClawConfig;
|
||||
const route = resolveAgentRoute({
|
||||
@@ -2645,7 +2648,10 @@ Second paragraph should still reach the agent after Slack's preview cutoff.`;
|
||||
const { storePath } = storeFixture.makeTmpStorePath();
|
||||
const now = Date.now();
|
||||
const cfg = {
|
||||
session: { store: storePath },
|
||||
session: {
|
||||
store: storePath,
|
||||
resetByType: { thread: { mode: "idle", idleMinutes: 60 } },
|
||||
},
|
||||
channels: { slack: { enabled: true, replyToMode: "all", groupPolicy: "open" } },
|
||||
} as OpenClawConfig;
|
||||
const route = resolveAgentRoute({
|
||||
|
||||
@@ -267,7 +267,10 @@ describe("Synology Chat durable ingress", () => {
|
||||
|
||||
releaseDelivery();
|
||||
await stopping;
|
||||
expect(await queue.listClaims()).toHaveLength(1);
|
||||
expect(await queue.listClaims()).toHaveLength(0);
|
||||
expect(await queue.listPending()).toEqual([
|
||||
expect.objectContaining({ id: "post-active", lastError: expect.any(String) }),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -211,11 +211,15 @@ function readTelegramSendMediaUrls(params: Record<string, unknown>) {
|
||||
function resolveTelegramButtonsFromParams(
|
||||
params: Record<string, unknown>,
|
||||
presentation = normalizeMessagePresentation(params.presentation),
|
||||
options?: { allowWebAppButtons?: boolean },
|
||||
) {
|
||||
return resolveTelegramInlineButtons({
|
||||
presentation,
|
||||
interactive: params.interactive,
|
||||
});
|
||||
return resolveTelegramInlineButtons(
|
||||
{
|
||||
presentation,
|
||||
interactive: params.interactive,
|
||||
},
|
||||
options,
|
||||
);
|
||||
}
|
||||
|
||||
function readTelegramSendContent(params: {
|
||||
@@ -459,7 +463,9 @@ export async function handleTelegramAction(
|
||||
const firstMediaUrl = mediaUrls[0];
|
||||
const location = normalizeOutboundLocation(params.location);
|
||||
const presentation = normalizeMessagePresentation(params.presentation);
|
||||
const buttons = resolveTelegramButtonsFromParams(params, presentation);
|
||||
const buttons = resolveTelegramButtonsFromParams(params, presentation, {
|
||||
allowWebAppButtons: resolveTelegramTargetChatType(to) === "direct",
|
||||
});
|
||||
const content = readTelegramSendContent({
|
||||
args: params,
|
||||
mediaUrl: firstMediaUrl,
|
||||
@@ -698,7 +704,9 @@ export async function handleTelegramAction(
|
||||
readStringParam(params, "content", { allowEmpty: false }) ??
|
||||
readStringParam(params, "message", { allowEmpty: false });
|
||||
const caption = readStringParam(params, "caption", { allowEmpty: false });
|
||||
const buttons = resolveTelegramButtonsFromParams(params);
|
||||
const buttons = resolveTelegramButtonsFromParams(params, undefined, {
|
||||
allowWebAppButtons: resolveTelegramTargetChatType(chatId ?? "") === "direct",
|
||||
});
|
||||
if (content == null && caption == null && buttons === undefined) {
|
||||
throw new Error("content required.");
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@ vi.mock("openclaw/plugin-sdk/channel-inbound", async () => {
|
||||
}
|
||||
const plan: import("openclaw/plugin-sdk/channel-inbound").ChannelInboundTurnPlan =
|
||||
resolved;
|
||||
return {
|
||||
const prepared: Awaited<ReturnType<typeof resolveTurn>> = {
|
||||
...plan,
|
||||
runDispatch: async () =>
|
||||
await harness.dispatchReplyWithBufferedBlockDispatcher({
|
||||
@@ -93,7 +93,14 @@ vi.mock("openclaw/plugin-sdk/channel-inbound", async () => {
|
||||
replyOptions: plan.replyOptions,
|
||||
replyResolver: plan.replyResolver,
|
||||
}),
|
||||
} as typeof resolved;
|
||||
// Prepared dispatch owns the outer durable-ingress lifecycle. If
|
||||
// core suppresses dispatch, release that claim instead of orphaning it.
|
||||
runDispatchLifecycle: {
|
||||
turnAdoptionLifecycle: params.turnAdoptionLifecycle,
|
||||
onDispatchSkipped: () => params.turnAdoptionLifecycle?.onAbandoned?.(),
|
||||
},
|
||||
};
|
||||
return prepared;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -121,13 +121,14 @@ function chunkInteractiveButtons(
|
||||
*/
|
||||
function buildTelegramInteractiveButtons(
|
||||
interactive?: LegacyInteractiveReply,
|
||||
options?: { allowWebAppButtons?: boolean },
|
||||
): TelegramInlineButtons | undefined {
|
||||
const rows = reduceLegacyInteractiveReply(
|
||||
interactive,
|
||||
[] as TelegramInlineButton[][],
|
||||
(state, block) => {
|
||||
if (block.type === "buttons") {
|
||||
chunkInteractiveButtons(block.buttons, state);
|
||||
chunkInteractiveButtons(block.buttons, state, options);
|
||||
return state;
|
||||
}
|
||||
if (block.type === "select") {
|
||||
@@ -173,14 +174,17 @@ export function buildTelegramPresentationButtons(
|
||||
}
|
||||
|
||||
/** Resolve Telegram inline buttons, preserving explicit and legacy button precedence. */
|
||||
export function resolveTelegramInlineButtons(params: {
|
||||
buttons?: TelegramInlineButtons;
|
||||
presentation?: unknown;
|
||||
interactive?: unknown;
|
||||
}): TelegramInlineButtons | undefined {
|
||||
export function resolveTelegramInlineButtons(
|
||||
params: {
|
||||
buttons?: TelegramInlineButtons;
|
||||
presentation?: unknown;
|
||||
interactive?: unknown;
|
||||
},
|
||||
options?: { allowWebAppButtons?: boolean },
|
||||
): TelegramInlineButtons | undefined {
|
||||
return (
|
||||
params.buttons ??
|
||||
buildTelegramInteractiveButtons(normalizeLegacyInteractiveReply(params.interactive)) ??
|
||||
buildTelegramPresentationButtons(normalizeMessagePresentation(params.presentation))
|
||||
buildTelegramInteractiveButtons(normalizeLegacyInteractiveReply(params.interactive), options) ??
|
||||
buildTelegramPresentationButtons(normalizeMessagePresentation(params.presentation), options)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -294,25 +294,6 @@ describe("Zoom meetings plugin surface", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("accepts timeoutMs on testListen and reports a bounded caption timeout", async () => {
|
||||
const { invoke } = authorizationHarness();
|
||||
const response = await invoke("zoommeetings.testListen", {
|
||||
mode: "transcribe",
|
||||
timeoutMs: 1,
|
||||
url: MEETING_URL,
|
||||
});
|
||||
|
||||
expect(response).toMatchObject({
|
||||
ok: true,
|
||||
payload: {
|
||||
captioning: undefined,
|
||||
createdSession: true,
|
||||
listenTimedOut: true,
|
||||
listenVerified: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
["zoommeetings.testSpeech", "transcribe", "test_speech requires mode: agent or bidi"],
|
||||
["zoommeetings.testListen", "agent", "test_listen requires mode: transcribe"],
|
||||
|
||||
@@ -4624,12 +4624,15 @@ function sanitizeVitestCachePathSegment(value) {
|
||||
|
||||
export function applyParallelVitestCachePaths(specs, params = {}) {
|
||||
const baseEnv = params.env ?? process.env;
|
||||
if (baseEnv[FS_MODULE_CACHE_PATH_ENV_KEY]?.trim()) {
|
||||
return specs;
|
||||
}
|
||||
const cwd = params.cwd ?? process.cwd();
|
||||
const configuredCacheRoot = baseEnv[FS_MODULE_CACHE_PATH_ENV_KEY]?.trim() || undefined;
|
||||
// CI publishes a persistent cache root, not a writer-safe leaf. Every
|
||||
// concurrent Vitest process still needs its own live directory below it.
|
||||
const cacheRoot =
|
||||
configuredCacheRoot ?? path.join(cwd, "node_modules", ".experimental-vitest-cache");
|
||||
return specs.map((spec, index) => {
|
||||
if (spec.env?.[FS_MODULE_CACHE_PATH_ENV_KEY]?.trim()) {
|
||||
const specCachePath = spec.env?.[FS_MODULE_CACHE_PATH_ENV_KEY]?.trim();
|
||||
if (specCachePath && specCachePath !== configuredCacheRoot) {
|
||||
return spec;
|
||||
}
|
||||
const cacheSegment = sanitizeVitestCachePathSegment(`${index}-${spec.config}`);
|
||||
@@ -4637,12 +4640,7 @@ export function applyParallelVitestCachePaths(specs, params = {}) {
|
||||
...spec,
|
||||
env: {
|
||||
...spec.env,
|
||||
[FS_MODULE_CACHE_PATH_ENV_KEY]: path.join(
|
||||
cwd,
|
||||
"node_modules",
|
||||
".experimental-vitest-cache",
|
||||
cacheSegment,
|
||||
),
|
||||
[FS_MODULE_CACHE_PATH_ENV_KEY]: path.join(cacheRoot, cacheSegment),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -627,5 +627,5 @@ describe("SQLite active transcript event projection", () => {
|
||||
await reconciliation;
|
||||
expect(order).toEqual(["event-loop-responsive", "live-write", "reconciled"]);
|
||||
expect(readSessionTranscriptMessageEventCount(scope)).toBe(100_001);
|
||||
}, 30_000);
|
||||
}, 60_000);
|
||||
});
|
||||
|
||||
@@ -578,22 +578,25 @@ describe("test-projects args", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("preserves explicit Vitest filesystem module cache paths", () => {
|
||||
const specs = [
|
||||
it("splits an explicit Vitest filesystem module cache root", () => {
|
||||
const [spec] = applyParallelVitestCachePaths(
|
||||
[
|
||||
{
|
||||
config: "test/vitest/vitest.gateway.config.ts",
|
||||
env: {},
|
||||
},
|
||||
],
|
||||
{
|
||||
config: "test/vitest/vitest.gateway.config.ts",
|
||||
env: {},
|
||||
},
|
||||
];
|
||||
|
||||
expect(
|
||||
applyParallelVitestCachePaths(specs, {
|
||||
cwd: "/repo",
|
||||
env: {
|
||||
OPENCLAW_VITEST_FS_MODULE_CACHE_PATH: "/tmp/cache",
|
||||
},
|
||||
}),
|
||||
).toBe(specs);
|
||||
},
|
||||
);
|
||||
|
||||
expect(spec?.env.OPENCLAW_VITEST_FS_MODULE_CACHE_PATH).toBe(
|
||||
"/tmp/cache/0-test-vitest-vitest.gateway.config.ts",
|
||||
);
|
||||
});
|
||||
|
||||
it("routes cli targets to the cli config", () => {
|
||||
|
||||
@@ -17,7 +17,7 @@ describe("heartbeat active-hours runtime evidence", () => {
|
||||
const evidence = await runHeartbeatActiveHoursRuntime({
|
||||
artifactBase,
|
||||
repoRoot: process.cwd(),
|
||||
timeoutMs: 2_000,
|
||||
timeoutMs: 5_000,
|
||||
});
|
||||
|
||||
expect(evidence.entries[0]?.result.status).toBe("pass");
|
||||
|
||||
@@ -23,7 +23,7 @@ function runProof(args: string[]) {
|
||||
{
|
||||
cwd: process.cwd(),
|
||||
encoding: "utf8",
|
||||
timeout: 120_000,
|
||||
timeout: 240_000,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4922,13 +4922,42 @@ describe("scripts/test-projects parallel cache paths", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps an explicit global cache path", () => {
|
||||
const [spec] = applyParallelVitestCachePaths(
|
||||
[{ config: "test/vitest/vitest.gateway.config.ts", env: {}, pnpmArgs: [] }],
|
||||
it("splits an explicit global cache root per parallel shard", () => {
|
||||
const specs = applyParallelVitestCachePaths(
|
||||
[
|
||||
{
|
||||
config: "test/vitest/vitest.gateway.config.ts",
|
||||
env: { OPENCLAW_VITEST_FS_MODULE_CACHE_PATH: "/tmp/cache" },
|
||||
pnpmArgs: [],
|
||||
},
|
||||
{
|
||||
config: "test/vitest/vitest.extension-telegram.config.ts",
|
||||
env: { OPENCLAW_VITEST_FS_MODULE_CACHE_PATH: "/tmp/cache" },
|
||||
pnpmArgs: [],
|
||||
},
|
||||
],
|
||||
{ cwd: "/repo", env: { OPENCLAW_VITEST_FS_MODULE_CACHE_PATH: "/tmp/cache" } },
|
||||
);
|
||||
|
||||
expect(spec?.env.OPENCLAW_VITEST_FS_MODULE_CACHE_PATH).toBeUndefined();
|
||||
expect(specs.map((spec) => spec.env.OPENCLAW_VITEST_FS_MODULE_CACHE_PATH)).toEqual([
|
||||
path.join("/tmp/cache", "0-test-vitest-vitest.gateway.config.ts"),
|
||||
path.join("/tmp/cache", "1-test-vitest-vitest.extension-telegram.config.ts"),
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps an already isolated cache path", () => {
|
||||
const [spec] = applyParallelVitestCachePaths(
|
||||
[
|
||||
{
|
||||
config: "test/vitest/vitest.gateway.config.ts",
|
||||
env: { OPENCLAW_VITEST_FS_MODULE_CACHE_PATH: "/tmp/cache/gateway" },
|
||||
pnpmArgs: [],
|
||||
},
|
||||
],
|
||||
{ cwd: "/repo", env: { OPENCLAW_VITEST_FS_MODULE_CACHE_PATH: "/tmp/cache" } },
|
||||
);
|
||||
|
||||
expect(spec?.env.OPENCLAW_VITEST_FS_MODULE_CACHE_PATH).toBe("/tmp/cache/gateway");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -216,8 +216,8 @@ describe("projects vitest config", () => {
|
||||
const config = createUiVitestConfig();
|
||||
const testConfig = requireTestConfig(config);
|
||||
expect(testConfig.environment).toBe("jsdom");
|
||||
expect(testConfig.isolate).toBe(false);
|
||||
expect(normalizeConfigPath(testConfig.runner)).toBe("test/non-isolated-runner.ts");
|
||||
expect(testConfig.isolate).toBe(true);
|
||||
expect(testConfig.runner).toBeUndefined();
|
||||
const setupFiles = normalizeConfigPaths(testConfig.setupFiles);
|
||||
expect(setupFiles).not.toContain("test/setup-openclaw-runtime.ts");
|
||||
expect(setupFiles).toContain("ui/src/test-helpers/lit-warnings.setup.ts");
|
||||
|
||||
@@ -607,7 +607,8 @@ describe("scoped vitest configs", () => {
|
||||
|
||||
expectForkedNonIsolatedRunner(defaultCommandsConfig);
|
||||
|
||||
expectThreadedNonIsolatedRunner(defaultUiConfig);
|
||||
expect(requireTestConfig(defaultUiConfig).isolate).toBe(true);
|
||||
expect(requireTestConfig(defaultUiConfig).runner).toBeUndefined();
|
||||
expectThreadedIsolatedRunner(defaultExtensionMemoryConfig);
|
||||
expectThreadedIsolatedRunner(defaultExtensionProvidersConfig);
|
||||
expectForkedIsolatedRunner(defaultInfraConfig);
|
||||
|
||||
@@ -15,9 +15,10 @@ export function createUiVitestConfig(
|
||||
exclude,
|
||||
excludeUnitFastTests: false,
|
||||
includeOpenClawRuntimeSetup: false,
|
||||
isolate: false,
|
||||
isolate: true,
|
||||
name: options?.name ?? "ui",
|
||||
setupFiles: ["ui/src/test-helpers/lit-warnings.setup.ts"],
|
||||
useNonIsolatedRunner: false,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user