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:
Peter Steinberger
2026-07-19 08:39:16 -07:00
committed by GitHub
parent 8c7f09a3c4
commit ca3a1873a6
18 changed files with 122 additions and 72 deletions
@@ -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),
}),
]);
});
});
+2 -2
View File
@@ -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({
@@ -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) }),
]);
});
});
+14 -6
View File
@@ -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.");
}
+9 -2
View File
@@ -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;
},
},
});
+12 -8
View File
@@ -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)
);
}
-19
View File
@@ -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"],
+8 -10
View File
@@ -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);
});
+14 -11
View File
@@ -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,
},
);
}
+33 -4
View File
@@ -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");
});
});
+2 -2
View File
@@ -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");
+2 -1
View File
@@ -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);
+2 -1
View File
@@ -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,
});
}