Stabilize sharded CI runtime tests
This commit is contained in:
parent
f1653d2ea1
commit
b2874db605
11
README.md
11
README.md
@ -30,6 +30,17 @@ OpenClaw Onboard guides you step by step through setting up the gateway, workspa
|
||||
Works with npm, pnpm, or bun.
|
||||
New install? Start here: [Getting started](https://docs.openclaw.ai/start/getting-started)
|
||||
|
||||
## Cortex Companion
|
||||
|
||||
This integration branch also pairs OpenClaw with [Cortex AI](https://github.com/Junebugg1214/Cortex-AI) for local memory, previewable context, conflict handling, and coding sync. If you want the assistant's memory to stay inspectable and versionable on your machine, Cortex is the companion repo for that flow.
|
||||
|
||||
```bash
|
||||
openclaw memory cortex enable
|
||||
/cortex preview
|
||||
/cortex conflicts
|
||||
/cortex sync coding
|
||||
```
|
||||
|
||||
## Sponsors
|
||||
|
||||
| OpenAI | Vercel | Blacksmith | Convex |
|
||||
|
||||
@ -266,6 +266,10 @@ const parseEnvNumber = (name, fallback) => {
|
||||
const parsed = Number.parseInt(process.env[name] ?? "", 10);
|
||||
return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback;
|
||||
};
|
||||
const shardedCi = isCI && shardCount > 1;
|
||||
const shardedCiTopLevelConcurrency = shardedCi
|
||||
? Math.max(1, parseEnvNumber("OPENCLAW_TEST_TOP_LEVEL_CONCURRENCY", 3))
|
||||
: null;
|
||||
const allKnownUnitFiles = allKnownTestFiles.filter((file) => {
|
||||
return isUnitConfigTestFile(file);
|
||||
});
|
||||
@ -664,25 +668,49 @@ const defaultWorkerBudget =
|
||||
extensions: Math.max(1, Math.min(4, Math.floor(localWorkers / 4))),
|
||||
gateway: 1,
|
||||
};
|
||||
const shardedCiWorkerBudget =
|
||||
shardedCi && !isMacOS
|
||||
? {
|
||||
// Sharded Linux/Windows CI runs already divide the file set, so a smaller worker
|
||||
// fan-out keeps vmFork/fork heaps under control without penalizing local runs.
|
||||
unit: 2,
|
||||
unitIsolated: 1,
|
||||
extensions: 2,
|
||||
gateway: 1,
|
||||
}
|
||||
: null;
|
||||
|
||||
// Keep worker counts predictable for local runs; trim macOS CI workers to avoid worker crashes/OOM.
|
||||
// In CI on linux/windows, prefer Vitest defaults to avoid cross-test interference from lower worker counts.
|
||||
// On sharded Linux/Windows CI, cap worker fan-out to avoid unit-fast heap blowups while still
|
||||
// keeping enough concurrency to finish quickly. Non-sharded CI keeps Vitest defaults.
|
||||
const maxWorkersForRun = (name) => {
|
||||
if (resolvedOverride) {
|
||||
return resolvedOverride;
|
||||
}
|
||||
if (isCI && !isMacOS) {
|
||||
return null;
|
||||
}
|
||||
if (isCI && isMacOS) {
|
||||
return 1;
|
||||
}
|
||||
if (name.endsWith("-threads") || name.endsWith("-vmforks")) {
|
||||
return 1;
|
||||
}
|
||||
if (name.endsWith("-isolated") && name !== "unit-isolated") {
|
||||
return 1;
|
||||
}
|
||||
if (isCI && !isMacOS) {
|
||||
if (shardedCiWorkerBudget) {
|
||||
if (name === "unit-isolated" || name.startsWith("unit-heavy-")) {
|
||||
return shardedCiWorkerBudget.unitIsolated;
|
||||
}
|
||||
if (name === "extensions") {
|
||||
return shardedCiWorkerBudget.extensions;
|
||||
}
|
||||
if (name === "gateway") {
|
||||
return shardedCiWorkerBudget.gateway;
|
||||
}
|
||||
return shardedCiWorkerBudget.unit;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
if (isCI && isMacOS) {
|
||||
return 1;
|
||||
}
|
||||
if (name === "unit-isolated" || name.startsWith("unit-heavy-")) {
|
||||
return defaultWorkerBudget.unitIsolated;
|
||||
}
|
||||
@ -1079,6 +1107,9 @@ const runEntriesWithLimit = async (entries, extraArgs = [], concurrency = 1) =>
|
||||
|
||||
const runEntries = async (entries, extraArgs = []) => {
|
||||
if (topLevelParallelEnabled) {
|
||||
if (shardedCiTopLevelConcurrency !== null) {
|
||||
return runEntriesWithLimit(entries, extraArgs, shardedCiTopLevelConcurrency);
|
||||
}
|
||||
const codes = await Promise.all(entries.map((entry) => run(entry, extraArgs)));
|
||||
return codes.find((code) => code !== 0);
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { ensureAuthProfileStore, type AuthProfileStore } from "../agents/auth-profiles.js";
|
||||
import {
|
||||
clearConfigCache,
|
||||
@ -10,6 +10,7 @@ import {
|
||||
writeConfigFile,
|
||||
} from "../config/config.js";
|
||||
import { withTempHome } from "../config/home-env.test-harness.js";
|
||||
import type { PluginWebSearchProviderEntry } from "../plugins/types.js";
|
||||
import {
|
||||
activateSecretsRuntimeSnapshot,
|
||||
clearSecretsRuntimeSnapshot,
|
||||
@ -18,6 +19,72 @@ import {
|
||||
prepareSecretsRuntimeSnapshot,
|
||||
} from "./runtime.js";
|
||||
|
||||
function ensureRecord(target: Record<string, unknown>, key: string): Record<string, unknown> {
|
||||
const current = target[key];
|
||||
if (typeof current === "object" && current !== null && !Array.isArray(current)) {
|
||||
return current as Record<string, unknown>;
|
||||
}
|
||||
const next: Record<string, unknown> = {};
|
||||
target[key] = next;
|
||||
return next;
|
||||
}
|
||||
|
||||
function setConfiguredProviderKey(configTarget: OpenClawConfig, value: unknown): void {
|
||||
const plugins = ensureRecord(configTarget as Record<string, unknown>, "plugins");
|
||||
const entries = ensureRecord(plugins, "entries");
|
||||
const googleEntry = ensureRecord(entries, "google");
|
||||
const config = ensureRecord(googleEntry, "config");
|
||||
const webSearch = ensureRecord(config, "webSearch");
|
||||
webSearch.apiKey = value;
|
||||
}
|
||||
|
||||
function buildGeminiTestProvider(): PluginWebSearchProviderEntry {
|
||||
return {
|
||||
pluginId: "google",
|
||||
id: "gemini",
|
||||
label: "gemini",
|
||||
hint: "gemini test provider",
|
||||
envVars: ["GEMINI_API_KEY"],
|
||||
placeholder: "AIza...",
|
||||
signupUrl: "https://example.com/gemini",
|
||||
autoDetectOrder: 20,
|
||||
credentialPath: "plugins.entries.google.config.webSearch.apiKey",
|
||||
inactiveSecretPaths: ["plugins.entries.google.config.webSearch.apiKey"],
|
||||
getCredentialValue: (searchConfig) => {
|
||||
const gemini = searchConfig?.gemini;
|
||||
return gemini && typeof gemini === "object" && !Array.isArray(gemini)
|
||||
? (gemini as Record<string, unknown>).apiKey
|
||||
: undefined;
|
||||
},
|
||||
setCredentialValue: (searchConfigTarget, value) => {
|
||||
const scoped = searchConfigTarget.gemini;
|
||||
if (!scoped || typeof scoped !== "object" || Array.isArray(scoped)) {
|
||||
searchConfigTarget.gemini = { apiKey: value };
|
||||
return;
|
||||
}
|
||||
(scoped as Record<string, unknown>).apiKey = value;
|
||||
},
|
||||
getConfiguredCredentialValue: (config) => {
|
||||
const entryConfig = config?.plugins?.entries?.google?.config;
|
||||
return entryConfig && typeof entryConfig === "object"
|
||||
? (entryConfig as { webSearch?: { apiKey?: unknown } }).webSearch?.apiKey
|
||||
: undefined;
|
||||
},
|
||||
setConfiguredCredentialValue: (configTarget, value) => {
|
||||
setConfiguredProviderKey(configTarget, value);
|
||||
},
|
||||
createTool: () => null,
|
||||
};
|
||||
}
|
||||
|
||||
const { resolvePluginWebSearchProvidersMock } = vi.hoisted(() => ({
|
||||
resolvePluginWebSearchProvidersMock: vi.fn(() => [buildGeminiTestProvider()]),
|
||||
}));
|
||||
|
||||
vi.mock("../plugins/web-search-providers.js", () => ({
|
||||
resolvePluginWebSearchProviders: resolvePluginWebSearchProvidersMock,
|
||||
}));
|
||||
|
||||
const OPENAI_ENV_KEY_REF = { source: "env", provider: "default", id: "OPENAI_API_KEY" } as const;
|
||||
const allowInsecureTempSecretFile = process.platform === "win32";
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user